UNPKG

@dialpad/dialtone

Version:

Dialpad's Dialtone design system monorepo

669 lines (668 loc) 21.1 kB
import DtRecipeComboboxWithPopover from "../combobox_with_popover/combobox_with_popover.vue.js"; import DtInput from "../../../components/input/input.vue.js"; import DtChip from "../../../components/chip/chip.vue.js"; import DtValidationMessages from "../../../components/validation_messages/validation_messages.vue.js"; import { validationMessageValidator } from "../../../common/validators.js"; import { hasSlotContent, returnFirstEl } from "../../../common/utils.js"; import { POPOVER_APPEND_TO_VALUES } from "../../../components/popover/popover_constants.js"; import { MULTI_SELECT_SIZES, CHIP_SIZES, CHIP_TOP_POSITION } from "./combobox_multi_select_constants.js"; import SrOnlyCloseButtonMixin from "../../../common/mixins/sr_only_close_button.js"; import { resolveComponent, openBlock, createBlock, createSlots, withCtx, createElementVNode, normalizeClass, createElementBlock, Fragment, renderList, mergeProps, toHandlers, withKeys, createTextVNode, toDisplayString, createVNode, withModifiers, renderSlot } from "vue"; import _export_sfc from "../../../_virtual/_plugin-vue_export-helper.js"; const _sfc_main = { compatConfig: { MODE: 3 }, name: "DtRecipeComboboxMultiSelect", components: { DtRecipeComboboxWithPopover, DtInput, DtChip, DtValidationMessages }, mixins: [SrOnlyCloseButtonMixin], props: { /** * String to use for the input label. */ label: { type: String, required: true }, /** * Determines visibility of input label. * @values true, false */ labelVisible: { type: Boolean, default: true }, /** * Description for the input */ description: { type: String, default: "" }, /** * Input placeholder */ placeholder: { type: String, default: "Select one or start typing" }, /** * Input validation messages */ inputMessages: { type: Array, default: () => [], validator: (inputMessages) => { return validationMessageValidator(inputMessages); } }, /** * Show input validation message */ showInputMessages: { type: Boolean, default: true }, // @TODO: https://dialpad.atlassian.net/browse/DP-52324 // type: { // type: String, // values: ['input', 'select'], // default: 'select', // }, /** * Determines if the list is loading */ loading: { type: Boolean, default: false }, /** * The message when the list is loading */ loadingMessage: { type: String, default: "loading..." }, /** * Determines when to show the list element and also controls the aria-expanded attribute. * Leaving this null will have the combobox trigger on input focus by default. * If you set this value, the default trigger behavior will be disabled and you can * control it as you need. */ showList: { type: Boolean, default: null }, /** * Determines maximum height for the popover before overflow. * Possible units rem|px|em */ listMaxHeight: { type: String, default: "300px" }, /** * The selected items */ selectedItems: { type: Array, default: function() { return []; } }, /** * Would be the maximum number of selections you can make. 0 is unlimited */ maxSelected: { type: Number, default: 0 }, /** * Max select message when the max selections is exceeded with the structure: * `[{"message": string, "type": VALIDATION_MESSAGE_TYPES }]` */ maxSelectedMessage: { type: Array, default: function() { return []; } }, /** * Displays the list when the combobox is focused, before the user has typed anything. * When this is enabled the list will not close after selection. */ hasSuggestionList: { type: Boolean, default: true }, /** * Size of the chip, one of `xs`, `sm`, `md` */ size: { type: String, default: "md", validator: (t) => Object.values(MULTI_SELECT_SIZES).includes(t) }, /** * Sets the element to which the popover is going to append to. * 'body' will append to the nearest body (supports shadow DOM). * @values 'body', 'parent', HTMLElement, */ appendTo: { type: [HTMLElement, String], default: "body", validator: (appendTo) => { return POPOVER_APPEND_TO_VALUES.includes(appendTo) || appendTo instanceof HTMLElement; } }, /** * Named transition when the content display is toggled. * @see DtLazyShow */ transition: { type: String, default: "fade" }, /** * Determines whether the combobox should collapse to a single when losing focus. * @type {boolean} */ collapseOnFocusOut: { type: Boolean, default: false }, /** * Determines maximum width for the popover before overflow. * Possible units rem|px|em */ listMaxWidth: { type: String, default: "" }, /** * Amount of reserved space (in px) on the right side of the input * before the chips and the input caret jump to the next line. * default is 64 */ reservedRightSpace: { type: Number, default: 64 }, /** * Determines the maximum width of a single chip. If the text within this chip exceeds the value * it will be truncated with ellipses. * Possible units rem|px|em */ chipMaxWidth: { type: String, default: "" }, /** * Additional class name for the input element. * Can accept String, Object, and Array, i.e. has the * same API as Vue's built-in handling of the class attribute. */ inputClass: { type: [String, Object, Array], default: "" }, /** * Additional class name for the input wrapper element. * Can accept all of String, Object, and Array, i.e. has the * same api as Vue's built-in handling of the class attribute. */ inputWrapperClass: { type: [String, Object, Array], default: "" } }, emits: [ /** * Native input event * * @event input * @type {String } */ "input", /** * Event fired when item selected * * @event select * @type {Number} */ "select", /** * Event fired when item removed * * @event remove * @type {String} */ "remove", /** * Event fired when max selected items limit is reached * * @event max-selected * @type {Object} */ "max-selected", /** * Native keyup event * * @event keyup * @type {KeyboardEvent} */ "keyup", /** * Event fired when combobox item is highlighted * * @event combobox-highlight * @type {Object} */ "combobox-highlight" ], data() { return { value: "", popoverOffset: [0, 4], showValidationMessages: false, resizeWindowObserver: null, initialInputHeight: null, CHIP_SIZES, hasSlotContent, inputFocused: false, hideInputText: false }; }, computed: { inputPlaceHolder() { var _a; return ((_a = this.selectedItems) == null ? void 0 : _a.length) > 0 ? "" : this.placeholder; }, chipListeners() { return { keyup: (event) => { this.onChipKeyup(event); this.$emit("keyup", event); } }; }, inputListeners() { return { input: (event) => { this.$emit("input", event); if (this.hasSuggestionList) { this.showComboboxList(); } }, keyup: (event) => { this.onInputKeyup(event); this.$emit("keyup", event); }, click: (event) => { if (this.hasSuggestionList) { this.showComboboxList(); } } }; }, chipWrapperClass() { return { [`d-recipe-combobox-multi-select__chip-wrapper-${this.size}--collapsed`]: !this.inputFocused && this.collapseOnFocusOut }; } }, watch: { selectedItems: { deep: true, handler: async function() { this.initSelectedItems(); } }, chipMaxWidth: { async handler() { this.initSelectedItems(); } }, async label() { await this.$nextTick(); this.setChipsTopPosition(); }, async description() { await this.$nextTick(); this.setChipsTopPosition(); }, size: { async handler() { await this.$nextTick(); const input = this.getInput(); this.revertInputPadding(input); this.initialInputHeight = input.getBoundingClientRect().height; this.setInputPadding(); this.setChipsTopPosition(); } } }, mounted() { this.setInitialInputHeight(); this.resizeWindowObserver = new ResizeObserver(async () => { this.setChipsTopPosition(); this.setInputPadding(); }); this.resizeWindowObserver.observe(document.body); this.initSelectedItems(); }, beforeUnmount() { var _a; (_a = this.resizeWindowObserver) == null ? void 0 : _a.unobserve(document.body); }, methods: { comboboxHighlight(highlightIndex) { this.$emit("combobox-highlight", highlightIndex); }, async initSelectedItems() { await this.$nextTick(); this.setInputPadding(); this.setChipsTopPosition(); this.setInputMinWidth(); this.checkMaxSelected(); }, onChipRemove(item) { var _a; this.$emit("remove", item); (_a = this.$refs.input) == null ? void 0 : _a.focus(); }, onComboboxSelect(i) { if (this.loading) return; this.value = ""; this.$emit("select", i); }, showComboboxList() { var _a; if (this.showList != null) { return; } (_a = this.$refs.comboboxWithPopover) == null ? void 0 : _a.showComboboxList(); }, closeComboboxList() { var _a; if (this.showList != null) { return; } (_a = this.$refs.comboboxWithPopover) == null ? void 0 : _a.closeComboboxList(); }, getChipButtons() { return this.$refs.chips && this.$refs.chips.map((chip) => returnFirstEl(chip.$el).querySelector("button")); }, getChips() { return this.$refs.chips && this.$refs.chips.map((chip) => returnFirstEl(chip.$el)); }, getLastChipButton() { return this.$refs.chips && this.getChipButtons()[this.getChipButtons().length - 1]; }, getLastChip() { return this.$refs.chips && this.getChips()[this.getChips().length - 1]; }, getFirstChip() { return this.$refs.chips && this.getChips()[0]; }, getInput() { var _a; return (_a = this.$refs.input) == null ? void 0 : _a.$refs.input; }, onChipKeyup(event) { var _a; const key = (_a = event.code) == null ? void 0 : _a.toLowerCase(); if (key === "arrowleft") { this.navigateBetweenChips(event.target, true); } else if (key === "arrowright") { if (event.target.id === this.getLastChipButton().id) { this.moveFromChipToInput(); } else { this.navigateBetweenChips(event.target, false); } } }, onInputKeyup(event) { var _a; const key = (_a = event.code) == null ? void 0 : _a.toLowerCase(); if (this.selectedItems.length > 0 && event.target.selectionStart === 0) { if (key === "backspace" || key === "arrowleft") { this.moveFromInputToChip(); } } }, moveFromInputToChip() { var _a; this.getLastChipButton().focus(); (_a = this.$refs.input) == null ? void 0 : _a.blur(); this.closeComboboxList(); }, moveFromChipToInput() { var _a; this.getLastChipButton().blur(); (_a = this.$refs.input) == null ? void 0 : _a.focus(); this.showComboboxList(); }, navigateBetweenChips(target, toLeft) { var _a; const from = this.getChipButtons().indexOf(target); const to = toLeft ? from - 1 : from + 1; if (to < 0 || to >= ((_a = this.$refs.chips) == null ? void 0 : _a.length)) { return; } this.getChipButtons()[from].blur(); this.getChipButtons()[to].focus(); this.closeComboboxList(); }, setChipsTopPosition() { const input = this.getInput(); if (!input) return; const inputSlotWrapper = this.$refs.inputSlotWrapper; const top = input.getBoundingClientRect().top - inputSlotWrapper.getBoundingClientRect().top; const chipsWrapper = this.$refs.chipsWrapper; chipsWrapper.style.top = top - CHIP_TOP_POSITION[this.size] + "px"; }, setInputPadding() { const lastChip = this.getLastChip(); const input = this.getInput(); const chipsWrapper = this.$refs.chipsWrapper; if (!input) return; this.revertInputPadding(input); this.popoverOffset = [0, 4]; if (!lastChip) return; if (this.collapseOnFocusOut && !this.inputFocused) return; const left = lastChip.offsetLeft + this.getFullWidth(lastChip); const spaceLeft = input.getBoundingClientRect().width - left; if (spaceLeft > this.reservedRightSpace) { input.style.paddingLeft = left + "px"; } else { input.style.paddingLeft = "4px"; } const chipsWrapperHeight = chipsWrapper.getBoundingClientRect().height - 4; const lastChipHeight = lastChip.getBoundingClientRect().height - 4; const top = spaceLeft > this.reservedRightSpace ? lastChip.offsetTop + 2 : chipsWrapperHeight + lastChipHeight - 9; input.style.paddingTop = `${top}px`; }, revertInputPadding(input) { input.style.paddingLeft = ""; input.style.paddingTop = ""; input.style.paddingBottom = ""; }, getFullWidth(el) { const styles = window.getComputedStyle(el); return el.offsetWidth + parseInt(styles.marginLeft) + parseInt(styles.marginRight); }, setInputMinWidth() { const firstChip = this.getFirstChip(); const input = this.getInput(); if (!input) return; if (firstChip) { input.style.minWidth = this.getFullWidth(firstChip) + 4 + "px"; } else { input.style.minWidth = ""; } }, checkMaxSelected() { if (this.maxSelected === 0) return; if (this.selectedItems.length > this.maxSelected) { this.showValidationMessages = true; this.$emit("max-selected"); } else { this.showValidationMessages = false; } }, setInitialInputHeight() { const input = this.getInput(); if (!input) return; this.initialInputHeight = input.getBoundingClientRect().height; }, async handleInputFocusIn() { this.inputFocused = true; if (this.collapseOnFocusOut) { await this.$nextTick(); this.setInputPadding(); this.hideInputText = false; } }, async handleInputFocusOut() { this.inputFocused = false; if (this.collapseOnFocusOut) { const input = this.getInput(); if (!input) return; if (!input.style.paddingTop) { return; } this.hideInputText = true; this.revertInputPadding(input); } } } }; const _hoisted_1 = { ref: "header" }; const _hoisted_2 = { key: 1, class: "d-recipe-combobox-multi-select__list--loading" }; const _hoisted_3 = { ref: "footer" }; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { const _component_dt_chip = resolveComponent("dt-chip"); const _component_dt_input = resolveComponent("dt-input"); const _component_dt_validation_messages = resolveComponent("dt-validation-messages"); const _component_dt_recipe_combobox_with_popover = resolveComponent("dt-recipe-combobox-with-popover"); return openBlock(), createBlock(_component_dt_recipe_combobox_with_popover, { ref: "comboboxWithPopover", label: $props.label, "show-list": $props.showList, "max-height": $props.listMaxHeight, "max-width": $props.listMaxWidth, "popover-offset": $data.popoverOffset, "has-suggestion-list": $props.hasSuggestionList, "visually-hidden-close-label": _ctx.visuallyHiddenCloseLabel, "visually-hidden-close": _ctx.visuallyHiddenClose, "content-width": "anchor", "append-to": $props.appendTo, transition: $props.transition, onSelect: $options.onComboboxSelect, onHighlight: $options.comboboxHighlight }, createSlots({ input: withCtx(({ onInput }) => [ createElementVNode("span", { ref: "inputSlotWrapper", class: "d-recipe-combobox-multi-select__input-wrapper", onFocusin: _cache[1] || (_cache[1] = (...args) => $options.handleInputFocusIn && $options.handleInputFocusIn(...args)), onFocusout: _cache[2] || (_cache[2] = (...args) => $options.handleInputFocusOut && $options.handleInputFocusOut(...args)) }, [ createElementVNode("span", { ref: "chipsWrapper", class: normalizeClass(["d-recipe-combobox-multi-select__chip-wrapper", $options.chipWrapperClass]) }, [ (openBlock(true), createElementBlock(Fragment, null, renderList($props.selectedItems, (item) => { return openBlock(), createBlock(_component_dt_chip, mergeProps({ ref_for: true, ref: "chips", key: item, "label-class": ["d-chip__label"], class: [ "d-recipe-combobox-multi-select__chip", { "d-recipe-combobox-multi-select__chip--truncate": !!$props.chipMaxWidth } ], style: { maxWidth: $props.chipMaxWidth }, "close-button-props": { ariaLabel: "close" }, size: $data.CHIP_SIZES[$props.size] }, toHandlers($options.chipListeners), { onKeyup: withKeys(($event) => $options.onChipRemove(item), ["backspace"]), onClose: ($event) => $options.onChipRemove(item) }), { default: withCtx(() => [ createTextVNode(toDisplayString(item), 1) ]), _: 2 }, 1040, ["class", "style", "size", "onKeyup", "onClose"]); }), 128)) ], 2), createVNode(_component_dt_input, mergeProps({ ref: "input", modelValue: $data.value, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => $data.value = $event), class: "d-recipe-combobox-multi-select__input", "input-class": [ $props.inputClass, { "d-recipe-combobox-multi-select__input--hidden": $data.hideInputText } ], "input-wrapper-class": $props.inputWrapperClass, "aria-label": $props.label, label: $props.labelVisible ? $props.label : "", description: $props.description, placeholder: $options.inputPlaceHolder, "show-messages": $props.showInputMessages, messages: $props.inputMessages, size: $props.size }, toHandlers($options.inputListeners), { onInput }), null, 16, ["modelValue", "input-class", "input-wrapper-class", "aria-label", "label", "description", "placeholder", "show-messages", "messages", "size", "onInput"]), createVNode(_component_dt_validation_messages, { "validation-messages": $props.maxSelectedMessage, "show-messages": $data.showValidationMessages }, null, 8, ["validation-messages", "show-messages"]) ], 544) ]), list: withCtx(() => [ createElementVNode("div", { ref: "list", class: "d-recipe-combobox-multi-select__list", onMousedown: _cache[3] || (_cache[3] = withModifiers(() => { }, ["prevent"])) }, [ !$props.loading ? renderSlot(_ctx.$slots, "list", { key: 0 }) : (openBlock(), createElementBlock("div", _hoisted_2, toDisplayString($props.loadingMessage), 1)) ], 544) ]), _: 2 }, [ $data.hasSlotContent(_ctx.$slots.header) ? { name: "header", fn: withCtx(() => [ createElementVNode("div", _hoisted_1, [ renderSlot(_ctx.$slots, "header") ], 512) ]), key: "0" } : void 0, $data.hasSlotContent(_ctx.$slots.footer) ? { name: "footer", fn: withCtx(() => [ createElementVNode("div", _hoisted_3, [ renderSlot(_ctx.$slots, "footer") ], 512) ]), key: "1" } : void 0 ]), 1032, ["label", "show-list", "max-height", "max-width", "popover-offset", "has-suggestion-list", "visually-hidden-close-label", "visually-hidden-close", "append-to", "transition", "onSelect", "onHighlight"]); } const combobox_multi_select = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]); export { combobox_multi_select as default }; //# sourceMappingURL=combobox_multi_select.vue.js.map