Merge pull request #10396 from Budibase/grid-tweaks

Grid tweaks
This commit is contained in:
Andrew Kingston 2023-04-24 16:53:46 +01:00 committed by GitHub
commit 98ca05d97f
34 changed files with 752 additions and 462 deletions

View File

@ -42,13 +42,14 @@
<GridCreateViewButton /> <GridCreateViewButton />
{/if} {/if}
<GridManageAccessButton /> <GridManageAccessButton />
{#if isUsersTable}
<EditRolesButton />
{/if}
{#if !isInternal} {#if !isInternal}
<GridRelationshipButton /> <GridRelationshipButton />
{/if} {/if}
<GridImportButton disabled={isUsersTable} /> {#if isUsersTable}
<EditRolesButton />
{:else}
<GridImportButton />
{/if}
<GridExportButton /> <GridExportButton />
<GridFilterButton /> <GridFilterButton />
<GridAddColumnModal /> <GridAddColumnModal />

View File

@ -16,7 +16,7 @@
</script> </script>
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}> <ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
Manage access Access
</ActionButton> </ActionButton>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<ManageAccessModal <ManageAccessModal

View File

@ -11,7 +11,7 @@
</script> </script>
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}> <ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
Create view Add view
</ActionButton> </ActionButton>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<CreateViewModal /> <CreateViewModal />

View File

@ -72,6 +72,7 @@
api = { api = {
focus: () => open(), focus: () => open(),
blur: () => close(), blur: () => close(),
isActive: () => isOpen,
onKeyDown, onKeyDown,
} }
}) })

View File

@ -50,6 +50,7 @@
const cellAPI = { const cellAPI = {
focus: () => api?.focus(), focus: () => api?.focus(),
blur: () => api?.blur(), blur: () => api?.blur(),
isActive: () => api?.isActive?.() ?? false,
onKeyDown: (...params) => api?.onKeyDown(...params), onKeyDown: (...params) => api?.onKeyDown(...params),
isReadonly: () => readonly, isReadonly: () => readonly,
getType: () => column.schema.type, getType: () => column.schema.type,
@ -67,6 +68,7 @@
{rowIdx} {rowIdx}
{focused} {focused}
{selectedUser} {selectedUser}
{readonly}
error={$error} error={$error}
on:click={() => focusedCellId.set(cellId)} on:click={() => focusedCellId.set(cellId)}
on:contextmenu={e => menu.actions.open(cellId, e)} on:contextmenu={e => menu.actions.open(cellId, e)}

View File

@ -8,6 +8,7 @@
export let rowIdx export let rowIdx
export let defaultHeight = false export let defaultHeight = false
export let center = false export let center = false
export let readonly = false
$: style = getStyle(width, selectedUser) $: style = getStyle(width, selectedUser)
@ -27,6 +28,7 @@
class:focused class:focused
class:error class:error
class:center class:center
class:readonly
class:default-height={defaultHeight} class:default-height={defaultHeight}
class:selected-other={selectedUser != null} class:selected-other={selectedUser != null}
on:focus on:focus
@ -121,7 +123,8 @@
.cell:hover { .cell:hover {
cursor: default; cursor: default;
} }
.cell.highlighted:not(.focused) { .cell.highlighted:not(.focused),
.cell.focused.readonly {
--cell-background: var(--cell-background-hover); --cell-background: var(--cell-background-hover);
} }
.cell.selected:not(.focused) { .cell.selected:not(.focused) {

View File

@ -0,0 +1,123 @@
<script>
import { GutterWidth } from "../lib/constants"
import { getContext } from "svelte"
import { Checkbox, Icon } from "@budibase/bbui"
import GridCell from "./GridCell.svelte"
import { createEventDispatcher } from "svelte"
export let row
export let rowFocused = false
export let rowHovered = false
export let rowSelected = false
export let disableExpand = false
export let disableNumber = false
export let defaultHeight = false
export let disabled = false
const { config, dispatch, selectedRows } = getContext("grid")
const svelteDispatch = createEventDispatcher()
const select = () => {
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
})
}
}
const expand = () => {
svelteDispatch("expand")
if (row) {
dispatch("edit-row", row)
}
}
</script>
<GridCell
width={GutterWidth}
highlighted={rowFocused || rowHovered}
selected={rowSelected}
{defaultHeight}
>
<div class="gutter">
{#if $$slots.default}
<slot />
{:else}
<div
on:click={select}
class="checkbox"
class:visible={$config.allowDeleteRows &&
(disableNumber || rowSelected || rowHovered || rowFocused)}
>
<Checkbox value={rowSelected} {disabled} />
</div>
{#if !disableNumber}
<div
class="number"
class:visible={!$config.allowDeleteRows ||
!(rowSelected || rowHovered || rowFocused)}
>
{row.__idx + 1}
</div>
{/if}
{/if}
{#if $config.allowExpandRows}
<div
class="expand"
class:visible={!disableExpand && (rowFocused || rowHovered)}
>
<Icon name="Maximize" hoverable size="S" on:click={expand} />
</div>
{/if}
</div>
</GridCell>
<style>
.gutter {
flex: 1 1 auto;
display: grid;
align-items: center;
padding: var(--cell-padding);
grid-template-columns: 1fr auto;
gap: var(--cell-spacing);
}
.checkbox,
.number {
display: none;
flex-direction: row;
justify-content: center;
align-items: center;
}
.checkbox :global(.spectrum-Checkbox) {
min-height: 0;
height: 20px;
}
.checkbox :global(.spectrum-Checkbox-box) {
margin: 3px 0 0 0;
}
.number {
color: var(--spectrum-global-color-gray-500);
}
.checkbox.visible,
.number.visible {
display: flex;
}
.expand {
opacity: 0;
pointer-events: none;
margin-right: 4px;
}
.expand.visible {
opacity: 1;
pointer-events: all;
}
</style>

View File

@ -20,6 +20,7 @@
ui, ui,
columns, columns,
} = getContext("grid") } = getContext("grid")
const bannedDisplayColumnTypes = [ const bannedDisplayColumnTypes = [
"link", "link",
"array", "array",
@ -100,7 +101,7 @@
style="flex: 0 0 {column.width}px;" style="flex: 0 0 {column.width}px;"
bind:this={anchor} bind:this={anchor}
class:disabled={$isReordering || $isResizing} class:disabled={$isReordering || $isResizing}
class:sorted={sortedBy} class:sticky={idx === "sticky"}
> >
<GridCell <GridCell
on:mousedown={onMouseDown} on:mousedown={onMouseDown}
@ -194,6 +195,10 @@
.header-cell { .header-cell {
display: flex; display: flex;
} }
.header-cell:not(.sticky):hover,
.header-cell:not(.sticky) :global(.cell:hover) {
cursor: grab;
}
.header-cell.disabled { .header-cell.disabled {
pointer-events: none; pointer-events: none;
} }
@ -202,9 +207,6 @@
gap: calc(2 * var(--cell-spacing)); gap: calc(2 * var(--cell-spacing));
background: var(--spectrum-global-color-gray-100); background: var(--spectrum-global-color-gray-100);
} }
.header-cell.sorted :global(.cell) {
background: var(--spectrum-global-color-gray-200);
}
.name { .name {
flex: 1 1 auto; flex: 1 1 auto;

View File

@ -31,7 +31,9 @@
isOpen = true isOpen = true
await tick() await tick()
textarea.focus() textarea.focus()
textarea.setSelectionRange(0, 0) if (value?.length > 100) {
textarea.setSelectionRange(0, 0)
}
} }
const close = () => { const close = () => {
@ -43,6 +45,7 @@
api = { api = {
focus: () => open(), focus: () => open(),
blur: () => close(), blur: () => close(),
isActive: () => isOpen,
onKeyDown, onKeyDown,
} }
}) })

View File

@ -73,6 +73,7 @@
api = { api = {
focus: open, focus: open,
blur: close, blur: close,
isActive: () => isOpen,
onKeyDown, onKeyDown,
} }
}) })

View File

@ -235,6 +235,7 @@
api = { api = {
focus: open, focus: open,
blur: close, blur: close,
isActive: () => isOpen,
onKeyDown, onKeyDown,
} }
}) })

View File

@ -33,6 +33,7 @@
api = { api = {
focus: () => input?.focus(), focus: () => input?.focus(),
blur: () => input?.blur(), blur: () => input?.blur(),
isActive: () => active,
onKeyDown, onKeyDown,
} }
}) })

View File

@ -12,5 +12,5 @@
on:click={() => dispatch("add-column")} on:click={() => dispatch("add-column")}
disabled={!$config.allowAddColumns} disabled={!$config.allowAddColumns}
> >
Create column Add column
</ActionButton> </ActionButton>

View File

@ -9,10 +9,10 @@
icon="TableRowAddBottom" icon="TableRowAddBottom"
quiet quiet
size="M" size="M"
on:click={() => dispatch("add-row")} on:click={() => dispatch("add-row-inline")}
disabled={!loaded || disabled={!loaded ||
!$config.allowAddRows || !$config.allowAddRows ||
(!$columns.length && !$stickyColumn)} (!$columns.length && !$stickyColumn)}
> >
Create row Add row
</ActionButton> </ActionButton>

View File

