Add column sorting and reordering via popover

This commit is contained in:
Andrew Kingston 2023-03-07 08:15:27 +00:00
parent f0ac9e9d9c
commit 9579c9c0d2
7 changed files with 136 additions and 61 deletions

View File

@ -60,7 +60,7 @@
context = { ...context, ...createUserStores(context) } context = { ...context, ...createUserStores(context) }
// Reference some stores for local use // Reference some stores for local use
const isResizing = context.isResizing const { isResizing, isReordering } = context
// Keep config store up to date // Keep config store up to date
$: config.set({ $: config.set({
@ -80,6 +80,7 @@
<div <div
class="sheet" class="sheet"
class:is-resizing={$isResizing} class:is-resizing={$isResizing}
class:is-reordering={$isReordering}
style="--cell-height:{cellHeight}px;" style="--cell-height:{cellHeight}px;"
id="sheet-{rand}" id="sheet-{rand}"
> >
@ -129,6 +130,9 @@
.sheet.is-resizing :global(*) { .sheet.is-resizing :global(*) {
cursor: col-resize !important; cursor: col-resize !important;
} }
.sheet.is-reordering :global(*) {
cursor: grabbing !important;
}
.sheet-data { .sheet-data {
flex: 1 1 auto; flex: 1 1 auto;

View File

@ -7,30 +7,51 @@
export let column export let column
export let orderable = true export let orderable = true
const { reorder, isReordering, isResizing, rand } = getContext("sheet") const { reorder, isReordering, isResizing, rand, sort, columns } =
getContext("sheet")
let timeout
let anchor let anchor
let open = false let open = false
let isClick = true let timeout
$: sortedBy = column.name === $sort.column
$: canMoveLeft = orderable && column.idx > 0
$: canMoveRight = orderable && column.idx < $columns.length - 1
const startReordering = e => { const startReordering = e => {
isClick = true
timeout = setTimeout(() => { timeout = setTimeout(() => {
isClick = false
reorder.actions.startReordering(column.name, e) reorder.actions.startReordering(column.name, e)
}, 250) }, 200)
} }
const stopReordering = () => { const stopReordering = () => {
clearTimeout(timeout) clearTimeout(timeout)
} }
const onClick = () => { const sortAscending = () => {
if (isClick) { sort.set({
stopReordering() column: column.name,
open = true order: "ascending",
} })
open = false
}
const sortDescending = () => {
sort.set({
column: column.name,
order: "descending",
})
open = false
}
const moveLeft = () => {
reorder.actions.moveColumnLeft(column.name)
open = false
}
const moveRight = () => {
reorder.actions.moveColumnRight(column.name)
open = false
} }
</script> </script>
@ -40,26 +61,34 @@
style="flex: 0 0 {column.width}px;" style="flex: 0 0 {column.width}px;"
bind:this={anchor} bind:this={anchor}
class:disabled={$isReordering || $isResizing} class:disabled={$isReordering || $isResizing}
class:sorted={sortedBy}
> >
<SheetCell <SheetCell
reorderSource={$reorder.sourceColumn === column.name} reorderSource={$reorder.sourceColumn === column.name}
reorderTarget={$reorder.targetColumn === column.name} reorderTarget={$reorder.targetColumn === column.name}
on:mousedown={orderable ? startReordering : null} on:mousedown={orderable ? startReordering : null}
on:mouseup={orderable ? stopReordering : null} on:mouseup={orderable ? stopReordering : null}
on:click={onClick}
width={column.width} width={column.width}
left={column.left} left={column.left}
> >
<Icon <Icon
size="S" size="S"
name={getIconForField(column)} name={getIconForField(column)}
color="var(--spectrum-global-color-gray-600)" color={`var(--spectrum-global-color-gray-600)`}
/> />
<div class="name"> <div class="name">
{column.name} {column.name}
</div> </div>
<div class="more"> <div
<Icon size="S" name="MoreVertical" /> class="more"
on:mousedown|stopPropagation
on:click={() => (open = true)}
>
<Icon
size="S"
name="MoreVertical"
color={`var(--spectrum-global-color-gray-600)`}
/>
</div> </div>
</SheetCell> </SheetCell>
</div> </div>
@ -67,19 +96,25 @@
<Popover <Popover
bind:open bind:open
{anchor} {anchor}
align="left" align="right"
offset={0} offset={0}
popoverTarget={document.getElementById(`sheet-${rand}`)} popoverTarget={document.getElementById(`sheet-${rand}`)}
animate={false} animate={false}
> >
<Menu> <Menu>
<MenuItem icon="Edit">Edit column</MenuItem> <MenuItem icon="Edit">Edit column</MenuItem>
<MenuItem icon="SortOrderUp">Sort ascending</MenuItem> <MenuItem icon="SortOrderUp" on:click={sortAscending}>
<MenuItem icon="SortOrderDown">Sort descending</MenuItem> Sort ascending
{#if orderable} </MenuItem>
<MenuItem icon="ArrowLeft">Move left</MenuItem> <MenuItem icon="SortOrderDown" on:click={sortDescending}>
<MenuItem icon="ArrowRight">Move right</MenuItem> Sort descending
{/if} </MenuItem>
<MenuItem disabled={!canMoveLeft} icon="ArrowLeft" on:click={moveLeft}>
Move left
</MenuItem>
<MenuItem disabled={!canMoveRight} icon="ArrowRight" on:click={moveRight}>
Move right
</MenuItem>
<MenuItem icon="Delete">Delete</MenuItem> <MenuItem icon="Delete">Delete</MenuItem>
</Menu> </Menu>
</Popover> </Popover>
@ -88,17 +123,15 @@
.header-cell { .header-cell {
display: flex; display: flex;
} }
.header-cell:not(.disabled):hover :global(.cell),
.header-cell:not(.disabled).open :global(.cell) {
cursor: pointer;
background: var(--spectrum-global-color-gray-200);
}
.header-cell :global(.cell) { .header-cell :global(.cell) {
background: var(--background); background: var(--background);
padding: 0 var(--cell-padding); padding: 0 var(--cell-padding);
gap: calc(2 * var(--cell-spacing)); gap: calc(2 * var(--cell-spacing));
border-bottom: none; border-bottom: none;
} }
.header-cell.sorted :global(.cell) {
}
.name { .name {
flex: 1 1 auto; flex: 1 1 auto;
@ -109,10 +142,13 @@
} }
.more { .more {
display: none; padding: 4px;
margin: 0 -4px;
} }
.header-cell:not(.disabled):hover .more, .more:hover {
.header-cell:not(.disabled).open .more { cursor: pointer;
display: block; }
.more:hover :global(.spectrum-Icon) {
color: var(--spectrum-global-color-gray-800) !important;
} }
</style> </style>

View File

@ -1,4 +1,4 @@
import { get, writable } from "svelte/store" import { derived, get, writable } from "svelte/store"
export const createColumnsStores = context => { export const createColumnsStores = context => {
const { schema } = context const { schema } = context
@ -6,6 +6,21 @@ export const createColumnsStores = context => {
const columns = writable([]) const columns = writable([])
const stickyColumn = writable(null) const stickyColumn = writable(null)
// Derive an enriched version of columns with left offsets and indexes
// automatically calculated
const enrichedColumns = derived(columns, $columns => {
let offset = 0
return $columns.map((column, idx) => {
const enriched = {
...column,
idx,
left: offset,
}
offset += column.width
return enriched
})
})
// Merge new schema fields with existing schema in order to preserve widths // Merge new schema fields with existing schema in order to preserve widths
schema.subscribe($schema => { schema.subscribe($schema => {
const currentColumns = get(columns) const currentColumns = get(columns)
@ -23,19 +38,14 @@ export const createColumnsStores = context => {
}) })
// Update columns, removing extraneous columns and adding missing ones // Update columns, removing extraneous columns and adding missing ones
let offset = 0
columns.set( columns.set(
fields.map((field, idx) => { fields.map(field => {
const existing = currentColumns.find(x => x.name === field) const existing = currentColumns.find(x => x.name === field)
const newCol = { return {
idx,
name: field, name: field,
width: existing?.width || defaultWidth, width: existing?.width || defaultWidth,
left: offset,
schema: $schema[field], schema: $schema[field],
} }
offset += newCol.width
return newCol
}) })
) )
}) })
@ -60,7 +70,10 @@ export const createColumnsStores = context => {
}) })
return { return {
columns, columns: {
...columns,
subscribe: enrichedColumns.subscribe,
},
stickyColumn, stickyColumn,
} }
} }

View File

@ -77,27 +77,19 @@ export const createReorderStores = context => {
// Callback when stopping reordering columns // Callback when stopping reordering columns
const stopReordering = () => { const stopReordering = () => {
// Swap position of columns // Swap position of columns
const $columns = get(columns)
let { sourceColumn, targetColumn } = get(reorder) let { sourceColumn, targetColumn } = get(reorder)
const $columns = get(columns)
let sourceIdx = $columns.findIndex(x => x.name === sourceColumn) let sourceIdx = $columns.findIndex(x => x.name === sourceColumn)
let targetIdx = $columns.findIndex(x => x.name === targetColumn) let targetIdx = $columns.findIndex(x => x.name === targetColumn)
targetIdx++ targetIdx++
console.log(sourceIdx, targetIdx)
columns.update(state => { columns.update(state => {
const removed = state.splice(sourceIdx, 1) const removed = state.splice(sourceIdx, 1)
if (--targetIdx < sourceIdx) { if (--targetIdx < sourceIdx) {
targetIdx++ targetIdx++
} }
state.splice(targetIdx, 0, removed[0]) state.splice(targetIdx, 0, removed[0])
let offset = 0 return state.slice()
return state.map((col, idx) => {
const newCol = {
...col,
idx,
left: offset,
}
offset += col.width
return newCol
})
}) })
// Reset state // Reset state
@ -108,12 +100,42 @@ export const createReorderStores = context => {
document.removeEventListener("mouseup", stopReordering) document.removeEventListener("mouseup", stopReordering)
} }
const moveColumnLeft = column => {
const $columns = get(columns)
const sourceIdx = $columns.findIndex(x => x.name === column)
if (sourceIdx === 0) {
return
}
columns.update(state => {
let tmp = state[sourceIdx]
state[sourceIdx] = state[sourceIdx - 1]
state[sourceIdx - 1] = tmp
return state.slice()
})
}
const moveColumnRight = column => {
const $columns = get(columns)
const sourceIdx = $columns.findIndex(x => x.name === column)
if (sourceIdx === $columns.length - 1) {
return
}
columns.update(state => {
let tmp = state[sourceIdx]
state[sourceIdx] = state[sourceIdx + 1]
state[sourceIdx + 1] = tmp
return state.slice()
})
}
return { return {
reorder: { reorder: {
...reorder, ...reorder,
actions: { actions: {
startReordering, startReordering,
stopReordering, stopReordering,
moveColumnLeft,
moveColumnRight,
}, },
}, },
isReordering, isReordering,

View File

@ -51,11 +51,6 @@ export const createResizeStores = context => {
} else { } else {
columns.update(state => { columns.update(state => {
state[columnIdx].width = newWidth 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] return [...state]
}) })
} }

View File

@ -7,6 +7,10 @@ export const createRowsStore = context => {
const { config, API } = context const { config, API } = context
const tableId = derived(config, $config => $config.tableId) const tableId = derived(config, $config => $config.tableId)
const filter = derived(config, $config => $config.filter) const filter = derived(config, $config => $config.filter)
const sort = writable({
column: null,
order: "ascending",
})
// Flag for whether this is the first time loading our fetch // Flag for whether this is the first time loading our fetch
let loaded = false let loaded = false
@ -20,7 +24,7 @@ export const createRowsStore = context => {
// Local stores for managing fetching data // Local stores for managing fetching data
const query = derived(filter, $filter => buildLuceneQuery($filter)) const query = derived(filter, $filter => buildLuceneQuery($filter))
const fetch = derived([tableId, query], ([$tableId, $query]) => { const fetch = derived([tableId, query, sort], ([$tableId, $query, $sort]) => {
if (!$tableId) { if (!$tableId) {
return null return null
} }
@ -35,8 +39,8 @@ export const createRowsStore = context => {
tableId: $tableId, tableId: $tableId,
}, },
options: { options: {
sortColumn: null, sortColumn: $sort.column,
sortOrder: null, sortOrder: $sort.order,
query: $query, query: $query,
limit: 100, limit: 100,
paginate: true, paginate: true,
@ -241,5 +245,6 @@ export const createRowsStore = context => {
}, },
}, },
schema, schema,
sort,
} }
} }

View File

@ -8,7 +8,7 @@ export const getColor = (idx, opacity = 0.3) => {
export const getIconForField = field => { export const getIconForField = field => {
const type = field.schema.type const type = field.schema.type
if (type === "options") { if (type === "options") {
return "ChevronDown" return "Dropdown"
} else if (type === "datetime") { } else if (type === "datetime") {
return "Date" return "Date"
} }