1
0
Files
breadcrumb-the-shire/web/js/core/app-grid-factory.js
fs a0d7670dd7 feat(helpdesk): align module with core list/drawer standards
- add helpdesk module pages, services, settings and tests

- standardize debtor list on drawer/grid contracts and robust filter drawer behavior

- add helpdesk aside panel navigation and settings visibility provider

- switch primary list slug to helpdesk/debitor and remove helpdesk/search compatibility

- include required core contract updates for list contracts and detail/drawer integration
2026-04-02 17:48:27 +02:00

1522 lines
52 KiB
JavaScript

import { warnOnce } from './app-dom.js';
import { confirmDialog } from './app-confirm-dialog.js';
import { requestFsLightboxRefresh } from './app-fslightbox-bridge.js';
import { createUiStorage } from './app-ui-storage.js';
import { gridFiltersFromSchema, withCurrentListReturn } from '../pages/app-list-utils.js';
import { initListFilterExperience } from '../pages/app-list-filter-experience.js';
import { buildChipsFromMeta, removeChipFromMetaState, clearMetaState } from '../pages/app-list-filter-state.js';
const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
const PAGE_SIZE_STORAGE_PREFIX = 'grid:page-size';
const parsePositiveInt = (value) => {
const parsed = Number.parseInt(String(value ?? ''), 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return parsed;
};
const normalizePageSizeOptions = (options) => {
const source = Array.isArray(options) && options.length
? options
: DEFAULT_PAGE_SIZE_OPTIONS;
const normalized = [];
source.forEach((option) => {
const parsed = parsePositiveInt(option);
if (parsed !== null && !normalized.includes(parsed)) {
normalized.push(parsed);
}
});
return normalized.length ? normalized : [...DEFAULT_PAGE_SIZE_OPTIONS];
};
const escapeHtmlAttr = (value) => String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
const normalizeColumnIdPart = (value) => String(value ?? '')
.trim()
.toLowerCase()
.replaceAll(/[^a-z0-9]+/g, '_')
.replaceAll(/^_+|_+$/g, '');
const uniqueColumnId = (candidate, usedIds, fallbackPrefix = 'col') => {
const base = normalizeColumnIdPart(candidate) || fallbackPrefix;
let next = base;
let counter = 2;
while (usedIds.has(next)) {
next = `${base}_${counter}`;
counter += 1;
}
usedIds.add(next);
return next;
};
const DEFAULT_MIN_WIDTH = '100px';
const normalizeGridColumns = (columns, sortColumns) => {
const source = Array.isArray(columns) ? columns : [];
const sortKeys = Array.isArray(sortColumns) ? sortColumns : [];
const usedIds = new Set();
return source.map((column, index) => {
const baseColumn = (column && typeof column === 'object' && !Array.isArray(column))
? column
: { name: String(column ?? '') };
const explicitId = typeof baseColumn.id === 'string' ? baseColumn.id : '';
const sortKey = typeof sortKeys[index] === 'string' ? sortKeys[index] : '';
const label = typeof baseColumn.name === 'string' ? baseColumn.name : '';
const fallback = `col_${index + 1}`;
const id = uniqueColumnId(explicitId || sortKey || label || fallback, usedIds, fallback);
const skipMinWidth = baseColumn.hidden || baseColumn.plugin || baseColumn.minWidth;
const minWidth = skipMinWidth ? baseColumn.minWidth : DEFAULT_MIN_WIDTH;
return { ...baseColumn, id, ...(minWidth ? { minWidth } : {}) };
});
};
export function createServerGrid(options) {
const {
container,
dataUrl,
appBase = '/',
columns,
mapData,
total = (data) => data.total,
sortColumns = [],
paginationLimit = 10,
language = {},
search = null,
filters = [],
filterBindingMode = 'live',
urlSync = true,
urlState = null,
gridjs,
rowDblClick = null,
actions = null,
rowInteraction = null,
rowDataset = null,
selection = null,
pageSize = null
} = options || {};
const normalizedFilterBindingMode = String(filterBindingMode || 'live').trim().toLowerCase() === 'manual'
? 'manual'
: 'live';
const gridjsLib = gridjs || window.gridjs;
const containerEl = typeof container === 'string' ? document.querySelector(container) : container;
if (!containerEl || !gridjsLib) {
if (!containerEl) {
warnOnce('UI_EL_MISSING', `Missing grid container: ${container}`, { module: 'grid-factory' });
}
if (!gridjsLib) {
warnOnce('UI_LIB_MISSING', 'Grid.js not available', { module: 'grid-factory' });
}
return null;
}
const normalizedColumns = normalizeGridColumns(columns, sortColumns);
const params = new URLSearchParams(window.location.search);
const urlStateConfig = {
pageParam: 'page',
sortOrderParam: 'order',
sortDirParam: 'dir',
limitParam: 'limit',
cleanFirstPage: true,
cleanDefaultLimit: true,
syncPage: true,
syncSort: true,
syncLimit: true,
...(urlState || {})
};
const pageParam = urlStateConfig.pageParam || 'page';
const sortOrderParam = urlStateConfig.sortOrderParam || 'order';
const sortDirParam = urlStateConfig.sortDirParam || 'dir';
const limitParam = urlStateConfig.limitParam || 'limit';
const resolveUrl = (path) => new URL(path, appBase).toString();
const addParam = (url, key, value) => {
const nextUrl = new URL(url);
nextUrl.searchParams.set(key, value);
return nextUrl.toString();
};
const deleteParam = (url, key) => {
const nextUrl = new URL(url);
nextUrl.searchParams.delete(key);
return nextUrl.toString();
};
const clearSortParams = (url) => deleteParam(deleteParam(url, sortOrderParam), sortDirParam);
const parsePage = (rawValue) => {
const parsed = Number.parseInt(String(rawValue ?? ''), 10);
if (!Number.isFinite(parsed) || parsed < 1) {
return 1;
}
return parsed;
};
const validSortKeys = new Set(
(sortColumns || [])
.filter((key) => typeof key === 'string')
.map((key) => key.trim())
.filter(Boolean)
);
const normalizeDir = (rawValue) => {
const normalized = String(rawValue ?? '').trim().toLowerCase();
return normalized === 'asc' || normalized === 'desc' ? normalized : '';
};
const pageSizeConfig = pageSize && typeof pageSize === 'object'
? pageSize
: {};
const pageSizeEnabled = pageSizeConfig.enabled !== false;
const pageSizeOptions = normalizePageSizeOptions(pageSizeConfig.options);
const defaultLimit = pageSizeEnabled
? (
pageSizeOptions.includes(parsePositiveInt(paginationLimit))
? Number(parsePositiveInt(paginationLimit))
: pageSizeOptions[0]
)
: (parsePositiveInt(paginationLimit) ?? 10);
const syncLimit = pageSizeEnabled && urlStateConfig.syncLimit !== false;
const cleanDefaultLimit = urlStateConfig.cleanDefaultLimit !== false;
const dataUrlPath = (() => {
try {
return new URL(dataUrl, appBase).pathname || String(dataUrl || '');
} catch {
return String(dataUrl || '');
}
})();
const containerKey = typeof container === 'string'
? container
: (containerEl.id ? `#${containerEl.id}` : 'grid');
const autoPageSizeStorageKey = [
PAGE_SIZE_STORAGE_PREFIX,
encodeURIComponent(window.location.pathname || ''),
encodeURIComponent(dataUrlPath || ''),
encodeURIComponent(containerKey || '')
].join(':');
const pageSizeStorageKey = pageSizeEnabled
? (String(pageSizeConfig.storageKey || autoPageSizeStorageKey).trim() || autoPageSizeStorageKey)
: '';
const pageSizeStorage = createUiStorage({
namespace: pageSizeConfig.storageNamespace || 'app-ui',
version: pageSizeConfig.storageVersion || 'v1',
scope: pageSizeConfig.storageScope || 'grid-page-size',
});
const scopedPageSizeStorageKey = pageSizeEnabled
? pageSizeStorage.buildKey('state', pageSizeStorageKey)
: '';
const parseAllowedLimit = (value) => {
if (!pageSizeEnabled) {
return parsePositiveInt(value);
}
const parsed = parsePositiveInt(value);
if (parsed === null) {
return null;
}
return pageSizeOptions.includes(parsed) ? parsed : null;
};
const pageSizeLabel = String(
pageSizeConfig.label
|| language?.pagination?.rowsPerPage
|| 'Rows per page'
).trim() || 'Rows per page';
const pageSizeCompactLabel = String(
pageSizeConfig.compactLabel
|| language?.pagination?.rowsPerPageShort
|| 'Per page'
).trim() || 'Per page';
let currentLimit = defaultLimit;
const urlLimit = syncLimit ? parseAllowedLimit(params.get(limitParam)) : null;
if (urlLimit !== null) {
currentLimit = urlLimit;
} else if (pageSizeEnabled) {
const storedLimit = parseAllowedLimit(
scopedPageSizeStorageKey ? pageSizeStorage.getItem(scopedPageSizeStorageKey) : null
);
if (storedLimit !== null) {
currentLimit = storedLimit;
}
}
if (scopedPageSizeStorageKey) {
pageSizeStorage.setItem(scopedPageSizeStorageKey, String(currentLimit));
}
let currentPage = urlStateConfig.syncPage ? parsePage(params.get(pageParam)) : 1;
let currentSort = null;
if (urlStateConfig.syncSort) {
const initialOrder = String(params.get(sortOrderParam) ?? '').trim();
const initialDir = normalizeDir(params.get(sortDirParam));
if (initialOrder !== '' && initialDir !== '' && validSortKeys.has(initialOrder)) {
currentSort = { order: initialOrder, dir: initialDir };
}
}
const rowInteractionConfig = rowInteraction && typeof rowInteraction === 'object'
? rowInteraction
: {};
const hasRowOpenHandler = Boolean(rowDblClick && typeof rowDblClick.getUrl === 'function');
const rowInteractionEnabled = hasRowOpenHandler && rowInteractionConfig.enabled !== false;
const keepDblClick = rowInteractionEnabled && rowInteractionConfig.keepDblClick !== false;
const enableEnterOnRow = rowInteractionEnabled && rowInteractionConfig.enableEnterOnRow !== false;
const linkColumnIndex = rowInteractionEnabled && rowInteractionConfig.linkColumn !== false
? (Number.isInteger(rowInteractionConfig.linkColumn) ? rowInteractionConfig.linkColumn : 0)
: -1;
const rowEnterHint = String(
rowInteractionConfig.rowEnterHint
|| language?.actions?.rowEnterHint
|| 'Press Enter to open row'
).trim() || 'Press Enter to open row';
const gridRetryLabel = String(language?.errorRetry || 'Retry').trim() || 'Retry';
const resolveRowOpenUrl = (rowData, rowIndex = -1) => {
if (!rowInteractionEnabled) {
return '';
}
const url = rowDblClick.getUrl(rowData, rowIndex);
const normalized = String(url || '').trim();
if (normalized === '') {
return '';
}
return withCurrentListReturn(normalized);
};
const getInput = (input) => {
if (!input) {return null;}
return typeof input === 'string' ? document.querySelector(input) : input;
};
const getInputValue = (input) => {
if (!input) {return '';}
if (input.type === 'checkbox') {
return input.checked ? (input.value || '1') : '';
}
if (input.tagName === 'SELECT' && input.multiple) {
return Array.from(input.selectedOptions || []).map((option) => option.value).filter(Boolean);
}
return (input.value ?? '').toString();
};
const setInputValue = (input, value) => {
if (!input) {return;}
if (input.type === 'checkbox') {
input.checked = value === true || value === 'true' || value === '1';
} else if (input.tagName === 'SELECT' && input.multiple) {
const values = Array.isArray(value)
? value.map((item) => item.toString())
: value
? value.toString().split(',').map((item) => item.trim()).filter(Boolean)
: [];
Array.from(input.options || []).forEach((option) => {
option.selected = values.includes(option.value);
});
} else {
input.value = value;
}
if (input._multiSelectInstance && typeof input._multiSelectInstance.setValues === 'function') {
const values = Array.isArray(value)
? value.map((item) => item.toString())
: value
? value.toString().split(',').map((item) => item.trim()).filter(Boolean)
: [];
input._multiSelectInstance.setValues(values);
}
};
let searchValue = '';
let searchParam = 'search';
let searchInput = null;
let searchDebounce = 250;
if (search) {
searchInput = getInput(search.input);
searchParam = search.param || searchParam;
searchDebounce = search.debounce ?? searchDebounce;
const initial = params.get(searchParam) ?? search.default ?? '';
searchValue = initial || '';
if (searchInput) {
setInputValue(searchInput, initial);
}
}
const filterState = filters.map((filter) => {
const input = getInput(filter.input);
const initial = params.get(filter.param);
const value = initial !== null ? initial : (filter.default ?? '');
if (input) {
const parsed = typeof filter.parse === 'function' ? filter.parse(value) : value;
setInputValue(input, parsed);
}
return { ...filter, input, value };
});
const normalizeValue = (filter, value) => {
if (typeof filter.normalize === 'function') {
return filter.normalize(value);
}
return value;
};
const resolveFilterRawValue = (filter) => {
if (!filter) {
return '';
}
if (normalizedFilterBindingMode === 'manual') {
return filter.value ?? '';
}
if (!filter.input) {
return filter.value ?? '';
}
return getInputValue(filter.input);
};
const buildDataParams = () => {
const query = new URLSearchParams();
if (searchValue) {
query.set(searchParam, searchValue);
}
filterState.forEach((filter) => {
const raw = resolveFilterRawValue(filter);
const normalized = normalizeValue(filter, raw);
if (Array.isArray(normalized)) {
const list = normalized.map((item) => item.toString()).filter(Boolean);
if (list.length) {
query.set(filter.param, list.join(','));
}
return;
}
if (normalized !== '' && normalized !== null && typeof normalized !== 'undefined') {
query.set(filter.param, normalized);
}
});
if (currentSort?.order && currentSort?.dir) {
query.set(sortOrderParam, currentSort.order);
query.set(sortDirParam, currentSort.dir);
}
return query;
};
const buildUrlParams = () => {
const query = buildDataParams();
if (!urlStateConfig.syncSort) {
query.delete(sortOrderParam);
query.delete(sortDirParam);
}
if (urlStateConfig.syncPage) {
if (urlStateConfig.cleanFirstPage && currentPage <= 1) {
query.delete(pageParam);
} else {
query.set(pageParam, String(currentPage));
}
} else {
query.delete(pageParam);
}
if (syncLimit) {
if (!cleanDefaultLimit || currentLimit !== defaultLimit) {
query.set(limitParam, String(currentLimit));
} else {
query.delete(limitParam);
}
} else {
query.delete(limitParam);
}
return query;
};
const baseUrl = () => {
const url = new URL(dataUrl, appBase);
const query = buildDataParams();
query.forEach((value, key) => url.searchParams.set(key, value));
return url.toString();
};
const updateUrl = (options = {}) => {
if (!urlSync) {return;}
const historyMode = String(options?.historyMode || 'replace').trim().toLowerCase() === 'push'
? 'push'
: 'replace';
const url = new URL(window.location.href);
if (searchParam) { url.searchParams.delete(searchParam); }
filterState.forEach((filter) => url.searchParams.delete(filter.param));
url.searchParams.delete(sortOrderParam);
url.searchParams.delete(sortDirParam);
url.searchParams.delete(pageParam);
url.searchParams.delete(limitParam);
const query = buildUrlParams();
query.forEach((value, key) => url.searchParams.set(key, value));
if (historyMode === 'push') {
window.history.pushState({}, '', url.toString());
return;
}
window.history.replaceState({}, '', url.toString());
};
const explicitActionConfig = actions && actions.enabled ? {
id: String(actions.id || 'row_actions').trim() || 'row_actions',
label: actions.label || 'Actions',
index: Number.isInteger(actions.index) ? actions.index : null,
items: Array.isArray(actions.items) ? actions.items : [],
getRowId: typeof actions.getRowId === 'function' ? actions.getRowId : null,
wrapperClass: actions.wrapperClass || 'grid-actions',
onAction: typeof actions.onAction === 'function' ? actions.onAction : null
} : null;
const actionConfig = explicitActionConfig || null;
// Link-column: wrap the target column's formatter output in an <a> for row navigation.
if (linkColumnIndex >= 0 && linkColumnIndex < normalizedColumns.length) {
const targetCol = normalizedColumns[linkColumnIndex];
const originalFormatter = targetCol.formatter || null;
targetCol.formatter = (cell, row) => {
const rowOpenUrl = resolveRowOpenUrl(row);
const inner = originalFormatter ? originalFormatter(cell, row) : cell;
if (!rowOpenUrl) {
return inner;
}
const innerHtml = (inner && typeof inner === 'object' && typeof inner.props?.content === 'string')
? inner.props.content
: escapeHtmlAttr(String(inner ?? ''));
// Avoid nesting <a> inside <a> — if the formatter already has links, skip wrapping.
if (/<a[\s>]/i.test(innerHtml)) {
return inner;
}
return gridjsLib.html(`<a class="app-grid-link-cell" href="${escapeHtmlAttr(rowOpenUrl)}">${innerHtml}</a>`);
};
}
const selectionConfig = selection && selection.enabled ? {
id: selection.id || 'selectRow',
index: Number.isInteger(selection.index) ? selection.index : 0,
component: selection.component || null,
props: selection.props || null,
onChange: typeof selection.onChange === 'function' ? selection.onChange : null,
getSelectedIds: typeof selection.getSelectedIds === 'function' ? selection.getSelectedIds : null,
selectAllLabel: selection.selectAllLabel || 'Select all',
selectAllEnabled: selection.selectAllEnabled !== false
} : null;
if (selectionConfig) {
const usedColumnIds = new Set(
normalizedColumns
.map((column) => (column && typeof column === 'object' ? String(column.id || '').trim() : ''))
.filter(Boolean)
);
selectionConfig.id = uniqueColumnId(selectionConfig.id, usedColumnIds, 'select_row');
}
const insertActionColumn = (cols) => {
if (!actionConfig) {return cols;}
const usedColumnIds = new Set(
cols
.map((column) => (column && typeof column === 'object' ? String(column.id || '').trim() : ''))
.filter(Boolean)
);
const actionColumnId = uniqueColumnId(actionConfig.id, usedColumnIds, 'row_actions');
const actionColumn = {
id: actionColumnId,
name: actionConfig.label,
sort: false,
formatter: (_cell, row) => {
const id = actionConfig.getRowId ? actionConfig.getRowId(row) : null;
if (!id) {
return gridjsLib.html('');
}
const buttons = actionConfig.items.map((item) => {
if (!item || !item.key) {return '';}
const label = typeof item.label === 'function' ? item.label(row, id) : item.label;
if (!label) {return '';}
const ariaLabelRaw = typeof item.ariaLabel === 'function'
? item.ariaLabel(row, id, label)
: item.ariaLabel;
const titleRaw = typeof item.title === 'function'
? item.title(row, id, label)
: item.title;
const ariaAttr = ariaLabelRaw ? ` aria-label="${escapeHtmlAttr(ariaLabelRaw)}"` : '';
const titleAttr = titleRaw ? ` title="${escapeHtmlAttr(titleRaw)}"` : '';
const className = item.className ? ` ${item.className}` : '';
const type = item.type || 'button';
const safeLabel = escapeHtmlAttr(String(label));
if (type === 'link') {
const href = typeof item.href === 'function' ? item.href({ row, id }) : item.href;
const url = escapeHtmlAttr(href || '#');
return `<a role="button" class="${className.trim()}" data-grid-action="${item.key}" href="${url}"${ariaAttr}${titleAttr}>${safeLabel}</a>`;
}
return `<button type="button" class="${className.trim()}" data-grid-action="${item.key}"${ariaAttr}${titleAttr}>${safeLabel}</button>`;
}).join('');
return gridjsLib.html(`<div class="${actionConfig.wrapperClass}" role="group">${buttons}</div>`);
}
};
const next = [...cols];
const index = actionConfig.index === null ? next.length : Math.min(Math.max(actionConfig.index, 0), next.length);
next.splice(index, 0, actionColumn);
return next;
};
const insertSelectionColumn = (cols) => {
if (!selectionConfig) {return cols;}
const headerLabel = selectionConfig.selectAllLabel || 'Select all';
const header = selectionConfig.selectAllEnabled
? gridjsLib.html(`<input type="checkbox" data-grid-select-all aria-label="${headerLabel}">`)
: '';
const selectionColumn = {
id: selectionConfig.id,
name: header,
sort: false,
plugin: {
id: selectionConfig.id
}
};
const next = [...cols];
const index = Math.min(Math.max(selectionConfig.index ?? 0, 0), next.length);
next.splice(index, 0, selectionColumn);
return next;
};
const insertActionSort = (cols) => {
if (!actionConfig || !cols.length) {return cols;}
const next = [...cols];
const index = actionConfig.index === null ? next.length : Math.min(Math.max(actionConfig.index, 0), next.length);
next.splice(index, 0, null);
return next;
};
const insertSelectionSort = (cols) => {
if (!selectionConfig || !cols.length) {return cols;}
const next = [...cols];
const index = Math.min(Math.max(selectionConfig.index ?? 0, 0), next.length);
next.splice(index, 0, null);
return next;
};
const mapDataWithActions = (data) => {
const rows = mapData ? mapData(data) : [];
if (!actionConfig || !Array.isArray(rows)) {
return rows;
}
const index = actionConfig.index === null ? null : Math.min(Math.max(actionConfig.index, 0), Infinity);
return rows.map((row) => {
if (!Array.isArray(row)) {return row;}
const next = [...row];
const insertAt = index === null ? next.length : Math.min(index, next.length);
next.splice(insertAt, 0, null);
return next;
});
};
const serverConfig = {
url: baseUrl(),
then: mapDataWithActions,
total
};
let finalSortColumns = sortColumns;
if (selectionConfig) {
finalSortColumns = insertSelectionSort(finalSortColumns);
}
if (actionConfig) {
finalSortColumns = insertActionSort(finalSortColumns);
}
const sortConfig = finalSortColumns.length ? {
enabled: true,
server: {
url: (prev, columns) => {
if (!columns.length) {
// Keep URL/bootstrap sort when grid has no active sort columns yet (e.g. initial load from deep-link).
if (currentSort?.order && currentSort?.dir) {
return addParam(addParam(clearSortParams(prev), sortOrderParam, currentSort.order), sortDirParam, currentSort.dir);
}
currentSort = null;
return clearSortParams(prev);
}
const column = columns[0];
const key = finalSortColumns[column.index];
if (!key) {
if (currentSort?.order && currentSort?.dir) {
return addParam(addParam(clearSortParams(prev), sortOrderParam, currentSort.order), sortDirParam, currentSort.dir);
}
currentSort = null;
return clearSortParams(prev);
}
const direction = column.direction === 1 ? 'asc' : 'desc';
currentSort = { order: key, dir: direction };
if (urlSync && urlStateConfig.syncSort) {
updateUrl();
}
return addParam(addParam(clearSortParams(prev), sortOrderParam, key), sortDirParam, direction);
}
}
} : { enabled: false };
const paginationConfig = {
limit: currentLimit,
page: Math.max(currentPage - 1, 0),
// We control page resets explicitly (search/filter/reset actions) to keep deep-link pages stable.
resetPageOnUpdate: false,
server: {
url: (prev, page, limit) => {
const parsedPage = Number.parseInt(String(page ?? ''), 10);
const parsedLimit = parseAllowedLimit(limit);
const nextPage = Number.isFinite(parsedPage) && parsedPage >= 0 ? parsedPage : 0;
const nextLimit = parsedLimit ?? currentLimit;
currentLimit = nextLimit;
if (scopedPageSizeStorageKey) {
pageSizeStorage.setItem(scopedPageSizeStorageKey, String(currentLimit));
}
currentPage = nextPage + 1;
if (urlSync && (urlStateConfig.syncPage || syncLimit)) {
updateUrl();
}
const offset = nextPage * nextLimit;
return addParam(addParam(prev, 'limit', nextLimit), 'offset', offset);
}
}
};
const plugins = [];
if (selectionConfig?.component) {
plugins.push({
id: selectionConfig.id,
component: selectionConfig.component,
props: selectionConfig.props || {}
});
}
const grid = new gridjsLib.Grid({
columns: insertActionColumn(insertSelectionColumn(normalizedColumns)),
server: serverConfig,
search: false,
sort: sortConfig,
pagination: paginationConfig,
language,
plugins
});
grid.render(containerEl);
setTimeout(() => {
initSelectAll();
}, 0);
// Prevent empty-state flash before data arrives.
let hasLoadedOnce = false;
const markLoadedOnce = () => {
if (hasLoadedOnce) {return;}
hasLoadedOnce = true;
containerEl.classList.add('gridjs-has-loaded');
};
const setUpdating = () => containerEl.classList.add('gridjs-is-updating');
const clearUpdating = () => containerEl.classList.remove('gridjs-is-updating');
const ensureErrorRetryControl = () => {
const errorMessage = containerEl.querySelector('.gridjs-message.gridjs-error');
if (!errorMessage) {
return;
}
if (errorMessage.querySelector('[data-grid-retry]')) {
return;
}
const actionsWrap = document.createElement('div');
actionsWrap.className = 'app-grid-error-actions';
const retryButton = document.createElement('button');
retryButton.type = 'button';
retryButton.className = 'outline secondary app-grid-retry';
retryButton.setAttribute('data-grid-retry', '1');
retryButton.textContent = gridRetryLabel;
actionsWrap.appendChild(retryButton);
errorMessage.appendChild(actionsWrap);
};
const store = grid?.config?.store;
if (store?.subscribe) {
store.subscribe((state) => {
if (!state) {return;}
// Grid.js status: 0=Init, 1=Loading, 2=Loaded, 3=Rendered, 4=Error
if (state.status === 1) {
setUpdating();
} else {
clearUpdating();
}
if (state.status === 4) {
setTimeout(() => {
ensureErrorRetryControl();
}, 0);
}
if (state.status >= 2) {
markLoadedOnce();
}
if (state.status >= 3) {
initSelectAll();
}
});
}
const applyRowDataset = () => {
const shouldApplyCustomDataset = typeof rowDataset === 'function';
if (!shouldApplyCustomDataset && !rowInteractionEnabled) {return;}
const rowEls = containerEl.querySelectorAll('.gridjs-tbody .gridjs-tr');
const dataRows = grid.config.store.getState().data?.rows || [];
rowEls.forEach((rowEl, index) => {
const rowData = dataRows[index] || null;
if (!rowData) {return;}
if (rowInteractionEnabled) {
const rowOpenUrl = resolveRowOpenUrl(rowData, index);
if (rowOpenUrl !== '') {
rowEl.setAttribute('data-row-open-url', rowOpenUrl);
rowEl.setAttribute('tabindex', '-1');
} else {
rowEl.removeAttribute('data-row-open-url');
if (rowEl.getAttribute('tabindex') === '-1') {
rowEl.removeAttribute('tabindex');
}
}
}
if (shouldApplyCustomDataset) {
const attrs = rowDataset(rowData, index) || {};
Object.keys(attrs).forEach((key) => {
const value = attrs[key];
const attr = `data-${key}`;
if (value === null || typeof value === 'undefined' || value === '') {
rowEl.removeAttribute(attr);
} else {
rowEl.setAttribute(attr, String(value));
}
});
}
});
};
const initSelectAll = () => {
if (!selectionConfig?.component || !selectionConfig.selectAllEnabled) {return;}
const header = containerEl.querySelector(`th[data-column-id="${selectionConfig.id}"] input[data-grid-select-all]`);
if (!header) {return;}
if (header.dataset.bound === '1') {
return;
}
header.dataset.bound = '1';
const getRowCheckboxes = () => Array.from(
containerEl.querySelectorAll(`td[data-column-id="${selectionConfig.id}"] input[type="checkbox"]`)
);
const updateHeaderState = () => {
const boxes = getRowCheckboxes();
const total = boxes.length;
const checked = boxes.filter((box) => box.checked).length;
header.indeterminate = total > 0 && checked > 0 && checked < total;
header.checked = total > 0 && checked === total;
header.disabled = total === 0;
};
header.addEventListener('change', () => {
const boxes = getRowCheckboxes();
boxes.forEach((box) => {
if (box.checked !== header.checked) {
box.click();
}
});
updateHeaderState();
});
updateHeaderState();
};
if (typeof rowDataset === 'function' || rowInteractionEnabled) {
const observer = new MutationObserver(() => applyRowDataset());
observer.observe(containerEl, { childList: true, subtree: true });
applyRowDataset();
}
const getRowDataByElement = (rowEl) => {
if (!rowEl) {return null;}
const rows = Array.from(rowEl.parentElement?.children || []);
const rowIndex = rows.indexOf(rowEl);
if (rowIndex < 0) {return null;}
const dataRows = grid.config.store.getState().data?.rows || [];
const rowData = dataRows[rowIndex] || null;
if (!rowData) {return null;}
return { rowData, rowIndex };
};
const resolveRowOpenUrlByElement = (rowEl) => {
if (!rowInteractionEnabled || !rowEl) {
return '';
}
const storedUrl = String(rowEl.getAttribute('data-row-open-url') || '').trim();
if (storedUrl !== '') {
return storedUrl;
}
const rowInfo = getRowDataByElement(rowEl);
if (!rowInfo) {
return '';
}
return resolveRowOpenUrl(rowInfo.rowData, rowInfo.rowIndex);
};
const navigateToRow = (rowEl) => {
const url = resolveRowOpenUrlByElement(rowEl);
if (url === '') {
return false;
}
window.location.href = url;
return true;
};
if (keepDblClick && rowDblClick) {
const excludeSelector = rowDblClick.exclude || '.grid-actions, button, a, input, select, textarea';
containerEl.addEventListener('dblclick', (event) => {
if (excludeSelector && event.target.closest(excludeSelector)) {
return;
}
const rowEl = event.target.closest('.gridjs-tr');
navigateToRow(rowEl);
});
}
if (enableEnterOnRow) {
const interactiveSelector = '.grid-actions, button, a, input, select, textarea, [role="button"], [contenteditable="true"]';
containerEl.addEventListener('click', (event) => {
const target = event.target instanceof Element ? event.target : null;
if (!target || target.closest(interactiveSelector)) {
return;
}
const rowEl = target.closest('.gridjs-tr[data-row-open-url]');
if (!rowEl) {
return;
}
rowEl.focus({ preventScroll: true });
});
containerEl.addEventListener('keydown', (event) => {
if (event.key !== 'Enter' || event.defaultPrevented) {
return;
}
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
const target = event.target instanceof Element ? event.target : null;
if (!target || target.closest(interactiveSelector)) {
return;
}
const rowEl = target.closest('.gridjs-tr');
if (!rowEl) {
return;
}
if (navigateToRow(rowEl)) {
event.preventDefault();
}
});
}
if (actionConfig) {
containerEl.addEventListener('click', async (event) => {
const actionEl = event.target.closest('[data-grid-action]');
if (!actionEl) {
return;
}
const rowEl = actionEl.closest('.gridjs-tr');
const rowInfo = getRowDataByElement(rowEl);
if (!rowInfo) {
return;
}
const { rowData, rowIndex } = rowInfo;
const actionKey = actionEl.getAttribute('data-grid-action') || '';
const actionItem = actionConfig.items.find((item) => item && item.key === actionKey);
if (!actionItem) {
return;
}
const id = actionConfig.getRowId ? actionConfig.getRowId(rowData, rowIndex) : null;
if (!id) {
return;
}
const confirmText = typeof actionItem.confirm === 'function'
? actionItem.confirm({ row: rowData, id })
: actionItem.confirm;
if (confirmText) {
const confirmOptionsFromItem = typeof actionItem.confirmOptions === 'function'
? (actionItem.confirmOptions({ row: rowData, id, action: actionKey }) || {})
: (actionItem.confirmOptions || {});
const approved = await confirmDialog.confirm({
...confirmOptionsFromItem,
message: String(confirmText),
actionKind: String(confirmOptionsFromItem.actionKind || actionKey || '')
});
if (!approved) {
return;
}
}
if (actionItem.type === 'link') {
const href = typeof actionItem.href === 'function'
? actionItem.href({ row: rowData, id })
: actionItem.href;
if (href) {
event.preventDefault();
window.location.href = href;
}
return;
}
if (actionConfig.onAction) {
event.preventDefault();
await actionConfig.onAction({ action: actionKey, id, row: rowData, event, grid, container: containerEl });
}
});
}
containerEl.addEventListener('click', (event) => {
const retryButton = event.target.closest('[data-grid-retry]');
if (!retryButton) {
return;
}
event.preventDefault();
retryButton.setAttribute('aria-busy', 'true');
updateGrid({ resetPage: false });
});
let pageSizeSelect = null;
const syncPageSizeControl = () => {
if (!pageSizeSelect) {
return;
}
pageSizeSelect.value = String(currentLimit);
};
const resolvePageSizeToolbar = () => {
const selector = String(pageSizeConfig.toolbarSelector || '.app-list-toolbar').trim() || '.app-list-toolbar';
const allToolbars = Array.from(document.querySelectorAll(selector));
if (!allToolbars.length) {
return null;
}
const precedingToolbars = allToolbars.filter((toolbar) => (
Boolean(toolbar.compareDocumentPosition(containerEl) & Node.DOCUMENT_POSITION_FOLLOWING)
));
const pool = precedingToolbars.length ? precedingToolbars : allToolbars;
const withFilterButton = pool.filter((toolbar) => toolbar.querySelector('[data-filter-drawer-open]'));
const targetPool = withFilterButton.length ? withFilterButton : pool;
return targetPool[targetPool.length - 1] || null;
};
const mountPageSizeControl = () => {
if (!pageSizeEnabled) {
return;
}
const toolbar = resolvePageSizeToolbar();
if (!toolbar) {
warnOnce(
'UI_PAGE_SIZE_HOST_MISSING',
`Missing page-size toolbar near grid: ${containerKey || 'unknown'}`,
{ module: 'grid-factory' }
);
return;
}
const controlKey = encodeURIComponent(`${containerKey}:${dataUrlPath}`);
const existing = toolbar.querySelector(`.app-grid-page-size[data-grid-page-size-key="${controlKey}"]`);
if (existing) {
pageSizeSelect = existing.querySelector('select');
syncPageSizeControl();
return;
}
const field = document.createElement('label');
field.className = 'app-field app-grid-page-size';
field.dataset.gridPageSizeKey = controlKey;
const caption = document.createElement('span');
caption.textContent = pageSizeCompactLabel;
const select = document.createElement('select');
select.setAttribute('aria-label', pageSizeLabel);
select.setAttribute('title', pageSizeLabel);
pageSizeOptions.forEach((option) => {
const optionEl = document.createElement('option');
optionEl.value = String(option);
optionEl.textContent = String(option);
select.appendChild(optionEl);
});
select.value = String(currentLimit);
select.addEventListener('change', () => {
const nextLimit = parseAllowedLimit(select.value);
if (nextLimit === null || nextLimit === currentLimit) {
syncPageSizeControl();
return;
}
currentLimit = nextLimit;
currentPage = 1;
if (scopedPageSizeStorageKey) {
pageSizeStorage.setItem(scopedPageSizeStorageKey, String(currentLimit));
}
syncPageSizeControl();
updateGrid({ resetPage: true, urlHistoryMode: 'push' });
});
field.appendChild(caption);
field.appendChild(select);
const filterButton = toolbar.querySelector(':scope > [data-filter-drawer-open]');
if (filterButton) {
filterButton.insertAdjacentElement('afterend', field);
} else {
toolbar.appendChild(field);
}
pageSizeSelect = select;
syncPageSizeControl();
};
const updateGrid = (options = {}) => {
const resetPage = options && options.resetPage === true;
const urlHistoryMode = options?.urlHistoryMode || 'replace';
if (resetPage) {
currentPage = 1;
}
updateUrl({ historyMode: urlHistoryMode });
const paginationState = grid?.config?.pagination && typeof grid.config.pagination === 'object'
? grid.config.pagination
: paginationConfig;
const nextPagination = {
...paginationState,
limit: currentLimit,
page: resetPage ? 0 : Math.max(currentPage - 1, 0)
};
const nextConfig = {
server: { ...serverConfig, url: baseUrl() },
pagination: nextPagination
};
// Use Grid.js reconfiguration API so server pipeline is refreshed reliably on filter/search changes.
if (typeof grid?.updateConfig === 'function') {
grid.updateConfig(nextConfig).forceRender();
return;
}
if (grid?.config) {
grid.config.server = nextConfig.server;
grid.config.pagination = nextConfig.pagination;
}
grid.forceRender();
};
mountPageSizeControl();
const debounce = (fn, delay = 250) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
if (searchInput) {
searchInput.addEventListener('input', debounce((event) => {
searchValue = event.target.value.trim();
updateGrid({ resetPage: true });
}, searchDebounce));
}
if (normalizedFilterBindingMode === 'live') {
filterState.forEach((filter) => {
if (!filter.input) {return;}
const eventName = filter.event || 'change';
filter.input.addEventListener(eventName, () => {
filter.value = getInputValue(filter.input);
updateGrid({ resetPage: true });
});
});
}
const commitFilterInputValues = () => {
filterState.forEach((filter) => {
if (!filter.input) {return;}
filter.value = getInputValue(filter.input);
});
};
const applyFilters = (options = {}) => {
const resetPage = options?.resetPage !== false;
const urlHistoryMode = options?.urlHistoryMode || 'replace';
commitFilterInputValues();
updateGrid({ resetPage, urlHistoryMode });
};
const resetFilters = (options = {}) => {
const preserveFilterParams = Array.isArray(options?.preserveFilterParams)
? options.preserveFilterParams.map((item) => String(item ?? '').trim()).filter(Boolean)
: [];
const preserveFilterParamSet = new Set(preserveFilterParams);
const preserveSearch = options?.preserveSearch === true;
const resetPage = options?.resetPage !== false;
const urlHistoryMode = options?.urlHistoryMode || 'replace';
if (search && !preserveSearch) {
const defaultValue = search.default ?? '';
searchValue = defaultValue;
if (searchInput) {
setInputValue(searchInput, defaultValue);
}
}
filterState.forEach((filter) => {
if (preserveFilterParamSet.has(filter.param)) {
if (filter.input) {
filter.value = getInputValue(filter.input);
}
return;
}
const defaultValue = filter.default ?? '';
filter.value = defaultValue;
if (filter.input) {
setInputValue(filter.input, defaultValue);
}
});
updateGrid({ resetPage, urlHistoryMode });
};
const captureAppliedState = () => {
const filterValues = {};
filterState.forEach((filter) => {
filterValues[filter.param] = filter.value ?? filter.default ?? '';
});
return {
search: searchValue,
filters: filterValues,
sort: currentSort ? { ...currentSort } : null,
page: currentPage,
limit: currentLimit
};
};
const captureDraftState = () => {
const filterValues = {};
filterState.forEach((filter) => {
const raw = filter.input ? getInputValue(filter.input) : (filter.value ?? filter.default ?? '');
filterValues[filter.param] = raw;
});
const draftSearch = searchInput
? String(getInputValue(searchInput) ?? '').trim()
: searchValue;
return {
search: draftSearch,
filters: filterValues,
sort: currentSort ? { ...currentSort } : null,
page: currentPage,
limit: currentLimit
};
};
const restoreAppliedState = (state = {}) => {
if (search) {
const nextSearch = typeof state?.search === 'string' ? state.search : (search.default ?? '');
searchValue = nextSearch;
if (searchInput) {
setInputValue(searchInput, nextSearch);
}
}
const nextFilters = state?.filters && typeof state.filters === 'object'
? state.filters
: {};
filterState.forEach((filter) => {
const nextValue = Object.prototype.hasOwnProperty.call(nextFilters, filter.param)
? nextFilters[filter.param]
: (filter.default ?? '');
filter.value = nextValue;
if (filter.input) {
setInputValue(filter.input, nextValue);
}
});
if (state?.sort && typeof state.sort === 'object') {
const order = String(state.sort.order ?? '').trim();
const dir = normalizeDir(state.sort.dir ?? '');
if (order !== '' && dir !== '' && validSortKeys.has(order)) {
currentSort = { order, dir };
} else {
currentSort = null;
}
} else if (state?.sort === null) {
currentSort = null;
}
if (Number.isFinite(Number(state?.page))) {
const parsedPage = parsePage(state.page);
currentPage = parsedPage;
}
const nextLimit = parseAllowedLimit(state?.limit);
if (nextLimit !== null) {
currentLimit = nextLimit;
if (scopedPageSizeStorageKey) {
pageSizeStorage.setItem(scopedPageSizeStorageKey, String(currentLimit));
}
syncPageSizeControl();
}
};
const getSortParams = () => (currentSort ? { ...currentSort } : null);
const selectionApi = selectionConfig && selectionConfig.component ? {
getSelectedIds: () => {
if (selectionConfig.getSelectedIds) {
return selectionConfig.getSelectedIds({ grid, container: containerEl });
}
const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
const store = plugin?.props?.store;
const state = store?.state ?? store?.getState?.();
if (!state) {return [];}
if (Array.isArray(state.selected)) {return state.selected;}
if (Array.isArray(state.ids)) {return state.ids;}
if (state.rows && typeof state.rows === 'object') {
return Object.keys(state.rows).filter((key) => state.rows[key]);
}
if (state instanceof Set) {return Array.from(state);}
if (Array.isArray(state)) {return state;}
if (typeof state === 'object') {
return Object.keys(state).filter((key) => state[key]);
}
return [];
},
clear: () => {
const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
const store = plugin?.props?.store;
if (store?.dispatch) {
store.dispatch('reset');
} else if (store?.reset) {
store.reset();
}
}
} : null;
if (selectionConfig?.onChange && selectionConfig.component) {
grid.on('ready', () => {
const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
const store = plugin?.props?.store;
const sync = () => {
selectionConfig.onChange(selectionApi?.getSelectedIds?.() || []);
};
if (store?.on) {store.on('updated', sync);}
if (store?.subscribe) {store.subscribe(sync);}
sync();
});
}
grid.on('ready', () => {
initSelectAll();
setTimeout(() => {
requestFsLightboxRefresh();
}, 0);
});
return {
grid,
container: containerEl,
updateGrid,
updateUrl,
baseUrl,
resolveUrl,
getSortParams,
resetFilters,
applyFilters,
captureAppliedState,
captureDraftState,
restoreAppliedState,
selection: selectionApi
};
}
const DEFAULT_LIST_FILTER_LABELS = {
chipsClearAll: 'Clear all filters',
chipsRemove: 'Remove filter',
filtersApplied: 'Filters applied',
filterRemoved: 'Filter removed',
filtersCleared: 'All filters cleared'
};
const resolveDomQueryElement = (target, root = document) => {
if (!target) {
return null;
}
if (typeof target === 'string') {
if (root && typeof root.querySelector === 'function') {
return root.querySelector(target) || document.querySelector(target);
}
return document.querySelector(target);
}
return target;
};
const readDomLabelDefaults = (options = {}) => {
const drawerRoot = options.drawerRoot && typeof options.drawerRoot.querySelector === 'function'
? options.drawerRoot
: document;
const chipsSelector = options.chipsSelector || '[data-active-filter-chips]';
const liveSelector = options.liveSelector || '[data-filter-live-region]';
const chipsEl = resolveDomQueryElement(chipsSelector, drawerRoot);
const liveEl = resolveDomQueryElement(liveSelector, drawerRoot);
const chipsClearAll = String(chipsEl?.dataset?.filterChipsClearAllLabel || '').trim();
const chipsRemove = String(chipsEl?.dataset?.filterChipsRemoveLabel || '').trim();
const filtersApplied = String(liveEl?.dataset?.filterLiveAppliedMessage || '').trim();
const filterRemoved = String(liveEl?.dataset?.filterLiveRemovedMessage || '').trim();
const filtersCleared = String(liveEl?.dataset?.filterLiveClearedMessage || '').trim();
return {
...(chipsClearAll !== '' ? { chipsClearAll } : {}),
...(chipsRemove !== '' ? { chipsRemove } : {}),
...(filtersApplied !== '' ? { filtersApplied } : {}),
...(filterRemoved !== '' ? { filterRemoved } : {}),
...(filtersCleared !== '' ? { filtersCleared } : {})
};
};
const normalizeListMode = (value) => {
const mode = String(value || '').trim().toLowerCase();
if (mode === 'drawer' || mode === 'search-only' || mode === 'none') {
return mode;
}
return 'drawer';
};
const resolveDrawerRoot = (gridConfig, filterOptions = {}) => {
const explicitRoot = filterOptions?.drawerRoot;
if (explicitRoot) {
return resolveDomQueryElement(explicitRoot) || document;
}
const containerEl = gridConfig?.container instanceof Element
? gridConfig.container
: null;
if (!containerEl) {
return document;
}
const drawerSelector = String(filterOptions?.drawer?.drawerSelector || '[data-filter-drawer]').trim() || '[data-filter-drawer]';
const openSelector = String(filterOptions?.drawer?.openSelector || '[data-filter-drawer-open]').trim() || '[data-filter-drawer-open]';
let current = containerEl;
while (current && current !== document.documentElement) {
const hasDrawer = Boolean(current.querySelector(drawerSelector));
const hasOpenButton = Boolean(current.querySelector(openSelector));
if (hasDrawer && hasOpenButton) {
return current;
}
current = current.parentElement;
}
return containerEl.parentElement || document;
};
/**
* Standardized list-page bootstrap:
* - builds grid filters from schema (or uses explicit filters)
* - initializes server grid
* - optionally wires drawer/chip filter experience
*/
export function initStandardListPage(options = {}) {
const gridOptions = options?.grid && typeof options.grid === 'object'
? options.grid
: {};
const filterOptions = options?.filters && typeof options.filters === 'object'
? options.filters
: {};
const hooks = options?.hooks && typeof options.hooks === 'object'
? options.hooks
: {};
const mode = normalizeListMode(filterOptions.mode);
const filterSchema = Array.isArray(gridOptions.filterSchema) ? gridOptions.filterSchema : [];
const extraFilters = Array.isArray(gridOptions.extraFilters) ? gridOptions.extraFilters : [];
const resolvedFilters = Array.isArray(gridOptions.filters)
? gridOptions.filters
: [...gridFiltersFromSchema(filterSchema), ...extraFilters];
const hasExplicitBindingMode = Object.prototype.hasOwnProperty.call(gridOptions, 'filterBindingMode');
const defaultBindingMode = mode === 'drawer' ? 'manual' : 'live';
const createOptions = {
...gridOptions,
filters: resolvedFilters,
filterBindingMode: hasExplicitBindingMode ? gridOptions.filterBindingMode : defaultBindingMode
};
delete createOptions.filterSchema;
delete createOptions.extraFilters;
const gridConfig = createServerGrid(createOptions);
if (typeof hooks.onGridReady === 'function') {
hooks.onGridReady({ gridConfig, mode });
}
let filterExperience = null;
if (mode === 'drawer' && gridConfig) {
const chipMeta = filterOptions.chipMeta && typeof filterOptions.chipMeta === 'object'
? filterOptions.chipMeta
: {};
const drawerConfig = filterOptions.drawer && typeof filterOptions.drawer === 'object'
? filterOptions.drawer
: {};
const liveConfig = filterOptions.live && typeof filterOptions.live === 'object'
? filterOptions.live
: {};
const chipUiConfig = filterOptions.chips && typeof filterOptions.chips === 'object'
? filterOptions.chips
: {};
const drawerRoot = resolveDrawerRoot(gridConfig, filterOptions);
const domLabels = readDomLabelDefaults({
drawerRoot,
chipsSelector: chipUiConfig.container || '[data-active-filter-chips]',
liveSelector: liveConfig.regionSelector || '[data-filter-live-region]'
});
const labels = {
...DEFAULT_LIST_FILTER_LABELS,
...domLabels,
...(filterOptions.labels && typeof filterOptions.labels === 'object' ? filterOptions.labels : {})
};
const preserveFilterParamsOnReset = Array.isArray(filterOptions.preserveFilterParamsOnReset)
? filterOptions.preserveFilterParamsOnReset
: [];
const watchInputs = Array.isArray(filterOptions.watchInputs)
? filterOptions.watchInputs
: [];
const clearMetaOptions = filterOptions.clearMetaOptions && typeof filterOptions.clearMetaOptions === 'object'
? filterOptions.clearMetaOptions
: {};
const buildChips = typeof filterOptions.buildChips === 'function'
? filterOptions.buildChips
: (state = {}) => buildChipsFromMeta(state, chipMeta);
const removeChip = typeof filterOptions.removeChip === 'function'
? filterOptions.removeChip
: (chip, state = {}) => removeChipFromMetaState(chip, state, chipMeta);
const clearAllState = typeof filterOptions.clearAllState === 'function'
? filterOptions.clearAllState
: (state = {}) => clearMetaState(state, chipMeta, clearMetaOptions);
filterExperience = initListFilterExperience({
gridConfig,
drawerRoot,
drawer: drawerConfig,
live: {
regionSelector: '[data-filter-live-region]',
appliedMessage: labels.filtersApplied,
removedMessage: labels.filterRemoved,
clearedMessage: labels.filtersCleared,
...liveConfig
},
chips: {
container: '[data-active-filter-chips]',
clearAllLabel: labels.chipsClearAll,
removeButtonLabel: labels.chipsRemove,
...chipUiConfig
},
preserveFilterParamsOnReset,
buildChips,
removeChip,
clearAllState,
watchInputs
});
}
if (typeof hooks.onFilterExperienceReady === 'function') {
hooks.onFilterExperienceReady({ gridConfig, filterExperience, mode });
}
return {
gridConfig,
filterExperience
};
}