Refactor resizing logic into store and improve UX around hover events when resizing/reordering

This commit is contained in:
Andrew Kingston 2023-03-06 15:39:50 +00:00
parent b5a72438e1
commit f0ac9e9d9c
5 changed files with 121 additions and 83 deletions

View File

@ -1,77 +1,23 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const { columns, rand, scroll, visibleColumns, stickyColumn, isReordering } = const {
getContext("sheet") columns,
const MinColumnWidth = 100 resize,
scroll,
let initialMouseX = null visibleColumns,
let initialWidth = null stickyColumn,
let columnIdx = null isReordering,
let width = 0 } = getContext("sheet")
let left = 0
let columnCount = 0
$: scrollLeft = $scroll.left $: scrollLeft = $scroll.left
$: cutoff = scrollLeft + 40 + ($columns[0]?.width || 0) $: cutoff = scrollLeft + 40 + ($columns[0]?.width || 0)
$: offset = 40 + ($stickyColumn?.width || 0) $: offset = 40 + ($stickyColumn?.width || 0)
$: columnIdx = $resize.columnIdx
const startResizing = (idx, e) => { const getStyle = (column, offset, scrollLeft) => {
// Prevent propagation to stop reordering triggering const left = offset + column.left + column.width - scrollLeft
e.stopPropagation() return `left:${left}px;`
const col = idx === "sticky" ? $stickyColumn : $columns[idx]
width = col.width
left = col.left
initialWidth = width
initialMouseX = e.clientX
columnIdx = idx
columnCount = $columns.length
// Add mouse event listeners to handle resizing
document.addEventListener("mousemove", onResizeMouseMove)
document.addEventListener("mouseup", stopResizing)
document.getElementById(`sheet-${rand}`).classList.add("is-resizing")
}
const onResizeMouseMove = e => {
const dx = e.clientX - initialMouseX
const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx))
if (Math.abs(width - newWidth) < 10) {
return
}
if (columnIdx === "sticky") {
stickyColumn.update(state => ({
...state,
width: newWidth,
}))
} else {
columns.update(state => {
state[columnIdx].width = newWidth
let offset = state[columnIdx].left + newWidth
for (let i = columnIdx + 1; i < state.length; i++) {
state[i].left = offset
offset += state[i].width
}
return [...state]
})
}
width = newWidth
}
const stopResizing = () => {
columnIdx = null
document.removeEventListener("mousemove", onResizeMouseMove)
document.removeEventListener("mouseup", stopResizing)
document.getElementById(`sheet-${rand}`).classList.remove("is-resizing")
}
const getStyle = (col, offset, scrollLeft) => {
const left = offset + col.left + col.width - scrollLeft
return `--left:${left}px;`
} }
</script> </script>
@ -80,18 +26,18 @@
<div <div
class="resize-slider sticky" class="resize-slider sticky"
class:visible={columnIdx === "sticky"} class:visible={columnIdx === "sticky"}
on:mousedown={e => startResizing("sticky", e)} on:mousedown={e => resize.actions.startResizing($stickyColumn, e)}
style="--left:{40 + $stickyColumn.width}px;" style="left:{40 + $stickyColumn.width}px;"
> >
<div class="resize-indicator" /> <div class="resize-indicator" />
</div> </div>
{/if} {/if}
{#each $visibleColumns as col} {#each $visibleColumns as column}
<div <div
class="resize-slider" class="resize-slider"
class:visible={columnIdx === col.idx} class:visible={columnIdx === column.idx}
on:mousedown={e => startResizing(col.idx, e)} on:mousedown={e => resize.actions.startResizing(column, e)}
style={getStyle(col, offset, scrollLeft)} style={getStyle(column, offset, scrollLeft)}
> >
<div class="resize-indicator" /> <div class="resize-indicator" />
</div> </div>
@ -104,7 +50,6 @@
top: 0; top: 0;
z-index: 1; z-index: 1;
height: var(--cell-height); height: var(--cell-height);
left: var(--left);
opacity: 0; opacity: 0;
padding: 0 8px; padding: 0 8px;
transform: translateX(-50%); transform: translateX(-50%);
@ -124,11 +69,4 @@
height: 100%; height: 100%;
background: var(--spectrum-global-color-blue-400); background: var(--spectrum-global-color-blue-400);
} }
:global(.sheet.is-resizing *) {
cursor: col-resize !important;
}
:global(.sheet.is-reordering .resize-slider) {
display: none;
}
</style> </style>

View File

@ -12,6 +12,7 @@
export { createUserStores } from "./stores/users" export { createUserStores } from "./stores/users"
import { createWebsocket } from "./websocket" import { createWebsocket } from "./websocket"
import { createUserStores } from "./stores/users" import { createUserStores } from "./stores/users"
import { createResizeStores } from "./stores/resize"
import DeleteButton from "./DeleteButton.svelte" import DeleteButton from "./DeleteButton.svelte"
import SheetBody from "./SheetBody.svelte" import SheetBody from "./SheetBody.svelte"
import ResizeOverlay from "./ResizeOverlay.svelte" import ResizeOverlay from "./ResizeOverlay.svelte"
@ -50,6 +51,7 @@
} }
context = { ...context, ...createRowsStore(context) } context = { ...context, ...createRowsStore(context) }
context = { ...context, ...createColumnsStores(context) } context = { ...context, ...createColumnsStores(context) }
context = { ...context, ...createResizeStores(context) }
context = { ...context, ...createBoundsStores(context) } context = { ...context, ...createBoundsStores(context) }
context = { ...context, ...createScrollStores(context) } context = { ...context, ...createScrollStores(context) }
context = { ...context, ...createViewportStores(context) } context = { ...context, ...createViewportStores(context) }
@ -57,6 +59,9 @@
context = { ...context, ...createInterfaceStores(context) } context = { ...context, ...createInterfaceStores(context) }
context = { ...context, ...createUserStores(context) } context = { ...context, ...createUserStores(context) }
// Reference some stores for local use
const isResizing = context.isResizing
// Keep config store up to date // Keep config store up to date
$: config.set({ $: config.set({
tableId, tableId,
@ -72,7 +77,12 @@
onMount(() => createWebsocket(context)) onMount(() => createWebsocket(context))
</script> </script>
<div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}"> <div
class="sheet"
class:is-resizing={$isResizing}
style="--cell-height:{cellHeight}px;"
id="sheet-{rand}"
>
<div class="controls"> <div class="controls">
<div class="controls-left"> <div class="controls-left">
<slot name="controls" /> <slot name="controls" />
@ -116,6 +126,9 @@
.sheet :global(*) { .sheet :global(*) {
box-sizing: border-box; box-sizing: border-box;
} }
.sheet.is-resizing :global(*) {
cursor: col-resize !important;
}
.sheet-data { .sheet-data {
flex: 1 1 auto; flex: 1 1 auto;

View File

@ -7,7 +7,7 @@
export let column export let column
export let orderable = true export let orderable = true
const { reorder, isReordering, rand } = getContext("sheet") const { reorder, isReordering, isResizing, rand } = getContext("sheet")
let timeout let timeout
let anchor let anchor
@ -39,7 +39,7 @@
class:open class:open
style="flex: 0 0 {column.width}px;" style="flex: 0 0 {column.width}px;"
bind:this={anchor} bind:this={anchor}
class:disabled={$isReordering} class:disabled={$isReordering || $isResizing}
> >
<SheetCell <SheetCell
reorderSource={$reorder.sourceColumn === column.name} reorderSource={$reorder.sourceColumn === column.name}

View File

@ -55,6 +55,7 @@ export const createColumnsStores = context => {
width: same ? existingWidth : defaultWidth, width: same ? existingWidth : defaultWidth,
left: 40, left: 40,
schema: primaryDisplay[1], schema: primaryDisplay[1],
idx: "sticky",
}) })
}) })

View File

@ -0,0 +1,86 @@
import { writable, get, derived } from "svelte/store"
export const createResizeStores = context => {
const { columns, stickyColumn } = context
const initialState = {
initialMouseX: null,
initialWidth: null,
columnIdx: null,
width: 0,
left: 0,
}
const resize = writable(initialState)
const isResizing = derived(resize, $resize => $resize.columnIdx != null)
const MinColumnWidth = 100
// Starts resizing a certain column
const startResizing = (column, e) => {
// Prevent propagation to stop reordering triggering
e.stopPropagation()
resize.set({
width: column.width,
left: column.left,
initialWidth: column.width,
initialMouseX: e.clientX,
columnIdx: column.idx,
})
// Add mouse event listeners to handle resizing
document.addEventListener("mousemove", onResizeMouseMove)
document.addEventListener("mouseup", stopResizing)
}
// Handler for moving the mouse to resize columns
const onResizeMouseMove = e => {
const { initialMouseX, initialWidth, width, columnIdx } = get(resize)
const dx = e.clientX - initialMouseX
const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx))
// Ignore small changes
if (Math.abs(width - newWidth) < 5) {
return
}
// Update column state
if (columnIdx === "sticky") {
stickyColumn.update(state => ({
...state,
width: newWidth,
}))
} else {
columns.update(state => {
state[columnIdx].width = newWidth
let offset = state[columnIdx].left + newWidth
for (let i = columnIdx + 1; i < state.length; i++) {
state[i].left = offset
offset += state[i].width
}
return [...state]
})
}
// Update state
resize.update(state => ({
...state,
width: newWidth,
}))
}
// Stop resizing any columns
const stopResizing = () => {
resize.set(initialState)
document.removeEventListener("mousemove", onResizeMouseMove)
document.removeEventListener("mouseup", stopResizing)
}
return {
resize: {
...resize,
actions: {
startResizing,
},
},
isResizing,
}
}