Merge pull request #1846 from umap-project/autocomplete-module

chore: move autocomplete to modules/
This commit is contained in:
Yohan Boniface 2024-05-23 15:43:25 +02:00 committed by GitHub
commit 0c9c79195a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 314 additions and 344 deletions

View file

@ -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()
}
}

View file

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

View file

@ -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()
},
})

View file

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

View file

@ -46,7 +46,6 @@
<script src="{% static 'umap/vendors/simple-statistics/simple-statistics.min.js' %}"
defer></script>
<script src="{% static 'umap/js/umap.core.js' %}" defer></script>
<script src="{% static 'umap/js/umap.autocomplete.js' %}" defer></script>
<script src="{% static 'umap/js/umap.popup.js' %}" defer></script>
<script src="{% static 'umap/js/umap.forms.js' %}" defer></script>
<script src="{% static 'umap/js/umap.icon.js' %}" defer></script>