@schukai/monster 4.136.5 → 4.136.6
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/CHANGELOG.md
CHANGED
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
- **popper:** guard show/hide/update flows against disconnected hosts and missing internal elements
|
|
18
18
|
- **message-state-button:** distinguish overlay, prose, and wide message layouts for smart popper sizing and overflow ([#401](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/401))
|
|
19
19
|
- **popper:** support kebab-case camelCase option attributes such as `data-monster-option-popper-content-overflow` ([#401](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/401))
|
|
20
|
+
- **popper/select:** let nested `monster-select` poppers escape parent popper content wrappers without shrinking normal parent content sizing ([#416](https://gitlab.schukai.com/oss/libraries/javascript/monster/-/work_items/416))
|
|
20
21
|
|
|
21
22
|
### Changes
|
|
22
23
|
|
|
23
24
|
- document lifecycle ownership rules for Updater-driven and stateful custom element implementations
|
|
24
25
|
- document smart message popper layout behavior for forms and feedback flows ([#401](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/401))
|
|
26
|
+
- document and add an issue repro for nested `monster-select` overflow behavior in `monster-popper-button` ([#416](https://gitlab.schukai.com/oss/libraries/javascript/monster/-/work_items/416))
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.136.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.136.6"}
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
openPositionedPopper,
|
|
72
72
|
positionPopper,
|
|
73
73
|
resolveClippingBoundaryElement,
|
|
74
|
+
resolveParentPopperContentBoundary,
|
|
74
75
|
} from "./util/floating-ui.mjs";
|
|
75
76
|
import { Pathfinder } from "../../data/pathfinder.mjs";
|
|
76
77
|
import { TokenList } from "../../types/tokenlist.mjs";
|
|
@@ -261,21 +262,17 @@ const remoteInfoElementSymbol = Symbol("remoteInfoElement");
|
|
|
261
262
|
const areOptionsAvailableAndInitSymbol = Symbol("@@areOptionsAvailableAndInit");
|
|
262
263
|
|
|
263
264
|
/**
|
|
265
|
+
* Internal sentinel used to suppress a remote request after URL formatting.
|
|
266
|
+
*
|
|
267
|
+
* This is currently inserted only when `filter.defaultValue` resolves to
|
|
268
|
+
* `undefined` or `null`. An empty string is treated as a real filter value and
|
|
269
|
+
* therefore still produces a request.
|
|
270
|
+
*
|
|
264
271
|
* @private
|
|
265
272
|
* @type {symbol}
|
|
266
273
|
*/
|
|
267
274
|
const disabledRequestMarker = Symbol("@@disabledRequestMarker");
|
|
268
275
|
|
|
269
|
-
/**
|
|
270
|
-
* @private
|
|
271
|
-
* @type {symbol}
|
|
272
|
-
*/
|
|
273
|
-
const runLookupOnceSymbol = Symbol("runLookupOnce");
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* @private
|
|
277
|
-
* @type {symbol}
|
|
278
|
-
*/
|
|
279
276
|
const cleanupOptionsListSymbol = Symbol("cleanupOptionsList");
|
|
280
277
|
const optionsVersionSymbol = Symbol("optionsVersion");
|
|
281
278
|
const pendingSelectionSymbol = Symbol("pendingSelection");
|
|
@@ -300,16 +297,6 @@ const debounceOptionsMutationObserverSymbol = Symbol(
|
|
|
300
297
|
*/
|
|
301
298
|
const currentPageSymbol = Symbol("currentPage");
|
|
302
299
|
|
|
303
|
-
/**
|
|
304
|
-
* @private
|
|
305
|
-
* @type {symbol}
|
|
306
|
-
*/
|
|
307
|
-
const remoteFilterFirstOpendSymbol = Symbol("remoteFilterFirstOpend");
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* @private
|
|
311
|
-
* @type {symbol}
|
|
312
|
-
*/
|
|
313
300
|
const lookupCacheSymbol = Symbol("lookupCache");
|
|
314
301
|
|
|
315
302
|
/**
|
|
@@ -317,6 +304,7 @@ const lookupCacheSymbol = Symbol("lookupCache");
|
|
|
317
304
|
* @type {symbol}
|
|
318
305
|
*/
|
|
319
306
|
const lookupInProgressSymbol = Symbol("lookupInProgress");
|
|
307
|
+
const unresolvedSelectionValuesSymbol = Symbol("unresolvedSelectionValues");
|
|
320
308
|
const fetchRequestVersionSymbol = Symbol("fetchRequestVersion");
|
|
321
309
|
|
|
322
310
|
/**
|
|
@@ -398,6 +386,7 @@ class Select extends CustomControl {
|
|
|
398
386
|
this[currentPageSymbol] = 1;
|
|
399
387
|
this[lookupCacheSymbol] = new Map();
|
|
400
388
|
this[lookupInProgressSymbol] = new Map();
|
|
389
|
+
this[unresolvedSelectionValuesSymbol] = new Set();
|
|
401
390
|
this[optionsMapSymbol] = new Map();
|
|
402
391
|
this[closeOnSelectAutoSymbol] = true;
|
|
403
392
|
initOptionObserver.call(this);
|
|
@@ -480,8 +469,8 @@ class Select extends CustomControl {
|
|
|
480
469
|
* @property {string} name - Name of the hidden form field for form submission.
|
|
481
470
|
* @property {string|null} url - URL to dynamically fetch options via HTTP when opening or filtering.
|
|
482
471
|
* @property {number|null} total - Total number of available options, useful for pagination with remote data.
|
|
483
|
-
* @property {Object} lookup - Configuration for
|
|
484
|
-
* @property {string|null} lookup.url - URL template with a `${filter}` placeholder to look up selected entries
|
|
472
|
+
* @property {Object} lookup - Configuration for hydrating already selected values.
|
|
473
|
+
* @property {string|null} lookup.url - URL template with a `${filter}` placeholder to look up selected entries. Prefer a stable value-based lookup endpoint. This is used for initial hydration and for resolving labels of externally assigned selections that are not yet present in the local option map.
|
|
485
474
|
* @property {boolean} lookup.grouping - If `true`, all selected values are fetched in a single request; otherwise, a separate request is sent for each value.
|
|
486
475
|
* @property {Object} fetch - Configuration for HTTP requests via `fetch`.
|
|
487
476
|
* @property {string} fetch.redirect - Fetch redirect mode (e.g., "error", "follow").
|
|
@@ -490,7 +479,7 @@ class Select extends CustomControl {
|
|
|
490
479
|
* @property {string} fetch.credentials - Credentials policy for fetch (e.g., "include", "same-origin").
|
|
491
480
|
* @property {Object.<string, string>} fetch.headers - HTTP headers to be sent with every request.
|
|
492
481
|
* @property {Object} labels - Text labels for various states and UI elements.
|
|
493
|
-
* @property {string} labels.cannot-be-loaded - Message displayed when options cannot be loaded.
|
|
482
|
+
* @property {string} labels.cannot-be-loaded - Message displayed when options cannot be loaded. Unresolved selected values keep their raw key instead of replacing it with this label.
|
|
494
483
|
* @property {string} labels.no-options-available - Message displayed when no static options are provided.
|
|
495
484
|
* @property {string} labels.click-to-load-options - Prompt to load options when `features.lazyLoad` is enabled.
|
|
496
485
|
* @property {string} labels.select-an-option - Placeholder text when no selection has been made.
|
|
@@ -513,10 +502,10 @@ class Select extends CustomControl {
|
|
|
513
502
|
* @property {Object} placeholder - Placeholder texts for input fields.
|
|
514
503
|
* @property {string} placeholder.filter - Placeholder text for the filter input field.
|
|
515
504
|
* @property {Object} filter - Configuration for the filtering functionality.
|
|
516
|
-
* @property {string|null} filter.defaultValue - Default filter value for remote requests. An empty string
|
|
505
|
+
* @property {string|null} filter.defaultValue - Default filter value for remote requests. In the current implementation only `undefined` or `null` suppress the request by inserting an internal disabled marker. An empty string `""` is still formatted into the URL and therefore does not prevent the request.
|
|
517
506
|
* @property {"options"|"remote"|"disabled"} filter.mode - Filter mode: `"options"` (client-side), `"remote"` (server-side, `lazyLoad` is ignored), or `"disabled"`.
|
|
518
507
|
* @property {"inline"|"popper"} filter.position - Position of the filter input: `"inline"` (inside the control) or `"popper"` (inside the dropdown).
|
|
519
|
-
* @property {string|null} filter.defaultOptionsUrl - URL to load
|
|
508
|
+
* @property {string|null} filter.defaultOptionsUrl - URL to load options when `filter.mode` is `"remote"` and no filter value has been entered. This is used as the empty-filter source both on open and after the filter is cleared again.
|
|
520
509
|
* @property {Object} filter.marker - Markers for embedding the filter value into the `url` for server-side filtering.
|
|
521
510
|
* @property {string} filter.marker.open - Opening marker (e.g., `{`).
|
|
522
511
|
* @property {string} filter.marker.close - Closing marker (e.g., `}`).
|
|
@@ -544,6 +533,7 @@ class Select extends CustomControl {
|
|
|
544
533
|
* @property {Function} formatter.selection - Callback `(value, option) => string` to format the display text of selected values.
|
|
545
534
|
* @property {Object} classes - CSS classes for various elements.
|
|
546
535
|
* @property {string} classes.badge - CSS class for selection badges.
|
|
536
|
+
* @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.
|
|
547
537
|
* @property {string} classes.statusOrRemoveBadge - CSS class for the status or remove badge.
|
|
548
538
|
* @property {string} classes.remoteInfo - CSS class for the remote info badge.
|
|
549
539
|
* @property {string} classes.noOptions - CSS class for the "no options" message.
|
|
@@ -625,6 +615,7 @@ class Select extends CustomControl {
|
|
|
625
615
|
|
|
626
616
|
classes: {
|
|
627
617
|
badge: "monster-badge-primary",
|
|
618
|
+
badgeUnresolved: "monster-badge-warning",
|
|
628
619
|
statusOrRemoveBadge: "empty",
|
|
629
620
|
remoteInfo: "monster-margin-start-4 monster-margin-top-4",
|
|
630
621
|
noOptions: "monster-margin-top-4 monster-margin-start-4",
|
|
@@ -681,6 +672,7 @@ class Select extends CustomControl {
|
|
|
681
672
|
// Clear the lookup cache
|
|
682
673
|
this[lookupCacheSymbol].clear();
|
|
683
674
|
this[lookupInProgressSymbol].clear();
|
|
675
|
+
this[unresolvedSelectionValuesSymbol].clear();
|
|
684
676
|
|
|
685
677
|
setSelection
|
|
686
678
|
.call(this, null)
|
|
@@ -702,7 +694,6 @@ class Select extends CustomControl {
|
|
|
702
694
|
resetErrorAttribute(this);
|
|
703
695
|
|
|
704
696
|
this[lazyLoadDoneSymbol] = false;
|
|
705
|
-
this[runLookupOnceSymbol] = false;
|
|
706
697
|
|
|
707
698
|
checkOptionState.call(this);
|
|
708
699
|
calcAndSetOptionsDimension.call(this);
|
|
@@ -753,7 +744,9 @@ class Select extends CustomControl {
|
|
|
753
744
|
|
|
754
745
|
if (self.getOption("url") !== null) {
|
|
755
746
|
if (lazyLoadFlag || remoteFilterFlag) {
|
|
756
|
-
|
|
747
|
+
if (self.getOption("lookup.url")) {
|
|
748
|
+
lookupSelection.call(self);
|
|
749
|
+
}
|
|
757
750
|
} else {
|
|
758
751
|
self
|
|
759
752
|
.fetch()
|
|
@@ -1776,9 +1769,12 @@ function getTranslations() {
|
|
|
1776
1769
|
}
|
|
1777
1770
|
|
|
1778
1771
|
/**
|
|
1779
|
-
*
|
|
1780
|
-
|
|
1781
|
-
|
|
1772
|
+
* Hydrates the current selection once the component becomes visible.
|
|
1773
|
+
*
|
|
1774
|
+
* This path is used for initial hydration when remote or lazy-loaded options
|
|
1775
|
+
* are not yet present locally. It is intentionally limited to `lookup.url`
|
|
1776
|
+
* and does not fall back to the main remote option source.
|
|
1777
|
+
*
|
|
1782
1778
|
* @private
|
|
1783
1779
|
*/
|
|
1784
1780
|
function lookupSelection() {
|
|
@@ -1817,7 +1813,10 @@ function runSelectionLookupWhenVisible(self) {
|
|
|
1817
1813
|
return;
|
|
1818
1814
|
}
|
|
1819
1815
|
|
|
1820
|
-
|
|
1816
|
+
const url = self.getOption("lookup.url");
|
|
1817
|
+
if (!url) {
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1821
1820
|
self[cleanupOptionsListSymbol] = false;
|
|
1822
1821
|
|
|
1823
1822
|
if (self.getOption("lookup.grouping") === true) {
|
|
@@ -2237,22 +2236,120 @@ function parseSlotsToOptions() {
|
|
|
2237
2236
|
* @return {*}
|
|
2238
2237
|
*/
|
|
2239
2238
|
function buildSelectionLabel(value) {
|
|
2240
|
-
// First, check the lookup cache.
|
|
2241
|
-
if (this[lookupCacheSymbol].has(value)) {
|
|
2242
|
-
return this[lookupCacheSymbol].get(value);
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
2239
|
const strict = this.getOption("features.useStrictValueComparison") === true;
|
|
2246
2240
|
const map = this[optionsMapSymbol];
|
|
2247
2241
|
const key = strict ? value : String(value);
|
|
2248
2242
|
|
|
2249
2243
|
if (map && map.has(key)) {
|
|
2244
|
+
if (clearUnresolvedSelectionValue.call(this, value)) {
|
|
2245
|
+
this[lookupCacheSymbol].delete(value);
|
|
2246
|
+
}
|
|
2250
2247
|
return map.get(key);
|
|
2251
2248
|
}
|
|
2252
2249
|
|
|
2250
|
+
if (this[lookupCacheSymbol].has(value)) {
|
|
2251
|
+
return this[lookupCacheSymbol].get(value);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2253
2254
|
return undefined;
|
|
2254
2255
|
}
|
|
2255
2256
|
|
|
2257
|
+
/**
|
|
2258
|
+
* @private
|
|
2259
|
+
* @param {*} value
|
|
2260
|
+
* @returns {*}
|
|
2261
|
+
*/
|
|
2262
|
+
function getSelectionStateKey(value) {
|
|
2263
|
+
return this.getOption("features.useStrictValueComparison") === true
|
|
2264
|
+
? value
|
|
2265
|
+
: String(value);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
/**
|
|
2269
|
+
* @private
|
|
2270
|
+
* @param {*} value
|
|
2271
|
+
* @returns {boolean}
|
|
2272
|
+
*/
|
|
2273
|
+
function markSelectionAsUnresolved(value) {
|
|
2274
|
+
const key = getSelectionStateKey.call(this, value);
|
|
2275
|
+
if (this[unresolvedSelectionValuesSymbol].has(key)) {
|
|
2276
|
+
return false;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
this[unresolvedSelectionValuesSymbol].add(key);
|
|
2280
|
+
return true;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
/**
|
|
2284
|
+
* @private
|
|
2285
|
+
* @param {*} value
|
|
2286
|
+
* @returns {boolean}
|
|
2287
|
+
*/
|
|
2288
|
+
function clearUnresolvedSelectionValue(value) {
|
|
2289
|
+
const key = getSelectionStateKey.call(this, value);
|
|
2290
|
+
return this[unresolvedSelectionValuesSymbol].delete(key);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/**
|
|
2294
|
+
* @private
|
|
2295
|
+
* @param {*} value
|
|
2296
|
+
* @returns {boolean}
|
|
2297
|
+
*/
|
|
2298
|
+
function isSelectionValueUnresolved(value) {
|
|
2299
|
+
const key = getSelectionStateKey.call(this, value);
|
|
2300
|
+
return this[unresolvedSelectionValuesSymbol].has(key);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
/**
|
|
2304
|
+
* @private
|
|
2305
|
+
* @param {*} value
|
|
2306
|
+
* @returns {string}
|
|
2307
|
+
*/
|
|
2308
|
+
function getSelectionBadgeClass(value) {
|
|
2309
|
+
const classes = new TokenList(this.getOption("classes.badge"));
|
|
2310
|
+
if (!isSelectionValueUnresolved.call(this, value)) {
|
|
2311
|
+
return classes.toString();
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
const unresolvedClasses = new TokenList(
|
|
2315
|
+
this.getOption("classes.badgeUnresolved"),
|
|
2316
|
+
);
|
|
2317
|
+
for (const token of classes.entries()) {
|
|
2318
|
+
if (token.startsWith("monster-badge-")) {
|
|
2319
|
+
classes.remove(token);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
classes.add(unresolvedClasses.entries());
|
|
2324
|
+
return classes.toString();
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/**
|
|
2328
|
+
* @private
|
|
2329
|
+
* @param {*} value
|
|
2330
|
+
* @param {string} [preferredLabel]
|
|
2331
|
+
* @returns {{class: string, label: string, unresolved: boolean, value: *}}
|
|
2332
|
+
*/
|
|
2333
|
+
function buildSelectionItem(value, preferredLabel) {
|
|
2334
|
+
let label = getSelectionLabel.call(this, value);
|
|
2335
|
+
const unresolved = isSelectionValueUnresolved.call(this, value);
|
|
2336
|
+
if (
|
|
2337
|
+
!unresolved &&
|
|
2338
|
+
`${label}` === `${value}` &&
|
|
2339
|
+
isString(preferredLabel) &&
|
|
2340
|
+
preferredLabel.length > 0
|
|
2341
|
+
) {
|
|
2342
|
+
label = preferredLabel;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
return {
|
|
2346
|
+
label,
|
|
2347
|
+
value,
|
|
2348
|
+
class: getSelectionBadgeClass.call(this, value),
|
|
2349
|
+
unresolved,
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2256
2353
|
/**
|
|
2257
2354
|
* @private
|
|
2258
2355
|
* @param {string} value The value to look up.
|
|
@@ -2278,6 +2375,7 @@ async function lookupValueAndCache(value) {
|
|
|
2278
2375
|
|
|
2279
2376
|
let hasError = false;
|
|
2280
2377
|
let found = false;
|
|
2378
|
+
let refreshSelection = false;
|
|
2281
2379
|
try {
|
|
2282
2380
|
this[lookupInProgressSymbol].set(value, true);
|
|
2283
2381
|
|
|
@@ -2309,28 +2407,47 @@ async function lookupValueAndCache(value) {
|
|
|
2309
2407
|
}
|
|
2310
2408
|
|
|
2311
2409
|
// The lookup might return more than the requested value, so we cache all of them.
|
|
2312
|
-
if (
|
|
2410
|
+
if (this[lookupCacheSymbol].get(itemValue) !== itemLabel) {
|
|
2313
2411
|
this[lookupCacheSymbol].set(itemValue, itemLabel);
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2412
|
+
refreshSelection = true;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
if (clearUnresolvedSelectionValue.call(this, itemValue)) {
|
|
2416
|
+
refreshSelection = true;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
if (`${itemValue}` === `${value}`) {
|
|
2420
|
+
found = true;
|
|
2317
2421
|
}
|
|
2318
2422
|
}
|
|
2319
2423
|
|
|
2320
2424
|
if (!found && !this[lookupCacheSymbol].has(value)) {
|
|
2321
|
-
|
|
2322
|
-
|
|
2425
|
+
this[lookupCacheSymbol].set(value, `${value}`);
|
|
2426
|
+
refreshSelection = true;
|
|
2323
2427
|
}
|
|
2324
2428
|
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
await setSelection.call(this, this.getOption("selection"));
|
|
2429
|
+
if (!found && markSelectionAsUnresolved.call(this, value)) {
|
|
2430
|
+
refreshSelection = true;
|
|
2328
2431
|
}
|
|
2329
2432
|
} catch (e) {
|
|
2330
2433
|
hasError = true;
|
|
2331
2434
|
addErrorAttribute(this, e);
|
|
2435
|
+
|
|
2436
|
+
if (!this[lookupCacheSymbol].has(value)) {
|
|
2437
|
+
this[lookupCacheSymbol].set(value, `${value}`);
|
|
2438
|
+
refreshSelection = true;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
if (markSelectionAsUnresolved.call(this, value)) {
|
|
2442
|
+
refreshSelection = true;
|
|
2443
|
+
}
|
|
2332
2444
|
} finally {
|
|
2333
2445
|
this[lookupInProgressSymbol].delete(value);
|
|
2446
|
+
|
|
2447
|
+
if (refreshSelection) {
|
|
2448
|
+
await setSelection.call(this, this.getOption("selection"));
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2334
2451
|
if (hasError) {
|
|
2335
2452
|
setStatusOrRemoveBadges.call(this, "error");
|
|
2336
2453
|
} else {
|
|
@@ -2932,6 +3049,17 @@ function getSelectPopperPositionOptions() {
|
|
|
2932
3049
|
getDefaultSelectPopperPositionProfile().placement;
|
|
2933
3050
|
}
|
|
2934
3051
|
|
|
3052
|
+
if (
|
|
3053
|
+
resolveParentPopperContentBoundary(
|
|
3054
|
+
this[controlElementSymbol],
|
|
3055
|
+
this[popperElementSymbol],
|
|
3056
|
+
)
|
|
3057
|
+
) {
|
|
3058
|
+
// Nested selects inside another popper must position against the viewport
|
|
3059
|
+
// so the parent content wrapper does not become their clipping boundary.
|
|
3060
|
+
popperOptions.strategy = "fixed";
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2935
3063
|
return popperOptions;
|
|
2936
3064
|
}
|
|
2937
3065
|
|
|
@@ -3178,12 +3306,6 @@ function filterFromRemote() {
|
|
|
3178
3306
|
return Promise.reject(new Error("Missing Filter Element."));
|
|
3179
3307
|
}
|
|
3180
3308
|
|
|
3181
|
-
const url = this.getOption("url");
|
|
3182
|
-
if (!url) {
|
|
3183
|
-
addErrorAttribute(this, "Missing URL for Remote Filter.");
|
|
3184
|
-
return Promise.reject(new Error("Missing URL for Remote Filter."));
|
|
3185
|
-
}
|
|
3186
|
-
|
|
3187
3309
|
let filterValue;
|
|
3188
3310
|
let showFlag = false;
|
|
3189
3311
|
|
|
@@ -3207,6 +3329,16 @@ function filterFromRemote() {
|
|
|
3207
3329
|
page: this[currentPageSymbol] || 1,
|
|
3208
3330
|
};
|
|
3209
3331
|
|
|
3332
|
+
if (shouldUseDefaultOptionsUrl.call(this, filterValue)) {
|
|
3333
|
+
return loadDefaultOptionsFromUrl.call(this, showFlag);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
const url = this.getOption("url");
|
|
3337
|
+
if (!url) {
|
|
3338
|
+
addErrorAttribute(this, "Missing URL for Remote Filter.");
|
|
3339
|
+
return Promise.reject(new Error("Missing URL for Remote Filter."));
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3210
3342
|
return filterFromRemoteByValue.call(this, url, params, showFlag);
|
|
3211
3343
|
}
|
|
3212
3344
|
|
|
@@ -3216,7 +3348,8 @@ function filterFromRemote() {
|
|
|
3216
3348
|
* @param {object} params
|
|
3217
3349
|
* @returns {string}
|
|
3218
3350
|
*/
|
|
3219
|
-
function formatURL(url, params = {}) {
|
|
3351
|
+
function formatURL(url, params = {}, formatOptions = {}) {
|
|
3352
|
+
const preserveEmptyFilter = formatOptions?.preserveEmptyFilter === true;
|
|
3220
3353
|
const paramsDefaults = this.getOption("filter.paramsDefaults");
|
|
3221
3354
|
const externalParams = this.getOption("filter.params");
|
|
3222
3355
|
if (isObject(paramsDefaults) || isObject(externalParams)) {
|
|
@@ -3244,7 +3377,7 @@ function formatURL(url, params = {}) {
|
|
|
3244
3377
|
if (
|
|
3245
3378
|
params.filter === undefined ||
|
|
3246
3379
|
params.filter === null ||
|
|
3247
|
-
params.filter === ""
|
|
3380
|
+
(params.filter === "" && preserveEmptyFilter !== true)
|
|
3248
3381
|
) {
|
|
3249
3382
|
const defaultValue = this.getOption("filter.defaultValue");
|
|
3250
3383
|
if (defaultValue === undefined || defaultValue === null) {
|
|
@@ -3284,9 +3417,14 @@ function formatURL(url, params = {}) {
|
|
|
3284
3417
|
* @param {boolean} [openPopper] Flag indicating whether to open the popper.
|
|
3285
3418
|
* @return {string} The formatted URL with the applied filters and markers.
|
|
3286
3419
|
*/
|
|
3287
|
-
function filterFromRemoteByValue(
|
|
3420
|
+
function filterFromRemoteByValue(
|
|
3421
|
+
optionUrl,
|
|
3422
|
+
params,
|
|
3423
|
+
openPopper,
|
|
3424
|
+
formatOptions = {},
|
|
3425
|
+
) {
|
|
3288
3426
|
return new Processing(() => {
|
|
3289
|
-
let url = formatURL.call(this, optionUrl, params);
|
|
3427
|
+
let url = formatURL.call(this, optionUrl, params, formatOptions);
|
|
3290
3428
|
|
|
3291
3429
|
if (url.indexOf(disabledRequestMarker.toString()) !== -1) {
|
|
3292
3430
|
this.setOption("total", null);
|
|
@@ -3400,6 +3538,51 @@ function getCurrentFilterValue() {
|
|
|
3400
3538
|
return "";
|
|
3401
3539
|
}
|
|
3402
3540
|
|
|
3541
|
+
function shouldUseDefaultOptionsUrl(filterValue) {
|
|
3542
|
+
if (!isString(this.getOption("filter.defaultOptionsUrl"))) {
|
|
3543
|
+
return false;
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
if (filterValue === undefined || filterValue === null) {
|
|
3547
|
+
return true;
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
if (isString(filterValue)) {
|
|
3551
|
+
return filterValue.trim() === "";
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
return false;
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
function loadDefaultOptionsFromUrl(openPopper = false) {
|
|
3558
|
+
const url = this.getOption("filter.defaultOptionsUrl");
|
|
3559
|
+
if (!isString(url)) {
|
|
3560
|
+
return Promise.resolve(false);
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
this[cleanupOptionsListSymbol] = true;
|
|
3564
|
+
|
|
3565
|
+
return filterFromRemoteByValue
|
|
3566
|
+
.call(
|
|
3567
|
+
this,
|
|
3568
|
+
url,
|
|
3569
|
+
{
|
|
3570
|
+
filter: "",
|
|
3571
|
+
page: this[currentPageSymbol] || 1,
|
|
3572
|
+
},
|
|
3573
|
+
openPopper,
|
|
3574
|
+
{
|
|
3575
|
+
preserveEmptyFilter: true,
|
|
3576
|
+
},
|
|
3577
|
+
)
|
|
3578
|
+
.then(() => {
|
|
3579
|
+
if (isPositionedPopperOpen(this[popperElementSymbol])) {
|
|
3580
|
+
setStatusOrRemoveBadges.call(this, "open");
|
|
3581
|
+
}
|
|
3582
|
+
return true;
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3403
3586
|
/**
|
|
3404
3587
|
* @private
|
|
3405
3588
|
*/
|
|
@@ -3519,10 +3702,7 @@ function gatherState() {
|
|
|
3519
3702
|
);
|
|
3520
3703
|
|
|
3521
3704
|
for (const e of elements) {
|
|
3522
|
-
selection.push(
|
|
3523
|
-
label: getSelectionLabel.call(this, e.value),
|
|
3524
|
-
value: e.value,
|
|
3525
|
-
});
|
|
3705
|
+
selection.push(buildSelectionItem.call(this, e.value));
|
|
3526
3706
|
}
|
|
3527
3707
|
|
|
3528
3708
|
filteredSelection = selection;
|
|
@@ -3539,10 +3719,7 @@ function gatherState() {
|
|
|
3539
3719
|
(sel) => !currentInputValues.has(sel.value),
|
|
3540
3720
|
);
|
|
3541
3721
|
for (const input of checkedElements) {
|
|
3542
|
-
filteredSelection.push(
|
|
3543
|
-
label: getSelectionLabel.call(this, input.value),
|
|
3544
|
-
value: input.value,
|
|
3545
|
-
});
|
|
3722
|
+
filteredSelection.push(buildSelectionItem.call(this, input.value));
|
|
3546
3723
|
}
|
|
3547
3724
|
}
|
|
3548
3725
|
|
|
@@ -3821,16 +3998,10 @@ function convertValueToSelection(value) {
|
|
|
3821
3998
|
}
|
|
3822
3999
|
|
|
3823
4000
|
if (isString(value) || isInteger(value)) {
|
|
3824
|
-
selection.push(
|
|
3825
|
-
label: getSelectionLabel.call(this, value),
|
|
3826
|
-
value: value,
|
|
3827
|
-
});
|
|
4001
|
+
selection.push(buildSelectionItem.call(this, value));
|
|
3828
4002
|
} else if (isArray(value)) {
|
|
3829
4003
|
for (const v of value) {
|
|
3830
|
-
selection.push(
|
|
3831
|
-
label: getSelectionLabel.call(this, v),
|
|
3832
|
-
value: v,
|
|
3833
|
-
});
|
|
4004
|
+
selection.push(buildSelectionItem.call(this, v));
|
|
3834
4005
|
}
|
|
3835
4006
|
|
|
3836
4007
|
value = value.join(",");
|
|
@@ -3955,8 +4126,6 @@ function areSelectionValuesEqual(current, next) {
|
|
|
3955
4126
|
* @returns {Promise<unknown | void>}
|
|
3956
4127
|
*/
|
|
3957
4128
|
function setSelection(selection) {
|
|
3958
|
-
const self = this;
|
|
3959
|
-
|
|
3960
4129
|
if (isString(selection) || isInteger(selection)) {
|
|
3961
4130
|
const result = convertValueToSelection.call(this, selection);
|
|
3962
4131
|
selection = result?.selection;
|
|
@@ -3973,15 +4142,9 @@ function setSelection(selection) {
|
|
|
3973
4142
|
continue;
|
|
3974
4143
|
}
|
|
3975
4144
|
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
}
|
|
3980
|
-
|
|
3981
|
-
resultSelection.push({
|
|
3982
|
-
label: l,
|
|
3983
|
-
value: selection[i].value,
|
|
3984
|
-
});
|
|
4145
|
+
resultSelection.push(
|
|
4146
|
+
buildSelectionItem.call(this, selection[i].value, selection[i].label),
|
|
4147
|
+
);
|
|
3985
4148
|
}
|
|
3986
4149
|
|
|
3987
4150
|
selection = resultSelection;
|
|
@@ -4016,17 +4179,6 @@ function setSelection(selection) {
|
|
|
4016
4179
|
fireEvent(this, "change"); // https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/291
|
|
4017
4180
|
}
|
|
4018
4181
|
|
|
4019
|
-
if (this[runLookupOnceSymbol] !== true && selection.length > 0) {
|
|
4020
|
-
this[runLookupOnceSymbol] = true;
|
|
4021
|
-
|
|
4022
|
-
const lazyLoadFlag =
|
|
4023
|
-
this.getOption("features.lazyLoad") && this[lazyLoadDoneSymbol] !== true;
|
|
4024
|
-
const remoteFilterFlag = getFilterMode.call(this) === FILTER_MODE_REMOTE;
|
|
4025
|
-
if (lazyLoadFlag || remoteFilterFlag) {
|
|
4026
|
-
lookupSelection.call(self);
|
|
4027
|
-
}
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
4182
|
return new Processing(() => {
|
|
4031
4183
|
const CLASSNAME = "selected";
|
|
4032
4184
|
|
|
@@ -4178,12 +4330,6 @@ function show() {
|
|
|
4178
4330
|
return;
|
|
4179
4331
|
}
|
|
4180
4332
|
|
|
4181
|
-
const hasDefaultOptionsUrl = isString(
|
|
4182
|
-
this.getOption("filter.defaultOptionsUrl"),
|
|
4183
|
-
);
|
|
4184
|
-
|
|
4185
|
-
initDefaultOptionsFromUrl.call(this);
|
|
4186
|
-
|
|
4187
4333
|
const hasPopperFilterFlag =
|
|
4188
4334
|
this.getOption("filter.position") === FILTER_POSITION_POPPER &&
|
|
4189
4335
|
getFilterMode.call(this) !== FILTER_MODE_DISABLED;
|
|
@@ -4206,17 +4352,23 @@ function show() {
|
|
|
4206
4352
|
registerWithHost.call(this);
|
|
4207
4353
|
|
|
4208
4354
|
new Processing(() => {
|
|
4209
|
-
|
|
4210
|
-
self
|
|
4355
|
+
const shouldLoadRemoteOptions =
|
|
4356
|
+
getFilterMode.call(self) === FILTER_MODE_REMOTE &&
|
|
4357
|
+
getOptionElements.call(self).length === 0;
|
|
4211
4358
|
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4359
|
+
if (shouldUseDefaultOptionsUrl.call(self, getCurrentFilterValue.call(self))) {
|
|
4360
|
+
setTimeout(() => {
|
|
4361
|
+
loadDefaultOptionsFromUrl.call(self).catch((e) => {
|
|
4362
|
+
addErrorAttribute(self, e);
|
|
4363
|
+
});
|
|
4364
|
+
}, 0);
|
|
4365
|
+
} else if (shouldLoadRemoteOptions) {
|
|
4366
|
+
setTimeout(() => {
|
|
4367
|
+
self[cleanupOptionsListSymbol] = true;
|
|
4368
|
+
filterFromRemote.call(self).catch((e) => {
|
|
4369
|
+
addErrorAttribute(self, e);
|
|
4370
|
+
});
|
|
4371
|
+
}, 0);
|
|
4220
4372
|
}
|
|
4221
4373
|
calcAndSetOptionsDimension.call(this);
|
|
4222
4374
|
focusFilter.call(this);
|
|
@@ -4280,31 +4432,6 @@ function unregisterFromHost() {
|
|
|
4280
4432
|
this[hostElementSymbol].unregisterDismissable?.(this);
|
|
4281
4433
|
}
|
|
4282
4434
|
|
|
4283
|
-
function initDefaultOptionsFromUrl() {
|
|
4284
|
-
const url = this.getOption("filter.defaultOptionsUrl");
|
|
4285
|
-
if (!url) {
|
|
4286
|
-
return;
|
|
4287
|
-
}
|
|
4288
|
-
|
|
4289
|
-
this.setOption("filter.defaultOptionsUrl", null);
|
|
4290
|
-
|
|
4291
|
-
fetchData
|
|
4292
|
-
.call(this, url)
|
|
4293
|
-
.then((data) => {
|
|
4294
|
-
this[cleanupOptionsListSymbol] = false;
|
|
4295
|
-
importOptionsIntern.call(this, data);
|
|
4296
|
-
setStatusOrRemoveBadges.call(this, "open");
|
|
4297
|
-
initTotal.call(this, data);
|
|
4298
|
-
})
|
|
4299
|
-
.catch((e) => {
|
|
4300
|
-
addErrorAttribute(this, e);
|
|
4301
|
-
setStatusOrRemoveBadges.call(this, "error");
|
|
4302
|
-
});
|
|
4303
|
-
}
|
|
4304
|
-
|
|
4305
|
-
/**
|
|
4306
|
-
* @private
|
|
4307
|
-
*/
|
|
4308
4435
|
/**
|
|
4309
4436
|
* @private
|
|
4310
4437
|
*/
|
|
@@ -4915,7 +5042,8 @@ function getTemplate() {
|
|
|
4915
5042
|
part="badge"
|
|
4916
5043
|
data-monster-attributes="
|
|
4917
5044
|
data-monster-value path:selection | index:value,
|
|
4918
|
-
|
|
5045
|
+
data-monster-unresolved path:selection | index:unresolved,
|
|
5046
|
+
class path:selection | index:class,
|
|
4919
5047
|
part path:type | suffix:-option | prefix: form-" tabindex="-1">
|
|
4920
5048
|
<div data-monster-replace="path:selection | index:label" part="badge-label"
|
|
4921
5049
|
data-monster-role="badge-label"></div>
|
|
@@ -375,7 +375,8 @@ function applyAdaptiveFloatingContentSize(floatingElement, maxHeight) {
|
|
|
375
375
|
return;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
|
|
378
|
+
const overflowMode = contentElement.dataset.monsterOverflowMode;
|
|
379
|
+
if (overflowMode === "visible") {
|
|
379
380
|
return;
|
|
380
381
|
}
|
|
381
382
|
|
|
@@ -392,6 +393,20 @@ function applyAdaptiveFloatingContentSize(floatingElement, maxHeight) {
|
|
|
392
393
|
Number.isFinite(contentMaxHeight) && contentMaxHeight > 0
|
|
393
394
|
? Math.max(contentMaxHeight, minimumReadableHeight)
|
|
394
395
|
: contentMaxHeight;
|
|
396
|
+
const preferredContentHeight = getRequiredContentHeight(contentElement);
|
|
397
|
+
|
|
398
|
+
if (overflowMode === "horizontal" || overflowMode === "both") {
|
|
399
|
+
// Overlay-aware modes keep the parent wrapper sized to the real inline content.
|
|
400
|
+
// Nested poppers may still escape, but they must not inflate the outer popper height.
|
|
401
|
+
applyOverlayAwareContentHeight(
|
|
402
|
+
contentElement,
|
|
403
|
+
preferredContentHeight,
|
|
404
|
+
nextContentMaxHeight,
|
|
405
|
+
minimumReadableHeight,
|
|
406
|
+
);
|
|
407
|
+
syncNestedScrollContainerHeight(contentElement, nextContentMaxHeight);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
395
410
|
|
|
396
411
|
if (Number.isFinite(nextContentMaxHeight) && nextContentMaxHeight > 0) {
|
|
397
412
|
contentElement.style.maxHeight = `${nextContentMaxHeight}px`;
|
|
@@ -402,6 +417,39 @@ function applyAdaptiveFloatingContentSize(floatingElement, maxHeight) {
|
|
|
402
417
|
syncNestedScrollContainerHeight(contentElement, nextContentMaxHeight);
|
|
403
418
|
}
|
|
404
419
|
|
|
420
|
+
function applyOverlayAwareContentHeight(
|
|
421
|
+
contentElement,
|
|
422
|
+
preferredContentHeight,
|
|
423
|
+
contentMaxHeight,
|
|
424
|
+
minimumReadableHeight,
|
|
425
|
+
) {
|
|
426
|
+
const preferredHeight =
|
|
427
|
+
Number.isFinite(preferredContentHeight) && preferredContentHeight > 0
|
|
428
|
+
? Math.max(preferredContentHeight, minimumReadableHeight)
|
|
429
|
+
: minimumReadableHeight;
|
|
430
|
+
const constrainedHeight =
|
|
431
|
+
Number.isFinite(contentMaxHeight) && contentMaxHeight > 0
|
|
432
|
+
? Math.min(preferredHeight, contentMaxHeight)
|
|
433
|
+
: preferredHeight;
|
|
434
|
+
const isConstrained =
|
|
435
|
+
Number.isFinite(contentMaxHeight) &&
|
|
436
|
+
contentMaxHeight > 0 &&
|
|
437
|
+
preferredHeight > contentMaxHeight;
|
|
438
|
+
|
|
439
|
+
contentElement.style.removeProperty("overflow");
|
|
440
|
+
contentElement.style.overflowX = "visible";
|
|
441
|
+
contentElement.style.overflowY = isConstrained ? "auto" : "visible";
|
|
442
|
+
|
|
443
|
+
if (Number.isFinite(constrainedHeight) && constrainedHeight > 0) {
|
|
444
|
+
contentElement.style.height = `${constrainedHeight}px`;
|
|
445
|
+
contentElement.style.maxHeight = `${constrainedHeight}px`;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
contentElement.style.removeProperty("height");
|
|
450
|
+
contentElement.style.removeProperty("maxHeight");
|
|
451
|
+
}
|
|
452
|
+
|
|
405
453
|
function getFloatingContentElement(floatingElement) {
|
|
406
454
|
for (const child of floatingElement.children) {
|
|
407
455
|
if (!(child instanceof HTMLElement)) {
|
|
@@ -628,7 +676,11 @@ function resetAdaptiveFloatingElementSize(floatingElement) {
|
|
|
628
676
|
return;
|
|
629
677
|
}
|
|
630
678
|
|
|
679
|
+
contentElement.style.removeProperty("height");
|
|
631
680
|
contentElement.style.removeProperty("maxHeight");
|
|
681
|
+
contentElement.style.removeProperty("overflow");
|
|
682
|
+
contentElement.style.removeProperty("overflowX");
|
|
683
|
+
contentElement.style.removeProperty("overflowY");
|
|
632
684
|
|
|
633
685
|
const nestedScrollableElement = getNestedScrollableElement(contentElement);
|
|
634
686
|
if (!(nestedScrollableElement instanceof HTMLElement)) {
|
|
@@ -659,6 +711,22 @@ function getRequiredContentWidth(contentElement) {
|
|
|
659
711
|
return Math.max(contentElement.scrollWidth, contentElement.clientWidth);
|
|
660
712
|
}
|
|
661
713
|
|
|
714
|
+
function getRequiredContentHeight(contentElement) {
|
|
715
|
+
const nestedScrollableElement = getNestedScrollableElement(contentElement);
|
|
716
|
+
if (nestedScrollableElement instanceof HTMLElement) {
|
|
717
|
+
const contentRect = contentElement.getBoundingClientRect();
|
|
718
|
+
const nestedRect = nestedScrollableElement.getBoundingClientRect();
|
|
719
|
+
const reservedHeight = Math.max(0, contentRect.height - nestedRect.height);
|
|
720
|
+
return Math.max(
|
|
721
|
+
contentElement.scrollHeight,
|
|
722
|
+
nestedScrollableElement.scrollHeight + reservedHeight,
|
|
723
|
+
contentElement.clientHeight,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return Math.max(contentElement.scrollHeight, contentElement.clientHeight);
|
|
728
|
+
}
|
|
729
|
+
|
|
662
730
|
function getNestedScrollableElement(contentElement) {
|
|
663
731
|
for (const child of contentElement.children) {
|
|
664
732
|
if (!(child instanceof HTMLElement)) {
|
|
@@ -293,6 +293,8 @@ class Popper extends CustomElement {
|
|
|
293
293
|
/**
|
|
294
294
|
* Resolves the effective content overflow mode for the rendered wrapper.
|
|
295
295
|
* Subclasses can override this when the configured option is only an intermediate mode.
|
|
296
|
+
* `smart` keeps regular content measurable inside the popper while nested overlays
|
|
297
|
+
* are allowed to escape horizontally without forcing the parent wrapper to size to them.
|
|
296
298
|
*
|
|
297
299
|
* @return {string}
|
|
298
300
|
*/
|
|
@@ -742,10 +744,27 @@ function applyContentOverflowMode() {
|
|
|
742
744
|
return;
|
|
743
745
|
}
|
|
744
746
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
)
|
|
747
|
+
const overflowMode = this.resolveContentOverflowMode();
|
|
748
|
+
contentElement.setAttribute("data-monster-overflow-mode", overflowMode);
|
|
749
|
+
|
|
750
|
+
switch (overflowMode) {
|
|
751
|
+
case "horizontal":
|
|
752
|
+
contentElement.style.overflow = "visible";
|
|
753
|
+
contentElement.style.removeProperty("max-height");
|
|
754
|
+
contentElement.style.removeProperty("max-width");
|
|
755
|
+
break;
|
|
756
|
+
case "both":
|
|
757
|
+
case "visible":
|
|
758
|
+
contentElement.style.overflow = "visible";
|
|
759
|
+
contentElement.style.maxHeight = "none";
|
|
760
|
+
contentElement.style.maxWidth = "none";
|
|
761
|
+
break;
|
|
762
|
+
default:
|
|
763
|
+
contentElement.style.removeProperty("overflow");
|
|
764
|
+
contentElement.style.removeProperty("max-height");
|
|
765
|
+
contentElement.style.removeProperty("max-width");
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
749
768
|
}
|
|
750
769
|
|
|
751
770
|
/**
|
|
@@ -33,6 +33,20 @@ let html2 = `
|
|
|
33
33
|
</div>
|
|
34
34
|
`;
|
|
35
35
|
|
|
36
|
+
function createJsonResponse(data, status = 200) {
|
|
37
|
+
let headers = new Map();
|
|
38
|
+
headers.set('content-type', 'application/json');
|
|
39
|
+
|
|
40
|
+
return Promise.resolve({
|
|
41
|
+
ok: status >= 200 && status < 300,
|
|
42
|
+
status,
|
|
43
|
+
headers,
|
|
44
|
+
text: function () {
|
|
45
|
+
return Promise.resolve(JSON.stringify(data));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
let Select,
|
|
37
51
|
getDefaultSelectPopperPositionProfile,
|
|
38
52
|
resolveSelectListDimension,
|
|
@@ -197,6 +211,8 @@ describe('Select', function () {
|
|
|
197
211
|
});
|
|
198
212
|
|
|
199
213
|
describe('Popper sizing', function () {
|
|
214
|
+
this.timeout(5000);
|
|
215
|
+
|
|
200
216
|
afterEach(() => {
|
|
201
217
|
let mocks = document.getElementById('mocks');
|
|
202
218
|
mocks.innerHTML = "";
|
|
@@ -722,25 +738,286 @@ describe('Select', function () {
|
|
|
722
738
|
|
|
723
739
|
setTimeout(() => {
|
|
724
740
|
try {
|
|
725
|
-
|
|
726
|
-
observer.enterNode();
|
|
741
|
+
expect(lookupCount).to.equal(0);
|
|
727
742
|
} catch (e) {
|
|
728
743
|
mockIntersectionObserver.restore();
|
|
729
744
|
return done(e);
|
|
730
745
|
}
|
|
731
746
|
|
|
747
|
+
mockIntersectionObserver.restore();
|
|
748
|
+
done();
|
|
749
|
+
}, 150);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('should not refetch after selecting an already loaded remote option', function (done) {
|
|
753
|
+
this.timeout(3000);
|
|
754
|
+
|
|
755
|
+
let mocks = document.getElementById('mocks');
|
|
756
|
+
const requests = [];
|
|
757
|
+
|
|
758
|
+
global['fetch'] = function (url) {
|
|
759
|
+
requests.push(String(url));
|
|
760
|
+
return createJsonResponse({
|
|
761
|
+
items: [
|
|
762
|
+
{id: 'alpha', name: 'Alpha'}
|
|
763
|
+
],
|
|
764
|
+
pagination: {
|
|
765
|
+
total: 1,
|
|
766
|
+
page: 1,
|
|
767
|
+
perPage: 1
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
const select = document.createElement('monster-select');
|
|
773
|
+
select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
|
|
774
|
+
select.setOption('filter.mode', 'remote');
|
|
775
|
+
select.setOption('filter.defaultValue', '*');
|
|
776
|
+
select.setOption('mapping.selector', 'items.*');
|
|
777
|
+
select.setOption('mapping.labelTemplate', '${name}');
|
|
778
|
+
select.setOption('mapping.valueTemplate', '${id}');
|
|
779
|
+
select.setOption('mapping.total', 'pagination.total');
|
|
780
|
+
select.setOption('mapping.currentPage', 'pagination.page');
|
|
781
|
+
select.setOption('mapping.objectsPerPage', 'pagination.perPage');
|
|
782
|
+
mocks.appendChild(select);
|
|
783
|
+
|
|
784
|
+
setTimeout(() => {
|
|
785
|
+
select.fetch('https://example.com/items?filter=*&page=1')
|
|
786
|
+
.then(() => {
|
|
787
|
+
try {
|
|
788
|
+
expect(requests).to.deep.equal([
|
|
789
|
+
'https://example.com/items?filter=*&page=1'
|
|
790
|
+
]);
|
|
791
|
+
|
|
792
|
+
select.value = 'alpha';
|
|
793
|
+
} catch (e) {
|
|
794
|
+
return done(e);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
setTimeout(() => {
|
|
798
|
+
try {
|
|
799
|
+
expect(requests).to.deep.equal([
|
|
800
|
+
'https://example.com/items?filter=*&page=1'
|
|
801
|
+
]);
|
|
802
|
+
} catch (e) {
|
|
803
|
+
return done(e);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
done();
|
|
807
|
+
}, 200);
|
|
808
|
+
})
|
|
809
|
+
.catch((e) => done(e));
|
|
810
|
+
}, 50);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it('should reuse defaultOptionsUrl after clearing the filter and reopening', function (done) {
|
|
814
|
+
this.timeout(5000);
|
|
815
|
+
|
|
816
|
+
let mocks = document.getElementById('mocks');
|
|
817
|
+
const requests = [];
|
|
818
|
+
|
|
819
|
+
global['fetch'] = function (url) {
|
|
820
|
+
const requestUrl = String(url);
|
|
821
|
+
requests.push(requestUrl);
|
|
822
|
+
|
|
823
|
+
if (requestUrl.indexOf('/defaults') !== -1) {
|
|
824
|
+
return createJsonResponse({
|
|
825
|
+
items: [
|
|
826
|
+
{id: 'seed', name: 'Seed'}
|
|
827
|
+
],
|
|
828
|
+
pagination: {
|
|
829
|
+
total: 1,
|
|
830
|
+
page: 1,
|
|
831
|
+
perPage: 1
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return createJsonResponse({
|
|
837
|
+
items: [
|
|
838
|
+
{id: 'match', name: 'Match'}
|
|
839
|
+
],
|
|
840
|
+
pagination: {
|
|
841
|
+
total: 1,
|
|
842
|
+
page: 1,
|
|
843
|
+
perPage: 1
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const select = document.createElement('monster-select');
|
|
849
|
+
select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
|
|
850
|
+
select.setOption('filter.mode', 'remote');
|
|
851
|
+
select.setOption('filter.position', 'popper');
|
|
852
|
+
select.setOption('filter.defaultOptionsUrl', 'https://example.com/defaults?page={page}');
|
|
853
|
+
select.setOption('mapping.selector', 'items.*');
|
|
854
|
+
select.setOption('mapping.labelTemplate', '${name}');
|
|
855
|
+
select.setOption('mapping.valueTemplate', '${id}');
|
|
856
|
+
select.setOption('mapping.total', 'pagination.total');
|
|
857
|
+
select.setOption('mapping.currentPage', 'pagination.page');
|
|
858
|
+
select.setOption('mapping.objectsPerPage', 'pagination.perPage');
|
|
859
|
+
mocks.appendChild(select);
|
|
860
|
+
|
|
861
|
+
const dispatchFilterKey = (input, code, key) => {
|
|
862
|
+
input.dispatchEvent(new KeyboardEvent('keydown', {
|
|
863
|
+
bubbles: true,
|
|
864
|
+
composed: true,
|
|
865
|
+
code,
|
|
866
|
+
key
|
|
867
|
+
}));
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
setTimeout(() => {
|
|
871
|
+
let container;
|
|
872
|
+
let filterInput;
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
container = select.shadowRoot.querySelector('[data-monster-role=container]');
|
|
876
|
+
filterInput = select.shadowRoot.querySelector('[data-monster-role=filter][name="popper-filter"]');
|
|
877
|
+
container.click();
|
|
878
|
+
} catch (e) {
|
|
879
|
+
return done(e);
|
|
880
|
+
}
|
|
881
|
+
|
|
732
882
|
setTimeout(() => {
|
|
733
883
|
try {
|
|
734
|
-
expect(
|
|
884
|
+
expect(requests[0]).to.equal('https://example.com/defaults?page=1');
|
|
885
|
+
|
|
886
|
+
filterInput.value = 'alpha';
|
|
887
|
+
dispatchFilterKey(filterInput, 'KeyA', 'a');
|
|
888
|
+
} catch (e) {
|
|
889
|
+
return done(e);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
setTimeout(() => {
|
|
893
|
+
try {
|
|
894
|
+
expect(requests[1]).to.equal('https://example.com/items?filter=alpha&page=1');
|
|
895
|
+
|
|
896
|
+
filterInput.value = '';
|
|
897
|
+
dispatchFilterKey(filterInput, 'Backspace', 'Backspace');
|
|
898
|
+
} catch (e) {
|
|
899
|
+
return done(e);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
setTimeout(() => {
|
|
903
|
+
try {
|
|
904
|
+
expect(requests[2]).to.equal('https://example.com/defaults?page=1');
|
|
905
|
+
|
|
906
|
+
container.click();
|
|
907
|
+
} catch (e) {
|
|
908
|
+
return done(e);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
setTimeout(() => {
|
|
912
|
+
try {
|
|
913
|
+
container.click();
|
|
914
|
+
} catch (e) {
|
|
915
|
+
return done(e);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
setTimeout(() => {
|
|
919
|
+
try {
|
|
920
|
+
expect(requests[3]).to.equal('https://example.com/defaults?page=1');
|
|
921
|
+
} catch (e) {
|
|
922
|
+
return done(e);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
done();
|
|
926
|
+
}, 250);
|
|
927
|
+
}, 50);
|
|
928
|
+
}, 300);
|
|
929
|
+
}, 300);
|
|
930
|
+
}, 250);
|
|
931
|
+
}, 50);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('should keep unresolved lookup values visible and mark their badge', function (done) {
|
|
935
|
+
this.timeout(3000);
|
|
936
|
+
|
|
937
|
+
let mocks = document.getElementById('mocks');
|
|
938
|
+
global['fetch'] = function () {
|
|
939
|
+
return createJsonResponse({
|
|
940
|
+
items: [],
|
|
941
|
+
pagination: {
|
|
942
|
+
total: 0,
|
|
943
|
+
page: 1,
|
|
944
|
+
perPage: 1
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
const select = document.createElement('monster-select');
|
|
950
|
+
select.setOption('lookup.url', 'https://example.com/items?filter={filter}');
|
|
951
|
+
select.setOption('mapping.selector', 'items.*');
|
|
952
|
+
select.setOption('mapping.labelTemplate', '${name}');
|
|
953
|
+
select.setOption('mapping.valueTemplate', '${id}');
|
|
954
|
+
select.setOption('selection', [{value: 'missing-key'}]);
|
|
955
|
+
mocks.appendChild(select);
|
|
956
|
+
|
|
957
|
+
setTimeout(() => {
|
|
958
|
+
try {
|
|
959
|
+
const badge = select.shadowRoot.querySelector('[data-monster-role=badge]');
|
|
960
|
+
const badgeLabel = select.shadowRoot.querySelector('[data-monster-role=badge-label]');
|
|
961
|
+
|
|
962
|
+
expect(badge).to.be.instanceof(HTMLDivElement);
|
|
963
|
+
expect(badgeLabel).to.be.instanceof(HTMLDivElement);
|
|
964
|
+
expect(badgeLabel.textContent.trim()).to.equal('missing-key');
|
|
965
|
+
expect(badge.className).to.contain('monster-badge-warning');
|
|
966
|
+
expect(badge.className).to.not.contain('monster-badge-primary');
|
|
967
|
+
expect(badge.getAttribute('data-monster-unresolved')).to.equal('true');
|
|
968
|
+
expect(badgeLabel.textContent).to.not.contain(select.getOption('labels.cannot-be-loaded'));
|
|
969
|
+
} catch (e) {
|
|
970
|
+
return done(e);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
done();
|
|
974
|
+
}, 250);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it('should clear the unresolved badge state once a local option can resolve the value', function (done) {
|
|
978
|
+
this.timeout(4000);
|
|
979
|
+
|
|
980
|
+
let mocks = document.getElementById('mocks');
|
|
981
|
+
global['fetch'] = function () {
|
|
982
|
+
return createJsonResponse({
|
|
983
|
+
items: [],
|
|
984
|
+
pagination: {
|
|
985
|
+
total: 0,
|
|
986
|
+
page: 1,
|
|
987
|
+
perPage: 1
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const select = document.createElement('monster-select');
|
|
993
|
+
select.setOption('lookup.url', 'https://example.com/items?filter={filter}');
|
|
994
|
+
select.setOption('mapping.selector', 'items.*');
|
|
995
|
+
select.setOption('mapping.labelTemplate', '${name}');
|
|
996
|
+
select.setOption('mapping.valueTemplate', '${id}');
|
|
997
|
+
select.setOption('selection', [{value: 'missing-key'}]);
|
|
998
|
+
mocks.appendChild(select);
|
|
999
|
+
|
|
1000
|
+
setTimeout(() => {
|
|
1001
|
+
select.setOption('options', [{label: 'Resolved Label', value: 'missing-key'}]);
|
|
1002
|
+
select.value = 'missing-key';
|
|
1003
|
+
|
|
1004
|
+
setTimeout(() => {
|
|
1005
|
+
try {
|
|
1006
|
+
const badge = select.shadowRoot.querySelector('[data-monster-role=badge]');
|
|
1007
|
+
const badgeLabel = select.shadowRoot.querySelector('[data-monster-role=badge-label]');
|
|
1008
|
+
|
|
1009
|
+
expect(badge).to.be.instanceof(HTMLDivElement);
|
|
1010
|
+
expect(badgeLabel.textContent.trim()).to.equal('Resolved Label');
|
|
1011
|
+
expect(badge.className).to.contain('monster-badge-primary');
|
|
1012
|
+
expect(badge.className).to.not.contain('monster-badge-warning');
|
|
1013
|
+
expect(badge.getAttribute('data-monster-unresolved')).to.not.equal('true');
|
|
735
1014
|
} catch (e) {
|
|
736
|
-
mockIntersectionObserver.restore();
|
|
737
1015
|
return done(e);
|
|
738
1016
|
}
|
|
739
1017
|
|
|
740
|
-
mockIntersectionObserver.restore();
|
|
741
1018
|
done();
|
|
742
1019
|
}, 150);
|
|
743
|
-
},
|
|
1020
|
+
}, 250);
|
|
744
1021
|
});
|
|
745
1022
|
|
|
746
1023
|
it('should not throw when IntersectionObserver is unavailable', function (done) {
|