@nextcloud/vue
Version:
Nextcloud vue components
534 lines (533 loc) • 16.6 kB
JavaScript
import '../assets/NcSelect-EIXtZSVn.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 t16, a as t } from "./_l10n-JYjUKekn.mjs";
import { C as ChevronDown } from "./ChevronDown-D3Tfshug.mjs";
import { C as Close } from "./Close-B6ccm1RP.mjs";
import NcEllipsisedOption from "../Components/NcEllipsisedOption.mjs";
import { N as NcListItemIcon } from "./NcListItemIcon-Bc4y0xg3.mjs";
import NcLoadingIcon from "../Components/NcLoadingIcon.mjs";
import { G as GenRandomId } from "./GenRandomId-CMooMQt0.mjs";
import "@nextcloud/vue-select/dist/vue-select.css";
import { n as normalizeComponent } from "./_plugin-vue2_normalizer-DU4iP6Vu.mjs";
register(t16);
const _sfc_main = {
name: "NcSelect",
components: {
ChevronDown,
NcEllipsisedOption,
NcListItemIcon,
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
},
/**
* 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-".concat(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
},
/**
* 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
*/
value: {
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
" "
],
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
};
},
data() {
return {
search: ""
};
},
computed: {
inputRequired() {
if (!this.required) {
return null;
}
return this.value === null || Array.isArray(this.value) && this.value.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: "".concat(x, "px"),
top: "".concat(y, "px"),
width: "".concat(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) => {
var _a, _b, _c;
const match = search.match(EMAIL_NOTATION);
return match && ((_c = (_b = (_a = option.subname) == null ? void 0 : _a.toLocaleLowerCase) == null ? void 0 : _b.call(_a)) == null ? void 0 : _c.indexOf(match[1].toLocaleLowerCase())) > -1 || "".concat(label, " ").concat(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) => {
var _a;
return Object.keys((_a = mixin.props) != null ? _a : {});
})
];
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,
filterBy: this.localFilterBy,
label: this.localLabel
};
return propsToForward;
}
},
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 } }, "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.$listeners));
};
var _sfc_staticRenderFns = [];
var __component__ = /* @__PURE__ */ normalizeComponent(
_sfc_main,
_sfc_render,
_sfc_staticRenderFns,
false,
null,
null
);
const NcSelect = __component__.exports;
export {
NcSelect as N
};