317 lines
10 KiB
JavaScript
Executable File
317 lines
10 KiB
JavaScript
Executable File
import "../../../../src/components/form/sass/form.sass";
|
|
import "../../../../src/components/dropdown/dropdown.sass";
|
|
import "../../../../src/components/form/autocomplete/autocomplete.sass";
|
|
import { StaticUseInputProps } from '../../../composables/input/useInput';
|
|
import { getUseModelPropsDefinition } from '../../../composables/model/useModel';
|
|
import { useProxy } from '../../../composables/proxy';
|
|
import { getEqPropsDefinition } from '../../../composables/shared';
|
|
import { useThemePropsDefinition } from '../../../composables/theme';
|
|
import { constEmptyArray, extractProp, isFunction, isHTMLElement, isObject, isString, toggleListItem } from '../../../utils/helpers';
|
|
import { DropdownThemeMap } from '../../dropdown';
|
|
import BDropdown from '../../dropdown/BDropdown';
|
|
import { isArrowDownEvent, isArrowUpEvent, isEnterEvent, isEscEvent, isTabEvent } from '../../../utils/eventHelpers';
|
|
import { constant, constVoid } from 'fp-ts/lib/function';
|
|
import BDropdownDivider from '../../dropdown/BDropdownDivider';
|
|
import BDropdownItem from '../../dropdown/BDropdownItem';
|
|
import { head, isEmpty, lookup } from 'fp-ts/lib/Array';
|
|
import { alt, chain, fold, fromNullable, isSome, map, none, some, toUndefined } from 'fp-ts/lib/Option';
|
|
import { pipe } from 'fp-ts/lib/pipeable';
|
|
import { defineComponent, shallowRef, computed, onBeforeUpdate, nextTick, toRef, h } from 'vue';
|
|
import { BInput } from '../input';
|
|
|
|
function getActiveDescendentId(selectedItems, itemId) {
|
|
return pipe(selectedItems, head, chain(item => {
|
|
if (isString(item) && isFunction(itemId)) {
|
|
const id = extractProp(itemId, item);
|
|
return isString(id) ? some(id) : none;
|
|
}
|
|
|
|
if (isString(item)) {
|
|
return some(item);
|
|
}
|
|
|
|
if (isString(itemId) && isObject(item) && Object.hasOwnProperty.call(item, itemId)) {
|
|
const id = item[itemId];
|
|
return isString(id) ? some(id) : none;
|
|
}
|
|
|
|
const id = extractProp(itemId, item);
|
|
return isString(id) ? some(id) : none;
|
|
}), toUndefined);
|
|
}
|
|
|
|
function getAutocompleteItems(items, selectedItems, itemId, itemText, eq, hoveredItem) {
|
|
return items.map((item, index) => {
|
|
const id = extractProp(itemId, item);
|
|
const nid = isString(id) ? id : String(id);
|
|
const text = extractProp(itemText, item);
|
|
return {
|
|
id: nid,
|
|
isSelected: selectedItems.some(i => eq.equals(i, item)),
|
|
isHovered: isSome(hoveredItem) ? hoveredItem.value.id === nid : false,
|
|
text: isString(text) ? text : String(text),
|
|
value: item,
|
|
index
|
|
};
|
|
});
|
|
}
|
|
|
|
function getSetSelected(props, closeDropdown, inputModel, selectedItemsModel) {
|
|
const toggle = toggleListItem(props.eq);
|
|
return item => {
|
|
const text = extractProp(props.itemText, item.value);
|
|
inputModel.value = props.clearOnSelect ? '' : isString(text) ? text : String(text);
|
|
selectedItemsModel.value = toggle(item.value, selectedItemsModel.value || []);
|
|
|
|
if (props.closeOnSelect) {
|
|
closeDropdown();
|
|
}
|
|
};
|
|
}
|
|
|
|
function getSetHovered(hoveredItem, templateItems) {
|
|
return item => {
|
|
const newItem = fromNullable(item);
|
|
|
|
if (isSome(newItem)) {
|
|
hoveredItem.value = newItem;
|
|
pipe(newItem, map(item => item.index), chain(index => lookup(index, templateItems.value)), fold(constant(constVoid), li => () => li.focus && li.focus()))();
|
|
}
|
|
};
|
|
}
|
|
|
|
function getOnKeydown(autocompleteItems, hoveredItem, closeDropdown, setSelected, setHovered) {
|
|
function onArrowPress(isUp) {
|
|
pipe(hoveredItem.value, map(item => item.index), alt(() => some(0)), chain(index => lookup(isUp ? Math.max(index - 1, 0) : Math.min(index + 1, autocompleteItems.value.length - 1), autocompleteItems.value)), fold(constant(constVoid), newItem => () => setHovered(newItem)))();
|
|
}
|
|
|
|
return function onKeydown(event) {
|
|
if (isEnterEvent(event)) {
|
|
event.preventDefault();
|
|
|
|
if (isSome(hoveredItem.value)) {
|
|
setSelected(hoveredItem.value.value);
|
|
}
|
|
} else if (isTabEvent(event)) {
|
|
event.preventDefault();
|
|
|
|
if (isSome(hoveredItem.value)) {
|
|
setSelected(hoveredItem.value.value);
|
|
} else {
|
|
nextTick(closeDropdown);
|
|
}
|
|
} else if (isArrowUpEvent(event)) {
|
|
event.preventDefault();
|
|
onArrowPress(true);
|
|
} else if (isArrowDownEvent(event)) {
|
|
event.preventDefault();
|
|
onArrowPress(false);
|
|
} else if (isEscEvent(event)) {
|
|
event.preventDefault();
|
|
nextTick(closeDropdown);
|
|
}
|
|
};
|
|
}
|
|
|
|
function getGenerateItem(itemsRef, length, onKeydown, setSelected, setHovered, slots) {
|
|
return function generateItem(item, index) {
|
|
return h(BDropdownItem, {
|
|
key: item.id,
|
|
ref: el => {
|
|
if (isHTMLElement(el)) {
|
|
itemsRef.value[index] = el;
|
|
}
|
|
},
|
|
id: item.id,
|
|
isActive: item.isSelected,
|
|
tabindex: item.isSelected ? -1 : 0,
|
|
'aria-selected': item.isSelected,
|
|
'aria-label': `Option ${index + 1} of ${length.value}`,
|
|
class: {
|
|
'is-hovered': item.isHovered
|
|
},
|
|
onClick: () => setSelected(item),
|
|
onMouseenter: () => setHovered(item),
|
|
onKeydown
|
|
}, () => slots.default ? slots.default({
|
|
option: item,
|
|
index
|
|
}) : item.text);
|
|
};
|
|
}
|
|
|
|
function generateHeaderItem(slots) {
|
|
return h('li', {
|
|
class: 'dropdown-item',
|
|
tabindex: -1
|
|
}, slots.header && slots.header());
|
|
}
|
|
|
|
function generateFooterItem(slots) {
|
|
return h('li', {
|
|
class: 'dropdown-item',
|
|
tabindex: -1
|
|
}, slots.footer && slots.footer());
|
|
}
|
|
|
|
function generateLoadingItem(slots) {
|
|
return h('li', {
|
|
tabindex: -1
|
|
}, [h(BDropdownItem, {
|
|
tag: 'div'
|
|
}, () => slots.loading ? slots.loading() : 'Loading results...')]);
|
|
}
|
|
|
|
function generateEmptyItem(modelValue, slots) {
|
|
return h(BDropdownItem, {
|
|
class: 'is-disabled'
|
|
}, () => slots.empty ? slots.empty({
|
|
searchValue: modelValue
|
|
}) : modelValue ? `No results` : `No results for ${modelValue}`);
|
|
}
|
|
|
|
function defineAutocomplete() {
|
|
return defineComponent({
|
|
name: 'b-autocomplete',
|
|
props: { ...StaticUseInputProps,
|
|
...getEqPropsDefinition(),
|
|
...useThemePropsDefinition(DropdownThemeMap),
|
|
...getUseModelPropsDefinition(),
|
|
...getUseModelPropsDefinition('selectedItems', 'onUpdate:selectedItems'),
|
|
selectedItems: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
items: {
|
|
type: Array,
|
|
default: constEmptyArray
|
|
},
|
|
itemFilter: {
|
|
type: Function,
|
|
required: false
|
|
},
|
|
itemId: {
|
|
type: [String, Function],
|
|
default: 'id'
|
|
},
|
|
itemText: {
|
|
type: [String, Function],
|
|
default: 'text'
|
|
},
|
|
closeOnSelect: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
clearOnSelect: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
openOnFocus: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
onSelected: {
|
|
type: Function,
|
|
required: false
|
|
}
|
|
},
|
|
|
|
setup(props, {
|
|
slots
|
|
}) {
|
|
const {
|
|
value: searchValue
|
|
} = useProxy(computed(() => props.modelValue ?? ''), toRef(props, 'onUpdate:modelValue'));
|
|
const {
|
|
value: selectedItems
|
|
} = useProxy(toRef(props, 'selectedItems'), toRef(props, 'onUpdate:selectedItems'));
|
|
const itemsRef = shallowRef([]);
|
|
const filteredItems = computed(() => {
|
|
if (props.itemFilter) {
|
|
return props.items.filter(props.itemFilter(searchValue.value));
|
|
} else {
|
|
const sv = searchValue.value.toLowerCase();
|
|
const extract = props.itemText;
|
|
return props.items.filter(i => extractProp(extract, i).toLowerCase().includes(sv));
|
|
}
|
|
});
|
|
onBeforeUpdate(() => {
|
|
itemsRef.value = [];
|
|
});
|
|
const dropdown = shallowRef(null);
|
|
|
|
function close() {
|
|
dropdown.value && dropdown.value.toggle.setOff();
|
|
}
|
|
|
|
const hoveredItem = shallowRef(none);
|
|
const activeDescendentId = computed(() => getActiveDescendentId(props.selectedItems, props.itemId));
|
|
const autocompleteItems = computed(() => getAutocompleteItems(filteredItems.value, selectedItems.value, props.itemId, props.itemText, props.eq, hoveredItem.value));
|
|
const numberOfItems = computed(() => autocompleteItems.value.length);
|
|
const setSelected = getSetSelected(props, close, searchValue, selectedItems);
|
|
const setHovered = getSetHovered(hoveredItem, itemsRef);
|
|
const onKeydown = getOnKeydown(autocompleteItems, hoveredItem, close, setSelected, setHovered);
|
|
const generateItem = getGenerateItem(itemsRef, numberOfItems, onKeydown, setSelected, setHovered, slots);
|
|
return () => {
|
|
return h(BDropdown, {
|
|
ref: dropdown,
|
|
isMobileModal: false,
|
|
class: ['b-autocomplete', {
|
|
'is-expanded': props.isExpanded
|
|
}]
|
|
}, {
|
|
trigger: () => {
|
|
return h(BInput, {
|
|
modelValue: searchValue.value,
|
|
type: 'text',
|
|
size: props.size,
|
|
isLoading: props.isLoading,
|
|
isRounded: props.isRounded,
|
|
icon: props.icon,
|
|
maxlength: props.maxlength,
|
|
autocomplete: 'off',
|
|
placeholder: props.placeholder,
|
|
role: 'searchbox',
|
|
'aria-activedescendant': activeDescendentId.value,
|
|
'onUpdate:modelValue': val => {
|
|
searchValue.value = val;
|
|
},
|
|
// onFocus: () => {
|
|
// nextTick().then(() => {
|
|
// if (props.openOnFocus && toggle.isOff.value) {
|
|
// toggle.setOn();
|
|
// }
|
|
// });
|
|
// },
|
|
onBlur: props.onBlur,
|
|
onKeydown
|
|
});
|
|
},
|
|
default: () => {
|
|
let nodes;
|
|
|
|
if (props.isLoading) {
|
|
nodes = [generateLoadingItem(slots)];
|
|
} else {
|
|
nodes = isEmpty(autocompleteItems.value) ? [generateEmptyItem(searchValue.value, slots)] : autocompleteItems.value.map(generateItem);
|
|
|
|
if (slots.header) {
|
|
nodes.unshift(generateHeaderItem(slots), h(BDropdownDivider));
|
|
}
|
|
|
|
if (slots.footer) {
|
|
nodes.push(h(BDropdownDivider), generateFooterItem(slots));
|
|
}
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
export const BAutocomplete = defineAutocomplete();
|
|
//# sourceMappingURL=BAutocomplete.js.map
|