converted autoComplete.js to ts

This commit is contained in:
huangjx
2022-05-02 07:52:02 +08:00
parent 6195928b74
commit 2e9954c88e

353
src/lib/autoComplete.ts Normal file
View 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;