fix: the event-opened dialogs live where they are always mounted
The ribbon mounts only the open tab, so the link, bookmark, cross- reference and formula dialogs in InsertTab.svelte heard neither Ctrl+K, nor the context menu, nor a double-click on a formula while any other tab was up. They move to Ribbon.svelte; the Insert tab's buttons fire the same events, and a formula event without a position opens the empty dialog the button used to open directly.
This commit is contained in:
1 parent
57dccb26fa
commit
fc5c39322f
4 files changed
+128
-110
No files matched your search
@@ -140,6 +140,12 @@ margins, Save As.
|
||||
`ParagraphDialog` and `TabsDialog` open from Word's ↘ launcher in a group's corner. They are
|
||||
chrome-agnostic, so the modern chrome could gain launchers of its own at no cost.
|
||||
|
||||
Every dialog opened by an **event** — link (Ctrl+K, the context menu), bookmark, cross-reference,
|
||||
formula (a double-click on one) — is mounted in `Ribbon.svelte`, not in the Insert tab whose
|
||||
buttons also open it: only the open tab is mounted, so a listener in a closed one hears nothing
|
||||
and the double-click did nothing unless Insert happened to be up. The tab's buttons fire the same
|
||||
events; a formula event without a position starts an empty dialog.
|
||||
|
||||
There is **no Font dialog**: the Home tab already carries change case and the underline and
|
||||
strikethrough line styles, and the two things left over — `letterSpacingPt` and `kerning` — live
|
||||
on a named style, not on a run. Setting them as direct formatting would mean a new mark attribute
|
||||
|
||||
@@ -15,11 +15,19 @@
|
||||
import UiLanguagePicker from '../UiLanguagePicker.svelte';
|
||||
import ParagraphDialog from '../ParagraphDialog.svelte';
|
||||
import TabsDialog from '../TabsDialog.svelte';
|
||||
import LinkDialog from '../LinkDialog.svelte';
|
||||
import BookmarkDialog from '../BookmarkDialog.svelte';
|
||||
import CrossRefDialog from '../CrossRefDialog.svelte';
|
||||
import FormulaDialog from '../FormulaDialog.svelte';
|
||||
import { clickOutside, isMenuOpen, pinPanels, toggleMenu, closeMenu } from './menu.svelte';
|
||||
import { t } from '../../i18n/i18n.svelte';
|
||||
import { withShortcut } from '../../i18n/shortcut';
|
||||
import { shortcutHint } from '../../editor/shortcuts';
|
||||
import { findTextBox } from '../../editor/extensions/textBox';
|
||||
import { OPEN_LINK_DIALOG_EVENT } from '../../editor/extensions/link';
|
||||
import { OPEN_BOOKMARK_DIALOG_EVENT, bookmarkNames, findBookmark } from '../../editor/extensions/bookmark';
|
||||
import { OPEN_CROSS_REF_DIALOG_EVENT } from '../../editor/extensions/crossReference';
|
||||
import { EDIT_FORMULA_EVENT } from '../../editor/extensions/formula';
|
||||
import { loadRibbonCollapsed, saveRibbonCollapsed, type ChromeMode, type ThemeMode } from '../../storage/theme';
|
||||
import type { StyleFamily } from '../../styles/styleSheet';
|
||||
import { DEFAULT_MARGINS, type PageMargins } from '../../storage/pageMargins';
|
||||
@@ -177,6 +185,99 @@
|
||||
if (!shown.includes(tab)) tab = 'home';
|
||||
});
|
||||
|
||||
// The four dialogs the Insert tab's buttons, Ctrl+K, the context menu and a
|
||||
// double-click on a formula all open. They live here, not in the tab: the ribbon
|
||||
// mounts only the open tab, and a dialog in a closed one hears no event.
|
||||
let linkOpen = $state(false);
|
||||
let linkUrl = $state('');
|
||||
let bookmarkOpen = $state(false);
|
||||
let crossRefOpen = $state(false);
|
||||
let formulaOpen = $state(false);
|
||||
let formulaLatex = $state('');
|
||||
let formulaDisplay = $state(false);
|
||||
let formulaPos = $state<number | null>(null);
|
||||
|
||||
let hasSelection = $derived(tick >= 0 && !!editor && !editor.state.selection.empty);
|
||||
let isLink = $derived(tick >= 0 && !!editor?.isActive('link'));
|
||||
let bmNames = $derived(tick >= 0 && editor && !hfActive ? bookmarkNames(editor.state.doc) : []);
|
||||
|
||||
function openLink() {
|
||||
if (!editor || hfActive) return; // body-only; the HF schema has no link mark
|
||||
linkUrl = (editor.getAttributes('link').href as string) ?? '';
|
||||
linkOpen = true;
|
||||
}
|
||||
|
||||
// A bare host or e-mail gets a scheme, as in Word and LibreOffice.
|
||||
function normalizeUrl(raw: string): string {
|
||||
const s = raw.trim();
|
||||
if (!s) return '';
|
||||
if (/^(https?:|mailto:|tel:|ftp:|#|\/)/i.test(s)) return s;
|
||||
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) return `mailto:${s}`;
|
||||
return `https://${s}`;
|
||||
}
|
||||
|
||||
function applyLink(raw: string) {
|
||||
linkOpen = false;
|
||||
if (!editor) return;
|
||||
const href = normalizeUrl(raw);
|
||||
if (!href) return;
|
||||
const { empty } = editor.state.selection;
|
||||
// With nothing selected the URL becomes its own link text.
|
||||
if (empty) editor.chain().focus().insertContent({ type: 'text', text: href, marks: [{ type: 'link', attrs: { href } }] }).run();
|
||||
else editor.chain().focus().extendMarkRange('link').setLink({ href }).run();
|
||||
}
|
||||
|
||||
function openBookmark() {
|
||||
if (!editor || hfActive || !hasSelection) return;
|
||||
crossRefOpen = false;
|
||||
bookmarkOpen = true;
|
||||
}
|
||||
|
||||
function openCrossRef() {
|
||||
if (!editor || hfActive || !bmNames.length) return;
|
||||
bookmarkOpen = false;
|
||||
crossRefOpen = true;
|
||||
}
|
||||
|
||||
function goToBookmark(name: string) {
|
||||
const found = editor && findBookmark(editor.state.doc, name);
|
||||
if (!editor || !found) return;
|
||||
bookmarkOpen = false;
|
||||
editor.chain().focus().setTextSelection({ from: found.from, to: found.to }).scrollIntoView().run();
|
||||
}
|
||||
|
||||
function applyFormula(latex: string, display: boolean) {
|
||||
if (!editor) return;
|
||||
if (formulaPos != null) editor.chain().focus().updateFormula(formulaPos, { latex, display }).run();
|
||||
else editor.chain().focus().insertFormula({ latex, display }).run();
|
||||
formulaPos = null;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const onLink = () => openLink();
|
||||
const onBm = () => openBookmark();
|
||||
const onXr = () => openCrossRef();
|
||||
// With a formula's position and source: the double-click on an existing one.
|
||||
// Without: the Insert tab's button, which starts an empty dialog.
|
||||
const onFormula = (e: Event) => {
|
||||
const d = (e as CustomEvent<{ pos: number; latex: string; display: boolean }>).detail;
|
||||
formulaPos = d ? d.pos : null;
|
||||
formulaLatex = d ? d.latex : '';
|
||||
formulaDisplay = d ? d.display : false;
|
||||
formulaOpen = true;
|
||||
};
|
||||
window.addEventListener(OPEN_LINK_DIALOG_EVENT, onLink);
|
||||
window.addEventListener(OPEN_BOOKMARK_DIALOG_EVENT, onBm);
|
||||
window.addEventListener(OPEN_CROSS_REF_DIALOG_EVENT, onXr);
|
||||
window.addEventListener(EDIT_FORMULA_EVENT, onFormula);
|
||||
return () => {
|
||||
window.removeEventListener(OPEN_LINK_DIALOG_EVENT, onLink);
|
||||
window.removeEventListener(OPEN_BOOKMARK_DIALOG_EVENT, onBm);
|
||||
window.removeEventListener(OPEN_CROSS_REF_DIALOG_EVENT, onXr);
|
||||
window.removeEventListener(EDIT_FORMULA_EVENT, onFormula);
|
||||
};
|
||||
});
|
||||
|
||||
let docNameSizerWidth = $state(0);
|
||||
let paragraphDialogOpen = $state(false);
|
||||
let tabsDialogOpen = $state(false);
|
||||
@@ -393,6 +494,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<LinkDialog open={linkOpen} initialUrl={linkUrl} canRemove={isLink} onApply={applyLink} onRemove={() => { linkOpen = false; editor?.chain().focus().extendMarkRange('link').unsetLink().run(); }} onClose={() => (linkOpen = false)} />
|
||||
<BookmarkDialog open={bookmarkOpen} names={bmNames} onApply={(n) => { bookmarkOpen = false; editor?.chain().focus().setBookmark(n).run(); }} onRemove={(n) => editor?.chain().focus().removeBookmark(n).run()} onGoTo={goToBookmark} onClose={() => (bookmarkOpen = false)} />
|
||||
<CrossRefDialog open={crossRefOpen} names={bmNames} onInsert={(n, f) => { crossRefOpen = false; editor?.chain().focus().insertCrossRef({ name: n, format: f }).run(); }} onClose={() => (crossRefOpen = false)} />
|
||||
<FormulaDialog bind:open={formulaOpen} initialLatex={formulaLatex} initialDisplay={formulaDisplay} onApply={applyFormula} />
|
||||
<ParagraphDialog bind:open={paragraphDialogOpen} {editor} {tick} onTabs={() => (tabsDialogOpen = true)} />
|
||||
<TabsDialog bind:open={tabsDialogOpen} {editor} {tick} bind:tabIntervalCm />
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,10 @@
|
||||
import TablePicker from '../../TablePicker.svelte';
|
||||
import SpecialCharPicker from '../../SpecialCharPicker.svelte';
|
||||
import DateTimePicker from '../../DateTimePicker.svelte';
|
||||
import LinkDialog from '../../LinkDialog.svelte';
|
||||
import BookmarkDialog from '../../BookmarkDialog.svelte';
|
||||
import CrossRefDialog from '../../CrossRefDialog.svelte';
|
||||
import FormulaDialog from '../../FormulaDialog.svelte';
|
||||
import RubyDialog from '../../RubyDialog.svelte';
|
||||
import { captionClicks, anchored, clickOutside, isMenuOpen, toggleMenu, closeMenu } from '../menu.svelte';
|
||||
import { OPEN_LINK_DIALOG_EVENT } from '../../../editor/extensions/link';
|
||||
import { OPEN_BOOKMARK_DIALOG_EVENT, bookmarkNames, findBookmark } from '../../../editor/extensions/bookmark';
|
||||
import { OPEN_BOOKMARK_DIALOG_EVENT, bookmarkNames } from '../../../editor/extensions/bookmark';
|
||||
import { OPEN_CROSS_REF_DIALOG_EVENT } from '../../../editor/extensions/crossReference';
|
||||
import { EDIT_FORMULA_EVENT } from '../../../editor/extensions/formula';
|
||||
import { pageDimsCm, type PageFormat } from '../../../storage/pageFormat';
|
||||
@@ -88,103 +84,9 @@
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// --- Link ---
|
||||
let linkOpen = $state(false);
|
||||
let linkUrl = $state('');
|
||||
|
||||
function openLink() {
|
||||
if (!editor || hfActive) return; // body-only; the HF schema has no link mark
|
||||
linkUrl = (editor.getAttributes('link').href as string) ?? '';
|
||||
linkOpen = true;
|
||||
}
|
||||
|
||||
// A bare host or e-mail gets a scheme, as in Word and LibreOffice.
|
||||
function normalizeUrl(raw: string): string {
|
||||
const s = raw.trim();
|
||||
if (!s) return '';
|
||||
if (/^(https?:|mailto:|tel:|ftp:|#|\/)/i.test(s)) return s;
|
||||
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) return `mailto:${s}`;
|
||||
return `https://${s}`;
|
||||
}
|
||||
|
||||
function applyLink(raw: string) {
|
||||
linkOpen = false;
|
||||
if (!editor) return;
|
||||
const href = normalizeUrl(raw);
|
||||
if (!href) return;
|
||||
const { empty } = editor.state.selection;
|
||||
// With nothing selected the URL becomes its own link text.
|
||||
if (empty) editor.chain().focus().insertContent({ type: 'text', text: href, marks: [{ type: 'link', attrs: { href } }] }).run();
|
||||
else editor.chain().focus().extendMarkRange('link').setLink({ href }).run();
|
||||
}
|
||||
|
||||
// --- Bookmarks and cross-references ---
|
||||
let bookmarkOpen = $state(false);
|
||||
let crossRefOpen = $state(false);
|
||||
|
||||
function openBookmark() {
|
||||
if (!editor || hfActive || !hasSelection) return;
|
||||
crossRefOpen = false;
|
||||
bookmarkOpen = true;
|
||||
}
|
||||
|
||||
function openCrossRef() {
|
||||
if (!editor || hfActive || !bmNames.length) return;
|
||||
bookmarkOpen = false;
|
||||
crossRefOpen = true;
|
||||
}
|
||||
|
||||
function goToBookmark(name: string) {
|
||||
const found = editor && findBookmark(editor.state.doc, name);
|
||||
if (!editor || !found) return;
|
||||
bookmarkOpen = false;
|
||||
editor.chain().focus().setTextSelection({ from: found.from, to: found.to }).scrollIntoView().run();
|
||||
}
|
||||
|
||||
// --- Formula ---
|
||||
let formulaOpen = $state(false);
|
||||
let formulaLatex = $state('');
|
||||
let formulaDisplay = $state(false);
|
||||
let formulaPos = $state<number | null>(null);
|
||||
|
||||
function openFormula() {
|
||||
formulaPos = null;
|
||||
formulaLatex = '';
|
||||
formulaDisplay = false;
|
||||
formulaOpen = true;
|
||||
}
|
||||
|
||||
function applyFormula(latex: string, display: boolean) {
|
||||
if (!editor) return;
|
||||
if (formulaPos != null) editor.chain().focus().updateFormula(formulaPos, { latex, display }).run();
|
||||
else editor.chain().focus().insertFormula({ latex, display }).run();
|
||||
formulaPos = null;
|
||||
}
|
||||
|
||||
// Ctrl+K, the context menu and a double-click on a formula all arrive as events.
|
||||
$effect(() => {
|
||||
const onLink = () => openLink();
|
||||
const onBm = () => openBookmark();
|
||||
const onXr = () => openCrossRef();
|
||||
const onFormula = (e: Event) => {
|
||||
const d = (e as CustomEvent<{ pos: number; latex: string; display: boolean }>).detail;
|
||||
if (!d) return;
|
||||
formulaPos = d.pos;
|
||||
formulaLatex = d.latex;
|
||||
formulaDisplay = d.display;
|
||||
formulaOpen = true;
|
||||
};
|
||||
window.addEventListener(OPEN_LINK_DIALOG_EVENT, onLink);
|
||||
window.addEventListener(OPEN_BOOKMARK_DIALOG_EVENT, onBm);
|
||||
window.addEventListener(OPEN_CROSS_REF_DIALOG_EVENT, onXr);
|
||||
window.addEventListener(EDIT_FORMULA_EVENT, onFormula);
|
||||
return () => {
|
||||
window.removeEventListener(OPEN_LINK_DIALOG_EVENT, onLink);
|
||||
window.removeEventListener(OPEN_BOOKMARK_DIALOG_EVENT, onBm);
|
||||
window.removeEventListener(OPEN_CROSS_REF_DIALOG_EVENT, onXr);
|
||||
window.removeEventListener(EDIT_FORMULA_EVENT, onFormula);
|
||||
};
|
||||
});
|
||||
// The dialogs live in Ribbon.svelte — only the open tab is mounted, so one in here
|
||||
// would miss Ctrl+K, the context menu and the double-click on a formula. Same events.
|
||||
const open = (name: string) => () => window.dispatchEvent(new CustomEvent(name));
|
||||
|
||||
// A page field only means anything inside a zone, so open the footer first.
|
||||
function insertPageField(type: 'pageNumber' | 'pageCount') {
|
||||
@@ -237,12 +139,9 @@
|
||||
|
||||
<RibbonGroup label={t().ribbon.groups.links}>
|
||||
<div class="rb-col link-anchor">
|
||||
<RibbonButton variant="small" icon="link" label={t().ribbon.link} title={`${isLink ? t().link.dialogLabel : t().ribbon.link} (${shortcutHint('link')})`} disabled={!editor || !!hfActive} onclick={openLink} />
|
||||
<RibbonButton variant="small" icon="bookmark" label={t().ribbon.bookmark} title={hfActive ? t().toolbarExpanded.bookmarkNotInHf : hasSelection ? t().toolbarExpanded.insertBookmark : t().toolbarExpanded.bookmarkNeedsSelection} disabled={!editor || !!hfActive || !hasSelection} onclick={openBookmark} />
|
||||
<RibbonButton variant="small" icon="crossRef" label={t().ribbon.crossRef} title={hfActive ? t().toolbarExpanded.bookmarkNotInHf : bmNames.length ? t().toolbarExpanded.insertCrossRef : t().toolbarExpanded.crossRefNeedsBookmark} disabled={!editor || !!hfActive || !bmNames.length} onclick={openCrossRef} />
|
||||
<LinkDialog open={linkOpen} initialUrl={linkUrl} canRemove={isLink} onApply={applyLink} onRemove={() => { linkOpen = false; editor?.chain().focus().extendMarkRange('link').unsetLink().run(); }} onClose={() => (linkOpen = false)} />
|
||||
<BookmarkDialog open={bookmarkOpen} names={bmNames} onApply={(n) => { bookmarkOpen = false; editor?.chain().focus().setBookmark(n).run(); }} onRemove={(n) => editor?.chain().focus().removeBookmark(n).run()} onGoTo={goToBookmark} onClose={() => (bookmarkOpen = false)} />
|
||||
<CrossRefDialog open={crossRefOpen} names={bmNames} onInsert={(n, f) => { crossRefOpen = false; editor?.chain().focus().insertCrossRef({ name: n, format: f }).run(); }} onClose={() => (crossRefOpen = false)} />
|
||||
<RibbonButton variant="small" icon="link" label={t().ribbon.link} title={`${isLink ? t().link.dialogLabel : t().ribbon.link} (${shortcutHint('link')})`} disabled={!editor || !!hfActive} onclick={open(OPEN_LINK_DIALOG_EVENT)} />
|
||||
<RibbonButton variant="small" icon="bookmark" label={t().ribbon.bookmark} title={hfActive ? t().toolbarExpanded.bookmarkNotInHf : hasSelection ? t().toolbarExpanded.insertBookmark : t().toolbarExpanded.bookmarkNeedsSelection} disabled={!editor || !!hfActive || !hasSelection} onclick={open(OPEN_BOOKMARK_DIALOG_EVENT)} />
|
||||
<RibbonButton variant="small" icon="crossRef" label={t().ribbon.crossRef} title={hfActive ? t().toolbarExpanded.bookmarkNotInHf : bmNames.length ? t().toolbarExpanded.insertCrossRef : t().toolbarExpanded.crossRefNeedsBookmark} disabled={!editor || !!hfActive || !bmNames.length} onclick={open(OPEN_CROSS_REF_DIALOG_EVENT)} />
|
||||
</div>
|
||||
</RibbonGroup>
|
||||
|
||||
@@ -331,7 +230,7 @@
|
||||
/>
|
||||
<span class="rb-caption">{t().ribbon.symbol}</span>
|
||||
</div>
|
||||
<RibbonButton variant="big" content={equationIcon} label={t().ribbon.equation} title={t().formula.insert} disabled={!editor} onclick={openFormula} />
|
||||
<RibbonButton variant="big" content={equationIcon} label={t().ribbon.equation} title={t().formula.insert} disabled={!editor} onclick={open(EDIT_FORMULA_EVENT)} />
|
||||
</RibbonGroup>
|
||||
|
||||
<input
|
||||
@@ -343,7 +242,6 @@
|
||||
/>
|
||||
|
||||
<RubyDialog bind:open={rubyOpen} {editor} />
|
||||
<FormulaDialog bind:open={formulaOpen} initialLatex={formulaLatex} initialDisplay={formulaDisplay} onApply={applyFormula} />
|
||||
|
||||
<!-- Word's Equation button is a π, as its Symbol button beside it is an Ω. Same
|
||||
canvas, baseline and weight as that Ω; the size is 20 rather than its 14
|
||||
|
||||
@@ -375,6 +375,15 @@ try {
|
||||
check(Math.abs(bdx - 50) <= 2 && Math.abs(bdy - 25) <= 2,
|
||||
`a text box out of the flow is dragged by its ring (moved ${bdx}/${bdy}, wanted 50/25)`);
|
||||
|
||||
// The ribbon mounts only the open tab, so a dialog that lives in one hears no event:
|
||||
// the formula's double-click, Ctrl+K and the context menu fire while Home is up.
|
||||
await page.evaluate(() => document.querySelector('.tiptap').editor.chain().focus().insertFormula({ latex: 'a^2', display: false }).run());
|
||||
await page.waitForSelector('.tiptap .formula', { timeout: 15_000 });
|
||||
await settle(page, true);
|
||||
await page.dblclick('.tiptap .formula');
|
||||
const dialog = await page.waitForSelector('.formula-dialog', { timeout: 5_000 }).then(() => true).catch(() => false);
|
||||
check(dialog, `a double-click on a formula opens its dialog from the ${await page.locator('.ribbon-tab.active').textContent()} tab`);
|
||||
|
||||
} catch (err) {
|
||||
check(false, `dom run threw: ${err.message ?? err}`);
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user