dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
grid.js
Go to the documentation of this file.
1/**
2 * ============================================================
3 * DBX GRID – INVARIANTEN (UNVERLETZBAR)
4 * ============================================================
5 *
6 * Diese Regeln definieren das unveränderliche Verhalten des Grids.
7 * Sie gelten IMMER – unabhängig von Features, Bugfixes oder Refactorings.
8 *
9 * ------------------------------------------------------------
10 * INVARIANTE 1: EDIT ≠ SORT
11 * ------------------------------------------------------------
12 * - Eine Datenänderung (cellEdited, Save, Autosave)
13 * darf NIEMALS eine Sortierung auslösen oder verändern.
14 * - Sortierung ändert sich ausschließlich durch:
15 * - explizite User-Aktion (Header-Klick)
16 * - expliziten Restore beim Reload
17 * - explizite Remote-Neulieferung durch Server
18 * - Kein implizites Re-Sort durch row.update(), reactiveData o.ä.
19 *
20 * ------------------------------------------------------------
21 * INVARIANTE 2: RELOAD DARF KEINE DATEN VERLIEREN
22 * ------------------------------------------------------------
23 * - Nach Reload dürfen keine Zeilen verschwinden.
24 * - Auch nicht bei:
25 * - veraltetem Sort-State
26 * - geänderten Spalten / Schema
27 * - kaputtem Layout-State
28 * - Im Zweifel:
29 * - Sort verwerfen
30 * - Layout best-effort anwenden
31 * - Default anzeigen
32 *
33 * ------------------------------------------------------------
34 * INVARIANTE 3: PERSISTENTER STATE IST OPTIONAL
35 * ------------------------------------------------------------
36 * - Gespeicherter State (Layout, Sort, Height, Pagination)
37 * ist immer hilfreich,
38 * aber niemals verpflichtend.
39 * - Ungültiger oder inkompatibler State wird ignoriert oder bereinigt.
40 * - Persistenz darf UX niemals verschlechtern.
41 *
42 * ------------------------------------------------------------
43 * INVARIANTE 4: SYSTEM-SPALTEN SIND HEILIG
44 * ------------------------------------------------------------
45 * - System-Spalten (z.B. _actions, _rownum, _*)
46 * sind NICHT Teil des User-Layouts.
47 * - Sie dürfen:
48 * - nicht gespeichert
49 * - nicht sortiert
50 * - nicht verschoben
51 * - nicht ausgeblendet
52 * werden.
53 * - User-State darf System-Spalten niemals beeinflussen.
54 *
55 * ------------------------------------------------------------
56 * INVARIANTE 5: NUR USER-AKTIONEN SIND SICHTBAR
57 * ------------------------------------------------------------
58 * - Systeminterne Aktionen (Save, Autosave, Restore, Sync)
59 * dürfen keine sichtbaren Layout-, Sort- oder UI-Sprünge erzeugen.
60 * - Wenn der User nichts geklickt hat,
61 * darf sich visuell nichts „magisch“ verändern.
62 *
63 * ------------------------------------------------------------
64 * INVARIANTE 6: DEFENSIVES RESTORE
65 * ------------------------------------------------------------
66 * - Restore ist immer best-effort.
67 * - Unbekannte Spalten, Sort-Felder oder States
68 * werden verworfen oder auf Default gesetzt.
69 * - Der User darf jederzeit sauber neu sortieren oder anordnen.
70 *
71 * ============================================================
72 * MERKSATZ:
73 * Das Grid darf NIE überraschen.
74 * Vorhersehbares Verhalten ist wichtiger als Feature-Vollständigkeit.
75 * ============================================================
76 */
77
78
79/**
80 * dbx grid feature (Tabulator)
81 * -------------------------------------------------
82 * requires: core.js (dbx namespace + loader)
83 * -------------------------------------------------
84 */
85
86(function() {
87 window.dbxGrid = window.dbxGrid || {};
88
89 if (!window.dbx || !dbx.feature) {
90 console.error('[dbx][grid] dbx core missing');
91 return;
92 }
93
94 dbx.feature.register('grid', {
95
96 prio: 'mid',
97
98 css: [
99 ['css','root','add_ons/tabulator/dist/css/tabulator.min.css'],
100 ['css','design','c-grid.css']
101 ],
102
103 js: [
104 ['js','lib','ajax.js'],
105 ['js','root','add_ons/tabulator/dist/js/tabulator.min.js']
106 ],
107
108 scope: 'element',
109
110
111 /* =========================================================
112 * SCHEMA AUTOLOAD (design/js/<schema>.js)
113 * ========================================================= */
114 loadSchema(schemaName, done) {
115
116 if (
117 window.dbxGridSchema &&
118 window.dbxGridSchema[schemaName]
119 ) {
120 done();
121 return;
122 }
123
124 const url =
125 dbx.config.rootPath +
126 'design/' +
127 dbx.getDesign() +
128 '/js/' +
129 schemaName +
130 '.js';
131
132 dbx.log('[grid][schema] load', url);
133
134 dbx.loader.js(url, () => {
135 if (
136 window.dbxGridSchema &&
137 window.dbxGridSchema[schemaName]
138 ) {
139 done();
140 } else {
141 dbx.error('[grid][schema] loaded but not registered:', schemaName);
142 }
143 });
144 },
145
146
147 /* =========================================================
148 * INIT
149 * ========================================================= */
150 init(el, cfg) {
151
152 if (typeof window.Tabulator === "undefined") {
153 alert(
154 "[DBX ERROR]\n" +
155 "Missing dependency: Tabulator\n\n" +
156 "lib=grid\n" +
157 "id=" + (cfg.id || "undef") + "\n\n" +
158 "Check PREPARE js loading."
159 );
160 dbx.error("Tabulator missing");
161 return;
162 }
163
164 const heightRaw = String(cfg.height ?? '').trim().toLowerCase();
165 const autoHeight = (heightRaw === '' || heightRaw === 'auto' || heightRaw === 'content');
166 const height = autoHeight ? false : (parseInt(cfg.height, 10) || 400);
167 const minHeight = cfg.minheight ? parseInt(cfg.minheight, 10) : false;
168 const maxHeight = cfg.maxheight ? parseInt(cfg.maxheight, 10) : false;
169
170 const colsDef = cfg.cols || '';
171
172 const allowDelete = ((cfg.allowdelete ?? cfg.allowDelete ?? '1') == '1') && !!cfg.delete;
173 const allowEdit = ((cfg.allowedit ?? cfg.allowEdit ?? '1') == '1') && !!cfg.save;
174 const allowInsert = ((cfg.allowinsert ?? cfg.allowInsert ?? '1') == '1') && !!cfg.insert;
175
176 const headerFilter = this._bool(cfg.headerfilter ?? cfg.headerFilter ?? 1, true);
177 const headerSort = this._bool(cfg.headersort ?? cfg.headerSort ?? 1, true);
178
179 const headerFilterLiveFilter = this._bool(cfg.headerfilterlivefilter ?? cfg.headerFilterLiveFilter ?? 1, true);
180 const headerFilterPlaceholder = String(cfg.headerfilterplaceholder ?? cfg.headerFilterPlaceholder ?? '');
181
182 const pagination = this._bool(cfg.pagination ?? 0, false);
183 const paginationMode = String(cfg.paginationmode || cfg.paginationMode || 'local').toLowerCase();
184 const progressiveLoad = String(cfg.progressiveload || cfg.progressiveLoad || '').toLowerCase();
185 const pageSize = parseInt(cfg.pagesize ?? cfg.paginationSize ?? 15, 10) || 15;
186
187 const paginationSizeSelector = this._parsePaginationSizeSelector(
188 cfg.pagesizeselector ?? cfg.paginationSizeSelector ?? false
189 );
190
191 const paginationButtonCount = this._int(
192 cfg.paginationbuttoncount ?? cfg.paginationButtonCount ?? 5,
193 5
194 );
195
196 const paginationCounter = this._normalizePaginationCounter(
197 cfg.paginationcounter ?? cfg.paginationCounter ?? false
198 );
199
200 const paginationAddRow = String(
201 cfg.paginationaddrow ?? cfg.paginationAddRow ?? 'page'
202 ).toLowerCase() === 'table' ? 'table' : 'page';
203
204 const paginationOutOfRange = this._normalizePaginationOutOfRange(
205 cfg.paginationoutofrange ?? cfg.paginationOutOfRange ?? false
206 );
207
208 const paginationControls = this._bool(
209 cfg.paginationcontrols ?? cfg.paginationControls ?? 1,
210 true
211 );
212
213 const syncMode = String(cfg.syncmode || 'delta').toLowerCase();
214 const searchMode = String(cfg.searchmode || 'local').toLowerCase();
215 const syncRun = this._bool(cfg.sync_run ?? cfg.syncrun ?? 1, true);
216 const syncLed = this._bool(cfg.sync_led ?? cfg.syncled ?? 1, true);
217
218 const responsiveLayoutRaw = String(cfg.responsivelayout ?? cfg.responsiveLayout ?? '').toLowerCase().trim();
219 const responsiveLayout =
220 (!responsiveLayoutRaw || responsiveLayoutRaw === '0' || responsiveLayoutRaw === 'false' || responsiveLayoutRaw === 'off')
221 ? false
222 : responsiveLayoutRaw;
223
224 const movableColumns = this._bool(cfg.movablecolumns ?? cfg.movableColumns ?? 1, true);
225 const resizableColumns = this._bool(cfg.resizablecolumns ?? cfg.resizableColumns ?? 1, true);
226
227 const headerSortStart = this._normalizeHeaderSortStart(
228 cfg.headersortstart ?? cfg.headerSortStart ?? 'asc'
229 );
230
231 const headerSortTristate = this._bool(
232 cfg.headersorttristate ?? cfg.headerSortTristate ?? 0,
233 false
234 );
235
236 const searchPlaceholder = String(cfg.searchplaceholder ?? '🔍');
237 const searchWidth = this._int(cfg.searchwidth ?? 220, 220);
238
239 const heightMin = this._int(cfg.heightmin ?? 320, 320);
240 const heightMax = this._int(cfg.heightmax ?? 960, 960);
241 const heightStep = this._int(cfg.heightstep ?? 40, 40);
242
243 const showSearch = this._bool(cfg.showsearch ?? 1, true);
244 const showAutosave = this._bool(cfg.showautosave ?? 1, true);
245 const showGridLines = this._bool(cfg.showgridlines ?? 1, true);
246 const showHeight = this._bool(cfg.showheight ?? 1, true);
247 const showReload = this._bool(cfg.showreload ?? 1, true);
248 const showReset = this._bool(cfg.showreset ?? 1, true);
249 const showSave = this._bool(cfg.showsave ?? 1, true);
250 const showInsert = this._bool(cfg.showinsert ?? cfg.showInsert ?? 1, true);
251 const showColumns = this._bool(cfg.showcolumns ?? 1, true);
252 const showSyncStatus = this._bool(cfg.showsyncstatus ?? 1, true);
253 const showExportExcel = this._bool(cfg.showexportexcel ?? 0, false);
254 const showExportPdf = this._bool(cfg.showexportpdf ?? 0, false);
255
256 const exportFileName = String(cfg.exportfilename ?? (cfg.id || 'grid'));
257 const exportSheetName = String(cfg.exportsheetname ?? (cfg.id || 'grid'));
258 const pdfOrientation = String(cfg.pdforientation ?? 'landscape').toLowerCase() === 'portrait' ? 'portrait' : 'landscape';
259 const pdfTitle = String(cfg.pdftitle ?? document.title ?? 'Export');
260
261 let sortUrl = null;
262
263 if (cfg.sort && cfg.sort !== '0') {
264 sortUrl = cfg.sort;
265 }
266
267 const urls = {
268 read: cfg.read || null,
269 save: cfg.save || null,
270 delete: cfg.delete || null,
271 insert: cfg.insert || null,
272 sync: cfg.sync || null,
273 sort: sortUrl
274 };
275
276 dbx.log('[grid] init', {
277 id: cfg.id || 'undef',
278 pagination,
279 paginationMode,
280 pageSize,
281 paginationSizeSelector,
282 paginationButtonCount,
283 paginationCounter,
284 paginationControls,
285 progressiveLoad,
286 searchMode,
287 syncMode,
288 syncRun,
289 syncLed,
290 headerSort,
291 headerSortStart,
292 headerSortTristate,
293 read: urls.read,
294 save: urls.save,
295 sync: urls.sync,
296 sort: urls.sort
297 });
298
299 this.createTable(el, {
300 height,
301 minHeight,
302 maxHeight,
303 colsDef,
304 urls,
305 allowDelete,
306 allowEdit,
307 allowInsert,
308 deleteConfirmTitle: String(cfg.deleteconfirmtitle || dbx.translate({
309 de: '<i class="bi bi-trash"></i> Datensatz löschen',
310 en: '<i class="bi bi-trash"></i> Delete record',
311 es: '<i class="bi bi-trash"></i> Eliminar registro'
312 })),
313 deleteConfirmQuestion: String(cfg.deleteconfirmquestion || dbx.translate({
314 de: 'Diesen Datensatz wirklich löschen?',
315 en: 'Do you really want to delete this record?',
316 es: '¿Desea eliminar este registro?'
317 })),
318 deleteConfirmHint: String(cfg.deleteconfirmhint || dbx.translate({
319 de: '<small>Dieser Vorgang kann nicht rückgängig gemacht werden.</small>',
320 en: '<small>This action cannot be undone.</small>',
321 es: '<small>Esta acción no se puede deshacer.</small>'
322 })),
323 headerFilter,
324 headerSort,
325 headerFilterLiveFilter,
326 headerFilterPlaceholder,
327 headerSortStart,
328 headerSortTristate,
329 pagination,
330 paginationMode,
331 pageSize,
332 paginationSizeSelector,
333 paginationButtonCount,
334 paginationCounter,
335 paginationAddRow,
336 paginationOutOfRange,
337 paginationControls,
338 progressiveLoad,
339 syncMode,
340 searchMode,
341 syncRun,
342 syncLed,
343 responsiveLayout,
344 movableColumns,
345 resizableColumns,
346 searchPlaceholder,
347 searchWidth,
348 heightMin,
349 heightMax,
350 heightStep,
351 showSearch,
352 showAutosave,
353 showGridLines,
354 showHeight,
355 showReload,
356 showReset,
357 showSave,
358 showInsert,
359 showColumns,
360 showSyncStatus,
361 showExportExcel,
362 showExportPdf,
363 exportFileName,
364 exportSheetName,
365 pdfOrientation,
366 pdfTitle,
367 cfg
368 });
369 },
370
371
372 /* =========================================================
373 * DESTROY
374 * ========================================================= */
375 destroy(el, cfg) {
376
377 const table = el && el._dbxTable ? el._dbxTable : null;
378
379 try {
380 if (table) {
381 table._dbxDestroyed = true;
382 }
383 } catch (e) {}
384
385 try {
386 if (table && table._dbxLoopId) {
387 dbx.loop.hint(table._dbxLoopId, 'pause');
388 }
389 } catch (e) {
390 dbx.warn('[grid] destroy loop pause failed', e);
391 }
392
393 try {
394 if (table && table._dbxAutoTimer) {
395 clearTimeout(table._dbxAutoTimer);
396 table._dbxAutoTimer = null;
397 }
398 } catch (e) {
399 dbx.warn('[grid] destroy auto timer clear failed', e);
400 }
401
402 try {
403 if (table && table._dbxLayoutTimer) {
404 clearTimeout(table._dbxLayoutTimer);
405 table._dbxLayoutTimer = null;
406 }
407 } catch (e) {
408 dbx.warn('[grid] destroy layout timer clear failed', e);
409 }
410
411 try {
412 if (table && table._dbxPageLayoutTimer) {
413 clearTimeout(table._dbxPageLayoutTimer);
414 table._dbxPageLayoutTimer = null;
415 }
416 } catch (e) {
417 dbx.warn('[grid] destroy page layout timer clear failed', e);
418 }
419
420 try {
421 if (table && table._dbxChooserTimer) {
422 clearTimeout(table._dbxChooserTimer);
423 table._dbxChooserTimer = null;
424 }
425 } catch (e) {
426 dbx.warn('[grid] destroy chooser timer clear failed', e);
427 }
428
429 try {
430 if (table && typeof table.destroy === 'function') {
431 table.destroy();
432 }
433 } catch (e) {
434 dbx.warn('[grid] destroy table failed', e);
435 }
436
437 if (el) {
438 delete el._dbxGridInitialized;
439 delete el._dbxTable;
440 delete el._dbxFeature;
441 delete el._dbxOpt;
442 delete el._dbxSchemaParsed;
443 delete el._dbxApplyGridLines;
444 }
445 },
446
447
448 /* =========================================================
449 * DBX AJAX URL HELPER
450 * ========================================================= */
451 _dbxAjaxUrl(url, opts) {
452
453 opts = opts || {};
454
455 if (!url) return url;
456
457 let finalUrl = (dbx.ajax && typeof dbx.ajax.url === 'function')
458 ? dbx.ajax.url(url)
459 : url;
460
461 if (opts.background === true && finalUrl.indexOf('dbx_sync=') === -1) {
462 finalUrl += (finalUrl.indexOf('?') === -1 ? '?' : '&') + 'dbx_sync=0';
463 }
464
465 return finalUrl;
466 },
467
468
469 _getAjaxSorters(table) {
470
471 if (!table || !table.element) return [];
472
473 const opt = table.element._dbxOpt || {};
474
475 if (opt.headerSort !== true) {
476 return [];
477 }
478
479 if (table._dbxBuilt !== true) {
480 return [];
481 }
482
483 if (table._dbxIsRemotePagination === true) {
484 const sorters = table.getSorters ? table.getSorters() : [];
485 return Array.isArray(sorters) ? sorters : [];
486 }
487
488 if (opt.urls && opt.urls.sort) {
489 const s = table._dbxServerSort;
490
491 if (s && s.field && s.dir) {
492 return [{
493 field: s.field,
494 dir: s.dir
495 }];
496 }
497 }
498
499 const sorters = table.getSorters ? table.getSorters() : [];
500 return Array.isArray(sorters) ? sorters : [];
501 },
502
503 _applyServerSortIndicators(table) {
504
505 if (!table || !table.element) return;
506
507 const opt = table.element._dbxOpt || {};
508
509 if (!opt.urls || !opt.urls.sort) return;
510 if (table._dbxIsRemotePagination === true) return;
511
512 const active = table._dbxServerSort || null;
513 const cols = this._getLeafColumns(table);
514
515 cols.forEach(col => {
516
517 const field = col.getField ? col.getField() : null;
518 if (!field || field.startsWith('_')) return;
519
520 const el = col.getElement ? col.getElement() : null;
521 if (!el) return;
522
523 let aria = 'none';
524
525 if (active && active.field === field) {
526 aria = (active.dir === 'asc') ? 'ascending' : 'descending';
527 }
528
529 el.setAttribute('aria-sort', aria);
530 });
531 },
532
533
534
535 _dbxRequest(url, options = {}) {
536
537 if (!url) {
538 return Promise.reject(new Error('Missing URL'));
539 }
540
541 if (!dbx.ajax || typeof dbx.ajax.request !== 'function') {
542 return Promise.reject(new Error('ajax.js nicht geladen.'));
543 }
544
545 const method = String(options.method || 'GET').toUpperCase();
546 const headers = options.headers || {};
547 const body = (typeof options.body === 'undefined') ? null : options.body;
548 const responseType = options.responseType || 'json';
549 const startedAt = Date.now();
550 const skipRuntime = options.skipRuntime === true
551 || /[?&]dbx_sync=0(?:&|$)/.test(String(url || ''));
552
553 dbx.log('[grid][ajax] start', {
554 method: method,
555 url: url,
556 responseType: responseType
557 });
558
559 return dbx.ajax.request({
560 url: url,
561 method: method,
562 mode: responseType === 'json' ? 'json' : 'text',
563 body: body,
564 headers: headers,
565 timeout: options.timeout || 30000,
566 skipRuntime: skipRuntime
567 }).then(out => {
568 dbx.log('[grid][ajax] success', {
569 method: method,
570 url: url,
571 duration_ms: Date.now() - startedAt
572 });
573 return out;
574 }).catch(error => {
575 dbx.error('[grid][ajax] error', {
576 method: method,
577 url: url,
578 duration_ms: Date.now() - startedAt,
579 error: error
580 });
581 throw error;
582 });
583 },
584
585
586 _parsePaginationSizeSelector(v) {
587
588 if (v === undefined || v === null || v === '' || v === false || v === 0 || v === '0' || v === 'off' || v === 'false') {
589 return false;
590 }
591
592 if (v === true || v === 1 || v === '1' || v === 'on' || v === 'true' || v === 'auto') {
593 return true;
594 }
595
596 const normalizeValue = (item) => {
597 if (item === true) return 99999;
598
599 const txt = String(item).trim().toLowerCase();
600
601 if (!txt) return null;
602 if (txt === 'true') return 99999;
603 if (txt === 'all') return 99999;
604 if (txt === '*') return 99999;
605 if (txt === '__all') return 99999;
606
607 const n = parseInt(txt, 10);
608 if (!isNaN(n) && n > 0) return n;
609
610 return null;
611 };
612
613 if (Array.isArray(v)) {
614 const out = v.map(normalizeValue).filter(x => x !== null);
615 return out.length ? this._normalizePaginationSizeSelectorOrder(out) : false;
616 }
617
618 const out = String(v)
619 .split(',')
620 .map(normalizeValue)
621 .filter(x => x !== null);
622
623 return out.length ? this._normalizePaginationSizeSelectorOrder(out) : false;
624 },
625
626 _normalizePaginationSizeSelectorOrder(values) {
627
628 return [15, 5, 25, 50, 100, 99999];
629 },
630
631 _pageSizeSelectOptions() {
632
633 return [
634 { value: '1', text: '1' },
635 { value: '5', text: '5' },
636 { value: '15', text: '15' },
637 { value: '25', text: '25' },
638 { value: '50', text: '50' },
639 { value: '100', text: '100' },
640 { value: '99999', text: '*' }
641 ];
642 },
643
644 _normalizePageSizeValue(v, def = 15, selector = false) {
645
646 if (v === true) return 99999;
647
648 const txt = String(v ?? '').toLowerCase().trim();
649 if (txt === 'true' || txt === 'all' || txt === '*' || txt === '__all') return 99999;
650
651 const n = parseInt(txt, 10);
652 if (!isNaN(n) && n > 0) {
653 return n;
654 }
655
656 const defTxt = String(def ?? '').toLowerCase().trim();
657 if (def === true || defTxt === 'true' || defTxt === 'all' || defTxt === '*' || defTxt === '__all') {
658 return 99999;
659 }
660
661 const defNum = parseInt(def, 10);
662 if (!isNaN(defNum) && defNum > 0) {
663 return defNum;
664 }
665
666 if (Array.isArray(selector) && selector.length) {
667 return selector[0] === true ? 99999 : (parseInt(selector[0], 10) || 15);
668 }
669
670 return 15;
671 },
672
673 _storePageSizeState(gridId, value, def = 15, selector = false) {
674
675 const normalized = this._normalizePageSizeValue(value, def, selector);
676 dbx.uiSet('grid', gridId, 'PAGE.SIZE', String(normalized));
677 return normalized;
678 },
679
680 _getPageSizeState(gridId, def = 15, selector = false) {
681
682 const defaultSize = this._normalizePageSizeValue(def, 15, selector);
683 return this._normalizePageSizeValue(
684 dbx.uiGet('grid', gridId, 'PAGE.SIZE', String(defaultSize)),
685 defaultSize,
686 selector
687 );
688 },
689
690 _changePageSize(table, value, opt = {}) {
691
692 if (!table || !table.element) return;
693
694 const gridId = table.element.id || 'grid';
695 const pageSize = this._storePageSizeState(gridId, value, opt.pageSize || 15, opt.paginationSizeSelector);
696
697 table._dbxPageSizeState = pageSize;
698 opt.pageSize = pageSize;
699
700 try {
701 table._dbxPageSizeChanging = true;
702 if (typeof table.setPageSize === 'function') {
703 table.setPageSize(pageSize);
704 }
705 if (typeof table.setPage === 'function') {
706 table.setPage(1);
707 }
708 } catch (err) {
709 dbx.warn('[grid] page size change failed', err);
710 } finally {
711 table._dbxPageSizeChanging = false;
712 }
713
714 this.reloadTable(table, opt, { resetPage: true });
715 window.setTimeout(() => this._applyPaginationButtonLabels(table), 0);
716 },
717
718 _normalizePaginationCounter(v) {
719
720 if (v === undefined || v === null || v === '' || v === false || v === 0 || v === '0' || v === 'off' || v === 'false') {
721 return false;
722 }
723
724 const txt = String(v).toLowerCase().trim();
725
726 if (txt === '1' || txt === 'on' || txt === 'true') return 'rows';
727 if (txt === 'rows') return 'rows';
728 if (txt === 'pages') return 'pages';
729
730 return false;
731 },
732
733 _normalizeHeaderSortStart(v) {
734
735 const txt = String(v || 'asc').toLowerCase().trim();
736 return (txt === 'desc') ? 'desc' : 'asc';
737 },
738
739 _normalizePaginationOutOfRange(v) {
740
741 if (v === undefined || v === null || v === '') return false;
742
743 const txt = String(v).toLowerCase().trim();
744
745 if (txt === 'false' || txt === 'off' || txt === '0') return false;
746 if (txt === 'first' || txt === 'last' || txt === 'reset') return txt;
747
748 const n = parseInt(txt, 10);
749 if (!isNaN(n)) return n;
750
751 return false;
752 },
753
754 _getPaginationUiEls(el) {
755
756 const root = this._getRoot(el);
757
758 return {
759 root: root,
760 bar: root ? root.querySelector('[data-dbx-role="pagination-bar"]') : null,
761 controls: root ? root.querySelector('[data-dbx-role="pagination-controls"]') : null,
762 counter: root ? root.querySelector('[data-dbx-role="pagination-counter"]') : null
763 };
764 },
765
766 _setRoleVisible(root, role, show) {
767
768 if (!root) return;
769
770 const el = root.querySelector('[data-dbx-role="' + role + '"]');
771 if (!el) return;
772
773 el.style.display = show ? '' : 'none';
774 },
775
776 _loadRootScript(file, done) {
777
778 window.dbxGridExportDeps = window.dbxGridExportDeps || {};
779
780 const state = window.dbxGridExportDeps[file] || { status: 'new', callbacks: [] };
781 window.dbxGridExportDeps[file] = state;
782
783 if (state.status === 'loaded') {
784 done && done(true);
785 return;
786 }
787
788 if (state.status === 'loading') {
789 state.callbacks.push(done);
790 return;
791 }
792
793 state.status = 'loading';
794 state.callbacks = done ? [done] : [];
795
796 let url = dbx.config.rootPath + file;
797
798 const searchParams = new URLSearchParams(location.search);
799 const cacheBust = searchParams.get('dbx_nocache') || searchParams.get('cachebust');
800 if (cacheBust) {
801 url += (url.indexOf('?') === -1 ? '?' : '&') + 'dbx_nocache=' + encodeURIComponent(cacheBust);
802 }
803
804 const finish = (ok) => {
805
806 state.status = ok ? 'loaded' : 'error';
807
808 state.callbacks.forEach(cb => cb && cb(ok === true));
809 state.callbacks = [];
810 };
811
812 const xhr = new XMLHttpRequest();
813 xhr.open('GET', url, true);
814
815 xhr.onload = () => {
816 if (xhr.status < 200 || xhr.status >= 300) {
817 dbx.error('[grid] export dependency load failed', url, 'HTTP ' + xhr.status);
818 finish(false);
819 return;
820 }
821
822 try {
823 const run = new Function(
824 'window',
825 'self',
826 'globalThis',
827 'global',
828 'exports',
829 'module',
830 'define',
831 xhr.responseText + '\n//# sourceURL=' + url
832 );
833
834 run.call(window, window, window, window, window, undefined, undefined, undefined);
835 finish(true);
836 } catch (e) {
837 dbx.error('[grid] export dependency load failed', url, e);
838 finish(false);
839 }
840 };
841
842 xhr.onerror = () => {
843 dbx.error('[grid] export dependency load failed', url);
844 finish(false);
845 };
846
847 xhr.send();
848 },
849
850 _waitForExportDep(check, done, attempts) {
851
852 const maxAttempts = attempts || 20;
853
854 if (check()) {
855 done && done(true);
856 return;
857 }
858
859 if (maxAttempts <= 0) {
860 done && done(false);
861 return;
862 }
863
864 window.setTimeout(() => {
865 this._waitForExportDep(check, done, maxAttempts - 1);
866 }, 50);
867 },
868
869 _setTabulatorDependency(table, key, value) {
870
871 if (!table || !table.dependencyRegistry || !value) return false;
872
873 table.dependencyRegistry.deps = table.dependencyRegistry.deps || {};
874 table.dependencyRegistry.deps[key] = value;
875
876 return true;
877 },
878
879 _ensureExcelExportDeps(table, done) {
880
881 if (window.XLSX) {
882 done && done(this._setTabulatorDependency(table, 'XLSX', window.XLSX));
883 return;
884 }
885
886 this._loadRootScript('add_ons/tabulator-deps/xlsx.full.min.js', (ok) => {
887 if (ok !== true) {
888 done && done(false);
889 return;
890 }
891
892 this._waitForExportDep(
893 () => !!window.XLSX,
894 (ready) => {
895 done && done(ready === true && this._setTabulatorDependency(table, 'XLSX', window.XLSX));
896 }
897 );
898 });
899 },
900
901 _ensurePdfExportDeps(table, done) {
902
903 const hasAutoTable = () => !!(
904 window.jspdf &&
905 window.jspdf.jsPDF &&
906 window.jspdf.jsPDF.API &&
907 window.jspdf.jsPDF.API.autoTable
908 );
909
910 if (hasAutoTable()) {
911 done && done(this._setTabulatorDependency(table, 'jspdf', window.jspdf));
912 return;
913 }
914
915 const loadAutoTable = () => {
916 this._loadRootScript('add_ons/tabulator-deps/jspdf.plugin.autotable.min.js', (ok) => {
917 if (ok !== true) {
918 done && done(false);
919 return;
920 }
921
922 this._waitForExportDep(
923 hasAutoTable,
924 (ready) => {
925 done && done(ready === true && this._setTabulatorDependency(table, 'jspdf', window.jspdf));
926 }
927 );
928 });
929 };
930
931 if (window.jspdf && window.jspdf.jsPDF) {
932 loadAutoTable();
933 return;
934 }
935
936 this._loadRootScript('add_ons/tabulator-deps/jspdf.umd.min.js', (ok) => {
937 if (ok !== true || !(window.jspdf && window.jspdf.jsPDF)) {
938 done && done(false);
939 return;
940 }
941
942 loadAutoTable();
943 });
944 },
945
946 _applyPaginationButtonLabels(table) {
947
948 if (!table || !table.element) return;
949
950 const language = String(document.documentElement.lang || 'de')
951 .toLowerCase()
952 .slice(0, 2);
953 const translations = {
954 de: {
955 rowsPerPage: 'Zeilen pro Seite',
956 allRows: 'Alle Zeilen',
957 rows: 'Zeilen',
958 first: 'Erste Seite',
959 prev: 'Vorherige Seite',
960 next: 'Nächste Seite',
961 last: 'Letzte Seite',
962 showPage: 'Seite {page} anzeigen'
963 },
964 en: {
965 rowsPerPage: 'Rows per page',
966 allRows: 'All rows',
967 rows: 'rows',
968 first: 'First page',
969 prev: 'Previous page',
970 next: 'Next page',
971 last: 'Last page',
972 showPage: 'Show page {page}'
973 },
974 es: {
975 rowsPerPage: 'Filas por página',
976 allRows: 'Todas las filas',
977 rows: 'filas',
978 first: 'Primera página',
979 prev: 'Página anterior',
980 next: 'Página siguiente',
981 last: 'Última página',
982 showPage: 'Mostrar página {page}'
983 }
984 };
985 const text = translations[language] || translations.de;
986
987 const ui = this._getPaginationUiEls(table.element);
988 const controls = ui && ui.controls ? ui.controls : null;
989 if (!controls) return;
990
991 const sizeSelect = controls.querySelector('.tabulator-page-size');
992 if (sizeSelect) {
993 let icon = controls.querySelector('.dbx-grid-page-size-icon');
994
995 controls.querySelectorAll('label').forEach(label => {
996 if (String(label.textContent || '').trim().toLowerCase() === 'page size') {
997 label.remove();
998 }
999 });
1000
1001 if (!icon) {
1002 icon = document.createElement('span');
1003 icon.className = 'dbx-grid-page-size-icon';
1004 icon.innerHTML = '<i class="bi bi-list-ol"></i>';
1005 icon.setAttribute('title', text.rowsPerPage);
1006 icon.setAttribute('aria-hidden', 'true');
1007
1008 controls.insertBefore(icon, sizeSelect);
1009 }
1010
1011 sizeSelect.setAttribute('title', text.rowsPerPage);
1012 sizeSelect.setAttribute('aria-label', text.rowsPerPage);
1013
1014 const currentPageSize = table._dbxPageSizeState || (table.getPageSize ? table.getPageSize() : '');
1015 const currentValue = String(currentPageSize || sizeSelect.value || '15');
1016
1017 sizeSelect.innerHTML = '';
1018
1019 this._pageSizeSelectOptions().forEach(item => {
1020 const option = document.createElement('option');
1021 option.value = item.value;
1022 option.textContent = item.text;
1023 option.setAttribute('title', item.value === '99999' ? text.allRows : item.text + ' ' + text.rows);
1024 sizeSelect.appendChild(option);
1025 });
1026
1027 if (currentValue && sizeSelect.querySelector('option[value="' + currentValue + '"]')) {
1028 sizeSelect.value = currentValue;
1029 }
1030
1031 }
1032
1033 if (controls._dbxPageSizeStateBound !== true) {
1034 controls._dbxPageSizeStateBound = true;
1035 controls.addEventListener('change', (e) => {
1036 const select = e.target && e.target.closest ? e.target.closest('.tabulator-page-size') : null;
1037 if (!select) return;
1038
1039 e.preventDefault();
1040 e.stopImmediatePropagation();
1041
1042 const opt = table.element && table.element._dbxOpt ? table.element._dbxOpt : {};
1043 const value = this._normalizePageSizeValue(select.value, opt.pageSize || 15, opt.paginationSizeSelector);
1044 this._queueTableTimer(table, '_dbxPageSizeChangeTimer', () => {
1045 this._changePageSize(table, value, opt);
1046 }, 0);
1047 }, true);
1048 }
1049
1050 const buttons = controls.querySelectorAll('.tabulator-page');
1051 if (!buttons || !buttons.length) return;
1052
1053 const detectType = (btn) => {
1054
1055 const values = [
1056 String(btn.getAttribute('data-page') || '').toLowerCase().trim(),
1057 String(btn.getAttribute('aria-label') || '').toLowerCase().trim(),
1058 String(btn.getAttribute('title') || '').toLowerCase().trim(),
1059 String(btn.textContent || '').toLowerCase().trim()
1060 ];
1061
1062 const has = (needle) => values.some(v => v === needle || v.indexOf(needle) !== -1);
1063
1064 if (has('first')) return 'first';
1065 if (has('previous') || has('prev')) return 'prev';
1066 if (has('next')) return 'next';
1067 if (has('last')) return 'last';
1068
1069 return null;
1070 };
1071
1072 const defs = {
1073 first: {
1074 html: '<i class="bi bi-chevron-bar-left"></i>',
1075 label: text.first
1076 },
1077 prev: {
1078 html: '<i class="bi bi-chevron-left"></i>',
1079 label: text.prev
1080 },
1081 next: {
1082 html: '<i class="bi bi-chevron-right"></i>',
1083 label: text.next
1084 },
1085 last: {
1086 html: '<i class="bi bi-chevron-bar-right"></i>',
1087 label: text.last
1088 }
1089 };
1090
1091 buttons.forEach(btn => {
1092
1093 const type = detectType(btn);
1094 if (!type || !defs[type]) {
1095 const page = String(btn.textContent || '').trim();
1096 if (/^\d+$/.test(page)) {
1097 const label = text.showPage.replace('{page}', page);
1098 btn.setAttribute('title', label);
1099 btn.setAttribute('aria-label', label);
1100 }
1101 return;
1102 }
1103
1104 btn.dataset.dbxPageType = type;
1105 btn.innerHTML = defs[type].html;
1106 btn.setAttribute('title', defs[type].label);
1107 btn.setAttribute('aria-label', defs[type].label);
1108 });
1109 },
1110
1111 _ensureSortIcons(table) {
1112
1113 if (!table || !table.element) return;
1114 if (table._dbxBuilt !== true) return;
1115
1116 const opt = table.element._dbxOpt || {};
1117 const cols = this._getLeafColumns(table);
1118
1119 cols.forEach(col => {
1120
1121 const field = col.getField ? col.getField() : null;
1122 if (!field || field.startsWith('_')) return;
1123
1124 const def = col.getDefinition ? col.getDefinition() : {};
1125 const sortable =
1126 (opt.headerSort === true) &&
1127 (
1128 (def && def.headerSort === true) ||
1129 (def && typeof def.headerClick === 'function')
1130 );
1131
1132 const headerEl = col.getElement ? col.getElement() : null;
1133 if (!headerEl) return;
1134
1135 const titleEl =
1136 headerEl.querySelector('.tabulator-col-title') ||
1137 headerEl.querySelector('.tabulator-col-content') ||
1138 headerEl;
1139
1140 let iconEl = headerEl.querySelector('.dbx-grid-sort-icon');
1141
1142 if (!sortable) {
1143 if (iconEl) iconEl.remove();
1144 headerEl.classList.remove('dbx-grid-sortable');
1145 headerEl.classList.remove('dbx-grid-sort-asc');
1146 headerEl.classList.remove('dbx-grid-sort-desc');
1147 headerEl.classList.remove('dbx-grid-sort-none');
1148 headerEl.setAttribute('aria-sort', 'none');
1149 return;
1150 }
1151
1152 headerEl.classList.add('dbx-grid-sortable');
1153
1154 if (!iconEl) {
1155 iconEl = document.createElement('span');
1156 iconEl.className = 'dbx-grid-sort-icon';
1157 iconEl.innerHTML = '<i class="bi bi-arrow-down-up"></i>';
1158 titleEl.appendChild(iconEl);
1159 }
1160 });
1161 },
1162
1163 _applySortIndicators(table) {
1164
1165 if (!table || !table.element) return;
1166 if (table._dbxBuilt !== true) return;
1167
1168 const opt = table.element._dbxOpt || {};
1169 const cols = this._getLeafColumns(table);
1170
1171 this._ensureSortIcons(table);
1172
1173 let activeMap = {};
1174
1175 if (opt.headerSort === true) {
1176
1177 const sorters = this._getAjaxSorters(table);
1178
1179 if (Array.isArray(sorters)) {
1180 sorters.forEach(s => {
1181 if (!s || !s.field) return;
1182 activeMap[s.field] = s.dir || 'asc';
1183 });
1184 }
1185 }
1186
1187 cols.forEach(col => {
1188
1189 const field = col.getField ? col.getField() : null;
1190 if (!field || field.startsWith('_')) return;
1191
1192 const def = col.getDefinition ? col.getDefinition() : {};
1193 const sortable =
1194 (opt.headerSort === true) &&
1195 (
1196 (def && def.headerSort === true) ||
1197 (def && typeof def.headerClick === 'function')
1198 );
1199
1200 const headerEl = col.getElement ? col.getElement() : null;
1201 if (!headerEl) return;
1202
1203 const iconEl = headerEl.querySelector('.dbx-grid-sort-icon');
1204
1205 headerEl.classList.remove('dbx-grid-sort-asc');
1206 headerEl.classList.remove('dbx-grid-sort-desc');
1207 headerEl.classList.remove('dbx-grid-sort-none');
1208
1209 if (!sortable) {
1210 if (iconEl) iconEl.remove();
1211 headerEl.setAttribute('aria-sort', 'none');
1212 return;
1213 }
1214
1215 const dir = activeMap[field] || null;
1216
1217 if (!iconEl) return;
1218
1219 if (dir === 'asc') {
1220 iconEl.innerHTML = '<i class="bi bi-caret-up-fill"></i>';
1221 headerEl.classList.add('dbx-grid-sort-asc');
1222 headerEl.setAttribute('aria-sort', 'ascending');
1223 } else if (dir === 'desc') {
1224 iconEl.innerHTML = '<i class="bi bi-caret-down-fill"></i>';
1225 headerEl.classList.add('dbx-grid-sort-desc');
1226 headerEl.setAttribute('aria-sort', 'descending');
1227 } else {
1228 iconEl.innerHTML = '<i class="bi bi-arrow-down-up"></i>';
1229 headerEl.classList.add('dbx-grid-sort-none');
1230 headerEl.setAttribute('aria-sort', 'none');
1231 }
1232 });
1233 },
1234
1235 /* =========================================================
1236 * HELPERS
1237 * ========================================================= */
1238 _bool(v, def = false) {
1239 if (v === undefined || v === null || v === '') return def;
1240 if (v === true || v === 1 || v === '1' || v === 'on' || v === 'true') return true;
1241 if (v === false || v === 0 || v === '0' || v === 'off' || v === 'false') return false;
1242 return def;
1243 },
1244
1245 _int(v, def = 0) {
1246 const n = parseInt(v, 10);
1247 return isNaN(n) ? def : n;
1248 },
1249
1250 _isTableAlive(table) {
1251 return !!(
1252 table &&
1253 table._dbxDestroyed !== true &&
1254 table.element &&
1255 table.element.isConnected === true
1256 );
1257 },
1258
1259 _isTableLayoutReady(table) {
1260
1261 if (!this._isTableAlive(table)) return false;
1262
1263 const root = table.element;
1264 if (!root) return false;
1265
1266 return !!(
1267 root.querySelector('.tabulator-header') ||
1268 root.querySelector('.tabulator-tableholder')
1269 );
1270 },
1271
1272 _queueTableTimer(table, key, fn, delay = 0) {
1273
1274 if (!table || !key || typeof fn !== 'function') return;
1275
1276 if (table[key]) {
1277 clearTimeout(table[key]);
1278 table[key] = null;
1279 }
1280
1281 table[key] = setTimeout(() => {
1282 table[key] = null;
1283
1284 if (!this._isTableAlive(table)) return;
1285
1286 fn();
1287 }, delay);
1288 },
1289
1290 _getLeafColumns(table) {
1291
1292 const out = [];
1293 if (!table || typeof table.getColumns !== 'function') return out;
1294
1295 const walk = (cols) => {
1296 cols.forEach(col => {
1297 const field = col.getField && col.getField();
1298 if (field && !field.startsWith('_')) {
1299 out.push(col);
1300 }
1301 if (col.getSubColumns) {
1302 const subs = col.getSubColumns();
1303 if (subs && subs.length) {
1304 walk(subs);
1305 }
1306 }
1307 });
1308 };
1309
1310 walk(table.getColumns());
1311 return out;
1312 },
1313
1314 _restoreStoredColumnWidths(table, gridId) {
1315
1316 if (!this._isTableLayoutReady(table)) return;
1317
1318 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
1319 const cols = this._getLeafColumns(table);
1320
1321 cols.forEach(col => {
1322 const field = col.getField();
1323 if (!field || field.startsWith('_')) return;
1324
1325 const w = uiGet('COLUMNS.SIZE.' + field, null);
1326 if (w === null) return;
1327
1328 const width = parseInt(w, 10);
1329 if (isNaN(width) || width <= 0) return;
1330
1331 const currentWidth = col.getWidth();
1332 if (typeof currentWidth === 'number' && Math.abs(currentWidth - width) <= 1) {
1333 return;
1334 }
1335
1336 try {
1337 col.setWidth(width);
1338 } catch (e) {
1339 dbx.warn('[grid] restore width failed', field, width, e);
1340 }
1341 });
1342 },
1343
1344 _restoreStoredColumnVisibility(table, gridId) {
1345
1346 if (!this._isTableLayoutReady(table)) return;
1347
1348 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
1349 const cols = this._getLeafColumns(table);
1350
1351 cols.forEach(col => {
1352 const field = col.getField();
1353 if (!field || field.startsWith('_')) return;
1354
1355 const vis = uiGet('COLUMNS.VISIBLE.' + field, null);
1356 if (vis === null) return;
1357
1358 try {
1359 if (vis === '0' && col.isVisible()) col.hide();
1360 if (vis === '1' && !col.isVisible()) col.show();
1361 } catch (e) {
1362 dbx.warn('[grid] restore visibility failed', field, vis, e);
1363 }
1364 });
1365 },
1366
1367 _applyShiftGroupLabels(table) {
1368
1369 if (!this._isTableLayoutReady(table)) return;
1370
1371 const root = table.element;
1372 if (!root || !root.innerHTML.includes('~~')) return;
1373
1374 root.querySelectorAll('.tabulator-col-group').forEach(groupEl => {
1375
1376 const titleEl = groupEl.querySelector('.tabulator-col-title');
1377 if (!titleEl) return;
1378
1379 if (titleEl.querySelector('.dbx-shift-label')) return;
1380
1381 const raw = titleEl.textContent;
1382 if (!raw || !raw.includes('~~')) return;
1383
1384 const parts = raw.split('~~');
1385 if (parts.length !== 2) return;
1386
1387 const left = parts[0].trim();
1388 const right = parts[1].trim();
1389
1390 titleEl.innerHTML =
1391 '<div class="dbx-shift-label">' +
1392 '<span class="left">' + left + '</span>' +
1393 '<span class="right">' + right + '</span>' +
1394 '</div>';
1395 });
1396 },
1397
1398 _applyInitialLayoutState(table, gridId) {
1399
1400 if (!this._isTableLayoutReady(table)) return false;
1401
1402 try {
1403 table.blockRedraw();
1404
1405 this._restoreStoredColumnWidths(table, gridId);
1406 this._restoreStoredColumnVisibility(table, gridId);
1407 this._applyShiftGroupLabels(table);
1408
1409 } finally {
1410 try {
1411 table.restoreRedraw(true);
1412 } catch (e) {
1413 dbx.warn('[grid] restoreRedraw failed', e);
1414 }
1415 }
1416
1417 return true;
1418 },
1419
1420 _getRoot(el) {
1421 return el.closest('.dbx-grid');
1422 },
1423
1424 _findSaveButton(el) {
1425 const root = this._getRoot(el);
1426 let btn = root ? root.querySelector('[data-dbx="grid-save"]') : null;
1427 if (!btn) {
1428 const panel = el.closest('.dbx-panel');
1429 btn = panel ? panel.querySelector('[data-dbx="grid-save"]') : null;
1430 }
1431 return btn;
1432 },
1433
1434 _tableHasPendingEdits(table) {
1435 if (!table || typeof table.getEditedCells !== 'function') {
1436 return false;
1437 }
1438 try {
1439 const edited = table.getEditedCells();
1440 return Array.isArray(edited) && edited.length > 0;
1441 } catch (e) {
1442 return false;
1443 }
1444 },
1445
1446 _syncDirtyState(table) {
1447 const hasEdits = this._tableHasPendingEdits(table);
1448 if (hasEdits) {
1449 table._dbxDirty = true;
1450 }
1451 return table._dbxDirty === true || hasEdits;
1452 },
1453
1454 _markTableDirty(table, el) {
1455 table._dbxDirty = true;
1456 this.updateSaveButton(el, table);
1457 },
1458
1459 _getSyncEls(el) {
1460 const root = this._getRoot(el);
1461 return {
1462 root,
1463 led: root ? root.querySelector('.dbx-grid-sync-led') : null,
1464 count: root ? root.querySelector('.dbx-grid-sync-count') : null
1465 };
1466 },
1467
1468 _setLedState(led, state) {
1469
1470 if (!led) return;
1471
1472 if (led._dbxSyncLedEnabled === false) {
1473 if (led.style.display !== 'none') {
1474 led.style.display = 'none';
1475 }
1476 return;
1477 }
1478
1479 if (led.style.display === 'none') {
1480 led.style.display = 'inline-block';
1481 }
1482
1483 if (led._dbxState === state) return;
1484 led._dbxState = state;
1485
1486 let color = '#bbb';
1487
1488 if (state === 'loading') color = '#0d6efd';
1489 if (state === 'ok') color = '#198754';
1490 if (state === 'idle') color = '#bbb';
1491 if (state === 'error') color = '#dc3545';
1492
1493 if (led._dbxLastColor !== color) {
1494 led.style.backgroundColor = color;
1495 led._dbxLastColor = color;
1496 }
1497 },
1498
1499 _setSyncCount(countEl, value) {
1500
1501 if (!countEl) return;
1502
1503 const txt = String(value ?? '');
1504 if (countEl.textContent !== txt) {
1505 countEl.textContent = txt;
1506 }
1507 },
1508
1509 _clearConflictFlags(table) {
1510 if (!table || !table.element) return;
1511 table.element.querySelectorAll('.dbx-cell-conflict').forEach(el => {
1512 el.classList.remove('dbx-cell-conflict');
1513 });
1514 },
1515
1516 _rowIdField(table) {
1517 return (table && table.options && table.options.index) ? table.options.index : 'id';
1518 },
1519
1520 _collectEditedMap(table) {
1521
1522 const editedMap = {};
1523 const editedCells = table.getEditedCells();
1524
1525 if (!editedCells || !editedCells.length) {
1526 return editedMap;
1527 }
1528
1529 for (let i = 0; i < editedCells.length; i++) {
1530 const c = editedCells[i];
1531 const r = c.getRow();
1532 if (!r) continue;
1533
1534 const id = r.getData()?.[this._rowIdField(table)];
1535 const f = c.getField();
1536
1537 if (id == null || !f) continue;
1538
1539 if (!editedMap[id]) editedMap[id] = {};
1540 editedMap[id][f] = true;
1541 }
1542
1543 return editedMap;
1544 },
1545
1546 _applySchemaCellStyle(cell, rowData) {
1547
1548 if (!cell) return;
1549
1550 const table = cell.getTable();
1551 const schema = table?.element?._dbxSchemaParsed;
1552 if (!schema || !schema.columns) return;
1553
1554 const field = cell.getField();
1555 const colSchema = schema.columns[field];
1556 if (!colSchema) return;
1557
1558 const style = dbxGrid.evalCell(colSchema, cell.getValue(), rowData || cell.getRow().getData());
1559 if (style) {
1560 dbxGridApplyCellStyle(cell, style);
1561 }
1562 },
1563
1564 _ajaxResponse(table, url, params, response) {
1565
1566 if (response && typeof response === 'object') {
1567 if (typeof response.server_time !== 'undefined') {
1568 table._dbxServerTime = response.server_time || null;
1569 }
1570
1571 if (typeof response.count !== 'undefined') {
1572 table._dbxSyncCount = response.count || 0;
1573 }
1574 }
1575
1576 if (table._dbxIsRemotePagination === true || table._dbxIsProgressive === true) {
1577
1578 if (response && Array.isArray(response.data)) {
1579 return {
1580 last_page: response.last_page || 1,
1581 last_row: response.last_row,
1582 data: response.data
1583 };
1584 }
1585
1586 if (response && Array.isArray(response.rows)) {
1587 return {
1588 last_page: response.last_page || 1,
1589 last_row: response.last_row,
1590 data: response.rows
1591 };
1592 }
1593
1594 if (Array.isArray(response)) {
1595 return {
1596 last_page: 1,
1597 data: response
1598 };
1599 }
1600
1601 dbx.error('[grid] invalid paginated response', response);
1602 return {
1603 last_page: 1,
1604 data: []
1605 };
1606 }
1607
1608 if (response && Array.isArray(response.rows)) {
1609 return response.rows;
1610 }
1611
1612 if (Array.isArray(response)) {
1613 return response;
1614 }
1615
1616 dbx.error('[grid] invalid response', response);
1617 return [];
1618 },
1619
1620
1621 /* =========================================================
1622 * BUILD COLUMNS
1623 * ========================================================= */
1624 _escapeHtml(value) {
1625 return String(value ?? '')
1626 .replace(/&/g, '&amp;')
1627 .replace(/</g, '&lt;')
1628 .replace(/>/g, '&gt;')
1629 .replace(/"/g, '&quot;')
1630 .replace(/'/g, '&#039;');
1631 },
1632
1633 _deleteRecordLabel(data, idField) {
1634 if (!data || typeof data !== 'object') return '';
1635
1636 const parts = [];
1637 const name = data.display_name || data.name2 || data.name || data.uname || data.title || data.label || '';
1638 const email = data.email || '';
1639 const id = data[idField] ?? data.id ?? '';
1640
1641 if (name) parts.push(String(name));
1642 if (email) parts.push(String(email));
1643 if (id !== '') parts.push('ID ' + String(id));
1644
1645 return parts.join(' - ');
1646 },
1647
1648 _confirmDelete(data, idField, opt, source) {
1649 const feature = this;
1650 const label = feature._deleteRecordLabel(data, idField);
1651 const labelHtml = label
1652 ? '<div class="mt-2"><strong>' + feature._escapeHtml(label) + '</strong></div>'
1653 : '';
1654
1655 const openConfirm = function() {
1656 if (!window.dbx || !dbx.confirm || typeof dbx.confirm.open !== 'function') {
1657 (window.dbx && dbx.error ? dbx.error : console.error)('[grid] confirm feature missing');
1658 return Promise.resolve(false);
1659 }
1660
1661 return dbx.confirm.open({
1662 id: 'grid-delete-' + String((data && (data[idField] ?? data.id)) || Date.now()),
1663 root: source ? source.closest('[data-dbx]') : document.body,
1664 source,
1665 title: opt.deleteConfirmTitle,
1666 question: opt.deleteConfirmQuestion + labelHtml,
1667 hint: opt.deleteConfirmHint,
1668 buttons: 'yesno',
1669 labelyes: '<i class="bi bi-trash"></i> ' + dbx.translate({
1670 de: 'Löschen',
1671 en: 'Delete',
1672 es: 'Eliminar'
1673 }),
1674 labelno: '<i class="bi bi-x-lg"></i> ' + dbx.translate({
1675 de: 'Abbrechen',
1676 en: 'Cancel',
1677 es: 'Cancelar'
1678 }),
1679 closable: true,
1680 backdropclose: false,
1681 escclose: true
1682 }).then(result => result && result.action === 'yes');
1683 };
1684
1685 if (window.dbx && typeof dbx.loadFeature === 'function' && (!dbx.confirm || typeof dbx.confirm.open !== 'function')) {
1686 return new Promise(resolve => {
1687 dbx.loadFeature('confirm', function() {
1688 openConfirm().then(resolve).catch(() => resolve(false));
1689 });
1690 });
1691 }
1692
1693 return openConfirm().catch(() => false);
1694 },
1695
1696 buildColumns(opt) {
1697
1698 const colsDef = opt.colsDef;
1699 const gridId = opt._gridId;
1700
1701 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
1702
1703 const cols = [];
1704 const groups = {};
1705 const ungrouped = [];
1706 let hasGroups = false;
1707
1708 const hasActions = !!(opt.allowDelete);
1709
1710 const orderRaw = uiGet('COLUMNS.ORDER', null);
1711 const orderList = orderRaw
1712 ? orderRaw.split('|').filter(f => f && !f.startsWith('_'))
1713 : null;
1714
1715 const sortEnabled = (opt.headerSort === true);
1716 const useDedicatedServerSort = sortEnabled && !!(opt.urls.sort && opt._dbxIsRemotePagination !== true);
1717 const useTabulatorSort = sortEnabled && !useDedicatedServerSort;
1718
1719 const actionsCol = {
1720 title: '<i class="bi bi-gear"></i>',
1721 headerHozAlign: 'center',
1722 field: '_actions',
1723 width: 124,
1724 minWidth: 124,
1725 maxWidth: 124,
1726 hozAlign: 'center',
1727 headerHozAlign: 'center',
1728 frozen: true,
1729 headerSort: false,
1730 headerFilter: false,
1731 resizable: false,
1732 cssClass: 'dbx-col-actions',
1733 formatter: function(cell) {
1734 const wrap = document.createElement('div');
1735 wrap.style.display = 'flex';
1736 wrap.style.gap = '6px';
1737 wrap.style.justifyContent = 'center';
1738 wrap.style.alignItems = 'center';
1739
1740 const row = cell.getRow();
1741 const table = row.getTable();
1742 const data = row.getData();
1743
1744 if (data && data.show_link) {
1745 const btnShow = document.createElement('button');
1746 btnShow.className = 'btn btn-sm btn-outline-primary';
1747 btnShow.style.minWidth = '28px';
1748 btnShow.style.height = '25px';
1749 btnShow.style.padding = '2px 6px';
1750 btnShow.style.lineHeight = '1';
1751 btnShow.title = 'Anzeigen';
1752 btnShow.innerHTML = '<i class="bi bi-eye"></i>';
1753
1754 btnShow.addEventListener('click', function(e) {
1755 e.stopPropagation();
1756 const url = String(data.show_link || '');
1757 if (!url) return;
1758
1759 if (window.dbx && dbx.openWin && typeof dbx.openWin.open === 'function') {
1760 dbx.openWin.open({
1761 url: url,
1762 title: dbx.translate({
1763 de: 'Vorschau',
1764 en: 'Preview',
1765 es: 'Vista previa'
1766 }) + ': ' + String(data.title || 'Content'),
1767 width: 1280,
1768 height: 820,
1769 modal: 0,
1770 ajax: 1,
1771 scroll: 1,
1772 position: 'center',
1773 reloadable: 1,
1774 reuse: 1,
1775 allowDuplicate: 0
1776 }, btnShow);
1777 } else {
1778 if (window.dbx && dbx.utilities && dbx.utilities.leaveGuard) {
1779 dbx.utilities.leaveGuard.allowIfInternal(url);
1780 }
1781 window.location.href = url;
1782 }
1783 });
1784
1785 wrap.appendChild(btnShow);
1786 }
1787
1788 if (data && data.profile_link) {
1789 const btnEdit = document.createElement('button');
1790 btnEdit.className = 'btn btn-sm btn-outline-primary';
1791 btnEdit.style.minWidth = '28px';
1792 btnEdit.style.height = '25px';
1793 btnEdit.style.padding = '2px 6px';
1794 btnEdit.style.lineHeight = '1';
1795 btnEdit.title = dbx.translate({
1796 de: 'Bearbeiten',
1797 en: 'Edit',
1798 es: 'Editar'
1799 });
1800 btnEdit.innerHTML = '<i class="bi bi-pencil-square"></i>';
1801
1802 btnEdit.addEventListener('click', function(e) {
1803 e.stopPropagation();
1804 const url = String(data.profile_link || '');
1805 if (!url) return;
1806
1807 if (window.dbx && dbx.openWin && typeof dbx.openWin.open === 'function') {
1808 dbx.openWin.open({
1809 url: url,
1810 title: dbx.translate({
1811 de: 'Benutzer',
1812 en: 'User',
1813 es: 'Usuario'
1814 }),
1815 height: 760,
1816 width: 1280,
1817 modal: 1,
1818 scroll: 1,
1819 position: 'center'
1820 }, btnEdit);
1821 } else {
1822 if (window.dbx && dbx.utilities && dbx.utilities.leaveGuard) {
1823 dbx.utilities.leaveGuard.allowIfInternal(url);
1824 }
1825 window.location.href = url;
1826 }
1827 });
1828
1829 wrap.appendChild(btnEdit);
1830 }
1831
1832 if (opt.allowDelete) {
1833 const btnDel = document.createElement('button');
1834 btnDel.className = 'btn btn-sm btn-danger';
1835 btnDel.style.minWidth = '28px';
1836 btnDel.style.height = '25px';
1837 btnDel.style.padding = '2px 6px';
1838 btnDel.style.lineHeight = '1';
1839 btnDel.innerHTML = '<i class="bi bi-trash"></i>';
1840
1841 btnDel.addEventListener('click', function(e) {
1842 e.stopPropagation();
1843 const idField = table.element._dbxFeature._rowIdField(table);
1844 if (!data || typeof data[idField] === 'undefined') return;
1845
1846 table.element._dbxFeature._confirmDelete(data, idField, opt, btnDel)
1847 .then(confirmed => {
1848 if (!confirmed) return;
1849
1850 return table.element._dbxFeature._dbxRequest(
1851 table.element._dbxFeature._dbxAjaxUrl(opt.urls.delete || ''),
1852 {
1853 method: 'POST',
1854 headers: { 'Content-Type': 'application/json' },
1855 body: JSON.stringify({ id: data[idField] }),
1856 responseType: 'json'
1857 }
1858 );
1859 })
1860 .then(res => {
1861 if (!res) return;
1862 if (res && (res.ok || res.success)) {
1863 row.delete();
1864 } else {
1865 dbx.error('[grid] delete failed', res);
1866 }
1867 })
1868 .catch(err => {
1869 dbx.error('[grid] delete error', err);
1870 });
1871 });
1872
1873 wrap.appendChild(btnDel);
1874 }
1875
1876 return wrap;
1877 }
1878 };
1879
1880 const fieldDefinitions = colsDef.split(',');
1881 const colMap = {};
1882
1883 fieldDefinitions.forEach(def => {
1884
1885 let groupName = null;
1886
1887 if (def.includes('@')) {
1888 const tmp = def.split('@');
1889 def = tmp[0].trim();
1890 groupName = tmp[1].trim();
1891 hasGroups = true;
1892 }
1893
1894 const parts = def.split(':').map(s => s.trim());
1895
1896 const fieldInfo = dbxExtractLabel(parts[0]);
1897 const field = fieldInfo.key;
1898 const title = fieldInfo.label;
1899
1900 const gridType = String(parts[1] || '').toLowerCase();
1901 let flag = parts[2] || null;
1902 let optionRaw = parts.slice(3).join(':');
1903
1904 if (flag && flag.indexOf('=') !== -1) {
1905 optionRaw = parts.slice(2).join(':');
1906 flag = null;
1907 }
1908
1909 const colOptions = dbxGridParseColumnOptions(optionRaw);
1910
1911 if (!field || field.startsWith('_') || flag === '!v') return;
1912
1913 const visState = uiGet('COLUMNS.VISIBLE.' + field, '1');
1914
1915 const col = {
1916 title: title,
1917 field,
1918 visible: (visState !== '0'),
1919 headerSort: useTabulatorSort,
1920 headerSortStartingDir: opt.headerSortStart || 'asc',
1921 headerSortTristate: useTabulatorSort ? !!opt.headerSortTristate : false,
1922 sorter: colOptions.sorter || (gridType === 'number' ? 'number' : (gridType === 'date' ? 'date' : 'string')),
1923 headerFilter: opt.headerFilter ? 'input' : false,
1924 headerFilterLiveFilter: !!opt.headerFilterLiveFilter,
1925 headerFilterPlaceholder: opt.headerFilterPlaceholder || undefined,
1926 editor: (opt.allowEdit && flag !== 'p') ? 'input' : false,
1927 editable: (opt.allowEdit && flag !== 'p'),
1928 resizable: true,
1929
1930 formatter: (cell) => {
1931
1932 const value = cell.getValue();
1933
1934 if (gridType === 'image') {
1935 if (!value) return '';
1936 const img = document.createElement('img');
1937 img.src = String(value);
1938 img.alt = '';
1939 img.loading = 'lazy';
1940 img.style.width = colOptions.imgWidth || '38px';
1941 img.style.height = colOptions.imgHeight || '38px';
1942 img.style.objectFit = 'cover';
1943 img.style.borderRadius = colOptions.radius || '50%';
1944 img.style.display = 'block';
1945 img.style.margin = '0 auto';
1946 return img;
1947 }
1948
1949 if (colOptions.formatter === 'truncate' || colOptions.truncate === '1') {
1950 const text = (value === null || value === undefined) ? '' : String(value);
1951 const maxChars = parseInt(colOptions.maxChars || colOptions.maxchars || 180, 10) || 180;
1952 const shortText = text.length > maxChars ? text.substring(0, maxChars) + '...' : text;
1953 const div = document.createElement('div');
1954 div.className = 'dbx-grid-cell-truncate';
1955 div.textContent = shortText.replace(/\s+/g, ' ').trim();
1956 div.title = text;
1957 return div;
1958 }
1959
1960 const table = cell.getTable();
1961 const schema = table?.element?._dbxSchemaParsed;
1962 if (!schema || !schema.columns) return value;
1963
1964 const field = cell.getField();
1965 const colSchema = schema.columns[field];
1966 if (!colSchema) return value;
1967
1968 const rowData = cell.getRow().getData();
1969 const style = dbxGrid.evalCell(colSchema, value, rowData);
1970
1971 if (style) {
1972 dbxGridApplyCellStyle(cell, style);
1973 }
1974
1975 return value;
1976 },
1977 };
1978
1979 if (colOptions.width) col.width = parseInt(colOptions.width, 10) || col.width;
1980 if (colOptions.minWidth) col.minWidth = parseInt(colOptions.minWidth, 10) || col.minWidth;
1981 if (colOptions.maxWidth) col.maxWidth = parseInt(colOptions.maxWidth, 10) || col.maxWidth;
1982 if (colOptions.hozAlign) col.hozAlign = colOptions.hozAlign;
1983 if (colOptions.headerHozAlign) col.headerHozAlign = colOptions.headerHozAlign;
1984 if (colOptions.bigEditor === '1' || colOptions.bigeditor === '1') {
1985 col.cssClass = [col.cssClass, 'dbx-grid-cell-big-editor'].filter(Boolean).join(' ');
1986 }
1987
1988 if (gridType === 'image') {
1989 col.editor = false;
1990 col.editable = false;
1991 col.headerFilter = false;
1992 col.headerSort = false;
1993 }
1994
1995 if (opt.allowEdit && flag !== 'p' && colOptions.editor) {
1996 if (colOptions.editor === 'list' || colOptions.editor === 'select') {
1997 const lookupValues = dbxGridParseEditorValues(colOptions.values || '');
1998 col.editor = 'list';
1999 col.editorParams = {
2000 values: lookupValues
2001 };
2002 const baseFormatter = col.formatter;
2003 col.formatter = (cell) => {
2004 const value = cell.getValue();
2005 const key = (value === null || value === undefined) ? '' : String(value);
2006 const display = Object.prototype.hasOwnProperty.call(lookupValues, key)
2007 ? lookupValues[key]
2008 : value;
2009
2010 const table = cell.getTable();
2011 const schema = table?.element?._dbxSchemaParsed;
2012 if (schema && schema.columns) {
2013 const field = cell.getField();
2014 const colSchema = schema.columns[field];
2015 if (colSchema) {
2016 const rowData = cell.getRow().getData();
2017 const style = dbxGrid.evalCell(colSchema, value, rowData);
2018 if (style) {
2019 dbxGridApplyCellStyle(cell, style);
2020 }
2021 }
2022 }
2023
2024 if (!Object.prototype.hasOwnProperty.call(lookupValues, key) && typeof baseFormatter === 'function') {
2025 return baseFormatter(cell);
2026 }
2027
2028 return display;
2029 };
2030 } else if (colOptions.editor === 'textarea') {
2031 col.editor = 'textarea';
2032 } else if (colOptions.editor === 'input') {
2033 col.editor = 'input';
2034 }
2035 }
2036
2037 if (useDedicatedServerSort) {
2038 col.headerClick = (e, column) => {
2039
2040 const table = column.getTable();
2041 const field = column.getField();
2042 if (!field || field.startsWith('_')) return;
2043
2044 const current = table._dbxServerSort || null;
2045 const startDir = opt.headerSortStart || 'asc';
2046 const otherDir = (startDir === 'asc') ? 'desc' : 'asc';
2047
2048 let nextSort = null;
2049
2050 if (!current || current.field !== field) {
2051 nextSort = { field: field, dir: startDir };
2052 } else if (current.dir === startDir) {
2053 nextSort = { field: field, dir: otherDir };
2054 } else if (current.dir === otherDir && opt.headerSortTristate === true) {
2055 nextSort = null;
2056 } else {
2057 nextSort = { field: field, dir: startDir };
2058 }
2059
2060 table._dbxServerSort = nextSort;
2061
2062 table.element._dbxFeature._applySortIndicators(table);
2063
2064 if (!nextSort) {
2065 dbx.log('[grid] server sort cleared', {
2066 id: gridId,
2067 field: field
2068 });
2069
2070 table.setData(table.element._dbxFeature._dbxAjaxUrl(opt.urls.read));
2071 return;
2072 }
2073
2074 const url =
2075 table.element._dbxFeature._dbxAjaxUrl(
2076 opt.urls.sort +
2077 '&field=' + encodeURIComponent(nextSort.field) +
2078 '&dir=' + encodeURIComponent(nextSort.dir)
2079 );
2080
2081 dbx.log('[grid] server sort click', {
2082 id: gridId,
2083 field: nextSort.field,
2084 dir: nextSort.dir,
2085 url: url
2086 });
2087
2088 table.setData(url);
2089 };
2090 }
2091
2092 colMap[field] = col;
2093
2094 if (groupName) {
2095 if (!groups[groupName]) groups[groupName] = [];
2096 groups[groupName].push(col);
2097 } else {
2098 ungrouped.push(col);
2099 }
2100 });
2101
2102 if (orderList && !hasGroups) {
2103
2104 const ordered = [];
2105 const used = {};
2106
2107 orderList.forEach(f => {
2108 if (colMap[f]) {
2109 ordered.push(colMap[f]);
2110 used[f] = true;
2111 }
2112 });
2113
2114 Object.keys(colMap).forEach(f => {
2115 if (!used[f]) {
2116 ordered.push(colMap[f]);
2117 }
2118 });
2119
2120 if (hasActions) {
2121 ordered.unshift(actionsCol);
2122 }
2123
2124 return ordered;
2125 }
2126
2127 if (hasGroups) {
2128
2129 let idx = 0;
2130
2131 if (hasActions) {
2132 cols.unshift(actionsCol);
2133 }
2134
2135 if (ungrouped.length) {
2136 ungrouped.forEach(col => cols.push(col));
2137 }
2138
2139 Object.keys(groups).forEach(groupName => {
2140
2141 if (idx > 0 || ungrouped.length > 0) {
2142 cols.push({
2143 title: '',
2144 field: `_sep_${idx}`,
2145 width: 6,
2146 minWidth: 6,
2147 maxWidth: 6,
2148 headerSort: false,
2149 headerFilter: false,
2150 resizable: false,
2151 cssClass: 'dbx-col-separator',
2152 formatter: () => '',
2153 print: false,
2154 download: false
2155 });
2156 }
2157
2158 cols.push({
2159 title: groupName,
2160 columns: groups[groupName]
2161 });
2162
2163 idx++;
2164 });
2165
2166 return cols;
2167 }
2168
2169 if (hasActions) {
2170 cols.push(actionsCol);
2171 }
2172
2173 return cols.concat(ungrouped);
2174 },
2175
2176
2177 /* =========================================================
2178 * SAVE BUTTON UI
2179 * ========================================================= */
2180 updateSaveButton(el, table) {
2181
2182 const btn = this._findSaveButton(el);
2183 if (!btn) return;
2184
2185 const isDirty = this._syncDirtyState(table);
2186
2187 if (btn._dbxDirtyState === isDirty) return;
2188
2189 btn._dbxDirtyState = isDirty;
2190
2191 if (isDirty) {
2192 btn.classList.remove('btn-outline-primary');
2193 btn.classList.add('btn-primary');
2194 } else {
2195 btn.classList.remove('btn-primary');
2196 btn.classList.add('btn-outline-primary');
2197 }
2198 },
2199
2200
2201 /* =========================================================
2202 * LAYOUT STATE
2203 * ========================================================= */
2204 bindLayoutState(el, table) {
2205
2206 let saveTimeout = null;
2207 let lastResizedField = null;
2208
2209 const gridId = el.id || 'grid';
2210 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2211
2212 const getLeafColumns = () => this._getLeafColumns(table);
2213
2214 function saveLayout(type) {
2215
2216 if (saveTimeout) clearTimeout(saveTimeout);
2217
2218 saveTimeout = setTimeout(() => {
2219
2220 if (type === 'order') {
2221 const cols = getLeafColumns();
2222 const order = cols.map(c => c.getField()).join('|');
2223 uiSet('COLUMNS.ORDER', order);
2224 return;
2225 }
2226
2227 if (type === 'width') {
2228 if (!lastResizedField) return;
2229
2230 const col = table.getColumn(lastResizedField);
2231 if (!col) return;
2232
2233 const w = col.getWidth();
2234
2235 if (typeof w === 'number' && w > 0) {
2236 uiSet('COLUMNS.SIZE.' + lastResizedField, String(w));
2237 }
2238 return;
2239 }
2240
2241 }, 300);
2242 }
2243
2244 table.on('columnResized', col => {
2245 const f = col.getField();
2246 if (!f || f.startsWith('_')) return;
2247
2248 lastResizedField = f;
2249 saveLayout('width');
2250 });
2251
2252 table.on('columnMoved', col => {
2253 const f = col.getField();
2254 if (!f || f.startsWith('_')) return;
2255 saveLayout('order');
2256 });
2257
2258 table.on('columnVisibilityChanged', (col, visible) => {
2259 const f = col.getField();
2260 if (!f || f.startsWith('_')) return;
2261 uiSet('COLUMNS.VISIBLE.' + f, visible ? '1' : '0');
2262 });
2263
2264 table.on('pageSizeChanged', (pageSize) => {
2265 if (table._dbxPageSizeChanging === true) {
2266 table._dbxPageSizeState = this._normalizePageSizeValue(
2267 pageSize,
2268 opt.pageSize || 15,
2269 opt.paginationSizeSelector
2270 );
2271 }
2272
2273 if (table._dbxIsRemotePagination === true) {
2274 try {
2275 table.setPage(1);
2276 } catch (e) {
2277 dbx.warn('[grid] setPage failed after pageSizeChanged', e);
2278 }
2279 table.replaceData();
2280 }
2281 });
2282 },
2283
2284
2285 /* =========================================================
2286 * TOOLBAR
2287 * ========================================================= */
2288 bindToolbar(el, table, opt, uiState, root) {
2289
2290 const gridId = el.id || 'grid';
2291
2292 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
2293 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2294
2295 el._dbxApplyGridLines = function(force) {
2296
2297 const on = uiGet('GRIDLINES', '1') == '1';
2298 const tabRoot = table.element;
2299 if (!tabRoot) return;
2300
2301 if (on) {
2302 tabRoot.classList.add('dbx-grid-lines');
2303 } else {
2304 tabRoot.classList.remove('dbx-grid-lines');
2305 }
2306 };
2307
2308 uiState.gridLines = uiGet('GRIDLINES', '1') == '1';
2309 uiState.autosave = uiGet('AUTOSAVE', '1') == '1';
2310
2311 const heightStored = uiGet('HEIGHT', null);
2312
2313 const autosave = root ? root.querySelector('[data-dbx="grid-autosave"]') : null;
2314 const gridLinesCb = root ? root.querySelector('[data-dbx="grid-lines"]') : null;
2315 const saveBtn = root ? root.querySelector('[data-dbx="grid-save"]') : null;
2316 const insertBtn = root ? root.querySelector('[data-dbx="grid-insert"]') : null;
2317 const reloadBtn = root ? root.querySelector('[data-dbx="grid-reload"]') : null;
2318 const resetBtn = root ? root.querySelector('[data-dbx="grid-reset"]') : null;
2319 const colBtn = root ? root.querySelector('[data-dbx="grid-columns"]') : null;
2320 const excelBtn = root ? root.querySelector('[data-dbx="grid-export-excel"]') : null;
2321 const pdfBtn = root ? root.querySelector('[data-dbx="grid-export-pdf"]') : null;
2322 const heightSlider = root ? root.querySelector('[data-dbx="grid-height"]') : null;
2323 const searchInput = root ? root.querySelector('[data-dbx="grid-search"]') : null;
2324
2325 this._setRoleVisible(root, 'search', opt.showSearch === true);
2326 this._setRoleVisible(root, 'autosave', opt.showAutosave === true && opt.allowEdit === true);
2327 this._setRoleVisible(root, 'gridlines', opt.showGridLines === true);
2328 this._setRoleVisible(root, 'height', opt.showHeight === true);
2329 this._setRoleVisible(root, 'reload', opt.showReload === true);
2330 this._setRoleVisible(root, 'reset', opt.showReset === true);
2331 this._setRoleVisible(root, 'save', opt.showSave === true && opt.allowEdit === true);
2332 this._setRoleVisible(root, 'insert', opt.showInsert === true && opt.allowInsert === true);
2333 this._setRoleVisible(root, 'columns', opt.showColumns === true);
2334 this._setRoleVisible(root, 'syncstatus', opt.showSyncStatus === true && opt.syncLed !== false);
2335 this._setRoleVisible(root, 'export-excel', opt.showExportExcel === true);
2336 this._setRoleVisible(root, 'export-pdf', opt.showExportPdf === true);
2337
2338 this._setRoleVisible(
2339 root,
2340 'pagination-bar',
2341 opt.pagination === true && (
2342 (opt.paginationControls === true) ||
2343 (opt.paginationCounter !== false)
2344 )
2345 );
2346
2347 this._setRoleVisible(
2348 root,
2349 'pagination-controls',
2350 opt.pagination === true && opt.paginationControls === true
2351 );
2352
2353 this._setRoleVisible(
2354 root,
2355 'pagination-counter',
2356 opt.pagination === true && opt.paginationCounter !== false
2357 );
2358
2359 if (searchInput) {
2360 searchInput.placeholder = opt.searchPlaceholder || '🔍';
2361 if (opt.searchWidth > 0) {
2362 searchInput.style.width = opt.searchWidth + 'px';
2363 }
2364 }
2365
2366 if (autosave) {
2367 autosave.checked = uiState.autosave;
2368 autosave.addEventListener('change', () => {
2369 uiSet('AUTOSAVE', autosave.checked ? '1' : '0');
2370 });
2371 }
2372
2373 if (gridLinesCb) {
2374 gridLinesCb.checked = uiState.gridLines;
2375 gridLinesCb.addEventListener('change', () => {
2376 uiSet('GRIDLINES', gridLinesCb.checked ? '1' : '0');
2377 el._dbxApplyGridLines(false);
2378 });
2379 }
2380
2381 if (heightSlider) {
2382
2383 const heightMin = Math.max(120, parseInt(opt.heightMin, 10) || 320);
2384 const heightMax = Math.max(heightMin, parseInt(opt.heightMax, 10) || 960);
2385 const heightStep = Math.max(10, parseInt(opt.heightStep, 10) || 40);
2386
2387 heightSlider.min = String(heightMin);
2388 heightSlider.max = String(heightMax);
2389 heightSlider.step = String(heightStep);
2390
2391 if (heightStored !== null) {
2392 heightSlider.value = heightStored;
2393 }
2394
2395 const sliderHeight = parseInt(heightSlider.value, 10);
2396 if (!isNaN(sliderHeight)) {
2397 heightSlider.value = String(Math.min(heightMax, Math.max(heightMin, sliderHeight)));
2398 }
2399
2400 heightSlider.addEventListener('input', () => {
2401 const h = parseInt(heightSlider.value, 10);
2402 if (isNaN(h)) return;
2403
2404 uiSet('HEIGHT', String(h));
2405 table.setHeight(h);
2406 });
2407 }
2408
2409 if (saveBtn) {
2410 saveBtn.addEventListener('click', () => {
2411 if (table._dbxDirty === true) {
2412 this.saveTable(table, opt);
2413 }
2414 });
2415 }
2416
2417 if (insertBtn) {
2418 insertBtn.addEventListener('click', () => {
2419 this.insertRow(table, opt);
2420 });
2421 }
2422
2423 if (reloadBtn) {
2424 reloadBtn.addEventListener('click', () => {
2425 if (table._dbxSaving === true) return;
2426 this.reloadTable(table, opt);
2427 });
2428 }
2429
2430 if (resetBtn) {
2431 resetBtn.addEventListener('click', () => {
2432
2433 const keys = [
2434 'GRIDLINES',
2435 'AUTOSAVE',
2436 'HEIGHT',
2437 'COLUMNS.ORDER',
2438 'PAGE.SIZE',
2439 'PAGE.NO'
2440 ];
2441
2442 keys.forEach(k => uiSet(k, null));
2443
2444 const cols = table.getColumns();
2445 const fields = [];
2446
2447 (function walk(cols){
2448 cols.forEach(c => {
2449 const f = c.getField && c.getField();
2450 if (f && !f.startsWith('_')) fields.push(f);
2451 if (c.getSubColumns) {
2452 const sub = c.getSubColumns();
2453 if (sub && sub.length) walk(sub);
2454 }
2455 });
2456 })(cols);
2457
2458 fields.forEach(f => {
2459 uiSet('COLUMNS.SIZE.' + f, null);
2460 uiSet('COLUMNS.VISIBLE.' + f, null);
2461 });
2462
2463 uiSet('GRIDLINES', '1');
2464 uiSet('AUTOSAVE', '1');
2465
2466 if (window.dbx && dbx.utilities && dbx.utilities.leaveGuard) {
2467 dbx.utilities.leaveGuard.allowOnce();
2468 }
2469 location.reload();
2470 });
2471 }
2472
2473 if (colBtn) {
2474 colBtn.addEventListener('click', () => {
2475 this.openColumnChooser(colBtn, table);
2476 });
2477 }
2478
2479 if (excelBtn) {
2480 excelBtn.addEventListener('click', () => {
2481 this._ensureExcelExportDeps(table, (ok) => {
2482 if (ok !== true) {
2483 dbx.error('[grid] excel export dependencies missing');
2484 return;
2485 }
2486
2487 try {
2488 table.download('xlsx', (opt.exportFileName || gridId) + '.xlsx', {
2489 sheetName: opt.exportSheetName || gridId
2490 });
2491 } catch (e) {
2492 dbx.error('[grid] excel export failed', e);
2493 }
2494 });
2495 });
2496 }
2497
2498 if (pdfBtn) {
2499 pdfBtn.addEventListener('click', () => {
2500 this._ensurePdfExportDeps(table, (ok) => {
2501 if (ok !== true) {
2502 dbx.error('[grid] pdf export dependencies missing');
2503 return;
2504 }
2505
2506 try {
2507 table.download('pdf', (opt.exportFileName || gridId) + '.pdf', {
2508 orientation: opt.pdfOrientation || 'landscape',
2509 title: opt.pdfTitle || document.title || 'Export'
2510 });
2511 } catch (e) {
2512 dbx.error('[grid] pdf export failed', e);
2513 }
2514 });
2515 });
2516 }
2517
2518 if (searchInput) {
2519
2520 if (!table._dbxGlobalSearchFilter) {
2521 table._dbxGlobalSearchFilter = function (data, filterParams) {
2522 const val = String((filterParams && filterParams.value) || '').toLowerCase();
2523 if (!val) {
2524 return true;
2525 }
2526 const fields = (filterParams && filterParams.fields) || [];
2527 for (let i = 0; i < fields.length; i++) {
2528 const v = data[fields[i]];
2529 if (v != null && String(v).toLowerCase().indexOf(val) !== -1) {
2530 return true;
2531 }
2532 }
2533 return false;
2534 };
2535 }
2536
2537 const getFields = () => {
2538 const out = [];
2539 (function walk(cols){
2540 cols.forEach(c => {
2541 const f = c.getField && c.getField();
2542 if (f && !f.startsWith('_')) out.push(f);
2543 if (c.getSubColumns) {
2544 const sub = c.getSubColumns();
2545 if (sub && sub.length) walk(sub);
2546 }
2547 });
2548 })(table.getColumns());
2549 return out;
2550 };
2551
2552 let timer = null;
2553
2554 const applyLocalSearch = () => {
2555 const val = searchInput.value.trim();
2556
2557 if (opt.searchMode === 'remote') {
2558 table._dbxSearchValue = val.toLowerCase();
2559 this.reloadTable(table, opt, { resetPage: true });
2560 return;
2561 }
2562
2563 if (!val) {
2564 table.clearFilter();
2565 return;
2566 }
2567
2568 table.setFilter(table._dbxGlobalSearchFilter, {
2569 value: val,
2570 fields: getFields()
2571 });
2572 };
2573
2574 searchInput.addEventListener('input', () => {
2575
2576 if (timer) clearTimeout(timer);
2577
2578 if (opt.searchMode === 'remote') {
2579 timer = setTimeout(applyLocalSearch, 250);
2580 return;
2581 }
2582
2583 applyLocalSearch();
2584 });
2585
2586 table.on('dataLoaded', () => {
2587 if (opt.searchMode === 'remote') {
2588 return;
2589 }
2590 if (!searchInput.value.trim()) {
2591 return;
2592 }
2593 applyLocalSearch();
2594 });
2595 }
2596
2597 table.on('tableBuilt', () => {
2598 table._dbxBuilt = true;
2599 if (table._dbxPageSizeState && table.getPageSize && table.getPageSize() !== table._dbxPageSizeState) {
2600 try {
2601 table._dbxPageSizeChanging = true;
2602 table.setPageSize(table._dbxPageSizeState);
2603 } catch (e) {
2604 dbx.warn('[grid] restore page size failed', e);
2605 } finally {
2606 table._dbxPageSizeChanging = false;
2607 }
2608 }
2609 el._dbxApplyGridLines(false);
2610 this._applySortIndicators(table);
2611 });
2612 },
2613
2614 /* =========================================================
2615 * COLUMN CHOOSER
2616 * ========================================================= */
2617 openColumnChooser(btn, table) {
2618
2619 const el = table.element;
2620 const gridId = el.id || 'grid';
2621
2622 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
2623 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2624
2625 const old = document.querySelector(`.dbx-col-chooser[data-grid-id="${gridId}"]`);
2626 if (old) old.remove();
2627
2628 const box = document.createElement('div');
2629 box.className = 'dbx-col-chooser shadow p-2 bg-white border rounded';
2630 box.dataset.gridId = gridId;
2631
2632 box.style.position = 'fixed';
2633 box.style.zIndex = 9999;
2634 box.style.minWidth = '260px';
2635 box.style.maxHeight = '70vh';
2636 box.style.overflowY = 'auto';
2637
2638 const rect = btn.getBoundingClientRect();
2639 box.style.left = rect.left + 'px';
2640 box.style.top = (rect.bottom + 4) + 'px';
2641
2642 const groupMap = {};
2643 const allCols = [];
2644
2645 this._getLeafColumns(table).forEach(col => {
2646
2647 const field = col.getField();
2648 if (!field || field.startsWith('_')) return;
2649
2650 allCols.push(col);
2651
2652 let groupTitle = '-';
2653 const parent = col.getParentColumn();
2654 if (parent) {
2655 const def = parent.getDefinition();
2656 if (def?.title?.trim()) groupTitle = def.title.trim();
2657 }
2658
2659 if (!groupMap[groupTitle]) groupMap[groupTitle] = [];
2660 groupMap[groupTitle].push(col);
2661 });
2662
2663 const queueWidthRestore = () => {
2664 this._queueTableTimer(table, '_dbxChooserTimer', () => {
2665 if (!this._isTableLayoutReady(table)) return;
2666 this._restoreStoredColumnWidths(table, gridId);
2667 }, 0);
2668 };
2669
2670 const saveVisibility = () => {
2671 allCols.forEach(c => {
2672 const field = c.getField();
2673 uiSet('COLUMNS.VISIBLE.' + field, c.isVisible() ? '1' : '0');
2674 });
2675 };
2676
2677 Object.keys(groupMap).forEach(groupTitle => {
2678
2679 const cols = groupMap[groupTitle];
2680
2681 const groupLabel = document.createElement('label');
2682 groupLabel.className = 'fw-bold d-flex align-items-center gap-2 mb-1';
2683
2684 const groupCb = document.createElement('input');
2685 groupCb.type = 'checkbox';
2686
2687 const updateGroupState = () => {
2688 const visibleCount = cols.filter(c => c.isVisible()).length;
2689 groupCb.checked = (visibleCount === cols.length);
2690 groupCb.indeterminate = (visibleCount > 0 && visibleCount < cols.length);
2691 };
2692
2693 updateGroupState();
2694
2695 groupCb.addEventListener('change', () => {
2696
2697 if (!this._isTableLayoutReady(table)) return;
2698
2699 table.blockRedraw();
2700
2701 cols.forEach(c => {
2702 const f = c.getField();
2703 if (!f || f.startsWith('_')) return;
2704
2705 groupCb.checked
2706 ? table.showColumn(f)
2707 : table.hideColumn(f);
2708 });
2709
2710 table.restoreRedraw(true);
2711
2712 saveVisibility();
2713 updateGroupState();
2714 queueWidthRestore();
2715 });
2716
2717 groupLabel.appendChild(groupCb);
2718 groupLabel.appendChild(document.createTextNode(groupTitle));
2719 box.appendChild(groupLabel);
2720
2721 cols.forEach(col => {
2722
2723 const field = col.getField();
2724 const def = col.getDefinition();
2725 const labelText = def?.title ? def.title : field;
2726
2727 const label = document.createElement('label');
2728 label.className = 'd-flex align-items-center gap-2 small ms-3';
2729
2730 const cb = document.createElement('input');
2731 cb.type = 'checkbox';
2732 cb.checked = col.isVisible();
2733
2734 cb.addEventListener('change', () => {
2735
2736 if (!this._isTableLayoutReady(table)) return;
2737
2738 table.blockRedraw();
2739
2740 cb.checked
2741 ? table.showColumn(field)
2742 : table.hideColumn(field);
2743
2744 table.restoreRedraw(true);
2745
2746 saveVisibility();
2747 updateGroupState();
2748 queueWidthRestore();
2749 });
2750
2751 label.appendChild(cb);
2752 label.appendChild(document.createTextNode(labelText));
2753 box.appendChild(label);
2754 });
2755
2756 box.appendChild(document.createElement('hr'));
2757 });
2758
2759 document.body.appendChild(box);
2760
2761 setTimeout(() => {
2762 const close = (e) => {
2763 if (!box.contains(e.target) && e.target !== btn) {
2764 box.remove();
2765 document.removeEventListener('click', close);
2766 }
2767 };
2768 document.addEventListener('click', close);
2769 }, 0);
2770 },
2771
2772
2773 /* =========================================================
2774 * PARAMS / REMOTE STATE
2775 * ========================================================= */
2776 buildAjaxParams(table, params, optFallback) {
2777
2778 const out = Object.assign({}, params || {});
2779 const gridEl = table && table.element ? table.element : null;
2780 const opt = (gridEl && gridEl._dbxOpt) ? gridEl._dbxOpt : (optFallback || {});
2781 const gridId = (gridEl && gridEl.id) ? gridEl.id : (opt._gridId || 'grid');
2782 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2783
2784 delete out.sorters;
2785 delete out.filter;
2786 delete out.filters;
2787
2788 if (table && table._dbxSearchValue) {
2789 out.dbx_search = table._dbxSearchValue;
2790 }
2791
2792 const isRemote = table && table._dbxIsRemotePagination === true;
2793
2794 if (isRemote) {
2795 const page = parseInt(out.page, 10) || (table.getPage ? table.getPage() : 1) || 1;
2796 const rawSize = out.size ?? (table.getPageSize ? table.getPageSize() : null) ?? opt.pageSize ?? 50;
2797 const normalizedSize = this._normalizePageSizeValue(rawSize, opt.pageSize || 15, opt.paginationSizeSelector);
2798 const size = normalizedSize;
2799
2800 out.page = page;
2801 out.size = size;
2802
2803 uiSet('PAGE.NO', page);
2804 this._storePageSizeState(gridId, normalizedSize, opt.pageSize || 15, opt.paginationSizeSelector);
2805 }
2806
2807 if (!table || typeof table.getHeaderFilters !== 'function') {
2808 return out;
2809 }
2810
2811 const sorters = this._getAjaxSorters(table);
2812 if (Array.isArray(sorters) && sorters.length) {
2813 out.dbx_sorters = JSON.stringify(sorters);
2814 }
2815
2816 const headerFilters = table.getHeaderFilters ? table.getHeaderFilters() : [];
2817 if (Array.isArray(headerFilters) && headerFilters.length) {
2818 out.dbx_filters = JSON.stringify(headerFilters);
2819 }
2820
2821 return out;
2822 },
2823
2824
2825 /* =========================================================
2826 * RELOAD
2827 * ========================================================= */
2828 reloadTable(table, opt, cfg = {}) {
2829
2830 const resetPage = !!cfg.resetPage;
2831
2832 if (table._dbxSaving === true) return;
2833
2834 if (table._dbxIsRemotePagination) {
2835 if (resetPage) {
2836 try {
2837 table.setPage(1);
2838 } catch (e) {
2839 dbx.warn('[grid] remote reset page failed', e);
2840 }
2841 }
2842 table.replaceData();
2843 return;
2844 }
2845
2846 if (table._dbxIsProgressive) {
2847 table.setData(this._dbxAjaxUrl(opt.urls.read));
2848 return;
2849 }
2850
2851 table.replaceData(this._dbxAjaxUrl(opt.urls.read));
2852 },
2853
2854 insertRow(table, opt) {
2855
2856 if (!table || !opt || !opt.urls || !opt.urls.insert) return;
2857 if (table._dbxSaving === true) return;
2858
2859 const url = this._dbxAjaxUrl(opt.urls.insert);
2860 table._dbxSaving = true;
2861
2862 this._dbxRequest(url, {
2863 method: 'POST',
2864 headers: {
2865 'Content-Type': 'application/json'
2866 },
2867 body: JSON.stringify({}),
2868 responseType: 'json'
2869 })
2870 .then(resp => {
2871 table._dbxSaving = false;
2872
2873 if (!resp || !(resp.ok || resp.success)) {
2874 dbx.error('[grid] insert failed', resp);
2875 return;
2876 }
2877
2878 const row = resp.row || (Array.isArray(resp.rows) ? resp.rows[0] : null);
2879 if (row) {
2880 table.addData([row], false);
2881 } else {
2882 this.reloadTable(table, opt);
2883 }
2884 })
2885 .catch(err => {
2886 table._dbxSaving = false;
2887 dbx.error('[grid] insert error', err);
2888 });
2889 },
2890
2891
2892 /* =========================================================
2893 * SYNC
2894 * ========================================================= */
2895 bindSyncLoop(el, table, opt) {
2896
2897 const syncUrl = opt.urls.sync;
2898 if (!syncUrl) return;
2899
2900 const syncEls = this._getSyncEls(el);
2901
2902 if (syncEls.led) {
2903 syncEls.led._dbxSyncLedEnabled = (opt.syncLed !== false);
2904
2905 if (opt.syncLed === false) {
2906 syncEls.led.style.display = 'none';
2907 } else {
2908 syncEls.led.style.display = 'inline-block';
2909 }
2910 }
2911
2912 if (opt.syncRun === false) {
2913 dbx.log('[grid][sync] disabled by sync_run=0', {
2914 id: el.id || 'grid'
2915 });
2916 return;
2917 }
2918
2919 let synctime = parseFloat(opt.cfg.synctime || 2);
2920 if (isNaN(synctime)) synctime = 2;
2921
2922 if (synctime === 0) return;
2923 if (synctime < 0.5) synctime = 0.5;
2924 if (synctime > 60) synctime = 60;
2925
2926 const interval = Math.round(synctime * 1000);
2927
2928 const loopId = 'grid-sync-' + (el.id || 'grid') + '-' + Date.now();
2929 table._dbxLoopId = loopId;
2930 table._dbxSyncRunning = false;
2931 table._dbxSyncMode = opt.syncMode || 'delta';
2932
2933 dbx.log('[grid][sync] bind', {
2934 id: el.id || 'grid',
2935 synctime: synctime,
2936 interval: interval,
2937 mode: table._dbxSyncMode,
2938 remotePagination: table._dbxIsRemotePagination === true
2939 });
2940
2941 dbx.loop.add({
2942 id: loopId,
2943 timing: {
2944 base: interval,
2945 idle: Math.max(interval * 2, interval + 1000),
2946 hidden: Math.max(interval * 3, interval + 2000),
2947 min: 500,
2948 max: 60000
2949 },
2950 onRun: () => {
2951
2952 if (table._dbxSyncRunning === true) return;
2953 if (table._dbxSaving === true) return;
2954 if (!table._dbxServerTime) return;
2955 if (!dbx.device.isVisible()) return;
2956 if (!this._isTableAlive(table)) return;
2957
2958 table._dbxSyncRunning = true;
2959
2960 const startedAt = Date.now();
2961
2962 dbx.log('[grid][sync] request start', {
2963 id: el.id || 'grid',
2964 last_update: table._dbxServerTime,
2965 remotePagination: table._dbxIsRemotePagination === true,
2966 page: table._dbxIsRemotePagination ? (table.getPage() || 1) : null,
2967 size: table._dbxIsRemotePagination ? (table.getPageSize() || opt.pageSize || 50) : null
2968 });
2969
2970 let loadingTimer = setTimeout(() => {
2971 if (table._dbxSyncRunning === true) {
2972 dbx.log('[grid][sync] loader threshold reached', {
2973 id: el.id || 'grid'
2974 });
2975 this._setLedState(syncEls.led, 'loading');
2976 }
2977 }, 120);
2978
2979 const editedMap = this._collectEditedMap(table);
2980
2981 let url = this._dbxAjaxUrl(syncUrl, { background: true }) +
2982 '&last_update=' + encodeURIComponent(table._dbxServerTime);
2983
2984 if (table._dbxIsRemotePagination) {
2985 url += '&dbx_page=' + encodeURIComponent(table.getPage() || 1);
2986 url += '&dbx_size=' + encodeURIComponent(table.getPageSize() || opt.pageSize || 50);
2987 }
2988
2989 return this._dbxRequest(url, {
2990 method: 'GET',
2991 responseType: 'json'
2992 })
2993 .then(res => {
2994
2995 const rows = Array.isArray(res?.rows) ? res.rows : [];
2996
2997 dbx.log('[grid][sync] response', {
2998 id: el.id || 'grid',
2999 ok: res?.ok,
3000 rows: rows.length,
3001 count: (typeof res?.count !== 'undefined') ? res.count : null
3002 });
3003
3004 if (!res || res.ok !== 1) {
3005 this._setLedState(syncEls.led, 'idle');
3006 return;
3007 }
3008
3009 if (typeof res.server_time !== 'undefined' && res.server_time) {
3010 table._dbxServerTime = res.server_time;
3011 }
3012
3013 if (typeof res.count !== 'undefined') {
3014 this._setSyncCount(syncEls.count, res.count || '');
3015 }
3016
3017 if (!rows.length) {
3018 this._setLedState(syncEls.led, 'idle');
3019 return;
3020 }
3021
3022 if (table._dbxIsRemotePagination) {
3023
3024 dbx.log('[grid][sync] remote reload triggered', {
3025 id: el.id || 'grid',
3026 rows: rows.length
3027 });
3028
3029 this.reloadTable(table, opt, {
3030 reason: 'sync-remote-delta'
3031 });
3032
3033 this._setLedState(syncEls.led, 'ok');
3034 return;
3035 }
3036
3037 let changed = 0;
3038
3039 for (let i = 0; i < rows.length; i++) {
3040
3041 const r = rows[i];
3042 if (!r || typeof r.id === 'undefined') continue;
3043
3044 const row = table.getRow(r.id);
3045
3046 if (!row) {
3047 table.addData([r], false);
3048 changed++;
3049 continue;
3050 }
3051
3052 const data = row.getData();
3053 const editedFields = editedMap[r.id] || null;
3054 const patch = {};
3055
3056 for (const k in r) {
3057
3058 if (k === 'id') continue;
3059
3060 const newVal = r[k];
3061 const oldVal = data[k];
3062
3063 if (
3064 newVal === oldVal ||
3065 String(newVal) === String(oldVal)
3066 ) {
3067 continue;
3068 }
3069
3070 if (editedFields && editedFields[k] === true) {
3071
3072 const cell = row.getCell(k);
3073 if (cell && newVal !== oldVal) {
3074 const cellEl = cell.getElement();
3075 if (cellEl) {
3076 cellEl.classList.add('dbx-cell-conflict');
3077 }
3078 }
3079
3080 continue;
3081 }
3082
3083 patch[k] = newVal;
3084 }
3085
3086 const keys = Object.keys(patch);
3087 if (!keys.length) continue;
3088
3089 row.update(patch);
3090 changed++;
3091
3092 keys.forEach(k => {
3093 const cell = row.getCell(k);
3094 if (cell) {
3095 this._applySchemaCellStyle(cell, row.getData());
3096 }
3097 });
3098 }
3099
3100 dbx.log('[grid][sync] local apply done', {
3101 id: el.id || 'grid',
3102 incoming: rows.length,
3103 changed: changed
3104 });
3105
3106 this._setLedState(syncEls.led, changed > 0 ? 'ok' : 'idle');
3107 })
3108 .catch(err => {
3109 dbx.error('[grid][sync] error', err);
3110 this._setLedState(syncEls.led, 'error');
3111 })
3112 .finally(() => {
3113 clearTimeout(loadingTimer);
3114 loadingTimer = null;
3115 table._dbxSyncRunning = false;
3116
3117 dbx.log('[grid][sync] request end', {
3118 id: el.id || 'grid',
3119 duration_ms: (Date.now() - startedAt)
3120 });
3121 });
3122 }
3123 });
3124 },
3125
3126
3127 /* =========================================================
3128 * CREATE TABLE
3129 * ========================================================= */
3130 createTable(el, opt) {
3131
3132 if (el._dbxGridInitialized) return;
3133 el._dbxGridInitialized = true;
3134
3135 const gridId = el.id || 'grid';
3136 opt._gridId = gridId;
3137
3138 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
3139 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
3140
3141 const schemaName =
3142 opt.cfg && typeof opt.cfg.schema === 'string'
3143 ? opt.cfg.schema.trim()
3144 : '';
3145
3146 const buildGrid = () => {
3147
3148 const isRemotePagination =
3149 opt.pagination === true &&
3150 (opt.paginationMode === 'remote');
3151
3152 const isProgressive =
3153 (opt.progressiveLoad === 'scroll' || opt.progressiveLoad === 'load');
3154
3155 opt._dbxIsRemotePagination = isRemotePagination;
3156 opt._dbxIsProgressive = isProgressive;
3157
3158 const columns = this.buildColumns(opt);
3159
3160 const pageSizeStored = this._getPageSizeState(
3161 gridId,
3162 opt.pageSize || 15,
3163 opt.paginationSizeSelector
3164 );
3165 const pageSizeInitial = pageSizeStored === 1 ? 15 : pageSizeStored;
3166 opt.pageSize = pageSizeInitial;
3167 const pageNoStored = this._int(uiGet('PAGE.NO', 1), 1);
3168
3169 const heightStoredRaw = opt.height === false ? null : uiGet('HEIGHT', null);
3170 const heightStoredInt = heightStoredRaw !== null ? parseInt(heightStoredRaw, 10) : NaN;
3171 const initialHeightRaw = opt.height === false ? false : (!isNaN(heightStoredInt) ? heightStoredInt : opt.height);
3172 const heightMinBound = Math.max(120, parseInt(opt.heightMin, 10) || 320);
3173 const heightMaxBound = Math.max(heightMinBound, parseInt(opt.heightMax, 10) || 960);
3174 const initialHeight = initialHeightRaw === false
3175 ? false
3176 : Math.min(heightMaxBound, Math.max(heightMinBound, initialHeightRaw));
3177
3178 const paginationUi = this._getPaginationUiEls(el);
3179
3180 dbx.log('[grid] createTable', {
3181 id: gridId,
3182 remotePagination: isRemotePagination,
3183 progressive: isProgressive,
3184 pageSizeStored: pageSizeStored,
3185 pageNoStored: pageNoStored,
3186 initialHeight: initialHeight,
3187 searchMode: opt.searchMode,
3188 syncRun: opt.syncRun,
3189 syncLed: opt.syncLed,
3190 dedicatedServerSort: !!(opt.headerSort === true && opt.urls.sort && !isRemotePagination),
3191 paginationCounter: opt.paginationCounter,
3192 paginationSizeSelector: opt.paginationSizeSelector
3193 });
3194
3195 let table = null;
3196
3197 const ajaxURLGenerator = (url, config, params) => {
3198
3199 let finalUrl = this._dbxAjaxUrl(url);
3200 const merged = this.buildAjaxParams(table, params || {}, opt);
3201
3202 const usp = new URLSearchParams();
3203
3204 Object.keys(merged).forEach(key => {
3205 const val = merged[key];
3206 if (val === undefined || val === null || val === '') return;
3207 usp.append(key, val);
3208 });
3209
3210 if (String(finalUrl).includes('?')) {
3211 finalUrl += '&' + usp.toString();
3212 } else {
3213 finalUrl += '?' + usp.toString();
3214 }
3215
3216 dbx.log('[grid][ajaxURL]', {
3217 id: gridId,
3218 url: finalUrl,
3219 params: merged
3220 });
3221
3222 return finalUrl;
3223 };
3224
3225 const ajaxRequestFunc = (url, config, params) => {
3226
3227 const method =
3228 (typeof config === 'string')
3229 ? config
3230 : ((config && config.method) ? config.method : 'GET');
3231
3232 dbx.log('[grid][ajaxRequestFunc]', {
3233 id: gridId,
3234 method: method,
3235 url: url,
3236 params: params || {}
3237 });
3238
3239 return this._dbxRequest(url, {
3240 method: method,
3241 responseType: 'json'
3242 });
3243 };
3244
3245 const tabulatorOptions = {
3246
3247 height: initialHeight,
3248 minHeight: opt.minHeight || false,
3249 maxHeight: opt.maxHeight || false,
3250 layout: (opt.cfg && opt.cfg.layout) ? opt.cfg.layout : 'fitColumns',
3251 responsiveLayout: opt.responsiveLayout || false,
3252 placeholder: String(opt.cfg.placeholder ?? ''),
3253
3254 dataLoader: false,
3255
3256 sortMode: isRemotePagination ? 'remote' : 'local',
3257
3258 filterMode: opt.searchMode === 'remote' ? 'remote' : 'local',
3259
3260 ajaxURL: this._dbxAjaxUrl(opt.urls.read),
3261 ajaxConfig: 'GET',
3262 ajaxContentType: 'json',
3263 ajaxURLGenerator: ajaxURLGenerator,
3264 ajaxRequestFunc: ajaxRequestFunc,
3265 ajaxResponse: (url, params, response) => this._ajaxResponse(table, url, params, response),
3266
3267 pagination: opt.pagination === true,
3268 paginationMode: isRemotePagination ? 'remote' : 'local',
3269 paginationSize: pageSizeInitial,
3270 paginationInitialPage: pageNoStored,
3271 paginationAddRow: opt.paginationAddRow || 'page',
3272 paginationButtonCount: opt.paginationButtonCount || 5,
3273 progressiveLoad: isProgressive ? opt.progressiveLoad : false,
3274
3275 index: String(opt.cfg.index || 'id'),
3276 columns: columns,
3277
3278 reactiveData: false,
3279 movableColumns: opt.movableColumns !== false,
3280 resizableColumns: opt.resizableColumns !== false,
3281
3282 rowFormatter: function(row) {
3283
3284 const rowEl = row.getElement();
3285 if (!rowEl) return;
3286
3287 const schema = row.getTable().element._dbxSchemaParsed;
3288 if (!schema || !Array.isArray(schema.rows)) return;
3289
3290 const data = row.getData();
3291
3292 rowEl.style.removeProperty('background-color');
3293 rowEl.style.removeProperty('color');
3294
3295 for (let i = 0; i < schema.rows.length; i++) {
3296 const rule = schema.rows[i];
3297 if (!dbxGrid.evalRule(rule, null, data)) continue;
3298
3299 if (rule.style?.bg) {
3300 rowEl.style.setProperty('background-color', rule.style.bg, 'important');
3301 }
3302 if (rule.style?.color) {
3303 rowEl.style.setProperty('color', rule.style.color, 'important');
3304 }
3305 break;
3306 }
3307 }
3308 };
3309
3310 if (opt.pagination === true && opt.paginationControls === true && paginationUi.controls) {
3311 tabulatorOptions.paginationElement = paginationUi.controls;
3312 }
3313
3314 if (opt.pagination === true && opt.paginationCounter !== false) {
3315 tabulatorOptions.paginationCounter = opt.paginationCounter;
3316
3317 if (paginationUi.counter) {
3318 tabulatorOptions.paginationCounterElement = paginationUi.counter;
3319 }
3320 }
3321
3322 if (opt.pagination === true && opt.paginationSizeSelector !== false) {
3323 tabulatorOptions.paginationSizeSelector = opt.paginationSizeSelector;
3324 }
3325
3326 if (opt.pagination === true && opt.paginationOutOfRange !== false) {
3327 tabulatorOptions.paginationOutOfRange = opt.paginationOutOfRange;
3328 }
3329
3330 table = new Tabulator(el, tabulatorOptions);
3331
3332 table._dbxIsRemotePagination = isRemotePagination;
3333 table._dbxIsProgressive = isProgressive;
3334 table._dbxSortRestored = false;
3335 table._dbxDirty = false;
3336 table._dbxSaving = false;
3337 table._dbxAutoTimer = null;
3338 table._dbxSearchValue = '';
3339 table._dbxLayoutRestored = false;
3340 table._dbxServerSort = null;
3341 table._dbxPageLayoutTimer = null;
3342 table._dbxBuilt = false;
3343 table._dbxPageSizeState = pageSizeStored;
3344
3345 const syncEls = this._getSyncEls(el);
3346
3347 if (syncEls.led) {
3348 syncEls.led._dbxSyncLedEnabled = (opt.syncLed !== false);
3349
3350 if (opt.syncLed === false) {
3351 syncEls.led.style.display = 'none';
3352 } else {
3353 syncEls.led.style.display = 'inline-block';
3354 }
3355 }
3356
3357 const queueLocalPageStabilize = (reason) => {
3358
3359 if (table._dbxIsRemotePagination === true) return;
3360 if (table._dbxLayoutRestored !== true) return;
3361
3362 this._queueTableTimer(table, '_dbxPageLayoutTimer', () => {
3363
3364 if (!this._isTableLayoutReady(table) || table._dbxBuilt !== true) {
3365 this._queueTableTimer(table, '_dbxPageLayoutTimer', () => {
3366 queueLocalPageStabilize(reason);
3367 }, 30);
3368 return;
3369 }
3370
3371 dbx.log('[grid] local page stabilize start', {
3372 id: gridId,
3373 reason: reason,
3374 page: table.getPage ? table.getPage() : null
3375 });
3376
3377 try {
3378 table.redraw(true);
3379 } catch (e) {
3380 dbx.warn('[grid] local page redraw failed', e);
3381 }
3382
3383 this._applySortIndicators(table);
3384
3385 dbx.log('[grid] local page stabilize done', {
3386 id: gridId,
3387 reason: reason,
3388 page: table.getPage ? table.getPage() : null
3389 });
3390
3391 }, 0);
3392 };
3393
3394 table.on('pageLoaded', (pageno) => {
3395 uiSet('PAGE.NO', pageno);
3396 this._storePageSizeState(
3397 gridId,
3398 table._dbxPageSizeState || (table.getPageSize ? table.getPageSize() : opt.pageSize),
3399 opt.pageSize,
3400 opt.paginationSizeSelector
3401 );
3402
3403 dbx.log('[grid] pageLoaded', {
3404 id: gridId,
3405 page: pageno,
3406 pageSize: table.getPageSize()
3407 });
3408
3409 this._applyPaginationButtonLabels(table);
3410
3411 if (table._dbxIsRemotePagination !== true) {
3412 queueLocalPageStabilize('pageLoaded');
3413 }
3414 });
3415
3416 table.on('cellEdited', (cell) => {
3417
3418 if (opt.allowEdit === false) return;
3419
3420 this._markTableDirty(table, el);
3421
3422 dbx.log('[grid] cellEdited', {
3423 id: gridId,
3424 rowId: cell?.getRow?.()?.getData?.()?.id,
3425 field: cell?.getField?.()
3426 });
3427
3428 const autosave = uiGet('AUTOSAVE', '1') == '1';
3429 if (!autosave) return;
3430
3431 if (table._dbxAutoTimer) {
3432 clearTimeout(table._dbxAutoTimer);
3433 }
3434
3435 table._dbxAutoTimer = setTimeout(() => {
3436
3437 if (table._dbxSaving === true) return;
3438 if (this._syncDirtyState(table) !== true) return;
3439
3440 dbx.log('[grid] autosave trigger', {
3441 id: gridId
3442 });
3443
3444 this.saveTable(table, opt);
3445
3446 }, 300);
3447 });
3448
3449 table.on('renderComplete', () => {
3450 if (opt.allowEdit !== false) {
3451 this.updateSaveButton(el, table);
3452 }
3453 });
3454
3455 table.on('dataLoaded', (data) => {
3456
3457 const syncEls = this._getSyncEls(el);
3458
3459 if (typeof table._dbxSyncCount !== 'undefined') {
3460 this._setSyncCount(syncEls.count, table._dbxSyncCount || '');
3461 }
3462
3463 if (table._dbxPageSizeState === 1 && table._dbxPageSizeOneApplied !== true) {
3464 table._dbxPageSizeOneApplied = true;
3465
3466 try {
3467 table._dbxPageSizeChanging = true;
3468 table.setPageSize(1);
3469 if (typeof table.setPage === 'function') {
3470 table.setPage(1);
3471 }
3472 table.redraw(true);
3473 } catch (e) {
3474 dbx.warn('[grid] restore page size 1 failed', e);
3475 } finally {
3476 table._dbxPageSizeChanging = false;
3477 }
3478 }
3479
3480 this._applySortIndicators(table);
3481 this._applyPaginationButtonLabels(table);
3482
3483 dbx.log('[grid] dataLoaded', {
3484 id: gridId,
3485 rows: Array.isArray(data) ? data.length : null,
3486 sortRestored: table._dbxSortRestored === true,
3487 remotePagination: table._dbxIsRemotePagination === true,
3488 progressive: table._dbxIsProgressive === true
3489 });
3490
3491 if (table._dbxIsRemotePagination !== true) {
3492 queueLocalPageStabilize('dataLoaded');
3493 }
3494
3495 if (!table._dbxSortRestored) {
3496 table._dbxSortRestored = true;
3497 this.bindSyncLoop(el, table, opt);
3498 }
3499 });
3500
3501 table.on('dataSorted', () => {
3502 this._applySortIndicators(table);
3503 });
3504
3505 table.on('renderComplete', () => {
3506 this._applySortIndicators(table);
3507 this._applyPaginationButtonLabels(table);
3508 });
3509
3510 table.on('columnsLoaded', () => {
3511
3512 if (table._dbxLayoutRestored === true) {
3513 dbx.log('[grid] columnsLoaded skipped (already restored)', {
3514 id: gridId
3515 });
3516 return;
3517 }
3518
3519 dbx.log('[grid] columnsLoaded -> restore layout start', {
3520 id: gridId
3521 });
3522
3523 const tryRestore = () => {
3524
3525 if (table._dbxLayoutRestored === true) return;
3526
3527 const applied = this._applyInitialLayoutState(table, gridId);
3528
3529 if (applied !== true) {
3530 this._queueTableTimer(table, '_dbxLayoutTimer', tryRestore, 30);
3531 return;
3532 }
3533
3534 table._dbxLayoutRestored = true;
3535 this._applySortIndicators(table);
3536
3537 dbx.log('[grid] columnsLoaded -> restore layout done', {
3538 id: gridId
3539 });
3540 };
3541
3542 this._queueTableTimer(table, '_dbxLayoutTimer', tryRestore, 0);
3543 });
3544
3545 this.bindLayoutState(el, table);
3546
3547 el._dbxTable = table;
3548 el._dbxFeature = this;
3549 el._dbxOpt = opt;
3550
3551 this.bindToolbar(
3552 el,
3553 table,
3554 opt,
3555 opt._uiState || (opt._uiState = {}),
3556 el.closest('.dbx-grid')
3557 );
3558
3559 this.updateSaveButton(el, table);
3560 };
3561
3562 if (schemaName) {
3563 this.loadSchema(schemaName, () => {
3564 el._dbxSchemaParsed = dbxGridParseSchema(window.dbxGridSchema[schemaName]);
3565 buildGrid();
3566 });
3567 } else {
3568 buildGrid();
3569 }
3570 },
3571
3572 /* =========================================================
3573 * SAVE
3574 * ========================================================= */
3575 saveTable(table, opt) {
3576
3577 if (!opt || !opt.urls || !opt.urls.save) {
3578 table._dbxSaving = false;
3579 dbx.error('[grid] saveTable → missing save URL', {
3580 id: table.element?.id || 'undef'
3581 });
3582 return;
3583 }
3584
3585 if (table._dbxSaving === true) {
3586 return;
3587 }
3588
3589 if (table._dbxDirty !== true && this._syncDirtyState(table) !== true) {
3590 table._dbxSaving = false;
3591 return;
3592 }
3593
3594 const editedCells = table.getEditedCells();
3595
3596 if (!editedCells || editedCells.length === 0) {
3597
3598 table._dbxSaving = false;
3599 table._dbxDirty = false;
3600
3601 if (table.element && table.element._dbxFeature) {
3602 table.element._dbxFeature.updateSaveButton(table.element, table);
3603 }
3604
3605 return;
3606 }
3607
3608 const rowsMap = {};
3609
3610 editedCells.forEach(cell => {
3611
3612 const row = cell.getRow();
3613 if (!row) return;
3614
3615 const data = row.getData();
3616 const idField = this._rowIdField(table);
3617 if (!data || typeof data[idField] === 'undefined') return;
3618
3619 if (!rowsMap[data[idField]]) {
3620 rowsMap[data[idField]] = Object.assign({}, data);
3621 }
3622 });
3623
3624 const rows = Object.values(rowsMap);
3625
3626 if (!rows.length) {
3627
3628 table._dbxSaving = false;
3629 table._dbxDirty = false;
3630
3631 if (table.element && table.element._dbxFeature) {
3632 table.element._dbxFeature.updateSaveButton(table.element, table);
3633 }
3634
3635 return;
3636 }
3637
3638 const url = this._dbxAjaxUrl(opt.urls.save);
3639
3640 table._dbxSaving = true;
3641
3642 this._dbxRequest(url, {
3643 method: 'POST',
3644 headers: {
3645 'Content-Type': 'application/json'
3646 },
3647 body: JSON.stringify({ rows: rows }),
3648 responseType: 'json'
3649 })
3650 .then(resp => {
3651
3652 table._dbxSaving = false;
3653 table._dbxDirty = false;
3654
3655 table.getEditedCells().forEach(cell => {
3656 try {
3657 cell.clearEdited();
3658 } catch (e) {}
3659 });
3660
3661 this._clearConflictFlags(table);
3662
3663 if (table.element && table.element._dbxFeature) {
3664 table.element._dbxFeature.updateSaveButton(table.element, table);
3665 }
3666
3667 })
3668 .catch(err => {
3669 table._dbxSaving = false;
3670 this._syncDirtyState(table);
3671 if (table.element && table.element._dbxFeature) {
3672 table.element._dbxFeature.updateSaveButton(table.element, table);
3673 }
3674 dbx.error('[grid] SAVE failed', {
3675 error: err,
3676 url: url
3677 });
3678 });
3679 }
3680
3681 });
3682
3683
3684 function dbxExtractLabel(token) {
3685 if (!token) return { key:'', label:'' };
3686 const m = token.match(/^([^\‍[]+)\‍[(.+)\‍]$/);
3687 if (!m) return { key: token.trim(), label: token.trim() };
3688 return { key: m[1].trim(), label: m[2].trim() };
3689 }
3690
3691 function dbxGridParseColumnOptions(raw) {
3692 const out = {};
3693 String(raw || '').split(';').forEach(part => {
3694 part = part.trim();
3695 if (!part) return;
3696
3697 const pos = part.indexOf('=');
3698 if (pos === -1) {
3699 out[part] = '1';
3700 return;
3701 }
3702
3703 const key = part.substring(0, pos).trim();
3704 const value = part.substring(pos + 1).trim();
3705 if (key) out[key] = value;
3706 });
3707
3708 return out;
3709 }
3710
3711 function dbxGridParseEditorValues(raw) {
3712 const values = {};
3713
3714 String(raw || '').split('~').forEach(part => {
3715 const pos = part.indexOf('=');
3716 let value = part;
3717 let label = part;
3718
3719 if (pos !== -1) {
3720 value = part.substring(0, pos);
3721 label = part.substring(pos + 1);
3722 }
3723
3724 values[value] = label;
3725 });
3726
3727 return values;
3728 }
3729
3730
3731 /* =================================================
3732 * [dbx][grid][schema][step2]
3733 * schema parser & normalizer
3734 * ================================================= */
3735 (function() {
3736
3737 if (!window.dbx) return;
3738
3739 window.dbxGridParseSchema = function(rawSchema) {
3740
3741 const out = {
3742 meta: rawSchema.meta || {},
3743 conditions: rawSchema.conditions || {},
3744 rows: [],
3745 columns: {}
3746 };
3747
3748 if (Array.isArray(rawSchema.rows)) {
3749 rawSchema.rows.forEach(rowRule => {
3750
3751 const norm = dbxGridNormalizeRule(
3752 rowRule,
3753 out.conditions,
3754 null
3755 );
3756
3757 if (!norm) return;
3758
3759 norm.style = {
3760 bg: rowRule.bg || null,
3761 color: rowRule.color || null,
3762 cls: rowRule.cls || null
3763 };
3764
3765 out.rows.push(norm);
3766 });
3767 }
3768
3769 if (!rawSchema.columns || typeof rawSchema.columns !== 'object') {
3770 return out;
3771 }
3772
3773 Object.keys(rawSchema.columns).forEach(colName => {
3774
3775 const colDef = rawSchema.columns[colName];
3776 if (!colDef || !Array.isArray(colDef.rules)) return;
3777
3778 out.columns[colName] = { rules: [] };
3779
3780 colDef.rules.forEach(rule => {
3781
3782 const norm = dbxGridNormalizeRule(
3783 Object.assign({}, rule, { col: colName }),
3784 out.conditions,
3785 colName
3786 );
3787
3788 if (!norm) return;
3789
3790 out.columns[colName].rules.push(norm);
3791 });
3792 });
3793
3794 return out;
3795 };
3796
3797 function dbxGridNormalizeRule(rule, conditions, currentCol) {
3798
3799 if (!rule || typeof rule !== 'object') return null;
3800
3801 const resolveCondition = (c) => {
3802 if (typeof c === 'string') {
3803 if (!conditions[c]) {
3804 console.warn('[normalize] unknown condition', c);
3805 return null;
3806 }
3807 return Object.assign({}, conditions[c]);
3808 }
3809 return Object.assign({}, c);
3810 };
3811
3812 if (Array.isArray(rule.all)) {
3813
3814 if (rule.all.length === 0) {
3815 return {
3816 all: [],
3817 style: {
3818 bg: rule.bg || null,
3819 color: rule.color || rule.text || null,
3820 cls: rule.cls || null
3821 }
3822 };
3823 }
3824
3825 const subs = rule.all
3826 .map(resolveCondition)
3827 .map(r => dbxGridNormalizeRule(r, conditions, currentCol))
3828 .filter(Boolean);
3829
3830 if (!subs.length) return null;
3831
3832 return {
3833 all: subs,
3834 style: {
3835 bg: rule.bg || null,
3836 color: rule.color || rule.text || null,
3837 cls: rule.cls || null
3838 }
3839 };
3840 }
3841
3842 if (Array.isArray(rule.any)) {
3843
3844 const subs = rule.any
3845 .map(resolveCondition)
3846 .map(r => dbxGridNormalizeRule(r, conditions, currentCol))
3847 .filter(Boolean);
3848
3849 if (!subs.length) return null;
3850
3851 return {
3852 any: subs,
3853 style: {
3854 bg: rule.bg || null,
3855 color: rule.color || rule.text || null,
3856 cls: rule.cls || null
3857 }
3858 };
3859 }
3860
3861 const col = rule.col || currentCol;
3862
3863 if (!col) {
3864 console.warn('[normalize] rule dropped (no col)', rule);
3865 return null;
3866 }
3867
3868 return {
3869 col,
3870 if: rule.if,
3871 normalize: rule.normalize,
3872 value: rule.value,
3873 isReserved: rule.isReserved || false,
3874 style: {
3875 bg: rule.bg || null,
3876 color: rule.color || rule.text || null,
3877 cls: rule.cls || null
3878 }
3879 };
3880 }
3881
3882 })();
3883
3884
3885 /* =================================================
3886 * [dbx][grid][schema][step3]
3887 * rule evaluator
3888 * ================================================= */
3889 (function() {
3890
3891 window.dbxGrid.evalCell = function(colRules, cellValue, rowData) {
3892
3893 if (!colRules || !Array.isArray(colRules.rules)) {
3894 return null;
3895 }
3896
3897 let finalStyle = null;
3898 let matched = false;
3899
3900 for (let i = 0; i < colRules.rules.length; i++) {
3901
3902 const rule = colRules.rules[i];
3903 const ok = window.dbxGrid.evalRule(rule, cellValue, rowData);
3904
3905 if (!ok) continue;
3906
3907 matched = true;
3908
3909 if (rule.style) {
3910 finalStyle = Object.assign({}, finalStyle || {}, rule.style);
3911 }
3912 }
3913
3914 if (matched) {
3915 return finalStyle || {};
3916 }
3917
3918 return null;
3919 };
3920
3921 window.dbxGrid.evalRule = function(rule, cellValue, rowData) {
3922
3923 if (!rule) return false;
3924
3925 if (Array.isArray(rule.all)) {
3926 return rule.all.every(r =>
3927 window.dbxGrid.evalRule(r, cellValue, rowData)
3928 );
3929 }
3930
3931 if (Array.isArray(rule.any)) {
3932 return rule.any.some(r =>
3933 window.dbxGrid.evalRule(r, cellValue, rowData)
3934 );
3935 }
3936
3937 let left;
3938
3939 if (rule.col === '$cell') {
3940 left = cellValue;
3941 } else if (rule.col) {
3942 left = rowData[rule.col];
3943 } else {
3944 left = cellValue;
3945 }
3946
3947 if (typeof left === 'string' && rule.normalize === 'trim') {
3948 left = left.trim();
3949 }
3950
3951 const right = dbxResolveCompareValue(
3952 rule.value,
3953 rule.isReserved || false,
3954 rowData
3955 );
3956
3957 return dbxCompare(left, rule.if, right);
3958 };
3959
3960 function dbxResolveCompareValue(value, isReserved, rowData) {
3961
3962 if (isReserved) {
3963 if (value === 'today') {
3964 const d = new Date();
3965 d.setHours(0, 0, 0, 0);
3966 return d;
3967 }
3968
3969 if (typeof value === 'string' && /^[+-]\d+(day|month|year)s?$/.test(value)) {
3970 return dbxShiftDate(new Date(), value);
3971 }
3972
3973 return value;
3974 }
3975
3976 if (typeof value === 'string' && value.charAt(0) === '$') {
3977 return rowData[value.substring(1)];
3978 }
3979
3980 return value;
3981 }
3982
3983 function dbxShiftDate(base, expr) {
3984 const d = new Date(base);
3985 const n = parseInt(expr, 10);
3986
3987 if (expr.includes('day')) d.setDate(d.getDate() + n);
3988 if (expr.includes('month')) d.setMonth(d.getMonth() + n);
3989 if (expr.includes('year')) d.setFullYear(d.getFullYear() + n);
3990
3991 return d;
3992 }
3993
3994 function dbxCompare(left, op, right) {
3995
3996 if (left === null || left === undefined) left = '';
3997 if (right === null || right === undefined) right = '';
3998
3999 if (op === 'empty') return left === '';
4000 if (op === 'notEmpty') return left !== '';
4001 if (op === 'startsWith') return String(left).startsWith(String(right));
4002 if (op === 'contains') return String(left).includes(String(right));
4003 if (op === '==') return left == right;
4004 if (op === '!=') return left != right;
4005
4006 const l = dbxToComparable(left);
4007 const r = dbxToComparable(right);
4008
4009 if (l === null || r === null) return false;
4010
4011 if (op === '<') return l < r;
4012 if (op === '<=') return l <= r;
4013 if (op === '>') return l > r;
4014 if (op === '>=') return l >= r;
4015
4016 return false;
4017 }
4018
4019 function dbxToComparable(v) {
4020
4021 if (v instanceof Date) return v.getTime();
4022
4023 if (typeof v === 'string') {
4024 const d = dbxParseDate(v);
4025 if (d) return d.getTime();
4026 }
4027
4028 if (!isNaN(v)) return Number(v);
4029
4030 return null;
4031 }
4032
4033 window.dbxParseDate = function(v) {
4034
4035 if (!v) return null;
4036
4037 if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
4038 const d = new Date(v);
4039 return isNaN(d) ? null : d;
4040 }
4041
4042 if (/^\d{2}\.\d{2}\.\d{4}$/.test(v)) {
4043 const [d, m, y] = v.split('.');
4044 const dt = new Date(`${y}-${m}-${d}`);
4045 return isNaN(dt) ? null : dt;
4046 }
4047
4048 return null;
4049 };
4050
4051 })();
4052
4053
4054 /* =================================================
4055 * [dbx][grid][schema][step4]
4056 * apply style helper
4057 * ================================================= */
4058 window.dbxGridApplyCellStyle = function(cell, style) {
4059
4060 const el = cell.getElement();
4061 if (!el || !style) return;
4062
4063 if (style.bg) {
4064 el.style.removeProperty('background-color');
4065 el.style.setProperty('background-color', style.bg, 'important');
4066 }
4067
4068 if (style.color) {
4069 el.style.removeProperty('color');
4070 el.style.setProperty('color', style.color, 'important');
4071 }
4072
4073 if (style.cls) {
4074 el.classList.add(style.cls);
4075 }
4076 };
4077
4078})();