diff --git a/packages/frontend-core/src/components/grid/cells/BooleanCell.svelte b/packages/frontend-core/src/components/grid/cells/BooleanCell.svelte index 52aecb07a7..c3449c0b39 100644 --- a/packages/frontend-core/src/components/grid/cells/BooleanCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/BooleanCell.svelte @@ -37,6 +37,9 @@ .boolean-cell { padding: 2px var(--cell-padding); pointer-events: none; + flex: 1 1 auto; + display: flex; + justify-content: center; } .boolean-cell.editable { pointer-events: all; diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte index f39b820632..cb8616a735 100644 --- a/packages/frontend-core/src/components/grid/cells/DataCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte @@ -11,6 +11,7 @@ export let selected export let rowFocused export let rowIdx + export let topRow = false export let focused export let selectedUser export let column @@ -68,6 +69,7 @@ {highlighted} {selected} {rowIdx} + {topRow} {focused} {selectedUser} {readonly} diff --git a/packages/frontend-core/src/components/grid/cells/GridCell.svelte b/packages/frontend-core/src/components/grid/cells/GridCell.svelte index 6589c18d07..7e38a989d6 100644 --- a/packages/frontend-core/src/components/grid/cells/GridCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/GridCell.svelte @@ -6,6 +6,7 @@ export let selectedUser = null export let error = null export let rowIdx + export let topRow = false export let defaultHeight = false export let center = false export let readonly = false @@ -31,13 +32,14 @@ class:readonly class:default-height={defaultHeight} class:selected-other={selectedUser != null} + class:alt={rowIdx % 2 === 1} + class:top={topRow} on:focus on:mousedown on:mouseup on:click on:contextmenu {style} - data-row={rowIdx} > {#if error}
@@ -70,6 +72,9 @@ width: 0; --cell-color: transparent; } + .cell.alt { + --cell-background: var(--cell-background-alt); + } .cell.default-height { height: var(--default-row-height); } @@ -98,8 +103,8 @@ .cell.selected-other:not(.focused):after { border-radius: 0 2px 2px 2px; } - .cell[data-row="0"].error:after, - .cell[data-row="0"].selected-other:not(.focused):after { + .cell.top.error:after, + .cell.top.selected-other:not(.focused):after { border-radius: 2px 2px 2px 0; } @@ -152,7 +157,7 @@ overflow: hidden; user-select: none; } - .cell[data-row="0"] .label { + .cell.top .label { bottom: auto; top: 100%; border-radius: 0 2px 2px 2px; diff --git a/packages/frontend-core/src/components/grid/cells/GutterCell.svelte b/packages/frontend-core/src/components/grid/cells/GutterCell.svelte index 00b99c0711..56c4c20d54 100644 --- a/packages/frontend-core/src/components/grid/cells/GutterCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/GutterCell.svelte @@ -21,16 +21,7 @@ svelteDispatch("select") const id = row?._id if (id) { - selectedRows.update(state => { - let newState = { - ...state, - [id]: !state[id], - } - if (!newState[id]) { - delete newState[id] - } - return newState - }) + selectedRows.actions.toggleRow(id) } } @@ -47,6 +38,7 @@ highlighted={rowFocused || rowHovered} selected={rowSelected} {defaultHeight} + rowIdx={row?.__idx} >
{#if $$slots.default} diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index 72b0ad0ff1..21ee210233 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -196,7 +196,11 @@ Move right - Hide column + Hide column diff --git a/packages/frontend-core/src/components/grid/layout/Grid.svelte b/packages/frontend-core/src/components/grid/layout/Grid.svelte index cb02263be3..e63e6d0048 100644 --- a/packages/frontend-core/src/components/grid/layout/Grid.svelte +++ b/packages/frontend-core/src/components/grid/layout/Grid.svelte @@ -41,6 +41,7 @@ export let allowExpandRows = true export let allowEditRows = true export let allowDeleteRows = true + export let stripeRows = false // Unique identifier for DOM nodes inside this instance const rand = Math.random() @@ -55,6 +56,7 @@ allowExpandRows, allowEditRows, allowDeleteRows, + stripeRows, }) // Build up context @@ -90,6 +92,7 @@ allowExpandRows, allowEditRows, allowDeleteRows, + stripeRows, }) // Set context for children to consume @@ -107,6 +110,7 @@ id="grid-{rand}" class:is-resizing={$isResizing} class:is-reordering={$isReordering} + class:stripe={$config.stripeRows} style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};" >
@@ -169,6 +173,7 @@ /* Variables */ --cell-background: var(--spectrum-global-color-gray-50); --cell-background-hover: var(--spectrum-global-color-gray-100); + --cell-background-alt: var(--cell-background); --cell-padding: 8px; --cell-spacing: 4px; --cell-border: 1px solid var(--spectrum-global-color-gray-200); @@ -185,6 +190,9 @@ .grid.is-reordering :global(*) { cursor: grabbing !important; } + .grid.stripe { + --cell-background-alt: var(--spectrum-global-color-gray-75); + } .grid-data-outer, .grid-data-inner { diff --git a/packages/frontend-core/src/components/grid/layout/GridBody.svelte b/packages/frontend-core/src/components/grid/layout/GridBody.svelte index 67f5f03898..016369df49 100644 --- a/packages/frontend-core/src/components/grid/layout/GridBody.svelte +++ b/packages/frontend-core/src/components/grid/layout/GridBody.svelte @@ -36,7 +36,11 @@
{#each $renderedRows as row, idx} - = $rowVerticalInversionIndex} /> + = $rowVerticalInversionIndex} + /> {/each} {#if $config.allowAddRows && $renderedColumns.length}
= $columnHorizontalInversionIndex} highlighted={rowHovered || rowFocused || reorderSource === column.name} selected={rowSelected} - rowIdx={idx} + rowIdx={row.__idx} + topRow={top} focused={$focusedCellId === cellId} selectedUser={$selectedCellMap[cellId]} width={column.width} diff --git a/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte b/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte index be9fad00d0..9d6cc2275b 100644 --- a/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte @@ -61,7 +61,7 @@ border-right: var(--cell-border); border-bottom: var(--cell-border); background: var(--spectrum-global-color-gray-100); - z-index: 20; + z-index: 1; } .add:hover { background: var(--spectrum-global-color-gray-200); diff --git a/packages/frontend-core/src/components/grid/layout/KeyboardShortcut.svelte b/packages/frontend-core/src/components/grid/layout/KeyboardShortcut.svelte index 5024a24ea7..cac39bbf2f 100644 --- a/packages/frontend-core/src/components/grid/layout/KeyboardShortcut.svelte +++ b/packages/frontend-core/src/components/grid/layout/KeyboardShortcut.svelte @@ -38,7 +38,7 @@ padding: 2px 6px; font-size: 12px; font-weight: 600; - background-color: var(--spectrum-global-color-gray-200); + background-color: var(--spectrum-global-color-gray-300); color: var(--spectrum-global-color-gray-700); border-radius: 4px; text-align: center; diff --git a/packages/frontend-core/src/components/grid/layout/NewRow.svelte b/packages/frontend-core/src/components/grid/layout/NewRow.svelte index 8048a4e2fa..85b430f79b 100644 --- a/packages/frontend-core/src/components/grid/layout/NewRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/NewRow.svelte @@ -167,7 +167,7 @@ focused={$focusedCellId === cellId} width={$stickyColumn.width} {updateValue} - rowIdx={0} + topRow={offset === 0} {invertY} > {#if $stickyColumn?.schema?.autocolumn} @@ -193,7 +193,7 @@ row={newRow} focused={$focusedCellId === cellId} width={column.width} - rowIdx={0} + topRow={offset === 0} invertX={columnIdx >= $columnHorizontalInversionIndex} {invertY} > @@ -219,7 +219,7 @@
diff --git a/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte b/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte index 6301112110..801772ed51 100644 --- a/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte +++ b/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte @@ -82,7 +82,8 @@ {rowFocused} selected={rowSelected} highlighted={rowHovered || rowFocused} - rowIdx={idx} + rowIdx={row.__idx} + topRow={idx === 0} focused={$focusedCellId === cellId} selectedUser={$selectedCellMap[cellId]} width={$stickyColumn.width} diff --git a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte index c7fa0a5cb7..6d16acc7c5 100644 --- a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte +++ b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte @@ -224,10 +224,7 @@ if (!id || id === NewRowID) { return } - selectedRows.update(state => { - state[id] = !state[id] - return state - }) + selectedRows.actions.toggleRow(id) } onMount(() => { diff --git a/packages/frontend-core/src/components/grid/stores/reorder.js b/packages/frontend-core/src/components/grid/stores/reorder.js index de343987db..a99c1b1ab2 100644 --- a/packages/frontend-core/src/components/grid/stores/reorder.js +++ b/packages/frontend-core/src/components/grid/stores/reorder.js @@ -4,9 +4,10 @@ const reorderInitialState = { sourceColumn: null, targetColumn: null, breakpoints: [], - initialMouseX: null, - scrollLeft: 0, gridLeft: 0, + width: 0, + latestX: 0, + increment: 0, } export const createStores = () => { @@ -23,14 +24,24 @@ export const createStores = () => { } export const deriveStores = context => { - const { reorder, columns, visibleColumns, scroll, bounds, stickyColumn, ui } = - context + const { + reorder, + columns, + visibleColumns, + scroll, + bounds, + stickyColumn, + ui, + maxScrollLeft, + } = context + + let autoScrollInterval + let isAutoScrolling // Callback when dragging on a colum header and starting reordering const startReordering = (column, e) => { const $visibleColumns = get(visibleColumns) const $bounds = get(bounds) - const $scroll = get(scroll) const $stickyColumn = get(stickyColumn) ui.actions.blur() @@ -51,9 +62,8 @@ export const deriveStores = context => { sourceColumn: column, targetColumn: null, breakpoints, - initialMouseX: e.clientX, - scrollLeft: $scroll.left, gridLeft: $bounds.left, + width: $bounds.width, }) // Add listeners to handle mouse movement @@ -66,12 +76,44 @@ export const deriveStores = context => { // Callback when moving the mouse when reordering columns const onReorderMouseMove = e => { + // Immediately handle the current position + const x = e.clientX + reorder.update(state => ({ + ...state, + latestX: x, + })) + considerReorderPosition() + + // Check if we need to start auto-scrolling const $reorder = get(reorder) + const proximityCutoff = 140 + const speedFactor = 8 + const rightProximity = Math.max(0, $reorder.gridLeft + $reorder.width - x) + const leftProximity = Math.max(0, x - $reorder.gridLeft) + if (rightProximity < proximityCutoff) { + const weight = proximityCutoff - rightProximity + const increment = (weight / proximityCutoff) * speedFactor + reorder.update(state => ({ ...state, increment })) + startAutoScroll() + } else if (leftProximity < proximityCutoff) { + const weight = -1 * (proximityCutoff - leftProximity) + const increment = (weight / proximityCutoff) * speedFactor + reorder.update(state => ({ ...state, increment })) + startAutoScroll() + } else { + stopAutoScroll() + } + } + + // Actual logic to consider the current position and determine the new order + const considerReorderPosition = () => { + const $reorder = get(reorder) + const $scroll = get(scroll) // Compute the closest breakpoint to the current position let targetColumn let minDistance = Number.MAX_SAFE_INTEGER - const mouseX = e.clientX - $reorder.gridLeft + $reorder.scrollLeft + const mouseX = $reorder.latestX - $reorder.gridLeft + $scroll.left $reorder.breakpoints.forEach(point => { const distance = Math.abs(point.x - mouseX) if (distance < minDistance) { @@ -79,7 +121,6 @@ export const deriveStores = context => { targetColumn = point.column } }) - if (targetColumn !== $reorder.targetColumn) { reorder.update(state => ({ ...state, @@ -88,8 +129,35 @@ export const deriveStores = context => { } } + // Commences auto-scrolling in a certain direction, triggered when the mouse + // approaches the edges of the grid + const startAutoScroll = () => { + if (isAutoScrolling) { + return + } + isAutoScrolling = true + autoScrollInterval = setInterval(() => { + const $maxLeft = get(maxScrollLeft) + const { increment } = get(reorder) + scroll.update(state => ({ + ...state, + left: Math.max(0, Math.min($maxLeft, state.left + increment)), + })) + considerReorderPosition() + }, 10) + } + + // Stops auto scrolling + const stopAutoScroll = () => { + isAutoScrolling = false + clearInterval(autoScrollInterval) + } + // Callback when stopping reordering columns const stopReordering = async () => { + // Ensure auto-scrolling is stopped + stopAutoScroll() + // Swap position of columns let { sourceColumn, targetColumn } = get(reorder) moveColumn(sourceColumn, targetColumn) diff --git a/packages/frontend-core/src/components/grid/stores/ui.js b/packages/frontend-core/src/components/grid/stores/ui.js index 85afad8a90..b62e883437 100644 --- a/packages/frontend-core/src/components/grid/stores/ui.js +++ b/packages/frontend-core/src/components/grid/stores/ui.js @@ -25,14 +25,33 @@ export const createStores = () => { null ) + // Toggles whether a certain row ID is selected or not + const toggleSelectedRow = id => { + selectedRows.update(state => { + let newState = { + ...state, + [id]: !state[id], + } + if (!newState[id]) { + delete newState[id] + } + return newState + }) + } + return { focusedCellId, focusedCellAPI, focusedRowId, previousFocusedRowId, - selectedRows, hoveredRowId, rowHeight, + selectedRows: { + ...selectedRows, + actions: { + toggleRow: toggleSelectedRow, + }, + }, } }