mirror of
https://github.com/Icinga/icinga-php-library.git
synced 2025-07-02 03:14:30 +02:00
900 lines
30 KiB
JavaScript
900 lines
30 KiB
JavaScript
define(["../notjQuery", "Completer"], function ($, Completer) {
|
|
|
|
"use strict";
|
|
|
|
class BaseInput {
|
|
constructor(input) {
|
|
this.input = input;
|
|
this.disabled = false;
|
|
this.separator = '';
|
|
this.usedTerms = [];
|
|
this.completer = null;
|
|
this.lastCompletedTerm = null;
|
|
this._dataInput = null;
|
|
this._termInput = null;
|
|
this._termContainer = null;
|
|
}
|
|
|
|
get dataInput() {
|
|
if (this._dataInput === null) {
|
|
this._dataInput = document.querySelector(this.input.dataset.dataInput);
|
|
}
|
|
|
|
return this._dataInput;
|
|
}
|
|
|
|
get termInput() {
|
|
if (this._termInput === null) {
|
|
this._termInput = document.querySelector(this.input.dataset.termInput);
|
|
}
|
|
|
|
return this._termInput;
|
|
}
|
|
|
|
get termContainer() {
|
|
if (this._termContainer === null) {
|
|
this._termContainer = document.querySelector(this.input.dataset.termContainer);
|
|
}
|
|
|
|
return this._termContainer;
|
|
}
|
|
|
|
bind() {
|
|
// Form submissions
|
|
$(this.input.form).on('submit', this.onSubmit, this);
|
|
$(this.input.form).on(
|
|
'click', 'button:not([type]), button[type="submit"], input[type="submit"]', this.onButtonClick, this);
|
|
|
|
// User interactions
|
|
$(this.input).on('input', this.onInput, this);
|
|
$(this.input).on('keydown', this.onKeyDown, this);
|
|
$(this.input).on('keyup', this.onKeyUp, this);
|
|
$(this.input).on('blur', this.onInputBlur, this);
|
|
$(this.input).on('focusin', this.onTermFocus, this);
|
|
$(this.termContainer).on('input', '[data-label]', this.onInput, this);
|
|
$(this.termContainer).on('keydown', '[data-label]', this.onKeyDown, this);
|
|
$(this.termContainer).on('keyup', '[data-label]', this.onKeyUp, this);
|
|
$(this.termContainer).on('focusout', '[data-index]', this.onTermFocusOut, this);
|
|
$(this.termContainer).on('focusin', '[data-index]', this.onTermFocus, this);
|
|
|
|
// Copy/Paste
|
|
$(this.input).on('paste', this.onPaste, this);
|
|
$(this.input).on('copy', this.onCopyAndCut, this);
|
|
$(this.input).on('cut', this.onCopyAndCut, this);
|
|
|
|
// Should terms be completed?
|
|
if (this.input.dataset.suggestUrl) {
|
|
if (this.completer === null) {
|
|
this.completer = new Completer(this.input, true);
|
|
this.completer.bind(this.termContainer);
|
|
}
|
|
|
|
$(this.input).on('suggestion', this.onSuggestion, this);
|
|
$(this.input).on('completion', this.onCompletion, this);
|
|
$(this.termContainer).on('suggestion', '[data-label]', this.onSuggestion, this);
|
|
$(this.termContainer).on('completion', '[data-label]', this.onCompletion, this);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
refresh(input) {
|
|
if (input === this.input) {
|
|
// If the DOM node is still the same, nothing has changed
|
|
return;
|
|
}
|
|
|
|
this._termInput = null;
|
|
this._termContainer = null;
|
|
|
|
this.input = input;
|
|
this.bind();
|
|
|
|
if (this.completer !== null) {
|
|
this.completer.refresh(input, this.termContainer);
|
|
}
|
|
|
|
if (! this.restoreTerms()) {
|
|
this.reset();
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this.usedTerms = [];
|
|
this.lastCompletedTerm = null;
|
|
|
|
this.togglePlaceholder();
|
|
this.termInput.value = '';
|
|
this.termContainer.innerHTML = '';
|
|
}
|
|
|
|
destroy() {
|
|
this._termContainer = null;
|
|
this._termInput = null;
|
|
this.input = null;
|
|
|
|
if (this.completer !== null) {
|
|
this.completer.destroy();
|
|
this.completer = null;
|
|
}
|
|
}
|
|
|
|
disable() {
|
|
this.disabled = true;
|
|
this.input.disabled = true;
|
|
this.input.form.classList.add('disabled');
|
|
this.termContainer.querySelectorAll('[data-index]').forEach(el => el.firstChild.disabled = true);
|
|
|
|
if (this.completer !== null) {
|
|
this.completer.reset();
|
|
}
|
|
}
|
|
|
|
enable() {
|
|
this.input.disabled = false;
|
|
this.input.form.classList.remove('disabled');
|
|
this.termContainer.querySelectorAll('[data-index]').forEach(el => el.firstChild.disabled = false);
|
|
this.disabled = false;
|
|
}
|
|
|
|
restoreTerms() {
|
|
if (this.hasTerms()) {
|
|
this.usedTerms.forEach((termData, termIndex) => this.addTerm(termData, termIndex));
|
|
this.togglePlaceholder();
|
|
this.clearPartialTerm(this.input);
|
|
} else {
|
|
this.registerTerms();
|
|
this.togglePlaceholder();
|
|
}
|
|
|
|
return this.hasTerms();
|
|
}
|
|
|
|
registerTerms() {
|
|
this.termContainer.querySelectorAll('[data-index]').forEach((label) => {
|
|
let termData = { ...label.dataset };
|
|
delete termData.index;
|
|
|
|
if (label.className) {
|
|
termData['class'] = label.className;
|
|
}
|
|
|
|
if (label.title) {
|
|
termData['title'] = label.title;
|
|
}
|
|
|
|
this.registerTerm(this.decodeTerm(termData), label.dataset.index);
|
|
});
|
|
}
|
|
|
|
registerTerm(termData, termIndex = null) {
|
|
if (termIndex !== null) {
|
|
this.usedTerms.splice(termIndex, 0, termData);
|
|
return termIndex;
|
|
} else {
|
|
return this.usedTerms.push(termData) - 1;
|
|
}
|
|
}
|
|
|
|
updateTerms(changedTerms) {
|
|
// Reset the data input, otherwise the value remains and is sent continuously with subsequent requests
|
|
this.dataInput.value = '';
|
|
|
|
for (const termIndex of Object.keys(changedTerms)) {
|
|
let label = this.termContainer.querySelector(`[data-index="${ termIndex }"]`);
|
|
if (! label) {
|
|
continue;
|
|
}
|
|
|
|
let input = label.firstChild;
|
|
let termData = changedTerms[termIndex];
|
|
|
|
if (termData.label) {
|
|
this.writePartialTerm(termData.label, input);
|
|
}
|
|
|
|
this.updateTermData(termData, input);
|
|
this.usedTerms[termIndex] = termData;
|
|
}
|
|
}
|
|
|
|
clearPartialTerm(input) {
|
|
if (this.completer !== null) {
|
|
this.completer.reset();
|
|
}
|
|
|
|
this.writePartialTerm('', input);
|
|
}
|
|
|
|
writePartialTerm(value, input) {
|
|
input.value = value;
|
|
this.updateTermData({ label: value }, input);
|
|
}
|
|
|
|
readPartialTerm(input) {
|
|
return input.value.trim();
|
|
}
|
|
|
|
readFullTerm(input, termIndex = null) {
|
|
let value = this.readPartialTerm(input);
|
|
if (! value) {
|
|
return false;
|
|
}
|
|
|
|
let termData = {};
|
|
|
|
if (termIndex !== null) {
|
|
termData = { ...this.usedTerms[termIndex] };
|
|
}
|
|
|
|
termData.label = value;
|
|
termData.search = value;
|
|
|
|
if (this.lastCompletedTerm !== null) {
|
|
if (termData.label === this.lastCompletedTerm.label) {
|
|
Object.assign(termData, this.lastCompletedTerm);
|
|
}
|
|
|
|
this.lastCompletedTerm = null;
|
|
}
|
|
|
|
return termData;
|
|
}
|
|
|
|
exchangeTerm() {
|
|
if (this.completer !== null) {
|
|
this.completer.reset();
|
|
}
|
|
|
|
let termData = this.readFullTerm(this.input);
|
|
if (! termData) {
|
|
return {};
|
|
}
|
|
|
|
let addedTerms = {};
|
|
if (Array.isArray(termData)) {
|
|
for (let data of termData) {
|
|
this.addTerm(data);
|
|
addedTerms[this.usedTerms.length - 1] = data;
|
|
}
|
|
} else {
|
|
this.addTerm(termData);
|
|
addedTerms[this.usedTerms.length - 1] = termData;
|
|
}
|
|
|
|
this.clearPartialTerm(this.input);
|
|
|
|
return addedTerms;
|
|
}
|
|
|
|
insertTerm(termData, termIndex) {
|
|
this.reIndexTerms(termIndex, 1, true);
|
|
this.registerTerm(termData, termIndex);
|
|
return this.insertRenderedTerm(this.renderTerm(termData, termIndex));
|
|
}
|
|
|
|
insertRenderedTerm(label) {
|
|
let next = this.termContainer.querySelector(`[data-index="${ label.dataset.index + 1 }"]`);
|
|
this.termContainer.insertBefore(label, next);
|
|
return label;
|
|
}
|
|
|
|
addTerm(termData, termIndex = null) {
|
|
if (termIndex === null) {
|
|
termIndex = this.registerTerm(termData);
|
|
}
|
|
|
|
this.addRenderedTerm(this.renderTerm(termData, termIndex));
|
|
}
|
|
|
|
addRenderedTerm(label) {
|
|
this.termContainer.appendChild(label);
|
|
}
|
|
|
|
hasTerms() {
|
|
return this.usedTerms.length > 0;
|
|
}
|
|
|
|
hasSyntaxError(input) {
|
|
if (typeof input === 'undefined') {
|
|
input = this.input;
|
|
}
|
|
|
|
return 'hasSyntaxError' in input.dataset;
|
|
}
|
|
|
|
clearSyntaxError(input) {
|
|
if (typeof input === 'undefined') {
|
|
input = this.input;
|
|
}
|
|
|
|
delete input.dataset.hasSyntaxError;
|
|
input.removeAttribute('pattern');
|
|
input.removeAttribute('title');
|
|
}
|
|
|
|
getQueryString() {
|
|
return this.termsToQueryString(this.usedTerms);
|
|
}
|
|
|
|
saveTerm(input, updateDOM = true, force = false) {
|
|
let termIndex = input.parentNode.dataset.index;
|
|
let termData = this.readFullTerm(input, termIndex);
|
|
|
|
// Only save if something has changed, unless forced
|
|
if (termData === false) {
|
|
console.warn('[BaseInput] Input is empty, cannot save');
|
|
} else if (force || this.usedTerms[termIndex].label !== termData.label) {
|
|
let oldTermData = this.usedTerms[termIndex];
|
|
this.usedTerms[termIndex] = termData;
|
|
this.updateTermData(termData, input);
|
|
|
|
return oldTermData;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
updateTermData(termData, input) {
|
|
let label = input.parentNode;
|
|
label.dataset.label = termData.label;
|
|
|
|
if (!! termData.search || termData.search === '') {
|
|
label.dataset.search = termData.search;
|
|
}
|
|
|
|
if (!! termData.title) {
|
|
label.title = termData.title;
|
|
} else {
|
|
label.title = '';
|
|
}
|
|
}
|
|
|
|
termsToQueryString(terms) {
|
|
return terms.map(e => this.encodeTerm(e).search).join(this.separator).trim();
|
|
}
|
|
|
|
lastTerm() {
|
|
if (! this.hasTerms()) {
|
|
return null;
|
|
}
|
|
|
|
return this.usedTerms[this.usedTerms.length - 1];
|
|
}
|
|
|
|
popTerm() {
|
|
let lastTermIndex = this.usedTerms.length - 1;
|
|
return this.removeTerm(this.termContainer.querySelector(`[data-index="${ lastTermIndex }"]`));
|
|
}
|
|
|
|
removeTerm(label, updateDOM = true) {
|
|
if (this.completer !== null) {
|
|
this.completer.reset();
|
|
}
|
|
|
|
let termIndex = Number(label.dataset.index);
|
|
|
|
// Re-index following remaining terms
|
|
this.reIndexTerms(termIndex);
|
|
|
|
// Cut the term's data
|
|
let [termData] = this.usedTerms.splice(termIndex, 1);
|
|
|
|
// Avoid saving the term, it's removed after all
|
|
label.firstChild.skipSaveOnBlur = true;
|
|
|
|
if (updateDOM) {
|
|
// Remove it from the DOM
|
|
this.removeRenderedTerm(label);
|
|
}
|
|
|
|
return termData;
|
|
}
|
|
|
|
removeRenderedTerm(label) {
|
|
label.remove();
|
|
}
|
|
|
|
removeRange(labels) {
|
|
let from = Number(labels[0].dataset.index);
|
|
let to = Number(labels[labels.length - 1].dataset.index);
|
|
let deleteCount = to - from + 1;
|
|
|
|
if (to < this.usedTerms.length - 1) {
|
|
// Only re-index if there's something left
|
|
this.reIndexTerms(to, deleteCount);
|
|
}
|
|
|
|
let removedData = this.usedTerms.splice(from, deleteCount);
|
|
|
|
this.removeRenderedRange(labels);
|
|
|
|
let removedTerms = {};
|
|
for (let i = from; removedData.length; i++) {
|
|
removedTerms[i] = removedData.shift();
|
|
}
|
|
|
|
return removedTerms;
|
|
}
|
|
|
|
removeRenderedRange(labels) {
|
|
labels.forEach(label => this.removeRenderedTerm(label));
|
|
}
|
|
|
|
reIndexTerms(from, howMuch = 1, forward = false) {
|
|
if (forward) {
|
|
for (let i = this.usedTerms.length - 1; i >= from; i--) {
|
|
let label = this.termContainer.querySelector(`[data-index="${ i }"]`);
|
|
label.dataset.index = `${ i + howMuch }`;
|
|
}
|
|
} else {
|
|
for (let i = ++from; i < this.usedTerms.length; i++) {
|
|
let label = this.termContainer.querySelector(`[data-index="${ i }"]`);
|
|
label.dataset.index = `${ i - howMuch }`;
|
|
}
|
|
}
|
|
}
|
|
|
|
complete(input, data) {
|
|
if (this.completer !== null) {
|
|
$(input).trigger('complete', data);
|
|
}
|
|
}
|
|
|
|
selectTerms() {
|
|
this.termContainer.querySelectorAll('[data-index]').forEach(el => el.classList.add('selected'));
|
|
}
|
|
|
|
deselectTerms() {
|
|
this.termContainer.querySelectorAll('.selected').forEach(el => el.classList.remove('selected'));
|
|
}
|
|
|
|
clearSelectedTerms() {
|
|
if (this.hasTerms()) {
|
|
let labels = this.termContainer.querySelectorAll('.selected');
|
|
if (labels.length) {
|
|
return this.removeRange(Array.from(labels));
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
togglePlaceholder() {
|
|
let placeholder = '';
|
|
|
|
if (! this.hasTerms()) {
|
|
if (this.input.dataset.placeholder) {
|
|
placeholder = this.input.dataset.placeholder;
|
|
} else {
|
|
return;
|
|
}
|
|
} else if (this.input.placeholder) {
|
|
if (! this.input.dataset.placeholder) {
|
|
this.input.dataset.placeholder = this.input.placeholder;
|
|
}
|
|
}
|
|
|
|
this.input.placeholder = placeholder;
|
|
}
|
|
|
|
renderTerm(termData, termIndex) {
|
|
let label = $.render('<label><input type="text"></label>');
|
|
|
|
if (termData.class) {
|
|
label.classList.add(termData.class);
|
|
}
|
|
|
|
if (termData.title) {
|
|
label.title = termData.title;
|
|
}
|
|
|
|
label.dataset.label = termData.label;
|
|
label.dataset.search = termData.search;
|
|
label.dataset.index = termIndex;
|
|
|
|
label.firstChild.value = termData.label;
|
|
|
|
return label;
|
|
}
|
|
|
|
encodeTerm(termData) {
|
|
termData = { ...termData };
|
|
termData.search = encodeURIComponent(termData.search);
|
|
|
|
return termData;
|
|
}
|
|
|
|
decodeTerm(termData) {
|
|
termData.search = decodeURIComponent(termData.search);
|
|
|
|
return termData;
|
|
}
|
|
|
|
shouldNotAutoSubmit() {
|
|
return 'noAutoSubmit' in this.input.dataset;
|
|
}
|
|
|
|
autoSubmit(input, changeType, changedTerms) {
|
|
if (this.shouldNotAutoSubmit()) {
|
|
return;
|
|
}
|
|
|
|
if (changeType === 'save') {
|
|
// Replace old term data with the new one, as required by the backend
|
|
for (const termIndex of Object.keys(changedTerms)) {
|
|
changedTerms[termIndex] = this.usedTerms[termIndex];
|
|
}
|
|
}
|
|
|
|
this.dataInput.value = JSON.stringify({
|
|
type: changeType,
|
|
terms: changedTerms
|
|
});
|
|
|
|
if (Object.keys(changedTerms).length) {
|
|
$(this.input.form).trigger('submit', { submittedBy: input });
|
|
}
|
|
}
|
|
|
|
submitTerms(terms) {
|
|
$(this.input.form).trigger(
|
|
'submit',
|
|
{ terms: terms }
|
|
);
|
|
}
|
|
|
|
moveFocusForward(from = null) {
|
|
let toFocus;
|
|
|
|
let inputs = Array.from(this.termContainer.querySelectorAll('input'));
|
|
if (from === null) {
|
|
let focused = this.termContainer.querySelector('input:focus');
|
|
from = inputs.indexOf(focused);
|
|
}
|
|
|
|
if (from === -1) {
|
|
toFocus = inputs.shift();
|
|
} else if (from + 1 < inputs.length) {
|
|
toFocus = inputs[from + 1];
|
|
} else {
|
|
toFocus = this.input;
|
|
}
|
|
|
|
toFocus.selectionStart = toFocus.selectionEnd = 0;
|
|
$(toFocus).focus();
|
|
|
|
return toFocus;
|
|
}
|
|
|
|
moveFocusBackward(from = null) {
|
|
let toFocus;
|
|
|
|
let inputs = Array.from(this.termContainer.querySelectorAll('input'));
|
|
if (from === null) {
|
|
let focused = this.termContainer.querySelector('input:focus');
|
|
from = inputs.indexOf(focused);
|
|
}
|
|
|
|
if (from === -1) {
|
|
toFocus = inputs.pop();
|
|
} else if (from > 0 && from - 1 < inputs.length) {
|
|
toFocus = inputs[from - 1];
|
|
} else {
|
|
toFocus = this.input;
|
|
}
|
|
|
|
toFocus.selectionStart = toFocus.selectionEnd = toFocus.value.length;
|
|
$(toFocus).focus();
|
|
|
|
return toFocus;
|
|
}
|
|
|
|
/**
|
|
* Event listeners
|
|
*/
|
|
|
|
onSubmit(event) {
|
|
// Unset the input's name, to prevent its submission (It may actually have a name, as no-js fallback)
|
|
this.input.name = '';
|
|
|
|
// Set the hidden input's value, it's what's sent
|
|
if (event.detail && 'terms' in event.detail) {
|
|
this.termInput.value = event.detail.terms;
|
|
} else {
|
|
let renderedTerms = this.termsToQueryString(this.usedTerms);
|
|
if (this.hasSyntaxError()) {
|
|
renderedTerms += this.input.value;
|
|
}
|
|
|
|
this.termInput.value = renderedTerms;
|
|
}
|
|
|
|
// Enable the hidden input, otherwise it's not submitted
|
|
this.termInput.disabled = false;
|
|
}
|
|
|
|
onSuggestion(event) {
|
|
let data = event.detail;
|
|
let input = event.target;
|
|
|
|
let termData;
|
|
if (typeof data === 'object') {
|
|
termData = data;
|
|
} else {
|
|
termData = { label: data, search: data };
|
|
}
|
|
|
|
this.lastCompletedTerm = termData;
|
|
this.writePartialTerm(termData.label, input);
|
|
}
|
|
|
|
onCompletion(event) {
|
|
let input = event.target;
|
|
let termData = event.detail;
|
|
let termIndex = Number(input.parentNode.dataset.index);
|
|
|
|
this.lastCompletedTerm = termData;
|
|
this.writePartialTerm(termData.label, input);
|
|
|
|
if (termIndex >= 0) {
|
|
this.autoSubmit(input, 'save', { [termIndex]: this.saveTerm(input, false, true) });
|
|
} else {
|
|
this.autoSubmit(input, 'exchange', this.exchangeTerm());
|
|
this.togglePlaceholder();
|
|
}
|
|
}
|
|
|
|
onInput(event) {
|
|
let input = event.target;
|
|
let isTerm = input.parentNode.dataset.index >= 0;
|
|
|
|
let termData = { label: this.readPartialTerm(input) };
|
|
this.updateTermData(termData, input);
|
|
|
|
if (! input.value && this.hasSyntaxError(input)) {
|
|
this.clearSyntaxError(input);
|
|
}
|
|
|
|
if (! this.hasSyntaxError(input)) {
|
|
this.complete(input, { term: termData });
|
|
}
|
|
|
|
if (! isTerm) {
|
|
this.autoSubmit(this.input, 'remove', this.clearSelectedTerms());
|
|
this.togglePlaceholder();
|
|
}
|
|
}
|
|
|
|
onKeyDown(event) {
|
|
let input = event.target;
|
|
let termIndex = Number(input.parentNode.dataset.index);
|
|
|
|
if (this.hasSyntaxError(input) && ! (/[A-Z]/.test(event.key.charAt(0)) || event.ctrlKey || event.metaKey)) {
|
|
// Clear syntax error flag if the user types entirely new input after having selected the entire input
|
|
// (This way the input isn't empty but switches from input to input immediately, causing the clearing
|
|
// in onInput to not work)
|
|
if (input.selectionEnd - input.selectionStart === input.value.length) {
|
|
this.clearSyntaxError(input);
|
|
}
|
|
}
|
|
|
|
let removedTerms;
|
|
switch (event.key) {
|
|
case ' ':
|
|
if (! this.readPartialTerm(input)) {
|
|
this.complete(input, { term: { label: '' } });
|
|
event.preventDefault();
|
|
}
|
|
break;
|
|
case 'Backspace':
|
|
removedTerms = this.clearSelectedTerms();
|
|
|
|
if (termIndex >= 0 && ! input.value) {
|
|
let removedTerm = this.removeTerm(input.parentNode);
|
|
if (removedTerm !== false) {
|
|
input = this.moveFocusBackward(termIndex);
|
|
if (event.ctrlKey || event.metaKey) {
|
|
this.clearPartialTerm(input);
|
|
} else {
|
|
this.writePartialTerm(input.value.slice(0, -1), input);
|
|
}
|
|
|
|
removedTerms[termIndex] = removedTerm;
|
|
event.preventDefault();
|
|
}
|
|
} else if (isNaN(termIndex)) {
|
|
if (! input.value && this.hasTerms()) {
|
|
let termData = this.popTerm();
|
|
if (! event.ctrlKey && ! event.metaKey) {
|
|
// Removing the last char programmatically is not
|
|
// necessary since the browser default is not prevented
|
|
this.writePartialTerm(termData.label, input);
|
|
}
|
|
|
|
removedTerms[this.usedTerms.length] = termData;
|
|
}
|
|
}
|
|
|
|
this.togglePlaceholder();
|
|
this.autoSubmit(input, 'remove', removedTerms);
|
|
break;
|
|
case 'Delete':
|
|
removedTerms = this.clearSelectedTerms();
|
|
|
|
if (termIndex >= 0 && ! input.value) {
|
|
let removedTerm = this.removeTerm(input.parentNode);
|
|
if (removedTerm !== false) {
|
|
input = this.moveFocusForward(termIndex - 1);
|
|
if (event.ctrlKey || event.metaKey) {
|
|
this.clearPartialTerm(input);
|
|
} else {
|
|
this.writePartialTerm(input.value.slice(1), input);
|
|
}
|
|
|
|
removedTerms[termIndex] = removedTerm;
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
this.togglePlaceholder();
|
|
this.autoSubmit(input, 'remove', removedTerms);
|
|
break;
|
|
case 'Enter':
|
|
if (termIndex >= 0) {
|
|
if (this.readPartialTerm(input)) {
|
|
this.saveTerm(input, false);
|
|
} else {
|
|
this.removeTerm(input.parentNode, false);
|
|
}
|
|
}
|
|
break;
|
|
case 'ArrowLeft':
|
|
if (input.selectionStart === 0 && this.hasTerms()) {
|
|
event.preventDefault();
|
|
this.moveFocusBackward();
|
|
}
|
|
break;
|
|
case 'ArrowRight':
|
|
if (input.selectionStart === input.value.length && this.hasTerms()) {
|
|
event.preventDefault();
|
|
this.moveFocusForward();
|
|
}
|
|
break;
|
|
case 'a':
|
|
if ((event.ctrlKey || event.metaKey) && ! this.readPartialTerm(input)) {
|
|
this.selectTerms();
|
|
}
|
|
}
|
|
}
|
|
|
|
onKeyUp(event) {
|
|
if (event.target.parentNode.dataset.index >= 0) {
|
|
return;
|
|
}
|
|
|
|
switch (event.key) {
|
|
case 'End':
|
|
case 'ArrowLeft':
|
|
case 'ArrowRight':
|
|
this.deselectTerms();
|
|
break;
|
|
case 'Home':
|
|
if (this.input.selectionStart === 0 && this.input.selectionEnd === 0) {
|
|
if (event.shiftKey) {
|
|
this.selectTerms();
|
|
} else {
|
|
this.deselectTerms();
|
|
}
|
|
}
|
|
|
|
break;
|
|
case 'Delete':
|
|
this.autoSubmit(event.target, 'remove', this.clearSelectedTerms());
|
|
this.togglePlaceholder();
|
|
break;
|
|
}
|
|
}
|
|
|
|
onInputBlur() {
|
|
this.deselectTerms();
|
|
}
|
|
|
|
onTermFocusOut(event) {
|
|
let input = event.target;
|
|
if (this.hasSyntaxError(input)) {
|
|
return;
|
|
}
|
|
|
|
// skipSaveOnBlur is set if the input is about to be removed anyway.
|
|
// If we remove the input as well, the other removal will fail without
|
|
// any chance to handle it. (Element.remove() blurs the input)
|
|
if (typeof input.skipSaveOnBlur === 'undefined' || ! input.skipSaveOnBlur) {
|
|
setTimeout(() => {
|
|
if (this.completer === null || ! this.completer.isBeingCompleted(input)) {
|
|
let termIndex = Number(input.parentNode.dataset.index);
|
|
if (this.readPartialTerm(input)) {
|
|
let previousTerm = this.saveTerm(input);
|
|
if (previousTerm !== false) {
|
|
this.autoSubmit(input, 'save', { [termIndex]: previousTerm });
|
|
}
|
|
} else {
|
|
this.autoSubmit(input, 'remove', { [termIndex]: this.removeTerm(input.parentNode) });
|
|
}
|
|
}
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
onTermFocus(event) {
|
|
if (event.detail.scripted) {
|
|
// Only request suggestions if the user manually focuses the term
|
|
return;
|
|
}
|
|
|
|
this.deselectTerms();
|
|
|
|
let input = event.target;
|
|
if (! this.hasSyntaxError(input) && ! this.completer.isBeingCompleted(input, false)) {
|
|
// Only request suggestions if the input is valid and not already being completed
|
|
let value = this.readPartialTerm(input);
|
|
this.complete(input, { trigger: 'script', term: { label: value } });
|
|
}
|
|
}
|
|
|
|
onButtonClick(event) {
|
|
if (! this.hasSyntaxError()) {
|
|
// Register current input value, otherwise it's not included
|
|
this.exchangeTerm();
|
|
}
|
|
|
|
if (this.hasTerms()) {
|
|
this.input.required = false;
|
|
|
|
// This is not part of `onSubmit()` because otherwise it would override what `autoSubmit()` does
|
|
this.dataInput.value = JSON.stringify({ type: 'submit', terms: this.usedTerms });
|
|
} else if (typeof this.input.dataset.manageRequired !== 'undefined') {
|
|
this.input.required = true;
|
|
}
|
|
}
|
|
|
|
onPaste(event) {
|
|
if (this.hasTerms() || this.input.value) {
|
|
return;
|
|
}
|
|
|
|
this.submitTerms(event.clipboardData.getData('text/plain'));
|
|
|
|
event.preventDefault();
|
|
}
|
|
|
|
onCopyAndCut(event) {
|
|
if (! this.hasTerms()) {
|
|
return;
|
|
}
|
|
|
|
let data = '';
|
|
|
|
let selectedTerms = this.termContainer.querySelectorAll('.selected');
|
|
if (selectedTerms.length) {
|
|
data = Array.from(selectedTerms).map(label => label.dataset.search).join(this.separator);
|
|
}
|
|
|
|
if (this.input.selectionStart < this.input.selectionEnd) {
|
|
data += this.separator + this.input.value.slice(this.input.selectionStart, this.input.selectionEnd);
|
|
}
|
|
|
|
event.clipboardData.setData('text/plain', data);
|
|
event.preventDefault();
|
|
|
|
if (event.type === 'cut') {
|
|
this.clearPartialTerm(this.input);
|
|
this.autoSubmit(this.input, 'remove', this.clearSelectedTerms());
|
|
this.togglePlaceholder();
|
|
}
|
|
}
|
|
}
|
|
|
|
return BaseInput;
|
|
});
|