Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions v2/pink-sb/src/lib/spreadsheet/Cell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,17 @@
}

function handleContextMenu(event: MouseEvent) {
if (!root.enableContextMenu) return;

event.preventDefault();
const contextEvent = new CustomEvent('contextmenu', {
detail: { event, id: isEditable ? id : undefined }
});

if (root.handleCellContextMenu) {
root.handleCellContextMenu(contextEvent);
}

dispatch('contextmenu', { event, id: isEditable ? id : undefined });
}

Expand Down
84 changes: 83 additions & 1 deletion v2/pink-sb/src/lib/spreadsheet/Root.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@

export let nextPageTriggerOffset: number = 5;

// Context menu props
export let enableContextMenu: boolean = false;

let loadingTriggered = false;
let lastCheckedPages = new Set<number>();

Expand All @@ -66,6 +69,13 @@
let currentFocusedRow: { rowId: string; rowIndex: number } | null = null;
let cellGridRegistry: (HTMLElement | undefined)[][] = [];

// Context menu state
let showContextMenu = false;
let contextMenuX = 0;
let contextMenuY = 0;
let contextMenuRowId: string | undefined = undefined;
let contextMenuElement: HTMLDivElement;

let dragManager: DragManager;
const dispatch = createEventDispatcher();

Expand Down Expand Up @@ -324,6 +334,43 @@
}
}

function handleCellContextMenu(event: CustomEvent<{ event: MouseEvent; id?: string }>) {
const { event: mouseEvent, id } = event.detail;

// Only allow context menu for data rows (must have a row id)
if (!enableContextMenu || !id) return;

contextMenuX = mouseEvent.clientX;
contextMenuY = mouseEvent.clientY;
contextMenuRowId = id;
showContextMenu = true;

// Ensure menu stays within viewport
tick().then(() => {
if (contextMenuElement) {
const rect = contextMenuElement.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;

if (rect.right > viewportWidth) {
contextMenuX = viewportWidth - rect.width - 8;
}
if (rect.bottom > viewportHeight) {
contextMenuY = viewportHeight - rect.height - 8;
}
}
});

dispatch('contextmenu', { event: mouseEvent, rowId: id });
}

function handleContextMenuClick(event: MouseEvent) {
if (!enableContextMenu || !showContextMenu || !contextMenuElement || !event.target) return;
if (!contextMenuElement.contains(event.target as Node)) {
showContextMenu = false;
}
}

function startDrag(columnId: string, event?: DragEvent) {
draggingColumn = columnId;
dragManager.startDrag(columnId, event);
Expand Down Expand Up @@ -630,7 +677,9 @@
currentlyHoveredColumnHeader: currentlyHoveredColumn,
expandKbdShortcut,
currentFocusedRow,
setFocusedRow
setFocusedRow,
enableContextMenu,
handleCellContextMenu
} as SpreadsheetRootProps;

const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
Expand Down Expand Up @@ -693,9 +742,29 @@
on:keydown={(e) => {
clearNavFocusOnEscape(e);
handleExpandKbdShortcut(e);
if (enableContextMenu && e.key === 'Escape' && showContextMenu) {
showContextMenu = false;
}
}}
on:click={handleContextMenuClick}
/>

{#if enableContextMenu}

{#if showContextMenu}
<div
bind:this={contextMenuElement}
class="context-menu"
style:left={`${contextMenuX}px`}
style:top={`${contextMenuY}px`}
role="menu"
tabindex="-1"
>
<slot name="contextmenu" rowId={contextMenuRowId} />
</div>
{/if}
{/if}

<div class="root" bind:this={rootEl} style:height style:--sheet-border-radius={borderRadiusValue}>
<div class="spreadsheet-container" bind:this={sheetContainer} on:scroll={handleScroll}>
<div
Expand Down Expand Up @@ -853,4 +922,17 @@
align-items: center;
}
}

.context-menu {
position: fixed;
z-index: 9999;
background: var(--bgcolor-neutral-primary);
border: var(--border-width-s) solid var(--border-neutral);
border-radius: var(--border-radius-m);
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.03),
0 4px 4px 0 rgba(0, 0, 0, 0.04),
0 8px 16px 0 rgba(0, 0, 0, 0.08);
min-width: 200px;
}
</style>
2 changes: 2 additions & 0 deletions v2/pink-sb/src/lib/spreadsheet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export type SpreadsheetRootProps = Readonly<{
expandKbdShortcut?: string | undefined;
currentFocusedRow: { rowId: string; rowIndex: number } | null;
setFocusedRow: (rowId: string | null, rowIndex: number | null) => void;
enableContextMenu: boolean;
handleCellContextMenu?: (event: CustomEvent<{ event: MouseEvent; id?: string }>) => void;
}>;

export type SpreadsheetAlignment = TableAlignment;
Expand Down
21 changes: 21 additions & 0 deletions v2/pink-sb/src/lib/table/cell/Base.svelte
Original file line number Diff line number Diff line change
@@ -1,24 +1,45 @@
<script lang="ts">
import type { TableAlignment, TableRootProps } from '../index.js';
import { createEventDispatcher } from 'svelte';

export let column: string | undefined = undefined;
export let root: TableRootProps;
export let alignment: TableAlignment = 'middle-middle';
export let id: string | undefined = undefined;

const dispatch = createEventDispatcher();

$: options = column !== undefined && root.columnsMap?.[column];
$: isVerticalStart = alignment.startsWith('start');
$: isVerticalEnd = alignment.startsWith('end');
$: isHorizontalStart = alignment.endsWith('start');
$: isHorizontalEnd = alignment.endsWith('end');

function handleContextMenu(event: MouseEvent) {
if (!root.enableContextMenu) return;

event.preventDefault();
const contextEvent = new CustomEvent('contextmenu', {
detail: { event, id }
});

if (root.handleCellContextMenu) {
root.handleCellContextMenu(contextEvent);
}

dispatch('contextmenu', { event, id });
}
</script>

{#if !options || options?.hide !== true}
<div
role="cell"
tabindex="-1"
class:vertical-start={isVerticalStart}
class:vertical-end={isVerticalEnd}
class:horizontal-start={isHorizontalStart}
class:horizontal-end={isHorizontalEnd}
on:contextmenu={handleContextMenu}
>
<slot />
</div>
Expand Down
2 changes: 2 additions & 0 deletions v2/pink-sb/src/lib/table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export type TableRootProps = Readonly<{
toggleAll: () => void;
addAvailableId: (id: string) => void;
removeAvailableId: (id: string) => void;
enableContextMenu: boolean;
handleCellContextMenu?: (event: CustomEvent<{ event: MouseEvent; id?: string }>) => void;
}>;

export type TableAlignment =
Expand Down
92 changes: 91 additions & 1 deletion v2/pink-sb/src/lib/table/root/Base.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
<script lang="ts">
import type { TableColumn, TableRootProps } from '../index.js';
import { createEventDispatcher, tick } from 'svelte';

export let columns: Array<TableColumn> | number;
export let allowSelection: boolean = false;
export let selectedRows: Array<string> = [];
export let element: HTMLElement | undefined = undefined;

// Context menu props
export let enableContextMenu: boolean = false;

let availableIds: Set<string> = new Set();

// Context menu state
let showContextMenu = false;
let contextMenuX = 0;
let contextMenuY = 0;
let contextMenuRowId: string | undefined = undefined;
let contextMenuElement: HTMLDivElement;

const dispatch = createEventDispatcher();

$: someRowsSelected =
availableIds.size > 0 &&
Expand Down Expand Up @@ -54,6 +67,43 @@
}, {});
}

function handleCellContextMenu(event: CustomEvent<{ event: MouseEvent; id?: string }>) {
const { event: mouseEvent, id } = event.detail;

// Only allow context menu for data rows (must have a row id)
if (!enableContextMenu || !id) return;

contextMenuX = mouseEvent.clientX;
contextMenuY = mouseEvent.clientY;
contextMenuRowId = id;
showContextMenu = true;

// Ensure menu stays within viewport
tick().then(() => {
if (contextMenuElement) {
const rect = contextMenuElement.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;

if (rect.right > viewportWidth) {
contextMenuX = viewportWidth - rect.width - 8;
}
if (rect.bottom > viewportHeight) {
contextMenuY = viewportHeight - rect.height - 8;
}
}
});

dispatch('contextmenu', { event: mouseEvent, rowId: id });
}

function handleContextMenuClick(event: MouseEvent) {
if (!enableContextMenu || !showContextMenu || !contextMenuElement || !event.target) return;
if (!contextMenuElement.contains(event.target as Node)) {
showContextMenu = false;
}
}

$: root = {
allowSelection,
selectedRows,
Expand All @@ -65,11 +115,38 @@
selectedNone: !someRowsSelected,
selectedAll: allRowsSelected,
addAvailableId,
removeAvailableId
removeAvailableId,
enableContextMenu,
handleCellContextMenu
} as TableRootProps;
const { class: className, ...rest } = $$restProps;
</script>

<svelte:window
on:click={handleContextMenuClick}
on:keydown={(e) => {
if (enableContextMenu && e.key === 'Escape' && showContextMenu) {
showContextMenu = false;
}
}}
/>

{#if enableContextMenu}

{#if showContextMenu}
<div
bind:this={contextMenuElement}
class="context-menu"
style:left={`${contextMenuX}px`}
style:top={`${contextMenuY}px`}
role="menu"
tabindex="-1"
>
<slot name="contextmenu" rowId={contextMenuRowId} />
</div>
{/if}
{/if}

<div class="root {className || ''}" bind:this={element} {...rest}>
<slot {root} />
</div>
Expand All @@ -86,4 +163,17 @@
display: none;
}
}

.context-menu {
position: fixed;
z-index: 9999;
background: var(--bgcolor-neutral-primary);
border: var(--border-width-s) solid var(--border-neutral);
border-radius: var(--border-radius-m);
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.03),
0 4px 4px 0 rgba(0, 0, 0, 0.04),
0 8px 16px 0 rgba(0, 0, 0, 0.08);
min-width: 200px;
}
</style>
3 changes: 3 additions & 0 deletions v2/pink-sb/src/lib/table/root/Default.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
{/if}
<slot {root} />
</div>
<svelte:fragment slot="contextmenu" let:rowId>
<slot name="contextmenu" {rowId} />
</svelte:fragment>
</Base>

<style lang="scss">
Expand Down
Loading
Loading