buefy
Version:
Lightweight UI components for Vue.js (v3) based on Bulma
863 lines (859 loc) • 29.3 kB
JavaScript
import { defineComponent, resolveComponent, createElementBlock, openBlock, mergeProps, createVNode, withKeys, withModifiers, Transition, withCtx, withDirectives, createElementVNode, normalizeStyle, normalizeClass, createCommentVNode, renderSlot, Fragment, renderList, toDisplayString, vShow } from 'vue';
import { removeElement, createAbsoluteElement, getValueByPath, isCustomElement, toCssWidth } from './helpers.js';
import { C as CompatFallthroughMixin } from './CompatFallthroughMixin-C8LPuwDr.js';
import { F as FormElementMixin } from './FormElementMixin-Dd_wkBN5.js';
import { B as BInput } from './Input-C4L520az.js';
import { _ as _export_sfc } from './_plugin-vue_export-helper-OJRSZE6i.js';
var _sfc_main = defineComponent({
name: "BAutocomplete",
components: { BInput },
mixins: [CompatFallthroughMixin, FormElementMixin],
props: {
modelValue: [Number, String, null],
data: {
type: Array,
default: () => []
},
field: {
type: String,
default: "value"
},
keepFirst: Boolean,
clearOnSelect: Boolean,
openOnFocus: Boolean,
customFormatter: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Function
},
checkInfiniteScroll: Boolean,
keepOpen: Boolean,
selectOnClickOutside: Boolean,
clearable: Boolean,
maxHeight: [String, Number],
dropdownPosition: {
type: String,
default: "auto"
},
groupField: String,
groupOptions: String,
iconRight: String,
iconRightClickable: Boolean,
appendToBody: Boolean,
type: {
type: String,
default: "text"
},
confirmKeys: {
type: Array,
default: () => ["Tab", "Enter"]
},
selectableHeader: Boolean,
selectableFooter: Boolean,
// Native options to use in HTML5 validation
autocomplete: String
},
emits: {
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */
active: (active) => true,
blur: (event) => true,
focus: (event) => true,
"icon-click": (event) => true,
"icon-right-click": (event) => true,
"infinite-scroll": () => true,
select: (selected, event) => true,
"select-footer": (event) => true,
"select-header": (event) => true,
typing: (value) => true,
"update:modelValue": (value) => true
/* eslint-enable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */
},
data() {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
selected: null,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
hovered: null,
headerHovered: null,
footerHovered: null,
isActive: false,
newValue: this.modelValue,
newAutocomplete: this.autocomplete || "off",
ariaAutocomplete: this.keepFirst ? "both" : "list",
isListInViewportVertically: true,
hasFocus: false,
style: {},
_isAutocomplete: true,
_elementRef: "input",
_bodyEl: void 0,
// Used to append to body
timeOutID: void 0
};
},
computed: {
computedData() {
const { groupField, groupOptions } = this;
if (groupField) {
if (groupOptions) {
const newData = [];
this.data.forEach((option) => {
const group = getValueByPath(option, groupField);
const items = getValueByPath(option, groupOptions);
newData.push({ group, items });
});
return newData;
} else {
const tmp = {};
this.data.forEach((option) => {
const group = getValueByPath(option, groupField);
if (!tmp[group]) tmp[group] = [];
tmp[group].push(option);
});
const newData = [];
Object.keys(tmp).forEach((group) => {
newData.push({ group, items: tmp[group] });
});
return newData;
}
}
return [{ items: this.data }];
},
isEmpty() {
if (!this.computedData) return true;
return !this.computedData.some(
(element) => element.items && element.items.length
);
},
/*
* White-listed items to not close when clicked.
* Add input, dropdown and all children.
*/
whiteList() {
var _a;
this.computedData;
const whiteList = [];
whiteList.push(this.$refs.input.$el.querySelector("input"));
whiteList.push(this.$refs.dropdown);
if (this.$refs.dropdown != null) {
const children = this.$refs.dropdown.querySelectorAll("*");
for (const child of children) {
whiteList.push(child);
}
}
if (((_a = this.$parent) == null ? void 0 : _a.$data)._isTaginput) {
whiteList.push(this.$parent.$el);
const tagInputChildren = this.$parent.$el.querySelectorAll("*");
for (const tagInputChild of tagInputChildren) {
whiteList.push(tagInputChild);
}
}
return whiteList;
},
/*
* Check if exists default slot
*/
hasDefaultSlot() {
return !!this.$slots.default;
},
/*
* Check if exists group slot
*/
hasGroupSlot() {
return !!this.$slots.group;
},
/*
* Check if exists "empty" slot
*/
hasEmptySlot() {
return !!this.$slots.empty;
},
/*
* Check if exists "header" slot
*/
hasHeaderSlot() {
return !!this.$slots.header;
},
/*
* Check if exists "footer" slot
*/
hasFooterSlot() {
return !!this.$slots.footer;
},
/*
* Apply dropdownPosition property
*/
isOpenedTop() {
return this.dropdownPosition === "top" || this.dropdownPosition === "auto" && !this.isListInViewportVertically;
},
newIconRight() {
if (this.clearable && this.newValue) {
return "close-circle";
}
return this.iconRight;
},
newIconRightClickable() {
if (this.clearable) {
return true;
}
return this.iconRightClickable;
},
contentStyle() {
return {
maxHeight: toCssWidth(this.maxHeight) || void 0
};
}
},
watch: {
/*
* When dropdown is toggled, check the visibility to know when
* to open upwards.
*/
isActive(active) {
if (this.dropdownPosition === "auto") {
if (active) {
this.calcDropdownInViewportVertical();
} else {
this.timeOutID = setTimeout(() => {
this.calcDropdownInViewportVertical();
}, 100);
}
}
this.$nextTick(() => {
this.$emit("active", active);
});
},
/*
* When checkInfiniteScroll property changes scroll event should be removed or added
*/
checkInfiniteScroll(checkInfiniteScroll) {
if (!this.$refs.dropdown) return;
const list = this.$refs.dropdown.querySelector(
".dropdown-content"
);
if (!list) return;
if (checkInfiniteScroll === true) {
list.addEventListener(
"scroll",
this.checkIfReachedTheEndOfScroll
);
return;
}
list.removeEventListener(
"scroll",
this.checkIfReachedTheEndOfScroll
);
},
/*
* When updating input's value
* 1. Emit changes
* 2. If value isn't the same as selected, set null
* 3. Close dropdown if value is clear or else open it
*/
newValue(value) {
this.$emit("update:modelValue", value);
const currentValue = this.getValue(this.selected);
if (currentValue !== void 0 && currentValue !== null && currentValue !== value) {
this.setSelected(null, false);
}
if (this.hasFocus && (!this.openOnFocus || value !== "")) {
this.isActive = value !== "" && value !== void 0 && value !== null;
}
},
/*
* When v-model is changed:
* 1. Update internal value.
* 2. If it's invalid, validate again.
*/
modelValue(value) {
this.newValue = value;
},
keepFirst(value) {
this.ariaAutocomplete = value ? "both" : "list";
},
/*
* Select first option if "keep-first
*/
data() {
if (this.keepFirst) {
this.$nextTick(() => {
if (this.isActive) {
this.selectFirstOption(this.computedData);
} else {
this.setHovered(null);
}
});
} else {
if (this.hovered) {
const hoveredValue = this.getValue(this.hovered);
const data = this.computedData.map((d) => d.items).reduce((a, b) => [...a, ...b], []);
if (!data.some((d) => this.getValue(d) === hoveredValue)) {
this.setHovered(null);
}
}
}
},
/*
* When appendToBody property changes, handle the transition properly
*/
appendToBody(newValue, oldValue) {
if (newValue && !oldValue) {
if (this.isActive && this.$refs.dropdown && !this.$data._bodyEl) {
this.$data._bodyEl = createAbsoluteElement(
this.$refs.dropdown
);
this.updateAppendToBody();
}
} else if (!newValue && oldValue) {
if (this.$data._bodyEl) {
removeElement(this.$data._bodyEl);
this.$data._bodyEl = void 0;
}
}
}
},
methods: {
/*
* Set which option is currently hovered.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setHovered(option) {
if (option === void 0) return;
this.hovered = option;
},
/*
* Set which option is currently selected, update v-model,
* update input value and close dropdown.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setSelected(option, closeDropdown = true, event) {
if (option === void 0) return;
this.selected = option;
this.$emit("select", this.selected, event);
if (this.selected !== null) {
if (this.clearOnSelect) {
this.newValue = "";
} else {
this.newValue = this.getValue(this.selected);
}
this.setHovered(null);
}
closeDropdown && this.$nextTick(() => {
this.isActive = false;
});
this.checkValidity();
},
/*
* Select first option
*/
selectFirstOption(computedData) {
this.$nextTick(() => {
const nonEmptyElements = computedData.filter(
(element) => element.items && element.items.length
);
if (nonEmptyElements.length) {
const option = nonEmptyElements[0].items[0];
this.setHovered(option);
} else {
this.setHovered(null);
}
});
},
/*
* Find index of hovered item in data array by comparing display values
* instead of object references. This fixes the bug with computed data
* where proxy objects cause indexOf to fail.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
findHoveredIndex(data) {
if (this.hovered === null || this.hovered === void 0) {
return -1;
}
const exactIndex = data.indexOf(this.hovered);
if (exactIndex !== -1) {
return exactIndex;
}
const hoveredValue = this.getValue(this.hovered);
if (hoveredValue === null || hoveredValue === void 0) {
return -1;
}
return data.findIndex((item) => {
if (item === null || item === void 0) {
return hoveredValue === null || hoveredValue === void 0;
}
return this.getValue(item) === hoveredValue;
});
},
keydown(event) {
const { key } = event;
if (key === "Enter") event.preventDefault();
if (key === "Escape" || key === "Tab") {
this.isActive = false;
}
if (this.confirmKeys.indexOf(key) >= 0) {
if (key === ",") event.preventDefault();
const closeDropdown = !this.keepOpen || key === "Tab";
if (this.hovered === null) {
this.checkIfHeaderOrFooterSelected(
event,
null,
closeDropdown
);
return;
}
this.setSelected(this.hovered, closeDropdown, event);
}
},
selectHeaderOrFoterByClick(event, origin) {
this.checkIfHeaderOrFooterSelected(event, { origin });
},
/*
* Check if header or footer was selected.
*/
checkIfHeaderOrFooterSelected(event, triggerClick, closeDropdown = true) {
if (this.selectableHeader && (this.headerHovered || triggerClick && triggerClick.origin === "header")) {
this.$emit("select-header", event);
this.headerHovered = false;
if (triggerClick) this.setHovered(null);
if (closeDropdown) this.isActive = false;
}
if (this.selectableFooter && (this.footerHovered || triggerClick && triggerClick.origin === "footer")) {
this.$emit("select-footer", event);
this.footerHovered = false;
if (triggerClick) this.setHovered(null);
if (closeDropdown) this.isActive = false;
}
},
/*
* Close dropdown if clicked outside.
*/
clickedOutside(event) {
const target = isCustomElement(this) ? event.composedPath()[0] : event.target;
if (!this.hasFocus && this.whiteList.indexOf(target) < 0) {
if (this.keepFirst && this.hovered && this.selectOnClickOutside) {
this.setSelected(this.hovered, true);
} else {
this.isActive = false;
}
}
},
/*
* Return display text for the input.
* If object, get value from path, or else just the value.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getValue(option) {
if (option === null) return;
if (typeof this.customFormatter !== "undefined") {
return this.customFormatter(option);
}
return typeof option === "object" ? getValueByPath(option, this.field) : option;
},
/*
* Check if the scroll list inside the dropdown
* reached it's end.
*/
checkIfReachedTheEndOfScroll() {
const list = this.$refs.dropdown.querySelector(
".dropdown-content"
);
const footerHeight = this.hasFooterSlot ? list.querySelectorAll("div.dropdown-footer")[0].clientHeight : 0;
if (list.clientHeight !== list.scrollHeight && list.scrollTop + list.parentElement.clientHeight + footerHeight >= list.scrollHeight) {
this.$emit("infinite-scroll");
}
},
/*
* Calculate if the dropdown is vertically visible when activated,
* otherwise it is openened upwards.
*/
calcDropdownInViewportVertical() {
this.$nextTick(() => {
var _a;
if (this.$refs.dropdown == null) return;
const rect = this.$refs.dropdown.getBoundingClientRect();
this.isListInViewportVertically = rect.top >= 0 && rect.bottom <= ((window == null ? void 0 : window.innerHeight) || ((_a = document == null ? void 0 : document.documentElement) == null ? void 0 : _a.clientHeight));
if (this.appendToBody) {
this.updateAppendToBody();
}
});
},
/*
* Arrows keys listener.
* If dropdown is active, set hovered option, or else just open.
*/
keyArrows(direction) {
const sum = direction === "down" ? 1 : -1;
if (this.isActive) {
const data = this.computedData.map((d) => d.items).reduce((a, b) => [...a, ...b], []);
if (this.hasHeaderSlot && this.selectableHeader) {
data.unshift(void 0);
}
if (this.hasFooterSlot && this.selectableFooter) {
data.push(void 0);
}
let index;
if (this.headerHovered) {
index = 0 + sum;
} else if (this.footerHovered) {
index = data.length - 1 + sum;
} else {
index = this.findHoveredIndex(data) + sum;
}
index = index > data.length - 1 ? data.length - 1 : index;
index = index < 0 ? 0 : index;
this.footerHovered = false;
this.headerHovered = false;
this.setHovered(data[index] !== void 0 ? data[index] : null);
if (this.hasFooterSlot && this.selectableFooter && index === data.length - 1) {
this.footerHovered = true;
}
if (this.hasHeaderSlot && this.selectableHeader && index === 0) {
this.headerHovered = true;
}
const list = this.$refs.dropdown.querySelector(
".dropdown-content"
);
let querySelectorText = "a.dropdown-item:not(.is-disabled)";
if (this.hasHeaderSlot && this.selectableHeader) {
querySelectorText += ",div.dropdown-header";
}
if (this.hasFooterSlot && this.selectableFooter) {
querySelectorText += ",div.dropdown-footer";
}
const element = list.querySelectorAll(querySelectorText)[index];
if (!element) return;
const visMin = list.scrollTop;
const visMax = list.scrollTop + list.clientHeight - element.clientHeight;
if (element.offsetTop < visMin) {
list.scrollTop = element.offsetTop;
} else if (element.offsetTop >= visMax) {
list.scrollTop = element.offsetTop - list.clientHeight + element.clientHeight;
}
} else {
this.isActive = true;
}
},
/*
* Focus listener.
* If value is the same as selected, select all text.
*/
focused(event) {
if (this.getValue(this.selected) === this.newValue) {
this.$el.querySelector("input").select();
}
if (this.openOnFocus) {
this.isActive = true;
if (this.keepFirst) {
this.selectFirstOption(this.computedData);
}
}
this.hasFocus = true;
this.$emit("focus", event);
},
/*
* Blur listener.
*/
onBlur(event) {
this.hasFocus = false;
this.$emit("blur", event);
},
onInput() {
const currentValue = this.getValue(this.selected);
if (currentValue !== void 0 && currentValue !== null && currentValue === this.newValue) {
return;
}
this.$emit("typing", this.newValue);
this.checkValidity();
},
rightIconClick(event) {
if (this.clearable) {
this.newValue = "";
this.setSelected(null, false);
if (this.openOnFocus) {
this.$refs.input.$el.focus();
}
} else {
this.$emit("icon-right-click", event);
}
},
checkValidity() {
if (this.useHtml5Validation) {
this.$nextTick(() => {
this.checkHtml5Validity();
});
}
},
updateAppendToBody() {
const dropdownMenu = this.$refs.dropdown;
const trigger = this.$parent.$data._isTaginput ? this.$parent.$el : this.$refs.input.$el;
if (dropdownMenu && trigger) {
if (!this.$data._bodyEl) {
this.$data._bodyEl = createAbsoluteElement(dropdownMenu);
}
const root = this.$data._bodyEl;
root.classList.forEach((item) => root.classList.remove(item));
root.classList.add("autocomplete");
root.classList.add("control");
if (this.expanded) {
root.classList.add("is-expanded");
}
const rect = trigger.getBoundingClientRect();
let top = rect.top + window.scrollY;
const left = rect.left + window.scrollX;
if (!this.isOpenedTop) {
top += trigger.clientHeight;
} else {
top -= dropdownMenu.clientHeight;
}
this.style = {
position: "absolute",
top: `${top}px`,
left: `${left}px`,
width: `${trigger.clientWidth}px`,
maxWidth: `${trigger.clientWidth}px`,
zIndex: "99"
};
}
}
},
created() {
if (typeof window !== "undefined") {
document.addEventListener("click", this.clickedOutside);
if (this.dropdownPosition === "auto") {
window.addEventListener(
"resize",
this.calcDropdownInViewportVertical
);
}
if (this.appendToBody) {
window.addEventListener(
"scroll",
this.calcDropdownInViewportVertical
);
}
}
},
mounted() {
if (this.checkInfiniteScroll && this.$refs.dropdown && this.$refs.dropdown.querySelector(".dropdown-content")) {
const list = this.$refs.dropdown.querySelector(
".dropdown-content"
);
list.addEventListener("scroll", this.checkIfReachedTheEndOfScroll);
}
if (this.appendToBody) {
this.$data._bodyEl = createAbsoluteElement(
this.$refs.dropdown
);
this.updateAppendToBody();
}
},
beforeUnmount() {
if (typeof window !== "undefined") {
document.removeEventListener("click", this.clickedOutside);
if (this.dropdownPosition === "auto") {
window.removeEventListener(
"resize",
this.calcDropdownInViewportVertical
);
}
if (this.appendToBody) {
window.removeEventListener(
"scroll",
this.calcDropdownInViewportVertical
);
}
}
if (this.checkInfiniteScroll && this.$refs.dropdown && this.$refs.dropdown.querySelector(".dropdown-content")) {
const list = this.$refs.dropdown.querySelector(
".dropdown-content"
);
list.removeEventListener(
"scroll",
this.checkIfReachedTheEndOfScroll
);
}
if (this.appendToBody && this.$data._bodyEl) {
removeElement(this.$data._bodyEl);
}
clearTimeout(this.timeOutID);
}
});
const _hoisted_1 = {
key: 1,
class: "has-text-weight-bold"
};
const _hoisted_2 = ["onClick"];
const _hoisted_3 = { key: 1 };
const _hoisted_4 = {
key: 1,
class: "dropdown-item is-disabled"
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_b_input = resolveComponent("b-input");
return openBlock(), createElementBlock(
"div",
mergeProps({
class: ["autocomplete control", { "is-expanded": _ctx.expanded }]
}, _ctx.rootAttrs),
[
createVNode(_component_b_input, mergeProps({
modelValue: _ctx.newValue,
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => _ctx.newValue = $event),
ref: "input",
type: _ctx.type,
size: _ctx.size,
loading: _ctx.loading,
rounded: _ctx.rounded,
icon: _ctx.icon,
"icon-right": _ctx.newIconRight,
"icon-right-clickable": _ctx.newIconRightClickable,
"icon-pack": _ctx.iconPack,
maxlength: _ctx.maxlength,
autocomplete: _ctx.newAutocomplete,
"use-html5-validation": false,
"aria-autocomplete": _ctx.ariaAutocomplete
}, _ctx.fallthroughAttrs, {
"onUpdate:modelValue": _ctx.onInput,
onFocus: _ctx.focused,
onBlur: _ctx.onBlur,
onKeydown: [
_ctx.keydown,
_cache[1] || (_cache[1] = withKeys(withModifiers(($event) => _ctx.keyArrows("up"), ["prevent"]), ["up"])),
_cache[2] || (_cache[2] = withKeys(withModifiers(($event) => _ctx.keyArrows("down"), ["prevent"]), ["down"]))
],
onIconRightClick: _ctx.rightIconClick,
onIconClick: _cache[3] || (_cache[3] = (event) => _ctx.$emit("icon-click", event))
}), null, 16, ["modelValue", "type", "size", "loading", "rounded", "icon", "icon-right", "icon-right-clickable", "icon-pack", "maxlength", "autocomplete", "aria-autocomplete", "onUpdate:modelValue", "onFocus", "onBlur", "onKeydown", "onIconRightClick"]),
createVNode(Transition, {
name: "fade",
persisted: ""
}, {
default: withCtx(() => [
withDirectives(createElementVNode(
"div",
{
class: normalizeClass(["dropdown dropdown-menu", { "is-opened-top": _ctx.isOpenedTop && !_ctx.appendToBody }]),
style: normalizeStyle(_ctx.style),
ref: "dropdown"
},
[
withDirectives(createElementVNode(
"div",
{
class: "dropdown-content",
style: normalizeStyle(_ctx.contentStyle)
},
[
_ctx.hasHeaderSlot ? (openBlock(), createElementBlock(
"div",
{
key: 0,
class: normalizeClass(["dropdown-item dropdown-header", { "is-hovered": _ctx.headerHovered }]),
role: "button",
tabindex: "0",
onClick: _cache[4] || (_cache[4] = ($event) => _ctx.selectHeaderOrFoterByClick($event, "header"))
},
[
renderSlot(_ctx.$slots, "header")
],
2
/* CLASS */
)) : createCommentVNode("v-if", true),
(openBlock(true), createElementBlock(
Fragment,
null,
renderList(_ctx.computedData, (element, groupindex) => {
return openBlock(), createElementBlock(
Fragment,
null,
[
element.group ? (openBlock(), createElementBlock("div", {
key: groupindex + "group",
class: "dropdown-item"
}, [
_ctx.hasGroupSlot ? renderSlot(_ctx.$slots, "group", {
key: 0,
group: element.group,
index: groupindex
}) : (openBlock(), createElementBlock(
"span",
_hoisted_1,
toDisplayString(element.group),
1
/* TEXT */
))
])) : createCommentVNode("v-if", true),
(openBlock(true), createElementBlock(
Fragment,
null,
renderList(element.items, (option, index) => {
return openBlock(), createElementBlock("a", {
key: groupindex + ":" + index,
class: normalizeClass(["dropdown-item", { "is-hovered": option === _ctx.hovered }]),
role: "button",
tabindex: "0",
onClick: withModifiers(($event) => _ctx.setSelected(option, !_ctx.keepOpen, $event), ["stop"])
}, [
_ctx.hasDefaultSlot ? renderSlot(_ctx.$slots, "default", {
key: 0,
option,
index
}) : (openBlock(), createElementBlock(
"span",
_hoisted_3,
toDisplayString(_ctx.getValue(option)),
1
/* TEXT */
))
], 10, _hoisted_2);
}),
128
/* KEYED_FRAGMENT */
))
],
64
/* STABLE_FRAGMENT */
);
}),
256
/* UNKEYED_FRAGMENT */
)),
_ctx.isEmpty && _ctx.hasEmptySlot ? (openBlock(), createElementBlock("div", _hoisted_4, [
renderSlot(_ctx.$slots, "empty")
])) : createCommentVNode("v-if", true),
_ctx.hasFooterSlot ? (openBlock(), createElementBlock(
"div",
{
key: 2,
class: normalizeClass(["dropdown-item dropdown-footer", { "is-hovered": _ctx.footerHovered }]),
role: "button",
tabindex: "0",
onClick: _cache[5] || (_cache[5] = ($event) => _ctx.selectHeaderOrFoterByClick($event, "footer"))
},
[
renderSlot(_ctx.$slots, "footer")
],
2
/* CLASS */
)) : createCommentVNode("v-if", true)
],
4
/* STYLE */
), [
[vShow, _ctx.isActive]
])
],
6
/* CLASS, STYLE */
), [
[vShow, _ctx.isActive && (!_ctx.isEmpty || _ctx.hasEmptySlot || _ctx.hasHeaderSlot || _ctx.hasFooterSlot)]
])
]),
_: 3
/* FORWARDED */
})
],
16
/* FULL_PROPS */
);
}
var BAutocomplete = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
export { BAutocomplete as B };