diff --git a/.playwright/screenshots/custom-style-fonts-visual.png b/.playwright/screenshots/custom-style-fonts-visual.png
new file mode 100644
index 000000000..60949a5db
Binary files /dev/null and b/.playwright/screenshots/custom-style-fonts-visual.png differ
diff --git a/.playwright/tests/customStyleFonts.spec.ts b/.playwright/tests/customStyleFonts.spec.ts
new file mode 100644
index 000000000..4b4afc701
--- /dev/null
+++ b/.playwright/tests/customStyleFonts.spec.ts
@@ -0,0 +1,330 @@
+import { test, expect, type Page } from '@playwright/test';
+
+import {
+ editorLocator,
+ focusEnrichedEditable,
+ getSerializedHtml,
+ gotoVisualRegression,
+ setEditorHtml,
+} from '../helpers/visual-regression';
+import { toolbarButton } from '../helpers/toolbar';
+
+function fontSizeButton(page: Page) {
+ return page.locator('[data-testid="toolbar-font-size"]');
+}
+
+function fontFamilyButton(page: Page) {
+ return page.locator('[data-testid="toolbar-font-family"]');
+}
+
+function fontSizeOption(page: Page, size: number) {
+ return page.locator(`[data-testid="font-size-${size}"]`);
+}
+
+function fontFamilyOption(page: Page, value: string) {
+ return page.locator(`[data-testid="font-family-${value}"]`);
+}
+
+function fontSizeClear(page: Page) {
+ return page.locator('[data-testid="font-size-clear"]');
+}
+
+function fontFamilyClear(page: Page) {
+ return page.locator('[data-testid="font-family-clear"]');
+}
+
+async function applyFontSize(page: Page, size: number) {
+ await fontSizeButton(page).click();
+ await fontSizeOption(page, size).click();
+}
+
+async function clearFontSize(page: Page) {
+ await fontSizeButton(page).click();
+ await fontSizeClear(page).click();
+}
+
+async function applyFontFamily(page: Page, value: string) {
+ await fontFamilyButton(page).click();
+ await fontFamilyOption(page, value).click();
+}
+
+async function clearFontFamily(page: Page) {
+ await fontFamilyButton(page).click();
+ await fontFamilyClear(page).click();
+}
+
+const ROUND_TRIP_CASES: { name: string; input: string; expected: string }[] = [
+ {
+ name: 'font size only',
+ input:
+ '
Sized text
',
+ expected:
+ 'Sized text
',
+ },
+ {
+ name: 'font family only',
+ input:
+ 'Courier text
',
+ expected:
+ 'Courier text
',
+ },
+ {
+ name: 'font family with a comma-separated fallback is reduced to first family',
+ input:
+ 'Courier text
',
+ expected:
+ 'Courier text
',
+ },
+ {
+ name: 'font size and font family together',
+ input:
+ 'Both
',
+ expected:
+ 'Both
',
+ },
+ {
+ name: 'font size and color together',
+ input:
+ 'Red big
',
+ expected:
+ 'Red big
',
+ },
+ {
+ name: 'font size inside heading',
+ input:
+ 'Big heading ',
+ expected:
+ 'Big heading ',
+ },
+ {
+ name: 'font size wraps bold mark',
+ input:
+ 'Bold big
',
+ expected:
+ 'Bold big
',
+ },
+ {
+ name: 'multiple sized spans in one paragraph',
+ input:
+ 'Small plain Big
',
+ expected:
+ 'Small plain Big
',
+ },
+];
+
+test.describe('custom style fonts - HTML serialization', () => {
+ test.beforeEach(async ({ page }) => {
+ await gotoVisualRegression(page);
+ });
+
+ for (const { name, input, expected } of ROUND_TRIP_CASES) {
+ test(name, async ({ page }) => {
+ await setEditorHtml(page, input);
+ await expect.poll(async () => getSerializedHtml(page)).toBe(expected);
+ });
+ }
+});
+
+test('custom style fonts visual regression', async ({ page }) => {
+ await gotoVisualRegression(page);
+
+ const html = [
+ '',
+ 'Regular family
',
+ '24px plain
',
+ 'Sans family
',
+ '32 Courier
',
+ 'Bold 20px ',
+ 'Italic family ',
+ 'Code mono
',
+ 'H5 40px ',
+ 'Quote mono family
',
+ '',
+ ].join('');
+
+ await setEditorHtml(page, html);
+
+ const editor = editorLocator(page);
+ await expect(editor).toHaveScreenshot('custom-style-fonts-visual.png');
+});
+
+test.describe('custom style fonts - toolbar interaction', () => {
+ test.beforeEach(async ({ page }) => {
+ await gotoVisualRegression(page);
+ });
+
+ test('apply font size then type text', async ({ page }) => {
+ const editor = editorLocator(page);
+ await editor.click();
+
+ await applyFontSize(page, 20);
+ await editor.pressSequentially('Sized text', { delay: 80 });
+
+ await expect
+ .poll(async () => getSerializedHtml(page))
+ .toBe(
+ 'Sized text
'
+ );
+ });
+
+ test('clear font size stops sizing new text', async ({ page }) => {
+ const editor = editorLocator(page);
+ await editor.click();
+
+ await applyFontSize(page, 20);
+ await editor.pressSequentially('Sized', { delay: 80 });
+ await clearFontSize(page);
+ await editor.pressSequentially(' plain', { delay: 80 });
+
+ await expect
+ .poll(async () => getSerializedHtml(page))
+ .toBe(
+ 'Sized plain
'
+ );
+ });
+
+ test('apply font family then type text', async ({ page }) => {
+ const editor = editorLocator(page);
+ await editor.click();
+
+ await applyFontFamily(page, 'Georgia');
+ await editor.pressSequentially('Serif text', { delay: 80 });
+
+ await expect
+ .poll(async () => getSerializedHtml(page))
+ .toBe(
+ 'Serif text
'
+ );
+ });
+
+ test('clear font family stops styling new text', async ({ page }) => {
+ const editor = editorLocator(page);
+ await editor.click();
+
+ await applyFontFamily(page, 'Georgia');
+ await editor.pressSequentially('Serif', { delay: 80 });
+ await clearFontFamily(page);
+ await editor.pressSequentially(' plain', { delay: 80 });
+
+ await expect
+ .poll(async () => getSerializedHtml(page))
+ .toBe(
+ 'Serif plain
'
+ );
+ });
+
+ test('apply font size and font family together', async ({ page }) => {
+ const editor = editorLocator(page);
+ await editor.click();
+
+ await applyFontSize(page, 24);
+ await applyFontFamily(page, 'Georgia');
+ await editor.pressSequentially('Big serif', { delay: 80 });
+
+ await expect
+ .poll(async () => getSerializedHtml(page))
+ .toBe(
+ 'Big serif
'
+ );
+ });
+
+ test('font size with bold', async ({ page }) => {
+ const editor = editorLocator(page);
+ const boldBtn = toolbarButton(page, 'bold');
+ await editor.click();
+
+ await boldBtn.click();
+ await applyFontSize(page, 32);
+ await editor.pressSequentially('Bold big', { delay: 80 });
+
+ await expect
+ .poll(async () => getSerializedHtml(page))
+ .toBe(
+ 'Bold big
'
+ );
+ });
+
+ test('font family with italic', async ({ page }) => {
+ const editor = editorLocator(page);
+ const italicBtn = toolbarButton(page, 'italic');
+ await editor.click();
+
+ await italicBtn.click();
+ await applyFontFamily(page, 'Georgia');
+ await editor.pressSequentially('Italic serif', { delay: 80 });
+
+ await expect
+ .poll(async () => getSerializedHtml(page))
+ .toBe(
+ 'Italic serif
'
+ );
+ });
+
+ test('changing font size on a selection preserves per-run font family', async ({
+ page,
+ }) => {
+ // Two runs with different families, then re-size the whole line and ensure
+ // each run keeps its own family (per-range merge, not a flattened span).
+ await setEditorHtml(
+ page,
+ [
+ '',
+ 'One ',
+ 'Two ',
+ '
',
+ ].join('')
+ );
+
+ await focusEnrichedEditable(page);
+ await page.keyboard.press('ControlOrMeta+a');
+ await applyFontSize(page, 20);
+
+ await expect
+ .poll(async () => getSerializedHtml(page))
+ .toBe(
+ [
+ '',
+ 'One ',
+ 'Two ',
+ '
',
+ ].join('')
+ );
+ });
+
+ test('toolbar font-size button shows active size when set', async ({
+ page,
+ }) => {
+ const editor = editorLocator(page);
+ await editor.click();
+
+ await applyFontSize(page, 20);
+
+ await expect(fontSizeButton(page)).toContainText('20');
+ await expect(fontSizeButton(page)).toHaveClass(/toolbar-btn--active/);
+
+ // Re-open the picker – the chosen size should be marked as active
+ await fontSizeButton(page).click();
+ await expect(fontSizeOption(page, 20)).toHaveClass(
+ /toolbar-font-option--active/
+ );
+ });
+
+ test('toolbar font-family button shows active family when set', async ({
+ page,
+ }) => {
+ const editor = editorLocator(page);
+ await editor.click();
+
+ await applyFontFamily(page, 'Georgia');
+
+ // The button shows the family's label ("Serif" maps to "Georgia").
+ await expect(fontFamilyButton(page)).toContainText('Serif');
+ await expect(fontFamilyButton(page)).toHaveClass(/toolbar-btn--active/);
+
+ // Re-open the picker – the chosen family should be marked as active
+ await fontFamilyButton(page).click();
+ await expect(fontFamilyOption(page, 'Georgia')).toHaveClass(
+ /toolbar-font-option--active/
+ );
+ });
+});
diff --git a/apps/example-web/src/components/Toolbar.css b/apps/example-web/src/components/Toolbar.css
index 2186d24d8..77bb06a03 100644
--- a/apps/example-web/src/components/Toolbar.css
+++ b/apps/example-web/src/components/Toolbar.css
@@ -139,3 +139,50 @@
.toolbar-color-swatch:focus-visible {
outline: none;
}
+
+.toolbar-font-picker {
+ display: flex;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ padding: 8px;
+ gap: 8px;
+ background: rgba(0, 26, 114, 0.95);
+ scrollbar-width: thin;
+ -webkit-overflow-scrolling: touch;
+}
+
+.toolbar-font-option {
+ min-width: 36px;
+ height: 28px;
+ padding: 0 8px;
+ border: 2px solid transparent;
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.15);
+ color: #fff;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 1;
+ cursor: pointer;
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.toolbar-font-option--clear {
+ width: 28px;
+ min-width: 28px;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-color: rgba(255, 255, 255, 0.4);
+ background: transparent;
+}
+
+.toolbar-font-option--active {
+ background: rgb(0, 26, 114);
+ border-color: #fff;
+}
+
+.toolbar-font-option:focus-visible {
+ outline: none;
+}
diff --git a/apps/example-web/src/components/Toolbar.tsx b/apps/example-web/src/components/Toolbar.tsx
index 26c979aa1..7fc8dbef5 100644
--- a/apps/example-web/src/components/Toolbar.tsx
+++ b/apps/example-web/src/components/Toolbar.tsx
@@ -23,7 +23,22 @@ const COLORS = [
'#ADD8E6',
];
-type OpenPicker = 'text-color' | 'bg-color' | null;
+const FONT_SIZES = [12, 16, 20, 24, 28, 32, 36, 40];
+
+const FONT_FAMILIES = [
+ { label: 'Sans', value: 'Arial' },
+ { label: 'Serif', value: 'Georgia' },
+ { label: 'Mono', value: 'Courier New' },
+ { label: 'System', value: 'system-ui' },
+ { label: 'Cursive', value: 'cursive' },
+] as const;
+
+type OpenPicker =
+ | 'text-color'
+ | 'bg-color'
+ | 'font-size'
+ | 'font-family'
+ | null;
interface ToolbarProps {
editorRef: RefObject;
@@ -84,6 +99,11 @@ export function Toolbar({
const activeFgColor = s?.customStyle.foregroundColor ?? '';
const activeBgColor = s?.customStyle.backgroundColor ?? '';
+ const activeFontSize = s?.customStyle.fontSize ?? 0;
+ const activeFontFamily = s?.customStyle.fontFamily ?? '';
+ const activeFontFamilyLabel =
+ FONT_FAMILIES.find((family) => family.value === activeFontFamily)?.label ??
+ '';
const handleSelectFgColor = (color: string) => {
editorRef.current?.setStyle({ foregroundColor: color });
@@ -101,6 +121,22 @@ export function Toolbar({
editorRef.current?.setStyle({ backgroundColor: null });
setOpenPicker(null);
};
+ const handleSelectFontSize = (size: number) => {
+ editorRef.current?.setStyle({ fontSize: size });
+ setOpenPicker(null);
+ };
+ const handleClearFontSize = () => {
+ editorRef.current?.setStyle({ fontSize: null });
+ setOpenPicker(null);
+ };
+ const handleSelectFontFamily = (family: string) => {
+ editorRef.current?.setStyle({ fontFamily: family });
+ setOpenPicker(null);
+ };
+ const handleClearFontFamily = () => {
+ editorRef.current?.setStyle({ fontFamily: null });
+ setOpenPicker(null);
+ };
const toolbarItems = [
{
@@ -332,10 +368,52 @@ export function Toolbar({
}}
/>
+ 0
+ ? ' toolbar-btn--active'
+ : ''
+ }`}
+ onPointerDown={(e) => {
+ if (e.pointerType === 'mouse') e.preventDefault();
+ }}
+ onClick={() => {
+ setOpenPicker((prev) =>
+ prev === 'font-size' ? null : 'font-size'
+ );
+ }}
+ >
+
+ {activeFontSize > 0 ? String(activeFontSize) : 'Aa'}
+
+
+ 0
+ ? ' toolbar-btn--active'
+ : ''
+ }`}
+ onPointerDown={(e) => {
+ if (e.pointerType === 'mouse') e.preventDefault();
+ }}
+ onClick={() => {
+ setOpenPicker((prev) =>
+ prev === 'font-family' ? null : 'font-family'
+ );
+ }}
+ >
+
+ {activeFontFamilyLabel || 'Ff'}
+
+
- {openPicker !== null && (
+ {(openPicker === 'text-color' || openPicker === 'bg-color') && (
)}
+ {openPicker === 'font-size' && (
+
+ {
+ if (e.pointerType === 'mouse') e.preventDefault();
+ }}
+ onClick={handleClearFontSize}
+ >
+ ✕
+
+ {FONT_SIZES.map((size) => (
+ {
+ if (e.pointerType === 'mouse') e.preventDefault();
+ }}
+ onClick={() => {
+ handleSelectFontSize(size);
+ }}
+ >
+ {size}
+
+ ))}
+
+ )}
+ {openPicker === 'font-family' && (
+
+ {
+ if (e.pointerType === 'mouse') e.preventDefault();
+ }}
+ onClick={handleClearFontFamily}
+ >
+ ✕
+
+ {FONT_FAMILIES.map(({ label, value }) => (
+ {
+ if (e.pointerType === 'mouse') e.preventDefault();
+ }}
+ onClick={() => {
+ handleSelectFontFamily(value);
+ }}
+ >
+ {label}
+
+ ))}
+
+ )}
);
}
diff --git a/src/web/EnrichedTextInput.tsx b/src/web/EnrichedTextInput.tsx
index 0fea130e2..6ac51e9ee 100644
--- a/src/web/EnrichedTextInput.tsx
+++ b/src/web/EnrichedTextInput.tsx
@@ -425,6 +425,12 @@ export const EnrichedTextInput = ({
...('backgroundColor' in customStyle && {
backgroundColor: normalizeColorValue(customStyle.backgroundColor),
}),
+ ...('fontSize' in customStyle && {
+ fontSize: customStyle.fontSize,
+ }),
+ ...('fontFamily' in customStyle && {
+ fontFamily: customStyle.fontFamily,
+ }),
})
);
},
diff --git a/src/web/formats/EnrichedCustomStyle.ts b/src/web/formats/EnrichedCustomStyle.ts
index 98a71c1fe..c4380ca41 100644
--- a/src/web/formats/EnrichedCustomStyle.ts
+++ b/src/web/formats/EnrichedCustomStyle.ts
@@ -1,12 +1,50 @@
import { Mark } from '@tiptap/core';
+import type { Attrs } from '@tiptap/pm/model';
+
+type CustomStyleAttrs = {
+ foregroundColor?: string | null;
+ backgroundColor?: string | null;
+ fontSize?: number | null;
+ fontFamily?: string | null;
+};
+
+function normalizeFontFamily(value: string | null | undefined): string | null {
+ if (!value) return null;
+
+ let fontFamily = value.trim();
+ const commaIndex = fontFamily.indexOf(',');
+ if (commaIndex !== -1) {
+ fontFamily = fontFamily.slice(0, commaIndex).trim();
+ }
+
+ if (
+ (fontFamily.startsWith("'") && fontFamily.endsWith("'")) ||
+ (fontFamily.startsWith('"') && fontFamily.endsWith('"'))
+ ) {
+ fontFamily = fontFamily.slice(1, -1);
+ }
+
+ return fontFamily.length > 0 ? fontFamily : null;
+}
+
+function resolveFontSize(value: number | null | undefined): number | null {
+ if (value == null || value <= 0) return null;
+ return value;
+}
+
+function parseFontSize(value: string | null | undefined): number | null {
+ if (!value) return null;
+ const trimmed = value.trim();
+ const match = /^([0-9.]+)\s*(?:px)?$/i.exec(trimmed);
+ if (!match) return null;
+ const n = parseFloat(match[1]!);
+ return !Number.isNaN(n) && n > 0 ? n : null;
+}
declare module '@tiptap/core' {
interface Commands {
customStyle: {
- setCustomStyle: (attrs: {
- foregroundColor?: string | null;
- backgroundColor?: string | null;
- }) => ReturnType;
+ setCustomStyle: (attrs: CustomStyleAttrs) => ReturnType;
};
}
}
@@ -28,6 +66,15 @@ export const EnrichedCustomStyle = Mark.create({
default: null,
parseHTML: (el: HTMLElement) => el.style.backgroundColor || null,
},
+ fontSize: {
+ default: null,
+ parseHTML: (el: HTMLElement) => parseFontSize(el.style.fontSize),
+ },
+ fontFamily: {
+ default: null,
+ parseHTML: (el: HTMLElement) =>
+ normalizeFontFamily(el.style.fontFamily),
+ },
};
},
@@ -36,7 +83,12 @@ export const EnrichedCustomStyle = Mark.create({
{
tag: 'span',
getAttrs: (el: HTMLElement) => {
- if (!el.style.color && !el.style.backgroundColor) {
+ if (
+ !el.style.color &&
+ !el.style.backgroundColor &&
+ !el.style.fontSize &&
+ !el.style.fontFamily
+ ) {
return false;
}
// let addAttributes handle the actual parsing
@@ -54,6 +106,18 @@ export const EnrichedCustomStyle = Mark.create({
if (mark.attrs.backgroundColor) {
parts.push(`background-color: ${mark.attrs.backgroundColor}`);
}
+ if (mark.attrs.fontSize) {
+ parts.push(`font-size: ${mark.attrs.fontSize}px`);
+ }
+ if (mark.attrs.fontFamily) {
+ const fontFamily = mark.attrs.fontFamily as string;
+ // if the font family contains a space, wrap it in quotes
+ parts.push(
+ /\s/.test(fontFamily)
+ ? `font-family: '${fontFamily}'`
+ : `font-family: ${fontFamily}`
+ );
+ }
return ['span', { style: parts.join('; ') }, 0];
},
@@ -88,27 +152,89 @@ export const EnrichedCustomStyle = Mark.create({
return {
setCustomStyle:
(attrs) =>
- ({ chain, editor }) => {
- const current = editor.getAttributes('customStyle');
- const resolvedColor =
- 'foregroundColor' in attrs
- ? attrs.foregroundColor
- : current.foregroundColor;
- const resolvedBg =
- 'backgroundColor' in attrs
- ? attrs.backgroundColor
- : current.backgroundColor;
-
- if (!resolvedColor && !resolvedBg) {
- return chain().unsetMark('customStyle').run();
+ ({ state, tr, dispatch }) => {
+ const markType = state.schema.marks.customStyle;
+ if (!markType) return false;
+
+ // Only the fields explicitly present in `attrs` should override.
+ // Everything else must be preserved per existing inline run, so a
+ // selection spanning multiple fonts/colors keeps its differences.
+ const patch: CustomStyleAttrs = {};
+ if ('foregroundColor' in attrs) {
+ patch.foregroundColor = attrs.foregroundColor ?? null;
}
+ if ('backgroundColor' in attrs) {
+ patch.backgroundColor = attrs.backgroundColor ?? null;
+ }
+ if ('fontSize' in attrs) {
+ patch.fontSize = resolveFontSize(attrs.fontSize);
+ }
+ if ('fontFamily' in attrs) {
+ patch.fontFamily = normalizeFontFamily(attrs.fontFamily);
+ }
+
+ const mergeAttrs = (
+ existing: Attrs | undefined
+ ): Required => ({
+ foregroundColor: existing?.foregroundColor || null,
+ backgroundColor: existing?.backgroundColor || null,
+ fontSize: resolveFontSize(existing?.fontSize),
+ fontFamily: normalizeFontFamily(existing?.fontFamily),
+ ...patch,
+ });
- return chain()
- .setMark('customStyle', {
- foregroundColor: resolvedColor ?? null,
- backgroundColor: resolvedBg ?? null,
- })
- .run();
+ const isEmpty = (a: Required) =>
+ !a.foregroundColor &&
+ !a.backgroundColor &&
+ !a.fontSize &&
+ !a.fontFamily;
+
+ const { selection } = state;
+
+ if (selection.empty) {
+ // Cursor only: merge into the stored (typing) mark.
+ const existing = markType.isInSet(
+ state.storedMarks ?? selection.$from.marks()
+ );
+ const merged = mergeAttrs(existing?.attrs);
+ if (dispatch) {
+ if (isEmpty(merged)) {
+ tr.removeStoredMark(markType);
+ } else {
+ tr.addStoredMark(markType.create(merged));
+ }
+ dispatch(tr);
+ }
+ return true;
+ }
+
+ if (dispatch) {
+ selection.ranges.forEach((range) => {
+ const rFrom = range.$from.pos;
+ const rTo = range.$to.pos;
+ state.doc.nodesBetween(rFrom, rTo, (node, pos) => {
+ // Only inline runs carry the mark; block nodes are skipped so
+ // per-run attributes are preserved. ProseMirror's addMark step
+ // itself skips any inline node that disallows the mark type.
+ if (!node.isInline) {
+ return;
+ }
+ const start = Math.max(pos, rFrom);
+ const end = Math.min(pos + node.nodeSize, rTo);
+ if (start >= end) return;
+
+ const existing = markType.isInSet(node.marks);
+ const merged = mergeAttrs(existing?.attrs);
+
+ tr.removeMark(start, end, markType);
+ if (!isEmpty(merged)) {
+ tr.addMark(start, end, markType.create(merged));
+ }
+ });
+ });
+ dispatch(tr);
+ }
+ return true;
},
};
},
diff --git a/src/web/normalization/htmlNormalizer.ts b/src/web/normalization/htmlNormalizer.ts
index cdef19dc7..257463a07 100644
--- a/src/web/normalization/htmlNormalizer.ts
+++ b/src/web/normalization/htmlNormalizer.ts
@@ -599,6 +599,8 @@ function walkNode(node: Node, out: { buf: string }): void {
const fg = htmlNode.style.color;
const bg = htmlNode.style.backgroundColor;
+ const fontSize = htmlNode.style.fontSize;
+ const fontFamily = escapeText(htmlNode.style.fontFamily);
// build the preserved span if colors exist
let spanOpen = '';
@@ -607,6 +609,8 @@ function walkNode(node: Node, out: { buf: string }): void {
if (fg) styleParts.push(`color: ${fg}`);
if (bg) styleParts.push(`background-color: ${bg}`);
+ if (fontSize) styleParts.push(`font-size: ${fontSize}`);
+ if (fontFamily) styleParts.push(`font-family: ${fontFamily}`);
if (styleParts.length > 0) {
spanOpen = ``;
diff --git a/src/web/useOnChangeState.ts b/src/web/useOnChangeState.ts
index 2015f86e2..c5f315803 100644
--- a/src/web/useOnChangeState.ts
+++ b/src/web/useOnChangeState.ts
@@ -104,8 +104,8 @@ function buildState(
editor.getAttributes('customStyle').foregroundColor ?? '',
backgroundColor:
editor.getAttributes('customStyle').backgroundColor ?? '',
- fontSize: 0,
- fontFamily: '',
+ fontSize: editor.getAttributes('customStyle').fontSize ?? 0,
+ fontFamily: editor.getAttributes('customStyle').fontFamily ?? '',
},
};
}
@@ -125,7 +125,7 @@ function hashState(state: OnChangeStateEvent): string {
})
.join('');
- return `${formatHash}|${state.alignment}|${state.customStyle.foregroundColor}|${state.customStyle.backgroundColor}`;
+ return `${formatHash}|${state.alignment}|${state.customStyle.foregroundColor}|${state.customStyle.backgroundColor}|${state.customStyle.fontSize}|${state.customStyle.fontFamily}`;
}
function getFormatHash(