UNPKG

@nextcloud/vue

Version:
630 lines (629 loc) 20.3 kB
import '../assets/NcSelect.css'; import { autoUpdate, computePosition, flip, offset, shift, limitShift } from "@floating-ui/dom"; import { d as mdiCheck, j as mdiAlertCircleOutline } from "./mdi.mjs"; import { VueSelect } from "@nextcloud/vue-select"; import { resolveComponent, openBlock, createBlock, mergeProps, createSlots, withCtx, createTextVNode, toDisplayString, createCommentVNode, renderSlot, normalizeProps, guardReactiveProps, createVNode, normalizeClass, createElementVNode, createElementBlock, renderList, warn, h } from "vue"; import { C as ChevronDown } from "./ChevronDown.mjs"; import { I as IconClose } from "./Close.mjs"; import { N as NcEllipsisedOption } from "./NcEllipsisedOption.mjs"; import { N as NcIconSvgWrapper } from "./NcIconSvgWrapper.mjs"; import { N as NcLoadingIcon } from "./NcLoadingIcon.mjs"; import { _ as _sfc_main$1 } from "./NcTextField.vue_vue_type_script_setup_true_lang.mjs"; import { r as register, g as t18, a as t } from "./_l10n.mjs"; import { c as createElementId } from "./createElementId.mjs"; import { a as isLegacy } from "./legacy.mjs"; import { _ as _export_sfc } from "./_plugin-vue_export-helper.mjs"; register(t18); const _sfc_main = { name: "NcSelect", components: { ChevronDown, NcEllipsisedOption, NcIconSvgWrapper, NcLoadingIcon, NcTextField: _sfc_main$1, VueSelect }, 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 }, /** * Keep the dropdown open after selecting an option. * * @default false * @since 8.25.0 */ keepOpen: { type: Boolean, default: false }, /** * Replace default vue-select components * * @see https://vue-select.org/api/props.html#components */ components: { type: Object, default: () => ({ Deselect: { render: () => h(IconClose, { 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 * * @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: () => createElementId() }, /** * Visible label for the input element */ inputLabel: { type: String, default: null }, /** * Additional helper text shown beneath the select. */ helperText: { type: String, default: "" }, /** * Toggle the error state, styling the helper text as an error. */ error: { type: Boolean, default: false }, /** * Toggle the success state, styling the helper text as a success. */ success: { type: Boolean, default: false }, /** * 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 * * @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, (event: KeyboardEvent) => void>} map - Mapped keyCode to handlers { <keyCode>:<callback> } * @param {import('vue').ComponentPublicInstance} vm - VueSelect instance * @return {Record<number, (event: KeyboardEvent) => void>} 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: () => createElementId() }, /** * 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 }, /** * 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 " ", "update:modelValue" ], 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; return { avatarSize, isLegacy, mdiAlertCircleOutline, mdiCheck }; }, data() { return { search: "" }; }, computed: { inputRequired() { if (!this.required) { return null; } return this.modelValue === null || Array.isArray(this.modelValue) && this.modelValue.length === 0; }, localCalculatePosition() { if (this.calculatePosition !== null) { return this.calculatePosition; } return (dropdownMenu, component, { width }) => { dropdownMenu.style.width = width; const addClass = { name: "addClass", fn() { dropdownMenu.classList.add("vs__dropdown-menu--floating", "nc-select__dropdown"); 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: [ // Flip first so the placement-dependent middleware below see the final placement flip(), addClass, togglePlacementClass, // On top placement, leave a gap for the floating label overhang // (about half the floated label line-height) instead of connecting seamlessly offset(({ placement }) => placement.startsWith("top") ? 10 : -1), 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() { return this.filterBy ?? VueSelect.props.filterBy.default; }, localLabel() { return this.label ?? 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 calculatePosition: this.localCalculatePosition, closeOnSelect: !this.keepOpen, filterBy: this.localFilterBy, label: this.localLabel }; return propsToForward; } }, mounted() { if (!this.labelOutside && !this.inputLabel && !this.ariaLabelCombobox) { 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) { warn("[NcSelect] Only one of `inputLabel` or `ariaLabelCombobox` should to be set."); } }, methods: { t, /** * Consumer-provided slots forwarded to vue-select, excluding `footer` * which we render ourselves to host the helper text. * * `$slots` is not reactive, so this is a method (re-evaluated on every * render) rather than a computed, to keep the forwarded slot list fresh. * * @return {string[]} */ forwardedSlots() { return Object.keys(this.$slots).filter((name) => name !== "footer"); } } }; const _hoisted_1 = ["for"]; const _hoisted_2 = ["id"]; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { const _component_NcTextField = resolveComponent("NcTextField"); const _component_ChevronDown = resolveComponent("ChevronDown"); const _component_NcEllipsisedOption = resolveComponent("NcEllipsisedOption"); const _component_NcLoadingIcon = resolveComponent("NcLoadingIcon"); const _component_NcIconSvgWrapper = resolveComponent("NcIconSvgWrapper"); const _component_VueSelect = resolveComponent("VueSelect"); return openBlock(), createBlock(_component_VueSelect, mergeProps({ class: ["select nc-select", { "select--legacy": $setup.isLegacy, "select--no-wrap": $props.noWrap }] }, $options.propsToForward, { onSearch: _cache[0] || (_cache[0] = ($event) => $data.search = $event), "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => _ctx.$emit("update:modelValue", $event)) }), createSlots({ search: withCtx(({ attributes, events }) => [ createVNode(_component_NcTextField, { id: attributes.id, class: normalizeClass(["vs__search", [$props.inputClass]]), type: attributes.type, modelValue: attributes.value, placeholder: $props.multiple || $props.modelValue ? "" : $props.placeholder || "", label: !$props.labelOutside && !$props.multiple && $props.inputLabel ? $props.inputLabel : "", labelOutside: $props.labelOutside || $props.multiple, disabled: $props.disabled, required: $options.inputRequired, role: attributes.role, tabindex: attributes.tabindex, readonly: attributes.readonly, autocomplete: attributes.autocomplete, "aria-label": attributes["aria-label"], "aria-describedby": [attributes["aria-describedby"], $props.helperText ? `${$props.inputId}-helper-text` : null].filter(Boolean).join(" ") || void 0, "aria-autocomplete": attributes["aria-autocomplete"], "aria-controls": attributes["aria-controls"], "aria-owns": attributes["aria-owns"], "aria-expanded": attributes["aria-expanded"], "aria-activedescendant": attributes["aria-activedescendant"], onKeydown: events.keydown, onKeypress: events.keypress, onFocus: events.focus, onBlur: events.blur, onCompositionstart: events.compositionstart, onCompositionend: events.compositionend, "onUpdate:modelValue": ($event) => events.input({ target: { value: $event } }) }, null, 8, ["id", "class", "type", "modelValue", "placeholder", "label", "labelOutside", "disabled", "required", "role", "tabindex", "readonly", "autocomplete", "aria-label", "aria-describedby", "aria-autocomplete", "aria-controls", "aria-owns", "aria-expanded", "aria-activedescendant", "onKeydown", "onKeypress", "onFocus", "onBlur", "onCompositionstart", "onCompositionend", "onUpdate:modelValue"]) ]), "open-indicator": withCtx(({ attributes }) => [ createVNode(_component_ChevronDown, mergeProps(attributes, { fillColor: "var(--vs-controls-color)", style: { cursor: !$props.disabled ? "pointer" : null }, size: 20 }), null, 16, ["style"]) ]), option: withCtx((option) => [ renderSlot(_ctx.$slots, "option", normalizeProps(guardReactiveProps(option)), () => [ createVNode(_component_NcEllipsisedOption, { name: String(option[$options.localLabel]), search: $data.search }, null, 8, ["name", "search"]) ]) ]), "selected-option": withCtx((selectedOption) => [ renderSlot(_ctx.$slots, "selected-option", normalizeProps(guardReactiveProps(selectedOption)), () => [ createVNode(_component_NcEllipsisedOption, { name: String(selectedOption[$options.localLabel]), search: $data.search }, null, 8, ["name", "search"]) ]) ]), spinner: withCtx((spinner) => [ spinner.loading ? (openBlock(), createBlock(_component_NcLoadingIcon, { key: 0 })) : createCommentVNode("", true) ]), "no-options": withCtx(() => [ createTextVNode(toDisplayString($options.t("No results")), 1) ]), _: 2 }, [ $props.multiple && !$props.labelOutside && $props.inputLabel ? { name: "header", fn: withCtx(() => [ createElementVNode("label", { for: $props.inputId, class: "select__label" }, toDisplayString($props.inputLabel), 9, _hoisted_1) ]), key: "0" } : void 0, $props.helperText || _ctx.$slots.footer ? { name: "footer", fn: withCtx((data) => [ $props.helperText ? (openBlock(), createElementBlock("p", { key: 0, id: `${$props.inputId}-helper-text`, class: normalizeClass(["select__helper-text", { "select__helper-text--error": $props.error, "select__helper-text--success": $props.success }]) }, [ $props.success ? (openBlock(), createBlock(_component_NcIconSvgWrapper, { key: 0, class: "select__helper-text-icon", path: $setup.mdiCheck, inline: "" }, null, 8, ["path"])) : $props.error ? (openBlock(), createBlock(_component_NcIconSvgWrapper, { key: 1, class: "select__helper-text-icon", path: $setup.mdiAlertCircleOutline, inline: "" }, null, 8, ["path"])) : createCommentVNode("", true), createTextVNode(" " + toDisplayString($props.helperText), 1) ], 10, _hoisted_2)) : createCommentVNode("", true), renderSlot(_ctx.$slots, "footer", normalizeProps(guardReactiveProps(data))) ]), key: "1" } : void 0, renderList($options.forwardedSlots(), (name) => { return { name, fn: withCtx((data) => [ renderSlot(_ctx.$slots, name, normalizeProps(guardReactiveProps(data))) ]) }; }) ]), 1040, ["class"]); } const NcSelect = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]); export { NcSelect as N }; //# sourceMappingURL=NcSelect.mjs.map