web-mojo 2.6.4 → 2.7.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/CHANGELOG.md +26 -0
- package/dist/admin-models.cjs.js +1 -1
- package/dist/admin-models.es.js +1 -1
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.es.js +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -1
- package/dist/chunks/ChatView-B76p_4u8.js +2 -0
- package/dist/chunks/ChatView-B76p_4u8.js.map +1 -0
- package/dist/chunks/ChatView-CC4SaAGA.js +2 -0
- package/dist/chunks/ChatView-CC4SaAGA.js.map +1 -0
- package/dist/chunks/{ContextMenu-3_bOvSwa.js → ContextMenu-B6u-y5X8.js} +2 -2
- package/dist/chunks/ContextMenu-B6u-y5X8.js.map +1 -0
- package/dist/chunks/{ContextMenu-xjj8Quls.js → ContextMenu-DkIc9twn.js} +2 -2
- package/dist/chunks/ContextMenu-DkIc9twn.js.map +1 -0
- package/dist/chunks/{DataView-CdyrhKrD.js → DataView-CnbbvE5S.js} +2 -2
- package/dist/chunks/{DataView-CdyrhKrD.js.map → DataView-CnbbvE5S.js.map} +1 -1
- package/dist/chunks/{DataView-B7XoPBZd.js → DataView-CvXW1Ewe.js} +2 -2
- package/dist/chunks/{DataView-B7XoPBZd.js.map → DataView-CvXW1Ewe.js.map} +1 -1
- package/dist/chunks/FormView-BMjKplJy.js +2 -0
- package/dist/chunks/FormView-BMjKplJy.js.map +1 -0
- package/dist/chunks/FormView-geCsNsn1.js +2 -0
- package/dist/chunks/FormView-geCsNsn1.js.map +1 -0
- package/dist/chunks/ListView-B6cE3WKB.js +2 -0
- package/dist/chunks/ListView-B6cE3WKB.js.map +1 -0
- package/dist/chunks/ListView-DXfD53sU.js +2 -0
- package/dist/chunks/ListView-DXfD53sU.js.map +1 -0
- package/dist/chunks/{MetricsCountryMapView-9e2t5sfH.js → MetricsCountryMapView-BuFNN1Ey.js} +2 -2
- package/dist/chunks/{MetricsCountryMapView-9e2t5sfH.js.map → MetricsCountryMapView-BuFNN1Ey.js.map} +1 -1
- package/dist/chunks/{MetricsCountryMapView-ki6auFBe.js → MetricsCountryMapView-Dse3HsUv.js} +2 -2
- package/dist/chunks/{MetricsCountryMapView-ki6auFBe.js.map → MetricsCountryMapView-Dse3HsUv.js.map} +1 -1
- package/dist/chunks/{Modal-Bbn-54-6.js → Modal-BmXvplWi.js} +3 -3
- package/dist/chunks/Modal-BmXvplWi.js.map +1 -0
- package/dist/chunks/{Modal-BRmaTaWa.js → Modal-DRrfsnNP.js} +3 -3
- package/dist/chunks/Modal-DRrfsnNP.js.map +1 -0
- package/dist/chunks/Passkeys-D2LFSJpQ.js +2 -0
- package/dist/chunks/Passkeys-D2LFSJpQ.js.map +1 -0
- package/dist/chunks/Passkeys-vneS9ywK.js +2 -0
- package/dist/chunks/Passkeys-vneS9ywK.js.map +1 -0
- package/dist/chunks/{TokenManager-qlmZ7eW3.js → TokenManager-DBwv-cF4.js} +2 -2
- package/dist/chunks/{TokenManager-qlmZ7eW3.js.map → TokenManager-DBwv-cF4.js.map} +1 -1
- package/dist/chunks/{TokenManager-BkW1MQ1m.js → TokenManager-DXtiygmr.js} +2 -2
- package/dist/chunks/{TokenManager-BkW1MQ1m.js.map → TokenManager-DXtiygmr.js.map} +1 -1
- package/dist/chunks/User-DJHaSCL9.js +2 -0
- package/dist/chunks/User-DJHaSCL9.js.map +1 -0
- package/dist/chunks/User-ao-iRfPc.js +2 -0
- package/dist/chunks/User-ao-iRfPc.js.map +1 -0
- package/dist/chunks/{UserProfileView-Dbei45vn.js → UserProfileView-DRpHVJtE.js} +2 -2
- package/dist/chunks/{UserProfileView-Dbei45vn.js.map → UserProfileView-DRpHVJtE.js.map} +1 -1
- package/dist/chunks/{UserProfileView-OcqSynHt.js → UserProfileView-DtIBSw3O.js} +2 -2
- package/dist/chunks/{UserProfileView-OcqSynHt.js.map → UserProfileView-DtIBSw3O.js.map} +1 -1
- package/dist/chunks/{WebApp-CIhrSvuV.js → WebApp-7COAyECf.js} +2 -2
- package/dist/chunks/{WebApp-CIhrSvuV.js.map → WebApp-7COAyECf.js.map} +1 -1
- package/dist/chunks/{WebApp-DAviV-_c.js → WebApp-CSvy-GUm.js} +2 -2
- package/dist/chunks/{WebApp-DAviV-_c.js.map → WebApp-CSvy-GUm.js.map} +1 -1
- package/dist/chunks/{admin-models-BBmATRhV.js → admin-models-DHXtrq87.js} +2 -2
- package/dist/chunks/{admin-models-BBmATRhV.js.map → admin-models-DHXtrq87.js.map} +1 -1
- package/dist/chunks/{admin-models-C-SHgZGn.js → admin-models-vhiH86Tq.js} +2 -2
- package/dist/chunks/{admin-models-C-SHgZGn.js.map → admin-models-vhiH86Tq.js.map} +1 -1
- package/dist/chunks/{exportChart-2I2hxr7t.js → exportChart-B1pjkCdm.js} +2 -2
- package/dist/chunks/{exportChart-2I2hxr7t.js.map → exportChart-B1pjkCdm.js.map} +1 -1
- package/dist/chunks/{exportChart-B5optjiT.js → exportChart-Vq4_f06o.js} +2 -2
- package/dist/chunks/{exportChart-B5optjiT.js.map → exportChart-Vq4_f06o.js.map} +1 -1
- package/dist/chunks/{index-B543iMCu.js → index-Bj5AxsEG.js} +2 -2
- package/dist/chunks/{index-B543iMCu.js.map → index-Bj5AxsEG.js.map} +1 -1
- package/dist/chunks/{index-C6reyIyu.js → index-DrmdnYWO.js} +2 -2
- package/dist/chunks/{index-C6reyIyu.js.map → index-DrmdnYWO.js.map} +1 -1
- package/dist/chunks/{version-D_d_sDRM.js → version-CSb8B3nS.js} +2 -2
- package/dist/chunks/{version-D_d_sDRM.js.map → version-CSb8B3nS.js.map} +1 -1
- package/dist/chunks/{version-DWWDfVB9.js → version-_kAV0pF7.js} +2 -2
- package/dist/chunks/{version-DWWDfVB9.js.map → version-_kAV0pF7.js.map} +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.es.js +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -1
- package/dist/map.cjs.js +1 -1
- package/dist/map.es.js +1 -1
- package/dist/timeline.cjs.js +1 -1
- package/dist/timeline.es.js +1 -1
- package/dist/user-profile.cjs.js +1 -1
- package/dist/user-profile.es.js +1 -1
- package/dist/web-mojo.lite.iife.js +1406 -43
- package/dist/web-mojo.lite.iife.js.map +1 -1
- package/dist/web-mojo.lite.iife.min.js +336 -95
- package/dist/web-mojo.lite.iife.min.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunks/ChatView-C1Vc-P_O.js +0 -2
- package/dist/chunks/ChatView-C1Vc-P_O.js.map +0 -1
- package/dist/chunks/ChatView-Cv6kH3MO.js +0 -2
- package/dist/chunks/ChatView-Cv6kH3MO.js.map +0 -1
- package/dist/chunks/ContextMenu-3_bOvSwa.js.map +0 -1
- package/dist/chunks/ContextMenu-xjj8Quls.js.map +0 -1
- package/dist/chunks/FormView-BE4qZvNA.js +0 -2
- package/dist/chunks/FormView-BE4qZvNA.js.map +0 -1
- package/dist/chunks/FormView-BqR4x5nz.js +0 -2
- package/dist/chunks/FormView-BqR4x5nz.js.map +0 -1
- package/dist/chunks/ListView-BF442PdR.js +0 -2
- package/dist/chunks/ListView-BF442PdR.js.map +0 -1
- package/dist/chunks/ListView-DqfZmgXS.js +0 -2
- package/dist/chunks/ListView-DqfZmgXS.js.map +0 -1
- package/dist/chunks/Modal-BRmaTaWa.js.map +0 -1
- package/dist/chunks/Modal-Bbn-54-6.js.map +0 -1
- package/dist/chunks/Passkeys-B_bxdIVa.js +0 -2
- package/dist/chunks/Passkeys-B_bxdIVa.js.map +0 -1
- package/dist/chunks/Passkeys-LRaiCx3H.js +0 -2
- package/dist/chunks/Passkeys-LRaiCx3H.js.map +0 -1
- package/dist/chunks/User-B7jwnBx2.js +0 -2
- package/dist/chunks/User-B7jwnBx2.js.map +0 -1
- package/dist/chunks/User-Dbw_wZAz.js +0 -2
- package/dist/chunks/User-Dbw_wZAz.js.map +0 -1
|
@@ -4693,6 +4693,7 @@ var MOJO = (function(exports) {
|
|
|
4693
4693
|
this.template = opts.template || opts.templateUrl || "";
|
|
4694
4694
|
this.data = opts.data ?? {};
|
|
4695
4695
|
this.isRendering = false;
|
|
4696
|
+
this._renderQueued = false;
|
|
4696
4697
|
this.lastRenderTime = 0;
|
|
4697
4698
|
this.mounted = false;
|
|
4698
4699
|
this.debug = opts.debug ?? false;
|
|
@@ -4854,6 +4855,11 @@ var MOJO = (function(exports) {
|
|
|
4854
4855
|
// ---------------------------------------------
|
|
4855
4856
|
async render(allowMount = true, container = null) {
|
|
4856
4857
|
const now = Date.now();
|
|
4858
|
+
if (this.isRendering) {
|
|
4859
|
+
this._renderQueued = true;
|
|
4860
|
+
this._renderQueuedArgs = { allowMount, container };
|
|
4861
|
+
return this;
|
|
4862
|
+
}
|
|
4857
4863
|
if (!this.canRender()) {
|
|
4858
4864
|
return this;
|
|
4859
4865
|
}
|
|
@@ -4879,6 +4885,12 @@ var MOJO = (function(exports) {
|
|
|
4879
4885
|
} finally {
|
|
4880
4886
|
this.isRendering = false;
|
|
4881
4887
|
}
|
|
4888
|
+
if (this._renderQueued) {
|
|
4889
|
+
this._renderQueued = false;
|
|
4890
|
+
const queued = this._renderQueuedArgs || { allowMount: true, container: null };
|
|
4891
|
+
this._renderQueuedArgs = null;
|
|
4892
|
+
await this.render(queued.allowMount, queued.container);
|
|
4893
|
+
}
|
|
4882
4894
|
return this;
|
|
4883
4895
|
}
|
|
4884
4896
|
async _renderChildren() {
|
|
@@ -5262,9 +5274,7 @@ var MOJO = (function(exports) {
|
|
|
5262
5274
|
*/
|
|
5263
5275
|
escapeHtml(str) {
|
|
5264
5276
|
if (typeof str !== "string") return str;
|
|
5265
|
-
|
|
5266
|
-
div.textContent = str;
|
|
5267
|
-
return div.innerHTML;
|
|
5277
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5268
5278
|
}
|
|
5269
5279
|
contains(el) {
|
|
5270
5280
|
if (typeof el === "string") {
|
|
@@ -5654,19 +5664,19 @@ var MOJO = (function(exports) {
|
|
|
5654
5664
|
if (menuItems.length === 0) {
|
|
5655
5665
|
return this.closeButton ? '<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>' : "";
|
|
5656
5666
|
}
|
|
5657
|
-
const triggerIcon = this.contextMenu.icon || "bi-three-dots-vertical";
|
|
5658
|
-
const buttonClass = this.contextMenu.buttonClass || "btn btn-link p-1 mojo-modal-context-menu-btn";
|
|
5667
|
+
const triggerIcon = this.escapeHtml(this.contextMenu.icon || "bi-three-dots-vertical");
|
|
5668
|
+
const buttonClass = this.escapeHtml(this.contextMenu.buttonClass || "btn btn-link p-1 mojo-modal-context-menu-btn");
|
|
5659
5669
|
const menuItemsHtml = menuItems.map((item) => {
|
|
5660
5670
|
if (item.type === "divider") {
|
|
5661
5671
|
return '<li><hr class="dropdown-divider"></li>';
|
|
5662
5672
|
}
|
|
5663
|
-
const icon = item.icon ? `<i class="${item.icon} me-2"></i>` : "";
|
|
5664
|
-
const label = item.label || "";
|
|
5673
|
+
const icon = item.icon ? `<i class="${this.escapeHtml(item.icon)} me-2"></i>` : "";
|
|
5674
|
+
const label = this.escapeHtml(item.label || "");
|
|
5665
5675
|
if (item.href) {
|
|
5666
|
-
return `<li><a class="dropdown-item" href="${item.href}"${item.target ? ` target="${item.target}"` : ""}>${icon}${label}</a></li>`;
|
|
5676
|
+
return `<li><a class="dropdown-item" href="${this.escapeHtml(item.href)}"${item.target ? ` target="${this.escapeHtml(item.target)}"` : ""}>${icon}${label}</a></li>`;
|
|
5667
5677
|
} else if (item.action) {
|
|
5668
|
-
const dataAttrs = Object.keys(item).filter((key) => key.startsWith("data-")).map((key) => `${key}="${item[key]}"`).join(" ");
|
|
5669
|
-
return `<li><a class="dropdown-item" data-action="${item.action}" ${dataAttrs}>${icon}${label}</a></li>`;
|
|
5678
|
+
const dataAttrs = Object.keys(item).filter((key) => key.startsWith("data-")).map((key) => `${key}="${this.escapeHtml(item[key])}"`).join(" ");
|
|
5679
|
+
return `<li><a class="dropdown-item" data-action="${this.escapeHtml(item.action)}" ${dataAttrs}>${icon}${label}</a></li>`;
|
|
5670
5680
|
}
|
|
5671
5681
|
return "";
|
|
5672
5682
|
}).join("");
|
|
@@ -14555,9 +14565,7 @@ var MOJO = (function(exports) {
|
|
|
14555
14565
|
*/
|
|
14556
14566
|
escapeHtml(str) {
|
|
14557
14567
|
if (str == null) return "";
|
|
14558
|
-
|
|
14559
|
-
div.textContent = String(str);
|
|
14560
|
-
return div.innerHTML;
|
|
14568
|
+
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
14561
14569
|
}
|
|
14562
14570
|
/**
|
|
14563
14571
|
* Render a tooltip icon for a field
|
|
@@ -15282,9 +15290,7 @@ var MOJO = (function(exports) {
|
|
|
15282
15290
|
*/
|
|
15283
15291
|
escapeHtml(str) {
|
|
15284
15292
|
if (str == null) return "";
|
|
15285
|
-
|
|
15286
|
-
div.textContent = String(str);
|
|
15287
|
-
return div.innerHTML;
|
|
15293
|
+
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
15288
15294
|
}
|
|
15289
15295
|
/**
|
|
15290
15296
|
* Get form value (for integration with forms)
|
|
@@ -20372,9 +20378,7 @@ var MOJO = (function(exports) {
|
|
|
20372
20378
|
*/
|
|
20373
20379
|
escapeHtml(str) {
|
|
20374
20380
|
if (str == null) return "";
|
|
20375
|
-
|
|
20376
|
-
div.textContent = String(str);
|
|
20377
|
-
return div.innerHTML;
|
|
20381
|
+
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
20378
20382
|
}
|
|
20379
20383
|
/**
|
|
20380
20384
|
* Escape regex special characters
|
|
@@ -23331,6 +23335,7 @@ var MOJO = (function(exports) {
|
|
|
23331
23335
|
this._toolbarRightMounted = false;
|
|
23332
23336
|
this.dayRangeFilter = this._normalizeDayRangeFilter(options.dayRangeFilter);
|
|
23333
23337
|
this.dayRangeControl = null;
|
|
23338
|
+
this.filterPresets = this._normalizeFilterPresets(options.filterPresets);
|
|
23334
23339
|
this.exportOptions = options.exportOptions || null;
|
|
23335
23340
|
this.exportSource = options.exportSource || "remote";
|
|
23336
23341
|
this.persistSelection = options.persistSelection === void 0 ? this.paginationMode === "more" : options.persistSelection === true;
|
|
@@ -23352,6 +23357,17 @@ var MOJO = (function(exports) {
|
|
|
23352
23357
|
this.groupHeaderStyle = ListView.GROUP_HEADER_STYLES.includes(options.groupHeaderStyle) ? options.groupHeaderStyle : "banner";
|
|
23353
23358
|
this.groupHeaderViews = /* @__PURE__ */ new Map();
|
|
23354
23359
|
this._renderOrder = [];
|
|
23360
|
+
this.emptyState = this._normalizeEmptyState(options.emptyState);
|
|
23361
|
+
this.loadingStyle = options.loadingStyle === "spinner" || options.loadingStyle === "default" ? "spinner" : "skeleton";
|
|
23362
|
+
this.showResultCount = options.showResultCount === true;
|
|
23363
|
+
this._autoRefreshMs = this._normalizeAutoRefresh(options.autoRefresh);
|
|
23364
|
+
this._autoRefreshTimer = null;
|
|
23365
|
+
this._autoRefreshPaused = false;
|
|
23366
|
+
this._autoRefreshHandlers = null;
|
|
23367
|
+
this.autoRefreshIndicator = options.autoRefreshIndicator !== false;
|
|
23368
|
+
this._autoRefreshPulseTimer = null;
|
|
23369
|
+
this.persistState = options.persistState === true;
|
|
23370
|
+
this.persistKey = options.persistKey || null;
|
|
23355
23371
|
this.loadingMore = false;
|
|
23356
23372
|
if (this._isToolbarEnabled() || this.paginationMode !== "none") {
|
|
23357
23373
|
this.template = this.buildListTemplate();
|
|
@@ -23363,7 +23379,12 @@ var MOJO = (function(exports) {
|
|
|
23363
23379
|
// Lifecycle
|
|
23364
23380
|
// ============================================================
|
|
23365
23381
|
async onInit() {
|
|
23382
|
+
const queryKeys = this.persistState && this.options.collection && this.options.collection.params ? Object.keys(this.options.collection.params) : [];
|
|
23366
23383
|
this._initCollection(this.options.collection || this.options.Collection);
|
|
23384
|
+
if (this.persistState) {
|
|
23385
|
+
this._restorePersistedState(queryKeys);
|
|
23386
|
+
this.on("params-changed", () => this._savePersistedState());
|
|
23387
|
+
}
|
|
23367
23388
|
if (this.dayRangeFilter) {
|
|
23368
23389
|
this._seedDayRangeParams();
|
|
23369
23390
|
this.dayRangeControl = new SegmentControl({
|
|
@@ -23381,10 +23402,17 @@ var MOJO = (function(exports) {
|
|
|
23381
23402
|
if (this.collection && (this.options.fetchOnMount || !this.collection.lastFetchTime)) {
|
|
23382
23403
|
this.collection.fetch();
|
|
23383
23404
|
}
|
|
23405
|
+
this._startAutoRefresh();
|
|
23406
|
+
}
|
|
23407
|
+
async onBeforeUnmount() {
|
|
23408
|
+
await super.onBeforeUnmount();
|
|
23409
|
+
this._stopAutoRefresh();
|
|
23384
23410
|
}
|
|
23385
23411
|
async onBeforeRender() {
|
|
23386
23412
|
this.searchValue = this.getActiveFilters().search || "";
|
|
23387
23413
|
this.hasMore = this._computeHasMore();
|
|
23414
|
+
this.skeletonHtml = this.loadingStyle === "skeleton" && this.loading ? this._buildSkeletonHtml() : "";
|
|
23415
|
+
this.emptyStateHtml = this.emptyState && this.isEmpty && !this.loading ? this._buildEmptyStateHtml() : "";
|
|
23388
23416
|
}
|
|
23389
23417
|
async onAfterRender() {
|
|
23390
23418
|
await super.onAfterRender();
|
|
@@ -23414,6 +23442,7 @@ var MOJO = (function(exports) {
|
|
|
23414
23442
|
}
|
|
23415
23443
|
if (this._isToolbarEnabled()) {
|
|
23416
23444
|
this.updateFilterPills();
|
|
23445
|
+
this.renderFilterPresets();
|
|
23417
23446
|
this.setupSearchClearListener();
|
|
23418
23447
|
}
|
|
23419
23448
|
}
|
|
@@ -23428,20 +23457,21 @@ var MOJO = (function(exports) {
|
|
|
23428
23457
|
*/
|
|
23429
23458
|
_defaultBareTemplate() {
|
|
23430
23459
|
return `
|
|
23460
|
+
${this._hasFeedbackFeature() ? this._buildFeedbackStyles() : ""}
|
|
23431
23461
|
<div class="list-view-container">
|
|
23432
23462
|
{{#loading}}
|
|
23433
|
-
|
|
23463
|
+
${this._loadingContent(`<div class="list-loading">
|
|
23434
23464
|
<div class="spinner-border spinner-border-sm" role="status">
|
|
23435
23465
|
<span class="visually-hidden">Loading...</span>
|
|
23436
23466
|
</div>
|
|
23437
23467
|
Loading...
|
|
23438
|
-
</div
|
|
23468
|
+
</div>`)}
|
|
23439
23469
|
{{/loading}}
|
|
23440
23470
|
{{^loading}}
|
|
23441
23471
|
{{#isEmpty}}
|
|
23442
|
-
|
|
23472
|
+
${this._emptyContent(`<div class="list-empty">
|
|
23443
23473
|
{{emptyMessage}}
|
|
23444
|
-
</div
|
|
23474
|
+
</div>`)}
|
|
23445
23475
|
{{/isEmpty}}
|
|
23446
23476
|
{{^isEmpty}}
|
|
23447
23477
|
<div class="list-items" data-container="items"></div>
|
|
@@ -23465,21 +23495,22 @@ var MOJO = (function(exports) {
|
|
|
23465
23495
|
const pagination = this.paginationMode === "pages" ? this.buildPaginationTemplate() : "";
|
|
23466
23496
|
return `
|
|
23467
23497
|
<div class="list-view-wrapper">
|
|
23498
|
+
${this._hasFeedbackFeature() ? this._buildFeedbackStyles() : ""}
|
|
23468
23499
|
${toolbar}
|
|
23469
23500
|
<div class="list-view-container">
|
|
23470
23501
|
{{#loading}}
|
|
23471
|
-
|
|
23502
|
+
${this._loadingContent(`<div class="list-loading text-center py-4">
|
|
23472
23503
|
<div class="spinner-border spinner-border-sm" role="status">
|
|
23473
23504
|
<span class="visually-hidden">Loading...</span>
|
|
23474
23505
|
</div>
|
|
23475
23506
|
Loading...
|
|
23476
|
-
</div
|
|
23507
|
+
</div>`)}
|
|
23477
23508
|
{{/loading}}
|
|
23478
23509
|
{{^loading}}
|
|
23479
23510
|
{{#isEmpty}}
|
|
23480
|
-
|
|
23511
|
+
${this._emptyContent(`<div class="list-empty text-center py-4 text-muted">
|
|
23481
23512
|
{{emptyMessage}}
|
|
23482
|
-
</div
|
|
23513
|
+
</div>`)}
|
|
23483
23514
|
{{/isEmpty}}
|
|
23484
23515
|
{{^isEmpty}}
|
|
23485
23516
|
<div class="list-items" data-container="items"></div>
|
|
@@ -23496,7 +23527,16 @@ var MOJO = (function(exports) {
|
|
|
23496
23527
|
* @private
|
|
23497
23528
|
*/
|
|
23498
23529
|
_isToolbarEnabled() {
|
|
23499
|
-
return !!(this.title || this.eyebrow || this.searchable || this.filterable && this._hasAnyFilters() || this.sortOptions && this.sortOptions.length > 0 || this.toolbarRight || this.dayRangeFilter || this.toolbarButtons && this.toolbarButtons.length > 0 || this.options.showAdd || this.options.showExport);
|
|
23530
|
+
return !!(this.title || this.eyebrow || this.searchable || this.filterable && this._hasAnyFilters() || this.sortOptions && this.sortOptions.length > 0 || this.toolbarRight || this.dayRangeFilter || this.filterPresets && this.filterPresets.length > 0 || this.toolbarButtons && this.toolbarButtons.length > 0 || this._autoRefreshIndicatorEnabled() || this.options.showAdd || this.options.showExport);
|
|
23531
|
+
}
|
|
23532
|
+
/**
|
|
23533
|
+
* @returns {boolean} true when the auto-refresh toolbar indicator should
|
|
23534
|
+
* render — auto-refresh is active and it wasn't opted out. Gates both the
|
|
23535
|
+
* toolbar slot and its style block (off → zero markup, byte-identical).
|
|
23536
|
+
* @private
|
|
23537
|
+
*/
|
|
23538
|
+
_autoRefreshIndicatorEnabled() {
|
|
23539
|
+
return this._autoRefreshMs > 0 && this.autoRefreshIndicator !== false;
|
|
23500
23540
|
}
|
|
23501
23541
|
_hasAnyFilters() {
|
|
23502
23542
|
return this.filters && Object.keys(this.filters).length > 0 || this.additionalFilters && this.additionalFilters.length > 0;
|
|
@@ -23511,22 +23551,35 @@ var MOJO = (function(exports) {
|
|
|
23511
23551
|
const rightSlot = this.toolbarRight ? `<div data-container="toolbar-right"></div>` : "";
|
|
23512
23552
|
const dayRangeSlot = this.dayRangeFilter ? `<div data-container="toolbar-day-range"></div>` : "";
|
|
23513
23553
|
const sortDropdown = this.sortOptions && this.sortOptions.length > 0 ? this.buildSortDropdownTemplate() : "";
|
|
23554
|
+
const hasPresets = this.filterPresets && this.filterPresets.length > 0;
|
|
23555
|
+
const presetsInline = hasPresets && this._presetsInline();
|
|
23556
|
+
const presetInlineSlot = presetsInline ? `<div data-container="filter-presets" class="preset-segment-slot"></div>` : "";
|
|
23557
|
+
const presetRowSlot = hasPresets && !presetsInline ? `<div data-container="filter-presets" class="preset-scroll mt-3"></div>` : "";
|
|
23558
|
+
const presetStyle = hasPresets ? this._buildPresetStyle() : "";
|
|
23559
|
+
const showIndicator = this._autoRefreshIndicatorEnabled();
|
|
23560
|
+
const indicatorSlot = showIndicator ? this._buildAutoRefreshIndicatorSlot() : "";
|
|
23561
|
+
const indicatorStyle = showIndicator ? this._buildAutoRefreshIndicatorStyle() : "";
|
|
23514
23562
|
const rightGroup = `
|
|
23515
23563
|
<div class="d-flex align-items-center gap-2 flex-wrap ${titleBlock ? "ms-auto" : ""}">
|
|
23516
23564
|
${this.buildActionButtonsTemplate()}
|
|
23565
|
+
${indicatorSlot}
|
|
23517
23566
|
${sortDropdown}
|
|
23518
23567
|
${this.filterable ? this.buildFilterDropdownTemplate() : ""}
|
|
23519
23568
|
${this.searchable && this.searchPlacement === "toolbar" ? this.buildSearchTemplate() : ""}
|
|
23569
|
+
${presetInlineSlot}
|
|
23520
23570
|
${dayRangeSlot}
|
|
23521
23571
|
${rightSlot}
|
|
23522
23572
|
</div>
|
|
23523
23573
|
`;
|
|
23524
23574
|
return `
|
|
23525
23575
|
<div class="table-action-buttons mb-3">
|
|
23576
|
+
${presetStyle}
|
|
23577
|
+
${indicatorStyle}
|
|
23526
23578
|
<div class="d-flex align-items-center gap-3 flex-wrap">
|
|
23527
23579
|
${titleBlock}
|
|
23528
23580
|
${rightGroup}
|
|
23529
23581
|
</div>
|
|
23582
|
+
${presetRowSlot}
|
|
23530
23583
|
<div data-container="filter-pills"></div>
|
|
23531
23584
|
</div>
|
|
23532
23585
|
`;
|
|
@@ -23747,7 +23800,10 @@ var MOJO = (function(exports) {
|
|
|
23747
23800
|
`;
|
|
23748
23801
|
}
|
|
23749
23802
|
buildActivePills() {
|
|
23750
|
-
|
|
23803
|
+
const countHtml = this.showResultCount ? this._buildResultCountSummary() : "";
|
|
23804
|
+
if (this.hideActivePills) {
|
|
23805
|
+
return countHtml ? this._wrapPillRow(countHtml) : "";
|
|
23806
|
+
}
|
|
23751
23807
|
const activeFilters = this.getActiveFilters();
|
|
23752
23808
|
const hasSearch = activeFilters.search && activeFilters.search.toString().trim() !== "";
|
|
23753
23809
|
let filterEntries = Object.entries(activeFilters).filter(
|
|
@@ -23756,7 +23812,9 @@ var MOJO = (function(exports) {
|
|
|
23756
23812
|
if (this.hideActivePillNames && this.hideActivePillNames.length > 0) {
|
|
23757
23813
|
filterEntries = filterEntries.filter(([key]) => !this.hideActivePillNames.includes(key));
|
|
23758
23814
|
}
|
|
23759
|
-
if (filterEntries.length === 0 && !hasSearch)
|
|
23815
|
+
if (filterEntries.length === 0 && !hasSearch) {
|
|
23816
|
+
return countHtml ? this._wrapPillRow(countHtml) : "";
|
|
23817
|
+
}
|
|
23760
23818
|
const pills = filterEntries.map(([paramKey, value]) => {
|
|
23761
23819
|
const { field } = parseFilterKey(paramKey);
|
|
23762
23820
|
const label = this.getFilterLabel(field);
|
|
@@ -23795,13 +23853,30 @@ var MOJO = (function(exports) {
|
|
|
23795
23853
|
<div class="row mt-2">
|
|
23796
23854
|
<div class="col-12">
|
|
23797
23855
|
<div class="d-flex flex-wrap align-items-center">
|
|
23798
|
-
${pills}
|
|
23856
|
+
${countHtml}${pills}
|
|
23799
23857
|
${clearAllButton}
|
|
23800
23858
|
</div>
|
|
23801
23859
|
</div>
|
|
23802
23860
|
</div>
|
|
23803
23861
|
`;
|
|
23804
23862
|
}
|
|
23863
|
+
/**
|
|
23864
|
+
* Wrap arbitrary inner markup in the standard active-pills row shell. Used
|
|
23865
|
+
* for the count-only case (WM-033) where the result-count summary renders
|
|
23866
|
+
* without any filter pills.
|
|
23867
|
+
* @private
|
|
23868
|
+
*/
|
|
23869
|
+
_wrapPillRow(inner) {
|
|
23870
|
+
return `
|
|
23871
|
+
<div class="row mt-2">
|
|
23872
|
+
<div class="col-12">
|
|
23873
|
+
<div class="d-flex flex-wrap align-items-center">
|
|
23874
|
+
${inner}
|
|
23875
|
+
</div>
|
|
23876
|
+
</div>
|
|
23877
|
+
</div>
|
|
23878
|
+
`;
|
|
23879
|
+
}
|
|
23805
23880
|
buildPaginationTemplate() {
|
|
23806
23881
|
return `
|
|
23807
23882
|
<div class="table-status-bar mt-3">
|
|
@@ -23848,6 +23923,215 @@ var MOJO = (function(exports) {
|
|
|
23848
23923
|
`;
|
|
23849
23924
|
}
|
|
23850
23925
|
// ============================================================
|
|
23926
|
+
// Feedback states (WM-033) — rich empty, skeletons, result count
|
|
23927
|
+
// ============================================================
|
|
23928
|
+
/**
|
|
23929
|
+
* Normalize the `emptyState:` option into `{ icon, title, message, action }`
|
|
23930
|
+
* or null when absent. `action` is `{ label, action, icon }` where `action`
|
|
23931
|
+
* is a kebab-case `data-action` name (routed like any toolbar action, e.g.
|
|
23932
|
+
* `'add'` → onActionAdd). Returns null for a non-object so the feature stays
|
|
23933
|
+
* inert and `emptyMessage` remains the fallback.
|
|
23934
|
+
* @private
|
|
23935
|
+
*/
|
|
23936
|
+
_normalizeEmptyState(raw) {
|
|
23937
|
+
if (!raw || typeof raw !== "object") return null;
|
|
23938
|
+
let action = null;
|
|
23939
|
+
if (raw.action && typeof raw.action === "object" && raw.action.action) {
|
|
23940
|
+
action = {
|
|
23941
|
+
label: raw.action.label || "Action",
|
|
23942
|
+
action: raw.action.action,
|
|
23943
|
+
icon: raw.action.icon || null
|
|
23944
|
+
};
|
|
23945
|
+
}
|
|
23946
|
+
return {
|
|
23947
|
+
icon: raw.icon || "inbox",
|
|
23948
|
+
title: raw.title || null,
|
|
23949
|
+
message: raw.message || null,
|
|
23950
|
+
action
|
|
23951
|
+
};
|
|
23952
|
+
}
|
|
23953
|
+
/**
|
|
23954
|
+
* True when any of the three opt-in feedback features is enabled. Gates the
|
|
23955
|
+
* one-time `<style>` block emission so opt-out pages carry zero extra CSS.
|
|
23956
|
+
* @private
|
|
23957
|
+
*/
|
|
23958
|
+
_hasFeedbackFeature() {
|
|
23959
|
+
return !!this.emptyState || this.loadingStyle === "skeleton" || this.showResultCount;
|
|
23960
|
+
}
|
|
23961
|
+
/**
|
|
23962
|
+
* Loading-block content for the body template. Returns the unescaped
|
|
23963
|
+
* skeleton slot when `loadingStyle: 'skeleton'`, otherwise the caller's
|
|
23964
|
+
* default spinner markup verbatim (byte-identical opt-out).
|
|
23965
|
+
* @private
|
|
23966
|
+
*/
|
|
23967
|
+
_loadingContent(defaultMarkup) {
|
|
23968
|
+
return this.loadingStyle === "skeleton" ? "{{{skeletonHtml}}}" : defaultMarkup;
|
|
23969
|
+
}
|
|
23970
|
+
/**
|
|
23971
|
+
* Empty-block content for the body template. Returns the unescaped rich
|
|
23972
|
+
* empty-state slot when `emptyState` is configured, otherwise the caller's
|
|
23973
|
+
* default `emptyMessage` markup verbatim (byte-identical opt-out).
|
|
23974
|
+
* @private
|
|
23975
|
+
*/
|
|
23976
|
+
_emptyContent(defaultMarkup) {
|
|
23977
|
+
return this.emptyState ? "{{{emptyStateHtml}}}" : defaultMarkup;
|
|
23978
|
+
}
|
|
23979
|
+
/**
|
|
23980
|
+
* Skeleton row count: `collection.params.size` capped at 8, defaulting to 5
|
|
23981
|
+
* when size isn't set. Shared by the ListView (card) and TableView (row)
|
|
23982
|
+
* skeleton builders.
|
|
23983
|
+
* @private
|
|
23984
|
+
*/
|
|
23985
|
+
_skeletonRowCount() {
|
|
23986
|
+
const size = this.collection?.params?.size;
|
|
23987
|
+
const n = typeof size === "number" && size > 0 ? size : 5;
|
|
23988
|
+
return Math.min(n, 8);
|
|
23989
|
+
}
|
|
23990
|
+
/**
|
|
23991
|
+
* ListView skeleton — stacked card silhouettes. TableView overrides this to
|
|
23992
|
+
* emit a column-matched `<table>`. Called from onBeforeRender only while
|
|
23993
|
+
* loading + `loadingStyle: 'skeleton'`.
|
|
23994
|
+
* @private
|
|
23995
|
+
*/
|
|
23996
|
+
_buildSkeletonHtml() {
|
|
23997
|
+
const rows = this._skeletonRowCount();
|
|
23998
|
+
let cards = "";
|
|
23999
|
+
for (let i = 0; i < rows; i++) {
|
|
24000
|
+
cards += '<div class="list-skeleton-card"><span class="skeleton-line w-40"></span><span class="skeleton-line w-90"></span><span class="skeleton-line w-60"></span></div>';
|
|
24001
|
+
}
|
|
24002
|
+
return `<div class="list-skeleton" aria-hidden="true">${cards}</div>`;
|
|
24003
|
+
}
|
|
24004
|
+
/**
|
|
24005
|
+
* Build the rich empty-state panel. Branches on `getActiveFilters()`:
|
|
24006
|
+
* non-empty → the filtered variant ("No results match your filters" +
|
|
24007
|
+
* Clear filters button reusing onActionClearAllFilters); empty → the
|
|
24008
|
+
* configured truly-empty content with its optional CTA. Search counts as
|
|
24009
|
+
* an active filter. Returns '' when `emptyState` isn't configured.
|
|
24010
|
+
* @private
|
|
24011
|
+
*/
|
|
24012
|
+
_buildEmptyStateHtml() {
|
|
24013
|
+
const es = this.emptyState;
|
|
24014
|
+
if (!es) return "";
|
|
24015
|
+
const activeCount = Object.keys(this.getActiveFilters()).length;
|
|
24016
|
+
const isFiltered = activeCount > 0;
|
|
24017
|
+
let icon;
|
|
24018
|
+
let title;
|
|
24019
|
+
let message;
|
|
24020
|
+
let buttonHtml;
|
|
24021
|
+
if (isFiltered) {
|
|
24022
|
+
icon = "funnel";
|
|
24023
|
+
title = "No results match your filters";
|
|
24024
|
+
const plural = activeCount === 1 ? "filter" : "filters";
|
|
24025
|
+
message = `No items match the ${activeCount} active ${plural}. Try widening or removing a filter.`;
|
|
24026
|
+
buttonHtml = '<button class="btn btn-outline-primary" data-action="clear-all-filters"><i class="bi bi-x-circle me-1"></i>Clear filters</button>';
|
|
24027
|
+
} else {
|
|
24028
|
+
icon = es.icon || "inbox";
|
|
24029
|
+
title = es.title || this.emptyMessage;
|
|
24030
|
+
message = es.message || "";
|
|
24031
|
+
if (es.action) {
|
|
24032
|
+
const iconHtml = es.action.icon ? `<i class="bi ${this.escapeHtml(es.action.icon)} me-1"></i>` : "";
|
|
24033
|
+
buttonHtml = `<button class="btn btn-primary" data-action="${this.escapeHtml(es.action.action)}">${iconHtml}${this.escapeHtml(es.action.label)}</button>`;
|
|
24034
|
+
} else {
|
|
24035
|
+
buttonHtml = "";
|
|
24036
|
+
}
|
|
24037
|
+
}
|
|
24038
|
+
const titleHtml = title ? `<div class="empty-state-title h6">${this.escapeHtml(title)}</div>` : "";
|
|
24039
|
+
const messageHtml = message ? `<p class="empty-state-message">${this.escapeHtml(message)}</p>` : "";
|
|
24040
|
+
return `
|
|
24041
|
+
<div class="table-empty-state">
|
|
24042
|
+
<span class="empty-state-icon"><i class="bi bi-${this.escapeHtml(icon)}"></i></span>
|
|
24043
|
+
${titleHtml}
|
|
24044
|
+
${messageHtml}
|
|
24045
|
+
${buttonHtml}
|
|
24046
|
+
</div>
|
|
24047
|
+
`;
|
|
24048
|
+
}
|
|
24049
|
+
/**
|
|
24050
|
+
* Build the "Showing N of M" result-count summary from `collection.meta`
|
|
24051
|
+
* (`count`) and the current loaded length. Appends a "· filtered" marker
|
|
24052
|
+
* when any filter is active. Returns '' when `showResultCount` is off or the
|
|
24053
|
+
* total count isn't known yet. Rendered inside the filter-pills row by
|
|
24054
|
+
* `buildActivePills`.
|
|
24055
|
+
* @private
|
|
24056
|
+
*/
|
|
24057
|
+
_buildResultCountSummary() {
|
|
24058
|
+
if (!this.showResultCount || !this.collection) return "";
|
|
24059
|
+
const meta = this.collection.meta || {};
|
|
24060
|
+
const total = typeof meta.count === "number" ? meta.count : null;
|
|
24061
|
+
if (total === null) return "";
|
|
24062
|
+
const shown = this.collection.length();
|
|
24063
|
+
const isFiltered = Object.keys(this.getActiveFilters()).length > 0;
|
|
24064
|
+
const fmt = (n) => Number(n).toLocaleString();
|
|
24065
|
+
const filteredSuffix = isFiltered ? ' <span class="count-filtered">· filtered</span>' : "";
|
|
24066
|
+
return `<span class="result-count-summary me-2 mb-1">Showing <strong>${fmt(shown)}</strong> of <strong>${fmt(total)}</strong>${filteredSuffix}</span>`;
|
|
24067
|
+
}
|
|
24068
|
+
/**
|
|
24069
|
+
* One-time `<style>` block for the three feedback features (skeleton
|
|
24070
|
+
* shimmer, empty-state chip, result-count summary). Token-based so both
|
|
24071
|
+
* themes track automatically; the shimmer's moving band is a component tint
|
|
24072
|
+
* (black-on-light) with its rgba(255,255,255) dark companion, and the
|
|
24073
|
+
* animation is disabled under prefers-reduced-motion. Emitted once per
|
|
24074
|
+
* template, only when `_hasFeedbackFeature()`.
|
|
24075
|
+
* @private
|
|
24076
|
+
*/
|
|
24077
|
+
_buildFeedbackStyles() {
|
|
24078
|
+
return `
|
|
24079
|
+
<style>
|
|
24080
|
+
.table-empty-state { text-align: center; padding: 3rem 1.5rem; }
|
|
24081
|
+
.table-empty-state .empty-state-icon {
|
|
24082
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
24083
|
+
width: 4rem; height: 4rem; border-radius: 50%;
|
|
24084
|
+
margin-bottom: 1rem; font-size: 1.75rem;
|
|
24085
|
+
background: var(--bs-secondary-bg);
|
|
24086
|
+
color: var(--bs-secondary-color);
|
|
24087
|
+
border: 1px solid var(--bs-border-color);
|
|
24088
|
+
}
|
|
24089
|
+
.table-empty-state .empty-state-title {
|
|
24090
|
+
font-weight: 600; color: var(--bs-emphasis-color); margin-bottom: 0.35rem;
|
|
24091
|
+
}
|
|
24092
|
+
.table-empty-state .empty-state-message {
|
|
24093
|
+
color: var(--bs-secondary-color); max-width: 26rem; margin: 0 auto 1.25rem;
|
|
24094
|
+
}
|
|
24095
|
+
|
|
24096
|
+
.skeleton-line {
|
|
24097
|
+
display: block; height: 0.85rem; border-radius: 0.35rem;
|
|
24098
|
+
background-color: var(--bs-secondary-bg);
|
|
24099
|
+
background-image: linear-gradient(90deg,
|
|
24100
|
+
rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.06) 50%, rgba(0, 0, 0, 0) 100%);
|
|
24101
|
+
background-size: 200% 100%; background-repeat: no-repeat;
|
|
24102
|
+
animation: mojo-skeleton-shimmer 1.4s ease-in-out infinite;
|
|
24103
|
+
}
|
|
24104
|
+
[data-bs-theme="dark"] .skeleton-line {
|
|
24105
|
+
background-image: linear-gradient(90deg,
|
|
24106
|
+
rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.07) 50%, rgba(255, 255, 255, 0) 100%);
|
|
24107
|
+
}
|
|
24108
|
+
.skeleton-line.w-90 { width: 90%; }
|
|
24109
|
+
.skeleton-line.w-75 { width: 75%; }
|
|
24110
|
+
.skeleton-line.w-60 { width: 60%; }
|
|
24111
|
+
.skeleton-line.w-40 { width: 40%; }
|
|
24112
|
+
.skeleton-line.w-25 { width: 25%; }
|
|
24113
|
+
.skeleton-line.skeleton-pill { height: 1.35rem; width: 4rem; border-radius: 999px; }
|
|
24114
|
+
.list-skeleton-card {
|
|
24115
|
+
display: flex; flex-direction: column; gap: 0.5rem;
|
|
24116
|
+
padding: 0.85rem 1rem; margin-bottom: 0.5rem;
|
|
24117
|
+
border: 1px solid var(--bs-border-color-translucent);
|
|
24118
|
+
border-radius: 0.5rem;
|
|
24119
|
+
}
|
|
24120
|
+
@keyframes mojo-skeleton-shimmer {
|
|
24121
|
+
0% { background-position: 150% 0; }
|
|
24122
|
+
100% { background-position: -50% 0; }
|
|
24123
|
+
}
|
|
24124
|
+
@media (prefers-reduced-motion: reduce) {
|
|
24125
|
+
.skeleton-line { animation: none; }
|
|
24126
|
+
}
|
|
24127
|
+
|
|
24128
|
+
.result-count-summary { font-size: 0.8rem; color: var(--bs-secondary-color); }
|
|
24129
|
+
.result-count-summary strong { color: var(--bs-emphasis-color); font-weight: 600; }
|
|
24130
|
+
.result-count-summary .count-filtered { color: var(--bs-primary); }
|
|
24131
|
+
</style>
|
|
24132
|
+
`;
|
|
24133
|
+
}
|
|
24134
|
+
// ============================================================
|
|
23851
24135
|
// Collection wiring (existing + persistSelection-aware)
|
|
23852
24136
|
// ============================================================
|
|
23853
24137
|
_initCollection(collectionOrClass) {
|
|
@@ -23942,6 +24226,325 @@ var MOJO = (function(exports) {
|
|
|
23942
24226
|
if (!silent) this._onDayRangeChange({ value, previous });
|
|
23943
24227
|
return true;
|
|
23944
24228
|
}
|
|
24229
|
+
// ============================================================
|
|
24230
|
+
// Filter presets (compact joined btn-group segment)
|
|
24231
|
+
//
|
|
24232
|
+
// Author-defined "common queries" bundled behind a toolbar segment.
|
|
24233
|
+
// Mutual-exclusion + toggle-off only; active state is derived from
|
|
24234
|
+
// strict param matching against getActiveFilters(). The `'@me'` token
|
|
24235
|
+
// in a preset's params resolves to the active user's id at apply AND
|
|
24236
|
+
// match time (skipped when no active user).
|
|
24237
|
+
// ============================================================
|
|
24238
|
+
/**
|
|
24239
|
+
* Normalize the `filterPresets:` option into a clean array of
|
|
24240
|
+
* `{ key, label, icon, params, description }`. Drops entries without a
|
|
24241
|
+
* `key`, warns on (and drops) duplicate keys. Returns `[]` when the
|
|
24242
|
+
* option is absent / empty so the whole feature stays inert.
|
|
24243
|
+
* @private
|
|
24244
|
+
*/
|
|
24245
|
+
_normalizeFilterPresets(raw) {
|
|
24246
|
+
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
24247
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24248
|
+
const presets = [];
|
|
24249
|
+
raw.forEach((p) => {
|
|
24250
|
+
if (!p || !p.key) return;
|
|
24251
|
+
if (seen.has(p.key)) {
|
|
24252
|
+
console.warn(`ListView: duplicate filterPresets key '${p.key}' — ignoring the later one.`);
|
|
24253
|
+
return;
|
|
24254
|
+
}
|
|
24255
|
+
seen.add(p.key);
|
|
24256
|
+
presets.push({
|
|
24257
|
+
key: p.key,
|
|
24258
|
+
label: p.label || p.key,
|
|
24259
|
+
icon: p.icon || null,
|
|
24260
|
+
params: p.params && typeof p.params === "object" ? p.params : {},
|
|
24261
|
+
description: p.description || null
|
|
24262
|
+
});
|
|
24263
|
+
});
|
|
24264
|
+
return presets;
|
|
24265
|
+
}
|
|
24266
|
+
/**
|
|
24267
|
+
* Placement branch: ≤4 presets render inline (sibling of the day-range
|
|
24268
|
+
* segment); 5+ get their own scrollable row above the filter pills.
|
|
24269
|
+
* @private
|
|
24270
|
+
*/
|
|
24271
|
+
_presetsInline() {
|
|
24272
|
+
return this.filterPresets.length <= 4;
|
|
24273
|
+
}
|
|
24274
|
+
_getPreset(key) {
|
|
24275
|
+
return this.filterPresets.find((p) => p.key === key) || null;
|
|
24276
|
+
}
|
|
24277
|
+
/**
|
|
24278
|
+
* Resolve a preset's raw params into concrete filter values. The `'@me'`
|
|
24279
|
+
* token resolves to the active user's id; when no active user can be
|
|
24280
|
+
* resolved that param is skipped (both at apply and match time so the
|
|
24281
|
+
* derived active state stays consistent).
|
|
24282
|
+
* @private
|
|
24283
|
+
*/
|
|
24284
|
+
_resolvePresetParams(preset) {
|
|
24285
|
+
const out = {};
|
|
24286
|
+
const raw = preset?.params || {};
|
|
24287
|
+
Object.keys(raw).forEach((key) => {
|
|
24288
|
+
const value = raw[key];
|
|
24289
|
+
if (value === "@me") {
|
|
24290
|
+
const uid = this.getApp()?.activeUser?.id;
|
|
24291
|
+
if (uid === void 0 || uid === null) return;
|
|
24292
|
+
out[key] = uid;
|
|
24293
|
+
} else {
|
|
24294
|
+
out[key] = value;
|
|
24295
|
+
}
|
|
24296
|
+
});
|
|
24297
|
+
return out;
|
|
24298
|
+
}
|
|
24299
|
+
/**
|
|
24300
|
+
* A preset is active when every one of its resolved params strictly
|
|
24301
|
+
* matches the current active filters. Matching goes through
|
|
24302
|
+
* getActiveFilters() (not raw collection.params) so `__in` collapsing is
|
|
24303
|
+
* honored. A preset whose params all resolve away (e.g. only `'@me'`
|
|
24304
|
+
* with no active user) never matches.
|
|
24305
|
+
* @private
|
|
24306
|
+
*/
|
|
24307
|
+
_presetMatches(preset) {
|
|
24308
|
+
const resolved = this._resolvePresetParams(preset);
|
|
24309
|
+
const keys = Object.keys(resolved);
|
|
24310
|
+
if (keys.length === 0) return false;
|
|
24311
|
+
const active = this.getActiveFilters();
|
|
24312
|
+
return keys.every((key) => {
|
|
24313
|
+
const value = resolved[key];
|
|
24314
|
+
if (Array.isArray(value)) {
|
|
24315
|
+
if (value.length === 0) return true;
|
|
24316
|
+
const { field } = parseFilterKey(key);
|
|
24317
|
+
if (value.length === 1) {
|
|
24318
|
+
return active[field] !== void 0 && String(active[field]) === String(value[0]);
|
|
24319
|
+
}
|
|
24320
|
+
const inVal = active[`${field}__in`];
|
|
24321
|
+
return inVal !== void 0 && String(inVal) === value.join(",");
|
|
24322
|
+
}
|
|
24323
|
+
return active[key] !== void 0 && String(active[key]) === String(value);
|
|
24324
|
+
});
|
|
24325
|
+
}
|
|
24326
|
+
/**
|
|
24327
|
+
* Null out every resolved param key belonging to a preset (used by
|
|
24328
|
+
* toggle-off and by mutual-exclusion when switching presets).
|
|
24329
|
+
* @private
|
|
24330
|
+
*/
|
|
24331
|
+
_clearPresetParams(preset) {
|
|
24332
|
+
const resolved = this._resolvePresetParams(preset);
|
|
24333
|
+
Object.keys(resolved).forEach((key) => this.setFilter(key, null));
|
|
24334
|
+
}
|
|
24335
|
+
/**
|
|
24336
|
+
* Get the currently-active preset's full config object, or null. Active
|
|
24337
|
+
* state is derived — no stored "active preset" field to drift.
|
|
24338
|
+
*
|
|
24339
|
+
* Most-specific-match-wins: when several presets match at once (e.g. one
|
|
24340
|
+
* preset's params are a strict subset of another's — Errors `{level__gte:4}`
|
|
24341
|
+
* ⊂ Auth `{path__icontains:'/auth', level__gte:4}`), the one with the MOST
|
|
24342
|
+
* resolved param keys is returned so the correct (narrower) chip highlights
|
|
24343
|
+
* and mutual-exclusion/toggle logic reads it. Ties resolve to the first
|
|
24344
|
+
* matching preset in array order.
|
|
24345
|
+
* @returns {object|null}
|
|
24346
|
+
*/
|
|
24347
|
+
getActivePreset() {
|
|
24348
|
+
if (!this.filterPresets || this.filterPresets.length === 0) return null;
|
|
24349
|
+
let best = null;
|
|
24350
|
+
let bestCount = -1;
|
|
24351
|
+
for (const preset of this.filterPresets) {
|
|
24352
|
+
if (!this._presetMatches(preset)) continue;
|
|
24353
|
+
const count = Object.keys(this._resolvePresetParams(preset)).length;
|
|
24354
|
+
if (count > bestCount) {
|
|
24355
|
+
best = preset;
|
|
24356
|
+
bestCount = count;
|
|
24357
|
+
}
|
|
24358
|
+
}
|
|
24359
|
+
return best;
|
|
24360
|
+
}
|
|
24361
|
+
/**
|
|
24362
|
+
* Apply a preset by key. Toggles off if it's already the active preset;
|
|
24363
|
+
* otherwise clears the previously-active preset's params (mutual
|
|
24364
|
+
* exclusion), applies the new bundle, refetches, and repaints.
|
|
24365
|
+
* @param {string} key
|
|
24366
|
+
* @returns {Promise<boolean>} false when the key is unknown.
|
|
24367
|
+
*/
|
|
24368
|
+
async applyPreset(key) {
|
|
24369
|
+
const preset = this._getPreset(key);
|
|
24370
|
+
if (!preset) return false;
|
|
24371
|
+
const active = this.getActivePreset();
|
|
24372
|
+
if (active && active.key === preset.key) {
|
|
24373
|
+
await this.clearPreset();
|
|
24374
|
+
return true;
|
|
24375
|
+
}
|
|
24376
|
+
if (active) this._clearPresetParams(active);
|
|
24377
|
+
const resolved = this._resolvePresetParams(preset);
|
|
24378
|
+
Object.keys(resolved).forEach((k) => this.setFilter(k, resolved[k]));
|
|
24379
|
+
await this.applyFilters();
|
|
24380
|
+
this.renderFilterPresets();
|
|
24381
|
+
this.emit("preset:change", { key: preset.key, params: resolved });
|
|
24382
|
+
return true;
|
|
24383
|
+
}
|
|
24384
|
+
/**
|
|
24385
|
+
* Clear the active preset (removes its params). No-op when nothing is
|
|
24386
|
+
* active, but still repaints and emits `preset:change` with null.
|
|
24387
|
+
* @returns {Promise<void>}
|
|
24388
|
+
*/
|
|
24389
|
+
async clearPreset() {
|
|
24390
|
+
const active = this.getActivePreset();
|
|
24391
|
+
if (active) {
|
|
24392
|
+
this._clearPresetParams(active);
|
|
24393
|
+
await this.applyFilters();
|
|
24394
|
+
}
|
|
24395
|
+
this.renderFilterPresets();
|
|
24396
|
+
this.emit("preset:change", null);
|
|
24397
|
+
}
|
|
24398
|
+
/**
|
|
24399
|
+
* Toolbar action — apply/toggle a preset from its segment button.
|
|
24400
|
+
*/
|
|
24401
|
+
async onActionApplyPreset(event, element) {
|
|
24402
|
+
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
24403
|
+
const key = element?.getAttribute("data-preset-key");
|
|
24404
|
+
if (!key) return;
|
|
24405
|
+
await this.applyPreset(key);
|
|
24406
|
+
}
|
|
24407
|
+
/**
|
|
24408
|
+
* Fill the `filter-presets` container with the joined segment, painting
|
|
24409
|
+
* the derived-active button as `btn-primary`. innerHTML re-render pattern
|
|
24410
|
+
* mirroring `updateFilterPills`. No-op when presets aren't configured or
|
|
24411
|
+
* the container isn't in the DOM yet.
|
|
24412
|
+
*/
|
|
24413
|
+
renderFilterPresets() {
|
|
24414
|
+
if (!this.filterPresets || this.filterPresets.length === 0) return;
|
|
24415
|
+
const container = this.element?.querySelector('[data-container="filter-presets"]');
|
|
24416
|
+
if (!container) return;
|
|
24417
|
+
container.innerHTML = this._buildPresetSegment();
|
|
24418
|
+
}
|
|
24419
|
+
/**
|
|
24420
|
+
* Build the joined `btn-group btn-sm preset-segment` markup with the
|
|
24421
|
+
* active button styled `btn-primary` (derived from param matching).
|
|
24422
|
+
* @private
|
|
24423
|
+
*/
|
|
24424
|
+
_buildPresetSegment() {
|
|
24425
|
+
const active = this.getActivePreset();
|
|
24426
|
+
const activeKey = active ? active.key : null;
|
|
24427
|
+
const buttons = this.filterPresets.map((preset) => {
|
|
24428
|
+
const isActive = preset.key === activeKey;
|
|
24429
|
+
const cls = isActive ? "btn btn-primary" : "btn btn-outline-secondary";
|
|
24430
|
+
const icon = preset.icon ? `<i class="bi ${this.escapeHtml(preset.icon)}"></i> ` : "";
|
|
24431
|
+
const titleAttr = preset.description ? ` title="${this.escapeHtml(preset.description)}"` : "";
|
|
24432
|
+
return `<button type="button"
|
|
24433
|
+
class="${cls}"
|
|
24434
|
+
data-action="apply-preset"
|
|
24435
|
+
data-preset-key="${this.escapeHtml(preset.key)}"
|
|
24436
|
+
aria-pressed="${isActive ? "true" : "false"}"${titleAttr}>${icon}${this.escapeHtml(preset.label)}</button>`;
|
|
24437
|
+
}).join("");
|
|
24438
|
+
return `<div class="btn-group btn-group-sm preset-segment" role="group" aria-label="Filter presets">${buttons}</div>`;
|
|
24439
|
+
}
|
|
24440
|
+
/**
|
|
24441
|
+
* Inline `<style>` for the preset segment. Token-based (auto-tracks both
|
|
24442
|
+
* themes); the single scrollbar-thumb literal carries its dark companion.
|
|
24443
|
+
* Emitted once from the toolbar template (only when presets exist).
|
|
24444
|
+
* @private
|
|
24445
|
+
*/
|
|
24446
|
+
_buildPresetStyle() {
|
|
24447
|
+
return `
|
|
24448
|
+
<style>
|
|
24449
|
+
.preset-segment .btn {
|
|
24450
|
+
--bs-btn-padding-y: 0.25rem;
|
|
24451
|
+
--bs-btn-padding-x: 0.6rem;
|
|
24452
|
+
font-size: 0.8125rem;
|
|
24453
|
+
font-weight: 500;
|
|
24454
|
+
}
|
|
24455
|
+
.preset-segment .btn .bi { font-size: 0.8rem; }
|
|
24456
|
+
|
|
24457
|
+
/* Touch: bump the tap target only on coarse pointers — desktop stays compact. */
|
|
24458
|
+
@media (pointer: coarse) {
|
|
24459
|
+
.preset-segment .btn {
|
|
24460
|
+
--bs-btn-padding-y: 0.5rem;
|
|
24461
|
+
--bs-btn-padding-x: 0.85rem;
|
|
24462
|
+
}
|
|
24463
|
+
}
|
|
24464
|
+
|
|
24465
|
+
/* 5+ presets: own row, scroll horizontally instead of wrapping. */
|
|
24466
|
+
.preset-scroll {
|
|
24467
|
+
overflow-x: auto;
|
|
24468
|
+
-webkit-overflow-scrolling: touch;
|
|
24469
|
+
scrollbar-width: thin;
|
|
24470
|
+
padding-bottom: 0.2rem;
|
|
24471
|
+
}
|
|
24472
|
+
.preset-scroll .btn-group { flex-wrap: nowrap; }
|
|
24473
|
+
.preset-scroll .btn { white-space: nowrap; }
|
|
24474
|
+
.preset-scroll::-webkit-scrollbar { height: 6px; }
|
|
24475
|
+
.preset-scroll::-webkit-scrollbar-thumb {
|
|
24476
|
+
background: rgba(0, 0, 0, 0.15);
|
|
24477
|
+
border-radius: 999px;
|
|
24478
|
+
}
|
|
24479
|
+
[data-bs-theme="dark"] .preset-scroll::-webkit-scrollbar-thumb {
|
|
24480
|
+
background: rgba(255, 255, 255, 0.12);
|
|
24481
|
+
}
|
|
24482
|
+
</style>
|
|
24483
|
+
`;
|
|
24484
|
+
}
|
|
24485
|
+
/**
|
|
24486
|
+
* Compact, quiet auto-refresh indicator for the toolbar right-group: a
|
|
24487
|
+
* muted `bi-arrow-repeat` icon with no button chrome and no text label. The
|
|
24488
|
+
* icon is `aria-hidden`; a visually-hidden span carries the text
|
|
24489
|
+
* alternative for assistive tech. `_pulseAutoRefreshIndicator()` adds the
|
|
24490
|
+
* `is-refreshing` class on each successful tick to fire a brief spin.
|
|
24491
|
+
* @private
|
|
24492
|
+
*/
|
|
24493
|
+
_buildAutoRefreshIndicatorSlot() {
|
|
24494
|
+
const secs = Math.round(this._autoRefreshMs / 1e3);
|
|
24495
|
+
const label = `Auto-refresh: ${secs}s`;
|
|
24496
|
+
return `<span class="mojo-autorefresh-indicator" data-autorefresh-indicator title="${this.escapeHtml(label)}">
|
|
24497
|
+
<i class="bi bi-arrow-repeat" aria-hidden="true"></i>
|
|
24498
|
+
<span class="visually-hidden">${this.escapeHtml(label)}</span>
|
|
24499
|
+
</span>`;
|
|
24500
|
+
}
|
|
24501
|
+
/**
|
|
24502
|
+
* Inline `<style>` for the auto-refresh indicator. Muted via
|
|
24503
|
+
* `--bs-secondary-color` (auto-tracks both themes — no literals). The spin
|
|
24504
|
+
* animation only plays while `is-refreshing` is set and is suppressed under
|
|
24505
|
+
* `prefers-reduced-motion`. Emitted once, only when the indicator renders.
|
|
24506
|
+
* @private
|
|
24507
|
+
*/
|
|
24508
|
+
_buildAutoRefreshIndicatorStyle() {
|
|
24509
|
+
return `
|
|
24510
|
+
<style>
|
|
24511
|
+
.mojo-autorefresh-indicator {
|
|
24512
|
+
display: inline-flex;
|
|
24513
|
+
align-items: center;
|
|
24514
|
+
color: var(--bs-secondary-color);
|
|
24515
|
+
font-size: 0.9rem;
|
|
24516
|
+
line-height: 1;
|
|
24517
|
+
}
|
|
24518
|
+
.mojo-autorefresh-indicator .bi { display: block; }
|
|
24519
|
+
.mojo-autorefresh-indicator.is-refreshing .bi {
|
|
24520
|
+
animation: mojo-autorefresh-spin 0.6s ease;
|
|
24521
|
+
}
|
|
24522
|
+
@keyframes mojo-autorefresh-spin {
|
|
24523
|
+
from { transform: rotate(0deg); }
|
|
24524
|
+
to { transform: rotate(360deg); }
|
|
24525
|
+
}
|
|
24526
|
+
@media (prefers-reduced-motion: reduce) {
|
|
24527
|
+
.mojo-autorefresh-indicator.is-refreshing .bi { animation: none; }
|
|
24528
|
+
}
|
|
24529
|
+
</style>
|
|
24530
|
+
`;
|
|
24531
|
+
}
|
|
24532
|
+
/**
|
|
24533
|
+
* Flash the toolbar auto-refresh indicator: add the `is-refreshing` class
|
|
24534
|
+
* (fires the CSS spin) and strip it after ~600ms. Coalesces rapid ticks by
|
|
24535
|
+
* resetting its own timer; a no-op when the indicator isn't in the DOM.
|
|
24536
|
+
* @private
|
|
24537
|
+
*/
|
|
24538
|
+
_pulseAutoRefreshIndicator() {
|
|
24539
|
+
const el = this.element?.querySelector("[data-autorefresh-indicator]");
|
|
24540
|
+
if (!el) return;
|
|
24541
|
+
el.classList.add("is-refreshing");
|
|
24542
|
+
if (this._autoRefreshPulseTimer) clearTimeout(this._autoRefreshPulseTimer);
|
|
24543
|
+
this._autoRefreshPulseTimer = setTimeout(() => {
|
|
24544
|
+
el.classList.remove("is-refreshing");
|
|
24545
|
+
this._autoRefreshPulseTimer = null;
|
|
24546
|
+
}, 600);
|
|
24547
|
+
}
|
|
23945
24548
|
setCollection(collection) {
|
|
23946
24549
|
if (this.collection === collection) return this;
|
|
23947
24550
|
if (this.collection) {
|
|
@@ -25183,9 +25786,304 @@ var MOJO = (function(exports) {
|
|
|
25183
25786
|
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
25184
25787
|
}
|
|
25185
25788
|
// ============================================================
|
|
25789
|
+
// Auto-refresh (WM-034) — opt-in interval refetch with smart pause
|
|
25790
|
+
// ============================================================
|
|
25791
|
+
/**
|
|
25792
|
+
* Normalize the `autoRefresh:` option into an interval in milliseconds.
|
|
25793
|
+
* Accepts a positive number of seconds, clamped to a 5s floor to prevent
|
|
25794
|
+
* hammering the API. Anything else (absent / non-number / ≤0) → 0, which
|
|
25795
|
+
* disables the feature entirely (no timer, no listeners).
|
|
25796
|
+
* @private
|
|
25797
|
+
*/
|
|
25798
|
+
_normalizeAutoRefresh(raw) {
|
|
25799
|
+
if (typeof raw !== "number" || !isFinite(raw) || raw <= 0) return 0;
|
|
25800
|
+
return Math.max(5, raw) * 1e3;
|
|
25801
|
+
}
|
|
25802
|
+
/**
|
|
25803
|
+
* Start the auto-refresh interval + focus/visibility listeners. No-op when
|
|
25804
|
+
* `autoRefresh` wasn't configured, and idempotent — a second call while a
|
|
25805
|
+
* timer is already live returns immediately, so cached-page re-entries
|
|
25806
|
+
* (which re-fire onAfterMount) never stack a second interval.
|
|
25807
|
+
* @private
|
|
25808
|
+
*/
|
|
25809
|
+
_startAutoRefresh() {
|
|
25810
|
+
if (!this._autoRefreshMs) return;
|
|
25811
|
+
if (this._autoRefreshTimer) return;
|
|
25812
|
+
this._autoRefreshPaused = this._autoRefreshDocHidden();
|
|
25813
|
+
this._bindAutoRefreshListeners();
|
|
25814
|
+
this._autoRefreshTimer = setInterval(() => this._autoRefreshTick(), this._autoRefreshMs);
|
|
25815
|
+
}
|
|
25816
|
+
/**
|
|
25817
|
+
* Fully tear down the timer and its focus/visibility listeners. Safe to
|
|
25818
|
+
* call when nothing is running.
|
|
25819
|
+
* @private
|
|
25820
|
+
*/
|
|
25821
|
+
_stopAutoRefresh() {
|
|
25822
|
+
if (this._autoRefreshTimer) {
|
|
25823
|
+
clearInterval(this._autoRefreshTimer);
|
|
25824
|
+
this._autoRefreshTimer = null;
|
|
25825
|
+
}
|
|
25826
|
+
if (this._autoRefreshPulseTimer) {
|
|
25827
|
+
clearTimeout(this._autoRefreshPulseTimer);
|
|
25828
|
+
this._autoRefreshPulseTimer = null;
|
|
25829
|
+
}
|
|
25830
|
+
this._unbindAutoRefreshListeners();
|
|
25831
|
+
this._autoRefreshPaused = false;
|
|
25832
|
+
}
|
|
25833
|
+
/**
|
|
25834
|
+
* Bind window focus/blur + document visibilitychange listeners so the
|
|
25835
|
+
* timer can pause while the tab is backgrounded and resume (with an
|
|
25836
|
+
* immediate refetch) when the user returns. Idempotent.
|
|
25837
|
+
* @private
|
|
25838
|
+
*/
|
|
25839
|
+
_bindAutoRefreshListeners() {
|
|
25840
|
+
if (this._autoRefreshHandlers) return;
|
|
25841
|
+
const onBlur = () => {
|
|
25842
|
+
this._autoRefreshPaused = true;
|
|
25843
|
+
};
|
|
25844
|
+
const onFocus = () => {
|
|
25845
|
+
this._autoRefreshPaused = false;
|
|
25846
|
+
this._autoRefreshResume();
|
|
25847
|
+
};
|
|
25848
|
+
const onVisibility = () => {
|
|
25849
|
+
const hidden = this._autoRefreshDocHidden();
|
|
25850
|
+
this._autoRefreshPaused = hidden;
|
|
25851
|
+
if (!hidden) this._autoRefreshResume();
|
|
25852
|
+
};
|
|
25853
|
+
this._autoRefreshHandlers = { onBlur, onFocus, onVisibility };
|
|
25854
|
+
if (typeof window !== "undefined") {
|
|
25855
|
+
window.addEventListener("blur", onBlur);
|
|
25856
|
+
window.addEventListener("focus", onFocus);
|
|
25857
|
+
}
|
|
25858
|
+
if (typeof document !== "undefined") {
|
|
25859
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
25860
|
+
}
|
|
25861
|
+
}
|
|
25862
|
+
/** @private */
|
|
25863
|
+
_unbindAutoRefreshListeners() {
|
|
25864
|
+
const h = this._autoRefreshHandlers;
|
|
25865
|
+
if (!h) return;
|
|
25866
|
+
if (typeof window !== "undefined") {
|
|
25867
|
+
window.removeEventListener("blur", h.onBlur);
|
|
25868
|
+
window.removeEventListener("focus", h.onFocus);
|
|
25869
|
+
}
|
|
25870
|
+
if (typeof document !== "undefined") {
|
|
25871
|
+
document.removeEventListener("visibilitychange", h.onVisibility);
|
|
25872
|
+
}
|
|
25873
|
+
this._autoRefreshHandlers = null;
|
|
25874
|
+
}
|
|
25875
|
+
/** @private */
|
|
25876
|
+
_autoRefreshDocHidden() {
|
|
25877
|
+
return typeof document !== "undefined" && (document.visibilityState === "hidden" || document.hidden === true);
|
|
25878
|
+
}
|
|
25879
|
+
/**
|
|
25880
|
+
* Regaining focus / visibility fires an immediate guarded refetch, then the
|
|
25881
|
+
* existing interval carries on at its normal cadence. No-op when no timer
|
|
25882
|
+
* is live (so a stray focus event after teardown can't refetch).
|
|
25883
|
+
* @private
|
|
25884
|
+
*/
|
|
25885
|
+
_autoRefreshResume() {
|
|
25886
|
+
if (!this._autoRefreshTimer) return;
|
|
25887
|
+
this._autoRefreshTick();
|
|
25888
|
+
}
|
|
25889
|
+
/**
|
|
25890
|
+
* One interval tick. Self-terminates if the view is no longer mounted
|
|
25891
|
+
* (covers a cached Page that exited without an unmount hook reaching this
|
|
25892
|
+
* child). Otherwise skips silently while any pause condition holds, then
|
|
25893
|
+
* issues a silent `collection.fetch()` that preserves current params —
|
|
25894
|
+
* Collection's own dedup/cancel machinery handles overlap.
|
|
25895
|
+
* @private
|
|
25896
|
+
*/
|
|
25897
|
+
_autoRefreshTick() {
|
|
25898
|
+
if (!this.isMounted()) {
|
|
25899
|
+
this._stopAutoRefresh();
|
|
25900
|
+
return;
|
|
25901
|
+
}
|
|
25902
|
+
if (this._autoRefreshShouldSkip()) return;
|
|
25903
|
+
if (!this.collection || !this.collection.restEnabled) return;
|
|
25904
|
+
Promise.resolve(this.collection.fetch()).then(() => this._pulseAutoRefreshIndicator()).catch((err) => {
|
|
25905
|
+
console.warn("ListView autoRefresh: fetch failed", err);
|
|
25906
|
+
});
|
|
25907
|
+
}
|
|
25908
|
+
/**
|
|
25909
|
+
* Pause predicate evaluated at tick time. Base ListView pauses while the
|
|
25910
|
+
* tab is hidden/blurred or a selection is active. TableView overrides this
|
|
25911
|
+
* to also pause during an inline cell edit or an open row context menu.
|
|
25912
|
+
* @protected
|
|
25913
|
+
* @returns {boolean} true → skip this tick (do not clear the timer).
|
|
25914
|
+
*/
|
|
25915
|
+
_autoRefreshShouldSkip() {
|
|
25916
|
+
if (this._autoRefreshPaused) return true;
|
|
25917
|
+
if (this._autoRefreshDocHidden()) return true;
|
|
25918
|
+
if (this.selectedItems && this.selectedItems.size > 0) return true;
|
|
25919
|
+
return false;
|
|
25920
|
+
}
|
|
25921
|
+
// ============================================================
|
|
25922
|
+
// View persistence (WM-035) — opt-in per-table saved view
|
|
25923
|
+
//
|
|
25924
|
+
// Persisted blob shape (versioned): `{ v: 1, sort?, size?, dayRange?,
|
|
25925
|
+
// filters?: {…raw params…}, hidden?: [columnKey] }`. `filters` is the raw
|
|
25926
|
+
// `collection.params` set MINUS start/size/sort and the day-range field
|
|
25927
|
+
// param (which is re-derived from `dayRange`), so `field__in` collapsed keys
|
|
25928
|
+
// and `dr_*` daterange triplets round-trip verbatim (preset matching depends
|
|
25929
|
+
// on it). `hidden` is written by TableView's column chooser.
|
|
25930
|
+
// ============================================================
|
|
25931
|
+
/**
|
|
25932
|
+
* The localStorage key for this view's saved state. Uses the explicit
|
|
25933
|
+
* `persistKey` when supplied, else a stable identity of page route +
|
|
25934
|
+
* collection endpoint (`<pathname>::<endpoint>`), namespaced under
|
|
25935
|
+
* `mojo:tableview:`.
|
|
25936
|
+
* @private
|
|
25937
|
+
*/
|
|
25938
|
+
_persistStorageKey() {
|
|
25939
|
+
const id = this.persistKey || this._defaultPersistKey();
|
|
25940
|
+
return `mojo:tableview:${id}`;
|
|
25941
|
+
}
|
|
25942
|
+
/** @private */
|
|
25943
|
+
_defaultPersistKey() {
|
|
25944
|
+
const route = typeof window !== "undefined" && window.location && window.location.pathname || "";
|
|
25945
|
+
const endpoint = this.collection && this.collection.endpoint || "";
|
|
25946
|
+
return `${route}::${endpoint}`;
|
|
25947
|
+
}
|
|
25948
|
+
/**
|
|
25949
|
+
* Resolve the localStorage backing object, or null when it's unavailable
|
|
25950
|
+
* (SSR / privacy mode / access throws). All storage access funnels through
|
|
25951
|
+
* here so the feature degrades to a no-op rather than throwing.
|
|
25952
|
+
* @private
|
|
25953
|
+
*/
|
|
25954
|
+
_persistStorage() {
|
|
25955
|
+
try {
|
|
25956
|
+
const ls = typeof globalThis !== "undefined" ? globalThis.localStorage : null;
|
|
25957
|
+
if (ls && typeof ls.getItem === "function") return ls;
|
|
25958
|
+
} catch (e) {
|
|
25959
|
+
}
|
|
25960
|
+
return null;
|
|
25961
|
+
}
|
|
25962
|
+
/**
|
|
25963
|
+
* Read + validate the saved blob. Discards (and clears) anything that isn't
|
|
25964
|
+
* this schema version or won't parse, returning null.
|
|
25965
|
+
* @private
|
|
25966
|
+
*/
|
|
25967
|
+
_readPersistedState() {
|
|
25968
|
+
const store = this._persistStorage();
|
|
25969
|
+
if (!store) return null;
|
|
25970
|
+
let raw;
|
|
25971
|
+
try {
|
|
25972
|
+
raw = store.getItem(this._persistStorageKey());
|
|
25973
|
+
} catch (e) {
|
|
25974
|
+
return null;
|
|
25975
|
+
}
|
|
25976
|
+
if (!raw) return null;
|
|
25977
|
+
let parsed;
|
|
25978
|
+
try {
|
|
25979
|
+
parsed = JSON.parse(raw);
|
|
25980
|
+
} catch (e) {
|
|
25981
|
+
this._clearPersistedStorage();
|
|
25982
|
+
return null;
|
|
25983
|
+
}
|
|
25984
|
+
if (!parsed || typeof parsed !== "object" || parsed.v !== 1) {
|
|
25985
|
+
this._clearPersistedStorage();
|
|
25986
|
+
return null;
|
|
25987
|
+
}
|
|
25988
|
+
return parsed;
|
|
25989
|
+
}
|
|
25990
|
+
/** @private */
|
|
25991
|
+
_writePersistedState(state) {
|
|
25992
|
+
const store = this._persistStorage();
|
|
25993
|
+
if (!store) return;
|
|
25994
|
+
try {
|
|
25995
|
+
store.setItem(this._persistStorageKey(), JSON.stringify(state));
|
|
25996
|
+
} catch (e) {
|
|
25997
|
+
}
|
|
25998
|
+
}
|
|
25999
|
+
/** @private */
|
|
26000
|
+
_clearPersistedStorage() {
|
|
26001
|
+
const store = this._persistStorage();
|
|
26002
|
+
if (!store) return;
|
|
26003
|
+
try {
|
|
26004
|
+
store.removeItem(this._persistStorageKey());
|
|
26005
|
+
} catch (e) {
|
|
26006
|
+
}
|
|
26007
|
+
}
|
|
26008
|
+
/**
|
|
26009
|
+
* Rehydrate the collection params from the saved blob. Fills only slots the
|
|
26010
|
+
* current query didn't already set (URL > saved), so a shared link always
|
|
26011
|
+
* wins. The day-range value is restored onto `this.dayRangeFilter` so the
|
|
26012
|
+
* subsequent seed/control build uses it. Column visibility is delegated to
|
|
26013
|
+
* the TableView hook. No-op unless persistState is on.
|
|
26014
|
+
* @private
|
|
26015
|
+
*/
|
|
26016
|
+
_restorePersistedState(queryKeys = []) {
|
|
26017
|
+
if (!this.persistState || !this.collection) return;
|
|
26018
|
+
const saved = this._readPersistedState();
|
|
26019
|
+
if (!saved) return;
|
|
26020
|
+
const params = this.collection.params || (this.collection.params = {});
|
|
26021
|
+
const fromQuery = new Set(queryKeys);
|
|
26022
|
+
fromQuery.delete("start");
|
|
26023
|
+
fromQuery.delete("size");
|
|
26024
|
+
if (saved.sort !== void 0 && !fromQuery.has("sort")) params.sort = saved.sort;
|
|
26025
|
+
if (saved.size !== void 0) params.size = saved.size;
|
|
26026
|
+
if (saved.dayRange !== void 0 && this.dayRangeFilter) {
|
|
26027
|
+
const field = this.dayRangeFilter.field || "created";
|
|
26028
|
+
if (!fromQuery.has(`${field}__gte`)) {
|
|
26029
|
+
this.dayRangeFilter.value = saved.dayRange;
|
|
26030
|
+
}
|
|
26031
|
+
}
|
|
26032
|
+
if (saved.filters && typeof saved.filters === "object") {
|
|
26033
|
+
Object.keys(saved.filters).forEach((key) => {
|
|
26034
|
+
if (!fromQuery.has(key)) params[key] = saved.filters[key];
|
|
26035
|
+
});
|
|
26036
|
+
}
|
|
26037
|
+
if (typeof this._restoreHiddenColumns === "function") {
|
|
26038
|
+
this._restoreHiddenColumns(saved.hidden);
|
|
26039
|
+
}
|
|
26040
|
+
}
|
|
26041
|
+
/**
|
|
26042
|
+
* Serialize the current view state to localStorage. Called on every
|
|
26043
|
+
* `params-changed` (and by TableView on a column toggle). No-op unless
|
|
26044
|
+
* persistState is on.
|
|
26045
|
+
* @private
|
|
26046
|
+
*/
|
|
26047
|
+
_savePersistedState() {
|
|
26048
|
+
if (!this.persistState) return;
|
|
26049
|
+
const params = this.collection && this.collection.params || {};
|
|
26050
|
+
const dayRangeGteKey = this.dayRangeFilter ? `${this.dayRangeFilter.field || "created"}__gte` : null;
|
|
26051
|
+
const filters = {};
|
|
26052
|
+
Object.keys(params).forEach((key) => {
|
|
26053
|
+
if (key === "start" || key === "size" || key === "sort") return;
|
|
26054
|
+
if (dayRangeGteKey && key === dayRangeGteKey) return;
|
|
26055
|
+
const value = params[key];
|
|
26056
|
+
if (value === void 0 || value === null || value === "") return;
|
|
26057
|
+
filters[key] = value;
|
|
26058
|
+
});
|
|
26059
|
+
const state = { v: 1 };
|
|
26060
|
+
if (params.sort !== void 0) state.sort = params.sort;
|
|
26061
|
+
if (params.size !== void 0) state.size = params.size;
|
|
26062
|
+
if (this.dayRangeControl && typeof this.dayRangeControl.getValue === "function") {
|
|
26063
|
+
state.dayRange = this.dayRangeControl.getValue();
|
|
26064
|
+
} else if (this.dayRangeFilter) {
|
|
26065
|
+
state.dayRange = this.dayRangeFilter.value;
|
|
26066
|
+
}
|
|
26067
|
+
if (Object.keys(filters).length > 0) state.filters = filters;
|
|
26068
|
+
if (typeof this._getHiddenColumnKeys === "function") {
|
|
26069
|
+
const hidden = this._getHiddenColumnKeys();
|
|
26070
|
+
if (hidden && hidden.length > 0) state.hidden = hidden;
|
|
26071
|
+
}
|
|
26072
|
+
this._writePersistedState(state);
|
|
26073
|
+
}
|
|
26074
|
+
/**
|
|
26075
|
+
* Public escape hatch — forget this table's saved view (both the view state
|
|
26076
|
+
* and, via the TableView hook, any hidden columns), reverting to defaults.
|
|
26077
|
+
*/
|
|
26078
|
+
clearPersistedState() {
|
|
26079
|
+
this._clearPersistedStorage();
|
|
26080
|
+
if (typeof this._onClearPersistedState === "function") this._onClearPersistedState();
|
|
26081
|
+
}
|
|
26082
|
+
// ============================================================
|
|
25186
26083
|
// Cleanup
|
|
25187
26084
|
// ============================================================
|
|
25188
26085
|
async destroy() {
|
|
26086
|
+
this._stopAutoRefresh();
|
|
25189
26087
|
if (this.collection) {
|
|
25190
26088
|
this.collection.off("add", this._onModelsAdded, this);
|
|
25191
26089
|
this.collection.off("remove", this._onModelsRemoved, this);
|
|
@@ -25255,11 +26153,31 @@ var MOJO = (function(exports) {
|
|
|
25255
26153
|
}
|
|
25256
26154
|
return "";
|
|
25257
26155
|
}
|
|
26156
|
+
/**
|
|
26157
|
+
* Whether this row's TableView has an `rowExpand` callback configured.
|
|
26158
|
+
* Guarded (feature-detect the method) so plain/stub tableViews — and the
|
|
26159
|
+
* "no rowExpand option" case — render byte-identical markup with no chevron
|
|
26160
|
+
* column.
|
|
26161
|
+
*/
|
|
26162
|
+
_isRowExpandEnabled() {
|
|
26163
|
+
return !!(this.tableView && typeof this.tableView.isRowExpandEnabled === "function" && this.tableView.isRowExpandEnabled());
|
|
26164
|
+
}
|
|
25258
26165
|
/**
|
|
25259
26166
|
* Build the row template with table cells
|
|
25260
26167
|
*/
|
|
25261
26168
|
buildRowTemplate() {
|
|
25262
26169
|
let template = "";
|
|
26170
|
+
if (this._isRowExpandEnabled()) {
|
|
26171
|
+
template += `
|
|
26172
|
+
<td class="col-expand mojo-expand-cell">
|
|
26173
|
+
<button type="button" class="mojo-expand-toggle"
|
|
26174
|
+
data-action="toggle-expand"
|
|
26175
|
+
aria-label="Toggle details" aria-expanded="false">
|
|
26176
|
+
<i class="bi bi-chevron-right"></i>
|
|
26177
|
+
</button>
|
|
26178
|
+
</td>
|
|
26179
|
+
`;
|
|
26180
|
+
}
|
|
25263
26181
|
if (this.tableView && this.tableView.isSelectable()) {
|
|
25264
26182
|
template += `
|
|
25265
26183
|
<td style="padding: 0;">
|
|
@@ -25510,6 +26428,24 @@ var MOJO = (function(exports) {
|
|
|
25510
26428
|
if (id) {
|
|
25511
26429
|
this.element.setAttribute("data-id", id);
|
|
25512
26430
|
}
|
|
26431
|
+
if (this._isRowExpandEnabled()) {
|
|
26432
|
+
const expanded = !!(this.tableView.expandedRows && this.tableView.expandedRows.has(id));
|
|
26433
|
+
this.element.classList.toggle("expanded", expanded);
|
|
26434
|
+
const toggle = this.element.querySelector(".mojo-expand-toggle");
|
|
26435
|
+
if (toggle) toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
26436
|
+
}
|
|
26437
|
+
}
|
|
26438
|
+
/**
|
|
26439
|
+
* Toggle the inline detail row for this model. Delegates to the TableView,
|
|
26440
|
+
* which owns the detail-row DOM (it spans every column, so it lives outside
|
|
26441
|
+
* this single-`<tr>` view). `stopPropagation` prevents the click from also
|
|
26442
|
+
* triggering row-level actions or the context menu.
|
|
26443
|
+
*/
|
|
26444
|
+
async onActionToggleExpand(event, _element) {
|
|
26445
|
+
event.stopPropagation();
|
|
26446
|
+
if (this.tableView && typeof this.tableView.toggleRowExpand === "function") {
|
|
26447
|
+
await this.tableView.toggleRowExpand(this.model, this);
|
|
26448
|
+
}
|
|
25513
26449
|
}
|
|
25514
26450
|
/**
|
|
25515
26451
|
* Handle edit cell action
|
|
@@ -25898,6 +26834,12 @@ var MOJO = (function(exports) {
|
|
|
25898
26834
|
this.actions = options.actions || null;
|
|
25899
26835
|
this.contextMenu = options.contextMenu || options.rowContextMenu || null;
|
|
25900
26836
|
this.batchActions = options.batchActions || null;
|
|
26837
|
+
this.rowExpand = typeof options.rowExpand === "function" ? options.rowExpand : null;
|
|
26838
|
+
this.rowExpandMultiple = options.rowExpandMultiple === true;
|
|
26839
|
+
this.expandedRows = /* @__PURE__ */ new Set();
|
|
26840
|
+
this.expandChildViews = /* @__PURE__ */ new Map();
|
|
26841
|
+
this.columnChooser = options.columnChooser === true;
|
|
26842
|
+
this._hiddenColumns = /* @__PURE__ */ new Set();
|
|
25901
26843
|
this.searchable = options.searchable !== false;
|
|
25902
26844
|
this.sortable = options.sortable !== false;
|
|
25903
26845
|
this.filterable = options.filterable !== false;
|
|
@@ -25971,6 +26913,210 @@ var MOJO = (function(exports) {
|
|
|
25971
26913
|
}
|
|
25972
26914
|
});
|
|
25973
26915
|
}
|
|
26916
|
+
// ============================================================
|
|
26917
|
+
// Column chooser / visibility (WM-035)
|
|
26918
|
+
// ============================================================
|
|
26919
|
+
/**
|
|
26920
|
+
* Whether a column may be hidden via the chooser. Columns opt OUT with
|
|
26921
|
+
* `hideable: false` (e.g. an id or actions column) — everything else is
|
|
26922
|
+
* hideable by default.
|
|
26923
|
+
* @private
|
|
26924
|
+
*/
|
|
26925
|
+
_isColumnHideable(column) {
|
|
26926
|
+
return column && column.hideable !== false;
|
|
26927
|
+
}
|
|
26928
|
+
/**
|
|
26929
|
+
* Whether a column is currently hidden. A locked (`hideable: false`) column
|
|
26930
|
+
* is never hidden regardless of any stale persisted key.
|
|
26931
|
+
* @private
|
|
26932
|
+
*/
|
|
26933
|
+
_isColumnHidden(column) {
|
|
26934
|
+
if (!this.columnChooser) return false;
|
|
26935
|
+
if (!this._isColumnHideable(column)) return false;
|
|
26936
|
+
return this._hiddenColumns.has(column.key);
|
|
26937
|
+
}
|
|
26938
|
+
/**
|
|
26939
|
+
* The columns to actually render. Returns the caller's `columns` array
|
|
26940
|
+
* verbatim (same reference) when the chooser is off or nothing is hidden, so
|
|
26941
|
+
* the opt-out path is byte-identical. Never mutates `this.columns`.
|
|
26942
|
+
* @private
|
|
26943
|
+
*/
|
|
26944
|
+
_getVisibleColumns() {
|
|
26945
|
+
if (!this.columnChooser || this._hiddenColumns.size === 0) return this.columns;
|
|
26946
|
+
return this.columns.filter((column) => !this._isColumnHidden(column));
|
|
26947
|
+
}
|
|
26948
|
+
/**
|
|
26949
|
+
* Hidden column keys for persistence (only genuinely-hideable ones). Read by
|
|
26950
|
+
* ListView's `_savePersistedState`.
|
|
26951
|
+
* @private
|
|
26952
|
+
*/
|
|
26953
|
+
_getHiddenColumnKeys() {
|
|
26954
|
+
if (!this.columnChooser) return [];
|
|
26955
|
+
return this.columns.filter((column) => this._isColumnHideable(column) && this._hiddenColumns.has(column.key)).map((column) => column.key);
|
|
26956
|
+
}
|
|
26957
|
+
/**
|
|
26958
|
+
* Restore hidden columns from a persisted blob (called by ListView during
|
|
26959
|
+
* `_restorePersistedState`, before the first render). Rebuilds the baked
|
|
26960
|
+
* template + any already-built rows so the restored visibility takes effect.
|
|
26961
|
+
* @private
|
|
26962
|
+
*/
|
|
26963
|
+
_restoreHiddenColumns(hidden) {
|
|
26964
|
+
if (!this.columnChooser || !Array.isArray(hidden) || hidden.length === 0) return;
|
|
26965
|
+
hidden.forEach((key) => {
|
|
26966
|
+
const column = this.columns.find((c) => c.key === key);
|
|
26967
|
+
if (column && this._isColumnHideable(column)) this._hiddenColumns.add(key);
|
|
26968
|
+
});
|
|
26969
|
+
if (this._hiddenColumns.size === 0) return;
|
|
26970
|
+
this.template = this.buildTableTemplate();
|
|
26971
|
+
this._templateCache = null;
|
|
26972
|
+
if (this.itemViews.size > 0) this._buildItems();
|
|
26973
|
+
}
|
|
26974
|
+
/**
|
|
26975
|
+
* ListView `clearPersistedState()` hook — reset visibility to defaults (all
|
|
26976
|
+
* columns shown) and repaint.
|
|
26977
|
+
* @private
|
|
26978
|
+
*/
|
|
26979
|
+
_onClearPersistedState() {
|
|
26980
|
+
if (this._hiddenColumns.size === 0) return;
|
|
26981
|
+
this._hiddenColumns.clear();
|
|
26982
|
+
this._rebuildForColumnChange();
|
|
26983
|
+
}
|
|
26984
|
+
/**
|
|
26985
|
+
* Re-bake the table template (header/footer reflect the visible columns) and
|
|
26986
|
+
* rebuild the row views (TableRow re-generates its cells from the visible
|
|
26987
|
+
* column set), then re-render once. Returns the render promise so callers can
|
|
26988
|
+
* await a settled DOM. Used after any visibility change.
|
|
26989
|
+
* @private
|
|
26990
|
+
*/
|
|
26991
|
+
_rebuildForColumnChange() {
|
|
26992
|
+
this.template = this.buildTableTemplate();
|
|
26993
|
+
this._templateCache = null;
|
|
26994
|
+
this._clearItems();
|
|
26995
|
+
if (this.collection && !this.collection.isEmpty()) {
|
|
26996
|
+
this.isEmpty = false;
|
|
26997
|
+
this.collection.forEach((model, index) => this._createItemView(model, index));
|
|
26998
|
+
this._applyPersistedSelections();
|
|
26999
|
+
this._buildGroupHeaders();
|
|
27000
|
+
} else {
|
|
27001
|
+
this.isEmpty = true;
|
|
27002
|
+
}
|
|
27003
|
+
return this.render();
|
|
27004
|
+
}
|
|
27005
|
+
/**
|
|
27006
|
+
* Toggle a data-action="toggle-column" checkbox. Ignores locked columns.
|
|
27007
|
+
* Persists the new hidden set when persistState is on.
|
|
27008
|
+
*/
|
|
27009
|
+
async onActionToggleColumn(_event, element) {
|
|
27010
|
+
const key = element.getAttribute("data-column-key");
|
|
27011
|
+
const column = this.columns.find((c) => c.key === key);
|
|
27012
|
+
if (!column || !this._isColumnHideable(column)) return;
|
|
27013
|
+
if (this._hiddenColumns.has(key)) {
|
|
27014
|
+
this._hiddenColumns.delete(key);
|
|
27015
|
+
} else {
|
|
27016
|
+
this._hiddenColumns.add(key);
|
|
27017
|
+
}
|
|
27018
|
+
await this._rebuildForColumnChange();
|
|
27019
|
+
if (this.persistState) this._savePersistedState();
|
|
27020
|
+
this.emit("columns:change", { hidden: this._getHiddenColumnKeys() });
|
|
27021
|
+
}
|
|
27022
|
+
/**
|
|
27023
|
+
* "Reset to defaults" chooser entry — show every column again AND clear the
|
|
27024
|
+
* saved view (both column + view state share one storage entry).
|
|
27025
|
+
*/
|
|
27026
|
+
async onActionColumnChooserReset(event, _element) {
|
|
27027
|
+
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
27028
|
+
const hadHidden = this._hiddenColumns.size > 0;
|
|
27029
|
+
this._hiddenColumns.clear();
|
|
27030
|
+
this._clearPersistedStorage();
|
|
27031
|
+
if (hadHidden) await this._rebuildForColumnChange();
|
|
27032
|
+
this.emit("columns:reset");
|
|
27033
|
+
}
|
|
27034
|
+
/**
|
|
27035
|
+
* The icon-only "Columns" toolbar dropdown. Locked (`hideable: false`)
|
|
27036
|
+
* columns render as disabled, lock-marked rows; hideable columns are
|
|
27037
|
+
* whole-row-clickable checkboxes. The persistence footer only shows when
|
|
27038
|
+
* persistState is on. Returns '' when the chooser is off.
|
|
27039
|
+
* @private
|
|
27040
|
+
*/
|
|
27041
|
+
_buildColumnChooserTemplate() {
|
|
27042
|
+
if (!this.columnChooser) return "";
|
|
27043
|
+
const items = this.columns.map((column) => {
|
|
27044
|
+
const label = this.escapeHtml(column.label || column.title || column.key);
|
|
27045
|
+
if (!this._isColumnHideable(column)) {
|
|
27046
|
+
return `
|
|
27047
|
+
<label class="column-chooser-item is-locked" title="Always shown">
|
|
27048
|
+
<input class="form-check-input" type="checkbox" checked disabled>
|
|
27049
|
+
<span class="form-check-label">${label}</span>
|
|
27050
|
+
<i class="bi bi-lock-fill column-chooser-lock" aria-hidden="true"></i>
|
|
27051
|
+
</label>`;
|
|
27052
|
+
}
|
|
27053
|
+
const checked = this._isColumnHidden(column) ? "" : "checked";
|
|
27054
|
+
return `
|
|
27055
|
+
<label class="column-chooser-item">
|
|
27056
|
+
<input class="form-check-input" type="checkbox" ${checked}
|
|
27057
|
+
data-action="toggle-column" data-column-key="${this.escapeHtml(column.key)}">
|
|
27058
|
+
<span class="form-check-label">${label}</span>
|
|
27059
|
+
</label>`;
|
|
27060
|
+
}).join("");
|
|
27061
|
+
const persistHint = this.persistState ? '<div class="column-chooser-persist"><i class="bi bi-check2-circle"></i>Saved for this table</div>' : "";
|
|
27062
|
+
return `
|
|
27063
|
+
<div class="dropdown">
|
|
27064
|
+
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
|
|
27065
|
+
data-bs-toggle="dropdown" aria-expanded="false" title="Choose columns">
|
|
27066
|
+
<i class="bi bi-layout-three-columns"></i><span class="d-none d-xxl-inline ms-1">Columns</span>
|
|
27067
|
+
</button>
|
|
27068
|
+
<div class="dropdown-menu dropdown-menu-end column-chooser-menu" role="menu">
|
|
27069
|
+
<h6 class="dropdown-header">Show columns</h6>
|
|
27070
|
+
${items}
|
|
27071
|
+
<li><hr class="dropdown-divider"></li>
|
|
27072
|
+
<a class="dropdown-item d-flex align-items-center" href="#" data-action="column-chooser-reset">
|
|
27073
|
+
<i class="bi bi-arrow-counterclockwise me-2"></i>Reset to defaults
|
|
27074
|
+
</a>
|
|
27075
|
+
${persistHint}
|
|
27076
|
+
</div>
|
|
27077
|
+
</div>
|
|
27078
|
+
`;
|
|
27079
|
+
}
|
|
27080
|
+
/**
|
|
27081
|
+
* Scoped styles for the column-chooser dropdown. Token-first per
|
|
27082
|
+
* `.claude/rules/theming.md`; the one black-at-4% hover carries its
|
|
27083
|
+
* rgba(255,255,255) dark companion. Emitted once, only when the chooser is
|
|
27084
|
+
* enabled (opt-out path carries zero CSS).
|
|
27085
|
+
* @private
|
|
27086
|
+
*/
|
|
27087
|
+
_buildColumnChooserStyles() {
|
|
27088
|
+
return `
|
|
27089
|
+
<style>
|
|
27090
|
+
.column-chooser-menu { min-width: 268px; }
|
|
27091
|
+
.column-chooser-menu .dropdown-header {
|
|
27092
|
+
color: var(--bs-secondary-color);
|
|
27093
|
+
text-transform: uppercase;
|
|
27094
|
+
letter-spacing: 0.05em;
|
|
27095
|
+
font-size: 0.7rem;
|
|
27096
|
+
font-weight: 600;
|
|
27097
|
+
}
|
|
27098
|
+
.column-chooser-item {
|
|
27099
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
27100
|
+
margin: 0; padding: 0.35rem 1rem; cursor: pointer;
|
|
27101
|
+
}
|
|
27102
|
+
.column-chooser-item .form-check-input { margin-top: 0; flex: 0 0 auto; }
|
|
27103
|
+
.column-chooser-item .form-check-label { cursor: pointer; flex: 1 1 auto; color: var(--bs-body-color); }
|
|
27104
|
+
.column-chooser-item:hover { background: rgba(0, 0, 0, 0.04); }
|
|
27105
|
+
.column-chooser-item.is-locked { cursor: default; opacity: 0.72; }
|
|
27106
|
+
.column-chooser-item.is-locked:hover { background: transparent; }
|
|
27107
|
+
.column-chooser-item.is-locked .form-check-label { color: var(--bs-secondary-color); }
|
|
27108
|
+
.column-chooser-lock { color: var(--bs-secondary-color); font-size: 0.8rem; }
|
|
27109
|
+
.column-chooser-persist {
|
|
27110
|
+
padding: 0.4rem 1rem 0.15rem;
|
|
27111
|
+
color: var(--bs-secondary-color);
|
|
27112
|
+
font-size: 0.72rem;
|
|
27113
|
+
display: flex; align-items: center; gap: 0.35rem;
|
|
27114
|
+
}
|
|
27115
|
+
|
|
27116
|
+
[data-bs-theme="dark"] .column-chooser-item:hover { background: var(--bs-secondary-bg); }
|
|
27117
|
+
</style>
|
|
27118
|
+
`;
|
|
27119
|
+
}
|
|
25974
27120
|
/**
|
|
25975
27121
|
* Get responsive CSS classes for column visibility
|
|
25976
27122
|
* @param {string|object} visibility - Bootstrap breakpoint or config object
|
|
@@ -26125,6 +27271,170 @@ var MOJO = (function(exports) {
|
|
|
26125
27271
|
isSelectable() {
|
|
26126
27272
|
return this.batchActions && this.batchActions.length > 0 && this.selectionMode === "multiple";
|
|
26127
27273
|
}
|
|
27274
|
+
/**
|
|
27275
|
+
* Whether expandable detail rows are enabled (a `rowExpand` callback was
|
|
27276
|
+
* provided). Read by TableRow to decide whether to render the chevron cell.
|
|
27277
|
+
*/
|
|
27278
|
+
isRowExpandEnabled() {
|
|
27279
|
+
return typeof this.rowExpand === "function";
|
|
27280
|
+
}
|
|
27281
|
+
/**
|
|
27282
|
+
* Colspan for the full-width detail row: chevron col (+1 when enabled) +
|
|
27283
|
+
* selection col + data cols + actions col. Mirrors the group-header colspan
|
|
27284
|
+
* math (see `_groupHeaderViewOptions`).
|
|
27285
|
+
*/
|
|
27286
|
+
_getRowExpandColspan() {
|
|
27287
|
+
const dataCols = this._getVisibleColumns().length || 0;
|
|
27288
|
+
const selectCol = this.isSelectable() ? 1 : 0;
|
|
27289
|
+
const actionsCol = this.actions || this.contextMenu ? 1 : 0;
|
|
27290
|
+
const expandCol = this.isRowExpandEnabled() ? 1 : 0;
|
|
27291
|
+
return Math.max(1, expandCol + selectCol + dataCols + actionsCol);
|
|
27292
|
+
}
|
|
27293
|
+
/**
|
|
27294
|
+
* Toggle the inline detail row for `model`. Called by TableRow's chevron
|
|
27295
|
+
* handler. Single-open by default (`rowExpandMultiple: true` allows several).
|
|
27296
|
+
*/
|
|
27297
|
+
async toggleRowExpand(model, rowView) {
|
|
27298
|
+
if (!this.isRowExpandEnabled() || !model) return;
|
|
27299
|
+
const id = model.id;
|
|
27300
|
+
const isOpen = this.expandedRows.has(id);
|
|
27301
|
+
if (isOpen) {
|
|
27302
|
+
this._collapseRow(id);
|
|
27303
|
+
} else {
|
|
27304
|
+
if (!this.rowExpandMultiple) {
|
|
27305
|
+
Array.from(this.expandedRows).forEach((otherId) => this._collapseRow(otherId));
|
|
27306
|
+
}
|
|
27307
|
+
this.expandedRows.add(id);
|
|
27308
|
+
const rv = rowView || this.itemViews.get(id);
|
|
27309
|
+
if (rv && rv.element) {
|
|
27310
|
+
rv.element.classList.add("expanded");
|
|
27311
|
+
const toggle = rv.element.querySelector(".mojo-expand-toggle");
|
|
27312
|
+
if (toggle) toggle.setAttribute("aria-expanded", "true");
|
|
27313
|
+
}
|
|
27314
|
+
await this._renderExpandedRow(id);
|
|
27315
|
+
}
|
|
27316
|
+
this.emit("row:expand:toggle", { model, expanded: this.expandedRows.has(id) });
|
|
27317
|
+
}
|
|
27318
|
+
/**
|
|
27319
|
+
* Collapse a single expanded row: drop it from state, remove its detail
|
|
27320
|
+
* `<tr>` from the DOM, destroy any View payload, and reset the chevron.
|
|
27321
|
+
* @private
|
|
27322
|
+
*/
|
|
27323
|
+
_collapseRow(id) {
|
|
27324
|
+
this.expandedRows.delete(id);
|
|
27325
|
+
const detail = this.element?.querySelector(`tr.mojo-detail-row[data-detail-for="${id}"]`);
|
|
27326
|
+
if (detail) detail.remove();
|
|
27327
|
+
const child = this.expandChildViews.get(id);
|
|
27328
|
+
if (child) {
|
|
27329
|
+
this.removeChild(child);
|
|
27330
|
+
this.expandChildViews.delete(id);
|
|
27331
|
+
}
|
|
27332
|
+
const rv = this.itemViews.get(id);
|
|
27333
|
+
if (rv && rv.element) {
|
|
27334
|
+
rv.element.classList.remove("expanded");
|
|
27335
|
+
const toggle = rv.element.querySelector(".mojo-expand-toggle");
|
|
27336
|
+
if (toggle) toggle.setAttribute("aria-expanded", "false");
|
|
27337
|
+
}
|
|
27338
|
+
}
|
|
27339
|
+
/**
|
|
27340
|
+
* Build and insert the detail `<tr>` for an expanded model directly after
|
|
27341
|
+
* its data row. String payloads template into the panel; View payloads are
|
|
27342
|
+
* mounted via `addChild()` + an explicit `render()` (the Dynamic Children
|
|
27343
|
+
* pattern for children added after the parent has already rendered).
|
|
27344
|
+
* @private
|
|
27345
|
+
*/
|
|
27346
|
+
async _renderExpandedRow(id) {
|
|
27347
|
+
const rowView = this.itemViews.get(id);
|
|
27348
|
+
if (!rowView || !rowView.element || !rowView.element.parentNode) return;
|
|
27349
|
+
const existing = this.element?.querySelector(`tr.mojo-detail-row[data-detail-for="${id}"]`);
|
|
27350
|
+
if (existing) existing.remove();
|
|
27351
|
+
const model = rowView.model;
|
|
27352
|
+
let content;
|
|
27353
|
+
try {
|
|
27354
|
+
content = this.rowExpand(model);
|
|
27355
|
+
} catch (error) {
|
|
27356
|
+
console.error("TableView rowExpand() threw for model", id, error);
|
|
27357
|
+
return;
|
|
27358
|
+
}
|
|
27359
|
+
const colspan = this._getRowExpandColspan();
|
|
27360
|
+
const detailRow = document.createElement("tr");
|
|
27361
|
+
detailRow.className = "mojo-detail-row";
|
|
27362
|
+
detailRow.setAttribute("data-detail-for", id);
|
|
27363
|
+
const isView = content && typeof content === "object" && typeof content.render === "function";
|
|
27364
|
+
if (isView) {
|
|
27365
|
+
const containerId = `row-expand-${id}`;
|
|
27366
|
+
detailRow.innerHTML = `<td colspan="${colspan}"><div class="mojo-detail-panel"><div data-container="${containerId}"></div></div></td>`;
|
|
27367
|
+
rowView.element.after(detailRow);
|
|
27368
|
+
this.addChild(content, { containerId });
|
|
27369
|
+
this.expandChildViews.set(id, content);
|
|
27370
|
+
await content.render();
|
|
27371
|
+
} else {
|
|
27372
|
+
const html = content == null ? "" : String(content);
|
|
27373
|
+
detailRow.innerHTML = `<td colspan="${colspan}"><div class="mojo-detail-panel">${html}</div></td>`;
|
|
27374
|
+
rowView.element.after(detailRow);
|
|
27375
|
+
}
|
|
27376
|
+
}
|
|
27377
|
+
/**
|
|
27378
|
+
* Re-materialize every expanded detail row after a full re-render (which
|
|
27379
|
+
* wipes `innerHTML`, detaching detail rows and any View payload elements).
|
|
27380
|
+
* Rows whose model is no longer present — e.g. after a page change — are
|
|
27381
|
+
* collapsed, satisfying "page change collapses all".
|
|
27382
|
+
* @private
|
|
27383
|
+
*/
|
|
27384
|
+
async _renderExpandedRows() {
|
|
27385
|
+
if (!this.isRowExpandEnabled()) return;
|
|
27386
|
+
Array.from(this.expandedRows).forEach((id) => {
|
|
27387
|
+
if (!this.itemViews.has(id)) this._collapseRow(id);
|
|
27388
|
+
});
|
|
27389
|
+
if (this.expandedRows.size === 0) return;
|
|
27390
|
+
this.expandChildViews.forEach((child) => this.removeChild(child));
|
|
27391
|
+
this.expandChildViews.clear();
|
|
27392
|
+
await Promise.all(Array.from(this.expandedRows, (id) => this._renderExpandedRow(id)));
|
|
27393
|
+
}
|
|
27394
|
+
/**
|
|
27395
|
+
* Scoped inline styles for the chevron cell + detail panel. Emitted only
|
|
27396
|
+
* when `rowExpand` is enabled so the opt-out path has zero markup/CSS delta.
|
|
27397
|
+
* Light defaults first; the literal-tint dark companions are clustered at
|
|
27398
|
+
* the bottom (per .claude/rules/theming.md).
|
|
27399
|
+
* @private
|
|
27400
|
+
*/
|
|
27401
|
+
_buildRowExpandStyles() {
|
|
27402
|
+
return `
|
|
27403
|
+
<style>
|
|
27404
|
+
.table-view-component .col-expand { width: 2.4rem; padding: 0 !important; }
|
|
27405
|
+
.table-view-component .mojo-expand-toggle {
|
|
27406
|
+
display: flex; align-items: center; justify-content: center;
|
|
27407
|
+
width: 100%; height: 100%; padding: 0.4rem 0;
|
|
27408
|
+
cursor: pointer; color: var(--bs-secondary-color);
|
|
27409
|
+
background: transparent; border: 0;
|
|
27410
|
+
}
|
|
27411
|
+
.table-view-component .mojo-expand-toggle:hover { color: var(--bs-emphasis-color); }
|
|
27412
|
+
.table-view-component .mojo-expand-toggle .bi {
|
|
27413
|
+
transition: transform 0.18s ease; font-size: 0.95rem; line-height: 1;
|
|
27414
|
+
}
|
|
27415
|
+
.table-view-component .table-row.expanded .mojo-expand-toggle .bi { transform: rotate(90deg); }
|
|
27416
|
+
.table-view-component .table-row.expanded .mojo-expand-toggle { color: var(--bs-primary); }
|
|
27417
|
+
|
|
27418
|
+
.table-view-component tr.mojo-detail-row > td {
|
|
27419
|
+
padding: 0 !important;
|
|
27420
|
+
border-bottom: 1px solid var(--bs-border-color) !important;
|
|
27421
|
+
background: rgba(0, 0, 0, 0.02);
|
|
27422
|
+
}
|
|
27423
|
+
.table-view-component .mojo-detail-panel {
|
|
27424
|
+
margin: 0.6rem 0.85rem 0.9rem 3rem;
|
|
27425
|
+
background: var(--bs-tertiary-bg);
|
|
27426
|
+
border: 1px solid var(--bs-border-color);
|
|
27427
|
+
border-left: 3px solid var(--bs-primary);
|
|
27428
|
+
border-radius: 0.5rem;
|
|
27429
|
+
padding: 0.95rem 1.1rem;
|
|
27430
|
+
}
|
|
27431
|
+
|
|
27432
|
+
[data-bs-theme="dark"] .table-view-component tr.mojo-detail-row > td {
|
|
27433
|
+
background: rgba(255, 255, 255, 0.015);
|
|
27434
|
+
}
|
|
27435
|
+
</style>
|
|
27436
|
+
`;
|
|
27437
|
+
}
|
|
26128
27438
|
/**
|
|
26129
27439
|
* Build the complete table template
|
|
26130
27440
|
*/
|
|
@@ -26138,22 +27448,25 @@ var MOJO = (function(exports) {
|
|
|
26138
27448
|
})();
|
|
26139
27449
|
return `
|
|
26140
27450
|
<div class="mojo-table-wrapper">
|
|
27451
|
+
${this.isRowExpandEnabled() ? this._buildRowExpandStyles() : ""}
|
|
27452
|
+
${this.columnChooser ? this._buildColumnChooserStyles() : ""}
|
|
27453
|
+
${this._hasFeedbackFeature() ? this._buildFeedbackStyles() : ""}
|
|
26141
27454
|
${this.buildToolbarTemplate()}
|
|
26142
27455
|
${batchPanelTop}
|
|
26143
27456
|
<div class="table-container"${fontSize}>
|
|
26144
27457
|
{{#loading}}
|
|
26145
|
-
|
|
27458
|
+
${this._loadingContent(`<div class="mojo-table-loading d-flex justify-content-center align-items-center py-5">
|
|
26146
27459
|
<div class="spinner-border" role="status">
|
|
26147
27460
|
<span class="visually-hidden">Loading...</span>
|
|
26148
27461
|
</div>
|
|
26149
|
-
</div
|
|
27462
|
+
</div>`)}
|
|
26150
27463
|
{{/loading}}
|
|
26151
27464
|
{{^loading}}
|
|
26152
27465
|
{{#isEmpty}}
|
|
26153
|
-
|
|
27466
|
+
${this._emptyContent(`<div class="table-empty text-center py-5">
|
|
26154
27467
|
<i class="bi bi-inbox fa-2x mb-2 text-muted"></i>
|
|
26155
27468
|
<p class="text-muted">{{emptyMessage}}</p>
|
|
26156
|
-
</div
|
|
27469
|
+
</div>`)}
|
|
26157
27470
|
{{/isEmpty}}
|
|
26158
27471
|
{{^isEmpty}}
|
|
26159
27472
|
<table class="${this.buildTableClasses()}">
|
|
@@ -26169,6 +27482,31 @@ var MOJO = (function(exports) {
|
|
|
26169
27482
|
</div>
|
|
26170
27483
|
`;
|
|
26171
27484
|
}
|
|
27485
|
+
/**
|
|
27486
|
+
* TableView skeleton (WM-033) — a full `<table>` whose header matches the
|
|
27487
|
+
* real column layout and whose body is N shimmer rows (N from
|
|
27488
|
+
* `_skeletonRowCount()`). Cell widths echo the real columns via cycled
|
|
27489
|
+
* `skeleton-line w-*` classes; leading (expand / selection) and trailing
|
|
27490
|
+
* (actions) cells mirror the row structure so the silhouette lines up.
|
|
27491
|
+
* Overrides the ListView card-silhouette version.
|
|
27492
|
+
* @private
|
|
27493
|
+
*/
|
|
27494
|
+
_buildSkeletonHtml() {
|
|
27495
|
+
const rowCount = this._skeletonRowCount();
|
|
27496
|
+
const widths = ["w-90", "w-75", "w-60", "w-40", "w-25"];
|
|
27497
|
+
const lead = (this.isRowExpandEnabled() ? '<td class="col-expand"></td>' : "") + (this.isSelectable() ? "<td></td>" : "");
|
|
27498
|
+
const trail = this.actions || this.contextMenu ? '<td class="text-end"><span class="skeleton-line w-40 ms-auto"></span></td>' : "";
|
|
27499
|
+
const cells = this._getVisibleColumns().map((column, i) => {
|
|
27500
|
+
const alignClass = this.getAlignClass(column.align);
|
|
27501
|
+
const w = widths[i % widths.length];
|
|
27502
|
+
return `<td class="${alignClass}"><span class="skeleton-line ${w}"></span></td>`;
|
|
27503
|
+
}).join("");
|
|
27504
|
+
let body = "";
|
|
27505
|
+
for (let i = 0; i < rowCount; i++) {
|
|
27506
|
+
body += `<tr class="mojo-skeleton-row" aria-hidden="true">${lead}${cells}${trail}</tr>`;
|
|
27507
|
+
}
|
|
27508
|
+
return `<table class="${this.buildTableClasses()}">${this.buildTableHeaderTemplate()}<tbody>${body}</tbody></table>`;
|
|
27509
|
+
}
|
|
26172
27510
|
/**
|
|
26173
27511
|
* Build table CSS classes
|
|
26174
27512
|
*/
|
|
@@ -26208,6 +27546,9 @@ var MOJO = (function(exports) {
|
|
|
26208
27546
|
baseButtons = fullscreenBtn + baseButtons;
|
|
26209
27547
|
}
|
|
26210
27548
|
}
|
|
27549
|
+
if (this.columnChooser) {
|
|
27550
|
+
baseButtons += this._buildColumnChooserTemplate();
|
|
27551
|
+
}
|
|
26211
27552
|
return baseButtons;
|
|
26212
27553
|
}
|
|
26213
27554
|
/**
|
|
@@ -26215,6 +27556,9 @@ var MOJO = (function(exports) {
|
|
|
26215
27556
|
*/
|
|
26216
27557
|
buildTableHeaderTemplate() {
|
|
26217
27558
|
let headerCells = "";
|
|
27559
|
+
if (this.isRowExpandEnabled()) {
|
|
27560
|
+
headerCells += '<th class="col-expand mojo-expand-cell"></th>';
|
|
27561
|
+
}
|
|
26218
27562
|
if (this.isSelectable()) {
|
|
26219
27563
|
headerCells += `
|
|
26220
27564
|
<th style="width: 40px; padding: 0;">
|
|
@@ -26226,7 +27570,7 @@ var MOJO = (function(exports) {
|
|
|
26226
27570
|
</th>
|
|
26227
27571
|
`;
|
|
26228
27572
|
}
|
|
26229
|
-
this.
|
|
27573
|
+
this._getVisibleColumns().forEach((column) => {
|
|
26230
27574
|
const { fieldKey } = this.parseColumnKey(column.key);
|
|
26231
27575
|
const sortable = this.sortable && column.sortable !== false;
|
|
26232
27576
|
const currentSort = this.getSortBy() === fieldKey ? this.getSortDirection() : null;
|
|
@@ -26285,15 +27629,17 @@ var MOJO = (function(exports) {
|
|
|
26285
27629
|
*/
|
|
26286
27630
|
buildTableFooterTemplate() {
|
|
26287
27631
|
let footerCells = "";
|
|
27632
|
+
if (this.isRowExpandEnabled()) {
|
|
27633
|
+
footerCells += "<td></td>";
|
|
27634
|
+
}
|
|
26288
27635
|
if (this.isSelectable()) {
|
|
26289
27636
|
footerCells += "<td></td>";
|
|
26290
27637
|
}
|
|
26291
|
-
|
|
26292
|
-
this.columns.forEach((column, index) => {
|
|
27638
|
+
this._getVisibleColumns().forEach((column, index) => {
|
|
26293
27639
|
const responsiveClasses = this.getResponsiveClasses(column.visibility);
|
|
26294
27640
|
const alignClass = this.getAlignClass(column.align);
|
|
26295
27641
|
if (column.footer_total) {
|
|
26296
|
-
const safeKey = `col_${
|
|
27642
|
+
const safeKey = `col_${this.footerTotalColumns.indexOf(column)}`;
|
|
26297
27643
|
const formatter = this.parseColumnKey(column.key).formatter || column.formatter;
|
|
26298
27644
|
let cellContent;
|
|
26299
27645
|
if (formatter && typeof formatter === "string") {
|
|
@@ -26302,7 +27648,6 @@ var MOJO = (function(exports) {
|
|
|
26302
27648
|
cellContent = `{{footerTotals.${safeKey}.value}}`;
|
|
26303
27649
|
}
|
|
26304
27650
|
footerCells += `<td class="table-footer-total ${responsiveClasses} ${alignClass}" data-total-column="${safeKey}">${cellContent}</td>`;
|
|
26305
|
-
totalColumnIndex++;
|
|
26306
27651
|
} else if (index === 0) {
|
|
26307
27652
|
footerCells += `<td class="table-footer-label ${responsiveClasses} ${alignClass}"><strong>Totals</strong></td>`;
|
|
26308
27653
|
} else {
|
|
@@ -26406,7 +27751,7 @@ var MOJO = (function(exports) {
|
|
|
26406
27751
|
listView: this,
|
|
26407
27752
|
tableView: this,
|
|
26408
27753
|
template: this.itemTemplate,
|
|
26409
|
-
columns: this.
|
|
27754
|
+
columns: this._getVisibleColumns(),
|
|
26410
27755
|
actions: this.actions,
|
|
26411
27756
|
contextMenu: this.contextMenu,
|
|
26412
27757
|
batchActions: this.batchActions,
|
|
@@ -26437,6 +27782,7 @@ var MOJO = (function(exports) {
|
|
|
26437
27782
|
await super.onAfterRender();
|
|
26438
27783
|
if (this.hasFooterTotals) this.updateFooterTotals();
|
|
26439
27784
|
this.updateSortIcons();
|
|
27785
|
+
await this._renderExpandedRows();
|
|
26440
27786
|
}
|
|
26441
27787
|
// -------- Cell-editing events (table-only) --------
|
|
26442
27788
|
_onCellEdit(event) {
|
|
@@ -26448,6 +27794,22 @@ var MOJO = (function(exports) {
|
|
|
26448
27794
|
_onCellCancel(event) {
|
|
26449
27795
|
this.emit("cell:cancel", event);
|
|
26450
27796
|
}
|
|
27797
|
+
/**
|
|
27798
|
+
* WM-034 — extend ListView's auto-refresh pause predicate with the two
|
|
27799
|
+
* table-only cases: an in-progress inline cell edit (any TableRow with a
|
|
27800
|
+
* non-empty `editingCells` set) or an open row context menu / dropdown
|
|
27801
|
+
* (cheap `.dropdown-menu.show` DOM check). A silent refetch mid-edit would
|
|
27802
|
+
* discard the editor; mid-menu it would close the menu under the user.
|
|
27803
|
+
* @protected
|
|
27804
|
+
*/
|
|
27805
|
+
_autoRefreshShouldSkip() {
|
|
27806
|
+
if (super._autoRefreshShouldSkip()) return true;
|
|
27807
|
+
for (const row of this.itemViews.values()) {
|
|
27808
|
+
if (row && row.editingCells && row.editingCells.size > 0) return true;
|
|
27809
|
+
}
|
|
27810
|
+
if (this.element && this.element.querySelector(".dropdown-menu.show")) return true;
|
|
27811
|
+
return false;
|
|
27812
|
+
}
|
|
26451
27813
|
// ============================================================
|
|
26452
27814
|
// Grouped rows (TableView-aware overrides)
|
|
26453
27815
|
//
|
|
@@ -26473,13 +27835,14 @@ var MOJO = (function(exports) {
|
|
|
26473
27835
|
* @protected
|
|
26474
27836
|
*/
|
|
26475
27837
|
_groupHeaderViewOptions(_model, _key, _index) {
|
|
26476
|
-
const dataCols = this.
|
|
27838
|
+
const dataCols = this._getVisibleColumns().length || 0;
|
|
26477
27839
|
const selectCol = this.isSelectable() ? 1 : 0;
|
|
26478
27840
|
const actionsCol = this.actions || this.contextMenu ? 1 : 0;
|
|
27841
|
+
const expandCol = this.isRowExpandEnabled() ? 1 : 0;
|
|
26479
27842
|
return {
|
|
26480
27843
|
tagName: "tr",
|
|
26481
27844
|
className: `list-group-header-row list-group-header-row--${this.groupHeaderStyle}`,
|
|
26482
|
-
colspan: Math.max(1, dataCols + selectCol + actionsCol)
|
|
27845
|
+
colspan: Math.max(1, expandCol + dataCols + selectCol + actionsCol)
|
|
26483
27846
|
};
|
|
26484
27847
|
}
|
|
26485
27848
|
// ============================================================
|