import { Component, NewState, Util, RefreshState } from '@web/state' import { State } from '@web/base' import { createPerfMonitor } from './perf.js' import { createScrollManager } from './scroll.js' import { createSelectionManager } from './selection.js' // Static configuration maps const MODE_MAP = { text: ['contains', 'equals', 'starts', 'ends'], textarea: ['contains', 'equals', 'starts', 'ends'], number: ['=', '>', '<', 'between'], date: ['=', '>', '<', 'between'], select: ['contains', 'equals'], TagsInput: ['contains', 'equals', 'starts', 'ends'] }; const MODE_ICONS = { 'contains': 'bi-search', 'equals': 'bi-distribute-vertical', 'starts': 'bi-align-start', 'ends': 'bi-align-end', '=': 'bi-calculator', '>': 'bi-chevron-right', '<': 'bi-chevron-left', 'between': 'bi-arrows-expand' }; export const DataTable = { _fieldTypes: new Map(), registerFieldType: (config) => { DataTable._fieldTypes.set(config.value, config); }, getFieldTypes: () => Array.from(DataTable._fieldTypes.values()) }; // Register Built-in Types DataTable.registerFieldType({ value: 'text', label: '{#Text#}', typeForDB: 'v4096', schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text', if: 'this.data.user_type=="text"' }] }); DataTable.registerFieldType({ value: 'number', label: '{#Number#}', typeForDB: 'ff', schema: [ { name: 'decimals', label: 'Decimals', type: 'number', setting: { min: 0, max: 10 }, if: 'this.data.user_type=="number"' }, { name: 'prefix', label: 'Prefix (e.g. $)', type: 'text', if: 'this.data.user_type=="number"' }, { name: 'suffix', label: 'Suffix (e.g. %)', type: 'text', if: 'this.data.user_type=="number"' }, { name: 'thousandSep', label: 'Thousand Sep', type: 'switch', if: 'this.data.user_type=="number"' } ], formatter: (val, field) => { if (val == null || val === '') return ''; let num = Number(val); if (isNaN(num)) return val; const s = field.settings || {}; if (s.decimals !== undefined) num = num.toFixed(s.decimals); let str = String(num); if (s.thousandSep) { const parts = str.split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); str = parts.join('.'); } return (s.prefix || '') + str + (s.suffix || ''); } }); DataTable.registerFieldType({ value: 'select', label: '{#Single Select#}', typeForDB: 'v1024', schema: [{ name: 'options_str', label: 'Options', type: 'textarea', setting: { rows: 3, placeholder: 'Label:Value per line' }, if: 'this.data.user_type=="select"' }], formatter: (val, field) => { if (val == null || val === '') return ''; const opts = field.settings?.options || []; const opt = opts.find(o => typeof o === 'object' ? o.value == val : o == val); return opt ? (typeof opt === 'object' ? opt.label : opt) : val; } }); DataTable.registerFieldType({ value: 'checkbox', label: '{#Multi Select#}', typeForDB: 'v4096', schema: [{ name: 'options_str', label: 'Options', type: 'textarea', setting: { rows: 3, placeholder: 'Label:Value per line' }, if: 'this.data.user_type=="checkbox"' }], formatter: (val, field) => { if (!Array.isArray(val)) return val == null ? '' : String(val); const opts = field.settings?.options || []; return val.map(v => { const opt = opts.find(o => typeof o === 'object' ? o.value == v : o == v); return opt ? (typeof opt === 'object' ? opt.label : opt) : v; }).join(', '); } }); DataTable.registerFieldType({ value: 'switch', label: '{#Switch#}', typeForDB: 'b', schema: [ { name: 'labelOn', label: 'Label On', type: 'text', if: 'this.data.user_type=="switch"' }, { name: 'labelOff', label: 'Label Off', type: 'text', if: 'this.data.user_type=="switch"' } ], formatter: (val, field) => { const s = field.settings || {}; return val ? (s.labelOn || 'Yes') : (s.labelOff || 'No'); } }); DataTable.registerFieldType({ value: 'datetime', label: '{#DateTime#}', typeForDB: 'dt', schema: [{ name: 'format', label: 'Format', type: 'text', setting: { placeholder: 'YYYY-MM-DD' }, if: 'this.data.user_type=="datetime"' }] }); DataTable.registerFieldType({ value: 'textarea', label: '{#Long Text#}', typeForDB: 't', schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text', if: 'this.data.user_type=="textarea"' }] }); Component.register('DataTable', container => { if (!container.state) container.state = NewState({}) const state = container.state Object.assign(state, { list: [], fields: [], _renderedList: [], prevHeight: 0, postHeight: 0, _listStartIndex: 0, selectedRowCount: 0, _originalList: [], sortConfig: { fieldId: null, direction: null }, filterConfig: {}, // fieldId -> { mode, value, value2, selectedValues: [] } activeFieldId: null, activeField: null, activeModes: [], _columnStats: {}, _internalUpdate: false, _appliedHash: '', _fieldsDirty: false, _masterCellNodes: null, isDirty: false, isBulkEdit: null }) const perf = createPerfMonitor(); state.perf = perf.stats; const selection = createSelectionManager(container, state); const scroll = createScrollManager(container, state, () => selection.applySelectionUI()); const menuNode = container.querySelector('.dt-column-menu'); if (menuNode) menuNode._thisObj = container; container.onColumnResizing = (field, e) => container.style.setProperty(`--w-${field.id}`, e.detail.newSize + 'px'); container.onColumnResize = (field, e) => { const idx = state.fields.findIndex(f => f.id === field.id); if (idx !== -1) { state.fields[idx].width = e.detail.newSize; state.fields = [...state.fields]; } }; let _editorOverlay, currentEditingNode = null; container.format = (val, field) => { if (field.formatter) return field.formatter(val, field); const typeInfo = DataTable._fieldTypes.get(field.settings?.formType || field.type || 'text'); if (typeInfo && typeInfo.formatter) return typeInfo.formatter(val, field); return val == null ? '' : (typeof val === 'object' ? JSON.stringify(val) : String(val)); }; container.onScroll = () => { perf.onScroll(); scroll.refresh(); container.hideColumnMenu(); const prev = container.querySelector('.dt-spacer-prev'), post = container.querySelector('.dt-spacer-post'); if (prev) { prev.style.height = (state.prevHeight || 0) + 'px'; prev.style.display = state.prevHeight > 0 ? 'block' : 'none'; } if (post) { post.style.height = (state.postHeight || 0) + 'px'; post.style.display = state.postHeight > 0 ? 'block' : 'none'; } }; container.applySortFilter = (options = {}) => { if (state._internalUpdate) return; const targetFilters = { ...state.filterConfig, ...(options.filters || {}) }; const targetSort = options.sort !== undefined ? (options.sort ? { fieldId: state.activeFieldId, direction: options.sort } : { fieldId: null, direction: null }) : state.sortConfig; const currentHash = JSON.stringify({ s: targetSort, f: targetFilters }); if (state._appliedHash === currentHash && !options.force) return; state._internalUpdate = true; let list = [...(state._originalList || [])]; Object.keys(targetFilters).forEach(fieldId => { const filter = targetFilters[fieldId]; if (!filter) return; const { mode = 'contains', value, value2, selectedValues } = filter; if (selectedValues?.length > 0) { list = list.filter(item => selectedValues.includes(String(item[fieldId] ?? ''))); return; } if (value === '' || value == null) return; const lowV = String(value).toLowerCase(), n1 = Number(value), n2 = Number(value2); list = list.filter(item => { const iv = item[fieldId], sv = String(iv ?? '').toLowerCase(); switch (mode) { case 'contains': return sv.includes(lowV); case 'equals': return sv === lowV; case 'starts': return sv.startsWith(lowV); case 'ends': return sv.endsWith(lowV); case '>': return Number(iv) > n1; case '<': return Number(iv) < n1; case '=': return Number(iv) === n1; case 'between': return Number(iv) >= n1 && Number(iv) <= n2; default: return sv.includes(lowV); } }); }); if (targetSort.fieldId && targetSort.direction) { list.sort((a, b) => { let va = a[targetSort.fieldId], vb = b[targetSort.fieldId]; if (va === vb) return 0; const res = va > vb ? 1 : -1; return targetSort.direction === 'asc' ? res : -res; }); } state._appliedHash = currentHash; state.sortConfig = targetSort; state.list = list; state._internalUpdate = false; }; container.showColumnMenu = (field, e) => { e.stopPropagation(); const btn = e.currentTarget, menu = container.querySelector('.dt-column-menu'); const type = field.type || 'text'; state.activeModes = MODE_MAP[type] || (['boolean', 'switch', 'checkbox', 'radio'].includes(type) ? [] : MODE_MAP.text); if (!state.filterConfig[field.id]) { state.filterConfig[field.id] = { mode: state.activeModes[0] || 'contains', value: '', selectedValues: [] }; } state.activeField = field; state.activeFieldId = field.id; menu.style.display = 'block'; const cellNode = btn.closest('.dt-cell'); const rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect(); const menuWidth = menu.offsetWidth || 260; let leftPos = rect.right - rootRect.left - menuWidth; if (leftPos < 0) leftPos = Math.max(0, rect.left - rootRect.left); menu.style.left = leftPos + 'px'; menu.style.top = (rect.bottom - rootRect.top + 5) + 'px'; const onGlobalClick = (ev) => { if (menu.contains(ev.target) || btn.contains(ev.target)) return; container.hideColumnMenu(); container.applySortFilter(); document.removeEventListener('mousedown', onGlobalClick); }; document.addEventListener('mousedown', onGlobalClick); setTimeout(() => menu.querySelector('input')?.focus(), 50); }; container.toggleSelectedValue = (val) => { const filter = state.filterConfig[state.activeFieldId]; if (!filter) return; const idx = filter.selectedValues.indexOf(val); if (idx === -1) filter.selectedValues.push(val); else filter.selectedValues.splice(idx, 1); state.filterConfig = { ...state.filterConfig }; container.applySortFilter(); }; container.filterOnlyThis = (val) => { state.filterConfig[state.activeFieldId] = { mode: 'contains', value: '', selectedValues: [String(val)] }; state.filterConfig = { ...state.filterConfig }; container.applySortFilter(); }; container.hideColumnMenu = () => { const menu = container.querySelector('.dt-column-menu'); if (menu) menu.style.display = 'none'; }; container.setSort = (dir) => { const newDir = state.sortConfig.direction === dir && state.sortConfig.fieldId === state.activeFieldId ? null : dir; container.applySortFilter({ sort: newDir }); }; container.clearColumnSettings = () => { if (state.activeFieldId) { delete state.filterConfig[state.activeFieldId]; state.filterConfig = { ...state.filterConfig }; container.applySortFilter(); } }; container._initRow = (rowNode) => { const row = rowNode._ref?.item; if (row && row._editingF === undefined) { Object.defineProperty(row, '_editingF', { set: (v) => { if (v === null) container.hideEditor(true); }, configurable: true }); } Array.from(rowNode.children).forEach(cell => { const fIdx = parseInt(cell.dataset.fidx); if (!isNaN(fIdx)) cell._refExt = { f: state.fields[fIdx], fIdx: fIdx }; }); }; state.__watch('fields', fields => { if (!fields) return; state._fieldsDirty = true; state._masterCellNodes = null; // Force template rebuild container.style.setProperty('--dt-grid-template', fields.map(f => `var(--w-${f.id}, ${(f.settings?.width || f.width) || 150}px)`).join(' ')); container.style.setProperty('--dt-row-width', fields.reduce((sum, f) => sum + ((f.settings?.width || f.width) || 150), 0) + 'px'); let leftSum = 0; fields.forEach(f => { const pinned = f.settings?.pinned || f.pinned; if (pinned === 'left') { container.style.setProperty(`--l-${f.id}`, leftSum + 'px'); leftSum += ((f.settings?.width || f.width) || 150); } }); let rightSum = 0; [...fields].reverse().forEach(f => { const pinned = f.settings?.pinned || f.pinned; if (pinned === 'right') { container.style.setProperty(`--r-${f.id}`, rightSum + 'px'); rightSum += ((f.settings?.width || f.width) || 150); } }); }); state.__watch('list', list => { if (state._fieldsDirty) { state._fieldsDirty = false; const fieldTemplate = container.querySelector('.dt-body template[index="rIdx"]')?.content.querySelector('template[as="f"]'); if (fieldTemplate) { const masters = state._masterCellNodes || (state._masterCellNodes = Array.from(fieldTemplate.content.childNodes).map(n => n.cloneNode(true))); fieldTemplate.removeAttribute('$each'); fieldTemplate.setAttribute('$if', 'true'); fieldTemplate.content.textContent = ''; state.fields.forEach((f, fIdx) => masters.forEach(master => { const clone = master.cloneNode(true); if (clone.nodeType === 1) { clone.dataset.fidx = fIdx; const pinned = f.settings?.pinned || f.pinned; if (pinned) { clone.classList.add('pinned-' + pinned); clone.style.position = 'sticky'; clone.style.zIndex = '1'; clone.style.backgroundColor = 'inherit'; if (pinned === 'left') { clone.style.left = `var(--l-${f.id})`; clone.style.borderRight = '1px solid var(--bs-border-color)'; clone.style.boxShadow = '2px 0 5px -2px rgba(0,0,0,0.1)'; } else { clone.style.right = `var(--r-${f.id})`; clone.style.borderLeft = '1px solid var(--bs-border-color)'; clone.style.boxShadow = '-2px 0 5px -2px rgba(0,0,0,0.1)'; } } } fieldTemplate.content.appendChild(clone); })); } } if (!state._internalUpdate) { state._originalList = [...(list || [])]; setTimeout(() => { const stats = {}; state.fields.forEach(f => { const counts = {}; state._originalList.forEach(item => { const val = item[f.id], key = (val == null || val === '') ? '' : String(val); counts[key] = (counts[key] || 0) + 1; }); stats[f.id] = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([val, count]) => ({ val, count })); }); state._columnStats = stats; }, 200); } scroll.init(); scroll.reset(list); }); container.editCell = (row, field, cellNode) => { const overlay = container.querySelector('.dt-editor-overlay'), rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect(); currentEditingNode = cellNode; const formType = field.settings?.formType || field.type || 'text'; const form = overlay.querySelector('AutoForm'); if (form) { // row 已经是 Proxy (NewState),直接赋值即可实现双向同步 form.data = row; form.state.schema = [{ ...field, type: formType, options: field.settings?.options || field.options, name: field.id, label: '' }]; RefreshState(form); } Object.assign(overlay.style, { display: 'flex', left: (rect.left - rootRect.left) + 'px', top: (rect.top - rootRect.top) + 'px', width: (formType === 'textarea' || formType === 'TagsInput' ? Math.max(rect.width, 300) : rect.width) + 'px', height: (formType === 'textarea' || formType === 'TagsInput' ? 'auto' : rect.height + 'px') }); setTimeout(() => overlay.querySelector('input, textarea, select, .form-control')?.focus(), 30); }; container.hideEditor = (save = true) => { if (!_editorOverlay) _editorOverlay = container.querySelector('.dt-editor-overlay'); if (!_editorOverlay || _editorOverlay.style.display === 'none') return; const form = _editorOverlay.querySelector('AutoForm'); if (save && form && form.data) { const input = _editorOverlay.querySelector('input:focus, select:focus, textarea:focus'); if (input) input.dispatchEvent(new Event(input.type === 'number' || input.tagName === 'SELECT' ? 'change' : 'input', { bubbles: true })); RefreshState(form); // 手动同步数据回原始行 (解决 Proxy 隔离问题) const schema = form.state.schema || []; schema.forEach(field => { const row = currentEditingNode?.closest('.dt-row')?._ref?.item; if (row) row[field.name] = form.data[field.name]; }); if (state.isBulkEdit) { const { minRow, maxRow, fIdx } = state.isBulkEdit; const field = state.fields[fIdx]; const newValue = form.data[field.id]; for (let i = minRow; i <= maxRow; i++) { if (state.list[i]) { state.list[i][field.id] = newValue; } } state.list = [...state.list]; } if (currentEditingNode) RefreshState(currentEditingNode); state.isDirty = true; } _editorOverlay.style.display = 'none'; if (form) { form.state.schema = []; form.data = null; } currentEditingNode = null; state.isBulkEdit = null; container.focus(); }; container.onMainMouseDown = e => { const cell = e.target.closest('.dt-cell'), row = cell?.closest('.dt-row'); if (!row || row.classList.contains('dt-header-row')) return; const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll('.dt-cell')).indexOf(cell); const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row); selection.startSelect(rIdx + state._listStartIndex, fIdx, e); }; container.onMainMouseOver = e => { if (state.isSelecting) { const cell = e.target.closest('.dt-cell'), row = cell?.closest('.dt-row'); if (row && !row.classList.contains('dt-header-row')) { const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll('.dt-cell')).indexOf(cell); const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row); selection.updateSelect(rIdx + state._listStartIndex, fIdx); } } }; container.onMainDblClick = e => { const cell = e.target.closest('.dt-cell'), row = cell?.closest('.dt-row'); if (row && !row.classList.contains('dt-header-row')) { const item = row._ref?.item, fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll('.dt-cell')).indexOf(cell); const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row); const absoluteRow = rIdx + state._listStartIndex; if (item && state.fields[fIdx]) { const bounds = selection.getSelectionBounds(); if (bounds && absoluteRow >= bounds.minRow && absoluteRow <= bounds.maxRow && fIdx >= bounds.minCol && fIdx <= bounds.maxCol) { const affectedRows = bounds.maxRow - bounds.minRow + 1; if (affectedRows > 1) { state.isBulkEdit = { ...bounds, fIdx }; if (globalThis.UI?.toast) UI.toast(`Bulk Edit: Updating ${affectedRows} rows in column "${state.fields[fIdx].name}"`, { type: 'warning' }); } } container.editCell(item, state.fields[fIdx], cell); } } }; container.addRow = () => { const newRow = {}; state.fields.forEach(f => newRow[f.id] = ''); state._originalList.push(newRow); state.list = [...state._originalList]; // Apply to current view too state.isDirty = true; setTimeout(() => { scroll.reset(state.list); container.querySelector('.dt-main').scrollTop = container.querySelector('.dt-main').scrollHeight; }, 50); }; container.deleteSelectedRow = async () => { const bounds = selection.getSelectionBounds(); if (!bounds) return; const count = bounds.maxRow - bounds.minRow + 1; if (await UI.confirm(`Are you sure you want to delete ${count} row(s)?`)) { const rMin = bounds.minRow, rMax = bounds.maxRow; const removedItems = state.list.slice(rMin, rMax + 1); state.list = state.list.filter((_, i) => !(i >= rMin && i <= rMax)); state._originalList = state._originalList.filter(item => !removedItems.includes(item)); state.isDirty = true; selection.clearAllActive(); container.dispatchEvent(new CustomEvent('remove', { detail: { items: removedItems } })); } }; container.saveChanges = () => { container.dispatchEvent(new CustomEvent('save', { detail: { list: state._originalList, fields: state.fields } })); state.isDirty = false; }; const getFieldSchema = () => { const types = DataTable.getFieldTypes(); const baseSchema = [ { name: 'id', label: 'Field ID', type: 'text', setting: { required: true, placeholder: 'e.g. user_name' } }, { name: 'name', label: 'Display Name', type: 'text', setting: { required: true, placeholder: 'e.g. 用户名' } }, { name: 'user_type', label: 'Field Type', type: 'select', options: types.map(t => ({ label: t.label, value: t.value })) } ]; const dynamicSchema = types.reduce((acc, t) => acc.concat(t.schema || []), []); return baseSchema.concat(dynamicSchema, [ { name: 'isIndex', label: 'Index', type: 'switch' }, { name: 'memo', label: 'Memo', type: 'text' } ]); }; const parseOptionsStr = (str) => { if (!str) return undefined; return str.split('\n').map(s => s.trim()).filter(Boolean).map(line => { const idx = line.indexOf(':'); if (idx > -1) return { label: line.slice(0, idx).trim(), value: line.slice(idx + 1).trim() }; return line; }); }; const formatOptionsStr = (opts) => { if (!opts) return ''; return opts.map(o => typeof o === 'object' ? `${o.label}:${o.value}` : o).join('\n'); }; container.addField = async () => { container.hideColumnMenu(); const data = NewState({ id: 'c' + Date.now().toString().slice(-4), name: 'New Field', user_type: 'text', decimals: 0, isIndex: false, memo: '', options_str: '' }); const d = document.body.appendChild(document.createElement('Dialog')); await new Promise(r => setTimeout(r, 0)); Object.assign(d.state, { title: 'Add Field', buttons: ['Cancel', 'Save'] }); RefreshState(d); const body = d.querySelector('.modal-body'); const form = body.appendChild(document.createElement('AutoForm')); form.setAttribute('nobutton', ''); RefreshState(form); form.data = data; form.state.schema = getFieldSchema(); form.addEventListener('change', (e) => e.stopPropagation()); d.show(); const result = await new Promise(resolve => d.addEventListener('change', e => resolve(d.result))); if (result === 2) { const typeInfo = DataTable._fieldTypes.get(data.user_type); let dbType = typeInfo?.typeForDB || 'v1024'; if (data.user_type === 'number') dbType = data.decimals > 0 ? 'ff' : 'bi'; const field = { id: data.id, name: data.name, memo: data.memo, isIndex: !!data.isIndex, type: dbType, settings: { formType: data.user_type, decimals: data.decimals, prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep, labelOn: data.labelOn, labelOff: data.labelOff, format: data.format, placeholder: data.placeholder, options: parseOptionsStr(data.options_str) } }; state.fields = [...state.fields, field]; state.isDirty = true; container.dispatchEvent(new CustomEvent('savefields', { detail: state.fields })); state.list = [...state.list]; } d.remove(); }; container.editField = async () => { if (!state.activeField) return; container.hideColumnMenu(); const f = state.activeField; const s = f.settings || {}; const data = NewState({ id: f.id, name: f.name, memo: f.memo || '', isIndex: !!f.isIndex, user_type: s.formType || 'text', decimals: s.decimals || 0, prefix: s.prefix || '', suffix: s.suffix || '', thousandSep: !!s.thousandSep, labelOn: s.labelOn || '', labelOff: s.labelOff || '', format: s.format || '', placeholder: s.placeholder || '', options_str: formatOptionsStr(s.options) }); const d = document.body.appendChild(document.createElement('Dialog')); await new Promise(r => setTimeout(r, 0)); Object.assign(d.state, { title: 'Edit Field', buttons: ['Cancel', 'Save'] }); RefreshState(d); const body = d.querySelector('.modal-body'); const form = body.appendChild(document.createElement('AutoForm')); form.setAttribute('nobutton', ''); RefreshState(form); form.data = data; form.state.schema = getFieldSchema(); form.addEventListener('change', (e) => e.stopPropagation()); d.show(); const result = await new Promise(resolve => d.addEventListener('change', e => resolve(d.result))); if (result === 2) { const idx = state.fields.findIndex(item => item.id === f.id); if (idx !== -1) { const typeInfo = DataTable._fieldTypes.get(data.user_type); let dbType = typeInfo?.typeForDB || 'v1024'; if (data.user_type === 'number') dbType = data.decimals > 0 ? 'ff' : 'bi'; const updatedField = { ...f, id: data.id, name: data.name, memo: data.memo, isIndex: !!data.isIndex, type: dbType, settings: { ...f.settings, formType: data.user_type, decimals: data.decimals, prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep, labelOn: data.labelOn, labelOff: data.labelOff, format: data.format, placeholder: data.placeholder, options: parseOptionsStr(data.options_str) } }; state.fields[idx] = updatedField; state.fields = [...state.fields]; state.isDirty = true; container.dispatchEvent(new CustomEvent('savefields', { detail: state.fields })); state.list = [...state.list]; } } d.remove(); }; container.deleteField = async () => { if (!state.activeField) return; container.hideColumnMenu(); if (await UI.confirm(`Are you sure you want to delete field "${state.activeField.name}"?`)) { const idx = state.fields.findIndex(f => f.id === state.activeField.id); if (idx !== -1) { state.fields.splice(idx, 1); state.fields = [...state.fields]; state.isDirty = true; container.dispatchEvent(new CustomEvent('savefields', { detail: state.fields })); state.list = [...state.list]; // Trigger body refresh } } }; window.addEventListener('mouseup', selection.endSelect); document.addEventListener('mousedown', e => { const overlay = container.querySelector('.dt-editor-overlay'); const menu = container.querySelector('.dt-column-menu'); if (overlay?.style.display !== 'none' && !overlay.contains(e.target)) container.hideEditor(true); if (!container.contains(e.target) && !overlay?.contains(e.target) && !menu?.contains(e.target)) selection.clearAllActive(); }); // Exposure for templates state._MODE_ICONS = MODE_ICONS; // NOTE: For $class and $style directives, ALWAYS use the template literal syntax: // $class="base-class \${condition ? 'active' : ''}" // DO NOT use string concatenation like $class="'base-class ' + (condition ? 'active' : '')". // Since the HTML is wrapped in backticks (``), remember to escape the dollar sign: \${ } }, Util.makeDom(/*html*/`
`), Util.makeDom(/*html*/` `))