/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
release-1.14
src/widgets/InlineMenu.js
473 строки
15 KB
Nitesh Kumar
Fixed ExtractToVariable Menu Close Issue due to scrolling (#14492)
02 авг 2018, 16:24
02 авг 2018, 16:24
0cee7d4
Код
Авторство
О чём код?
/* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ define(function (require, exports, module) { "use strict"; // Load dependent modules var KeyBindingManager = require("command/KeyBindingManager"), Menus = require("command/Menus"), KeyEvent = require("utils/KeyEvent"), StringUtils = require("utils/StringUtils"), ValidationUtils = require("utils/ValidationUtils"), ViewUtils = require("utils/ViewUtils"), PopUpManager = require("widgets/PopUpManager"), Mustache = require("thirdparty/mustache/mustache"); var MenuHTML = require("text!htmlContent/inline-menu.html"); /** * Displays a popup list of items for a given editor context * * @constructor * @param {Editor} editor * @param {string} menuText */ function InlineMenu(editor, menuText) { /** * The list of items to display * * @type {Array.<{id: number, name: string>} */ this.items = []; /** * The selected position in the list; otherwise -1. * * @type {number} */ this.selectedIndex = -1; /** * Is the list currently open? * * @type {boolean} */ this.opened = false; /** * The editor context * * @type {Editor} */ this.editor = editor; /** * The menu selection callback function * * @type {Function} */ this.handleSelect = null; /** * The menu closure callback function * * @type {Function} */ this.handleClose = null; /** * The menu object * * @type {jQuery.Object} */ this.$menu = $("<li class='dropdown inlinemenu-menu'></li>") .append($("<a href='#' class='dropdown-toggle' data-toggle='dropdown'></a>") .hide()) .append("<ul class='dropdown-menu'>" + "<li class='inlinemenu-header'>" + "<a>" + menuText + "</a>" + "</li>" + "</ul>"); this._keydownHook = this._keydownHook.bind(this); } /** * Select the item in the menu at the specified index, or remove the * selection if index < 0. * * @private * @param {number} index */ InlineMenu.prototype._setSelectedIndex = function (index) { var items = this.$menu.find("li.inlinemenu-item"); // Range check index = Math.max(-1, Math.min(index, items.length - 1)); // Clear old highlight if (this.selectedIndex !== -1) { $(items[this.selectedIndex]).find("a").removeClass("highlight"); } this.selectedIndex = index; // Highlight the new selected item, if necessary if (this.selectedIndex !== -1) { var $item = $(items[this.selectedIndex]); var $view = this.$menu.find("ul.dropdown-menu"); $item.find("a").addClass("highlight"); ViewUtils.scrollElementIntoView($view, $item, false); } // Invoke handleHover callback if any if (this.handleHover) { this.handleHover(this.items[index].id); } }; /** * Rebuilds the list items for the menu. * * @private */ InlineMenu.prototype._buildListView = function (items) { var self = this, view = { items: [] }, _addItem; this.items = items; _addItem = function (item) { view.items.push({ formattedItem: "<span>" + item.name + "</span>"}); }; // clear the list this.$menu.find("li.inlinemenu-item").remove(); // if there are no items then close the list; otherwise add them and // set the selection if (this.items.length === 0) { if (this.handleClose) { this.handleClose(); } } else { this.items.some(function (item, index) { _addItem(item); }); // render the menu list var $ul = this.$menu.find("ul.dropdown-menu"), $parent = $ul.parent(); // remove list temporarily to save rendering time $ul.remove().append(Mustache.render(MenuHTML, view)); $ul.children("li.inlinemenu-item").each(function (index, element) { var item = self.items[index], $element = $(element); // store id of item in the element $element.data("itemid", item.id); }); // delegate list item events to the top-level ul list element $ul.on("click", "li.inlinemenu-item", function (e) { // Don't let the click propagate upward (otherwise it will // hit the close handler in bootstrap-dropdown). e.stopPropagation(); if (self.handleSelect) { self.handleSelect($(this).data("itemid")); } }); $ul.on("mouseover", "li.inlinemenu-item", function (e) { e.stopPropagation(); // _setSelectedIndex sets the selected index and call handle hover // callback funtion self._setSelectedIndex(self.items.findIndex(function(element) { return element.id === $(e.currentTarget).data("itemid"); })); }); $parent.append($ul); this._setSelectedIndex(0); } }; /** * Computes top left location for menu so that the menu is not clipped by the window. * Also computes the largest available width. * * @private * @return {{left: number, top: number, width: number}} */ InlineMenu.prototype._calcMenuLocation = function () { var cursor = this.editor._codeMirror.cursorCoords(), posTop = cursor.bottom, posLeft = cursor.left, textHeight = this.editor.getTextHeight(), $window = $(window), $menuWindow = this.$menu.children("ul"), menuHeight = $menuWindow.outerHeight(); // TODO Ty: factor out menu repositioning logic so inline menu and Context menus share code // adjust positioning so menu is not clipped off bottom or right var bottomOverhang = posTop + menuHeight - $window.height(); if (bottomOverhang > 0) { posTop -= (textHeight + 2 + menuHeight); } posTop -= 30; // shift top for hidden parent element var menuWidth = $menuWindow.width(); var availableWidth = menuWidth; var rightOverhang = posLeft + menuWidth - $window.width(); if (rightOverhang > 0) { // Right overhang is negative posLeft = Math.max(0, posLeft - rightOverhang); } return {left: posLeft, top: posTop, width: availableWidth}; }; /** * Check whether Event is one of the keys that we handle or not. * * @param {KeyBoardEvent|keyBoardEvent.keyCode} keyEvent */ InlineMenu.prototype.isHandlingKeyCode = function (keyCodeOrEvent) { var keyCode = typeof keyCodeOrEvent === "object" ? keyCodeOrEvent.keyCode : keyCodeOrEvent; var ctrlKey = typeof keyCodeOrEvent === "object" ? keyCodeOrEvent.ctrlKey : false; return (keyCode === KeyEvent.DOM_VK_UP || keyCode === KeyEvent.DOM_VK_DOWN || keyCode === KeyEvent.DOM_VK_PAGE_UP || keyCode === KeyEvent.DOM_VK_PAGE_DOWN || keyCode === KeyEvent.DOM_VK_RETURN || keyCode === KeyEvent.DOM_VK_ESCAPE ); }; /** * Convert keydown events into hint list navigation actions. * * @param {KeyBoardEvent} keyEvent */ InlineMenu.prototype._keydownHook = function (event) { var keyCode, self = this; // positive distance rotates down; negative distance rotates up function _rotateSelection(distance) { var len = self.items.length, pos; if (self.selectedIndex < 0) { // set the initial selection pos = (distance > 0) ? distance - 1 : len - 1; } else { // adjust current selection pos = self.selectedIndex; // Don't "rotate" until all items have been shown if (distance > 0) { if (pos === (len - 1)) { pos = 0; // wrap } else { pos = Math.min(pos + distance, len - 1); } } else { if (pos === 0) { pos = (len - 1); // wrap } else { pos = Math.max(pos + distance, 0); } } } self._setSelectedIndex(pos); } // Calculate the number of items per scroll page. function _itemsPerPage() { var itemsPerPage = 1, $items = self.$menu.find("li.inlinemenu-item"), $view = self.$menu.find("ul.dropdown-menu"), itemHeight; if ($items.length !== 0) { itemHeight = $($items[0]).height(); if (itemHeight) { // round down to integer value itemsPerPage = Math.floor($view.height() / itemHeight); itemsPerPage = Math.max(1, Math.min(itemsPerPage, $items.length)); } } return itemsPerPage; } // If we're no longer visible, skip handling the key and end the session. if (!this.isOpen()) { this.handleClose(); return false; } // (page) up, (page) down, enter are handled by the list if ((event.type === "keydown") && this.isHandlingKeyCode(event)) { keyCode = event.keyCode; if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) { event.stopImmediatePropagation(); this.handleClose(); return false; } else if (event.shiftKey && (event.keyCode === KeyEvent.DOM_VK_UP || event.keyCode === KeyEvent.DOM_VK_DOWN || event.keyCode === KeyEvent.DOM_VK_PAGE_UP || event.keyCode === KeyEvent.DOM_VK_PAGE_DOWN)) { this.handleClose(); // Let the event bubble. return false; } else if (keyCode === KeyEvent.DOM_VK_UP) { _rotateSelection.call(this, -1); } else if (keyCode === KeyEvent.DOM_VK_DOWN) { _rotateSelection.call(this, 1); } else if (keyCode === KeyEvent.DOM_VK_PAGE_UP) { _rotateSelection.call(this, -_itemsPerPage()); } else if (keyCode === KeyEvent.DOM_VK_PAGE_DOWN) { _rotateSelection.call(this, _itemsPerPage()); } else if (this.selectedIndex !== -1 && (keyCode === KeyEvent.DOM_VK_RETURN)) { // Trigger a click handler to commmit the selected item $(this.$menu.find("li.inlinemenu-item")[this.selectedIndex]).trigger("click"); } else { return false; } event.stopImmediatePropagation(); event.preventDefault(); return true; } return false; }; /** * Is the Inline menu open? * * @return {boolean} */ InlineMenu.prototype.isOpen = function () { // We don't get a notification when the dropdown closes. The best // we can do is keep an "opened" flag and check to see if we // still have the "open" class applied. if (this.opened && !this.$menu.hasClass("open")) { this.opened = false; } return this.opened; }; /** * Displays the menu at the current cursor position * * @param {Array.<{id: number, name: string>} hints */ InlineMenu.prototype.open = function (items) { Menus.closeAll(); this._buildListView(items); if (this.items.length) { // Need to add the menu to the DOM before trying to calculate its ideal location. $("#inlinemenu-menu-bar > ul").append(this.$menu); var menuPos = this._calcMenuLocation(); this.$menu.addClass("open") .css({"left": menuPos.left, "top": menuPos.top, "width": menuPos.width + "px"}); this.opened = true; KeyBindingManager.addGlobalKeydownHook(this._keydownHook); } }; /** * Displays the last menu which was closed due to Scrolling */ InlineMenu.prototype.openRemovedMenu = function () { if (this.opened === true) { if (this.$menu && !this.$menu.hasClass("open")) { var menuPos = this._calcMenuLocation(); this.$menu.addClass("open") .css({"left": menuPos.left, "top": menuPos.top, "width": menuPos.width + "px"}); } } }; /** * Closes the menu */ InlineMenu.prototype.close = function () { this.opened = false; if (this.$menu) { this.$menu.removeClass("open"); PopUpManager.removePopUp(this.$menu); this.$menu.remove(); } KeyBindingManager.removeGlobalKeydownHook(this._keydownHook); }; /** * Set the menu selection callback function * * @param {Function} callback */ InlineMenu.prototype.onSelect = function (callback) { this.handleSelect = callback; }; /** * Set the hover callback function * * @param {Function} callback */ InlineMenu.prototype.onHover = function (callback) { this.handleHover = callback; }; /** * Set the menu closure callback function * * @param {Function} callback */ InlineMenu.prototype.onClose = function (callback) { this.handleClose = callback; }; // Define public API exports.InlineMenu = InlineMenu; });