Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,23 @@ const { mockCreateHeaderFooterEditor, mockOnHeaderFooterDataUpdate, mockToFlowBl
return editorStub;
};

const mockCreateHeaderFooterEditor = vi.fn(() => {
const editor = createSectionEditor();
editors.push({ editor, emit: editor.emit });
queueMicrotask(() => {
editor.emit('create');
});
return editor;
});
const mockCreateHeaderFooterEditor = vi.fn(
(input?: { editorContainer?: HTMLElement; editorHost?: HTMLElement }) => {
const editor = createSectionEditor();
if (input?.editorContainer instanceof HTMLElement) {
if (input.editorHost instanceof HTMLElement) {
input.editorHost.appendChild(input.editorContainer);
} else {
document.body.appendChild(input.editorContainer);
}
}
editors.push({ editor, emit: editor.emit });
queueMicrotask(() => {
editor.emit('create');
});
return editor;
},
);

return {
mockCreateHeaderFooterEditor,
Expand Down Expand Up @@ -192,6 +201,39 @@ describe('HeaderFooterEditorManager', () => {
);
});

it('ensureEditorSync creates a reusable editor instance immediately for presentation activation', () => {
const editor = createMockEditor();
const manager = new HeaderFooterEditorManager(editor);
const descriptor = { id: 'rId-header-default', kind: 'header' } as const;
const host = document.createElement('div');

const first = manager.ensureEditorSync(descriptor, { editorHost: host });
const second = manager.ensureEditorSync(descriptor, { editorHost: host });

expect(first).toBeDefined();
expect(second).toBe(first);
expect(mockCreateHeaderFooterEditor).toHaveBeenCalledTimes(1);
expect(host.children).toHaveLength(1);
});

it('ensureEditorSync reattaches the cached editor container to a new host', () => {
const editor = createMockEditor();
const manager = new HeaderFooterEditorManager(editor);
const descriptor = { id: 'rId-header-default', kind: 'header' } as const;
const firstHost = document.createElement('div');
const secondHost = document.createElement('div');

const sectionEditor = manager.ensureEditorSync(descriptor, { editorHost: firstHost });
expect(sectionEditor).toBeDefined();
expect(firstHost.children).toHaveLength(1);

const sameEditor = manager.ensureEditorSync(descriptor, { editorHost: secondHost });

expect(sameEditor).toBe(sectionEditor);
expect(firstHost.children).toHaveLength(0);
expect(secondHost.children).toHaveLength(1);
});

