Skip to content

Commit 39e1477

Browse files
fix: show correct paragraph font in toolbar when selection is empty (SD-2145) (#2402)
* fix(toolbar): show correct paragraph font family for empty selection * fix(toolbar): preserve linked style font over empty paragraph fallback
1 parent d231640 commit 39e1477

2 files changed

Lines changed: 120 additions & 1 deletion

File tree

packages/super-editor/src/components/toolbar/super-toolbar.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { isList } from '@core/commands/list-helpers';
2020
import { calculateResolvedParagraphProperties } from '@extensions/paragraph/resolvedPropertiesCache.js';
2121
import { twipsToLines } from '@converter/helpers';
2222
import { parseSizeUnit } from '@core/utilities';
23+
import { encodeMarksFromRPr } from '@core/super-converter/styles.js';
2324
import { NodeSelection } from 'prosemirror-state';
2425

2526
/**
@@ -899,9 +900,16 @@ export class SuperToolbar extends EventEmitter {
899900
state.doc.resolve(paragraphParent.pos),
900901
)
901902
: null;
903+
const selectionIsCollapsed = selection.empty;
904+
const paragraphIsEmpty = paragraphParent?.node?.content?.size === 0;
905+
const paragraphFontFamily = getParagraphFontFamilyFromProperties(
906+
paragraphProps,
907+
this.activeEditor?.converter?.convertedXml ?? {},
908+
);
902909

903910
this.toolbarItems.forEach((item) => {
904911
item.resetDisabled();
912+
let activatedFromLinkedStyle = false;
905913

906914
if (item.name.value === 'undo') {
907915
item.setDisabled(this.undoDepth === 0);
@@ -966,12 +974,25 @@ export class SuperToolbar extends EventEmitter {
966974
[item.name.value]: linkedStylesItem,
967975
};
968976
item.activate(value);
977+
activatedFromLinkedStyle = true;
969978
}
970979
}
971980
if (item.name.value === 'textAlign' && paragraphProps?.justification) {
972981
item.activate({ textAlign: paragraphProps.justification });
973982
}
974983

984+
if (
985+
item.name.value === 'fontFamily' &&
986+
selectionIsCollapsed &&
987+
paragraphIsEmpty &&
988+
!activeMark &&
989+
!markNegated &&
990+
!activatedFromLinkedStyle &&
991+
paragraphFontFamily
992+
) {
993+
item.activate({ fontFamily: paragraphFontFamily });
994+
}
995+
975996
if (item.name.value === 'lineHeight') {
976997
if (paragraphProps?.spacing) {
977998
item.selectedValue.value = twipsToLines(paragraphProps.spacing.line);
@@ -1358,3 +1379,10 @@ export class SuperToolbar extends EventEmitter {
13581379
}
13591380
}
13601381
}
1382+
1383+
function getParagraphFontFamilyFromProperties(paragraphProps, convertedXml = {}) {
1384+
const fontFamilyProps = paragraphProps?.runProperties?.fontFamily;
1385+
if (!fontFamilyProps) return null;
1386+
const [markDef] = encodeMarksFromRPr({ fontFamily: fontFamilyProps }, convertedXml);
1387+
return markDef?.attrs?.fontFamily ?? null;
1388+
}

packages/super-editor/src/tests/toolbar/updateToolbarState.test.js

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,18 @@ describe('updateToolbarState', () => {
4040
let mockGetQuickFormatList;
4141
let mockCollectTrackedChanges;
4242
let mockIsTrackedChangeActionAllowed;
43+
let mockFindParentNode;
44+
let mockCalculateResolvedParagraphProperties;
4345

4446
beforeEach(async () => {
4547
vi.clearAllMocks();
4648

4749
mockEditor = {
4850
state: {
49-
selection: { from: 1, to: 1 },
51+
selection: { from: 1, to: 1, empty: true },
52+
doc: {
53+
resolve: vi.fn().mockReturnValue({}),
54+
},
5055
},
5156
commands: {
5257
setFieldAnnotationsFontSize: vi.fn(),
@@ -60,6 +65,7 @@ describe('updateToolbarState', () => {
6065
getDocumentDefaultStyles: vi.fn(() => ({ typeface: 'Arial', fontSizePt: 12 })),
6166
linkedStyles: [],
6267
docHiglightColors: new Set(['#ff0000', '#00ff00']),
68+
convertedXml: {},
6369
},
6470
options: {
6571
mode: 'docx',
@@ -79,6 +85,13 @@ describe('updateToolbarState', () => {
7985
const { collectTrackedChanges, isTrackedChangeActionAllowed } = await import(
8086
'@extensions/track-changes/permission-helpers.js'
8187
);
88+
const helpersModule = await import('@helpers/index.js');
89+
mockFindParentNode = helpersModule.findParentNode;
90+
mockFindParentNode.mockImplementation(() => vi.fn().mockReturnValue(null));
91+
const resolvedPropsModule = await import('@extensions/paragraph/resolvedPropertiesCache.js');
92+
mockCalculateResolvedParagraphProperties = vi
93+
.spyOn(resolvedPropsModule, 'calculateResolvedParagraphProperties')
94+
.mockReturnValue({});
8295

8396
getActiveFormatting.mockImplementation(mockGetActiveFormatting);
8497
isInTable.mockImplementation(mockIsInTable);
@@ -154,6 +167,7 @@ describe('updateToolbarState', () => {
154167
setDisabled: vi.fn(),
155168
defaultLabel: { value: '' },
156169
allowWithoutEditor: { value: false },
170+
active: { value: false },
157171
},
158172
{
159173
name: { value: 'lineHeight' },
@@ -195,6 +209,10 @@ describe('updateToolbarState', () => {
195209
toolbar.documentMode = 'editing';
196210
});
197211

212+
afterEach(() => {
213+
mockCalculateResolvedParagraphProperties?.mockRestore?.();
214+
});
215+
198216
describe('document mode dropdown sync', () => {
199217
let documentModeItem;
200218

@@ -508,6 +526,79 @@ describe('updateToolbarState', () => {
508526
expect(fontFamilyItem.activate).not.toHaveBeenCalledWith({ fontFamily: 'Arial' });
509527
});
510528

529+
it('falls back to paragraph runProperties font family for empty paragraph with collapsed selection', () => {
530+
const paragraphParent = {
531+
node: {
532+
content: { size: 0 },
533+
attrs: { paragraphProperties: {} },
534+
},
535+
pos: 5,
536+
};
537+
538+
mockFindParentNode.mockImplementation(() => () => paragraphParent);
539+
const paragraphFontFamily = 'Fancy Font, serif';
540+
mockCalculateResolvedParagraphProperties.mockReturnValue({
541+
runProperties: { fontFamily: { 'w:ascii': paragraphFontFamily } },
542+
});
543+
mockGetActiveFormatting.mockReturnValue([]);
544+
545+
toolbar.updateToolbarState();
546+
547+
const fontFamilyItem = toolbar.toolbarItems.find((item) => item.name.value === 'fontFamily');
548+
expect(mockCalculateResolvedParagraphProperties).toHaveBeenCalled();
549+
expect(fontFamilyItem.activate).toHaveBeenCalledWith({ fontFamily: paragraphFontFamily });
550+
});
551+
552+
it('does not fallback to paragraph font when paragraph already contains text', () => {
553+
const paragraphParent = {
554+
node: {
555+
content: { size: 1 },
556+
attrs: { paragraphProperties: {} },
557+
},
558+
pos: 5,
559+
};
560+
561+
mockFindParentNode.mockImplementation(() => () => paragraphParent);
562+
mockCalculateResolvedParagraphProperties.mockReturnValue({
563+
runProperties: { fontFamily: { 'w:ascii': 'Never Used' } },
564+
});
565+
mockGetActiveFormatting.mockReturnValue([]);
566+
567+
toolbar.updateToolbarState();
568+
569+
const fontFamilyItem = toolbar.toolbarItems.find((item) => item.name.value === 'fontFamily');
570+
expect(fontFamilyItem.activate).not.toHaveBeenCalled();
571+
});
572+
573+
it('keeps linked style font family over paragraph fallback in empty paragraphs', () => {
574+
const paragraphParent = {
575+
node: {
576+
content: { size: 0 },
577+
attrs: { paragraphProperties: {} },
578+
},
579+
pos: 5,
580+
};
581+
582+
mockFindParentNode.mockImplementation(() => () => paragraphParent);
583+
mockCalculateResolvedParagraphProperties.mockReturnValue({
584+
styleId: 'test-style',
585+
runProperties: { fontFamily: { 'w:ascii': 'Paragraph Font, serif' } },
586+
});
587+
mockEditor.converter.linkedStyles = [
588+
{
589+
id: 'test-style',
590+
definition: { styles: { 'font-family': 'Linked Style Font' } },
591+
},
592+
];
593+
mockGetActiveFormatting.mockReturnValue([]);
594+
595+
toolbar.updateToolbarState();
596+
597+
const fontFamilyItem = toolbar.toolbarItems.find((item) => item.name.value === 'fontFamily');
598+
expect(fontFamilyItem.activate).toHaveBeenCalledWith({ fontFamily: 'Linked Style Font' });
599+
expect(fontFamilyItem.activate).not.toHaveBeenCalledWith({ fontFamily: 'Paragraph Font, serif' });
600+
});
601+
511602
it('should prioritize active mark over linked styles (font size)', () => {
512603
mockGetActiveFormatting.mockReturnValue([
513604
{ name: 'fontSize', attrs: { fontSize: '20pt' } },

0 commit comments

Comments
 (0)