Add support for selecting multiple cells
This commit is contained in:
parent
8a8d835a1a
commit
7349910572
|
@ -3,13 +3,13 @@
|
|||
--ink: #000000;
|
||||
|
||||
/* Brand colours */
|
||||
--bb-coral: #FF4E4E;
|
||||
--bb-coral-light: #F97777;
|
||||
--bb-indigo: #6E56FF;
|
||||
--bb-indigo-light: #9F8FFF;
|
||||
--bb-lime: #ECFFB5;
|
||||
--bb-coral: #ff4e4e;
|
||||
--bb-coral-light: #f97777;
|
||||
--bb-indigo: #6e56ff;
|
||||
--bb-indigo-light: #9f8fff;
|
||||
--bb-lime: #ecffb5;
|
||||
--bb-forest-green: #053835;
|
||||
--bb-beige: #F6EFEA;
|
||||
--bb-beige: #f6efea;
|
||||
|
||||
--grey-1: #fafafa;
|
||||
--grey-2: #f5f5f5;
|
||||
|
@ -49,10 +49,10 @@
|
|||
--rounded-medium: 8px;
|
||||
--rounded-large: 16px;
|
||||
|
||||
--font-sans: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, "Inter",
|
||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
--font-accent: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, "Inter",
|
||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
--font-sans: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI,
|
||||
"Inter", "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
--font-accent: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI,
|
||||
"Inter", "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
--font-serif: "Georgia", Cambria, Times New Roman, Times, serif;
|
||||
--font-mono: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
|
@ -111,7 +111,7 @@ a {
|
|||
/* Custom theme additions */
|
||||
.spectrum--darkest {
|
||||
--drop-shadow: rgba(0, 0, 0, 0.6);
|
||||
--spectrum-global-color-blue-100: rgb(28, 33, 43);
|
||||
--spectrum-global-color-blue-100: rgb(30, 36, 50);
|
||||
}
|
||||
.spectrum--dark {
|
||||
--drop-shadow: rgba(0, 0, 0, 0.3);
|
||||
|
|
|
@ -4,12 +4,19 @@
|
|||
import { getCellRenderer } from "../lib/renderers"
|
||||
import { derived, writable } from "svelte/store"
|
||||
|
||||
const { rows, focusedCellId, focusedCellAPI, menu, config, validation } =
|
||||
getContext("grid")
|
||||
const {
|
||||
rows,
|
||||
focusedCellId,
|
||||
focusedCellAPI,
|
||||
menu,
|
||||
config,
|
||||
validation,
|
||||
cellSelection,
|
||||
} = getContext("grid")
|
||||
|
||||
export let highlighted
|
||||
export let selected
|
||||
export let rowFocused
|
||||
export let rowSelected
|
||||
export let rowIdx
|
||||
export let topRow = false
|
||||
export let focused
|
||||
|
@ -20,6 +27,8 @@
|
|||
export let updateValue = rows.actions.updateValue
|
||||
export let contentLines = 1
|
||||
export let hidden = false
|
||||
export let isSelectingCells = false
|
||||
export let selectedCells = {}
|
||||
|
||||
const emptyError = writable(null)
|
||||
|
||||
|
@ -43,6 +52,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Callbacks for cell selection
|
||||
$: cellSelected = selectedCells[cellId]
|
||||
$: updateSelectionCallback = isSelectingCells ? updateSelection : null
|
||||
$: stopSelectionCallback = isSelectingCells ? stopSelection : null
|
||||
|
||||
const getErrorStore = (selected, cellId) => {
|
||||
if (!selected) {
|
||||
return emptyError
|
||||
|
@ -68,20 +82,38 @@
|
|||
})
|
||||
},
|
||||
}
|
||||
|
||||
const startSelection = e => {
|
||||
if (e.button !== 0) {
|
||||
return
|
||||
}
|
||||
focusedCellId.set(cellId)
|
||||
cellSelection.actions.start(cellId)
|
||||
}
|
||||
|
||||
const updateSelection = e => {
|
||||
cellSelection.actions.update(cellId)
|
||||
}
|
||||
|
||||
const stopSelection = e => {
|
||||
cellSelection.actions.stop()
|
||||
}
|
||||
</script>
|
||||
|
||||
<GridCell
|
||||
{highlighted}
|
||||
{selected}
|
||||
{rowIdx}
|
||||
{topRow}
|
||||
{focused}
|
||||
{selectedUser}
|
||||
{readonly}
|
||||
{hidden}
|
||||
selected={rowSelected || cellSelected}
|
||||
error={$error}
|
||||
on:click={() => focusedCellId.set(cellId)}
|
||||
on:contextmenu={e => menu.actions.open(cellId, e)}
|
||||
on:mousedown={startSelection}
|
||||
on:mouseenter={updateSelectionCallback}
|
||||
on:mouseup={stopSelectionCallback}
|
||||
width={column.width}
|
||||
>
|
||||
<svelte:component
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
on:touchstart
|
||||
on:touchend
|
||||
on:touchcancel
|
||||
on:mouseenter
|
||||
{style}
|
||||
>
|
||||
{#if error}
|
||||
|
@ -155,6 +156,7 @@
|
|||
.cell.focused.readonly {
|
||||
--cell-background: var(--cell-background-hover);
|
||||
}
|
||||
.cell.selected.focused,
|
||||
.cell.selected:not(.focused) {
|
||||
--cell-background: var(--spectrum-global-color-blue-100);
|
||||
}
|
||||
|
|
|
@ -19,10 +19,14 @@
|
|||
dispatch,
|
||||
rows,
|
||||
columnRenderMap,
|
||||
isSelectingCells,
|
||||
selectedCells,
|
||||
selectedCellCount,
|
||||
} = getContext("grid")
|
||||
|
||||
$: rowSelected = !!$selectedRows[row._id]
|
||||
$: rowHovered = $hoveredRowId === row._id
|
||||
$: rowHovered =
|
||||
$hoveredRowId === row._id && (!$selectedCellCount || !$isSelectingCells)
|
||||
$: rowFocused = $focusedRow?._id === row._id
|
||||
$: reorderSource = $reorder.sourceColumn
|
||||
</script>
|
||||
|
@ -43,8 +47,8 @@
|
|||
{column}
|
||||
{row}
|
||||
{rowFocused}
|
||||
{rowSelected}
|
||||
highlighted={rowHovered || rowFocused || reorderSource === column.name}
|
||||
selected={rowSelected}
|
||||
rowIdx={row.__idx}
|
||||
topRow={top}
|
||||
focused={$focusedCellId === cellId}
|
||||
|
@ -52,6 +56,8 @@
|
|||
width={column.width}
|
||||
contentLines={$contentLines}
|
||||
hidden={!$columnRenderMap[column.name]}
|
||||
isSelectingCells={$isSelectingCells}
|
||||
selectedCells={$selectedCells}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
dispatch,
|
||||
contentLines,
|
||||
isDragging,
|
||||
isSelectingCells,
|
||||
selectedCells,
|
||||
selectedCellCount,
|
||||
} = getContext("grid")
|
||||
|
||||
$: rowCount = $rows.length
|
||||
|
@ -70,7 +73,9 @@
|
|||
<GridScrollWrapper scrollVertically attachHandlers>
|
||||
{#each $renderedRows as row, idx}
|
||||
{@const rowSelected = !!$selectedRows[row._id]}
|
||||
{@const rowHovered = $hoveredRowId === row._id}
|
||||
{@const rowHovered =
|
||||
$hoveredRowId === row._id &&
|
||||
(!$selectedCellCount || !$isSelectingCells)}
|
||||
{@const rowFocused = $focusedRow?._id === row._id}
|
||||
{@const cellId = getCellID(row._id, $stickyColumn?.name)}
|
||||
<div
|
||||
|
@ -85,7 +90,7 @@
|
|||
{row}
|
||||
{cellId}
|
||||
{rowFocused}
|
||||
selected={rowSelected}
|
||||
{rowSelected}
|
||||
highlighted={rowHovered || rowFocused}
|
||||
rowIdx={row.__idx}
|
||||
topRow={idx === 0}
|
||||
|
@ -94,6 +99,8 @@
|
|||
width={$stickyColumn.width}
|
||||
column={$stickyColumn}
|
||||
contentLines={$contentLines}
|
||||
isSelectingCells={$isSelectingCells}
|
||||
selectedCells={$selectedCells}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
notifications,
|
||||
hasBudibaseIdentifiers,
|
||||
selectedRowCount,
|
||||
selectedRows,
|
||||
} = getContext("grid")
|
||||
|
||||
let anchor
|
||||
|
@ -83,6 +82,25 @@
|
|||
>
|
||||
Delete {$selectedRowCount} rows
|
||||
</MenuItem>
|
||||
{:else if $menu.multiCellMode}
|
||||
<MenuItem
|
||||
icon="Copy"
|
||||
on:click={clipboard.actions.copy}
|
||||
on:click={menu.actions.close}
|
||||
>
|
||||
Copy
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="Paste"
|
||||
disabled={$copiedCell == null || $focusedCellAPI?.isReadonly()}
|
||||
on:click={clipboard.actions.paste}
|
||||
on:click={menu.actions.close}
|
||||
>
|
||||
Paste
|
||||
</MenuItem>
|
||||
<MenuItem icon="Delete" disabled={isNewRow} on:click={() => {}}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
{:else}
|
||||
<MenuItem
|
||||
icon="Copy"
|
||||
|
|
|
@ -45,7 +45,7 @@ export const createStores = () => {
|
|||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { columns, stickyColumn } = context
|
||||
const { columns, stickyColumn, visibleColumns } = context
|
||||
|
||||
// Quick access to all columns
|
||||
const allColumns = derived(
|
||||
|
@ -67,9 +67,19 @@ export const deriveStores = context => {
|
|||
return normalCols.length > 0
|
||||
})
|
||||
|
||||
// Derive a lookup map for column indices by name
|
||||
const columnLookupMap = derived(visibleColumns, $visibleColumns => {
|
||||
let map = {}
|
||||
$visibleColumns.forEach((column, idx) => {
|
||||
map[column.name] = idx
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
return {
|
||||
allColumns,
|
||||
hasNonAutoColumn,
|
||||
columnLookupMap,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import * as Table from "./datasources/table"
|
|||
import * as ViewV2 from "./datasources/viewV2"
|
||||
import * as NonPlus from "./datasources/nonPlus"
|
||||
import * as Cache from "./cache"
|
||||
import * as Selection from "./selection"
|
||||
|
||||
const DependencyOrderedStores = [
|
||||
Sort,
|
||||
|
@ -44,6 +45,7 @@ const DependencyOrderedStores = [
|
|||
Config,
|
||||
Notifications,
|
||||
Cache,
|
||||
Selection,
|
||||
]
|
||||
|
||||
export const attachStores = context => {
|
||||
|
|
|
@ -3,10 +3,11 @@ import { parseCellID } from "../lib/utils"
|
|||
|
||||
export const createStores = () => {
|
||||
const menu = writable({
|
||||
x: 0,
|
||||
y: 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
visible: false,
|
||||
selectedRow: null,
|
||||
multiRowMode: false,
|
||||
multiCellMode: false,
|
||||
})
|
||||
return {
|
||||
menu,
|
||||
|
@ -14,8 +15,15 @@ export const createStores = () => {
|
|||
}
|
||||
|
||||
export const createActions = context => {
|
||||
const { menu, focusedCellId, gridID, selectedRows, selectedRowCount } =
|
||||
context
|
||||
const {
|
||||
menu,
|
||||
focusedCellId,
|
||||
gridID,
|
||||
selectedRows,
|
||||
selectedRowCount,
|
||||
selectedCells,
|
||||
selectedCellCount,
|
||||
} = context
|
||||
|
||||
const open = (cellId, e) => {
|
||||
e.preventDefault()
|
||||
|
@ -32,7 +40,7 @@ export const createActions = context => {
|
|||
const targetBounds = e.target.getBoundingClientRect()
|
||||
const dataBounds = dataNode.getBoundingClientRect()
|
||||
|
||||
// Check if there are multiple rows selected, and this is one of them
|
||||
// Check if there are multiple rows selected, and if this is one of them
|
||||
let multiRowMode = false
|
||||
if (get(selectedRowCount) > 1) {
|
||||
const rowId = parseCellID(cellId).id
|
||||
|
@ -41,8 +49,16 @@ export const createActions = context => {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if there are multiple cells selected, and if this is one of them
|
||||
let multiCellMode = false
|
||||
if (!multiRowMode && get(selectedCellCount) > 1) {
|
||||
if (get(selectedCells)[cellId]) {
|
||||
multiCellMode = true
|
||||
}
|
||||
}
|
||||
|
||||
// Only focus this cell if not in multi row mode
|
||||
if (!multiRowMode) {
|
||||
if (!multiRowMode && !multiCellMode) {
|
||||
focusedCellId.set(cellId)
|
||||
}
|
||||
|
||||
|
@ -51,6 +67,7 @@ export const createActions = context => {
|
|||
top: targetBounds.top - dataBounds.top + e.offsetY,
|
||||
visible: true,
|
||||
multiRowMode,
|
||||
multiCellMode,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import { derived, writable, get } from "svelte/store"
|
||||
import { getCellID, parseCellID } from "../lib/utils"
|
||||
|
||||
export const createStores = () => {
|
||||
const cellSelection = writable({
|
||||
active: false,
|
||||
sourceCellId: null,
|
||||
targetCellId: null,
|
||||
})
|
||||
|
||||
return {
|
||||
cellSelection,
|
||||
}
|
||||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { cellSelection, rowLookupMap, columnLookupMap, rows, visibleColumns } =
|
||||
context
|
||||
|
||||
const isSelectingCells = derived(cellSelection, $cellSelection => {
|
||||
return $cellSelection.active
|
||||
})
|
||||
|
||||
const selectedCells = derived(
|
||||
[cellSelection, rowLookupMap, columnLookupMap],
|
||||
([$cellSelection, $rowLookupMap, $columnLookupMap]) => {
|
||||
const { sourceCellId, targetCellId } = $cellSelection
|
||||
if (!sourceCellId || !targetCellId || sourceCellId === targetCellId) {
|
||||
return {}
|
||||
}
|
||||
const $rows = get(rows)
|
||||
const $visibleColumns = get(visibleColumns)
|
||||
|
||||
// Get source and target row and column indices
|
||||
const sourceInfo = parseCellID(sourceCellId)
|
||||
const targetInfo = parseCellID(targetCellId)
|
||||
|
||||
// Row indices
|
||||
const sourceRowIndex = $rowLookupMap[sourceInfo.id]
|
||||
const targetRowIndex = $rowLookupMap[targetInfo.id]
|
||||
const lowerRowIndex = Math.min(sourceRowIndex, targetRowIndex)
|
||||
const upperRowIndex = Math.max(sourceRowIndex, targetRowIndex)
|
||||
|
||||
// Column indices
|
||||
const sourceColIndex = $columnLookupMap[sourceInfo.field]
|
||||
const targetColIndex = $columnLookupMap[targetInfo.field]
|
||||
const lowerColIndex = Math.min(sourceColIndex, targetColIndex)
|
||||
const upperColIndex = Math.max(sourceColIndex, targetColIndex)
|
||||
|
||||
// Build map of all cells inside these bounds
|
||||
let map = {}
|
||||
let rowId, colName, cellId
|
||||
for (let rowIdx = lowerRowIndex; rowIdx <= upperRowIndex; rowIdx++) {
|
||||
for (let colIdx = lowerColIndex; colIdx <= upperColIndex; colIdx++) {
|
||||
rowId = $rows[rowIdx]._id
|
||||
colName = $visibleColumns[colIdx].name
|
||||
cellId = getCellID(rowId, colName)
|
||||
map[cellId] = true
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
)
|
||||
|
||||
const selectedCellCount = derived(selectedCells, $selectedCells => {
|
||||
return Object.keys($selectedCells).length
|
||||
})
|
||||
|
||||
return {
|
||||
isSelectingCells,
|
||||
selectedCells,
|
||||
selectedCellCount,
|
||||
}
|
||||
}
|
||||
|
||||
export const createActions = context => {
|
||||
const { cellSelection } = context
|
||||
|
||||
const startCellSelection = sourceCellId => {
|
||||
cellSelection.set({
|
||||
active: true,
|
||||
sourceCellId,
|
||||
targetCellId: sourceCellId,
|
||||
})
|
||||
}
|
||||
|
||||
const updateCellSelection = targetCellId => {
|
||||
cellSelection.update(state => ({
|
||||
...state,
|
||||
targetCellId,
|
||||
}))
|
||||
}
|
||||
|
||||
const stopCellSelection = () => {
|
||||
cellSelection.update(state => ({
|
||||
...state,
|
||||
active: false,
|
||||
}))
|
||||
}
|
||||
|
||||
const clearCellSelection = () => {
|
||||
cellSelection.set({
|
||||
active: false,
|
||||
sourceCellId: null,
|
||||
targetCellId: null,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
cellSelection: {
|
||||
...cellSelection,
|
||||
actions: {
|
||||
start: startCellSelection,
|
||||
update: updateCellSelection,
|
||||
stop: stopCellSelection,
|
||||
clear: clearCellSelection,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const initialise = context => {
|
||||
const { selectedCellCount, selectedRowCount, selectedRows } = context
|
||||
|
||||
selectedCellCount.subscribe($selectedCellCount => {
|
||||
if ($selectedCellCount && get(selectedRowCount)) {
|
||||
selectedRows.set({})
|
||||
}
|
||||
})
|
||||
}
|
|
@ -188,6 +188,9 @@ export const initialise = context => {
|
|||
rowHeight,
|
||||
fixedRowHeight,
|
||||
selectedRowCount,
|
||||
menu,
|
||||
cellSelection,
|
||||
selectedCellCount,
|
||||
} = context
|
||||
|
||||
// Ensure we clear invalid rows from state if they disappear
|
||||
|
@ -248,6 +251,14 @@ export const initialise = context => {
|
|||
if (id && get(selectedRowCount)) {
|
||||
selectedRows.set({})
|
||||
}
|
||||
|
||||
// Clear cell selection when focusing a cell
|
||||
if (id && get(selectedCellCount)) {
|
||||
cellSelection.actions.clear()
|
||||
}
|
||||
|
||||
// Close the menu if it was open
|
||||
menu.actions.close()
|
||||
})
|
||||
|
||||
// Pull row height from table as long as we don't have a fixed height
|
||||
|
@ -268,8 +279,13 @@ export const initialise = context => {
|
|||
|
||||
// Clear focused cell when selecting rows
|
||||
selectedRowCount.subscribe(count => {
|
||||
if (get(focusedCellId) && count) {
|
||||
if (count) {
|
||||
if (get(focusedCellId)) {
|
||||
focusedCellId.set(null)
|
||||
}
|
||||
if (get(selectedCellCount)) {
|
||||
cellSelection.actions.clear()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -16,5 +16,5 @@
|
|||
/* Custom additions */
|
||||
--modal-background: var(--spectrum-global-color-gray-50);
|
||||
--drop-shadow: rgba(0, 0, 0, 0.25) !important;
|
||||
--spectrum-global-color-blue-100: rgba(35, 40, 50) !important;
|
||||
--spectrum-global-color-blue-100: rgba(36, 44, 64) !important;
|
||||
}
|
||||
|
|
|
@ -49,5 +49,5 @@
|
|||
/* Custom additions */
|
||||
--modal-background: var(--spectrum-global-color-gray-50);
|
||||
--drop-shadow: rgba(0, 0, 0, 0.15) !important;
|
||||
--spectrum-global-color-blue-100: rgb(56, 65, 84) !important;
|
||||
--spectrum-global-color-blue-100: rgb(56, 65, 90) !important;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue