@schukai/monster 4.136.20 → 4.136.21

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 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.20"}
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.21"}
@@ -221,14 +221,7 @@ class Pagination extends CustomElement {
221
221
  this.setOption("pages", totalPages);
222
222
 
223
223
  if (stateUnchanged) {
224
- if (isAdaptivePagination.call(this)) {
225
- schedulePaginationLayoutUpdate.call(this);
226
- } else {
227
- const list = this.shadowRoot?.querySelector(".pagination-list");
228
- if (list) {
229
- list.setAttribute("data-monster-adaptive-ready", "true");
230
- }
231
- }
224
+ this.refreshLayout({ schedule: true });
232
225
  return;
233
226
  }
234
227
 
@@ -250,20 +243,35 @@ class Pagination extends CustomElement {
250
243
  // 4. Set the 'pagination' option, which will trigger the component to re-render.
251
244
  this.setOption("pagination", pagination);
252
245
 
253
- if (isAdaptivePagination.call(this)) {
254
- const list = this.shadowRoot?.querySelector(".pagination-list");
255
- if (list) {
256
- list.setAttribute("data-monster-adaptive-ready", "false");
257
- }
258
- schedulePaginationLayoutUpdate.call(this);
259
- } else {
260
- const list = this.shadowRoot?.querySelector(".pagination-list");
261
- if (list) {
262
- list.setAttribute("data-monster-adaptive-ready", "true");
246
+ this.refreshLayout({ schedule: true });
247
+
248
+ syncPaginationStateToDom.call(this, pagination);
249
+ }
250
+
251
+ refreshLayout({ schedule = false } = {}) {
252
+ if (schedule === true) {
253
+ if (isAdaptivePagination.call(this)) {
254
+ schedulePaginationLayoutUpdate.call(this);
255
+ } else {
256
+ const list = this.shadowRoot?.querySelector(".pagination-list");
257
+ if (list) {
258
+ list.setAttribute("data-monster-adaptive-ready", "true");
259
+ }
263
260
  }
261
+ return this;
264
262
  }
265
263
 
266
- syncPaginationStateToDom.call(this, pagination);
264
+ if (isAdaptivePagination.call(this)) {
265
+ applyPaginationLayout.call(this);
266
+ return this;
267
+ }
268
+
269
+ const list = this.shadowRoot?.querySelector(".pagination-list");
270
+ if (list) {
271
+ list.setAttribute("data-monster-adaptive-ready", "true");
272
+ }
273
+
274
+ return this;
267
275
  }
268
276
  /**
269
277
  *
@@ -700,14 +708,7 @@ function handleDataSourceChanges() {
700
708
 
701
709
  this.setOption("pagination", pagination);
702
710
 
703
- if (isAdaptivePagination.call(this)) {
704
- schedulePaginationLayoutUpdate.call(this);
705
- } else {
706
- const list = this.shadowRoot?.querySelector(".pagination-list");
707
- if (list) {
708
- list.setAttribute("data-monster-adaptive-ready", "true");
709
- }
710
- }
711
+ this.refreshLayout({ schedule: isAdaptivePagination.call(this) });
711
712
  }
712
713
 
713
714
  /**
@@ -138,6 +138,12 @@ const clearOptionEventHandler = Symbol("clearOptionEventHandler");
138
138
  */
139
139
  const resizeObserverSymbol = Symbol("resizeObserver");
140
140
  const resizeObserverFrameSymbol = Symbol("resizeObserverFrame");
141
+ const layoutCycleFrameSymbol = Symbol("layoutCycleFrame");
142
+ const layoutCycleTokenSymbol = Symbol("layoutCycleToken");
143
+ const layoutCycleModeSymbol = Symbol("layoutCycleMode");
144
+ const layoutCyclePendingFlagsSymbol = Symbol("layoutCyclePendingFlags");
145
+ const layoutCyclePendingPrioritySymbol = Symbol("layoutCyclePendingPriority");
146
+ const layoutCycleRunningSymbol = Symbol("layoutCycleRunning");
141
147
  const visualViewportResizeHandlerSymbol = Symbol("visualViewportResizeHandler");
142
148
  const visualViewportScrollHandlerSymbol = Symbol("visualViewportScrollHandler");
143
149
  const visibilityChangeHandlerSymbol = Symbol("visibilityChangeHandler");
@@ -307,6 +313,19 @@ const lookupInProgressSymbol = Symbol("lookupInProgress");
307
313
  const unresolvedSelectionValuesSymbol = Symbol("unresolvedSelectionValues");
308
314
  const fetchRequestVersionSymbol = Symbol("fetchRequestVersion");
309
315
  const remoteInfoRequestSymbol = Symbol("remoteInfoRequest");
316
+ const remoteInfoStableMessageSymbol = Symbol("remoteInfoStableMessage");
317
+
318
+ const SELECT_LAYOUT_PRIORITY_PASSIVE = 1;
319
+ const SELECT_LAYOUT_PRIORITY_INTERACTIVE = 2;
320
+ const SELECT_LAYOUT_PRIORITY_CRITICAL = 3;
321
+
322
+ const SELECT_LAYOUT_REASON_OPTION_STATE = 1 << 0;
323
+ const SELECT_LAYOUT_REASON_PAGINATION = 1 << 1;
324
+ const SELECT_LAYOUT_REASON_POSITION = 1 << 2;
325
+ const SELECT_LAYOUT_REASON_ALL =
326
+ SELECT_LAYOUT_REASON_OPTION_STATE |
327
+ SELECT_LAYOUT_REASON_PAGINATION |
328
+ SELECT_LAYOUT_REASON_POSITION;
310
329
 
311
330
  /**
312
331
  * @private
@@ -709,19 +728,21 @@ class Select extends CustomControl {
709
728
  }
710
729
 
711
730
  this.setOption("messages.selected", "");
712
- this.setOption("messages.total", "");
731
+ setRemoteInfoText.call(this, "");
713
732
  this.setOption("messages.summary", "");
714
733
  this.setOption("total", null);
715
734
  resetPaginationState.call(this);
716
735
 
717
- resetErrorAttribute(this);
736
+ resetErrorAttribute(this);
718
737
 
719
- this[lazyLoadDoneSymbol] = false;
738
+ this[lazyLoadDoneSymbol] = false;
720
739
 
721
- checkOptionState.call(this);
722
- calcAndSetOptionsDimension.call(this);
723
- updatePopper.call(this);
724
- })
740
+ scheduleSelectLayoutCycle.call(
741
+ this,
742
+ SELECT_LAYOUT_PRIORITY_CRITICAL,
743
+ SELECT_LAYOUT_REASON_ALL,
744
+ );
745
+ })
725
746
  .catch((e) => {
726
747
  addErrorAttribute(this, e);
727
748
  });
@@ -1102,6 +1123,44 @@ function processAndApplyRemoteInfoTotal(data) {
1102
1123
  }
1103
1124
  }
1104
1125
 
1126
+ /**
1127
+ * @private
1128
+ * @param {string|null|undefined} message
1129
+ * @param {object} [options]
1130
+ * @param {boolean} [options.reserveSpace=false]
1131
+ * @returns {void}
1132
+ */
1133
+ function setRemoteInfoText(message, { reserveSpace = false } = {}) {
1134
+ const normalizedMessage =
1135
+ message === undefined || message === null ? "" : `${message}`;
1136
+ const previousStableMessage =
1137
+ typeof this[remoteInfoStableMessageSymbol] === "string"
1138
+ ? this[remoteInfoStableMessageSymbol]
1139
+ : "";
1140
+ const renderedMessage =
1141
+ reserveSpace === true &&
1142
+ normalizedMessage === "" &&
1143
+ previousStableMessage !== ""
1144
+ ? previousStableMessage
1145
+ : normalizedMessage;
1146
+
1147
+ this.setOption("messages.total", renderedMessage);
1148
+
1149
+ if (normalizedMessage !== "") {
1150
+ this[remoteInfoStableMessageSymbol] = normalizedMessage;
1151
+ } else if (reserveSpace !== true) {
1152
+ this[remoteInfoStableMessageSymbol] = "";
1153
+ }
1154
+
1155
+ if (this[remoteInfoElementSymbol] instanceof HTMLElement) {
1156
+ if (reserveSpace === true && renderedMessage !== "") {
1157
+ this[remoteInfoElementSymbol].style.visibility = "hidden";
1158
+ } else {
1159
+ this[remoteInfoElementSymbol].style.removeProperty("visibility");
1160
+ }
1161
+ }
1162
+ }
1163
+
1105
1164
  /**
1106
1165
  * @private
1107
1166
  * @returns {number}
@@ -1964,7 +2023,9 @@ function fetchIt(url, controlOptions) {
1964
2023
  classes.add("d-none");
1965
2024
  this.setOption("classes.noOptions", classes.toString());
1966
2025
  this.setOption("messages.emptyOptions", "");
1967
- this.setOption("messages.total", "");
2026
+ setRemoteInfoText.call(this, "", {
2027
+ reserveSpace: true,
2028
+ });
1968
2029
  }
1969
2030
 
1970
2031
  new Processing(10, () => {
@@ -2158,6 +2219,7 @@ function disconnectResizeObserver() {
2158
2219
  this[resizeObserverSymbol].disconnect();
2159
2220
  }
2160
2221
  cancelScheduledResizeObserverPopperUpdate.call(this);
2222
+ cancelScheduledSelectLayoutCycle.call(this);
2161
2223
 
2162
2224
  const viewport = getGlobal().visualViewport;
2163
2225
  if (
@@ -2217,7 +2279,11 @@ function scheduleResizeObserverPopperUpdate() {
2217
2279
 
2218
2280
  this[resizeObserverFrameSymbol] = schedule(() => {
2219
2281
  delete this[resizeObserverFrameSymbol];
2220
- updatePopper.call(this);
2282
+ scheduleSelectLayoutCycle.call(
2283
+ this,
2284
+ SELECT_LAYOUT_PRIORITY_CRITICAL,
2285
+ SELECT_LAYOUT_REASON_ALL,
2286
+ );
2221
2287
  });
2222
2288
  }
2223
2289
 
@@ -2236,6 +2302,155 @@ function cancelScheduledResizeObserverPopperUpdate() {
2236
2302
  delete this[resizeObserverFrameSymbol];
2237
2303
  }
2238
2304
 
2305
+ function applySelectLayoutState(layoutReasons = 0) {
2306
+ if (layoutReasons & SELECT_LAYOUT_REASON_OPTION_STATE) {
2307
+ checkOptionState.call(this);
2308
+ }
2309
+
2310
+ calcAndSetOptionsDimension.call(this);
2311
+
2312
+ if (layoutReasons & SELECT_LAYOUT_REASON_PAGINATION) {
2313
+ refreshSelectPaginationLayout.call(this);
2314
+ }
2315
+ }
2316
+
2317
+ function performSelectLayoutCycle(layoutReasons = SELECT_LAYOUT_REASON_ALL) {
2318
+ if (!isPositionedPopperOpen(this[popperElementSymbol])) {
2319
+ return Promise.resolve();
2320
+ }
2321
+
2322
+ if (this.getOption("disabled", false) === true) {
2323
+ return Promise.resolve();
2324
+ }
2325
+
2326
+ return new Processing(() => {
2327
+ applySelectLayoutState.call(this, layoutReasons);
2328
+ if (layoutReasons & SELECT_LAYOUT_REASON_POSITION) {
2329
+ return positionPopper.call(
2330
+ this,
2331
+ this[controlElementSymbol],
2332
+ this[popperElementSymbol],
2333
+ getSelectPopperPositionOptions.call(this),
2334
+ );
2335
+ }
2336
+ }).run();
2337
+ }
2338
+
2339
+ function cancelScheduledSelectLayoutCycle() {
2340
+ const globalObject = getGlobal();
2341
+ const frameId = this[layoutCycleFrameSymbol];
2342
+ if (typeof frameId === "number") {
2343
+ if (globalObject?.cancelAnimationFrame instanceof Function) {
2344
+ globalObject.cancelAnimationFrame(frameId);
2345
+ } else {
2346
+ globalObject.clearTimeout(frameId);
2347
+ }
2348
+ }
2349
+
2350
+ delete this[layoutCycleFrameSymbol];
2351
+ this[layoutCycleTokenSymbol] = (this[layoutCycleTokenSymbol] || 0) + 1;
2352
+ delete this[layoutCycleModeSymbol];
2353
+ }
2354
+
2355
+ function flushScheduledSelectLayoutCycle() {
2356
+ if (this[layoutCycleRunningSymbol] === true) {
2357
+ return;
2358
+ }
2359
+
2360
+ const layoutReasons =
2361
+ this[layoutCyclePendingFlagsSymbol] || SELECT_LAYOUT_REASON_ALL;
2362
+ const layoutPriority =
2363
+ this[layoutCyclePendingPrioritySymbol] || SELECT_LAYOUT_PRIORITY_INTERACTIVE;
2364
+
2365
+ delete this[layoutCyclePendingFlagsSymbol];
2366
+ delete this[layoutCyclePendingPrioritySymbol];
2367
+
2368
+ this[layoutCycleRunningSymbol] = true;
2369
+ performSelectLayoutCycle
2370
+ .call(this, layoutReasons)
2371
+ .catch((e) => {
2372
+ addErrorAttribute(this, e);
2373
+ })
2374
+ .finally(() => {
2375
+ delete this[layoutCycleRunningSymbol];
2376
+ if (this[layoutCyclePendingFlagsSymbol]) {
2377
+ scheduleSelectLayoutCycle.call(
2378
+ this,
2379
+ layoutPriority,
2380
+ this[layoutCyclePendingFlagsSymbol],
2381
+ );
2382
+ }
2383
+ });
2384
+ }
2385
+
2386
+ function scheduleSelectLayoutCycle(
2387
+ layoutPriority = SELECT_LAYOUT_PRIORITY_INTERACTIVE,
2388
+ layoutReasons = SELECT_LAYOUT_REASON_ALL,
2389
+ ) {
2390
+ this[layoutCyclePendingFlagsSymbol] =
2391
+ (this[layoutCyclePendingFlagsSymbol] || 0) | layoutReasons;
2392
+ this[layoutCyclePendingPrioritySymbol] = Math.max(
2393
+ this[layoutCyclePendingPrioritySymbol] || 0,
2394
+ layoutPriority,
2395
+ );
2396
+
2397
+ if (this[layoutCycleRunningSymbol] === true) {
2398
+ return;
2399
+ }
2400
+
2401
+ const pendingPriority = this[layoutCyclePendingPrioritySymbol];
2402
+ const currentMode = this[layoutCycleModeSymbol];
2403
+ if (
2404
+ currentMode === "microtask" ||
2405
+ (currentMode === "frame" &&
2406
+ pendingPriority !== SELECT_LAYOUT_PRIORITY_CRITICAL)
2407
+ ) {
2408
+ return;
2409
+ }
2410
+
2411
+ if (pendingPriority === SELECT_LAYOUT_PRIORITY_CRITICAL) {
2412
+ cancelScheduledSelectLayoutCycle.call(this);
2413
+ const token = this[layoutCycleTokenSymbol];
2414
+ this[layoutCycleModeSymbol] = "microtask";
2415
+ queueMicrotask(() => {
2416
+ if (
2417
+ this[layoutCycleModeSymbol] !== "microtask" ||
2418
+ this[layoutCycleTokenSymbol] !== token
2419
+ ) {
2420
+ return;
2421
+ }
2422
+
2423
+ delete this[layoutCycleModeSymbol];
2424
+ flushScheduledSelectLayoutCycle.call(this);
2425
+ });
2426
+ return;
2427
+ }
2428
+
2429
+ const globalObject = getGlobal();
2430
+ const schedule =
2431
+ globalObject?.requestAnimationFrame instanceof Function
2432
+ ? globalObject.requestAnimationFrame.bind(globalObject)
2433
+ : (callback) => {
2434
+ return globalObject.setTimeout(callback, 16);
2435
+ };
2436
+
2437
+ this[layoutCycleModeSymbol] = "frame";
2438
+ const token = (this[layoutCycleTokenSymbol] || 0) + 1;
2439
+ this[layoutCycleTokenSymbol] = token;
2440
+ this[layoutCycleFrameSymbol] = schedule(() => {
2441
+ delete this[layoutCycleFrameSymbol];
2442
+ if (
2443
+ this[layoutCycleModeSymbol] !== "frame" ||
2444
+ this[layoutCycleTokenSymbol] !== token
2445
+ ) {
2446
+ return;
2447
+ }
2448
+
2449
+ delete this[layoutCycleModeSymbol];
2450
+ flushScheduledSelectLayoutCycle.call(this);
2451
+ });
2452
+ }
2453
+
2239
2454
  /**
2240
2455
  * @private
2241
2456
  * @returns {string}
@@ -2327,6 +2542,27 @@ function buildSelectionLabel(value) {
2327
2542
  return map.get(key);
2328
2543
  }
2329
2544
 
2545
+ const options = this.getOption("options");
2546
+ if (isArray(options)) {
2547
+ for (const option of options) {
2548
+ if (!isObject(option) || option.value === undefined) {
2549
+ continue;
2550
+ }
2551
+
2552
+ const optionKey = strict
2553
+ ? option.value
2554
+ : getSelectionStateKey.call(this, option.value);
2555
+ if (optionKey !== key) {
2556
+ continue;
2557
+ }
2558
+
2559
+ if (clearUnresolvedSelectionValue.call(this, value)) {
2560
+ this[lookupCacheSymbol].delete(getSelectionCacheKey.call(this, value));
2561
+ }
2562
+ return option.label;
2563
+ }
2564
+ }
2565
+
2330
2566
  const cacheKey = getSelectionCacheKey.call(this, value);
2331
2567
  if (this[lookupCacheSymbol].has(cacheKey)) {
2332
2568
  return this[lookupCacheSymbol].get(cacheKey);
@@ -2773,19 +3009,21 @@ function setTotalText() {
2773
3009
  }
2774
3010
 
2775
3011
  if (this[isLoadingSymbol] === true) {
2776
- this.setOption("messages.total", "");
3012
+ setRemoteInfoText.call(this, "", {
3013
+ reserveSpace: true,
3014
+ });
2777
3015
  return;
2778
3016
  }
2779
3017
 
2780
3018
  const count = this.getOption("options").length;
2781
3019
  if (count === 0) {
2782
- this.setOption("messages.total", "");
3020
+ setRemoteInfoText.call(this, "");
2783
3021
  return;
2784
3022
  }
2785
3023
 
2786
3024
  const total = Number.parseInt(this.getOption("total"));
2787
3025
  if (Number.isNaN(total)) {
2788
- this.setOption("messages.total", "");
3026
+ setRemoteInfoText.call(this, "");
2789
3027
  return;
2790
3028
  }
2791
3029
 
@@ -2793,7 +3031,7 @@ function setTotalText() {
2793
3031
 
2794
3032
  const diff = total - count;
2795
3033
  if (diff < 0) {
2796
- this.setOption("messages.total", "");
3034
+ setRemoteInfoText.call(this, "");
2797
3035
  return;
2798
3036
  }
2799
3037
  const text = translations.getPluralRuleText("total", diff, "");
@@ -2801,7 +3039,7 @@ function setTotalText() {
2801
3039
  count: String(diff),
2802
3040
  }).format(text);
2803
3041
 
2804
- this.setOption("messages.total", selectedText);
3042
+ setRemoteInfoText.call(this, selectedText);
2805
3043
  }
2806
3044
 
2807
3045
  /**
@@ -3361,12 +3599,16 @@ function filterOptions() {
3361
3599
  }
3362
3600
  }
3363
3601
  })
3364
- .run()
3365
- .then(() => {
3366
- new Processing(100, () => {
3367
- calcAndSetOptionsDimension.call(this);
3368
- focusFilter.call(this);
3369
- })
3602
+ .run()
3603
+ .then(() => {
3604
+ new Processing(100, () => {
3605
+ scheduleSelectLayoutCycle.call(
3606
+ this,
3607
+ SELECT_LAYOUT_PRIORITY_CRITICAL,
3608
+ SELECT_LAYOUT_REASON_PAGINATION | SELECT_LAYOUT_REASON_POSITION,
3609
+ );
3610
+ focusFilter.call(this);
3611
+ })
3370
3612
  .run()
3371
3613
  .catch((e) => {
3372
3614
  addErrorAttribute(this, e);
@@ -4537,12 +4779,13 @@ function show() {
4537
4779
  } else {
4538
4780
  initTotal.call(self);
4539
4781
  }
4540
- calcAndSetOptionsDimension.call(this);
4541
- focusFilter.call(this);
4542
- this[popperElementSymbol].style.removeProperty("visibility");
4543
- refreshSelectPaginationLayout.call(this);
4544
- updatePopper.call(this);
4545
- })
4782
+ focusFilter.call(this);
4783
+ return performSelectLayoutCycle
4784
+ .call(this, SELECT_LAYOUT_REASON_ALL)
4785
+ .then(() => {
4786
+ this[popperElementSymbol].style.removeProperty("visibility");
4787
+ });
4788
+ })
4546
4789
  .run()
4547
4790
  .catch((e) => {
4548
4791
  addErrorAttribute(this, e);
@@ -4627,7 +4870,9 @@ function initTotal() {
4627
4870
  }
4628
4871
 
4629
4872
  const fetchOptions = this.getOption("fetch", {});
4630
- this.setOption("messages.total", "");
4873
+ setRemoteInfoText.call(this, "", {
4874
+ reserveSpace: true,
4875
+ });
4631
4876
 
4632
4877
  const remoteInfoRequest = getGlobal()
4633
4878
  .fetch(url, fetchOptions)
@@ -4684,7 +4929,7 @@ function resetPaginationState(clearTotalMessage = true) {
4684
4929
  paginationElement.setOption("currentPage", null);
4685
4930
  paginationElement.setOption("objectsPerPage", null);
4686
4931
  if (clearTotalMessage === true) {
4687
- this.setOption("messages.total", "");
4932
+ setRemoteInfoText.call(this, "");
4688
4933
  }
4689
4934
  }
4690
4935
 
@@ -4695,13 +4940,21 @@ function clearOptionsOnError() {
4695
4940
 
4696
4941
  function refreshSelectPaginationLayout() {
4697
4942
  const paginationElement = this[paginationElementSymbol];
4698
- if (!paginationElement || typeof paginationElement.getOption !== "function") {
4943
+ if (!paginationElement) {
4944
+ return;
4945
+ }
4946
+
4947
+ if (typeof paginationElement.refreshLayout === "function") {
4948
+ paginationElement.refreshLayout();
4949
+ return;
4950
+ }
4951
+
4952
+ if (typeof paginationElement.getOption !== "function") {
4699
4953
  return;
4700
4954
  }
4701
4955
 
4702
4956
  const currentPage = paginationElement.getOption("currentPage");
4703
4957
  const totalPages = paginationElement.getOption("pages");
4704
-
4705
4958
  if (!isInteger(currentPage) || !isInteger(totalPages)) {
4706
4959
  return;
4707
4960
  }
@@ -4959,15 +5212,17 @@ function initEventHandler() {
4959
5212
  }
4960
5213
  }
4961
5214
 
4962
- this[debounceOptionsMutationObserverSymbol] = new DeadMansSwitch(
4963
- 100,
4964
- () => {
4965
- checkOptionState.call(self);
4966
- calcAndSetOptionsDimension.call(self);
4967
- updatePopper.call(self);
4968
- delete this[debounceOptionsMutationObserverSymbol];
4969
- },
4970
- );
5215
+ this[debounceOptionsMutationObserverSymbol] = new DeadMansSwitch(
5216
+ 100,
5217
+ () => {
5218
+ scheduleSelectLayoutCycle.call(
5219
+ self,
5220
+ SELECT_LAYOUT_PRIORITY_INTERACTIVE,
5221
+ SELECT_LAYOUT_REASON_ALL,
5222
+ );
5223
+ delete this[debounceOptionsMutationObserverSymbol];
5224
+ },
5225
+ );
4971
5226
  };
4972
5227
 
4973
5228
  const observer = new MutationObserver(callback);
@@ -5140,7 +5395,7 @@ function initControlReferences() {
5140
5395
  `[${ATTRIBUTE_ROLE}=popper]`,
5141
5396
  );
5142
5397
  this[popperElementSymbol].monsterBeforeFloatingUpdate = () => {
5143
- calcAndSetOptionsDimension.call(this);
5398
+ applySelectLayoutState.call(this, SELECT_LAYOUT_REASON_PAGINATION);
5144
5399
  };
5145
5400
  this[inlineFilterElementSymbol] = this.shadowRoot.querySelector(
5146
5401
  `[${ATTRIBUTE_ROLE}=filter][name="inline-filter"]`,
@@ -5170,31 +5425,11 @@ function initControlReferences() {
5170
5425
  * @private
5171
5426
  */
5172
5427
  function updatePopper() {
5173
- if (!isPositionedPopperOpen(this[popperElementSymbol])) {
5174
- return;
5175
- }
5176
-
5177
- if (this.getOption("disabled", false) === true) {
5178
- return;
5179
- }
5180
-
5181
- new Processing(() => {
5182
- calcAndSetOptionsDimension.call(this);
5183
- positionPopper.call(
5184
- this,
5185
- this[controlElementSymbol],
5186
- this[popperElementSymbol],
5187
- getSelectPopperPositionOptions.call(this),
5188
- );
5189
- requestAnimationFrame(() => {
5190
- refreshSelectPaginationLayout.call(this);
5191
- });
5192
- })
5193
- .run()
5194
- .catch((e) => {
5195
- addErrorAttribute(this, e);
5196
- });
5197
-
5428
+ scheduleSelectLayoutCycle.call(
5429
+ this,
5430
+ SELECT_LAYOUT_PRIORITY_INTERACTIVE,
5431
+ SELECT_LAYOUT_REASON_ALL,
5432
+ );
5198
5433
  return this;
5199
5434
  }
5200
5435
 
@@ -41,6 +41,7 @@ const settlingFrameMap = new WeakMap();
41
41
  const floatingResizeObserverMap = new WeakMap();
42
42
  const floatingSyncCycleMap = new WeakMap();
43
43
  const floatingAppearanceFrameMap = new WeakMap();
44
+ const floatingAppearanceTimeoutMap = new WeakMap();
44
45
 
45
46
  /**
46
47
  * @private
@@ -1048,6 +1049,15 @@ function scheduleFloatingAppearanceOpen(popperElement) {
1048
1049
  return;
1049
1050
  }
1050
1051
 
1052
+ const finishAppearanceOpen = () => {
1053
+ cancelFloatingAppearanceFrame(popperElement);
1054
+
1055
+ if (!isPositionedPopperOpen(popperElement)) {
1056
+ return;
1057
+ }
1058
+
1059
+ popperElement.dataset.monsterAppearance = "open";
1060
+ };
1051
1061
  const schedule =
1052
1062
  typeof requestAnimationFrame === "function"
1053
1063
  ? requestAnimationFrame
@@ -1055,35 +1065,40 @@ function scheduleFloatingAppearanceOpen(popperElement) {
1055
1065
  return setTimeout(callback, 16);
1056
1066
  };
1057
1067
  const frameId = schedule(() => {
1058
- floatingAppearanceFrameMap.delete(popperElement);
1059
-
1060
- if (!isPositionedPopperOpen(popperElement)) {
1061
- return;
1062
- }
1063
-
1064
- popperElement.dataset.monsterAppearance = "open";
1068
+ finishAppearanceOpen();
1065
1069
  });
1070
+ const timeoutId = setTimeout(() => {
1071
+ finishAppearanceOpen();
1072
+ }, 24);
1066
1073
 
1067
1074
  floatingAppearanceFrameMap.set(popperElement, frameId);
1075
+ floatingAppearanceTimeoutMap.set(popperElement, timeoutId);
1068
1076
  }
1069
1077
 
1070
1078
  function cancelFloatingAppearanceFrame(popperElement) {
1071
1079
  const frameId = floatingAppearanceFrameMap.get(popperElement);
1072
1080
  if (
1073
- frameId === undefined ||
1074
- frameId === null ||
1075
- Number.isNaN(frameId) === true
1081
+ frameId !== undefined &&
1082
+ frameId !== null &&
1083
+ Number.isNaN(frameId) === false
1076
1084
  ) {
1077
- return;
1085
+ if (typeof cancelAnimationFrame === "function") {
1086
+ cancelAnimationFrame(frameId);
1087
+ } else {
1088
+ clearTimeout(frameId);
1089
+ }
1078
1090
  }
1091
+ floatingAppearanceFrameMap.delete(popperElement);
1079
1092
 
1080
- if (typeof cancelAnimationFrame === "function") {
1081
- cancelAnimationFrame(frameId);
1082
- } else {
1083
- clearTimeout(frameId);
1093
+ const timeoutId = floatingAppearanceTimeoutMap.get(popperElement);
1094
+ if (
1095
+ timeoutId !== undefined &&
1096
+ timeoutId !== null &&
1097
+ Number.isNaN(timeoutId) === false
1098
+ ) {
1099
+ clearTimeout(timeoutId);
1084
1100
  }
1085
-
1086
- floatingAppearanceFrameMap.delete(popperElement);
1101
+ floatingAppearanceTimeoutMap.delete(popperElement);
1087
1102
  }
1088
1103
 
1089
1104
  function applyFloatingArrowStyles(arrowElement, placement, arrowData) {
@@ -235,4 +235,44 @@ describe("PopperButton", function () {
235
235
  }
236
236
  }, 0);
237
237
  });
238
+
239
+ it("should finish the opening appearance state when requestAnimationFrame stalls", function (done) {
240
+ let mocks = document.getElementById("mocks");
241
+ const button = document.createElement("monster-popper-button");
242
+ const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
243
+ const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
244
+
245
+ globalThis.requestAnimationFrame = () => 1;
246
+ globalThis.cancelAnimationFrame = () => {};
247
+ mocks.appendChild(button);
248
+
249
+ setTimeout(() => {
250
+ try {
251
+ button.showDialog();
252
+
253
+ const popper = button.shadowRoot.querySelector(
254
+ '[data-monster-role="popper"]',
255
+ );
256
+ expect(popper).to.exist;
257
+ expect(popper.dataset.monsterAppearance).to.equal("opening");
258
+
259
+ setTimeout(() => {
260
+ try {
261
+ expect(popper.dataset.monsterAppearance).to.equal("open");
262
+ button.hideDialog();
263
+ done();
264
+ } catch (e) {
265
+ done(e);
266
+ } finally {
267
+ globalThis.requestAnimationFrame = originalRequestAnimationFrame;
268
+ globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
269
+ }
270
+ }, 30);
271
+ } catch (e) {
272
+ globalThis.requestAnimationFrame = originalRequestAnimationFrame;
273
+ globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
274
+ done(e);
275
+ }
276
+ }, 0);
277
+ });
238
278
  });
@@ -47,6 +47,18 @@ function createJsonResponse(data, status = 200) {
47
47
  });
48
48
  }
49
49
 
50
+ function createDeferred() {
51
+ let resolve;
52
+ let reject;
53
+
54
+ const promise = new Promise((res, rej) => {
55
+ resolve = res;
56
+ reject = rej;
57
+ });
58
+
59
+ return {promise, resolve, reject};
60
+ }
61
+
50
62
  function waitForCondition(check, {timeout = 4000, interval = 25} = {}) {
51
63
  return new Promise((resolve, reject) => {
52
64
  const start = Date.now();
@@ -1141,6 +1153,195 @@ describe('Select', function () {
1141
1153
  expect(select.getOption('messages.total')).to.contain('No additional entries are available');
1142
1154
  });
1143
1155
 
1156
+ it('should keep the remote-info footer height stable while a remote page request is pending', async function () {
1157
+ this.timeout(4000);
1158
+
1159
+ let mocks = document.getElementById('mocks');
1160
+ const firstRequest = createDeferred();
1161
+ const secondRequest = createDeferred();
1162
+ let requestCount = 0;
1163
+
1164
+ global['fetch'] = function () {
1165
+ requestCount += 1;
1166
+
1167
+ if (requestCount === 1) {
1168
+ return firstRequest.promise;
1169
+ }
1170
+
1171
+ if (requestCount === 2) {
1172
+ return secondRequest.promise;
1173
+ }
1174
+
1175
+ return Promise.reject(new Error('unexpected fetch request'));
1176
+ };
1177
+
1178
+ const select = document.createElement('monster-select');
1179
+ select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
1180
+ select.setOption('filter.mode', 'remote');
1181
+ select.setOption('filter.position', 'popper');
1182
+ select.setOption('mapping.selector', 'items.*');
1183
+ select.setOption('mapping.labelTemplate', '${name}');
1184
+ select.setOption('mapping.valueTemplate', '${id}');
1185
+ select.setOption('mapping.total', 'pagination.total');
1186
+ select.setOption('mapping.currentPage', 'pagination.page');
1187
+ select.setOption('mapping.objectsPerPage', 'pagination.perPage');
1188
+ mocks.appendChild(select);
1189
+
1190
+ await waitForCondition(() => {
1191
+ return select.shadowRoot.querySelector('[data-monster-role=container]') instanceof HTMLElement;
1192
+ });
1193
+
1194
+ const firstFetch = select.fetch('https://example.com/items?filter=*&page=1');
1195
+ firstRequest.resolve(await createJsonResponse({
1196
+ items: [
1197
+ {id: 'alpha', name: 'Alpha'}
1198
+ ],
1199
+ pagination: {
1200
+ total: 12,
1201
+ page: 1,
1202
+ perPage: 1
1203
+ }
1204
+ }));
1205
+ await firstFetch;
1206
+
1207
+ await waitForCondition(() => {
1208
+ return select.getOption('messages.total').includes('additional entries are available');
1209
+ });
1210
+
1211
+ const container = select.shadowRoot.querySelector('[data-monster-role=container]');
1212
+ const remoteInfoElement = select.shadowRoot.querySelector('[data-monster-role=remote-info]');
1213
+
1214
+ container.click();
1215
+
1216
+ await waitForCondition(() => {
1217
+ return select.shadowRoot
1218
+ .querySelector('[data-monster-role=control]')
1219
+ .classList
1220
+ .contains('open');
1221
+ });
1222
+
1223
+ expect(remoteInfoElement.style.visibility).to.equal('');
1224
+
1225
+ const secondFetch = select.fetch('https://example.com/items?filter=*&page=2');
1226
+
1227
+ await waitForCondition(() => {
1228
+ return remoteInfoElement.style.visibility === 'hidden';
1229
+ });
1230
+
1231
+ expect(select.getOption('messages.total')).to.contain('additional entries are available');
1232
+
1233
+ secondRequest.resolve(await createJsonResponse({
1234
+ items: [
1235
+ {id: 'beta', name: 'Beta'}
1236
+ ],
1237
+ pagination: {
1238
+ total: 12,
1239
+ page: 2,
1240
+ perPage: 1
1241
+ }
1242
+ }));
1243
+
1244
+ await secondFetch;
1245
+
1246
+ await waitForCondition(() => {
1247
+ return remoteInfoElement.style.visibility === '';
1248
+ });
1249
+
1250
+ expect(select.getOption('messages.total')).to.contain('additional entries are available');
1251
+ });
1252
+
1253
+ it('should clear preserved remote-info text after a later empty remote result settles', async function () {
1254
+ this.timeout(4000);
1255
+
1256
+ let mocks = document.getElementById('mocks');
1257
+ const firstRequest = createDeferred();
1258
+ const secondRequest = createDeferred();
1259
+ let requestCount = 0;
1260
+
1261
+ global['fetch'] = function () {
1262
+ requestCount += 1;
1263
+
1264
+ if (requestCount === 1) {
1265
+ return firstRequest.promise;
1266
+ }
1267
+
1268
+ if (requestCount === 2) {
1269
+ return secondRequest.promise;
1270
+ }
1271
+
1272
+ return Promise.reject(new Error('unexpected fetch request'));
1273
+ };
1274
+
1275
+ const select = document.createElement('monster-select');
1276
+ select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
1277
+ select.setOption('filter.mode', 'remote');
1278
+ select.setOption('filter.position', 'popper');
1279
+ select.setOption('mapping.selector', 'items.*');
1280
+ select.setOption('mapping.labelTemplate', '${name}');
1281
+ select.setOption('mapping.valueTemplate', '${id}');
1282
+ select.setOption('mapping.total', 'pagination.total');
1283
+ select.setOption('mapping.currentPage', 'pagination.page');
1284
+ select.setOption('mapping.objectsPerPage', 'pagination.perPage');
1285
+ mocks.appendChild(select);
1286
+
1287
+ await waitForCondition(() => {
1288
+ return select.shadowRoot.querySelector('[data-monster-role=container]') instanceof HTMLElement;
1289
+ });
1290
+
1291
+ const firstFetch = select.fetch('https://example.com/items?filter=*&page=1');
1292
+ firstRequest.resolve(await createJsonResponse({
1293
+ items: [
1294
+ {id: 'alpha', name: 'Alpha'}
1295
+ ],
1296
+ pagination: {
1297
+ total: 12,
1298
+ page: 1,
1299
+ perPage: 1
1300
+ }
1301
+ }));
1302
+ await firstFetch;
1303
+
1304
+ await waitForCondition(() => {
1305
+ return select.getOption('messages.total').includes('additional entries are available');
1306
+ });
1307
+
1308
+ const container = select.shadowRoot.querySelector('[data-monster-role=container]');
1309
+ const remoteInfoElement = select.shadowRoot.querySelector('[data-monster-role=remote-info]');
1310
+
1311
+ container.click();
1312
+
1313
+ await waitForCondition(() => {
1314
+ return select.shadowRoot
1315
+ .querySelector('[data-monster-role=control]')
1316
+ .classList
1317
+ .contains('open');
1318
+ });
1319
+
1320
+ const secondFetch = select.fetch('https://example.com/items?filter=leer&page=1');
1321
+
1322
+ await waitForCondition(() => {
1323
+ return remoteInfoElement.style.visibility === 'hidden';
1324
+ });
1325
+
1326
+ secondRequest.resolve(await createJsonResponse({
1327
+ items: [],
1328
+ pagination: {
1329
+ total: 0,
1330
+ page: 1,
1331
+ perPage: 1
1332
+ }
1333
+ }));
1334
+
1335
+ await secondFetch;
1336
+
1337
+ await waitForCondition(() => {
1338
+ return select.getOption('messages.total') === '';
1339
+ });
1340
+
1341
+ expect(remoteInfoElement.style.visibility).to.equal('');
1342
+ expect(select.getOption('messages.total')).to.equal('');
1343
+ });
1344
+
1144
1345
  it('should avoid duplicate remote-info badges for empty remote filter results', async function () {
1145
1346
  this.timeout(4000);
1146
1347