|
|
|
|
@@ -3,6 +3,55 @@ import { gridFiltersFromSchema } 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 safeStorageGet = (key) => {
|
|
|
|
|
const storageKey = String(key || '').trim();
|
|
|
|
|
if (storageKey === '') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return window.localStorage.getItem(storageKey);
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const safeStorageSet = (key, value) => {
|
|
|
|
|
const storageKey = String(key || '').trim();
|
|
|
|
|
if (storageKey === '') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
window.localStorage.setItem(storageKey, value);
|
|
|
|
|
} catch {
|
|
|
|
|
// localStorage can be unavailable; fail safe.
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function createServerGrid(options) {
|
|
|
|
|
const {
|
|
|
|
|
container,
|
|
|
|
|
@@ -22,8 +71,10 @@ export function createServerGrid(options) {
|
|
|
|
|
gridjs,
|
|
|
|
|
rowDblClick = null,
|
|
|
|
|
actions = null,
|
|
|
|
|
rowInteraction = null,
|
|
|
|
|
rowDataset = null,
|
|
|
|
|
selection = null
|
|
|
|
|
selection = null,
|
|
|
|
|
pageSize = null
|
|
|
|
|
} = options || {};
|
|
|
|
|
|
|
|
|
|
const normalizedFilterBindingMode = String(filterBindingMode || 'live').trim().toLowerCase() === 'manual'
|
|
|
|
|
@@ -47,14 +98,18 @@ export function createServerGrid(options) {
|
|
|
|
|
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);
|
|
|
|
|
@@ -84,6 +139,68 @@ export function createServerGrid(options) {
|
|
|
|
|
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 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';
|
|
|
|
|
|
|
|
|
|
let currentLimit = defaultLimit;
|
|
|
|
|
const urlLimit = syncLimit ? parseAllowedLimit(params.get(limitParam)) : null;
|
|
|
|
|
if (urlLimit !== null) {
|
|
|
|
|
currentLimit = urlLimit;
|
|
|
|
|
} else if (pageSizeEnabled) {
|
|
|
|
|
const storedLimit = parseAllowedLimit(safeStorageGet(pageSizeStorageKey));
|
|
|
|
|
if (storedLimit !== null) {
|
|
|
|
|
currentLimit = storedLimit;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (pageSizeEnabled) {
|
|
|
|
|
safeStorageSet(pageSizeStorageKey, String(currentLimit));
|
|
|
|
|
}
|
|
|
|
|
let currentPage = urlStateConfig.syncPage ? parsePage(params.get(pageParam)) : 1;
|
|
|
|
|
let currentSort = null;
|
|
|
|
|
if (urlStateConfig.syncSort) {
|
|
|
|
|
@@ -93,6 +210,36 @@ export function createServerGrid(options) {
|
|
|
|
|
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 showRowActionButton = rowInteractionEnabled && rowInteractionConfig.showActionButton !== false;
|
|
|
|
|
const rowActionColumnLabel = String(
|
|
|
|
|
rowInteractionConfig.actionColumnLabel
|
|
|
|
|
|| language?.actions?.column
|
|
|
|
|
|| 'Actions'
|
|
|
|
|
).trim() || 'Actions';
|
|
|
|
|
const rowActionOpenLabel = String(language?.actions?.open || 'Open').trim() || 'Open';
|
|
|
|
|
const rowActionEditLabel = String(language?.actions?.edit || 'Edit').trim() || 'Edit';
|
|
|
|
|
const defaultResolveRowActionLabel = (url) => (
|
|
|
|
|
/(^|\/)edit(\/|$|\?)/i.test(String(url || '').trim())
|
|
|
|
|
? rowActionEditLabel
|
|
|
|
|
: rowActionOpenLabel
|
|
|
|
|
);
|
|
|
|
|
const resolveActionLabel = typeof rowInteractionConfig.resolveActionLabel === 'function'
|
|
|
|
|
? rowInteractionConfig.resolveActionLabel
|
|
|
|
|
: defaultResolveRowActionLabel;
|
|
|
|
|
const resolveRowOpenUrl = (rowData, rowIndex = -1) => {
|
|
|
|
|
if (!rowInteractionEnabled) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
const url = rowDblClick.getUrl(rowData, rowIndex);
|
|
|
|
|
return String(url || '').trim();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getInput = (input) => {
|
|
|
|
|
if (!input) {return null;}
|
|
|
|
|
@@ -223,6 +370,15 @@ export function createServerGrid(options) {
|
|
|
|
|
} 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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -244,6 +400,7 @@ export function createServerGrid(options) {
|
|
|
|
|
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') {
|
|
|
|
|
@@ -253,7 +410,7 @@ export function createServerGrid(options) {
|
|
|
|
|
window.history.replaceState({}, '', url.toString());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const actionConfig = actions && actions.enabled ? {
|
|
|
|
|
const explicitActionConfig = actions && actions.enabled ? {
|
|
|
|
|
label: actions.label || 'Actions',
|
|
|
|
|
index: Number.isInteger(actions.index) ? actions.index : null,
|
|
|
|
|
items: Array.isArray(actions.items) ? actions.items : [],
|
|
|
|
|
@@ -261,6 +418,26 @@ export function createServerGrid(options) {
|
|
|
|
|
wrapperClass: actions.wrapperClass || 'grid-actions',
|
|
|
|
|
onAction: typeof actions.onAction === 'function' ? actions.onAction : null
|
|
|
|
|
} : null;
|
|
|
|
|
const rowOpenActionConfig = !explicitActionConfig && showRowActionButton ? {
|
|
|
|
|
label: rowActionColumnLabel,
|
|
|
|
|
index: null,
|
|
|
|
|
items: [{
|
|
|
|
|
key: 'row-open',
|
|
|
|
|
type: 'link',
|
|
|
|
|
className: 'outline secondary app-grid-row-open',
|
|
|
|
|
label: (rowData, url) => {
|
|
|
|
|
const dynamicLabel = resolveActionLabel(url, rowData);
|
|
|
|
|
const normalizedLabel = String(dynamicLabel || '').trim();
|
|
|
|
|
return normalizedLabel || defaultResolveRowActionLabel(url);
|
|
|
|
|
},
|
|
|
|
|
href: ({ id }) => id
|
|
|
|
|
}],
|
|
|
|
|
// For row-open actions the URL itself is the stable row identifier.
|
|
|
|
|
getRowId: (rowData) => resolveRowOpenUrl(rowData),
|
|
|
|
|
wrapperClass: 'grid-actions grid-actions-row-open',
|
|
|
|
|
onAction: null
|
|
|
|
|
} : null;
|
|
|
|
|
const actionConfig = explicitActionConfig || rowOpenActionConfig;
|
|
|
|
|
const selectionConfig = selection && selection.enabled ? {
|
|
|
|
|
id: selection.id || 'selectRow',
|
|
|
|
|
index: Number.isInteger(selection.index) ? selection.index : 0,
|
|
|
|
|
@@ -402,18 +579,22 @@ export function createServerGrid(options) {
|
|
|
|
|
} : { enabled: false };
|
|
|
|
|
|
|
|
|
|
const paginationConfig = {
|
|
|
|
|
limit: paginationLimit,
|
|
|
|
|
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 = Number.parseInt(String(limit ?? ''), 10);
|
|
|
|
|
const parsedLimit = parseAllowedLimit(limit);
|
|
|
|
|
const nextPage = Number.isFinite(parsedPage) && parsedPage >= 0 ? parsedPage : 0;
|
|
|
|
|
const nextLimit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : paginationLimit;
|
|
|
|
|
const nextLimit = parsedLimit ?? currentLimit;
|
|
|
|
|
currentLimit = nextLimit;
|
|
|
|
|
if (pageSizeEnabled) {
|
|
|
|
|
safeStorageSet(pageSizeStorageKey, String(currentLimit));
|
|
|
|
|
}
|
|
|
|
|
currentPage = nextPage + 1;
|
|
|
|
|
if (urlSync && urlStateConfig.syncPage) {
|
|
|
|
|
if (urlSync && (urlStateConfig.syncPage || syncLimit)) {
|
|
|
|
|
updateUrl();
|
|
|
|
|
}
|
|
|
|
|
const offset = nextPage * nextLimit;
|
|
|
|
|
@@ -476,22 +657,37 @@ export function createServerGrid(options) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const applyRowDataset = () => {
|
|
|
|
|
if (typeof rowDataset !== 'function') {return;}
|
|
|
|
|
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;}
|
|
|
|
|
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);
|
|
|
|
|
if (rowInteractionEnabled) {
|
|
|
|
|
const rowOpenUrl = resolveRowOpenUrl(rowData, index);
|
|
|
|
|
if (rowOpenUrl !== '') {
|
|
|
|
|
rowEl.setAttribute('data-row-open-url', rowOpenUrl);
|
|
|
|
|
rowEl.setAttribute('tabindex', '-1');
|
|
|
|
|
} else {
|
|
|
|
|
rowEl.setAttribute(attr, String(value));
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -530,7 +726,7 @@ export function createServerGrid(options) {
|
|
|
|
|
updateHeaderState();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (typeof rowDataset === 'function') {
|
|
|
|
|
if (typeof rowDataset === 'function' || rowInteractionEnabled) {
|
|
|
|
|
const observer = new MutationObserver(() => applyRowDataset());
|
|
|
|
|
observer.observe(containerEl, { childList: true, subtree: true });
|
|
|
|
|
applyRowDataset();
|
|
|
|
|
@@ -547,25 +743,71 @@ export function createServerGrid(options) {
|
|
|
|
|
return { rowData, rowIndex };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (rowDblClick) {
|
|
|
|
|
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');
|
|
|
|
|
const rowInfo = getRowDataByElement(rowEl);
|
|
|
|
|
if (!rowInfo) {
|
|
|
|
|
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 { rowData, rowIndex } = rowInfo;
|
|
|
|
|
const getUrl = rowDblClick.getUrl;
|
|
|
|
|
if (typeof getUrl !== 'function') {
|
|
|
|
|
const rowEl = target.closest('.gridjs-tr[data-row-open-url]');
|
|
|
|
|
if (!rowEl) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const url = getUrl(rowData, rowIndex);
|
|
|
|
|
if (url) {
|
|
|
|
|
window.location.href = url;
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
@@ -581,13 +823,13 @@ export function createServerGrid(options) {
|
|
|
|
|
if (!rowInfo) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const { rowData } = rowInfo;
|
|
|
|
|
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) : null;
|
|
|
|
|
const id = actionConfig.getRowId ? actionConfig.getRowId(rowData, rowIndex) : null;
|
|
|
|
|
if (!id) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -614,6 +856,91 @@ export function createServerGrid(options) {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 = pageSizeLabel;
|
|
|
|
|
|
|
|
|
|
const select = document.createElement('select');
|
|
|
|
|
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;
|
|
|
|
|
safeStorageSet(pageSizeStorageKey, 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';
|
|
|
|
|
@@ -621,12 +948,18 @@ export function createServerGrid(options) {
|
|
|
|
|
currentPage = 1;
|
|
|
|
|
}
|
|
|
|
|
updateUrl({ historyMode: urlHistoryMode });
|
|
|
|
|
const nextConfig = {
|
|
|
|
|
server: { ...serverConfig, url: baseUrl() }
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
if (resetPage && grid?.config?.pagination && typeof grid.config.pagination === 'object') {
|
|
|
|
|
nextConfig.pagination = { ...grid.config.pagination, page: 0 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use Grid.js reconfiguration API so server pipeline is refreshed reliably on filter/search changes.
|
|
|
|
|
if (typeof grid?.updateConfig === 'function') {
|
|
|
|
|
@@ -636,13 +969,13 @@ export function createServerGrid(options) {
|
|
|
|
|
|
|
|
|
|
if (grid?.config) {
|
|
|
|
|
grid.config.server = nextConfig.server;
|
|
|
|
|
if (nextConfig.pagination) {
|
|
|
|
|
grid.config.pagination = nextConfig.pagination;
|
|
|
|
|
}
|
|
|
|
|
grid.config.pagination = nextConfig.pagination;
|
|
|
|
|
}
|
|
|
|
|
grid.forceRender();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mountPageSizeControl();
|
|
|
|
|
|
|
|
|
|
const debounce = (fn, delay = 250) => {
|
|
|
|
|
let timer;
|
|
|
|
|
return (...args) => {
|
|
|
|
|
@@ -724,7 +1057,8 @@ export function createServerGrid(options) {
|
|
|
|
|
search: searchValue,
|
|
|
|
|
filters: filterValues,
|
|
|
|
|
sort: currentSort ? { ...currentSort } : null,
|
|
|
|
|
page: currentPage
|
|
|
|
|
page: currentPage,
|
|
|
|
|
limit: currentLimit
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -741,7 +1075,8 @@ export function createServerGrid(options) {
|
|
|
|
|
search: draftSearch,
|
|
|
|
|
filters: filterValues,
|
|
|
|
|
sort: currentSort ? { ...currentSort } : null,
|
|
|
|
|
page: currentPage
|
|
|
|
|
page: currentPage,
|
|
|
|
|
limit: currentLimit
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -783,6 +1118,12 @@ export function createServerGrid(options) {
|
|
|
|
|
const parsedPage = parsePage(state.page);
|
|
|
|
|
currentPage = parsedPage;
|
|
|
|
|
}
|
|
|
|
|
const nextLimit = parseAllowedLimit(state?.limit);
|
|
|
|
|
if (nextLimit !== null) {
|
|
|
|
|
currentLimit = nextLimit;
|
|
|
|
|
safeStorageSet(pageSizeStorageKey, String(currentLimit));
|
|
|
|
|
syncPageSizeControl();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getSortParams = () => (currentSort ? { ...currentSort } : null);
|
|
|
|
|
|