Skip to content

Commit 75c8f51

Browse files
artem-harbourArtem Nistuley
andauthored
fix: applying formatting to header/footer (#2904)
Co-authored-by: Artem Nistuley <artem@superdoc.dev>
1 parent 3a53eb3 commit 75c8f51

8 files changed

Lines changed: 73 additions & 25 deletions

File tree

packages/super-editor/src/editors/v1/components/toolbar/Toolbar.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,34 @@ describe('Toolbar', () => {
115115
removeSpy.mockRestore();
116116
});
117117

118+
it('does not restore selection when active editor is header/footer', async () => {
119+
const restoreSelection = vi.fn();
120+
const mockToolbar = createMockToolbar();
121+
mockToolbar.activeEditor = {
122+
options: { isHeaderOrFooter: true },
123+
commands: { restoreSelection },
124+
};
125+
126+
const ButtonGroupStub = defineComponent({
127+
emits: ['item-clicked'],
128+
template: '<button data-test="emit-item-clicked" @click="$emit(\'item-clicked\')">emit</button>',
129+
});
130+
131+
const wrapper = mount(Toolbar, {
132+
global: {
133+
stubs: { ButtonGroup: ButtonGroupStub },
134+
plugins: [
135+
(app) => {
136+
app.config.globalProperties.$toolbar = mockToolbar;
137+
},
138+
],
139+
},
140+
});
141+
142+
await wrapper.find('[data-test="emit-item-clicked"]').trigger('click');
143+
expect(restoreSelection).not.toHaveBeenCalled();
144+
});
145+
118146
it('does not attach ResizeObserver when responsiveToContainer is disabled', () => {
119147
const observe = vi.fn();
120148
const disconnect = vi.fn();

packages/super-editor/src/editors/v1/components/toolbar/Toolbar.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ const handleCommand = ({ item, argument, option }) => {
110110
};
111111
112112
const restoreSelection = () => {
113-
proxy.$toolbar.activeEditor?.commands?.restoreSelection();
113+
const editor = proxy.$toolbar.activeEditor;
114+
if (!editor) return;
115+
if (editor.options?.isHeaderOrFooter) return;
116+
editor.commands?.restoreSelection();
114117
};
115118
116119
/**

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -775,9 +775,10 @@ export class SuperToolbar extends EventEmitter {
775775
return;
776776
}
777777

778-
// If the editor wasn't focused and this is a mark toggle, queue it and keep the button active
779-
// until the next selection update (after the user clicks into the editor).
780-
if (!wasFocused && isMarkToggle) {
778+
// Queue unfocused mark toggles only for body editors.
779+
// Header/footer mark toggles execute immediately to avoid waiting for
780+
// selectionUpdate and requiring an extra selection change.
781+
if (!wasFocused && isMarkToggle && !this.activeEditor?.options?.isHeaderOrFooter) {
781782
this.pendingMarkCommands.push({ command, argument, item });
782783
const labelAttr = item?.labelAttr?.value;
783784
if (labelAttr && argument) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function resolveHeaderFooterSelection({ tr }) {
2+
// Keep selection resolution centralized here so header/footer-specific fallback
3+
// logic can be reintroduced in one place if we need it again.
4+
return tr?.selection;
5+
}

packages/super-editor/src/editors/v1/core/commands/setMark.js

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { Attribute } from '../Attribute.js';
22
import { getMarkType } from '../helpers/getMarkType.js';
33
import { isTextSelection } from '../helpers/isTextSelection.js';
4+
import { resolveHeaderFooterSelection } from './helpers/resolveHeaderFooterSelection.js';
45
import { addParagraphRunProperty } from '../helpers/syncParagraphRunProperties.js';
56

6-
function canSetMark(editor, state, tr, newMarkType) {
7-
let { selection } = tr;
8-
if (editor.options.isHeaderOrFooter) {
9-
selection = editor.options.lastSelection;
10-
}
7+
function canSetMark(state, tr, newMarkType) {
8+
const selection = resolveHeaderFooterSelection({ tr });
119
let cursor = null;
1210

1311
if (isTextSelection(selection)) {
@@ -53,11 +51,8 @@ function canSetMark(editor, state, tr, newMarkType) {
5351
* @param attributes Attributes to add.
5452
*/
5553
//prettier-ignore
56-
export const setMark = (typeOrName, attributes = {}) => ({ tr, state, dispatch, editor }) => {
57-
let { selection } = tr;
58-
if (editor.options.isHeaderOrFooter) {
59-
selection = editor.options.lastSelection;
60-
}
54+
export const setMark = (typeOrName, attributes = {}) => ({ tr, state, dispatch }) => {
55+
const selection = resolveHeaderFooterSelection({ tr });
6156
const { empty, ranges } = selection;
6257
const type = getMarkType(typeOrName, state.schema);
6358

@@ -107,5 +102,5 @@ export const setMark = (typeOrName, attributes = {}) => ({ tr, state, dispatch,
107102
}
108103
}
109104

110-
return canSetMark(editor, state, tr, type);
105+
return canSetMark(state, tr, type);
111106
};

packages/super-editor/src/editors/v1/core/commands/unsetAllMarks.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { resolveHeaderFooterSelection } from './helpers/resolveHeaderFooterSelection.js';
2+
13
/**
24
* Remove all marks in the current selection.
35
*
@@ -11,11 +13,8 @@
1113
* only, the undo path restores the exact marks that were visible to the user.
1214
*/
1315
//prettier-ignore
14-
export const unsetAllMarks = () => ({ tr, dispatch, editor }) => {
15-
let { selection } = tr;
16-
if (editor.options.isHeaderOrFooter) {
17-
selection = editor.options.lastSelection;
18-
}
16+
export const unsetAllMarks = () => ({ tr, dispatch }) => {
17+
const selection = resolveHeaderFooterSelection({ tr });
1918
const { empty, ranges } = selection;
2019

2120
if (dispatch) {

packages/super-editor/src/editors/v1/core/commands/unsetMark.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getMarkRange } from '../helpers/getMarkRange.js';
22
import { getMarkType } from '../helpers/getMarkType.js';
3+
import { resolveHeaderFooterSelection } from './helpers/resolveHeaderFooterSelection.js';
34
import { removeParagraphRunProperty } from '../helpers/syncParagraphRunProperties.js';
45

56
/**
@@ -8,12 +9,9 @@ import { removeParagraphRunProperty } from '../helpers/syncParagraphRunPropertie
89
* @param options.extendEmptyMarkRange Removes the mark even across the current selection.
910
*/
1011
//prettier-ignore
11-
export const unsetMark = (typeOrName, options = {}) => ({ tr, state, dispatch, editor }) => {
12+
export const unsetMark = (typeOrName, options = {}) => ({ tr, state, dispatch }) => {
1213
const { extendEmptyMarkRange = false } = options;
13-
let { selection } = tr;
14-
if (editor.options.isHeaderOrFooter) {
15-
selection = editor.options.lastSelection;
16-
}
14+
const selection = resolveHeaderFooterSelection({ tr });
1715
const type = getMarkType(typeOrName, state.schema);
1816
const { $from, empty, ranges } = selection;
1917

packages/super-editor/src/editors/v1/tests/toolbar/super-toolbar-commands.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,25 @@ describe('SuperToolbar sticky mark persistence', () => {
267267

268268
expect(item.activate).toHaveBeenCalledWith();
269269
});
270+
271+
it('executes mark toggles immediately for header/footer editors instead of queueing', () => {
272+
mockEditor.options.isHeaderOrFooter = true;
273+
mockEditor.view.hasFocus = vi.fn(() => false);
274+
const setFontSize = vi.fn();
275+
mockEditor.commands.setFontSize = setFontSize;
276+
277+
const item = {
278+
command: 'setFontSize',
279+
name: { value: 'fontSize' },
280+
labelAttr: { value: 'fontSize' },
281+
activate: vi.fn(),
282+
};
283+
284+
toolbar.emitCommand({ item, argument: '24pt' });
285+
286+
expect(toolbar.pendingMarkCommands).toHaveLength(0);
287+
expect(setFontSize).toHaveBeenCalledWith('24pt');
288+
});
270289
});
271290

272291
describe('SuperToolbar error handling for command failures', () => {

0 commit comments

Comments
 (0)