feat: add font selection to extended toolbar

This commit is contained in:
Steffen Becker committed 2026-05-25 22:40:20 +02:00
1 parent b7ab64948d
commit 747ac44a34
5 files changed
+180 -15

No files matched your search

+3 -2
View File
@@ -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
+28
View File
@@ -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",
+2
View File
@@ -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"
+144 -13
View File
@@ -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); } };
}
</script>
<div class="toolbar-expanded">
{#if editor}
<div class="toolbar-group">
<select
class="font-select"
disabled
title="Font family (coming soon)"
>
<option>Sans-serif</option>
</select>
<div class="font-picker" use:fontPickerClickOutside>
<button class="font-trigger" onclick={openFontPicker} title="Font Name">
<span class="font-trigger-label" style={currentFont ? `font-family: ${currentFont}` : ''}>{currentFont}</span>
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
<path d="M1 2.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
{#if fontOpen}
<div class="font-dropdown">
{#each FONTS as font}
<button
class="font-option"
class:active={currentFont === font.value}
style="font-family: {font.value}"
onclick={() => pickFont(font.value)}
>{font.label}</button>
{/each}
</div>
{/if}
</div>
<div class="toolbar-separator"></div>
@@ -178,18 +247,80 @@
cursor: not-allowed;
}
.font-select {
.font-picker {
position: relative;
}
.font-trigger {
display: inline-flex;
align-items: center;
gap: 4px;
height: 2rem;
padding: 0 0.5rem;
padding: 0 0.4rem 0 0.6rem;
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);
cursor: not-allowed;
opacity: 0.35;
max-width: 110px;
cursor: pointer;
width: 145px;
transition: border-color 0.15s;
}
.font-trigger:hover {
border-color: var(--color-primary);
}
.font-trigger-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
font-size: 0.8rem;
}
.font-dropdown {
position: absolute;
top: calc(100% + 3px);
left: 0;
min-width: 100%;
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: 2px;
display: flex;
flex-direction: column;
}
.font-option {
display: block;
width: 100%;
padding: 0.35rem 0.6rem;
border: none;
border-radius: calc(var(--radius) - 2px);
background: transparent;
color: var(--color-text);
font-size: 0.85rem;
text-align: left;
cursor: pointer;
white-space: nowrap;
transition: background 0.1s;
min-width: unset;
height: auto;
justify-content: flex-start;
}
.font-option:hover {
background: var(--color-btn-hover);
}
.font-option.active {
background: var(--color-primary);
color: white;
}
.font-size-select {
+3
View File
@@ -4,6 +4,7 @@ import Text from '@tiptap/extension-text';
import Bold from '@tiptap/extension-bold';
import Italic from '@tiptap/extension-italic';
import Underline from '@tiptap/extension-underline';
import { TextStyle, FontFamily } from '@tiptap/extension-text-style';
import Heading from '@tiptap/extension-heading';
import BulletList from '@tiptap/extension-bullet-list';
import OrderedList from '@tiptap/extension-ordered-list';
@@ -19,6 +20,8 @@ export const extensions = [
Bold,
Italic,
Underline,
TextStyle,
FontFamily,
Heading.configure({ levels: [1, 2, 3] }),
BulletList,
OrderedList,