diff --git a/v2/pink-sb/src/lib/spreadsheet/Cell.svelte b/v2/pink-sb/src/lib/spreadsheet/Cell.svelte index 308365f30e..49e5e42dc3 100644 --- a/v2/pink-sb/src/lib/spreadsheet/Cell.svelte +++ b/v2/pink-sb/src/lib/spreadsheet/Cell.svelte @@ -45,24 +45,20 @@ $: isVerticalEnd = alignment.startsWith('end'); $: isHorizontalStart = alignment.endsWith('start'); $: isHorizontalEnd = alignment.endsWith('end'); - $: endsBeforeFixedRight = column === root.lastColumnBeforeAction; + $: isEmptyCell = id?.includes(EMPTY_ROW_ID) || false; $: options = typeof column !== 'undefined' ? root.columns?.[column] : undefined; - $: resizable = (options?.resizable ?? true) && column !== root.lastResizableColumnId; - $: hasKeyboardNavigation = root.keyboardNavigation ?? false; - $: columnIndex = Array.isArray(root.columns) - ? root.columns.findIndex((col) => col.id === column) - : Object.values(root.columns).findIndex((col) => col.id === column); - - $: isHeaderBeingHovered = false; - $: isAction = options?.isAction ?? false; - $: isEditing = root.currentlyEditingCellId === id; $: isSelect = (root.allowSelection && column?.includes('__select_')) || false; $: isFixed = isSelect || isAction || options?.fixed; + $: endsBeforeFixedRight = column === root.lastColumnBeforeAction; + $: resizable = (options?.resizable ?? true) && column !== root.lastResizableColumnId; + $: isEditing = root.currentlyEditingCellId === id; $: isDraggedOver = root.dragOverColumn === column; $: isDragging = root.draggingColumn === column; - $: isEmptyCell = id?.includes(EMPTY_ROW_ID) || false; + + $: columnIndex = column && root.columnIndexMap ? (root.columnIndexMap.get(column) ?? -1) : -1; + $: columnWidth = typeof options?.width === 'number' ? options?.width @@ -70,6 +66,8 @@ ? options?.width.min : 100; + let isHeaderBeingHovered = false; + function handleKeydown(e: KeyboardEvent) { e.stopPropagation(); if (e.key === 'Escape') { @@ -216,6 +214,48 @@ } } + function handleCellClick() { + if (!openEditOnTap || !isEditable || isEmptyCell || isAction) return; + originalValue = value; + root.setEditing(id); + } + + function handleCellDoubleClick() { + if (!isEditable || isEmptyCell || isAction) return; + originalValue = value; + root.setEditing(id); + } + + function handleDragStart(e: DragEvent) { + root.startDrag(column, e); + } + + function handleDragOver(e: DragEvent) { + root.overDrag(column, e); + } + + function handleDragLeave() { + root.clearDragOver(); + } + + function handleMouseEnter() { + if (isHeader && options?.draggable) { + isHeaderBeingHovered = true; + root.setColumnHeaderHovered(column); + } + } + + function handleMouseLeave() { + if (isHeader && options?.draggable) { + isHeaderBeingHovered = false; + root.setColumnHeaderHovered(null); + } + } + + function handleClickOutside() { + if (isEditing) root.setEditing(null); + } + $: if (isEditing) { tick().then(() => { const selects = cellEl?.querySelector('button.input') as HTMLDivElement; @@ -283,41 +323,18 @@ style:left={isSelect ? '0' : undefined} style:right={isAction ? '0' : undefined} on:contextmenu={isEmptyCell ? undefined : handleContextMenu} - use:clickOutside={() => { - if (isEditing) root.setEditing(null); - }} - on:click={() => { - if (!openEditOnTap || !isEditable || isEmptyCell || isAction) return; - originalValue = value; - root.setEditing(id); - }} - on:dblclick={() => { - if (!isEditable || isEmptyCell || isAction) return; - originalValue = value; - root.setEditing(id); - }} - on:dragstart={(e) => root.startDrag(column, e)} - on:dragover={(e) => root.overDrag(column, e)} - on:dragleave={() => { - // Clear drag over when leaving the element - root.clearDragOver(); - }} + use:clickOutside={handleClickOutside} + on:click={handleCellClick} + on:dblclick={handleCellDoubleClick} + on:dragstart={handleDragStart} + on:dragover={handleDragOver} + on:dragleave={handleDragLeave} on:drop={root.endDrag} on:keydown={handleCellKeydown} on:focus={handleCellFocus} on:blur={handleCellBlur} - on:mouseenter={() => { - if (isHeader && options?.draggable) { - isHeaderBeingHovered = true; - root.setColumnHeaderHovered(column); - } - }} - on:mouseleave={() => { - if (isHeader && options?.draggable) { - isHeaderBeingHovered = false; - root.setColumnHeaderHovered(null); - } - }} + on:mouseenter={handleMouseEnter} + on:mouseleave={handleMouseLeave} > {#if isLoading && !isHeader} {@const variant = isSelect || isAction ? 'square' : 'line'} diff --git a/v2/pink-sb/src/lib/spreadsheet/Root.svelte b/v2/pink-sb/src/lib/spreadsheet/Root.svelte index bcb62b3ecf..f490542f5e 100644 --- a/v2/pink-sb/src/lib/spreadsheet/Root.svelte +++ b/v2/pink-sb/src/lib/spreadsheet/Root.svelte @@ -75,68 +75,80 @@ initColumns(); } + let scrollRafId: number | null = null; + const handleScroll = () => { - const totalPages = Math.ceil(rowCount / itemsPerPage) || 1; + if (scrollRafId !== null) return; - const scrollTop = $virtualizer.scrollElement?.scrollTop ?? 0; - const topVisibleIndex = Math.floor(scrollTop / ESTIMATED_ROW_HEIGHT); - const calculatedPage = Math.floor(topVisibleIndex / itemsPerPage) + 1; + scrollRafId = requestAnimationFrame(() => { + scrollRafId = null; - // update `currentPage` regardless of listeners availability on scroll! - if (calculatedPage !== currentPage && calculatedPage > 0 && calculatedPage <= totalPages) { - currentPage = calculatedPage; - } + const totalPages = Math.ceil(rowCount / itemsPerPage) || 1; - if (!virtualizer || loadingTriggered || loadingMore) return; - if (!loadPreviousPage && !loadNextPage) return; + const scrollTop = $virtualizer.scrollElement?.scrollTop ?? 0; + const topVisibleIndex = Math.floor(scrollTop / ESTIMATED_ROW_HEIGHT); + const calculatedPage = Math.floor(topVisibleIndex / itemsPerPage) + 1; - const virtualItems = $virtualizer.getVirtualItems(); - if (virtualItems.length === 0) return; + // update `currentPage` regardless of listeners availability on scroll! + if ( + calculatedPage !== currentPage && + calculatedPage > 0 && + calculatedPage <= totalPages + ) { + currentPage = calculatedPage; + } - // next page loading - if (loadNextPage) { - const scrollElement = $virtualizer.scrollElement; - if (!scrollElement) return; + if (!virtualizer || loadingTriggered || loadingMore) return; + if (!loadPreviousPage && !loadNextPage) return; - const { scrollTop, scrollHeight, clientHeight } = scrollElement; - const distanceFromBottom = scrollHeight - (scrollTop + clientHeight); - const triggerDistance = nextPageTriggerOffset * ESTIMATED_ROW_HEIGHT; - const hasBufferSpace = distanceFromBottom <= triggerDistance; + const virtualItems = $virtualizer.getVirtualItems(); + if (virtualItems.length === 0) return; - if (hasBufferSpace && !loadingTriggered) { - loadingTriggered = true; - const nextPage = Math.floor(rowCount / itemsPerPage) + 1; + // next page loading + if (loadNextPage) { + const scrollElement = $virtualizer.scrollElement; + if (!scrollElement) return; - loadNextPage(nextPage) - .then(() => { - loadingTriggered = false; - tick().then(() => $virtualizer.measure()); - }) - .catch(() => (loadingTriggered = false)); - } - } + const { scrollTop, scrollHeight, clientHeight } = scrollElement; + const distanceFromBottom = scrollHeight - (scrollTop + clientHeight); + const triggerDistance = nextPageTriggerOffset * ESTIMATED_ROW_HEIGHT; + const hasBufferSpace = distanceFromBottom <= triggerDistance; - if (loadPreviousPage) { - const firstVisibleItem = virtualItems[0]; - if (firstVisibleItem && firstVisibleItem.index >= 0) { - const pageOfFirstItem = Math.floor(firstVisibleItem.index / itemsPerPage) + 1; - - if (!lastCheckedPages.has(pageOfFirstItem)) { - lastCheckedPages.add(pageOfFirstItem); + if (hasBufferSpace && !loadingTriggered) { loadingTriggered = true; + const nextPage = Math.floor(rowCount / itemsPerPage) + 1; - loadPreviousPage(pageOfFirstItem) + loadNextPage(nextPage) .then(() => { loadingTriggered = false; - setTimeout(() => lastCheckedPages.delete(pageOfFirstItem), 2000); + tick().then(() => $virtualizer.measure()); }) - .catch(() => { - loadingTriggered = false; - lastCheckedPages.delete(pageOfFirstItem); - }); + .catch(() => (loadingTriggered = false)); } } - } + + if (loadPreviousPage) { + const firstVisibleItem = virtualItems[0]; + if (firstVisibleItem && firstVisibleItem.index >= 0) { + const pageOfFirstItem = Math.floor(firstVisibleItem.index / itemsPerPage) + 1; + + if (!lastCheckedPages.has(pageOfFirstItem)) { + lastCheckedPages.add(pageOfFirstItem); + loadingTriggered = true; + + loadPreviousPage(pageOfFirstItem) + .then(() => { + loadingTriggered = false; + setTimeout(() => lastCheckedPages.delete(pageOfFirstItem), 2000); + }) + .catch(() => { + loadingTriggered = false; + lastCheckedPages.delete(pageOfFirstItem); + }); + } + } + } + }); }; onMount(initColumns); @@ -271,22 +283,23 @@ } function toggleAll() { + const selectedSet = new Set(selectedRows); if (allRowsSelected) { - selectedRows = selectedRows.filter((row) => !availableIds.has(row)); + availableIds.forEach((id) => selectedSet.delete(id)); } else { - selectedRows = [ - ...selectedRows, - ...[...availableIds].filter((row) => !selectedRows.includes(row)) - ]; + availableIds.forEach((id) => selectedSet.add(id)); } + selectedRows = Array.from(selectedSet); } function toggle(id: string) { - if (selectedRows.includes(id)) { - selectedRows = selectedRows.filter((row) => row !== id); + const selectedSet = new Set(selectedRows); + if (selectedSet.has(id)) { + selectedSet.delete(id); } else { - selectedRows = [...selectedRows, id]; + selectedSet.add(id); } + selectedRows = Array.from(selectedSet); } function addAvailableId(id: string) { @@ -329,7 +342,6 @@ function endDrag() { const oldPositions = new Map(); - // should be equal as the length of columns const columnEls = Array.from(rootEl.querySelectorAll('[data-column-id]')) as HTMLElement[]; for (const column of columnEls) { @@ -353,11 +365,8 @@ requestAnimationFrame(() => { const movedElements: HTMLElement[] = []; - const swappedElements = Array.from( - rootEl.querySelectorAll('[data-column-id]') - ) as HTMLElement[]; - - for (const swappedElement of swappedElements) { + // reuse cached columnEls instead of querying again + for (const swappedElement of columnEls) { const id = swappedElement.getAttribute('data-column-id'); if (!id || !oldPositions.has(id)) continue; @@ -574,12 +583,28 @@ $: allRowsSelected = availableIds.size > 0 && [...availableIds].every((row) => selectedRows.includes(row)); + $: columnsById = groupById(columns); + $: columnIndexMap = createColumnIndexMap(columns); + $: gridTemplateColumns = createGridTemplateColumns(columns); + $: lastResizableColumnId = calculateLastResizableId(columns); + $: lastColumnBeforeAction = getLastVisibleColumnBeforeActions(columns); + $: borderRadiusValue = resolveBorderRadius(); + + function createColumnIndexMap(cols: typeof columns): Map { + const map = new Map(); + cols.forEach((col, index) => { + map.set(col.id, index); + }); + return map; + } + $: root = { loading, selectedRows, allowSelection, keyboardNavigation, - columns: groupById(columns), + columns: columnsById, + columnIndexMap, toggleAll, toggle, updateCells, @@ -596,8 +621,8 @@ overDrag, endDrag, clearDragOver, - lastResizableColumnId: calculateLastResizableId(columns), - lastColumnBeforeAction: getLastVisibleColumnBeforeActions(columns), + lastResizableColumnId, + lastColumnBeforeAction, registerForNavigation, unregisterForNavigation, moveFocus, @@ -671,18 +696,13 @@ }} /> -
+
{#if $$slots.header} diff --git a/v2/pink-sb/src/lib/spreadsheet/index.ts b/v2/pink-sb/src/lib/spreadsheet/index.ts index 5cdf4e820b..2c87d3f3ed 100644 --- a/v2/pink-sb/src/lib/spreadsheet/index.ts +++ b/v2/pink-sb/src/lib/spreadsheet/index.ts @@ -30,6 +30,7 @@ export type SpreadsheetRootProps = Readonly<{ selectedNone: boolean; selectedSome: boolean; columns: Record; + columnIndexMap: Map; toggle: (id: string) => void; toggleAll: () => void; addAvailableId: (id: string) => void; diff --git a/v2/pink-sb/src/lib/spreadsheet/page/SparsePagedData.ts b/v2/pink-sb/src/lib/spreadsheet/page/SparsePagedData.ts index e118c08493..b87b27c2b6 100644 --- a/v2/pink-sb/src/lib/spreadsheet/page/SparsePagedData.ts +++ b/v2/pink-sb/src/lib/spreadsheet/page/SparsePagedData.ts @@ -96,14 +96,7 @@ export class SparsePagedData { // try cache first if (this._itemCache.has(virtualIndex)) { this._cacheHits++; - const cachedItem = this._itemCache.get(virtualIndex)!; - - const pageNum = Math.floor(virtualIndex / this._itemsPerPage) + 1; - if (this._pageData.has(pageNum)) { - this._updateLRU(pageNum); - } - - return cachedItem; + return this._itemCache.get(virtualIndex)!; } this._cacheMisses++; @@ -117,8 +110,6 @@ export class SparsePagedData { return null; } - this._updateLRU(pageNum); - const pageItems = this._pageData.get(pageNum)!; const item = offset < pageItems.length ? pageItems[offset] : null; @@ -202,11 +193,7 @@ export class SparsePagedData { } hasPage(pageNum: number): boolean { - const hasPage = this._loadedPagesSet.has(pageNum); - if (hasPage) { - this._updateLRU(pageNum); - } - return hasPage; + return this._loadedPagesSet.has(pageNum); } hasItemAtVirtualIndex(virtualIndex: number): boolean { diff --git a/v2/pink-sb/src/lib/spreadsheet/row/Base.svelte b/v2/pink-sb/src/lib/spreadsheet/row/Base.svelte index d8b4cb1015..2b379a986d 100644 --- a/v2/pink-sb/src/lib/spreadsheet/row/Base.svelte +++ b/v2/pink-sb/src/lib/spreadsheet/row/Base.svelte @@ -132,7 +132,6 @@ root.loading || select === 'disabled'} on:change={isHeader ? root.toggleAll : toggle} - on:blur={() => console.log(`checkbox blurred?`)} checked={isHeader ? root.selectedAll ? true