UNPKG

@dialpad/dialtone-vue

Version:

Vue component library for Dialpad's design system Dialtone

590 lines (589 loc) • 19 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 { 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 { getUniqueString } from "../../../common/utils.js"; import normalizeComponent from "../../../_virtual/_plugin-vue2_normalizer.js"; const _sfc_main = { 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, inputFocused: false, hideInputText: false }; }, computed: { inputPlaceHolder() { var _a; return ((_a = this.selectedItems) == null ? void 0 : _a.length) > 0 ? "" : this.placeholder; }, chipListeners() { return { ...this.$listeners, keyup: (event) => { this.onChipKeyup(event); this.$emit("keyup", event); } }; }, inputListeners() { return { ...this.$listeners, 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(); } } }; }, selectedItemsWithKeys() { return this.selectedItems.map((selectedItem) => ({ item: selectedItem, key: getUniqueString(selectedItem) })); }, chipWrapperClass() { return { [`d-recipe-combobox-multi-select__chip-wrapper-${this.size}--collapsed`]: !this.inputFocused && this.collapseOnFocusOut }; } }, watch: { selectedItems: { async handler() { 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(); }, beforeDestroy() { 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) => chip.$el.querySelector("button")); }, getChips() { return this.$refs.chips && this.$refs.chips.map((chip) => 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); } } } }; var _sfc_render = function render() { var _vm = this, _c = _vm._self._c; return _c("dt-recipe-combobox-with-popover", { ref: "comboboxWithPopover", attrs: { "label": _vm.label, "show-list": _vm.showList, "max-height": _vm.listMaxHeight, "max-width": _vm.listMaxWidth, "popover-offset": _vm.popoverOffset, "has-suggestion-list": _vm.hasSuggestionList, "visually-hidden-close-label": _vm.visuallyHiddenCloseLabel, "visually-hidden-close": _vm.visuallyHiddenClose, "content-width": "anchor", "append-to": _vm.appendTo, "transition": _vm.transition }, on: { "select": _vm.onComboboxSelect, "highlight": _vm.comboboxHighlight }, scopedSlots: _vm._u([{ key: "input", fn: function({ onInput }) { return [_c("span", { ref: "inputSlotWrapper", staticClass: "d-recipe-combobox-multi-select__input-wrapper", on: { "focusin": _vm.handleInputFocusIn, "focusout": _vm.handleInputFocusOut } }, [_c("span", { ref: "chipsWrapper", class: ["d-recipe-combobox-multi-select__chip-wrapper", _vm.chipWrapperClass] }, _vm._l(_vm.selectedItemsWithKeys, function({ item, key }) { return _c("dt-chip", _vm._g({ key, ref: "chips", refInFor: true, class: [ "d-recipe-combobox-multi-select__chip", { "d-recipe-combobox-multi-select__chip--truncate": !!_vm.chipMaxWidth } ], style: { maxWidth: _vm.chipMaxWidth }, attrs: { "label-class": ["d-chip__label"], "close-button-props": { ariaLabel: "close" }, "size": _vm.CHIP_SIZES[_vm.size] }, on: { "keyup": function($event) { if (!$event.type.indexOf("key") && _vm._k($event.keyCode, "backspace", void 0, $event.key, void 0)) return null; return _vm.onChipRemove(item); }, "close": function($event) { return _vm.onChipRemove(item); } } }, _vm.chipListeners), [_vm._v(" " + _vm._s(item) + " ")]); }), 1), _c("dt-input", _vm._g({ ref: "input", staticClass: "d-recipe-combobox-multi-select__input", attrs: { "input-class": [ _vm.inputClass, { "d-recipe-combobox-multi-select__input--hidden": _vm.hideInputText } ], "input-wrapper-class": _vm.inputWrapperClass, "aria-label": _vm.label, "label": _vm.labelVisible ? _vm.label : "", "description": _vm.description, "placeholder": _vm.inputPlaceHolder, "show-messages": _vm.showInputMessages, "messages": _vm.inputMessages, "size": _vm.size }, on: { "input": onInput }, model: { value: _vm.value, callback: function($$v) { _vm.value = $$v; }, expression: "value" } }, _vm.inputListeners)), _c("dt-validation-messages", { attrs: { "validation-messages": _vm.maxSelectedMessage, "show-messages": _vm.showValidationMessages } })], 1)]; } }, _vm.$slots.header ? { key: "header", fn: function() { return [_c("div", { ref: "header" }, [_vm._t("header")], 2)]; }, proxy: true } : null, { key: "list", fn: function() { return [_c("div", { ref: "list", staticClass: "d-recipe-combobox-multi-select__list", on: { "mousedown": function($event) { $event.preventDefault(); } } }, [!_vm.loading ? _vm._t("list") : _c("div", { staticClass: "d-recipe-combobox-multi-select__list--loading" }, [_vm._v(" " + _vm._s(_vm.loadingMessage) + " ")])], 2)]; }, proxy: true }, _vm.$slots.footer ? { key: "footer", fn: function() { return [_c("div", { ref: "footer" }, [_vm._t("footer")], 2)]; }, proxy: true } : null], null, true) }); }; var _sfc_staticRenderFns = []; var __component__ = /* @__PURE__ */ normalizeComponent( _sfc_main, _sfc_render, _sfc_staticRenderFns ); const combobox_multi_select = __component__.exports; export { combobox_multi_select as default }; //# sourceMappingURL=combobox_multi_select.vue.js.map