manuel
Version:
A super customizable VDOM autocomplete with *production ready* defaults.
633 lines (551 loc) • 14.8 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.manuel = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
// @ts-check
/* eslint-disable fp/no-mutating-methods */
/**
* @typedef { (x: VNode ) => VNode } VNodeTransform
* @param { VNodeTransform } f
*/
function $root(f){
/**
* @param { VNode } o
*/
function action(o){
var r =
f(o)
return typeof r !== 'undefined'
? r
: o
}
return action
}
/**
* @param { VNodeTransform } f
*/
function $ul(f){
/**
* @param { VNode } div
*/
function action(div){
var r =
f(div.children[1])
if( typeof r !== 'undefined' ){
// eslint-disable-next-line fp/no-mutation
div.children[1] = r
}
return div
}
return action
}
/**
* @param { ( o: VNode[]) => VNode[] } f
*/
function $li( f ){
return $ul(function(ul){
var r =
f(ul.children)
if( typeof r !== 'undefined' ){
// eslint-disable-next-line fp/no-mutation
ul.children = r
}
return ul
})
}
/**
* @param { VNodeTransform } f
*/
function $input(f){
/**
* @param { VNode } div
*/
function action(div){
var r =
f(div.children[0])
if( typeof r !== 'undefined' ){
// eslint-disable-next-line fp/no-mutation
div.children[0] = r
}
return div
}
return action
}
/*
* The following utilities are all adapted from
* https://github.com/LeaVerou/awesomplete
*/
/**
* @type { (input: string, text: string ) => boolean }
*/
var contains = function contains(input, text){
return input.trim().length
? RegExp(regExpEscape(input.trim()), "i").test(text)
: true
}
/**
* @type { (s: string ) => string }
*/
var regExpEscape = function regExpEscape(s) {
return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
}
/**
* @type { {(a: string, b: string ) : number} }
*/
var sortByLength = function sortByLength(a, b) {
if (a.length !== b.length) {
return a.length - b.length;
}
return a < b ? -1 : 1;
};
/**
@type {
(p: { [k: string]: any }, b: [string, any] ) => { [k:string] : any }
}
*/
var assignPair = function assignPair(p, pair){
// eslint-disable-next-line fp/no-mutation
p[ pair[0] ] = pair[1]
return p
}
var keyboard = {
submit:
function submit(
/** @type {number} */code
,/** @type {string} */ highlighted
) {
return code == 13 && highlighted
? [highlighted]
: []
}
,dismiss:
function dismiss(
/** @type {number} */ code
){
return code == 27
? [true]
: []
}
,navigate:
function navigate(
/** @type {boolean} */ showingDrawer
,/** @type {string} */ highlighted
,/** @type {string[]} */ renderedList
,/** @type {number} */ 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 []
}
}
}
/**
@typedef {any} VNode
@typedef {
(tagName: string, attrs: object, children: VNode[] ) => VNode
} HyperscriptConstructor
@typedef { ( key: any ) => any } FrameworkGet
@typedef { ( key: any, value: any ) => any } FrameworkSet
@typedef {{
get: FrameworkGet
set: FrameworkSet
hyperscript: HyperscriptConstructor
}} Framework
@typedef {{
list: any
input: any
chosen: any
open: any
highlighted: any
}} Model
@typedef { Event & { currentTarget: { value: String }} } InputEvent
@typedef {{
minChars: number
maxItems: number
sort: { (input: string, text: string ): number }
filter: { (value: string, s: string): boolean }
filteredList: string[]
eventNames: { [k:string]: string }
showingDrawer: boolean
choose: { (x:string) : void }
clickItem: { (x:string) : void }
PATTERN_INPUT: RegExp | null
mark: { (x:string) : VNode }
highlight: { (x:string) : VNode }
oninput: { (e: InputEvent ): void }
onfocus: { (e: FocusEvent ): void }
close: { (): void }
onblur: { (e: FocusEvent ): void }
renderInput: { (config:Overrides) : VNode }
itemClassNames: { (x:string, config: Overrides) : string }
renderItem: { (x:string, config: Overrides): VNode }
renderItems: { (config: Overrides): VNode }
classNames: { () : string }
renderRoot: { (config: Overrides): VNode }
keyboardSubmit: { (code: number, highlighted: string ): string[] }
keyboardDismiss: { (code: number ): boolean[] }
keyboardNavigate: {
( showingDrawer: boolean
, highlighted: string
, renderedList: string[]
, code: number
) : string[]
}
onkeydown: { (e: KeyboardEvent ): void }
}} Overrides
@param {Framework} framework
*/
function BaseAutocomplete(framework){
var h = framework.hyperscript
var get = framework.get
var set = framework.set
/**
*
* @param {Model} model
* @param { Partial<Overrides> } nullableOverrides
*/
function Autocomplete(model, nullableOverrides){
var overrides = nullableOverrides || {}
var list = model.list
var input = model.input
var chosen = model.chosen
var open = model.open
/** @type { () => string[] } */
var getList = function(){
return get(list)
}
/** @type { () => string } */
var getInput = function(){
return get(input)
}
/** @type { () => string } */
var getChosen = function(){
return get(chosen)
}
/** @type { () => boolean } */
var getOpen = function(){
return get(open)
}
/** @type { () => string } */
var getHighlighted = function(){
return get(highlighted)
}
var highlighted = model.highlighted
var value = getInput()
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
: getList()
.filter(function (s){
return filter(value, s)
})
.sort( sort )
.slice(0, maxItems)
if( getHighlighted() != null
&& filteredList.indexOf( get( highlighted ) ) == -1
){
set(highlighted, null)
}
if( getChosen() != null
&& getChosen() != value
){
set(chosen, null)
}
/** @type {Overrides} */
var config = {
filteredList: filteredList
,minChars: minChars
,maxItems: maxItems
,sort: sort
,filter: filter
,eventNames:
typeof overrides.eventNames != 'undefined'
? overrides.eventNames
: { oninput: 'oninput'
, onfocus: 'onfocus'
, onblur: 'onblur'
, onkeydown: 'onkeydown'
, onmousedown: 'onmousedown'
}
,showingDrawer:
typeof overrides.showingDrawer != 'undefined'
? overrides.showingDrawer
: getOpen()
&& value.length >= minChars
&& filteredList.length > 0
,choose:
typeof overrides.choose != 'undefined'
? overrides.choose
: function choose(x){
if( getInput() != x ){
set(input, x)
}
if( getChosen() != x ){
set(chosen, 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 ){
/** @type {string[] | null} */
var matches =
config.PATTERN_INPUT != null
? x.match( config.PATTERN_INPUT )
: null
/** @type {{ buffer: string, output: string[] }} */
var initial = {
buffer: x
,output: []
}
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)
)
]
)
}
}, initial )
: { output: [x], buffer: '' }
return processed.output.concat(
processed.buffer || []
)
}
,oninput:
typeof overrides.oninput != 'undefined'
? overrides.oninput
: function oninput(e){
var v = e.currentTarget.value
if( getInput() != v ){
set(input, v)
}
if( !getOpen() ){
set(open, true)
}
}
,onfocus:
typeof overrides.onfocus != 'undefined'
? overrides.onfocus
: function onfocus(){
if( !getOpen() ){
set(open, true)
}
}
,close:
typeof overrides.close != 'undefined'
? overrides.close
: function close(){
//eslint-disable-next-line fp/no-mutation
if( getOpen() ){
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]
, [config.eventNames.oninput, config.oninput]
, [config.eventNames.onfocus, config.onfocus]
, [config.eventNames.onblur, config.onblur]
]
.reduce(assignPair, {})
, []
)
}
,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) ]
, [ config.eventNames.onmousedown, function(
/** @type {Event} */ e
){
config.clickItem(x)
e.stopPropagation()
}]
]
.reduce(assignPair, {})
, 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'] : []
,getList().length > 0 ? ['loaded'] : []
)
.join(' ')
}
,renderRoot:
typeof overrides.renderRoot != 'undefined'
? overrides.renderRoot
: function renderRoot(config){
return h('div'
,[[ 'className', config.classNames() ]
, [config.eventNames.onkeydown, config.onkeydown]
]
.reduce(assignPair, {})
,[ 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 =
e.shiftKey
? []
: 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 )
new_chosen.length
+ dismiss.length
+ new_highlighted.length
> 0
&& e.preventDefault()
}
}
return config.renderRoot(config)
}
return Autocomplete
}
module.exports = BaseAutocomplete
// eslint-disable-next-line fp/no-mutation
BaseAutocomplete.queries = {
listItems: $li
, list: $ul
, root: $root
, input: $input
}
},{}]},{},[1])(1)
});