Refactor spreadsheet into more discreet components
This commit is contained in:
parent
43eadf2ec6
commit
40df22d791
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue