diff --git a/assets/creator-studio/src/editor-runtime.ts b/assets/creator-studio/src/editor-runtime.ts index ddda5df5..9b2caecc 100644 --- a/assets/creator-studio/src/editor-runtime.ts +++ b/assets/creator-studio/src/editor-runtime.ts @@ -118,6 +118,39 @@ export function syncEditorHeight(editor: any): void { iframe.setAttribute('scrolling', 'no'); } +/** + * 图片 / 视频落地是异步的:插入那一刻高度按 0 计算,资源加载完成后正文才真正 + * 变高。这里在 iframe 内监听资源加载与尺寸变化,加载完成后重新同步一次, + * 否则图片下方的段落会被 iframe 的固定高度裁掉。 + * + * 只在 body 上挂一次(installBlockActions 同一条 init 路径),避免重复监听。 + */ +export function installHeightWatchers(editor: any): void { + const doc = editor?.getDoc?.(); + if (!doc || !doc.body || (doc.body as any).__jioHeightWatched) { + return; + } + (doc.body as any).__jioHeightWatched = true; + + // load 不冒泡,但捕获阶段能收到 img / iframe / video 的加载完成。 + doc.body.addEventListener( + 'load', + (event: Event) => { + const target = event.target as HTMLElement | null; + const tag = target?.tagName; + if (tag === 'IMG' || tag === 'IFRAME' || tag === 'VIDEO' || tag === 'AUDIO') { + syncEditorHeight(editor); + } + }, + true + ); + + // 图片 CSS 生效后的尺寸变化、以及响应式重排,都由 ResizeObserver 兜底。 + if (typeof (window as any).ResizeObserver === 'function') { + new (window as any).ResizeObserver(() => syncEditorHeight(editor)).observe(doc.body); + } +} + /** 块选中状态写在块自身的 class 上,跨 iframe 重排也能保留。 */ const SELECTED_CLASS = 'jio-creator__block-selected'; diff --git a/assets/creator-studio/src/index.tsx b/assets/creator-studio/src/index.tsx index 07dc84e0..56679c64 100644 --- a/assets/creator-studio/src/index.tsx +++ b/assets/creator-studio/src/index.tsx @@ -39,6 +39,7 @@ import { BlockDialog, type BlockDialogKind, type BlockDialogResult } from './com import { type Draft, type Limits, initialDraft } from './draft'; import { installBlockActions, + installHeightWatchers, removeAdjacentBlock, syncEditorHeight, } from './editor-runtime'; @@ -445,6 +446,7 @@ function Studio(): JSX.Element { } syncEditorHeight(editor); installBlockActions(editor); + installHeightWatchers(editor); updateHistoryAvailability(editor); updateFormatValue(editor); updateBlockValue(editor); @@ -453,7 +455,7 @@ function Studio(): JSX.Element { updateFormatValue(editor); updateBlockValue(editor); }); - editor.on('input change Undo Redo AddUndo', () => { + editor.on('input change Undo Redo AddUndo SetContent', () => { updateContent(serializeBlocks(editor.getContent())); updateHistoryAvailability(editor); syncEditorHeight(editor); @@ -492,7 +494,13 @@ function Studio(): JSX.Element { // 快照存的是保存格式(pre > code),编辑器要可编辑的形态。 const content = hydrateBlocks(parsed.content); if (editorReadyRef.current) { - editorRef.current?.setContent(content); + const editor = editorRef.current; + editor?.setContent(content); + // setContent 不在 height 监听的事件列表里:正文一口气变长后 + // iframe 会停在原来的高度,恢复出来的长文只剩开头几段可见。 + if (editor && !editor.removed) { + syncEditorHeight(editor); + } } else { pendingRestoreContentRef.current = content; } @@ -752,6 +760,9 @@ function Studio(): JSX.Element { target.nodeChanged(); updateContent(serializeBlocks(target.getContent())); updateHistoryAvailability(target); + // 图片/视频按加载后的尺寸撑高正文;插入瞬间先同步一次, + // 资源落地后由 installHeightWatchers 的 load 监听再补一次。 + syncEditorHeight(target); } }, onEmbed: (html: string) => { @@ -761,6 +772,7 @@ function Studio(): JSX.Element { target.nodeChanged(); updateContent(serializeBlocks(target.getContent())); updateHistoryAvailability(target); + syncEditorHeight(target); } }, }); diff --git a/tests/widget-system/creator-studio-contract.test.cjs b/tests/widget-system/creator-studio-contract.test.cjs index f57f0b74..23225aab 100644 --- a/tests/widget-system/creator-studio-contract.test.cjs +++ b/tests/widget-system/creator-studio-contract.test.cjs @@ -137,7 +137,7 @@ test('studio app uses TinyMCE while preserving autosave, recovery, and publishin assert.match(appSource, /PublishDrawer/); assert.match(appSource, /wpEditor\.initialize\(EDITOR_ID/); assert.match(appSource, /editor\.getContent\(\)/); - assert.match(appSource, /editorRef\.current\?\.setContent/); + assert.match(appSource, /editor\?\.setContent\(content\)/); assert.doesNotMatch(appSource, /BlockEditorProvider|registerCoreBlocks|BlockList|registerCoreBlocks/); assert.doesNotMatch(creatorRest, /block_content\(/); assert.match(creatorRest, /'post_content'\s*=>\s*\$payload\['content'\]/); @@ -774,3 +774,38 @@ test('draft saves never demote a submitted post or consume the publish quota', ( // REST 侧要把 autosave 传给 persist()。 assert.match(read('src/Core/CreatorRest.php'), /'autosave'\s*=>\s*\$is_auto/); }); + +// 恢复本地草稿会一次性把正文换成很长的一篇:iframe 必须跟着变高, +// 否则超出的段落留在 iframe 内部滚动区,看起来像「只剩第一段」。 +test('restoring a draft resyncs the editor height', () => { + const app = read('assets/creator-studio/src/index.tsx'); + const restoreAt = app.indexOf('const restore = useCallback'); + const restoreBody = app.slice(restoreAt, app.indexOf('const rememberSelection')); + + // setContent 之后必须重新同步高度。 + assert.match(restoreBody, /editor\?\.setContent\(content\)/); + assert.match(restoreBody, /syncEditorHeight\(editor\)/); + // SetContent 事件本身也要参与高度同步与内容同步。 + assert.match(app, /editor\.on\('input change Undo Redo AddUndo SetContent'/); +}); + +// 图片/视频插入后正文会变高,且资源是异步加载的:高度必须跟着重算, +// 否则图片下方的段落会被 iframe 的固定高度裁掉。 +test('media inserts resync the editor height, including after async loading', () => { + const app = read('assets/creator-studio/src/index.tsx'); + const runtime = read('assets/creator-studio/src/editor-runtime.ts'); + + // 两个媒体回调(选择器与嵌入)插入后都要同步高度。 + const mediaAt = app.indexOf('const openMedia = useCallback'); + const mediaBody = app.slice(mediaAt, app.indexOf('return (', mediaAt)); + assert.equal((mediaBody.match(/syncEditorHeight\(target\)/g) || []).length, 2); + + // 图片 / iframe / video 加载完成后补一次同步。 + assert.match(runtime, /export function installHeightWatchers/); + assert.match(runtime, /addEventListener\(\s*'load'/); + assert.match(runtime, /tag === 'IMG' \|\| tag === 'IFRAME' \|\| tag === 'VIDEO' \|\| tag === 'AUDIO'/); + assert.match(runtime, /ResizeObserver/); + // 只在 init 路径挂一次,重复安装要短路。 + assert.match(runtime, /__jioHeightWatched/); + assert.match(app, /installHeightWatchers\(editor\)/); +});