diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b2be1..a704e81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,12 +30,13 @@ - Dynamic page snapping (editor always shows full pages) - Print-ready @media print styles with @page A4 sizing - add bottom toolbar to show page number +- Zoom feature +- font size ### Not yet implemented - ODT import / open existing file -- font size +- undo history - Images, tables -- Zoom feature - Custom right-click context menu (formatting, cut/copy) - Multi-document management - PWA / offline support diff --git a/package-lock.json b/package-lock.json index d8b60fb..d952fca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tiptap/extension-bold": "^3.23.6", "@tiptap/extension-bullet-list": "^3.23.6", "@tiptap/extension-document": "^3.23.6", + "@tiptap/extension-font-family": "^3.23.6", "@tiptap/extension-heading": "^3.23.6", "@tiptap/extension-history": "^3.23.6", "@tiptap/extension-italic": "^3.23.6", @@ -20,6 +21,7 @@ "@tiptap/extension-paragraph": "^3.23.6", "@tiptap/extension-placeholder": "^3.23.6", "@tiptap/extension-text": "^3.23.6", + "@tiptap/extension-text-style": "^3.23.6", "@tiptap/extension-underline": "^3.23.6", "@tiptap/pm": "^3.23.6", "odf-kit": "^0.13.4" @@ -509,6 +511,19 @@ "@tiptap/core": "3.23.6" } }, + "node_modules/@tiptap/extension-font-family": { + "version": "3.23.6", + "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-3.23.6.tgz", + "integrity": "sha512-V7ttSAWf+vhXU9fACPOqkIiBjNI4XRDhVpghloDgYt1LqBczyFMey97cG8tZpkNVvWmVpy/wJpHHl46AHkLc1g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-text-style": "3.23.6" + } + }, "node_modules/@tiptap/extension-heading": { "version": "3.23.6", "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.23.6.tgz", @@ -628,6 +643,19 @@ "@tiptap/core": "3.23.6" } }, + "node_modules/@tiptap/extension-text-style": { + "version": "3.23.6", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.23.6.tgz", + "integrity": "sha512-Auasgj469wkQ5ip+Zi2gaRzvqxx9qKG58+1mkT8yPE3QAGnOIg/AaKyQ7pTV77UyL3FHvLnU+KsWCad+qcobww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.23.6" + } + }, "node_modules/@tiptap/extension-underline": { "version": "3.23.6", "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.23.6.tgz", diff --git a/package.json b/package.json index fb2e56a..93ec325 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@tiptap/extension-bold": "^3.23.6", "@tiptap/extension-bullet-list": "^3.23.6", "@tiptap/extension-document": "^3.23.6", + "@tiptap/extension-font-family": "^3.23.6", "@tiptap/extension-heading": "^3.23.6", "@tiptap/extension-history": "^3.23.6", "@tiptap/extension-italic": "^3.23.6", @@ -28,6 +29,7 @@ "@tiptap/extension-paragraph": "^3.23.6", "@tiptap/extension-placeholder": "^3.23.6", "@tiptap/extension-text": "^3.23.6", + "@tiptap/extension-text-style": "^3.23.6", "@tiptap/extension-underline": "^3.23.6", "@tiptap/pm": "^3.23.6", "odf-kit": "^0.13.4" diff --git a/src/lib/editor/ToolbarExpanded.svelte b/src/lib/editor/ToolbarExpanded.svelte index a3bae78..c129989 100644 --- a/src/lib/editor/ToolbarExpanded.svelte +++ b/src/lib/editor/ToolbarExpanded.svelte @@ -7,18 +7,87 @@ let isAlignCenter = $derived(tick >= 0 && !!editor?.isActive({ textAlign: 'center' })); let isAlignRight = $derived(tick >= 0 && !!editor?.isActive({ textAlign: 'right' })); let isAlignJustify = $derived(tick >= 0 && !!editor?.isActive({ textAlign: 'justify' })); + + const FONTS = [ + { value: 'Arial', label: 'Arial' }, + { value: 'Verdana', label: 'Verdana' }, + { value: 'Trebuchet MS', label: 'Trebuchet MS' }, + { value: 'Georgia', label: 'Georgia' }, + { value: 'Times New Roman', label: 'Times New Roman' }, + { value: 'Courier New', label: 'Courier New' }, + ] as const; + + // Returns the uniform font of the selection, or '' when fonts are mixed. + let currentFont = $derived.by(() => { + if (tick < 0 || !editor) return ''; + const { from, to, empty } = editor.state.selection; + if (empty) { + const marks = editor.state.storedMarks ?? editor.state.selection.$head.marks(); + return marks.find(m => m.type.name === 'textStyle')?.attrs.fontFamily ?? ''; + } + let font: string | undefined; + let mixed = false; + editor.state.doc.nodesBetween(from, to, (node) => { + if (mixed || !node.isText) return; + const f: string = node.marks.find(m => m.type.name === 'textStyle')?.attrs.fontFamily ?? ''; + if (font === undefined) font = f; + else if (font !== f) mixed = true; + }); + return mixed ? '' : (font ?? ''); + }); + + let fontOpen = $state(false); + let savedFrom: number | null = null; + let savedTo: number | null = null; + + function openFontPicker() { + if (!editor) return; + // Save selection before the picker button steals focus. + savedFrom = editor.state.selection.from; + savedTo = editor.state.selection.to; + fontOpen = !fontOpen; + } + + function pickFont(value: string) { + if (!editor) return; + fontOpen = false; + const from = savedFrom ?? editor.state.selection.from; + const to = savedTo ?? editor.state.selection.to; + savedFrom = null; + savedTo = null; + editor.chain().focus().setTextSelection({ from, to }).setFontFamily(value).run(); + } + + function fontPickerClickOutside(node: HTMLElement) { + function handler(e: MouseEvent) { + if (!node.contains(e.target as Node)) fontOpen = false; + } + window.addEventListener('mousedown', handler); + return { destroy() { window.removeEventListener('mousedown', handler); } }; + }