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 { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
||||
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 { createResizeStore } from "./stores/resize"
|
||||
import ReorderPlaceholder from "./ReorderPlaceholder.svelte"
|
||||
import ResizeSlider from "./ResizeSlider.svelte"
|
||||
import SpreadsheetHeader from "./SpreadsheetHeader.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 filter
|
||||
|
@ -25,6 +23,7 @@
|
|||
const component = getContext("component")
|
||||
|
||||
// Sheet constants
|
||||
const cellHeight = 32
|
||||
const limit = 100
|
||||
const defaultWidth = 160
|
||||
const rand = Math.random()
|
||||
|
@ -36,6 +35,9 @@
|
|||
const selectedCellId = writable(null)
|
||||
const selectedRows = writable({})
|
||||
const tableId = writable(table?.tableId)
|
||||
const changeCache = writable({})
|
||||
const newRows = writable([])
|
||||
const visibleRows = writable([0, 0])
|
||||
|
||||
// Build up spreadsheet context and additional stores
|
||||
const context = {
|
||||
|
@ -46,27 +48,14 @@
|
|||
selectedCellId,
|
||||
selectedRows,
|
||||
tableId,
|
||||
changeCache,
|
||||
newRows,
|
||||
cellHeight,
|
||||
visibleRows,
|
||||
}
|
||||
const { reorder, reorderPlaceholder } = createReorderStores(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)
|
||||
$: query = LuceneUtils.buildLuceneQuery(filter)
|
||||
$: fetch = createFetch(table)
|
||||
|
@ -79,7 +68,7 @@
|
|||
$: generateColumns($fetch)
|
||||
$: rowCount = $rows.length
|
||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
||||
$: updateSortedRows($fetch.rows, newRows)
|
||||
$: updateSortedRows($fetch.rows, $newRows)
|
||||
|
||||
const createFetch = datasource => {
|
||||
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 type = field.schema.type
|
||||
if (type === "options") {
|
||||
|
@ -138,13 +111,6 @@
|
|||
return "Text"
|
||||
}
|
||||
|
||||
const selectRow = id => {
|
||||
selectedRows.update(state => {
|
||||
state[id] = !state[id]
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const selectAll = () => {
|
||||
const allSelected = selectedRowCount === rowCount
|
||||
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)
|
||||
if (!row) {
|
||||
return
|
||||
|
@ -167,19 +133,25 @@
|
|||
if (row[field.name] === value) {
|
||||
return
|
||||
}
|
||||
changeCache[rowId] = { [field.name]: value }
|
||||
changeCache.update(state => {
|
||||
state[rowId] = { [field.name]: value }
|
||||
return state
|
||||
})
|
||||
await API.saveRow({
|
||||
...row,
|
||||
...changeCache[rowId],
|
||||
...$changeCache[rowId],
|
||||
})
|
||||
await fetch.refresh()
|
||||
delete changeCache[rowId]
|
||||
changeCache.update(state => {
|
||||
delete state[rowId]
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const addRow = async field => {
|
||||
const res = await API.saveRow({ tableId: table.tableId })
|
||||
$selectedCellId = `${res._id}-${field.name}`
|
||||
newRows.push(res._id)
|
||||
newRows.update(state => [...state, res._id])
|
||||
await fetch.refresh()
|
||||
}
|
||||
|
||||
|
@ -192,25 +164,44 @@
|
|||
})
|
||||
$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>
|
||||
|
||||
<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 />
|
||||
<SpreadsheetBody>
|
||||
<!-- Field headers -->
|
||||
<div class="header cell label" on:click={selectAll}>
|
||||
<SpreadsheetCell header label on:click={selectAll}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rowCount && selectedRowCount === rowCount}
|
||||
/>
|
||||
</div>
|
||||
</SpreadsheetCell>
|
||||
{#each $columns as field, fieldIdx}
|
||||
<div
|
||||
class="header cell"
|
||||
class:sticky={fieldIdx === 0}
|
||||
class:reorder-source={$reorder.columnIdx === fieldIdx}
|
||||
class:reorder-target={$reorder.swapColumnIdx === fieldIdx}
|
||||
<SpreadsheetCell
|
||||
header
|
||||
sticky={fieldIdx === 0}
|
||||
reorderSource={$reorder.columnIdx === fieldIdx}
|
||||
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
|
||||
on:mousedown={e => reorder.actions.startReordering(fieldIdx, e)}
|
||||
id={`sheet-${rand}-header-${fieldIdx}`}
|
||||
>
|
||||
|
@ -223,101 +214,44 @@
|
|||
{field.name}
|
||||
</span>
|
||||
<ResizeSlider columnIdx={fieldIdx} />
|
||||
</div>
|
||||
</SpreadsheetCell>
|
||||
{/each}
|
||||
<!-- Horizontal spacer -->
|
||||
<div
|
||||
class="header cell spacer"
|
||||
class:reorder-target={$reorder.swapColumnIdx === $columns.length}
|
||||
<SpacerCell
|
||||
header
|
||||
reorderTarget={$reorder.swapColumnIdx === $columns.length}
|
||||
/>
|
||||
|
||||
<!-- All real rows -->
|
||||
{#each $rows as row, rowIdx (row._id)}
|
||||
{@const rowSelected = !!$selectedRows[row._id]}
|
||||
{@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}
|
||||
/>
|
||||
<SpreadsheetRow {row} {rowIdx} />
|
||||
{/each}
|
||||
|
||||
<!-- New row placeholder -->
|
||||
<div
|
||||
class="cell label new"
|
||||
<SpreadsheetCell
|
||||
label
|
||||
on:click={addRow}
|
||||
on:focus
|
||||
on:mouseover={() => ($hoveredRowId = "new")}
|
||||
class:hovered={$hoveredRowId === "new"}
|
||||
on:mouseenter={() => ($hoveredRowId = "new")}
|
||||
rowHovered={$hoveredRowId === "new"}
|
||||
>
|
||||
<Icon hoverable name="Add" size="S" />
|
||||
</div>
|
||||
</SpreadsheetCell>
|
||||
{#each $columns as field, fieldIdx}
|
||||
<div
|
||||
class="cell new"
|
||||
class:sticky={fieldIdx === 0}
|
||||
class:hovered={$hoveredRowId === "new"}
|
||||
class:reorder-source={$reorder.columnIdx === fieldIdx}
|
||||
class:reorder-target={$reorder.swapColumnIdx === fieldIdx}
|
||||
<SpreadsheetCell
|
||||
sticky={fieldIdx === 0}
|
||||
rowHovered={$hoveredRowId === "new"}
|
||||
reorderSource={$reorder.columnIdx === fieldIdx}
|
||||
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
|
||||
on:click={() => addRow(field)}
|
||||
on:focus
|
||||
on:mouseover={() => ($hoveredRowId = "new")}
|
||||
on:mouseenter={() => ($hoveredRowId = "new")}
|
||||
/>
|
||||
{/each}
|
||||
<!-- Horizontal spacer -->
|
||||
<div
|
||||
class="cell spacer"
|
||||
class:reorder-target={$reorder.swapColumnIdx === $columns.length}
|
||||
/>
|
||||
<SpacerCell reorderTarget={$reorder.swapColumnIdx === $columns.length} />
|
||||
|
||||
<!-- Vertical spacer -->
|
||||
<div class="vertical-spacer" />
|
||||
<!-- Vertical spacer to pad bottom of sheet -->
|
||||
<VerticalSpacer />
|
||||
</SpreadsheetBody>
|
||||
|
||||
<!-- Reorder placeholder -->
|
||||
<!-- Placeholder overlay for new column position -->
|
||||
<ReorderPlaceholder />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -337,121 +271,12 @@
|
|||
--cell-background-hover: var(--spectrum-global-color-gray-100);
|
||||
--cell-padding: 8px;
|
||||
--cell-spacing: 4px;
|
||||
--cell-height: 32px;
|
||||
--cell-font-size: 14px;
|
||||
}
|
||||
.wrapper.resize *:hover {
|
||||
cursor: col-resize;
|
||||
}
|
||||
.wrapper ::-webkit-scrollbar-track {
|
||||
.wrapper::-webkit-scrollbar-track {
|
||||
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>
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
<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 scrollTop = 0
|
||||
|
||||
$: gridStyles = getGridStyles($columns)
|
||||
$: computeVisibleRows(scrollTop, height)
|
||||
|
||||
const getGridStyles = columns => {
|
||||
const widths = columns?.map(x => x.width)
|
||||
|
@ -15,12 +21,35 @@
|
|||
return `--grid: 40px ${widths.map(x => `${x}px`).join(" ")} 180px;`
|
||||
}
|
||||
|
||||
// Store the current scroll position
|
||||
const handleScroll = e => {
|
||||
// Update horizontally scrolled flag
|
||||
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>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class="spreadsheet"
|
||||
class:horizontally-scrolled={horizontallyScrolled}
|
||||
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)
|
||||
|
||||
// Skip small updates
|
||||
if (Math.abs(width - newWidth) < 10) {
|
||||
if (Math.abs(width - newWidth) < 20) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue