Skip to content

Commit 2d5a7bb

Browse files
authored
feat(bundle): add changePreviewVisible method to editor api (#1153)
1 parent 58dffe0 commit 2d5a7bb

6 files changed

Lines changed: 186 additions & 24 deletions

File tree

demo/src/components/Playground.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export const Playground = memo<PlaygroundProps>((props) => {
154154
yfmMods,
155155
} = props;
156156
const [editorMode, setEditorMode] = useState<MarkdownEditorMode>(initialEditor ?? 'wysiwyg');
157+
const [previewVisible, setPreviewVisible] = useState(false);
157158
const [mdRaw, setMdRaw] = useState<MarkupString>(initial || '');
158159

159160
useEffect(() => {
@@ -352,6 +353,10 @@ export const Playground = memo<PlaygroundProps>((props) => {
352353
function onChangeToolbarVisibility({visible}: {visible: boolean}) {
353354
console.info('Toolbar visible: ' + visible);
354355
}
356+
function onChangePreviewVisible({visible}: {visible: boolean}) {
357+
setPreviewVisible(visible);
358+
console.info(`Preview visible: ${visible}`);
359+
}
355360

356361
mdEditor.on('cancel', onCancel);
357362
mdEditor.on('submit', onSubmit);
@@ -360,6 +365,7 @@ export const Playground = memo<PlaygroundProps>((props) => {
360365
mdEditor.on('change-editor-mode', onChangeEditorType);
361366
mdEditor.on('change-split-mode-enabled', onChangeSplitModeEnabled);
362367
mdEditor.on('change-toolbar-visibility', onChangeToolbarVisibility);
368+
mdEditor.on('change-preview-visible', onChangePreviewVisible);
363369

364370
return () => {
365371
mdEditor.off('cancel', onCancel);
@@ -369,6 +375,7 @@ export const Playground = memo<PlaygroundProps>((props) => {
369375
mdEditor.off('change-editor-mode', onChangeEditorType);
370376
mdEditor.off('change-split-mode-enabled', onChangeSplitModeEnabled);
371377
mdEditor.off('change-toolbar-visibility', onChangeToolbarVisibility);
378+
mdEditor.off('change-preview-visible', onChangePreviewVisible);
372379
};
373380
}, [mdEditor]);
374381

@@ -466,6 +473,14 @@ export const Playground = memo<PlaygroundProps>((props) => {
466473
mdEditor.focus();
467474
}}
468475
/>
476+
{editorMode === 'markup' && (
477+
<DropdownMenu.Item
478+
text={`Toggle Preview (${previewVisible ? 'on' : 'off'})`}
479+
action={() => {
480+
mdEditor.changePreviewVisible();
481+
}}
482+
/>
483+
)}
469484
</DropdownMenu>
470485
{mdEditor.currentMode === 'markup' && (
471486
<MoveToLine
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/// <reference types="jest" />
2+
import {ReactRenderStorage} from '../extensions';
3+
import {Logger2} from '../logger';
4+
import {DirectiveSyntaxContext} from '../utils/directive';
5+
6+
import {EditorImpl} from './Editor';
7+
8+
function createEditor(overrides: Partial<ConstructorParameters<typeof EditorImpl>[0]> = {}) {
9+
return new EditorImpl({
10+
logger: new Logger2(),
11+
renderStorage: new ReactRenderStorage(),
12+
preset: 'full',
13+
directiveSyntax: new DirectiveSyntaxContext(undefined),
14+
pmTransformers: [],
15+
...overrides,
16+
});
17+
}
18+
19+
function createEditorWithPreview(
20+
overrides: Partial<ConstructorParameters<typeof EditorImpl>[0]> = {},
21+
) {
22+
return createEditor({
23+
markupConfig: {renderPreview: () => null},
24+
...overrides,
25+
});
26+
}
27+
28+
describe('EditorImpl: changePreviewVisible', () => {
29+
it('should be a no-op when renderPreview is not configured', () => {
30+
const editor = createEditor();
31+
const listener = jest.fn();
32+
editor.on('change-preview-visible', listener);
33+
34+
editor.changePreviewVisible(true);
35+
36+
expect(editor.previewVisible).toBe(false);
37+
expect(listener).not.toHaveBeenCalled();
38+
});
39+
40+
it('should show preview and emit change-preview-visible event', () => {
41+
const editor = createEditorWithPreview();
42+
const listener = jest.fn();
43+
editor.on('change-preview-visible', listener);
44+
45+
editor.changePreviewVisible(true);
46+
47+
expect(editor.previewVisible).toBe(true);
48+
expect(listener).toHaveBeenCalledTimes(1);
49+
expect(listener).toHaveBeenCalledWith({visible: true});
50+
});
51+
52+
it('should toggle preview when called without argument', () => {
53+
const editor = createEditorWithPreview();
54+
const listener = jest.fn();
55+
editor.on('change-preview-visible', listener);
56+
57+
editor.changePreviewVisible(); // false → true
58+
expect(editor.previewVisible).toBe(true);
59+
60+
editor.changePreviewVisible(); // true → false
61+
expect(editor.previewVisible).toBe(false);
62+
63+
expect(listener).toHaveBeenCalledTimes(2);
64+
expect(listener).toHaveBeenNthCalledWith(1, {visible: true});
65+
expect(listener).toHaveBeenNthCalledWith(2, {visible: false});
66+
});
67+
68+
it('should be a no-op when split mode is enabled', () => {
69+
const editor = createEditorWithPreview({
70+
initial: {splitModeEnabled: true},
71+
markupConfig: {renderPreview: () => null, splitMode: 'vertical'},
72+
});
73+
const listener = jest.fn();
74+
editor.on('change-preview-visible', listener);
75+
76+
editor.changePreviewVisible(true);
77+
78+
expect(editor.previewVisible).toBe(false);
79+
expect(listener).not.toHaveBeenCalled();
80+
});
81+
});
82+
83+
describe('EditorImpl: changeSplitModeEnabled', () => {
84+
it('should enable split mode and emit change-split-mode-enabled event', () => {
85+
const editor = createEditor();
86+
const listener = jest.fn();
87+
editor.on('change-split-mode-enabled', listener);
88+
89+
editor.changeSplitModeEnabled({splitModeEnabled: true});
90+
91+
expect(editor.splitModeEnabled).toBe(true);
92+
expect(listener).toHaveBeenCalledTimes(1);
93+
expect(listener).toHaveBeenCalledWith({splitModeEnabled: true});
94+
});
95+
});
96+
97+
describe('EditorImpl: mutual exclusion between preview and split mode', () => {
98+
it('should not show preview when split mode is active (changePreviewVisible is a no-op)', () => {
99+
const editor = createEditorWithPreview({
100+
initial: {splitModeEnabled: true},
101+
markupConfig: {renderPreview: () => null, splitMode: 'vertical'},
102+
});
103+
const splitListener = jest.fn();
104+
editor.on('change-split-mode-enabled', splitListener);
105+
106+
expect(editor.splitModeEnabled).toBe(true);
107+
108+
editor.changePreviewVisible(true);
109+
110+
expect(editor.previewVisible).toBe(false);
111+
expect(editor.splitModeEnabled).toBe(true);
112+
// split-mode state did not change
113+
expect(splitListener).not.toHaveBeenCalled();
114+
});
115+
116+
it('should not emit events when state does not change', () => {
117+
const editor = createEditorWithPreview();
118+
const previewListener = jest.fn();
119+
const splitListener = jest.fn();
120+
editor.on('change-preview-visible', previewListener);
121+
editor.on('change-split-mode-enabled', splitListener);
122+
123+
// previewVisible already false
124+
editor.changePreviewVisible(false);
125+
// splitModeEnabled already false
126+
editor.changeSplitModeEnabled({splitModeEnabled: false});
127+
128+
expect(previewListener).not.toHaveBeenCalled();
129+
expect(splitListener).not.toHaveBeenCalled();
130+
});
131+
});

packages/editor/src/bundle/Editor.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ export interface EditorInt
8787

8888
changeSplitModeEnabled(opts: {splitModeEnabled: boolean}): void;
8989

90+
readonly previewVisible: boolean;
91+
92+
changePreviewVisible(visible?: boolean): void;
93+
9094
destroy(): void;
9195
}
9296

@@ -111,6 +115,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
111115
#toolbarVisible: boolean;
112116
#splitModeEnabled: boolean;
113117
#splitMode: SplitMode;
118+
#previewVisible: boolean;
114119
#renderPreview?: RenderPreview;
115120
#wysiwygEditor?: WysiwygEditor;
116121
#markupEditor?: MarkupEditor;
@@ -198,6 +203,10 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
198203
return this.#splitMode;
199204
}
200205

206+
get previewVisible(): boolean {
207+
return this.#previewVisible;
208+
}
209+
201210
get preset(): EditorPreset {
202211
return this.#preset;
203212
}
@@ -341,6 +350,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
341350
this.#toolbarVisible = initial.toolbarVisible ?? true;
342351
this.#splitMode = (markupConfig.renderPreview && markupConfig.splitMode) ?? false;
343352
this.#splitModeEnabled = (this.#splitMode && initial.splitModeEnabled) ?? false;
353+
this.#previewVisible = false;
344354
this.#renderPreview = markupConfig.renderPreview;
345355

346356
this.#markup = initial.markup ?? '';
@@ -433,6 +443,14 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
433443
this.emit('change-split-mode-enabled', opts);
434444
}
435445

446+
changePreviewVisible(visible = !this.#previewVisible): void {
447+
if (!this.#renderPreview || this.#splitModeEnabled) return;
448+
if (this.#previewVisible === visible) return;
449+
this.#previewVisible = visible;
450+
this.emit('rerender', null);
451+
this.emit('change-preview-visible', {visible});
452+
}
453+
436454
focus(): void {
437455
return this.currentEditor.focus();
438456
}

packages/editor/src/bundle/MarkdownEditorView.tsx

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {ClassNameProps} from '../classname';
1616
import {i18n} from '../i18n/bundle';
1717
import {globalLogger} from '../logger';
1818
import type {ToolbarsPreset} from '../modules/toolbars/types';
19-
import {useBooleanState, useSticky} from '../react-utils';
19+
import {useSticky} from '../react-utils';
2020
import {isMac} from '../utils';
2121

2222
import type {Editor, EditorInt} from './Editor';
@@ -41,9 +41,6 @@ interface EditorWrapperProps extends QAProps, ToolbarConfigs, Omit<ViewProps, 'e
4141
editor: EditorInt;
4242
editorMode: MarkdownEditorMode;
4343
isFocused: boolean;
44-
showPreview: boolean;
45-
toggleShowPreview: () => void;
46-
unsetShowPreview: () => void;
4744
}
4845
const EditorWrapper = forwardRef<HTMLDivElement, EditorWrapperProps>(
4946
(
@@ -58,16 +55,14 @@ const EditorWrapper = forwardRef<HTMLDivElement, EditorWrapperProps>(
5855
markupToolbarConfig: initialMarkupToolbarConfig,
5956
qa,
6057
settingsVisible: settingsVisibleProp,
61-
showPreview,
6258
stickyToolbar,
63-
toggleShowPreview,
6459
toolbarsPreset,
65-
unsetShowPreview,
6660
wysiwygHiddenActionsConfig: initialWysiwygHiddenActionsConfig,
6761
wysiwygToolbarConfig: initialWysiwygToolbarConfig,
6862
},
6963
ref,
7064
) => {
65+
const showPreview = editor.previewVisible;
7166
const {
7267
wysiwygToolbarConfig,
7368
markupToolbarConfig,
@@ -97,9 +92,9 @@ const EditorWrapper = forwardRef<HTMLDivElement, EditorWrapperProps>(
9792
const onModeChange = useCallback(
9893
(type: MarkdownEditorMode) => {
9994
editor.changeEditorMode({mode: type, reason: 'settings'});
100-
unsetShowPreview();
95+
editor.changePreviewVisible(false);
10196
},
102-
[editor, unsetShowPreview],
97+
[editor],
10398
);
10499
const onToolbarVisibilityChange = useCallback(
105100
(visible: boolean) => {
@@ -109,17 +104,15 @@ const EditorWrapper = forwardRef<HTMLDivElement, EditorWrapperProps>(
109104
);
110105
const onSplitModeChange = useCallback(
111106
(splitModeEnabled: boolean) => {
112-
unsetShowPreview();
113107
editor.changeSplitModeEnabled({splitModeEnabled});
114108
},
115-
[editor, unsetShowPreview],
109+
[editor],
116110
);
117111
const onShowPreviewChange = useCallback(
118112
(showPreviewValue: boolean) => {
119-
editor.changeSplitModeEnabled({splitModeEnabled: false});
120-
if (showPreviewValue !== showPreview) toggleShowPreview();
113+
editor.changePreviewVisible(showPreviewValue);
121114
},
122-
[editor, showPreview, toggleShowPreview],
115+
[editor],
123116
);
124117
const canRenderPreview = Boolean(
125118
editor.renderPreview && editorMode === 'markup' && !editor.splitModeEnabled,
@@ -129,10 +122,10 @@ const EditorWrapper = forwardRef<HTMLDivElement, EditorWrapperProps>(
129122
(e) => canRenderPreview && isPreviewKeyDown(e),
130123
(e) => {
131124
e.preventDefault();
132-
onShowPreviewChange(!showPreview);
125+
editor.changePreviewVisible();
133126
},
134127
{event: 'keydown'},
135-
[showPreview, editorMode, onShowPreviewChange, canRenderPreview],
128+
[editorMode, editor, canRenderPreview],
136129
);
137130

138131
useKey(
@@ -141,11 +134,11 @@ const EditorWrapper = forwardRef<HTMLDivElement, EditorWrapperProps>(
141134
editor.emit('submit', null);
142135

143136
if (hidePreviewAfterSubmit) {
144-
onShowPreviewChange(false);
137+
editor.changePreviewVisible(false);
145138
}
146139
},
147140
{event: 'keydown'},
148-
[hidePreviewAfterSubmit, enableSubmitInPreview, showPreview, showPreview],
141+
[hidePreviewAfterSubmit, enableSubmitInPreview, showPreview, editor],
149142
);
150143

151144
const settingsProps = {
@@ -262,7 +255,6 @@ export const MarkdownEditorView = forwardRef<HTMLDivElement, MarkdownEditorViewP
262255
(props, ref) => {
263256
const divRef = useEnsuredForwardedRef(ref as React.MutableRefObject<HTMLDivElement>);
264257
const editorWrapperRef = useRef(null);
265-
const [showPreview, , unsetShowPreview, toggleShowPreview] = useBooleanState(false);
266258

267259
const [isMounted, setIsMounted] = useState(false);
268260
useEffect(() => {
@@ -308,10 +300,10 @@ export const MarkdownEditorView = forwardRef<HTMLDivElement, MarkdownEditorViewP
308300
const toaster = useToaster();
309301

310302
useEffect(() => {
311-
if (showPreview) {
303+
if (editor.previewVisible) {
312304
divRef.current.focus();
313305
}
314-
}, [divRef, showPreview]);
306+
}, [divRef, editor, editor.previewVisible]);
315307

316308
const areSettingsVisible =
317309
settingsVisible === true ||
@@ -366,11 +358,8 @@ export const MarkdownEditorView = forwardRef<HTMLDivElement, MarkdownEditorViewP
366358
qa="g-md-editor-mode"
367359
ref={editorWrapperRef}
368360
settingsVisible={settingsVisible}
369-
showPreview={showPreview}
370361
stickyToolbar={stickyToolbar}
371-
toggleShowPreview={toggleShowPreview}
372362
toolbarsPreset={toolbarsPreset}
373-
unsetShowPreview={unsetShowPreview}
374363
wysiwygHiddenActionsConfig={wysiwygHiddenActionsConfig}
375364
wysiwygToolbarConfig={wysiwygToolbarConfig}
376365
/>

packages/editor/src/bundle/editor-public-types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,17 @@ export interface MarkdownEditorInstance extends Receiver<EventMap>, CommonEditor
1717
readonly logger: Logger2.LogReceiver;
1818
readonly currentMode: MarkdownEditorMode;
1919
readonly toolbarVisible: boolean;
20+
/** Whether the full-screen markup preview is currently visible. */
21+
readonly previewVisible: boolean;
2022
setEditorMode(mode: MarkdownEditorMode, opts?: Pick<ChangeEditorModeOptions, 'emit'>): void;
2123
moveCursor(position: 'start' | 'end' | {line: number}): void;
2224
insert(markup: MarkupString): void;
25+
/**
26+
* Control the full-screen markup preview.
27+
* Pass `true`/`false` to explicitly show/hide it, or call with no argument to toggle.
28+
* No-op if `renderPreview` is not configured or split mode is currently enabled.
29+
*/
30+
changePreviewVisible(visible?: boolean): void;
2331
/** @internal used in demo for dev-tools */
2432
readonly _wysiwygView?: PMEditorView;
2533
}

packages/editor/src/bundle/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ export interface EventMap {
1616
'change-editor-mode': {mode: MarkdownEditorMode};
1717
'change-toolbar-visibility': {visible: boolean};
1818
'change-split-mode-enabled': {splitModeEnabled: boolean};
19+
'change-preview-visible': {visible: boolean};
1920
}

0 commit comments

Comments
 (0)