@postnord/web-components
Version:
PostNord Web Components
1,160 lines (1,159 loc) • 59.5 kB
JavaScript
/*!
* Built with Stencil
* By PostNord.
*/
import { h, Host, Mixin } from "@stencil/core";
import { animateHeightFactory } from "../../../globals/mixins/index";
import { translations } from "./translations";
import { close, alert_exclamation_circle, chevron_down } from "pn-design-assets/pn-assets/icons.js";
import { uuidv4, awaitTopbar, ripple, en, getMenuWidth, getTotalHeightOffset } from "../../../index";
/**
* The `pn-multiselect` gets its options via javascript.
*
* Native HTML does not accept arrays of objects. Most frameworks solves this automatically (Vue, react, etc...),
* but keep this in mind if you use this component outside of a framework environment.
*
* @nativeChange The `pn-multiselect` is built with `input[type=checkbox]` elements, so the `change` event works. However, we recommend the customEvents described above.
*
* @slot helpertext - You can use this slot instead of the prop `helpertext`. Recommended, only if you need to include additional HTML markup. Such as a `pn-text-link`. Use a `span` element to wrap the text and link. {@since v7.25.0}
* @slot error - You can use this slot instead of the prop `error`. Recommended, only if you need to include additional HTML markup. Such as a `pn-text-link`. Use a `span` element to wrap the text and link. {@since v7.25.0}
*
* @see {@link https://portal.postnord.com/web-components/?path=/docs/components-input-multiselect--docs#pn-multiselect%20(options) Options documentation}
*/
export class PnMultiselect extends Mixin(animateHeightFactory) {
constructor() {
super();
}
id = `pn-multiselect-${uuidv4()}`;
idLegend = `${this.id}-legend`;
idButton = `${this.id}-button`;
idOptions = `${this.id}-options`;
idHelper = `${this.id}-text`;
idError = `${this.id}-error`;
idChips = `${this.id}-chips`;
idSr = `${this.id}-sr`;
elGroup;
elButton;
elInput;
elList;
elChips;
srTimer;
hostElement;
isSearching = false;
/** Layout state. */
open = false;
upwards = false;
srMessage;
/** Label placed above the select */
label;
/** Display a helper text underneath the select */
helpertext;
/** Manually set the language, not needed if you have the pnTopbar available */
language = undefined;
/** HTML name of the checkbox elements. Used for each checkbox inside the multiselect. @category Native attributes */
name;
/** HTML name of the input element. @category Native attributes*/
selectName;
/**
* This is what will be shown on load if no value is used.
* The `placeholder` will override the default text used if you have the `search` prop active.
* @category Native attributes
*/
placeholder;
/** Set the select as required. @category Native attributes */
required = false;
/** Disable the select. @category Native attributes */
disabled = false;
/**
* The array of `PnMultiselectOption` objects.
* @see {@link PnMultiselectOption} for the option object properties.
* @category Features
**/
options;
/** Set the date picker label as compact. If used, the `placeholder` will no longer be displayed. @since v7.21.0 @category Features */
compact = false;
/** Display an icon to the left of the select input @category Features */
icon;
/**
* Adds a "Select all" option into the list.
* If you use the search feature at the same time, clicking this option will only toggle the options found.
* @see {@link search}
* @category Features
**/
selectAll = false;
/**
* Set a custom value for the "Select all value" option.
* @see {@link selectAll}
* @category Features
*/
allValue = 'pn_multiselect_all';
/**
* Allow the user to search among the options.
* The selected options will now display underneath the multiselect element.
*
* @category Features
**/
search = false;
/** Set the search query of the multiselect.
*
* @see {@link search}
* @category Features
*/
searchQuery = '';
/**
* Decide how many items should be shown before ellipsis. Requires the `search` prop to work.
*
* @see {@link search}
* @category Features
**/
itemCount = 5;
/**
* The component will automatically set the max height of the dropdown list on its own.
* It takes the `pn-topbar` into account and will open in the direction that fits best.
* Use this prop to override this behaviour and use a custom max-height.
* @category Features
**/
maxHeight;
/**
* Force the dropdown to always open upwards.
* @category Features
**/
top = false;
/**
* Force the dropdown to always open downwards.
* @category Features
**/
bottom = false;
/** Trigger the invalid state. @category Features */
invalid = false;
/** Display an error message and trigger the invalid state. @category Features */
error;
/** Set a custom ID for the select. @since v7.25.0 @category HTML attributes */
pnId;
/**
* Provide the label via an aria attribute.
* We strongly recommend you use the `label` prop instead.
* @since v7.25.0
* @category HTML attributes
*/
pnAriaLabel;
/**
* Provide the label from another element via its ID.
* We strongly recommend you use the `label` prop instead.
* @since v7.25.0
* @category HTML attributes
*/
pnAriaLabelledby;
/** Select HTML id @deprecated Use `pn-id` instead. @category HTML attributes */
selectId;
handleOpen() {
if (this.open) {
this.addGlobalEventListeners();
this.handleDirection();
this.handleMaxHeight();
this.handleOffset();
}
else {
this.removeGlobalEventListeners();
}
this.dropdownHandler();
}
handleSearch() {
if (!this.search)
this.options = [
...this.options.map(option => {
const item = {
...option,
hide: false,
};
if (item?.group?.length)
item.group.map(opt => ({ ...opt, hide: false }));
return item;
}),
];
}
handleSearchQuery() {
if (this.search && typeof this.searchQuery === 'string')
this.handleInputSearch();
}
handleSelectId() {
const id = this.getId();
this.idLegend = `${id}-legend`;
this.idButton = `${id}-button`;
this.idOptions = `${id}-options`;
this.idHelper = `${id}-text`;
this.idError = `${id}-error`;
this.idChips = `${id}-chips`;
this.idSr = `${id}-sr`;
}
/**
* If the select is open and you resize the window.
* Remove all css props and disable the animations entierly.
**/
handleResize() {
if (!this.open)
return;
this.toggleOpen(false);
this.elGroup.style.removeProperty('--pn-select-max-height');
this.elGroup.style.removeProperty('--pn-select-options-offset');
}
/** Dispatched everytime the multiselect is opened or closed. */
toggleSelect;
/** This event contains the entire options array with the new props. Dispatched everytime you make changes to any option. */
allOptions;
/** Dispatched when you toggle an option. Includes all the props of the option. */
selectedOption;
/**
* This event is dispatched when the user toggles the "Choose all options" box.
* Also triggers when you click the "Select {number} options" if you are performing a search at the same time.
**/
selectedAllOptions;
/**
* Contains the search text and the options found for that query.
* Dispacthed everytime you change the search query.
**/
searchInput;
async componentWillLoad() {
if (this.language === undefined)
await awaitTopbar(this.hostElement);
}
componentDidLoad() {
if (!!this.searchQuery?.length)
this.handleSearchQuery();
}
getId() {
return this.pnId || this.selectId || this.id;
}
hasHelperText() {
return this.helpertext?.length > 0 || this.checkSlottedHelper();
}
/** If any `error` text is present, either via prop/slot. */
hasErrorMessage() {
return this.error?.length > 0 || this.checkSlottedError();
}
/** If any `error` is active, either via the prop `invalid` or `error` prop/slot. */
hasError() {
return this.hasErrorMessage() || this.invalid;
}
checkSlottedHelper() {
return !!this.hostElement.querySelector('[slot=helpertext]');
}
checkSlottedError() {
return !!this.hostElement.querySelector('[slot=error]');
}
hideHelpertext() {
return this.hasErrorMessage() || !this.hasHelperText();
}
hideError() {
return !this.hasErrorMessage();
}
globalEvents = (event) => {
const target = event.target;
if (!this.hostElement.contains(target) || event?.key === 'Escape') {
this.toggleOpen(false);
}
};
addGlobalEventListeners() {
const root = this.hostElement.getRootNode();
root.addEventListener('click', this.globalEvents);
}
removeGlobalEventListeners() {
const root = this.hostElement.getRootNode();
root.removeEventListener('click', this.globalEvents);
}
dropdownHandler() {
if (this.open)
this.openDropdown(this.elList);
else
this.closeDropdown(this.elList);
}
emitEvents(option, all) {
this.allOptions.emit(this.options);
if (option)
return this.selectedOption.emit(option);
const data = {
checked: all,
searching: this.isSearching,
};
if (this.search && this.optionsTotal() !== this.optionsSearch().length) {
data.query = this.searchQuery;
data.found = this.optionsSearch();
}
this.selectedAllOptions.emit(data);
}
optionsChecked() {
const list = this.options?.reduce((sum, item) => {
const subgroup = item.group ? [...item.group.filter(({ checked }) => checked)] : [];
if (item.checked)
sum.push(item);
if (subgroup?.length)
sum.push(...subgroup);
return sum;
}, []);
return list;
}
optionsTotal() {
return this.options?.reduce((sum, item) => sum + (item?.group?.length ? item.group.length + 1 : 1), 0);
}
optionsIndex(val) {
const findNested = ({ group = [] }) => group?.findIndex(({ value }) => value === val);
const indexGroup = this.options.findIndex(option => option.value === val || findNested(option) !== -1);
const indexNested = findNested(this.options?.[indexGroup]);
return {
indexGroup,
indexNested,
};
}
optionsCheckedTotal() {
return this.optionsChecked()?.length;
}
optionsCheckedPreview() {
return this.optionsChecked()?.slice(0, this.itemCount);
}
optionsCheckedLabels() {
return this.optionsChecked()
?.map(({ label }) => label)
?.join(', ')
?.trim();
}
optionsSearch() {
const list = this.options?.reduce((sum, item) => {
const subgroup = item.group ? [...item.group.filter(({ hide }) => !hide)] : [];
if (!item.hide)
sum.push(item);
if (subgroup?.length)
sum.push(...subgroup);
return sum;
}, []);
return list;
}
noResults() {
return this.optionsSearch()?.length === 0;
}
isIndeterminate(groupIndex) {
const option = this.options?.[groupIndex];
if (option?.group?.length) {
const all = option.group.every(({ checked }) => checked);
const none = option.group.every(({ checked }) => !checked);
return !(all || none);
}
return false;
}
optionSelect({ val, checked, chip }) {
if (val === this.allValue)
return this.optionSelectAll(checked);
const options = this.options;
const { indexGroup, indexNested } = this.optionsIndex(val);
const group = options[indexGroup]?.group;
const nested = group?.[indexNested];
if (nested?.value) {
options[indexGroup].group[indexNested].checked = checked;
const allSiblingsChecked = group?.every(({ checked }) => checked);
if (allSiblingsChecked)
options[indexGroup].checked = true;
else
options[indexGroup].checked = false;
}
else {
options[indexGroup].checked = checked;
if (group?.length)
options[indexGroup].group = group?.map(opt => ({ ...opt, checked: checked }));
}
const option = indexNested === -1 ? options[indexGroup] : options[indexGroup].group[indexNested];
this.emitEvents(option);
this.options = [...options];
if (typeof chip === 'number') {
this.handleSrMesssage(`${this.translate('REMOVED')} ${option.label}`);
requestAnimationFrame(() => {
const index = chip === this.optionsCheckedTotal() ? chip - 1 : chip;
const btn = Array.from(this.elChips.children)?.[index];
btn?.querySelector('button').focus({ preventScroll: true });
});
}
}
optionSelectAll(checked) {
this.options = [
...this.options.map(option => {
const opt = {
...option,
checked: this.isSearching ? (option.hide ? option.checked : checked) : checked,
};
if (opt.group)
opt.group = [
...opt.group.map(item => ({
...item,
checked: this.isSearching ? (item.hide ? item.checked : checked) : checked,
})),
];
const allChecked = opt?.group?.every(({ checked }) => checked);
if (opt?.group?.length)
opt.checked = allChecked;
return opt;
}),
];
this.emitEvents(null, checked);
}
optionSelected(val) {
return !!this.optionsChecked()?.find(({ value, checked }) => value === val && checked);
}
translate(prop) {
const text = translations?.[prop]?.[this.language || en];
if (text.includes('{number}'))
return text.replace('{number}', this.optionsSearch().length);
return text;
}
getRect(element) {
return element?.getBoundingClientRect();
}
ripple({ clientX, clientY, target }) {
const isKeyboard = clientX === 0 && clientY === 0;
const element = target.nextElementSibling;
const { x, width, y, top } = this.getRect(element);
const clientCor = isKeyboard ? { clientX: x + width - 24, clientY: y - top } : { clientX, clientY };
ripple(clientCor, element);
}
handleSrMesssage(text) {
this.srMessage = text;
clearTimeout(this.srTimer);
this.srTimer = setTimeout(() => (this.srMessage = null), 2000);
}
handleDirection() {
if (this.top)
return (this.upwards = true);
if (this.bottom)
return (this.upwards = false);
const { bottom, top } = this.getRect(this.elInput);
const oneEm = 16;
/*
* Take the inner window height and subtract the elements bottom and 1em (16px)
* If the max height of this calculation is less than the space above the element, open it upwards.
* Do another calculation with the top value - 1em.
*/
const offsetTop = window.innerHeight - bottom - oneEm;
const offsetBottom = top - oneEm;
/** Always point downwards, unless the space to the bottom is less than half of the top offset space. */
this.upwards = offsetBottom / 2 > offsetTop;
}
handleMaxHeight() {
if (this.maxHeight)
return this.elGroup.style.setProperty('--pn-select-max-height', this.maxHeight);
const { bottom, top } = this.getRect(this.elInput);
const inputHeight = this.upwards ? top : bottom;
const offsetTop = getTotalHeightOffset();
const offset = this.upwards ? offsetTop + 16 : 16;
const maxHeight = this.upwards ? inputHeight - offset : window.innerHeight - inputHeight - offset;
this.elGroup.style.setProperty('--pn-select-max-height', `${Math.ceil(maxHeight)}px`);
}
handleOffset() {
this.elGroup.style.removeProperty('--pn-select-options-offset');
requestAnimationFrame(() => {
const { left, right } = this.getRect(this.elList);
const menuWidth = getMenuWidth();
const leftBoundary = menuWidth + 8;
const rightBoundary = window.innerWidth - 8;
let offset = 0;
if (left < leftBoundary)
offset = leftBoundary - left;
else if (right > rightBoundary)
offset = rightBoundary - right;
if (offset !== 0)
this.elGroup.style.setProperty('--pn-select-options-offset', `${Math.floor(offset)}px`);
});
}
setFocus() {
if (this.search)
this.elInput.focus({ preventScroll: true });
else
this.elButton.focus({ preventScroll: true });
}
toggleOpen(state) {
if (typeof state === 'boolean' && state === this.open)
return;
this.open = state ?? !this.open;
this.toggleSelect.emit({ open: this.open });
}
handleInputKeyboard(event) {
event.stopImmediatePropagation();
const { code } = event;
if (code === 'Escape')
return this.toggleOpen(false);
if (!this.open && code.match(/^(Space|Enter)$|Arrow|^Key.*$/))
this.toggleOpen();
}
setSearchQuery(value) {
if (!this.open)
this.toggleOpen(true);
this.searchQuery = value;
}
handleInputSearch() {
if (this.isEmpty())
return;
const term = this.searchQuery;
const performSearch = ({ label, helpertext, value }) => {
return `${label} ${helpertext} ${value}`.trim().toLowerCase().includes(term.toLowerCase());
};
const options = this.options.map(option => {
const foundNested = option?.group?.map(item => ({ ...item, hide: !performSearch(item) })) || [];
const found = performSearch(option) || foundNested?.some(({ hide }) => !hide);
option.hide = !found;
if (option?.group?.length)
option.group = [...foundNested];
return option;
});
this.isSearching = term !== '';
this.options = [...options];
this.searchInput.emit({ query: term, found: this.optionsSearch() });
}
getListItem(index) {
const { value } = this.options[index];
return this.elList.querySelector(`.pn-multiselect-option-input[value='${value}']`);
}
getOptionIndex(val, triggers, code) {
const total = this.optionsTotal() - 1;
if (code === 'End')
return total;
if (code === 'Home')
return 0;
const valIndex = this.options.findIndex(({ value }) => value === val);
const count = triggers.find(item => typeof item === 'number');
const index = valIndex + count;
if (index >= total)
return total;
if (index <= 0)
return 0;
return index;
}
checkboxNav(e, val) {
const { code } = e;
const arrowUp = code === 'ArrowUp' && -1;
const arrowDown = code === 'ArrowDown' && 1;
const pageUp = code === 'PageUp' && -10;
const pageDown = code === 'PageDown' && 10;
const home = code === 'Home' && 0;
const end = code === 'End' && this.optionsTotal() - 1;
const tab = code === 'Tab';
const space = code === 'Space';
const enter = code === 'Enter';
const escape = code === 'Escape';
const triggers = [arrowUp, arrowDown, pageUp, pageDown, home, end, tab, space, enter, escape];
if (!triggers.some(item => typeof item === 'number' || item))
return;
if (tab || space || enter)
return;
e.stopImmediatePropagation();
e.preventDefault();
if (escape) {
this.setFocus();
return this.toggleOpen(false);
}
const item = this.getOptionIndex(val, triggers, code);
return this.getListItem(item)?.focus();
}
handleBlur(event) {
const target = event.target;
const related = event.relatedTarget;
const jumpingToChips = (related || target)?.className === 'pn-multiselect-chip-button';
if (jumpingToChips)
this.toggleOpen(false);
}
handleLabel() {
if (this.disabled)
return;
this.setFocus();
}
/** Check if there are no options in the list. */
isEmpty() {
return (this.options?.length || 0) === 0;
}
showSelectAll() {
return this.selectAll && !this.isEmpty() && !this.noResults();
}
/** Check if the empty/nothing found should be visible. */
showEmptyOption() {
return (this.isSearching && this.noResults()) || this.isEmpty();
}
getPlaceholder() {
const autoTranslation = this.search ? 'SEARCH' : 'SELECT_AN_OPTION';
return this.placeholder || this.translate(autoTranslation);
}
handleChange({ target }) {
const { value, checked } = target;
this.optionSelect({ val: value, checked });
}
/** Display the "X more selected options" text. */
additonalOptions() {
const count = this.optionsCheckedTotal() - this.itemCount;
const single = `MORE_OPTION${count === 1 ? '' : 'S'}`;
return `${count} ${this.translate(single)}`;
}
displayLabel() {
return !!this.label;
}
getAriaLabel() {
return !this.displayLabel() && !this.pnAriaLabelledby ? this.pnAriaLabel : null;
}
/**
* If you use a label (technically a legend), use the `idLegend` to point to it.
* If not, make sure there is no pnAriaLabel and use pnAriaLabelledby.
* This is to make sure there are no double label/labelledby.
*/
getAriaLabelledby() {
return this.displayLabel() ? this.idLegend : !this.pnAriaLabel ? this.pnAriaLabelledby : null;
}
describedBy() {
const list = [];
if (this.search)
list.push(this.idChips);
if (this.srMessage)
list.push(this.idSr);
if (this.hasHelperText() && !this.hasErrorMessage())
list.push(this.idHelper);
if (this.hasErrorMessage())
list.push(this.idError);
if (list.length)
return list.join(' ');
return null;
}
/**
* This is to prevent a rare issue with rendering flag icons.
* Flags use a set ID to identify gradient colors. If the icon is rendered while the dropdown is closed,
* the ID is not unique and the icon will not be rendered for other pn-icons using it.
* This is a temp fix to address the root issue which is how the flags SVG styling is built.
*/
renderItemIcon(icon, hide) {
return (this.open || this.isMoving()) && !!icon && !hide;
}
renderOption({ label, value, checked, helpertext, icon, id = `${this.getId()}-${value || label}`, invalid, disabled, hide, group, }, indexGroup, indexNested) {
return (h("li", { class: "pn-multiselect-option", key: id, hidden: hide }, h("input", { type: "checkbox", id: id, class: "pn-multiselect-option-input", checked: checked ?? this.optionSelected(value), indeterminate: indexNested === undefined && this.isIndeterminate(indexGroup), name: this.name, value: value, disabled: this.disabled || disabled, required: this.required, "aria-invalid": invalid ? 'true' : null, "aria-describedby": helpertext ? `${id}-helper` : null, onClick: (event) => this.ripple(event), onKeyDown: (event) => this.checkboxNav(event, value), onBlur: (event) => this.handleBlur(event) }), h("div", { class: "pn-multiselect-option-content" }, this.renderItemIcon(icon, hide) && h("pn-icon", { icon: icon, color: "blue900" }), h("div", { class: "pn-multiselect-option-text" }, h("label", { class: "pn-multiselect-option-label", htmlFor: id }, h("span", null, label)), helpertext && (h("p", { class: "pn-multiselect-option-helper", id: `${id}-helper` }, helpertext))), h("div", { class: "pn-multiselect-option-checkbox" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none" }, h("polyline", { class: "pn-multiselect-option-checkbox-checkmark-path", points: "4,12 9,17 20,6", "stroke-width": "3" }), h("polyline", { class: "pn-multiselect-option-checkbox-indeterminate-path", points: "4,12 20,12", "stroke-width": "3" })))), group?.length && (h("ul", { class: "pn-multiselect-optgroup" }, group?.map((option, i) => this.renderOption(option, indexGroup, i))))));
}
renderOptions() {
return (h("ul", { id: this.idOptions, class: "pn-multiselect-options", "data-open": this.open, "data-upwards": this.upwards, onChange: (event) => this.handleChange(event), style: { height: '0px' }, ref: el => (this.elList = el) }, this.showSelectAll() &&
this.renderOption({
label: this.translate(`SELECT_${this.isSearching ? 'FOUND' : 'ALL'}_OPTIONS`),
value: this.allValue,
checked: this.optionsTotal() === this.optionsCheckedTotal(),
}), this.options?.map((option, index) => this.renderOption(option, index)), this.showEmptyOption() && (h("li", { class: "pn-multiselect-option" }, h("div", { class: "pn-multiselect-option-content", role: "alert" }, h("div", { class: "pn-multiselect-option-text" }, h("span", { class: "pn-multiselect-option-label" }, this.translate(this.isEmpty() ? 'NO_OPTIONS' : 'NO_SEARCH_RESULTS'))))))));
}
renderChips() {
return (h("ul", { id: this.idChips, class: "pn-multiselect-chips", "aria-label": this.translate('SELECTED_OPTIONS'), ref: el => (this.elChips = el) }, this.optionsCheckedPreview()?.map(({ label, value, icon }, index) => (h("li", { class: "pn-multiselect-chip", "aria-setsize": this.optionsCheckedTotal(), "aria-posinset": index + 1, key: `${label}-${value}` }, !!icon && h("pn-icon", { icon: icon, small: true }), h("span", { class: "pn-multiselect-chip-label" }, label), h("button", { type: "button", class: "pn-multiselect-chip-button", "aria-label": `${this.translate('REMOVE')} ${label}`, onClick: () => this.optionSelect({ val: value, checked: false, chip: index }) }, h("pn-icon", { icon: close, small: true, color: "blue700" }))))), this.optionsCheckedTotal() > this.itemCount && (h("li", { class: "pn-multiselect-chip", "data-count": true }, h("span", { class: "pn-multiselect-chip-label" }, "+ ", this.additonalOptions())))));
}
renderLabel(compactLabel = false) {
if (!this.label)
return null;
const Tag = compactLabel ? 'div' : 'legend';
const id = compactLabel ? null : this.idLegend;
const attrs = compactLabel ? { inert: true } : { onClick: () => this.handleLabel() };
return (h(Tag, { id: id, class: { 'pn-multiselect-label': true, 'pn-multiselect-sr-only': this.compact && !compactLabel }, "data-compact": this.compact, ...attrs }, h("span", null, this.label), !!this.optionsChecked()?.length && (h("span", null, this.optionsCheckedTotal(), "/", this.optionsTotal()))));
}
render() {
return (h(Host, { key: '326b1351c1600920a89b49a4ca04fb48af112bb7' }, h("fieldset", { key: '069b0689d499ad99b65f29597cc3968a47a3fc86', class: "pn-multiselect", "data-icon": !!this.icon, "data-error": this.hasError(), disabled: this.disabled }, this.renderLabel(), h("div", { key: '0a96f922421c6a1adb664714ec0d593cfdf950e5', class: "pn-multiselect-group", ref: el => (this.elGroup = el) }, h("div", { key: '3c6cadb7ff3dbc67fb8bab786133d64ad330c5d7', class: "pn-multiselect-input" }, !!this.icon && h("pn-icon", { key: '20bb5b6b273bea94903648a3465e14cf8dad2f77', class: "pn-multiselect-icon", "data-custom": true, icon: this.icon }), h("input", { key: '905179f50885d27bc7f41df667be93731dd5e4be', tabindex: this.search ? null : '-1', type: this.search ? 'search' : 'input', id: this.getId(), class: "pn-multiselect-element", value: this.search ? this.searchQuery : this.optionsCheckedLabels(), name: this.selectName, placeholder: this.getPlaceholder(), required: this.search ? null : this.required, "aria-label": this.getAriaLabel(), "aria-labelledby": this.getAriaLabelledby(), "aria-describedby": this.describedBy(), "aria-controls": `${this.idOptions} ${this.search ? this.idChips : ''}`, "aria-invalid": this.hasError()?.toString(), disabled: this.disabled, readonly: !this.search, "data-compact": this.compact, onClick: () => (this.search && this.open ? null : this.toggleOpen()), onKeyDown: e => this.handleInputKeyboard(e), onBlur: e => this.handleBlur(e), onInput: e => this.search && this.setSearchQuery(e.target.value), ref: el => (this.elInput = el) }), this.compact && this.renderLabel(true), this.hasError() && (h("pn-icon", { key: '635d97c6b8f780e6bd6daab225a97d1057b4bf0d', class: "pn-multiselect-icon", "data-error": true, icon: alert_exclamation_circle, color: "warning" })), h("button", { key: '77e9862561cd9ed80c5aa1b386ae772476a34e76', id: this.idButton, type: "button", class: "pn-multiselect-button", "aria-label": this.translate(`BUTTON_${this.open ? 'CLOSE' : 'OPEN'}`), "aria-describedby": this.search ? null : this.getId(), "aria-controls": this.idOptions, "aria-expanded": this.open.toString(), onClick: () => this.toggleOpen(), ref: el => (this.elButton = el) }, h("pn-icon", { key: '28092398e64c04af103142d8f459579eb2b70a1b', class: "pn-multiselect-icon", icon: chevron_down, color: "blue700" }))), this.renderOptions()), h("p", { key: '17d396f1b3175b502ce8d54c6ec059588bbe8128', id: this.idHelper, class: "pn-multiselect-text", hidden: this.hideHelpertext() }, h("span", { key: '693ba0a687b9b9766249e6703d0252beeeb762e9' }, this.helpertext), h("slot", { key: 'a70b1dff00b4ec936461f73354ebe18ce1a0a0c1', name: "helpertext" })), h("p", { key: '94c44db96a270fd7d3180c6dfddb53a46e85c2aa', id: this.idError, class: "pn-multiselect-text", role: "alert", hidden: this.hideError() }, h("span", { key: '0297ed1e18f374c0c3f0909a1ee5685b75c73a7c' }, this.error), h("slot", { key: '9c545b960d440978e46858b69ac86ad0112510c7', name: "error" })), this.search && this.renderChips(), h("slot", { key: '9551563c6a72fa6e06ce473eadbc73a0428b551e' }), this.search && (h("p", { key: '96863325f57e613f0272d652e05a70e1c5c35dde', id: this.idSr, class: "pn-multiselect-sr-only", role: "alert", "aria-live": "assertive" }, this.srMessage && h("span", { key: '21b84e0522ca16702a9ccabb071d30c0317631ef' }, this.srMessage))))));
}
static get is() { return "pn-multiselect"; }
static get originalStyleUrls() {
return {
"$": ["pn-multiselect.scss"]
};
}
static get styleUrls() {
return {
"$": ["pn-multiselect.css"]
};
}
static get properties() {
return {
"label": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Label placed above the select"
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "label"
},
"helpertext": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Display a helper text underneath the select"
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "helpertext"
},
"language": {
"type": "string",
"mutable": false,
"complexType": {
"original": "PnLanguages",
"resolved": "\"\" | \"da\" | \"en\" | \"fi\" | \"no\" | \"sv\"",
"references": {
"PnLanguages": {
"location": "import",
"path": "@/index",
"id": "src/index.ts::PnLanguages",
"referenceLocation": "PnLanguages"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Manually set the language, not needed if you have the pnTopbar available"
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "language",
"defaultValue": "undefined"
},
"name": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Native attributes"
}],
"text": "HTML name of the checkbox elements. Used for each checkbox inside the multiselect."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "name"
},
"selectName": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Native attributes"
}],
"text": "HTML name of the input element."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "select-name"
},
"placeholder": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Native attributes"
}],
"text": "This is what will be shown on load if no value is used.\nThe `placeholder` will override the default text used if you have the `search` prop active."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "placeholder"
},
"required": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Native attributes"
}],
"text": "Set the select as required."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "required",
"defaultValue": "false"
},
"disabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Native attributes"
}],
"text": "Disable the select."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "disabled",
"defaultValue": "false"
},
"options": {
"type": "unknown",
"mutable": true,
"complexType": {
"original": "PnMultiselectOption[]",
"resolved": "PnMultiselectOption[]",
"references": {
"PnMultiselectOption": {
"location": "import",
"path": "@/index",
"id": "src/index.ts::PnMultiselectOption",
"referenceLocation": "PnMultiselectOption"
}
}
},
"required": true,
"optional": false,
"docs": {
"tags": [{
"name": "see",
"text": "{@link PnMultiselectOption } for the option object properties."
}, {
"name": "category",
"text": "Features"
}],
"text": "The array of `PnMultiselectOption` objects."
},
"getter": false,
"setter": false
},
"compact": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "since",
"text": "v7.21.0"
}, {
"name": "category",
"text": "Features"
}],
"text": "Set the date picker label as compact. If used, the `placeholder` will no longer be displayed."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "compact",
"defaultValue": "false"
},
"icon": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Features"
}],
"text": "Display an icon to the left of the select input"
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "icon"
},
"selectAll": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "see",
"text": "{@link search }"
}, {
"name": "category",
"text": "Features"
}],
"text": "Adds a \"Select all\" option into the list.\nIf you use the search feature at the same time, clicking this option will only toggle the options found."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "select-all",
"defaultValue": "false"
},
"allValue": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "see",
"text": "{@link selectAll }"
}, {
"name": "category",
"text": "Features"
}],
"text": "Set a custom value for the \"Select all value\" option."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "all-value",
"defaultValue": "'pn_multiselect_all'"
},
"search": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Features"
}],
"text": "Allow the user to search among the options.\nThe selected options will now display underneath the multiselect element."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "search",
"defaultValue": "false"
},
"searchQuery": {
"type": "string",
"mutable": true,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "see",
"text": "{@link search }"
}, {
"name": "category",
"text": "Features"
}],
"text": "Set the search query of the multiselect."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "search-query",
"defaultValue": "''"
},
"itemCount": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "see",
"text": "{@link search }"
}, {
"name": "category",
"text": "Features"
}],
"text": "Decide how many items should be shown before ellipsis. Requires the `search` prop to work."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "item-count",
"defaultValue": "5"
},
"maxHeight": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Features"
}],
"text": "The component will automatically set the max height of the dropdown list on its own.\nIt takes the `pn-topbar` into account and will open in the direction that fits best.\nUse this prop to override this behaviour and use a custom max-height."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "max-height"
},
"top": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Features"
}],
"text": "Force the dropdown to always open upwards."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "top",
"defaultValue": "false"
},
"bottom": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Features"
}],
"text": "Force the dropdown to always open downwards."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "bottom",
"defaultValue": "false"
},
"invalid": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Features"
}],
"text": "Trigger the invalid state."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "invalid",
"defaultValue": "false"
},
"error": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "category",
"text": "Features"
}],
"text": "Display an error message and trigger the invalid state."
},
"getter": false,
"setter": false,
"reflect": false,
"attribute": "error"
},
"pnId": {