Split into more modular components and try virtual rendering
This commit is contained in:
parent
0060025cca
commit
ddb70ba3f6
|
@ -0,0 +1,13 @@
|
||||||
|
<script>
|
||||||
|
import SpreadsheetCell from "./SpreadsheetCell.svelte"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { selectedCellId, hoveredRowId } = getContext("spreadsheet")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SpreadsheetCell
|
||||||
|
{...$$props}
|
||||||
|
spacer
|
||||||
|
on:click={() => ($selectedCellId = null)}
|
||||||
|
on:mouseenter={() => ($hoveredRowId = null)}
|
||||||
|
/>
|
|
@ -3,18 +3,16 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import TextCell from "./cells/TextCell.svelte"
|
|
||||||
import OptionsCell from "./cells/OptionsCell.svelte"
|
|
||||||
import DateCell from "./cells/DateCell.svelte"
|
|
||||||
import MultiSelectCell from "./cells/MultiSelectCell.svelte"
|
|
||||||
import NumberCell from "./cells/NumberCell.svelte"
|
|
||||||
import RelationshipCell from "./cells/RelationshipCell.svelte"
|
|
||||||
import { createReorderStores } from "./stores/reorder"
|
import { createReorderStores } from "./stores/reorder"
|
||||||
import { createResizeStore } from "./stores/resize"
|
import { createResizeStore } from "./stores/resize"
|
||||||
import ReorderPlaceholder from "./ReorderPlaceholder.svelte"
|
import ReorderPlaceholder from "./ReorderPlaceholder.svelte"
|
||||||
import ResizeSlider from "./ResizeSlider.svelte"
|
import ResizeSlider from "./ResizeSlider.svelte"
|
||||||
import SpreadsheetHeader from "./SpreadsheetHeader.svelte"
|
import SpreadsheetHeader from "./SpreadsheetHeader.svelte"
|
||||||
import SpreadsheetBody from "./SpreadsheetBody.svelte"
|
import SpreadsheetBody from "./SpreadsheetBody.svelte"
|
||||||
|
import SpreadsheetCell from "./SpreadsheetCell.svelte"
|
||||||
|
import SpacerCell from "./SpacerCell.svelte"
|
||||||
|
import VerticalSpacer from "./VerticalSpacer.svelte"
|
||||||
|
import SpreadsheetRow from "./SpreadsheetRow.svelte"
|
||||||
|
|
||||||
export let table
|
export let table
|
||||||
export let filter
|
export let filter
|
||||||
|
@ -25,6 +23,7 @@
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
// Sheet constants
|
// Sheet constants
|
||||||
|
const cellHeight = 32
|
||||||
const limit = 100
|
const limit = 100
|
||||||
const defaultWidth = 160
|
const defaultWidth = 160
|
||||||
const rand = Math.random()
|
const rand = Math.random()
|
||||||
|
@ -36,6 +35,9 @@
|
||||||
const selectedCellId = writable(null)
|
const selectedCellId = writable(null)
|
||||||
const selectedRows = writable({})
|
const selectedRows = writable({})
|
||||||
const tableId = writable(table?.tableId)
|
const tableId = writable(table?.tableId)
|
||||||
|
const changeCache = writable({})
|
||||||
|
const newRows = writable([])
|
||||||
|
const visibleRows = writable([0, 0])
|
||||||
|
|
||||||
// Build up spreadsheet context and additional stores
|
// Build up spreadsheet context and additional stores
|
||||||
const context = {
|
const context = {
|
||||||
|
@ -46,27 +48,14 @@
|
||||||
selectedCellId,
|
selectedCellId,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
tableId,
|
tableId,
|
||||||
|
changeCache,
|
||||||
|
newRows,
|
||||||
|
cellHeight,
|
||||||
|
visibleRows,
|
||||||
}
|
}
|
||||||
const { reorder, reorderPlaceholder } = createReorderStores(context)
|
const { reorder, reorderPlaceholder } = createReorderStores(context)
|
||||||
const resize = createResizeStore(context)
|
const resize = createResizeStore(context)
|
||||||
|
|
||||||
// API for children to consume
|
|
||||||
const spreadsheetAPI = {
|
|
||||||
refreshData: () => fetch?.refresh(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set context for children to consume
|
|
||||||
setContext("spreadsheet", {
|
|
||||||
...context,
|
|
||||||
reorder,
|
|
||||||
reorderPlaceholder,
|
|
||||||
resize,
|
|
||||||
spreadsheetAPI,
|
|
||||||
})
|
|
||||||
|
|
||||||
let changeCache = {}
|
|
||||||
let newRows = []
|
|
||||||
|
|
||||||
$: tableId.set(table?.tableId)
|
$: tableId.set(table?.tableId)
|
||||||
$: query = LuceneUtils.buildLuceneQuery(filter)
|
$: query = LuceneUtils.buildLuceneQuery(filter)
|
||||||
$: fetch = createFetch(table)
|
$: fetch = createFetch(table)
|
||||||
|
@ -79,7 +68,7 @@
|
||||||
$: generateColumns($fetch)
|
$: generateColumns($fetch)
|
||||||
$: rowCount = $rows.length
|
$: rowCount = $rows.length
|
||||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
||||||
$: updateSortedRows($fetch.rows, newRows)
|
$: updateSortedRows($fetch.rows, $newRows)
|
||||||
|
|
||||||
const createFetch = datasource => {
|
const createFetch = datasource => {
|
||||||
return fetchData({
|
return fetchData({
|
||||||
|
@ -112,22 +101,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCellForField = field => {
|
|
||||||
const type = field.schema.type
|
|
||||||
if (type === "options") {
|
|
||||||
return OptionsCell
|
|
||||||
} else if (type === "datetime") {
|
|
||||||
return DateCell
|
|
||||||
} else if (type === "array") {
|
|
||||||
return MultiSelectCell
|
|
||||||
} else if (type === "number") {
|
|
||||||
return NumberCell
|
|
||||||
} else if (type === "link") {
|
|
||||||
return RelationshipCell
|
|
||||||
}
|
|
||||||
return TextCell
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIconForField = field => {
|
const getIconForField = field => {
|
||||||
const type = field.schema.type
|
const type = field.schema.type
|
||||||
if (type === "options") {
|
if (type === "options") {
|
||||||
|
@ -138,13 +111,6 @@
|
||||||
return "Text"
|
return "Text"
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectRow = id => {
|
|
||||||
selectedRows.update(state => {
|
|
||||||
state[id] = !state[id]
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectAll = () => {
|
const selectAll = () => {
|
||||||
const allSelected = selectedRowCount === rowCount
|
const allSelected = selectedRowCount === rowCount
|
||||||
if (allSelected) {
|
if (allSelected) {
|
||||||
|
@ -159,7 +125,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = async (rowId, field, value) => {
|
const updateValue = async (rowId, field, value) => {
|
||||||
let row = $rows.find(x => x._id === rowId)
|
let row = $rows.find(x => x._id === rowId)
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return
|
return
|
||||||
|
@ -167,19 +133,25 @@
|
||||||
if (row[field.name] === value) {
|
if (row[field.name] === value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
changeCache[rowId] = { [field.name]: value }
|
changeCache.update(state => {
|
||||||
|
state[rowId] = { [field.name]: value }
|
||||||
|
return state
|
||||||
|
})
|
||||||
await API.saveRow({
|
await API.saveRow({
|
||||||
...row,
|
...row,
|
||||||
...changeCache[rowId],
|
...$changeCache[rowId],
|
||||||
})
|
})
|
||||||
await fetch.refresh()
|
await fetch.refresh()
|
||||||
delete changeCache[rowId]
|
changeCache.update(state => {
|
||||||
|
delete state[rowId]
|
||||||
|
return state
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addRow = async field => {
|
const addRow = async field => {
|
||||||
const res = await API.saveRow({ tableId: table.tableId })
|
const res = await API.saveRow({ tableId: table.tableId })
|
||||||
$selectedCellId = `${res._id}-${field.name}`
|
$selectedCellId = `${res._id}-${field.name}`
|
||||||
newRows.push(res._id)
|
newRows.update(state => [...state, res._id])
|
||||||
await fetch.refresh()
|
await fetch.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,25 +164,44 @@
|
||||||
})
|
})
|
||||||
$rows = sortedRows
|
$rows = sortedRows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API for children to consume
|
||||||
|
const spreadsheetAPI = {
|
||||||
|
refreshData: () => fetch?.refresh(),
|
||||||
|
updateValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set context for children to consume
|
||||||
|
setContext("spreadsheet", {
|
||||||
|
...context,
|
||||||
|
reorder,
|
||||||
|
reorderPlaceholder,
|
||||||
|
resize,
|
||||||
|
spreadsheetAPI,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
<div use:styleable={$component.styles}>
|
||||||
<div class="wrapper" class:resize={$resize.columnIdx != null}>
|
<div
|
||||||
|
class="wrapper"
|
||||||
|
class:resize={$resize.columnIdx != null}
|
||||||
|
style="--cell-height:{cellHeight}px;"
|
||||||
|
>
|
||||||
<SpreadsheetHeader />
|
<SpreadsheetHeader />
|
||||||
<SpreadsheetBody>
|
<SpreadsheetBody>
|
||||||
<!-- Field headers -->
|
<!-- Field headers -->
|
||||||
<div class="header cell label" on:click={selectAll}>
|
<SpreadsheetCell header label on:click={selectAll}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={rowCount && selectedRowCount === rowCount}
|
checked={rowCount && selectedRowCount === rowCount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</SpreadsheetCell>
|
||||||
{#each $columns as field, fieldIdx}
|
{#each $columns as field, fieldIdx}
|
||||||
<div
|
<SpreadsheetCell
|
||||||
class="header cell"
|
header
|
||||||
class:sticky={fieldIdx === 0}
|
sticky={fieldIdx === 0}
|
||||||
class:reorder-source={$reorder.columnIdx === fieldIdx}
|
reorderSource={$reorder.columnIdx === fieldIdx}
|
||||||
class:reorder-target={$reorder.swapColumnIdx === fieldIdx}
|
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
|
||||||
on:mousedown={e => reorder.actions.startReordering(fieldIdx, e)}
|
on:mousedown={e => reorder.actions.startReordering(fieldIdx, e)}
|
||||||
id={`sheet-${rand}-header-${fieldIdx}`}
|
id={`sheet-${rand}-header-${fieldIdx}`}
|
||||||
>
|
>
|
||||||
|
@ -223,101 +214,44 @@
|
||||||
{field.name}
|
{field.name}
|
||||||
</span>
|
</span>
|
||||||
<ResizeSlider columnIdx={fieldIdx} />
|
<ResizeSlider columnIdx={fieldIdx} />
|
||||||
</div>
|
</SpreadsheetCell>
|
||||||
{/each}
|
{/each}
|
||||||
<!-- Horizontal spacer -->
|
<SpacerCell
|
||||||
<div
|
header
|
||||||
class="header cell spacer"
|
reorderTarget={$reorder.swapColumnIdx === $columns.length}
|
||||||
class:reorder-target={$reorder.swapColumnIdx === $columns.length}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- All real rows -->
|
<!-- All real rows -->
|
||||||
{#each $rows as row, rowIdx (row._id)}
|
{#each $rows as row, rowIdx (row._id)}
|
||||||
{@const rowSelected = !!$selectedRows[row._id]}
|
<SpreadsheetRow {row} {rowIdx} />
|
||||||
{@const rowHovered = $hoveredRowId === row._id}
|
|
||||||
{@const data = { ...row, ...changeCache[row._id] }}
|
|
||||||
<div
|
|
||||||
class="cell label"
|
|
||||||
class:row-selected={rowSelected}
|
|
||||||
class:hovered={rowHovered}
|
|
||||||
on:focus
|
|
||||||
on:mouseover={() => ($hoveredRowId = row._id)}
|
|
||||||
on:click={() => selectRow(row._id)}
|
|
||||||
>
|
|
||||||
{#if rowSelected || rowHovered}
|
|
||||||
<input type="checkbox" checked={rowSelected} />
|
|
||||||
{:else}
|
|
||||||
<span>
|
|
||||||
{rowIdx + 1}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#each $columns as field, fieldIdx}
|
|
||||||
{@const cellIdx = `${row._id}-${field.name}`}
|
|
||||||
{#key cellIdx}
|
|
||||||
<div
|
|
||||||
class="cell"
|
|
||||||
class:row-selected={rowSelected}
|
|
||||||
class:sticky={fieldIdx === 0}
|
|
||||||
class:hovered={rowHovered}
|
|
||||||
class:selected={$selectedCellId === cellIdx}
|
|
||||||
class:reorder-source={$reorder.columnIdx === fieldIdx}
|
|
||||||
class:reorder-target={$reorder.swapColumnIdx === fieldIdx}
|
|
||||||
on:focus
|
|
||||||
on:mouseover={() => ($hoveredRowId = row._id)}
|
|
||||||
on:click={() => ($selectedCellId = cellIdx)}
|
|
||||||
>
|
|
||||||
<svelte:component
|
|
||||||
this={getCellForField(field)}
|
|
||||||
value={data[field.name]}
|
|
||||||
schema={field.schema}
|
|
||||||
selected={$selectedCellId === cellIdx}
|
|
||||||
onChange={val => handleChange(row._id, field, val)}
|
|
||||||
readonly={field.schema.autocolumn}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/key}
|
|
||||||
{/each}
|
|
||||||
<!-- Horizontal spacer -->
|
|
||||||
<div
|
|
||||||
class="cell spacer"
|
|
||||||
class:reorder-target={$reorder.swapColumnIdx === $columns.length}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<!-- New row placeholder -->
|
<!-- New row placeholder -->
|
||||||
<div
|
<SpreadsheetCell
|
||||||
class="cell label new"
|
label
|
||||||
on:click={addRow}
|
on:click={addRow}
|
||||||
on:focus
|
on:mouseenter={() => ($hoveredRowId = "new")}
|
||||||
on:mouseover={() => ($hoveredRowId = "new")}
|
rowHovered={$hoveredRowId === "new"}
|
||||||
class:hovered={$hoveredRowId === "new"}
|
|
||||||
>
|
>
|
||||||
<Icon hoverable name="Add" size="S" />
|
<Icon hoverable name="Add" size="S" />
|
||||||
</div>
|
</SpreadsheetCell>
|
||||||
{#each $columns as field, fieldIdx}
|
{#each $columns as field, fieldIdx}
|
||||||
<div
|
<SpreadsheetCell
|
||||||
class="cell new"
|
sticky={fieldIdx === 0}
|
||||||
class:sticky={fieldIdx === 0}
|
rowHovered={$hoveredRowId === "new"}
|
||||||
class:hovered={$hoveredRowId === "new"}
|
reorderSource={$reorder.columnIdx === fieldIdx}
|
||||||
class:reorder-source={$reorder.columnIdx === fieldIdx}
|
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
|
||||||
class:reorder-target={$reorder.swapColumnIdx === fieldIdx}
|
|
||||||
on:click={() => addRow(field)}
|
on:click={() => addRow(field)}
|
||||||
on:focus
|
on:mouseenter={() => ($hoveredRowId = "new")}
|
||||||
on:mouseover={() => ($hoveredRowId = "new")}
|
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
<!-- Horizontal spacer -->
|
<SpacerCell reorderTarget={$reorder.swapColumnIdx === $columns.length} />
|
||||||
<div
|
|
||||||
class="cell spacer"
|
|
||||||
class:reorder-target={$reorder.swapColumnIdx === $columns.length}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Vertical spacer -->
|
<!-- Vertical spacer to pad bottom of sheet -->
|
||||||
<div class="vertical-spacer" />
|
<VerticalSpacer />
|
||||||
</SpreadsheetBody>
|
</SpreadsheetBody>
|
||||||
|
|
||||||
<!-- Reorder placeholder -->
|
<!-- Placeholder overlay for new column position -->
|
||||||
<ReorderPlaceholder />
|
<ReorderPlaceholder />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -337,121 +271,12 @@
|
||||||
--cell-background-hover: var(--spectrum-global-color-gray-100);
|
--cell-background-hover: var(--spectrum-global-color-gray-100);
|
||||||
--cell-padding: 8px;
|
--cell-padding: 8px;
|
||||||
--cell-spacing: 4px;
|
--cell-spacing: 4px;
|
||||||
--cell-height: 32px;
|
|
||||||
--cell-font-size: 14px;
|
--cell-font-size: 14px;
|
||||||
}
|
}
|
||||||
.wrapper.resize *:hover {
|
.wrapper.resize *:hover {
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
}
|
}
|
||||||
.wrapper ::-webkit-scrollbar-track {
|
.wrapper::-webkit-scrollbar-track {
|
||||||
background: var(--cell-background);
|
background: var(--cell-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cells */
|
|
||||||
.cell {
|
|
||||||
height: var(--cell-height);
|
|
||||||
border-style: solid;
|
|
||||||
border-color: var(--spectrum-global-color-gray-300);
|
|
||||||
border-width: 0;
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
border-left-width: 1px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--spectrum-global-color-gray-900);
|
|
||||||
font-size: var(--cell-font-size);
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
background: var(--cell-background);
|
|
||||||
position: relative;
|
|
||||||
transition: border-color 130ms ease-out;
|
|
||||||
}
|
|
||||||
.cell.hovered {
|
|
||||||
background: var(--cell-background-hover);
|
|
||||||
}
|
|
||||||
.cell.selected {
|
|
||||||
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.cell:not(.selected) {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.cell:hover {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.cell.row-selected {
|
|
||||||
background-color: rgb(224, 242, 255);
|
|
||||||
}
|
|
||||||
.cell.new:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header cells */
|
|
||||||
.header {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
padding: 0 var(--cell-padding);
|
|
||||||
z-index: 3;
|
|
||||||
border-color: var(--spectrum-global-color-gray-400);
|
|
||||||
}
|
|
||||||
.header span {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.header.sticky {
|
|
||||||
z-index: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sticky styles */
|
|
||||||
.sticky {
|
|
||||||
position: sticky;
|
|
||||||
left: 40px;
|
|
||||||
z-index: 2;
|
|
||||||
border-left-color: transparent;
|
|
||||||
}
|
|
||||||
.sticky.selected {
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Spacer cells */
|
|
||||||
.spacer {
|
|
||||||
background: none;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.vertical-spacer {
|
|
||||||
grid-column: 1/-1;
|
|
||||||
height: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reorder styles */
|
|
||||||
.cell.reorder-source {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
}
|
|
||||||
.cell.reorder-target {
|
|
||||||
border-left-color: var(--spectrum-global-color-blue-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
padding: 0 12px;
|
|
||||||
border-right: none;
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.label.header {
|
|
||||||
z-index: 4;
|
|
||||||
}
|
|
||||||
.label span {
|
|
||||||
min-width: 14px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--spectrum-global-color-gray-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
const { columns, selectedCellId, rand } = getContext("spreadsheet")
|
const { columns, selectedCellId, rand, visibleRows, cellHeight } =
|
||||||
|
getContext("spreadsheet")
|
||||||
|
|
||||||
|
let ref
|
||||||
|
let height = 0
|
||||||
let horizontallyScrolled = false
|
let horizontallyScrolled = false
|
||||||
|
let scrollTop = 0
|
||||||
|
|
||||||
$: gridStyles = getGridStyles($columns)
|
$: gridStyles = getGridStyles($columns)
|
||||||
|
$: computeVisibleRows(scrollTop, height)
|
||||||
|
|
||||||
const getGridStyles = columns => {
|
const getGridStyles = columns => {
|
||||||
const widths = columns?.map(x => x.width)
|
const widths = columns?.map(x => x.width)
|
||||||
|
@ -15,12 +21,35 @@
|
||||||
return `--grid: 40px ${widths.map(x => `${x}px`).join(" ")} 180px;`
|
return `--grid: 40px ${widths.map(x => `${x}px`).join(" ")} 180px;`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the current scroll position
|
||||||
const handleScroll = e => {
|
const handleScroll = e => {
|
||||||
|
// Update horizontally scrolled flag
|
||||||
horizontallyScrolled = e.target.scrollLeft > 0
|
horizontallyScrolled = e.target.scrollLeft > 0
|
||||||
|
|
||||||
|
// Only update scroll top offset when a sizable change happens
|
||||||
|
scrollTop = e.target.scrollTop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const computeVisibleRows = Utils.debounce((scrollTop, height) => {
|
||||||
|
const rows = Math.ceil(height / cellHeight) + 16
|
||||||
|
const firstRow = Math.max(0, Math.floor(scrollTop / cellHeight) - 8)
|
||||||
|
visibleRows.set([firstRow, firstRow + rows])
|
||||||
|
}, 50)
|
||||||
|
|
||||||
|
// Observe and record the height of the body
|
||||||
|
onMount(() => {
|
||||||
|
const observer = new ResizeObserver(entries => {
|
||||||
|
height = entries[0].contentRect.height
|
||||||
|
})
|
||||||
|
observer.observe(ref)
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
bind:this={ref}
|
||||||
class="spreadsheet"
|
class="spreadsheet"
|
||||||
class:horizontally-scrolled={horizontallyScrolled}
|
class:horizontally-scrolled={horizontallyScrolled}
|
||||||
on:scroll={handleScroll}
|
on:scroll={handleScroll}
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
<script>
|
||||||
|
export let header = false
|
||||||
|
export let label = false
|
||||||
|
export let spacer = false
|
||||||
|
export let rowHovered = false
|
||||||
|
export let rowSelected = false
|
||||||
|
export let sticky = false
|
||||||
|
export let selected = false
|
||||||
|
export let reorderSource = false
|
||||||
|
export let reorderTarget = false
|
||||||
|
export let id = null
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="cell"
|
||||||
|
class:header
|
||||||
|
class:label
|
||||||
|
class:spacer
|
||||||
|
class:row-selected={rowSelected}
|
||||||
|
class:row-hovered={rowHovered}
|
||||||
|
class:sticky
|
||||||
|
class:selected
|
||||||
|
class:reorder-source={reorderSource}
|
||||||
|
class:reorder-target={reorderTarget}
|
||||||
|
on:focus
|
||||||
|
on:mouseenter
|
||||||
|
on:click
|
||||||
|
on:mousedown
|
||||||
|
{id}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Cells */
|
||||||
|
.cell {
|
||||||
|
height: var(--cell-height);
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--spectrum-global-color-gray-300);
|
||||||
|
border-width: 0;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-left-width: 1px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
font-size: var(--cell-font-size);
|
||||||
|
gap: var(--cell-spacing);
|
||||||
|
background: var(--cell-background);
|
||||||
|
position: relative;
|
||||||
|
transition: border-color 130ms ease-out;
|
||||||
|
}
|
||||||
|
.cell.row-hovered {
|
||||||
|
background: var(--cell-background-hover);
|
||||||
|
}
|
||||||
|
.cell.selected {
|
||||||
|
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.cell:not(.selected) {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.cell:hover {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.cell.row-selected {
|
||||||
|
background-color: rgb(224, 242, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header cells */
|
||||||
|
.cell.header {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
padding: 0 var(--cell-padding);
|
||||||
|
z-index: 3;
|
||||||
|
border-color: var(--spectrum-global-color-gray-400);
|
||||||
|
}
|
||||||
|
.cell.header :global(span) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sticky styles */
|
||||||
|
.cell.sticky {
|
||||||
|
position: sticky;
|
||||||
|
left: 40px;
|
||||||
|
z-index: 2;
|
||||||
|
border-left-color: transparent;
|
||||||
|
}
|
||||||
|
.cell.sticky.selected {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.cell.header.sticky {
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reorder styles */
|
||||||
|
.cell.reorder-source {
|
||||||
|
background: var(--spectrum-global-color-gray-100);
|
||||||
|
}
|
||||||
|
.cell.header.reorder-source {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
.cell.reorder-target {
|
||||||
|
border-left-color: var(--spectrum-global-color-blue-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Label cells */
|
||||||
|
.cell.label {
|
||||||
|
padding: 0 12px;
|
||||||
|
border-left-width: 0;
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.cell.label.header {
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.cell.label :global(span) {
|
||||||
|
min-width: 14px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
.cell.label :global(input[type="checkbox"]) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spacer cells */
|
||||||
|
.cell.spacer {
|
||||||
|
background: none;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,107 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import SpreadsheetCell from "./SpreadsheetCell.svelte"
|
||||||
|
import SpacerCell from "./SpacerCell.svelte"
|
||||||
|
import OptionsCell from "./cells/OptionsCell.svelte"
|
||||||
|
import DateCell from "./cells/DateCell.svelte"
|
||||||
|
import MultiSelectCell from "./cells/MultiSelectCell.svelte"
|
||||||
|
import NumberCell from "./cells/NumberCell.svelte"
|
||||||
|
import RelationshipCell from "./cells/RelationshipCell.svelte"
|
||||||
|
import TextCell from "./cells/TextCell.svelte"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
export let rowIdx
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedCellId,
|
||||||
|
reorder,
|
||||||
|
hoveredRowId,
|
||||||
|
columns,
|
||||||
|
selectedRows,
|
||||||
|
changeCache,
|
||||||
|
spreadsheetAPI,
|
||||||
|
visibleRows,
|
||||||
|
} = getContext("spreadsheet")
|
||||||
|
|
||||||
|
$: rowSelected = !!$selectedRows[row._id]
|
||||||
|
$: rowHovered = $hoveredRowId === row._id
|
||||||
|
$: data = { ...row, ...$changeCache[row._id] }
|
||||||
|
$: visible = rowIdx >= $visibleRows[0] && rowIdx <= $visibleRows[1]
|
||||||
|
|
||||||
|
const getCellForField = field => {
|
||||||
|
const type = field.schema.type
|
||||||
|
if (type === "options") {
|
||||||
|
return OptionsCell
|
||||||
|
} else if (type === "datetime") {
|
||||||
|
return DateCell
|
||||||
|
} else if (type === "array") {
|
||||||
|
return MultiSelectCell
|
||||||
|
} else if (type === "number") {
|
||||||
|
return NumberCell
|
||||||
|
} else if (type === "link") {
|
||||||
|
return RelationshipCell
|
||||||
|
}
|
||||||
|
return TextCell
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectRow = id => {
|
||||||
|
selectedRows.update(state => {
|
||||||
|
state[id] = !state[id]
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !visible}
|
||||||
|
<div class="row-placeholder" />
|
||||||
|
{:else}
|
||||||
|
<SpreadsheetCell
|
||||||
|
label
|
||||||
|
{rowSelected}
|
||||||
|
{rowHovered}
|
||||||
|
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||||
|
on:click={() => selectRow(row._id)}
|
||||||
|
>
|
||||||
|
{#if rowSelected || rowHovered}
|
||||||
|
<input type="checkbox" checked={rowSelected} />
|
||||||
|
{:else}
|
||||||
|
<span>
|
||||||
|
{rowIdx + 1}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</SpreadsheetCell>
|
||||||
|
{#each $columns as field, fieldIdx}
|
||||||
|
{@const cellIdx = `${row._id}-${field.name}`}
|
||||||
|
{#key cellIdx}
|
||||||
|
<SpreadsheetCell
|
||||||
|
{rowSelected}
|
||||||
|
{rowHovered}
|
||||||
|
sticky={fieldIdx === 0}
|
||||||
|
selected={$selectedCellId === cellIdx}
|
||||||
|
reorderSource={$reorder.columnIdx === fieldIdx}
|
||||||
|
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
|
||||||
|
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||||
|
on:click={() => ($selectedCellId = cellIdx)}
|
||||||
|
>
|
||||||
|
<svelte:component
|
||||||
|
this={getCellForField(field)}
|
||||||
|
value={data[field.name]}
|
||||||
|
schema={field.schema}
|
||||||
|
selected={$selectedCellId === cellIdx}
|
||||||
|
onChange={val => spreadsheetAPI.updateValue(row._id, field, val)}
|
||||||
|
readonly={field.schema.autocolumn}
|
||||||
|
/>
|
||||||
|
</SpreadsheetCell>
|
||||||
|
{/key}
|
||||||
|
{/each}
|
||||||
|
<SpacerCell reorderTarget={$reorder.swapColumnIdx === $columns.length} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.row-placeholder {
|
||||||
|
height: var(--cell-height);
|
||||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
background: var(--cell-background);
|
||||||
|
grid-column: 1/-1;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { selectedCellId, hoveredRowId } = getContext("spreadsheet")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
on:click={() => ($selectedCellId = null)}
|
||||||
|
on:mouseenter={() => ($hoveredRowId = null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
grid-column: 1/-1;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -34,7 +34,7 @@ export const createResizeStore = context => {
|
||||||
const newWidth = Math.max(MinColumnWidth, $resize.initialWidth + dx)
|
const newWidth = Math.max(MinColumnWidth, $resize.initialWidth + dx)
|
||||||
|
|
||||||
// Skip small updates
|
// Skip small updates
|
||||||
if (Math.abs(width - newWidth) < 10) {
|
if (Math.abs(width - newWidth) < 20) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue