converted autoComplete.js to ts
This commit is contained in:
353
src/lib/autoComplete.ts
Normal file
353
src/lib/autoComplete.ts
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import eventHandler from "./eventHandler";
|
||||||
|
type callback = (event: any) => void;
|
||||||
|
type target = string | Element | HTMLElement | Window | Document
|
||||||
|
type commonEle = Node | HTMLElement
|
||||||
|
type listData = Array<string>;
|
||||||
|
interface sgBox<T extends HTMLElement = HTMLDivElement> {
|
||||||
|
box: T;
|
||||||
|
maxHeight: number;
|
||||||
|
suggestionHeight: number;
|
||||||
|
};
|
||||||
|
interface icache {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
interface Entity<T extends Node = HTMLInputElement> {
|
||||||
|
element: T;
|
||||||
|
sgBox: sgBox;
|
||||||
|
cache: icache;
|
||||||
|
lastValue: string;
|
||||||
|
};
|
||||||
|
interface ioptions {
|
||||||
|
selector: string | NodeList;
|
||||||
|
data: Array<string>;
|
||||||
|
sourceHandler: () => listData;
|
||||||
|
minChars: number;
|
||||||
|
delay: number;
|
||||||
|
offsetLeft: number;
|
||||||
|
offsetTop: number;
|
||||||
|
cache: boolean;
|
||||||
|
menuClass: string;
|
||||||
|
onSelect: (event: Event, item: string, search: Element) => void;
|
||||||
|
renderer: (term: string, search: string) => void;
|
||||||
|
}
|
||||||
|
class autoComplete {
|
||||||
|
options: ioptions;
|
||||||
|
element: Entity;
|
||||||
|
elements: Array<Entity> = [];
|
||||||
|
static UP = 38;
|
||||||
|
static DOWN = 40;
|
||||||
|
static ENTER = 13;
|
||||||
|
static ESC = 27;
|
||||||
|
constructor(options: ioptions) {
|
||||||
|
|
||||||
|
this.options = {
|
||||||
|
selector: "",
|
||||||
|
sourceHandler: null,
|
||||||
|
minChars: 3,
|
||||||
|
delay: 150,
|
||||||
|
offsetLeft: 0,
|
||||||
|
offsetTop: 1,
|
||||||
|
cache: true,
|
||||||
|
menuClass: '',
|
||||||
|
onSelect: function (e, term, item) { },
|
||||||
|
data: [],
|
||||||
|
renderer: function (item: string, search: string) {
|
||||||
|
search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
var re = new RegExp(`(${search.split(' ').join('|')})`, "gi");
|
||||||
|
return `<div class="suggestion-item" data-val="${item}">${item.replace(re, "<b>$1</b>")}</div>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(this.options, options);
|
||||||
|
if (typeof this.options.selector !== 'string' && !(this.options.selector instanceof NodeList))
|
||||||
|
throw ("invalid selecor!");
|
||||||
|
let nodelist = this.options.selector instanceof NodeList ? this.options.selector : document.querySelectorAll(this.options.selector);
|
||||||
|
if (nodelist.length < 1) {
|
||||||
|
console.log("no element found for autoComplete")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nodelist.forEach((node) => {
|
||||||
|
let element: Entity = {
|
||||||
|
element: node as HTMLInputElement,
|
||||||
|
sgBox: null,
|
||||||
|
cache: [],
|
||||||
|
lastValue: "",
|
||||||
|
};
|
||||||
|
this.elements.push(element)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
static getInstance(options: ioptions) {
|
||||||
|
return new autoComplete(options);
|
||||||
|
}
|
||||||
|
run() {
|
||||||
|
for (const element of this.elements) {
|
||||||
|
this.init(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
init(value: Entity) {
|
||||||
|
let ele = value.element;
|
||||||
|
ele.setAttribute('autocomplete', 'off');
|
||||||
|
value.sgBox = this.createSuggestionBox();
|
||||||
|
this.attach('resize', window, () => {
|
||||||
|
this.updateSuggestionBox(value);
|
||||||
|
});
|
||||||
|
document.body.appendChild(value.sgBox.box);
|
||||||
|
this.live('suggestion-item', 'mouseleave', function (e) {
|
||||||
|
var sel = value.sgBox.box.querySelector('.suggestion-item.selected');
|
||||||
|
if (sel)
|
||||||
|
setTimeout(function () { sel.className = sel.className.replace('selected', ''); }, 20);
|
||||||
|
}, value.sgBox.box);
|
||||||
|
|
||||||
|
this.live('suggestion-item', 'mouseover', function (e) {
|
||||||
|
var sel = value.sgBox.box.querySelector('.suggestion-item.selected');
|
||||||
|
if (sel) {
|
||||||
|
sel.classList.remove("selected");
|
||||||
|
}
|
||||||
|
this.classList.add("selected");
|
||||||
|
}, value.sgBox.box);
|
||||||
|
const selectHandler = (selected: Element, entity: Entity, e: Event) => {
|
||||||
|
if (autoComplete.hasClass(selected, 'suggestion-item')) {
|
||||||
|
let v = selected.getAttribute('data-val');
|
||||||
|
entity.element.value = v;
|
||||||
|
this.options.onSelect(e, v, selected);
|
||||||
|
this.hideBox(entity.sgBox.box);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.live('suggestion-item', 'mousedown,pointerdown', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
//this refers to the found element within;
|
||||||
|
let selected = this;
|
||||||
|
selectHandler(selected, value, e);
|
||||||
|
}, value.sgBox.box);
|
||||||
|
|
||||||
|
this.attach('blur', ele, () => this.blurCallback(value));
|
||||||
|
this.attach('keydown', ele, (e) => this.keyDownCallback(value, e));
|
||||||
|
this.attach('keyup', ele, (e) => this.keyUpCallback(value, e));
|
||||||
|
if (!this.options.minChars)
|
||||||
|
this.attach('focus', ele, (e) => this.focusCallback(value, e));
|
||||||
|
}
|
||||||
|
getCache(key: string, cache: icache) {
|
||||||
|
let data: listData = [];
|
||||||
|
if (!cache) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (key in cache) {
|
||||||
|
data = cache[key];
|
||||||
|
} else {
|
||||||
|
//test partial terms against the cache if the full term is not found
|
||||||
|
for (let i = 1; i < key.length - this.options.minChars; i++) {
|
||||||
|
let part = key.slice(0, key.length - i);
|
||||||
|
if (part in cache && !cache[part].length) {
|
||||||
|
data.push(cache[part]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
hideBox(box: HTMLDivElement) {
|
||||||
|
box.style.display = 'none';
|
||||||
|
}
|
||||||
|
showResult(term: string, entity: Entity) {
|
||||||
|
term = term.toLowerCase();
|
||||||
|
let suggestions: listData = [];
|
||||||
|
let data: listData
|
||||||
|
if (this.options.sourceHandler) {
|
||||||
|
data = this.options.sourceHandler()
|
||||||
|
} else {
|
||||||
|
data = this.options.data;
|
||||||
|
}
|
||||||
|
if (!this.options.cache) {
|
||||||
|
for (const item of data) {
|
||||||
|
if (item.toLowerCase().indexOf(term, 0) !== -1) {
|
||||||
|
suggestions.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.setTimeout(() => this.suggest(term, entity, suggestions), this.options.delay)
|
||||||
|
}
|
||||||
|
suggestions = this.getCache(term, entity.cache)
|
||||||
|
//cache found
|
||||||
|
if (suggestions.length >= 1) {
|
||||||
|
this.suggest(term, entity, suggestions)
|
||||||
|
} else {
|
||||||
|
for (const item of data) {
|
||||||
|
if (item.toLowerCase().indexOf(term, 0) !== -1) {
|
||||||
|
suggestions.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entity.cache[term] = suggestions;
|
||||||
|
window.setTimeout(() => this.suggest(term, entity, suggestions), this.options.delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
suggest(term: string, entity: Entity, data: any[]) {
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let sgBox = entity.sgBox;
|
||||||
|
if (data.length && term.length >= this.options.minChars) {
|
||||||
|
let s = '';
|
||||||
|
for (var i = 0; i < data.length; i++) s += this.options.renderer(data[i], term);
|
||||||
|
sgBox.box.innerHTML = s;
|
||||||
|
this.updateSuggestionBox(entity, false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.hideBox(sgBox.box);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatePosition(ele: HTMLDivElement, ref: HTMLInputElement) {
|
||||||
|
let rect = ref.getBoundingClientRect();
|
||||||
|
ele.style.left = Math.round(rect.left + (window.pageXOffset || document.documentElement.scrollLeft) + this.options.offsetLeft) + 'px';
|
||||||
|
ele.style.top = Math.round(rect.bottom + (window.pageYOffset || document.documentElement.scrollTop) + this.options.offsetTop) + 'px';
|
||||||
|
ele.style.width = Math.round(rect.right - rect.left) + 'px';
|
||||||
|
}
|
||||||
|
updateSuggestionBox(value: Entity, resize?: boolean, sibling?: commonEle) {
|
||||||
|
let ele = value.element;
|
||||||
|
let sgBox = value.sgBox;
|
||||||
|
let box = sgBox.box;
|
||||||
|
this.updatePosition(box, ele);
|
||||||
|
if (resize && !sibling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.style.display = 'block';
|
||||||
|
if (!sgBox.maxHeight) {
|
||||||
|
sgBox.maxHeight = autoComplete.getMaxHeight(sgBox.box);
|
||||||
|
}
|
||||||
|
if (!sgBox.suggestionHeight) {
|
||||||
|
sgBox.suggestionHeight = (<HTMLDivElement>sgBox.box.querySelector('.suggestion-item')).offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sgBox.suggestionHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!sibling) {
|
||||||
|
sgBox.box.scrollTop = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scrTop = sgBox.box.scrollTop
|
||||||
|
let gap = (<HTMLDivElement>sibling).getBoundingClientRect().top - sgBox.box.getBoundingClientRect().top;
|
||||||
|
if (gap + sgBox.suggestionHeight - sgBox.maxHeight > 0) {
|
||||||
|
sgBox.box.scrollTop = gap + sgBox.suggestionHeight + scrTop - sgBox.maxHeight;
|
||||||
|
} else if (gap < 0) {
|
||||||
|
sgBox.box.scrollTop = gap + scrTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static hasClass(el: Element, className: string) {
|
||||||
|
return el.classList ? el.classList.contains(className) : new RegExp('\\b' + className + '\\b').test(el.className);
|
||||||
|
}
|
||||||
|
attach(eventType: string, target: target, selector: callback | target, callback?: callback) {
|
||||||
|
eventHandler.add(eventType, target, selector, callback);
|
||||||
|
}
|
||||||
|
live(elClass: string, event: string, cb: callback, context: target) {
|
||||||
|
let events: Array<string>;
|
||||||
|
if (typeof event === 'string' && event.indexOf(',')) {
|
||||||
|
events = event.split(',');
|
||||||
|
} else {
|
||||||
|
events = [event]
|
||||||
|
}
|
||||||
|
for (const val of events) {
|
||||||
|
this.attach(val, context || window.document, function (e) {
|
||||||
|
let el = e.target || e.srcElement;
|
||||||
|
let found: boolean;
|
||||||
|
while (el && !(found = autoComplete.hasClass(el, elClass))) {
|
||||||
|
el = el.parentElement;
|
||||||
|
}
|
||||||
|
if (found) cb.call(el, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blurCallback(entity: Entity) {
|
||||||
|
let sgBox = entity.sgBox;
|
||||||
|
let hoverActive;
|
||||||
|
try {
|
||||||
|
hoverActive = document.querySelector('.suggestion-container:hover');
|
||||||
|
} catch (e) {
|
||||||
|
hoverActive = 0;
|
||||||
|
}
|
||||||
|
if (!hoverActive) {
|
||||||
|
entity.lastValue = entity.element.value;
|
||||||
|
window.setTimeout(() => this.hideBox(sgBox.box), 350);
|
||||||
|
} else if (entity.element !== document.activeElement) {
|
||||||
|
window.setTimeout(function () {
|
||||||
|
entity.element.focus();
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//display results matching the term
|
||||||
|
keyUpCallback(entity: Entity, e: KeyboardEvent) {
|
||||||
|
let sgBox = entity.sgBox, options = this.options;
|
||||||
|
var key = window.event ? e.keyCode : e.which;
|
||||||
|
if (!key || (key < 35 || key > 40) && ![autoComplete.ENTER, autoComplete.ESC].includes(key)) {
|
||||||
|
let val = entity.element.value;
|
||||||
|
entity.lastValue = val;
|
||||||
|
if (val.length >= options.minChars) {
|
||||||
|
this.showResult(val, entity);
|
||||||
|
} else {
|
||||||
|
this.hideBox(sgBox.box);;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//capture events when the followings key are pressed(down,up,esc,and tab keys)
|
||||||
|
keyDownCallback(entity: Entity, e: KeyboardEvent) {
|
||||||
|
let sgBox = entity.sgBox, options = this.options;
|
||||||
|
var key = window.event ? e.keyCode : e.which;
|
||||||
|
// down =40, up =38
|
||||||
|
if ((key == 40 || key == 38) && sgBox.box.innerHTML) {
|
||||||
|
let sel = sgBox.box.querySelector('.suggestion-item.selected');
|
||||||
|
let next: commonEle;
|
||||||
|
if (!sel) {
|
||||||
|
next = (key == 40) ? sgBox.box.querySelector('.suggestion-item') : sgBox.box.childNodes[sgBox.box.childNodes.length - 1]; // first : last
|
||||||
|
(<HTMLElement>next).className += ' selected';
|
||||||
|
entity.element.value = (<HTMLElement>next).getAttribute('data-val');
|
||||||
|
} else {
|
||||||
|
next = (key == 40) ? sel.nextSibling : sel.previousSibling;
|
||||||
|
if (next) {
|
||||||
|
sel.className = sel.className.replace('selected', '');
|
||||||
|
if (next instanceof Element) {
|
||||||
|
next.className += ' selected';
|
||||||
|
entity.element.value = next.getAttribute('data-val');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sel.className = sel.className.replace('selected', '');
|
||||||
|
entity.element.value = entity.lastValue; next = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.updateSuggestionBox(entity, false, next);
|
||||||
|
|
||||||
|
//ESC = 27
|
||||||
|
} else if (key == 27) {
|
||||||
|
entity.element.value = entity.lastValue;
|
||||||
|
this.hideBox(sgBox.box);
|
||||||
|
//enter = 13,tab = 9
|
||||||
|
} else if (key == 13 || key == 9) {
|
||||||
|
var sel = sgBox.box.querySelector('.suggestion-item.selected');
|
||||||
|
if (sel && sgBox.box.style.display != 'none') {
|
||||||
|
options.onSelect(e, sel.getAttribute('data-val'), sel);
|
||||||
|
window.setTimeout(() => this.hideBox(sgBox.box), 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focusCallback(entity: Entity, e: KeyboardEvent) {
|
||||||
|
entity.lastValue = '\n';
|
||||||
|
this.keyUpCallback(entity, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getMaxHeight(element: Element) {
|
||||||
|
let style = getComputedStyle(element, null)
|
||||||
|
return parseInt(style.maxHeight);
|
||||||
|
}
|
||||||
|
createSuggestionBox(): sgBox {
|
||||||
|
let sgBox: sgBox = {
|
||||||
|
box: document.createElement('div'),
|
||||||
|
maxHeight: 0,
|
||||||
|
suggestionHeight: 0
|
||||||
|
};
|
||||||
|
sgBox.box.classList.add('suggestion-container');
|
||||||
|
//suggestionBox.classList.add(options.menuClass);
|
||||||
|
return sgBox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default autoComplete;
|
||||||
Reference in New Issue
Block a user