Add column sorting and reordering via popover
This commit is contained in:
parent
f0ac9e9d9c
commit
9579c9c0d2
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue