From d6ba87232e6d4a9274de5b35da51404a918288fc Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 3 Apr 2025 17:08:45 +0530 Subject: [PATCH 01/14] feat: spreadsheet component [WIP] --- pnpm-lock.yaml | 65 +++++- v2/pink-sb/src/lib/helpers/helpers.ts | 16 ++ v2/pink-sb/src/lib/index.ts | 1 + v2/pink-sb/src/lib/input/Textarea.svelte | 2 + v2/pink-sb/src/lib/spreadsheet/Cell.svelte | 179 ++++++++++++++++ v2/pink-sb/src/lib/spreadsheet/Root.svelte | 191 ++++++++++++++++++ .../src/lib/spreadsheet/header/Cell.svelte | 11 + .../src/lib/spreadsheet/header/index.ts | 5 + v2/pink-sb/src/lib/spreadsheet/index.ts | 53 +++++ .../src/lib/spreadsheet/row/Base.svelte | 70 +++++++ v2/pink-sb/src/lib/spreadsheet/row/index.ts | 11 + .../src/stories/Spreadsheet.stories.svelte | 148 ++++++++++++++ 12 files changed, 745 insertions(+), 7 deletions(-) create mode 100644 v2/pink-sb/src/lib/spreadsheet/Cell.svelte create mode 100644 v2/pink-sb/src/lib/spreadsheet/Root.svelte create mode 100644 v2/pink-sb/src/lib/spreadsheet/header/Cell.svelte create mode 100644 v2/pink-sb/src/lib/spreadsheet/header/index.ts create mode 100644 v2/pink-sb/src/lib/spreadsheet/index.ts create mode 100644 v2/pink-sb/src/lib/spreadsheet/row/Base.svelte create mode 100644 v2/pink-sb/src/lib/spreadsheet/row/index.ts create mode 100644 v2/pink-sb/src/stories/Spreadsheet.stories.svelte diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f52266473..13908d1900 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,10 +146,10 @@ importers: version: 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.3(@babel/core@7.25.2))) '@sveltejs/adapter-auto': specifier: ^3.2.3 - version: 3.2.3(@sveltejs/kit@2.5.21(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8))) + version: 3.2.3(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8))) '@sveltejs/kit': specifier: ^2.5.21 - version: 2.5.21(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)) + version: 2.20.2(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)) '@sveltejs/package': specifier: ^2.3.3 version: 2.3.3(svelte@4.2.18)(typescript@5.5.4) @@ -215,7 +215,7 @@ importers: version: 4.2.18 svelte-check: specifier: ^3.8.5 - version: 3.8.5(@babel/core@7.25.2)(postcss-load-config@3.1.4(postcss@8.4.41))(postcss@8.4.41)(sass@1.77.8)(svelte@4.2.18) + version: 3.8.6(@babel/core@7.25.2)(postcss-load-config@3.1.4(postcss@8.4.41))(postcss@8.4.41)(sass@1.77.8)(svelte@4.2.18) the-new-css-reset: specifier: ^1.11.2 version: 1.11.2 @@ -1789,6 +1789,15 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 + '@sveltejs/kit@2.20.2': + resolution: {integrity: sha512-Dv8TOAZC9vyfcAB9TMsvUEJsRbklRTeNfcYBPaeH6KnABJ99i3CvCB2eNx8fiiliIqe+9GIchBg4RodRH5p1BQ==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 + '@sveltejs/kit@2.5.21': resolution: {integrity: sha512-zHkaVZB5WNKVtosPhpPHLLvCdhUs3j6rRhDjRcXz9Mi7ZOeMe+xpzFkm7vs7RYQKMWDPUIfDngFDM3iGPyntMw==} engines: {node: '>=18.13'} @@ -3022,6 +3031,9 @@ packages: devalue@5.0.0: resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==} + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -3279,6 +3291,9 @@ packages: esm-env@1.0.0: resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==} + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + espree@10.1.0: resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5252,6 +5267,10 @@ packages: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -5423,8 +5442,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-check@3.8.5: - resolution: {integrity: sha512-3OGGgr9+bJ/+1nbPgsvulkLC48xBsqsgtc8Wam281H4G9F5v3mYGa2bHRsPuwHC5brKl4AxJH95QF73kmfihGQ==} + svelte-check@3.8.6: + resolution: {integrity: sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q==} hasBin: true peerDependencies: svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 @@ -8001,11 +8020,33 @@ snapshots: dependencies: storybook: 8.2.9(@babel/preset-env@7.25.3(@babel/core@7.25.2)) + '@sveltejs/adapter-auto@3.2.3(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))': + dependencies: + '@sveltejs/kit': 2.20.2(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)) + import-meta-resolve: 4.1.0 + '@sveltejs/adapter-auto@3.2.3(@sveltejs/kit@2.5.21(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))': dependencies: '@sveltejs/kit': 2.5.21(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)) import-meta-resolve: 4.1.0 + '@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8))': + dependencies: + '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)) + '@types/cookie': 0.6.0 + cookie: 0.6.0 + devalue: 5.1.1 + esm-env: 1.2.2 + import-meta-resolve: 4.1.0 + kleur: 4.1.5 + magic-string: 0.30.11 + mrmime: 2.0.0 + sade: 1.8.1 + set-cookie-parser: 2.7.0 + sirv: 3.0.1 + svelte: 4.2.18 + vite: 5.4.0(@types/node@20.14.15)(sass@1.77.8) + '@sveltejs/kit@2.5.21(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)))(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8))': dependencies: '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@20.14.15)(sass@1.77.8)) @@ -9371,6 +9412,8 @@ snapshots: devalue@5.0.0: {} + devalue@5.1.1: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -9710,6 +9753,8 @@ snapshots: esm-env@1.0.0: {} + esm-env@1.2.2: {} + espree@10.1.0: dependencies: acorn: 8.12.1 @@ -12064,6 +12109,12 @@ snapshots: mrmime: 2.0.0 totalist: 3.0.1 + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -12281,11 +12332,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@3.8.5(@babel/core@7.25.2)(postcss-load-config@3.1.4(postcss@8.4.41))(postcss@8.4.41)(sass@1.77.8)(svelte@4.2.18): + svelte-check@3.8.6(@babel/core@7.25.2)(postcss-load-config@3.1.4(postcss@8.4.41))(postcss@8.4.41)(sass@1.77.8)(svelte@4.2.18): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 3.6.0 - picocolors: 1.0.1 + picocolors: 1.1.1 sade: 1.8.1 svelte: 4.2.18 svelte-preprocess: 5.1.4(@babel/core@7.25.2)(postcss-load-config@3.1.4(postcss@8.4.41))(postcss@8.4.41)(sass@1.77.8)(svelte@4.2.18)(typescript@5.5.4) diff --git a/v2/pink-sb/src/lib/helpers/helpers.ts b/v2/pink-sb/src/lib/helpers/helpers.ts index 2b142dea68..c52c497dc2 100644 --- a/v2/pink-sb/src/lib/helpers/helpers.ts +++ b/v2/pink-sb/src/lib/helpers/helpers.ts @@ -8,3 +8,19 @@ export function clickOnEnter( event.currentTarget.click(); } } + +export function clickOutside(node: HTMLElement, callback: () => void) { + const handleClick = (event: MouseEvent) => { + if (!node.contains(event.target as Node)) { + callback(); + } + }; + + document.addEventListener('click', handleClick, true); + + return { + destroy() { + document.removeEventListener('click', handleClick, true); + } + }; +} diff --git a/v2/pink-sb/src/lib/index.ts b/v2/pink-sb/src/lib/index.ts index c10ed07de6..e551cfadd6 100644 --- a/v2/pink-sb/src/lib/index.ts +++ b/v2/pink-sb/src/lib/index.ts @@ -52,3 +52,4 @@ export { default as Lights } from './lab/Lights.svelte'; export { default as InlineInput } from './lab/InlineInput.svelte'; export { default as Sonner } from './lab/Sonner.svelte'; export { default as Logs } from './Logs.svelte'; +export { default as Spreadsheet } from './spreadsheet/index.js'; diff --git a/v2/pink-sb/src/lib/input/Textarea.svelte b/v2/pink-sb/src/lib/input/Textarea.svelte index d60c4e3dd7..02df9ce2dd 100644 --- a/v2/pink-sb/src/lib/input/Textarea.svelte +++ b/v2/pink-sb/src/lib/input/Textarea.svelte @@ -44,6 +44,8 @@ on:invalid on:change bind:value + on:blur + on:keydown rows={rows || value?.split('\n').length} {disabled} {readonly} diff --git a/v2/pink-sb/src/lib/spreadsheet/Cell.svelte b/v2/pink-sb/src/lib/spreadsheet/Cell.svelte new file mode 100644 index 0000000000..1e56fe606a --- /dev/null +++ b/v2/pink-sb/src/lib/spreadsheet/Cell.svelte @@ -0,0 +1,179 @@ + + +{#if !options || options?.hide !== true} +
{ + if (root.currentlyEditing === cellEl) { + root.setEditing(null); + } + }} + on:dblclick={() => { + originalValue = value; + root.setEditing(cellEl); + }} + > + {#if value} + {#if isEditing} + + {:else} + {value} + {/if} + {:else} + + {/if} + + {#if !isSelect} + {#if icon} + + {/if} + +
+ {/if} +
+{/if} + + diff --git a/v2/pink-sb/src/lib/spreadsheet/Root.svelte b/v2/pink-sb/src/lib/spreadsheet/Root.svelte new file mode 100644 index 0000000000..653ff1e8f3 --- /dev/null +++ b/v2/pink-sb/src/lib/spreadsheet/Root.svelte @@ -0,0 +1,191 @@ + + +
+
+ {#if $$slots.header} + + + + {/if} + +
+
+ + diff --git a/v2/pink-sb/src/lib/spreadsheet/header/Cell.svelte b/v2/pink-sb/src/lib/spreadsheet/header/Cell.svelte new file mode 100644 index 0000000000..1499e1f540 --- /dev/null +++ b/v2/pink-sb/src/lib/spreadsheet/header/Cell.svelte @@ -0,0 +1,11 @@ + + + diff --git a/v2/pink-sb/src/lib/spreadsheet/header/index.ts b/v2/pink-sb/src/lib/spreadsheet/header/index.ts new file mode 100644 index 0000000000..c1d12b91d4 --- /dev/null +++ b/v2/pink-sb/src/lib/spreadsheet/header/index.ts @@ -0,0 +1,5 @@ +import Cell from './Cell.svelte'; + +export default { + Cell +}; diff --git a/v2/pink-sb/src/lib/spreadsheet/index.ts b/v2/pink-sb/src/lib/spreadsheet/index.ts new file mode 100644 index 0000000000..3611b830ef --- /dev/null +++ b/v2/pink-sb/src/lib/spreadsheet/index.ts @@ -0,0 +1,53 @@ +import Root from './Root.svelte'; +import Cell from './Cell.svelte'; +import Row from './row/index.js'; +import Header from './header/index.js'; +export type Column = { + id: string; + width?: + | { + min: number; + max: number; + } + | { + min: number; + } + | number; + hide?: boolean; + resizable?: boolean; +}; + +export type RootProp = { + allowSelection: boolean; + selectedRows: string[]; + selectedAll: boolean; + selectedNone: boolean; + selectedSome: boolean; + columns: Record; + toggle: (id: string) => void; + toggleAll: () => void; + addAvailableId: (id: string) => void; + removeAvailableId: (id: string) => void; + dragGhostBorder: HTMLElement; + updateCells: (columnId: string, newWidth: number) => void; + currentlyEditing?: HTMLElement | null; + setEditing: (el: HTMLElement | null) => void; +}; + +export type Alignment = + | 'middle-middle' + | 'middle-start' + | 'middle-end' + | 'start-middle' + | 'start-start' + | 'start-end' + | 'end-middle' + | 'end-start' + | 'end-end'; + +export default { + Root, + Cell, + Row, + Header +}; diff --git a/v2/pink-sb/src/lib/spreadsheet/row/Base.svelte b/v2/pink-sb/src/lib/spreadsheet/row/Base.svelte new file mode 100644 index 0000000000..637bd5e7c0 --- /dev/null +++ b/v2/pink-sb/src/lib/spreadsheet/row/Base.svelte @@ -0,0 +1,70 @@ + + +
+ {#if root.allowSelection} + {@const isHeader = type === 'header'} + + + + {/if} + + +
+ + diff --git a/v2/pink-sb/src/lib/spreadsheet/row/index.ts b/v2/pink-sb/src/lib/spreadsheet/row/index.ts new file mode 100644 index 0000000000..a8f6c2b738 --- /dev/null +++ b/v2/pink-sb/src/lib/spreadsheet/row/index.ts @@ -0,0 +1,11 @@ +import type { RootProp } from '../index.js'; +import Base from './Base.svelte'; + +export type RowBaseProps = { + id?: string; + root: RootProp; +}; + +export default { + Base +}; diff --git a/v2/pink-sb/src/stories/Spreadsheet.stories.svelte b/v2/pink-sb/src/stories/Spreadsheet.stories.svelte new file mode 100644 index 0000000000..1a95e873ac --- /dev/null +++ b/v2/pink-sb/src/stories/Spreadsheet.stories.svelte @@ -0,0 +1,148 @@ + + + + + + + + {#each dynamicColumns as col} + + {#if col.meta?.isPrimary} + + {col.meta.label} + Primary key + + {:else if col.meta?.isAction} + + + + {:else} + {col.meta?.label ?? col.id} + {/if} + + {/each} + + + {#each Array(3) as _, i} + + {#each dynamicColumns as col} + + {/each} + + {/each} + + + + + + + {#each dynamicColumns as col} + + {#if col.meta?.isPrimary} + + {col.meta.label} + Primary key + + {:else if col.meta?.isAction} + + + + {:else} + {col.meta?.label ?? col.id} + {/if} + + {/each} + + + {#each Array(3) as _, i} + + {#each dynamicColumns as col} + + {/each} + + {/each} + + From 57d4183ff4f12a26a20e83407d4b89d9dfb0b8d3 Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 3 Apr 2025 19:57:55 +0530 Subject: [PATCH 02/14] add: drag <> swap column behaviour. --- v2/pink-sb/src/lib/spreadsheet/Cell.svelte | 40 ++-- v2/pink-sb/src/lib/spreadsheet/Root.svelte | 172 ++++++++++++------ v2/pink-sb/src/lib/spreadsheet/helper.ts | 62 +++++++ v2/pink-sb/src/lib/spreadsheet/index.ts | 6 + .../src/stories/Spreadsheet.stories.svelte | 47 +++++ 5 files changed, 260 insertions(+), 67 deletions(-) create mode 100644 v2/pink-sb/src/lib/spreadsheet/helper.ts diff --git a/v2/pink-sb/src/lib/spreadsheet/Cell.svelte b/v2/pink-sb/src/lib/spreadsheet/Cell.svelte index 1e56fe606a..de54145fbe 100644 --- a/v2/pink-sb/src/lib/spreadsheet/Cell.svelte +++ b/v2/pink-sb/src/lib/spreadsheet/Cell.svelte @@ -18,6 +18,7 @@ let resizerEl: HTMLElement; let isEditing = false; + let wasDraggable = false; let originalValue = value; const dispatch = createEventDispatcher(); @@ -26,9 +27,11 @@ $: isHorizontalStart = alignment.endsWith('start'); $: isHorizontalEnd = alignment.endsWith('end'); $: options = typeof column !== 'undefined' ? root.columns?.[column] : undefined; - $: resizable = options?.resizable ?? true; + $: isEditing = root.currentlyEditing === cellEl; + $: isSelect = root.allowSelection && column?.includes('__select_'); + function handleKeydown(e: KeyboardEvent) { if (e.key === 'Escape') { value = originalValue; @@ -48,14 +51,16 @@ function handlePointerDown(e: PointerEvent) { if (!cellEl || typeof column !== 'string') return; - resizing = true; + wasDraggable = cellEl.draggable; + cellEl.draggable = false; + + resizing = true; startX = e.clientX; startWidth = cellEl.offsetWidth; const ghost = root.dragGhostBorder; const bounds = ghost.parentElement?.getBoundingClientRect(); - if (ghost && bounds) { ghost.style.left = `${startX}px`; ghost.style.top = `${bounds.top}px`; @@ -65,7 +70,6 @@ document.body.style.userSelect = 'none'; document.body.style.cursor = 'col-resize'; - resizerEl.setPointerCapture(e.pointerId); } @@ -77,7 +81,6 @@ function handlePointerUp(e: PointerEvent) { if (!resizing || typeof column !== 'string') return; resizing = false; - document.body.style.userSelect = ''; document.body.style.cursor = ''; root.dragGhostBorder.style.display = 'none'; @@ -85,10 +88,9 @@ const deltaX = e.clientX - startX; const newWidth = Math.max(40, startWidth + deltaX); root.updateCells(column, newWidth); - } - $: isSelect = root.allowSelection && column?.includes('__select_'); - $: isEditing = root.currentlyEditing === cellEl; + if (wasDraggable) cellEl.draggable = true; + } {#if !options || options?.hide !== true} @@ -96,20 +98,24 @@ role="cell" tabindex="-1" bind:this={cellEl} + data-column={column} + draggable={!!options?.draggable} class:space-between={!!icon} class:vertical-start={isVerticalStart} class:vertical-end={isVerticalEnd} class:horizontal-start={isHorizontalStart} class:horizontal-end={isHorizontalEnd} + class:dragging-column={root.draggingColumn === column} use:clickOutside={() => { - if (root.currentlyEditing === cellEl) { - root.setEditing(null); - } + if (root.currentlyEditing === cellEl) root.setEditing(null); }} on:dblclick={() => { originalValue = value; root.setEditing(cellEl); }} + on:dragstart={(e) => root.startDrag(column, e)} + on:dragover={(e) => root.overDrag(column, e)} + on:drop={root.endDrag} > {#if value} {#if isEditing} @@ -125,7 +131,6 @@ {#if icon} {/if} -
.column-resizer { position: absolute; top: 0; - right: -3px; - width: 6px; + right: 0; + width: 1px; height: 100%; cursor: col-resize; z-index: 1; @@ -175,5 +180,12 @@ touch-action: none; border-left: var(--border-width-s) solid var(--border-neutral); } + &[draggable='true'] { + cursor: grab; + } + &.dragging-column { + cursor: grabbing; + background-color: rgba(237, 237, 240, 5%); + } } diff --git a/v2/pink-sb/src/lib/spreadsheet/Root.svelte b/v2/pink-sb/src/lib/spreadsheet/Root.svelte index 653ff1e8f3..a684df3209 100644 --- a/v2/pink-sb/src/lib/spreadsheet/Root.svelte +++ b/v2/pink-sb/src/lib/spreadsheet/Root.svelte @@ -1,29 +1,30 @@ diff --git a/v2/pink-sb/src/lib/spreadsheet/helper.ts b/v2/pink-sb/src/lib/spreadsheet/helper.ts new file mode 100644 index 0000000000..c558ae9e54 --- /dev/null +++ b/v2/pink-sb/src/lib/spreadsheet/helper.ts @@ -0,0 +1,62 @@ +export function createDragPreview( + rootElement: HTMLElement, + columnId: string, + event: DragEvent +): HTMLElement | null { + const cellElements: HTMLElement[] = []; + + const walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_ELEMENT); + while (walker.nextNode()) { + const el = walker.currentNode as HTMLElement; + if (el.dataset.column === columnId) { + cellElements.push(el); + } + } + + if (!cellElements.length) return null; + + const referenceCell = cellElements[0]; + const colWidth = referenceCell.offsetWidth; + const referenceStyle = getComputedStyle(referenceCell); + + const preview = document.createElement('div'); + Object.assign(preview.style, { + position: 'absolute', + top: '-99999px', + left: '-99999px', + width: `${colWidth}px`, + boxSizing: 'border-box', + border: 'var(--border-width-s) solid var(--border-neutral)', + background: 'var(--bgcolor-neutral-primary)', + pointerEvents: 'none', + overflow: 'hidden', + zIndex: '9999' + }); + + for (const cell of cellElements) { + const clone = cell.cloneNode(true) as HTMLElement; + + const resizer = Array.from(clone.children).find((c) => + (c as HTMLElement).classList?.contains('column-resizer') + ); + if (resizer) resizer.remove(); + + Object.assign(clone.style, { + width: '100%', + boxSizing: 'border-box', + padding: referenceStyle.padding, + font: referenceStyle.font, + background: referenceStyle.background, + border: 'none', + borderBottom: referenceStyle.borderBottom, + minHeight: `${cell.offsetHeight}px` + }); + + preview.appendChild(clone); + } + + document.body.appendChild(preview); + event.dataTransfer?.setDragImage(preview, 0, 0); + + return preview; +} diff --git a/v2/pink-sb/src/lib/spreadsheet/index.ts b/v2/pink-sb/src/lib/spreadsheet/index.ts index 3611b830ef..8a6611a773 100644 --- a/v2/pink-sb/src/lib/spreadsheet/index.ts +++ b/v2/pink-sb/src/lib/spreadsheet/index.ts @@ -15,6 +15,7 @@ export type Column = { | number; hide?: boolean; resizable?: boolean; + draggable?: boolean; }; export type RootProp = { @@ -32,6 +33,11 @@ export type RootProp = { updateCells: (columnId: string, newWidth: number) => void; currentlyEditing?: HTMLElement | null; setEditing: (el: HTMLElement | null) => void; + draggingColumn?: string | null; + dragOverColumn?: string | null; + startDrag: (columnId?: string, event?: DragEvent) => void; + overDrag: (columnId?: string, event?: DragEvent) => void; + endDrag: () => void; }; export type Alignment = diff --git a/v2/pink-sb/src/stories/Spreadsheet.stories.svelte b/v2/pink-sb/src/stories/Spreadsheet.stories.svelte index 1a95e873ac..180d808742 100644 --- a/v2/pink-sb/src/stories/Spreadsheet.stories.svelte +++ b/v2/pink-sb/src/stories/Spreadsheet.stories.svelte @@ -41,6 +41,7 @@ }, { id: 'second', + draggable: true, meta: { label: 'Ipsum', icon: IconText @@ -48,6 +49,7 @@ }, { id: 'third', + draggable: true, meta: { label: 'Dolor', icon: IconCalendar @@ -71,6 +73,7 @@ const newId = `col-${colCount++}`; const newCol: StoryColumn = { id: newId, + draggable: true, meta: { label: `Column ${colCount - 1}` } @@ -83,6 +86,10 @@ ...dynamicColumns.slice(insertIndex) ]; } + + function swapColumns(e: CustomEvent) { + dynamicColumns = e.detail; + } @@ -146,3 +153,43 @@ {/each} + + + + + {#each dynamicColumns as col} + + {#if col.meta?.isPrimary} + + {col.meta.label} + Primary key + + {:else if col.meta?.isAction} + + + + {:else} + {col.meta?.label ?? col.id} + {/if} + + {/each} + + + {#each Array(3) as _, i} + + {#each dynamicColumns as col} + + {/each} + + {/each} + + From 211263f104f13fbccae67b72e0034a83665e6174 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 6 Jun 2025 13:38:12 +0530 Subject: [PATCH 03/14] rewrite: spreadsheet. --- v2/pink-sb/src/lib/spreadsheet/Cell.svelte | 164 +++++--- v2/pink-sb/src/lib/spreadsheet/Root.svelte | 346 ++++++++++------ .../src/lib/spreadsheet/drag/manager.ts | 129 ++++++ .../src/lib/spreadsheet/header/Cell.svelte | 3 +- v2/pink-sb/src/lib/spreadsheet/helper.ts | 62 --- v2/pink-sb/src/lib/spreadsheet/index.ts | 13 +- .../src/stories/Spreadsheet.stories.svelte | 195 --------- .../spreadsheet/Spreadsheet.stories.svelte | 372 ++++++++++++++++++ v2/pink-sb/src/stories/spreadsheet/helper.ts | 208 ++++++++++ 9 files changed, 1045 insertions(+), 447 deletions(-) create mode 100644 v2/pink-sb/src/lib/spreadsheet/drag/manager.ts delete mode 100644 v2/pink-sb/src/lib/spreadsheet/helper.ts delete mode 100644 v2/pink-sb/src/stories/Spreadsheet.stories.svelte create mode 100644 v2/pink-sb/src/stories/spreadsheet/Spreadsheet.stories.svelte create mode 100644 v2/pink-sb/src/stories/spreadsheet/helper.ts diff --git a/v2/pink-sb/src/lib/spreadsheet/Cell.svelte b/v2/pink-sb/src/lib/spreadsheet/Cell.svelte index de54145fbe..a225d0f00e 100644 --- a/v2/pink-sb/src/lib/spreadsheet/Cell.svelte +++ b/v2/pink-sb/src/lib/spreadsheet/Cell.svelte @@ -1,6 +1,6 @@ {#if !options || options?.hide !== true} @@ -98,28 +96,38 @@ role="cell" tabindex="-1" bind:this={cellEl} - data-column={column} - draggable={!!options?.draggable} + data-fixed={isFixed} + data-select={isSelect} + data-action={isAction} + data-header={isHeader} + data-column-id={column} + data-editing-mode={isEditing} + draggable={!!options?.draggable && isHeader} class:space-between={!!icon} - class:vertical-start={isVerticalStart} + class:resizing-column={resizing} class:vertical-end={isVerticalEnd} - class:horizontal-start={isHorizontalStart} + class:vertical-start={isVerticalStart} class:horizontal-end={isHorizontalEnd} + class:horizontal-start={isHorizontalStart} class:dragging-column={root.draggingColumn === column} + style:left={isSelect ? '0' : undefined} + style:right={isAction ? '0' : undefined} + on:contextmenu={handleContextMenu} use:clickOutside={() => { - if (root.currentlyEditing === cellEl) root.setEditing(null); + if (isEditing) root.setEditing(null); }} on:dblclick={() => { + if (!isEditable) return; originalValue = value; - root.setEditing(cellEl); + root.setEditing(id); }} on:dragstart={(e) => root.startDrag(column, e)} on:dragover={(e) => root.overDrag(column, e)} on:drop={root.endDrag} > - {#if value} + {#if value && !isAction} {#if isEditing} - +