@@ -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
3941export 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 && (
0 commit comments