Skip to content

Commit

Permalink
update typeahead
Browse files Browse the repository at this point in the history
  • Loading branch information
marcodpt committed Oct 14, 2024
1 parent 17bf905 commit fcf89fc
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 30 deletions.
156 changes: 126 additions & 30 deletions src/ctrl/inputs/typeahead/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import wrapper from '../wrapper.js'
import {uid} from '../../../lib.js'

export default ({
title,
Expand All @@ -10,33 +9,88 @@ export default ({
options,
value,
label
}) => wrapper(({input, datalist, option, text}) => {
const clear = ev => {
if (ev.target.value) {
value = ev.target.value
ev.target.value = ''
}) => wrapper(({input, div, button, text}) => {
const sanitize = str => str.trim().toUpperCase()

const max = options.length
var index = -1
const O = options.map((item, i) => {
index = index == -1 && item.value == value ? i : index
return {
...item,
search: sanitize(item.label),
hide: false
}
}
})
var active = index

const change = (ev, blur) => {
const i = options.reduce(
(p, o, i) => p < 0 && o.label == ev.target.value ? i : p
, -1)
const next = n => {
if (active < 0) {
for (var i = n < 0 ? max - 1 : 0; i < max && i >= 0; i = i + n) {
if (!O[i].hide) {
active = i
list.children[i].classList.add('active')
break
}
}
} else {
for (var i = active + max + n; !O[i % max].active; i = i + n) {
const j = i % max
if (!O[j].hide) {
list.children[active].classList.remove('active')
list.children[j].classList.add('active')
active = j
break
}
}
}
}

const select = i => {
if (i == null) {
i = active
}
if (active != i) {
if (active >= 0) {
list.children[active].classList.remove('active')
}
}
if (i >= 0) {
value = ev.target.value
update(options[i].value, options[i].label)
if (options.length == 1) {
ev.target.disabled = true
list.children[i].classList.add('active')
index = i
active = index
update(O[i].value, O[i].label)
if (max == 1) {
field.disabled = true
}
} else if (blur) {
ev.target.value = value
}
close()
}

const search = query => {
query = sanitize(query)
O.forEach((item, i) => {
item.hide = query && item.search.indexOf(query) < 0
if (item.hide) {
list.children[i].classList.add('d-none')
} else {
list.children[i].classList.remove('d-none')
}
})
}

const id = uid('list')
const open = () => {
field.value = ''
search('')
list.classList.remove('d-none')
}

const close = () => {
field.value = index >= 0 ? O[index].label : label
list.classList.add('d-none')
}

return [
const field =
input({
class: [
'validate',
Expand All @@ -47,20 +101,62 @@ export default ({
name: title,
value: label,
placeholder: description,
list: id,
disabled: readOnly || !options.length ||
(options.length == 1 && options[0].value == value),
oninput: change,
onfocus: clear,
onmousedown: clear,
onblur: ev => change(ev, true)
}),
datalist({
id
}, options.map(o =>
option({}, [
oninput: () => {
list.classList.remove('d-none')
search(field.value)
},
onfocus: () => open(),
onmousedown: () => list.classList.contains('d-none') ? open() : close(),
onblur: () => setTimeout(close, 300),
onkeydown: ev => {
const key = Allow => Allow.indexOf(ev.key) >= 0

if (key(['Escape', 'Enter', 'Tab', 'ArrowDown', 'ArrowUp'])) {
if (list.classList.contains('d-none')) {
if (key(['Enter', 'ArrowDown', 'ArrowUp'])) {
open()
ev.preventDefault()
}
} else {
if (key(['ArrowDown', 'ArrowUp'])) {
next(key(['ArrowUp']) ? -1 : 1)
ev.preventDefault()
} else if (key(['Enter'])) {
select()
ev.preventDefault()
} else if (key(['Tab'])) {
select()
} else if (key(['Escape'])) {
close()
}
}
}
}
})

const list =
div({
class: [
'list-group',
'd-none',
'w-100',
'position-absolute',
'z-3'
]
}, options.map((o, i) =>
button({
class: [
'list-group-item',
'list-group-item-action',
i == active ? 'active' : ''
],
onclick: () => select(i)
}, [
text(o.label)
])
))
]

return [field, list]
})
1 change: 1 addition & 0 deletions src/ctrl/inputs/wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {node} from '../../components.js'

export default (target, noFeedback) => node(({div}) =>
div({
class: 'position-relative',
validate: (el, error) => {
el.querySelectorAll('.invalid-feedback').forEach(feedback => {
feedback.textContent = ''
Expand Down

0 comments on commit fcf89fc

Please sign in to comment.