Refactor stores to fix dependency issues, use modals for adding rows, simplify sheet
This commit is contained in:
parent
81a28eb4da
commit
fe70793e75
|
@ -4,7 +4,7 @@
|
||||||
import { getCellRenderer } from "../lib/renderers"
|
import { getCellRenderer } from "../lib/renderers"
|
||||||
import { derived, writable } from "svelte/store"
|
import { derived, writable } from "svelte/store"
|
||||||
|
|
||||||
const { rows, focusedCellId, menu, sheetAPI, config, validation } =
|
const { rows, focusedCellId, focusedCellAPI, menu, config, validation } =
|
||||||
getContext("sheet")
|
getContext("sheet")
|
||||||
|
|
||||||
export let rowSelected
|
export let rowSelected
|
||||||
|
@ -25,13 +25,6 @@
|
||||||
|
|
||||||
let api
|
let api
|
||||||
|
|
||||||
$: {
|
|
||||||
// Wipe error if row is unfocused
|
|
||||||
if (!rowFocused && $error) {
|
|
||||||
validation.actions.setError(cellId, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the error for this cell if the row is focused
|
// Get the error for this cell if the row is focused
|
||||||
$: error = getErrorStore(rowFocused, cellId)
|
$: error = getErrorStore(rowFocused, cellId)
|
||||||
|
|
||||||
|
@ -40,8 +33,8 @@
|
||||||
|
|
||||||
// Register this cell API if the row is focused
|
// Register this cell API if the row is focused
|
||||||
$: {
|
$: {
|
||||||
if (rowFocused) {
|
if (focused) {
|
||||||
sheetAPI.actions.registerCellAPI(cellId, cellAPI)
|
focusedCellAPI.set(cellAPI)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,30 +50,9 @@
|
||||||
blur: () => api?.blur(),
|
blur: () => api?.blur(),
|
||||||
onKeyDown: (...params) => api?.onKeyDown(...params),
|
onKeyDown: (...params) => api?.onKeyDown(...params),
|
||||||
isReadonly: () => readonly,
|
isReadonly: () => readonly,
|
||||||
isRequired: () => !!column.schema.constraints?.presence,
|
|
||||||
validate: value => {
|
|
||||||
// Validate the current value if no new value is provided
|
|
||||||
if (value === undefined) {
|
|
||||||
value = row[column.name]
|
|
||||||
}
|
|
||||||
let newError = null
|
|
||||||
if (cellAPI.isReadonly() && !(value == null || value === "")) {
|
|
||||||
// Ensure cell isn't readonly
|
|
||||||
newError = "Auto columns can't be edited"
|
|
||||||
} else if (cellAPI.isRequired() && (value == null || value === "")) {
|
|
||||||
// Sanity check required fields
|
|
||||||
newError = "Required field"
|
|
||||||
} else {
|
|
||||||
newError = null
|
|
||||||
}
|
|
||||||
validation.actions.setError(cellId, newError)
|
|
||||||
return newError
|
|
||||||
},
|
|
||||||
updateValue: value => {
|
updateValue: value => {
|
||||||
cellAPI.validate(value)
|
validation.actions.setError(cellId, null)
|
||||||
if (!$error) {
|
updateRow(row._id, column.name, value)
|
||||||
updateRow(row._id, column.name, value)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton } from "@budibase/bbui"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { config, dispatch } = getContext("sheet")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
icon="TableColumnAddRight"
|
||||||
|
quiet
|
||||||
|
size="M"
|
||||||
|
on:click={() => dispatch("add-column")}
|
||||||
|
disabled={!$config.allowAddColumns}
|
||||||
|
>
|
||||||
|
Create column
|
||||||
|
</ActionButton>
|
|
@ -2,15 +2,15 @@
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { ActionButton } from "@budibase/bbui"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const { dispatch, columns, stickyColumn } = getContext("sheet")
|
const { dispatch, columns, stickyColumn, config } = getContext("sheet")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="Add"
|
icon="TableRowAddBottom"
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => dispatch("add-row-inline")}
|
on:click={() => dispatch("add-row")}
|
||||||
disabled={!$columns.length && !$stickyColumn}
|
disabled={!$config.allowAddRows || (!$columns.length && !$stickyColumn)}
|
||||||
>
|
>
|
||||||
Create row
|
Create row
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -2,14 +2,8 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
|
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
|
||||||
import HeaderCell from "../cells/HeaderCell.svelte"
|
import HeaderCell from "../cells/HeaderCell.svelte"
|
||||||
import { Icon } from "@budibase/bbui"
|
|
||||||
|
|
||||||
const { renderedColumns, dispatch, config, ui } = getContext("sheet")
|
const { renderedColumns } = getContext("sheet")
|
||||||
|
|
||||||
const addColumn = () => {
|
|
||||||
ui.actions.blur()
|
|
||||||
dispatch("add-column")
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
@ -20,11 +14,6 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</SheetScrollWrapper>
|
</SheetScrollWrapper>
|
||||||
{#if $config.allowAddColumns}
|
|
||||||
<div class="new-column" on:click={addColumn}>
|
|
||||||
<Icon size="S" name="Add" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -38,20 +27,4 @@
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.new-column {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
height: var(--row-height);
|
|
||||||
background: var(--spectrum-global-color-gray-100);
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
width: 46px;
|
|
||||||
border-left: var(--cell-border);
|
|
||||||
border-bottom: var(--cell-border);
|
|
||||||
}
|
|
||||||
.new-column:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import SheetCell from "../cells/SheetCell.svelte"
|
import SheetCell from "../cells/SheetCell.svelte"
|
||||||
import { getContext, onMount, tick } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { Icon, Button } from "@budibase/bbui"
|
import { Icon, Button, clickOutside } from "@budibase/bbui"
|
||||||
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
|
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
|
||||||
import DataCell from "../cells/DataCell.svelte"
|
import DataCell from "../cells/DataCell.svelte"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
showHScrollbar,
|
showHScrollbar,
|
||||||
tableId,
|
tableId,
|
||||||
subscribe,
|
subscribe,
|
||||||
sheetAPI,
|
scrollLeft,
|
||||||
} = getContext("sheet")
|
} = getContext("sheet")
|
||||||
|
|
||||||
let isAdding = false
|
let isAdding = false
|
||||||
|
@ -30,22 +30,24 @@
|
||||||
$: rowHovered = $hoveredRowId === "new"
|
$: rowHovered = $hoveredRowId === "new"
|
||||||
$: rowFocused = $focusedCellId?.startsWith("new-")
|
$: rowFocused = $focusedCellId?.startsWith("new-")
|
||||||
$: width = gutterWidth + ($stickyColumn?.width || 0)
|
$: width = gutterWidth + ($stickyColumn?.width || 0)
|
||||||
$: scrollLeft = $scroll.left
|
|
||||||
$: $tableId, (isAdding = false)
|
$: $tableId, (isAdding = false)
|
||||||
|
|
||||||
const addRow = async () => {
|
const addRow = async () => {
|
||||||
// Create row
|
// Create row
|
||||||
const savedRow = await rows.actions.addRow(newRow, 0)
|
const savedRow = await rows.actions.addRow(newRow, 0)
|
||||||
if (savedRow && firstColumn) {
|
if (savedRow) {
|
||||||
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
|
// Select the first cell if possible
|
||||||
isAdding = false
|
if (firstColumn) {
|
||||||
}
|
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
|
||||||
|
}
|
||||||
|
|
||||||
// Reset scroll
|
// Reset state
|
||||||
scroll.set({
|
isAdding = false
|
||||||
left: 0,
|
scroll.set({
|
||||||
top: 0,
|
left: 0,
|
||||||
})
|
top: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
|
@ -151,17 +153,28 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--row-height);
|
top: var(--row-height);
|
||||||
left: 0;
|
left: 0;
|
||||||
background: linear-gradient(
|
/*background: linear-gradient(*/
|
||||||
to bottom,
|
/* to bottom,*/
|
||||||
var(--cell-background) 20%,
|
/* var(--cell-background) 20%,*/
|
||||||
transparent 100%
|
/* transparent 100%*/
|
||||||
);
|
/*);*/
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 100px;
|
padding-bottom: 800px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
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 {
|
.content {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
|
@ -227,7 +240,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin: 24px 0 0 16px;
|
margin: 24px 0 0 var(--gutter-width);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,20 +3,7 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { createEventManagers } from "../lib/events"
|
import { createEventManagers } from "../lib/events"
|
||||||
import { createAPIClient } from "../../../api"
|
import { createAPIClient } from "../../../api"
|
||||||
import { createReorderStores } from "../stores/reorder"
|
import { createStores } from "../stores"
|
||||||
import { createViewportStores } from "../stores/viewport"
|
|
||||||
import { createRowsStore } from "../stores/rows"
|
|
||||||
import { createColumnsStores } from "../stores/columns"
|
|
||||||
import { createScrollStores } from "../stores/scroll"
|
|
||||||
import { createBoundsStores } from "../stores/bounds"
|
|
||||||
import { createUIStores } from "../stores/ui"
|
|
||||||
import { createUserStores } from "../stores/users"
|
|
||||||
import { createResizeStores } from "../stores/resize"
|
|
||||||
import { createMenuStores } from "../stores/menu"
|
|
||||||
import { createMaxScrollStores } from "../stores/max-scroll"
|
|
||||||
import { createPaginationStores } from "../stores/pagination"
|
|
||||||
import { createSheetAPIStores } from "../stores/sheet-api"
|
|
||||||
import { createValidationStores } from "../stores/validation"
|
|
||||||
import DeleteButton from "../controls/DeleteButton.svelte"
|
import DeleteButton from "../controls/DeleteButton.svelte"
|
||||||
import SheetBody from "./SheetBody.svelte"
|
import SheetBody from "./SheetBody.svelte"
|
||||||
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
|
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
|
||||||
|
@ -28,8 +15,11 @@
|
||||||
import KeyboardManager from "../overlays/KeyboardManager.svelte"
|
import KeyboardManager from "../overlays/KeyboardManager.svelte"
|
||||||
import { clickOutside } from "@budibase/bbui"
|
import { clickOutside } from "@budibase/bbui"
|
||||||
import SheetControls from "./SheetControls.svelte"
|
import SheetControls from "./SheetControls.svelte"
|
||||||
import NewRowTop from "./NewRowTop.svelte"
|
|
||||||
import { MaxCellRenderHeight } from "../lib/constants"
|
import { MaxCellRenderHeight } from "../lib/constants"
|
||||||
|
import SortButton from "../controls/SortButton.svelte"
|
||||||
|
import AddColumnButton from "../controls/AddColumnButton.svelte"
|
||||||
|
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
||||||
|
import AddRowButton from "../controls/AddRowButton.svelte"
|
||||||
|
|
||||||
export let API
|
export let API
|
||||||
export let tableId
|
export let tableId
|
||||||
|
@ -56,7 +46,6 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build up spreadsheet context
|
// Build up spreadsheet context
|
||||||
// Stores are listed in order of dependency on each other
|
|
||||||
let context = {
|
let context = {
|
||||||
API: API || createAPIClient(),
|
API: API || createAPIClient(),
|
||||||
rand,
|
rand,
|
||||||
|
@ -65,20 +54,7 @@
|
||||||
tableId: tableIdStore,
|
tableId: tableIdStore,
|
||||||
}
|
}
|
||||||
context = { ...context, ...createEventManagers() }
|
context = { ...context, ...createEventManagers() }
|
||||||
context = { ...context, ...createValidationStores(context) }
|
context = { ...context, ...createStores(context) }
|
||||||
context = { ...context, ...createBoundsStores(context) }
|
|
||||||
context = { ...context, ...createScrollStores(context) }
|
|
||||||
context = { ...context, ...createRowsStore(context) }
|
|
||||||
context = { ...context, ...createColumnsStores(context) }
|
|
||||||
context = { ...context, ...createUIStores(context) }
|
|
||||||
context = { ...context, ...createSheetAPIStores(context) }
|
|
||||||
context = { ...context, ...createResizeStores(context) }
|
|
||||||
context = { ...context, ...createViewportStores(context) }
|
|
||||||
context = { ...context, ...createMaxScrollStores(context) }
|
|
||||||
context = { ...context, ...createReorderStores(context) }
|
|
||||||
context = { ...context, ...createUserStores(context) }
|
|
||||||
context = { ...context, ...createMenuStores(context) }
|
|
||||||
context = { ...context, ...createPaginationStores(context) }
|
|
||||||
|
|
||||||
// Reference some stores for local use
|
// Reference some stores for local use
|
||||||
const { isResizing, isReordering, ui, loaded, rowHeight } = context
|
const { isResizing, isReordering, ui, loaded, rowHeight } = context
|
||||||
|
@ -113,8 +89,12 @@
|
||||||
>
|
>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="controls-left">
|
<div class="controls-left">
|
||||||
|
<AddRowButton />
|
||||||
|
<AddColumnButton />
|
||||||
<SheetControls />
|
<SheetControls />
|
||||||
<slot name="controls" />
|
<slot name="controls" />
|
||||||
|
<HideColumnsButton />
|
||||||
|
<SortButton />
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-right">
|
<div class="controls-right">
|
||||||
<DeleteButton />
|
<DeleteButton />
|
||||||
|
@ -130,9 +110,6 @@
|
||||||
<SheetBody />
|
<SheetBody />
|
||||||
</div>
|
</div>
|
||||||
<div class="overlays">
|
<div class="overlays">
|
||||||
{#if $config.allowAddRows}
|
|
||||||
<NewRowTop />
|
|
||||||
{/if}
|
|
||||||
<ResizeOverlay />
|
<ResizeOverlay />
|
||||||
<ScrollOverlay />
|
<ScrollOverlay />
|
||||||
<MenuOverlay />
|
<MenuOverlay />
|
||||||
|
|
|
@ -2,10 +2,5 @@
|
||||||
import SortButton from "../controls/SortButton.svelte"
|
import SortButton from "../controls/SortButton.svelte"
|
||||||
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 AddColumnButton from "../controls/AddColumnButton.svelte"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AddRowButton />
|
|
||||||
<HideColumnsButton />
|
|
||||||
<SortButton />
|
|
||||||
<RowHeightButton />
|
|
||||||
|
|
|
@ -20,9 +20,9 @@
|
||||||
focusedRow,
|
focusedRow,
|
||||||
gutterWidth,
|
gutterWidth,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
scrollLeft,
|
||||||
} = getContext("sheet")
|
} = getContext("sheet")
|
||||||
|
|
||||||
$: scrollLeft = $scroll.left
|
|
||||||
$: rowCount = $rows.length
|
$: rowCount = $rows.length
|
||||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
||||||
$: width = gutterWidth + ($stickyColumn?.width || 0)
|
$: width = gutterWidth + ($stickyColumn?.width || 0)
|
||||||
|
@ -91,6 +91,7 @@
|
||||||
{@const rowSelected = !!$selectedRows[row._id]}
|
{@const rowSelected = !!$selectedRows[row._id]}
|
||||||
{@const rowHovered = $hoveredRowId === row._id}
|
{@const rowHovered = $hoveredRowId === row._id}
|
||||||
{@const rowFocused = $focusedRow?._id === row._id}
|
{@const rowFocused = $focusedRow?._id === row._id}
|
||||||
|
{@const cellId = `${row._id}-${$stickyColumn.name}`}
|
||||||
<div
|
<div
|
||||||
class="row"
|
class="row"
|
||||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||||
|
@ -132,9 +133,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SheetCell>
|
</SheetCell>
|
||||||
|
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
{@const cellId = `${row._id}-${$stickyColumn.name}`}
|
|
||||||
<DataCell
|
<DataCell
|
||||||
{rowSelected}
|
{rowSelected}
|
||||||
{rowHovered}
|
{rowHovered}
|
||||||
|
@ -176,7 +175,7 @@
|
||||||
/*z-index: 1;*/
|
/*z-index: 1;*/
|
||||||
}
|
}
|
||||||
.sticky-column.scrolled {
|
.sticky-column.scrolled {
|
||||||
/*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/
|
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Don't show borders between cells in the sticky column */
|
/* Don't show borders between cells in the sticky column */
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { debounce } from "../../../utils/utils"
|
import { debounce } from "../../../utils/utils"
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rows,
|
rows,
|
||||||
|
@ -9,7 +8,7 @@
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
focusedRow,
|
focusedRow,
|
||||||
stickyColumn,
|
stickyColumn,
|
||||||
selectedCellAPI,
|
focusedCellAPI,
|
||||||
} = getContext("sheet")
|
} = getContext("sheet")
|
||||||
|
|
||||||
const handleKeyDown = e => {
|
const handleKeyDown = e => {
|
||||||
|
@ -22,7 +21,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always intercept certain key presses
|
// Always intercept certain key presses
|
||||||
const api = $selectedCellAPI
|
const api = $focusedCellAPI
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
api?.blur?.()
|
api?.blur?.()
|
||||||
} else if (e.key === "Tab") {
|
} else if (e.key === "Tab") {
|
||||||
|
@ -112,17 +111,17 @@
|
||||||
|
|
||||||
// Debounce to avoid holding down delete and spamming requests
|
// Debounce to avoid holding down delete and spamming requests
|
||||||
const deleteSelectedCell = debounce(() => {
|
const deleteSelectedCell = debounce(() => {
|
||||||
if (!$focusedCellId) {
|
if ($focusedCellAPI?.isReadonly()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
$selectedCellAPI.updateValue(null)
|
$focusedCellAPI.updateValue(null)
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
const focusSelectedCell = () => {
|
const focusSelectedCell = () => {
|
||||||
if ($selectedCellAPI?.isReadonly()) {
|
if ($focusedCellAPI?.isReadonly()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
$selectedCellAPI?.focus?.()
|
$focusedCellAPI?.focus?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
stickyColumn,
|
stickyColumn,
|
||||||
isReordering,
|
isReordering,
|
||||||
gutterWidth,
|
gutterWidth,
|
||||||
|
scrollLeft,
|
||||||
} = getContext("sheet")
|
} = getContext("sheet")
|
||||||
|
|
||||||
$: scrollLeft = $scroll.left
|
|
||||||
$: cutoff = scrollLeft + gutterWidth + ($columns[0]?.width || 0)
|
$: cutoff = scrollLeft + gutterWidth + ($columns[0]?.width || 0)
|
||||||
$: offset = gutterWidth + ($stickyColumn?.width || 0)
|
$: offset = gutterWidth + ($stickyColumn?.width || 0)
|
||||||
$: column = $resize.column
|
$: column = $resize.column
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
const {
|
const {
|
||||||
scroll,
|
scroll,
|
||||||
bounds,
|
|
||||||
rowHeight,
|
rowHeight,
|
||||||
contentHeight,
|
contentHeight,
|
||||||
maxScrollTop,
|
maxScrollTop,
|
||||||
|
@ -13,6 +12,9 @@
|
||||||
screenWidth,
|
screenWidth,
|
||||||
showHScrollbar,
|
showHScrollbar,
|
||||||
showVScrollbar,
|
showVScrollbar,
|
||||||
|
scrollLeft,
|
||||||
|
scrollTop,
|
||||||
|
height,
|
||||||
} = getContext("sheet")
|
} = getContext("sheet")
|
||||||
|
|
||||||
// Bar config
|
// Bar config
|
||||||
|
@ -22,32 +24,27 @@
|
||||||
let initialMouse
|
let initialMouse
|
||||||
let initialScroll
|
let initialScroll
|
||||||
|
|
||||||
// Memoize store primitives to reduce reactive statement invalidations
|
|
||||||
$: scrollTop = $scroll.top
|
|
||||||
$: scrollLeft = $scroll.left
|
|
||||||
$: height = $bounds.height
|
|
||||||
$: width = $bounds.width
|
|
||||||
|
|
||||||
// Calculate V scrollbar size and offset
|
// Calculate V scrollbar size and offset
|
||||||
// Terminology is the same for both axes:
|
// Terminology is the same for both axes:
|
||||||
// renderX - the space available to render the bar in, edge to edge
|
// renderX - the space available to render the bar in, edge to edge
|
||||||
// availX - the space available to render the bar in, until the edge
|
// availX - the space available to render the bar in, until the edge
|
||||||
$: renderHeight = height - 2 * barOffset
|
$: renderHeight = $height - 2 * barOffset
|
||||||
$: barHeight = Math.max(50, (height / $contentHeight) * renderHeight)
|
$: barHeight = Math.max(50, ($height / $contentHeight) * renderHeight)
|
||||||
$: availHeight = renderHeight - barHeight
|
$: availHeight = renderHeight - barHeight
|
||||||
$: barTop = barOffset + $rowHeight + availHeight * (scrollTop / $maxScrollTop)
|
$: barTop =
|
||||||
|
barOffset + $rowHeight + availHeight * ($scrollTop / $maxScrollTop)
|
||||||
|
|
||||||
// Calculate H scrollbar size and offset
|
// Calculate H scrollbar size and offset
|
||||||
$: renderWidth = $screenWidth - 2 * barOffset
|
$: renderWidth = $screenWidth - 2 * barOffset
|
||||||
$: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth)
|
$: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth)
|
||||||
$: availWidth = renderWidth - barWidth
|
$: availWidth = renderWidth - barWidth
|
||||||
$: barLeft = barOffset + availWidth * (scrollLeft / $maxScrollLeft)
|
$: barLeft = barOffset + availWidth * ($scrollLeft / $maxScrollLeft)
|
||||||
|
|
||||||
// V scrollbar drag handlers
|
// V scrollbar drag handlers
|
||||||
const startVDragging = e => {
|
const startVDragging = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
initialMouse = e.clientY
|
initialMouse = e.clientY
|
||||||
initialScroll = scrollTop
|
initialScroll = $scrollTop
|
||||||
document.addEventListener("mousemove", moveVDragging)
|
document.addEventListener("mousemove", moveVDragging)
|
||||||
document.addEventListener("mouseup", stopVDragging)
|
document.addEventListener("mouseup", stopVDragging)
|
||||||
}
|
}
|
||||||
|
@ -69,7 +66,7 @@
|
||||||
const startHDragging = e => {
|
const startHDragging = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
initialMouse = e.clientX
|
initialMouse = e.clientX
|
||||||
initialScroll = scrollLeft
|
initialScroll = $scrollLeft
|
||||||
document.addEventListener("mousemove", moveHDragging)
|
document.addEventListener("mousemove", moveHDragging)
|
||||||
document.addEventListener("mouseup", stopHDragging)
|
document.addEventListener("mouseup", stopHDragging)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import { writable } from "svelte/store"
|
import { derived, writable } from "svelte/store"
|
||||||
|
|
||||||
export const createBoundsStores = () => {
|
export const createStores = () => {
|
||||||
const bounds = writable({
|
const bounds = writable({
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
})
|
})
|
||||||
return { bounds }
|
|
||||||
|
// Derive height and width as primitives to avoid wasted computation
|
||||||
|
const width = derived(bounds, $bounds => $bounds.width, 0)
|
||||||
|
const height = derived(bounds, $bounds => $bounds.height, 0)
|
||||||
|
|
||||||
|
return { bounds, height, width }
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,7 @@ import { derived, get, writable } from "svelte/store"
|
||||||
|
|
||||||
export const DefaultColumnWidth = 200
|
export const DefaultColumnWidth = 200
|
||||||
|
|
||||||
export const createColumnsStores = context => {
|
export const createStores = () => {
|
||||||
const { table, gutterWidth } = context
|
|
||||||
const columns = writable([])
|
const columns = writable([])
|
||||||
const stickyColumn = writable(null)
|
const stickyColumn = writable(null)
|
||||||
|
|
||||||
|
@ -36,6 +35,19 @@ export const createColumnsStores = context => {
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns: {
|
||||||
|
...columns,
|
||||||
|
subscribe: enrichedColumns.subscribe,
|
||||||
|
},
|
||||||
|
stickyColumn,
|
||||||
|
visibleColumns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveStores = context => {
|
||||||
|
const { table, gutterWidth, columns, stickyColumn } = context
|
||||||
|
|
||||||
// Merge new schema fields with existing schema in order to preserve widths
|
// Merge new schema fields with existing schema in order to preserve widths
|
||||||
table.subscribe($table => {
|
table.subscribe($table => {
|
||||||
const schema = $table?.schema
|
const schema = $table?.schema
|
||||||
|
@ -109,12 +121,5 @@ export const createColumnsStores = context => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return null
|
||||||
columns: {
|
|
||||||
...columns,
|
|
||||||
subscribe: enrichedColumns.subscribe,
|
|
||||||
},
|
|
||||||
stickyColumn,
|
|
||||||
visibleColumns,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import * as Bounds from "./bounds"
|
||||||
|
import * as Columns from "./columns"
|
||||||
|
import * as Menu from "./menu"
|
||||||
|
import * as Pagination from "./pagination"
|
||||||
|
import * as Reorder from "./reorder"
|
||||||
|
import * as Resize from "./resize"
|
||||||
|
import * as Rows from "./rows"
|
||||||
|
import * as Scroll from "./scroll"
|
||||||
|
import * as UI from "./ui"
|
||||||
|
import * as Users from "./users"
|
||||||
|
import * as Validation from "./validation"
|
||||||
|
import * as Viewport from "./viewport"
|
||||||
|
|
||||||
|
const DependencyOrderedStores = [
|
||||||
|
Bounds,
|
||||||
|
Scroll,
|
||||||
|
Rows,
|
||||||
|
Columns,
|
||||||
|
UI,
|
||||||
|
Validation,
|
||||||
|
Resize,
|
||||||
|
Viewport,
|
||||||
|
Reorder,
|
||||||
|
Users,
|
||||||
|
Menu,
|
||||||
|
Pagination,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const createStores = context => {
|
||||||
|
let stores = {}
|
||||||
|
|
||||||
|
// Atomic store creation
|
||||||
|
for (let store of DependencyOrderedStores) {
|
||||||
|
stores = { ...stores, ...store.createStores?.({ ...context, ...stores }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derived store creation
|
||||||
|
for (let store of DependencyOrderedStores) {
|
||||||
|
stores = { ...stores, ...store.deriveStores?.({ ...context, ...stores }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return stores
|
||||||
|
}
|
|
@ -1,182 +0,0 @@
|
||||||
import { derived, get } from "svelte/store"
|
|
||||||
import { tick } from "svelte"
|
|
||||||
|
|
||||||
export const createMaxScrollStores = context => {
|
|
||||||
const {
|
|
||||||
rows,
|
|
||||||
visibleColumns,
|
|
||||||
stickyColumn,
|
|
||||||
bounds,
|
|
||||||
rowHeight,
|
|
||||||
scroll,
|
|
||||||
focusedRow,
|
|
||||||
focusedCellId,
|
|
||||||
gutterWidth,
|
|
||||||
} = context
|
|
||||||
const padding = 264
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
const width = derived(bounds, $bounds => $bounds.width, 0)
|
|
||||||
const contentHeight = derived(
|
|
||||||
[rows, rowHeight],
|
|
||||||
([$rows, $rowHeight]) => $rows.length * $rowHeight + padding,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
const maxScrollTop = derived(
|
|
||||||
[height, contentHeight],
|
|
||||||
([$height, $contentHeight]) => Math.max($contentHeight - $height, 0),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
// Derive horizontal limits
|
|
||||||
const contentWidth = derived(
|
|
||||||
[visibleColumns, stickyColumnWidth],
|
|
||||||
([$visibleColumns, $stickyColumnWidth]) => {
|
|
||||||
let width = gutterWidth + padding + $stickyColumnWidth
|
|
||||||
$visibleColumns.forEach(col => {
|
|
||||||
width += col.width
|
|
||||||
})
|
|
||||||
return width
|
|
||||||
},
|
|
||||||
0
|
|
||||||
)
|
|
||||||
const screenWidth = derived(
|
|
||||||
[width, stickyColumnWidth],
|
|
||||||
([$width, $stickyColumnWidth]) => $width + gutterWidth + $stickyColumnWidth,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
const maxScrollLeft = derived(
|
|
||||||
[contentWidth, screenWidth],
|
|
||||||
([$contentWidth, $screenWidth]) => {
|
|
||||||
return Math.max($contentWidth - $screenWidth, 0)
|
|
||||||
},
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure scroll state never goes invalid, which can happen when changing
|
|
||||||
// rows or tables
|
|
||||||
const overscrollTop = derived(
|
|
||||||
[scrollTop, maxScrollTop],
|
|
||||||
([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
const overscrollLeft = derived(
|
|
||||||
[scrollLeft, maxScrollLeft],
|
|
||||||
([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
overscrollTop.subscribe(overscroll => {
|
|
||||||
if (overscroll) {
|
|
||||||
scroll.update(state => ({
|
|
||||||
...state,
|
|
||||||
top: get(maxScrollTop),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
overscrollLeft.subscribe(overscroll => {
|
|
||||||
if (overscroll) {
|
|
||||||
scroll.update(state => ({
|
|
||||||
...state,
|
|
||||||
left: get(maxScrollLeft),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Ensure the selected cell is visible
|
|
||||||
focusedCellId.subscribe(async $focusedCellId => {
|
|
||||||
await tick()
|
|
||||||
const $focusedRow = get(focusedRow)
|
|
||||||
const $scroll = get(scroll)
|
|
||||||
const $bounds = get(bounds)
|
|
||||||
const $rowHeight = get(rowHeight)
|
|
||||||
const verticalOffset = $rowHeight * 1.5
|
|
||||||
|
|
||||||
// Ensure vertical position is viewable
|
|
||||||
if ($focusedRow) {
|
|
||||||
// Ensure row is not below bottom of screen
|
|
||||||
const rowYPos = $focusedRow.__idx * $rowHeight
|
|
||||||
const bottomCutoff =
|
|
||||||
$scroll.top + $bounds.height - $rowHeight - verticalOffset
|
|
||||||
let delta = rowYPos - bottomCutoff
|
|
||||||
if (delta > 0) {
|
|
||||||
scroll.update(state => ({
|
|
||||||
...state,
|
|
||||||
top: state.top + delta,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure row is not above top of screen
|
|
||||||
else {
|
|
||||||
const delta = $scroll.top - rowYPos + verticalOffset
|
|
||||||
if (delta > 0) {
|
|
||||||
scroll.update(state => ({
|
|
||||||
...state,
|
|
||||||
top: Math.max(0, state.top - delta),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure horizontal position is viewable
|
|
||||||
// Check horizontal position of columns next
|
|
||||||
const $visibleColumns = get(visibleColumns)
|
|
||||||
const columnName = $focusedCellId?.split("-")[1]
|
|
||||||
const column = $visibleColumns.find(col => col.name === columnName)
|
|
||||||
const horizontalOffset = 24
|
|
||||||
if (!column) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure column is not cutoff on left edge
|
|
||||||
let delta = $scroll.left - column.left + horizontalOffset
|
|
||||||
if (delta > 0) {
|
|
||||||
scroll.update(state => ({
|
|
||||||
...state,
|
|
||||||
left: Math.max(0, state.left - delta),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure column is not cutoff on right edge
|
|
||||||
else {
|
|
||||||
const rightEdge = column.left + column.width
|
|
||||||
const rightBound = $bounds.width + $scroll.left - horizontalOffset
|
|
||||||
delta = rightEdge - rightBound
|
|
||||||
if (delta > 0) {
|
|
||||||
scroll.update(state => ({
|
|
||||||
...state,
|
|
||||||
left: state.left + delta,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +1,19 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
|
|
||||||
export const createMenuStores = context => {
|
export const createStores = () => {
|
||||||
const { bounds, focusedCellId, stickyColumn, rowHeight } = context
|
|
||||||
const menu = writable({
|
const menu = writable({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
visible: false,
|
visible: false,
|
||||||
selectedRow: null,
|
selectedRow: null,
|
||||||
})
|
})
|
||||||
|
return {
|
||||||
|
menu,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveStores = context => {
|
||||||
|
const { menu, bounds, focusedCellId, stickyColumn, rowHeight } = context
|
||||||
|
|
||||||
const open = (cellId, e) => {
|
const open = (cellId, e) => {
|
||||||
const $bounds = get(bounds)
|
const $bounds = get(bounds)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
|
|
||||||
export const createPaginationStores = context => {
|
export const deriveStores = context => {
|
||||||
const { scrolledRowCount, rows, visualRowCapacity } = context
|
const { scrolledRowCount, rows, visualRowCapacity } = context
|
||||||
|
|
||||||
// Derive how many rows we have in total
|
// Derive how many rows we have in total
|
||||||
|
|
|
@ -1,21 +1,29 @@
|
||||||
import { get, writable, derived } from "svelte/store"
|
import { get, writable, derived } from "svelte/store"
|
||||||
|
|
||||||
export const createReorderStores = context => {
|
const reorderInitialState = {
|
||||||
const { columns, scroll, bounds, stickyColumn, ui } = context
|
sourceColumn: null,
|
||||||
const reorderInitialState = {
|
targetColumn: null,
|
||||||
sourceColumn: null,
|
breakpoints: [],
|
||||||
targetColumn: null,
|
initialMouseX: null,
|
||||||
breakpoints: [],
|
scrollLeft: 0,
|
||||||
initialMouseX: null,
|
sheetLeft: 0,
|
||||||
scrollLeft: 0,
|
}
|
||||||
sheetLeft: 0,
|
|
||||||
}
|
export const createStores = () => {
|
||||||
const reorder = writable(reorderInitialState)
|
const reorder = writable(reorderInitialState)
|
||||||
const isReordering = derived(
|
const isReordering = derived(
|
||||||
reorder,
|
reorder,
|
||||||
$reorder => !!$reorder.sourceColumn,
|
$reorder => !!$reorder.sourceColumn,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
reorder,
|
||||||
|
isReordering,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveStores = context => {
|
||||||
|
const { reorder, columns, scroll, bounds, stickyColumn, ui } = context
|
||||||
|
|
||||||
// Callback when dragging on a colum header and starting reordering
|
// Callback when dragging on a colum header and starting reordering
|
||||||
const startReordering = (column, e) => {
|
const startReordering = (column, e) => {
|
||||||
|
@ -142,6 +150,5 @@ export const createReorderStores = context => {
|
||||||
moveColumnRight,
|
moveColumnRight,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
isReordering,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,26 @@ import { DefaultColumnWidth } from "./columns"
|
||||||
|
|
||||||
export const MinColumnWidth = 100
|
export const MinColumnWidth = 100
|
||||||
|
|
||||||
export const createResizeStores = context => {
|
const initialState = {
|
||||||
const { columns, stickyColumn, ui } = context
|
initialMouseX: null,
|
||||||
const initialState = {
|
initialWidth: null,
|
||||||
initialMouseX: null,
|
column: null,
|
||||||
initialWidth: null,
|
columnIdx: null,
|
||||||
column: null,
|
width: 0,
|
||||||
columnIdx: null,
|
left: 0,
|
||||||
width: 0,
|
}
|
||||||
left: 0,
|
|
||||||
}
|
export const createStores = () => {
|
||||||
const resize = writable(initialState)
|
const resize = writable(initialState)
|
||||||
const isResizing = derived(resize, $resize => $resize.column != null, false)
|
const isResizing = derived(resize, $resize => $resize.column != null, false)
|
||||||
|
return {
|
||||||
|
resize,
|
||||||
|
isResizing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveStores = context => {
|
||||||
|
const { resize, columns, stickyColumn, ui } = context
|
||||||
|
|
||||||
// Starts resizing a certain column
|
// Starts resizing a certain column
|
||||||
const startResizing = (column, e) => {
|
const startResizing = (column, e) => {
|
||||||
|
@ -105,6 +113,5 @@ export const createResizeStores = context => {
|
||||||
resetSize,
|
resetSize,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
isResizing,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,18 +2,16 @@ 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"
|
||||||
|
|
||||||
export const createRowsStore = context => {
|
const initialSortState = {
|
||||||
const { tableId, API, scroll, validation } = context
|
column: null,
|
||||||
|
order: "ascending",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createStores = () => {
|
||||||
const rows = writable([])
|
const rows = writable([])
|
||||||
const table = writable(null)
|
const table = writable(null)
|
||||||
const filter = writable([])
|
const filter = writable([])
|
||||||
const loaded = writable(false)
|
const loaded = writable(false)
|
||||||
const instanceLoaded = writable(false)
|
|
||||||
const fetch = writable(null)
|
|
||||||
const initialSortState = {
|
|
||||||
column: null,
|
|
||||||
order: "ascending",
|
|
||||||
}
|
|
||||||
const sort = writable(initialSortState)
|
const sort = writable(initialSortState)
|
||||||
|
|
||||||
// Enrich rows with an index property
|
// Enrich rows with an index property
|
||||||
|
@ -41,6 +39,37 @@ export const createRowsStore = context => {
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows: {
|
||||||
|
...rows,
|
||||||
|
subscribe: enrichedRows.subscribe,
|
||||||
|
},
|
||||||
|
rowLookupMap,
|
||||||
|
table,
|
||||||
|
filter,
|
||||||
|
loaded,
|
||||||
|
sort,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveStores = context => {
|
||||||
|
const {
|
||||||
|
rows,
|
||||||
|
rowLookupMap,
|
||||||
|
table,
|
||||||
|
filter,
|
||||||
|
loaded,
|
||||||
|
sort,
|
||||||
|
tableId,
|
||||||
|
API,
|
||||||
|
scroll,
|
||||||
|
validation,
|
||||||
|
focusedCellId,
|
||||||
|
columns,
|
||||||
|
} = context
|
||||||
|
const instanceLoaded = writable(false)
|
||||||
|
const fetch = writable(null)
|
||||||
|
|
||||||
// Local cache of row IDs to speed up checking if a row exists
|
// Local cache of row IDs to speed up checking if a row exists
|
||||||
let rowCacheMap = {}
|
let rowCacheMap = {}
|
||||||
|
|
||||||
|
@ -117,19 +146,32 @@ export const createRowsStore = context => {
|
||||||
// Gets a row by ID
|
// Gets a row by ID
|
||||||
const getRow = id => {
|
const getRow = id => {
|
||||||
const index = get(rowLookupMap)[id]
|
const index = get(rowLookupMap)[id]
|
||||||
return index >= 0 ? get(enrichedRows)[index] : null
|
return index >= 0 ? get(rows)[index] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles validation errors from the rows API and updates local validation
|
// Handles validation errors from the rows API and updates local validation
|
||||||
// state, storing error messages against relevant cells
|
// state, storing error messages against relevant cells
|
||||||
const handleValidationError = (rowId, error) => {
|
const handleValidationError = (rowId, error) => {
|
||||||
if (error?.json?.validationErrors) {
|
if (error?.json?.validationErrors) {
|
||||||
for (let column of Object.keys(error.json.validationErrors)) {
|
const keys = Object.keys(error.json.validationErrors)
|
||||||
|
const $columns = get(columns)
|
||||||
|
for (let column of keys) {
|
||||||
validation.actions.setError(
|
validation.actions.setError(
|
||||||
`${rowId}-${column}`,
|
`${rowId}-${column}`,
|
||||||
`${column} ${error.json.validationErrors[column]}`
|
`${column} ${error.json.validationErrors[column]}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Ensure the column is visible
|
||||||
|
const index = $columns.findIndex(x => x.name === column)
|
||||||
|
if (index !== -1 && !$columns[index].visible) {
|
||||||
|
columns.update(state => {
|
||||||
|
state[index].visible = true
|
||||||
|
return state.slice()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Focus the first cell with an error
|
||||||
|
focusedCellId.set(`${rowId}-${keys[0]}`)
|
||||||
} else {
|
} else {
|
||||||
notifications.error(`Error saving row: ${error?.message}`)
|
notifications.error(`Error saving row: ${error?.message}`)
|
||||||
}
|
}
|
||||||
|
@ -299,7 +341,6 @@ export const createRowsStore = context => {
|
||||||
return {
|
return {
|
||||||
rows: {
|
rows: {
|
||||||
...rows,
|
...rows,
|
||||||
subscribe: enrichedRows.subscribe,
|
|
||||||
actions: {
|
actions: {
|
||||||
addRow,
|
addRow,
|
||||||
getRow,
|
getRow,
|
||||||
|
@ -312,10 +353,5 @@ export const createRowsStore = context => {
|
||||||
refreshTableDefinition,
|
refreshTableDefinition,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rowLookupMap,
|
|
||||||
table,
|
|
||||||
sort,
|
|
||||||
filter,
|
|
||||||
loaded,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,199 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
|
import { tick } from "svelte"
|
||||||
|
|
||||||
export const createScrollStores = () => {
|
export const createStores = () => {
|
||||||
const scroll = writable({
|
const scroll = writable({
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Derive height and width as primitives to avoid wasted computation
|
||||||
|
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
|
||||||
|
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scroll,
|
scroll,
|
||||||
|
scrollTop,
|
||||||
|
scrollLeft,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveStores = context => {
|
||||||
|
const {
|
||||||
|
scroll,
|
||||||
|
rows,
|
||||||
|
visibleColumns,
|
||||||
|
stickyColumn,
|
||||||
|
bounds,
|
||||||
|
rowHeight,
|
||||||
|
focusedRow,
|
||||||
|
focusedCellId,
|
||||||
|
gutterWidth,
|
||||||
|
} = context
|
||||||
|
const padding = 264
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
const width = derived(bounds, $bounds => $bounds.width, 0)
|
||||||
|
const contentHeight = derived(
|
||||||
|
[rows, rowHeight],
|
||||||
|
([$rows, $rowHeight]) => $rows.length * $rowHeight + padding,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const maxScrollTop = derived(
|
||||||
|
[height, contentHeight],
|
||||||
|
([$height, $contentHeight]) => Math.max($contentHeight - $height, 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
// Derive horizontal limits
|
||||||
|
const contentWidth = derived(
|
||||||
|
[visibleColumns, stickyColumnWidth],
|
||||||
|
([$visibleColumns, $stickyColumnWidth]) => {
|
||||||
|
let width = gutterWidth + padding + $stickyColumnWidth
|
||||||
|
$visibleColumns.forEach(col => {
|
||||||
|
width += col.width
|
||||||
|
})
|
||||||
|
return width
|
||||||
|
},
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const screenWidth = derived(
|
||||||
|
[width, stickyColumnWidth],
|
||||||
|
([$width, $stickyColumnWidth]) => $width + gutterWidth + $stickyColumnWidth,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const maxScrollLeft = derived(
|
||||||
|
[contentWidth, screenWidth],
|
||||||
|
([$contentWidth, $screenWidth]) => {
|
||||||
|
return Math.max($contentWidth - $screenWidth, 0)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure scroll state never goes invalid, which can happen when changing
|
||||||
|
// rows or tables
|
||||||
|
const overscrollTop = derived(
|
||||||
|
[scrollTop, maxScrollTop],
|
||||||
|
([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
const overscrollLeft = derived(
|
||||||
|
[scrollLeft, maxScrollLeft],
|
||||||
|
([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
overscrollTop.subscribe(overscroll => {
|
||||||
|
if (overscroll) {
|
||||||
|
scroll.update(state => ({
|
||||||
|
...state,
|
||||||
|
top: get(maxScrollTop),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
overscrollLeft.subscribe(overscroll => {
|
||||||
|
if (overscroll) {
|
||||||
|
scroll.update(state => ({
|
||||||
|
...state,
|
||||||
|
left: get(maxScrollLeft),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure the selected cell is visible
|
||||||
|
focusedCellId.subscribe(async $focusedCellId => {
|
||||||
|
await tick()
|
||||||
|
const $focusedRow = get(focusedRow)
|
||||||
|
const $scroll = get(scroll)
|
||||||
|
const $bounds = get(bounds)
|
||||||
|
const $rowHeight = get(rowHeight)
|
||||||
|
const verticalOffset = $rowHeight * 1.5
|
||||||
|
|
||||||
|
// Ensure vertical position is viewable
|
||||||
|
if ($focusedRow) {
|
||||||
|
// Ensure row is not below bottom of screen
|
||||||
|
const rowYPos = $focusedRow.__idx * $rowHeight
|
||||||
|
const bottomCutoff =
|
||||||
|
$scroll.top + $bounds.height - $rowHeight - verticalOffset
|
||||||
|
let delta = rowYPos - bottomCutoff
|
||||||
|
if (delta > 0) {
|
||||||
|
scroll.update(state => ({
|
||||||
|
...state,
|
||||||
|
top: state.top + delta,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure row is not above top of screen
|
||||||
|
else {
|
||||||
|
const delta = $scroll.top - rowYPos + verticalOffset
|
||||||
|
if (delta > 0) {
|
||||||
|
scroll.update(state => ({
|
||||||
|
...state,
|
||||||
|
top: Math.max(0, state.top - delta),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure horizontal position is viewable
|
||||||
|
// Check horizontal position of columns next
|
||||||
|
const $visibleColumns = get(visibleColumns)
|
||||||
|
const columnName = $focusedCellId?.split("-")[1]
|
||||||
|
const column = $visibleColumns.find(col => col.name === columnName)
|
||||||
|
const horizontalOffset = 24
|
||||||
|
if (!column) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure column is not cutoff on left edge
|
||||||
|
let delta = $scroll.left - column.left + horizontalOffset
|
||||||
|
if (delta > 0) {
|
||||||
|
scroll.update(state => ({
|
||||||
|
...state,
|
||||||
|
left: Math.max(0, state.left - delta),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure column is not cutoff on right edge
|
||||||
|
else {
|
||||||
|
const rightEdge = column.left + column.width
|
||||||
|
const rightBound = $bounds.width + $scroll.left - horizontalOffset
|
||||||
|
delta = rightEdge - rightBound
|
||||||
|
if (delta > 0) {
|
||||||
|
scroll.update(state => ({
|
||||||
|
...state,
|
||||||
|
left: state.left + delta,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { derived, get, writable } from "svelte/store"
|
|
||||||
|
|
||||||
export const createSheetAPIStores = context => {
|
|
||||||
const { focusedCellId } = context
|
|
||||||
const cellAPIs = writable({})
|
|
||||||
|
|
||||||
const registerCellAPI = (cellId, api) => {
|
|
||||||
// Ignore registration if cell is not selected
|
|
||||||
const [rowId, column] = cellId.split("-")
|
|
||||||
if (rowId !== "new" && !get(focusedCellId)?.startsWith(rowId)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store API
|
|
||||||
cellAPIs.update(state => ({
|
|
||||||
...state,
|
|
||||||
[column]: api,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCellAPI = column => {
|
|
||||||
return get(cellAPIs)[column]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive the selected cell API
|
|
||||||
const selectedCellAPI = derived(
|
|
||||||
[cellAPIs, focusedCellId],
|
|
||||||
([$apis, $focusedCellId]) => {
|
|
||||||
if (!$focusedCellId) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const [, column] = $focusedCellId.split("-")
|
|
||||||
return $apis[column]
|
|
||||||
},
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
const focusedRowAPI = derived(cellAPIs, $apis => {
|
|
||||||
return {
|
|
||||||
validate: () => {
|
|
||||||
let errors = null
|
|
||||||
for (let [column, api] of Object.entries($apis || {})) {
|
|
||||||
const error = api.validate()
|
|
||||||
if (error) {
|
|
||||||
errors = {
|
|
||||||
...errors,
|
|
||||||
[column]: error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedCellAPI,
|
|
||||||
focusedRowAPI,
|
|
||||||
sheetAPI: {
|
|
||||||
actions: {
|
|
||||||
registerCellAPI,
|
|
||||||
getCellAPI,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +1,38 @@
|
||||||
import { writable, get, derived } from "svelte/store"
|
import { writable, get, derived } from "svelte/store"
|
||||||
|
|
||||||
export const createUIStores = context => {
|
export const createStores = () => {
|
||||||
const { rows, rowLookupMap } = context
|
|
||||||
const focusedCellId = writable(null)
|
const focusedCellId = writable(null)
|
||||||
|
const focusedCellAPI = writable(null)
|
||||||
const selectedRows = writable({})
|
const selectedRows = writable({})
|
||||||
const hoveredRowId = writable(null)
|
const hoveredRowId = writable(null)
|
||||||
const rowHeight = writable(36)
|
const rowHeight = writable(36)
|
||||||
|
return {
|
||||||
|
focusedCellId,
|
||||||
|
focusedCellAPI,
|
||||||
|
selectedRows,
|
||||||
|
hoveredRowId,
|
||||||
|
rowHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveStores = context => {
|
||||||
|
const { focusedCellId, selectedRows, hoveredRowId, rows, rowLookupMap } =
|
||||||
|
context
|
||||||
|
|
||||||
// Derive the row that contains the selected cell
|
// Derive the row that contains the selected cell
|
||||||
const focusedRow = derived(
|
const focusedRow = derived(
|
||||||
[focusedCellId, rowLookupMap, rows],
|
[focusedCellId, rowLookupMap, rows],
|
||||||
([$focusedCellId, $rowLookupMap, $rows]) => {
|
([$focusedCellId, $rowLookupMap, $rows]) => {
|
||||||
const rowId = $focusedCellId?.split("-")[0]
|
const rowId = $focusedCellId?.split("-")[0]
|
||||||
const index = $rowLookupMap[rowId]
|
|
||||||
return $rows[index]
|
if (rowId === "new") {
|
||||||
|
// Edge case for new row
|
||||||
|
return { _id: rowId }
|
||||||
|
} else {
|
||||||
|
// All normal rows
|
||||||
|
const index = $rowLookupMap[rowId]
|
||||||
|
return $rows[index]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
@ -80,11 +99,7 @@ export const createUIStores = context => {
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
focusedCellId,
|
|
||||||
selectedRows,
|
|
||||||
hoveredRowId,
|
|
||||||
focusedRow,
|
focusedRow,
|
||||||
rowHeight,
|
|
||||||
ui: {
|
ui: {
|
||||||
actions: {
|
actions: {
|
||||||
blur,
|
blur,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { writable, get, derived } from "svelte/store"
|
import { writable, get, derived } from "svelte/store"
|
||||||
|
|
||||||
export const createUserStores = () => {
|
export const createStores = () => {
|
||||||
const users = writable([])
|
const users = writable([])
|
||||||
const userId = writable(null)
|
const userId = writable(null)
|
||||||
|
|
||||||
|
@ -55,10 +55,22 @@ export const createUserStores = () => {
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
users: {
|
||||||
|
...users,
|
||||||
|
subscribe: enrichedUsers.subscribe,
|
||||||
|
},
|
||||||
|
userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveStores = context => {
|
||||||
|
const { users, userId } = context
|
||||||
|
|
||||||
// Generate a lookup map of cell ID to the user that has it selected, to make
|
// Generate a lookup map of cell ID to the user that has it selected, to make
|
||||||
// lookups inside sheet cells extremely fast
|
// lookups inside sheet cells extremely fast
|
||||||
const selectedCellMap = derived(
|
const selectedCellMap = derived(
|
||||||
[enrichedUsers, userId],
|
[users, userId],
|
||||||
([$enrichedUsers, $userId]) => {
|
([$enrichedUsers, $userId]) => {
|
||||||
let map = {}
|
let map = {}
|
||||||
$enrichedUsers.forEach(user => {
|
$enrichedUsers.forEach(user => {
|
||||||
|
@ -92,15 +104,12 @@ export const createUserStores = () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users: {
|
users: {
|
||||||
...enrichedUsers,
|
...users,
|
||||||
set: users.set,
|
|
||||||
update: users.update,
|
|
||||||
actions: {
|
actions: {
|
||||||
updateUser,
|
updateUser,
|
||||||
removeUser,
|
removeUser,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
selectedCellMap,
|
selectedCellMap,
|
||||||
userId,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,52 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get, derived } from "svelte/store"
|
||||||
|
|
||||||
export const createValidationStores = () => {
|
export const createStores = () => {
|
||||||
const validation = writable({})
|
const validation = writable({})
|
||||||
|
|
||||||
|
const setError = (cellId, error) => {
|
||||||
|
if (!cellId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
validation.update(state => ({
|
||||||
|
...state,
|
||||||
|
[cellId]: error,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validation: {
|
validation: {
|
||||||
subscribe: validation.subscribe,
|
...validation,
|
||||||
actions: {
|
actions: {
|
||||||
setError: (cellId, error) => {
|
setError,
|
||||||
if (!cellId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
validation.update(state => ({
|
|
||||||
...state,
|
|
||||||
[cellId]: error,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
getError: cellId => {
|
|
||||||
return get(validation)[cellId]
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const deriveStores = context => {
|
||||||
|
const { validation, focusedRow, columns, stickyColumn } = context
|
||||||
|
const focusedRowId = derived(focusedRow, $focusedRow => $focusedRow?._id)
|
||||||
|
|
||||||
|
// Store the row ID that was previously focused, so we can remove errors from
|
||||||
|
// it when we focus a new row
|
||||||
|
let previousFocusedRowId = null
|
||||||
|
focusedRowId.subscribe(id => {
|
||||||
|
// Remove validation errors from previous focused row
|
||||||
|
if (previousFocusedRowId) {
|
||||||
|
const $columns = get(columns)
|
||||||
|
const $stickyColumn = get(stickyColumn)
|
||||||
|
validation.update(state => {
|
||||||
|
$columns.forEach(column => {
|
||||||
|
state[`${previousFocusedRowId}-${column.name}`] = null
|
||||||
|
})
|
||||||
|
if ($stickyColumn) {
|
||||||
|
state[`${previousFocusedRowId}-${$stickyColumn.name}`] = null
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store row ID
|
||||||
|
previousFocusedRowId = id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { derived, get } from "svelte/store"
|
import { derived, get } from "svelte/store"
|
||||||
|
|
||||||
export const createViewportStores = context => {
|
export const deriveStores = context => {
|
||||||
const { rowHeight, visibleColumns, rows, scroll, bounds } = context
|
const {
|
||||||
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
|
rowHeight,
|
||||||
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
|
visibleColumns,
|
||||||
|
rows,
|
||||||
// Derive height and width as primitives to avoid wasted computation
|
scrollTop,
|
||||||
const width = derived(bounds, $bounds => $bounds.width, 0)
|
scrollLeft,
|
||||||
const height = derived(bounds, $bounds => $bounds.height, 0)
|
width,
|
||||||
|
height,
|
||||||
|
} = context
|
||||||
|
|
||||||
// Derive visible rows
|
// Derive visible rows
|
||||||
// Split into multiple stores containing primitives to optimise invalidation
|
// Split into multiple stores containing primitives to optimise invalidation
|
||||||
|
|
Loading…
Reference in New Issue