UNPKG

@eclipse-glsp/client

Version:

A sprotty-based client for GLSP

280 lines 12.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.toActionArray = exports.AutoCompleteWidget = void 0; /******************************************************************************** * Copyright (c) 2020-2024 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the Eclipse * Public License v. 2.0 are satisfied: GNU General Public License, version 2 * with the GNU Classpath Exception which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ const sprotty_1 = require("@eclipse-glsp/sprotty"); const auto_complete_actions_1 = require("./auto-complete-actions"); const validation_decorator_1 = require("./validation-decorator"); // eslint-disable-next-line @typescript-eslint/no-var-requires const configureAutocomplete = require('autocompleter'); /** * The `AutoCompleteWidget` is a reusable UI element that provides a text input supporting auto-completion, * validation, validation messages, etc. * * An example for using it is available in the workflow diagram: * https://github.com/eclipse-glsp/glsp-client/blob/master/examples/workflow-glsp/src/direct-task-editing/direct-task-editor.ts */ class AutoCompleteWidget { constructor(autoSuggestionSettings, suggestionProvider, suggestionSubmitHandler, // eslint-disable-next-line @typescript-eslint/no-empty-function notifyClose = () => { }, logger, options) { this.autoSuggestionSettings = autoSuggestionSettings; this.suggestionProvider = suggestionProvider; this.suggestionSubmitHandler = suggestionSubmitHandler; this.notifyClose = notifyClose; this.logger = logger; this.options = options; this.loadingIndicatorClasses = (0, sprotty_1.codiconCSSClasses)('loading', false, true, ['loading']); this.validationDecorator = validation_decorator_1.IValidationDecorator.NO_DECORATION; } configureValidation(inputValidator, validationDecorator = validation_decorator_1.IValidationDecorator.NO_DECORATION) { this.inputValidator = inputValidator; this.validationDecorator = validationDecorator; } configureTextSubmitHandler(textSubmitHandler) { this.textSubmitHandler = textSubmitHandler; } initialize(containerElement) { this.containerElement = containerElement; this.inputElement = this.createInputElement(); this.containerElement.appendChild(this.inputElement); this.containerElement.style.position = 'absolute'; } createInputElement() { const inputElement = document.createElement('input'); inputElement.style.position = 'absolute'; inputElement.spellcheck = false; inputElement.autocapitalize = 'false'; inputElement.autocomplete = 'off'; inputElement.style.width = '100%'; inputElement.addEventListener('keydown', event => this.handleKeyDown(event)); inputElement.addEventListener('blur', () => { if (this.containerElement.style.visibility !== 'hidden') { window.setTimeout(() => this.notifyClose('blur'), 200); } }); return inputElement; } handleKeyDown(event) { if ((0, sprotty_1.matchesKeystroke)(event, 'Escape')) { this.notifyClose('escape'); return; } if ((0, sprotty_1.matchesKeystroke)(event, 'Enter') && !this.isInputElementChanged() && this.isSuggestionAvailable()) { return; } if (this.isInputElementChanged()) { this.invalidateValidationResultAndContextActions(); } if (!(0, sprotty_1.matchesKeystroke)(event, 'Enter') || this.isSuggestionAvailable()) { return; } if (!this.validationDecorator.isValidatedOk()) { event.stopImmediatePropagation(); return; } if (this.textSubmitHandler) { this.executeFromTextOnlyInput(); this.notifyClose('submission'); } } isInputElementChanged() { return this.inputElement.value !== this.previousContent; } invalidateValidationResultAndContextActions() { this.contextActions = undefined; this.validationDecorator.invalidate(); } open(root, ...contextElementIds) { this.contextActions = undefined; this.autoCompleteResult = configureAutocomplete(this.autocompleteSettings(root)); this.previousContent = this.inputElement.value; this.inputElement.setSelectionRange(0, this.inputElement.value.length); this.inputElement.focus(); } autocompleteSettings(root) { return { input: this.inputElement, emptyMsg: this.autoSuggestionSettings.noSuggestionsMessage, className: this.autoSuggestionSettings.suggestionsClass, showOnFocus: this.autoSuggestionSettings.showOnFocus, debounceWaitMs: this.autoSuggestionSettings.debounceWaitMs, minLength: -1, fetch: (text, update) => this.updateSuggestions(update, text, root), onSelect: (item) => this.onSelect(item), render: (item, currentValue) => this.renderSuggestions(item, currentValue), customize: (input, inputRect, container, maxHeight) => { var _a; this.customizeInputElement(input, inputRect, container, maxHeight); const selectedSuggestionChanged = (_a = this.options) === null || _a === void 0 ? void 0 : _a.selectedSuggestionChanged; if (selectedSuggestionChanged) { this.observer = new MutationObserver(mutations => this.handleContainerMutations(mutations, selectedSuggestionChanged)); this.observer.observe(container, { childList: true, attributes: true, subtree: true }); } } }; } customizeInputElement(input, inputRect, container, maxHeight) { // move container into our UIExtension container as this is already positioned correctly container.style.position = 'fixed'; if (this.containerElement) { this.containerElement.appendChild(container); } this.container = container; } handleContainerMutations(mutations, selectionChanged) { var _a; const selectedElement = this.container.querySelector('.selected'); // Trigger selection changed event // eslint-disable-next-line no-null/no-null if (selectedElement !== null && selectedElement !== undefined) { const index = Array.from(this.container.children).indexOf(selectedElement); selectionChanged((_a = this.contextActions) === null || _a === void 0 ? void 0 : _a[index]); } else { selectionChanged(undefined); } } updateSuggestions(update, text, root, ...contextElementIds) { this.onLoading(); this.doUpdateSuggestions(text, root) .then(actions => { var _a, _b; this.contextActions = this.filterActions(text, actions); update(this.contextActions); (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.visibleSuggestionsChanged) === null || _b === void 0 ? void 0 : _b.call(_a, this.contextActions); this.onLoaded('success'); }) .catch(reason => { if (this.logger) { this.logger.error(this, 'Failed to obtain suggestions', reason); } this.onLoaded('error'); }); } onLoading() { if (this.loadingIndicator && this.containerElement.contains(this.loadingIndicator)) { return; } this.loadingIndicator = document.createElement('span'); this.loadingIndicator.classList.add(...this.loadingIndicatorClasses); this.containerElement.appendChild(this.loadingIndicator); } doUpdateSuggestions(text, root, ...contextElementIds) { return this.suggestionProvider.provideSuggestions(text); } onLoaded(_success) { if (this.containerElement.contains(this.loadingIndicator)) { this.containerElement.removeChild(this.loadingIndicator); } this.validationDecorator.invalidate(); this.validateInputIfNoContextActions(); this.previousContent = this.inputElement.value; } renderSuggestions(item, value) { const itemElement = document.createElement('div'); const wordMatcher = this.escapeForRegExp(value).split(' ').join('|'); const regex = new RegExp(wordMatcher, 'gi'); if (item.icon) { this.renderIcon(itemElement, item.icon); } itemElement.innerHTML += item.label.replace(regex, match => '<em>' + match + '</em>').replace(/ /g, '&nbsp;'); return itemElement; } escapeForRegExp(value) { return value.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } renderIcon(itemElement, icon) { itemElement.innerHTML += `<span class="icon ${icon}"></span>`; } filterActions(filterText, actions) { return (0, sprotty_1.toArray)(actions.filter(action => { const label = action.label.toLowerCase(); const searchWords = filterText.split(' '); return searchWords.every(word => label.indexOf(word.toLowerCase()) !== -1); })); } onSelect(item) { if (auto_complete_actions_1.AutoCompleteValue.is(item)) { this.inputElement.value = item.text; // trigger update of suggestions with an keyup event window.setTimeout(() => this.inputElement.dispatchEvent(new Event('input'))); } else { this.executeFromSuggestion(item); this.notifyClose('submission'); } } validateInputIfNoContextActions() { if (this.isNoOrExactlyOneMatchingContextAction()) { this.validateInput(); } else { this.validationDecorator.dispose(); } } isNoOrExactlyOneMatchingContextAction() { return (!this.isSuggestionAvailable() || (this.contextActions && this.contextActions.length === 1 && this.inputElement.value.endsWith(this.contextActions[0].label))); } isSuggestionAvailable() { return this.contextActions && this.contextActions.length > 0; } validateInput() { if (this.inputValidator) { this.inputValidator .validate(this.inputElement.value) .then(result => this.validationDecorator.decorateValidationResult(result)) .catch(error => this.handleErrorDuringValidation(error)); } } handleErrorDuringValidation(error) { if (this.logger) { this.logger.error(this, 'Failed to validate input', error); } this.validationDecorator.dispose(); } executeFromSuggestion(input) { this.suggestionSubmitHandler.executeFromSuggestion(input); } executeFromTextOnlyInput() { if (this.textSubmitHandler) { this.textSubmitHandler.executeFromTextOnlyInput(this.inputElement.value); } } get inputField() { return this.inputElement; } dispose() { this.validationDecorator.dispose(); if (this.autoCompleteResult) { this.autoCompleteResult.destroy(); } } } exports.AutoCompleteWidget = AutoCompleteWidget; function toActionArray(input) { if (sprotty_1.LabeledAction.is(input)) { return input.actions; } else if (sprotty_1.Action.is(input)) { return [input]; } return []; } exports.toActionArray = toActionArray; //# sourceMappingURL=auto-complete-widget.js.map