Add new footer for adding rows, improve store memoization, support inverting all data types
This commit is contained in:
parent
11dd5fc805
commit
fc009b722f
|
@ -8,6 +8,7 @@
|
||||||
export let onChange
|
export let onChange
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
|
export let invert = false
|
||||||
|
|
||||||
const { API } = getContext("sheet")
|
const { API } = getContext("sheet")
|
||||||
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
|
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
|
||||||
|
@ -88,7 +89,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="dropzone">
|
<div class="dropzone" class:invert>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
{value}
|
{value}
|
||||||
compact
|
compact
|
||||||
|
@ -111,6 +112,7 @@
|
||||||
gap: var(--cell-spacing);
|
gap: var(--cell-spacing);
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.attachment-cell.editable:hover {
|
.attachment-cell.editable:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -136,12 +138,16 @@
|
||||||
}
|
}
|
||||||
.dropzone {
|
.dropzone {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -1px;
|
top: 100%;
|
||||||
left: -1px;
|
left: 0;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
background: var(--cell-background);
|
background: var(--cell-background);
|
||||||
border: var(--cell-border);
|
border: var(--cell-border);
|
||||||
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
||||||
padding: var(--cell-padding);
|
padding: var(--cell-padding);
|
||||||
}
|
}
|
||||||
|
.dropzone.invert {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -15,15 +15,42 @@
|
||||||
export let column
|
export let column
|
||||||
export let row
|
export let row
|
||||||
export let cellId
|
export let cellId
|
||||||
|
export let updateRow = rows.actions.updateRow
|
||||||
|
export let showPlaceholder = false
|
||||||
|
export let invert = false
|
||||||
|
|
||||||
let api
|
let api
|
||||||
|
let error
|
||||||
|
|
||||||
|
// Build cell API
|
||||||
|
$: cellAPI = {
|
||||||
|
...api,
|
||||||
|
isReadonly: () => !!column.schema.autocolumn,
|
||||||
|
isRequired: () => !!column.schema.constraints?.presence,
|
||||||
|
updateValue: value => {
|
||||||
|
error = null
|
||||||
|
try {
|
||||||
|
if (cellAPI.isReadonly()) {
|
||||||
|
// Ensure cell isn't readonly
|
||||||
|
error = "Auto columns can't be edited"
|
||||||
|
} else if (cellAPI.isRequired() && (value == null || value === "")) {
|
||||||
|
// Sanity check required fields
|
||||||
|
error = "Required field"
|
||||||
|
} else {
|
||||||
|
updateRow(row._id, column.name, value)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update selected cell API if selected
|
||||||
$: {
|
$: {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selectedCellAPI.set({
|
selectedCellAPI.set(cellAPI)
|
||||||
...api,
|
} else {
|
||||||
isReadonly: () => !!column.schema.autocolumn,
|
error = null
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -36,17 +63,32 @@
|
||||||
{selectedUser}
|
{selectedUser}
|
||||||
{reorderSource}
|
{reorderSource}
|
||||||
{reorderTarget}
|
{reorderTarget}
|
||||||
|
{error}
|
||||||
on:click={() => selectedCellId.set(cellId)}
|
on:click={() => selectedCellId.set(cellId)}
|
||||||
on:contextmenu={e => menu.actions.open(cellId, e)}
|
on:contextmenu={e => menu.actions.open(cellId, e)}
|
||||||
width={column.width}
|
width={column.width}
|
||||||
>
|
>
|
||||||
<svelte:component
|
{#if !selected && showPlaceholder && (row[column.name] == null || row[column.name] === "")}
|
||||||
this={getCellRenderer(column)}
|
<div class="placeholder">
|
||||||
bind:api
|
{column.name}
|
||||||
value={row[column.name]}
|
</div>
|
||||||
schema={column.schema}
|
{:else}
|
||||||
{selected}
|
<svelte:component
|
||||||
onChange={val => rows.actions.updateRow(row._id, column.name, val)}
|
this={getCellRenderer(column)}
|
||||||
readonly={column.schema.autocolumn}
|
bind:api
|
||||||
/>
|
value={row[column.name]}
|
||||||
|
schema={column.schema}
|
||||||
|
{selected}
|
||||||
|
onChange={cellAPI.updateValue}
|
||||||
|
readonly={column.schema.autocolumn}
|
||||||
|
{invert}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</SheetCell>
|
</SheetCell>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
padding: var(--cell-padding);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
export let onChange
|
export let onChange
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
|
export let invert = false
|
||||||
|
|
||||||
let textarea
|
let textarea
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
@ -48,6 +49,7 @@
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<textarea
|
<textarea
|
||||||
|
class:invert
|
||||||
bind:this={textarea}
|
bind:this={textarea}
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
|
@ -92,7 +94,10 @@
|
||||||
width: calc(100% + 100px);
|
width: calc(100% + 100px);
|
||||||
height: calc(5 * var(--cell-height) + 1px);
|
height: calc(5 * var(--cell-height) + 1px);
|
||||||
border: var(--cell-border);
|
border: var(--cell-border);
|
||||||
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400);
|
||||||
|
}
|
||||||
|
textarea.invert {
|
||||||
|
transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let multi = false
|
export let multi = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
|
export let invert = false
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
let focusedOptionIdx = null
|
let focusedOptionIdx = null
|
||||||
|
@ -98,7 +99,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="options" on:wheel={e => e.stopPropagation()}>
|
<div class="options" class:invert on:wheel={e => e.stopPropagation()}>
|
||||||
{#each values as val, idx}
|
{#each values as val, idx}
|
||||||
{@const color = getOptionColor(val)}
|
{@const color = getOptionColor(val)}
|
||||||
<div
|
<div
|
||||||
|
@ -183,10 +184,10 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.options {
|
.options {
|
||||||
min-width: calc(100% + 2px);
|
min-width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -1px;
|
top: 100%;
|
||||||
left: -1px;
|
left: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
||||||
|
@ -196,6 +197,10 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: var(--cell-border);
|
border: var(--cell-border);
|
||||||
}
|
}
|
||||||
|
.options.invert {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
.option {
|
.option {
|
||||||
flex: 0 0 var(--cell-height);
|
flex: 0 0 var(--cell-height);
|
||||||
padding: 0 var(--cell-padding);
|
padding: 0 var(--cell-padding);
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let selected
|
export let selected
|
||||||
export let schema
|
export let schema
|
||||||
export let onChange
|
export let onChange
|
||||||
|
export let invert = false
|
||||||
|
|
||||||
const { API } = getContext("sheet")
|
const { API } = getContext("sheet")
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
let primaryDisplay
|
let primaryDisplay
|
||||||
let candidateIndex
|
let candidateIndex
|
||||||
let lastSearchId
|
let lastSearchId
|
||||||
|
let results
|
||||||
|
|
||||||
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
||||||
$: editable = selected && !readonly
|
$: editable = selected && !readonly
|
||||||
|
@ -117,6 +119,9 @@
|
||||||
const open = async () => {
|
const open = async () => {
|
||||||
isOpen = true
|
isOpen = true
|
||||||
|
|
||||||
|
// Ensure results are properly reset
|
||||||
|
results = sortRows(value)
|
||||||
|
|
||||||
// Fetch definition if required
|
// Fetch definition if required
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
definition = await API.fetchTableDefinition(schema.tableId)
|
definition = await API.fetchTableDefinition(schema.tableId)
|
||||||
|
@ -214,7 +219,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="dropdown" on:wheel|stopPropagation>
|
<div class="dropdown" class:invert on:wheel|stopPropagation>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<Input autofocus quiet type="text" bind:value={searchString} />
|
<Input autofocus quiet type="text" bind:value={searchString} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -284,9 +289,9 @@
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -1px;
|
top: 100%;
|
||||||
left: -1px;
|
left: 0;
|
||||||
min-width: calc(100% + 2px);
|
min-width: 100%;
|
||||||
max-width: calc(100% + 240px);
|
max-width: calc(100% + 240px);
|
||||||
max-height: calc(var(--cell-height) + 240px);
|
max-height: calc(var(--cell-height) + 240px);
|
||||||
background: var(--cell-background);
|
background: var(--cell-background);
|
||||||
|
@ -297,6 +302,10 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
background-color: var(--cell-background-hover);
|
background-color: var(--cell-background-hover);
|
||||||
}
|
}
|
||||||
|
.dropdown.invert {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.results {
|
.results {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
export let center = false
|
export let center = false
|
||||||
export let selectedUser = null
|
export let selectedUser = null
|
||||||
export let rowIdx
|
export let rowIdx
|
||||||
|
export let error = null
|
||||||
|
|
||||||
$: style = getStyle(width, selectedUser)
|
$: style = getStyle(width, selectedUser)
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
class:reorder-source={reorderSource}
|
class:reorder-source={reorderSource}
|
||||||
class:reorder-target={reorderTarget}
|
class:reorder-target={reorderTarget}
|
||||||
class:center
|
class:center
|
||||||
|
class:error={error && selected}
|
||||||
on:focus
|
on:focus
|
||||||
on:mousedown
|
on:mousedown
|
||||||
on:mouseup
|
on:mouseup
|
||||||
|
@ -38,8 +40,12 @@
|
||||||
data-row={rowIdx}
|
data-row={rowIdx}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
{#if selectedUser && !selected}
|
{#if selected && error}
|
||||||
<div class="user">
|
<div class="label">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
{:else if selectedUser && !selected}
|
||||||
|
<div class="label">
|
||||||
{selectedUser.label}
|
{selectedUser.label}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -66,6 +72,9 @@
|
||||||
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400);
|
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
.cell.error {
|
||||||
|
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-red-400);
|
||||||
|
}
|
||||||
.cell.selected-other:not(.selected) {
|
.cell.selected-other:not(.selected) {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
box-shadow: inset 0 0 0 2px var(--user-color);
|
box-shadow: inset 0 0 0 2px var(--user-color);
|
||||||
|
@ -100,7 +109,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Other user email */
|
/* Other user email */
|
||||||
.user {
|
.label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
padding: 1px 4px;
|
padding: 1px 4px;
|
||||||
|
@ -114,14 +123,19 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.cell[data-row="0"] .user {
|
.cell[data-row="0"] .label {
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
border-radius: 0 0 2px 2px;
|
border-radius: 0 0 2px 2px;
|
||||||
padding: 0 4px 2px 4px;
|
padding: 0 4px 2px 4px;
|
||||||
}
|
}
|
||||||
.cell:hover .user {
|
.cell:hover .label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.error .label {
|
||||||
|
background: var(--spectrum-global-color-red-400);
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -31,7 +31,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if selectedRowCount}
|
{#if selectedRowCount}
|
||||||
<div class="delete-button" on:mousedown|stopPropagation={modal.show}>
|
<div
|
||||||
|
class="delete-button"
|
||||||
|
on:click|stopPropagation
|
||||||
|
on:mousedown|stopPropagation={modal.show}
|
||||||
|
>
|
||||||
<ActionButton icon="Delete" size="S">
|
<ActionButton icon="Delete" size="S">
|
||||||
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
|
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -1,141 +1,213 @@
|
||||||
<script>
|
<script>
|
||||||
import SheetCell from "../cells/SheetCell.svelte"
|
import SheetCell from "../cells/SheetCell.svelte"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon, Button } from "@budibase/bbui"
|
||||||
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
|
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
|
||||||
import { getCellRenderer } from "../lib/renderers"
|
import DataCell from "../cells/DataCell.svelte"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
renderedColumns,
|
renderedColumns,
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
rows,
|
|
||||||
selectedCellId,
|
selectedCellId,
|
||||||
reorder,
|
|
||||||
stickyColumn,
|
stickyColumn,
|
||||||
gutterWidth,
|
gutterWidth,
|
||||||
|
scroll,
|
||||||
|
config,
|
||||||
|
dispatch,
|
||||||
|
visibleColumns,
|
||||||
|
rows,
|
||||||
|
wheel,
|
||||||
|
showHScrollbar,
|
||||||
|
tableId,
|
||||||
} = getContext("sheet")
|
} = getContext("sheet")
|
||||||
|
|
||||||
let isAdding = false
|
let isAdding = false
|
||||||
let newRow = {}
|
let newRow = {}
|
||||||
|
let touched = false
|
||||||
|
|
||||||
|
$: firstColumn = $stickyColumn || $visibleColumns[0]
|
||||||
$: rowHovered = $hoveredRowId === "new"
|
$: rowHovered = $hoveredRowId === "new"
|
||||||
|
$: containsSelectedCell = $selectedCellId?.startsWith("new-")
|
||||||
$: width = gutterWidth + ($stickyColumn?.width || 0)
|
$: width = gutterWidth + ($stickyColumn?.width || 0)
|
||||||
|
$: scrollLeft = $scroll.left
|
||||||
|
$: $tableId, (isAdding = false)
|
||||||
|
|
||||||
const addRow = async field => {
|
const addRow = async () => {
|
||||||
// const newRow = await rows.actions.addRow()
|
const savedRow = await rows.actions.addRow(newRow)
|
||||||
// if (newRow) {
|
if (savedRow && firstColumn) {
|
||||||
// $selectedCellId = `${newRow._id}-${field.name}`
|
$selectedCellId = `${newRow._id}-${firstColumn.name}`
|
||||||
// }
|
}
|
||||||
|
isAdding = false
|
||||||
}
|
}
|
||||||
|
|
||||||
$: console.log(newRow)
|
const cancel = () => {
|
||||||
|
isAdding = false
|
||||||
|
}
|
||||||
|
|
||||||
const startAdding = () => {
|
const startAdding = () => {
|
||||||
if (isAdding) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
newRow = {}
|
newRow = {}
|
||||||
isAdding = true
|
isAdding = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateRow = (rowId, columnName, val) => {
|
||||||
|
touched = true
|
||||||
|
newRow[columnName] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const addViaModal = () => {
|
||||||
|
isAdding = false
|
||||||
|
dispatch("add-row")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="new" on:click={startAdding}>
|
<!-- Only show new row functionality if we have any columns -->
|
||||||
|
{#if firstColumn}
|
||||||
{#if !isAdding}
|
{#if !isAdding}
|
||||||
<div class="add">
|
<div class="add-button" class:above-scrollbar={$showHScrollbar}>
|
||||||
<div class="icon">
|
<Button size="M" cta icon="Add" on:click={startAdding}>Add row</Button>
|
||||||
<Icon name="Add" size="S" />
|
|
||||||
</div>
|
|
||||||
<div class="text">Add row</div>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
|
||||||
<div class="sticky" style="flex: 0 0 {width}px">
|
|
||||||
<SheetCell width={gutterWidth} center>
|
|
||||||
<Icon name="Add" size="S" />
|
|
||||||
</SheetCell>
|
|
||||||
{#if $stickyColumn}
|
|
||||||
{@const cellId = `new-${$stickyColumn.name}`}
|
|
||||||
<SheetCell
|
|
||||||
width={$stickyColumn.width}
|
|
||||||
selected={$selectedCellId === cellId}
|
|
||||||
on:click={() => ($selectedCellId = cellId)}
|
|
||||||
>
|
|
||||||
<svelte:component
|
|
||||||
this={getCellRenderer($stickyColumn)}
|
|
||||||
value={newRow[$stickyColumn.name]}
|
|
||||||
schema={$stickyColumn.schema}
|
|
||||||
selected={$selectedCellId === cellId}
|
|
||||||
onChange={val => (newRow[$stickyColumn.name] = val)}
|
|
||||||
readonly={$stickyColumn.schema.autocolumn}
|
|
||||||
/>
|
|
||||||
</SheetCell>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<SheetScrollWrapper scrollVertically={false}>
|
|
||||||
<div class="row">
|
|
||||||
{#each $renderedColumns as column}
|
|
||||||
{@const cellId = `new-${column.name}`}
|
|
||||||
<SheetCell
|
|
||||||
width={column.width}
|
|
||||||
selected={$selectedCellId === cellId}
|
|
||||||
on:click={() => ($selectedCellId = cellId)}
|
|
||||||
>
|
|
||||||
<svelte:component
|
|
||||||
this={getCellRenderer(column)}
|
|
||||||
value={newRow[column.name]}
|
|
||||||
schema={column.schema}
|
|
||||||
selected={$selectedCellId === cellId}
|
|
||||||
onChange={val => (newRow[column.name] = val)}
|
|
||||||
readonly={column.schema.autocolumn}
|
|
||||||
/>
|
|
||||||
</SheetCell>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</SheetScrollWrapper>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
<div
|
||||||
|
class="container"
|
||||||
|
class:visible={isAdding}
|
||||||
|
on:wheel={wheel.actions.handleWheel}
|
||||||
|
>
|
||||||
|
<div class="buttons">
|
||||||
|
<Button size="M" cta on:click={addRow}>Save</Button>
|
||||||
|
<Button size="M" secondary newStyles on:click={cancel}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
<div class="content" class:above-scrollbar={$showHScrollbar}>
|
||||||
|
<div
|
||||||
|
class="new-row"
|
||||||
|
on:mouseenter={() => ($hoveredRowId = "new")}
|
||||||
|
on:mouseleave={() => ($hoveredRowId = null)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="sticky-column"
|
||||||
|
style="flex: 0 0 {width}px"
|
||||||
|
class:scrolled={scrollLeft > 0}
|
||||||
|
>
|
||||||
|
<SheetCell
|
||||||
|
width={gutterWidth}
|
||||||
|
{rowHovered}
|
||||||
|
rowSelected={containsSelectedCell}
|
||||||
|
>
|
||||||
|
<div class="gutter">
|
||||||
|
{#if $config.allowExpandRows}
|
||||||
|
<Icon
|
||||||
|
name="Maximize"
|
||||||
|
size="S"
|
||||||
|
hoverable
|
||||||
|
on:click={addViaModal}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</SheetCell>
|
||||||
|
{#if $stickyColumn}
|
||||||
|
{@const cellId = `new-${$stickyColumn.name}`}
|
||||||
|
<DataCell
|
||||||
|
{cellId}
|
||||||
|
column={$stickyColumn}
|
||||||
|
row={newRow}
|
||||||
|
{rowHovered}
|
||||||
|
selected={$selectedCellId === cellId}
|
||||||
|
rowSelected={containsSelectedCell}
|
||||||
|
width={$stickyColumn.width}
|
||||||
|
{updateRow}
|
||||||
|
showPlaceholder
|
||||||
|
invert
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<SheetScrollWrapper scrollVertically={false}>
|
||||||
|
<div class="row">
|
||||||
|
{#each $renderedColumns as column}
|
||||||
|
{@const cellId = `new-${column.name}`}
|
||||||
|
<DataCell
|
||||||
|
{cellId}
|
||||||
|
{column}
|
||||||
|
row={newRow}
|
||||||
|
{rowHovered}
|
||||||
|
selected={$selectedCellId === cellId}
|
||||||
|
rowSelected={containsSelectedCell}
|
||||||
|
width={column.width}
|
||||||
|
{updateRow}
|
||||||
|
showPlaceholder
|
||||||
|
invert
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</SheetScrollWrapper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.new {
|
.add-button {
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
bottom: 16px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.add-button.above-scrollbar {
|
||||||
|
bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
z-index: 1;
|
||||||
|
transition: transform 130ms ease-out;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent,
|
||||||
|
var(--cell-background) 80%
|
||||||
|
);
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 64px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.container.visible {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
pointer-events: all;
|
||||||
|
background: var(--background);
|
||||||
|
border-top: var(--cell-border);
|
||||||
|
}
|
||||||
|
.content.above-scrollbar {
|
||||||
|
padding: 0 0 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-row {
|
||||||
|
display: flex;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(16px + var(--cell-height));
|
transition: margin-bottom 130ms ease-out;
|
||||||
padding-bottom: 16px;
|
margin-top: -1px;
|
||||||
z-index: 1;
|
}
|
||||||
background: var(--cell-background);
|
.new-row.visible {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.new-row :global(.cell) {
|
||||||
|
/*border-bottom: 0;*/
|
||||||
|
--cell-background: var(--background) !important;
|
||||||
border-top: var(--cell-border);
|
border-top: var(--cell-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add {
|
.sticky-column {
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.add:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background: var(--cell-background-hover);
|
|
||||||
}
|
|
||||||
.add .icon {
|
|
||||||
flex: 0 0 var(--gutter-width);
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
.add .text {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.new :global(.cell) {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticky {
|
|
||||||
z-index: 2;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
/* Don't show borders between cells in the sticky column */
|
/* Don't show borders between cells in the sticky column */
|
||||||
.sticky :global(.cell:not(:last-child)) {
|
.sticky-column :global(.cell:not(:last-child)) {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,4 +215,34 @@
|
||||||
width: 0;
|
width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add shadow when scrolled */
|
||||||
|
.sticky.scrolled :global(.cell:last-child:after) {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 100%;
|
||||||
|
left: 100%;
|
||||||
|
background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles for gutter */
|
||||||
|
.gutter {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--cell-padding);
|
||||||
|
grid-template-columns: 1fr 16px;
|
||||||
|
gap: var(--cell-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating buttons */
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0 0 16px 16px;
|
||||||
|
pointer-events: all;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import { createMenuStores } from "../stores/menu"
|
import { createMenuStores } from "../stores/menu"
|
||||||
import { createMaxScrollStores } from "../stores/max-scroll"
|
import { createMaxScrollStores } from "../stores/max-scroll"
|
||||||
import { createPaginationStores } from "../stores/pagination"
|
import { createPaginationStores } from "../stores/pagination"
|
||||||
|
import { createWheelStores } from "../stores/wheel"
|
||||||
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"
|
||||||
|
@ -25,8 +26,8 @@
|
||||||
import UserAvatars from "./UserAvatars.svelte"
|
import UserAvatars from "./UserAvatars.svelte"
|
||||||
import KeyboardManager from "../overlays/KeyboardManager.svelte"
|
import KeyboardManager from "../overlays/KeyboardManager.svelte"
|
||||||
import { clickOutside } from "@budibase/bbui"
|
import { clickOutside } from "@budibase/bbui"
|
||||||
import AddRowButton from "../controls/AddRowButton.svelte"
|
|
||||||
import SheetControls from "./SheetControls.svelte"
|
import SheetControls from "./SheetControls.svelte"
|
||||||
|
import NewRow from "./NewRow.svelte"
|
||||||
|
|
||||||
export let API
|
export let API
|
||||||
export let tableId
|
export let tableId
|
||||||
|
@ -74,6 +75,7 @@
|
||||||
context = { ...context, ...createUserStores(context) }
|
context = { ...context, ...createUserStores(context) }
|
||||||
context = { ...context, ...createMenuStores(context) }
|
context = { ...context, ...createMenuStores(context) }
|
||||||
context = { ...context, ...createPaginationStores(context) }
|
context = { ...context, ...createPaginationStores(context) }
|
||||||
|
context = { ...context, ...createWheelStores(context) }
|
||||||
|
|
||||||
// Reference some stores for local use
|
// Reference some stores for local use
|
||||||
const { isResizing, isReordering, ui, loaded } = context
|
const { isResizing, isReordering, ui, loaded } = context
|
||||||
|
@ -116,18 +118,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $loaded}
|
{#if $loaded}
|
||||||
<div class="sheet-data" use:clickOutside={ui.actions.blur}>
|
<div class="sheet-data-outer" use:clickOutside={ui.actions.blur}>
|
||||||
<StickyColumn />
|
<div class="sheet-data-inner">
|
||||||
<div class="sheet-main">
|
<StickyColumn />
|
||||||
<HeaderRow />
|
<div class="sheet-data-content">
|
||||||
<SheetBody />
|
<HeaderRow />
|
||||||
|
<SheetBody />
|
||||||
|
</div>
|
||||||
|
{#if $config.allowAddRows}
|
||||||
|
<NewRow />
|
||||||
|
{/if}
|
||||||
|
<ResizeOverlay />
|
||||||
|
<ScrollOverlay />
|
||||||
|
<MenuOverlay />
|
||||||
</div>
|
</div>
|
||||||
<ResizeOverlay />
|
|
||||||
<ScrollOverlay />
|
|
||||||
<MenuOverlay />
|
|
||||||
{#if $config.allowAddRows}
|
|
||||||
<AddRowButton />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<KeyboardManager />
|
<KeyboardManager />
|
||||||
|
@ -163,18 +167,25 @@
|
||||||
cursor: grabbing !important;
|
cursor: grabbing !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sheet-data {
|
.sheet-data-outer,
|
||||||
|
.sheet-data-inner {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
justify-items: flex-start;
|
justify-items: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 0;
|
|
||||||
position: relative;
|
|
||||||
background: var(--spectrum-global-color-gray-75);
|
|
||||||
}
|
}
|
||||||
.sheet-main {
|
.sheet-data-outer {
|
||||||
|
height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
/*background: var(--spectrum-global-color-gray-75);*/
|
||||||
|
background: var(--cell-background);
|
||||||
|
}
|
||||||
|
.sheet-data-inner {
|
||||||
|
flex-direction: row;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.sheet-data-content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { domDebounce } from "../../../utils/utils"
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
cellHeight,
|
cellHeight,
|
||||||
scroll,
|
scroll,
|
||||||
bounds,
|
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
renderedRows,
|
|
||||||
renderedColumns,
|
renderedColumns,
|
||||||
hoveredRowId,
|
|
||||||
maxScrollTop,
|
|
||||||
maxScrollLeft,
|
|
||||||
selectedCellId,
|
selectedCellId,
|
||||||
|
wheel,
|
||||||
} = getContext("sheet")
|
} = getContext("sheet")
|
||||||
|
|
||||||
export let scrollVertically = true
|
export let scrollVertically = true
|
||||||
|
@ -20,8 +15,6 @@
|
||||||
export let wheelInteractive = true
|
export let wheelInteractive = true
|
||||||
|
|
||||||
$: hiddenWidths = calculateHiddenWidths($renderedColumns)
|
$: hiddenWidths = calculateHiddenWidths($renderedColumns)
|
||||||
$: scrollLeft = $scroll.left
|
|
||||||
$: scrollTop = $scroll.top
|
|
||||||
$: style = generateStyle($scroll, hiddenWidths)
|
$: style = generateStyle($scroll, hiddenWidths)
|
||||||
|
|
||||||
const generateStyle = (scroll, hiddenWidths) => {
|
const generateStyle = (scroll, hiddenWidths) => {
|
||||||
|
@ -43,40 +36,11 @@
|
||||||
}
|
}
|
||||||
return width
|
return width
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles a wheel even and updates the scroll offsets
|
|
||||||
const handleWheel = e => {
|
|
||||||
e.preventDefault()
|
|
||||||
const modifier = e.ctrlKey || e.metaKey
|
|
||||||
let x = modifier ? e.deltaY : e.deltaX
|
|
||||||
let y = modifier ? e.deltaX : e.deltaY
|
|
||||||
debouncedHandleWheel(x, y, e.clientY)
|
|
||||||
}
|
|
||||||
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
|
||||||
// Calculate new scroll top
|
|
||||||
let newScrollTop = scrollTop + deltaY
|
|
||||||
newScrollTop = Math.max(0, Math.min(newScrollTop, $maxScrollTop))
|
|
||||||
|
|
||||||
// Calculate new scroll left
|
|
||||||
let newScrollLeft = scrollLeft + deltaX
|
|
||||||
newScrollLeft = Math.max(0, Math.min(newScrollLeft, $maxScrollLeft))
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
scroll.set({
|
|
||||||
left: newScrollLeft,
|
|
||||||
top: newScrollTop,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Hover row under cursor
|
|
||||||
const y = clientY - $bounds.top + (newScrollTop % cellHeight)
|
|
||||||
const hoveredRow = $renderedRows[Math.floor(y / cellHeight)]
|
|
||||||
$hoveredRowId = hoveredRow?._id
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="outer"
|
class="outer"
|
||||||
on:wheel={wheelInteractive ? handleWheel : null}
|
on:wheel={wheelInteractive ? wheel.actions.handleWheel : null}
|
||||||
on:click|self={() => ($selectedCellId = null)}
|
on:click|self={() => ($selectedCellId = null)}
|
||||||
>
|
>
|
||||||
<div {style}>
|
<div {style}>
|
||||||
|
|
|
@ -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 { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rows,
|
rows,
|
||||||
|
@ -114,11 +115,7 @@
|
||||||
if (!$selectedCellId) {
|
if (!$selectedCellId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ($selectedCellAPI?.isReadonly()) {
|
$selectedCellAPI.updateValue(null)
|
||||||
return
|
|
||||||
}
|
|
||||||
const [rowId, column] = $selectedCellId.split("-")
|
|
||||||
rows.actions.updateRow(rowId, column, null)
|
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
const focusSelectedCell = () => {
|
const focusSelectedCell = () => {
|
||||||
|
|
|
@ -6,16 +6,17 @@
|
||||||
scroll,
|
scroll,
|
||||||
bounds,
|
bounds,
|
||||||
cellHeight,
|
cellHeight,
|
||||||
stickyColumn,
|
|
||||||
contentHeight,
|
contentHeight,
|
||||||
maxScrollTop,
|
maxScrollTop,
|
||||||
contentWidth,
|
contentWidth,
|
||||||
maxScrollLeft,
|
maxScrollLeft,
|
||||||
gutterWidth,
|
screenWidth,
|
||||||
|
showHScrollbar,
|
||||||
|
showVScrollbar,
|
||||||
} = getContext("sheet")
|
} = getContext("sheet")
|
||||||
|
|
||||||
// Bar config
|
// Bar config
|
||||||
const barOffset = 4
|
const barOffset = 8
|
||||||
|
|
||||||
// State for dragging bars
|
// State for dragging bars
|
||||||
let initialMouse
|
let initialMouse
|
||||||
|
@ -37,16 +38,11 @@
|
||||||
$: barTop = barOffset + cellHeight + availHeight * (scrollTop / $maxScrollTop)
|
$: barTop = barOffset + cellHeight + availHeight * (scrollTop / $maxScrollTop)
|
||||||
|
|
||||||
// Calculate H scrollbar size and offset
|
// Calculate H scrollbar size and offset
|
||||||
$: totalWidth = width + gutterWidth + ($stickyColumn?.width || 0)
|
$: renderWidth = $screenWidth - 2 * barOffset
|
||||||
$: renderWidth = totalWidth - 2 * barOffset
|
$: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth)
|
||||||
$: barWidth = Math.max(50, (totalWidth / $contentWidth) * renderWidth)
|
|
||||||
$: availWidth = renderWidth - barWidth
|
$: availWidth = renderWidth - barWidth
|
||||||
$: barLeft = barOffset + availWidth * (scrollLeft / $maxScrollLeft)
|
$: barLeft = barOffset + availWidth * (scrollLeft / $maxScrollLeft)
|
||||||
|
|
||||||
// Calculate whether to show scrollbars or not
|
|
||||||
$: showVScrollbar = $contentHeight > height
|
|
||||||
$: showHScrollbar = $contentWidth > totalWidth
|
|
||||||
|
|
||||||
// V scrollbar drag handlers
|
// V scrollbar drag handlers
|
||||||
const startVDragging = e => {
|
const startVDragging = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -92,17 +88,17 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showVScrollbar}
|
{#if $showVScrollbar}
|
||||||
<div
|
<div
|
||||||
class="v-scrollbar"
|
class="v-scrollbar"
|
||||||
style="top:{barTop}px; height:{barHeight}px;"
|
style="top:{barTop}px; height:{barHeight}px;right:{barOffset}px;"
|
||||||
on:mousedown={startVDragging}
|
on:mousedown={startVDragging}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showHScrollbar}
|
{#if $showHScrollbar}
|
||||||
<div
|
<div
|
||||||
class="h-scrollbar"
|
class="h-scrollbar"
|
||||||
style="left:{barLeft}px; width:{barWidth}px;"
|
style="left:{barLeft}px; width:{barWidth}px;bottom:{barOffset}px;"
|
||||||
on:mousedown={startHDragging}
|
on:mousedown={startHDragging}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -110,21 +106,19 @@
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--spectrum-global-color-gray-600);
|
background: var(--spectrum-global-color-gray-500);
|
||||||
opacity: 0.6;
|
opacity: 0.7;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
z-index: 999;
|
z-index: 1;
|
||||||
transition: opacity 130ms ease-out;
|
transition: opacity 130ms ease-out;
|
||||||
}
|
}
|
||||||
div:hover {
|
div:hover {
|
||||||
opacity: 0.9;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.v-scrollbar {
|
.v-scrollbar {
|
||||||
right: 4px;
|
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
.h-scrollbar {
|
.h-scrollbar {
|
||||||
bottom: 4px;
|
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const createMaxScrollStores = context => {
|
||||||
// Memoize store primitives
|
// Memoize store primitives
|
||||||
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
|
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
|
||||||
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
|
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
|
||||||
|
const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
|
||||||
|
|
||||||
// Derive vertical limits
|
// Derive vertical limits
|
||||||
const height = derived(bounds, $bounds => $bounds.height, 0)
|
const height = derived(bounds, $bounds => $bounds.height, 0)
|
||||||
|
@ -34,9 +35,9 @@ export const createMaxScrollStores = context => {
|
||||||
|
|
||||||
// Derive horizontal limits
|
// Derive horizontal limits
|
||||||
const contentWidth = derived(
|
const contentWidth = derived(
|
||||||
[visibleColumns, stickyColumn],
|
[visibleColumns, stickyColumnWidth],
|
||||||
([$visibleColumns, $stickyColumn]) => {
|
([$visibleColumns, $stickyColumnWidth]) => {
|
||||||
let width = gutterWidth + padding + ($stickyColumn?.width || 0)
|
let width = gutterWidth + padding + $stickyColumnWidth
|
||||||
$visibleColumns.forEach(col => {
|
$visibleColumns.forEach(col => {
|
||||||
width += col.width
|
width += col.width
|
||||||
})
|
})
|
||||||
|
@ -45,9 +46,8 @@ export const createMaxScrollStores = context => {
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
const screenWidth = derived(
|
const screenWidth = derived(
|
||||||
[width, stickyColumn],
|
[width, stickyColumnWidth],
|
||||||
([$width, $stickyColumn]) =>
|
([$width, $stickyColumnWidth]) => $width + gutterWidth + $stickyColumnWidth,
|
||||||
$width + gutterWidth + ($stickyColumn?.width || 0),
|
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
const maxScrollLeft = derived(
|
const maxScrollLeft = derived(
|
||||||
|
@ -151,10 +151,27 @@ export const createMaxScrollStores = context => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Derive whether to show scrollbars or not
|
||||||
|
const showVScrollbar = derived(
|
||||||
|
[contentHeight, height],
|
||||||
|
([$contentHeight, $height]) => {
|
||||||
|
return $contentHeight > $height
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const showHScrollbar = derived(
|
||||||
|
[contentWidth, screenWidth],
|
||||||
|
([$contentWidth, $screenWidth]) => {
|
||||||
|
return $contentWidth > $screenWidth
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contentHeight,
|
contentHeight,
|
||||||
contentWidth,
|
contentWidth,
|
||||||
|
screenWidth,
|
||||||
maxScrollTop,
|
maxScrollTop,
|
||||||
maxScrollLeft,
|
maxScrollLeft,
|
||||||
|
showHScrollbar,
|
||||||
|
showVScrollbar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ export const createRowsStore = context => {
|
||||||
return index >= 0 ? get(enrichedRows)[index] : null
|
return index >= 0 ? get(enrichedRows)[index] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a new empty row
|
// Adds a new row
|
||||||
const addRow = async (row, idx) => {
|
const addRow = async (row, idx) => {
|
||||||
try {
|
try {
|
||||||
// Create row
|
// Create row
|
||||||
|
@ -136,6 +136,7 @@ export const createRowsStore = context => {
|
||||||
} else {
|
} else {
|
||||||
handleNewRows([newRow])
|
handleNewRows([newRow])
|
||||||
}
|
}
|
||||||
|
notifications.success("Row added successfully")
|
||||||
return newRow
|
return newRow
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Error adding row: ${error?.message}`)
|
notifications.error(`Error adding row: ${error?.message}`)
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const createViewportStores = 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
|
||||||
// as mich as possible
|
// as much as possible
|
||||||
const scrolledRowCount = derived(
|
const scrolledRowCount = derived(
|
||||||
scrollTop,
|
scrollTop,
|
||||||
$scrollTop => {
|
$scrollTop => {
|
||||||
|
@ -22,7 +22,7 @@ export const createViewportStores = context => {
|
||||||
const visualRowCapacity = derived(
|
const visualRowCapacity = derived(
|
||||||
height,
|
height,
|
||||||
$height => {
|
$height => {
|
||||||
return Math.ceil($height / cellHeight)
|
return Math.ceil($height / cellHeight) + 1
|
||||||
},
|
},
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { domDebounce } from "../../../utils/utils"
|
||||||
|
|
||||||
|
export const createWheelStores = context => {
|
||||||
|
const {
|
||||||
|
maxScrollLeft,
|
||||||
|
maxScrollTop,
|
||||||
|
hoveredRowId,
|
||||||
|
renderedRows,
|
||||||
|
bounds,
|
||||||
|
scroll,
|
||||||
|
cellHeight,
|
||||||
|
} = context
|
||||||
|
|
||||||
|
// Handles a wheel even and updates the scroll offsets
|
||||||
|
const handleWheel = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
const modifier = e.ctrlKey || e.metaKey
|
||||||
|
let x = modifier ? e.deltaY : e.deltaX
|
||||||
|
let y = modifier ? e.deltaX : e.deltaY
|
||||||
|
debouncedHandleWheel(x, y, e.clientY)
|
||||||
|
}
|
||||||
|
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
||||||
|
const { top, left } = get(scroll)
|
||||||
|
|
||||||
|
// Calculate new scroll top
|
||||||
|
let newScrollTop = top + deltaY
|
||||||
|
newScrollTop = Math.max(0, Math.min(newScrollTop, get(maxScrollTop)))
|
||||||
|
|
||||||
|
// Calculate new scroll left
|
||||||
|
let newScrollLeft = left + deltaX
|
||||||
|
newScrollLeft = Math.max(0, Math.min(newScrollLeft, get(maxScrollLeft)))
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
scroll.set({
|
||||||
|
left: newScrollLeft,
|
||||||
|
top: newScrollTop,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hover row under cursor
|
||||||
|
const y = clientY - get(bounds).top + (newScrollTop % cellHeight)
|
||||||
|
const hoveredRow = get(renderedRows)[Math.floor(y / cellHeight)]
|
||||||
|
hoveredRowId.set(hoveredRow?._id)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
wheel: {
|
||||||
|
actions: {
|
||||||
|
handleWheel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue