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