1
0
Fork 0
mirror of synced 2024-10-06 04:54:52 +13:00

Add new footer for adding rows, improve store memoization, support inverting all data types

This commit is contained in:
Andrew Kingston 2023-03-31 10:12:52 +01:00
parent 11dd5fc805
commit fc009b722f
16 changed files with 440 additions and 216 deletions

View file

@ -8,6 +8,7 @@
export let onChange
export let readonly = false
export let api
export let invert = false
const { API } = getContext("sheet")
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
@ -88,7 +89,7 @@
</div>
{#if isOpen}
<div class="dropzone">
<div class="dropzone" class:invert>
<Dropzone
{value}
compact
@ -111,6 +112,7 @@
gap: var(--cell-spacing);
align-self: stretch;
overflow: hidden;
user-select: none;
}
.attachment-cell.editable:hover {
cursor: pointer;
@ -136,12 +138,16 @@
}
.dropzone {
position: absolute;
top: -1px;
left: -1px;
top: 100%;
left: 0;
width: 320px;
background: var(--cell-background);
border: var(--cell-border);
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
padding: var(--cell-padding);
}
.dropzone.invert {
transform: translateY(-100%);
top: 0;
}
</style>

View file

@ -15,15 +15,42 @@
export let column
export let row
export let cellId
export let updateRow = rows.actions.updateRow
export let showPlaceholder = false
export let invert = false
let api
let error
// Build cell API
$: cellAPI = {
...api,
isReadonly: () => !!column.schema.autocolumn,
isRequired: () => !!column.schema.constraints?.presence,
updateValue: value => {
error = null
try {
if (cellAPI.isReadonly()) {
// Ensure cell isn't readonly
error = "Auto columns can't be edited"
} else if (cellAPI.isRequired() && (value == null || value === "")) {
// Sanity check required fields
error = "Required field"
} else {
updateRow(row._id, column.name, value)
}
} catch (err) {
error = err
}
},
}
// Update selected cell API if selected
$: {
if (selected) {
selectedCellAPI.set({
...api,
isReadonly: () => !!column.schema.autocolumn,
})
selectedCellAPI.set(cellAPI)
} else {
error = null
}
}
</script>
@ -36,17 +63,32 @@
{selectedUser}
{reorderSource}
{reorderTarget}
{error}
on:click={() => selectedCellId.set(cellId)}
on:contextmenu={e => menu.actions.open(cellId, e)}
width={column.width}
>
<svelte:component
this={getCellRenderer(column)}
bind:api
value={row[column.name]}
schema={column.schema}
{selected}
onChange={val => rows.actions.updateRow(row._id, column.name, val)}
readonly={column.schema.autocolumn}
/>
{#if !selected && showPlaceholder && (row[column.name] == null || row[column.name] === "")}
<div class="placeholder">
{column.name}
</div>
{:else}
<svelte:component
this={getCellRenderer(column)}
bind:api
value={row[column.name]}
schema={column.schema}
{selected}
onChange={cellAPI.updateValue}
readonly={column.schema.autocolumn}
{invert}
/>
{/if}
</SheetCell>
<style>
.placeholder {
font-style: italic;
padding: var(--cell-padding);
}
</style>

View file

@ -6,6 +6,7 @@
export let onChange
export let readonly = false
export let api
export let invert = false
let textarea
let isOpen = false
@ -48,6 +49,7 @@
{#if isOpen}
<textarea
class:invert
bind:this={textarea}
value={value || ""}
on:change={handleChange}
@ -92,7 +94,10 @@
width: calc(100% + 100px);
height: calc(5 * var(--cell-height) + 1px);
border: var(--cell-border);
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400);
}
textarea.invert {
transform: translateY(-100%);
}
textarea:focus {
outline: none;

View file

@ -10,6 +10,7 @@
export let multi = false
export let readonly = false
export let api
export let invert = false
let isOpen = false
let focusedOptionIdx = null
@ -98,7 +99,7 @@
</div>
{/if}
{#if isOpen}
<div class="options" on:wheel={e => e.stopPropagation()}>
<div class="options" class:invert on:wheel={e => e.stopPropagation()}>
{#each values as val, idx}
{@const color = getOptionColor(val)}
<div
@ -183,10 +184,10 @@
);
}
.options {
min-width: calc(100% + 2px);
min-width: 100%;
position: absolute;
top: -1px;
left: -1px;
top: 100%;
left: 0;
display: flex;
flex-direction: column;
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
@ -196,6 +197,10 @@
overflow-y: auto;
border: var(--cell-border);
}
.options.invert {
transform: translateY(-100%);
top: 0;
}
.option {
flex: 0 0 var(--cell-height);
padding: 0 var(--cell-padding);

View file

@ -10,6 +10,7 @@
export let selected
export let schema
export let onChange
export let invert = false
const { API } = getContext("sheet")
@ -21,6 +22,7 @@
let primaryDisplay
let candidateIndex
let lastSearchId
let results
$: oneRowOnly = schema?.relationshipType === "one-to-many"
$: editable = selected && !readonly
@ -117,6 +119,9 @@
const open = async () => {
isOpen = true
// Ensure results are properly reset
results = sortRows(value)
// Fetch definition if required
if (!definition) {
definition = await API.fetchTableDefinition(schema.tableId)
@ -214,7 +219,7 @@
</div>
{#if isOpen}
<div class="dropdown" on:wheel|stopPropagation>
<div class="dropdown" class:invert on:wheel|stopPropagation>
<div class="search">
<Input autofocus quiet type="text" bind:value={searchString} />
</div>
@ -284,9 +289,9 @@
.dropdown {
position: absolute;
top: -1px;
left: -1px;
min-width: calc(100% + 2px);
top: 100%;
left: 0;
min-width: 100%;
max-width: calc(100% + 240px);
max-height: calc(var(--cell-height) + 240px);
background: var(--cell-background);
@ -297,6 +302,10 @@
align-items: stretch;
background-color: var(--cell-background-hover);
}
.dropdown.invert {
transform: translateY(-100%);
top: 0;
}
.results {
overflow-y: auto;

View file

@ -8,6 +8,7 @@
export let center = false
export let selectedUser = null
export let rowIdx
export let error = null
$: style = getStyle(width, selectedUser)
@ -29,6 +30,7 @@
class:reorder-source={reorderSource}
class:reorder-target={reorderTarget}
class:center
class:error={error && selected}
on:focus
on:mousedown
on:mouseup
@ -38,8 +40,12 @@
data-row={rowIdx}
>
<slot />
{#if selectedUser && !selected}
<div class="user">
{#if selected && error}
<div class="label">
{error}
</div>
{:else if selectedUser && !selected}
<div class="label">
{selectedUser.label}
</div>
{/if}
@ -66,6 +72,9 @@
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400);
z-index: 2;
}
.cell.error {
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-red-400);
}
.cell.selected-other:not(.selected) {
z-index: 1;
box-shadow: inset 0 0 0 2px var(--user-color);
@ -100,7 +109,7 @@
}
/* Other user email */
.user {
.label {
position: absolute;
bottom: 100%;
padding: 1px 4px;
@ -114,14 +123,19 @@
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
user-select: none;
}
.cell[data-row="0"] .user {
.cell[data-row="0"] .label {
bottom: auto;
top: 100%;
border-radius: 0 0 2px 2px;
padding: 0 4px 2px 4px;
}
.cell:hover .user {
.cell:hover .label {
display: block;
}
.error .label {
background: var(--spectrum-global-color-red-400);
display: block;
}
</style>

View file

@ -31,7 +31,11 @@
</script>
{#if selectedRowCount}
<div class="delete-button" on:mousedown|stopPropagation={modal.show}>
<div
class="delete-button"
on:click|stopPropagation
on:mousedown|stopPropagation={modal.show}
>
<ActionButton icon="Delete" size="S">
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
</ActionButton>

View file

@ -1,141 +1,213 @@
<script>
import SheetCell from "../cells/SheetCell.svelte"
import { getContext } from "svelte"
import { Icon } from "@budibase/bbui"
import { Icon, Button } from "@budibase/bbui"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
import { getCellRenderer } from "../lib/renderers"
import DataCell from "../cells/DataCell.svelte"
const {
renderedColumns,
hoveredRowId,
rows,
selectedCellId,
reorder,
stickyColumn,
gutterWidth,
scroll,
config,
dispatch,
visibleColumns,
rows,
wheel,
showHScrollbar,
tableId,
} = getContext("sheet")
let isAdding = false
let newRow = {}
let touched = false
$: firstColumn = $stickyColumn || $visibleColumns[0]
$: rowHovered = $hoveredRowId === "new"
$: containsSelectedCell = $selectedCellId?.startsWith("new-")
$: width = gutterWidth + ($stickyColumn?.width || 0)
$: scrollLeft = $scroll.left
$: $tableId, (isAdding = false)
const addRow = async field => {
// const newRow = await rows.actions.addRow()
// if (newRow) {
// $selectedCellId = `${newRow._id}-${field.name}`
// }
const addRow = async () => {
const savedRow = await rows.actions.addRow(newRow)
if (savedRow && firstColumn) {
$selectedCellId = `${newRow._id}-${firstColumn.name}`
}
isAdding = false
}
$: console.log(newRow)
const cancel = () => {
isAdding = false
}
const startAdding = () => {
if (isAdding) {
return
}
newRow = {}
isAdding = true
}
const updateRow = (rowId, columnName, val) => {
touched = true
newRow[columnName] = val
}
const addViaModal = () => {
isAdding = false
dispatch("add-row")
}
</script>
<div class="new" on:click={startAdding}>
<!-- Only show new row functionality if we have any columns -->
{#if firstColumn}
{#if !isAdding}
<div class="add">
<div class="icon">
<Icon name="Add" size="S" />
</div>
<div class="text">Add row</div>
<div class="add-button" class:above-scrollbar={$showHScrollbar}>
<Button size="M" cta icon="Add" on:click={startAdding}>Add row</Button>
</div>
{:else}
<div class="sticky" style="flex: 0 0 {width}px">
<SheetCell width={gutterWidth} center>
<Icon name="Add" size="S" />
</SheetCell>
{#if $stickyColumn}
{@const cellId = `new-${$stickyColumn.name}`}
<SheetCell
width={$stickyColumn.width}
selected={$selectedCellId === cellId}
on:click={() => ($selectedCellId = cellId)}
>
<svelte:component
this={getCellRenderer($stickyColumn)}
value={newRow[$stickyColumn.name]}
schema={$stickyColumn.schema}
selected={$selectedCellId === cellId}
onChange={val => (newRow[$stickyColumn.name] = val)}
readonly={$stickyColumn.schema.autocolumn}
/>
</SheetCell>
{/if}
</div>
<SheetScrollWrapper scrollVertically={false}>
<div class="row">
{#each $renderedColumns as column}
{@const cellId = `new-${column.name}`}
<SheetCell
width={column.width}
selected={$selectedCellId === cellId}
on:click={() => ($selectedCellId = cellId)}
>
<svelte:component
this={getCellRenderer(column)}
value={newRow[column.name]}
schema={column.schema}
selected={$selectedCellId === cellId}
onChange={val => (newRow[column.name] = val)}
readonly={column.schema.autocolumn}
/>
</SheetCell>
{/each}
</div>
</SheetScrollWrapper>
{/if}
</div>
<div
class="container"
class:visible={isAdding}
on:wheel={wheel.actions.handleWheel}
>
<div class="buttons">
<Button size="M" cta on:click={addRow}>Save</Button>
<Button size="M" secondary newStyles on:click={cancel}>Cancel</Button>
</div>
<div class="content" class:above-scrollbar={$showHScrollbar}>
<div
class="new-row"
on:mouseenter={() => ($hoveredRowId = "new")}
on:mouseleave={() => ($hoveredRowId = null)}
>
<div
class="sticky-column"
style="flex: 0 0 {width}px"
class:scrolled={scrollLeft > 0}
>
<SheetCell
width={gutterWidth}
{rowHovered}
rowSelected={containsSelectedCell}
>
<div class="gutter">
{#if $config.allowExpandRows}
<Icon
name="Maximize"
size="S"
hoverable
on:click={addViaModal}
/>
{/if}
</div>
</SheetCell>
{#if $stickyColumn}
{@const cellId = `new-${$stickyColumn.name}`}
<DataCell
{cellId}
column={$stickyColumn}
row={newRow}
{rowHovered}
selected={$selectedCellId === cellId}
rowSelected={containsSelectedCell}
width={$stickyColumn.width}
{updateRow}
showPlaceholder
invert
/>
{/if}
</div>
<SheetScrollWrapper scrollVertically={false}>
<div class="row">
{#each $renderedColumns as column}
{@const cellId = `new-${column.name}`}
<DataCell
{cellId}
{column}
row={newRow}
{rowHovered}
selected={$selectedCellId === cellId}
rowSelected={containsSelectedCell}
width={column.width}
{updateRow}
showPlaceholder
invert
/>
{/each}
</div>
</SheetScrollWrapper>
</div>
</div>
</div>
{/if}
<style>
.new {
display: flex;
.add-button {
position: absolute;
left: 16px;
bottom: 16px;
z-index: 1;
}
.add-button.above-scrollbar {
bottom: 32px;
}
.container {
pointer-events: none;
position: absolute;
bottom: 0;
transform: translateY(100%);
z-index: 1;
transition: transform 130ms ease-out;
background: linear-gradient(
to bottom,
transparent,
var(--cell-background) 80%
);
width: 100%;
padding-top: 64px;
display: flex;
flex-direction: column;
align-items: stretch;
}
.container.visible {
transform: translateY(0);
}
.content {
pointer-events: all;
background: var(--background);
border-top: var(--cell-border);
}
.content.above-scrollbar {
padding: 0 0 24px 0;
}
.new-row {
display: flex;
bottom: 0;
left: 0;
width: 100%;
height: calc(16px + var(--cell-height));
padding-bottom: 16px;
z-index: 1;
background: var(--cell-background);
transition: margin-bottom 130ms ease-out;
margin-top: -1px;
}
.new-row.visible {
margin-bottom: 0;
}
.new-row :global(.cell) {
/*border-bottom: 0;*/
--cell-background: var(--background) !important;
border-top: var(--cell-border);
}
.add {
flex: 1 1 auto;
display: flex;
}
.add:hover {
cursor: pointer;
background: var(--cell-background-hover);
}
.add .icon {
flex: 0 0 var(--gutter-width);
display: grid;
place-items: center;
}
.add .text {
flex: 1 1 auto;
padding: var(--cell-padding);
}
.new :global(.cell) {
cursor: pointer;
}
.sticky {
z-index: 2;
.sticky-column {
display: flex;
z-index: 1;
}
/* Don't show borders between cells in the sticky column */
.sticky :global(.cell:not(:last-child)) {
.sticky-column :global(.cell:not(:last-child)) {
border-right: none;
}
@ -143,4 +215,34 @@
width: 0;
display: flex;
}
/* Add shadow when scrolled */
.sticky.scrolled :global(.cell:last-child:after) {
content: " ";
position: absolute;
width: 10px;
height: 100%;
left: 100%;
background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
}
/* Styles for gutter */
.gutter {
flex: 1 1 auto;
display: grid;
align-items: center;
padding: var(--cell-padding);
grid-template-columns: 1fr 16px;
gap: var(--cell-spacing);
}
/* Floating buttons */
.buttons {
display: flex;
flex-direction: row;
gap: 8px;
margin: 0 0 16px 16px;
pointer-events: all;
align-self: flex-start;
}
</style>

View file

@ -15,6 +15,7 @@
import { createMenuStores } from "../stores/menu"
import { createMaxScrollStores } from "../stores/max-scroll"
import { createPaginationStores } from "../stores/pagination"
import { createWheelStores } from "../stores/wheel"
import DeleteButton from "../controls/DeleteButton.svelte"
import SheetBody from "./SheetBody.svelte"
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
@ -25,8 +26,8 @@
import UserAvatars from "./UserAvatars.svelte"
import KeyboardManager from "../overlays/KeyboardManager.svelte"
import { clickOutside } from "@budibase/bbui"
import AddRowButton from "../controls/AddRowButton.svelte"
import SheetControls from "./SheetControls.svelte"
import NewRow from "./NewRow.svelte"
export let API
export let tableId
@ -74,6 +75,7 @@
context = { ...context, ...createUserStores(context) }
context = { ...context, ...createMenuStores(context) }
context = { ...context, ...createPaginationStores(context) }
context = { ...context, ...createWheelStores(context) }
// Reference some stores for local use
const { isResizing, isReordering, ui, loaded } = context
@ -116,18 +118,20 @@
</div>
</div>
{#if $loaded}
<div class="sheet-data" use:clickOutside={ui.actions.blur}>
<StickyColumn />
<div class="sheet-main">
<HeaderRow />
<SheetBody />
<div class="sheet-data-outer" use:clickOutside={ui.actions.blur}>
<div class="sheet-data-inner">
<StickyColumn />
<div class="sheet-data-content">
<HeaderRow />
<SheetBody />
</div>
{#if $config.allowAddRows}
<NewRow />
{/if}
<ResizeOverlay />
<ScrollOverlay />
<MenuOverlay />
</div>
<ResizeOverlay />
<ScrollOverlay />
<MenuOverlay />
{#if $config.allowAddRows}
<AddRowButton />
{/if}
</div>
{/if}
<KeyboardManager />
@ -163,18 +167,25 @@
cursor: grabbing !important;
}
.sheet-data {
.sheet-data-outer,
.sheet-data-inner {
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-items: flex-start;
align-items: stretch;
overflow: hidden;
height: 0;
position: relative;
background: var(--spectrum-global-color-gray-75);
}
.sheet-main {
.sheet-data-outer {
height: 0;
flex-direction: column;
/*background: var(--spectrum-global-color-gray-75);*/
background: var(--cell-background);
}
.sheet-data-inner {
flex-direction: row;
position: relative;
}
.sheet-data-content {
flex: 1 1 auto;
overflow: hidden;
display: flex;

View file

@ -1,18 +1,13 @@
<script>
import { getContext } from "svelte"
import { domDebounce } from "../../../utils/utils"
const {
cellHeight,
scroll,
bounds,
visibleColumns,
renderedRows,
renderedColumns,
hoveredRowId,
maxScrollTop,
maxScrollLeft,
selectedCellId,
wheel,
} = getContext("sheet")
export let scrollVertically = true
@ -20,8 +15,6 @@
export let wheelInteractive = true
$: hiddenWidths = calculateHiddenWidths($renderedColumns)
$: scrollLeft = $scroll.left
$: scrollTop = $scroll.top
$: style = generateStyle($scroll, hiddenWidths)
const generateStyle = (scroll, hiddenWidths) => {
@ -43,40 +36,11 @@
}
return width
}
// Handles a wheel even and updates the scroll offsets
const handleWheel = e => {
e.preventDefault()
const modifier = e.ctrlKey || e.metaKey
let x = modifier ? e.deltaY : e.deltaX
let y = modifier ? e.deltaX : e.deltaY
debouncedHandleWheel(x, y, e.clientY)
}
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
// Calculate new scroll top
let newScrollTop = scrollTop + deltaY
newScrollTop = Math.max(0, Math.min(newScrollTop, $maxScrollTop))
// Calculate new scroll left
let newScrollLeft = scrollLeft + deltaX
newScrollLeft = Math.max(0, Math.min(newScrollLeft, $maxScrollLeft))
// Update state
scroll.set({
left: newScrollLeft,
top: newScrollTop,
})
// Hover row under cursor
const y = clientY - $bounds.top + (newScrollTop % cellHeight)
const hoveredRow = $renderedRows[Math.floor(y / cellHeight)]
$hoveredRowId = hoveredRow?._id
})
</script>
<div
class="outer"
on:wheel={wheelInteractive ? handleWheel : null}
on:wheel={wheelInteractive ? wheel.actions.handleWheel : null}
on:click|self={() => ($selectedCellId = null)}
>
<div {style}>

View file

@ -1,6 +1,7 @@
<script>
import { getContext, onMount } from "svelte"
import { debounce } from "../../../utils/utils"
import { notifications } from "@budibase/bbui"
const {
rows,
@ -114,11 +115,7 @@
if (!$selectedCellId) {
return
}
if ($selectedCellAPI?.isReadonly()) {
return
}
const [rowId, column] = $selectedCellId.split("-")
rows.actions.updateRow(rowId, column, null)
$selectedCellAPI.updateValue(null)
}, 100)
const focusSelectedCell = () => {

View file

@ -6,16 +6,17 @@
scroll,
bounds,
cellHeight,
stickyColumn,
contentHeight,
maxScrollTop,
contentWidth,
maxScrollLeft,
gutterWidth,
screenWidth,
showHScrollbar,
showVScrollbar,
} = getContext("sheet")
// Bar config
const barOffset = 4
const barOffset = 8
// State for dragging bars
let initialMouse
@ -37,16 +38,11 @@
$: barTop = barOffset + cellHeight + availHeight * (scrollTop / $maxScrollTop)
// Calculate H scrollbar size and offset
$: totalWidth = width + gutterWidth + ($stickyColumn?.width || 0)
$: renderWidth = totalWidth - 2 * barOffset
$: barWidth = Math.max(50, (totalWidth / $contentWidth) * renderWidth)
$: renderWidth = $screenWidth - 2 * barOffset
$: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth)
$: availWidth = renderWidth - barWidth
$: barLeft = barOffset + availWidth * (scrollLeft / $maxScrollLeft)
// Calculate whether to show scrollbars or not
$: showVScrollbar = $contentHeight > height
$: showHScrollbar = $contentWidth > totalWidth
// V scrollbar drag handlers
const startVDragging = e => {
e.preventDefault()
@ -92,17 +88,17 @@
}
</script>
{#if showVScrollbar}
{#if $showVScrollbar}
<div
class="v-scrollbar"
style="top:{barTop}px; height:{barHeight}px;"
style="top:{barTop}px; height:{barHeight}px;right:{barOffset}px;"
on:mousedown={startVDragging}
/>
{/if}
{#if showHScrollbar}
{#if $showHScrollbar}
<div
class="h-scrollbar"
style="left:{barLeft}px; width:{barWidth}px;"
style="left:{barLeft}px; width:{barWidth}px;bottom:{barOffset}px;"
on:mousedown={startHDragging}
/>
{/if}
@ -110,21 +106,19 @@
<style>
div {
position: absolute;
background: var(--spectrum-global-color-gray-600);
opacity: 0.6;
background: var(--spectrum-global-color-gray-500);
opacity: 0.7;
border-radius: 4px;
z-index: 999;
z-index: 1;
transition: opacity 130ms ease-out;
}
div:hover {
opacity: 0.9;
opacity: 1;
}
.v-scrollbar {
right: 4px;
width: 8px;
}
.h-scrollbar {
bottom: 4px;
height: 8px;
}
</style>

View file

@ -17,6 +17,7 @@ export const createMaxScrollStores = context => {
// Memoize store primitives
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
// Derive vertical limits
const height = derived(bounds, $bounds => $bounds.height, 0)
@ -34,9 +35,9 @@ export const createMaxScrollStores = context => {
// Derive horizontal limits
const contentWidth = derived(
[visibleColumns, stickyColumn],
([$visibleColumns, $stickyColumn]) => {
let width = gutterWidth + padding + ($stickyColumn?.width || 0)
[visibleColumns, stickyColumnWidth],
([$visibleColumns, $stickyColumnWidth]) => {
let width = gutterWidth + padding + $stickyColumnWidth
$visibleColumns.forEach(col => {
width += col.width
})
@ -45,9 +46,8 @@ export const createMaxScrollStores = context => {
0
)
const screenWidth = derived(
[width, stickyColumn],
([$width, $stickyColumn]) =>
$width + gutterWidth + ($stickyColumn?.width || 0),
[width, stickyColumnWidth],
([$width, $stickyColumnWidth]) => $width + gutterWidth + $stickyColumnWidth,
0
)
const maxScrollLeft = derived(
@ -151,10 +151,27 @@ export const createMaxScrollStores = context => {
}
})
// Derive whether to show scrollbars or not
const showVScrollbar = derived(
[contentHeight, height],
([$contentHeight, $height]) => {
return $contentHeight > $height
}
)
const showHScrollbar = derived(
[contentWidth, screenWidth],
([$contentWidth, $screenWidth]) => {
return $contentWidth > $screenWidth
}
)
return {
contentHeight,
contentWidth,
screenWidth,
maxScrollTop,
maxScrollLeft,
showHScrollbar,
showVScrollbar,
}
}

View file

@ -120,7 +120,7 @@ export const createRowsStore = context => {
return index >= 0 ? get(enrichedRows)[index] : null
}
// Adds a new empty row
// Adds a new row
const addRow = async (row, idx) => {
try {
// Create row
@ -136,6 +136,7 @@ export const createRowsStore = context => {
} else {
handleNewRows([newRow])
}
notifications.success("Row added successfully")
return newRow
} catch (error) {
notifications.error(`Error adding row: ${error?.message}`)

View file

@ -11,7 +11,7 @@ export const createViewportStores = context => {
// Derive visible rows
// Split into multiple stores containing primitives to optimise invalidation
// as mich as possible
// as much as possible
const scrolledRowCount = derived(
scrollTop,
$scrollTop => {
@ -22,7 +22,7 @@ export const createViewportStores = context => {
const visualRowCapacity = derived(
height,
$height => {
return Math.ceil($height / cellHeight)
return Math.ceil($height / cellHeight) + 1
},
0
)

View file

@ -0,0 +1,53 @@
import { get } from "svelte/store"
import { domDebounce } from "../../../utils/utils"
export const createWheelStores = context => {
const {
maxScrollLeft,
maxScrollTop,
hoveredRowId,
renderedRows,
bounds,
scroll,
cellHeight,
} = context
// Handles a wheel even and updates the scroll offsets
const handleWheel = e => {
e.preventDefault()
const modifier = e.ctrlKey || e.metaKey
let x = modifier ? e.deltaY : e.deltaX
let y = modifier ? e.deltaX : e.deltaY
debouncedHandleWheel(x, y, e.clientY)
}
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
const { top, left } = get(scroll)
// Calculate new scroll top
let newScrollTop = top + deltaY
newScrollTop = Math.max(0, Math.min(newScrollTop, get(maxScrollTop)))
// Calculate new scroll left
let newScrollLeft = left + deltaX
newScrollLeft = Math.max(0, Math.min(newScrollLeft, get(maxScrollLeft)))
// Update state
scroll.set({
left: newScrollLeft,
top: newScrollTop,
})
// Hover row under cursor
const y = clientY - get(bounds).top + (newScrollTop % cellHeight)
const hoveredRow = get(renderedRows)[Math.floor(y / cellHeight)]
hoveredRowId.set(hoveredRow?._id)
})
return {
wheel: {
actions: {
handleWheel,
},
},
}
}