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 @@
-