UNPKG

@nextcloud/vue

Version:
562 lines (561 loc) 17.2 kB
import '../assets/NcSelect-GdIw6cIy.css'; import { VueSelect } from "@nextcloud/vue-select"; import { autoUpdate, computePosition, offset, flip, shift, limitShift } from "@floating-ui/dom"; import Vue from "vue"; import { r as register, c as t15, a as t } from "./_l10n-DDKxBWQL.mjs"; import { C as ChevronDown } from "./ChevronDown-DFQfzh63.mjs"; import { C as Close } from "./Close-BtLPUSdO.mjs"; import NcEllipsisedOption from "../Components/NcEllipsisedOption.mjs"; import { N as NcListItemIcon } from "./NcListItemIcon-7x7iDHYw.mjs"; import NcLoadingIcon from "../Components/NcLoadingIcon.mjs"; import { G as GenRandomId } from "./GenRandomId-CMooMQt0.mjs"; import { u as useModelMigration } from "./useModelMigration-EhAWvqDD.mjs"; import "@nextcloud/vue-select/dist/vue-select.css"; import { n as normalizeComponent } from "./_plugin-vue2_normalizer-DU4iP6Vu.mjs"; register(t15); const _sfc_main = { name: "NcSelect", components: { ChevronDown, NcEllipsisedOption, NcListItemIcon, NcLoadingIcon, VueSelect }, model: { prop: "modelValue", event: "update:modelValue" }, props: { // Add VueSelect props to $props ...VueSelect.props, ...VueSelect.mixins.reduce((allProps, mixin) => ({ ...allProps, ...mixin.props }), {}), /** * `aria-label` for the clear input button */ ariaLabelClearSelected: { type: String, default: t("Clear selected") }, /** * `aria-label` for the search input * * A descriptive `inputLabel` is preferred as this is not visible. */ ariaLabelCombobox: { type: String, default: null }, /** * `aria-label` for the listbox element */ ariaLabelListbox: { type: String, default: t("Options") }, /** * Allows to customize the `aria-label` for the deselect-option button * The default is "Deselect " + optionLabel * @type {(optionLabel: string) => string} */ ariaLabelDeselectOption: { type: Function, default: (optionLabel) => t("Deselect {option}", { option: optionLabel }) }, /** * Append the dropdown element to the end of the body * and size/position it dynamically. * * @see https://vue-select.org/api/props.html#appendtobody */ appendToBody: { type: Boolean, default: true }, /** * When `appendToBody` is true, this function is responsible for * positioning the drop down list. * * If a function is returned from `calculatePosition`, it will * be called when the drop down list is removed from the DOM. * This allows for any garbage collection you may need to do. * * @see https://vue-select.org/api/props.html#calculateposition */ calculatePosition: { type: Function, default: null }, /** * Close the dropdown when selecting an option * * @see https://vue-select.org/api/props.html#closeonselect */ closeOnSelect: { type: Boolean, default: true }, /** * Replace default vue-select components * * @see https://vue-select.org/api/props.html#components */ components: { type: Object, default: () => ({ Deselect: { render: (createElement) => createElement(Close, { props: { size: 20, fillColor: "var(--vs-controls-color)" }, style: { cursor: "pointer" } }) } }) }, /** * Sets the maximum number of options to display in the dropdown list */ limit: { type: Number, default: null }, /** * Disable the component * * @see https://vue-select.org/api/props.html#disabled */ disabled: { type: Boolean, default: false }, /** * Determines whether the dropdown should be open. * Receives the component instance as the only argument. * * @see https://vue-select.org/api/props.html#dropdownshouldopen */ dropdownShouldOpen: { type: Function, default: ({ noDrop, open }) => { return noDrop ? false : open; } }, /** * Callback to determine if the provided option should * match the current search text. Used to determine * if the option should be displayed. * * Defaults to the internal vue-select function documented at the link * below * * Enabling `userSelect` will automatically set this to filter by the * `displayName` and `subname` properties of the user option object * unless this prop is set explicitly * * @see https://vue-select.org/api/props.html#filterby */ filterBy: { type: Function, default: null }, /** * Class for the `input` * * Necessary for use in NcActionInput */ inputClass: { type: [String, Object], default: null }, /** * Input element id */ inputId: { type: String, default: () => `select-input-${GenRandomId()}` }, /** * Visible label for the input element * * @todo Set default for @nextcloud/vue 9 */ inputLabel: { type: String, default: null }, /** * Pass true if you are using an external label */ labelOutside: { type: Boolean, default: false }, /** * Display a visible border around dropdown options * which have keyboard focus */ keyboardFocusBorder: { type: Boolean, default: true }, /** * Key of the displayed label for object options * * Defaults to the internal vue-select string documented at the link * below * * Enabling `userSelect` will automatically set this to `'displayName'` * unless this prop is set explicitly * * @see https://vue-select.org/api/props.html#label */ label: { type: String, default: null }, /** * Show the loading icon * * @see https://vue-select.org/api/props.html#loading */ loading: { type: Boolean, default: false }, /** * Allow selection of multiple options * * @see https://vue-select.org/api/props.html#multiple */ multiple: { type: Boolean, default: false }, /** * Disable automatic wrapping when selected options overflow the width */ noWrap: { type: Boolean, default: false }, /** * Array of options * * @type {Array<string | number | Record<string | number, any>>} * * @see https://vue-select.org/api/props.html#options */ options: { type: Array, default: () => [] }, /** * Placeholder text * * @see https://vue-select.org/api/props.html#placeholder */ placeholder: { type: String, default: "" }, /** * Customized component's response to keydown events while the search input has focus * * @see https://vue-select.org/guide/keydown.html#mapkeydown */ mapKeydown: { type: Function, /** * Patched Vue-Select keydown events handlers map to stop Escape propagation in open select * * @param {Record<number, Function>} map - Mapped keyCode to handlers { <keyCode>:<callback> } * @param {import('@nextcloud/vue-select').VueSelect} vm - VueSelect instance * @return {Record<number, Function>} patched keydown event handlers */ default(map, vm) { return { ...map, /** * Patched Escape handler to stop propagation from open select * * @param {KeyboardEvent} event - default keydown event handler */ 27: (event) => { if (vm.open) { event.stopPropagation(); } map[27](event); } }; } }, /** * A unique identifier used to generate IDs and DOM attributes. Must be unique for every instance of the component. * * @see https://vue-select.org/api/props.html#uid */ uid: { type: String, default: () => GenRandomId() }, /** * When `appendToBody` is true, this sets the placement of the dropdown * * @type {'bottom' | 'top'} */ placement: { type: String, default: "bottom" }, /** * If false, the focused dropdown option will not be reset when filtered * options change */ resetFocusOnOptionsChange: { type: Boolean, default: true }, /** * Enable the user selector with avatars * * Objects must contain the data expected by the * [NcListItemIcon](#/Components/NcListItemIcon) and * [NcAvatar](#/Components/NcAvatar) components */ userSelect: { type: Boolean, default: false }, /** * Removed in v9 - use `modelValue` (`v-model`) instead * @deprecated */ value: { type: [String, Number, Object, Array], default: void 0 }, /** * Currently selected value * * The `v-model` directive may be used for two-way data binding * * @type {string | number | Record<string | number, any> | Array<any>} * * @see https://vue-select.org/api/props.html#value */ modelValue: { type: [String, Number, Object, Array], default: null }, /** * Enable if a value is required for native form validation */ required: { type: Boolean, default: false }, /** * Any available prop * * @see https://vue-select.org/api/props.html */ // Not an actual prop but needed to show in vue-styleguidist docs // eslint-disable-next-line " ": {} }, emits: [ /** * All events from https://vue-select.org/api/events.html */ // Not an actual event but needed to show in vue-styleguidist docs " ", /** * Removed in v9 - use `update:modelValue` (`v-model`) instead * @deprecated */ "input", "update:modelValue", /** Same as update:modelValue for Vue 2 compatibility */ "update:model-value" ], setup() { const clickableArea = Number.parseInt(window.getComputedStyle(document.body).getPropertyValue("--default-clickable-area")); const gridBaseLine = Number.parseInt(window.getComputedStyle(document.body).getPropertyValue("--default-grid-baseline")); const avatarSize = clickableArea - 2 * gridBaseLine; const model = useModelMigration("value", "input"); return { avatarSize, model }; }, data() { return { search: "" }; }, computed: { inputRequired() { if (!this.required) { return null; } return this.model === null || Array.isArray(this.model) && this.model.length === 0; }, localCalculatePosition() { if (this.calculatePosition !== null) { return this.calculatePosition; } return (dropdownMenu, component, { width }) => { dropdownMenu.style.width = width; const addClass = { name: "addClass", fn(_middlewareArgs) { dropdownMenu.classList.add("vs__dropdown-menu--floating"); return {}; } }; const togglePlacementClass = { name: "togglePlacementClass", fn({ placement }) { component.$el.classList.toggle( "select--drop-up", placement === "top" ); dropdownMenu.classList.toggle( "vs__dropdown-menu--floating-placement-top", placement === "top" ); return {}; } }; const updatePosition = () => { computePosition(component.$refs.toggle, dropdownMenu, { placement: this.placement, middleware: [ offset(-1), addClass, togglePlacementClass, // Match popperjs default collision prevention behavior by appending the following middleware in order flip(), shift({ limiter: limitShift() }) ] }).then(({ x, y }) => { Object.assign(dropdownMenu.style, { left: `${x}px`, top: `${y}px`, width: `${component.$refs.toggle.getBoundingClientRect().width}px` }); }); }; const cleanup = autoUpdate( component.$refs.toggle, dropdownMenu, updatePosition ); return cleanup; }; }, localFilterBy() { const EMAIL_NOTATION = /[^<]*<([^>]+)/; if (this.filterBy !== null) { return this.filterBy; } if (this.userSelect) { return (option, label, search) => { const match = search.match(EMAIL_NOTATION); return match && option.subname?.toLocaleLowerCase?.()?.indexOf(match[1].toLocaleLowerCase()) > -1 || `${label} ${option.subname}`.toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) > -1; }; } return VueSelect.props.filterBy.default; }, localLabel() { if (this.label !== null) { return this.label; } if (this.userSelect) { return "displayName"; } return VueSelect.props.label.default; }, propsToForward() { const vueSelectKeys = [ ...Object.keys(VueSelect.props), ...VueSelect.mixins.flatMap((mixin) => Object.keys(mixin.props ?? {})) ]; const initialPropsToForward = Object.fromEntries( Object.entries(this.$props).filter(([key, _value]) => vueSelectKeys.includes(key)) ); const propsToForward = { ...initialPropsToForward, // Custom overrides of vue-select props value: this.model, calculatePosition: this.localCalculatePosition, filterBy: this.localFilterBy, label: this.localLabel }; return propsToForward; }, listenersToForward() { return { ...this.$listeners, input: ($event) => { this.model = $event; } }; } }, mounted() { if (!this.labelOutside && !this.inputLabel && !this.ariaLabelCombobox) { Vue.util.warn("[NcSelect] An `inputLabel` or `ariaLabelCombobox` should be set. If an external label is used, `labelOutside` should be set to `true`."); } if (this.inputLabel && this.ariaLabelCombobox) { Vue.util.warn("[NcSelect] Only one of `inputLabel` or `ariaLabelCombobox` should to be set."); } }, methods: { t } }; var _sfc_render = function render() { var _vm = this, _c = _vm._self._c; return _c("VueSelect", _vm._g(_vm._b({ staticClass: "select", class: { "select--no-wrap": _vm.noWrap, "user-select": _vm.userSelect }, on: { "search": (searchString) => _vm.search = searchString }, scopedSlots: _vm._u([!_vm.labelOutside && _vm.inputLabel ? { key: "header", fn: function() { return [_c("label", { staticClass: "select__label", attrs: { "for": _vm.inputId } }, [_vm._v(" " + _vm._s(_vm.inputLabel) + " ")])]; }, proxy: true } : null, { key: "search", fn: function({ attributes, events }) { return [_c("input", _vm._g(_vm._b({ class: ["vs__search", _vm.inputClass], attrs: { "required": _vm.inputRequired, "dir": "auto" } }, "input", attributes, false), events))]; } }, { key: "open-indicator", fn: function({ attributes }) { return [_c("ChevronDown", _vm._b({ style: { cursor: !_vm.disabled ? "pointer" : null }, attrs: { "fill-color": "var(--vs-controls-color)", "size": 26 } }, "ChevronDown", attributes, false))]; } }, { key: "option", fn: function(option) { return [_vm.userSelect ? _c("NcListItemIcon", _vm._b({ attrs: { "avatar-size": 32, "name": option[_vm.localLabel], "search": _vm.search } }, "NcListItemIcon", option, false)) : _c("NcEllipsisedOption", { attrs: { "name": String(option[_vm.localLabel]), "search": _vm.search } })]; } }, { key: "selected-option", fn: function(selectedOption) { return [_vm.userSelect ? _c("NcListItemIcon", _vm._b({ attrs: { "avatar-size": _vm.avatarSize, "name": selectedOption[_vm.localLabel], "no-margin": "", "search": _vm.search } }, "NcListItemIcon", selectedOption, false)) : _c("NcEllipsisedOption", { attrs: { "name": String(selectedOption[_vm.localLabel]), "search": _vm.search } })]; } }, { key: "spinner", fn: function(spinner) { return [spinner.loading ? _c("NcLoadingIcon") : _vm._e()]; } }, { key: "no-options", fn: function() { return [_vm._v(" " + _vm._s(_vm.t("No results")) + " ")]; }, proxy: true }, _vm._l(_vm.$scopedSlots, function(_, name) { return { key: name, fn: function(data) { return [_vm._t(name, null, null, data)]; } }; })], null, true) }, "VueSelect", _vm.propsToForward, false), _vm.listenersToForward)); }; var _sfc_staticRenderFns = []; var __component__ = /* @__PURE__ */ normalizeComponent( _sfc_main, _sfc_render, _sfc_staticRenderFns, false, null, null ); const NcSelect = __component__.exports; export { NcSelect as N };