2 * ============================================================
3 * DBX GRID – INVARIANTEN (UNVERLETZBAR)
4 * ============================================================
6 * Diese Regeln definieren das unveränderliche Verhalten des Grids.
7 * Sie gelten IMMER – unabhängig von Features, Bugfixes oder Refactorings.
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.ä.
20 * ------------------------------------------------------------
21 * INVARIANTE 2: RELOAD DARF KEINE DATEN VERLIEREN
22 * ------------------------------------------------------------
23 * - Nach Reload dürfen keine Zeilen verschwinden.
25 * - veraltetem Sort-State
26 * - geänderten Spalten / Schema
27 * - kaputtem Layout-State
30 * - Layout best-effort anwenden
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.
42 * ------------------------------------------------------------
43 * INVARIANTE 4: SYSTEM-SPALTEN SIND HEILIG
44 * ------------------------------------------------------------
45 * - System-Spalten (z.B. _actions, _rownum, _*)
46 * sind NICHT Teil des User-Layouts.
51 * - nicht ausgeblendet
53 * - User-State darf System-Spalten niemals beeinflussen.
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.
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.
71 * ============================================================
73 * Das Grid darf NIE überraschen.
74 * Vorhersehbares Verhalten ist wichtiger als Feature-Vollständigkeit.
75 * ============================================================
80 * dbx grid feature (Tabulator)
81 * -------------------------------------------------
82 * requires: core.js (dbx namespace + loader)
83 * -------------------------------------------------
87 window.dbxGrid = window.dbxGrid || {};
89 if (!window.dbx || !dbx.feature) {
90 console.error('[dbx][grid] dbx core missing');
94 dbx.feature.register('grid', {
99 ['css','root','add_ons/tabulator/dist/css/tabulator.min.css'],
100 ['css','design','c-grid.css']
104 ['js','lib','ajax.js'],
105 ['js','root','add_ons/tabulator/dist/js/tabulator.min.js']
111 /* =========================================================
112 * SCHEMA AUTOLOAD (design/js/<schema>.js)
113 * ========================================================= */
114 loadSchema(schemaName, done) {
117 window.dbxGridSchema &&
118 window.dbxGridSchema[schemaName]
125 dbx.config.rootPath +
132 dbx.log('[grid][schema] load', url);
134 dbx.loader.js(url, () => {
136 window.dbxGridSchema &&
137 window.dbxGridSchema[schemaName]
141 dbx.error('[grid][schema] loaded but not registered:', schemaName);
147 /* =========================================================
149 * ========================================================= */
152 if (typeof window.Tabulator === "undefined") {
155 "Missing dependency: Tabulator\n\n" +
157 "id=" + (cfg.id || "undef") + "\n\n" +
158 "Check PREPARE js loading."
160 dbx.error("Tabulator missing");
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;
170 const colsDef = cfg.cols || '';
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;
176 const headerFilter = this._bool(cfg.headerfilter ?? cfg.headerFilter ?? 1, true);
177 const headerSort = this._bool(cfg.headersort ?? cfg.headerSort ?? 1, true);
179 const headerFilterLiveFilter = this._bool(cfg.headerfilterlivefilter ?? cfg.headerFilterLiveFilter ?? 1, true);
180 const headerFilterPlaceholder = String(cfg.headerfilterplaceholder ?? cfg.headerFilterPlaceholder ?? '');
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;
187 const paginationSizeSelector = this._parsePaginationSizeSelector(
188 cfg.pagesizeselector ?? cfg.paginationSizeSelector ?? false
191 const paginationButtonCount = this._int(
192 cfg.paginationbuttoncount ?? cfg.paginationButtonCount ?? 5,
196 const paginationCounter = this._normalizePaginationCounter(
197 cfg.paginationcounter ?? cfg.paginationCounter ?? false
200 const paginationAddRow = String(
201 cfg.paginationaddrow ?? cfg.paginationAddRow ?? 'page'
202 ).toLowerCase() === 'table' ? 'table' : 'page';
204 const paginationOutOfRange = this._normalizePaginationOutOfRange(
205 cfg.paginationoutofrange ?? cfg.paginationOutOfRange ?? false
208 const paginationControls = this._bool(
209 cfg.paginationcontrols ?? cfg.paginationControls ?? 1,
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);
218 const responsiveLayoutRaw = String(cfg.responsivelayout ?? cfg.responsiveLayout ?? '').toLowerCase().trim();
219 const responsiveLayout =
220 (!responsiveLayoutRaw || responsiveLayoutRaw === '0' || responsiveLayoutRaw === 'false' || responsiveLayoutRaw === 'off')
222 : responsiveLayoutRaw;
224 const movableColumns = this._bool(cfg.movablecolumns ?? cfg.movableColumns ?? 1, true);
225 const resizableColumns = this._bool(cfg.resizablecolumns ?? cfg.resizableColumns ?? 1, true);
227 const headerSortStart = this._normalizeHeaderSortStart(
228 cfg.headersortstart ?? cfg.headerSortStart ?? 'asc'
231 const headerSortTristate = this._bool(
232 cfg.headersorttristate ?? cfg.headerSortTristate ?? 0,
236 const searchPlaceholder = String(cfg.searchplaceholder ?? '🔍');
237 const searchWidth = this._int(cfg.searchwidth ?? 220, 220);
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);
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);
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');
263 if (cfg.sort && cfg.sort !== '0') {
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,
276 dbx.log('[grid] init', {
277 id: cfg.id || 'undef',
281 paginationSizeSelector,
282 paginationButtonCount,
299 this.createTable(el, {
308 deleteConfirmTitle: String(cfg.deleteconfirmtitle || dbx.translate({
309 de: '<i class="bi bi-trash"></i> Datensatz löschen',
310 en: '<i class="bi bi-trash"></i> Delete record',
311 es: '<i class="bi bi-trash"></i> Eliminar registro'
313 deleteConfirmQuestion: String(cfg.deleteconfirmquestion || dbx.translate({
314 de: 'Diesen Datensatz wirklich löschen?',
315 en: 'Do you really want to delete this record?',
316 es: '¿Desea eliminar este registro?'
318 deleteConfirmHint: String(cfg.deleteconfirmhint || dbx.translate({
319 de: '<small>Dieser Vorgang kann nicht rückgängig gemacht werden.</small>',
320 en: '<small>This action cannot be undone.</small>',
321 es: '<small>Esta acción no se puede deshacer.</small>'
325 headerFilterLiveFilter,
326 headerFilterPlaceholder,
332 paginationSizeSelector,
333 paginationButtonCount,
336 paginationOutOfRange,
372 /* =========================================================
374 * ========================================================= */
377 const table = el && el._dbxTable ? el._dbxTable : null;
381 table._dbxDestroyed = true;
386 if (table && table._dbxLoopId) {
387 dbx.loop.hint(table._dbxLoopId, 'pause');
390 dbx.warn('[grid] destroy loop pause failed', e);
394 if (table && table._dbxAutoTimer) {
395 clearTimeout(table._dbxAutoTimer);
396 table._dbxAutoTimer = null;
399 dbx.warn('[grid] destroy auto timer clear failed', e);
403 if (table && table._dbxLayoutTimer) {
404 clearTimeout(table._dbxLayoutTimer);
405 table._dbxLayoutTimer = null;
408 dbx.warn('[grid] destroy layout timer clear failed', e);
412 if (table && table._dbxPageLayoutTimer) {
413 clearTimeout(table._dbxPageLayoutTimer);
414 table._dbxPageLayoutTimer = null;
417 dbx.warn('[grid] destroy page layout timer clear failed', e);
421 if (table && table._dbxChooserTimer) {
422 clearTimeout(table._dbxChooserTimer);
423 table._dbxChooserTimer = null;
426 dbx.warn('[grid] destroy chooser timer clear failed', e);
430 if (table && typeof table.destroy === 'function') {
434 dbx.warn('[grid] destroy table failed', e);
438 delete el._dbxGridInitialized;
440 delete el._dbxFeature;
442 delete el._dbxSchemaParsed;
443 delete el._dbxApplyGridLines;
448 /* =========================================================
449 * DBX AJAX URL HELPER
450 * ========================================================= */
451 _dbxAjaxUrl(url, opts) {
455 if (!url) return url;
457 let finalUrl = (dbx.ajax && typeof dbx.ajax.url === 'function')
461 if (opts.background === true && finalUrl.indexOf('dbx_sync=') === -1) {
462 finalUrl += (finalUrl.indexOf('?') === -1 ? '?' : '&') + 'dbx_sync=0';
469 _getAjaxSorters(table) {
471 if (!table || !table.element) return [];
473 const opt = table.element._dbxOpt || {};
475 if (opt.headerSort !== true) {
479 if (table._dbxBuilt !== true) {
483 if (table._dbxIsRemotePagination === true) {
484 const sorters = table.getSorters ? table.getSorters() : [];
485 return Array.isArray(sorters) ? sorters : [];
488 if (opt.urls && opt.urls.sort) {
489 const s = table._dbxServerSort;
491 if (s && s.field && s.dir) {
499 const sorters = table.getSorters ? table.getSorters() : [];
500 return Array.isArray(sorters) ? sorters : [];
503 _applyServerSortIndicators(table) {
505 if (!table || !table.element) return;
507 const opt = table.element._dbxOpt || {};
509 if (!opt.urls || !opt.urls.sort) return;
510 if (table._dbxIsRemotePagination === true) return;
512 const active = table._dbxServerSort || null;
513 const cols = this._getLeafColumns(table);
515 cols.forEach(col => {
517 const field = col.getField ? col.getField() : null;
518 if (!field || field.startsWith('_')) return;
520 const el = col.getElement ? col.getElement() : null;
525 if (active && active.field === field) {
526 aria = (active.dir === 'asc') ? 'ascending' : 'descending';
529 el.setAttribute('aria-sort', aria);
535 _dbxRequest(url, options = {}) {
538 return Promise.reject(new Error('Missing URL'));
541 if (!dbx.ajax || typeof dbx.ajax.request !== 'function') {
542 return Promise.reject(new Error('ajax.js nicht geladen.'));
545 const method = String(options.method || 'GET').toUpperCase();
546 const headers = options.headers || {};
547 const body = (typeof options.body === 'undefined') ? null : options.body;
548 const responseType = options.responseType || 'json';
549 const startedAt = Date.now();
550 const skipRuntime = options.skipRuntime === true
551 || /[?&]dbx_sync=0(?:&|$)/.test(String(url || ''));
553 dbx.log('[grid][ajax] start', {
556 responseType: responseType
559 return dbx.ajax.request({
562 mode: responseType === 'json' ? 'json' : 'text',
565 timeout: options.timeout || 30000,
566 skipRuntime: skipRuntime
568 dbx.log('[grid][ajax] success', {
571 duration_ms: Date.now() - startedAt
575 dbx.error('[grid][ajax] error', {
578 duration_ms: Date.now() - startedAt,
586 _parsePaginationSizeSelector(v) {
588 if (v === undefined || v === null || v === '' || v === false || v === 0 || v === '0' || v === 'off' || v === 'false') {
592 if (v === true || v === 1 || v === '1' || v === 'on' || v === 'true' || v === 'auto') {
596 const normalizeValue = (item) => {
597 if (item === true) return 99999;
599 const txt = String(item).trim().toLowerCase();
601 if (!txt) return null;
602 if (txt === 'true') return 99999;
603 if (txt === 'all') return 99999;
604 if (txt === '*') return 99999;
605 if (txt === '__all') return 99999;
607 const n = parseInt(txt, 10);
608 if (!isNaN(n) && n > 0) return n;
613 if (Array.isArray(v)) {
614 const out = v.map(normalizeValue).filter(x => x !== null);
615 return out.length ? this._normalizePaginationSizeSelectorOrder(out) : false;
618 const out = String(v)
621 .filter(x => x !== null);
623 return out.length ? this._normalizePaginationSizeSelectorOrder(out) : false;
626 _normalizePaginationSizeSelectorOrder(values) {
628 return [15, 5, 25, 50, 100, 99999];
631 _pageSizeSelectOptions() {
634 { value: '1', text: '1' },
635 { value: '5', text: '5' },
636 { value: '15', text: '15' },
637 { value: '25', text: '25' },
638 { value: '50', text: '50' },
639 { value: '100', text: '100' },
640 { value: '99999', text: '*' }
644 _normalizePageSizeValue(v, def = 15, selector = false) {
646 if (v === true) return 99999;
648 const txt = String(v ?? '').toLowerCase().trim();
649 if (txt === 'true' || txt === 'all' || txt === '*' || txt === '__all') return 99999;
651 const n = parseInt(txt, 10);
652 if (!isNaN(n) && n > 0) {
656 const defTxt = String(def ?? '').toLowerCase().trim();
657 if (def === true || defTxt === 'true' || defTxt === 'all' || defTxt === '*' || defTxt === '__all') {
661 const defNum = parseInt(def, 10);
662 if (!isNaN(defNum) && defNum > 0) {
666 if (Array.isArray(selector) && selector.length) {
667 return selector[0] === true ? 99999 : (parseInt(selector[0], 10) || 15);
673 _storePageSizeState(gridId, value, def = 15, selector = false) {
675 const normalized = this._normalizePageSizeValue(value, def, selector);
676 dbx.uiSet('grid', gridId, 'PAGE.SIZE', String(normalized));
680 _getPageSizeState(gridId, def = 15, selector = false) {
682 const defaultSize = this._normalizePageSizeValue(def, 15, selector);
683 return this._normalizePageSizeValue(
684 dbx.uiGet('grid', gridId, 'PAGE.SIZE', String(defaultSize)),
690 _changePageSize(table, value, opt = {}) {
692 if (!table || !table.element) return;
694 const gridId = table.element.id || 'grid';
695 const pageSize = this._storePageSizeState(gridId, value, opt.pageSize || 15, opt.paginationSizeSelector);
697 table._dbxPageSizeState = pageSize;
698 opt.pageSize = pageSize;
701 table._dbxPageSizeChanging = true;
702 if (typeof table.setPageSize === 'function') {
703 table.setPageSize(pageSize);
705 if (typeof table.setPage === 'function') {
709 dbx.warn('[grid] page size change failed', err);
711 table._dbxPageSizeChanging = false;
714 this.reloadTable(table, opt, { resetPage: true });
715 window.setTimeout(() => this._applyPaginationButtonLabels(table), 0);
718 _normalizePaginationCounter(v) {
720 if (v === undefined || v === null || v === '' || v === false || v === 0 || v === '0' || v === 'off' || v === 'false') {
724 const txt = String(v).toLowerCase().trim();
726 if (txt === '1' || txt === 'on' || txt === 'true') return 'rows';
727 if (txt === 'rows') return 'rows';
728 if (txt === 'pages') return 'pages';
733 _normalizeHeaderSortStart(v) {
735 const txt = String(v || 'asc').toLowerCase().trim();
736 return (txt === 'desc') ? 'desc' : 'asc';
739 _normalizePaginationOutOfRange(v) {
741 if (v === undefined || v === null || v === '') return false;
743 const txt = String(v).toLowerCase().trim();
745 if (txt === 'false' || txt === 'off' || txt === '0') return false;
746 if (txt === 'first' || txt === 'last' || txt === 'reset') return txt;
748 const n = parseInt(txt, 10);
749 if (!isNaN(n)) return n;
754 _getPaginationUiEls(el) {
756 const root = this._getRoot(el);
760 bar: root ? root.querySelector('[data-dbx-role="pagination-bar"]') : null,
761 controls: root ? root.querySelector('[data-dbx-role="pagination-controls"]') : null,
762 counter: root ? root.querySelector('[data-dbx-role="pagination-counter"]') : null
766 _setRoleVisible(root, role, show) {
770 const el = root.querySelector('[data-dbx-role="' + role + '"]');
773 el.style.display = show ? '' : 'none';
776 _loadRootScript(file, done) {
778 window.dbxGridExportDeps = window.dbxGridExportDeps || {};
780 const state = window.dbxGridExportDeps[file] || { status: 'new', callbacks: [] };
781 window.dbxGridExportDeps[file] = state;
783 if (state.status === 'loaded') {
788 if (state.status === 'loading') {
789 state.callbacks.push(done);
793 state.status = 'loading';
794 state.callbacks = done ? [done] : [];
796 let url = dbx.config.rootPath + file;
798 const searchParams = new URLSearchParams(location.search);
799 const cacheBust = searchParams.get('dbx_nocache') || searchParams.get('cachebust');
801 url += (url.indexOf('?') === -1 ? '?' : '&') + 'dbx_nocache=' + encodeURIComponent(cacheBust);
804 const finish = (ok) => {
806 state.status = ok ? 'loaded' : 'error';
808 state.callbacks.forEach(cb => cb && cb(ok === true));
809 state.callbacks = [];
812 const xhr = new XMLHttpRequest();
813 xhr.open('GET', url, true);
816 if (xhr.status < 200 || xhr.status >= 300) {
817 dbx.error('[grid] export dependency load failed', url, 'HTTP ' + xhr.status);
823 const run = new Function(
831 xhr.responseText + '\n//# sourceURL=' + url
834 run.call(window, window, window, window, window, undefined, undefined, undefined);
837 dbx.error('[grid] export dependency load failed', url, e);
842 xhr.onerror = () => {
843 dbx.error('[grid] export dependency load failed', url);
850 _waitForExportDep(check, done, attempts) {
852 const maxAttempts = attempts || 20;
859 if (maxAttempts <= 0) {
864 window.setTimeout(() => {
865 this._waitForExportDep(check, done, maxAttempts - 1);
869 _setTabulatorDependency(table, key, value) {
871 if (!table || !table.dependencyRegistry || !value) return false;
873 table.dependencyRegistry.deps = table.dependencyRegistry.deps || {};
874 table.dependencyRegistry.deps[key] = value;
879 _ensureExcelExportDeps(table, done) {
882 done && done(this._setTabulatorDependency(table, 'XLSX', window.XLSX));
886 this._loadRootScript('add_ons/tabulator-deps/xlsx.full.min.js', (ok) => {
892 this._waitForExportDep(
895 done && done(ready === true && this._setTabulatorDependency(table, 'XLSX', window.XLSX));
901 _ensurePdfExportDeps(table, done) {
903 const hasAutoTable = () => !!(
905 window.jspdf.jsPDF &&
906 window.jspdf.jsPDF.API &&
907 window.jspdf.jsPDF.API.autoTable
910 if (hasAutoTable()) {
911 done && done(this._setTabulatorDependency(table, 'jspdf', window.jspdf));
915 const loadAutoTable = () => {
916 this._loadRootScript('add_ons/tabulator-deps/jspdf.plugin.autotable.min.js', (ok) => {
922 this._waitForExportDep(
925 done && done(ready === true && this._setTabulatorDependency(table, 'jspdf', window.jspdf));
931 if (window.jspdf && window.jspdf.jsPDF) {
936 this._loadRootScript('add_ons/tabulator-deps/jspdf.umd.min.js', (ok) => {
937 if (ok !== true || !(window.jspdf && window.jspdf.jsPDF)) {
946 _applyPaginationButtonLabels(table) {
948 if (!table || !table.element) return;
950 const language = String(document.documentElement.lang || 'de')
953 const translations = {
955 rowsPerPage: 'Zeilen pro Seite',
956 allRows: 'Alle Zeilen',
958 first: 'Erste Seite',
959 prev: 'Vorherige Seite',
960 next: 'Nächste Seite',
961 last: 'Letzte Seite',
962 showPage: 'Seite {page} anzeigen'
965 rowsPerPage: 'Rows per page',
969 prev: 'Previous page',
972 showPage: 'Show page {page}'
975 rowsPerPage: 'Filas por página',
976 allRows: 'Todas las filas',
978 first: 'Primera página',
979 prev: 'Página anterior',
980 next: 'Página siguiente',
981 last: 'Última página',
982 showPage: 'Mostrar página {page}'
985 const text = translations[language] || translations.de;
987 const ui = this._getPaginationUiEls(table.element);
988 const controls = ui && ui.controls ? ui.controls : null;
989 if (!controls) return;
991 const sizeSelect = controls.querySelector('.tabulator-page-size');
993 let icon = controls.querySelector('.dbx-grid-page-size-icon');
995 controls.querySelectorAll('label').forEach(label => {
996 if (String(label.textContent || '').trim().toLowerCase() === 'page size') {
1002 icon = document.createElement('span');
1003 icon.className = 'dbx-grid-page-size-icon';
1004 icon.innerHTML = '<i class="bi bi-list-ol"></i>';
1005 icon.setAttribute('title', text.rowsPerPage);
1006 icon.setAttribute('aria-hidden', 'true');
1008 controls.insertBefore(icon, sizeSelect);
1011 sizeSelect.setAttribute('title', text.rowsPerPage);
1012 sizeSelect.setAttribute('aria-label', text.rowsPerPage);
1014 const currentPageSize = table._dbxPageSizeState || (table.getPageSize ? table.getPageSize() : '');
1015 const currentValue = String(currentPageSize || sizeSelect.value || '15');
1017 sizeSelect.innerHTML = '';
1019 this._pageSizeSelectOptions().forEach(item => {
1020 const option = document.createElement('option');
1021 option.value = item.value;
1022 option.textContent = item.text;
1023 option.setAttribute('title', item.value === '99999' ? text.allRows : item.text + ' ' + text.rows);
1024 sizeSelect.appendChild(option);
1027 if (currentValue && sizeSelect.querySelector('option[value="' + currentValue + '"]')) {
1028 sizeSelect.value = currentValue;
1033 if (controls._dbxPageSizeStateBound !== true) {
1034 controls._dbxPageSizeStateBound = true;
1035 controls.addEventListener('change', (e) => {
1036 const select = e.target && e.target.closest ? e.target.closest('.tabulator-page-size') : null;
1037 if (!select) return;
1040 e.stopImmediatePropagation();
1042 const opt = table.element && table.element._dbxOpt ? table.element._dbxOpt : {};
1043 const value = this._normalizePageSizeValue(select.value, opt.pageSize || 15, opt.paginationSizeSelector);
1044 this._queueTableTimer(table, '_dbxPageSizeChangeTimer', () => {
1045 this._changePageSize(table, value, opt);
1050 const buttons = controls.querySelectorAll('.tabulator-page');
1051 if (!buttons || !buttons.length) return;
1053 const detectType = (btn) => {
1056 String(btn.getAttribute('data-page') || '').toLowerCase().trim(),
1057 String(btn.getAttribute('aria-label') || '').toLowerCase().trim(),
1058 String(btn.getAttribute('title') || '').toLowerCase().trim(),
1059 String(btn.textContent || '').toLowerCase().trim()
1062 const has = (needle) => values.some(v => v === needle || v.indexOf(needle) !== -1);
1064 if (has('first')) return 'first';
1065 if (has('previous') || has('prev')) return 'prev';
1066 if (has('next')) return 'next';
1067 if (has('last')) return 'last';
1074 html: '<i class="bi bi-chevron-bar-left"></i>',
1078 html: '<i class="bi bi-chevron-left"></i>',
1082 html: '<i class="bi bi-chevron-right"></i>',
1086 html: '<i class="bi bi-chevron-bar-right"></i>',
1091 buttons.forEach(btn => {
1093 const type = detectType(btn);
1094 if (!type || !defs[type]) {
1095 const page = String(btn.textContent || '').trim();
1096 if (/^\d+$/.test(page)) {
1097 const label = text.showPage.replace('{page}', page);
1098 btn.setAttribute('title', label);
1099 btn.setAttribute('aria-label', label);
1104 btn.dataset.dbxPageType = type;
1105 btn.innerHTML = defs[type].html;
1106 btn.setAttribute('title', defs[type].label);
1107 btn.setAttribute('aria-label', defs[type].label);
1111 _ensureSortIcons(table) {
1113 if (!table || !table.element) return;
1114 if (table._dbxBuilt !== true) return;
1116 const opt = table.element._dbxOpt || {};
1117 const cols = this._getLeafColumns(table);
1119 cols.forEach(col => {
1121 const field = col.getField ? col.getField() : null;
1122 if (!field || field.startsWith('_')) return;
1124 const def = col.getDefinition ? col.getDefinition() : {};
1126 (opt.headerSort === true) &&
1128 (def && def.headerSort === true) ||
1129 (def && typeof def.headerClick === 'function')
1132 const headerEl = col.getElement ? col.getElement() : null;
1133 if (!headerEl) return;
1136 headerEl.querySelector('.tabulator-col-title') ||
1137 headerEl.querySelector('.tabulator-col-content') ||
1140 let iconEl = headerEl.querySelector('.dbx-grid-sort-icon');
1143 if (iconEl) iconEl.remove();
1144 headerEl.classList.remove('dbx-grid-sortable');
1145 headerEl.classList.remove('dbx-grid-sort-asc');
1146 headerEl.classList.remove('dbx-grid-sort-desc');
1147 headerEl.classList.remove('dbx-grid-sort-none');
1148 headerEl.setAttribute('aria-sort', 'none');
1152 headerEl.classList.add('dbx-grid-sortable');
1155 iconEl = document.createElement('span');
1156 iconEl.className = 'dbx-grid-sort-icon';
1157 iconEl.innerHTML = '<i class="bi bi-arrow-down-up"></i>';
1158 titleEl.appendChild(iconEl);
1163 _applySortIndicators(table) {
1165 if (!table || !table.element) return;
1166 if (table._dbxBuilt !== true) return;
1168 const opt = table.element._dbxOpt || {};
1169 const cols = this._getLeafColumns(table);
1171 this._ensureSortIcons(table);
1175 if (opt.headerSort === true) {
1177 const sorters = this._getAjaxSorters(table);
1179 if (Array.isArray(sorters)) {
1180 sorters.forEach(s => {
1181 if (!s || !s.field) return;
1182 activeMap[s.field] = s.dir || 'asc';
1187 cols.forEach(col => {
1189 const field = col.getField ? col.getField() : null;
1190 if (!field || field.startsWith('_')) return;
1192 const def = col.getDefinition ? col.getDefinition() : {};
1194 (opt.headerSort === true) &&
1196 (def && def.headerSort === true) ||
1197 (def && typeof def.headerClick === 'function')
1200 const headerEl = col.getElement ? col.getElement() : null;
1201 if (!headerEl) return;
1203 const iconEl = headerEl.querySelector('.dbx-grid-sort-icon');
1205 headerEl.classList.remove('dbx-grid-sort-asc');
1206 headerEl.classList.remove('dbx-grid-sort-desc');
1207 headerEl.classList.remove('dbx-grid-sort-none');
1210 if (iconEl) iconEl.remove();
1211 headerEl.setAttribute('aria-sort', 'none');
1215 const dir = activeMap[field] || null;
1217 if (!iconEl) return;
1219 if (dir === 'asc') {
1220 iconEl.innerHTML = '<i class="bi bi-caret-up-fill"></i>';
1221 headerEl.classList.add('dbx-grid-sort-asc');
1222 headerEl.setAttribute('aria-sort', 'ascending');
1223 } else if (dir === 'desc') {
1224 iconEl.innerHTML = '<i class="bi bi-caret-down-fill"></i>';
1225 headerEl.classList.add('dbx-grid-sort-desc');
1226 headerEl.setAttribute('aria-sort', 'descending');
1228 iconEl.innerHTML = '<i class="bi bi-arrow-down-up"></i>';
1229 headerEl.classList.add('dbx-grid-sort-none');
1230 headerEl.setAttribute('aria-sort', 'none');
1235 /* =========================================================
1237 * ========================================================= */
1238 _bool(v, def = false) {
1239 if (v === undefined || v === null || v === '') return def;
1240 if (v === true || v === 1 || v === '1' || v === 'on' || v === 'true') return true;
1241 if (v === false || v === 0 || v === '0' || v === 'off' || v === 'false') return false;
1246 const n = parseInt(v, 10);
1247 return isNaN(n) ? def : n;
1250 _isTableAlive(table) {
1253 table._dbxDestroyed !== true &&
1255 table.element.isConnected === true
1259 _isTableLayoutReady(table) {
1261 if (!this._isTableAlive(table)) return false;
1263 const root = table.element;
1264 if (!root) return false;
1267 root.querySelector('.tabulator-header') ||
1268 root.querySelector('.tabulator-tableholder')
1272 _queueTableTimer(table, key, fn, delay = 0) {
1274 if (!table || !key || typeof fn !== 'function') return;
1277 clearTimeout(table[key]);
1281 table[key] = setTimeout(() => {
1284 if (!this._isTableAlive(table)) return;
1290 _getLeafColumns(table) {
1293 if (!table || typeof table.getColumns !== 'function') return out;
1295 const walk = (cols) => {
1296 cols.forEach(col => {
1297 const field = col.getField && col.getField();
1298 if (field && !field.startsWith('_')) {
1301 if (col.getSubColumns) {
1302 const subs = col.getSubColumns();
1303 if (subs && subs.length) {
1310 walk(table.getColumns());
1314 _restoreStoredColumnWidths(table, gridId) {
1316 if (!this._isTableLayoutReady(table)) return;
1318 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
1319 const cols = this._getLeafColumns(table);
1321 cols.forEach(col => {
1322 const field = col.getField();
1323 if (!field || field.startsWith('_')) return;
1325 const w = uiGet('COLUMNS.SIZE.' + field, null);
1326 if (w === null) return;
1328 const width = parseInt(w, 10);
1329 if (isNaN(width) || width <= 0) return;
1331 const currentWidth = col.getWidth();
1332 if (typeof currentWidth === 'number' && Math.abs(currentWidth - width) <= 1) {
1337 col.setWidth(width);
1339 dbx.warn('[grid] restore width failed', field, width, e);
1344 _restoreStoredColumnVisibility(table, gridId) {
1346 if (!this._isTableLayoutReady(table)) return;
1348 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
1349 const cols = this._getLeafColumns(table);
1351 cols.forEach(col => {
1352 const field = col.getField();
1353 if (!field || field.startsWith('_')) return;
1355 const vis = uiGet('COLUMNS.VISIBLE.' + field, null);
1356 if (vis === null) return;
1359 if (vis === '0' && col.isVisible()) col.hide();
1360 if (vis === '1' && !col.isVisible()) col.show();
1362 dbx.warn('[grid] restore visibility failed', field, vis, e);
1367 _applyShiftGroupLabels(table) {
1369 if (!this._isTableLayoutReady(table)) return;
1371 const root = table.element;
1372 if (!root || !root.innerHTML.includes('~~')) return;
1374 root.querySelectorAll('.tabulator-col-group').forEach(groupEl => {
1376 const titleEl = groupEl.querySelector('.tabulator-col-title');
1377 if (!titleEl) return;
1379 if (titleEl.querySelector('.dbx-shift-label')) return;
1381 const raw = titleEl.textContent;
1382 if (!raw || !raw.includes('~~')) return;
1384 const parts = raw.split('~~');
1385 if (parts.length !== 2) return;
1387 const left = parts[0].trim();
1388 const right = parts[1].trim();
1391 '<div class="dbx-shift-label">' +
1392 '<span class="left">' + left + '</span>' +
1393 '<span class="right">' + right + '</span>' +
1398 _applyInitialLayoutState(table, gridId) {
1400 if (!this._isTableLayoutReady(table)) return false;
1403 table.blockRedraw();
1405 this._restoreStoredColumnWidths(table, gridId);
1406 this._restoreStoredColumnVisibility(table, gridId);
1407 this._applyShiftGroupLabels(table);
1411 table.restoreRedraw(true);
1413 dbx.warn('[grid] restoreRedraw failed', e);
1421 return el.closest('.dbx-grid');
1424 _findSaveButton(el) {
1425 const root = this._getRoot(el);
1426 let btn = root ? root.querySelector('[data-dbx="grid-save"]') : null;
1428 const panel = el.closest('.dbx-panel');
1429 btn = panel ? panel.querySelector('[data-dbx="grid-save"]') : null;
1434 _tableHasPendingEdits(table) {
1435 if (!table || typeof table.getEditedCells !== 'function') {
1439 const edited = table.getEditedCells();
1440 return Array.isArray(edited) && edited.length > 0;
1446 _syncDirtyState(table) {
1447 const hasEdits = this._tableHasPendingEdits(table);
1449 table._dbxDirty = true;
1451 return table._dbxDirty === true || hasEdits;
1454 _markTableDirty(table, el) {
1455 table._dbxDirty = true;
1456 this.updateSaveButton(el, table);
1460 const root = this._getRoot(el);
1463 led: root ? root.querySelector('.dbx-grid-sync-led') : null,
1464 count: root ? root.querySelector('.dbx-grid-sync-count') : null
1468 _setLedState(led, state) {
1472 if (led._dbxSyncLedEnabled === false) {
1473 if (led.style.display !== 'none') {
1474 led.style.display = 'none';
1479 if (led.style.display === 'none') {
1480 led.style.display = 'inline-block';
1483 if (led._dbxState === state) return;
1484 led._dbxState = state;
1488 if (state === 'loading') color = '#0d6efd';
1489 if (state === 'ok') color = '#198754';
1490 if (state === 'idle') color = '#bbb';
1491 if (state === 'error') color = '#dc3545';
1493 if (led._dbxLastColor !== color) {
1494 led.style.backgroundColor = color;
1495 led._dbxLastColor = color;
1499 _setSyncCount(countEl, value) {
1501 if (!countEl) return;
1503 const txt = String(value ?? '');
1504 if (countEl.textContent !== txt) {
1505 countEl.textContent = txt;
1509 _clearConflictFlags(table) {
1510 if (!table || !table.element) return;
1511 table.element.querySelectorAll('.dbx-cell-conflict').forEach(el => {
1512 el.classList.remove('dbx-cell-conflict');
1516 _rowIdField(table) {
1517 return (table && table.options && table.options.index) ? table.options.index : 'id';
1520 _collectEditedMap(table) {
1522 const editedMap = {};
1523 const editedCells = table.getEditedCells();
1525 if (!editedCells || !editedCells.length) {
1529 for (let i = 0; i < editedCells.length; i++) {
1530 const c = editedCells[i];
1531 const r = c.getRow();
1534 const id = r.getData()?.[this._rowIdField(table)];
1535 const f = c.getField();
1537 if (id == null || !f) continue;
1539 if (!editedMap[id]) editedMap[id] = {};
1540 editedMap[id][f] = true;
1546 _applySchemaCellStyle(cell, rowData) {
1550 const table = cell.getTable();
1551 const schema = table?.element?._dbxSchemaParsed;
1552 if (!schema || !schema.columns) return;
1554 const field = cell.getField();
1555 const colSchema = schema.columns[field];
1556 if (!colSchema) return;
1558 const style = dbxGrid.evalCell(colSchema, cell.getValue(), rowData || cell.getRow().getData());
1560 dbxGridApplyCellStyle(cell, style);
1564 _ajaxResponse(table, url, params, response) {
1566 if (response && typeof response === 'object') {
1567 if (typeof response.server_time !== 'undefined') {
1568 table._dbxServerTime = response.server_time || null;
1571 if (typeof response.count !== 'undefined') {
1572 table._dbxSyncCount = response.count || 0;
1576 if (table._dbxIsRemotePagination === true || table._dbxIsProgressive === true) {
1578 if (response && Array.isArray(response.data)) {
1580 last_page: response.last_page || 1,
1581 last_row: response.last_row,
1586 if (response && Array.isArray(response.rows)) {
1588 last_page: response.last_page || 1,
1589 last_row: response.last_row,
1594 if (Array.isArray(response)) {
1601 dbx.error('[grid] invalid paginated response', response);
1608 if (response && Array.isArray(response.rows)) {
1609 return response.rows;
1612 if (Array.isArray(response)) {
1616 dbx.error('[grid] invalid response', response);
1621 /* =========================================================
1623 * ========================================================= */
1624 _escapeHtml(value) {
1625 return String(value ?? '')
1626 .replace(/&/g, '&')
1627 .replace(/</g, '<')
1628 .replace(/>/g, '>')
1629 .replace(/"/g, '"')
1630 .replace(/'/g, ''');
1633 _deleteRecordLabel(data, idField) {
1634 if (!data || typeof data !== 'object') return '';
1637 const name = data.display_name || data.name2 || data.name || data.uname || data.title || data.label || '';
1638 const email = data.email || '';
1639 const id = data[idField] ?? data.id ?? '';
1641 if (name) parts.push(String(name));
1642 if (email) parts.push(String(email));
1643 if (id !== '') parts.push('ID ' + String(id));
1645 return parts.join(' - ');
1648 _confirmDelete(data, idField, opt, source) {
1649 const feature = this;
1650 const label = feature._deleteRecordLabel(data, idField);
1651 const labelHtml = label
1652 ? '<div class="mt-2"><strong>' + feature._escapeHtml(label) + '</strong></div>'
1655 const openConfirm = function() {
1656 if (!window.dbx || !dbx.confirm || typeof dbx.confirm.open !== 'function') {
1657 (window.dbx && dbx.error ? dbx.error : console.error)('[grid] confirm feature missing');
1658 return Promise.resolve(false);
1661 return dbx.confirm.open({
1662 id: 'grid-delete-' + String((data && (data[idField] ?? data.id)) || Date.now()),
1663 root: source ? source.closest('[data-dbx]') : document.body,
1665 title: opt.deleteConfirmTitle,
1666 question: opt.deleteConfirmQuestion + labelHtml,
1667 hint: opt.deleteConfirmHint,
1669 labelyes: '<i class="bi bi-trash"></i> ' + dbx.translate({
1674 labelno: '<i class="bi bi-x-lg"></i> ' + dbx.translate({
1680 backdropclose: false,
1682 }).then(result => result && result.action === 'yes');
1685 if (window.dbx && typeof dbx.loadFeature === 'function' && (!dbx.confirm || typeof dbx.confirm.open !== 'function')) {
1686 return new Promise(resolve => {
1687 dbx.loadFeature('confirm', function() {
1688 openConfirm().then(resolve).catch(() => resolve(false));
1693 return openConfirm().catch(() => false);
1698 const colsDef = opt.colsDef;
1699 const gridId = opt._gridId;
1701 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
1705 const ungrouped = [];
1706 let hasGroups = false;
1708 const hasActions = !!(opt.allowDelete);
1710 const orderRaw = uiGet('COLUMNS.ORDER', null);
1711 const orderList = orderRaw
1712 ? orderRaw.split('|').filter(f => f && !f.startsWith('_'))
1715 const sortEnabled = (opt.headerSort === true);
1716 const useDedicatedServerSort = sortEnabled && !!(opt.urls.sort && opt._dbxIsRemotePagination !== true);
1717 const useTabulatorSort = sortEnabled && !useDedicatedServerSort;
1719 const actionsCol = {
1720 title: '<i class="bi bi-gear"></i>',
1721 headerHozAlign: 'center',
1727 headerHozAlign: 'center',
1730 headerFilter: false,
1732 cssClass: 'dbx-col-actions',
1733 formatter: function(cell) {
1734 const wrap = document.createElement('div');
1735 wrap.style.display = 'flex';
1736 wrap.style.gap = '6px';
1737 wrap.style.justifyContent = 'center';
1738 wrap.style.alignItems = 'center';
1740 const row = cell.getRow();
1741 const table = row.getTable();
1742 const data = row.getData();
1744 if (data && data.show_link) {
1745 const btnShow = document.createElement('button');
1746 btnShow.className = 'btn btn-sm btn-outline-primary';
1747 btnShow.style.minWidth = '28px';
1748 btnShow.style.height = '25px';
1749 btnShow.style.padding = '2px 6px';
1750 btnShow.style.lineHeight = '1';
1751 btnShow.title = 'Anzeigen';
1752 btnShow.innerHTML = '<i class="bi bi-eye"></i>';
1754 btnShow.addEventListener('click', function(e) {
1755 e.stopPropagation();
1756 const url = String(data.show_link || '');
1759 if (window.dbx && dbx.openWin && typeof dbx.openWin.open === 'function') {
1762 title: dbx.translate({
1766 }) + ': ' + String(data.title || 'Content'),
1778 if (window.dbx && dbx.utilities && dbx.utilities.leaveGuard) {
1779 dbx.utilities.leaveGuard.allowIfInternal(url);
1781 window.location.href = url;
1785 wrap.appendChild(btnShow);
1788 if (data && data.profile_link) {
1789 const btnEdit = document.createElement('button');
1790 btnEdit.className = 'btn btn-sm btn-outline-primary';
1791 btnEdit.style.minWidth = '28px';
1792 btnEdit.style.height = '25px';
1793 btnEdit.style.padding = '2px 6px';
1794 btnEdit.style.lineHeight = '1';
1795 btnEdit.title = dbx.translate({
1800 btnEdit.innerHTML = '<i class="bi bi-pencil-square"></i>';
1802 btnEdit.addEventListener('click', function(e) {
1803 e.stopPropagation();
1804 const url = String(data.profile_link || '');
1807 if (window.dbx && dbx.openWin && typeof dbx.openWin.open === 'function') {
1810 title: dbx.translate({
1822 if (window.dbx && dbx.utilities && dbx.utilities.leaveGuard) {
1823 dbx.utilities.leaveGuard.allowIfInternal(url);
1825 window.location.href = url;
1829 wrap.appendChild(btnEdit);
1832 if (opt.allowDelete) {
1833 const btnDel = document.createElement('button');
1834 btnDel.className = 'btn btn-sm btn-danger';
1835 btnDel.style.minWidth = '28px';
1836 btnDel.style.height = '25px';
1837 btnDel.style.padding = '2px 6px';
1838 btnDel.style.lineHeight = '1';
1839 btnDel.innerHTML = '<i class="bi bi-trash"></i>';
1841 btnDel.addEventListener('click', function(e) {
1842 e.stopPropagation();
1843 const idField = table.element._dbxFeature._rowIdField(table);
1844 if (!data || typeof data[idField] === 'undefined') return;
1846 table.element._dbxFeature._confirmDelete(data, idField, opt, btnDel)
1847 .then(confirmed => {
1848 if (!confirmed) return;
1850 return table.element._dbxFeature._dbxRequest(
1851 table.element._dbxFeature._dbxAjaxUrl(opt.urls.delete || ''),
1854 headers: { 'Content-Type': 'application/json' },
1855 body: JSON.stringify({ id: data[idField] }),
1856 responseType: 'json'
1862 if (res && (res.ok || res.success)) {
1865 dbx.error('[grid] delete failed', res);
1869 dbx.error('[grid] delete error', err);
1873 wrap.appendChild(btnDel);
1880 const fieldDefinitions = colsDef.split(',');
1883 fieldDefinitions.forEach(def => {
1885 let groupName = null;
1887 if (def.includes('@')) {
1888 const tmp = def.split('@');
1889 def = tmp[0].trim();
1890 groupName = tmp[1].trim();
1894 const parts = def.split(':').map(s => s.trim());
1896 const fieldInfo = dbxExtractLabel(parts[0]);
1897 const field = fieldInfo.key;
1898 const title = fieldInfo.label;
1900 const gridType = String(parts[1] || '').toLowerCase();
1901 let flag = parts[2] || null;
1902 let optionRaw = parts.slice(3).join(':');
1904 if (flag && flag.indexOf('=') !== -1) {
1905 optionRaw = parts.slice(2).join(':');
1909 const colOptions = dbxGridParseColumnOptions(optionRaw);
1911 if (!field || field.startsWith('_') || flag === '!v') return;
1913 const visState = uiGet('COLUMNS.VISIBLE.' + field, '1');
1918 visible: (visState !== '0'),
1919 headerSort: useTabulatorSort,
1920 headerSortStartingDir: opt.headerSortStart || 'asc',
1921 headerSortTristate: useTabulatorSort ? !!opt.headerSortTristate : false,
1922 sorter: colOptions.sorter || (gridType === 'number' ? 'number' : (gridType === 'date' ? 'date' : 'string')),
1923 headerFilter: opt.headerFilter ? 'input' : false,
1924 headerFilterLiveFilter: !!opt.headerFilterLiveFilter,
1925 headerFilterPlaceholder: opt.headerFilterPlaceholder || undefined,
1926 editor: (opt.allowEdit && flag !== 'p') ? 'input' : false,
1927 editable: (opt.allowEdit && flag !== 'p'),
1930 formatter: (cell) => {
1932 const value = cell.getValue();
1934 if (gridType === 'image') {
1935 if (!value) return '';
1936 const img = document.createElement('img');
1937 img.src = String(value);
1939 img.loading = 'lazy';
1940 img.style.width = colOptions.imgWidth || '38px';
1941 img.style.height = colOptions.imgHeight || '38px';
1942 img.style.objectFit = 'cover';
1943 img.style.borderRadius = colOptions.radius || '50%';
1944 img.style.display = 'block';
1945 img.style.margin = '0 auto';
1949 if (colOptions.formatter === 'truncate' || colOptions.truncate === '1') {
1950 const text = (value === null || value === undefined) ? '' : String(value);
1951 const maxChars = parseInt(colOptions.maxChars || colOptions.maxchars || 180, 10) || 180;
1952 const shortText = text.length > maxChars ? text.substring(0, maxChars) + '...' : text;
1953 const div = document.createElement('div');
1954 div.className = 'dbx-grid-cell-truncate';
1955 div.textContent = shortText.replace(/\s+/g, ' ').trim();
1960 const table = cell.getTable();
1961 const schema = table?.element?._dbxSchemaParsed;
1962 if (!schema || !schema.columns) return value;
1964 const field = cell.getField();
1965 const colSchema = schema.columns[field];
1966 if (!colSchema) return value;
1968 const rowData = cell.getRow().getData();
1969 const style = dbxGrid.evalCell(colSchema, value, rowData);
1972 dbxGridApplyCellStyle(cell, style);
1979 if (colOptions.width) col.width = parseInt(colOptions.width, 10) || col.width;
1980 if (colOptions.minWidth) col.minWidth = parseInt(colOptions.minWidth, 10) || col.minWidth;
1981 if (colOptions.maxWidth) col.maxWidth = parseInt(colOptions.maxWidth, 10) || col.maxWidth;
1982 if (colOptions.hozAlign) col.hozAlign = colOptions.hozAlign;
1983 if (colOptions.headerHozAlign) col.headerHozAlign = colOptions.headerHozAlign;
1984 if (colOptions.bigEditor === '1' || colOptions.bigeditor === '1') {
1985 col.cssClass = [col.cssClass, 'dbx-grid-cell-big-editor'].filter(Boolean).join(' ');
1988 if (gridType === 'image') {
1990 col.editable = false;
1991 col.headerFilter = false;
1992 col.headerSort = false;
1995 if (opt.allowEdit && flag !== 'p' && colOptions.editor) {
1996 if (colOptions.editor === 'list' || colOptions.editor === 'select') {
1997 const lookupValues = dbxGridParseEditorValues(colOptions.values || '');
1998 col.editor = 'list';
1999 col.editorParams = {
2000 values: lookupValues
2002 const baseFormatter = col.formatter;
2003 col.formatter = (cell) => {
2004 const value = cell.getValue();
2005 const key = (value === null || value === undefined) ? '' : String(value);
2006 const display = Object.prototype.hasOwnProperty.call(lookupValues, key)
2010 const table = cell.getTable();
2011 const schema = table?.element?._dbxSchemaParsed;
2012 if (schema && schema.columns) {
2013 const field = cell.getField();
2014 const colSchema = schema.columns[field];
2016 const rowData = cell.getRow().getData();
2017 const style = dbxGrid.evalCell(colSchema, value, rowData);
2019 dbxGridApplyCellStyle(cell, style);
2024 if (!Object.prototype.hasOwnProperty.call(lookupValues, key) && typeof baseFormatter === 'function') {
2025 return baseFormatter(cell);
2030 } else if (colOptions.editor === 'textarea') {
2031 col.editor = 'textarea';
2032 } else if (colOptions.editor === 'input') {
2033 col.editor = 'input';
2037 if (useDedicatedServerSort) {
2038 col.headerClick = (e, column) => {
2040 const table = column.getTable();
2041 const field = column.getField();
2042 if (!field || field.startsWith('_')) return;
2044 const current = table._dbxServerSort || null;
2045 const startDir = opt.headerSortStart || 'asc';
2046 const otherDir = (startDir === 'asc') ? 'desc' : 'asc';
2048 let nextSort = null;
2050 if (!current || current.field !== field) {
2051 nextSort = { field: field, dir: startDir };
2052 } else if (current.dir === startDir) {
2053 nextSort = { field: field, dir: otherDir };
2054 } else if (current.dir === otherDir && opt.headerSortTristate === true) {
2057 nextSort = { field: field, dir: startDir };
2060 table._dbxServerSort = nextSort;
2062 table.element._dbxFeature._applySortIndicators(table);
2065 dbx.log('[grid] server sort cleared', {
2070 table.setData(table.element._dbxFeature._dbxAjaxUrl(opt.urls.read));
2075 table.element._dbxFeature._dbxAjaxUrl(
2077 '&field=' + encodeURIComponent(nextSort.field) +
2078 '&dir=' + encodeURIComponent(nextSort.dir)
2081 dbx.log('[grid] server sort click', {
2083 field: nextSort.field,
2092 colMap[field] = col;
2095 if (!groups[groupName]) groups[groupName] = [];
2096 groups[groupName].push(col);
2098 ungrouped.push(col);
2102 if (orderList && !hasGroups) {
2107 orderList.forEach(f => {
2109 ordered.push(colMap[f]);
2114 Object.keys(colMap).forEach(f => {
2116 ordered.push(colMap[f]);
2121 ordered.unshift(actionsCol);
2132 cols.unshift(actionsCol);
2135 if (ungrouped.length) {
2136 ungrouped.forEach(col => cols.push(col));
2139 Object.keys(groups).forEach(groupName => {
2141 if (idx > 0 || ungrouped.length > 0) {
2144 field: `_sep_${idx}`,
2149 headerFilter: false,
2151 cssClass: 'dbx-col-separator',
2152 formatter: () => '',
2160 columns: groups[groupName]
2170 cols.push(actionsCol);
2173 return cols.concat(ungrouped);
2177 /* =========================================================
2179 * ========================================================= */
2180 updateSaveButton(el, table) {
2182 const btn = this._findSaveButton(el);
2185 const isDirty = this._syncDirtyState(table);
2187 if (btn._dbxDirtyState === isDirty) return;
2189 btn._dbxDirtyState = isDirty;
2192 btn.classList.remove('btn-outline-primary');
2193 btn.classList.add('btn-primary');
2195 btn.classList.remove('btn-primary');
2196 btn.classList.add('btn-outline-primary');
2201 /* =========================================================
2203 * ========================================================= */
2204 bindLayoutState(el, table) {
2206 let saveTimeout = null;
2207 let lastResizedField = null;
2209 const gridId = el.id || 'grid';
2210 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2212 const getLeafColumns = () => this._getLeafColumns(table);
2214 function saveLayout(type) {
2216 if (saveTimeout) clearTimeout(saveTimeout);
2218 saveTimeout = setTimeout(() => {
2220 if (type === 'order') {
2221 const cols = getLeafColumns();
2222 const order = cols.map(c => c.getField()).join('|');
2223 uiSet('COLUMNS.ORDER', order);
2227 if (type === 'width') {
2228 if (!lastResizedField) return;
2230 const col = table.getColumn(lastResizedField);
2233 const w = col.getWidth();
2235 if (typeof w === 'number' && w > 0) {
2236 uiSet('COLUMNS.SIZE.' + lastResizedField, String(w));
2244 table.on('columnResized', col => {
2245 const f = col.getField();
2246 if (!f || f.startsWith('_')) return;
2248 lastResizedField = f;
2249 saveLayout('width');
2252 table.on('columnMoved', col => {
2253 const f = col.getField();
2254 if (!f || f.startsWith('_')) return;
2255 saveLayout('order');
2258 table.on('columnVisibilityChanged', (col, visible) => {
2259 const f = col.getField();
2260 if (!f || f.startsWith('_')) return;
2261 uiSet('COLUMNS.VISIBLE.' + f, visible ? '1' : '0');
2264 table.on('pageSizeChanged', (pageSize) => {
2265 if (table._dbxPageSizeChanging === true) {
2266 table._dbxPageSizeState = this._normalizePageSizeValue(
2269 opt.paginationSizeSelector
2273 if (table._dbxIsRemotePagination === true) {
2277 dbx.warn('[grid] setPage failed after pageSizeChanged', e);
2279 table.replaceData();
2285 /* =========================================================
2287 * ========================================================= */
2288 bindToolbar(el, table, opt, uiState, root) {
2290 const gridId = el.id || 'grid';
2292 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
2293 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2295 el._dbxApplyGridLines = function(force) {
2297 const on = uiGet('GRIDLINES', '1') == '1';
2298 const tabRoot = table.element;
2299 if (!tabRoot) return;
2302 tabRoot.classList.add('dbx-grid-lines');
2304 tabRoot.classList.remove('dbx-grid-lines');
2308 uiState.gridLines = uiGet('GRIDLINES', '1') == '1';
2309 uiState.autosave = uiGet('AUTOSAVE', '1') == '1';
2311 const heightStored = uiGet('HEIGHT', null);
2313 const autosave = root ? root.querySelector('[data-dbx="grid-autosave"]') : null;
2314 const gridLinesCb = root ? root.querySelector('[data-dbx="grid-lines"]') : null;
2315 const saveBtn = root ? root.querySelector('[data-dbx="grid-save"]') : null;
2316 const insertBtn = root ? root.querySelector('[data-dbx="grid-insert"]') : null;
2317 const reloadBtn = root ? root.querySelector('[data-dbx="grid-reload"]') : null;
2318 const resetBtn = root ? root.querySelector('[data-dbx="grid-reset"]') : null;
2319 const colBtn = root ? root.querySelector('[data-dbx="grid-columns"]') : null;
2320 const excelBtn = root ? root.querySelector('[data-dbx="grid-export-excel"]') : null;
2321 const pdfBtn = root ? root.querySelector('[data-dbx="grid-export-pdf"]') : null;
2322 const heightSlider = root ? root.querySelector('[data-dbx="grid-height"]') : null;
2323 const searchInput = root ? root.querySelector('[data-dbx="grid-search"]') : null;
2325 this._setRoleVisible(root, 'search', opt.showSearch === true);
2326 this._setRoleVisible(root, 'autosave', opt.showAutosave === true && opt.allowEdit === true);
2327 this._setRoleVisible(root, 'gridlines', opt.showGridLines === true);
2328 this._setRoleVisible(root, 'height', opt.showHeight === true);
2329 this._setRoleVisible(root, 'reload', opt.showReload === true);
2330 this._setRoleVisible(root, 'reset', opt.showReset === true);
2331 this._setRoleVisible(root, 'save', opt.showSave === true && opt.allowEdit === true);
2332 this._setRoleVisible(root, 'insert', opt.showInsert === true && opt.allowInsert === true);
2333 this._setRoleVisible(root, 'columns', opt.showColumns === true);
2334 this._setRoleVisible(root, 'syncstatus', opt.showSyncStatus === true && opt.syncLed !== false);
2335 this._setRoleVisible(root, 'export-excel', opt.showExportExcel === true);
2336 this._setRoleVisible(root, 'export-pdf', opt.showExportPdf === true);
2338 this._setRoleVisible(
2341 opt.pagination === true && (
2342 (opt.paginationControls === true) ||
2343 (opt.paginationCounter !== false)
2347 this._setRoleVisible(
2349 'pagination-controls',
2350 opt.pagination === true && opt.paginationControls === true
2353 this._setRoleVisible(
2355 'pagination-counter',
2356 opt.pagination === true && opt.paginationCounter !== false
2360 searchInput.placeholder = opt.searchPlaceholder || '🔍';
2361 if (opt.searchWidth > 0) {
2362 searchInput.style.width = opt.searchWidth + 'px';
2367 autosave.checked = uiState.autosave;
2368 autosave.addEventListener('change', () => {
2369 uiSet('AUTOSAVE', autosave.checked ? '1' : '0');
2374 gridLinesCb.checked = uiState.gridLines;
2375 gridLinesCb.addEventListener('change', () => {
2376 uiSet('GRIDLINES', gridLinesCb.checked ? '1' : '0');
2377 el._dbxApplyGridLines(false);
2383 const heightMin = Math.max(120, parseInt(opt.heightMin, 10) || 320);
2384 const heightMax = Math.max(heightMin, parseInt(opt.heightMax, 10) || 960);
2385 const heightStep = Math.max(10, parseInt(opt.heightStep, 10) || 40);
2387 heightSlider.min = String(heightMin);
2388 heightSlider.max = String(heightMax);
2389 heightSlider.step = String(heightStep);
2391 if (heightStored !== null) {
2392 heightSlider.value = heightStored;
2395 const sliderHeight = parseInt(heightSlider.value, 10);
2396 if (!isNaN(sliderHeight)) {
2397 heightSlider.value = String(Math.min(heightMax, Math.max(heightMin, sliderHeight)));
2400 heightSlider.addEventListener('input', () => {
2401 const h = parseInt(heightSlider.value, 10);
2402 if (isNaN(h)) return;
2404 uiSet('HEIGHT', String(h));
2410 saveBtn.addEventListener('click', () => {
2411 if (table._dbxDirty === true) {
2412 this.saveTable(table, opt);
2418 insertBtn.addEventListener('click', () => {
2419 this.insertRow(table, opt);
2424 reloadBtn.addEventListener('click', () => {
2425 if (table._dbxSaving === true) return;
2426 this.reloadTable(table, opt);
2431 resetBtn.addEventListener('click', () => {
2442 keys.forEach(k => uiSet(k, null));
2444 const cols = table.getColumns();
2447 (function walk(cols){
2449 const f = c.getField && c.getField();
2450 if (f && !f.startsWith('_')) fields.push(f);
2451 if (c.getSubColumns) {
2452 const sub = c.getSubColumns();
2453 if (sub && sub.length) walk(sub);
2458 fields.forEach(f => {
2459 uiSet('COLUMNS.SIZE.' + f, null);
2460 uiSet('COLUMNS.VISIBLE.' + f, null);
2463 uiSet('GRIDLINES', '1');
2464 uiSet('AUTOSAVE', '1');
2466 if (window.dbx && dbx.utilities && dbx.utilities.leaveGuard) {
2467 dbx.utilities.leaveGuard.allowOnce();
2474 colBtn.addEventListener('click', () => {
2475 this.openColumnChooser(colBtn, table);
2480 excelBtn.addEventListener('click', () => {
2481 this._ensureExcelExportDeps(table, (ok) => {
2483 dbx.error('[grid] excel export dependencies missing');
2488 table.download('xlsx', (opt.exportFileName || gridId) + '.xlsx', {
2489 sheetName: opt.exportSheetName || gridId
2492 dbx.error('[grid] excel export failed', e);
2499 pdfBtn.addEventListener('click', () => {
2500 this._ensurePdfExportDeps(table, (ok) => {
2502 dbx.error('[grid] pdf export dependencies missing');
2507 table.download('pdf', (opt.exportFileName || gridId) + '.pdf', {
2508 orientation: opt.pdfOrientation || 'landscape',
2509 title: opt.pdfTitle || document.title || 'Export'
2512 dbx.error('[grid] pdf export failed', e);
2520 if (!table._dbxGlobalSearchFilter) {
2521 table._dbxGlobalSearchFilter = function (data, filterParams) {
2522 const val = String((filterParams && filterParams.value) || '').toLowerCase();
2526 const fields = (filterParams && filterParams.fields) || [];
2527 for (let i = 0; i < fields.length; i++) {
2528 const v = data[fields[i]];
2529 if (v != null && String(v).toLowerCase().indexOf(val) !== -1) {
2537 const getFields = () => {
2539 (function walk(cols){
2541 const f = c.getField && c.getField();
2542 if (f && !f.startsWith('_')) out.push(f);
2543 if (c.getSubColumns) {
2544 const sub = c.getSubColumns();
2545 if (sub && sub.length) walk(sub);
2548 })(table.getColumns());
2554 const applyLocalSearch = () => {
2555 const val = searchInput.value.trim();
2557 if (opt.searchMode === 'remote') {
2558 table._dbxSearchValue = val.toLowerCase();
2559 this.reloadTable(table, opt, { resetPage: true });
2564 table.clearFilter();
2568 table.setFilter(table._dbxGlobalSearchFilter, {
2574 searchInput.addEventListener('input', () => {
2576 if (timer) clearTimeout(timer);
2578 if (opt.searchMode === 'remote') {
2579 timer = setTimeout(applyLocalSearch, 250);
2586 table.on('dataLoaded', () => {
2587 if (opt.searchMode === 'remote') {
2590 if (!searchInput.value.trim()) {
2597 table.on('tableBuilt', () => {
2598 table._dbxBuilt = true;
2599 if (table._dbxPageSizeState && table.getPageSize && table.getPageSize() !== table._dbxPageSizeState) {
2601 table._dbxPageSizeChanging = true;
2602 table.setPageSize(table._dbxPageSizeState);
2604 dbx.warn('[grid] restore page size failed', e);
2606 table._dbxPageSizeChanging = false;
2609 el._dbxApplyGridLines(false);
2610 this._applySortIndicators(table);
2614 /* =========================================================
2616 * ========================================================= */
2617 openColumnChooser(btn, table) {
2619 const el = table.element;
2620 const gridId = el.id || 'grid';
2622 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
2623 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2625 const old = document.querySelector(`.dbx-col-chooser[data-grid-id="${gridId}"]`);
2626 if (old) old.remove();
2628 const box = document.createElement('div');
2629 box.className = 'dbx-col-chooser shadow p-2 bg-white border rounded';
2630 box.dataset.gridId = gridId;
2632 box.style.position = 'fixed';
2633 box.style.zIndex = 9999;
2634 box.style.minWidth = '260px';
2635 box.style.maxHeight = '70vh';
2636 box.style.overflowY = 'auto';
2638 const rect = btn.getBoundingClientRect();
2639 box.style.left = rect.left + 'px';
2640 box.style.top = (rect.bottom + 4) + 'px';
2642 const groupMap = {};
2645 this._getLeafColumns(table).forEach(col => {
2647 const field = col.getField();
2648 if (!field || field.startsWith('_')) return;
2652 let groupTitle = '-';
2653 const parent = col.getParentColumn();
2655 const def = parent.getDefinition();
2656 if (def?.title?.trim()) groupTitle = def.title.trim();
2659 if (!groupMap[groupTitle]) groupMap[groupTitle] = [];
2660 groupMap[groupTitle].push(col);
2663 const queueWidthRestore = () => {
2664 this._queueTableTimer(table, '_dbxChooserTimer', () => {
2665 if (!this._isTableLayoutReady(table)) return;
2666 this._restoreStoredColumnWidths(table, gridId);
2670 const saveVisibility = () => {
2671 allCols.forEach(c => {
2672 const field = c.getField();
2673 uiSet('COLUMNS.VISIBLE.' + field, c.isVisible() ? '1' : '0');
2677 Object.keys(groupMap).forEach(groupTitle => {
2679 const cols = groupMap[groupTitle];
2681 const groupLabel = document.createElement('label');
2682 groupLabel.className = 'fw-bold d-flex align-items-center gap-2 mb-1';
2684 const groupCb = document.createElement('input');
2685 groupCb.type = 'checkbox';
2687 const updateGroupState = () => {
2688 const visibleCount = cols.filter(c => c.isVisible()).length;
2689 groupCb.checked = (visibleCount === cols.length);
2690 groupCb.indeterminate = (visibleCount > 0 && visibleCount < cols.length);
2695 groupCb.addEventListener('change', () => {
2697 if (!this._isTableLayoutReady(table)) return;
2699 table.blockRedraw();
2702 const f = c.getField();
2703 if (!f || f.startsWith('_')) return;
2706 ? table.showColumn(f)
2707 : table.hideColumn(f);
2710 table.restoreRedraw(true);
2714 queueWidthRestore();
2717 groupLabel.appendChild(groupCb);
2718 groupLabel.appendChild(document.createTextNode(groupTitle));
2719 box.appendChild(groupLabel);
2721 cols.forEach(col => {
2723 const field = col.getField();
2724 const def = col.getDefinition();
2725 const labelText = def?.title ? def.title : field;
2727 const label = document.createElement('label');
2728 label.className = 'd-flex align-items-center gap-2 small ms-3';
2730 const cb = document.createElement('input');
2731 cb.type = 'checkbox';
2732 cb.checked = col.isVisible();
2734 cb.addEventListener('change', () => {
2736 if (!this._isTableLayoutReady(table)) return;
2738 table.blockRedraw();
2741 ? table.showColumn(field)
2742 : table.hideColumn(field);
2744 table.restoreRedraw(true);
2748 queueWidthRestore();
2751 label.appendChild(cb);
2752 label.appendChild(document.createTextNode(labelText));
2753 box.appendChild(label);
2756 box.appendChild(document.createElement('hr'));
2759 document.body.appendChild(box);
2762 const close = (e) => {
2763 if (!box.contains(e.target) && e.target !== btn) {
2765 document.removeEventListener('click', close);
2768 document.addEventListener('click', close);
2773 /* =========================================================
2774 * PARAMS / REMOTE STATE
2775 * ========================================================= */
2776 buildAjaxParams(table, params, optFallback) {
2778 const out = Object.assign({}, params || {});
2779 const gridEl = table && table.element ? table.element : null;
2780 const opt = (gridEl && gridEl._dbxOpt) ? gridEl._dbxOpt : (optFallback || {});
2781 const gridId = (gridEl && gridEl.id) ? gridEl.id : (opt._gridId || 'grid');
2782 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2788 if (table && table._dbxSearchValue) {
2789 out.dbx_search = table._dbxSearchValue;
2792 const isRemote = table && table._dbxIsRemotePagination === true;
2795 const page = parseInt(out.page, 10) || (table.getPage ? table.getPage() : 1) || 1;
2796 const rawSize = out.size ?? (table.getPageSize ? table.getPageSize() : null) ?? opt.pageSize ?? 50;
2797 const normalizedSize = this._normalizePageSizeValue(rawSize, opt.pageSize || 15, opt.paginationSizeSelector);
2798 const size = normalizedSize;
2803 uiSet('PAGE.NO', page);
2804 this._storePageSizeState(gridId, normalizedSize, opt.pageSize || 15, opt.paginationSizeSelector);
2807 if (!table || typeof table.getHeaderFilters !== 'function') {
2811 const sorters = this._getAjaxSorters(table);
2812 if (Array.isArray(sorters) && sorters.length) {
2813 out.dbx_sorters = JSON.stringify(sorters);
2816 const headerFilters = table.getHeaderFilters ? table.getHeaderFilters() : [];
2817 if (Array.isArray(headerFilters) && headerFilters.length) {
2818 out.dbx_filters = JSON.stringify(headerFilters);
2825 /* =========================================================
2827 * ========================================================= */
2828 reloadTable(table, opt, cfg = {}) {
2830 const resetPage = !!cfg.resetPage;
2832 if (table._dbxSaving === true) return;
2834 if (table._dbxIsRemotePagination) {
2839 dbx.warn('[grid] remote reset page failed', e);
2842 table.replaceData();
2846 if (table._dbxIsProgressive) {
2847 table.setData(this._dbxAjaxUrl(opt.urls.read));
2851 table.replaceData(this._dbxAjaxUrl(opt.urls.read));
2854 insertRow(table, opt) {
2856 if (!table || !opt || !opt.urls || !opt.urls.insert) return;
2857 if (table._dbxSaving === true) return;
2859 const url = this._dbxAjaxUrl(opt.urls.insert);
2860 table._dbxSaving = true;
2862 this._dbxRequest(url, {
2865 'Content-Type': 'application/json'
2867 body: JSON.stringify({}),
2868 responseType: 'json'
2871 table._dbxSaving = false;
2873 if (!resp || !(resp.ok || resp.success)) {
2874 dbx.error('[grid] insert failed', resp);
2878 const row = resp.row || (Array.isArray(resp.rows) ? resp.rows[0] : null);
2880 table.addData([row], false);
2882 this.reloadTable(table, opt);
2886 table._dbxSaving = false;
2887 dbx.error('[grid] insert error', err);
2892 /* =========================================================
2894 * ========================================================= */
2895 bindSyncLoop(el, table, opt) {
2897 const syncUrl = opt.urls.sync;
2898 if (!syncUrl) return;
2900 const syncEls = this._getSyncEls(el);
2903 syncEls.led._dbxSyncLedEnabled = (opt.syncLed !== false);
2905 if (opt.syncLed === false) {
2906 syncEls.led.style.display = 'none';
2908 syncEls.led.style.display = 'inline-block';
2912 if (opt.syncRun === false) {
2913 dbx.log('[grid][sync] disabled by sync_run=0', {
2919 let synctime = parseFloat(opt.cfg.synctime || 2);
2920 if (isNaN(synctime)) synctime = 2;
2922 if (synctime === 0) return;
2923 if (synctime < 0.5) synctime = 0.5;
2924 if (synctime > 60) synctime = 60;
2926 const interval = Math.round(synctime * 1000);
2928 const loopId = 'grid-sync-' + (el.id || 'grid') + '-' + Date.now();
2929 table._dbxLoopId = loopId;
2930 table._dbxSyncRunning = false;
2931 table._dbxSyncMode = opt.syncMode || 'delta';
2933 dbx.log('[grid][sync] bind', {
2934 id: el.id || 'grid',
2937 mode: table._dbxSyncMode,
2938 remotePagination: table._dbxIsRemotePagination === true
2945 idle: Math.max(interval * 2, interval + 1000),
2946 hidden: Math.max(interval * 3, interval + 2000),
2952 if (table._dbxSyncRunning === true) return;
2953 if (table._dbxSaving === true) return;
2954 if (!table._dbxServerTime) return;
2955 if (!dbx.device.isVisible()) return;
2956 if (!this._isTableAlive(table)) return;
2958 table._dbxSyncRunning = true;
2960 const startedAt = Date.now();
2962 dbx.log('[grid][sync] request start', {
2963 id: el.id || 'grid',
2964 last_update: table._dbxServerTime,
2965 remotePagination: table._dbxIsRemotePagination === true,
2966 page: table._dbxIsRemotePagination ? (table.getPage() || 1) : null,
2967 size: table._dbxIsRemotePagination ? (table.getPageSize() || opt.pageSize || 50) : null
2970 let loadingTimer = setTimeout(() => {
2971 if (table._dbxSyncRunning === true) {
2972 dbx.log('[grid][sync] loader threshold reached', {
2975 this._setLedState(syncEls.led, 'loading');
2979 const editedMap = this._collectEditedMap(table);
2981 let url = this._dbxAjaxUrl(syncUrl, { background: true }) +
2982 '&last_update=' + encodeURIComponent(table._dbxServerTime);
2984 if (table._dbxIsRemotePagination) {
2985 url += '&dbx_page=' + encodeURIComponent(table.getPage() || 1);
2986 url += '&dbx_size=' + encodeURIComponent(table.getPageSize() || opt.pageSize || 50);
2989 return this._dbxRequest(url, {
2991 responseType: 'json'
2995 const rows = Array.isArray(res?.rows) ? res.rows : [];
2997 dbx.log('[grid][sync] response', {
2998 id: el.id || 'grid',
3001 count: (typeof res?.count !== 'undefined') ? res.count : null
3004 if (!res || res.ok !== 1) {
3005 this._setLedState(syncEls.led, 'idle');
3009 if (typeof res.server_time !== 'undefined' && res.server_time) {
3010 table._dbxServerTime = res.server_time;
3013 if (typeof res.count !== 'undefined') {
3014 this._setSyncCount(syncEls.count, res.count || '');
3018 this._setLedState(syncEls.led, 'idle');
3022 if (table._dbxIsRemotePagination) {
3024 dbx.log('[grid][sync] remote reload triggered', {
3025 id: el.id || 'grid',
3029 this.reloadTable(table, opt, {
3030 reason: 'sync-remote-delta'
3033 this._setLedState(syncEls.led, 'ok');
3039 for (let i = 0; i < rows.length; i++) {
3042 if (!r || typeof r.id === 'undefined') continue;
3044 const row = table.getRow(r.id);
3047 table.addData([r], false);
3052 const data = row.getData();
3053 const editedFields = editedMap[r.id] || null;
3056 for (const k in r) {
3058 if (k === 'id') continue;
3060 const newVal = r[k];
3061 const oldVal = data[k];
3064 newVal === oldVal ||
3065 String(newVal) === String(oldVal)
3070 if (editedFields && editedFields[k] === true) {
3072 const cell = row.getCell(k);
3073 if (cell && newVal !== oldVal) {
3074 const cellEl = cell.getElement();
3076 cellEl.classList.add('dbx-cell-conflict');
3086 const keys = Object.keys(patch);
3087 if (!keys.length) continue;
3093 const cell = row.getCell(k);
3095 this._applySchemaCellStyle(cell, row.getData());
3100 dbx.log('[grid][sync] local apply done', {
3101 id: el.id || 'grid',
3102 incoming: rows.length,
3106 this._setLedState(syncEls.led, changed > 0 ? 'ok' : 'idle');
3109 dbx.error('[grid][sync] error', err);
3110 this._setLedState(syncEls.led, 'error');
3113 clearTimeout(loadingTimer);
3114 loadingTimer = null;
3115 table._dbxSyncRunning = false;
3117 dbx.log('[grid][sync] request end', {
3118 id: el.id || 'grid',
3119 duration_ms: (Date.now() - startedAt)
3127 /* =========================================================
3129 * ========================================================= */
3130 createTable(el, opt) {
3132 if (el._dbxGridInitialized) return;
3133 el._dbxGridInitialized = true;
3135 const gridId = el.id || 'grid';
3136 opt._gridId = gridId;
3138 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
3139 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
3142 opt.cfg && typeof opt.cfg.schema === 'string'
3143 ? opt.cfg.schema.trim()
3146 const buildGrid = () => {
3148 const isRemotePagination =
3149 opt.pagination === true &&
3150 (opt.paginationMode === 'remote');
3152 const isProgressive =
3153 (opt.progressiveLoad === 'scroll' || opt.progressiveLoad === 'load');
3155 opt._dbxIsRemotePagination = isRemotePagination;
3156 opt._dbxIsProgressive = isProgressive;
3158 const columns = this.buildColumns(opt);
3160 const pageSizeStored = this._getPageSizeState(
3163 opt.paginationSizeSelector
3165 const pageSizeInitial = pageSizeStored === 1 ? 15 : pageSizeStored;
3166 opt.pageSize = pageSizeInitial;
3167 const pageNoStored = this._int(uiGet('PAGE.NO', 1), 1);
3169 const heightStoredRaw = opt.height === false ? null : uiGet('HEIGHT', null);
3170 const heightStoredInt = heightStoredRaw !== null ? parseInt(heightStoredRaw, 10) : NaN;
3171 const initialHeightRaw = opt.height === false ? false : (!isNaN(heightStoredInt) ? heightStoredInt : opt.height);
3172 const heightMinBound = Math.max(120, parseInt(opt.heightMin, 10) || 320);
3173 const heightMaxBound = Math.max(heightMinBound, parseInt(opt.heightMax, 10) || 960);
3174 const initialHeight = initialHeightRaw === false
3176 : Math.min(heightMaxBound, Math.max(heightMinBound, initialHeightRaw));
3178 const paginationUi = this._getPaginationUiEls(el);
3180 dbx.log('[grid] createTable', {
3182 remotePagination: isRemotePagination,
3183 progressive: isProgressive,
3184 pageSizeStored: pageSizeStored,
3185 pageNoStored: pageNoStored,
3186 initialHeight: initialHeight,
3187 searchMode: opt.searchMode,
3188 syncRun: opt.syncRun,
3189 syncLed: opt.syncLed,
3190 dedicatedServerSort: !!(opt.headerSort === true && opt.urls.sort && !isRemotePagination),
3191 paginationCounter: opt.paginationCounter,
3192 paginationSizeSelector: opt.paginationSizeSelector
3197 const ajaxURLGenerator = (url, config, params) => {
3199 let finalUrl = this._dbxAjaxUrl(url);
3200 const merged = this.buildAjaxParams(table, params || {}, opt);
3202 const usp = new URLSearchParams();
3204 Object.keys(merged).forEach(key => {
3205 const val = merged[key];
3206 if (val === undefined || val === null || val === '') return;
3207 usp.append(key, val);
3210 if (String(finalUrl).includes('?')) {
3211 finalUrl += '&' + usp.toString();
3213 finalUrl += '?' + usp.toString();
3216 dbx.log('[grid][ajaxURL]', {
3225 const ajaxRequestFunc = (url, config, params) => {
3228 (typeof config === 'string')
3230 : ((config && config.method) ? config.method : 'GET');
3232 dbx.log('[grid][ajaxRequestFunc]', {
3236 params: params || {}
3239 return this._dbxRequest(url, {
3241 responseType: 'json'
3245 const tabulatorOptions = {
3247 height: initialHeight,
3248 minHeight: opt.minHeight || false,
3249 maxHeight: opt.maxHeight || false,
3250 layout: (opt.cfg && opt.cfg.layout) ? opt.cfg.layout : 'fitColumns',
3251 responsiveLayout: opt.responsiveLayout || false,
3252 placeholder: String(opt.cfg.placeholder ?? ''),
3256 sortMode: isRemotePagination ? 'remote' : 'local',
3258 filterMode: opt.searchMode === 'remote' ? 'remote' : 'local',
3260 ajaxURL: this._dbxAjaxUrl(opt.urls.read),
3262 ajaxContentType: 'json',
3263 ajaxURLGenerator: ajaxURLGenerator,
3264 ajaxRequestFunc: ajaxRequestFunc,
3265 ajaxResponse: (url, params, response) => this._ajaxResponse(table, url, params, response),
3267 pagination: opt.pagination === true,
3268 paginationMode: isRemotePagination ? 'remote' : 'local',
3269 paginationSize: pageSizeInitial,
3270 paginationInitialPage: pageNoStored,
3271 paginationAddRow: opt.paginationAddRow || 'page',
3272 paginationButtonCount: opt.paginationButtonCount || 5,
3273 progressiveLoad: isProgressive ? opt.progressiveLoad : false,
3275 index: String(opt.cfg.index || 'id'),
3278 reactiveData: false,
3279 movableColumns: opt.movableColumns !== false,
3280 resizableColumns: opt.resizableColumns !== false,
3282 rowFormatter: function(row) {
3284 const rowEl = row.getElement();
3287 const schema = row.getTable().element._dbxSchemaParsed;
3288 if (!schema || !Array.isArray(schema.rows)) return;
3290 const data = row.getData();
3292 rowEl.style.removeProperty('background-color');
3293 rowEl.style.removeProperty('color');
3295 for (let i = 0; i < schema.rows.length; i++) {
3296 const rule = schema.rows[i];
3297 if (!dbxGrid.evalRule(rule, null, data)) continue;
3299 if (rule.style?.bg) {
3300 rowEl.style.setProperty('background-color', rule.style.bg, 'important');
3302 if (rule.style?.color) {
3303 rowEl.style.setProperty('color', rule.style.color, 'important');
3310 if (opt.pagination === true && opt.paginationControls === true && paginationUi.controls) {
3311 tabulatorOptions.paginationElement = paginationUi.controls;
3314 if (opt.pagination === true && opt.paginationCounter !== false) {
3315 tabulatorOptions.paginationCounter = opt.paginationCounter;
3317 if (paginationUi.counter) {
3318 tabulatorOptions.paginationCounterElement = paginationUi.counter;
3322 if (opt.pagination === true && opt.paginationSizeSelector !== false) {
3323 tabulatorOptions.paginationSizeSelector = opt.paginationSizeSelector;
3326 if (opt.pagination === true && opt.paginationOutOfRange !== false) {
3327 tabulatorOptions.paginationOutOfRange = opt.paginationOutOfRange;
3330 table = new Tabulator(el, tabulatorOptions);
3332 table._dbxIsRemotePagination = isRemotePagination;
3333 table._dbxIsProgressive = isProgressive;
3334 table._dbxSortRestored = false;
3335 table._dbxDirty = false;
3336 table._dbxSaving = false;
3337 table._dbxAutoTimer = null;
3338 table._dbxSearchValue = '';
3339 table._dbxLayoutRestored = false;
3340 table._dbxServerSort = null;
3341 table._dbxPageLayoutTimer = null;
3342 table._dbxBuilt = false;
3343 table._dbxPageSizeState = pageSizeStored;
3345 const syncEls = this._getSyncEls(el);
3348 syncEls.led._dbxSyncLedEnabled = (opt.syncLed !== false);
3350 if (opt.syncLed === false) {
3351 syncEls.led.style.display = 'none';
3353 syncEls.led.style.display = 'inline-block';
3357 const queueLocalPageStabilize = (reason) => {
3359 if (table._dbxIsRemotePagination === true) return;
3360 if (table._dbxLayoutRestored !== true) return;
3362 this._queueTableTimer(table, '_dbxPageLayoutTimer', () => {
3364 if (!this._isTableLayoutReady(table) || table._dbxBuilt !== true) {
3365 this._queueTableTimer(table, '_dbxPageLayoutTimer', () => {
3366 queueLocalPageStabilize(reason);
3371 dbx.log('[grid] local page stabilize start', {
3374 page: table.getPage ? table.getPage() : null
3380 dbx.warn('[grid] local page redraw failed', e);
3383 this._applySortIndicators(table);
3385 dbx.log('[grid] local page stabilize done', {
3388 page: table.getPage ? table.getPage() : null
3394 table.on('pageLoaded', (pageno) => {
3395 uiSet('PAGE.NO', pageno);
3396 this._storePageSizeState(
3398 table._dbxPageSizeState || (table.getPageSize ? table.getPageSize() : opt.pageSize),
3400 opt.paginationSizeSelector
3403 dbx.log('[grid] pageLoaded', {
3406 pageSize: table.getPageSize()
3409 this._applyPaginationButtonLabels(table);
3411 if (table._dbxIsRemotePagination !== true) {
3412 queueLocalPageStabilize('pageLoaded');
3416 table.on('cellEdited', (cell) => {
3418 if (opt.allowEdit === false) return;
3420 this._markTableDirty(table, el);
3422 dbx.log('[grid] cellEdited', {
3424 rowId: cell?.getRow?.()?.getData?.()?.id,
3425 field: cell?.getField?.()
3428 const autosave = uiGet('AUTOSAVE', '1') == '1';
3429 if (!autosave) return;
3431 if (table._dbxAutoTimer) {
3432 clearTimeout(table._dbxAutoTimer);
3435 table._dbxAutoTimer = setTimeout(() => {
3437 if (table._dbxSaving === true) return;
3438 if (this._syncDirtyState(table) !== true) return;
3440 dbx.log('[grid] autosave trigger', {
3444 this.saveTable(table, opt);
3449 table.on('renderComplete', () => {
3450 if (opt.allowEdit !== false) {
3451 this.updateSaveButton(el, table);
3455 table.on('dataLoaded', (data) => {
3457 const syncEls = this._getSyncEls(el);
3459 if (typeof table._dbxSyncCount !== 'undefined') {
3460 this._setSyncCount(syncEls.count, table._dbxSyncCount || '');
3463 if (table._dbxPageSizeState === 1 && table._dbxPageSizeOneApplied !== true) {
3464 table._dbxPageSizeOneApplied = true;
3467 table._dbxPageSizeChanging = true;
3468 table.setPageSize(1);
3469 if (typeof table.setPage === 'function') {
3474 dbx.warn('[grid] restore page size 1 failed', e);
3476 table._dbxPageSizeChanging = false;
3480 this._applySortIndicators(table);
3481 this._applyPaginationButtonLabels(table);
3483 dbx.log('[grid] dataLoaded', {
3485 rows: Array.isArray(data) ? data.length : null,
3486 sortRestored: table._dbxSortRestored === true,
3487 remotePagination: table._dbxIsRemotePagination === true,
3488 progressive: table._dbxIsProgressive === true
3491 if (table._dbxIsRemotePagination !== true) {
3492 queueLocalPageStabilize('dataLoaded');
3495 if (!table._dbxSortRestored) {
3496 table._dbxSortRestored = true;
3497 this.bindSyncLoop(el, table, opt);
3501 table.on('dataSorted', () => {
3502 this._applySortIndicators(table);
3505 table.on('renderComplete', () => {
3506 this._applySortIndicators(table);
3507 this._applyPaginationButtonLabels(table);
3510 table.on('columnsLoaded', () => {
3512 if (table._dbxLayoutRestored === true) {
3513 dbx.log('[grid] columnsLoaded skipped (already restored)', {
3519 dbx.log('[grid] columnsLoaded -> restore layout start', {
3523 const tryRestore = () => {
3525 if (table._dbxLayoutRestored === true) return;
3527 const applied = this._applyInitialLayoutState(table, gridId);
3529 if (applied !== true) {
3530 this._queueTableTimer(table, '_dbxLayoutTimer', tryRestore, 30);
3534 table._dbxLayoutRestored = true;
3535 this._applySortIndicators(table);
3537 dbx.log('[grid] columnsLoaded -> restore layout done', {
3542 this._queueTableTimer(table, '_dbxLayoutTimer', tryRestore, 0);
3545 this.bindLayoutState(el, table);
3547 el._dbxTable = table;
3548 el._dbxFeature = this;
3555 opt._uiState || (opt._uiState = {}),
3556 el.closest('.dbx-grid')
3559 this.updateSaveButton(el, table);
3563 this.loadSchema(schemaName, () => {
3564 el._dbxSchemaParsed = dbxGridParseSchema(window.dbxGridSchema[schemaName]);
3572 /* =========================================================
3574 * ========================================================= */
3575 saveTable(table, opt) {
3577 if (!opt || !opt.urls || !opt.urls.save) {
3578 table._dbxSaving = false;
3579 dbx.error('[grid] saveTable → missing save URL', {
3580 id: table.element?.id || 'undef'
3585 if (table._dbxSaving === true) {
3589 if (table._dbxDirty !== true && this._syncDirtyState(table) !== true) {
3590 table._dbxSaving = false;
3594 const editedCells = table.getEditedCells();
3596 if (!editedCells || editedCells.length === 0) {
3598 table._dbxSaving = false;
3599 table._dbxDirty = false;
3601 if (table.element && table.element._dbxFeature) {
3602 table.element._dbxFeature.updateSaveButton(table.element, table);
3610 editedCells.forEach(cell => {
3612 const row = cell.getRow();
3615 const data = row.getData();
3616 const idField = this._rowIdField(table);
3617 if (!data || typeof data[idField] === 'undefined') return;
3619 if (!rowsMap[data[idField]]) {
3620 rowsMap[data[idField]] = Object.assign({}, data);
3624 const rows = Object.values(rowsMap);
3628 table._dbxSaving = false;
3629 table._dbxDirty = false;
3631 if (table.element && table.element._dbxFeature) {
3632 table.element._dbxFeature.updateSaveButton(table.element, table);
3638 const url = this._dbxAjaxUrl(opt.urls.save);
3640 table._dbxSaving = true;
3642 this._dbxRequest(url, {
3645 'Content-Type': 'application/json'
3647 body: JSON.stringify({ rows: rows }),
3648 responseType: 'json'
3652 table._dbxSaving = false;
3653 table._dbxDirty = false;
3655 table.getEditedCells().forEach(cell => {
3661 this._clearConflictFlags(table);
3663 if (table.element && table.element._dbxFeature) {
3664 table.element._dbxFeature.updateSaveButton(table.element, table);
3669 table._dbxSaving = false;
3670 this._syncDirtyState(table);
3671 if (table.element && table.element._dbxFeature) {
3672 table.element._dbxFeature.updateSaveButton(table.element, table);
3674 dbx.error('[grid] SAVE failed', {
3684 function dbxExtractLabel(token) {
3685 if (!token) return { key:'', label:'' };
3686 const m = token.match(/^([^\[]+)\[(.+)\]$/);
3687 if (!m) return { key: token.trim(), label: token.trim() };
3688 return { key: m[1].trim(), label: m[2].trim() };
3691 function dbxGridParseColumnOptions(raw) {
3693 String(raw || '').split(';').forEach(part => {
3697 const pos = part.indexOf('=');
3703 const key = part.substring(0, pos).trim();
3704 const value = part.substring(pos + 1).trim();
3705 if (key) out[key] = value;
3711 function dbxGridParseEditorValues(raw) {
3714 String(raw || '').split('~').forEach(part => {
3715 const pos = part.indexOf('=');
3720 value = part.substring(0, pos);
3721 label = part.substring(pos + 1);
3724 values[value] = label;
3731 /* =================================================
3732 * [dbx][grid][schema][step2]
3733 * schema parser & normalizer
3734 * ================================================= */
3737 if (!window.dbx) return;
3739 window.dbxGridParseSchema = function(rawSchema) {
3742 meta: rawSchema.meta || {},
3743 conditions: rawSchema.conditions || {},
3748 if (Array.isArray(rawSchema.rows)) {
3749 rawSchema.rows.forEach(rowRule => {
3751 const norm = dbxGridNormalizeRule(
3760 bg: rowRule.bg || null,
3761 color: rowRule.color || null,
3762 cls: rowRule.cls || null
3765 out.rows.push(norm);
3769 if (!rawSchema.columns || typeof rawSchema.columns !== 'object') {
3773 Object.keys(rawSchema.columns).forEach(colName => {
3775 const colDef = rawSchema.columns[colName];
3776 if (!colDef || !Array.isArray(colDef.rules)) return;
3778 out.columns[colName] = { rules: [] };
3780 colDef.rules.forEach(rule => {
3782 const norm = dbxGridNormalizeRule(
3783 Object.assign({}, rule, { col: colName }),
3790 out.columns[colName].rules.push(norm);
3797 function dbxGridNormalizeRule(rule, conditions, currentCol) {
3799 if (!rule || typeof rule !== 'object') return null;
3801 const resolveCondition = (c) => {
3802 if (typeof c === 'string') {
3803 if (!conditions[c]) {
3804 console.warn('[normalize] unknown condition', c);
3807 return Object.assign({}, conditions[c]);
3809 return Object.assign({}, c);
3812 if (Array.isArray(rule.all)) {
3814 if (rule.all.length === 0) {
3818 bg: rule.bg || null,
3819 color: rule.color || rule.text || null,
3820 cls: rule.cls || null
3825 const subs = rule.all
3826 .map(resolveCondition)
3827 .map(r => dbxGridNormalizeRule(r, conditions, currentCol))
3830 if (!subs.length) return null;
3835 bg: rule.bg || null,
3836 color: rule.color || rule.text || null,
3837 cls: rule.cls || null
3842 if (Array.isArray(rule.any)) {
3844 const subs = rule.any
3845 .map(resolveCondition)
3846 .map(r => dbxGridNormalizeRule(r, conditions, currentCol))
3849 if (!subs.length) return null;
3854 bg: rule.bg || null,
3855 color: rule.color || rule.text || null,
3856 cls: rule.cls || null
3861 const col = rule.col || currentCol;
3864 console.warn('[normalize] rule dropped (no col)', rule);
3871 normalize: rule.normalize,
3873 isReserved: rule.isReserved || false,
3875 bg: rule.bg || null,
3876 color: rule.color || rule.text || null,
3877 cls: rule.cls || null
3885 /* =================================================
3886 * [dbx][grid][schema][step3]
3888 * ================================================= */
3891 window.dbxGrid.evalCell = function(colRules, cellValue, rowData) {
3893 if (!colRules || !Array.isArray(colRules.rules)) {
3897 let finalStyle = null;
3898 let matched = false;
3900 for (let i = 0; i < colRules.rules.length; i++) {
3902 const rule = colRules.rules[i];
3903 const ok = window.dbxGrid.evalRule(rule, cellValue, rowData);
3910 finalStyle = Object.assign({}, finalStyle || {}, rule.style);
3915 return finalStyle || {};
3921 window.dbxGrid.evalRule = function(rule, cellValue, rowData) {
3923 if (!rule) return false;
3925 if (Array.isArray(rule.all)) {
3926 return rule.all.every(r =>
3927 window.dbxGrid.evalRule(r, cellValue, rowData)
3931 if (Array.isArray(rule.any)) {
3932 return rule.any.some(r =>
3933 window.dbxGrid.evalRule(r, cellValue, rowData)
3939 if (rule.col === '$cell') {
3941 } else if (rule.col) {
3942 left = rowData[rule.col];
3947 if (typeof left === 'string' && rule.normalize === 'trim') {
3951 const right = dbxResolveCompareValue(
3953 rule.isReserved || false,
3957 return dbxCompare(left, rule.if, right);
3960 function dbxResolveCompareValue(value, isReserved, rowData) {
3963 if (value === 'today') {
3964 const d = new Date();
3965 d.setHours(0, 0, 0, 0);
3969 if (typeof value === 'string' && /^[+-]\d+(day|month|year)s?$/.test(value)) {
3970 return dbxShiftDate(new Date(), value);
3976 if (typeof value === 'string' && value.charAt(0) === '$') {
3977 return rowData[value.substring(1)];
3983 function dbxShiftDate(base, expr) {
3984 const d = new Date(base);
3985 const n = parseInt(expr, 10);
3987 if (expr.includes('day')) d.setDate(d.getDate() + n);
3988 if (expr.includes('month')) d.setMonth(d.getMonth() + n);
3989 if (expr.includes('year')) d.setFullYear(d.getFullYear() + n);
3994 function dbxCompare(left, op, right) {
3996 if (left === null || left === undefined) left = '';
3997 if (right === null || right === undefined) right = '';
3999 if (op === 'empty') return left === '';
4000 if (op === 'notEmpty') return left !== '';
4001 if (op === 'startsWith') return String(left).startsWith(String(right));
4002 if (op === 'contains') return String(left).includes(String(right));
4003 if (op === '==') return left == right;
4004 if (op === '!=') return left != right;
4006 const l = dbxToComparable(left);
4007 const r = dbxToComparable(right);
4009 if (l === null || r === null) return false;
4011 if (op === '<') return l < r;
4012 if (op === '<=') return l <= r;
4013 if (op === '>') return l > r;
4014 if (op === '>=') return l >= r;
4019 function dbxToComparable(v) {
4021 if (v instanceof Date) return v.getTime();
4023 if (typeof v === 'string') {
4024 const d = dbxParseDate(v);
4025 if (d) return d.getTime();
4028 if (!isNaN(v)) return Number(v);
4033 window.dbxParseDate = function(v) {
4035 if (!v) return null;
4037 if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
4038 const d = new Date(v);
4039 return isNaN(d) ? null : d;
4042 if (/^\d{2}\.\d{2}\.\d{4}$/.test(v)) {
4043 const [d, m, y] = v.split('.');
4044 const dt = new Date(`${y}-${m}-${d}`);
4045 return isNaN(dt) ? null : dt;
4054 /* =================================================
4055 * [dbx][grid][schema][step4]
4056 * apply style helper
4057 * ================================================= */
4058 window.dbxGridApplyCellStyle = function(cell, style) {
4060 const el = cell.getElement();
4061 if (!el || !style) return;
4064 el.style.removeProperty('background-color');
4065 el.style.setProperty('background-color', style.bg, 'important');
4069 el.style.removeProperty('color');
4070 el.style.setProperty('color', style.color, 'important');
4074 el.classList.add(style.cls);