Update new row component, fix z-index issues, improve UX
This commit is contained in:
parent
5ab0652c87
commit
da2023974e
|
@ -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 {
|
||||
|
|
|
@ -179,7 +179,6 @@
|
|||
display: flex;
|
||||
}
|
||||
.header-cell :global(.cell) {
|
||||
background: var(--background);
|
||||
padding: 0 var(--cell-padding);
|
||||
gap: calc(2 * var(--cell-spacing));
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{#if editable}
|
||||
<div class="arrow">
|
||||
<div class="arrow" on:click={open}>
|
||||
<Icon name="ChevronDown" />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
.resize-slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
height: var(--row-height);
|
||||
opacity: 0;
|
||||
padding: 0 8px;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]) => {
|
||||
|
|
Loading…
Reference in New Issue