import { NileGrid } from './nile-grid'; import { getHead, getBody } from './nile-grid.utils'; import { getScrollbarComp } from './nile-grid.resize'; import { addVariablesValue, computeColumnWidths } from './nile-grid.width'; import { NILE_GRID_HEAD_ITEM } from './types/nile-grid.enums'; /** * @param nileGrid Parent NileGrid instance * @param slotEl element hosting head/body * @param ensureWidths Function to ensure columnWidths length matches colCount * @param stickyLeftIndexes Mutable list of left-sticky column indexes * @param stickyRightIndexes Mutable list of right-sticky column indexes * @returns void */ export const layout = ( nileGrid: NileGrid, slotEl: any, ensureWidths: any, stickyLeftIndexes: number[], stickyRightIndexes: number[] ) => { const head = getHead(slotEl); const body = getBody(slotEl); if (!head && !body) return; let colCount = nileGrid.colCount || 0; let headerChanged = false; if (nileGrid.needsStructureLayout) { headerChanged = true; nileGrid.headRows = head ? (Array.from(head.querySelectorAll('nile-grid-row')) as HTMLElement[]) : []; colCount = 0; if (nileGrid.headRows.length) { colCount = computeHeadLayout(nileGrid, nileGrid.headRows, [], colCount, stickyLeftIndexes, stickyRightIndexes); } if (body) { nileGrid.bodyRows = Array.from(body.querySelectorAll('nile-grid-row')) as HTMLElement[]; if (colCount) { computeBodyLayout(nileGrid, nileGrid.bodyRows, [], colCount, stickyLeftIndexes, stickyRightIndexes); } else { colCount = computeBodyLayout(nileGrid, nileGrid.bodyRows, [], colCount, stickyLeftIndexes, stickyRightIndexes); } } if (nileGrid.stickyLeftIndexes.length || nileGrid.stickyRightIndexes.length) { cacheStickyEls(nileGrid); } else { nileGrid.stickyLeftHeadEls = []; nileGrid.stickyRightHeadEls = []; nileGrid.stickyLeftBodyEls = []; nileGrid.stickyRightBodyEls = []; nileGrid.shadowLeftVisibleSticky = false; nileGrid.shadowRightVisibleSticky = false; } nileGrid.colCount = colCount; nileGrid.needsStructureLayout = false; nileGrid.needsSpanLayout = false; nileGrid.needsWidthLayout = true; } else if (nileGrid.needsSpanLayout && body) { const bodyRows = nileGrid.bodyRows; colCount = nileGrid.colCount || 0; if (colCount) { computeBodyLayout(nileGrid, bodyRows, [], colCount, stickyLeftIndexes, stickyRightIndexes); } nileGrid.needsSpanLayout = false; } if (nileGrid.needsWidthLayout) { colCount = nileGrid.colCount || colCount; if (!colCount) return; ensureWidths(colCount); computeColumnWidths(nileGrid, colCount, nileGrid.columnWidths, slotEl, nileGrid.lockedWidthColumns, headerChanged); addVariablesValue(nileGrid); applyColumnsTemplate(nileGrid, nileGrid.columnWidths); nileGrid.needsWidthLayout = false; if (!nileGrid.isGridReady) { nileGrid.isGridReady = true; nileGrid.emit('nile-grid-ready'); } } }; export function cacheStickyEls(nileGrid: NileGrid) { const { head, body } = getHeadAndBody(nileGrid); nileGrid.stickyLeftHeadEls = head ? Array.from(head.querySelectorAll('nile-grid-head-item[data-sticky="left"]')) : []; nileGrid.stickyRightHeadEls = head ? Array.from(head.querySelectorAll('nile-grid-head-item[data-sticky="right"]')) : []; nileGrid.stickyLeftBodyEls = body ? Array.from(body.querySelectorAll('nile-grid-cell-item[data-sticky="left"]')) : []; nileGrid.stickyRightBodyEls = body ? Array.from(body.querySelectorAll('nile-grid-cell-item[data-sticky="right"]')) : []; } /** * @param nileGrid The parent NileGrid instance * @param rows Array of grid row elements * @param carry Array tracking rowspan, colspan continuation * @param colCount Total column count * @param stickyLeftIndexes List of sticky-left column indexes * @param stickyRightIndexes List of sticky-right column indexes * @param columnWidths Computed column widths * @param opts Options controlling layout behavior * @returns Final computed column count */ function layoutRows( nileGrid: NileGrid, rows: HTMLElement[], carry: number[], colCount: number, stickyLeftIndexes: number[], stickyRightIndexes: number[], opts: { itemSelector: string; useStickyAttr?: boolean; handleRowBg?: boolean } ): number { rows.forEach((row, rIdx) => { if (nileGrid.hoverable) row.setAttribute('hover', 'true'); colCount = layoutSingleRow( nileGrid, row, rIdx, carry, colCount, stickyLeftIndexes, stickyRightIndexes, opts ); }); return colCount; } /** * @param nileGrid The grid element being processed * @param row Single row element to layout * @param rIdx Row index in the grid * @param carry Rowspan tracking array * @param colCount Current column count * @param stickyLeftIndexes Sticky-left indexes * @param stickyRightIndexes Sticky-right indexes * @param opts Layout options * @returns Updated column count */ function layoutSingleRow( nileGrid: NileGrid, row: HTMLElement, rIdx: number, carry: number[], colCount: number, stickyLeftIndexes: number[], stickyRightIndexes: number[], opts: { itemSelector: string; useStickyAttr?: boolean } ): number { let c = 1; const rStart = rIdx + 1; const tag = opts.itemSelector.toLowerCase(); const items = Array.from(row.children).filter( (child): child is HTMLElement => child.tagName.toLowerCase() === tag ); for (const el of items) { while ((carry[c - 1] || 0) > 0) c++; ({ c, colCount } = layoutCell( nileGrid, el, c, rStart, carry, colCount, stickyLeftIndexes, stickyRightIndexes, opts )); } decrementCarry(carry); return colCount; } /** * @param nileGrid The parent grid * @param el Cell element being laid out * @param c Current column index * @param rStart Row start index * @param carry Rowspan carry array * @param colCount Current max column count * @param stickyLeftIndexes Left sticky indexes * @param stickyRightIndexes Right sticky indexes * @param opts Sticky and layout options * @returns Updated column index and column count */ function layoutCell( nileGrid: NileGrid, el: HTMLElement, c: number, rStart: number, carry: number[], colCount: number, stickyLeftIndexes: number[], stickyRightIndexes: number[], opts: { useStickyAttr?: boolean } ) { const colspan = Math.max(1, +el.getAttribute('colspan')! || 1); const rowspan = Math.max(1, +el.getAttribute('rowspan')! || 1); handleRowspan(nileGrid, el, rowspan); const cStart = c; const cEnd = c + colspan; setGridPosition(el, cStart, cEnd, rStart, rowspan); if (opts.useStickyAttr || stickyLeftIndexes.length || stickyRightIndexes.length) { handleSticky(el, cStart, opts, stickyLeftIndexes, stickyRightIndexes); } for (let k = 0; k < colspan; k++) { carry[c - 1 + k] = Math.max(carry[c - 1 + k] || 0, rowspan); } return { c: cEnd, colCount: Math.max(colCount, cEnd - 1), }; } /** * @param nileGrid The grid container * @param el Element that spans multiple rows * @param rowspan Number of rows to span */ function handleRowspan(nileGrid: NileGrid, el: HTMLElement, rowspan: number) { if (rowspan <= 1) return; el.style.position = "absolute"; el.style.zIndex = "1"; if (el.tagName === NILE_GRID_HEAD_ITEM) { if (!nileGrid.headHeight) nileGrid.headHeight = el.getBoundingClientRect().height; el.style.height = `${(nileGrid.headHeight * rowspan) + (rowspan - 1)}px`; el.style.backgroundColor = "var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary))"; el.style.backgroundColor = "var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary))"; } else { if (!nileGrid.cellHeight) nileGrid.cellHeight = el.getBoundingClientRect().height; el.style.height = `${(nileGrid.cellHeight * rowspan) + (rowspan - 1)}px`; el.style.backgroundColor = "var(--nile-colors-white-base, var(--ng-colors-bg-primary))"; el.style.backgroundColor = "var(--nile-colors-white-base, var(--ng-colors-bg-primary))"; } } /** * @param el Grid item element * @param cStart Starting column index * @param cEnd Ending column index * @param rStart Starting row index * @param rowspan Row span value */ function setGridPosition( el: HTMLElement, cStart: number, cEnd: number, rStart: number, rowspan: number ) { const rEnd = rStart + rowspan; el.style.gridColumn = `${cStart} / ${cEnd}`; el.style.gridRow = `${rStart} / ${rEnd}`; (el as any).dataset.cStart = String(cStart); } /** * @param el Grid item element * @param cStart Column start index * @param opts Sticky configuration options * @param stickyLeftIndexes Left sticky indexes * @param stickyRightIndexes Right sticky indexes */ function handleSticky( el: HTMLElement, cStart: number, opts: { useStickyAttr?: boolean }, stickyLeftIndexes: number[], stickyRightIndexes: number[], ) { if (opts.useStickyAttr && el.getAttribute('sticky') === "left" && !stickyLeftIndexes.includes(cStart - 1)) { stickyLeftIndexes.push(cStart - 1); } if (opts.useStickyAttr && el.getAttribute('sticky') === "right" && !stickyRightIndexes.includes(cStart - 1)) { stickyRightIndexes.push(cStart - 1); } applySticky(el, cStart - 1, stickyLeftIndexes, stickyRightIndexes); } /** * @param carry Rowspan tracking array */ function decrementCarry(carry: number[]) { for (let i = 0; i < carry.length; i++) { if (carry[i] > 0) { carry[i]--; } } } /** * @param el Target grid cell element * @param colIndex Column index * @param stickyLeftIndexes Sticky-left indexes * @param stickyRightIndexes Sticky-right indexes */ function applySticky( el: HTMLElement, colIndex: number, stickyLeftIndexes: number[], stickyRightIndexes: number[], ) { const onLeft = stickyLeftIndexes.includes(colIndex); const onRight = stickyRightIndexes.includes(colIndex); if (!onLeft && !onRight) return; prepareStickyElement(el); if (onLeft && !onRight) { applyLeftSticky(el, colIndex); } else { applyRightSticky(el, colIndex); } } /** * @param el Grid item element to prepare for sticky positioning */ function prepareStickyElement(el: HTMLElement) { el.style.position = 'sticky'; el.style.left = ''; el.style.right = ''; el.removeAttribute('data-sticky'); } /** * @param el Target element * @param colIndex Column index * @param stickyLeftIndexes Sticky-left column indexes */ function applyLeftSticky( el: HTMLElement, colIndex: number, ) { el.setAttribute('data-sticky', 'left'); if (el.tagName === NILE_GRID_HEAD_ITEM) { el.style.backgroundColor = "var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary))"; } else { el.style.backgroundColor = 'var(--nile-colors-white-base, var(--ng-colors-bg-primary))'; } el.style.zIndex = '1'; el.style.left = `var(--sticky-left-${colIndex})`; } /** * @param el Target element * @param colIndex Column index * @param stickyRightIndexes Sticky-right indexes */ function applyRightSticky( el: HTMLElement, colIndex: number, ) { el.setAttribute('data-sticky', 'right'); if (el.tagName === NILE_GRID_HEAD_ITEM) { el.style.backgroundColor = "var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary))"; } else { el.style.backgroundColor = 'var(--nile-colors-white-base, var(--ng-colors-bg-primary))'; } el.style.zIndex = '1'; el.style.right = `var(--sticky-right-${colIndex})`; } /** * @param nileGrid Grid instance to sync shadows for */ export function syncStickyShadows(nileGrid: NileGrid) { if (!nileGrid.stickyLeftIndexes.length && !nileGrid.stickyRightIndexes.length) { return; } const { showRightOnLeftStickies, showLeftOnRightStickies } = getShadowStates(nileGrid); if ( nileGrid.shadowLeftVisibleSticky === showRightOnLeftStickies && nileGrid.shadowRightVisibleSticky === showLeftOnRightStickies ) { return; } nileGrid.shadowLeftVisibleSticky = showRightOnLeftStickies; nileGrid.shadowRightVisibleSticky = showLeftOnRightStickies; for (const el of nileGrid.stickyLeftHeadEls) { el.classList.toggle('shadow-left', showRightOnLeftStickies); } for (const el of nileGrid.stickyLeftBodyEls) { el.classList.toggle('shadow-left', showRightOnLeftStickies); } for (const el of nileGrid.stickyRightHeadEls) { el.classList.toggle('shadow-right', showLeftOnRightStickies); } for (const el of nileGrid.stickyRightBodyEls) { el.classList.toggle('shadow-right', showLeftOnRightStickies); } } /** * @param nileGrid Grid instance * @returns Object containing booleans for left/right shadow visibility */ function getShadowStates(nileGrid: NileGrid) { const max = Math.max(0, nileGrid.scrollWidth - nileGrid.clientWidth); return { showRightOnLeftStickies: nileGrid.scrollLeft > 0, showLeftOnRightStickies: nileGrid.scrollLeft < max }; } /** computeHeadLayout * @param nileGrid Grid instance * @param headRows Header rows ([]) * @param carryHead Rowspan carry tracking array * @param colCount Current column count * @param stickyLeftIndexes Left-sticky indexes * @param stickyRightIndexes Right-sticky indexes * @returns Final column count after header layout */ function computeHeadLayout( nileGrid: NileGrid, headRows: HTMLElement[], carryHead: number[], colCount: number, stickyLeftIndexes: number[], stickyRightIndexes: number[], ): number { return layoutRows( nileGrid, headRows, carryHead, colCount, stickyLeftIndexes, stickyRightIndexes, { itemSelector: 'nile-grid-head-item', useStickyAttr: true, handleRowBg: false, } ); } /** computeBodyLayout * @param nileGrid Grid instance * @param bodyRows Body rows ([]) * @param carryBody Rowspan carry tracking array * @param colCount Current column count * @param stickyLeftIndexes Left-sticky indexes * @param stickyRightIndexes Right-sticky indexes * @returns Final column count after body layout */ function computeBodyLayout( nileGrid: NileGrid, bodyRows: HTMLElement[], carryBody: number[], colCount: number, stickyLeftIndexes: number[], stickyRightIndexes: number[], ): number { return layoutRows( nileGrid, bodyRows, carryBody, colCount, stickyLeftIndexes, stickyRightIndexes, { itemSelector: 'nile-grid-cell-item', useStickyAttr: false, handleRowBg: true, } ); } /** * @param nileGrid Grid container element * @param colCount Total number of columns * @param columnWidths Column width array */ export function applyColumnsTemplate( nileGrid: NileGrid, columnWidths: number[] ) { const cols = columnWidths .map((w, idx) => { if (idx === columnWidths.length - 1) { return `minmax(${Math.round(w)}px, auto)`; } return `${Math.max(0, Math.round(w))}px`; }) .join(' '); nileGrid.style.setProperty('--nile-grid-columns', cols); } /** * @param nileGrid Grid instance * @returns Head and body elements from the grid slot */ function getHeadAndBody(nileGrid: NileGrid) { if (!nileGrid.shadowRoot) return { head: undefined, body: undefined }; const assigned = nileGrid.slotEl.assignedElements(); const find = (tag: string) => assigned.find(n => n.tagName.toLowerCase() === tag) as HTMLElement | undefined; return { head: find('nile-grid-head'), body: find('nile-grid-body') }; }