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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { get, writable } from "svelte/store"
export const createReorderStores = context => {
const { columns, bounds, rows, scroll, rand } = context
const { columns, visibleColumns, rand, scroll, bounds } = context
const reorderInitialState = {
columnIdx: null,
swapColumnIdx: null,
@ -10,33 +10,14 @@ export const createReorderStores = context => {
}
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
const startReordering = (columnIdx, e) => {
const $columns = get(columns)
const $bounds = get(bounds)
const $rows = get(rows)
const $scroll = get(scroll)
// Generate new breakpoints for the current columns
let breakpoints = []
$columns.forEach((col, idx) => {
breakpoints.push(col.left)
if (idx === $columns.length - 1) {
breakpoints.push(col.left + col.width)
}
})
const self = $columns[columnIdx]
let breakpoints = $columns.map(col => col.left + col.width)
// Update state
reorder.set({
@ -44,15 +25,8 @@ export const createReorderStores = context => {
breakpoints,
swapColumnIdx: null,
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,
sheetLeft: $bounds.left,
})
// Add listeners to handle mouse movement
@ -71,34 +45,18 @@ export const createReorderStores = context => {
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
let swapColumnIdx
let minDistance = Number.MAX_SAFE_INTEGER
const mouseX = e.clientX - $reorder.sheetLeft + $reorder.scrollLeft
$reorder.breakpoints.forEach((point, idx) => {
const distance = Math.abs(
point - e.clientX + $placeholder.sheetLeft - $placeholder.scrollLeft
)
const distance = Math.abs(point - mouseX)
if (distance < minDistance) {
minDistance = distance
swapColumnIdx = idx
}
})
// Update state
placeholder.update(state => {
state.x = newX
return state
})
if (swapColumnIdx !== $reorder.swapColumnIdx) {
reorder.update(state => {
state.swapColumnIdx = swapColumnIdx
@ -111,6 +69,7 @@ export const createReorderStores = context => {
const stopReordering = () => {
// Swap position of columns
let { columnIdx, swapColumnIdx } = get(reorder)
swapColumnIdx++
columns.update(state => {
const removed = state.splice(columnIdx, 1)
if (--swapColumnIdx < columnIdx) {
@ -131,7 +90,6 @@ export const createReorderStores = context => {
// Reset state
reorder.set(reorderInitialState)
placeholder.set(placeholderInitialState)
// Remove event handlers
document.removeEventListener("mousemove", onReorderMouseMove)
@ -147,6 +105,5 @@ export const createReorderStores = context => {
stopReordering,
},
},
reorderPlaceholder: placeholder,
}
}

View File

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