UNPKG

jupyterlab-emrys

Version:

A computational environment for Jupyter. Powered by Emrys

530 lines (529 loc) 17.9 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. "use strict"; var __extends = (this && this.__extends) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; var phosphor_signaling_1 = require('phosphor-signaling'); var phosphor_widget_1 = require('phosphor-widget'); /** * The class name added to completion menu widgets. */ var COMPLETION_CLASS = 'jp-Completion'; /** * The class name added to completion menu items. */ var ITEM_CLASS = 'jp-Completion-item'; /** * The class name added to an active completion menu item. */ var ACTIVE_CLASS = 'jp-mod-active'; /** * The class name added to a completion widget that is scrolled out of view. */ var OUTOFVIEW_CLASS = 'jp-mod-outofview'; /** * The minimum height of a completion widget. */ var MIN_HEIGHT = 75; /** * The maximum height of a completion widget. */ var MAX_HEIGHT = 250; /** * A flag to indicate that event handlers are caught in the capture phase. */ var USE_CAPTURE = true; /** * A widget that enables text completion. */ var CompletionWidget = (function (_super) { __extends(CompletionWidget, _super); /** * Construct a text completion menu widget. */ function CompletionWidget(options) { if (options === void 0) { options = {}; } _super.call(this); this._anchor = null; this._anchorPoint = 0; this._activeIndex = 0; this._model = null; this._renderer = null; this._renderer = options.renderer || CompletionWidget.defaultRenderer; this.anchor = options.anchor || null; this.model = options.model || null; this.addClass(COMPLETION_CLASS); // Completion widgets are hidden until they are populated. this.hide(); } /** * Create the DOM node for a text completion menu. */ CompletionWidget.createNode = function () { var node = document.createElement('ul'); return node; }; Object.defineProperty(CompletionWidget.prototype, "selected", { /** * A signal emitted when a selection is made from the completion menu. */ get: function () { return Private.selectedSignal.bind(this); }, enumerable: true, configurable: true }); Object.defineProperty(CompletionWidget.prototype, "visibilityChanged", { /** * A signal emitted when the completion widget's visibility changes. * * #### Notes * This signal is useful when there are multiple floating widgets that may * contend with the same space and ought to be mutually exclusive. */ get: function () { return Private.visibilityChangedSignal.bind(this); }, enumerable: true, configurable: true }); Object.defineProperty(CompletionWidget.prototype, "model", { /** * The model used by the completion widget. * * #### Notes * This is a read-only property. */ get: function () { return this._model; }, set: function (model) { if (!model && !this._model || model === this._model) { return; } if (this._model) { this._model.stateChanged.disconnect(this.onModelStateChanged, this); } this._model = model; if (this._model) { this._model.stateChanged.connect(this.onModelStateChanged, this); } }, enumerable: true, configurable: true }); Object.defineProperty(CompletionWidget.prototype, "anchor", { /** * The semantic parent of the completion widget, its anchor element. An * event listener will peg the position of the completion widget to the * anchor element's scroll position. Other event listeners will guarantee * the completion widget behaves like a child of the reference element even * if it does not appear as a descendant in the DOM. */ get: function () { return this._anchor; }, set: function (element) { if (this._anchor === element) { return; } // Clean up scroll listener if anchor is being replaced. if (this._anchor) { this._anchor.removeEventListener('scroll', this, USE_CAPTURE); } this._anchor = element; // Add scroll listener to anchor element. if (this._anchor) { this._anchor.addEventListener('scroll', this, USE_CAPTURE); } }, enumerable: true, configurable: true }); /** * Dispose of the resources held by the completion widget. */ CompletionWidget.prototype.dispose = function () { if (this.isDisposed) { return; } this._model = null; _super.prototype.dispose.call(this); }; /** * Reset the widget. */ CompletionWidget.prototype.reset = function () { if (this._model) { this._model.reset(); } this._activeIndex = 0; this._anchorPoint = 0; this.hide(); this.visibilityChanged.emit(void 0); }; /** * Handle the DOM events for the widget. * * @param event - The DOM event sent to the widget. * * #### Notes * This method implements the DOM `EventListener` interface and is * called in response to events on the dock panel's node. It should * not be called directly by user code. */ CompletionWidget.prototype.handleEvent = function (event) { if (this.isHidden || !this._anchor) { return; } switch (event.type) { case 'keydown': this._evtKeydown(event); break; case 'mousedown': this._evtMousedown(event); break; case 'scroll': this._evtScroll(event); break; default: break; } }; /** * Handle `after_attach` messages for the widget. */ CompletionWidget.prototype.onAfterAttach = function (msg) { document.addEventListener('keydown', this, USE_CAPTURE); document.addEventListener('mousedown', this, USE_CAPTURE); }; /** * Handle `before_detach` messages for the widget. */ CompletionWidget.prototype.onBeforeDetach = function (msg) { document.removeEventListener('keydown', this, USE_CAPTURE); document.removeEventListener('mousedown', this, USE_CAPTURE); if (this._anchor) { this._anchor.removeEventListener('scroll', this, USE_CAPTURE); } }; /** * Handle model state changes. */ CompletionWidget.prototype.onModelStateChanged = function () { if (this.isAttached) { this.update(); } }; /** * Handle `update_request` messages. */ CompletionWidget.prototype.onUpdateRequest = function (msg) { var model = this.model; if (!model) { return; } var items = model.items; // If there are no items, reset and bail. if (!items || !items.length) { this.hide(); this.visibilityChanged.emit(void 0); return; } // If there is only one item, signal and bail. if (items.length === 1) { this.selected.emit(items[0].raw); this.reset(); return; } var node = this.node; node.textContent = ''; for (var _i = 0, items_1 = items; _i < items_1.length; _i++) { var item = items_1[_i]; var li = this._renderer.createItemNode(item); // Set the raw, un-marked up value as a data attribute. li.dataset['value'] = item.raw; node.appendChild(li); } var active = node.querySelectorAll("." + ITEM_CLASS)[this._activeIndex]; active.classList.add(ACTIVE_CLASS); if (this.isHidden) { this.show(); this.visibilityChanged.emit(void 0); } this._anchorPoint = this._anchor.scrollTop; this._setGeometry(); }; /** * Cycle through the available completion items. */ CompletionWidget.prototype._cycle = function (direction) { var items = this.node.querySelectorAll("." + ITEM_CLASS); var index = this._activeIndex; var active = this.node.querySelector("." + ACTIVE_CLASS); active.classList.remove(ACTIVE_CLASS); if (direction === 'up') { this._activeIndex = index === 0 ? items.length - 1 : index - 1; } else { this._activeIndex = index < items.length - 1 ? index + 1 : 0; } active = items[this._activeIndex]; active.classList.add(ACTIVE_CLASS); Private.scrollIfNeeded(this.node, active); }; /** * Handle keydown events for the widget. */ CompletionWidget.prototype._evtKeydown = function (event) { if (this.isHidden || !this._anchor) { return; } var target = event.target; while (target !== document.documentElement) { if (target === this._anchor) { switch (event.keyCode) { case 9: event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); if (this._populateSubset()) { return; } this._selectActive(); return; case 13: event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); this._selectActive(); return; case 38: // Up arrow key case 40: event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); this._cycle(event.keyCode === 38 ? 'up' : 'down'); return; default: return; } } target = target.parentElement; } this.reset(); }; /** * Handle mousedown events for the widget. */ CompletionWidget.prototype._evtMousedown = function (event) { if (this.isHidden || !this._anchor) { return; } if (Private.nonstandardClick(event)) { this.reset(); return; } var target = event.target; while (target !== document.documentElement) { // If the user has made a selection, emit its value and reset the widget. if (target.classList.contains(ITEM_CLASS)) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); this.selected.emit(target.dataset['value']); this.reset(); return; } // If the mouse event happened anywhere else in the widget, bail. if (target === this.node) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); return; } target = target.parentElement; } this.reset(); }; /** * Handle scroll events for the widget */ CompletionWidget.prototype._evtScroll = function (event) { if (this.isHidden || !this._anchor) { return; } this._setGeometry(); }; /** * Populate the completion up to the longest initial subset of items. * * @returns `true` if a subset match was found and populated. */ CompletionWidget.prototype._populateSubset = function () { var items = this.node.querySelectorAll("." + ITEM_CLASS); var subset = Private.commonSubset(Private.itemValues(items)); var query = this.model.query; if (subset && subset !== query && subset.indexOf(query) === 0) { this.model.query = subset; this.selected.emit(subset); this.update(); return true; } return false; }; /** * Set the visible dimensions of the widget. */ CompletionWidget.prototype._setGeometry = function () { if (!this.model || !this._model.original) { return; } var node = this.node; var coords = this._model.current ? this._model.current.coords : this._model.original.coords; var scrollDelta = this._anchorPoint - this._anchor.scrollTop; var availableHeight = coords.top + scrollDelta; var maxHeight = Math.max(0, Math.min(availableHeight, MAX_HEIGHT)); if (maxHeight > MIN_HEIGHT) { node.classList.remove(OUTOFVIEW_CLASS); } else { node.classList.add(OUTOFVIEW_CLASS); return; } node.style.maxHeight = maxHeight + "px"; var borderLeftWidth = window.getComputedStyle(node).borderLeftWidth; var left = coords.left + (parseInt(borderLeftWidth, 10) || 0); var rect = node.getBoundingClientRect(); var top = availableHeight - rect.height; node.style.left = Math.floor(left) + "px"; node.style.top = Math.floor(top) + "px"; node.style.width = 'auto'; // Expand the menu width by the scrollbar size, if present. if (node.scrollHeight > maxHeight) { node.style.width = (2 * node.offsetWidth - node.clientWidth) + "px"; node.scrollTop = 0; } }; /** * Emit the selected signal for the current active item and reset. */ CompletionWidget.prototype._selectActive = function () { var active = this.node.querySelector("." + ACTIVE_CLASS); if (!active) { return; } this.selected.emit(active.dataset['value']); this.reset(); }; return CompletionWidget; }(phosphor_widget_1.Widget)); exports.CompletionWidget = CompletionWidget; var CompletionWidget; (function (CompletionWidget) { /** * The default implementation of an `IRenderer`. */ var Renderer = (function () { function Renderer() { } /** * Create an item node for a text completion menu. */ Renderer.prototype.createItemNode = function (item) { var li = document.createElement('li'); var code = document.createElement('code'); // Use innerHTML because search results include <mark> tags. code.innerHTML = item.text; li.className = ITEM_CLASS; li.appendChild(code); return li; }; return Renderer; }()); CompletionWidget.Renderer = Renderer; /** * The default `IRenderer` instance. */ CompletionWidget.defaultRenderer = new Renderer(); })(CompletionWidget = exports.CompletionWidget || (exports.CompletionWidget = {})); /** * A namespace for completion widget private data. */ var Private; (function (Private) { /** * A signal emitted when state of the completion menu changes. */ Private.selectedSignal = new phosphor_signaling_1.Signal(); /** * A signal emitted when the completion widget's visibility changes. */ Private.visibilityChangedSignal = new phosphor_signaling_1.Signal(); /** * Returns the common subset string that a list of strings shares. */ function commonSubset(values) { var len = values.length; var subset = ''; if (len < 2) { return subset; } var strlen = values[0].length; for (var i = 0; i < strlen; i++) { var ch = values[0][i]; for (var j = 1; j < len; j++) { if (values[j][i] !== ch) { return subset; } } subset += ch; } return subset; } Private.commonSubset = commonSubset; /** * Returns the list of raw item values currently in the DOM. */ function itemValues(items) { var values = []; for (var i = 0, len = items.length; i < len; i++) { values.push(items[i].dataset['value']); } return values; } Private.itemValues = itemValues; /** * Returns true for any modified click event (i.e., not a left-click). */ function nonstandardClick(event) { return event.button !== 0 || event.altKey || event.ctrlKey || event.shiftKey || event.metaKey; } Private.nonstandardClick = nonstandardClick; /** * Scroll an element into view if needed. * * @param area - The scroll area element. * * @param elem - The element of interest. */ function scrollIfNeeded(area, elem) { var ar = area.getBoundingClientRect(); var er = elem.getBoundingClientRect(); if (er.top < ar.top - 10) { area.scrollTop -= ar.top - er.top + 10; } else if (er.bottom > ar.bottom + 10) { area.scrollTop += er.bottom - ar.bottom + 10; } } Private.scrollIfNeeded = scrollIfNeeded; })(Private || (Private = {}));