@nextcloud/vue
Version:
Nextcloud vue components
536 lines (535 loc) • 15.9 kB
JavaScript
import '../assets/NcSelect-ZnE_MlqV.css';
import { autoUpdate, computePosition, offset, flip, shift, limitShift } from "@floating-ui/dom";
import { h, resolveComponent, createBlock, openBlock, mergeProps, createSlots, withCtx, createTextVNode, toDisplayString, createCommentVNode, renderSlot, createVNode, normalizeProps, guardReactiveProps, createElementVNode, toHandlers, renderList, warn } from "vue";
import VueSelect from "vue-select";
import { C as ChevronDown } from "./ChevronDown-FiGpp0KT.mjs";
import { I as IconClose } from "./Close-D6ngJ4t9.mjs";
import { N as NcEllipsisedOption } from "./NcEllipsisedOption-dT-CtXYp.mjs";
import { N as NcLoadingIcon } from "./NcLoadingIcon-b_ajZ_nQ.mjs";
import { r as register, d as t17, a as t } from "./_l10n-DrTiip5c.mjs";
import { c as createElementId } from "./createElementId-DhjFt1I9.mjs";
import { i as isLegacy } from "./legacy-DcjXBL_t.mjs";
import "vue-select/dist/vue-select.css";
import { _ as _export_sfc } from "./_plugin-vue_export-helper-1tPrXgE0.mjs";
register(t17);
const _sfc_main = {
name: "NcSelect",
components: {
ChevronDown,
NcEllipsisedOption,
NcLoadingIcon,
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
},
/**
* 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, 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: () => 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
};
},
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");
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() {
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
}
};
const _hoisted_1 = ["for"];
const _hoisted_2 = ["required"];
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_ChevronDown = resolveComponent("ChevronDown");
const _component_NcEllipsisedOption = resolveComponent("NcEllipsisedOption");
const _component_NcLoadingIcon = resolveComponent("NcLoadingIcon");
const _component_VueSelect = resolveComponent("VueSelect");
return openBlock(), createBlock(_component_VueSelect, mergeProps({
class: ["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 }) => [
createElementVNode("input", mergeProps({
class: ["vs__search", [$props.inputClass]]
}, attributes, {
required: $options.inputRequired,
dir: "auto"
}, toHandlers(events, true)), null, 16, _hoisted_2)
]),
"open-indicator": withCtx(({ attributes }) => [
createVNode(_component_ChevronDown, mergeProps(attributes, {
"fill-color": "var(--vs-controls-color)",
style: {
cursor: !$props.disabled ? "pointer" : null
},
size: 26
}), 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", { vBind: 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.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,
renderList(_ctx.$slots, (_, 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-Czzsi3P_.mjs.map