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

View File

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

View File

@ -1,4 +1,4 @@
import { get, writable } from "svelte/store"
import { derived, get, writable } from "svelte/store"
export const createColumnsStores = context => {
const { schema } = context
@ -6,6 +6,21 @@ export const createColumnsStores = context => {
const columns = writable([])
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
schema.subscribe($schema => {
const currentColumns = get(columns)
@ -23,19 +38,14 @@ export const createColumnsStores = context => {
})
// Update columns, removing extraneous columns and adding missing ones
let offset = 0
columns.set(
fields.map((field, idx) => {
fields.map(field => {
const existing = currentColumns.find(x => x.name === field)
const newCol = {
idx,
return {
name: field,
width: existing?.width || defaultWidth,
left: offset,
schema: $schema[field],
}
offset += newCol.width
return newCol
})
)
})
@ -60,7 +70,10 @@ export const createColumnsStores = context => {
})
return {
columns,
columns: {
...columns,
subscribe: enrichedColumns.subscribe,
},
stickyColumn,
}
}

View File

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

View File

@ -51,11 +51,6 @@ export const createResizeStores = context => {
} 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]
})
}

View File

@ -7,6 +7,10 @@ export const createRowsStore = context => {
const { config, API } = context
const tableId = derived(config, $config => $config.tableId)
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
let loaded = false
@ -20,7 +24,7 @@ export const createRowsStore = context => {
// Local stores for managing fetching data
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) {
return null
}
@ -35,8 +39,8 @@ export const createRowsStore = context => {
tableId: $tableId,
},
options: {
sortColumn: null,
sortOrder: null,
sortColumn: $sort.column,
sortOrder: $sort.order,
query: $query,
limit: 100,
paginate: true,
@ -241,5 +245,6 @@ export const createRowsStore = context => {
},
},
schema,
sort,
}
}

View File

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