295 lines
7.8 KiB
Svelte
295 lines
7.8 KiB
Svelte
<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>
|