@ -0,0 +1,91 @@
<script>
import { getContext } from "svelte"
import { ActionButton, Popover } from "@budibase/bbui"
import { DefaultColumnWidth } from "../lib/constants"
const { stickyColumn, columns } = getContext("grid")
const smallSize = 120
const mediumSize = DefaultColumnWidth
const largeSize = DefaultColumnWidth * 1.5
let open = false
let anchor
$: allCols = $columns.concat($stickyColumn ? [$stickyColumn] : [])
$: allSmall = allCols.every(col => col.width === smallSize)
$: allMedium = allCols.every(col => col.width === mediumSize)
$: allLarge = allCols.every(col => col.width === largeSize)
$: custom = !allSmall && !allMedium && !allLarge
$: sizeOptions = [
{
label: "Small",
size: smallSize,
selected: allSmall,
},
{
label: "Medium",
size: mediumSize,
selected: allMedium,
},
{
label: "Large",
size: largeSize,
selected: allLarge,
},
]
const changeColumnWidth = async width => {
columns.update(state => {
state.forEach(column => {
column.width = width
})
return state
})
if ($stickyColumn) {
stickyColumn.update(state => ({
...state,
width,
}))
}
await columns.actions.saveChanges()
}
</script>
<div bind:this={anchor}>
<ActionButton
icon="MoveLeftRight"
quiet
size="M"
on:click={() => (open = !open)}
selected={open}
disabled={!allCols.length}
>
Width
</ActionButton>
</div>
<Popover bind:open {anchor} align="left">
<div class="content">
{#each sizeOptions as option}
<ActionButton
quiet
on:click={() => changeColumnWidth(option.size)}
selected={option.selected}
>
{option.label}
</ActionButton>
{/each}
{#if custom}
<ActionButton selected={custom} quiet>Custom</ActionButton>
{/if}
</div>
</Popover>
<style>
.content {
padding: 12px;
display: flex;
align-items: center;
gap: 8px;
}
</style>

View File

@ -1,13 +1,8 @@
<script> <script>
import { import { Modal, ModalContent, Button, notifications } from "@budibase/bbui"
Modal, import { getContext, onMount } from "svelte"
ModalContent,
ActionButton,
notifications,
} from "@budibase/bbui"
import { getContext } from "svelte"
const { selectedRows, rows, config } = getContext("grid") const { selectedRows, rows, config, subscribe } = getContext("grid")
let modal let modal
@ -28,18 +23,21 @@
await rows.actions.deleteRows(rowsToDelete) await rows.actions.deleteRows(rowsToDelete)
notifications.success(`Deleted ${count} row${count === 1 ? "" : "s"}`) notifications.success(`Deleted ${count} row${count === 1 ? "" : "s"}`)
} }
onMount(() => subscribe("request-bulk-delete", () => modal?.show()))
</script> </script>
{#if selectedRowCount} {#if selectedRowCount}
<div class="delete-button" data-ignore-click-outside="true"> <div class="delete-button" data-ignore-click-outside="true">
<ActionButton <Button
icon="Delete" icon="Delete"
size="S" size="M"
on:click={modal.show} on:click={modal.show}
disabled={!$config.allowEditRows} disabled={!$config.allowEditRows}
cta
> >
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"} Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
</ActionButton> </Button>
</div> </div>
{/if} {/if}
@ -57,16 +55,12 @@
</Modal> </Modal>
<style> <style>
.delete-button :global(.spectrum-ActionButton:not(:disabled) *) { .delete-button :global(.spectrum-Button:not(:disabled)) {
color: var(--spectrum-global-color-red-400); background-color: var(--spectrum-global-color-red-400);
}
.delete-button :global(.spectrum-ActionButton:not(:disabled)) {
border-color: var(--spectrum-global-color-red-400); border-color: var(--spectrum-global-color-red-400);
} }
/*.delete-button.disabled :global(.spectrum-ActionButton *) {*/ .delete-button :global(.spectrum-Button:not(:disabled):hover) {
/* color: var(--spectrum-global-color-gray-600);*/ background-color: var(--spectrum-global-color-red-500);
/*}*/ border-color: var(--spectrum-global-color-red-500);
/*.delete-button.disabled :global(.spectrum-ActionButton) {*/ }
/* border-color: var(--spectrum-global-color-gray-400);*/
/*}*/
</style> </style>

View File

@ -48,7 +48,7 @@
selected={open || anyHidden} selected={open || anyHidden}
disabled={!$columns.length} disabled={!$columns.length}
> >
Hide columns Columns
</ActionButton> </ActionButton>
</div> </div>

View File

@ -36,13 +36,13 @@
<div bind:this={anchor}> <div bind:this={anchor}>
<ActionButton <ActionButton
icon="LineHeight" icon="MoveUpDown"
quiet quiet
size="M" size="M"
on:click={() => (open = !open)} on:click={() => (open = !open)}
selected={open} selected={open}
> >
Row height Height
</ActionButton> </ActionButton>
</div> </div>

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import { ActionButton, Popover, Select } from "@budibase/bbui" import { ActionButton, Popover, Select } from "@budibase/bbui"
const { sort, visibleColumns, stickyColumn } = getContext("grid") const { sort, columns, stickyColumn } = getContext("grid")
const orderOptions = [ const orderOptions = [
{ label: "A-Z", value: "ascending" }, { label: "A-Z", value: "ascending" },
{ label: "Z-A", value: "descending" }, { label: "Z-A", value: "descending" },
@ -11,15 +11,24 @@
let open = false let open = false
let anchor let anchor
$: columnOptions = getColumnOptions($stickyColumn, $visibleColumns) $: columnOptions = getColumnOptions($stickyColumn, $columns)
$: checkValidSortColumn($sort.column, $stickyColumn, $visibleColumns) $: checkValidSortColumn($sort.column, $stickyColumn, $columns)
const getColumnOptions = (stickyColumn, columns) => { const getColumnOptions = (stickyColumn, columns) => {
let options = [] let options = []
if (stickyColumn) { if (stickyColumn) {
options.push(stickyColumn.name) options.push({
label: stickyColumn.label || stickyColumn.name,
value: stickyColumn.name,
})
} }
return [...options, ...columns.map(col => col.name)] return [
...options,
...columns.map(col => ({
label: col.label || col.name,
value: col.name,
})),
]
} }
const updateSortColumn = e => { const updateSortColumn = e => {
@ -37,13 +46,13 @@
} }
// Ensure we never have a sort column selected that is not visible // Ensure we never have a sort column selected that is not visible
const checkValidSortColumn = (sortColumn, stickyColumn, visibleColumns) => { const checkValidSortColumn = (sortColumn, stickyColumn, columns) => {
if (!sortColumn) { if (!sortColumn) {
return return
} }
if ( if (
sortColumn !== stickyColumn?.name && sortColumn !== stickyColumn?.name &&
!visibleColumns.some(col => col.name === sortColumn) !columns.some(col => col.name === sortColumn)
) { ) {
if (stickyColumn) { if (stickyColumn) {
sort.update(state => ({ sort.update(state => ({
@ -53,7 +62,7 @@
} else { } else {
sort.update(state => ({ sort.update(state => ({
...state, ...state,
column: visibleColumns[0]?.name, column: columns[0]?.name,
})) }))
} }
} }
@ -66,7 +75,7 @@
quiet quiet
size="M" size="M"
on:click={() => (open = !open)} on:click={() => (open = !open)}
selected={open || $sort.column} selected={open}
disabled={!columnOptions.length} disabled={!columnOptions.length}
> >
Sort Sort

View File

@ -22,6 +22,8 @@
import HideColumnsButton from "../controls/HideColumnsButton.svelte" import HideColumnsButton from "../controls/HideColumnsButton.svelte"
import AddRowButton from "../controls/AddRowButton.svelte" import AddRowButton from "../controls/AddRowButton.svelte"
import RowHeightButton from "../controls/RowHeightButton.svelte" import RowHeightButton from "../controls/RowHeightButton.svelte"
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
import NewRow from "./NewRow.svelte"
import { import {
MaxCellRenderHeight, MaxCellRenderHeight,
MaxCellRenderWidthOverflow, MaxCellRenderWidthOverflow,
@ -110,6 +112,7 @@
<AddRowButton /> <AddRowButton />
<AddColumnButton /> <AddColumnButton />
<slot name="controls" /> <slot name="controls" />
<ColumnWidthButton />
<RowHeightButton /> <RowHeightButton />
<HideColumnsButton /> <HideColumnsButton />
<SortButton /> <SortButton />
@ -127,10 +130,11 @@
<HeaderRow /> <HeaderRow />
<GridBody /> <GridBody />
</div> </div>
<BetaButton />
<NewRow />
<div class="overlays"> <div class="overlays">
<ResizeOverlay /> <ResizeOverlay />
<ReorderOverlay /> <ReorderOverlay />
<BetaButton />
<ScrollOverlay /> <ScrollOverlay />
<MenuOverlay /> <MenuOverlay />
</div> </div>

View File

@ -2,11 +2,25 @@
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import GridRow from "./GridRow.svelte" import GridRow from "./GridRow.svelte"
import { BlankRowID } from "../lib/constants"
const { bounds, renderedRows, rowVerticalInversionIndex } = getContext("grid") const {
bounds,
renderedRows,
renderedColumns,
rowVerticalInversionIndex,
config,
hoveredRowId,
dispatch,
} = getContext("grid")
let body let body
$: renderColumnsWidth = $renderedColumns.reduce(
(total, col) => (total += col.width),
0
)
onMount(() => { onMount(() => {
// Observe and record the height of the body // Observe and record the height of the body
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
@ -24,6 +38,16 @@
{#each $renderedRows as row, idx} {#each $renderedRows as row, idx}
<GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} /> <GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
{/each} {/each}
{#if $config.allowAddRows && $renderedColumns.length}
<div
class="blank"
class:highlighted={$hoveredRowId === BlankRowID}
style="width:{renderColumnsWidth}px"
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
on:mouseleave={() => ($hoveredRowId = null)}
on:click={() => dispatch("add-row-inline")}
/>
{/if}
</GridScrollWrapper> </GridScrollWrapper>
</div> </div>
@ -35,4 +59,15 @@
overflow: hidden; overflow: hidden;
flex: 1 1 auto; flex: 1 1 auto;
} }
.blank {
height: var(--row-height);
background: var(--cell-background);
border-bottom: var(--cell-border);
border-right: var(--cell-border);
position: absolute;
}
.blank.highlighted {
background: var(--cell-background-hover);
cursor: pointer;
}
</style> </style>

View File

@ -29,10 +29,7 @@
// Handles a wheel even and updates the scroll offsets // Handles a wheel even and updates the scroll offsets
const handleWheel = e => { const handleWheel = e => {
e.preventDefault() e.preventDefault()
const modifier = e.ctrlKey || e.metaKey debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
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 debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
const { top, left } = $scroll const { top, left } = $scroll

View File

@ -2,8 +2,17 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import HeaderCell from "../cells/HeaderCell.svelte" import HeaderCell from "../cells/HeaderCell.svelte"
import { Icon } from "@budibase/bbui"
const { renderedColumns } = getContext("grid") const { renderedColumns, dispatch, scroll, hiddenColumnsWidth, width } =
getContext("grid")
$: columnsWidth = $renderedColumns.reduce(
(total, col) => (total += col.width),
0
)
$: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left
$: left = Math.min($width - 40, end)
</script> </script>
<div class="header"> <div class="header">
@ -14,6 +23,13 @@
{/each} {/each}
</div> </div>
</GridScrollWrapper> </GridScrollWrapper>
<div
class="add"
style="left:{left}px"
on:click={() => dispatch("add-column")}
>
<Icon name="Add" />
</div>
</div> </div>
<style> <style>
@ -27,4 +43,20 @@
.row { .row {
display: flex; display: flex;
} }
.add {
height: var(--default-row-height);
display: grid;
place-items: center;
width: 40px;
position: absolute;
top: 0;
border-left: var(--cell-border);
border-right: var(--cell-border);
border-bottom: var(--cell-border);
background: var(--spectrum-global-color-gray-100);
}
.add:hover {
background: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
</style> </style>

View File

@ -0,0 +1,265 @@
<script>
import { getContext, onDestroy, onMount, tick } from "svelte"
import { Icon, Button } from "@budibase/bbui"
import GridScrollWrapper from "./GridScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte"
import { fade } from "svelte/transition"
import { GutterWidth } from "../lib/constants"
import { NewRowID } from "../lib/constants"
import GutterCell from "../cells/GutterCell.svelte"
const {
hoveredRowId,
focusedCellId,
stickyColumn,
scroll,
dispatch,
rows,
focusedCellAPI,
tableId,
subscribe,
renderedRows,
renderedColumns,
rowHeight,
hasNextPage,
maxScrollTop,
rowVerticalInversionIndex,
columnHorizontalInversionIndex,
} = getContext("grid")
let isAdding = false
let newRow = {}
let offset = 0
$: firstColumn = $stickyColumn || $renderedColumns[0]
$: width = GutterWidth + ($stickyColumn?.width || 0)
$: $tableId, (isAdding = false)
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
const shouldInvertY = (offset, inversionIndex, rows) => {
if (offset === 0) {
return false
}
return rows.length >= inversionIndex
}
const addRow = async () => {
// Blur the active cell and tick to let final value updates propagate
$focusedCellAPI?.blur()
await tick()
// Create row
const newRowIndex = offset ? undefined : 0
const savedRow = await rows.actions.addRow(newRow, newRowIndex)
if (savedRow) {
// Reset state
clear()
// Select the first cell if possible
if (firstColumn) {
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
}
}
}
const clear = () => {
isAdding = false
$focusedCellId = null
$hoveredRowId = null
document.removeEventListener("keydown", handleKeyPress)
}
const startAdding = async () => {
if (isAdding) {
return
}
// If we have a next page of data then we aren't truly at the bottom, so we
// render the add row component at the top
if ($hasNextPage) {
offset = 0
}
// If we don't have a next page then we're at the bottom and can scroll to
// the max available offset
else {
scroll.update(state => ({
...state,
top: $maxScrollTop,
}))
offset = $renderedRows.length * $rowHeight - ($maxScrollTop % $rowHeight)
if ($renderedRows.length !== 0) {
offset -= 1
}
}
// Update state and select initial cell
newRow = {}
isAdding = true
$hoveredRowId = NewRowID
if (firstColumn) {
$focusedCellId = `${NewRowID}-${firstColumn.name}`
}
// Attach key listener
document.addEventListener("keydown", handleKeyPress)
}
const updateValue = (rowId, columnName, val) => {
newRow[columnName] = val
}
const addViaModal = () => {
clear()
dispatch("add-row")
}
const handleKeyPress = e => {
if (!isAdding) {
return
}
if (e.key === "Escape") {
// Only close the new row component if we aren't actively inside a cell
if (!$focusedCellAPI?.isActive()) {
e.preventDefault()
clear()
}
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
addRow()
}
}
onMount(() => subscribe("add-row-inline", startAdding))
onDestroy(() => {
document.removeEventListener("keydown", handleKeyPress)
})
</script>
<!-- Only show new row functionality if we have any columns -->
{#if isAdding}
<div
class="container"
class:floating={offset > 0}
style="--offset:{offset}px; --sticky-width:{width}px;"
>
<div class="underlay sticky" transition:fade={{ duration: 130 }} />
<div class="underlay" transition:fade={{ duration: 130 }} />
<div class="sticky-column" transition:fade={{ duration: 130 }}>
<GutterCell on:expand={addViaModal} rowHovered>
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
</GutterCell>
{#if $stickyColumn}
{@const cellId = `${NewRowID}-${$stickyColumn.name}`}
<DataCell
{cellId}
rowFocused
column={$stickyColumn}
row={newRow}
focused={$focusedCellId === cellId}
width={$stickyColumn.width}
{updateValue}
rowIdx={0}
{invertY}
/>
{/if}
</div>
<div class="normal-columns" transition:fade={{ duration: 130 }}>
<GridScrollWrapper scrollHorizontally wheelInteractive>
<div class="row">
{#each $renderedColumns as column, columnIdx}
{@const cellId = `new-${column.name}`}
{#key cellId}
<DataCell
{cellId}
{column}
{updateValue}
rowFocused
row={newRow}
focused={$focusedCellId === cellId}
width={column.width}
rowIdx={0}
invertX={columnIdx >= $columnHorizontalInversionIndex}
{invertY}
/>
{/key}
{/each}
</div>
</GridScrollWrapper>
</div>
<div class="buttons" transition:fade={{ duration: 130 }}>
<Button size="M" cta on:click={addRow}>Save</Button>
<Button size="M" secondary newStyles on:click={clear}>Cancel</Button>
</div>
</div>
{/if}
<style>
.container {
position: absolute;
top: var(--default-row-height);
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: stretch;
}
.container :global(.cell) {
--cell-background: var(--spectrum-global-color-gray-75) !important;
}
.container.floating :global(.cell) {
height: calc(var(--row-height) + 1px);
border-top: var(--cell-border);
}
/* Underlay sits behind everything */
.underlay {
position: absolute;
content: "";
left: 0;
top: 0;
height: 100%;
width: 100%;
background: var(--cell-background);
opacity: 0.8;
}
.underlay.sticky {
z-index: 2;
width: var(--sticky-width);
}
/* Floating buttons which sit on top of the underlay but below the sticky column */
.buttons {
display: flex;
flex-direction: row;
gap: 8px;
pointer-events: all;
z-index: 3;
position: absolute;
top: calc(var(--row-height) + var(--offset) + 24px);
left: var(--gutter-width);
}
/* Sticky column styles */
.sticky-column {
display: flex;
z-index: 4;
position: relative;
align-self: flex-start;
flex: 0 0 var(--sticky-width);
}
.sticky-column :global(.cell:not(:last-child)) {
border-right: none;
}
.sticky-column,
.normal-columns {
margin-top: var(--offset);
}
/* Normal column styles */
.row {
width: 0;
display: flex;
}
</style>

View File

@ -1,247 +0,0 @@
<script>
import GridCell from "../cells/GridCell.svelte"
import { getContext, onMount } from "svelte"
import { Icon, Button } from "@budibase/bbui"
import GridScrollWrapper from "./GridScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte"
import { fly } from "svelte/transition"
import { GutterWidth } from "../lib/constants"
const {
hoveredRowId,
focusedCellId,
stickyColumn,
scroll,
config,
dispatch,
visibleColumns,
rows,
showHScrollbar,
tableId,
subscribe,
scrollLeft,
} = getContext("grid")
let isAdding = false
let newRow = {}
let touched = false
$: firstColumn = $stickyColumn || $visibleColumns[0]
$: rowHovered = $hoveredRowId === "new"
$: rowFocused = $focusedCellId?.startsWith("new-")
$: width = GutterWidth + ($stickyColumn?.width || 0)
$: $tableId, (isAdding = false)
const addRow = async () => {
// Create row
const savedRow = await rows.actions.addRow(newRow, 0)
if (savedRow) {
// Select the first cell if possible
if (firstColumn) {
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
}
// Reset state
isAdding = false
scroll.set({
left: 0,
top: 0,
})
}
}
const cancel = () => {
isAdding = false
}
const startAdding = () => {
newRow = {}
isAdding = true
if (firstColumn) {
$focusedCellId = `new-${firstColumn.name}`
}
}
const updateValue = (rowId, columnName, val) => {
touched = true
newRow[columnName] = val
}
const addViaModal = () => {
isAdding = false
dispatch("add-row")
}
onMount(() => subscribe("add-row-inline", startAdding))
</script>
<!-- Only show new row functionality if we have any columns -->
{#if isAdding}
<div class="container" transition:fly={{ y: 20, duration: 130 }}>
<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}
>
<GridCell width={GutterWidth} {rowHovered} {rowFocused}>
<div class="gutter">
<div class="number">1</div>
{#if $config.allowExpandRows}
<Icon
name="Maximize"
size="S"
hoverable
on:click={addViaModal}
/>
{/if}
</div>
</GridCell>
{#if $stickyColumn}
{@const cellId = `new-${$stickyColumn.name}`}
<DataCell
{cellId}
column={$stickyColumn}
row={newRow}
{rowHovered}
focused={$focusedCellId === cellId}
{rowFocused}
width={$stickyColumn.width}
{updateValue}
rowIdx={0}
/>
{/if}
</div>
<GridScrollWrapper scrollHorizontally wheelInteractive>
<div class="row">
{#each $visibleColumns as column}
{@const cellId = `new-${column.name}`}
{#key cellId}
<DataCell
{cellId}
{column}
row={newRow}
{rowHovered}
focused={$focusedCellId === cellId}
{rowFocused}
width={column.width}
{updateValue}
rowIdx={0}
/>
{/key}
{/each}
</div>
</GridScrollWrapper>
</div>
</div>
<div class="buttons">
<Button size="M" cta on:click={addRow}>Save</Button>
<Button size="M" secondary newStyles on:click={cancel}>Cancel</Button>
</div>
</div>
{/if}
<style>
.container {
pointer-events: none;
position: absolute;
top: var(--row-height);
left: 0;
width: 100%;
padding-bottom: 800px;
display: flex;
flex-direction: column;
align-items: stretch;
}
.container:before {
position: absolute;
content: "";
left: 0;
top: 0;
height: 100%;
width: 100%;
background: var(--cell-background);
opacity: 0.8;
z-index: -1;
}
.content {
pointer-events: all;
background: var(--background);
border-bottom: var(--cell-border);
}
.new-row {
display: flex;
bottom: 0;
left: 0;
width: 100%;
transition: margin-bottom 130ms ease-out;
}
.new-row :global(.cell) {
--cell-background: var(--background) !important;
border-bottom: none;
}
.sticky-column {
display: flex;
z-index: 1;
position: relative;
}
/* Don't show borders between cells in the sticky column */
.sticky-column :global(.cell:not(:last-child)) {
border-right: none;
}
.row {
width: 0;
display: flex;
}
/* Add shadow when scrolled */
.sticky-column.scrolled {
/*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/
}
.sticky-column.scrolled:after {
content: "";
width: 10px;
height: 100%;
background: linear-gradient(to right, rgba(0, 0, 0, 0.05), transparent);
left: 100%;
top: 0;
position: absolute;
}
/* Styles for gutter */
.gutter {
flex: 1 1 auto;
display: grid;
align-items: center;
padding: var(--cell-padding);
grid-template-columns: 1fr auto;
gap: var(--cell-spacing);
}
/* Floating buttons */
.buttons {
display: flex;
flex-direction: row;
gap: 8px;
margin: 24px 0 0 var(--gutter-width);
pointer-events: all;
align-self: flex-start;
}
.number {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--spectrum-global-color-gray-500);
}
</style>

View File

@ -1,24 +1,26 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { Checkbox, Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import GridCell from "../cells/GridCell.svelte" import GridCell from "../cells/GridCell.svelte"
import DataCell from "../cells/DataCell.svelte" import DataCell from "../cells/DataCell.svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import HeaderCell from "../cells/HeaderCell.svelte" import HeaderCell from "../cells/HeaderCell.svelte"
import { GutterWidth } from "../lib/constants" import { GutterWidth, BlankRowID } from "../lib/constants"
import GutterCell from "../cells/GutterCell.svelte"
const { const {
rows, rows,
selectedRows, selectedRows,
stickyColumn, stickyColumn,
renderedColumns,
renderedRows, renderedRows,
focusedCellId, focusedCellId,
hoveredRowId, hoveredRowId,
config, config,
selectedCellMap, selectedCellMap,
focusedRow, focusedRow,
dispatch,
scrollLeft, scrollLeft,
dispatch,
} = getContext("grid") } = getContext("grid")
$: rowCount = $rows.length $: rowCount = $rows.length
@ -37,19 +39,6 @@
$selectedRows = allRows $selectedRows = allRows
} }
} }
const selectRow = id => {
selectedRows.update(state => {
let newState = {
...state,
[id]: !state[id],
}
if (!newState[id]) {
delete newState[id]
}
return newState
})
}
</script> </script>
<div <div
@ -58,26 +47,14 @@
class:scrolled={$scrollLeft > 0} class:scrolled={$scrollLeft > 0}
> >
<div class="header row"> <div class="header row">
<GridCell width={GutterWidth} defaultHeight center> <GutterCell
<div class="gutter"> disableExpand
<div class="checkbox visible"> disableNumber
{#if $config.allowDeleteRows} on:select={selectAll}
<div on:click={selectAll}> defaultHeight
<Checkbox rowSelected={selectedRowCount && selectedRowCount === rowCount}
value={rowCount && selectedRowCount === rowCount} disabled={!$renderedRows.length}
disabled={!$renderedRows.length} />
/>
</div>
{/if}
</div>
{#if $config.allowExpandRows}
<div class="expand">
<Icon name="Maximize" size="S" />
</div>
{/if}
</div>
</GridCell>
{#if $stickyColumn} {#if $stickyColumn}
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" /> <HeaderCell column={$stickyColumn} orderable={false} idx="sticky" />
{/if} {/if}
@ -95,41 +72,7 @@
on:mouseenter={() => ($hoveredRowId = row._id)} on:mouseenter={() => ($hoveredRowId = row._id)}
on:mouseleave={() => ($hoveredRowId = null)} on:mouseleave={() => ($hoveredRowId = null)}
> >
<GridCell <GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
width={GutterWidth}
highlighted={rowFocused || rowHovered}
selected={rowSelected}
>
<div class="gutter">
<div
on:click={() => selectRow(row._id)}
class="checkbox"
class:visible={$config.allowDeleteRows &&
(rowSelected || rowHovered || rowFocused)}
>
<Checkbox value={rowSelected} />
</div>
<div
class="number"
class:visible={!$config.allowDeleteRows ||
!(rowSelected || rowHovered || rowFocused)}
>
{row.__idx + 1}
</div>
{#if $config.allowExpandRows}
<div class="expand" class:visible={rowFocused || rowHovered}>
<Icon
name="Maximize"
hoverable
size="S"
on:click={() => {
dispatch("edit-row", row)
}}
/>
</div>
{/if}
</div>
</GridCell>
{#if $stickyColumn} {#if $stickyColumn}
<DataCell <DataCell
{row} {row}
@ -146,6 +89,24 @@
{/if} {/if}
</div> </div>
{/each} {/each}
{#if $config.allowAddRows && ($renderedColumns.length || $stickyColumn)}
<div
class="row new"
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
on:mouseleave={() => ($hoveredRowId = null)}
on:click={() => dispatch("add-row-inline")}
>
<GutterCell disableExpand rowHovered={$hoveredRowId === BlankRowID}>
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
</GutterCell>
{#if $stickyColumn}
<GridCell
width={$stickyColumn.width}
highlighted={$hoveredRowId === BlankRowID}
/>
{/if}
</div>
{/if}
</GridScrollWrapper> </GridScrollWrapper>
</div> </div>
</div> </div>
@ -156,6 +117,7 @@
flex-direction: column; flex-direction: column;
position: relative; position: relative;
z-index: 2; z-index: 2;
background: var(--cell-background);
} }
/* Add right border */ /* Add right border */
@ -203,43 +165,7 @@
position: relative; position: relative;
flex: 1 1 auto; flex: 1 1 auto;
} }
.row.new :global(*:hover) {
/* Styles for gutter */ cursor: pointer;
.gutter {
flex: 1 1 auto;
display: grid;
align-items: center;
padding: var(--cell-padding);
grid-template-columns: 1fr auto;
gap: var(--cell-spacing);
}
.checkbox,
.number {
display: none;
flex-direction: row;
justify-content: center;
align-items: center;
}
.checkbox :global(.spectrum-Checkbox) {
min-height: 0;
height: 20px;
}
.checkbox :global(.spectrum-Checkbox-box) {
margin: 3px 0 0 0;
}
.number {
color: var(--spectrum-global-color-gray-500);
}
.checkbox.visible,
.number.visible {
display: flex;
}
.expand {
opacity: 0;
pointer-events: none;
}
.expand.visible {
opacity: 1;
pointer-events: all;
} }
</style> </style>

View File

@ -1,11 +1,14 @@
export const Padding = 276 export const Padding = 128
export const MaxCellRenderHeight = 252 export const MaxCellRenderHeight = 252
export const MaxCellRenderWidthOverflow = 200 export const MaxCellRenderWidthOverflow = 200
export const ScrollBarSize = 8 export const ScrollBarSize = 8
export const GutterWidth = 72 export const GutterWidth = 72
export const DefaultColumnWidth = 200 export const DefaultColumnWidth = 200
export const MinColumnWidth = 100 export const MinColumnWidth = 80
export const SmallRowHeight = 36 export const SmallRowHeight = 36
export const MediumRowHeight = 64 export const MediumRowHeight = 64
export const LargeRowHeight = 92 export const LargeRowHeight = 92
export const DefaultRowHeight = SmallRowHeight export const DefaultRowHeight = SmallRowHeight
export const NewRowID = "new"
export const BlankRowID = "blank"
export const RowPageSize = 100

View File

@ -15,7 +15,7 @@ const TypeIconMap = {
number: "123", number: "123",
boolean: "Boolean", boolean: "Boolean",
attachment: "AppleFiles", attachment: "AppleFiles",
link: "Link", link: "DataCorrelated",
formula: "Calculator", formula: "Calculator",
json: "Brackets", json: "Brackets",
} }

View File

@ -1,6 +1,7 @@
<script> <script>
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { debounce } from "../../../utils/utils" import { debounce } from "../../../utils/utils"
import { NewRowID } from "../lib/constants"
const { const {
enrichedRows, enrichedRows,
@ -10,15 +11,24 @@
stickyColumn, stickyColumn,
focusedCellAPI, focusedCellAPI,
clipboard, clipboard,
dispatch,
selectedRows,
} = getContext("grid") } = getContext("grid")
// Global key listener which intercepts all key events // Global key listener which intercepts all key events
const handleKeyDown = e => { const handleKeyDown = e => {
// If nothing selected avoid processing further key presses // If nothing selected avoid processing further key presses
if (!$focusedCellId) { if (!$focusedCellId) {
if (e.key === "Tab") { if (e.key === "Tab" || e.key?.startsWith("Arrow")) {
e.preventDefault() e.preventDefault()
focusFirstCell() focusFirstCell()
} else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
dispatch("add-row-inline")
} else if (e.key === "Delete" || e.key === "Backspace") {
if (Object.keys($selectedRows).length) {
dispatch("request-bulk-delete")
}
} }
return return
} }
@ -26,10 +36,19 @@
// Always intercept certain key presses // Always intercept certain key presses
const api = $focusedCellAPI const api = $focusedCellAPI
if (e.key === "Escape") { if (e.key === "Escape") {
api?.blur?.() // By setting a tiny timeout here we can ensure that other listeners
// which depend on being able to read cell state on an escape keypress
// get a chance to observe the true state before we blur
if (api?.isActive()) {
setTimeout(api?.blur, 10)
} else {
$focusedCellId = null
}
return
} else if (e.key === "Tab") { } else if (e.key === "Tab") {
api?.blur?.() api?.blur?.()
changeFocusedColumn(1) changeFocusedColumn(1)
return
} }
// Pass the key event to the selected cell and let it decide whether to // Pass the key event to the selected cell and let it decide whether to
@ -54,8 +73,12 @@
clipboard.actions.copy() clipboard.actions.copy()
break break
case "v": case "v":
clipboard.actions.paste() if (!api?.isReadonly()) {
clipboard.actions.paste()
}
break break
case "Enter":
dispatch("add-row-inline")
} }
} else { } else {
switch (e.key) { switch (e.key) {
@ -73,11 +96,19 @@
break break
case "Delete": case "Delete":
case "Backspace": case "Backspace":
deleteSelectedCell() if (Object.keys($selectedRows).length) {
dispatch("request-bulk-delete")
} else {
deleteSelectedCell()
}
break break
case "Enter": case "Enter":
focusCell() focusCell()
break break
case " ":
case "Space":
toggleSelectRow()
break
default: default:
startEnteringValue(e.key, e.which) startEnteringValue(e.key, e.which)
} }
@ -156,7 +187,7 @@
// Focuses the cell and starts entering a new value // Focuses the cell and starts entering a new value
const startEnteringValue = (key, keyCode) => { const startEnteringValue = (key, keyCode) => {
if ($focusedCellAPI) { if ($focusedCellAPI && !$focusedCellAPI.isReadonly()) {
const type = $focusedCellAPI.getType() const type = $focusedCellAPI.getType()
if (type === "number" && keyCodeIsNumber(keyCode)) { if (type === "number" && keyCodeIsNumber(keyCode)) {
$focusedCellAPI.setValue(parseInt(key)) $focusedCellAPI.setValue(parseInt(key))
@ -171,6 +202,17 @@
} }
} }
const toggleSelectRow = () => {
const id = $focusedRow?._id
if (!id || id === NewRowID) {
return
}
selectedRows.update(state => {
state[id] = !state[id]
return state
})
}
onMount(() => { onMount(() => {
document.addEventListener("keydown", handleKeyDown) document.addEventListener("keydown", handleKeyDown)
return () => { return () => {

View File

@ -13,6 +13,7 @@
copiedCell, copiedCell,
clipboard, clipboard,
dispatch, dispatch,
focusedCellAPI,
} = getContext("grid") } = getContext("grid")
$: style = makeStyle($menu) $: style = makeStyle($menu)
@ -49,7 +50,7 @@
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="Paste" icon="Paste"
disabled={$copiedCell == null} disabled={$copiedCell == null || $focusedCellAPI?.isReadonly()}
on:click={clipboard.actions.paste} on:click={clipboard.actions.paste}
on:click={menu.actions.close} on:click={menu.actions.close}
> >

View File

@ -1,6 +1,7 @@
import { writable, derived, get } from "svelte/store" import { writable, derived, get } from "svelte/store"
import { fetchData } from "../../../fetch/fetchData" import { fetchData } from "../../../fetch/fetchData"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { NewRowID, RowPageSize } from "../lib/constants"
const initialSortState = { const initialSortState = {
column: null, column: null,
@ -16,6 +17,7 @@ export const createStores = () => {
const sort = writable(initialSortState) const sort = writable(initialSortState)
const rowChangeCache = writable({}) const rowChangeCache = writable({})
const inProgressChanges = writable({}) const inProgressChanges = writable({})
const hasNextPage = writable(false)
// Generate a lookup map to quick find a row by ID // Generate a lookup map to quick find a row by ID
const rowLookupMap = derived( const rowLookupMap = derived(
@ -50,6 +52,7 @@ export const createStores = () => {
sort, sort,
rowChangeCache, rowChangeCache,
inProgressChanges, inProgressChanges,
hasNextPage,
} }
} }
@ -70,6 +73,7 @@ export const deriveStores = context => {
rowChangeCache, rowChangeCache,
inProgressChanges, inProgressChanges,
previousFocusedRowId, previousFocusedRowId,
hasNextPage,
} = context } = context
const instanceLoaded = writable(false) const instanceLoaded = writable(false)
const fetch = writable(null) const fetch = writable(null)
@ -114,7 +118,7 @@ export const deriveStores = context => {
filter: [], filter: [],
sortColumn: initialSortState.column, sortColumn: initialSortState.column,
sortOrder: initialSortState.order, sortOrder: initialSortState.order,
limit: 100, limit: RowPageSize,
paginate: true, paginate: true,
}, },
}) })
@ -122,6 +126,7 @@ export const deriveStores = context => {
// Subscribe to changes of this fetch model // Subscribe to changes of this fetch model
unsubscribe = newFetch.subscribe($fetch => { unsubscribe = newFetch.subscribe($fetch => {
if ($fetch.loaded && !$fetch.loading) { if ($fetch.loaded && !$fetch.loading) {
hasNextPage.set($fetch.hasNextPage)
const $instanceLoaded = get(instanceLoaded) const $instanceLoaded = get(instanceLoaded)
const resetRows = $fetch.resetKey !== lastResetKey const resetRows = $fetch.resetKey !== lastResetKey
lastResetKey = $fetch.resetKey lastResetKey = $fetch.resetKey
@ -230,7 +235,7 @@ export const deriveStores = context => {
if (bubble) { if (bubble) {
throw error throw error
} else { } else {
handleValidationError("new", error) handleValidationError(NewRowID, error)
} }
} }
} }

View File

@ -1,6 +1,6 @@
import { writable, derived, get } from "svelte/store" import { writable, derived, get } from "svelte/store"
import { tick } from "svelte" import { tick } from "svelte"
import { Padding, GutterWidth, DefaultRowHeight } from "../lib/constants" import { Padding, GutterWidth } from "../lib/constants"
export const createStores = () => { export const createStores = () => {
const scroll = writable({ const scroll = writable({
@ -29,7 +29,7 @@ export const deriveStores = context => {
// Derive vertical limits // Derive vertical limits
const contentHeight = derived( const contentHeight = derived(
[rows, rowHeight], [rows, rowHeight],
([$rows, $rowHeight]) => $rows.length * $rowHeight + Padding, ([$rows, $rowHeight]) => ($rows.length + 1) * $rowHeight + Padding,
0 0
) )
const maxScrollTop = derived( const maxScrollTop = derived(
@ -138,7 +138,7 @@ export const initialise = context => {
const $scroll = get(scroll) const $scroll = get(scroll)
const $bounds = get(bounds) const $bounds = get(bounds)
const $rowHeight = get(rowHeight) const $rowHeight = get(rowHeight)
const verticalOffset = DefaultRowHeight * 1.5 const verticalOffset = 60
// Ensure vertical position is viewable // Ensure vertical position is viewable
if ($focusedRow) { if ($focusedRow) {

View File

@ -1,8 +1,10 @@
import { writable, get, derived } from "svelte/store" import { writable, get, derived } from "svelte/store"
import { tick } from "svelte"
import { import {
DefaultRowHeight, DefaultRowHeight,
LargeRowHeight, LargeRowHeight,
MediumRowHeight, MediumRowHeight,
NewRowID,
} from "../lib/constants" } from "../lib/constants"
export const createStores = () => { export const createStores = () => {
@ -49,14 +51,14 @@ export const deriveStores = context => {
([$focusedCellId, $rowLookupMap, $enrichedRows]) => { ([$focusedCellId, $rowLookupMap, $enrichedRows]) => {
const rowId = $focusedCellId?.split("-")[0] const rowId = $focusedCellId?.split("-")[0]
if (rowId === "new") { // Edge case for new rows
// Edge case for new row if (rowId === NewRowID) {
return { _id: rowId } return { _id: NewRowID }
} else {
// All normal rows
const index = $rowLookupMap[rowId]
return $enrichedRows[index]
} }
// All normal rows
const index = $rowLookupMap[rowId]
return $enrichedRows[index]
}, },
null null
) )
@ -101,7 +103,10 @@ export const initialise = context => {
} = context } = context
// Ensure we clear invalid rows from state if they disappear // Ensure we clear invalid rows from state if they disappear
rows.subscribe(() => { rows.subscribe(async () => {
// We tick here to ensure other derived stores have properly updated.
// We depend on the row lookup map which is a derived store,
await tick()
const $focusedCellId = get(focusedCellId) const $focusedCellId = get(focusedCellId)
const $selectedRows = get(selectedRows) const $selectedRows = get(selectedRows)
const $hoveredRowId = get(hoveredRowId) const $hoveredRowId = get(hoveredRowId)
@ -140,20 +145,6 @@ export const initialise = context => {
lastFocusedRowId = id lastFocusedRowId = id
}) })
// Reset selected rows when selected cell changes
focusedCellId.subscribe(id => {
if (id) {
selectedRows.set({})
}
})
// Unset selected cell when rows are selected
selectedRows.subscribe(rows => {
if (Object.keys(rows || {}).length) {
focusedCellId.set(null)
}
})
// Remove hovered row when a cell is selected // Remove hovered row when a cell is selected
focusedCellId.subscribe(cell => { focusedCellId.subscribe(cell => {
if (cell && get(hoveredRowId)) { if (cell && get(hoveredRowId)) {

View File

@ -2,6 +2,7 @@ import { derived, get } from "svelte/store"
import { import {
MaxCellRenderHeight, MaxCellRenderHeight,
MaxCellRenderWidthOverflow, MaxCellRenderWidthOverflow,
MinColumnWidth,
ScrollBarSize, ScrollBarSize,
} from "../lib/constants" } from "../lib/constants"
@ -45,11 +46,16 @@ export const deriveStores = context => {
) )
// Derive visible columns // Derive visible columns
const scrollLeftRounded = derived(scrollLeft, $scrollLeft => {
const interval = MinColumnWidth
return Math.round($scrollLeft / interval) * interval
})
const renderedColumns = derived( const renderedColumns = derived(
[visibleColumns, scrollLeft, width], [visibleColumns, scrollLeftRounded, width],
([$visibleColumns, $scrollLeft, $width]) => { ([$visibleColumns, $scrollLeft, $width], set) => {
if (!$visibleColumns.length) { if (!$visibleColumns.length) {
return [] set([])
return
} }
let startColIdx = 0 let startColIdx = 0
let rightEdge = $visibleColumns[0].width let rightEdge = $visibleColumns[0].width
@ -69,19 +75,17 @@ export const deriveStores = context => {
leftEdge += $visibleColumns[endColIdx].width leftEdge += $visibleColumns[endColIdx].width
endColIdx++ endColIdx++
} }
const nextRenderedColumns = $visibleColumns.slice(startColIdx, endColIdx) // Render an additional column on either side to account for
// debounce column updates based on scroll position
// Cautiously shrink the number of rendered columns. const next = $visibleColumns.slice(
// This is to avoid rapidly shrinking and growing the visible column count Math.max(0, startColIdx - 1),
// which results in remounting cells endColIdx + 1
const currentCount = get(renderedColumns).length )
if (currentCount === nextRenderedColumns.length + 1) { const current = get(renderedColumns)
return $visibleColumns.slice(startColIdx, endColIdx + 1) if (JSON.stringify(next) !== JSON.stringify(current)) {
} else { set(next)
return nextRenderedColumns
} }
}, }
[]
) )
const hiddenColumnsWidth = derived( const hiddenColumnsWidth = derived(