UNPKG

ractive-ez-combobox

Version:
366 lines (289 loc) 10.6 kB
import Ractive from 'ractive'; import utils from './utils.js'; import 'ractive-ez-core'; import './EzSelectBox.less'; const KEYS = { BACKSPACE : 8, TAB : 9, ENTER : 13, ESCAPE : 27, ARROW_UP : 38, ARROW_DOWN : 40, DELETE : 46 }; const EzSelectBox = Ractive.components.EzSelectBox = Ractive.extend({ template: require("./EzSelectBox.html"), prop: null, searchTimer: null, wndOnScroll: null, data() { return { placeholder: "", required: false, disabled: false, button: false, items: null, compare: utils.compare, filter: utils.filter, path: "", search: null, searchDelay: 150, searchOnInput: false, typeahead: false, _state: { text: "", textWidth: 25, filterText: "", typeaheadText: "", value: null, options: null, isActive: false, isOpen: false, isFiltered: false, isTypeahead: false, isSearching: false, isSelecting: false }, _offset: { top: 0, left :0 } }; }, oninit() { this.observe("path", name => this.prop = utils.prop(name)); this.observe("_state.text", text => this.updateInputWidth()); this.wndOnScroll = () => this.renderOptions(); this.searchTimer = null; window.addEventListener("scroll", this.wndOnScroll, false); }, onteardown() { window.removeEventListener("scroll", this.wndOnScroll, false); }, findItemIndex(values, item, compare = this.get("compare") || utils.compare) { return (values || []).findIndex(value => compare(item, value)); }, filterSelected(options, values, compare) { return (options || []).filter(option => this.findItemIndex(values, option, compare) == -1); }, updateInputWidth() { setImmediate(() => { if (this.el) { this.set("_state.textWidth", this.find(".ez-selectbox-input-width").clientWidth); } }); }, preview(item, activeText = "", typeahead = this.get("typeahead"), isTypeahead = this.get("_state.isTypeahead")) { const text = this.prop(item) || ""; const filterText = activeText || text; const typeaheadText = text.slice(filterText.length); this.set("_state.value", item); this.set("_state.filterText", filterText); this.set("_state.typeaheadText", typeaheadText); if (typeahead && isTypeahead && item != null) { this.set("_state.text", text); this.find("input").setSelectionRange(filterText.length, text.length); } else { this.set("_state.text", filterText); } this.scrollToPreview(); }, select(value = this.get("_state.value")) { this.set("_state.isSelecting", true); if (!this.get("_state.isSearching")) { this.set("_state.isSelecting", false); this.fire("select", {}, value); } }, selectFirst() { const items = this.get("items"); const hasItems = items && items.length; hasItems ? null : this.search(true); this.select(hasItems ? items[0] : null); }, filter(filterText = this.get("_state.filterText")) { const items = this.get("items") || []; const filter = this.get("searchOnInput") ? () => true : this.get("filter") || utils.filter; const options = items.filter(item => filter(filterText, this.prop(item))); this.set("_state.options", options); if (!filterText) { this.preview(null); } else { const value = this.get("_state.value"); const compare = this.get("compare") || utils.compare; if (options.every(option => !compare(option, value))) { this.preview(options[0], filterText); } else { this.preview(value, filterText); } } this.renderOptions(); this.scrollToPreview(); }, search(immediate = false) { if (this.searchTimer) clearTimeout(this.searchTimer); const search = this.get("search"); const delay = immediate ? 0 : this.get("searchDelay"); const text = this.get("_state.filterText") || ""; if (!search) return; this.set("_state.isSearching", true); const timer = this.searchTimer = setTimeout(() => { if (timer != this.searchTimer) return; search(text, (error, items) => { if (timer != this.searchTimer) return; this.searchTimer = null; if (error) items = []; this.set("_state.isSearching", false); this.set("items", items); this.filter(text); if (this.get("_state.isSelecting")) this.select(); }); }, delay || 0); }, open() { if (!this.get("_state.isOpen")) { this.set("_state.filterText", this.get("_state.text")); if (this.get("_state.isFiltered")) { this.filter(); } } if (!this.get("items") || !this.get("items.length")) { this.search(true); } this.set("_state.isActive", true); this.set("_state.isOpen", true); this.find("input").focus(); this.renderOptions(); this.scrollToPreview(); }, close() { this.set("_state.isOpen", false); this.set("_state.isActive", false); this.set("_state.isFiltered", true); this.find("input").blur(); }, renderOptions() { if (!this.get("_state.isOpen")) return; const root = this.find(".ez-selectbox-options"); const selectbox = this.find(".ez-selectbox").getBoundingClientRect(); const options = root.getBoundingClientRect(); const wndHeight = window.innerHeight; const top = selectbox.top - options.height; const bottom = selectbox.bottom + options.height; if (bottom > window.innerHeight && top > 0) { this.set("_offset", { width: selectbox.width, top: selectbox.top - options.height, left: selectbox.left }); } else { this.set("_offset", { width: selectbox.width, top: selectbox.top + selectbox.height, left: selectbox.left }); } }, scrollToPreview() { if (this.el) { const options = this.find(".ez-selectbox-options"); const preview = this.find(".ez-selected"); if (options && preview) { options.scrollTop = preview.offsetTop - (options.clientHeight / 2); } } }, handleEditorClick(event) { this.set("_state.isFiltered", true); this.open(); }, handleInputFocus(event) { this.set("_state.isFiltered", true); this.open(); }, handleInputBlur(event) { this.close(); this.select(); }, handleInputInput(event) { this.filter(event.target.value); this.open(); if (this.get("searchOnInput")) this.search(); }, handleInputKeyDown(event) { const key = event.keyCode; const txt = this.get("_state.filterText"); const cancel = () => { event.preventDefault(); event.stopPropagation(); } this.set("_state.isTypeahead", true); switch (key) { case KEYS.ENTER: return this.handleEnterKey(txt, cancel); case KEYS.TAB: return this.handleTabKey(txt, cancel); case KEYS.ESCAPE: return this.handleEscapeKey(txt, cancel); case KEYS.BACKSPACE: return this.handleBackspaceKey(txt, cancel); case KEYS.DELETE: return this.handleDeleteKey(txt, cancel); case KEYS.ARROW_UP: return this.handleArrowUpKey(txt, cancel); case KEYS.ARROW_DOWN: return this.handleArrowDownKey(txt, cancel); } }, handleEnterKey(txt, cancel) { }, handleTabKey(txt, cancel) { }, handleEscapeKey(txt, cancel) { this.close(); }, handleBackspaceKey(txt, cancel) { this.set("_state.isTypeahead", false); this.open(); }, handleDeleteKey(txt, cancel) { this.set("_state.isTypeahead", false); this.open(); }, handleArrowUpKey(txt, cancel) { cancel(); if (this.get("_state.isOpen")) { const options = this.get("_state.options") || []; const index = options.indexOf(this.get("_state.value")); if (index == -1) { this.preview(options[0]); } else if (index > 0) { this.preview(options[index - 1]); } } this.open(); }, handleArrowDownKey(txt, cancel) { cancel(); if (this.get("_state.isOpen")) { const options = this.get("_state.options") || []; const index = options.indexOf(this.get("_state.value")); if (index == -1) { this.preview(options[0]); } else if (index < options.length - 1) { this.preview(options[index + 1]); } } this.open(); }, handleButtonClick(event) { event.preventDefault(); this.set("_state.isFiltered", false); this.set("_state.options", this.get("items")); this.open(); }, handleOptionMouseDown(event, option) { event.preventDefault(); }, handleOptionClick(event, option) { this.select(option); event && event.preventDefault && event.preventDefault(); event && event.stopPropagation && event.stopPropagation(); } }); export default EzSelectBox;