Files
edentext/src/lib/components/TablePicker.svelte
T

295 lines
7.8 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import type { Editor } from '@tiptap/core';
import { t } from '../i18n/i18n.svelte';
let {
editor,
open = $bindable(false),
onOpen,
onInsert,
}: {
editor: Editor | null;
open?: boolean;
onOpen?: () => void;
// Mirrors ColorPicker's onApply: the parent runs the actual editor command
// against the saved selection range (focus is stolen by the popover).
onInsert: (rows: number, cols: number, range: { from: number; to: number }) => void;
} = $props();
// Quick-pick grid size (like Word/LibreOffice). Hovering selects the top-left
// rows×cols block; clicking inserts it.
const GRID_ROWS = 8;
const GRID_COLS = 10;
const ROW_INDICES = Array.from({ length: GRID_ROWS }, (_, i) => i + 1);
const COL_INDICES = Array.from({ length: GRID_COLS }, (_, i) => i + 1);
// Manual-entry bounds for the row/column number inputs.
const MAX_ROWS = 50;
const MAX_COLS = 20;
let hoverRows = $state(0);
let hoverCols = $state(0);
let rowsInput = $state('3');
let colsInput = $state('3');
let savedFrom: number | null = null;
let savedTo: number | null = null;
function clamp(n: number, min: number, max: number): number {
if (!Number.isFinite(n)) return min;
return Math.max(min, Math.min(max, Math.round(n)));
}
function openPicker() {
if (!editor) return;
onOpen?.();
savedFrom = editor.state.selection.from;
savedTo = editor.state.selection.to;
if (open) {
open = false;
} else {
hoverRows = 0;
hoverCols = 0;
open = true;
}
}
function insert(rows: number, cols: number) {
if (!editor) return;
open = false;
const from = savedFrom ?? editor.state.selection.from;
const to = savedTo ?? editor.state.selection.to;
savedFrom = null;
savedTo = null;
onInsert(rows, cols, { from, to });
}
function insertFromInputs() {
insert(clamp(parseInt(rowsInput, 10), 1, MAX_ROWS), clamp(parseInt(colsInput, 10), 1, MAX_COLS));
}
function onInputKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
insertFromInputs();
}
}
function tablePickerClickOutside(node: HTMLElement) {
function handler(e: MouseEvent) {
if (!node.contains(e.target as Node)) open = false;
}
window.addEventListener('mousedown', handler);
return { destroy() { window.removeEventListener('mousedown', handler); } };
}
</script>
<div class="table-picker" use:tablePickerClickOutside>
<button class="table-trigger" onclick={openPicker} title={t().table.insertTable} aria-pressed={open}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="1.5" y="1.5" width="13" height="13" rx="1" stroke="currentColor" stroke-width="1.5"/>
<line x1="1.5" y1="5.5" x2="14.5" y2="5.5" stroke="currentColor" stroke-width="1"/>
<line x1="1.5" y1="9.5" x2="14.5" y2="9.5" stroke="currentColor" stroke-width="1"/>
<line x1="5.5" y1="5.5" x2="5.5" y2="14.5" stroke="currentColor" stroke-width="1"/>
<line x1="10.5" y1="5.5" x2="10.5" y2="14.5" stroke="currentColor" stroke-width="1"/>
</svg>
</button>
{#if open}
<div class="table-dropdown">
<div class="table-dim-label">
{#if hoverRows > 0 && hoverCols > 0}
{t().table.dimensions(hoverCols, hoverRows)}
{:else}
{t().table.insertTable}
{/if}
</div>
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
<div
class="table-grid"
role="grid"
tabindex="-1"
aria-label={t().table.selectSize}
onmouseleave={() => { hoverRows = 0; hoverCols = 0; }}
>
{#each ROW_INDICES as r}
<div class="table-grid-row" role="row">
{#each COL_INDICES as c}
<button
class="table-grid-cell"
class:hot={r <= hoverRows && c <= hoverCols}
role="gridcell"
aria-label={t().table.byDimension(c, r)}
onmouseover={() => { hoverRows = r; hoverCols = c; }}
onfocus={() => { hoverRows = r; hoverCols = c; }}
onclick={() => insert(r, c)}
></button>
{/each}
</div>
{/each}
</div>
<div class="table-manual">
<label class="table-manual-field">
<span>{t().table.rows}</span>
<input
type="text"
inputmode="numeric"
bind:value={rowsInput}
onkeydown={onInputKeydown}
aria-label={t().table.numberOfRows}
/>
</label>
<label class="table-manual-field">
<span>{t().table.cols}</span>
<input
type="text"
inputmode="numeric"
bind:value={colsInput}
onkeydown={onInputKeydown}
aria-label={t().table.numberOfColumns}
/>
</label>
<button class="table-insert-btn" onclick={insertFromInputs}>{t().table.insert}</button>
</div>
</div>
{/if}
</div>
<style>
.table-picker {
position: relative;
}
.table-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: var(--toolbar-btn-size);
height: var(--toolbar-btn-size);
padding: 0 0.3rem;
border: 1px solid transparent;
border-radius: var(--radius);
background: transparent;
color: var(--color-text);
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.table-trigger:hover {
background: var(--color-btn-hover);
border-color: var(--color-primary);
}
.table-trigger[aria-pressed='true'] {
background: var(--color-primary);
color: white;
}
.table-dropdown {
position: absolute;
top: calc(100% + 3px);
left: 0;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
z-index: 200;
padding: 8px;
/* Anchor near the right edge of the toolbar — keep it from spilling off-screen. */
right: 0;
left: auto;
}
.table-dim-label {
font-size: 0.78rem;
font-family: var(--font-sans);
color: var(--color-text);
text-align: center;
margin-bottom: 6px;
user-select: none;
}
.table-grid {
display: flex;
flex-direction: column;
gap: 2px;
}
.table-grid-row {
display: flex;
gap: 2px;
}
.table-grid-cell {
width: 16px;
height: 16px;
min-width: unset;
padding: 0;
border: 1px solid var(--color-border);
border-radius: 2px;
background: var(--color-surface);
cursor: pointer;
}
.table-grid-cell.hot {
border-color: var(--color-primary);
background: var(--color-primary);
}
.table-manual {
display: flex;
align-items: flex-end;
gap: 6px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--color-border);
}
.table-manual-field {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 0.7rem;
font-family: var(--font-sans);
color: var(--color-text-muted);
}
.table-manual-field input {
width: 44px;
height: 1.8rem;
padding: 0 0.4rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text);
font-size: 0.8rem;
font-family: var(--font-sans);
outline: none;
transition: border-color 0.15s;
}
.table-manual-field input:hover,
.table-manual-field input:focus {
border-color: var(--color-primary);
}
.table-insert-btn {
height: 1.8rem;
padding: 0 0.7rem;
min-width: unset;
border: 1px solid var(--color-primary);
border-radius: var(--radius);
background: var(--color-primary);
color: white;
font-size: 0.78rem;
font-family: var(--font-sans);
cursor: pointer;
}
.table-insert-btn:hover {
opacity: 0.9;
}
</style>