Skip to content

Commit 9040cf0

Browse files
committed
refactor(editor): extract error banner into LayoutErrorBanner
Move layout error banner DOM creation, show/dismiss logic from PresentationEditor into a dedicated UI class. The retry button callback is injected as a dependency. Reduces PresentationEditor from 5424 to 5379 lines (-45).
1 parent 685daf9 commit 9040cf0

2 files changed

Lines changed: 92 additions & 61 deletions

File tree

packages/super-editor/src/core/presentation-editor/PresentationEditor.ts

Lines changed: 15 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CellSelection } from 'prosemirror-tables';
44
import { DecorationBridge } from './dom/DecorationBridge.js';
55
import { SdtSelectionStyleManager } from './selection/SdtSelectionStyleManager.js';
66
import { SemanticFlowController } from './layout/SemanticFlowController.js';
7+
import { LayoutErrorBanner } from './ui/LayoutErrorBanner.js';
78
import type { EditorState, Transaction } from 'prosemirror-state';
89
import type { Node as ProseMirrorNode, Mark } from 'prosemirror-model';
910
import type { Mapping } from 'prosemirror-transform';
@@ -287,8 +288,7 @@ export class PresentationEditor extends EventEmitter {
287288
#dragDropManager: DragDropManager | null = null;
288289
#layoutError: LayoutError | null = null;
289290
#layoutErrorState: 'healthy' | 'degraded' | 'failed' = 'healthy';
290-
#errorBanner: HTMLElement | null = null;
291-
#errorBannerMessage: HTMLElement | null = null;
291+
#errorBanner!: LayoutErrorBanner;
292292
#renderScheduled = false;
293293
#pendingDocChange = false;
294294
#pendingMapping: Mapping | null = null;
@@ -431,6 +431,16 @@ export class PresentationEditor extends EventEmitter {
431431
this.#painterHost.addEventListener('mouseover', this.#sdtStyles.handleMouseEnter);
432432
this.#painterHost.addEventListener('mouseout', this.#sdtStyles.handleMouseLeave);
433433

434+
this.#errorBanner = new LayoutErrorBanner({
435+
host: this.#visibleHost,
436+
getDebugLabel: () => this.#layoutOptions.debugLabel,
437+
onRetry: () => {
438+
this.#layoutError = null;
439+
this.#pendingDocChange = true;
440+
this.#scheduleRerender();
441+
},
442+
});
443+
434444
const win = this.#visibleHost?.ownerDocument?.defaultView ?? window;
435445
this.#domIndexObserverManager = new DomPositionIndexObserverManager({
436446
windowRoot: win,
@@ -2461,7 +2471,7 @@ export class PresentationEditor extends EventEmitter {
24612471
this.#modeBanner = null;
24622472
this.#ariaLiveRegion?.remove();
24632473
this.#ariaLiveRegion = null;
2464-
this.#errorBanner?.remove();
2474+
this.#errorBanner.destroy();
24652475
if (this.#editor) {
24662476
(this.#editor as Editor & { presentationEditor?: PresentationEditor | null }).presentationEditor = null;
24672477
this.#editor.destroy();
@@ -3588,7 +3598,7 @@ export class PresentationEditor extends EventEmitter {
35883598
// Reset error state on successful layout
35893599
this.#layoutError = null;
35903600
this.#layoutErrorState = 'healthy';
3591-
this.#dismissErrorBanner();
3601+
this.#errorBanner.dismiss();
35923602

35933603
// Update viewport dimensions after layout (page count may have changed)
35943604
this.#applyZoom();
@@ -5239,7 +5249,7 @@ export class PresentationEditor extends EventEmitter {
52395249
}
52405250

52415251
this.emit('layoutError', this.#layoutError);
5242-
this.#showLayoutErrorBanner(error);
5252+
this.#errorBanner.show(error);
52435253
}
52445254

52455255
#decorateError(error: unknown, stage: string): Error {
@@ -5250,62 +5260,6 @@ export class PresentationEditor extends EventEmitter {
52505260
return new Error(`[${stage}] ${String(error)}`);
52515261
}
52525262

5253-
#showLayoutErrorBanner(error: Error) {
5254-
const doc = this.#visibleHost.ownerDocument ?? document;
5255-
if (!this.#errorBanner) {
5256-
const banner = doc.createElement('div');
5257-
banner.className = 'presentation-editor__layout-error';
5258-
banner.style.display = 'flex';
5259-
banner.style.alignItems = 'center';
5260-
banner.style.justifyContent = 'space-between';
5261-
banner.style.gap = '8px';
5262-
banner.style.padding = '8px 12px';
5263-
banner.style.background = '#FFF6E5';
5264-
banner.style.border = '1px solid #F5B971';
5265-
banner.style.borderRadius = '6px';
5266-
banner.style.marginBottom = '8px';
5267-
5268-
const message = doc.createElement('span');
5269-
banner.appendChild(message);
5270-
5271-
const retry = doc.createElement('button');
5272-
retry.type = 'button';
5273-
retry.textContent = 'Reload layout';
5274-
retry.style.border = 'none';
5275-
retry.style.borderRadius = '4px';
5276-
retry.style.background = '#F5B971';
5277-
retry.style.color = '#3F2D00';
5278-
retry.style.padding = '6px 10px';
5279-
retry.style.cursor = 'pointer';
5280-
retry.addEventListener('click', () => {
5281-
this.#layoutError = null;
5282-
this.#dismissErrorBanner();
5283-
this.#pendingDocChange = true;
5284-
this.#scheduleRerender();
5285-
});
5286-
5287-
banner.appendChild(retry);
5288-
this.#visibleHost.prepend(banner);
5289-
5290-
this.#errorBanner = banner;
5291-
this.#errorBannerMessage = message;
5292-
}
5293-
5294-
if (this.#errorBannerMessage) {
5295-
this.#errorBannerMessage.textContent =
5296-
'Layout engine hit an error. Your document is safe — try reloading layout.';
5297-
if (this.#layoutOptions.debugLabel) {
5298-
this.#errorBannerMessage.textContent += ` (${this.#layoutOptions.debugLabel}: ${error.message})`;
5299-
}
5300-
}
5301-
}
5302-
5303-
#dismissErrorBanner() {
5304-
this.#errorBanner?.remove();
5305-
this.#errorBanner = null;
5306-
this.#errorBannerMessage = null;
5307-
}
5308-
53095263
/**
53105264
* Determines whether the current viewing mode should block edits.
53115265
* When documentMode is viewing but the active editor has been toggled
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
export interface LayoutErrorBannerDeps {
2+
host: HTMLElement;
3+
getDebugLabel: () => string | undefined;
4+
onRetry: () => void;
5+
}
6+
7+
/**
8+
* Manages the layout error banner UI: a dismissible warning bar shown at the
9+
* top of the visible host when the layout engine encounters an error.
10+
*/
11+
export class LayoutErrorBanner {
12+
#deps: LayoutErrorBannerDeps;
13+
#banner: HTMLElement | null = null;
14+
#message: HTMLElement | null = null;
15+
16+
constructor(deps: LayoutErrorBannerDeps) {
17+
this.#deps = deps;
18+
}
19+
20+
show(error: Error): void {
21+
const doc = this.#deps.host.ownerDocument ?? document;
22+
if (!this.#banner) {
23+
const banner = doc.createElement('div');
24+
banner.className = 'presentation-editor__layout-error';
25+
banner.style.display = 'flex';
26+
banner.style.alignItems = 'center';
27+
banner.style.justifyContent = 'space-between';
28+
banner.style.gap = '8px';
29+
banner.style.padding = '8px 12px';
30+
banner.style.background = '#FFF6E5';
31+
banner.style.border = '1px solid #F5B971';
32+
banner.style.borderRadius = '6px';
33+
banner.style.marginBottom = '8px';
34+
35+
const message = doc.createElement('span');
36+
banner.appendChild(message);
37+
38+
const retry = doc.createElement('button');
39+
retry.type = 'button';
40+
retry.textContent = 'Reload layout';
41+
retry.style.border = 'none';
42+
retry.style.borderRadius = '4px';
43+
retry.style.background = '#F5B971';
44+
retry.style.color = '#3F2D00';
45+
retry.style.padding = '6px 10px';
46+
retry.style.cursor = 'pointer';
47+
retry.addEventListener('click', () => {
48+
this.dismiss();
49+
this.#deps.onRetry();
50+
});
51+
52+
banner.appendChild(retry);
53+
this.#deps.host.prepend(banner);
54+
55+
this.#banner = banner;
56+
this.#message = message;
57+
}
58+
59+
if (this.#message) {
60+
this.#message.textContent = 'Layout engine hit an error. Your document is safe — try reloading layout.';
61+
const debugLabel = this.#deps.getDebugLabel();
62+
if (debugLabel) {
63+
this.#message.textContent += ` (${debugLabel}: ${error.message})`;
64+
}
65+
}
66+
}
67+
68+
dismiss(): void {
69+
this.#banner?.remove();
70+
this.#banner = null;
71+
this.#message = null;
72+
}
73+
74+
destroy(): void {
75+
this.dismiss();
76+
}
77+
}

0 commit comments

Comments
 (0)