Break out reordering logic into new stores
This commit is contained in:
parent
35dcd51322
commit
ca7aed617f
|
@ -0,0 +1,32 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { reorderingPlaceholder } = getContext("spreadsheet")
|
||||||
|
|
||||||
|
$: style = getStyle($reorderingPlaceholder)
|
||||||
|
|
||||||
|
const getStyle = state => {
|
||||||
|
return (
|
||||||
|
`--x:${state.x}px;` +
|
||||||
|
`--width:${state.width}px;` +
|
||||||
|
`--height:${state.height}px;`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $reorderingPlaceholder.x != null}
|
||||||
|
<div {style} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
height: min(calc(100% - 36px), var(--height));
|
||||||
|
width: var(--width);
|
||||||
|
left: var(--x);
|
||||||
|
position: absolute;
|
||||||
|
top: 36px;
|
||||||
|
background: var(--spectrum-global-color-blue-400);
|
||||||
|
opacity: 0.2;
|
||||||
|
z-index: 7;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,32 +1,55 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext, setContext } from "svelte"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
||||||
import { Icon, ActionButton } from "@budibase/bbui"
|
import { Icon, ActionButton } from "@budibase/bbui"
|
||||||
import TextCell from "./TextCell.svelte"
|
import TextCell from "./cells/TextCell.svelte"
|
||||||
import OptionsCell from "./OptionsCell.svelte"
|
import OptionsCell from "./cells/OptionsCell.svelte"
|
||||||
import DateCell from "./DateCell.svelte"
|
import DateCell from "./cells/DateCell.svelte"
|
||||||
import MultiSelectCell from "./MultiSelectCell.svelte"
|
import MultiSelectCell from "./cells/MultiSelectCell.svelte"
|
||||||
import NumberCell from "./NumberCell.svelte"
|
import NumberCell from "./cells/NumberCell.svelte"
|
||||||
import RelationshipCell from "./RelationshipCell.svelte"
|
import RelationshipCell from "./cells/RelationshipCell.svelte"
|
||||||
import { getColor } from "./utils.js"
|
import { getColor } from "./utils.js"
|
||||||
|
import { createReorderingStores } from "./stores/reordering"
|
||||||
|
import ReorderingPlaceholder from "./ReorderingPlaceholder.svelte"
|
||||||
|
|
||||||
export let table
|
export let table
|
||||||
export let filter
|
export let filter
|
||||||
export let sortColumn
|
export let sortColumn
|
||||||
export let sortOrder
|
export let sortOrder
|
||||||
|
|
||||||
const { styleable, API, confirmationStore, notificationStore } =
|
const { styleable, API, confirmationStore } = getContext("sdk")
|
||||||
getContext("sdk")
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
|
// Sheet constants
|
||||||
const limit = 100
|
const limit = 100
|
||||||
const defaultWidth = 160
|
const defaultWidth = 160
|
||||||
const minWidth = 100
|
const minWidth = 100
|
||||||
const rand = Math.random()
|
const rand = Math.random()
|
||||||
|
|
||||||
let fieldConfigs = []
|
// State stores
|
||||||
let hoveredRow
|
const columns = writable([])
|
||||||
let selectedCell
|
const hoveredRowId = writable(null)
|
||||||
let selectedRows = {}
|
const selectedCellId = writable(null)
|
||||||
|
const selectedRows = writable({})
|
||||||
|
const rows = writable([])
|
||||||
|
|
||||||
|
// Build up spreadsheet context and additional stores
|
||||||
|
const context = {
|
||||||
|
rand,
|
||||||
|
rows,
|
||||||
|
columns,
|
||||||
|
hoveredRowId,
|
||||||
|
selectedCellId,
|
||||||
|
selectedRows,
|
||||||
|
}
|
||||||
|
const { reordering, reorderingPlaceholder } = createReorderingStores(context)
|
||||||
|
setContext("spreadsheet", {
|
||||||
|
...context,
|
||||||
|
reordering,
|
||||||
|
reorderingPlaceholder,
|
||||||
|
})
|
||||||
|
|
||||||
let horizontallyScrolled = false
|
let horizontallyScrolled = false
|
||||||
let changeCache = {}
|
let changeCache = {}
|
||||||
let newRows = []
|
let newRows = []
|
||||||
|
@ -36,17 +59,6 @@
|
||||||
let resizeInitialWidth
|
let resizeInitialWidth
|
||||||
let resizeFieldIndex
|
let resizeFieldIndex
|
||||||
|
|
||||||
// State for reordering columns
|
|
||||||
let isReordering = false
|
|
||||||
let reorderFieldIndex
|
|
||||||
let reorderBreakpoints
|
|
||||||
let reorderPlaceholderX
|
|
||||||
let reorderPlaceholderInitialX
|
|
||||||
let reorderPlaceholderWidth
|
|
||||||
let reorderInitialX
|
|
||||||
let reorderPlaceholderHeight
|
|
||||||
let reorderCandidateFieldIdx
|
|
||||||
|
|
||||||
$: query = LuceneUtils.buildLuceneQuery(filter)
|
$: query = LuceneUtils.buildLuceneQuery(filter)
|
||||||
$: fetch = createFetch(table)
|
$: fetch = createFetch(table)
|
||||||
$: fetch.update({
|
$: fetch.update({
|
||||||
|
@ -55,11 +67,11 @@
|
||||||
query,
|
query,
|
||||||
limit,
|
limit,
|
||||||
})
|
})
|
||||||
$: updateFieldConfig($fetch)
|
$: generateColumns($fetch)
|
||||||
$: gridStyles = getGridStyles(fieldConfigs)
|
$: gridStyles = getGridStyles($columns)
|
||||||
$: rowCount = $fetch.rows?.length || 0
|
$: rowCount = $rows.length
|
||||||
$: selectedRowCount = Object.values(selectedRows).filter(x => !!x).length
|
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
||||||
$: rows = getSortedRows($fetch.rows, newRows)
|
$: updateSortedRows($fetch.rows, newRows)
|
||||||
|
|
||||||
const createFetch = datasource => {
|
const createFetch = datasource => {
|
||||||
return fetchData({
|
return fetchData({
|
||||||
|
@ -75,15 +87,15 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFieldConfig = ({ schema, definition }) => {
|
// Generates the column array the first time the schema loads
|
||||||
// Generate first time config if required
|
const generateColumns = ({ schema, definition }) => {
|
||||||
if (!fieldConfigs.length && schema) {
|
if (!$columns.length && schema) {
|
||||||
let fields = Object.keys(schema || {})
|
let fields = Object.keys(schema || {})
|
||||||
const primaryDisplay = definition?.primaryDisplay
|
const primaryDisplay = definition?.primaryDisplay
|
||||||
if (primaryDisplay) {
|
if (primaryDisplay) {
|
||||||
fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)]
|
fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)]
|
||||||
}
|
}
|
||||||
fieldConfigs = fields.map(field => ({
|
$columns = fields.map(field => ({
|
||||||
name: field,
|
name: field,
|
||||||
width: defaultWidth,
|
width: defaultWidth,
|
||||||
schema: schema[field],
|
schema: schema[field],
|
||||||
|
@ -92,8 +104,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGridStyles = fieldConfig => {
|
const getGridStyles = columns => {
|
||||||
const widths = fieldConfig?.map(x => x.width)
|
const widths = columns?.map(x => x.width)
|
||||||
if (!widths?.length) {
|
if (!widths?.length) {
|
||||||
return "--grid: 1fr;"
|
return "--grid: 1fr;"
|
||||||
}
|
}
|
||||||
|
@ -134,22 +146,28 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectRow = id => {
|
const selectRow = id => {
|
||||||
selectedRows[id] = !selectedRows[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) {
|
||||||
selectedRows = {}
|
$selectedRows = {}
|
||||||
} else {
|
} else {
|
||||||
rows.forEach(row => {
|
selectedRows.update(state => {
|
||||||
selectedRows[row._id] = true
|
$rows.forEach(row => {
|
||||||
|
state[row._id] = true
|
||||||
|
})
|
||||||
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = async (rowId, field, value) => {
|
const handleChange = 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,10 +185,10 @@
|
||||||
|
|
||||||
const deleteRows = () => {
|
const deleteRows = () => {
|
||||||
// Fetch full row objects to be deleted
|
// Fetch full row objects to be deleted
|
||||||
const rowsToDelete = Object.entries(selectedRows)
|
const rowsToDelete = Object.entries($selectedRows)
|
||||||
.map(entry => {
|
.map(entry => {
|
||||||
if (entry[1] === true) {
|
if (entry[1] === true) {
|
||||||
return rows.find(x => x._id === entry[0])
|
return $rows.find(x => x._id === entry[0])
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -186,9 +204,9 @@
|
||||||
await fetch.refresh()
|
await fetch.refresh()
|
||||||
|
|
||||||
// Refresh state
|
// Refresh state
|
||||||
selectedCell = null
|
$selectedCellId = null
|
||||||
hoveredRow = null
|
$hoveredRowId = null
|
||||||
selectedRows = {}
|
$selectedRows = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show confirmation
|
// Show confirmation
|
||||||
|
@ -203,95 +221,25 @@
|
||||||
|
|
||||||
const addRow = async field => {
|
const addRow = async field => {
|
||||||
const res = await API.saveRow({ tableId: table.tableId })
|
const res = await API.saveRow({ tableId: table.tableId })
|
||||||
selectedCell = `${res._id}-${field.name}`
|
$selectedCellId = `${res._id}-${field.name}`
|
||||||
newRows.push(res._id)
|
newRows.push(res._id)
|
||||||
await fetch.refresh()
|
await fetch.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSortedRows = (rows, newRows) => {
|
const updateSortedRows = (unsortedRows, newRows) => {
|
||||||
let sortedRows = rows.slice()
|
let sortedRows = unsortedRows.slice()
|
||||||
sortedRows.sort((a, b) => {
|
sortedRows.sort((a, b) => {
|
||||||
const aIndex = newRows.indexOf(a._id)
|
const aIndex = newRows.indexOf(a._id)
|
||||||
const bIndex = newRows.indexOf(b._id)
|
const bIndex = newRows.indexOf(b._id)
|
||||||
return aIndex < bIndex ? -1 : 1
|
return aIndex < bIndex ? -1 : 1
|
||||||
})
|
})
|
||||||
return sortedRows
|
$rows = sortedRows
|
||||||
}
|
|
||||||
|
|
||||||
const startReordering = (fieldIdx, e) => {
|
|
||||||
isReordering = true
|
|
||||||
reorderFieldIndex = fieldIdx
|
|
||||||
|
|
||||||
let breakpoints = []
|
|
||||||
fieldConfigs.forEach((config, idx) => {
|
|
||||||
const header = document.getElementById(`sheet-${rand}-header-${idx}`)
|
|
||||||
const bounds = header.getBoundingClientRect()
|
|
||||||
breakpoints.push(bounds.x)
|
|
||||||
if (idx === fieldConfigs.length - 1) {
|
|
||||||
breakpoints.push(bounds.x + bounds.width)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
reorderBreakpoints = breakpoints
|
|
||||||
const self = document.getElementById(`sheet-${rand}-header-${fieldIdx}`)
|
|
||||||
const selfBounds = self.getBoundingClientRect()
|
|
||||||
const body = document.getElementById(`sheet-${rand}-body`)
|
|
||||||
const bodyBounds = body.getBoundingClientRect()
|
|
||||||
reorderPlaceholderInitialX = selfBounds.x - bodyBounds.x
|
|
||||||
reorderPlaceholderX = reorderPlaceholderInitialX
|
|
||||||
reorderPlaceholderWidth = selfBounds.width
|
|
||||||
reorderInitialX = e.clientX
|
|
||||||
reorderPlaceholderHeight = (rows.length + 2) * 32
|
|
||||||
onReorderMove(e)
|
|
||||||
document.addEventListener("mousemove", onReorderMove)
|
|
||||||
document.addEventListener("mouseup", stopReordering)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onReorderMove = e => {
|
|
||||||
if (!isReordering) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reorderPlaceholderX =
|
|
||||||
e.clientX - reorderInitialX + reorderPlaceholderInitialX
|
|
||||||
reorderPlaceholderX = Math.max(0, reorderPlaceholderX)
|
|
||||||
let candidateFieldIdx
|
|
||||||
let minDistance = Number.MAX_SAFE_INTEGER
|
|
||||||
reorderBreakpoints.forEach((point, idx) => {
|
|
||||||
const distance = Math.abs(point - e.clientX)
|
|
||||||
if (distance < minDistance) {
|
|
||||||
minDistance = distance
|
|
||||||
candidateFieldIdx = idx
|
|
||||||
}
|
|
||||||
})
|
|
||||||
reorderCandidateFieldIdx = candidateFieldIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopReordering = () => {
|
|
||||||
const newConfigs = fieldConfigs.slice()
|
|
||||||
const removed = newConfigs.splice(reorderFieldIndex, 1)
|
|
||||||
if (--reorderCandidateFieldIdx < reorderFieldIndex) {
|
|
||||||
reorderCandidateFieldIdx++
|
|
||||||
}
|
|
||||||
newConfigs.splice(reorderCandidateFieldIdx, 0, removed[0])
|
|
||||||
fieldConfigs = newConfigs
|
|
||||||
|
|
||||||
isReordering = false
|
|
||||||
reorderFieldIndex = null
|
|
||||||
reorderBreakpoints = null
|
|
||||||
reorderPlaceholderX = null
|
|
||||||
reorderPlaceholderInitialX = null
|
|
||||||
reorderPlaceholderWidth = null
|
|
||||||
reorderInitialX = null
|
|
||||||
reorderPlaceholderHeight = null
|
|
||||||
reorderCandidateFieldIdx = null
|
|
||||||
|
|
||||||
document.removeEventListener("mousemove", onReorderMove)
|
|
||||||
document.removeEventListener("mouseup", stopReordering)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startResizing = (fieldIdx, e) => {
|
const startResizing = (fieldIdx, e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
resizeInitialX = e.clientX
|
resizeInitialX = e.clientX
|
||||||
resizeInitialWidth = fieldConfigs[fieldIdx].width
|
resizeInitialWidth = $columns[fieldIdx].width
|
||||||
resizeFieldIndex = fieldIdx
|
resizeFieldIndex = fieldIdx
|
||||||
document.addEventListener("mousemove", onResizeMove)
|
document.addEventListener("mousemove", onResizeMove)
|
||||||
document.addEventListener("mouseup", stopResizing)
|
document.addEventListener("mouseup", stopResizing)
|
||||||
|
@ -299,10 +247,13 @@
|
||||||
|
|
||||||
const onResizeMove = e => {
|
const onResizeMove = e => {
|
||||||
const dx = e.clientX - resizeInitialX
|
const dx = e.clientX - resizeInitialX
|
||||||
fieldConfigs[resizeFieldIndex].width = Math.max(
|
columns.update(state => {
|
||||||
minWidth,
|
state[resizeFieldIndex].width = Math.max(
|
||||||
resizeInitialWidth + dx
|
minWidth,
|
||||||
)
|
resizeInitialWidth + dx
|
||||||
|
)
|
||||||
|
return state
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopResizing = () => {
|
const stopResizing = () => {
|
||||||
|
@ -335,7 +286,7 @@
|
||||||
class="spreadsheet"
|
class="spreadsheet"
|
||||||
on:scroll={handleScroll}
|
on:scroll={handleScroll}
|
||||||
style={gridStyles}
|
style={gridStyles}
|
||||||
on:click|self={() => (selectedCell = null)}
|
on:click|self={() => ($selectedCellId = null)}
|
||||||
id={`sheet-${rand}-body`}
|
id={`sheet-${rand}-body`}
|
||||||
>
|
>
|
||||||
<!-- Field headers -->
|
<!-- Field headers -->
|
||||||
|
@ -345,14 +296,14 @@
|
||||||
checked={rowCount && selectedRowCount === rowCount}
|
checked={rowCount && selectedRowCount === rowCount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#each fieldConfigs as field, fieldIdx}
|
{#each $columns as field, fieldIdx}
|
||||||
<div
|
<div
|
||||||
class="header cell"
|
class="header cell"
|
||||||
class:sticky={fieldIdx === 0}
|
class:sticky={fieldIdx === 0}
|
||||||
class:shadow={horizontallyScrolled}
|
class:shadow={horizontallyScrolled}
|
||||||
class:reordering={reorderFieldIndex === fieldIdx}
|
class:reordering-source={$reordering.columnIdx === fieldIdx}
|
||||||
class:reorder-candidate={reorderCandidateFieldIdx === fieldIdx}
|
class:reordering-target={$reordering.swapColumnIdx === fieldIdx}
|
||||||
on:mousedown={e => startReordering(fieldIdx, e)}
|
on:mousedown={e => reordering.actions.startReordering(fieldIdx, e)}
|
||||||
id={`sheet-${rand}-header-${fieldIdx}`}
|
id={`sheet-${rand}-header-${fieldIdx}`}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -369,21 +320,20 @@
|
||||||
<!-- Horizontal spacer -->
|
<!-- Horizontal spacer -->
|
||||||
<div
|
<div
|
||||||
class="header cell spacer"
|
class="header cell spacer"
|
||||||
class:reorder-candidate={reorderCandidateFieldIdx ===
|
class:reordering-target={$reordering.swapColumnIdx === $columns.length}
|
||||||
fieldConfigs.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]}
|
{@const rowSelected = !!$selectedRows[row._id]}
|
||||||
{@const rowHovered = hoveredRow === row._id}
|
{@const rowHovered = $hoveredRowId === row._id}
|
||||||
{@const data = { ...row, ...changeCache[row._id] }}
|
{@const data = { ...row, ...changeCache[row._id] }}
|
||||||
<div
|
<div
|
||||||
class="cell label"
|
class="cell label"
|
||||||
class:row-selected={rowSelected}
|
class:row-selected={rowSelected}
|
||||||
class:hovered={rowHovered}
|
class:hovered={rowHovered}
|
||||||
on:focus
|
on:focus
|
||||||
on:mouseover={() => (hoveredRow = row._id)}
|
on:mouseover={() => ($hoveredRowId = row._id)}
|
||||||
on:click={() => selectRow(row._id)}
|
on:click={() => selectRow(row._id)}
|
||||||
>
|
>
|
||||||
{#if rowSelected || rowHovered}
|
{#if rowSelected || rowHovered}
|
||||||
|
@ -394,7 +344,7 @@
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#each fieldConfigs as field, fieldIdx}
|
{#each $columns as field, fieldIdx}
|
||||||
{@const cellIdx = `${row._id}-${field.name}`}
|
{@const cellIdx = `${row._id}-${field.name}`}
|
||||||
{#key cellIdx}
|
{#key cellIdx}
|
||||||
<div
|
<div
|
||||||
|
@ -402,19 +352,19 @@
|
||||||
class:row-selected={rowSelected}
|
class:row-selected={rowSelected}
|
||||||
class:sticky={fieldIdx === 0}
|
class:sticky={fieldIdx === 0}
|
||||||
class:hovered={rowHovered}
|
class:hovered={rowHovered}
|
||||||
class:selected={selectedCell === cellIdx}
|
class:selected={$selectedCellId === cellIdx}
|
||||||
class:shadow={horizontallyScrolled}
|
class:shadow={horizontallyScrolled}
|
||||||
class:reordering={reorderFieldIndex === fieldIdx}
|
class:reordering-source={$reordering.columnIdx === fieldIdx}
|
||||||
class:reorder-candidate={reorderCandidateFieldIdx === fieldIdx}
|
class:reordering-target={$reordering.swapColumnIdx === fieldIdx}
|
||||||
on:focus
|
on:focus
|
||||||
on:mouseover={() => (hoveredRow = row._id)}
|
on:mouseover={() => ($hoveredRowId = row._id)}
|
||||||
on:click={() => (selectedCell = cellIdx)}
|
on:click={() => ($selectedCellId = cellIdx)}
|
||||||
>
|
>
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={getCellForField(field)}
|
this={getCellForField(field)}
|
||||||
value={data[field.name]}
|
value={data[field.name]}
|
||||||
schema={field.schema}
|
schema={field.schema}
|
||||||
selected={selectedCell === cellIdx}
|
selected={$selectedCellId === cellIdx}
|
||||||
onChange={val => handleChange(row._id, field, val)}
|
onChange={val => handleChange(row._id, field, val)}
|
||||||
readonly={field.schema.autocolumn}
|
readonly={field.schema.autocolumn}
|
||||||
/>
|
/>
|
||||||
|
@ -424,8 +374,8 @@
|
||||||
<!-- Horizontal spacer -->
|
<!-- Horizontal spacer -->
|
||||||
<div
|
<div
|
||||||
class="cell spacer"
|
class="cell spacer"
|
||||||
class:reorder-candidate={reorderCandidateFieldIdx ===
|
class:reordering-target={$reordering.swapColumnIdx ===
|
||||||
fieldConfigs.length}
|
$columns.length}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
@ -434,29 +384,28 @@
|
||||||
class="cell label new"
|
class="cell label new"
|
||||||
on:click={addRow}
|
on:click={addRow}
|
||||||
on:focus
|
on:focus
|
||||||
on:mouseover={() => (hoveredRow = "new")}
|
on:mouseover={() => ($hoveredRowId = "new")}
|
||||||
class:hovered={hoveredRow === "new"}
|
class:hovered={$hoveredRowId === "new"}
|
||||||
>
|
>
|
||||||
<Icon hoverable name="Add" size="S" />
|
<Icon hoverable name="Add" size="S" />
|
||||||
</div>
|
</div>
|
||||||
{#each fieldConfigs as field, fieldIdx}
|
{#each $columns as field, fieldIdx}
|
||||||
<div
|
<div
|
||||||
class="cell new"
|
class="cell new"
|
||||||
class:sticky={fieldIdx === 0}
|
class:sticky={fieldIdx === 0}
|
||||||
class:shadow={horizontallyScrolled}
|
class:shadow={horizontallyScrolled}
|
||||||
class:hovered={hoveredRow === "new"}
|
class:hovered={$hoveredRowId === "new"}
|
||||||
class:reordering={reorderFieldIndex === fieldIdx}
|
class:reordering-source={$reordering.columnIdx === fieldIdx}
|
||||||
class:reorder-candidate={reorderCandidateFieldIdx === fieldIdx}
|
class:reordering-target={$reordering.swapColumnIdx === fieldIdx}
|
||||||
on:click={() => addRow(field)}
|
on:click={() => addRow(field)}
|
||||||
on:focus
|
on:focus
|
||||||
on:mouseover={() => (hoveredRow = "new")}
|
on:mouseover={() => ($hoveredRowId = "new")}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
<!-- Horizontal spacer -->
|
<!-- Horizontal spacer -->
|
||||||
<div
|
<div
|
||||||
class="cell spacer"
|
class="cell spacer"
|
||||||
class:reorder-candidate={reorderCandidateFieldIdx ===
|
class:reordering-target={$reordering.swapColumnIdx === $columns.length}
|
||||||
fieldConfigs.length}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Vertical spacer -->
|
<!-- Vertical spacer -->
|
||||||
|
@ -464,12 +413,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reorder placeholder -->
|
<!-- Reorder placeholder -->
|
||||||
{#if isReordering}
|
<ReorderingPlaceholder />
|
||||||
<div
|
|
||||||
class="reorder-placeholder"
|
|
||||||
style="--x:{reorderPlaceholderX}px;--width:{reorderPlaceholderWidth}px;--height:{reorderPlaceholderHeight}px;"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -630,10 +574,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reordering styles */
|
/* Reordering styles */
|
||||||
.cell.reordering {
|
.cell.reordering-source {
|
||||||
background: var(--spectrum-global-color-gray-200);
|
background: var(--spectrum-global-color-gray-200);
|
||||||
}
|
}
|
||||||
.cell.reorder-candidate {
|
.cell.reordering-target {
|
||||||
border-left-color: var(--spectrum-global-color-blue-400);
|
border-left-color: var(--spectrum-global-color-blue-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -682,14 +626,4 @@
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.reorder-placeholder {
|
|
||||||
height: min(calc(100% - 36px), var(--height));
|
|
||||||
width: var(--width);
|
|
||||||
left: var(--x);
|
|
||||||
position: absolute;
|
|
||||||
top: 36px;
|
|
||||||
background: var(--spectrum-global-color-blue-400);
|
|
||||||
opacity: 0.2;
|
|
||||||
z-index: 7;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { getColor } from "./utils"
|
import { getColor } from "../utils"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let schema
|
export let schema
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { getColor } from "./utils"
|
import { getColor } from "../utils"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
</script>
|
</script>
|
|
@ -0,0 +1,134 @@
|
||||||
|
import { get, writable } from "svelte/store"
|
||||||
|
|
||||||
|
export const createReorderingStores = context => {
|
||||||
|
const { columns, rand, rows } = context
|
||||||
|
const reorderingInitialState = {
|
||||||
|
columnIdx: null,
|
||||||
|
swapColumnIdx: null,
|
||||||
|
breakpoints: [],
|
||||||
|
initialMouseX: null,
|
||||||
|
}
|
||||||
|
const reordering = writable(reorderingInitialState)
|
||||||
|
|
||||||
|
// This is broken into its own store as it is rapidly updated, and we want to
|
||||||
|
// ensure good performance by avoiding updating other components which depend
|
||||||
|
// on other reordering state
|
||||||
|
const placeholderInitialState = {
|
||||||
|
x: null,
|
||||||
|
initialX: null,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
|
}
|
||||||
|
const placeholder = writable(placeholderInitialState)
|
||||||
|
|
||||||
|
// Callback when dragging on a colum header and starting reordering
|
||||||
|
const startReordering = (columnIdx, e) => {
|
||||||
|
// Generate new breakpoints for the current columns
|
||||||
|
let breakpoints = []
|
||||||
|
const cols = get(columns)
|
||||||
|
console.log(cols)
|
||||||
|
cols.forEach((col, idx) => {
|
||||||
|
const header = document.getElementById(`sheet-${rand}-header-${idx}`)
|
||||||
|
const bounds = header.getBoundingClientRect()
|
||||||
|
breakpoints.push(bounds.x)
|
||||||
|
if (idx === cols.length - 1) {
|
||||||
|
breakpoints.push(bounds.x + bounds.width)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get bounds of the selected header and sheet body
|
||||||
|
const self = document.getElementById(`sheet-${rand}-header-${columnIdx}`)
|
||||||
|
const selfBounds = self.getBoundingClientRect()
|
||||||
|
const body = document.getElementById(`sheet-${rand}-body`)
|
||||||
|
const bodyBounds = body.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
reordering.set({
|
||||||
|
columnIdx,
|
||||||
|
breakpoints,
|
||||||
|
swapColumnIdx: null,
|
||||||
|
initialMouseX: e.clientX,
|
||||||
|
})
|
||||||
|
placeholder.set({
|
||||||
|
initialX: selfBounds.x - bodyBounds.x,
|
||||||
|
x: selfBounds.x - bodyBounds.x,
|
||||||
|
width: selfBounds.width,
|
||||||
|
height: (get(rows).length + 2) * 32,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add listeners to handle mouse movement
|
||||||
|
document.addEventListener("mousemove", onReorderMouseMove)
|
||||||
|
document.addEventListener("mouseup", stopReordering)
|
||||||
|
|
||||||
|
// Trigger a move event immediately so ensure a candidate column is chosen
|
||||||
|
onReorderMouseMove(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when moving the mouse when reordering columns
|
||||||
|
const onReorderMouseMove = e => {
|
||||||
|
const $reordering = get(reordering)
|
||||||
|
if ($reordering.columnIdx == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute new placeholder position
|
||||||
|
const $placeholder = get(placeholder)
|
||||||
|
let newX = e.clientX - $reordering.initialMouseX + $placeholder.initialX
|
||||||
|
newX = Math.max(0, newX)
|
||||||
|
|
||||||
|
// Compute the closest breakpoint to the current position
|
||||||
|
let swapColumnIdx
|
||||||
|
let minDistance = Number.MAX_SAFE_INTEGER
|
||||||
|
$reordering.breakpoints.forEach((point, idx) => {
|
||||||
|
const distance = Math.abs(point - e.clientX)
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance
|
||||||
|
swapColumnIdx = idx
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
placeholder.update(state => {
|
||||||
|
state.x = newX
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
if (swapColumnIdx !== $reordering.swapColumnIdx) {
|
||||||
|
reordering.update(state => {
|
||||||
|
state.swapColumnIdx = swapColumnIdx
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when stopping reordering columns
|
||||||
|
const stopReordering = () => {
|
||||||
|
// Swap position of columns
|
||||||
|
let { columnIdx, swapColumnIdx } = get(reordering)
|
||||||
|
const newColumns = get(columns).slice()
|
||||||
|
const removed = newColumns.splice(columnIdx, 1)
|
||||||
|
if (--swapColumnIdx < columnIdx) {
|
||||||
|
swapColumnIdx++
|
||||||
|
}
|
||||||
|
newColumns.splice(swapColumnIdx, 0, removed[0])
|
||||||
|
columns.set(newColumns)
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
reordering.set(reorderingInitialState)
|
||||||
|
placeholder.set(placeholderInitialState)
|
||||||
|
|
||||||
|
// Remove event handlers
|
||||||
|
document.removeEventListener("mousemove", onReorderMouseMove)
|
||||||
|
document.removeEventListener("mouseup", stopReordering)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
reordering: {
|
||||||
|
...reordering,
|
||||||
|
actions: {
|
||||||
|
startReordering,
|
||||||
|
stopReordering,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reorderingPlaceholder: placeholder,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue