@phosphor/widgets
Version:
PhosphorJS - Widgets
218 lines (217 loc) • 8.11 kB
JavaScript
;
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
var algorithm_1 = require("@phosphor/algorithm");
var disposable_1 = require("@phosphor/disposable");
var domutils_1 = require("@phosphor/domutils");
var menu_1 = require("./menu");
/**
* An object which implements a universal context menu.
*
* #### Notes
* The items shown in the context menu are determined by CSS selector
* matching against the DOM hierarchy at the site of the mouse click.
* This is similar in concept to how keyboard shortcuts are matched
* in the command registry.
*/
var ContextMenu = /** @class */ (function () {
/**
* Construct a new context menu.
*
* @param options - The options for initializing the menu.
*/
function ContextMenu(options) {
this._idTick = 0;
this._items = [];
this.menu = new menu_1.Menu(options);
}
/**
* Add an item to the context menu.
*
* @param options - The options for creating the item.
*
* @returns A disposable which will remove the item from the menu.
*/
ContextMenu.prototype.addItem = function (options) {
var _this = this;
// Create an item from the given options.
var item = Private.createItem(options, this._idTick++);
// Add the item to the internal array.
this._items.push(item);
// Return a disposable which will remove the item.
return new disposable_1.DisposableDelegate(function () {
algorithm_1.ArrayExt.removeFirstOf(_this._items, item);
});
};
/**
* Open the context menu in response to a `'contextmenu'` event.
*
* @param event - The `'contextmenu'` event of interest.
*
* @returns `true` if the menu was opened, or `false` if no items
* matched the event and the menu was not opened.
*
* #### Notes
* This method will populate the context menu with items which match
* the propagation path of the event, then open the menu at the mouse
* position indicated by the event.
*/
ContextMenu.prototype.open = function (event) {
var _this = this;
// Clear the current contents of the context menu.
this.menu.clearItems();
// Bail early if there are no items to match.
if (this._items.length === 0) {
return false;
}
// Find the matching items for the event.
var items = Private.matchItems(this._items, event);
// Bail if there are no matching items.
if (!items || items.length === 0) {
return false;
}
// Add the filtered items to the menu.
algorithm_1.each(items, function (item) { _this.menu.addItem(item); });
// Open the context menu at the current mouse position.
this.menu.open(event.clientX, event.clientY);
// Indicate success.
return true;
};
return ContextMenu;
}());
exports.ContextMenu = ContextMenu;
/**
* The namespace for the module implementation details.
*/
var Private;
(function (Private) {
/**
* Create a normalized context menu item from an options object.
*/
function createItem(options, id) {
var selector = validateSelector(options.selector);
var rank = options.rank !== undefined ? options.rank : Infinity;
return __assign({}, options, { selector: selector, rank: rank, id: id });
}
Private.createItem = createItem;
/**
* Find the items which match a context menu event.
*
* The results are sorted by DOM level, specificity, and rank.
*/
function matchItems(items, event) {
// Look up the target of the event.
var target = event.target;
// Bail if there is no target.
if (!target) {
return null;
}
// Look up the current target of the event.
var currentTarget = event.currentTarget;
// Bail if there is no current target.
if (!currentTarget) {
return null;
}
// There are some third party libraries that cause the `target` to
// be detached from the DOM before Phosphor can process the event.
// If that happens, search for a new target node by point. If that
// node is still dangling, bail.
if (!currentTarget.contains(target)) {
target = document.elementFromPoint(event.clientX, event.clientY);
if (!target || !currentTarget.contains(target)) {
return null;
}
}
// Set up the result array.
var result = [];
// Copy the items array to allow in-place modification.
var availableItems = items.slice();
// Walk up the DOM hierarchy searching for matches.
while (target !== null) {
// Set up the match array for this DOM level.
var matches = [];
// Search the remaining items for matches.
for (var i = 0, n = availableItems.length; i < n; ++i) {
// Fetch the item.
var item = availableItems[i];
// Skip items which are already consumed.
if (!item) {
continue;
}
// Skip items which do not match the element.
if (!domutils_1.Selector.matches(target, item.selector)) {
continue;
}
// Add the matched item to the result for this DOM level.
matches.push(item);
// Mark the item as consumed.
availableItems[i] = null;
}
// Sort the matches for this level and add them to the results.
if (matches.length !== 0) {
matches.sort(itemCmp);
result.push.apply(result, matches);
}
// Stop searching at the limits of the DOM range.
if (target === currentTarget) {
break;
}
// Step to the parent DOM level.
target = target.parentElement;
}
// Return the matched and sorted results.
return result;
}
Private.matchItems = matchItems;
/**
* Validate the selector for a menu item.
*
* This returns the validated selector, or throws if the selector is
* invalid or contains commas.
*/
function validateSelector(selector) {
if (selector.indexOf(',') !== -1) {
throw new Error("Selector cannot contain commas: " + selector);
}
if (!domutils_1.Selector.isValid(selector)) {
throw new Error("Invalid selector: " + selector);
}
return selector;
}
/**
* A sort comparison function for a context menu item.
*/
function itemCmp(a, b) {
// Sort first based on selector specificity.
var s1 = domutils_1.Selector.calculateSpecificity(a.selector);
var s2 = domutils_1.Selector.calculateSpecificity(b.selector);
if (s1 !== s2) {
return s2 - s1;
}
// If specificities are equal, sort based on rank.
var r1 = a.rank;
var r2 = b.rank;
if (r1 !== r2) {
return r1 < r2 ? -1 : 1; // Infinity-safe
}
// When all else fails, sort by item id.
return a.id - b.id;
}
})(Private || (Private = {}));