Update new row component, fix z-index issues, improve UX

This commit is contained in:
Andrew Kingston 2023-04-05 17:36:38 +02:00
parent 5ab0652c87
commit da2023974e
14 changed files with 190 additions and 153 deletions

View File

@ -1,5 +1,5 @@
<script>
import { getContext } from "svelte"
import { getContext, onMount } from "svelte"
import SheetCell from "./SheetCell.svelte"
import { getCellRenderer } from "../lib/renderers"
@ -17,30 +17,36 @@
export let row
export let cellId
export let updateRow = rows.actions.updateRow
export let showPlaceholder = false
export let invert = false
let api
let error
// Determine if the cell is editable
$: readonly = column.schema.autocolumn || (!$config.allowEditRows && row._id)
// Build cell API
$: cellAPI = {
...api,
const cellAPI = {
focus: () => api?.focus(),
blur: () => api?.blur(),
onKeyDown: (...params) => api?.onKeyDown(...params),
isReadonly: () => readonly,
isRequired: () => !!column.schema.constraints?.presence,
validate: value => {
if (value === undefined) {
value = row[column.name]
}
if (cellAPI.isReadonly() && !(value == null || value === "")) {
// Ensure cell isn't readonly
error = "Auto columns can't be edited"
} else if (cellAPI.isRequired() && (value == null || value === "")) {
// Sanity check required fields
error = "Required field"
} else {
error = null
}
return error
},
updateValue: value => {
error = null
try {
if (cellAPI.isReadonly()) {
// Ensure cell isn't readonly
error = "Auto columns can't be edited"
} else if (cellAPI.isRequired() && (value == null || value === "")) {
// Sanity check required fields
error = "Required field"
} else {
cellAPI.validate(value)
if (!error) {
updateRow(row._id, column.name, value)
}
} catch (err) {
@ -49,34 +55,33 @@
},
}
// Determine if the cell is editable
$: readonly = column.schema.autocolumn || (!$config.allowEditRows && row._id)
// Update selected cell API if selected
$: {
if (selected) {
selectedCellAPI.set(cellAPI)
} else {
error = null
} else if (error) {
// error = null
}
}
</script>
<SheetCell
{rowSelected}
{rowHovered}
{rowIdx}
{selected}
{selectedUser}
{reorderSource}
{reorderTarget}
{error}
on:click={() => selectedCellId.set(cellId)}
on:contextmenu={e => menu.actions.open(cellId, e)}
width={column.width}
>
{#if !selected && showPlaceholder && (row[column.name] == null || row[column.name] === "")}
<div class="placeholder">
{column.name}
</div>
{:else}
{#key error}
<SheetCell
{rowSelected}
{rowHovered}
{rowIdx}
{selected}
{selectedUser}
{reorderSource}
{reorderTarget}
{error}
on:click={() => selectedCellId.set(cellId)}
on:contextmenu={e => menu.actions.open(cellId, e)}
width={column.width}
>
<svelte:component
this={getCellRenderer(column)}
bind:api
@ -86,9 +91,10 @@
onChange={cellAPI.updateValue}
{readonly}
{invert}
placeholder="error"
/>
{/if}
</SheetCell>
</SheetCell>
{/key}
<style>
.placeholder {

View File

@ -179,7 +179,6 @@
display: flex;
}
.header-cell :global(.cell) {
background: var(--background);
padding: 0 var(--cell-padding);
gap: calc(2 * var(--cell-spacing));
}

View File

@ -92,7 +92,7 @@
{/each}
</div>
{#if editable}
<div class="arrow">
<div class="arrow" on:click={open}>
<Icon name="ChevronDown" />
</div>
{/if}

View File

@ -40,11 +40,7 @@
data-row={rowIdx}
>
<slot />
{#if selected && error}
<div class="label">
{error}
</div>
{:else if selectedUser && !selected}
{#if selectedUser && !selected}
<div class="label">
{selectedUser.label}
</div>

View File

@ -113,7 +113,6 @@
rowSelected={containsSelectedCell}
width={$stickyColumn.width}
{updateRow}
showPlaceholder
invert
/>
{/if}
@ -131,7 +130,6 @@
rowSelected={containsSelectedCell}
width={column.width}
{updateRow}
showPlaceholder
invert
/>
{/each}

View File

@ -1,6 +1,6 @@
<script>
import SheetCell from "../cells/SheetCell.svelte"
import { getContext, onMount } from "svelte"
import { getContext, onMount, tick } from "svelte"
import { Icon, Button } from "@budibase/bbui"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte"
@ -16,7 +16,6 @@
dispatch,
visibleColumns,
rows,
wheel,
showHScrollbar,
tableId,
subscribe,
@ -34,12 +33,36 @@
$: scrollLeft = $scroll.left
$: $tableId, (isAdding = false)
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const addRow = async () => {
// Validate new row
let allColumns = []
if ($stickyColumn) {
allColumns.push($stickyColumn)
}
allColumns = allColumns.concat($visibleColumns)
for (let col of allColumns) {
$selectedCellId = `new-${col.name}`
await tick()
const error = $selectedCellAPI.validate()
if (error) {
return
}
}
// Create row
const savedRow = await rows.actions.addRow(newRow, 0)
if (savedRow && firstColumn) {
$selectedCellId = `${savedRow._id}-${firstColumn.name}`
isAdding = false
}
// Reset scroll
scroll.set({
left: 0,
top: 0,
})
}
const cancel = () => {
@ -47,13 +70,14 @@
}
const startAdding = () => {
scroll.set({
left: 0,
top: 0,
})
newRow = {}
isAdding = true
if (firstColumn) {
$selectedCellId = `new-${firstColumn.name}`
setTimeout(() => {
$selectedCellAPI?.focus()
}, 100)
}
}
@ -79,59 +103,61 @@
on:mouseenter={() => ($hoveredRowId = "new")}
on:mouseleave={() => ($hoveredRowId = null)}
>
<SheetScrollWrapper scrollHorizontally={false} scrollVertically={false}>
<div
class="sticky-column"
style="flex: 0 0 {width}px"
class:scrolled={scrollLeft > 0}
<div
class="sticky-column"
style="flex: 0 0 {width}px"
class:scrolled={scrollLeft > 0}
>
<SheetCell
width={gutterWidth}
{rowHovered}
rowSelected={containsSelectedCell}
>
<SheetCell
width={gutterWidth}
<div class="gutter">
<div class="number">1</div>
{#if $config.allowExpandRows}
<Icon
name="Maximize"
size="S"
hoverable
on:click={addViaModal}
/>
{/if}
</div>
</SheetCell>
{#if $stickyColumn}
{@const cellId = `new-${$stickyColumn.name}`}
<DataCell
{cellId}
column={$stickyColumn}
row={newRow}
{rowHovered}
selected={$selectedCellId === cellId}
rowSelected={containsSelectedCell}
>
<div class="gutter">
<div class="number">1</div>
{#if $config.allowExpandRows}
<Icon
name="Maximize"
size="S"
hoverable
on:click={addViaModal}
/>
{/if}
</div>
</SheetCell>
{#if $stickyColumn}
{@const cellId = `new-${$stickyColumn.name}`}
<DataCell
{cellId}
column={$stickyColumn}
row={newRow}
{rowHovered}
selected={$selectedCellId === cellId}
rowSelected={containsSelectedCell}
width={$stickyColumn.width}
{updateRow}
/>
{/if}
</div>
</SheetScrollWrapper>
width={$stickyColumn.width}
{updateRow}
rowIdx={0}
/>
{/if}
</div>
<SheetScrollWrapper scrollVertically={false}>
<SheetScrollWrapper scrollVertically={false} foo>
<div class="row">
{#each $renderedColumns as column}
{#each $visibleColumns as column}
{@const cellId = `new-${column.name}`}
<DataCell
{cellId}
{column}
row={newRow}
{rowHovered}
selected={$selectedCellId === cellId}
rowSelected={containsSelectedCell}
width={column.width}
{updateRow}
/>
{#key cellId}
<DataCell
{cellId}
{column}
row={newRow}
{rowHovered}
selected={$selectedCellId === cellId}
rowSelected={containsSelectedCell}
width={column.width}
{updateRow}
rowIdx={0}
/>
{/key}
{/each}
</div>
</SheetScrollWrapper>
@ -149,8 +175,8 @@
pointer-events: none;
position: absolute;
top: var(--row-height);
left: 0;
transform: translateY(-100%);
z-index: 1;
transition: transform 130ms ease-out;
background: linear-gradient(
to bottom,

View File

@ -75,6 +75,7 @@
context = { ...context, ...createUserStores(context) }
context = { ...context, ...createMenuStores(context) }
context = { ...context, ...createPaginationStores(context) }
context = { ...context, ...context }
// Reference some stores for local use
const { isResizing, isReordering, ui, loaded, rowHeight } = context
@ -125,13 +126,14 @@
<HeaderRow />
<SheetBody />
</div>
{#if $config.allowAddRows}
<!-- <NewRow />-->
<NewRowTop />
{/if}
<ResizeOverlay />
<ScrollOverlay />
<MenuOverlay />
<div class="overlays">
{#if $config.allowAddRows}
<NewRowTop />
{/if}
<ResizeOverlay />
<ScrollOverlay />
<MenuOverlay />
</div>
</div>
</div>
{/if}
@ -215,4 +217,9 @@
.controls-right {
gap: 12px;
}
/* Overlays */
.overlays {
z-index: 10;
}
</style>

View File

@ -18,13 +18,20 @@
export let scrollVertically = true
export let scrollHorizontally = true
export let wheelInteractive = true
export let foo = false
$: hiddenWidths = calculateHiddenWidths($renderedColumns)
$: style = generateStyle($scroll, $rowHeight, hiddenWidths)
$: style = generateStyle($scroll, $rowHeight, hiddenWidths, foo)
const generateStyle = (scroll, rowHeight, hiddenWidths) => {
const offsetX = scrollHorizontally ? -1 * scroll.left + hiddenWidths : 0
const offsetY = scrollVertically ? -1 * (scroll.top % rowHeight) : 0
const generateStyle = (scroll, rowHeight, hiddenWidths, foo) => {
let offsetX, offsetY
if (!foo) {
offsetX = scrollHorizontally ? -1 * scroll.left + hiddenWidths : 0
offsetY = scrollVertically ? -1 * (scroll.top % rowHeight) : 0
} else {
offsetX = scrollHorizontally ? -1 * scroll.left : 0
offsetY = scrollVertically ? -1 * scroll.top : 0
}
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
}
@ -63,8 +70,8 @@
// Update state
scroll.set({
left: newScrollLeft,
top: newScrollTop,
left: scrollHorizontally ? newScrollLeft : left,
top: scrollVertically ? newScrollTop : top,
})
// Hover row under cursor

View File

@ -160,16 +160,14 @@
.sticky-column {
display: flex;
flex-direction: column;
position: relative;
border-right: var(--cell-border);
z-index: 3;
}
/* Add shadow when scrolled */
.sticky-column.scrolled :global(.cell:last-child:after) {
/*content: " ";*/
/*position: absolute;*/
/*width: 10px;*/
/*height: 100%;*/
/*left: 100%;*/
/*background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);*/
.sticky-column.scrolled {
box-shadow: 0 0 8px -2px rgba(0, 0, 0, 0.2);
}
/* Don't show borders between cells in the sticky column */
@ -178,8 +176,7 @@
}
.header {
position: relative;
z-index: 3;
z-index: 1;
}
.header :global(.cell) {
background: var(--spectrum-global-color-gray-100);
@ -192,7 +189,6 @@
}
.content {
position: relative;
z-index: 1;
flex: 1 1 auto;
}

View File

@ -57,7 +57,6 @@
<style>
.menu {
z-index: 1;
position: absolute;
background: var(--cell-background);
border: var(--cell-border);
@ -65,5 +64,6 @@
border-radius: 4px;
display: flex;
flex-direction: column;
box-shadow: 0 0 32px 0 rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -51,7 +51,6 @@
.resize-slider {
position: absolute;
top: 0;
z-index: 2;
height: var(--row-height);
opacity: 0;
padding: 0 8px;

View File

@ -109,7 +109,6 @@
background: var(--spectrum-global-color-gray-500);
opacity: 0.7;
border-radius: 4px;
z-index: 1;
transition: opacity 130ms ease-out;
}
div:hover {

View File

@ -1,4 +1,5 @@
import { derived, get } from "svelte/store"
import { tick } from "svelte"
export const createMaxScrollStores = context => {
const {
@ -88,65 +89,68 @@ export const createMaxScrollStores = context => {
})
// Ensure the selected cell is visible
selectedCellRow.subscribe(row => {
if (!row) {
return
}
selectedCellId.subscribe(async $selectedCellId => {
await tick()
const $selectedCellRow = get(selectedCellRow)
const $scroll = get(scroll)
const $bounds = get(bounds)
const $rowHeight = get(rowHeight)
const scrollBarOffset = 16
const verticalOffset = $rowHeight * 1.5
// Ensure row is not below bottom of screen
const rowYPos = row.__idx * $rowHeight
const bottomCutoff =
$scroll.top + $bounds.height - $rowHeight - scrollBarOffset
let delta = rowYPos - bottomCutoff
if (delta > 0) {
scroll.update(state => ({
...state,
top: state.top + delta,
}))
}
// Ensure row is not above top of screen
else {
delta = $scroll.top - rowYPos
// Ensure vertical position is viewable
if ($selectedCellRow) {
// Ensure row is not below bottom of screen
const rowYPos = $selectedCellRow.__idx * $rowHeight
const bottomCutoff =
$scroll.top + $bounds.height - $rowHeight - verticalOffset
let delta = rowYPos - bottomCutoff
if (delta > 0) {
scroll.update(state => ({
...state,
top: Math.max(0, state.top - delta),
top: state.top + delta,
}))
}
// Ensure row is not above top of screen
else {
const delta = $scroll.top - rowYPos + verticalOffset
if (delta > 0) {
scroll.update(state => ({
...state,
top: Math.max(0, state.top - delta),
}))
}
}
}
// Ensure horizontal position is viewable
// Check horizontal position of columns next
const $selectedCellId = get(selectedCellId)
const $visibleColumns = get(visibleColumns)
const columnName = $selectedCellId?.split("-")[1]
const column = $visibleColumns.find(col => col.name === columnName)
const horizontalOffset = 24
if (!column) {
return
}
// Ensure column is not cutoff on left edge
delta = $scroll.left - column.left
let delta = $scroll.left - column.left + horizontalOffset
if (delta > 0) {
scroll.update(state => ({
...state,
left: state.left - delta,
left: Math.max(0, state.left - delta),
}))
}
// Ensure column is not cutoff on right edge
else {
const rightEdge = column.left + column.width
const rightBound = $bounds.width + $scroll.left
const rightBound = $bounds.width + $scroll.left - horizontalOffset
delta = rightEdge - rightBound
if (delta > 0) {
scroll.update(state => ({
...state,
left: state.left + delta + scrollBarOffset,
left: state.left + delta,
}))
}
}

View File

@ -8,7 +8,7 @@ export const createUIStores = context => {
const selectedCellAPI = writable(null)
const rowHeight = writable(36)
// Derive the row that contains the selected cell.
// Derive the row that contains the selected cell
const selectedCellRow = derived(
[selectedCellId, rowLookupMap, rows],
([$selectedCellId, $rowLookupMap, $rows]) => {