@schukai/monster 4.150.8 → 4.151.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/source/components/content/style/viewer.pcss +46 -1
- package/source/components/content/stylesheet/viewer.mjs +1 -1
- package/source/components/content/viewer/html.mjs +2 -2
- package/source/components/content/viewer/message.mjs +2 -2
- package/source/components/content/viewer/sanitize-html.mjs +194 -0
- package/source/components/content/viewer.mjs +237 -192
- package/source/components/form/select.mjs +413 -53
- package/source/components/form/style/select.pcss +40 -1
- package/source/components/form/stylesheet/select.mjs +2 -2
- package/test/cases/components/content/viewer.mjs +250 -3
- package/test/cases/components/form/select.mjs +141 -0
|
@@ -313,6 +313,9 @@ const selectionVersionSymbol = Symbol("selectionVersion");
|
|
|
313
313
|
const closeOnSelectAutoSymbol = Symbol("closeOnSelectAuto");
|
|
314
314
|
const lastFilterValueSymbol = Symbol("lastFilterValue");
|
|
315
315
|
const filterInputVersionSymbol = Symbol("filterInputVersion");
|
|
316
|
+
const createOptionElementSymbol = Symbol("createOptionElement");
|
|
317
|
+
const createOptionCandidateSymbol = Symbol("createOptionCandidate");
|
|
318
|
+
const createOptionEventsAttachedSymbol = Symbol("createOptionEventsAttached");
|
|
316
319
|
|
|
317
320
|
/**
|
|
318
321
|
* @private
|
|
@@ -422,6 +425,7 @@ const SELECT_VIEWPORT_PADDING = 12;
|
|
|
422
425
|
* @fires monster-options-set this event is fired when the options are set
|
|
423
426
|
* @fires monster-selection-removed
|
|
424
427
|
* @fires monster-selection-cleared
|
|
428
|
+
* @fires monster-option-created
|
|
425
429
|
*/
|
|
426
430
|
class Select extends CustomControl {
|
|
427
431
|
/**
|
|
@@ -555,6 +559,7 @@ class Select extends CustomControl {
|
|
|
555
559
|
* @property {string} labels.select-an-option - Placeholder text when no selection has been made.
|
|
556
560
|
* @property {string} labels.no-options - Message displayed when no options are available from slots or fetch.
|
|
557
561
|
* @property {string} labels.no-options-found - Message displayed when the filter yields no matching options.
|
|
562
|
+
* @property {string} labels.create-option - Template for the create action. `${label}` and `${value}` are replaced with the candidate data.
|
|
558
563
|
* @property {string} labels.summary-text.zero - Pluralization template for zero selected entries (e.g., "No entries selected").
|
|
559
564
|
* @property {string} labels.summary-text.one - Pluralization template for one selected entry.
|
|
560
565
|
* @property {string} labels.summary-text.other - Pluralization template for multiple selected entries.
|
|
@@ -567,6 +572,7 @@ class Select extends CustomControl {
|
|
|
567
572
|
* @property {boolean} features.storeFetchedData - If `true`, the raw fetched data is stored and can be retrieved via `getLastFetchedData()`.
|
|
568
573
|
* @property {boolean} features.useStrictValueComparison - Uses strict comparison (`===`) when matching option values.
|
|
569
574
|
* @property {boolean} features.showRemoteInfo - When `filter.mode === "remote"`, displays an info badge indicating that more options may exist on the server.
|
|
575
|
+
* @property {boolean} features.createOption - Shows an action for creating and selecting a missing filtered option.
|
|
570
576
|
* @property {Object} remoteInfo - Configuration for the remote info display.
|
|
571
577
|
* @property {string|null} remoteInfo.url - URL to fetch the total count of remote options (used when `filter.mode === "remote"`).
|
|
572
578
|
* @property {Object} placeholder - Placeholder texts for input fields.
|
|
@@ -601,12 +607,14 @@ class Select extends CustomControl {
|
|
|
601
607
|
* @property {Array} empty.equivalents - Values that are considered "empty" (e.g., `undefined`, `null`, `""`, `NaN`) and are normalized to the default value. Matching is type-safe: a configured string `"0"` only matches the string `"0"`; configure numeric `0` explicitly, via `data-monster-options` JSON, `data-monster-properties`, or `setOption()`, when the number `0` should be treated as empty.
|
|
602
608
|
* @property {Object} formatter - Functions for formatting display values.
|
|
603
609
|
* @property {Function} formatter.selection - Callback `(value, option) => string` to format the display text of selected values.
|
|
610
|
+
* @property {Function} formatter.createOption - Callback `(filterValue) => {label, value}|null` used to build a missing option. Return `null` to suppress creation.
|
|
604
611
|
* @property {Object} classes - CSS classes for various elements.
|
|
605
612
|
* @property {string} classes.badge - CSS class for selection badges.
|
|
606
613
|
* @property {string} classes.badgeUnresolved - CSS class used for selection badges whose value could not be hydrated. By default this swaps the badge into a warning style while keeping the raw key visible.
|
|
607
614
|
* @property {string} classes.statusOrRemoveBadge - CSS class for the status or remove badge.
|
|
608
615
|
* @property {string} classes.remoteInfo - CSS class for the remote info badge.
|
|
609
616
|
* @property {string} classes.noOptions - CSS class for the "no options" message.
|
|
617
|
+
* @property {string} classes.createOption - Badge class for the create-option action.
|
|
610
618
|
* @property {Object} messages - Internal messages for ARIA attributes and screen readers (should not normally be changed).
|
|
611
619
|
* @property {string|null} messages.control - Message for the main control element.
|
|
612
620
|
* @property {string|null} messages.selected - Message for the selected items area.
|
|
@@ -634,6 +642,7 @@ class Select extends CustomControl {
|
|
|
634
642
|
storeFetchedData: false,
|
|
635
643
|
useStrictValueComparison: false,
|
|
636
644
|
showRemoteInfo: true,
|
|
645
|
+
createOption: false,
|
|
637
646
|
},
|
|
638
647
|
|
|
639
648
|
placeholder: {
|
|
@@ -689,6 +698,7 @@ class Select extends CustomControl {
|
|
|
689
698
|
statusOrRemoveBadge: "empty",
|
|
690
699
|
remoteInfo: "monster-margin-start-4 monster-margin-top-4",
|
|
691
700
|
noOptions: "monster-margin-top-4 monster-margin-start-4",
|
|
701
|
+
createOption: "monster-badge-primary",
|
|
692
702
|
},
|
|
693
703
|
|
|
694
704
|
mapping: {
|
|
@@ -710,6 +720,7 @@ class Select extends CustomControl {
|
|
|
710
720
|
|
|
711
721
|
formatter: {
|
|
712
722
|
selection: buildSelectionLabel,
|
|
723
|
+
createOption: buildCreatedOption,
|
|
713
724
|
},
|
|
714
725
|
|
|
715
726
|
templates: {
|
|
@@ -1500,6 +1511,7 @@ function getTranslations() {
|
|
|
1500
1511
|
case "de":
|
|
1501
1512
|
return {
|
|
1502
1513
|
"cannot-be-loaded": "Kann nicht geladen werden",
|
|
1514
|
+
"create-option": "„${label}“ hinzufügen",
|
|
1503
1515
|
"no-options-available": "Keine Optionen verfügbar.",
|
|
1504
1516
|
"click-to-load-options": "Klicken, um Optionen zu laden.",
|
|
1505
1517
|
"select-an-option": "Option auswählen",
|
|
@@ -1957,6 +1969,7 @@ function getTranslations() {
|
|
|
1957
1969
|
// Fallback to English if locale.language is unrecognized
|
|
1958
1970
|
return {
|
|
1959
1971
|
"cannot-be-loaded": "Cannot be loaded",
|
|
1972
|
+
"create-option": "Add “${label}”",
|
|
1960
1973
|
"no-options-available": "No options available.",
|
|
1961
1974
|
"click-to-load-options": "Click to load options.",
|
|
1962
1975
|
"select-an-option": "Select an option",
|
|
@@ -2974,6 +2987,300 @@ function buildSelectionItem(value, preferredLabel) {
|
|
|
2974
2987
|
};
|
|
2975
2988
|
}
|
|
2976
2989
|
|
|
2990
|
+
/**
|
|
2991
|
+
* @private
|
|
2992
|
+
* @param {string} filterValue
|
|
2993
|
+
* @returns {{label: string, value: string}}
|
|
2994
|
+
*/
|
|
2995
|
+
function buildCreatedOption(filterValue) {
|
|
2996
|
+
return {
|
|
2997
|
+
label: filterValue,
|
|
2998
|
+
value: filterValue,
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
/**
|
|
3003
|
+
* @private
|
|
3004
|
+
* @returns {string}
|
|
3005
|
+
*/
|
|
3006
|
+
function getRawFilterInputValue() {
|
|
3007
|
+
const element =
|
|
3008
|
+
this.getOption("filter.position") === FILTER_POSITION_INLINE
|
|
3009
|
+
? this[inlineFilterElementSymbol]
|
|
3010
|
+
: this[popperFilterElementSymbol];
|
|
3011
|
+
|
|
3012
|
+
return element instanceof HTMLInputElement ? element.value.trim() : "";
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
/**
|
|
3016
|
+
* @private
|
|
3017
|
+
* @param {string} filterValue
|
|
3018
|
+
* @returns {{label: string, value: *}|null}
|
|
3019
|
+
*/
|
|
3020
|
+
function buildCreateOptionCandidate(filterValue) {
|
|
3021
|
+
const formatter = this.getOption("formatter.createOption");
|
|
3022
|
+
if (!isFunction(formatter)) {
|
|
3023
|
+
return null;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
const result = formatter.call(this, filterValue);
|
|
3027
|
+
if (result === null || result === undefined) {
|
|
3028
|
+
return null;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
const candidate = isObject(result)
|
|
3032
|
+
? { ...result }
|
|
3033
|
+
: { label: String(result), value: result };
|
|
3034
|
+
if (candidate.value === undefined) {
|
|
3035
|
+
candidate.value = candidate.label;
|
|
3036
|
+
}
|
|
3037
|
+
if (candidate.label === undefined) {
|
|
3038
|
+
candidate.label = getSelectionValueLabel.call(this, candidate.value);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
candidate.label = String(candidate.label).trim();
|
|
3042
|
+
if (candidate.label === "" || isValueIsEmpty.call(this, candidate.value)) {
|
|
3043
|
+
return null;
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
return candidate;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
/**
|
|
3050
|
+
* @private
|
|
3051
|
+
* @param {{label: string, value: *}} candidate
|
|
3052
|
+
* @param {string} filterValue
|
|
3053
|
+
* @returns {boolean}
|
|
3054
|
+
*/
|
|
3055
|
+
function hasEquivalentCreateOption(candidate, filterValue) {
|
|
3056
|
+
const normalizedFilter = filterValue.toLocaleLowerCase();
|
|
3057
|
+
const options = this.getOption("options", []);
|
|
3058
|
+
if (!isArray(options)) {
|
|
3059
|
+
return false;
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
return options.some((option) => {
|
|
3063
|
+
const value = isObject(option) ? option.value : option;
|
|
3064
|
+
const label = isObject(option) ? option.label : option;
|
|
3065
|
+
const normalizedLabel = String(label ?? "")
|
|
3066
|
+
.trim()
|
|
3067
|
+
.toLocaleLowerCase();
|
|
3068
|
+
|
|
3069
|
+
return (
|
|
3070
|
+
normalizedLabel === normalizedFilter ||
|
|
3071
|
+
areLookupValuesEqual.call(this, value, candidate.value)
|
|
3072
|
+
);
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
/**
|
|
3077
|
+
* @private
|
|
3078
|
+
* @returns {HTMLElement|null}
|
|
3079
|
+
*/
|
|
3080
|
+
function ensureCreateOptionElement() {
|
|
3081
|
+
if (!this.shadowRoot) {
|
|
3082
|
+
return null;
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
const element = this.shadowRoot.querySelector(
|
|
3086
|
+
`[${ATTRIBUTE_ROLE}="create-option"]`,
|
|
3087
|
+
);
|
|
3088
|
+
if (!(element instanceof HTMLElement)) {
|
|
3089
|
+
return null;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
if (this[createOptionEventsAttachedSymbol] !== true) {
|
|
3093
|
+
const activate = (event) => {
|
|
3094
|
+
if (this.getOption("disabled") === true) {
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
event.preventDefault();
|
|
3098
|
+
createFilteredOption.call(this).catch((error) => {
|
|
3099
|
+
addErrorAttribute(this, error);
|
|
3100
|
+
});
|
|
3101
|
+
};
|
|
3102
|
+
const types = new Set(["click", ...this.getOption("toggleEventType", [])]);
|
|
3103
|
+
for (const type of types) {
|
|
3104
|
+
element.addEventListener(type, activate);
|
|
3105
|
+
}
|
|
3106
|
+
this[createOptionEventsAttachedSymbol] = true;
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
this[createOptionElementSymbol] = element;
|
|
3110
|
+
return element;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
/**
|
|
3114
|
+
* @private
|
|
3115
|
+
*/
|
|
3116
|
+
function updateCreateOptionAction() {
|
|
3117
|
+
const element = ensureCreateOptionElement.call(this);
|
|
3118
|
+
if (!(element instanceof HTMLElement)) {
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
let candidate = null;
|
|
3123
|
+
const filterValue = getRawFilterInputValue.call(this);
|
|
3124
|
+
if (
|
|
3125
|
+
this.getOption("features.createOption") === true &&
|
|
3126
|
+
this.getOption("disabled") !== true &&
|
|
3127
|
+
getFilterMode.call(this) !== FILTER_MODE_DISABLED &&
|
|
3128
|
+
filterValue !== ""
|
|
3129
|
+
) {
|
|
3130
|
+
try {
|
|
3131
|
+
candidate = buildCreateOptionCandidate.call(this, filterValue);
|
|
3132
|
+
if (
|
|
3133
|
+
candidate &&
|
|
3134
|
+
hasEquivalentCreateOption.call(this, candidate, filterValue)
|
|
3135
|
+
) {
|
|
3136
|
+
candidate = null;
|
|
3137
|
+
}
|
|
3138
|
+
} catch (error) {
|
|
3139
|
+
addErrorAttribute(this, error);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
this[createOptionCandidateSymbol] = candidate;
|
|
3144
|
+
if (!candidate) {
|
|
3145
|
+
element.setAttribute(`${ATTRIBUTE_PREFIX}visibility`, "hidden");
|
|
3146
|
+
element.removeAttribute(`${ATTRIBUTE_PREFIX}focused`);
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
const template = this.getOption("labels.create-option", "Add “${label}”");
|
|
3151
|
+
const text = new Formatter({
|
|
3152
|
+
label: candidate.label,
|
|
3153
|
+
value: getSelectionValueLabel.call(this, candidate.value),
|
|
3154
|
+
}).format(template);
|
|
3155
|
+
const button = element.querySelector("button");
|
|
3156
|
+
const label = element.querySelector(
|
|
3157
|
+
`[${ATTRIBUTE_ROLE}="create-option-label"]`,
|
|
3158
|
+
);
|
|
3159
|
+
if (button instanceof HTMLButtonElement) {
|
|
3160
|
+
button.className = this.getOption(
|
|
3161
|
+
"classes.createOption",
|
|
3162
|
+
"monster-badge-primary",
|
|
3163
|
+
);
|
|
3164
|
+
button.setAttribute("aria-label", text);
|
|
3165
|
+
}
|
|
3166
|
+
element.setAttribute("aria-label", text);
|
|
3167
|
+
if (label instanceof HTMLElement) {
|
|
3168
|
+
label.textContent = text;
|
|
3169
|
+
}
|
|
3170
|
+
element.setAttribute(`${ATTRIBUTE_PREFIX}visibility`, "visible");
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
/**
|
|
3174
|
+
* Waits until the updater has projected a newly appended option.
|
|
3175
|
+
*
|
|
3176
|
+
* @private
|
|
3177
|
+
* @param {*} value
|
|
3178
|
+
* @returns {Promise<HTMLInputElement>}
|
|
3179
|
+
*/
|
|
3180
|
+
function waitForCreatedOptionControl(value) {
|
|
3181
|
+
return new Promise((resolve, reject) => {
|
|
3182
|
+
const startedAt = Date.now();
|
|
3183
|
+
const find = () => {
|
|
3184
|
+
for (const input of this[optionsElementSymbol].querySelectorAll(
|
|
3185
|
+
`[${ATTRIBUTE_ROLE}="option-control"]`,
|
|
3186
|
+
)) {
|
|
3187
|
+
if (
|
|
3188
|
+
input instanceof HTMLInputElement &&
|
|
3189
|
+
input.value === String(value)
|
|
3190
|
+
) {
|
|
3191
|
+
return input;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
return null;
|
|
3195
|
+
};
|
|
3196
|
+
const poll = () => {
|
|
3197
|
+
const input = find();
|
|
3198
|
+
if (input) {
|
|
3199
|
+
resolve(input);
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
if (Date.now() - startedAt >= 1000) {
|
|
3203
|
+
reject(new Error("Created option was not rendered."));
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
setTimeout(poll, 10);
|
|
3207
|
+
};
|
|
3208
|
+
poll();
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
/**
|
|
3213
|
+
* @private
|
|
3214
|
+
* @returns {Promise<boolean>}
|
|
3215
|
+
*/
|
|
3216
|
+
async function createFilteredOption() {
|
|
3217
|
+
updateCreateOptionAction.call(this);
|
|
3218
|
+
const candidate = this[createOptionCandidateSymbol];
|
|
3219
|
+
if (!candidate) {
|
|
3220
|
+
return false;
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
const options = this.getOption("options", []);
|
|
3224
|
+
const nextOptions = (isArray(options) ? options : []).map((option) => {
|
|
3225
|
+
return isObject(option) ? { ...option, filtered: undefined } : option;
|
|
3226
|
+
});
|
|
3227
|
+
nextOptions.push({
|
|
3228
|
+
...candidate,
|
|
3229
|
+
visibility: candidate.visibility ?? "visible",
|
|
3230
|
+
filtered: undefined,
|
|
3231
|
+
});
|
|
3232
|
+
|
|
3233
|
+
for (const input of [
|
|
3234
|
+
this[inlineFilterElementSymbol],
|
|
3235
|
+
this[popperFilterElementSymbol],
|
|
3236
|
+
]) {
|
|
3237
|
+
if (input instanceof HTMLInputElement) {
|
|
3238
|
+
input.value = "";
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
this[lastFilterValueSymbol] = "";
|
|
3242
|
+
bumpFilterInputVersion.call(this);
|
|
3243
|
+
this[createOptionCandidateSymbol] = null;
|
|
3244
|
+
this[createOptionElementSymbol]?.setAttribute(
|
|
3245
|
+
`${ATTRIBUTE_PREFIX}visibility`,
|
|
3246
|
+
"hidden",
|
|
3247
|
+
);
|
|
3248
|
+
|
|
3249
|
+
this.setOption("options", nextOptions);
|
|
3250
|
+
fireCustomEvent(this, "monster-options-set", { options: nextOptions });
|
|
3251
|
+
|
|
3252
|
+
const createdInput = await waitForCreatedOptionControl.call(
|
|
3253
|
+
this,
|
|
3254
|
+
candidate.value,
|
|
3255
|
+
);
|
|
3256
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
3257
|
+
const selection =
|
|
3258
|
+
this.getOption("type") === "checkbox"
|
|
3259
|
+
? [...this.getOption("selection", []), candidate]
|
|
3260
|
+
: [candidate];
|
|
3261
|
+
const selectedValues = new Set(
|
|
3262
|
+
selection.map((item) => String(isObject(item) ? item.value : item)),
|
|
3263
|
+
);
|
|
3264
|
+
for (const input of this[optionsElementSymbol].querySelectorAll(
|
|
3265
|
+
`[${ATTRIBUTE_ROLE}="option-control"]`,
|
|
3266
|
+
)) {
|
|
3267
|
+
if (input instanceof HTMLInputElement) {
|
|
3268
|
+
input.checked = selectedValues.has(input.value);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
createdInput.checked = true;
|
|
3272
|
+
await setSelection.call(this, selection);
|
|
3273
|
+
fireCustomEvent(this, "monster-option-created", { option: candidate });
|
|
3274
|
+
|
|
3275
|
+
if (this.getOption("features.closeOnSelect") === true) {
|
|
3276
|
+
hide.call(this);
|
|
3277
|
+
} else {
|
|
3278
|
+
focusFilter.call(this, { preventScroll: true });
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
return true;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
2977
3284
|
/**
|
|
2978
3285
|
* @private
|
|
2979
3286
|
* @param {string} value The value to look up.
|
|
@@ -3234,6 +3541,7 @@ function initOptionObserver() {
|
|
|
3234
3541
|
}
|
|
3235
3542
|
|
|
3236
3543
|
setSummaryAndControlText.call(self);
|
|
3544
|
+
updateCreateOptionAction.call(self);
|
|
3237
3545
|
}).run();
|
|
3238
3546
|
}),
|
|
3239
3547
|
);
|
|
@@ -3351,6 +3659,34 @@ function getOptionElements() {
|
|
|
3351
3659
|
);
|
|
3352
3660
|
}
|
|
3353
3661
|
|
|
3662
|
+
/**
|
|
3663
|
+
* @private
|
|
3664
|
+
* @returns {HTMLElement[]}
|
|
3665
|
+
*/
|
|
3666
|
+
function getNavigableOptionElements() {
|
|
3667
|
+
if (!(this[optionsElementSymbol] instanceof HTMLElement)) {
|
|
3668
|
+
return [];
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
const elements = Array.from(
|
|
3672
|
+
this[optionsElementSymbol].querySelectorAll(`[${ATTRIBUTE_ROLE}=option]`),
|
|
3673
|
+
);
|
|
3674
|
+
const createOption = this.shadowRoot?.querySelector?.(
|
|
3675
|
+
`[${ATTRIBUTE_ROLE}="create-option"]`,
|
|
3676
|
+
);
|
|
3677
|
+
if (createOption instanceof HTMLElement) {
|
|
3678
|
+
elements.push(createOption);
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
return elements.filter((element) => {
|
|
3682
|
+
return (
|
|
3683
|
+
element instanceof HTMLElement &&
|
|
3684
|
+
element.getAttribute(`${ATTRIBUTE_PREFIX}visibility`) === "visible" &&
|
|
3685
|
+
element.getAttribute(`${ATTRIBUTE_PREFIX}filtered`) !== "true"
|
|
3686
|
+
);
|
|
3687
|
+
});
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3354
3690
|
/**
|
|
3355
3691
|
* With the help of this filter callback, values can be filtered out. Only if the filter function returns true, the value is taken for the map.
|
|
3356
3692
|
*
|
|
@@ -3372,11 +3708,14 @@ function getOptionElements() {
|
|
|
3372
3708
|
* @private
|
|
3373
3709
|
*/
|
|
3374
3710
|
function calcAndSetOptionsDimension() {
|
|
3375
|
-
const options = getOptionElements.call(this);
|
|
3711
|
+
const options = Array.from(getOptionElements.call(this));
|
|
3712
|
+
const createOption = this.shadowRoot?.querySelector?.(
|
|
3713
|
+
`[${ATTRIBUTE_ROLE}="create-option"][${ATTRIBUTE_PREFIX}visibility="visible"]`,
|
|
3714
|
+
);
|
|
3376
3715
|
const container = this[optionsElementSymbol];
|
|
3377
3716
|
const content =
|
|
3378
3717
|
this[popperElementSymbol]?.querySelector?.('[part="content"]');
|
|
3379
|
-
if (!(container instanceof HTMLElement && options
|
|
3718
|
+
if (!(container instanceof HTMLElement && isArray(options))) {
|
|
3380
3719
|
return;
|
|
3381
3720
|
}
|
|
3382
3721
|
|
|
@@ -3395,7 +3734,7 @@ function calcAndSetOptionsDimension() {
|
|
|
3395
3734
|
visible++;
|
|
3396
3735
|
}
|
|
3397
3736
|
|
|
3398
|
-
if (visible === 0) {
|
|
3737
|
+
if (visible === 0 && !(createOption instanceof HTMLElement)) {
|
|
3399
3738
|
if (this.getOption("classes.statusOrRemoveBadge") === "error") {
|
|
3400
3739
|
this.setOption(
|
|
3401
3740
|
"messages.emptyOptions",
|
|
@@ -3442,11 +3781,21 @@ function calcAndSetOptionsDimension() {
|
|
|
3442
3781
|
geometry.availableHeight,
|
|
3443
3782
|
SELECT_MAX_POPPER_HEIGHT,
|
|
3444
3783
|
);
|
|
3445
|
-
|
|
3784
|
+
let listAvailableHeight = resolveSelectListAvailableHeight({
|
|
3446
3785
|
popperElement: this[popperElementSymbol],
|
|
3447
3786
|
contentElement: content,
|
|
3448
3787
|
popperMaxHeight,
|
|
3449
3788
|
});
|
|
3789
|
+
if (createOption instanceof HTMLElement) {
|
|
3790
|
+
const createStyle = getGlobal().getComputedStyle(createOption);
|
|
3791
|
+
listAvailableHeight = Math.max(
|
|
3792
|
+
0,
|
|
3793
|
+
listAvailableHeight -
|
|
3794
|
+
createOption.getBoundingClientRect().height -
|
|
3795
|
+
readSelectBoxDimension(createStyle.marginTop) -
|
|
3796
|
+
readSelectBoxDimension(createStyle.marginBottom),
|
|
3797
|
+
);
|
|
3798
|
+
}
|
|
3450
3799
|
const listDimension = resolveSelectListDimension({
|
|
3451
3800
|
visibleOptionHeights,
|
|
3452
3801
|
maxVisibleOptions: max,
|
|
@@ -3887,54 +4236,24 @@ function activateCurrentOption(direction) {
|
|
|
3887
4236
|
throw new Error("no shadow-root is defined");
|
|
3888
4237
|
}
|
|
3889
4238
|
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
focused = e;
|
|
3901
|
-
break;
|
|
3902
|
-
}
|
|
3903
|
-
}
|
|
4239
|
+
const options = getNavigableOptionElements.call(this);
|
|
4240
|
+
const current = this.shadowRoot.querySelector(`[${ATTRIBUTE_PREFIX}focused]`);
|
|
4241
|
+
const currentIndex = options.indexOf(current);
|
|
4242
|
+
let focused = null;
|
|
4243
|
+
if (currentIndex === -1) {
|
|
4244
|
+
focused = direction === FOCUS_DIRECTION_UP ? options.at(-1) : options[0];
|
|
4245
|
+
} else if (direction === FOCUS_DIRECTION_DOWN) {
|
|
4246
|
+
focused = options[currentIndex + 1] ?? current;
|
|
4247
|
+
} else if (currentIndex > 0) {
|
|
4248
|
+
focused = options[currentIndex - 1];
|
|
3904
4249
|
} else {
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
focused.getAttribute(ATTRIBUTE_ROLE) === "option" &&
|
|
3913
|
-
focused.matches("[data-monster-visibility=visible]") &&
|
|
3914
|
-
focused.matches(":not([data-monster-filtered=true])")
|
|
3915
|
-
) {
|
|
3916
|
-
break;
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
3919
|
-
} else {
|
|
3920
|
-
let found = false;
|
|
3921
|
-
while (focused.previousSibling) {
|
|
3922
|
-
focused = focused.previousSibling;
|
|
3923
|
-
if (
|
|
3924
|
-
focused instanceof HTMLElement &&
|
|
3925
|
-
focused.hasAttribute(ATTRIBUTE_ROLE) &&
|
|
3926
|
-
focused.getAttribute(ATTRIBUTE_ROLE) === "option" &&
|
|
3927
|
-
focused.matches("[data-monster-visibility=visible]") &&
|
|
3928
|
-
focused.matches(":not([data-monster-filtered=true])")
|
|
3929
|
-
) {
|
|
3930
|
-
found = true;
|
|
3931
|
-
break;
|
|
3932
|
-
}
|
|
3933
|
-
}
|
|
3934
|
-
if (found === false) {
|
|
3935
|
-
focusFilter.call(this, { preventScroll: true });
|
|
3936
|
-
}
|
|
3937
|
-
}
|
|
4250
|
+
this.shadowRoot
|
|
4251
|
+
.querySelectorAll(`[${ATTRIBUTE_PREFIX}focused]`)
|
|
4252
|
+
.forEach((element) => {
|
|
4253
|
+
element.removeAttribute(`${ATTRIBUTE_PREFIX}focused`);
|
|
4254
|
+
});
|
|
4255
|
+
focusFilter.call(this, { preventScroll: true });
|
|
4256
|
+
return;
|
|
3938
4257
|
}
|
|
3939
4258
|
|
|
3940
4259
|
new Processing(() => {
|
|
@@ -4017,6 +4336,15 @@ function handleFilterKeyboardEvents(event) {
|
|
|
4017
4336
|
const shiftKey = event?.["shiftKey"];
|
|
4018
4337
|
|
|
4019
4338
|
switch (event?.["code"]) {
|
|
4339
|
+
case "Enter":
|
|
4340
|
+
updateCreateOptionAction.call(this);
|
|
4341
|
+
if (this[createOptionCandidateSymbol]) {
|
|
4342
|
+
createFilteredOption.call(this).catch((error) => {
|
|
4343
|
+
addErrorAttribute(this, error);
|
|
4344
|
+
});
|
|
4345
|
+
event.preventDefault();
|
|
4346
|
+
}
|
|
4347
|
+
break;
|
|
4020
4348
|
case "Tab":
|
|
4021
4349
|
activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN);
|
|
4022
4350
|
event.preventDefault();
|
|
@@ -4053,6 +4381,7 @@ function handleFilterKeyboardEvents(event) {
|
|
|
4053
4381
|
* @return {void} This method does not return anything.
|
|
4054
4382
|
*/
|
|
4055
4383
|
function handleFilterInputEvents() {
|
|
4384
|
+
updateCreateOptionAction.call(this);
|
|
4056
4385
|
const filterValue = getCurrentFilterInputValue.call(this);
|
|
4057
4386
|
|
|
4058
4387
|
if (filterValue === this[lastFilterValueSymbol]) {
|
|
@@ -4339,6 +4668,24 @@ function handleOptionKeyboardEvents(event) {
|
|
|
4339
4668
|
}
|
|
4340
4669
|
}
|
|
4341
4670
|
|
|
4671
|
+
/**
|
|
4672
|
+
* @private
|
|
4673
|
+
* @param {KeyboardEvent} event
|
|
4674
|
+
*/
|
|
4675
|
+
function handleCreateOptionKeyboardEvents(event) {
|
|
4676
|
+
switch (event?.code) {
|
|
4677
|
+
case "Enter":
|
|
4678
|
+
case "Space":
|
|
4679
|
+
createFilteredOption.call(this).catch((error) => {
|
|
4680
|
+
addErrorAttribute(this, error);
|
|
4681
|
+
});
|
|
4682
|
+
event.preventDefault();
|
|
4683
|
+
break;
|
|
4684
|
+
default:
|
|
4685
|
+
handleOptionKeyboardEvents.call(this, event);
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4342
4689
|
/**
|
|
4343
4690
|
* @private
|
|
4344
4691
|
* @return {string}
|
|
@@ -5235,8 +5582,7 @@ function show() {
|
|
|
5235
5582
|
|
|
5236
5583
|
delete this[lazyLoadRequestSymbol];
|
|
5237
5584
|
this[lazyLoadDoneSymbol] = false;
|
|
5238
|
-
this[lazyLoadErrorSymbol] =
|
|
5239
|
-
e instanceof Error ? e.message : String(e);
|
|
5585
|
+
this[lazyLoadErrorSymbol] = e instanceof Error ? e.message : String(e);
|
|
5240
5586
|
addErrorAttribute(this, e);
|
|
5241
5587
|
setStatusOrRemoveBadges.call(this, "error");
|
|
5242
5588
|
});
|
|
@@ -5664,6 +6010,9 @@ function initEventHandler() {
|
|
|
5664
6010
|
case "filter":
|
|
5665
6011
|
handleFilterKeyboardEvents.call(self, event);
|
|
5666
6012
|
break;
|
|
6013
|
+
case "create-option":
|
|
6014
|
+
handleCreateOptionKeyboardEvents.call(self, event);
|
|
6015
|
+
break;
|
|
5667
6016
|
case "option-label":
|
|
5668
6017
|
case "option-control":
|
|
5669
6018
|
case "option":
|
|
@@ -5755,6 +6104,8 @@ function initEventHandler() {
|
|
|
5755
6104
|
self.addEventListener("keydown", self[keyEventHandler]);
|
|
5756
6105
|
|
|
5757
6106
|
const callback = () => {
|
|
6107
|
+
ensureCreateOptionElement.call(self);
|
|
6108
|
+
updateCreateOptionAction.call(self);
|
|
5758
6109
|
if (this[debounceOptionsMutationObserverSymbol] instanceof DeadMansSwitch) {
|
|
5759
6110
|
try {
|
|
5760
6111
|
this[debounceOptionsMutationObserverSymbol].touch();
|
|
@@ -5962,6 +6313,8 @@ function initControlReferences() {
|
|
|
5962
6313
|
this[optionsElementSymbol] = this.shadowRoot.querySelector(
|
|
5963
6314
|
`[${ATTRIBUTE_ROLE}=options]`,
|
|
5964
6315
|
);
|
|
6316
|
+
ensureCreateOptionElement.call(this);
|
|
6317
|
+
updateCreateOptionAction.call(this);
|
|
5965
6318
|
this[noOptionsAvailableElementSymbol] = this.shadowRoot.querySelector(
|
|
5966
6319
|
`[${ATTRIBUTE_ROLE}="no-options"]`,
|
|
5967
6320
|
);
|
|
@@ -6054,6 +6407,13 @@ function getTemplate() {
|
|
|
6054
6407
|
<div part="content" class="flex" data-monster-replace="path:content">
|
|
6055
6408
|
<div part="options" data-monster-role="options" data-monster-insert="options path:options"
|
|
6056
6409
|
tabindex="-1"></div>
|
|
6410
|
+
<div data-monster-role="create-option" role="option" aria-selected="false"
|
|
6411
|
+
data-monster-visibility="hidden" tabindex="-1">
|
|
6412
|
+
<button type="button" part="create-option" tabindex="-1">
|
|
6413
|
+
<span aria-hidden="true" data-monster-role="create-option-icon">+</span>
|
|
6414
|
+
<span data-monster-role="create-option-label"></span>
|
|
6415
|
+
</button>
|
|
6416
|
+
</div>
|
|
6057
6417
|
</div>
|
|
6058
6418
|
<div part="pagination-footer" data-monster-role="pagination-footer">
|
|
6059
6419
|
<div part="remote-info"
|
|
@@ -100,6 +100,10 @@ div[data-monster-role=popper] {
|
|
|
100
100
|
z-index: var(--monster-z-index-popover);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
div[data-monster-role=popper] > [part=content] {
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
}
|
|
106
|
+
|
|
103
107
|
@media (prefers-color-scheme: light) {
|
|
104
108
|
[data-monster-role=control] {
|
|
105
109
|
background-color: var(--monster-bg-color-primary-1);
|
|
@@ -357,9 +361,44 @@ div[data-monster-role=selection] {
|
|
|
357
361
|
outline-offset: -2px;
|
|
358
362
|
}
|
|
359
363
|
|
|
364
|
+
[data-monster-role=create-option] {
|
|
365
|
+
align-items: center;
|
|
366
|
+
box-sizing: border-box;
|
|
367
|
+
cursor: pointer;
|
|
368
|
+
display: flex;
|
|
369
|
+
min-block-size: 2.75rem;
|
|
370
|
+
padding: var(--monster-select-option-padding, 2px 6px);
|
|
371
|
+
width: 100%;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
[data-monster-role=create-option][data-monster-visibility=hidden] {
|
|
375
|
+
display: none;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
[data-monster-role=create-option] > button {
|
|
379
|
+
appearance: none;
|
|
380
|
+
border: 0;
|
|
381
|
+
cursor: inherit;
|
|
382
|
+
display: inline-flex;
|
|
383
|
+
font-family: inherit;
|
|
384
|
+
gap: 0.4em;
|
|
385
|
+
pointer-events: none;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
[data-monster-role=create-option][data-monster-focused=true] {
|
|
389
|
+
outline: 1px dashed var(--monster-color-selection-2);
|
|
390
|
+
outline-offset: -2px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
[data-monster-role=create-option-icon] {
|
|
394
|
+
font-size: 1.15em;
|
|
395
|
+
line-height: 0.75;
|
|
396
|
+
}
|
|
397
|
+
|
|
360
398
|
@media (prefers-color-scheme: dark) {
|
|
361
399
|
|
|
362
|
-
[data-monster-role=option][data-monster-focused=true]
|
|
400
|
+
[data-monster-role=option][data-monster-focused=true],
|
|
401
|
+
[data-monster-role=create-option][data-monster-focused=true] {
|
|
363
402
|
outline: 1px dashed var(--monster-color-selection-4);
|
|
364
403
|
}
|
|
365
404
|
}
|