diff --git a/umap/static/umap/js/modules/autocomplete.js b/umap/static/umap/js/modules/autocomplete.js new file mode 100644 index 00000000..3c2949a1 --- /dev/null +++ b/umap/static/umap/js/modules/autocomplete.js @@ -0,0 +1,309 @@ +import { DomUtil, DomEvent, setOptions } from '../../vendors/leaflet/leaflet-src.esm.js' +import { translate } from './i18n.js' +import { ServerRequest } from './request.js' +import Alert from './ui/alert.js' + +export class BaseAutocomplete { + constructor(el, options) { + this.el = el + this.options = { + placeholder: translate('Start typing...'), + emptyMessage: translate('No result'), + allowFree: true, + minChar: 2, + maxResults: 5, + } + this.cache = '' + this.results = [] + this._current = null + setOptions(this, options) + this.createInput() + this.createContainer() + this.selectedContainer = this.initSelectedContainer() + } + + get current() { + return this._current + } + + set current(index) { + if (typeof index === 'object') { + index = this.resultToIndex(index) + } + this._current = index + } + + createInput() { + this.input = DomUtil.element({ + tagName: 'input', + type: 'text', + parent: this.el, + placeholder: this.options.placeholder, + autocomplete: 'off', + className: this.options.className, + }) + DomEvent.on(this.input, 'keydown', this.onKeyDown, this) + DomEvent.on(this.input, 'keyup', this.onKeyUp, this) + DomEvent.on(this.input, 'blur', this.onBlur, this) + } + + createContainer() { + this.container = DomUtil.element({ + tagName: 'ul', + parent: document.body, + className: 'umap-autocomplete', + }) + } + + resizeContainer() { + const l = this.getLeft(this.input) + const t = this.getTop(this.input) + this.input.offsetHeight + this.container.style.left = `${l}px` + this.container.style.top = `${t}px` + const width = this.options.width ? this.options.width : this.input.offsetWidth - 2 + this.container.style.width = `${width}px` + } + + onKeyDown(e) { + switch (e.key) { + case 'Tab': + if (this.current !== null) this.setChoice() + DomEvent.stop(e) + break + case 'Enter': + DomEvent.stop(e) + this.setChoice() + break + case 'Escape': + DomEvent.stop(e) + this.hide() + break + case 'ArrowDown': + if (this.results.length > 0) { + if (this.current !== null && this.current < this.results.length - 1) { + // what if one result? + this.current++ + this.highlight() + } else if (this.current === null) { + this.current = 0 + this.highlight() + } + } + break + case 'ArrowUp': + if (this.current !== null) { + DomEvent.stop(e) + } + if (this.results.length > 0) { + if (this.current > 0) { + this.current-- + this.highlight() + } else if (this.current === 0) { + this.current = null + this.highlight() + } + } + break + } + } + + onKeyUp(e) { + const special = [ + 'Tab', + 'Enter', + 'ArrowLeft', + 'ArrowRight', + 'ArrowDown', + 'ArrowUp', + 'Meta', + 'Shift', + 'Alt', + 'Control', + ] + if (!special.includes(e.key)) { + this.search() + } + } + + onBlur() { + setTimeout(() => this.hide(), 100) + } + + clear() { + this.results = [] + this.current = null + this.cache = '' + this.container.innerHTML = '' + } + + hide() { + this.clear() + this.container.style.display = 'none' + this.input.value = '' + } + + setChoice(choice) { + choice = choice || this.results[this.current] + if (choice) { + this.input.value = choice.item.label + this.options.on_select(choice) + this.displaySelected(choice) + this.hide() + if (this.options.callback) { + this.options.callback.bind(this)(choice) + } + } + } + + createResult(item) { + const el = DomUtil.element({ + tagName: 'li', + parent: this.container, + textContent: item.label, + }) + const result = { + item: item, + el: el, + } + DomEvent.on(el, 'mouseover', () => { + this.current = result + this.highlight() + }) + DomEvent.on(el, 'mousedown', () => this.setChoice()) + return result + } + + resultToIndex(result) { + return this.results.findIndex((item) => item.item.value === result.item.value) + } + + handleResults(data) { + this.clear() + this.container.style.display = 'block' + this.resizeContainer() + data.forEach((item) => { + this.results.push(this.createResult(item)) + }) + this.current = 0 + this.highlight() + //TODO manage no results + } + + highlight() { + this.results.forEach((result, index) => { + if (index === this.current) DomUtil.addClass(result.el, 'on') + else DomUtil.removeClass(result.el, 'on') + }) + } + + getLeft(el) { + let tmp = el.offsetLeft + el = el.offsetParent + while (el) { + tmp += el.offsetLeft + el = el.offsetParent + } + return tmp + } + + getTop(el) { + let tmp = el.offsetTop + el = el.offsetParent + while (el) { + tmp += el.offsetTop + el = el.offsetParent + } + return tmp + } +} + +class BaseAjax extends BaseAutocomplete { + constructor(el, options) { + super(el, options) + const alert = new Alert(document.querySelector('header')) + this.server = new ServerRequest(alert) + } + optionToResult(option) { + return { + value: option.value, + label: option.innerHTML, + } + } + + async search() { + let val = this.input.value + if (val.length < this.options.minChar) { + this.clear() + return + } + if (val === this.cache) return + else this.cache = val + val = val.toLowerCase() + const [{ data }, response] = await this.server.get( + `/agnocomplete/AutocompleteUser/?q=${encodeURIComponent(val)}` + ) + this.handleResults(data) + } +} + +export class AjaxAutocompleteMultiple extends BaseAjax { + initSelectedContainer() { + return DomUtil.after( + this.input, + DomUtil.element({ tagName: 'ul', className: 'umap-multiresult' }) + ) + } + + displaySelected(result) { + const result_el = DomUtil.element({ + tagName: 'li', + parent: this.selectedContainer, + }) + result_el.textContent = result.item.label + const close = DomUtil.element({ + tagName: 'span', + parent: result_el, + className: 'close', + textContent: '×', + }) + DomEvent.on(close, 'click', () => { + this.selectedContainer.removeChild(result_el) + this.options.on_unselect(result) + }) + this.hide() + } +} + +export class AjaxAutocomplete extends BaseAjax { + initSelectedContainer() { + return DomUtil.after( + this.input, + DomUtil.element({ tagName: 'div', className: 'umap-singleresult' }) + ) + } + + displaySelected(result) { + const result_el = DomUtil.element({ + tagName: 'div', + parent: this.selectedContainer, + }) + result_el.textContent = result.item.label + const close = DomUtil.element({ + tagName: 'span', + parent: result_el, + className: 'close', + textContent: '×', + }) + this.input.style.display = 'none' + DomEvent.on( + close, + 'click', + function () { + this.selectedContainer.innerHTML = '' + this.input.style.display = 'block' + }, + this + ) + this.hide() + } +} diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index 42aff04e..b6dfd8e2 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -9,6 +9,7 @@ import Tooltip from './ui/tooltip.js' import * as Utils from './utils.js' import { SCHEMA } from './schema.js' import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js' +import { AjaxAutocomplete, AjaxAutocompleteMultiple } from './autocomplete.js' import Orderable from './orderable.js' // Import modules and export them to the global scope. @@ -33,4 +34,6 @@ window.U = { SCHEMA, Orderable, Caption, + AjaxAutocomplete, + AjaxAutocompleteMultiple, } diff --git a/umap/static/umap/js/umap.autocomplete.js b/umap/static/umap/js/umap.autocomplete.js deleted file mode 100644 index 87c9561b..00000000 --- a/umap/static/umap/js/umap.autocomplete.js +++ /dev/null @@ -1,341 +0,0 @@ -U.AutoComplete = L.Class.extend({ - options: { - placeholder: 'Start typing...', - emptyMessage: 'No result', - allowFree: true, - minChar: 2, - maxResults: 5, - }, - - CACHE: '', - RESULTS: [], - - initialize: function (el, options) { - this.el = el - const alert = new U.Alert(document.querySelector('header')) - this.server = new U.ServerRequest(alert) - L.setOptions(this, options) - let CURRENT = null - try { - Object.defineProperty(this, 'CURRENT', { - get: function () { - return CURRENT - }, - set: function (index) { - if (typeof index === 'object') { - index = this.resultToIndex(index) - } - CURRENT = index - }, - }) - } catch (e) { - // Hello IE8 - } - return this - }, - - createInput: function () { - this.input = L.DomUtil.element({ - tagName: 'input', - type: 'text', - parent: this.el, - placeholder: this.options.placeholder, - autocomplete: 'off', - className: this.options.className, - }) - L.DomEvent.on(this.input, 'keydown', this.onKeyDown, this) - L.DomEvent.on(this.input, 'keyup', this.onKeyUp, this) - L.DomEvent.on(this.input, 'blur', this.onBlur, this) - }, - - createContainer: function () { - this.container = L.DomUtil.element({ - tagName: 'ul', - parent: document.body, - className: 'umap-autocomplete', - }) - }, - - resizeContainer: function () { - const l = this.getLeft(this.input) - const t = this.getTop(this.input) + this.input.offsetHeight - this.container.style.left = `${l}px` - this.container.style.top = `${t}px` - const width = this.options.width ? this.options.width : this.input.offsetWidth - 2 - this.container.style.width = `${width}px` - }, - - onKeyDown: function (e) { - switch (e.keyCode) { - case U.Keys.TAB: - if (this.CURRENT !== null) this.setChoice() - L.DomEvent.stop(e) - break - case U.Keys.ENTER: - L.DomEvent.stop(e) - this.setChoice() - break - case U.Keys.ESC: - L.DomEvent.stop(e) - this.hide() - break - case U.Keys.DOWN: - if (this.RESULTS.length > 0) { - if (this.CURRENT !== null && this.CURRENT < this.RESULTS.length - 1) { - // what if one result? - this.CURRENT++ - this.highlight() - } else if (this.CURRENT === null) { - this.CURRENT = 0 - this.highlight() - } - } - break - case U.Keys.UP: - if (this.CURRENT !== null) { - L.DomEvent.stop(e) - } - if (this.RESULTS.length > 0) { - if (this.CURRENT > 0) { - this.CURRENT-- - this.highlight() - } else if (this.CURRENT === 0) { - this.CURRENT = null - this.highlight() - } - } - break - } - }, - - onKeyUp: function (e) { - const special = [ - U.Keys.TAB, - U.Keys.ENTER, - U.Keys.LEFT, - U.Keys.RIGHT, - U.Keys.DOWN, - U.Keys.UP, - U.Keys.APPLE, - U.Keys.SHIFT, - U.Keys.ALT, - U.Keys.CTRL, - ] - if (special.indexOf(e.keyCode) === -1) { - this.search() - } - }, - - onBlur: function () { - setTimeout(() => this.hide(), 100) - }, - - clear: function () { - this.RESULTS = [] - this.CURRENT = null - this.CACHE = '' - this.container.innerHTML = '' - }, - - hide: function () { - this.clear() - this.container.style.display = 'none' - this.input.value = '' - }, - - setChoice: function (choice) { - choice = choice || this.RESULTS[this.CURRENT] - if (choice) { - this.input.value = choice.item.label - this.options.on_select(choice) - this.displaySelected(choice) - this.hide() - if (this.options.callback) { - L.Util.bind(this.options.callback, this)(choice) - } - } - }, - - search: async function () { - let val = this.input.value - if (val.length < this.options.minChar) { - this.clear() - return - } - if (`${val}` === `${this.CACHE}`) return - else this.CACHE = val - val = val.toLowerCase() - const [{ data }, response] = await this.server.get( - `/agnocomplete/AutocompleteUser/?q=${encodeURIComponent(val)}` - ) - this.handleResults(data) - }, - - createResult: function (item) { - const el = L.DomUtil.element({ - tagName: 'li', - parent: this.container, - textContent: item.label, - }) - const result = { - item: item, - el: el, - } - L.DomEvent.on( - el, - 'mouseover', - function () { - this.CURRENT = result - this.highlight() - }, - this - ) - L.DomEvent.on( - el, - 'mousedown', - function () { - this.setChoice() - }, - this - ) - return result - }, - - resultToIndex: function (result) { - let out = null - this.forEach(this.RESULTS, (item, index) => { - if (item.item.value == result.item.value) { - out = index - return - } - }) - return out - }, - - handleResults: function (data) { - this.clear() - this.container.style.display = 'block' - this.resizeContainer() - this.forEach(data, (item) => { - this.RESULTS.push(this.createResult(item)) - }) - this.CURRENT = 0 - this.highlight() - //TODO manage no results - }, - - highlight: function () { - this.forEach(this.RESULTS, (result, index) => { - if (index === this.CURRENT) L.DomUtil.addClass(result.el, 'on') - else L.DomUtil.removeClass(result.el, 'on') - }) - }, - - getLeft: function (el) { - let tmp = el.offsetLeft - el = el.offsetParent - while (el) { - tmp += el.offsetLeft - el = el.offsetParent - } - return tmp - }, - - getTop: function (el) { - let tmp = el.offsetTop - el = el.offsetParent - while (el) { - tmp += el.offsetTop - el = el.offsetParent - } - return tmp - }, - - forEach: function (els, callback) { - Array.prototype.forEach.call(els, callback) - }, -}) - -U.AutoComplete.Ajax = U.AutoComplete.extend({ - initialize: function (el, options) { - U.AutoComplete.prototype.initialize.call(this, el, options) - if (!this.el) return this - this.createInput() - this.createContainer() - this.selected_container = this.initSelectedContainer() - }, - - optionToResult: function (option) { - return { - value: option.value, - label: option.innerHTML, - } - }, -}) - -U.AutoComplete.Ajax.SelectMultiple = U.AutoComplete.Ajax.extend({ - initSelectedContainer: function () { - return L.DomUtil.after( - this.input, - L.DomUtil.element({ tagName: 'ul', className: 'umap-multiresult' }) - ) - }, - - displaySelected: function (result) { - const result_el = L.DomUtil.element({ - tagName: 'li', - parent: this.selected_container, - }) - result_el.textContent = result.item.label - const close = L.DomUtil.element({ - tagName: 'span', - parent: result_el, - className: 'close', - textContent: '×', - }) - L.DomEvent.on( - close, - 'click', - function () { - this.selected_container.removeChild(result_el) - this.options.on_unselect(result) - }, - this - ) - this.hide() - }, -}) - -U.AutoComplete.Ajax.Select = U.AutoComplete.Ajax.extend({ - initSelectedContainer: function () { - return L.DomUtil.after( - this.input, - L.DomUtil.element({ tagName: 'div', className: 'umap-singleresult' }) - ) - }, - - displaySelected: function (result) { - const result_el = L.DomUtil.element({ - tagName: 'div', - parent: this.selected_container, - }) - result_el.textContent = result.item.label - const close = L.DomUtil.element({ - tagName: 'span', - parent: result_el, - className: 'close', - textContent: '×', - }) - this.input.style.display = 'none' - L.DomEvent.on( - close, - 'click', - function () { - this.selected_container.innerHTML = '' - this.input.style.display = 'block' - }, - this - ) - this.hide() - }, -}) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 4915dfb3..4fb30f8a 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -1067,7 +1067,7 @@ L.FormBuilder.ManageOwner = L.FormBuilder.Element.extend({ className: 'edit-owner', on_select: L.bind(this.onSelect, this), } - this.autocomplete = new U.AutoComplete.Ajax.Select(this.parentNode, options) + this.autocomplete = new U.AjaxAutocomplete(this.parentNode, options) const owner = this.toHTML() if (owner) this.autocomplete.displaySelected({ @@ -1096,7 +1096,7 @@ L.FormBuilder.ManageEditors = L.FormBuilder.Element.extend({ on_select: L.bind(this.onSelect, this), on_unselect: L.bind(this.onUnselect, this), } - this.autocomplete = new U.AutoComplete.Ajax.SelectMultiple(this.parentNode, options) + this.autocomplete = new U.AjaxAutocompleteMultiple(this.parentNode, options) this._values = this.toHTML() if (this._values) for (let i = 0; i < this._values.length; i++) diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html index 966ed8d2..c6de81eb 100644 --- a/umap/templates/umap/js.html +++ b/umap/templates/umap/js.html @@ -46,7 +46,6 @@ -