diff --git a/docs/architecture/ribbon.md b/docs/architecture/ribbon.md index 68ed494..a7e420a 100644 --- a/docs/architecture/ribbon.md +++ b/docs/architecture/ribbon.md @@ -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 diff --git a/src/lib/components/ribbon/Ribbon.svelte b/src/lib/components/ribbon/Ribbon.svelte index ecb8bfe..6528cfa 100644 --- a/src/lib/components/ribbon/Ribbon.svelte +++ b/src/lib/components/ribbon/Ribbon.svelte @@ -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(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 @@ {/if} + { linkOpen = false; editor?.chain().focus().extendMarkRange('link').unsetLink().run(); }} onClose={() => (linkOpen = false)} /> + { bookmarkOpen = false; editor?.chain().focus().setBookmark(n).run(); }} onRemove={(n) => editor?.chain().focus().removeBookmark(n).run()} onGoTo={goToBookmark} onClose={() => (bookmarkOpen = false)} /> + { crossRefOpen = false; editor?.chain().focus().insertCrossRef({ name: n, format: f }).run(); }} onClose={() => (crossRefOpen = false)} /> + (tabsDialogOpen = true)} /> diff --git a/src/lib/components/ribbon/tabs/InsertTab.svelte b/src/lib/components/ribbon/tabs/InsertTab.svelte index 570697d..1c45d69 100644 --- a/src/lib/components/ribbon/tabs/InsertTab.svelte +++ b/src/lib/components/ribbon/tabs/InsertTab.svelte @@ -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(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 @@ @@ -331,7 +230,7 @@ /> {t().ribbon.symbol} - + -