123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- 'use strict';
- var sell = require('sell');
- var crossvent = require('crossvent');
- var bullseye = require('bullseye');
- var fuzzysearch = require('fuzzysearch');
- var KEY_BACKSPACE = 8;
- var KEY_ENTER = 13;
- var KEY_ESC = 27;
- var KEY_UP = 38;
- var KEY_DOWN = 40;
- var cache = [];
- var doc = document;
- var docElement = doc.documentElement;
- function find (el) {
- var entry;
- var i;
- for (i = 0; i < cache.length; i++) {
- entry = cache[i];
- if (entry.el === el) {
- return entry.api;
- }
- }
- return null;
- }
- function horsey (el, options) {
- var cached = find(el);
- if (cached) {
- return cached;
- }
- var o = options || {};
- var parent = o.appendTo || doc.body;
- var render = o.render || defaultRenderer;
- var getText = o.getText || defaultGetText;
- var getValue = o.getValue || defaultGetValue;
- var form = o.form;
- var limit = typeof o.limit === 'number' ? o.limit : Infinity;
- var suggestions = o.suggestions;
- var userFilter = o.filter || defaultFilter;
- var userSet = o.set || defaultSetter;
- var ul = tag('ul', 'sey-list');
- var selection = null;
- var oneload = once(loading);
- var eye;
- var deferredFiltering = defer(filtering);
- var attachment = el;
- var textInput;
- var anyInput;
- var ranchorleft;
- var ranchorright;
- if (o.autoHideOnBlur === void 0) { o.autoHideOnBlur = true; }
- if (o.autoHideOnClick === void 0) { o.autoHideOnClick = true; }
- if (o.autoShowOnUpDown === void 0) { o.autoShowOnUpDown = el.tagName === 'INPUT'; }
- if (o.anchor) {
- ranchorleft = new RegExp('^' + o.anchor);
- ranchorright = new RegExp(o.anchor + '$');
- }
- var api = {
- add: add,
- anchor: o.anchor,
- clear: clear,
- show: show,
- hide: hide,
- toggle: toggle,
- destroy: destroy,
- refreshPosition: refreshPosition,
- appendText: appendText,
- appendHTML: appendHTML,
- filterAnchoredText: filterAnchoredText,
- filterAnchoredHTML: filterAnchoredHTML,
- defaultAppendText: appendText,
- defaultFilter: defaultFilter,
- defaultGetText: defaultGetText,
- defaultGetValue: defaultGetValue,
- defaultRenderer: defaultRenderer,
- defaultSetter: defaultSetter,
- retarget: retarget,
- attachment: attachment,
- list: ul,
- suggestions: []
- };
- var entry = { el: el, api: api };
- retarget(el);
- cache.push(entry);
- parent.appendChild(ul);
- el.setAttribute('autocomplete', 'off');
- if (Array.isArray(suggestions)) {
- loaded(suggestions);
- }
- return api;
- function retarget (el) {
- inputEvents(true);
- attachment = api.attachment = el;
- textInput = attachment.tagName === 'INPUT' || attachment.tagName === 'TEXTAREA';
- anyInput = textInput || isEditable(attachment);
- inputEvents();
- }
- function refreshPosition () {
- if (eye) { eye.refresh(); }
- }
- function loading () {
- crossvent.remove(attachment, 'focus', oneload);
- suggestions(loaded);
- }
- function loaded (suggestions) {
- suggestions.forEach(add);
- api.suggestions = suggestions;
- }
- function clear () {
- while (ul.lastChild) {
- ul.removeChild(ul.lastChild);
- }
- }
- function add (suggestion) {
- var li = tag('li', 'sey-item');
- render(li, suggestion);
- crossvent.add(li, 'click', clickedSuggestion);
- crossvent.add(li, 'horsey-filter', filterItem);
- crossvent.add(li, 'horsey-hide', hideItem);
- ul.appendChild(li);
- api.suggestions.push(suggestion);
- return li;
- function clickedSuggestion () {
- var value = getValue(suggestion);
- set(value);
- hide();
- attachment.focus();
- crossvent.fabricate(attachment, 'horsey-selected', value);
- }
- function filterItem () {
- var value = textInput ? el.value : el.innerHTML;
- if (filter(value, suggestion)) {
- li.className = li.className.replace(/ sey-hide/g, '');
- } else {
- crossvent.fabricate(li, 'horsey-hide');
- }
- }
- function hideItem () {
- if (!hidden(li)) {
- li.className += ' sey-hide';
- if (selection === li) {
- unselect();
- }
- }
- }
- }
- function set (value) {
- if (o.anchor) {
- return (isText() ? api.appendText : api.appendHTML)(value);
- }
- userSet(value);
- }
- function filter (value, suggestion) {
- if (o.anchor) {
- var il = (isText() ? api.filterAnchoredText : api.filterAnchoredHTML)(value, suggestion);
- return il ? userFilter(il.input, il.suggestion) : false;
- }
- return userFilter(value, suggestion);
- }
- function isText () { return isInput(attachment); }
- function visible () { return ul.className.indexOf('sey-show') !== -1; }
- function hidden (li) { return li.className.indexOf('sey-hide') !== -1; }
- function show () {
- if (!visible()) {
- ul.className += ' sey-show';
- eye.refresh();
- crossvent.fabricate(attachment, 'horsey-show');
- }
- }
- function toggler (e) {
- var left = e.which === 1 && !e.metaKey && !e.ctrlKey;
- if (left === false) {
- return; // we only care about honest to god left-clicks
- }
- toggle();
- }
- function toggle () {
- if (!visible()) {
- show();
- } else {
- hide();
- }
- }
- function select (suggestion) {
- unselect();
- if (suggestion) {
- selection = suggestion;
- selection.className += ' sey-selected';
- }
- }
- function unselect () {
- if (selection) {
- selection.className = selection.className.replace(/ sey-selected/g, '');
- selection = null;
- }
- }
- function move (up, moves) {
- var total = ul.children.length;
- if (total < moves) {
- unselect();
- return;
- }
- if (total === 0) {
- return;
- }
- var first = up ? 'lastChild' : 'firstChild';
- var next = up ? 'previousSibling' : 'nextSibling';
- var suggestion = selection && selection[next] || ul[first];
- select(suggestion);
- if (hidden(suggestion)) {
- move(up, moves ? moves + 1 : 1);
- }
- }
- function hide () {
- eye.sleep();
- ul.className = ul.className.replace(/ sey-show/g, '');
- unselect();
- crossvent.fabricate(attachment, 'horsey-hide');
- }
- function keydown (e) {
- var shown = visible();
- var which = e.which || e.keyCode;
- if (which === KEY_DOWN) {
- if (anyInput && o.autoShowOnUpDown) {
- show();
- }
- if (shown) {
- move();
- stop(e);
- }
- } else if (which === KEY_UP) {
- if (anyInput && o.autoShowOnUpDown) {
- show();
- }
- if (shown) {
- move(true);
- stop(e);
- }
- } else if (which === KEY_BACKSPACE) {
- if (anyInput && o.autoShowOnUpDown) {
- show();
- }
- } else if (shown) {
- if (which === KEY_ENTER) {
- if (selection) {
- crossvent.fabricate(selection, 'click');
- } else {
- hide();
- }
- stop(e);
- } else if (which === KEY_ESC) {
- hide();
- stop(e);
- }
- }
- }
- function stop (e) {
- e.stopPropagation();
- e.preventDefault();
- }
- function filtering () {
- if (!visible()) {
- return;
- }
- crossvent.fabricate(attachment, 'horsey-filter');
- var li = ul.firstChild;
- var count = 0;
- while (li) {
- if (count >= limit) {
- crossvent.fabricate(li, 'horsey-hide');
- }
- if (count < limit) {
- crossvent.fabricate(li, 'horsey-filter');
- if (li.className.indexOf('sey-hide') === -1) {
- count++;
- }
- }
- li = li.nextSibling;
- }
- if (!selection) {
- move();
- }
- if (!selection) {
- hide();
- }
- }
- function deferredFilteringNoEnter (e) {
- var which = e.which || e.keyCode;
- if (which === KEY_ENTER) {
- return;
- }
- deferredFiltering();
- }
- function deferredShow (e) {
- var which = e.which || e.keyCode;
- if (which === KEY_ENTER) {
- return;
- }
- setTimeout(show, 0);
- }
- function horseyEventTarget (e) {
- var target = e.target;
- if (target === attachment) {
- return true;
- }
- while (target) {
- if (target === ul || target === attachment) {
- return true;
- }
- target = target.parentNode;
- }
- }
- function hideOnBlur (e) {
- if (horseyEventTarget(e)) {
- return;
- }
- hide();
- }
- function hideOnClick (e) {
- if (horseyEventTarget(e)) {
- return;
- }
- hide();
- }
- function inputEvents (remove) {
- var op = remove ? 'remove' : 'add';
- if (eye) {
- eye.destroy();
- eye = null;
- }
- if (!remove) {
- eye = bullseye(ul, attachment, { caret: anyInput && attachment.tagName !== 'INPUT' });
- if (!visible()) { eye.sleep(); }
- }
- if (typeof suggestions === 'function' && !oneload.used) {
- if (remove || (anyInput && doc.activeElement !== attachment)) {
- crossvent[op](attachment, 'focus', oneload);
- } else {
- oneload();
- }
- }
- if (anyInput) {
- crossvent[op](attachment, 'keypress', deferredShow);
- crossvent[op](attachment, 'keypress', deferredFiltering);
- crossvent[op](attachment, 'keydown', deferredFilteringNoEnter);
- crossvent[op](attachment, 'paste', deferredFiltering);
- crossvent[op](attachment, 'keydown', keydown);
- if (o.autoHideOnBlur) { crossvent[op](docElement, 'focus', hideOnBlur, true); }
- } else {
- crossvent[op](attachment, 'click', toggler);
- crossvent[op](docElement, 'keydown', keydown);
- }
- if (o.autoHideOnClick) { crossvent[op](doc, 'click', hideOnClick); }
- if (form) { crossvent[op](form, 'submit', hide); }
- }
- function destroy () {
- inputEvents(true);
- if (parent.contains(ul)) { parent.removeChild(ul); }
- cache.splice(cache.indexOf(entry), 1);
- }
- function defaultSetter (value) {
- if (textInput) {
- el.value = value;
- } else {
- el.innerHTML = value;
- }
- }
- function defaultRenderer (li, suggestion) {
- li.innerText = li.textContent = getText(suggestion);
- }
- function defaultFilter (q, suggestion) {
- var text = getText(suggestion) || '';
- var value = getValue(suggestion) || '';
- var needle = q.toLowerCase();
- return fuzzysearch(needle, text.toLowerCase()) || fuzzysearch(needle, value.toLowerCase());
- }
- function loopbackToAnchor (text, p) {
- var result = '';
- var anchored = false;
- var start = p.start;
- while (anchored === false && start >= 0) {
- result = text.substr(start - 1, p.start - start + 1);
- anchored = ranchorleft.test(result);
- start--;
- }
- return {
- text: anchored ? result : null,
- start: start
- };
- }
- function filterAnchoredText (q, suggestion) {
- var position = sell(el);
- var input = loopbackToAnchor(q, position).text;
- if (input) {
- return { input: input, suggestion: suggestion };
- }
- }
- function appendText (value) {
- var current = el.value;
- var position = sell(el);
- var input = loopbackToAnchor(current, position);
- var left = current.substr(0, input.start);
- var right = current.substr(input.start + input.text.length + (position.end - position.start));
- var before = left + value + ' ';
- el.value = before + right;
- sell(el, { start: before.length, end: before.length });
- }
- function filterAnchoredHTML () {
- throw new Error('Anchoring in editable elements is disabled by default.');
- }
- function appendHTML () {
- throw new Error('Anchoring in editable elements is disabled by default.');
- }
- }
- function isInput (el) { return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA'; }
- function defaultGetValue (suggestion) {
- return defaultGet('value', suggestion);
- }
- function defaultGetText (suggestion) {
- return defaultGet('text', suggestion);
- }
- function defaultGet (type, value) {
- return value && value[type] !== void 0 ? value[type] : value;
- }
- function tag (type, className) {
- var el = doc.createElement(type);
- el.className = className;
- return el;
- }
- function once (fn) {
- var disposed;
- function disposable () {
- if (disposed) { return; }
- disposable.used = disposed = true;
- (fn || noop).apply(null, arguments);
- }
- return disposable;
- }
- function defer (fn) { return function () { setTimeout(fn, 0); }; }
- function noop () {}
- function isEditable (el) {
- var value = el.getAttribute('contentEditable');
- if (value === 'false') {
- return false;
- }
- if (value === 'true') {
- return true;
- }
- if (el.parentElement) {
- return isEditable(el.parentElement);
- }
- return false;
- }
- horsey.find = find;
- module.exports = horsey;
|