You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Hydro/packages/ui-default/components/autocomplete/index.js

199 lines
5.1 KiB
JavaScript

import Drop from 'tether-drop';
import _ from 'lodash';
import 'jquery-scroll-lock';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import zIndexManager from 'vj/utils/zIndexManager';
export default class AutoComplete extends DOMAttachedObject {
static DOMAttachKey = 'vjAutoCompleteInstance';
constructor($dom, options = {}) {
super($dom);
this.options = {
items: async () => [],
render: () => '',
text: () => null,
minChar: 1,
cache: true,
clearDefaultValue: true,
position: 'bottom left',
classes: '',
multi: false,
...options,
};
this.clear(this.options.clearDefaultValue);
this.menuShown = false;
this.cache = {};
this.currentItems = [];
this.$menu = $(`<ol class="menu ${this.options.classes}"></ol>`);
this.$menu.scrollLock({ strict: false });
this.$menu.on('mousedown', this.onMenuClick.bind(this));
this.$menu.on('mousedown', '.menu__item', this.onItemClick.bind(this));
this.dropInstance = new Drop({
classes: 'autocomplete dropdown',
target: this.$dom[0],
content: this.$menu[0],
position: this.options.position,
constrainToWindow: false,
constrainToScrollParent: false,
openOn: false,
});
this.dropInstance.on('open', this.onDropOpen.bind(this));
this.attach();
}
clear(clearValue = true) {
if (clearValue) this.$dom.val('');
this._value = null;
this.lastText = null;
}
attach() {
this.$dom.on(`click.${this.eventNS}`, this.onClick.bind(this));
this.$dom.on(`focus.${this.eventNS}`, this.onFocus.bind(this));
this.$dom.on(`blur.${this.eventNS}`, this.onBlur.bind(this));
this.$dom.on(`keydown.${this.eventNS}`, this.onKeyDown.bind(this));
this.$dom.on(`keyup.${this.eventNS}`, this.onKeyUp.bind(this));
}
onClick() {
this.updateOpenState();
}
onFocus() {
this.isFocus = true;
this.updateOpenState();
}
onBlur() {
this.isFocus = false;
this.updateOpenState();
}
onKeyDown() {
// TODO: Implement keyboard navigation
}
onKeyUp(ev) {
if (ev.which === 27 /* ESC */) {
this.close();
return;
}
if (this.$dom.val() === this.lastText) return;
this.lastText = this.$dom.val();
this.updateOpenState();
if (this.isOpen) this.renderList();
}
onMenuClick(ev) {
ev.preventDefault(); // prevent from losing focus
}
onItemClick(ev) {
const idx = $(ev.currentTarget).attr('data-idx');
if (idx === undefined) return;
const item = this.currentItems[idx];
const text = this.options.text(item);
this._value = item;
this.$dom.trigger('vjAutoCompleteSelect', item);
if (text != null) {
if (!this.options.multi) this.$dom.val(text);
else {
const current = this.$dom.val().replace(//g, ',');
if (current.indexOf(',') !== -1) {
const items = current.split(',');
items[items.length - 1] = text;
this.$dom.val(items.join(', '));
} else this.$dom.val(`${text}, `);
}
}
this.close();
}
onDropOpen() {
$(this.dropInstance.drop).css('z-index', zIndexManager.getNext());
}
async getItems(val) {
if (this.cache[val] !== undefined) return this.cache[val];
const data = await this.options.items(val);
if (this.options.cache) this.cache[val] = data;
return data;
}
getHtml(items) {
if (items.length === 0) {
return tpl`<div class="empty-row">${i18n('Oops, there are no results.')}</div>`;
}
return items.map((item, idx) => `
<li class="menu__item" data-idx="${idx}"><a href="javascript:;" class="menu__link">
${this.options.render(item)}
</a></li>
`).join('\n');
}
async renderList() {
let val = this.$dom.val();
if (this.options.multi) {
const elems = val.split(',').map((i) => i.trim());
val = elems[elems.length - 1];
}
const items = await this.getItems(val);
const html = this.getHtml(items);
this.currentItems = items;
this.$menu.html(html);
}
open() {
if (this.isOpen) return;
this.dropInstance.open();
this.isOpen = true;
}
close() {
if (!this.isOpen) return;
this.dropInstance.close();
this.isOpen = false;
}
updateOpenState() {
let content = this.$dom.val();
if (this.options.multi) {
content = content.split(',');
content = content[content.length - 1].trim();
}
if (this.isFocus && content.length >= this.options.minChar) {
if (!this.isOpen) {
this.open();
this.renderList();
}
} else if (this.isOpen) this.close();
}
detach() {
if (this.detached) return;
super.detach();
this.$dom.off(`focus.${this.eventNS}`);
this.$dom.off(`blur.${this.eventNS}`);
this.$dom.off(`keydown.${this.eventNS}`);
this.$dom.off(`keyup.${this.eventNS}`);
this.dropInstance.destroy();
this.$menu.remove();
}
value() {
return this._value;
}
focus() {
this.$dom.focus();
}
}
_.assign(AutoComplete, DOMAttachedObject);
window.Hydro.components.autocomplete = AutoComplete;