diff --git a/packages/frontend-core/src/components/grid/cells/GridCell.svelte b/packages/frontend-core/src/components/grid/cells/GridCell.svelte index 74d98ec130..32a9dea83b 100644 --- a/packages/frontend-core/src/components/grid/cells/GridCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/GridCell.svelte @@ -43,6 +43,9 @@ on:mouseup on:click on:contextmenu + on:touchstart + on:touchend + on:touchcancel {style} > {#if error} diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index 657f618759..8a10556da9 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -18,7 +18,6 @@ export let column export let idx - export let orderable = true const { reorder, @@ -66,6 +65,7 @@ $: resetSearchValue(column.name) $: searching = searchValue != null $: debouncedUpdateFilter(searchValue) + $: orderable = !column.primaryDisplay const getSortingLabels = type => { switch (type) { @@ -112,16 +112,17 @@ } const onMouseDown = e => { - if (e.button === 0 && orderable) { + if ((e.touches?.length || e.button === 0) && orderable) { timeout = setTimeout(() => { reorder.actions.startReordering(column.name, e) }, 200) } } - const onMouseUp = e => { - if (e.button === 0 && orderable) { + const onMouseUp = () => { + if (timeout) { clearTimeout(timeout) + timeout = null } } @@ -258,6 +259,9 @@ Use as display column @@ -378,7 +383,7 @@ Move right diff --git a/packages/frontend-core/src/components/grid/controls/HideColumnsButton.svelte b/packages/frontend-core/src/components/grid/controls/HideColumnsButton.svelte index 01c9dc648b..4e19e64297 100644 --- a/packages/frontend-core/src/components/grid/controls/HideColumnsButton.svelte +++ b/packages/frontend-core/src/components/grid/controls/HideColumnsButton.svelte @@ -81,6 +81,7 @@ size="S" value={column.visible} on:change={e => toggleVisibility(column, e.detail)} + disabled={column.primaryDisplay} /> {/each} diff --git a/packages/frontend-core/src/components/grid/layout/NewRow.svelte b/packages/frontend-core/src/components/grid/layout/NewRow.svelte index 66c42e5303..f5de870f7e 100644 --- a/packages/frontend-core/src/components/grid/layout/NewRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/NewRow.svelte @@ -30,6 +30,7 @@ refreshing, config, filter, + inlineFilters, columnRenderMap, } = getContext("grid") @@ -157,7 +158,11 @@ {#if !visible && !selectedRowCount && $config.canAddRows} diff --git a/packages/frontend-core/src/components/grid/lib/utils.js b/packages/frontend-core/src/components/grid/lib/utils.js index c7c618e6f8..02e6d66e0e 100644 --- a/packages/frontend-core/src/components/grid/lib/utils.js +++ b/packages/frontend-core/src/components/grid/lib/utils.js @@ -20,3 +20,10 @@ export const getColumnIcon = column => { return result || "Text" } + +export const parseEventLocation = e => { + return { + x: e.clientX ?? e.touches?.[0]?.clientX, + y: e.clientY ?? e.touches?.[0]?.clientY, + } +} diff --git a/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte b/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte index 1140962583..e564108430 100644 --- a/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte @@ -21,6 +21,7 @@ class="resize-slider" class:visible={activeColumn === $stickyColumn.name} on:mousedown={e => resize.actions.startResizing($stickyColumn, e)} + on:touchstart={e => resize.actions.startResizing($stickyColumn, e)} on:dblclick={() => resize.actions.resetSize($stickyColumn)} style="left:{GutterWidth + $stickyColumn.width}px;" > @@ -32,6 +33,7 @@ class="resize-slider" class:visible={activeColumn === column.name} on:mousedown={e => resize.actions.startResizing(column, e)} + on:touchstart={e => resize.actions.startResizing(column, e)} on:dblclick={() => resize.actions.resetSize(column)} style={getStyle(column, offset, $scrollLeft)} > diff --git a/packages/frontend-core/src/components/grid/overlays/ScrollOverlay.svelte b/packages/frontend-core/src/components/grid/overlays/ScrollOverlay.svelte index 43a64f3fbd..e0ead3727c 100644 --- a/packages/frontend-core/src/components/grid/overlays/ScrollOverlay.svelte +++ b/packages/frontend-core/src/components/grid/overlays/ScrollOverlay.svelte @@ -2,6 +2,7 @@ import { getContext } from "svelte" import { domDebounce } from "../../../utils/utils" import { DefaultRowHeight, ScrollBarSize } from "../lib/constants" + import { parseEventLocation } from "../lib/utils" const { scroll, @@ -53,17 +54,10 @@ } } - const getLocation = e => { - return { - y: e.touches?.[0]?.clientY ?? e.clientY, - x: e.touches?.[0]?.clientX ?? e.clientX, - } - } - // V scrollbar drag handlers const startVDragging = e => { e.preventDefault() - initialMouse = getLocation(e).y + initialMouse = parseEventLocation(e).y initialScroll = $scrollTop document.addEventListener("mousemove", moveVDragging) document.addEventListener("touchmove", moveVDragging) @@ -73,7 +67,7 @@ closeMenu() } const moveVDragging = domDebounce(e => { - const delta = getLocation(e).y - initialMouse + const delta = parseEventLocation(e).y - initialMouse const weight = delta / availHeight const newScrollTop = initialScroll + weight * $maxScrollTop scroll.update(state => ({ @@ -92,7 +86,7 @@ // H scrollbar drag handlers const startHDragging = e => { e.preventDefault() - initialMouse = getLocation(e).x + initialMouse = parseEventLocation(e).x initialScroll = $scrollLeft document.addEventListener("mousemove", moveHDragging) document.addEventListener("touchmove", moveHDragging) @@ -102,7 +96,7 @@ closeMenu() } const moveHDragging = domDebounce(e => { - const delta = getLocation(e).x - initialMouse + const delta = parseEventLocation(e).x - initialMouse const weight = delta / availWidth const newScrollLeft = initialScroll + weight * $maxScrollLeft scroll.update(state => ({ diff --git a/packages/frontend-core/src/components/grid/stores/columns.js b/packages/frontend-core/src/components/grid/stores/columns.js index 31638e7c79..8ceaae105f 100644 --- a/packages/frontend-core/src/components/grid/stores/columns.js +++ b/packages/frontend-core/src/components/grid/stores/columns.js @@ -48,22 +48,28 @@ export const createStores = () => { export const deriveStores = context => { const { columns, stickyColumn } = context - // Derive if we have any normal columns - const hasNonAutoColumn = derived( + // Quick access to all columns + const allColumns = derived( [columns, stickyColumn], ([$columns, $stickyColumn]) => { let allCols = $columns || [] if ($stickyColumn) { allCols = [...allCols, $stickyColumn] } - const normalCols = allCols.filter(column => { - return !column.schema?.autocolumn - }) - return normalCols.length > 0 + return allCols } ) + // Derive if we have any normal columns + const hasNonAutoColumn = derived(allColumns, $allColumns => { + const normalCols = $allColumns.filter(column => { + return !column.schema?.autocolumn + }) + return normalCols.length > 0 + }) + return { + allColumns, hasNonAutoColumn, } } @@ -142,24 +148,26 @@ export const createActions = context => { } export const initialise = context => { - const { definition, columns, stickyColumn, enrichedSchema } = context + const { + definition, + columns, + stickyColumn, + allColumns, + enrichedSchema, + compact, + } = context // Merge new schema fields with existing schema in order to preserve widths - enrichedSchema.subscribe($enrichedSchema => { + const processColumns = $enrichedSchema => { if (!$enrichedSchema) { columns.set([]) stickyColumn.set(null) return } const $definition = get(definition) - const $columns = get(columns) + const $allColumns = get(allColumns) const $stickyColumn = get(stickyColumn) - - // Generate array of all columns to easily find pre-existing columns - let allColumns = $columns || [] - if ($stickyColumn) { - allColumns.push($stickyColumn) - } + const $compact = get(compact) // Find primary display let primaryDisplay @@ -171,7 +179,7 @@ export const initialise = context => { // Get field list let fields = [] Object.keys($enrichedSchema).forEach(field => { - if (field !== primaryDisplay) { + if ($compact || field !== primaryDisplay) { fields.push(field) } }) @@ -181,7 +189,7 @@ export const initialise = context => { fields .map(field => { const fieldSchema = $enrichedSchema[field] - const oldColumn = allColumns?.find(x => x.name === field) + const oldColumn = $allColumns?.find(x => x.name === field) return { name: field, label: fieldSchema.displayName || field, @@ -189,9 +197,18 @@ export const initialise = context => { width: fieldSchema.width || oldColumn?.width || DefaultColumnWidth, visible: fieldSchema.visible ?? true, order: fieldSchema.order ?? oldColumn?.order, + primaryDisplay: field === primaryDisplay, } }) .sort((a, b) => { + // If we don't have a pinned column then primary display will be in + // the normal columns list, and should be first + if (a.name === primaryDisplay) { + return -1 + } else if (b.name === primaryDisplay) { + return 1 + } + // Sort by order first const orderA = a.order const orderB = b.order @@ -214,12 +231,12 @@ export const initialise = context => { ) // Update sticky column - if (!primaryDisplay) { + if ($compact || !primaryDisplay) { stickyColumn.set(null) return } const stickySchema = $enrichedSchema[primaryDisplay] - const oldStickyColumn = allColumns?.find(x => x.name === primaryDisplay) + const oldStickyColumn = $allColumns?.find(x => x.name === primaryDisplay) stickyColumn.set({ name: primaryDisplay, label: stickySchema.displayName || primaryDisplay, @@ -228,6 +245,13 @@ export const initialise = context => { visible: true, order: 0, left: GutterWidth, + primaryDisplay: true, }) - }) + } + + // Process columns when schema changes + enrichedSchema.subscribe(processColumns) + + // Process columns when compact flag changes + compact.subscribe(() => processColumns(get(enrichedSchema))) } diff --git a/packages/frontend-core/src/components/grid/stores/reorder.js b/packages/frontend-core/src/components/grid/stores/reorder.js index f820593174..c068f82cba 100644 --- a/packages/frontend-core/src/components/grid/stores/reorder.js +++ b/packages/frontend-core/src/components/grid/stores/reorder.js @@ -1,4 +1,5 @@ import { get, writable, derived } from "svelte/store" +import { parseEventLocation } from "../lib/utils" const reorderInitialState = { sourceColumn: null, @@ -33,6 +34,7 @@ export const createActions = context => { stickyColumn, ui, maxScrollLeft, + width, } = context let autoScrollInterval @@ -55,6 +57,11 @@ export const createActions = context => { x: 0, column: $stickyColumn.name, }) + } else if (!$visibleColumns[0].primaryDisplay) { + breakpoints.unshift({ + x: 0, + column: null, + }) } // Update state @@ -69,6 +76,9 @@ export const createActions = context => { // Add listeners to handle mouse movement document.addEventListener("mousemove", onReorderMouseMove) document.addEventListener("mouseup", stopReordering) + document.addEventListener("touchmove", onReorderMouseMove) + document.addEventListener("touchend", stopReordering) + document.addEventListener("touchcancel", stopReordering) // Trigger a move event immediately so ensure a candidate column is chosen onReorderMouseMove(e) @@ -77,7 +87,7 @@ export const createActions = context => { // Callback when moving the mouse when reordering columns const onReorderMouseMove = e => { // Immediately handle the current position - const x = e.clientX + const { x } = parseEventLocation(e) reorder.update(state => ({ ...state, latestX: x, @@ -86,7 +96,7 @@ export const createActions = context => { // Check if we need to start auto-scrolling const $reorder = get(reorder) - const proximityCutoff = 140 + const proximityCutoff = Math.min(140, get(width) / 6) const speedFactor = 8 const rightProximity = Math.max(0, $reorder.gridLeft + $reorder.width - x) const leftProximity = Math.max(0, x - $reorder.gridLeft) @@ -158,19 +168,22 @@ export const createActions = context => { // Ensure auto-scrolling is stopped stopAutoScroll() - // Swap position of columns - let { sourceColumn, targetColumn } = get(reorder) - moveColumn(sourceColumn, targetColumn) - - // Reset state - reorder.set(reorderInitialState) - // Remove event handlers document.removeEventListener("mousemove", onReorderMouseMove) document.removeEventListener("mouseup", stopReordering) + document.removeEventListener("touchmove", onReorderMouseMove) + document.removeEventListener("touchend", stopReordering) + document.removeEventListener("touchcancel", stopReordering) - // Save column changes - await columns.actions.saveChanges() + // Ensure there's actually a change + let { sourceColumn, targetColumn } = get(reorder) + if (sourceColumn !== targetColumn) { + moveColumn(sourceColumn, targetColumn) + await columns.actions.saveChanges() + } + + // Reset state + reorder.set(reorderInitialState) } // Moves a column after another columns. @@ -185,8 +198,7 @@ export const createActions = context => { if (--targetIdx < sourceIdx) { targetIdx++ } - state.splice(targetIdx, 0, removed[0]) - return state.slice() + return state.toSpliced(targetIdx, 0, removed[0]) }) } diff --git a/packages/frontend-core/src/components/grid/stores/resize.js b/packages/frontend-core/src/components/grid/stores/resize.js index 2dc9e0784c..157465e838 100644 --- a/packages/frontend-core/src/components/grid/stores/resize.js +++ b/packages/frontend-core/src/components/grid/stores/resize.js @@ -1,5 +1,6 @@ import { writable, get, derived } from "svelte/store" import { MinColumnWidth, DefaultColumnWidth } from "../lib/constants" +import { parseEventLocation } from "../lib/utils" const initialState = { initialMouseX: null, @@ -24,8 +25,11 @@ export const createActions = context => { // Starts resizing a certain column const startResizing = (column, e) => { + const { x } = parseEventLocation(e) + // Prevent propagation to stop reordering triggering e.stopPropagation() + e.preventDefault() ui.actions.blur() // Find and cache index @@ -39,7 +43,7 @@ export const createActions = context => { width: column.width, left: column.left, initialWidth: column.width, - initialMouseX: e.clientX, + initialMouseX: x, column: column.name, columnIdx, }) @@ -47,12 +51,16 @@ export const createActions = context => { // Add mouse event listeners to handle resizing document.addEventListener("mousemove", onResizeMouseMove) document.addEventListener("mouseup", stopResizing) + document.addEventListener("touchmove", onResizeMouseMove) + document.addEventListener("touchend", stopResizing) + document.addEventListener("touchcancel", stopResizing) } // Handler for moving the mouse to resize columns const onResizeMouseMove = e => { const { initialMouseX, initialWidth, width, columnIdx } = get(resize) - const dx = e.clientX - initialMouseX + const { x } = parseEventLocation(e) + const dx = x - initialMouseX const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx)) // Ignore small changes @@ -87,6 +95,9 @@ export const createActions = context => { resize.set(initialState) document.removeEventListener("mousemove", onResizeMouseMove) document.removeEventListener("mouseup", stopResizing) + document.removeEventListener("touchmove", onResizeMouseMove) + document.removeEventListener("touchend", stopResizing) + document.removeEventListener("touchcancel", stopResizing) // Persist width if it changed if ($resize.width !== $resize.initialWidth) { diff --git a/packages/frontend-core/src/components/grid/stores/ui.js b/packages/frontend-core/src/components/grid/stores/ui.js index da0558bb5b..928f93f3e1 100644 --- a/packages/frontend-core/src/components/grid/stores/ui.js +++ b/packages/frontend-core/src/components/grid/stores/ui.js @@ -98,7 +98,7 @@ export const deriveStores = context => { // Derive whether we should use the compact UI, depending on width const compact = derived([stickyColumn, width], ([$stickyColumn, $width]) => { - return ($stickyColumn?.width || 0) + $width + GutterWidth < 1100 + return ($stickyColumn?.width || 0) + $width + GutterWidth < 800 }) return {