manuel
Version:
A super customizable VDOM autocomplete with *production ready* defaults.
382 lines (342 loc) • 8.73 kB
JavaScript
/* eslint-disable fp/no-mutating-methods */
/*
* The following utilities are all adapted from
* https://github.com/LeaVerou/awesomplete
*/
// String -> String -> Boolean
function contains(input, text) {
return input.trim().length
? RegExp(regExpEscape(input.trim()), "i").test(text)
: true
}
// String -> String
function regExpEscape(s) {
return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
}
// a -> b -> Number
function sortByLength(a, b) {
if (a.length !== b.length) {
return a.length - b.length;
}
return a < b? -1 : 1;
};
var keyboard = {
submit: function submit(code, highlighted){
return code == 13 && highlighted
? [highlighted]
: []
}
,dismiss: function dismiss(code){
return code == 27
? [true]
: []
}
,navigate: function navigate(
showingDrawer
, highlighted
, renderedList
, code
){
var KEY_UP = 38
var KEY_DOWN = 40
var i = renderedList.indexOf(highlighted)
var NO_MATCH = i == -1
var LOWER_BOUND = 0
var UPPER_BOUND = renderedList.length -1
var MATCH = i >= -1
var NEXT = i+1
var PREV = i-1
if( showingDrawer && (code == KEY_UP || code == KEY_DOWN) ){
if( code == KEY_UP && (NO_MATCH || i == LOWER_BOUND) ){
return [renderedList[UPPER_BOUND]]
} else if(code == KEY_UP && MATCH) {
return [renderedList[PREV]]
} else if (
code == KEY_DOWN && (
NO_MATCH || i == UPPER_BOUND
)
) {
return [renderedList[LOWER_BOUND]]
} else { // ( code == KEY_DOWN && MATCH )
return [renderedList[NEXT]]
}
} else {
return []
}
}
}
function BaseAutocomplete(framework){
var h = framework.hyperscript
var get = framework.get
var set = framework.set
function Autocomplete(model, nullableOverrides){
var overrides = nullableOverrides || {}
var list = model.list
var input = model.input
var chosen = model.chosen
var open = model.open
var highlighted = model.highlighted
var value = get(input)
var minChars = typeof overrides.minChars != 'undefined'
? overrides.minChars
: 2
var maxItems = typeof overrides.maxItems != 'undefined'
? overrides.maxItems
: 10
var sort = typeof overrides.sort != 'undefined'
? overrides.sort
: sortByLength
var filter = typeof overrides.filter != 'undefined'
? overrides.filter
: contains
var filteredList =
typeof overrides.filteredList != 'undefined'
? overrides.filteredList
: get( list )
.filter(function (s){
return filter(value, s)
})
.sort( sort )
.slice(0, maxItems)
if( filteredList.indexOf( get( highlighted ) ) == -1 ){
set(highlighted, null)
}
var config = {
filteredList: filteredList
,showingDrawer:
typeof overrides.showingDrawer != 'undefined'
? overrides.showingDrawer
: get(open)
&& value.length >= minChars
&& filteredList.length > 0
,choose:
typeof overrides.choose != 'undefined'
? overrides.choose
: function choose(x){
set(chosen, x)
set(input, x)
config.close()
}
,clickItem:
typeof overrides.clickItem != 'undefined'
? overrides.clickItem
: function clickItem(x){
return config.choose(x)
}
,PATTERN_INPUT:
typeof overrides.PATTERN_INPUT != 'undefined'
? overrides.PATTERN_INPUT
: value
? new RegExp(value, 'gi')
: null
,mark:
typeof overrides.mark != 'undefined'
? overrides.mark
: function(x){
return h('mark', x)
}
,highlight:
typeof overrides.highlight != 'undefined'
? overrides.highlight
: function highlight( x ){
var matches =
config.PATTERN_INPUT != null
? x.match( config.PATTERN_INPUT )
: null
var processed =
matches != null
? matches
.reduce(function(p, n){
var i = p.buffer.indexOf(n)
return {
buffer: p.buffer.slice(i+n.length)
,output: p.output.concat(
i === 0
? []
: p.buffer.slice(0, i)
,[ config.mark(
p.buffer.slice(i, i+n.length)
)
]
)
}
}, {
buffer: x
,output: []
})
: { output: x, buffer: '' }
return processed.output.concat(
processed.buffer || []
)
}
,oninput:
typeof overrides.oninput != 'undefined'
? overrides.oninput
: function oninput(e){
var v = e.currentTarget.value
set(input, v)
if( !get(open) ){
set(open, true)
}
if( get(chosen) != v ){
set(chosen, null)
}
}
,onfocus:
typeof overrides.onfocus != 'undefined'
? overrides.onfocus
: function onfocus(){
if( !get(open) ){
set(open, true)
}
}
,close:
typeof overrides.close != 'undefined'
? overrides.close
: function close(){
//eslint-disable-next-line fp/no-mutation
if( get(open) ){
set(open, false)
}
}
,onblur:
typeof overrides.onblur != 'undefined'
? overrides.onblur
: function onblur(){
config.close()
}
,renderInput:
typeof overrides.renderInput != 'undefined'
? overrides.renderInput
: function renderInput(){
return h('input'
,{ value: value
, oninput: config.oninput
, onfocus: config.onfocus
, onblur: config.onblur
}
)
}
,itemClassNames:
typeof overrides.itemClassNames != 'undefined'
? overrides.itemClassNames
: function itemClassNames(x){
return x == get(highlighted)
? 'highlight'
: ''
}
,renderItem:
typeof overrides.renderItem != 'undefined'
? overrides.renderItem
: function renderItem(x, config){
return h(
'li'
, { className:
config.itemClassNames(x, config)
, onmousedown: function(e){
config.clickItem(x)
e.stopPropagation()
}
}
, config.highlight(x)
)
}
,renderItems:
typeof overrides.renderItems != 'undefined'
? overrides.renderItems
: function renderItems(config){
return h(
'ul'
, config.filteredList.map(
function filteredList$map(x){
return config.renderItem(x, config)
}
)
)
}
,classNames:
typeof overrides.classNames != 'undefined'
? overrides.classNames
: function classNames(){
return ['manuel-complete']
.concat(
config.showingDrawer ? ['open'] : []
,value.length > 0 ? ['not-empty'] : []
,get(list).length > 0 ? ['loaded'] : []
)
.join(' ')
}
,renderRoot:
typeof overrides.renderRoot != 'undefined'
? overrides.renderRoot
: function renderRoot(config){
return h('div'
,{ className: config.classNames()
, onkeydown: config.onkeydown
}
,config.renderInput(config)
,config.renderItems(config)
)
}
,keyboardSubmit:
typeof overrides.keyboardSubmit != 'undefined'
? overrides.keyboardSubmit
: keyboard.submit
,keyboardDismiss:
typeof overrides.keyboardDismiss != 'undefined'
? overrides.keyboardDismiss
: keyboard.dismiss
,keyboardNavigate:
typeof overrides.keyboardNavigate != 'undefined'
? overrides.keyboardNavigate
: keyboard.navigate
,onkeydown:
typeof overrides.onkeydown != 'undefined'
? overrides.onkeydown
: function onkeydown(e){
var new_chosen =
config.keyboardSubmit(
e.keyCode
, get(highlighted)
)
var dismiss = config.keyboardDismiss(
e.keyCode
)
var new_highlighted =
config.keyboardNavigate(
config.showingDrawer
, get(highlighted)
, config.filteredList
, e.keyCode
)
new_chosen.map(
config.choose
)
new_highlighted.map(
function new_highlighted$map(v){
return set(highlighted, v)
}
)
dismiss.map(
config.close
)
;[]
.concat(
new_chosen
,dismiss
,new_highlighted
)
.slice(0,1)
.map(
function e$preventDefault(){
e.preventDefault()
return null;
}
)
}
}
return config.renderRoot(config)
}
return Autocomplete
}
module.exports = BaseAutocomplete