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 || '<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>'),
313 headerFilterLiveFilter,
314 headerFilterPlaceholder,
320 paginationSizeSelector,
321 paginationButtonCount,
324 paginationOutOfRange,
360 /* =========================================================
362 * ========================================================= */
365 const table = el && el._dbxTable ? el._dbxTable : null;
369 table._dbxDestroyed = true;
374 if (table && table._dbxLoopId) {
375 dbx.loop.hint(table._dbxLoopId, 'pause');
378 dbx.warn('[grid] destroy loop pause failed', e);
382 if (table && table._dbxAutoTimer) {
383 clearTimeout(table._dbxAutoTimer);
384 table._dbxAutoTimer = null;
387 dbx.warn('[grid] destroy auto timer clear failed', e);
391 if (table && table._dbxLayoutTimer) {
392 clearTimeout(table._dbxLayoutTimer);
393 table._dbxLayoutTimer = null;
396 dbx.warn('[grid] destroy layout timer clear failed', e);
400 if (table && table._dbxPageLayoutTimer) {
401 clearTimeout(table._dbxPageLayoutTimer);
402 table._dbxPageLayoutTimer = null;
405 dbx.warn('[grid] destroy page layout timer clear failed', e);
409 if (table && table._dbxChooserTimer) {
410 clearTimeout(table._dbxChooserTimer);
411 table._dbxChooserTimer = null;
414 dbx.warn('[grid] destroy chooser timer clear failed', e);
418 if (table && typeof table.destroy === 'function') {
422 dbx.warn('[grid] destroy table failed', e);
426 delete el._dbxGridInitialized;
428 delete el._dbxFeature;
430 delete el._dbxSchemaParsed;
431 delete el._dbxApplyGridLines;
436 /* =========================================================
437 * DBX AJAX URL HELPER
438 * ========================================================= */
439 _dbxAjaxUrl(url, opts) {
443 if (!url) return url;
447 if (url.indexOf('dbx_ajax=') === -1) {
448 if (url.indexOf('?') === -1) {
449 finalUrl = url + '?dbx_ajax=1';
451 finalUrl = url + '&dbx_ajax=1';
455 if (opts.background === true && finalUrl.indexOf('dbx_sync=') === -1) {
456 finalUrl += (finalUrl.indexOf('?') === -1 ? '?' : '&') + 'dbx_sync=0';
463 _getAjaxSorters(table) {
465 if (!table || !table.element) return [];
467 const opt = table.element._dbxOpt || {};
469 if (opt.headerSort !== true) {
473 if (table._dbxBuilt !== true) {
477 if (table._dbxIsRemotePagination === true) {
478 const sorters = table.getSorters ? table.getSorters() : [];
479 return Array.isArray(sorters) ? sorters : [];
482 if (opt.urls && opt.urls.sort) {
483 const s = table._dbxServerSort;
485 if (s && s.field && s.dir) {
493 const sorters = table.getSorters ? table.getSorters() : [];
494 return Array.isArray(sorters) ? sorters : [];
497 _applyServerSortIndicators(table) {
499 if (!table || !table.element) return;
501 const opt = table.element._dbxOpt || {};
503 if (!opt.urls || !opt.urls.sort) return;
504 if (table._dbxIsRemotePagination === true) return;
506 const active = table._dbxServerSort || null;
507 const cols = this._getLeafColumns(table);
509 cols.forEach(col => {
511 const field = col.getField ? col.getField() : null;
512 if (!field || field.startsWith('_')) return;
514 const el = col.getElement ? col.getElement() : null;
519 if (active && active.field === field) {
520 aria = (active.dir === 'asc') ? 'ascending' : 'descending';
523 el.setAttribute('aria-sort', aria);
529 _dbxRequest(url, options = {}) {
532 return Promise.reject(new Error('Missing URL'));
535 if (!dbx.ajax || typeof dbx.ajax.request !== 'function') {
536 return Promise.reject(new Error('ajax.js nicht geladen.'));
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 || ''));
547 dbx.log('[grid][ajax] start', {
550 responseType: responseType
553 return dbx.ajax.request({
556 mode: responseType === 'json' ? 'json' : 'text',
559 timeout: options.timeout || 30000,
560 skipRuntime: skipRuntime
562 dbx.log('[grid][ajax] success', {
565 duration_ms: Date.now() - startedAt
569 dbx.error('[grid][ajax] error', {
572 duration_ms: Date.now() - startedAt,
580 _parsePaginationSizeSelector(v) {
582 if (v === undefined || v === null || v === '' || v === false || v === 0 || v === '0' || v === 'off' || v === 'false') {
586 if (v === true || v === 1 || v === '1' || v === 'on' || v === 'true' || v === 'auto') {
590 const normalizeValue = (item) => {
591 if (item === true) return 99999;
593 const txt = String(item).trim().toLowerCase();
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;
601 const n = parseInt(txt, 10);
602 if (!isNaN(n) && n > 0) return n;
607 if (Array.isArray(v)) {
608 const out = v.map(normalizeValue).filter(x => x !== null);
609 return out.length ? this._normalizePaginationSizeSelectorOrder(out) : false;
612 const out = String(v)
615 .filter(x => x !== null);
617 return out.length ? this._normalizePaginationSizeSelectorOrder(out) : false;
620 _normalizePaginationSizeSelectorOrder(values) {
622 return [15, 5, 25, 50, 100, 99999];
625 _pageSizeSelectOptions() {
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: '*' }
638 _normalizePageSizeValue(v, def = 15, selector = false) {
640 if (v === true) return 99999;
642 const txt = String(v ?? '').toLowerCase().trim();
643 if (txt === 'true' || txt === 'all' || txt === '*' || txt === '__all') return 99999;
645 const n = parseInt(txt, 10);
646 if (!isNaN(n) && n > 0) {
650 const defTxt = String(def ?? '').toLowerCase().trim();
651 if (def === true || defTxt === 'true' || defTxt === 'all' || defTxt === '*' || defTxt === '__all') {
655 const defNum = parseInt(def, 10);
656 if (!isNaN(defNum) && defNum > 0) {
660 if (Array.isArray(selector) && selector.length) {
661 return selector[0] === true ? 99999 : (parseInt(selector[0], 10) || 15);
667 _storePageSizeState(gridId, value, def = 15, selector = false) {
669 const normalized = this._normalizePageSizeValue(value, def, selector);
670 dbx.uiSet('grid', gridId, 'PAGE.SIZE', String(normalized));
674 _getPageSizeState(gridId, def = 15, selector = false) {
676 const defaultSize = this._normalizePageSizeValue(def, 15, selector);
677 return this._normalizePageSizeValue(
678 dbx.uiGet('grid', gridId, 'PAGE.SIZE', String(defaultSize)),
684 _changePageSize(table, value, opt = {}) {
686 if (!table || !table.element) return;
688 const gridId = table.element.id || 'grid';
689 const pageSize = this._storePageSizeState(gridId, value, opt.pageSize || 15, opt.paginationSizeSelector);
691 table._dbxPageSizeState = pageSize;
692 opt.pageSize = pageSize;
695 table._dbxPageSizeChanging = true;
696 if (typeof table.setPageSize === 'function') {
697 table.setPageSize(pageSize);
699 if (typeof table.setPage === 'function') {
703 dbx.warn('[grid] page size change failed', err);
705 table._dbxPageSizeChanging = false;
708 this.reloadTable(table, opt, { resetPage: true });
709 window.setTimeout(() => this._applyPaginationButtonLabels(table), 0);
712 _normalizePaginationCounter(v) {
714 if (v === undefined || v === null || v === '' || v === false || v === 0 || v === '0' || v === 'off' || v === 'false') {
718 const txt = String(v).toLowerCase().trim();
720 if (txt === '1' || txt === 'on' || txt === 'true') return 'rows';
721 if (txt === 'rows') return 'rows';
722 if (txt === 'pages') return 'pages';
727 _normalizeHeaderSortStart(v) {
729 const txt = String(v || 'asc').toLowerCase().trim();
730 return (txt === 'desc') ? 'desc' : 'asc';
733 _normalizePaginationOutOfRange(v) {
735 if (v === undefined || v === null || v === '') return false;
737 const txt = String(v).toLowerCase().trim();
739 if (txt === 'false' || txt === 'off' || txt === '0') return false;
740 if (txt === 'first' || txt === 'last' || txt === 'reset') return txt;
742 const n = parseInt(txt, 10);
743 if (!isNaN(n)) return n;
748 _getPaginationUiEls(el) {
750 const root = this._getRoot(el);
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
760 _setRoleVisible(root, role, show) {
764 const el = root.querySelector('[data-dbx-role="' + role + '"]');
767 el.style.display = show ? '' : 'none';
770 _loadRootScript(file, done) {
772 window.dbxGridExportDeps = window.dbxGridExportDeps || {};
774 const state = window.dbxGridExportDeps[file] || { status: 'new', callbacks: [] };
775 window.dbxGridExportDeps[file] = state;
777 if (state.status === 'loaded') {
782 if (state.status === 'loading') {
783 state.callbacks.push(done);
787 state.status = 'loading';
788 state.callbacks = done ? [done] : [];
790 let url = dbx.config.rootPath + file;
792 const searchParams = new URLSearchParams(location.search);
793 const cacheBust = searchParams.get('dbx_nocache') || searchParams.get('cachebust');
795 url += (url.indexOf('?') === -1 ? '?' : '&') + 'dbx_nocache=' + encodeURIComponent(cacheBust);
798 const finish = (ok) => {
800 state.status = ok ? 'loaded' : 'error';
802 state.callbacks.forEach(cb => cb && cb(ok === true));
803 state.callbacks = [];
806 const xhr = new XMLHttpRequest();
807 xhr.open('GET', url, true);
810 if (xhr.status < 200 || xhr.status >= 300) {
811 dbx.error('[grid] export dependency load failed', url, 'HTTP ' + xhr.status);
817 const run = new Function(
825 xhr.responseText + '\n//# sourceURL=' + url
828 run.call(window, window, window, window, window, undefined, undefined, undefined);
831 dbx.error('[grid] export dependency load failed', url, e);
836 xhr.onerror = () => {
837 dbx.error('[grid] export dependency load failed', url);
844 _waitForExportDep(check, done, attempts) {
846 const maxAttempts = attempts || 20;
853 if (maxAttempts <= 0) {
858 window.setTimeout(() => {
859 this._waitForExportDep(check, done, maxAttempts - 1);
863 _setTabulatorDependency(table, key, value) {
865 if (!table || !table.dependencyRegistry || !value) return false;
867 table.dependencyRegistry.deps = table.dependencyRegistry.deps || {};
868 table.dependencyRegistry.deps[key] = value;
873 _ensureExcelExportDeps(table, done) {
876 done && done(this._setTabulatorDependency(table, 'XLSX', window.XLSX));
880 this._loadRootScript('add_ons/tabulator-deps/xlsx.full.min.js', (ok) => {
886 this._waitForExportDep(
889 done && done(ready === true && this._setTabulatorDependency(table, 'XLSX', window.XLSX));
895 _ensurePdfExportDeps(table, done) {
897 const hasAutoTable = () => !!(
899 window.jspdf.jsPDF &&
900 window.jspdf.jsPDF.API &&
901 window.jspdf.jsPDF.API.autoTable
904 if (hasAutoTable()) {
905 done && done(this._setTabulatorDependency(table, 'jspdf', window.jspdf));
909 const loadAutoTable = () => {
910 this._loadRootScript('add_ons/tabulator-deps/jspdf.plugin.autotable.min.js', (ok) => {
916 this._waitForExportDep(
919 done && done(ready === true && this._setTabulatorDependency(table, 'jspdf', window.jspdf));
925 if (window.jspdf && window.jspdf.jsPDF) {
930 this._loadRootScript('add_ons/tabulator-deps/jspdf.umd.min.js', (ok) => {
931 if (ok !== true || !(window.jspdf && window.jspdf.jsPDF)) {
940 _applyPaginationButtonLabels(table) {
942 if (!table || !table.element) return;
944 const ui = this._getPaginationUiEls(table.element);
945 const controls = ui && ui.controls ? ui.controls : null;
946 if (!controls) return;
948 const sizeSelect = controls.querySelector('.tabulator-page-size');
950 let icon = controls.querySelector('.dbx-grid-page-size-icon');
952 controls.querySelectorAll('label').forEach(label => {
953 if (String(label.textContent || '').trim().toLowerCase() === 'page size') {
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');
965 controls.insertBefore(icon, sizeSelect);
968 sizeSelect.setAttribute('title', 'Zeilen pro Seite');
969 sizeSelect.setAttribute('aria-label', 'Zeilen pro Seite');
971 const currentPageSize = table._dbxPageSizeState || (table.getPageSize ? table.getPageSize() : '');
972 const currentValue = String(currentPageSize || sizeSelect.value || '15');
974 sizeSelect.innerHTML = '';
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);
984 if (currentValue && sizeSelect.querySelector('option[value="' + currentValue + '"]')) {
985 sizeSelect.value = currentValue;
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;
997 e.stopImmediatePropagation();
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);
1007 const buttons = controls.querySelectorAll('.tabulator-page');
1008 if (!buttons || !buttons.length) return;
1010 const detectType = (btn) => {
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()
1019 const has = (needle) => values.some(v => v === needle || v.indexOf(needle) !== -1);
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';
1031 html: '<i class="bi bi-chevron-bar-left"></i>',
1032 label: 'Erste Seite'
1035 html: '<i class="bi bi-chevron-left"></i>',
1036 label: 'Vorherige Seite'
1039 html: '<i class="bi bi-chevron-right"></i>',
1040 label: 'Nächste Seite'
1043 html: '<i class="bi bi-chevron-bar-right"></i>',
1044 label: 'Letzte Seite'
1048 buttons.forEach(btn => {
1050 const type = detectType(btn);
1051 if (!type || !defs[type]) return;
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);
1060 _ensureSortIcons(table) {
1062 if (!table || !table.element) return;
1063 if (table._dbxBuilt !== true) return;
1065 const opt = table.element._dbxOpt || {};
1066 const cols = this._getLeafColumns(table);
1068 cols.forEach(col => {
1070 const field = col.getField ? col.getField() : null;
1071 if (!field || field.startsWith('_')) return;
1073 const def = col.getDefinition ? col.getDefinition() : {};
1075 (opt.headerSort === true) &&
1077 (def && def.headerSort === true) ||
1078 (def && typeof def.headerClick === 'function')
1081 const headerEl = col.getElement ? col.getElement() : null;
1082 if (!headerEl) return;
1085 headerEl.querySelector('.tabulator-col-title') ||
1086 headerEl.querySelector('.tabulator-col-content') ||
1089 let iconEl = headerEl.querySelector('.dbx-grid-sort-icon');
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');
1101 headerEl.classList.add('dbx-grid-sortable');
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);
1112 _applySortIndicators(table) {
1114 if (!table || !table.element) return;
1115 if (table._dbxBuilt !== true) return;
1117 const opt = table.element._dbxOpt || {};
1118 const cols = this._getLeafColumns(table);
1120 this._ensureSortIcons(table);
1124 if (opt.headerSort === true) {
1126 const sorters = this._getAjaxSorters(table);
1128 if (Array.isArray(sorters)) {
1129 sorters.forEach(s => {
1130 if (!s || !s.field) return;
1131 activeMap[s.field] = s.dir || 'asc';
1136 cols.forEach(col => {
1138 const field = col.getField ? col.getField() : null;
1139 if (!field || field.startsWith('_')) return;
1141 const def = col.getDefinition ? col.getDefinition() : {};
1143 (opt.headerSort === true) &&
1145 (def && def.headerSort === true) ||
1146 (def && typeof def.headerClick === 'function')
1149 const headerEl = col.getElement ? col.getElement() : null;
1150 if (!headerEl) return;
1152 const iconEl = headerEl.querySelector('.dbx-grid-sort-icon');
1154 headerEl.classList.remove('dbx-grid-sort-asc');
1155 headerEl.classList.remove('dbx-grid-sort-desc');
1156 headerEl.classList.remove('dbx-grid-sort-none');
1159 if (iconEl) iconEl.remove();
1160 headerEl.setAttribute('aria-sort', 'none');
1164 const dir = activeMap[field] || null;
1166 if (!iconEl) return;
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');
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');
1184 /* =========================================================
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;
1195 const n = parseInt(v, 10);
1196 return isNaN(n) ? def : n;
1199 _isTableAlive(table) {
1202 table._dbxDestroyed !== true &&
1204 table.element.isConnected === true
1208 _isTableLayoutReady(table) {
1210 if (!this._isTableAlive(table)) return false;
1212 const root = table.element;
1213 if (!root) return false;
1216 root.querySelector('.tabulator-header') ||
1217 root.querySelector('.tabulator-tableholder')
1221 _queueTableTimer(table, key, fn, delay = 0) {
1223 if (!table || !key || typeof fn !== 'function') return;
1226 clearTimeout(table[key]);
1230 table[key] = setTimeout(() => {
1233 if (!this._isTableAlive(table)) return;
1239 _getLeafColumns(table) {
1242 if (!table || typeof table.getColumns !== 'function') return out;
1244 const walk = (cols) => {
1245 cols.forEach(col => {
1246 const field = col.getField && col.getField();
1247 if (field && !field.startsWith('_')) {
1250 if (col.getSubColumns) {
1251 const subs = col.getSubColumns();
1252 if (subs && subs.length) {
1259 walk(table.getColumns());
1263 _restoreStoredColumnWidths(table, gridId) {
1265 if (!this._isTableLayoutReady(table)) return;
1267 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
1268 const cols = this._getLeafColumns(table);
1270 cols.forEach(col => {
1271 const field = col.getField();
1272 if (!field || field.startsWith('_')) return;
1274 const w = uiGet('COLUMNS.SIZE.' + field, null);
1275 if (w === null) return;
1277 const width = parseInt(w, 10);
1278 if (isNaN(width) || width <= 0) return;
1280 const currentWidth = col.getWidth();
1281 if (typeof currentWidth === 'number' && Math.abs(currentWidth - width) <= 1) {
1286 col.setWidth(width);
1288 dbx.warn('[grid] restore width failed', field, width, e);
1293 _restoreStoredColumnVisibility(table, gridId) {
1295 if (!this._isTableLayoutReady(table)) return;
1297 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
1298 const cols = this._getLeafColumns(table);
1300 cols.forEach(col => {
1301 const field = col.getField();
1302 if (!field || field.startsWith('_')) return;
1304 const vis = uiGet('COLUMNS.VISIBLE.' + field, null);
1305 if (vis === null) return;
1308 if (vis === '0' && col.isVisible()) col.hide();
1309 if (vis === '1' && !col.isVisible()) col.show();
1311 dbx.warn('[grid] restore visibility failed', field, vis, e);
1316 _applyShiftGroupLabels(table) {
1318 if (!this._isTableLayoutReady(table)) return;
1320 const root = table.element;
1321 if (!root || !root.innerHTML.includes('~~')) return;
1323 root.querySelectorAll('.tabulator-col-group').forEach(groupEl => {
1325 const titleEl = groupEl.querySelector('.tabulator-col-title');
1326 if (!titleEl) return;
1328 if (titleEl.querySelector('.dbx-shift-label')) return;
1330 const raw = titleEl.textContent;
1331 if (!raw || !raw.includes('~~')) return;
1333 const parts = raw.split('~~');
1334 if (parts.length !== 2) return;
1336 const left = parts[0].trim();
1337 const right = parts[1].trim();
1340 '<div class="dbx-shift-label">' +
1341 '<span class="left">' + left + '</span>' +
1342 '<span class="right">' + right + '</span>' +
1347 _applyInitialLayoutState(table, gridId) {
1349 if (!this._isTableLayoutReady(table)) return false;
1352 table.blockRedraw();
1354 this._restoreStoredColumnWidths(table, gridId);
1355 this._restoreStoredColumnVisibility(table, gridId);
1356 this._applyShiftGroupLabels(table);
1360 table.restoreRedraw(true);
1362 dbx.warn('[grid] restoreRedraw failed', e);
1370 return el.closest('.dbx-grid');
1373 _findSaveButton(el) {
1374 const root = this._getRoot(el);
1375 let btn = root ? root.querySelector('[data-dbx="grid-save"]') : null;
1377 const panel = el.closest('.dbx-panel');
1378 btn = panel ? panel.querySelector('[data-dbx="grid-save"]') : null;
1383 _tableHasPendingEdits(table) {
1384 if (!table || typeof table.getEditedCells !== 'function') {
1388 const edited = table.getEditedCells();
1389 return Array.isArray(edited) && edited.length > 0;
1395 _syncDirtyState(table) {
1396 const hasEdits = this._tableHasPendingEdits(table);
1398 table._dbxDirty = true;
1400 return table._dbxDirty === true || hasEdits;
1403 _markTableDirty(table, el) {
1404 table._dbxDirty = true;
1405 this.updateSaveButton(el, table);
1409 const root = this._getRoot(el);
1412 led: root ? root.querySelector('.dbx-grid-sync-led') : null,
1413 count: root ? root.querySelector('.dbx-grid-sync-count') : null
1417 _setLedState(led, state) {
1421 if (led._dbxSyncLedEnabled === false) {
1422 if (led.style.display !== 'none') {
1423 led.style.display = 'none';
1428 if (led.style.display === 'none') {
1429 led.style.display = 'inline-block';
1432 if (led._dbxState === state) return;
1433 led._dbxState = state;
1437 if (state === 'loading') color = '#0d6efd';
1438 if (state === 'ok') color = '#198754';
1439 if (state === 'idle') color = '#bbb';
1440 if (state === 'error') color = '#dc3545';
1442 if (led._dbxLastColor !== color) {
1443 led.style.backgroundColor = color;
1444 led._dbxLastColor = color;
1448 _setSyncCount(countEl, value) {
1450 if (!countEl) return;
1452 const txt = String(value ?? '');
1453 if (countEl.textContent !== txt) {
1454 countEl.textContent = txt;
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');
1465 _rowIdField(table) {
1466 return (table && table.options && table.options.index) ? table.options.index : 'id';
1469 _collectEditedMap(table) {
1471 const editedMap = {};
1472 const editedCells = table.getEditedCells();
1474 if (!editedCells || !editedCells.length) {
1478 for (let i = 0; i < editedCells.length; i++) {
1479 const c = editedCells[i];
1480 const r = c.getRow();
1483 const id = r.getData()?.[this._rowIdField(table)];
1484 const f = c.getField();
1486 if (id == null || !f) continue;
1488 if (!editedMap[id]) editedMap[id] = {};
1489 editedMap[id][f] = true;
1495 _applySchemaCellStyle(cell, rowData) {
1499 const table = cell.getTable();
1500 const schema = table?.element?._dbxSchemaParsed;
1501 if (!schema || !schema.columns) return;
1503 const field = cell.getField();
1504 const colSchema = schema.columns[field];
1505 if (!colSchema) return;
1507 const style = dbxGrid.evalCell(colSchema, cell.getValue(), rowData || cell.getRow().getData());
1509 dbxGridApplyCellStyle(cell, style);
1513 _ajaxResponse(table, url, params, response) {
1515 if (response && typeof response === 'object') {
1516 if (typeof response.server_time !== 'undefined') {
1517 table._dbxServerTime = response.server_time || null;
1520 if (typeof response.count !== 'undefined') {
1521 table._dbxSyncCount = response.count || 0;
1525 if (table._dbxIsRemotePagination === true || table._dbxIsProgressive === true) {
1527 if (response && Array.isArray(response.data)) {
1529 last_page: response.last_page || 1,
1530 last_row: response.last_row,
1535 if (response && Array.isArray(response.rows)) {
1537 last_page: response.last_page || 1,
1538 last_row: response.last_row,
1543 if (Array.isArray(response)) {
1550 dbx.error('[grid] invalid paginated response', response);
1557 if (response && Array.isArray(response.rows)) {
1558 return response.rows;
1561 if (Array.isArray(response)) {
1565 dbx.error('[grid] invalid response', response);
1570 /* =========================================================
1572 * ========================================================= */
1573 _escapeHtml(value) {
1574 return String(value ?? '')
1575 .replace(/&/g, '&')
1576 .replace(/</g, '<')
1577 .replace(/>/g, '>')
1578 .replace(/"/g, '"')
1579 .replace(/'/g, ''');
1582 _deleteRecordLabel(data, idField) {
1583 if (!data || typeof data !== 'object') return '';
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 ?? '';
1590 if (name) parts.push(String(name));
1591 if (email) parts.push(String(email));
1592 if (id !== '') parts.push('ID ' + String(id));
1594 return parts.join(' - ');
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>'
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);
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,
1614 title: opt.deleteConfirmTitle,
1615 question: opt.deleteConfirmQuestion + labelHtml,
1616 hint: opt.deleteConfirmHint,
1618 labelyes: '<i class="bi bi-trash"></i> Loeschen',
1619 labelno: '<i class="bi bi-x-lg"></i> Abbrechen',
1621 backdropclose: false,
1623 }).then(result => result && result.action === 'yes');
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));
1634 return openConfirm().catch(() => false);
1639 const colsDef = opt.colsDef;
1640 const gridId = opt._gridId;
1642 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
1646 const ungrouped = [];
1647 let hasGroups = false;
1649 const hasActions = !!(opt.allowDelete);
1651 const orderRaw = uiGet('COLUMNS.ORDER', null);
1652 const orderList = orderRaw
1653 ? orderRaw.split('|').filter(f => f && !f.startsWith('_'))
1656 const sortEnabled = (opt.headerSort === true);
1657 const useDedicatedServerSort = sortEnabled && !!(opt.urls.sort && opt._dbxIsRemotePagination !== true);
1658 const useTabulatorSort = sortEnabled && !useDedicatedServerSort;
1660 const actionsCol = {
1661 title: '<i class="bi bi-gear"></i>',
1662 headerHozAlign: 'center',
1668 headerHozAlign: 'center',
1671 headerFilter: 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';
1681 const row = cell.getRow();
1682 const table = row.getTable();
1683 const data = row.getData();
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>';
1695 btnShow.addEventListener('click', function(e) {
1696 e.stopPropagation();
1697 const url = String(data.show_link || '');
1700 if (window.dbx && dbx.openWin && typeof dbx.openWin.open === 'function') {
1703 title: 'Vorschau: ' + String(data.title || 'Content'),
1715 window.location.href = url;
1719 wrap.appendChild(btnShow);
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>';
1732 btnEdit.addEventListener('click', function(e) {
1733 e.stopPropagation();
1734 const url = String(data.profile_link || '');
1737 if (window.dbx && dbx.openWin && typeof dbx.openWin.open === 'function') {
1748 window.location.href = url;
1752 wrap.appendChild(btnEdit);
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>';
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;
1769 table.element._dbxFeature._confirmDelete(data, idField, opt, btnDel)
1770 .then(confirmed => {
1771 if (!confirmed) return;
1773 return table.element._dbxFeature._dbxRequest(
1774 table.element._dbxFeature._dbxAjaxUrl(opt.urls.delete || ''),
1777 headers: { 'Content-Type': 'application/json' },
1778 body: JSON.stringify({ id: data[idField] }),
1779 responseType: 'json'
1785 if (res && (res.ok || res.success)) {
1788 dbx.error('[grid] delete failed', res);
1792 dbx.error('[grid] delete error', err);
1796 wrap.appendChild(btnDel);
1803 const fieldDefinitions = colsDef.split(',');
1806 fieldDefinitions.forEach(def => {
1808 let groupName = null;
1810 if (def.includes('@')) {
1811 const tmp = def.split('@');
1812 def = tmp[0].trim();
1813 groupName = tmp[1].trim();
1817 const parts = def.split(':').map(s => s.trim());
1819 const fieldInfo = dbxExtractLabel(parts[0]);
1820 const field = fieldInfo.key;
1821 const title = fieldInfo.label;
1823 const gridType = String(parts[1] || '').toLowerCase();
1824 let flag = parts[2] || null;
1825 let optionRaw = parts.slice(3).join(':');
1827 if (flag && flag.indexOf('=') !== -1) {
1828 optionRaw = parts.slice(2).join(':');
1832 const colOptions = dbxGridParseColumnOptions(optionRaw);
1834 if (!field || field.startsWith('_') || flag === '!v') return;
1836 const visState = uiGet('COLUMNS.VISIBLE.' + field, '1');
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'),
1853 formatter: (cell) => {
1855 const value = cell.getValue();
1857 if (gridType === 'image') {
1858 if (!value) return '';
1859 const img = document.createElement('img');
1860 img.src = String(value);
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';
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();
1883 const table = cell.getTable();
1884 const schema = table?.element?._dbxSchemaParsed;
1885 if (!schema || !schema.columns) return value;
1887 const field = cell.getField();
1888 const colSchema = schema.columns[field];
1889 if (!colSchema) return value;
1891 const rowData = cell.getRow().getData();
1892 const style = dbxGrid.evalCell(colSchema, value, rowData);
1895 dbxGridApplyCellStyle(cell, style);
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(' ');
1911 if (gridType === 'image') {
1913 col.editable = false;
1914 col.headerFilter = false;
1915 col.headerSort = false;
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
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)
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];
1939 const rowData = cell.getRow().getData();
1940 const style = dbxGrid.evalCell(colSchema, value, rowData);
1942 dbxGridApplyCellStyle(cell, style);
1947 if (!Object.prototype.hasOwnProperty.call(lookupValues, key) && typeof baseFormatter === 'function') {
1948 return baseFormatter(cell);
1953 } else if (colOptions.editor === 'textarea') {
1954 col.editor = 'textarea';
1955 } else if (colOptions.editor === 'input') {
1956 col.editor = 'input';
1960 if (useDedicatedServerSort) {
1961 col.headerClick = (e, column) => {
1963 const table = column.getTable();
1964 const field = column.getField();
1965 if (!field || field.startsWith('_')) return;
1967 const current = table._dbxServerSort || null;
1968 const startDir = opt.headerSortStart || 'asc';
1969 const otherDir = (startDir === 'asc') ? 'desc' : 'asc';
1971 let nextSort = null;
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) {
1980 nextSort = { field: field, dir: startDir };
1983 table._dbxServerSort = nextSort;
1985 table.element._dbxFeature._applySortIndicators(table);
1988 dbx.log('[grid] server sort cleared', {
1993 table.setData(table.element._dbxFeature._dbxAjaxUrl(opt.urls.read));
1998 table.element._dbxFeature._dbxAjaxUrl(
2000 '&field=' + encodeURIComponent(nextSort.field) +
2001 '&dir=' + encodeURIComponent(nextSort.dir)
2004 dbx.log('[grid] server sort click', {
2006 field: nextSort.field,
2015 colMap[field] = col;
2018 if (!groups[groupName]) groups[groupName] = [];
2019 groups[groupName].push(col);
2021 ungrouped.push(col);
2025 if (orderList && !hasGroups) {
2030 orderList.forEach(f => {
2032 ordered.push(colMap[f]);
2037 Object.keys(colMap).forEach(f => {
2039 ordered.push(colMap[f]);
2044 ordered.unshift(actionsCol);
2055 cols.unshift(actionsCol);
2058 if (ungrouped.length) {
2059 ungrouped.forEach(col => cols.push(col));
2062 Object.keys(groups).forEach(groupName => {
2064 if (idx > 0 || ungrouped.length > 0) {
2067 field: `_sep_${idx}`,
2072 headerFilter: false,
2074 cssClass: 'dbx-col-separator',
2075 formatter: () => '',
2083 columns: groups[groupName]
2093 cols.push(actionsCol);
2096 return cols.concat(ungrouped);
2100 /* =========================================================
2102 * ========================================================= */
2103 updateSaveButton(el, table) {
2105 const btn = this._findSaveButton(el);
2108 const isDirty = this._syncDirtyState(table);
2110 if (btn._dbxDirtyState === isDirty) return;
2112 btn._dbxDirtyState = isDirty;
2115 btn.classList.remove('btn-outline-primary');
2116 btn.classList.add('btn-primary');
2118 btn.classList.remove('btn-primary');
2119 btn.classList.add('btn-outline-primary');
2124 /* =========================================================
2126 * ========================================================= */
2127 bindLayoutState(el, table) {
2129 let saveTimeout = null;
2130 let lastResizedField = null;
2132 const gridId = el.id || 'grid';
2133 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2135 const getLeafColumns = () => this._getLeafColumns(table);
2137 function saveLayout(type) {
2139 if (saveTimeout) clearTimeout(saveTimeout);
2141 saveTimeout = setTimeout(() => {
2143 if (type === 'order') {
2144 const cols = getLeafColumns();
2145 const order = cols.map(c => c.getField()).join('|');
2146 uiSet('COLUMNS.ORDER', order);
2150 if (type === 'width') {
2151 if (!lastResizedField) return;
2153 const col = table.getColumn(lastResizedField);
2156 const w = col.getWidth();
2158 if (typeof w === 'number' && w > 0) {
2159 uiSet('COLUMNS.SIZE.' + lastResizedField, String(w));
2167 table.on('columnResized', col => {
2168 const f = col.getField();
2169 if (!f || f.startsWith('_')) return;
2171 lastResizedField = f;
2172 saveLayout('width');
2175 table.on('columnMoved', col => {
2176 const f = col.getField();
2177 if (!f || f.startsWith('_')) return;
2178 saveLayout('order');
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');
2187 table.on('pageSizeChanged', (pageSize) => {
2188 if (table._dbxPageSizeChanging === true) {
2189 table._dbxPageSizeState = this._normalizePageSizeValue(
2192 opt.paginationSizeSelector
2196 if (table._dbxIsRemotePagination === true) {
2200 dbx.warn('[grid] setPage failed after pageSizeChanged', e);
2202 table.replaceData();
2208 /* =========================================================
2210 * ========================================================= */
2211 bindToolbar(el, table, opt, uiState, root) {
2213 const gridId = el.id || 'grid';
2215 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
2216 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2218 el._dbxApplyGridLines = function(force) {
2220 const on = uiGet('GRIDLINES', '1') == '1';
2221 const tabRoot = table.element;
2222 if (!tabRoot) return;
2225 tabRoot.classList.add('dbx-grid-lines');
2227 tabRoot.classList.remove('dbx-grid-lines');
2231 uiState.gridLines = uiGet('GRIDLINES', '1') == '1';
2232 uiState.autosave = uiGet('AUTOSAVE', '1') == '1';
2234 const heightStored = uiGet('HEIGHT', null);
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;
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);
2261 this._setRoleVisible(
2264 opt.pagination === true && (
2265 (opt.paginationControls === true) ||
2266 (opt.paginationCounter !== false)
2270 this._setRoleVisible(
2272 'pagination-controls',
2273 opt.pagination === true && opt.paginationControls === true
2276 this._setRoleVisible(
2278 'pagination-counter',
2279 opt.pagination === true && opt.paginationCounter !== false
2283 searchInput.placeholder = opt.searchPlaceholder || '🔍';
2284 if (opt.searchWidth > 0) {
2285 searchInput.style.width = opt.searchWidth + 'px';
2290 autosave.checked = uiState.autosave;
2291 autosave.addEventListener('change', () => {
2292 uiSet('AUTOSAVE', autosave.checked ? '1' : '0');
2297 gridLinesCb.checked = uiState.gridLines;
2298 gridLinesCb.addEventListener('change', () => {
2299 uiSet('GRIDLINES', gridLinesCb.checked ? '1' : '0');
2300 el._dbxApplyGridLines(false);
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);
2310 heightSlider.min = String(heightMin);
2311 heightSlider.max = String(heightMax);
2312 heightSlider.step = String(heightStep);
2314 if (heightStored !== null) {
2315 heightSlider.value = heightStored;
2318 const sliderHeight = parseInt(heightSlider.value, 10);
2319 if (!isNaN(sliderHeight)) {
2320 heightSlider.value = String(Math.min(heightMax, Math.max(heightMin, sliderHeight)));
2323 heightSlider.addEventListener('input', () => {
2324 const h = parseInt(heightSlider.value, 10);
2325 if (isNaN(h)) return;
2327 uiSet('HEIGHT', String(h));
2333 saveBtn.addEventListener('click', () => {
2334 if (table._dbxDirty === true) {
2335 this.saveTable(table, opt);
2341 insertBtn.addEventListener('click', () => {
2342 this.insertRow(table, opt);
2347 reloadBtn.addEventListener('click', () => {
2348 if (table._dbxSaving === true) return;
2349 this.reloadTable(table, opt);
2354 resetBtn.addEventListener('click', () => {
2365 keys.forEach(k => uiSet(k, null));
2367 const cols = table.getColumns();
2370 (function walk(cols){
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);
2381 fields.forEach(f => {
2382 uiSet('COLUMNS.SIZE.' + f, null);
2383 uiSet('COLUMNS.VISIBLE.' + f, null);
2386 uiSet('GRIDLINES', '1');
2387 uiSet('AUTOSAVE', '1');
2394 colBtn.addEventListener('click', () => {
2395 this.openColumnChooser(colBtn, table);
2400 excelBtn.addEventListener('click', () => {
2401 this._ensureExcelExportDeps(table, (ok) => {
2403 dbx.error('[grid] excel export dependencies missing');
2408 table.download('xlsx', (opt.exportFileName || gridId) + '.xlsx', {
2409 sheetName: opt.exportSheetName || gridId
2412 dbx.error('[grid] excel export failed', e);
2419 pdfBtn.addEventListener('click', () => {
2420 this._ensurePdfExportDeps(table, (ok) => {
2422 dbx.error('[grid] pdf export dependencies missing');
2427 table.download('pdf', (opt.exportFileName || gridId) + '.pdf', {
2428 orientation: opt.pdfOrientation || 'landscape',
2429 title: opt.pdfTitle || document.title || 'Export'
2432 dbx.error('[grid] pdf export failed', e);
2440 if (!table._dbxGlobalSearchFilter) {
2441 table._dbxGlobalSearchFilter = function (data, filterParams) {
2442 const val = String((filterParams && filterParams.value) || '').toLowerCase();
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) {
2457 const getFields = () => {
2459 (function walk(cols){
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);
2468 })(table.getColumns());
2474 const applyLocalSearch = () => {
2475 const val = searchInput.value.trim();
2477 if (opt.searchMode === 'remote') {
2478 table._dbxSearchValue = val.toLowerCase();
2479 this.reloadTable(table, opt, { resetPage: true });
2484 table.clearFilter();
2488 table.setFilter(table._dbxGlobalSearchFilter, {
2494 searchInput.addEventListener('input', () => {
2496 if (timer) clearTimeout(timer);
2498 if (opt.searchMode === 'remote') {
2499 timer = setTimeout(applyLocalSearch, 250);
2506 table.on('dataLoaded', () => {
2507 if (opt.searchMode === 'remote') {
2510 if (!searchInput.value.trim()) {
2517 table.on('tableBuilt', () => {
2518 table._dbxBuilt = true;
2519 if (table._dbxPageSizeState && table.getPageSize && table.getPageSize() !== table._dbxPageSizeState) {
2521 table._dbxPageSizeChanging = true;
2522 table.setPageSize(table._dbxPageSizeState);
2524 dbx.warn('[grid] restore page size failed', e);
2526 table._dbxPageSizeChanging = false;
2529 el._dbxApplyGridLines(false);
2530 this._applySortIndicators(table);
2534 /* =========================================================
2536 * ========================================================= */
2537 openColumnChooser(btn, table) {
2539 const el = table.element;
2540 const gridId = el.id || 'grid';
2542 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
2543 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
2545 const old = document.querySelector(`.dbx-col-chooser[data-grid-id="${gridId}"]`);
2546 if (old) old.remove();
2548 const box = document.createElement('div');
2549 box.className = 'dbx-col-chooser shadow p-2 bg-white border rounded';
2550 box.dataset.gridId = gridId;
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';
2558 const rect = btn.getBoundingClientRect();
2559 box.style.left = rect.left + 'px';
2560 box.style.top = (rect.bottom + 4) + 'px';
2562 const groupMap = {};
2565 this._getLeafColumns(table).forEach(col => {
2567 const field = col.getField();
2568 if (!field || field.startsWith('_')) return;
2572 let groupTitle = '-';
2573 const parent = col.getParentColumn();
2575 const def = parent.getDefinition();
2576 if (def?.title?.trim()) groupTitle = def.title.trim();
2579 if (!groupMap[groupTitle]) groupMap[groupTitle] = [];
2580 groupMap[groupTitle].push(col);
2583 const queueWidthRestore = () => {
2584 this._queueTableTimer(table, '_dbxChooserTimer', () => {
2585 if (!this._isTableLayoutReady(table)) return;
2586 this._restoreStoredColumnWidths(table, gridId);
2590 const saveVisibility = () => {
2591 allCols.forEach(c => {
2592 const field = c.getField();
2593 uiSet('COLUMNS.VISIBLE.' + field, c.isVisible() ? '1' : '0');
2597 Object.keys(groupMap).forEach(groupTitle => {
2599 const cols = groupMap[groupTitle];
2601 const groupLabel = document.createElement('label');
2602 groupLabel.className = 'fw-bold d-flex align-items-center gap-2 mb-1';
2604 const groupCb = document.createElement('input');
2605 groupCb.type = 'checkbox';
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);
2615 groupCb.addEventListener('change', () => {
2617 if (!this._isTableLayoutReady(table)) return;
2619 table.blockRedraw();
2622 const f = c.getField();
2623 if (!f || f.startsWith('_')) return;
2626 ? table.showColumn(f)
2627 : table.hideColumn(f);
2630 table.restoreRedraw(true);
2634 queueWidthRestore();
2637 groupLabel.appendChild(groupCb);
2638 groupLabel.appendChild(document.createTextNode(groupTitle));
2639 box.appendChild(groupLabel);
2641 cols.forEach(col => {
2643 const field = col.getField();
2644 const def = col.getDefinition();
2645 const labelText = def?.title ? def.title : field;
2647 const label = document.createElement('label');
2648 label.className = 'd-flex align-items-center gap-2 small ms-3';
2650 const cb = document.createElement('input');
2651 cb.type = 'checkbox';
2652 cb.checked = col.isVisible();
2654 cb.addEventListener('change', () => {
2656 if (!this._isTableLayoutReady(table)) return;
2658 table.blockRedraw();
2661 ? table.showColumn(field)
2662 : table.hideColumn(field);
2664 table.restoreRedraw(true);
2668 queueWidthRestore();
2671 label.appendChild(cb);
2672 label.appendChild(document.createTextNode(labelText));
2673 box.appendChild(label);
2676 box.appendChild(document.createElement('hr'));
2679 document.body.appendChild(box);
2682 const close = (e) => {
2683 if (!box.contains(e.target) && e.target !== btn) {
2685 document.removeEventListener('click', close);
2688 document.addEventListener('click', close);
2693 /* =========================================================
2694 * PARAMS / REMOTE STATE
2695 * ========================================================= */
2696 buildAjaxParams(table, params, optFallback) {
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);
2708 if (table && table._dbxSearchValue) {
2709 out.dbx_search = table._dbxSearchValue;
2712 const isRemote = table && table._dbxIsRemotePagination === true;
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;
2723 uiSet('PAGE.NO', page);
2724 this._storePageSizeState(gridId, normalizedSize, opt.pageSize || 15, opt.paginationSizeSelector);
2727 if (!table || typeof table.getHeaderFilters !== 'function') {
2731 const sorters = this._getAjaxSorters(table);
2732 if (Array.isArray(sorters) && sorters.length) {
2733 out.dbx_sorters = JSON.stringify(sorters);
2736 const headerFilters = table.getHeaderFilters ? table.getHeaderFilters() : [];
2737 if (Array.isArray(headerFilters) && headerFilters.length) {
2738 out.dbx_filters = JSON.stringify(headerFilters);
2745 /* =========================================================
2747 * ========================================================= */
2748 reloadTable(table, opt, cfg = {}) {
2750 const resetPage = !!cfg.resetPage;
2752 if (table._dbxSaving === true) return;
2754 if (table._dbxIsRemotePagination) {
2759 dbx.warn('[grid] remote reset page failed', e);
2762 table.replaceData();
2766 if (table._dbxIsProgressive) {
2767 table.setData(this._dbxAjaxUrl(opt.urls.read));
2771 table.replaceData(this._dbxAjaxUrl(opt.urls.read));
2774 insertRow(table, opt) {
2776 if (!table || !opt || !opt.urls || !opt.urls.insert) return;
2777 if (table._dbxSaving === true) return;
2779 const url = this._dbxAjaxUrl(opt.urls.insert);
2780 table._dbxSaving = true;
2782 this._dbxRequest(url, {
2785 'Content-Type': 'application/json'
2787 body: JSON.stringify({}),
2788 responseType: 'json'
2791 table._dbxSaving = false;
2793 if (!resp || !(resp.ok || resp.success)) {
2794 dbx.error('[grid] insert failed', resp);
2798 const row = resp.row || (Array.isArray(resp.rows) ? resp.rows[0] : null);
2800 table.addData([row], false);
2802 this.reloadTable(table, opt);
2806 table._dbxSaving = false;
2807 dbx.error('[grid] insert error', err);
2812 /* =========================================================
2814 * ========================================================= */
2815 bindSyncLoop(el, table, opt) {
2817 const syncUrl = opt.urls.sync;
2818 if (!syncUrl) return;
2820 const syncEls = this._getSyncEls(el);
2823 syncEls.led._dbxSyncLedEnabled = (opt.syncLed !== false);
2825 if (opt.syncLed === false) {
2826 syncEls.led.style.display = 'none';
2828 syncEls.led.style.display = 'inline-block';
2832 if (opt.syncRun === false) {
2833 dbx.log('[grid][sync] disabled by sync_run=0', {
2839 let synctime = parseFloat(opt.cfg.synctime || 2);
2840 if (isNaN(synctime)) synctime = 2;
2842 if (synctime === 0) return;
2843 if (synctime < 0.5) synctime = 0.5;
2844 if (synctime > 60) synctime = 60;
2846 const interval = Math.round(synctime * 1000);
2848 const loopId = 'grid-sync-' + (el.id || 'grid') + '-' + Date.now();
2849 table._dbxLoopId = loopId;
2850 table._dbxSyncRunning = false;
2851 table._dbxSyncMode = opt.syncMode || 'delta';
2853 dbx.log('[grid][sync] bind', {
2854 id: el.id || 'grid',
2857 mode: table._dbxSyncMode,
2858 remotePagination: table._dbxIsRemotePagination === true
2865 idle: Math.max(interval * 2, interval + 1000),
2866 hidden: Math.max(interval * 3, interval + 2000),
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;
2878 table._dbxSyncRunning = true;
2880 const startedAt = Date.now();
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
2890 let loadingTimer = setTimeout(() => {
2891 if (table._dbxSyncRunning === true) {
2892 dbx.log('[grid][sync] loader threshold reached', {
2895 this._setLedState(syncEls.led, 'loading');
2899 const editedMap = this._collectEditedMap(table);
2901 let url = this._dbxAjaxUrl(syncUrl, { background: true }) +
2902 '&last_update=' + encodeURIComponent(table._dbxServerTime);
2904 if (table._dbxIsRemotePagination) {
2905 url += '&dbx_page=' + encodeURIComponent(table.getPage() || 1);
2906 url += '&dbx_size=' + encodeURIComponent(table.getPageSize() || opt.pageSize || 50);
2909 return this._dbxRequest(url, {
2911 responseType: 'json'
2915 const rows = Array.isArray(res?.rows) ? res.rows : [];
2917 dbx.log('[grid][sync] response', {
2918 id: el.id || 'grid',
2921 count: (typeof res?.count !== 'undefined') ? res.count : null
2924 if (!res || res.ok !== 1) {
2925 this._setLedState(syncEls.led, 'idle');
2929 if (typeof res.server_time !== 'undefined' && res.server_time) {
2930 table._dbxServerTime = res.server_time;
2933 if (typeof res.count !== 'undefined') {
2934 this._setSyncCount(syncEls.count, res.count || '');
2938 this._setLedState(syncEls.led, 'idle');
2942 if (table._dbxIsRemotePagination) {
2944 dbx.log('[grid][sync] remote reload triggered', {
2945 id: el.id || 'grid',
2949 this.reloadTable(table, opt, {
2950 reason: 'sync-remote-delta'
2953 this._setLedState(syncEls.led, 'ok');
2959 for (let i = 0; i < rows.length; i++) {
2962 if (!r || typeof r.id === 'undefined') continue;
2964 const row = table.getRow(r.id);
2967 table.addData([r], false);
2972 const data = row.getData();
2973 const editedFields = editedMap[r.id] || null;
2976 for (const k in r) {
2978 if (k === 'id') continue;
2980 const newVal = r[k];
2981 const oldVal = data[k];
2984 newVal === oldVal ||
2985 String(newVal) === String(oldVal)
2990 if (editedFields && editedFields[k] === true) {
2992 const cell = row.getCell(k);
2993 if (cell && newVal !== oldVal) {
2994 const cellEl = cell.getElement();
2996 cellEl.classList.add('dbx-cell-conflict');
3006 const keys = Object.keys(patch);
3007 if (!keys.length) continue;
3013 const cell = row.getCell(k);
3015 this._applySchemaCellStyle(cell, row.getData());
3020 dbx.log('[grid][sync] local apply done', {
3021 id: el.id || 'grid',
3022 incoming: rows.length,
3026 this._setLedState(syncEls.led, changed > 0 ? 'ok' : 'idle');
3029 dbx.error('[grid][sync] error', err);
3030 this._setLedState(syncEls.led, 'error');
3033 clearTimeout(loadingTimer);
3034 loadingTimer = null;
3035 table._dbxSyncRunning = false;
3037 dbx.log('[grid][sync] request end', {
3038 id: el.id || 'grid',
3039 duration_ms: (Date.now() - startedAt)
3047 /* =========================================================
3049 * ========================================================= */
3050 createTable(el, opt) {
3052 if (el._dbxGridInitialized) return;
3053 el._dbxGridInitialized = true;
3055 const gridId = el.id || 'grid';
3056 opt._gridId = gridId;
3058 const uiGet = (k, def) => dbx.uiGet('grid', gridId, k, def);
3059 const uiSet = (k, v) => dbx.uiSet('grid', gridId, k, v);
3062 opt.cfg && typeof opt.cfg.schema === 'string'
3063 ? opt.cfg.schema.trim()
3066 const buildGrid = () => {
3068 const isRemotePagination =
3069 opt.pagination === true &&
3070 (opt.paginationMode === 'remote');
3072 const isProgressive =
3073 (opt.progressiveLoad === 'scroll' || opt.progressiveLoad === 'load');
3075 opt._dbxIsRemotePagination = isRemotePagination;
3076 opt._dbxIsProgressive = isProgressive;
3078 const columns = this.buildColumns(opt);
3080 const pageSizeStored = this._getPageSizeState(
3083 opt.paginationSizeSelector
3085 const pageSizeInitial = pageSizeStored === 1 ? 15 : pageSizeStored;
3086 opt.pageSize = pageSizeInitial;
3087 const pageNoStored = this._int(uiGet('PAGE.NO', 1), 1);
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
3096 : Math.min(heightMaxBound, Math.max(heightMinBound, initialHeightRaw));
3098 const paginationUi = this._getPaginationUiEls(el);
3100 dbx.log('[grid] createTable', {
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
3117 const ajaxURLGenerator = (url, config, params) => {
3119 let finalUrl = this._dbxAjaxUrl(url);
3120 const merged = this.buildAjaxParams(table, params || {}, opt);
3122 const usp = new URLSearchParams();
3124 Object.keys(merged).forEach(key => {
3125 const val = merged[key];
3126 if (val === undefined || val === null || val === '') return;
3127 usp.append(key, val);
3130 if (String(finalUrl).includes('?')) {
3131 finalUrl += '&' + usp.toString();
3133 finalUrl += '?' + usp.toString();
3136 dbx.log('[grid][ajaxURL]', {
3145 const ajaxRequestFunc = (url, config, params) => {
3148 (typeof config === 'string')
3150 : ((config && config.method) ? config.method : 'GET');
3152 dbx.log('[grid][ajaxRequestFunc]', {
3156 params: params || {}
3159 return this._dbxRequest(url, {
3161 responseType: 'json'
3165 const tabulatorOptions = {
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 ?? ''),
3176 sortMode: isRemotePagination ? 'remote' : 'local',
3178 filterMode: opt.searchMode === 'remote' ? 'remote' : 'local',
3180 ajaxURL: this._dbxAjaxUrl(opt.urls.read),
3182 ajaxContentType: 'json',
3183 ajaxURLGenerator: ajaxURLGenerator,
3184 ajaxRequestFunc: ajaxRequestFunc,
3185 ajaxResponse: (url, params, response) => this._ajaxResponse(table, url, params, response),
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,
3195 index: String(opt.cfg.index || 'id'),
3198 reactiveData: false,
3199 movableColumns: opt.movableColumns !== false,
3200 resizableColumns: opt.resizableColumns !== false,
3202 rowFormatter: function(row) {
3204 const rowEl = row.getElement();
3207 const schema = row.getTable().element._dbxSchemaParsed;
3208 if (!schema || !Array.isArray(schema.rows)) return;
3210 const data = row.getData();
3212 rowEl.style.removeProperty('background-color');
3213 rowEl.style.removeProperty('color');
3215 for (let i = 0; i < schema.rows.length; i++) {
3216 const rule = schema.rows[i];
3217 if (!dbxGrid.evalRule(rule, null, data)) continue;
3219 if (rule.style?.bg) {
3220 rowEl.style.setProperty('background-color', rule.style.bg, 'important');
3222 if (rule.style?.color) {
3223 rowEl.style.setProperty('color', rule.style.color, 'important');
3230 if (opt.pagination === true && opt.paginationControls === true && paginationUi.controls) {
3231 tabulatorOptions.paginationElement = paginationUi.controls;
3234 if (opt.pagination === true && opt.paginationCounter !== false) {
3235 tabulatorOptions.paginationCounter = opt.paginationCounter;
3237 if (paginationUi.counter) {
3238 tabulatorOptions.paginationCounterElement = paginationUi.counter;
3242 if (opt.pagination === true && opt.paginationSizeSelector !== false) {
3243 tabulatorOptions.paginationSizeSelector = opt.paginationSizeSelector;
3246 if (opt.pagination === true && opt.paginationOutOfRange !== false) {
3247 tabulatorOptions.paginationOutOfRange = opt.paginationOutOfRange;
3250 table = new Tabulator(el, tabulatorOptions);
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;
3265 const syncEls = this._getSyncEls(el);
3268 syncEls.led._dbxSyncLedEnabled = (opt.syncLed !== false);
3270 if (opt.syncLed === false) {
3271 syncEls.led.style.display = 'none';
3273 syncEls.led.style.display = 'inline-block';
3277 const queueLocalPageStabilize = (reason) => {
3279 if (table._dbxIsRemotePagination === true) return;
3280 if (table._dbxLayoutRestored !== true) return;
3282 this._queueTableTimer(table, '_dbxPageLayoutTimer', () => {
3284 if (!this._isTableLayoutReady(table) || table._dbxBuilt !== true) {
3285 this._queueTableTimer(table, '_dbxPageLayoutTimer', () => {
3286 queueLocalPageStabilize(reason);
3291 dbx.log('[grid] local page stabilize start', {
3294 page: table.getPage ? table.getPage() : null
3300 dbx.warn('[grid] local page redraw failed', e);
3303 this._applySortIndicators(table);
3305 dbx.log('[grid] local page stabilize done', {
3308 page: table.getPage ? table.getPage() : null
3314 table.on('pageLoaded', (pageno) => {
3315 uiSet('PAGE.NO', pageno);
3316 this._storePageSizeState(
3318 table._dbxPageSizeState || (table.getPageSize ? table.getPageSize() : opt.pageSize),
3320 opt.paginationSizeSelector
3323 dbx.log('[grid] pageLoaded', {
3326 pageSize: table.getPageSize()
3329 this._applyPaginationButtonLabels(table);
3331 if (table._dbxIsRemotePagination !== true) {
3332 queueLocalPageStabilize('pageLoaded');
3336 table.on('cellEdited', (cell) => {
3338 if (opt.allowEdit === false) return;
3340 this._markTableDirty(table, el);
3342 dbx.log('[grid] cellEdited', {
3344 rowId: cell?.getRow?.()?.getData?.()?.id,
3345 field: cell?.getField?.()
3348 const autosave = uiGet('AUTOSAVE', '1') == '1';
3349 if (!autosave) return;
3351 if (table._dbxAutoTimer) {
3352 clearTimeout(table._dbxAutoTimer);
3355 table._dbxAutoTimer = setTimeout(() => {
3357 if (table._dbxSaving === true) return;
3358 if (this._syncDirtyState(table) !== true) return;
3360 dbx.log('[grid] autosave trigger', {
3364 this.saveTable(table, opt);
3369 table.on('renderComplete', () => {
3370 if (opt.allowEdit !== false) {
3371 this.updateSaveButton(el, table);
3375 table.on('dataLoaded', (data) => {
3377 const syncEls = this._getSyncEls(el);
3379 if (typeof table._dbxSyncCount !== 'undefined') {
3380 this._setSyncCount(syncEls.count, table._dbxSyncCount || '');
3383 if (table._dbxPageSizeState === 1 && table._dbxPageSizeOneApplied !== true) {
3384 table._dbxPageSizeOneApplied = true;
3387 table._dbxPageSizeChanging = true;
3388 table.setPageSize(1);
3389 if (typeof table.setPage === 'function') {
3394 dbx.warn('[grid] restore page size 1 failed', e);
3396 table._dbxPageSizeChanging = false;
3400 this._applySortIndicators(table);
3401 this._applyPaginationButtonLabels(table);
3403 dbx.log('[grid] dataLoaded', {
3405 rows: Array.isArray(data) ? data.length : null,
3406 sortRestored: table._dbxSortRestored === true,
3407 remotePagination: table._dbxIsRemotePagination === true,
3408 progressive: table._dbxIsProgressive === true
3411 if (table._dbxIsRemotePagination !== true) {
3412 queueLocalPageStabilize('dataLoaded');
3415 if (!table._dbxSortRestored) {
3416 table._dbxSortRestored = true;
3417 this.bindSyncLoop(el, table, opt);
3421 table.on('dataSorted', () => {
3422 this._applySortIndicators(table);
3425 table.on('renderComplete', () => {
3426 this._applySortIndicators(table);
3427 this._applyPaginationButtonLabels(table);
3430 table.on('columnsLoaded', () => {
3432 if (table._dbxLayoutRestored === true) {
3433 dbx.log('[grid] columnsLoaded skipped (already restored)', {
3439 dbx.log('[grid] columnsLoaded -> restore layout start', {
3443 const tryRestore = () => {
3445 if (table._dbxLayoutRestored === true) return;
3447 const applied = this._applyInitialLayoutState(table, gridId);
3449 if (applied !== true) {
3450 this._queueTableTimer(table, '_dbxLayoutTimer', tryRestore, 30);
3454 table._dbxLayoutRestored = true;
3455 this._applySortIndicators(table);
3457 dbx.log('[grid] columnsLoaded -> restore layout done', {
3462 this._queueTableTimer(table, '_dbxLayoutTimer', tryRestore, 0);
3465 this.bindLayoutState(el, table);
3467 el._dbxTable = table;
3468 el._dbxFeature = this;
3475 opt._uiState || (opt._uiState = {}),
3476 el.closest('.dbx-grid')
3479 this.updateSaveButton(el, table);
3483 this.loadSchema(schemaName, () => {
3484 el._dbxSchemaParsed = dbxGridParseSchema(window.dbxGridSchema[schemaName]);
3492 /* =========================================================
3494 * ========================================================= */
3495 saveTable(table, opt) {
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'
3505 if (table._dbxSaving === true) {
3509 if (table._dbxDirty !== true && this._syncDirtyState(table) !== true) {
3510 table._dbxSaving = false;
3514 const editedCells = table.getEditedCells();
3516 if (!editedCells || editedCells.length === 0) {
3518 table._dbxSaving = false;
3519 table._dbxDirty = false;
3521 if (table.element && table.element._dbxFeature) {
3522 table.element._dbxFeature.updateSaveButton(table.element, table);
3530 editedCells.forEach(cell => {
3532 const row = cell.getRow();
3535 const data = row.getData();
3536 const idField = this._rowIdField(table);
3537 if (!data || typeof data[idField] === 'undefined') return;
3539 if (!rowsMap[data[idField]]) {
3540 rowsMap[data[idField]] = Object.assign({}, data);
3544 const rows = Object.values(rowsMap);
3548 table._dbxSaving = false;
3549 table._dbxDirty = false;
3551 if (table.element && table.element._dbxFeature) {
3552 table.element._dbxFeature.updateSaveButton(table.element, table);
3558 const url = this._dbxAjaxUrl(opt.urls.save);
3560 table._dbxSaving = true;
3562 this._dbxRequest(url, {
3565 'Content-Type': 'application/json'
3567 body: JSON.stringify({ rows: rows }),
3568 responseType: 'json'
3572 table._dbxSaving = false;
3573 table._dbxDirty = false;
3575 table.getEditedCells().forEach(cell => {
3581 this._clearConflictFlags(table);
3583 if (table.element && table.element._dbxFeature) {
3584 table.element._dbxFeature.updateSaveButton(table.element, table);
3589 table._dbxSaving = false;
3590 this._syncDirtyState(table);
3591 if (table.element && table.element._dbxFeature) {
3592 table.element._dbxFeature.updateSaveButton(table.element, table);
3594 dbx.error('[grid] SAVE failed', {
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() };
3611 function dbxGridParseColumnOptions(raw) {
3613 String(raw || '').split(';').forEach(part => {
3617 const pos = part.indexOf('=');
3623 const key = part.substring(0, pos).trim();
3624 const value = part.substring(pos + 1).trim();
3625 if (key) out[key] = value;
3631 function dbxGridParseEditorValues(raw) {
3634 String(raw || '').split('~').forEach(part => {
3635 const pos = part.indexOf('=');
3640 value = part.substring(0, pos);
3641 label = part.substring(pos + 1);
3644 values[value] = label;
3651 /* =================================================
3652 * [dbx][grid][schema][step2]
3653 * schema parser & normalizer
3654 * ================================================= */
3657 if (!window.dbx) return;
3659 window.dbxGridParseSchema = function(rawSchema) {
3662 meta: rawSchema.meta || {},
3663 conditions: rawSchema.conditions || {},
3668 if (Array.isArray(rawSchema.rows)) {
3669 rawSchema.rows.forEach(rowRule => {
3671 const norm = dbxGridNormalizeRule(
3680 bg: rowRule.bg || null,
3681 color: rowRule.color || null,
3682 cls: rowRule.cls || null
3685 out.rows.push(norm);
3689 if (!rawSchema.columns || typeof rawSchema.columns !== 'object') {
3693 Object.keys(rawSchema.columns).forEach(colName => {
3695 const colDef = rawSchema.columns[colName];
3696 if (!colDef || !Array.isArray(colDef.rules)) return;
3698 out.columns[colName] = { rules: [] };
3700 colDef.rules.forEach(rule => {
3702 const norm = dbxGridNormalizeRule(
3703 Object.assign({}, rule, { col: colName }),
3710 out.columns[colName].rules.push(norm);
3717 function dbxGridNormalizeRule(rule, conditions, currentCol) {
3719 if (!rule || typeof rule !== 'object') return null;
3721 const resolveCondition = (c) => {
3722 if (typeof c === 'string') {
3723 if (!conditions[c]) {
3724 console.warn('[normalize] unknown condition', c);
3727 return Object.assign({}, conditions[c]);
3729 return Object.assign({}, c);
3732 if (Array.isArray(rule.all)) {
3734 if (rule.all.length === 0) {
3738 bg: rule.bg || null,
3739 color: rule.color || rule.text || null,
3740 cls: rule.cls || null
3745 const subs = rule.all
3746 .map(resolveCondition)
3747 .map(r => dbxGridNormalizeRule(r, conditions, currentCol))
3750 if (!subs.length) return null;
3755 bg: rule.bg || null,
3756 color: rule.color || rule.text || null,
3757 cls: rule.cls || null
3762 if (Array.isArray(rule.any)) {
3764 const subs = rule.any
3765 .map(resolveCondition)
3766 .map(r => dbxGridNormalizeRule(r, conditions, currentCol))
3769 if (!subs.length) return null;
3774 bg: rule.bg || null,
3775 color: rule.color || rule.text || null,
3776 cls: rule.cls || null
3781 const col = rule.col || currentCol;
3784 console.warn('[normalize] rule dropped (no col)', rule);
3791 normalize: rule.normalize,
3793 isReserved: rule.isReserved || false,
3795 bg: rule.bg || null,
3796 color: rule.color || rule.text || null,
3797 cls: rule.cls || null
3805 /* =================================================
3806 * [dbx][grid][schema][step3]
3808 * ================================================= */
3811 window.dbxGrid.evalCell = function(colRules, cellValue, rowData) {
3813 if (!colRules || !Array.isArray(colRules.rules)) {
3817 let finalStyle = null;
3818 let matched = false;
3820 for (let i = 0; i < colRules.rules.length; i++) {
3822 const rule = colRules.rules[i];
3823 const ok = window.dbxGrid.evalRule(rule, cellValue, rowData);
3830 finalStyle = Object.assign({}, finalStyle || {}, rule.style);
3835 return finalStyle || {};
3841 window.dbxGrid.evalRule = function(rule, cellValue, rowData) {
3843 if (!rule) return false;
3845 if (Array.isArray(rule.all)) {
3846 return rule.all.every(r =>
3847 window.dbxGrid.evalRule(r, cellValue, rowData)
3851 if (Array.isArray(rule.any)) {
3852 return rule.any.some(r =>
3853 window.dbxGrid.evalRule(r, cellValue, rowData)
3859 if (rule.col === '$cell') {
3861 } else if (rule.col) {
3862 left = rowData[rule.col];
3867 if (typeof left === 'string' && rule.normalize === 'trim') {
3871 const right = dbxResolveCompareValue(
3873 rule.isReserved || false,
3877 return dbxCompare(left, rule.if, right);
3880 function dbxResolveCompareValue(value, isReserved, rowData) {
3883 if (value === 'today') {
3884 const d = new Date();
3885 d.setHours(0, 0, 0, 0);
3889 if (typeof value === 'string' && /^[+-]\d+(day|month|year)s?$/.test(value)) {
3890 return dbxShiftDate(new Date(), value);
3896 if (typeof value === 'string' && value.charAt(0) === '$') {
3897 return rowData[value.substring(1)];
3903 function dbxShiftDate(base, expr) {
3904 const d = new Date(base);
3905 const n = parseInt(expr, 10);
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);
3914 function dbxCompare(left, op, right) {
3916 if (left === null || left === undefined) left = '';
3917 if (right === null || right === undefined) right = '';
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;
3926 const l = dbxToComparable(left);
3927 const r = dbxToComparable(right);
3929 if (l === null || r === null) return false;
3931 if (op === '<') return l < r;
3932 if (op === '<=') return l <= r;
3933 if (op === '>') return l > r;
3934 if (op === '>=') return l >= r;
3939 function dbxToComparable(v) {
3941 if (v instanceof Date) return v.getTime();
3943 if (typeof v === 'string') {
3944 const d = dbxParseDate(v);
3945 if (d) return d.getTime();
3948 if (!isNaN(v)) return Number(v);
3953 window.dbxParseDate = function(v) {
3955 if (!v) return null;
3957 if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
3958 const d = new Date(v);
3959 return isNaN(d) ? null : d;
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;
3974 /* =================================================
3975 * [dbx][grid][schema][step4]
3976 * apply style helper
3977 * ================================================= */
3978 window.dbxGridApplyCellStyle = function(cell, style) {
3980 const el = cell.getElement();
3981 if (!el || !style) return;
3984 el.style.removeProperty('background-color');
3985 el.style.setProperty('background-color', style.bg, 'important');
3989 el.style.removeProperty('color');
3990 el.style.setProperty('color', style.color, 'important');
3994 el.classList.add(style.cls);