Fix multiple issues with z-index, reordering and resizing

This commit is contained in:
Andrew Kingston 2023-02-27 13:59:35 +00:00
parent efca3eef4f
commit b1f2fe326a
7 changed files with 98 additions and 176 deletions

View File

@ -1,5 +1,4 @@
<script> <script>
import { get } from "svelte/store"
import { getContext } from "svelte" import { getContext } from "svelte"
const { columns, rand, scroll, visibleColumns } = getContext("spreadsheet") const { columns, rand, scroll, visibleColumns } = getContext("spreadsheet")
@ -12,8 +11,9 @@
let left = 0 let left = 0
let columnCount = 0 let columnCount = 0
$: cutoff = $scroll.left + 40 + $columns[0]?.width || 0
const startResizing = (idx, e) => { const startResizing = (idx, e) => {
const $columns = get(columns)
// Prevent propagation to stop reordering triggering // Prevent propagation to stop reordering triggering
e.stopPropagation() e.stopPropagation()
@ -60,23 +60,25 @@
</script> </script>
{#each $visibleColumns as col} {#each $visibleColumns as col}
<div {#if col.idx === 0 || col.left + col.width > cutoff}
class="resize-slider" <div
class:visible={columnIdx === col.idx} class="resize-slider"
on:mousedown={e => startResizing(col.idx, e)} class:visible={columnIdx === col.idx}
style="--left:{col.left + on:mousedown={e => startResizing(col.idx, e)}
col.width - style="--left:{col.left +
(col.idx === 0 ? 0 : $scroll.left)}px;" col.width -
> (col.idx === 0 ? 0 : $scroll.left)}px;"
<div class="resize-indicator" /> >
</div> <div class="resize-indicator" />
</div>
{/if}
{/each} {/each}
<style> <style>
.resize-slider { .resize-slider {
position: absolute; position: absolute;
top: var(--controls-height); top: var(--controls-height);
z-index: 6; z-index: 10;
height: var(--cell-height); height: var(--cell-height);
left: var(--left); left: var(--left);
opacity: 0; opacity: 0;

View File

@ -21,13 +21,12 @@
// Sheet constants // Sheet constants
const cellHeight = 36 const cellHeight = 36
const limit = 100 const limit = 100
const defaultWidth = 160 const defaultWidth = 200
const rand = Math.random() const rand = Math.random()
// State stores // State stores
const rows = writable([]) const rows = writable([])
const columns = writable([]) const columns = writable([])
const hoveredRowId = writable(null)
const selectedCellId = writable(null) const selectedCellId = writable(null)
const selectedRows = writable({}) const selectedRows = writable({})
const changeCache = writable({}) const changeCache = writable({})
@ -44,12 +43,11 @@
}) })
// Build up spreadsheet context and additional stores // Build up spreadsheet context and additional stores
const context = { let context = {
API, API,
rand, rand,
rows, rows,
columns, columns,
hoveredRowId,
selectedCellId, selectedCellId,
selectedRows, selectedRows,
tableId, tableId,
@ -59,8 +57,9 @@
bounds, bounds,
scroll, scroll,
} }
const { reorder, reorderPlaceholder } = createReorderStores(context)
const { visibleRows, visibleColumns } = createViewportStores(context) const { visibleRows, visibleColumns } = createViewportStores(context)
context = { ...context, visibleRows, visibleColumns }
const { reorder } = createReorderStores(context)
$: query = LuceneUtils.buildLuceneQuery(filter) $: query = LuceneUtils.buildLuceneQuery(filter)
$: fetch = createFetch(tableId) $: fetch = createFetch(tableId)
@ -202,7 +201,6 @@
setContext("spreadsheet", { setContext("spreadsheet", {
...context, ...context,
reorder, reorder,
reorderPlaceholder,
visibleRows, visibleRows,
visibleColumns, visibleColumns,
spreadsheetAPI, spreadsheetAPI,
@ -226,7 +224,9 @@
sticky={column.idx === 0} sticky={column.idx === 0}
reorderSource={$reorder.columnIdx === column.idx} reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === column.idx} reorderTarget={$reorder.swapColumnIdx === column.idx}
on:mousedown={e => reorder.actions.startReordering(column.idx, e)} on:mousedown={column.idx === 123
? null
: e => reorder.actions.startReordering(column.idx, e)}
width={column.width} width={column.width}
left={column.left} left={column.left}
> >
@ -249,24 +249,15 @@
<!-- New row placeholder --> <!-- New row placeholder -->
<div class="row new" style="--top:{($rows.length + 1) * cellHeight}px;"> <div class="row new" style="--top:{($rows.length + 1) * cellHeight}px;">
<SpreadsheetCell <SpreadsheetCell label on:click={addRow} width="40" left="0">
label
on:click={addRow}
on:mouseenter={() => ($hoveredRowId = "new")}
rowHovered={$hoveredRowId === "new"}
width="40"
left="0"
>
<Icon hoverable name="Add" size="S" /> <Icon hoverable name="Add" size="S" />
</SpreadsheetCell> </SpreadsheetCell>
{#each $visibleColumns as column} {#each $visibleColumns as column}
<SpreadsheetCell <SpreadsheetCell
sticky={column.idx === 0} sticky={column.idx === 0}
rowHovered={$hoveredRowId === "new"}
reorderSource={$reorder.columnIdx === column.idx} reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === column.idx} reorderTarget={$reorder.swapColumnIdx === column.idx}
on:click={() => addRow(column)} on:click={() => addRow(column)}
on:mouseenter={() => ($hoveredRowId = "new")}
width={column.width} width={column.width}
left={column.left} left={column.left}
/> />
@ -304,7 +295,7 @@
position: sticky; position: sticky;
top: 0; top: 0;
width: inherit; width: inherit;
z-index: 4; z-index: 10;
} }
.row.new { .row.new {
position: absolute; position: absolute;
@ -313,4 +304,7 @@
.row :global(> :last-child) { .row :global(> :last-child) {
border-right-width: 1px; border-right-width: 1px;
} }
input[type="checkbox"] {
margin: 0;
}
</style> </style>

View File

@ -1,8 +1,6 @@
<script> <script>
export let header = false export let header = false
export let label = false export let label = false
export let spacer = false
export let rowHovered = false
export let rowSelected = false export let rowSelected = false
export let sticky = false export let sticky = false
export let selected = false export let selected = false
@ -17,9 +15,7 @@
class="cell col-{column}" class="cell col-{column}"
class:header class:header
class:label class:label
class:spacer
class:row-selected={rowSelected} class:row-selected={rowSelected}
class:row-hovered={rowHovered}
class:sticky class:sticky
class:selected class:selected
class:reorder-source={reorderSource} class:reorder-source={reorderSource}
@ -53,10 +49,7 @@
position: absolute; position: absolute;
transition: border-color 130ms ease-out; transition: border-color 130ms ease-out;
width: var(--width); width: var(--width);
transform: translateX(var(--left)); left: var(--left);
}
.cell.row-hovered {
background: var(--cell-background-hover);
} }
.cell.selected { .cell.selected {
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400); box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400);
@ -68,26 +61,18 @@
.cell:hover { .cell:hover {
cursor: default; cursor: default;
} }
.cell.row-selected:after { .cell.row-selected {
pointer-events: none; background-color: var(--spectrum-global-color-gray-100);
content: " ";
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
opacity: 0.2;
background-color: var(--spectrum-global-color-blue-600);
} }
/* Header cells */ /* Header cells */
.cell.header { .cell.header {
background: var(--background); background: var(--background);
padding: 0 var(--cell-padding); padding: 0 var(--cell-padding);
z-index: 3;
border-color: var(--spectrum-global-color-gray-200); border-color: var(--spectrum-global-color-gray-200);
font-weight: 600; font-weight: 600;
gap: calc(2 * var(--cell-spacing)); gap: calc(2 * var(--cell-spacing));
z-index: 10;
} }
.cell.header :global(span) { .cell.header :global(span) {
flex: 1 1 auto; flex: 1 1 auto;
@ -96,20 +81,24 @@
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
.cell.header:hover {
cursor: pointer;
}
.cell.header.sticky,
.cell.header.label {
z-index: 11;
}
/* Sticky styles */ /* Sticky styles */
.cell.sticky { .cell.sticky {
position: sticky; position: sticky;
z-index: 2;
border-left-width: 0; border-left-width: 0;
transform: none; transform: none;
left: 40px; left: 40px;
z-index: 5;
} }
.cell.sticky.selected { .cell.selected.sticky {
z-index: 3; z-index: 6;
}
.cell.header.sticky {
z-index: 4;
} }
/* Reorder styles */ /* Reorder styles */
@ -119,43 +108,22 @@
.cell.header.reorder-source { .cell.header.reorder-source {
background: var(--spectrum-global-color-gray-200); background: var(--spectrum-global-color-gray-200);
} }
.cell.reorder-target { .cell.reorder-target:after {
z-index: 100;
}
.cell.reorder-target:before {
content: " "; content: " ";
position: absolute; position: absolute;
left: -2px; right: 0;
background: var(--spectrum-global-color-blue-400); background: var(--spectrum-global-color-blue-400);
width: 2px; width: 2px;
z-index: 100;
height: calc(var(--cell-height) + 1px); height: calc(var(--cell-height) + 1px);
} }
/* Label cells */ /* Label cells */
.cell.label { .cell.label {
padding: var(--cell-padding);
width: 40px; width: 40px;
padding: 0 12px;
border-left-width: 0; border-left-width: 0;
position: sticky; position: sticky;
left: 0; left: 0;
z-index: 2; z-index: 5;
}
.cell.label.header {
z-index: 4;
}
.cell.label :global(span) {
min-width: 14px;
text-align: center;
color: var(--spectrum-global-color-gray-500);
}
.cell.label :global(input[type="checkbox"]) {
margin: 0;
}
/* Spacer cells */
.cell.spacer {
background: none;
border-bottom: none;
} }
</style> </style>

View File

@ -13,7 +13,6 @@
const { const {
selectedCellId, selectedCellId,
reorder, reorder,
hoveredRowId,
selectedRows, selectedRows,
changeCache, changeCache,
spreadsheetAPI, spreadsheetAPI,
@ -22,9 +21,7 @@
} = getContext("spreadsheet") } = getContext("spreadsheet")
$: rowSelected = !!$selectedRows[row._id] $: rowSelected = !!$selectedRows[row._id]
$: rowHovered = $hoveredRowId === row._id
$: data = { ...row, ...$changeCache[row._id] } $: data = { ...row, ...$changeCache[row._id] }
$: containsSelectedCell = $selectedCellId?.split("-")[0] === row._id
const getCellForField = field => { const getCellForField = field => {
const type = field.schema.type const type = field.schema.type
@ -50,36 +47,23 @@
} }
</script> </script>
<div <div class="row" style="--top:{(row.__idx + 1) * cellHeight}px;">
class="row" <SpreadsheetCell label {rowSelected} on:click={() => selectRow(row._id)}>
style="--top:{(row.__idx + 1) * cellHeight}px;" <div class="checkbox" class:visible={rowSelected}>
class:contains-selected-cell={containsSelectedCell}
>
<SpreadsheetCell
label
{rowSelected}
{rowHovered}
on:mouseenter={() => ($hoveredRowId = row._id)}
on:click={() => selectRow(row._id)}
>
{#if rowSelected || rowHovered}
<input type="checkbox" checked={rowSelected} /> <input type="checkbox" checked={rowSelected} />
{:else} </div>
<span> <div class="number" class:visible={!rowSelected}>
{row.__idx + 1} {row.__idx + 1}
</span> </div>
{/if}
</SpreadsheetCell> </SpreadsheetCell>
{#each $visibleColumns as column (column.name)} {#each $visibleColumns as column (column.name)}
{@const cellIdx = `${row._id}-${column.name}`} {@const cellIdx = `${row._id}-${column.name}`}
<SpreadsheetCell <SpreadsheetCell
{rowSelected} {rowSelected}
{rowHovered}
sticky={column.idx === 0} sticky={column.idx === 0}
selected={$selectedCellId === cellIdx} selected={$selectedCellId === cellIdx}
reorderSource={$reorder.columnIdx === column.idx} reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === column.idx} reorderTarget={$reorder.swapColumnIdx === column.idx}
on:mouseenter={() => ($hoveredRowId = row._id)}
on:click={() => ($selectedCellId = cellIdx)} on:click={() => ($selectedCellId = cellIdx)}
width={column.width} width={column.width}
left={column.left} left={column.left}
@ -101,14 +85,36 @@
.row { .row {
display: flex; display: flex;
position: absolute; position: absolute;
top: 0; top: var(--top);
transform: translateY(var(--top));
width: inherit; width: inherit;
} }
.row.contains-selected-cell { .row:hover :global(.cell) {
z-index: 1; background: var(--cell-background-hover);
} }
/* Styles for label cell */
.checkbox {
display: none;
}
input[type="checkbox"] {
margin: 0;
}
.number {
display: none;
min-width: 14px;
text-align: center;
color: var(--spectrum-global-color-gray-500);
}
.row:hover .checkbox,
.checkbox.visible,
.number.visible {
display: block;
}
.row:hover .number {
display: none;
}
/* Add right border to last cell */
.row :global(> :last-child) { .row :global(> :last-child) {
border-right-width: 1px; border-right-width: 1px;
} }

View File

@ -151,13 +151,6 @@
var(--cell-background) 40% var(--cell-background) 40%
); );
} }
:global(.cell.hovered) .arrow {
background: linear-gradient(
to right,
transparent 0%,
var(--cell-background-hover) 40%
);
}
.options { .options {
min-width: 100%; min-width: 100%;
position: absolute; position: absolute;
@ -170,7 +163,6 @@
align-items: stretch; align-items: stretch;
max-height: calc(6 * var(--cell-height) - 1px); max-height: calc(6 * var(--cell-height) - 1px);
overflow-y: auto; overflow-y: auto;
z-index: 1;
} }
.option { .option {
flex: 0 0 var(--cell-height); flex: 0 0 var(--cell-height);

View File

@ -1,7 +1,7 @@
import { get, writable } from "svelte/store" import { get, writable } from "svelte/store"
export const createReorderStores = context => { export const createReorderStores = context => {
const { columns, bounds, rows, scroll, rand } = context const { columns, visibleColumns, rand, scroll, bounds } = context
const reorderInitialState = { const reorderInitialState = {
columnIdx: null, columnIdx: null,
swapColumnIdx: null, swapColumnIdx: null,
@ -10,33 +10,14 @@ export const createReorderStores = context => {
} }
const reorder = writable(reorderInitialState) const reorder = writable(reorderInitialState)
// This is broken into its own store as it is rapidly updated, and we want to
// ensure good performance by avoiding updating other components which depend
// on other reordering state
const placeholderInitialState = {
x: null,
initialX: null,
width: null,
height: null,
}
const placeholder = writable(placeholderInitialState)
// Callback when dragging on a colum header and starting reordering // Callback when dragging on a colum header and starting reordering
const startReordering = (columnIdx, e) => { const startReordering = (columnIdx, e) => {
const $columns = get(columns) const $columns = get(columns)
const $bounds = get(bounds) const $bounds = get(bounds)
const $rows = get(rows)
const $scroll = get(scroll) const $scroll = get(scroll)
// Generate new breakpoints for the current columns // Generate new breakpoints for the current columns
let breakpoints = [] let breakpoints = $columns.map(col => col.left + col.width)
$columns.forEach((col, idx) => {
breakpoints.push(col.left)
if (idx === $columns.length - 1) {
breakpoints.push(col.left + col.width)
}
})
const self = $columns[columnIdx]
// Update state // Update state
reorder.set({ reorder.set({
@ -44,15 +25,8 @@ export const createReorderStores = context => {
breakpoints, breakpoints,
swapColumnIdx: null, swapColumnIdx: null,
initialMouseX: e.clientX, initialMouseX: e.clientX,
})
placeholder.set({
initialX: self.left,
x: self.left,
width: self.width,
height: ($rows.length + 2) * 32,
sheetLeft: $bounds.left,
maxX: $bounds.width - self.width,
scrollLeft: $scroll.left, scrollLeft: $scroll.left,
sheetLeft: $bounds.left,
}) })
// Add listeners to handle mouse movement // Add listeners to handle mouse movement
@ -71,34 +45,18 @@ export const createReorderStores = context => {
return return
} }
// Compute new placeholder position
const $placeholder = get(placeholder)
let newX =
e.clientX -
$reorder.initialMouseX +
$placeholder.initialX -
$placeholder.scrollLeft
newX = Math.max(0, newX)
newX = Math.min($placeholder.maxX, newX)
// Compute the closest breakpoint to the current position // Compute the closest breakpoint to the current position
let swapColumnIdx let swapColumnIdx
let minDistance = Number.MAX_SAFE_INTEGER let minDistance = Number.MAX_SAFE_INTEGER
const mouseX = e.clientX - $reorder.sheetLeft + $reorder.scrollLeft
$reorder.breakpoints.forEach((point, idx) => { $reorder.breakpoints.forEach((point, idx) => {
const distance = Math.abs( const distance = Math.abs(point - mouseX)
point - e.clientX + $placeholder.sheetLeft - $placeholder.scrollLeft
)
if (distance < minDistance) { if (distance < minDistance) {
minDistance = distance minDistance = distance
swapColumnIdx = idx swapColumnIdx = idx
} }
}) })
// Update state
placeholder.update(state => {
state.x = newX
return state
})
if (swapColumnIdx !== $reorder.swapColumnIdx) { if (swapColumnIdx !== $reorder.swapColumnIdx) {
reorder.update(state => { reorder.update(state => {
state.swapColumnIdx = swapColumnIdx state.swapColumnIdx = swapColumnIdx
@ -111,6 +69,7 @@ export const createReorderStores = context => {
const stopReordering = () => { const stopReordering = () => {
// Swap position of columns // Swap position of columns
let { columnIdx, swapColumnIdx } = get(reorder) let { columnIdx, swapColumnIdx } = get(reorder)
swapColumnIdx++
columns.update(state => { columns.update(state => {
const removed = state.splice(columnIdx, 1) const removed = state.splice(columnIdx, 1)
if (--swapColumnIdx < columnIdx) { if (--swapColumnIdx < columnIdx) {
@ -131,7 +90,6 @@ export const createReorderStores = context => {
// Reset state // Reset state
reorder.set(reorderInitialState) reorder.set(reorderInitialState)
placeholder.set(placeholderInitialState)
// Remove event handlers // Remove event handlers
document.removeEventListener("mousemove", onReorderMouseMove) document.removeEventListener("mousemove", onReorderMouseMove)
@ -147,6 +105,5 @@ export const createReorderStores = context => {
stopReordering, stopReordering,
}, },
}, },
reorderPlaceholder: placeholder,
} }
} }

View File

@ -1,4 +1,5 @@
import { writable, derived } from "svelte/store" import { writable, derived } from "svelte/store"
import { Utils } from "../../../utils"
export const createViewportStores = context => { export const createViewportStores = context => {
const { cellHeight, columns, rows, scroll, bounds } = context const { cellHeight, columns, rows, scroll, bounds } = context
@ -16,15 +17,17 @@ export const createViewportStores = context => {
// Debounce scroll updates so we can slow down visible row computation // Debounce scroll updates so we can slow down visible row computation
scroll.subscribe(({ left, top }) => { scroll.subscribe(({ left, top }) => {
// Only update local state when big changes occur window.requestAnimationFrame(() => {
if (Math.abs(top - scrollTop) > cellHeight * 2) { // Only update local state when big changes occur
scrollTop = top if (Math.abs(top - scrollTop) > cellHeight * 2) {
scrollTopStore.set(top) scrollTop = top
} scrollTopStore.set(top)
if (Math.abs(left - scrollLeft) > 100) { }
scrollLeft = left if (Math.abs(left - scrollLeft) > 100) {
scrollLeftStore.set(left) scrollLeft = left
} scrollLeftStore.set(left)
}
})
}) })
// Derive visible rows // Derive visible rows
@ -32,8 +35,8 @@ export const createViewportStores = context => {
[rows, scrollTopStore, height], [rows, scrollTopStore, height],
([$rows, $scrollTop, $height]) => { ([$rows, $scrollTop, $height]) => {
console.log("new rows") console.log("new rows")
const maxRows = Math.ceil($height / cellHeight) + 8 const maxRows = Math.ceil($height / cellHeight) + 16
const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight) - 4) const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight) - 8)
return $rows.slice(firstRow, firstRow + maxRows) return $rows.slice(firstRow, firstRow + maxRows)
} }
) )