summaryrefslogtreecommitdiffstats
path: root/roles/reverseproxy/files/conversejs/src/shared/autocomplete/autocomplete.js
diff options
context:
space:
mode:
Diffstat (limited to 'roles/reverseproxy/files/conversejs/src/shared/autocomplete/autocomplete.js')
-rw-r--r--roles/reverseproxy/files/conversejs/src/shared/autocomplete/autocomplete.js321
1 files changed, 321 insertions, 0 deletions
diff --git a/roles/reverseproxy/files/conversejs/src/shared/autocomplete/autocomplete.js b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/autocomplete.js
new file mode 100644
index 0000000..21eeb53
--- /dev/null
+++ b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/autocomplete.js
@@ -0,0 +1,321 @@
+/**
+ * @copyright Lea Verou and the Converse.js contributors
+ * @description
+ * Started as a fork of Lea Verou's "Awesomplete"
+ * https://leaverou.github.io/awesomplete/
+ * @license Mozilla Public License (MPLv2)
+ */
+
+import { Events } from '@converse/skeletor/src/events.js';
+import { helpers, FILTER_CONTAINS, ITEM, SORT_BY_QUERY_POSITION } from './utils.js';
+import Suggestion from './suggestion.js';
+import { converse } from "@converse/headless/core";
+
+
+const u = converse.env.utils;
+
+
+export class AutoComplete {
+
+ constructor (el, config={}) {
+ this.suggestions = [];
+ this.is_opened = false;
+
+ if (u.hasClass('suggestion-box', el)) {
+ this.container = el;
+ } else {
+ this.container = el.querySelector('.suggestion-box');
+ }
+ this.input = this.container.querySelector('.suggestion-box__input');
+ this.input.setAttribute("aria-autocomplete", "list");
+
+ this.ul = this.container.querySelector('.suggestion-box__results');
+ this.status = this.container.querySelector('.suggestion-box__additions');
+
+ Object.assign(this, {
+ 'match_current_word': false, // Match only the current word, otherwise all input is matched
+ 'ac_triggers': [], // Array of keys (`ev.key`) values that will trigger auto-complete
+ 'include_triggers': [], // Array of trigger keys which should be included in the returned value
+ 'min_chars': 2,
+ 'max_items': 10,
+ 'auto_evaluate': true, // Should evaluation happen automatically without any particular key as trigger?
+ 'auto_first': false, // Should the first element be automatically selected?
+ 'data': a => a,
+ 'filter': FILTER_CONTAINS,
+ 'sort': config.sort === false ? false : SORT_BY_QUERY_POSITION,
+ 'item': ITEM
+ }, config);
+
+ this.index = -1;
+
+ this.bindEvents()
+
+ if (this.input.hasAttribute("list")) {
+ this.list = "#" + this.input.getAttribute("list");
+ this.input.removeAttribute("list");
+ } else {
+ this.list = this.input.getAttribute("data-list") || config.list || [];
+ }
+ }
+
+ bindEvents () {
+ const input = {
+ "blur": () => this.close({'reason': 'blur'})
+ }
+ if (this.auto_evaluate) {
+ input["input"] = (e) => this.evaluate(e);
+ }
+
+ this._events = {
+ 'input': input,
+ 'form': {
+ "submit": () => this.close({'reason': 'submit'})
+ },
+ 'ul': {
+ "mousedown": (ev) => this.onMouseDown(ev),
+ "mouseover": (ev) => this.onMouseOver(ev)
+ }
+ };
+ helpers.bind(this.input, this._events.input);
+ helpers.bind(this.input.form, this._events.form);
+ helpers.bind(this.ul, this._events.ul);
+ }
+
+ set list (list) {
+ if (Array.isArray(list) || typeof list === "function") {
+ this._list = list;
+ } else if (typeof list === "string" && list.includes(",")) {
+ this._list = list.split(/\s*,\s*/);
+ } else { // Element or CSS selector
+ const children = helpers.getElement(list)?.children || [];
+ this._list = Array.from(children)
+ .filter(el => !el.disabled)
+ .map(el => {
+ const text = el.textContent.trim();
+ const value = el.value || text;
+ const label = el.label || text;
+ return (value !== "") ? { label, value } : null;
+ })
+ .filter(i => i);
+ }
+
+ if (document.activeElement === this.input) {
+ this.evaluate();
+ }
+ }
+
+ get list () {
+ return this._list;
+ }
+
+ get selected () {
+ return this.index > -1;
+ }
+
+ get opened () {
+ return this.is_opened;
+ }
+
+ close (o) {
+ if (!this.opened) {
+ return;
+ }
+ this.ul.setAttribute("hidden", "");
+ this.is_opened = false;
+ this.index = -1;
+ this.trigger("suggestion-box-close", o || {});
+ }
+
+ insertValue (suggestion) {
+ if (this.match_current_word) {
+ u.replaceCurrentWord(this.input, suggestion.value);
+ } else {
+ this.input.value = suggestion.value;
+ }
+ }
+
+ open () {
+ this.ul.removeAttribute("hidden");
+ this.is_opened = true;
+
+ if (this.auto_first && this.index === -1) {
+ this.goto(0);
+ }
+ this.trigger("suggestion-box-open");
+ }
+
+ destroy () {
+ //remove events from the input and its form
+ helpers.unbind(this.input, this._events.input);
+ helpers.unbind(this.input.form, this._events.form);
+ this.input.removeAttribute("aria-autocomplete");
+ }
+
+ next () {
+ const count = this.ul.children.length;
+ this.goto(this.index < count - 1 ? this.index + 1 : (count ? 0 : -1) );
+ }
+
+ previous () {
+ const count = this.ul.children.length,
+ pos = this.index - 1;
+ this.goto(this.selected && pos !== -1 ? pos : count - 1);
+ }
+
+ goto (i, scroll=true) {
+ // Should not be used directly, highlights specific item without any checks!
+ const list = this.ul.children;
+ if (this.selected) {
+ list[this.index].setAttribute("aria-selected", "false");
+ }
+ this.index = i;
+
+ if (i > -1 && list.length > 0) {
+ list[i].setAttribute("aria-selected", "true");
+ list[i].focus();
+ this.status.textContent = list[i].textContent;
+
+ if (scroll) {
+ // scroll to highlighted element in case parent's height is fixed
+ this.ul.scrollTop = list[i].offsetTop - this.ul.clientHeight + list[i].clientHeight;
+ }
+ this.trigger("suggestion-box-highlight", {'text': this.suggestions[this.index]});
+ }
+ }
+
+ select (selected) {
+ if (selected) {
+ this.index = u.siblingIndex(selected);
+ } else {
+ selected = this.ul.children[this.index];
+ }
+ if (selected) {
+ const suggestion = this.suggestions[this.index];
+ this.insertValue(suggestion);
+ this.close({'reason': 'select'});
+ this.auto_completing = false;
+ this.trigger("suggestion-box-selectcomplete", {'text': suggestion});
+ }
+ }
+
+ onMouseOver (ev) {
+ const li = u.ancestor(ev.target, 'li');
+ if (li) {
+ const index = Array.prototype.slice.call(this.ul.children).indexOf(li);
+ this.goto(index, false);
+ }
+ }
+
+ onMouseDown (ev) {
+ if (ev.button !== 0) {
+ return; // Only select on left click
+ }
+ const li = u.ancestor(ev.target, 'li');
+ if (li) {
+ ev.preventDefault();
+ this.select(li, ev.target);
+ }
+ }
+
+ onKeyDown (ev) {
+ if (this.opened) {
+ if ([converse.keycodes.ENTER, converse.keycodes.TAB].includes(ev.keyCode) && this.selected) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.select();
+ return true;
+ } else if (ev.keyCode === converse.keycodes.ESCAPE) {
+ this.close({'reason': 'esc'});
+ return true;
+ } else if ([converse.keycodes.UP_ARROW, converse.keycodes.DOWN_ARROW].includes(ev.keyCode)) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this[ev.keyCode === converse.keycodes.UP_ARROW ? "previous" : "next"]();
+ return true;
+ }
+ }
+
+ if ([converse.keycodes.SHIFT,
+ converse.keycodes.META,
+ converse.keycodes.META_RIGHT,
+ converse.keycodes.ESCAPE,
+ converse.keycodes.ALT
+ ].includes(ev.keyCode)) {
+
+ return;
+ }
+
+ if (this.ac_triggers.includes(ev.key)) {
+ if (ev.key === "Tab") {
+ ev.preventDefault();
+ }
+ this.auto_completing = true;
+ } else if (ev.key === "Backspace") {
+ const word = u.getCurrentWord(ev.target, ev.target.selectionEnd-1);
+ if (helpers.isMention(word, this.ac_triggers)) {
+ this.auto_completing = true;
+ }
+ }
+ }
+
+ async evaluate (ev) {
+ const selecting = this.selected && ev && (
+ ev.keyCode === converse.keycodes.UP_ARROW ||
+ ev.keyCode === converse.keycodes.DOWN_ARROW
+ );
+
+ if (!this.auto_evaluate && !this.auto_completing || selecting) {
+ return;
+ }
+
+ let value = this.match_current_word ? u.getCurrentWord(this.input) : this.input.value;
+
+ const contains_trigger = helpers.isMention(value, this.ac_triggers);
+ if (contains_trigger && !this.include_triggers.includes(ev.key)) {
+ value = u.isMentionBoundary(value[0])
+ ? value.slice('2')
+ : value.slice('1');
+ }
+
+ const is_long_enough = value.length && value.length >= this.min_chars;
+
+ if (contains_trigger || is_long_enough) {
+ this.auto_completing = true;
+
+ const list = typeof this._list === "function" ? await this._list(value) : this._list;
+ if (list.length === 0 || !this.auto_completing) {
+ this.close({'reason': 'nomatches'});
+ return;
+ }
+
+ this.index = -1;
+ this.ul.innerHTML = "";
+
+ this.suggestions = list
+ .map(item => new Suggestion(this.data(item, value), value))
+ .filter(item => this.filter(item, value));
+
+ if (this.sort !== false) {
+ this.suggestions = this.suggestions.sort(this.sort);
+ }
+ this.suggestions = this.suggestions.slice(0, this.max_items);
+ this.suggestions.forEach(text => this.ul.appendChild(this.item(text, value)));
+
+ if (this.ul.children.length === 0) {
+ this.close({'reason': 'nomatches'});
+ } else {
+ this.open();
+ }
+ } else {
+ this.close({'reason': 'nomatches'});
+ if (!contains_trigger) {
+ this.auto_completing = false;
+ }
+ }
+ }
+}
+
+// Make it an event emitter
+Object.assign(AutoComplete.prototype, Events);
+
+export default AutoComplete;