fix: a table popover opens where it fits, in both chromes
The three dialogs placed themselves, so the ribbon's viewport coordinates met CSS that hangs the box above its anchor: the formula popover landed under the band, clipped. Each caller now owns the placement — `anchored` in the ribbon, like every other menu there, and a wrapper in the editor that clamps a tall one into the scroll area instead of letting the overflow cut it.
This commit is contained in:
1 parent
3b24b14470
commit
8d866e4f1a
6 files changed
+184
-231
No files matched your search
@@ -70,7 +70,9 @@ at every height. A flex row cannot reserve width in part of its height, so it mo
|
||||
- **A menu must leave the flow.** `.ribbon-body` clips its own overflow, so the `anchored` action
|
||||
(`ribbon/menu.svelte.ts`) switches a panel to `position: fixed` and pins it under its wrapper,
|
||||
clamped inside the window. Panels drop below the **whole band**, not below their own button —
|
||||
inside it they would cover the controls and group labels underneath.
|
||||
inside it they would cover the controls and group labels underneath. A panel has to be out of
|
||||
the flow (`position: absolute`) *before* it mounts: in it, it stretches the wrapper `anchored`
|
||||
measures and lands its own height too low.
|
||||
- **A reused picker anchors its panel itself**, and no prop says when it opened. `pinPanels`,
|
||||
one action on the band, watches for a mounted absolutely-positioned node and pins that; the
|
||||
ColorPicker places its own and only needs the band-aware top.
|
||||
|
||||
@@ -497,6 +497,16 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor';
|
||||
$effect(() => {
|
||||
if (!tableUi.visible && tableDialog) tableDialog = null;
|
||||
});
|
||||
// A dialog hangs above the table like the toolbar, so a table near the top of the
|
||||
// scroller puts it outside the scroll area, where the overflow clips it. The
|
||||
// container's padding is what the toolbar island overlays, so clear that too.
|
||||
let tableDialogH = $state(0);
|
||||
let tableDialogTop = $derived.by(() => {
|
||||
const c = editorContainer;
|
||||
if (!c || !tableDialogH) return tableUi.top;
|
||||
const pad = parseFloat(getComputedStyle(c).paddingTop) || 0;
|
||||
return Math.max(tableUi.top, c.scrollTop + pad + tableDialogH + 6);
|
||||
});
|
||||
|
||||
// The DOM element of the table containing the current selection, or null.
|
||||
// nodeDOM(before(table)) returns the wrapper div the table node view renders.
|
||||
@@ -1109,42 +1119,39 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor';
|
||||
onDialog={(which) => (tableDialog = which)}
|
||||
/>
|
||||
{/if}
|
||||
{#if tableUi.visible}
|
||||
<TableSplitDialog
|
||||
open={tableDialog === 'split'}
|
||||
top={tableUi.top}
|
||||
left={tableUi.left}
|
||||
onApply={(cols, rows) => {
|
||||
editor?.chain().focus().splitCellInto(cols, rows).run();
|
||||
tableDialog = null;
|
||||
}}
|
||||
onClose={() => (tableDialog = null)}
|
||||
/>
|
||||
<TableSortDialog
|
||||
open={tableDialog === 'sort'}
|
||||
top={tableUi.top}
|
||||
left={tableUi.left}
|
||||
columns={tableGrid.columns}
|
||||
column={tableGrid.column}
|
||||
headerRow={tableGrid.headerRow}
|
||||
onApply={(options) => {
|
||||
editor?.chain().focus().sortTable(options).run();
|
||||
tableDialog = null;
|
||||
}}
|
||||
onClose={() => (tableDialog = null)}
|
||||
/>
|
||||
<TableFormulaDialog
|
||||
open={tableDialog === 'formula'}
|
||||
top={tableUi.top}
|
||||
left={tableUi.left}
|
||||
cell={tableGrid.cell}
|
||||
initial={tableGrid.formula}
|
||||
onApply={(formula) => {
|
||||
editor?.chain().focus().setCellFormula(formula).run();
|
||||
tableDialog = null;
|
||||
}}
|
||||
onClose={() => (tableDialog = null)}
|
||||
/>
|
||||
{#if tableUi.visible && tableDialog}
|
||||
<div class="table-dialog" bind:clientHeight={tableDialogH} style="top: {tableDialogTop}px; left: {tableUi.left}px;">
|
||||
{#if tableDialog === 'split'}
|
||||
<TableSplitDialog
|
||||
onApply={(cols, rows) => {
|
||||
editor?.chain().focus().splitCellInto(cols, rows).run();
|
||||
tableDialog = null;
|
||||
}}
|
||||
onClose={() => (tableDialog = null)}
|
||||
/>
|
||||
{:else if tableDialog === 'sort'}
|
||||
<TableSortDialog
|
||||
columns={tableGrid.columns}
|
||||
column={tableGrid.column}
|
||||
headerRow={tableGrid.headerRow}
|
||||
onApply={(options) => {
|
||||
editor?.chain().focus().sortTable(options).run();
|
||||
tableDialog = null;
|
||||
}}
|
||||
onClose={() => (tableDialog = null)}
|
||||
/>
|
||||
{:else}
|
||||
<TableFormulaDialog
|
||||
cell={tableGrid.cell}
|
||||
initial={tableGrid.formula}
|
||||
onApply={(formula) => {
|
||||
editor?.chain().focus().setCellFormula(formula).run();
|
||||
tableDialog = null;
|
||||
}}
|
||||
onClose={() => (tableDialog = null)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if imageUi.visible}
|
||||
<ImageToolbar {editor} top={imageUi.top} left={imageUi.left} wrap={imageUi.wrap} />
|
||||
@@ -1184,6 +1191,14 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor';
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
/* The table popovers sit above the table's top-left corner like the toolbar. The
|
||||
wrapper is what places them, so one measurement keeps a tall one in view. */
|
||||
.table-dialog {
|
||||
position: absolute;
|
||||
transform: translateY(calc(-100% - 6px));
|
||||
z-index: 300;
|
||||
}
|
||||
|
||||
/* Hover hint for links (Word/LibreOffice style): URL + modifier-click tip, sitting
|
||||
just above the link. A child of .editor so it scrolls with content but isn't scaled
|
||||
by the zoom transform; pointer-events:none keeps the link itself clickable. */
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
import { FORMULA_FUNCTIONS } from '../utils/tableFormula';
|
||||
// Word's Table ▸ Formula: the guessed formula, editable, plus the function list to
|
||||
// paste from. The parent owns `open` and holds the cell the formula is set on.
|
||||
// paste from. The parent mounts it and holds the cell the formula is set on.
|
||||
let {
|
||||
open,
|
||||
top,
|
||||
left,
|
||||
cell = null,
|
||||
initial = '=SUM(ABOVE)',
|
||||
onApply,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
top: number;
|
||||
left: number;
|
||||
/** The cell's name (A1), which is how a formula refers to it. */
|
||||
cell?: string | null;
|
||||
initial?: string;
|
||||
@@ -22,15 +17,13 @@
|
||||
onClose: () => void;
|
||||
} = $props();
|
||||
|
||||
let text = $state('');
|
||||
// Seeded from the cell the dialog opened on; from there the field is the user's.
|
||||
let text = $state(untrack(() => initial));
|
||||
let field = $state<HTMLInputElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
text = initial;
|
||||
queueMicrotask(() => field?.focus());
|
||||
queueMicrotask(() => field?.select());
|
||||
}
|
||||
queueMicrotask(() => field?.focus());
|
||||
queueMicrotask(() => field?.select());
|
||||
});
|
||||
|
||||
// A formula the cell no longer has is an empty field, which clears it.
|
||||
@@ -49,37 +42,25 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="formula-dialog"
|
||||
style="top: {top}px; left: {left}px;"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
aria-label={t().table.formulaDialogLabel}
|
||||
>
|
||||
<label class="formula-row">
|
||||
<span>{cell ? t().table.formulaCell(cell) : t().table.formulaField}</span>
|
||||
<input bind:this={field} bind:value={text} type="text" spellcheck="false" onkeydown={onKeydown} />
|
||||
</label>
|
||||
<div class="formula-funcs">
|
||||
{#each FORMULA_FUNCTIONS as fn}
|
||||
<button class="formula-fn" onclick={() => paste(fn)}>{fn}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="formula-actions">
|
||||
<span class="formula-spacer"></span>
|
||||
<button class="formula-cancel" onclick={onClose}>{t().common.cancel}</button>
|
||||
<button class="formula-apply" onclick={apply}>{t().common.ok}</button>
|
||||
</div>
|
||||
<div class="formula-dialog" role="dialog" tabindex="-1" aria-label={t().table.formulaDialogLabel}>
|
||||
<label class="formula-row">
|
||||
<span>{cell ? t().table.formulaCell(cell) : t().table.formulaField}</span>
|
||||
<input bind:this={field} bind:value={text} type="text" spellcheck="false" onkeydown={onKeydown} />
|
||||
</label>
|
||||
<div class="formula-funcs">
|
||||
{#each FORMULA_FUNCTIONS as fn}
|
||||
<button class="formula-fn" onclick={() => paste(fn)}>{fn}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="formula-actions">
|
||||
<span class="formula-spacer"></span>
|
||||
<button class="formula-cancel" onclick={onClose}>{t().common.cancel}</button>
|
||||
<button class="formula-apply" onclick={apply}>{t().common.ok}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.formula-dialog {
|
||||
position: absolute;
|
||||
/* Sit just above the table's top-left corner, like TableToolbar. */
|
||||
transform: translateY(calc(-100% - 6px));
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
import { colName } from '../utils/tableFormula';
|
||||
import type { TableSortOptions } from '../editor/extensions/tableSort';
|
||||
// LibreOffice's Table ▸ Sort / Word's Layout ▸ Sort, on the one key both put
|
||||
// first. The parent owns `open` and holds the cell selection the sort runs against.
|
||||
// first. The parent mounts it and holds the cell selection the sort runs against.
|
||||
let {
|
||||
open,
|
||||
top,
|
||||
left,
|
||||
columns,
|
||||
column = 0,
|
||||
headerRow = false,
|
||||
onApply,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
top: number;
|
||||
left: number;
|
||||
columns: number;
|
||||
/** The cursor's column, which is what the dialog offers first. */
|
||||
column?: number;
|
||||
@@ -25,18 +20,14 @@
|
||||
onClose: () => void;
|
||||
} = $props();
|
||||
|
||||
let pick = $state(0);
|
||||
let header = $state(false);
|
||||
// Seeded from the cell the dialog opened on; from there the fields are the user's.
|
||||
let pick = $state(untrack(() => Math.min(Math.max(0, column), Math.max(0, columns - 1))));
|
||||
let header = $state(untrack(() => headerRow));
|
||||
let descending = $state(false);
|
||||
let firstField = $state<HTMLSelectElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
pick = Math.min(Math.max(0, column), Math.max(0, columns - 1));
|
||||
header = headerRow;
|
||||
descending = false;
|
||||
queueMicrotask(() => firstField?.focus());
|
||||
}
|
||||
queueMicrotask(() => firstField?.focus());
|
||||
});
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
@@ -45,51 +36,38 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="sort-dialog"
|
||||
style="top: {top}px; left: {left}px;"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
aria-label={t().table.sortDialogLabel}
|
||||
onkeydown={onKeydown}
|
||||
>
|
||||
<label class="sort-row">
|
||||
<span>{t().table.sortBy}</span>
|
||||
<select bind:this={firstField} bind:value={pick}>
|
||||
{#each Array.from({ length: columns }, (_, i) => i) as i}
|
||||
<option value={i}>{colName(i)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<div class="sort-dirs" role="radiogroup" aria-label={t().table.sortDialogLabel}>
|
||||
{#each [{ v: false, label: t().table.sortAscending }, { v: true, label: t().table.sortDescending }] as d}
|
||||
<label class="sort-radio">
|
||||
<input type="radio" value={d.v} bind:group={descending} />
|
||||
<span>{d.label}</span>
|
||||
</label>
|
||||
<div class="sort-dialog" role="dialog" tabindex="-1" aria-label={t().table.sortDialogLabel} onkeydown={onKeydown}>
|
||||
<label class="sort-row">
|
||||
<span>{t().table.sortBy}</span>
|
||||
<select bind:this={firstField} bind:value={pick}>
|
||||
{#each Array.from({ length: columns }, (_, i) => i) as i}
|
||||
<option value={i}>{colName(i)}</option>
|
||||
{/each}
|
||||
</div>
|
||||
<label class="sort-radio">
|
||||
<input type="checkbox" bind:checked={header} />
|
||||
<span>{t().table.sortHeaderRow}</span>
|
||||
</label>
|
||||
<div class="sort-actions">
|
||||
<span class="sort-spacer"></span>
|
||||
<button class="sort-cancel" onclick={onClose}>{t().common.cancel}</button>
|
||||
<button class="sort-apply" onclick={() => onApply({ column: pick, descending, headerRow: header })}>
|
||||
{t().common.ok}
|
||||
</button>
|
||||
</div>
|
||||
</select>
|
||||
</label>
|
||||
<div class="sort-dirs" role="radiogroup" aria-label={t().table.sortDialogLabel}>
|
||||
{#each [{ v: false, label: t().table.sortAscending }, { v: true, label: t().table.sortDescending }] as d}
|
||||
<label class="sort-radio">
|
||||
<input type="radio" value={d.v} bind:group={descending} />
|
||||
<span>{d.label}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<label class="sort-radio">
|
||||
<input type="checkbox" bind:checked={header} />
|
||||
<span>{t().table.sortHeaderRow}</span>
|
||||
</label>
|
||||
<div class="sort-actions">
|
||||
<span class="sort-spacer"></span>
|
||||
<button class="sort-cancel" onclick={onClose}>{t().common.cancel}</button>
|
||||
<button class="sort-apply" onclick={() => onApply({ column: pick, descending, headerRow: header })}>
|
||||
{t().common.ok}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sort-dialog {
|
||||
position: absolute;
|
||||
/* Sit just above the table's top-left corner, like TableToolbar. */
|
||||
transform: translateY(calc(-100% - 6px));
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
// Word/LibreOffice "Split Cells…" popover: collects column + row counts. The parent
|
||||
// owns `open` and runs the splitCellInto command against the saved cell selection
|
||||
// mounts it and runs the splitCellInto command against the saved cell selection
|
||||
// (focus moves to the inputs, like LinkDialog/TablePicker).
|
||||
let {
|
||||
open,
|
||||
top,
|
||||
left,
|
||||
onApply,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
top: number;
|
||||
left: number;
|
||||
onApply: (cols: number, rows: number) => void;
|
||||
onClose: () => void;
|
||||
} = $props();
|
||||
@@ -24,14 +18,9 @@
|
||||
let rowsInput = $state('1');
|
||||
let firstField = $state<HTMLInputElement | null>(null);
|
||||
|
||||
// Reset to the defaults each time the popover opens, then focus the columns field.
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
colsInput = '2';
|
||||
rowsInput = '1';
|
||||
queueMicrotask(() => firstField?.focus());
|
||||
queueMicrotask(() => firstField?.select());
|
||||
}
|
||||
queueMicrotask(() => firstField?.focus());
|
||||
queueMicrotask(() => firstField?.select());
|
||||
});
|
||||
|
||||
function clamp(v: string): number {
|
||||
@@ -50,51 +39,39 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="split-dialog"
|
||||
style="top: {top}px; left: {left}px;"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
aria-label={t().table.splitDialogLabel}
|
||||
>
|
||||
<div class="split-fields">
|
||||
<label>
|
||||
<span>{t().table.columns}</span>
|
||||
<input
|
||||
bind:this={firstField}
|
||||
bind:value={colsInput}
|
||||
type="number"
|
||||
min={MIN}
|
||||
max={MAX}
|
||||
onkeydown={onKeydown}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t().table.rowsField}</span>
|
||||
<input
|
||||
bind:value={rowsInput}
|
||||
type="number"
|
||||
min={MIN}
|
||||
max={MAX}
|
||||
onkeydown={onKeydown}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="split-actions">
|
||||
<span class="split-spacer"></span>
|
||||
<button class="split-cancel" onclick={onClose}>{t().common.cancel}</button>
|
||||
<button class="split-apply" onclick={apply}>{t().table.split}</button>
|
||||
</div>
|
||||
<div class="split-dialog" role="dialog" tabindex="-1" aria-label={t().table.splitDialogLabel}>
|
||||
<div class="split-fields">
|
||||
<label>
|
||||
<span>{t().table.columns}</span>
|
||||
<input
|
||||
bind:this={firstField}
|
||||
bind:value={colsInput}
|
||||
type="number"
|
||||
min={MIN}
|
||||
max={MAX}
|
||||
onkeydown={onKeydown}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t().table.rowsField}</span>
|
||||
<input
|
||||
bind:value={rowsInput}
|
||||
type="number"
|
||||
min={MIN}
|
||||
max={MAX}
|
||||
onkeydown={onKeydown}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="split-actions">
|
||||
<span class="split-spacer"></span>
|
||||
<button class="split-cancel" onclick={onClose}>{t().common.cancel}</button>
|
||||
<button class="split-apply" onclick={apply}>{t().table.split}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.split-dialog {
|
||||
position: absolute;
|
||||
/* Sit just above the table's top-left corner, like TableToolbar. */
|
||||
transform: translateY(calc(-100% - 6px));
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
@@ -27,16 +27,6 @@
|
||||
which: 'design' | 'layout';
|
||||
} = $props();
|
||||
|
||||
// One popover at a time, opened under the button that owns it.
|
||||
let dialog = $state<'split' | 'sort' | 'formula' | null>(null);
|
||||
let dialogAt = $state({ top: 0, left: 0 });
|
||||
|
||||
function openDialog(e: MouseEvent, which: 'split' | 'sort' | 'formula') {
|
||||
const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
dialogAt = { top: r.bottom + 4, left: r.left };
|
||||
dialog = which;
|
||||
}
|
||||
|
||||
function run(cmd: (chain: ChainedCommands) => ChainedCommands) {
|
||||
if (!editor) return;
|
||||
cmd(editor.chain().focus()).run();
|
||||
@@ -200,17 +190,18 @@
|
||||
|
||||
<RibbonGroup label={t().ribbon.groups.merge}>
|
||||
<RibbonButton variant="big" icon="merge" label={t().table.mergeCells} disabled={!canMerge} onclick={() => run((c) => c.mergeCells())} />
|
||||
<span class="split-anchor">
|
||||
<button class="split-trigger" onclick={(e) => openDialog(e, 'split')} title={t().table.splitCells}>
|
||||
<span class="rb-menu-wrap" use:clickOutside={'tableSplit'}>
|
||||
<button class="split-trigger" onclick={() => toggleMenu('tableSplit')} title={t().table.splitCells}>
|
||||
<RibbonButton variant="big" icon="split" label={t().table.splitCellsAria} />
|
||||
</button>
|
||||
<TableSplitDialog
|
||||
open={dialog === 'split'}
|
||||
top={dialogAt.top}
|
||||
left={dialogAt.left}
|
||||
onApply={(cols, rows) => { dialog = null; editor?.chain().focus().splitCellInto(cols, rows).run(); }}
|
||||
onClose={() => (dialog = null)}
|
||||
/>
|
||||
{#if isMenuOpen('tableSplit')}
|
||||
<div class="table-dialog" use:anchored>
|
||||
<TableSplitDialog
|
||||
onApply={(cols, rows) => { closeMenu(); editor?.chain().focus().splitCellInto(cols, rows).run(); }}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
</RibbonGroup>
|
||||
|
||||
@@ -268,34 +259,36 @@
|
||||
<div class="ribbon-sep"></div>
|
||||
|
||||
<RibbonGroup label={t().ribbon.groups.tableData}>
|
||||
<span class="split-anchor">
|
||||
<button class="split-trigger" onclick={(e) => openDialog(e, 'sort')} title={t().table.sort}>
|
||||
<span class="rb-menu-wrap" use:clickOutside={'tableSort'}>
|
||||
<button class="split-trigger" onclick={() => toggleMenu('tableSort')} title={t().table.sort}>
|
||||
<RibbonButton variant="big" icon="sortRows" label={t().table.sortAria} disabled={!inTable} />
|
||||
</button>
|
||||
<TableSortDialog
|
||||
open={dialog === 'sort'}
|
||||
top={dialogAt.top}
|
||||
left={dialogAt.left}
|
||||
columns={grid.columns}
|
||||
column={grid.column}
|
||||
headerRow={grid.headerRow}
|
||||
onApply={(options) => { dialog = null; editor?.chain().focus().sortTable(options).run(); }}
|
||||
onClose={() => (dialog = null)}
|
||||
/>
|
||||
{#if isMenuOpen('tableSort') && inTable}
|
||||
<div class="table-dialog" use:anchored>
|
||||
<TableSortDialog
|
||||
columns={grid.columns}
|
||||
column={grid.column}
|
||||
headerRow={grid.headerRow}
|
||||
onApply={(options) => { closeMenu(); editor?.chain().focus().sortTable(options).run(); }}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="split-anchor">
|
||||
<button class="split-trigger" onclick={(e) => openDialog(e, 'formula')} title={t().table.formula}>
|
||||
<span class="rb-menu-wrap" use:clickOutside={'tableFormula'}>
|
||||
<button class="split-trigger" onclick={() => toggleMenu('tableFormula')} title={t().table.formula}>
|
||||
<RibbonButton variant="big" icon="formula" label={t().table.formulaAria} disabled={!inTable} />
|
||||
</button>
|
||||
<TableFormulaDialog
|
||||
open={dialog === 'formula'}
|
||||
top={dialogAt.top}
|
||||
left={dialogAt.left}
|
||||
cell={grid.cell}
|
||||
initial={grid.formula}
|
||||
onApply={(formula) => { dialog = null; editor?.chain().focus().setCellFormula(formula).run(); }}
|
||||
onClose={() => (dialog = null)}
|
||||
/>
|
||||
{#if isMenuOpen('tableFormula') && inTable}
|
||||
<div class="table-dialog" use:anchored>
|
||||
<TableFormulaDialog
|
||||
cell={grid.cell}
|
||||
initial={grid.formula}
|
||||
onApply={(formula) => { closeMenu(); editor?.chain().focus().setCellFormula(formula).run(); }}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
<RibbonButton
|
||||
variant="small"
|
||||
@@ -341,8 +334,8 @@
|
||||
|
||||
.inserts :global(.rb-stack-caret) { display: none; }
|
||||
|
||||
/* The dialog positions itself in viewport coordinates, so the trigger only has
|
||||
to hand it the button's rect. */
|
||||
/* A big button wrapped in the trigger the popover hangs from; `anchored` drops
|
||||
the popover below the whole band, like every other ribbon menu. */
|
||||
.split-trigger {
|
||||
display: contents;
|
||||
border: none;
|
||||
@@ -350,6 +343,13 @@
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Out of flow before `anchored` measures, like .ribbon-menu: in flow it would
|
||||
stretch its own anchor and land that much too low. */
|
||||
.table-dialog {
|
||||
position: absolute;
|
||||
z-index: 300;
|
||||
}
|
||||
.margin-menu { min-width: 210px; }
|
||||
|
||||
.margin-grid {
|
||||
|
||||
Reference in new issue
Block a user