fix table keyboard nav
This commit is contained in:
parent
b12127d220
commit
55c58158cc
4 changed files with 356 additions and 198 deletions
|
|
@ -42,31 +42,25 @@ $table-border-color: #ccc;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__cell {
|
|
||||||
align-items: stretch;
|
|
||||||
border: solid 1px $table-border-color;
|
|
||||||
border-left: none;
|
|
||||||
border-top: none;
|
|
||||||
display: flex;
|
|
||||||
flex: none;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&--insertable {
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__inserter {
|
|
||||||
align-items: stretch;
|
|
||||||
display: flex;
|
|
||||||
grid-area: inserter;
|
|
||||||
justify-items: flex-start;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lens-cell {
|
.lens-cell {
|
||||||
|
align-items: stretch;
|
||||||
|
border: solid 1px $table-border-color;
|
||||||
|
border-left: none;
|
||||||
|
border-top: none;
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--insertable {
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
&__container {
|
&__container {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -75,6 +69,11 @@ $table-border-color: #ccc;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&--selected {
|
&--selected {
|
||||||
|
outline: 1px solid #37f;
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--cursor {
|
||||||
outline: 3px solid #37f;
|
outline: 3px solid #37f;
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
@ -176,11 +175,26 @@ $table-border-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lens-inserter {
|
.lens-inserter {
|
||||||
|
grid-area: inserter;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
&__help {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: lighter;
|
||||||
|
margin: 8px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
align-items: stretch;
|
||||||
|
display: flex;
|
||||||
|
justify-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
&__rows {
|
&__rows {
|
||||||
.lens-table__cell {
|
.lens-cell {
|
||||||
border: dashed 1px $table-border-color;
|
border: dashed 1px $table-border-color;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-top: none;
|
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
} from "./editor-state.svelte";
|
} from "./editor-state.svelte";
|
||||||
import { type FieldInfo } from "./field.svelte";
|
import { type FieldInfo } from "./field.svelte";
|
||||||
|
|
||||||
|
const BLUR_DEBOUNCE_MS = 100;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
* For use cases in which the user may select between multiple datum types,
|
* For use cases in which the user may select between multiple datum types,
|
||||||
|
|
@ -17,7 +19,22 @@
|
||||||
|
|
||||||
field_info: FieldInfo;
|
field_info: FieldInfo;
|
||||||
|
|
||||||
on_change?(value?: Datum): void;
|
on_blur?(ev: FocusEvent): unknown;
|
||||||
|
|
||||||
|
on_cancel_edit?(): unknown;
|
||||||
|
|
||||||
|
on_change?(value?: Datum): unknown;
|
||||||
|
|
||||||
|
on_focus?(ev: FocusEvent): unknown;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In addition to `on_blur()`, this callback is invoked when the component
|
||||||
|
* blurs *itself*, for example when a user presses "Enter" or "Escape", as
|
||||||
|
* opposed to blurring in response to a user focusing another element. This
|
||||||
|
* typically indicates that the parent component should restore focus to a
|
||||||
|
* previously focused table cell.
|
||||||
|
*/
|
||||||
|
on_restore_focus?(): unknown;
|
||||||
|
|
||||||
value?: Datum;
|
value?: Datum;
|
||||||
};
|
};
|
||||||
|
|
@ -25,7 +42,11 @@
|
||||||
let {
|
let {
|
||||||
assignable_fields = [],
|
assignable_fields = [],
|
||||||
field_info = $bindable(),
|
field_info = $bindable(),
|
||||||
|
on_blur,
|
||||||
|
on_cancel_edit,
|
||||||
on_change,
|
on_change,
|
||||||
|
on_focus,
|
||||||
|
on_restore_focus,
|
||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
|
@ -35,6 +56,7 @@
|
||||||
>();
|
>();
|
||||||
let type_selector_popover_element = $state<HTMLDivElement | undefined>();
|
let type_selector_popover_element = $state<HTMLDivElement | undefined>();
|
||||||
let text_input_element = $state<HTMLInputElement | undefined>();
|
let text_input_element = $state<HTMLInputElement | undefined>();
|
||||||
|
let blur_timeout = $state<number | undefined>();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|
@ -46,6 +68,23 @@
|
||||||
text_input_element?.focus();
|
text_input_element?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handle_blur(ev: FocusEvent) {
|
||||||
|
// Propagating of blur events upwards is debounced, so that switching focus
|
||||||
|
// between elements does not cause spurious `on_blur()` calls.
|
||||||
|
if (blur_timeout !== undefined) {
|
||||||
|
clearTimeout(blur_timeout);
|
||||||
|
}
|
||||||
|
blur_timeout = setTimeout(() => on_blur?.(ev), BLUR_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_focus(ev: FocusEvent) {
|
||||||
|
if (blur_timeout === undefined) {
|
||||||
|
on_focus?.(ev);
|
||||||
|
} else {
|
||||||
|
clearTimeout(blur_timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handle_input() {
|
function handle_input() {
|
||||||
if (!editor_state) {
|
if (!editor_state) {
|
||||||
console.warn("preconditions for handle_input() not met");
|
console.warn("preconditions for handle_input() not met");
|
||||||
|
|
@ -55,7 +94,6 @@
|
||||||
editor_state,
|
editor_state,
|
||||||
field_info.field.presentation,
|
field_info.field.presentation,
|
||||||
);
|
);
|
||||||
console.log(value);
|
|
||||||
on_change?.(value);
|
on_change?.(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,16 +106,35 @@
|
||||||
type_selector_popover_element?.hidePopover();
|
type_selector_popover_element?.hidePopover();
|
||||||
type_selector_menu_button_element?.focus();
|
type_selector_menu_button_element?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handle_keydown(ev: KeyboardEvent) {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
on_restore_focus?.();
|
||||||
|
} else if (ev.key === "Escape") {
|
||||||
|
// Cancel edit before blurring, or else the table will try to commit it.
|
||||||
|
on_cancel_edit?.();
|
||||||
|
on_restore_focus?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const interactive_handlers = {
|
||||||
|
onblur: handle_blur,
|
||||||
|
onfocus: handle_focus,
|
||||||
|
onkeydown: handle_keydown,
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="datum-editor__container"
|
class="datum-editor__container"
|
||||||
class:datum-editor__container--incomplete={!value}
|
class:datum-editor__container--incomplete={!value}
|
||||||
|
onblur={handle_blur}
|
||||||
|
onfocus={handle_focus}
|
||||||
>
|
>
|
||||||
{#if editor_state}
|
{#if editor_state}
|
||||||
{#if assignable_fields?.length > 0}
|
{#if assignable_fields?.length > 0}
|
||||||
<div class="datum-editor__type-selector">
|
<div class="datum-editor__type-selector">
|
||||||
<button
|
<button
|
||||||
|
{...interactive_handlers}
|
||||||
bind:this={type_selector_menu_button_element}
|
bind:this={type_selector_menu_button_element}
|
||||||
class="datum-editor__type-selector-menu-button"
|
class="datum-editor__type-selector-menu-button"
|
||||||
onclick={handle_type_selector_menu_button_click}
|
onclick={handle_type_selector_menu_button_click}
|
||||||
|
|
@ -88,10 +145,13 @@
|
||||||
<div
|
<div
|
||||||
bind:this={type_selector_popover_element}
|
bind:this={type_selector_popover_element}
|
||||||
class="datum-editor__type-selector-popover"
|
class="datum-editor__type-selector-popover"
|
||||||
|
onblur={handle_blur}
|
||||||
|
onfocus={handle_focus}
|
||||||
popover="auto"
|
popover="auto"
|
||||||
>
|
>
|
||||||
{#each assignable_fields as assignable_field_info}
|
{#each assignable_fields as assignable_field_info}
|
||||||
<button
|
<button
|
||||||
|
{...interactive_handlers}
|
||||||
onclick={() =>
|
onclick={() =>
|
||||||
handle_type_selector_field_button_click(assignable_field_info)}
|
handle_type_selector_field_button_click(assignable_field_info)}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -103,6 +163,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
|
{...interactive_handlers}
|
||||||
type="button"
|
type="button"
|
||||||
class="datum-editor__null-control"
|
class="datum-editor__null-control"
|
||||||
class:datum-editor__null-control--disabled={editor_state.text_value !==
|
class:datum-editor__null-control--disabled={editor_state.text_value !==
|
||||||
|
|
@ -125,6 +186,7 @@
|
||||||
</button>
|
</button>
|
||||||
{#if field_info.field.presentation.t === "Dropdown" || field_info.field.presentation.t === "Text" || field_info.field.presentation.t === "Uuid"}
|
{#if field_info.field.presentation.t === "Dropdown" || field_info.field.presentation.t === "Text" || field_info.field.presentation.t === "Uuid"}
|
||||||
<input
|
<input
|
||||||
|
{...interactive_handlers}
|
||||||
bind:this={text_input_element}
|
bind:this={text_input_element}
|
||||||
value={editor_state.text_value}
|
value={editor_state.text_value}
|
||||||
oninput={({ currentTarget }) => {
|
oninput={({ currentTarget }) => {
|
||||||
|
|
@ -142,8 +204,16 @@
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
{:else if field_info.field.presentation.t === "Timestamp"}
|
{:else if field_info.field.presentation.t === "Timestamp"}
|
||||||
<input value={editor_state.date_value} type="date" />
|
<input
|
||||||
<input value={editor_state.time_value} type="time" />
|
{...interactive_handlers}
|
||||||
|
value={editor_state.date_value}
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
{...interactive_handlers}
|
||||||
|
value={editor_state.time_value}
|
||||||
|
type="time"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="datum-editor__helpers" tabindex="-1">
|
<div class="datum-editor__helpers" tabindex="-1">
|
||||||
{#if field_info.field.presentation.t === "Dropdown"}
|
{#if field_info.field.presentation.t === "Dropdown"}
|
||||||
|
|
@ -160,6 +230,7 @@
|
||||||
aria-selected={dropdown_option.value === value?.c}
|
aria-selected={dropdown_option.value === value?.c}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
{...interactive_handlers}
|
||||||
class="datum-editor__dropdown-option-button"
|
class="datum-editor__dropdown-option-button"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
if (!editor_state || !text_input_element) {
|
if (!editor_state || !text_input_element) {
|
||||||
|
|
|
||||||
131
svelte/src/table-cell.svelte
Normal file
131
svelte/src/table-cell.svelte
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { type Datum } from "./datum.svelte";
|
||||||
|
import { type FieldInfo, type Coords } from "./field.svelte";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
coords: Coords;
|
||||||
|
cursor: boolean;
|
||||||
|
field: FieldInfo;
|
||||||
|
onbecomecursor?(focus: () => unknown): unknown;
|
||||||
|
ondblclick?(ev: MouseEvent, coords: Coords): unknown;
|
||||||
|
onfocus?(ev: FocusEvent, coords: Coords): unknown;
|
||||||
|
onkeydown?(ev: KeyboardEvent, coords: Coords): unknown;
|
||||||
|
onmousedown?(ev: MouseEvent, coords: Coords): unknown;
|
||||||
|
selected: boolean;
|
||||||
|
table_region: "main" | "inserter";
|
||||||
|
value: Datum;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
coords,
|
||||||
|
cursor,
|
||||||
|
field,
|
||||||
|
onbecomecursor,
|
||||||
|
ondblclick,
|
||||||
|
onfocus,
|
||||||
|
onkeydown,
|
||||||
|
onmousedown,
|
||||||
|
selected,
|
||||||
|
table_region,
|
||||||
|
value,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let cell_element = $state<HTMLDivElement | undefined>();
|
||||||
|
|
||||||
|
let invalid_value = $derived(
|
||||||
|
field.not_null && !field.has_default && value.c === undefined,
|
||||||
|
);
|
||||||
|
let null_value_class = $derived(
|
||||||
|
table_region === "inserter" && field.has_default
|
||||||
|
? "ti-sparkles"
|
||||||
|
: "ti-cube-3d-sphere-off",
|
||||||
|
);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (cursor) {
|
||||||
|
onbecomecursor?.(() => cell_element?.focus());
|
||||||
|
cell_element?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
aria-colindex={coords[0]}
|
||||||
|
aria-rowindex={coords[1]}
|
||||||
|
aria-selected={selected}
|
||||||
|
bind:this={cell_element}
|
||||||
|
class="lens-cell"
|
||||||
|
onmousedown={(ev) => onmousedown?.(ev, coords)}
|
||||||
|
ondblclick={(ev) => ondblclick?.(ev, coords)}
|
||||||
|
onfocus={(ev) => onfocus?.(ev, coords)}
|
||||||
|
onkeydown={(ev) => onkeydown?.(ev, coords)}
|
||||||
|
role="gridcell"
|
||||||
|
style:width={`${field.field.table_width_px}px`}
|
||||||
|
tabindex={selected ? 0 : -1}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="lens-cell__container"
|
||||||
|
class:lens-cell__container--cursor={cursor}
|
||||||
|
class:lens-cell__container--selected={selected}
|
||||||
|
>
|
||||||
|
{#if field.field.presentation.t === "Dropdown"}
|
||||||
|
{#if value.t === "Text"}
|
||||||
|
<div
|
||||||
|
class="lens-cell__content lens-cell__content--dropdown"
|
||||||
|
class:lens-cell__content--null={value.c === undefined}
|
||||||
|
>
|
||||||
|
{#if value.c === undefined}
|
||||||
|
<i class={["ti", null_value_class]}></i>
|
||||||
|
{:else}
|
||||||
|
<!-- FIXME: validate or escape dropdown_option.color -->
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
"dropdown-option-badge",
|
||||||
|
`dropdown-option-badge--${
|
||||||
|
field.field.presentation.c.options
|
||||||
|
.find((option) => option.value === value.c)
|
||||||
|
?.color.toLocaleLowerCase("en-US") ?? "grey"
|
||||||
|
}`,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{value.c}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
UNKNOWN
|
||||||
|
{/if}
|
||||||
|
{:else if field.field.presentation.t === "Text"}
|
||||||
|
<div
|
||||||
|
class="lens-cell__content lens-cell__content--text"
|
||||||
|
class:lens-cell__content--null={value.c === undefined}
|
||||||
|
>
|
||||||
|
{#if value.c === undefined}
|
||||||
|
<i class={["ti", null_value_class]}></i>
|
||||||
|
{:else}
|
||||||
|
{value.c}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if field.field.presentation.t === "Uuid"}
|
||||||
|
<div
|
||||||
|
class="lens-cell__content lens-cell__content--uuid"
|
||||||
|
class:lens-cell__content--null={value.c === undefined}
|
||||||
|
>
|
||||||
|
{#if value.c === undefined}
|
||||||
|
<i class={["ti", null_value_class]}></i>
|
||||||
|
{:else}
|
||||||
|
{value.c}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="lens-cell__content lens-cell__content--unknown">
|
||||||
|
<div>UNKNOWN</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if invalid_value}
|
||||||
|
<div class="lens-cell__notice">
|
||||||
|
<i class="ti ti-alert-circle"></i>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
import FieldAdder from "./field-adder.svelte";
|
import FieldAdder from "./field-adder.svelte";
|
||||||
import FieldHeader from "./field-header.svelte";
|
import FieldHeader from "./field-header.svelte";
|
||||||
import { get_empty_datum_for } from "./presentation.svelte";
|
import { get_empty_datum_for } from "./presentation.svelte";
|
||||||
|
import TableCell from "./table-cell.svelte";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
columns?: {
|
columns?: {
|
||||||
|
|
@ -77,7 +78,7 @@
|
||||||
reverted: [],
|
reverted: [],
|
||||||
});
|
});
|
||||||
let datum_editor = $state<DatumEditor | undefined>();
|
let datum_editor = $state<DatumEditor | undefined>();
|
||||||
let table_element = $state<HTMLDivElement | undefined>();
|
let focus_cursor = $state<(() => unknown) | undefined>();
|
||||||
let inserter_rows = $state<Row[]>([]);
|
let inserter_rows = $state<Row[]>([]);
|
||||||
let lazy_data = $state<LazyData | undefined>();
|
let lazy_data = $state<LazyData | undefined>();
|
||||||
|
|
||||||
|
|
@ -313,67 +314,55 @@
|
||||||
});
|
});
|
||||||
// Reset editor input value
|
// Reset editor input value
|
||||||
set_selections(selections);
|
set_selections(selections);
|
||||||
table_element?.focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Event Handlers: Both Tables -------- //
|
// -------- Event Handlers -------- //
|
||||||
|
|
||||||
function handle_table_keydown(ev: KeyboardEvent) {
|
function handle_table_keydown(ev: KeyboardEvent) {
|
||||||
if (lazy_data) {
|
if (!lazy_data) {
|
||||||
const arrow_direction = arrow_key_direction(ev.key);
|
console.warn("preconditions for handle_table_keydown() not met");
|
||||||
if (arrow_direction) {
|
return;
|
||||||
move_selection(arrow_direction);
|
}
|
||||||
ev.preventDefault();
|
const arrow_direction = arrow_key_direction(ev.key);
|
||||||
|
if (arrow_direction) {
|
||||||
|
move_selection(arrow_direction);
|
||||||
|
ev.preventDefault();
|
||||||
|
} else if (/^[a-zA-Z0-9`~!@#$%^&*()_=+[\]{}\\|;:'",<.>/?-]$/.test(ev.key)) {
|
||||||
|
const sel = selections[0];
|
||||||
|
if (sel) {
|
||||||
|
editor_value = get_empty_datum_for(
|
||||||
|
lazy_data.fields[sel.coords[1]].field.presentation,
|
||||||
|
);
|
||||||
|
datum_editor?.focus();
|
||||||
}
|
}
|
||||||
if (ev.key === "Enter") {
|
} else if (ev.key === "Enter") {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
if (selections[0]?.region === "main") {
|
if (selections[0]?.region === "main") {
|
||||||
set_selections([
|
set_selections([
|
||||||
{
|
{
|
||||||
region: "inserter",
|
region: "inserter",
|
||||||
coords: [0, selections[0]?.coords[1] ?? 0],
|
coords: [0, selections[0]?.coords[1] ?? 0],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
inserter_rows = [
|
|
||||||
...inserter_rows,
|
|
||||||
{
|
|
||||||
key: inserter_rows.length,
|
|
||||||
data: lazy_data.fields.map(({ field: { presentation } }) =>
|
|
||||||
get_empty_datum_for(presentation),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
datum_editor?.focus();
|
inserter_rows = [
|
||||||
|
...inserter_rows,
|
||||||
|
{
|
||||||
|
key: inserter_rows.length,
|
||||||
|
data: lazy_data.fields.map(({ field: { presentation } }) =>
|
||||||
|
get_empty_datum_for(presentation),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
datum_editor?.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Event Handlers: Main Table -------- //
|
function handle_restore_focus() {
|
||||||
|
focus_cursor?.();
|
||||||
function handle_main_cell_click(ev: MouseEvent, coords: Coords) {
|
|
||||||
if (ev.metaKey || ev.ctrlKey) {
|
|
||||||
// TODO
|
|
||||||
// selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords];
|
|
||||||
// editor_input_value = "";
|
|
||||||
} else {
|
|
||||||
set_selections([{ region: "main", coords }]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- Event Handlers: Inserter Table -------- //
|
|
||||||
|
|
||||||
function handle_inserter_cell_click(ev: MouseEvent, coords: Coords) {
|
|
||||||
if (ev.metaKey || ev.ctrlKey) {
|
|
||||||
// TODO
|
|
||||||
// selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords];
|
|
||||||
// editor_input_value = "";
|
|
||||||
} else {
|
|
||||||
set_selections([{ region: "inserter", coords }]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Initial API Fetch -------- //
|
// -------- Initial API Fetch -------- //
|
||||||
|
|
@ -402,6 +391,14 @@
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
if (lazy_data.rows.length > 0 && lazy_data.fields.length > 0) {
|
||||||
|
set_selections([
|
||||||
|
{
|
||||||
|
region: "main",
|
||||||
|
coords: [0, 0],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
})().catch(console.error);
|
})().catch(console.error);
|
||||||
|
|
||||||
setInterval(tick_delta_queue, 500);
|
setInterval(tick_delta_queue, 500);
|
||||||
|
|
@ -420,97 +417,25 @@
|
||||||
{#each rows as row, row_index}
|
{#each rows as row, row_index}
|
||||||
<div class="lens-table__row" role="row">
|
<div class="lens-table__row" role="row">
|
||||||
{#each lazy_data.fields as field, field_index}
|
{#each lazy_data.fields as field, field_index}
|
||||||
{@const cell_data = row.data[field_index]}
|
<TableCell
|
||||||
{@const cell_coords: Coords = [row_index, field_index]}
|
coords={[row_index, field_index]}
|
||||||
{@const cell_selected = selections.some(
|
cursor={selections[0]?.region === region_name &&
|
||||||
(sel) =>
|
coords_eq(selections[0].coords, [row_index, field_index])}
|
||||||
sel.region === region_name && coords_eq(sel.coords, cell_coords),
|
{field}
|
||||||
)}
|
onbecomecursor={(focus) => {
|
||||||
{@const null_value_class =
|
focus_cursor = focus;
|
||||||
region_name === "inserter" && field.has_default
|
|
||||||
? "ti-sparkles"
|
|
||||||
: "ti-cube-3d-sphere-off"}
|
|
||||||
{@const invalid_value =
|
|
||||||
field.not_null && !field.has_default && cell_data.c === undefined}
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<div
|
|
||||||
aria-colindex={field_index}
|
|
||||||
aria-rowindex={row_index}
|
|
||||||
aria-selected={cell_selected}
|
|
||||||
class="lens-table__cell"
|
|
||||||
onmousedown={(ev) => on_cell_click(ev, cell_coords)}
|
|
||||||
ondblclick={() => {
|
|
||||||
datum_editor?.focus();
|
|
||||||
}}
|
}}
|
||||||
role="gridcell"
|
ondblclick={() => datum_editor?.focus()}
|
||||||
style:width={`${field.field.table_width_px}px`}
|
onkeydown={(ev) => handle_table_keydown(ev)}
|
||||||
tabindex="-1"
|
onmousedown={on_cell_click}
|
||||||
>
|
selected={selections.some(
|
||||||
<div
|
(sel) =>
|
||||||
class="lens-cell__container"
|
sel.region === region_name &&
|
||||||
class:lens-cell__container--selected={cell_selected}
|
coords_eq(sel.coords, [row_index, field_index]),
|
||||||
>
|
)}
|
||||||
{#if field.field.presentation.t === "Dropdown"}
|
table_region={region_name}
|
||||||
{#if cell_data.t === "Text"}
|
value={row.data[field_index]}
|
||||||
<div
|
/>
|
||||||
class="lens-cell__content lens-cell__content--dropdown"
|
|
||||||
class:lens-cell__content--null={cell_data.c === undefined}
|
|
||||||
>
|
|
||||||
{#if cell_data.c === undefined}
|
|
||||||
<i class="ti {null_value_class}"></i>
|
|
||||||
{:else}
|
|
||||||
<!-- FIXME: validate or escape dropdown_option.color -->
|
|
||||||
<div
|
|
||||||
class={[
|
|
||||||
"dropdown-option-badge",
|
|
||||||
`dropdown-option-badge--${
|
|
||||||
field.field.presentation.c.options
|
|
||||||
.find((option) => option.value === cell_data.c)
|
|
||||||
?.color.toLocaleLowerCase("en-US") ?? "grey"
|
|
||||||
}`,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{cell_data.c}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
UNKNOWN
|
|
||||||
{/if}
|
|
||||||
{:else if field.field.presentation.t === "Text"}
|
|
||||||
<div
|
|
||||||
class="lens-cell__content lens-cell__content--text"
|
|
||||||
class:lens-cell__content--null={cell_data.c === undefined}
|
|
||||||
>
|
|
||||||
{#if cell_data.c === undefined}
|
|
||||||
<i class="ti {null_value_class}"></i>
|
|
||||||
{:else}
|
|
||||||
{cell_data.c}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else if field.field.presentation.t === "Uuid"}
|
|
||||||
<div
|
|
||||||
class="lens-cell__content lens-cell__content--uuid"
|
|
||||||
class:lens-cell__content--null={cell_data.c === undefined}
|
|
||||||
>
|
|
||||||
{#if cell_data.c === undefined}
|
|
||||||
<i class="ti {null_value_class}"></i>
|
|
||||||
{:else}
|
|
||||||
{cell_data.c}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="lens-cell__content lens-cell__content--unknown">
|
|
||||||
<div>UNKNOWN</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if invalid_value}
|
|
||||||
<div class="lens-cell__notice">
|
|
||||||
<i class="ti ti-alert-circle"></i>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -519,16 +444,7 @@
|
||||||
|
|
||||||
<div class="lens-grid">
|
<div class="lens-grid">
|
||||||
{#if lazy_data}
|
{#if lazy_data}
|
||||||
<div
|
<div class="lens-table" role="grid">
|
||||||
bind:this={table_element}
|
|
||||||
class="lens-table"
|
|
||||||
onfocus={() => {
|
|
||||||
try_queue_delta();
|
|
||||||
}}
|
|
||||||
onkeydown={handle_table_keydown}
|
|
||||||
role="grid"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<div class={["lens-table__headers"]}>
|
<div class={["lens-table__headers"]}>
|
||||||
{#each lazy_data.fields as _, field_index}
|
{#each lazy_data.fields as _, field_index}
|
||||||
<FieldHeader
|
<FieldHeader
|
||||||
|
|
@ -544,29 +460,52 @@
|
||||||
{@render table_region({
|
{@render table_region({
|
||||||
region_name: "main",
|
region_name: "main",
|
||||||
rows: lazy_data.rows,
|
rows: lazy_data.rows,
|
||||||
on_cell_click: handle_main_cell_click,
|
on_cell_click: (ev: MouseEvent, coords: Coords) => {
|
||||||
|
if (ev.metaKey || ev.ctrlKey) {
|
||||||
|
// TODO
|
||||||
|
// selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords];
|
||||||
|
// editor_input_value = "";
|
||||||
|
} else {
|
||||||
|
set_selections([{ region: "main", coords }]);
|
||||||
|
}
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="insert">
|
<form method="post" action="insert">
|
||||||
<div class="lens-table__inserter">
|
<div class="lens-inserter">
|
||||||
<div class="lens-inserter__rows">
|
<h3 class="lens-inserter__help">
|
||||||
{@render table_region({
|
Insert rows — press "shift + enter" to jump here or add a row
|
||||||
region_name: "inserter",
|
</h3>
|
||||||
rows: inserter_rows,
|
<div class="lens-inserter__main">
|
||||||
on_cell_click: handle_inserter_cell_click,
|
<div class="lens-inserter__rows">
|
||||||
})}
|
{@render table_region({
|
||||||
|
region_name: "inserter",
|
||||||
|
rows: inserter_rows,
|
||||||
|
on_cell_click: (ev: MouseEvent, coords: Coords) => {
|
||||||
|
if (ev.metaKey || ev.ctrlKey) {
|
||||||
|
// TODO
|
||||||
|
// selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords];
|
||||||
|
// editor_input_value = "";
|
||||||
|
} else {
|
||||||
|
set_selections([{ region: "inserter", coords }]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label="Insert rows"
|
||||||
|
class="lens-inserter__submit"
|
||||||
|
onkeydown={(ev) => {
|
||||||
|
// Prevent keypress (e.g. pressing Enter on the button to submit
|
||||||
|
// it) from triggering a table interaction.
|
||||||
|
ev.stopPropagation();
|
||||||
|
}}
|
||||||
|
title="Insert rows"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<i class="ti ti-upload"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
class="lens-inserter__submit"
|
|
||||||
onkeydown={(ev) => {
|
|
||||||
// Prevent keypress (e.g. pressing Enter on the button to submit
|
|
||||||
// it) from triggering a table interaction.
|
|
||||||
ev.stopPropagation();
|
|
||||||
}}
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<i class="ti ti-upload"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{#each inserter_rows as row}
|
{#each inserter_rows as row}
|
||||||
{#each lazy_data.fields as field, field_index}
|
{#each lazy_data.fields as field, field_index}
|
||||||
|
|
@ -585,9 +524,12 @@
|
||||||
bind:this={datum_editor}
|
bind:this={datum_editor}
|
||||||
bind:value={editor_value}
|
bind:value={editor_value}
|
||||||
field_info={lazy_data.fields[selections[0].coords[1]]}
|
field_info={lazy_data.fields[selections[0].coords[1]]}
|
||||||
|
on_blur={() => try_queue_delta()}
|
||||||
|
on_cancel_edit={cancel_edit}
|
||||||
on_change={() => {
|
on_change={() => {
|
||||||
try_sync_edit_to_cells();
|
try_sync_edit_to_cells();
|
||||||
}}
|
}}
|
||||||
|
on_restore_focus={handle_restore_focus}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue