ractive-ez-combobox
Version:
Ractive Ez UI Combobox
366 lines (289 loc) • 10.6 kB
JavaScript
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;