jupyterlab-emrys
Version:
A computational environment for Jupyter. Powered by Emrys
530 lines (529 loc) • 17.9 kB
JavaScript
// 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 = {}));