Refactor spreadsheet into more discreet components

This commit is contained in:
Andrew Kingston 2023-03-01 11:53:09 +00:00
parent 43eadf2ec6
commit 40df22d791
16 changed files with 501 additions and 290 deletions

View File

@ -2,65 +2,37 @@
import SheetCell from "./SheetCell.svelte"
import { getContext } from "svelte"
import { Icon } from "@budibase/bbui"
import { Checkbox } from "@budibase/bbui"
import { getIconForField } from "./utils"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
const { visibleColumns, reorder, selectedRows, rows } =
getContext("spreadsheet")
$: rowCount = $rows.length
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
const getIconForField = field => {
const type = field.schema.type
if (type === "options") {
return "ChevronDown"
} else if (type === "datetime") {
return "Date"
}
return "Text"
}
const selectAll = () => {
const allSelected = selectedRowCount === rowCount
if (allSelected) {
$selectedRows = {}
} else {
let allRows = {}
$rows.forEach(row => {
allRows[row._id] = true
})
$selectedRows = allRows
}
}
</script>
<div class="row">
<!-- Field headers -->
<SheetCell header label on:click={selectAll} width="40" left="0">
<Checkbox value={rowCount && selectedRowCount === rowCount} />
</SheetCell>
{#each $visibleColumns as column}
<SheetCell
header
sticky={column.idx === 0}
reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === column.idx}
on:mousedown={column.idx === 0
? null
: e => reorder.actions.startReordering(column.idx, e)}
width={column.width}
left={column.left}
>
<Icon
size="S"
name={getIconForField(column)}
color="var(--spectrum-global-color-gray-600)"
/>
<span>
{column.name}
</span>
</SheetCell>
{/each}
<div>
<SheetScrollWrapper scrollVertically={false} wheelInteractive={false}>
<div class="row">
{#each $visibleColumns as column}
<SheetCell
header
reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === column.idx}
on:mousedown={e => reorder.actions.startReordering(column.idx, e)}
width={column.width}
left={column.left}
>
<Icon
size="S"
name={getIconForField(column)}
color="var(--spectrum-global-color-gray-600)"
/>
<span>
{column.name}
</span>
</SheetCell>
{/each}
</div>
</SheetScrollWrapper>
</div>
<style>
@ -70,6 +42,7 @@
top: 0;
width: inherit;
z-index: 10;
height: var(--cell-height);
}
.row :global(> :last-child) {
border-right-width: 1px;

View File

@ -15,12 +15,8 @@
</script>
<div class="row new">
<SheetCell label on:click={addRow} width="40" left="0">
<Icon hoverable name="Add" size="S" />
</SheetCell>
{#each $visibleColumns as column}
<SheetCell
sticky={column.idx === 0}
on:click={() => addRow(column)}
width={column.width}
left={column.left}
@ -34,12 +30,10 @@
.row {
display: flex;
width: inherit;
height: var(--cell-height);
}
:global(.sheet:not(.is-resizing):not(.is-reordering) .row:hover .cell) {
background: var(--cell-background-hover);
cursor: pointer;
}
.row :global(> :last-child) {
border-right-width: 1px;
}
</style>

View File

@ -1,8 +1,15 @@
<script>
import { getContext } from "svelte"
const { visibleRows, columns, rand, scroll, visibleColumns, cellHeight } =
getContext("spreadsheet")
const {
visibleRows,
columns,
rand,
scroll,
visibleColumns,
cellHeight,
stickyColumn,
} = getContext("spreadsheet")
const MinColumnWidth = 100
let initialMouseX = null
@ -14,14 +21,17 @@
$: scrollLeft = $scroll.left
$: cutoff = scrollLeft + 40 + $columns[0]?.width || 0
$: offset = 40 + $stickyColumn?.width || 0
$: rowCount = $visibleRows.length
$: contentHeight = (rowCount + 2) * cellHeight
const startResizing = (idx, e) => {
// Prevent propagation to stop reordering triggering
e.stopPropagation()
width = $columns[idx].width
left = $columns[idx].left
const col = idx === "sticky" ? $stickyColumn : $columns[idx]
width = col.width
left = col.left
initialWidth = width
initialMouseX = e.clientX
columnIdx = idx
@ -41,15 +51,22 @@
return
}
columns.update(state => {
state[columnIdx].width = newWidth
let offset = state[columnIdx].left + newWidth
for (let i = columnIdx + 1; i < state.length; i++) {
state[i].left = offset
offset += state[i].width
}
return [...state]
})
if (columnIdx === "sticky") {
stickyColumn.update(state => ({
...state,
width: newWidth,
}))
} else {
columns.update(state => {
state[columnIdx].width = newWidth
let offset = state[columnIdx].left + newWidth
for (let i = columnIdx + 1; i < state.length; i++) {
state[i].left = offset
offset += state[i].width
}
return [...state]
})
}
width = newWidth
}
@ -61,30 +78,38 @@
document.getElementById(`sheet-${rand}`).classList.remove("is-resizing")
}
const getStyle = (col, scrollLeft, rowCount) => {
const left = col.left + col.width - (col.idx === 0 ? 0 : scrollLeft)
const contentHeight = (rowCount + 2) * cellHeight
const getStyle = (col, offset, scrollLeft, contentHeight) => {
const left = offset + col.left + col.width - scrollLeft
return `--left:${left}px; --content-height:${contentHeight}px;`
}
</script>
{#if $stickyColumn}
<div
class="resize-slider sticky"
class:visible={columnIdx === "sticky"}
on:mousedown={e => startResizing("sticky", e)}
style="--left:{40 +
$stickyColumn.width}px; --content-height:{contentHeight}px;"
>
<div class="resize-indicator" />
</div>
{/if}
{#each $visibleColumns as col}
{#if col.idx === 0 || col.left + col.width > cutoff}
<div
class="resize-slider"
class:visible={columnIdx === col.idx}
on:mousedown={e => startResizing(col.idx, e)}
style={getStyle(col, scrollLeft, rowCount)}
>
<div class="resize-indicator" />
</div>
{/if}
<div
class="resize-slider"
class:visible={columnIdx === col.idx}
on:mousedown={e => startResizing(col.idx, e)}
style={getStyle(col, offset, scrollLeft, contentHeight)}
>
<div class="resize-indicator" />
</div>
{/each}
<style>
.resize-slider {
position: absolute;
top: var(--controls-height);
top: 0;
z-index: 10;
height: var(--cell-height);
left: var(--left);
@ -97,7 +122,10 @@
.resize-slider.visible {
cursor: col-resize;
opacity: 1;
height: min(var(--content-height), calc(100% - var(--controls-height)));
height: min(var(--content-height), 100%);
}
.resize-slider.sticky {
z-index: 25;
}
.resize-indicator {
margin-left: -1px;

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte"
import { domDebounce, debounce, throttle } from "../../utils/utils"
const { scroll, bounds, rows, cellHeight, columns } =
const { scroll, bounds, rows, cellHeight, columns, stickyColumn } =
getContext("spreadsheet")
// Bar config
@ -23,20 +23,28 @@
$: barHeight = Math.max(50, (height / contentHeight) * height)
$: availHeight = height - barHeight - 2 * barOffset
$: maxScrollTop = contentHeight - height
$: barTop = barOffset + availHeight * (scrollTop / maxScrollTop)
$: barTop = barOffset + cellHeight + availHeight * (scrollTop / maxScrollTop)
// Calculate H scrollbar size and offset
$: lastCol = $columns[$columns.length - 1]
$: contentWidth = lastCol ? lastCol?.left + lastCol?.width : 0
$: barWidth = Math.max(50, (width / contentWidth) * width)
$: availWidth = width - barWidth - 8
$: maxScrollLeft = contentWidth - width
$: barLeft = 4 + availWidth * (scrollLeft / maxScrollLeft)
$: contentWidth = calculateContentWidth($columns, $stickyColumn)
$: totalWidth = width + 40 + $stickyColumn?.width || 0
$: barWidth = Math.max(50, (totalWidth / contentWidth) * totalWidth)
$: availWidth = totalWidth - barWidth - 2 * barOffset
$: maxScrollLeft = contentWidth - totalWidth
$: barLeft = barOffset + availWidth * (scrollLeft / maxScrollLeft)
// Calculate whether to show scrollbars or not
$: showVScrollbar = contentHeight > height
$: showHScrollbar = contentWidth > width
const calculateContentWidth = (columns, stickyColumn) => {
let width = 40 + stickyColumn?.width
columns.forEach(col => {
width += col.width
})
return width
}
// V scrollbar drag handlers
const startVDragging = e => {
e.preventDefault()

View File

@ -6,13 +6,17 @@
import { createReorderStores } from "./stores/reorder"
import { createViewportStores } from "./stores/viewport"
import { createRowsStore } from "./stores/rows"
import SheetHeader from "./SheetHeader.svelte"
import { createColumnsStores } from "./stores/columns"
import SheetControls from "./SheetControls.svelte"
import SheetBody from "./SheetBody.svelte"
import SheetRow from "./SheetRow.svelte"
import ResizeOverlay from "./ResizeOverlay.svelte"
import HeaderRow from "./HeaderRow.svelte"
import NewRow from "./NewRow.svelte"
import { createAPIClient } from "../../api"
import ScrollOverlay from "./ScrollOverlay.svelte"
import StickyColumn from "./StickyColumn.svelte"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
export let tableId
export let filter
@ -22,12 +26,10 @@
// Sheet constants
const cellHeight = 36
const defaultWidth = 200
const rand = Math.random()
// State stores
const tableIdStore = writable()
const columns = writable([])
const selectedCellId = writable()
const selectedRows = writable({})
const scroll = writable({
@ -45,70 +47,45 @@
let context = {
API: API || createAPIClient(),
rand,
columns,
selectedCellId,
selectedRows,
cellHeight,
bounds,
scroll,
tableId: tableIdStore,
}
const { rows, schema, primaryDisplay } = createRowsStore(context)
context = { ...context, rows }
const { rows, schema } = createRowsStore(context)
context = { ...context, rows, schema }
const { columns, stickyColumn } = createColumnsStores(context)
context = { ...context, columns, stickyColumn }
const { visibleRows, visibleColumns } = createViewportStores(context)
context = { ...context, visibleRows, visibleColumns }
const { reorder } = createReorderStores(context)
context = { ...context, reorder }
$: tableIdStore.set(tableId)
$: generateColumns($schema, $primaryDisplay)
// Generates the column array the first time the schema loads
const generateColumns = (schema, primaryDisplay) => {
if (!schema) {
$columns = []
return
}
const currentColumns = $columns
// Get fields in new schema
let fields = Object.keys(schema || {})
if (primaryDisplay) {
fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)]
}
// Update columns, removing extraneous columns and adding missing ones
let offset = 40
$columns = fields.map((field, idx) => {
const existing = currentColumns.find(x => x.name === field)
const newCol = {
idx,
name: field,
width: existing?.width || defaultWidth,
left: offset,
schema: schema[field],
primaryDisplay: field === primaryDisplay,
}
offset += newCol.width
return newCol
})
}
// Set context for children to consume
setContext("spreadsheet", context)
</script>
<div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}">
<SheetHeader />
<HeaderRow />
<SheetBody>
{#each $visibleRows as row}
<SheetRow {row} />
{/each}
<NewRow />
</SheetBody>
<ResizeOverlay />
<SheetControls />
<div class="sheet-data">
<StickyColumn />
<div class="sheet-main">
<HeaderRow />
<SheetBody>
{#each $visibleRows as row}
<SheetRow {row} />
{/each}
<NewRow />
</SheetBody>
</div>
<ResizeOverlay />
<ScrollOverlay />
</div>
</div>
<style>
@ -133,4 +110,21 @@
.sheet :global(*) {
box-sizing: border-box;
}
.sheet-data {
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-items: flex-start;
align-items: stretch;
overflow: hidden;
height: 0;
position: relative;
}
.sheet-main {
flex: 1 1 auto;
overflow: hidden;
display: flex;
flex-direction: column;
}
</style>

View File

@ -1,7 +1,7 @@
<script>
import { getContext, onMount } from "svelte"
import { Utils } from "../../utils"
import ScrollOverlay from "./ScrollOverlay.svelte"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
const { columns, selectedCellId, cellHeight, rows, bounds, scroll } =
getContext("spreadsheet")
@ -10,47 +10,9 @@
let ref
$: contentHeight = ($rows.length + 2) * cellHeight
$: contentWidth = computeContentWidth($columns)
$: scrollLeft = $scroll.left
$: scrollTop = $scroll.top
const computeContentWidth = columns => {
if (!columns.length) {
return 0
}
const last = columns[columns.length - 1]
return last.left + last.width
}
const updateScrollStore = Utils.domDebounce((left, top) => {
scroll.set({ left, top })
})
const handleScroll = e => {
updateScrollStore(e.target.scrollLeft, e.target.scrollTop)
}
const handleWheel = e => {
const step = cellHeight * 3
const deltaY = e.deltaY < 0 ? -1 : 1
const offset = deltaY * step
let newScrollTop = scrollTop
newScrollTop += offset
newScrollTop = Math.max(0, newScrollTop)
newScrollTop = Math.min(
newScrollTop,
$rows.length * cellHeight - $bounds.height
)
scroll.update(state => ({
...state,
top: newScrollTop,
}))
}
$: fakeOffsetY = -1 * (scrollTop % cellHeight)
$: fakeOffsetX = -1 * scrollLeft
onMount(() => {
// Observe and record the height of the body
const observer = new ResizeObserver(() => {
@ -68,15 +30,10 @@
class="sheet-body"
class:horizontally-scrolled={scrollLeft > 0}
on:click|self={() => ($selectedCellId = null)}
on:wheel|passive={handleWheel}
>
<div
class="content"
style="width:{contentWidth}px; --offset-y:{fakeOffsetY}px; --offset-x:{fakeOffsetX}px;"
>
<SheetScrollWrapper>
<slot />
</div>
<ScrollOverlay />
</SheetScrollWrapper>
</div>
<style>
@ -86,18 +43,10 @@
cursor: default;
overflow: hidden;
flex: 1 1 auto;
height: 0;
}
.sheet-body::-webkit-scrollbar-track {
background: transparent;
}
.content {
min-width: 100%;
min-height: 100%;
background: var(--background-alt);
transform: translate3d(var(--offset-x), var(--offset-y), 0);
overflow: hidden;
}
/* Add shadow to sticky cells when horizontally scrolled */
.horizontally-scrolled :global(.cell.sticky) {

View File

@ -10,11 +10,10 @@
export let reorderTarget = false
export let left
export let width
export let column
</script>
<div
class="cell col-{column}"
class="cell"
class:header
class:label
class:row-selected={rowSelected}
@ -26,7 +25,7 @@
on:mouseenter
on:click
on:mousedown
style="--left: {left}px; --width:{width}px;"
style="--width:{width}px; --left:{left}px;"
>
<slot />
</div>
@ -39,7 +38,7 @@
border-color: var(--spectrum-global-color-gray-200);
border-width: 0;
border-bottom-width: 1px;
border-left-width: 1px;
border-right-width: 1px;
display: flex;
flex-direction: row;
justify-content: flex-start;
@ -48,10 +47,11 @@
font-size: var(--cell-font-size);
gap: var(--cell-spacing);
background: var(--cell-background);
position: absolute;
transition: border-color 130ms ease-out;
width: var(--width);
flex: 0 0 var(--width);
position: absolute;
left: var(--left);
width: var(--width);
}
.cell.selected {
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400);
@ -91,18 +91,6 @@
z-index: 11;
}
/* Sticky styles */
.cell.sticky {
position: sticky;
border-left-width: 0;
transform: none;
left: 40px;
z-index: 5;
}
.cell.selected.sticky {
z-index: 6;
}
/* Reorder styles */
.cell.reorder-source {
background: var(--spectrum-global-color-gray-100);
@ -122,8 +110,8 @@
/* Label cells */
.cell.label {
padding: var(--cell-padding);
width: 40px;
border-left-width: 0;
flex: 0 0 40px;
border-right-width: 0;
position: sticky;
left: 0;
z-index: 5;

View File

@ -2,14 +2,8 @@
<script>
import { getContext } from "svelte"
import SpreadsheetCell from "./SheetCell.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"
import { Checkbox } from "@budibase/bbui"
import SheetCell from "./SheetCell.svelte"
import { getCellComponent } from "./utils"
export let row
@ -21,57 +15,33 @@
visibleColumns,
cellHeight,
} = getContext("spreadsheet")
const TypeComponentMap = {
options: OptionsCell,
datetime: DateCell,
array: MultiSelectCell,
number: NumberCell,
link: RelationshipCell,
}
console.log("mount")
$: rowSelected = !!$selectedRows[row._id]
const selectRow = id => {
selectedRows.update(state => ({
...state,
[id]: !state[id],
}))
}
</script>
<div class="row">
<SpreadsheetCell label {rowSelected} on:click={() => selectRow(row._id)}>
<div class="checkbox" class:visible={rowSelected}>
<Checkbox value={rowSelected} />
</div>
<div class="number" class:visible={!rowSelected}>
{row.__idx + 1}
</div>
</SpreadsheetCell>
{#each $visibleColumns as column (column.name)}
{@const cellIdx = `${row._id}-${column.name}`}
<SpreadsheetCell
<SheetCell
{rowSelected}
sticky={column.idx === 0}
selected={$selectedCellId === cellIdx}
reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === column.idx}
on:click={() => ($selectedCellId = cellIdx)}
width={column.width}
left={column.left}
column={column.idx}
>
<svelte:component
this={TypeComponentMap[column.schema.type] || TextCell}
this={getCellComponent(column)}
value={row[column.name]}
schema={column.schema}
selected={$selectedCellId === cellIdx}
onChange={val => rows.actions.updateRow(row._id, column, val)}
readonly={column.schema.autocolumn}
/>
</SpreadsheetCell>
</SheetCell>
{/each}
</div>
@ -80,32 +50,9 @@
display: flex;
position: relative;
width: inherit;
height: var(--cell-height);
}
:global(.sheet:not(.is-resizing):not(.is-reordering) .row:hover .cell) {
background: var(--cell-background-hover);
}
/* Styles for label cell */
.checkbox {
display: none;
}
.number {
display: none;
color: var(--spectrum-global-color-gray-500);
}
.row:hover .checkbox,
.checkbox.visible {
display: flex;
}
.number.visible {
display: block;
}
.row:hover .number {
display: none;
}
/* Add right border to last cell */
.row :global(> :last-child) {
border-right-width: 1px;
}
</style>

View File

@ -0,0 +1,82 @@
<script>
import { getContext } from "svelte"
const { cellHeight, scroll, bounds, rows, columns, visibleRows } =
getContext("spreadsheet")
export let scrollVertically = true
export let scrollHorizontally = true
export let wheelInteractive = true
$: scrollTop = $scroll.top
$: scrollLeft = $scroll.left
$: offsetY = scrollVertically ? -1 * (scrollTop % cellHeight) : 0
$: offsetX = scrollHorizontally ? -1 * scrollLeft : 0
$: rowCount = $visibleRows.length
$: contentWidth = calculateContentWidth($columns, scrollHorizontally)
$: contentHeight = calculateContentHeight(rowCount, scrollVertically)
$: style = getStyle(offsetX, offsetY, contentWidth, contentHeight)
const getStyle = (offsetX, offsetY, contentWidth, contentHeight) => {
let style = `--offset-y:${offsetY}px; --offset-x:${offsetX}px;`
if (contentWidth) {
style += `--width:${contentWidth}px;`
}
if (contentHeight) {
style += `--height:${contentHeight}px;`
}
return style
}
const calculateContentWidth = (columns, scroll) => {
if (!scroll) {
return null
}
let width = 0
columns.forEach(col => (width += col.width))
return width
}
const calculateContentHeight = (rowCount, scroll) => {
if (!scroll) {
return null
}
return (rowCount + 1) * cellHeight
}
const handleWheel = e => {
const step = cellHeight * 3
const deltaY = e.deltaY < 0 ? -1 : 1
const offset = deltaY * step
let newScrollTop = scrollTop
newScrollTop += offset
newScrollTop = Math.max(0, newScrollTop)
newScrollTop = Math.min(
newScrollTop,
($rows.length + 1) * cellHeight - $bounds.height
)
scroll.update(state => ({
...state,
top: newScrollTop,
}))
}
</script>
<div
class="scroll-wrapper"
{style}
on:wheel|passive={wheelInteractive ? handleWheel : null}
>
<slot />
</div>
<style>
.scroll-wrapper {
background: var(--background-alt);
transform: translate3d(var(--offset-x), var(--offset-y), 0);
overflow: hidden;
width: var(--width);
height: var(--height);
position: relative;
}
</style>

View File

@ -0,0 +1,153 @@
<script>
import { getContext } from "svelte"
import { Checkbox, Icon } from "@budibase/bbui"
import { getIconForField } from "./utils"
import SheetCell from "./SheetCell.svelte"
import { getCellComponent } from "./utils"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
const { rows, selectedRows, stickyColumn, visibleRows, selectedCellId } =
getContext("spreadsheet")
$: rowCount = $rows.length
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
$: width = 40 + $stickyColumn?.width || 0
const selectAll = () => {
const allSelected = selectedRowCount === rowCount
if (allSelected) {
$selectedRows = {}
} else {
let allRows = {}
$rows.forEach(row => {
allRows[row._id] = true
})
$selectedRows = allRows
}
}
const selectRow = id => {
selectedRows.update(state => ({
...state,
[id]: !state[id],
}))
}
const addRow = async field => {
const newRow = await rows.actions.addRow()
if (newRow) {
$selectedCellId = `${newRow._id}-${field.name}`
}
}
</script>
<div class="sticky-column" style="--width:{width}px;">
<div class="row">
<!-- Field headers -->
<SheetCell header label on:click={selectAll} width="40" left="0">
<Checkbox value={rowCount && selectedRowCount === rowCount} />
</SheetCell>
{#if $stickyColumn}
<SheetCell
header
sticky
width={$stickyColumn.width}
left={$stickyColumn.left}
>
<Icon
size="S"
name={getIconForField($stickyColumn)}
color="var(--spectrum-global-color-gray-600)"
/>
<span>
{$stickyColumn.name}
</span>
</SheetCell>
{/if}
</div>
<SheetScrollWrapper scrollHorizontally={false}>
{#each $visibleRows as row}
{@const rowSelected = !!$selectedRows[row._id]}
<div class="row">
<SheetCell
label
{rowSelected}
on:click={() => selectRow(row._id)}
width="40"
>
<div class="checkbox" class:visible={rowSelected}>
<Checkbox value={rowSelected} />
</div>
<div class="number" class:visible={!rowSelected}>
{row.__idx + 1}
</div>
</SheetCell>
{#if $stickyColumn}
{@const cellIdx = `${row._id}-${$stickyColumn.name}`}
<SheetCell
{rowSelected}
sticky
selected={$selectedCellId === cellIdx}
on:click={() => ($selectedCellId = cellIdx)}
width={$stickyColumn.width}
left="40"
>
<svelte:component
this={getCellComponent($stickyColumn)}
value={row[$stickyColumn.name]}
schema={$stickyColumn.schema}
selected={$selectedCellId === cellIdx}
onChange={val =>
rows.actions.updateRow(row._id, $stickyColumn, val)}
readonly={$stickyColumn.schema.autocolumn}
/>
</SheetCell>
{/if}
</div>
{/each}
<div class="row">
<SheetCell label on:click={addRow} width="40">
<Icon hoverable name="Add" size="S" />
</SheetCell>
{#if $stickyColumn}
<SheetCell on:click={addRow} width={$stickyColumn.width} left="40" />
{/if}
</div>
</SheetScrollWrapper>
</div>
<style>
.sticky-column {
flex: 0 0 var(--width);
z-index: 20;
}
.row {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
}
/* Styles for label cell */
.checkbox {
display: none;
}
.number {
display: none;
color: var(--spectrum-global-color-gray-500);
}
.row:hover .checkbox,
.checkbox.visible {
display: flex;
}
.number.visible {
display: block;
}
.row:hover .number {
display: none;
}
</style>

View File

@ -0,0 +1,64 @@
import { get, writable } from "svelte/store"
export const createColumnsStores = context => {
const { schema } = context
const defaultWidth = 200
const columns = writable([])
const stickyColumn = writable(null)
schema.subscribe($schema => {
const currentColumns = get(columns)
if (!$schema) {
columns.set([])
return
}
// Get field list
let fields = []
Object.entries($schema || {}).forEach(([field, fieldSchema]) => {
if (!fieldSchema.primaryDisplay) {
fields.push(field)
}
})
// Update columns, removing extraneous columns and adding missing ones
let offset = 0
columns.set(
fields.map((field, idx) => {
const existing = currentColumns.find(x => x.name === field)
const newCol = {
idx,
name: field,
width: existing?.width || defaultWidth,
left: offset,
schema: $schema[field],
}
offset += newCol.width
return newCol
})
)
})
schema.subscribe($schema => {
const primaryDisplay = Object.entries($schema).find(entry => {
return entry[1].primaryDisplay
})
if (!primaryDisplay) {
stickyColumn.set(null)
return
}
const existingWidth = get(stickyColumn)?.width
const same = primaryDisplay[0] === get(stickyColumn)?.name
stickyColumn.set({
name: primaryDisplay[0],
width: same ? existingWidth : defaultWidth,
left: 40,
schema: primaryDisplay[1],
})
})
return {
columns,
stickyColumn,
}
}

View File

@ -76,7 +76,7 @@ export const createReorderStores = context => {
swapColumnIdx++
}
state.splice(swapColumnIdx, 0, removed[0])
let offset = 40
let offset = 0
return state.map((col, idx) => {
const newCol = {
...col,

View File

@ -15,7 +15,6 @@ export const createRowsStore = context => {
// Exported stores
const rows = writable([])
const schema = writable({})
const primaryDisplay = writable(null)
// Local stores for managing fetching data
const query = derived(filter, $filter => buildLuceneQuery($filter))
@ -62,8 +61,12 @@ export const createRowsStore = context => {
loaded = true
rowCacheMap = {}
rows.set([])
schema.set($$fetch.schema)
primaryDisplay.set($$fetch.definition?.primaryDisplay)
let newSchema = $$fetch.schema
const primaryDisplay = $$fetch.definition?.primaryDisplay
if (primaryDisplay && newSchema[primaryDisplay]) {
newSchema[primaryDisplay].primaryDisplay = true
}
schema.set(newSchema)
}
// Process new rows
@ -220,6 +223,5 @@ export const createRowsStore = context => {
},
},
schema,
primaryDisplay,
}
}

View File

@ -39,25 +39,26 @@ export const createViewportStores = context => {
if (!$columns.length) {
return []
}
let startColIdx = 1
let rightEdge = $columns[1].width
let startColIdx = 0
let rightEdge = $columns[0].width
while (rightEdge < $scrollLeft) {
startColIdx++
rightEdge += $columns[startColIdx].width
}
let endColIdx = startColIdx + 1
let leftEdge = $columns[0].width + 40 + rightEdge
let leftEdge = rightEdge
while (leftEdge < $width + $scrollLeft) {
leftEdge += $columns[endColIdx]?.width
endColIdx++
}
return [
$columns[0],
...$columns.slice(Math.max(1, startColIdx - 2), endColIdx + 2),
]
return $columns.slice(Math.max(0, startColIdx - 1), endColIdx + 1)
}
)
// visibleColumns.subscribe(state => {
// console.log(state)
// })
// Fetch next page when approaching end of data
visibleRows.subscribe($visibleRows => {
const lastVisible = $visibleRows[$visibleRows.length - 1]

View File

@ -1,6 +1,34 @@
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 const getColor = idx => {
if (idx == null || idx === -1) {
return null
}
return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, 0.3)`
}
export const getIconForField = field => {
const type = field.schema.type
if (type === "options") {
return "ChevronDown"
} else if (type === "datetime") {
return "Date"
}
return "Text"
}
const TypeComponentMap = {
options: OptionsCell,
datetime: DateCell,
array: MultiSelectCell,
number: NumberCell,
link: RelationshipCell,
}
export const getCellComponent = column => {
return TypeComponentMap[column?.schema?.type] || TextCell
}