it('emits contentChanged and syncs converter/Yjs data when section editor updates', async () => {
const editor = createMockEditor();
const manager = new HeaderFooterEditorManager(editor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,39 +330,7 @@ export class HeaderFooterEditorManager extends EventEmitter {
console.error('[HeaderFooterEditorManager] Editor initialization failed:', error);
this.emit('error', { descriptor, error });
});

// Move editor container to the new editorHost if provided
// This is necessary because cached editors may have been appended elsewhere
if (existing.container && options?.editorHost) {
// Only move if not already in the target host
if (existing.container.parentElement !== options.editorHost) {
options.editorHost.appendChild(existing.container);
}
}

// Update editor options if provided
if (existing.editor && options) {
const updateOptions: Record<string, unknown> = {};
if (options.currentPageNumber !== undefined) {
updateOptions.currentPageNumber = options.currentPageNumber;
}
if (options.totalPageCount !== undefined) {
updateOptions.totalPageCount = options.totalPageCount;
}
if (options.availableWidth !== undefined) {
updateOptions.availableWidth = options.availableWidth;
}
if (options.availableHeight !== undefined) {
updateOptions.availableHeight = options.availableHeight;
}
if (Object.keys(updateOptions).length > 0) {
existing.editor.setOptions(updateOptions);
// Refresh page number display after option changes.
// NodeViews read editor.options but PM doesn't re-render them
// when only options change (no document transaction).
this.#refreshPageNumberDisplay(existing.editor);
}
}
this.#mountAndUpdateEntry(existing, options);

return existing.editor;
}
Expand All @@ -380,7 +348,7 @@ export class HeaderFooterEditorManager extends EventEmitter {
// Start creation and track the promise
const creationPromise = (async () => {
try {
const entry = await this.#createEditor(descriptor, options);
const entry = this.#createEditorEntry(descriptor, options);
if (!entry) return null;

this.#editorEntries.set(descriptor.id, entry);
Expand All @@ -406,6 +374,44 @@ export class HeaderFooterEditorManager extends EventEmitter {
return creationPromise;
}

/**
* Synchronously returns the cached editor for a descriptor, creating it on demand.
*
* Presentation-mode story activation needs a stable editor instance and DOM
* target immediately so input can be forwarded into the hidden host without
* waiting for the async `create` event. The normal lifecycle hooks still run
* through the returned entry's `ready` promise.
*/
ensureEditorSync(
descriptor: HeaderFooterDescriptor,
options?: {
editorHost?: HTMLElement;
availableWidth?: number;
availableHeight?: number;
currentPageNumber?: number;
totalPageCount?: number;
},
): Editor | null {
if (!descriptor?.id) return null;

const existing = this.#editorEntries.get(descriptor.id);
if (existing) {
this.#cacheHits += 1;
this.#updateAccessOrder(descriptor.id);
this.#mountAndUpdateEntry(existing, options);
return existing.editor;
}

const entry = this.#createEditorEntry(descriptor, options);
if (!entry) return null;

this.#cacheMisses += 1;
this.#editorEntries.set(descriptor.id, entry);
this.#updateAccessOrder(descriptor.id);
this.#enforceCacheSizeLimit();
return entry.editor;
}

/**
* Updates page number DOM elements to reflect current editor options.
* Called after setOptions to sync NodeViews that read editor.options.
Expand Down Expand Up @@ -671,7 +677,7 @@ export class HeaderFooterEditorManager extends EventEmitter {
this.#editorEntries.clear();
}

async #createEditor(
#createEditorEntry(
descriptor: HeaderFooterDescriptor,
options?: {
editorHost?: HTMLElement;
Expand All @@ -680,7 +686,7 @@ export class HeaderFooterEditorManager extends EventEmitter {
currentPageNumber?: number;
totalPageCount?: number;
},
): Promise<HeaderFooterEditorEntry | null> {
): HeaderFooterEditorEntry | null {
const json = this.getDocumentJson(descriptor);
if (!json) return null;

Expand Down Expand Up @@ -799,6 +805,45 @@ export class HeaderFooterEditorManager extends EventEmitter {
};
}

#mountAndUpdateEntry(
entry: HeaderFooterEditorEntry,
options?: {
editorHost?: HTMLElement;
availableWidth?: number;
availableHeight?: number;
currentPageNumber?: number;
totalPageCount?: number;
},
): void {
if (entry.container && options?.editorHost && entry.container.parentElement !== options.editorHost) {
options.editorHost.appendChild(entry.container);
}

if (!options) {
return;
}

const updateOptions: Record<string, unknown> = {};
if (options.currentPageNumber !== undefined) {
updateOptions.currentPageNumber = options.currentPageNumber;
}
if (options.totalPageCount !== undefined) {
updateOptions.totalPageCount = options.totalPageCount;
}
if (options.availableWidth !== undefined) {
updateOptions.availableWidth = options.availableWidth;
}
if (options.availableHeight !== undefined) {
updateOptions.availableHeight = options.availableHeight;
}
if (Object.keys(updateOptions).length > 0) {
entry.editor.setOptions(updateOptions);
// NodeViews that render PAGE / NUMPAGES read editor.options, so refresh
// them when the presentation context changes without a document step.
this.#refreshPageNumberDisplay(entry.editor);
}
}

#createEditorContainer(): HTMLElement {
const doc =
(this.#editor.options?.element?.ownerDocument as Document | undefined) ?? globalThis.document ?? undefined;
Expand Down
Loading
Loading