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