Skip to content

Commit fccec46

Browse files
committed
feat(YfmHtmlBlock): add editable preview mode
1 parent d79c91b commit fccec46

3 files changed

Lines changed: 75 additions & 9 deletions

File tree

demo/src/components/Playground.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ export const Playground = memo<PlaygroundProps>((props) => {
242242
showButton: true,
243243
allowAdd: true,
244244
},
245+
editablePreview: true,
245246
head: `
246247
<base target="_blank" />
247248
<style>

packages/editor/src/extensions/additional/YfmHtmlBlock/YfmHtmlBlockNodeView/YfmHtmlBlockView.tsx

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ interface YfmHtmlBlockViewProps {
3434
html: string;
3535
onClick: () => void;
3636
config?: IHTMLIFrameElementConfig;
37+
editablePreview?: boolean;
38+
onInlineSave?: (innerHtml: string) => void;
3739
}
3840

3941
export function generateID() {
@@ -55,11 +57,18 @@ const createLinkCLickHandler = (value: Element, document: Document) => (event: E
5557
}
5658
};
5759

58-
const YfmHtmlBlockPreview: React.FC<YfmHtmlBlockViewProps> = ({html, onClick, config}) => {
60+
const YfmHtmlBlockPreview: React.FC<YfmHtmlBlockViewProps> = ({
61+
html,
62+
onClick,
63+
config,
64+
editablePreview,
65+
onInlineSave,
66+
}) => {
5967
const ref = useRef<HTMLIFrameElement>(null);
6068
const styles = useRef<Record<string, string>>({});
6169
const classNames = useRef<string[]>([]);
6270
const resizeConfig = useRef<Record<string, number>>({});
71+
const isInlineEditing = useRef(false);
6372

6473
const [height, setHeight] = useState('100%');
6574

@@ -68,19 +77,53 @@ const YfmHtmlBlockPreview: React.FC<YfmHtmlBlockViewProps> = ({html, onClick, co
6877
setClassNames(config?.classNames);
6978
}, [config, ref.current?.contentWindow?.document?.body]);
7079

80+
const enterInlineEdit = () => {
81+
const body = ref.current?.contentWindow?.document.body;
82+
if (!body) return;
83+
isInlineEditing.current = true;
84+
body.contentEditable = 'true';
85+
body.style.cursor = 'text';
86+
body.focus();
87+
};
88+
89+
const handleBodyBlur = () => {
90+
const body = ref.current?.contentWindow?.document.body;
91+
if (!body || !isInlineEditing.current) return;
92+
isInlineEditing.current = false;
93+
body.contentEditable = 'false';
94+
body.style.removeProperty('cursor');
95+
onInlineSave?.(body.innerHTML);
96+
};
97+
98+
const handleDblClick = () => {
99+
if (editablePreview) {
100+
enterInlineEdit();
101+
} else {
102+
onClick();
103+
}
104+
};
105+
71106
const handleLoadIFrame = () => {
72107
const contentWindow = ref.current?.contentWindow;
73108

109+
// fresh document after reload: not editing yet
110+
isInlineEditing.current = false;
74111
handleResizeIFrame();
75112

76113
if (contentWindow) {
77114
const frameDocument = contentWindow.document;
78-
frameDocument.addEventListener('dblclick', () => {
79-
onClick();
80-
});
115+
frameDocument.addEventListener('dblclick', handleDblClick);
116+
// blur does not bubble; capture catches focus leaving the body
117+
frameDocument.body?.addEventListener('blur', handleBodyBlur, true);
81118
}
82119
};
83120

121+
const handleUnloadIFrame = () => {
122+
const frameDocument = ref.current?.contentWindow?.document;
123+
frameDocument?.removeEventListener('dblclick', handleDblClick);
124+
frameDocument?.body?.removeEventListener('blur', handleBodyBlur, true);
125+
};
126+
84127
const handleResizeIFrame = () => {
85128
if (ref.current) {
86129
const contentWindow = ref.current?.contentWindow;
@@ -162,11 +205,13 @@ const YfmHtmlBlockPreview: React.FC<YfmHtmlBlockViewProps> = ({html, onClick, co
162205
};
163206

164207
useEffect(() => {
165-
ref.current?.addEventListener('load', handleLoadIFrame);
166-
ref.current?.addEventListener('load', createAnchorLinkHandlers('add'));
208+
const iframe = ref.current;
209+
iframe?.addEventListener('load', handleLoadIFrame);
210+
iframe?.addEventListener('load', createAnchorLinkHandlers('add'));
167211
return () => {
168-
ref.current?.removeEventListener('load', handleLoadIFrame);
169-
ref.current?.removeEventListener('load', createAnchorLinkHandlers('remove'));
212+
handleUnloadIFrame();
213+
iframe?.removeEventListener('load', handleLoadIFrame);
214+
iframe?.removeEventListener('load', createAnchorLinkHandlers('remove'));
170215
};
171216
}, [html]);
172217

@@ -255,6 +300,7 @@ export const YfmHtmlBlockView: React.FC<{
255300
baseTarget = '_parent',
256301
head: headContent = '',
257302
templates,
303+
editablePreview,
258304
} = options;
259305
const entityId: string = node.attrs[YfmHtmlBlockConsts.NodeAttrs.EntityId];
260306
const entityKey = useMemo(
@@ -284,6 +330,12 @@ export const YfmHtmlBlockView: React.FC<{
284330
closeTemplates();
285331
};
286332

333+
const handleInlineSave = (innerHtml: string) => {
334+
const current = node.attrs[YfmHtmlBlockConsts.NodeAttrs.srcdoc] ?? '';
335+
if (innerHtml === current) return;
336+
onChange({[YfmHtmlBlockConsts.NodeAttrs.srcdoc]: innerHtml});
337+
};
338+
287339
if (editing) {
288340
return (
289341
<CodeEditMode
@@ -319,7 +371,13 @@ export const YfmHtmlBlockView: React.FC<{
319371
<Label className={b('label')} icon={<Icon size={16} data={Eye} />}>
320372
{i18n('preview')}
321373
</Label>
322-
<YfmHtmlBlockPreview html={resultHtml} onClick={setEditing} config={config} />
374+
<YfmHtmlBlockPreview
375+
html={resultHtml}
376+
onClick={setEditing}
377+
config={config}
378+
editablePreview={editablePreview}
379+
onInlineSave={handleInlineSave}
380+
/>
323381

324382
<div className={b('menu')}>
325383
{showTemplatesButton && (

packages/editor/src/extensions/additional/YfmHtmlBlock/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ export interface YfmHtmlBlockOptions extends Omit<
1919
delay?: number; // по умолчанию 1000ms
2020
};
2121
templates?: YfmHtmlBlockTemplatesOptions;
22+
/**
23+
* Double-clicking the preview edits text inline inside the iframe instead of
24+
* opening the raw HTML code editor. The code editor stays reachable via the
25+
* "Edit" menu item.
26+
* @default false
27+
*/
28+
editablePreview?: boolean;
2229
}
2330

2431
export const YfmHtmlBlock: ExtensionAuto<YfmHtmlBlockOptions> = (

0 commit comments

Comments
 (0)