Block Editor: Fix iframe widget field lifecycle and repeater TinyMCE#2308
Closed
Misplon wants to merge 45 commits into
Closed
Block Editor: Fix iframe widget field lifecycle and repeater TinyMCE#2308Misplon wants to merge 45 commits into
Misplon wants to merge 45 commits into
Conversation
…punch, and utils into Block Editor iframe
Closed
8 tasks
- getEligibleTinyMCEFields: filter repeater template fields and _id_ placeholder textareas from any initialization pass - escapeSelectorValue: CSS.escape polyfill for safe data-block selector building - selectBestFormInstance: score candidate forms by connected/visible/ aria-hidden/repeater-template; DOM-order tie-breaker (-index/1000) - getTinyMCEFieldsFromForms: delegate multi-form case to selectBestFormInstance, then prefer visible forms - resolvePostMessageForms: narrow sowbBlockFormInit target using the is-selected + data-block block before falling back to selectBestFormInstance - sanitizeIdSegment: bracket-notation / special-char sanitizer for stable textarea ID generation - resolveWpEditor: single helper that prefers wp.oldEditor over wp.editor; shared by all init and teardown paths - sowbTinyMCEEventNamespace: shared namespace string for all document-level on/off calls
Bug fixes:
- siteEditorAddMediaOverride: null-check window.tinymce and editor
before insertContent/save/fire to avoid crash when tinymce is absent
- hasHealthyTinyMCEEditor: use getElement/getContainer to verify the
editor actually belongs to this field, not just that it exists
- removeStaleTinyMCEFieldState: use resolveWpEditor() instead of
duplicating the oldEditor vs editor resolution inline
- setupTinyMCEField: guard window.top.tinyMCEPreInit before accessing
.mceInit; wrap ed.save()/trigger('change') in if(ed); re-resolve
wpEditor fresh in wpautopToggleField handler and visibility poll to
avoid stale closures; use window.wp.oldEditor directly in the
wp.editor mutation block
Lifecycle hardening:
- clearTinyMCEFieldPendingSetup: also cancel sowb-pre-init-visibility-poll,
sowb-tinymce-init-timeout and clear the initializing lock data keys
- setupTinyMCEField: add sowb-tinymce-initializing re-entrancy lock with
a 5 s safety timeout; generate stable IDs via sanitizeIdSegment +
collision detection instead of Math.random(); deep-clone editorSettings
to prevent mutation; namespace wp-before-tinymce-init and
tinymce-editor-setup listeners per field ID so they don't accumulate
- setupTinyMCEFieldInitializer: add 250 ms pre-init visibility poll that
clears itself when visible; guard repeater template and _id_ fields
- setupSiteEditorTinyMCEFields: accept optional $targetFields so the
postMessage handler can scope init to the affected form's fields
- message handler: resolve target form via resolvePostMessageForms /
getTinyMCEFieldsFromForms; guard with sowbTinyMCEMessageListenerBound
to prevent duplicate listener registration; namespace on/off calls
- Remove setupTinyMCERenderWatchdog (was a no-op poll)
admin.js: - triggerVisibleRepeaterVisibilityFieldSetup: collect all matching fields (visible + hidden) before filtering; fire sowsetupformfield only on visible fields; always trigger sowrepeaterfieldsadded on document with all fields + clientId so iframe listeners can bind pre-init handlers before fields become visible widget-block.js: - initializeFormFieldsInIframe: replace burst of 5 scheduled postMessages with a single immediate send + persistent sowrepeaterfieldsadded listener; listener is namespaced per clientId and filters out events from other blocks via originClientId comparison; cleanup function removes the listener on unmount - iframeFormInitKeyRef fingerprint: sample both ends of widgetFormHtml (length + slice(0,32) + slice(-32)) instead of storing the full blob - sowbGetBlockForm / formSelector: use compound selector .siteorigin-widget-form.siteorigin-widget-form-main to exclude the React wrapper div which only carries siteorigin-widget-form-main - sowbSetupWidgetForm: trigger sowsetupform on the resolved form after initialization so iframe field scripts are notified ```> admin.js: - triggerVisibleRepeaterVisibilityFieldSetup: collect all matching fields (visible + hidden) before filtering; fire sowsetupformfield only on visible fields; always trigger sowrepeaterfieldsadded on document with all fields + clientId so iframe listeners can bind pre-init handlers before fields become visible widget-block.js: - initializeFormFieldsInIframe: replace burst of 5 scheduled postMessages with a single immediate send + persistent sowrepeaterfieldsadded listener; listener is namespaced per clientId and filters out events from other blocks via originClientId comparison; cleanup function removes the listener on unmount - iframeFormInitKeyRef fingerprint: sample both ends of widgetFormHtml (length + slice(0,32) + slice(-32)) instead of storing the full blob - sowbGetBlockForm / formSelector: use compound selector .siteorigin-widget-form.siteorigin-widget-form-main to exclude the React wrapper div which only carries siteorigin-widget-form-main - sowbSetupWidgetForm: trigger sowsetupform on the resolved form after initialization so iframe field scripts are notified
…omments
Fix several issues and add inline comments throughout
tinymce-field.js and widget-block.js to make the non-obvious behaviour
easier to understand for future readers — human or LLM.
tinymce-field.js:
- resolvePostMessageForms: remove dead `is-selected`/`data-block`/
`aria-selected` selectors. These attributes belong to the parent
document's block list and are never present in the iframe DOM where
this function runs; the fallback selectBestFormInstance path was
already sufficient.
- setupTinyMCEField: add comments explaining the two
clearTinyMCEFieldPendingSetup calls — the first tears down a previous
unhealthy init inside the data-initialized branch; the second
unconditionally clears any lingering pre-init state.
- setupTinyMCEFieldInitializer: clear data-initialized when the editor
is unhealthy before falling through to re-init. Previously leaving it
set caused setupTinyMCEField to attempt teardown of an editor that no
longer exists.
- sowsetupform iframe listener: replace filter().add(find()) with a
plain find(). The form wrapper element is never itself a TinyMCE
field, so the filter() was unnecessary and could produce duplicates.
- Drop the $( function() ) jQuery-ready wrapper around the initial
setupSiteEditorTinyMCEFields() call inside the frameElement branch —
the script already runs after the iframe DOM is ready.
widget-block.js:
- initializeFormFieldsInIframe: add a comment that the message object
is intentionally built once from stable closure values and reused
across all sendInitMessage() calls.
- iframeFFix several correctness issues and add LLM-oriented inline comments
throughout tinymce-field.js and widget-block.js to make non-obvious
behaviour easier to understand for future readers — human or LLM.
tinymce-field.js:
- resolvePostMessageForms: remove dead `is-selected`/`data-block`/
`aria-selected` selectors. These attributes belong to the parent
document's block list and are never present in the iframe DOM where
this function runs; the fallback selectBestFormInstance path was
already sufficient.
- resolvePostMessageForms: update JSDoc to match the simplified
implementation — remove the stale clientId param and explain why
clientId-based narrowing is not done inside the iframe.
- escapeSelectorValue: remove entirely. Its only call site was the
is-selected/data-block selector block that was removed above; no
dynamic CSS selectors are built from clientId anywhere in this file.
- setupTinyMCEField: move `$field.attr('data-initialized', true)` to
after the resolveWpEditor() null-check. Previously a transient null
wpEditor caused an early return that left the attr set, forcing the
next call into an unnecessary unhealthy-editor teardown cycle.
- setupTinyMCEField: add comments explaining the two
clearTinyMCEFieldPendingSetup calls — the first tears down a previous
unhealthy init inside the data-initialized branch; the second
unconditionally clears any lingering pre-init state.
- setupTinyMCEField: namespace the media button click handler with
`.sowbMedia` so that reinit does not accumulate duplicate listeners on
the same button element.
- setupTinyMCEField: namespace the $wpautopToggleField change handler
with fieldEventNamespace. The previous bare .off('change') silently
removed all change listeners on that checkbox, not just ours.
- setupTinyMCEField: in the 5 s safety timeout, also remove the
`tinymce-editor-setup` listener from window.top.document. Without
this, fields whose TinyMCE init event never fires (e.g. removed from
DOM) permanently leak a listener on the top-level document.
- setupTinyMCEFieldInitializer: clear data-initialized when the editor
is unhealthy before falling through to re-init. Previously leaving it
set caused setupTinyMCEField to attempt teardown of an editor that no
longer exists.
- sowsetupform iframe listener: replace filter().add(find()) with a
plain find(). The form wrapper element is never itself a TinyMCE
field, so the filter() was unnecessary and could produce duplicates.
- Drop the $( function() ) jQuery-ready wrapper around the initial
setupSiteEditorTinyMCEFields() call inside the frameElement branch —
the script already runs after the iframe DOM is ready.
widget-block.js:
- initializeFormFieldsInIframe: add a comment that the message object
is intentionally built once from stable closure values and reused
across all sendInitMessage() calls.
- initializeFormFieldsInIframe: add a comment explaining that $fields
passed with sowrepeaterfieldsadded is intentionally unused — admin.js
provides it for other listeners, but here a full-form postMessage is
sent rather than targeting individual fields.
- iframeFormInitKeyRef effect: add a comment explaining that the ref is
deliberately not updated when initializeFormFieldsInIframe returns
null (iframe not yet reachable), so the effect retries on the next
render cycle rather than skipping it permanently.
The inline comments are written to give sufficient context for LLMs
making future changes to understand why the code is structured the way
it is, reducing the risk of well-intentioned but incorrect simplification.ormInitKeyRef effect: add a comment explaining that the ref is
deliberately not updated when initializeFormFieldsInIframe returns
null (iframe not yet reachable), so the effect retries on the next
render cycle rather than skipping it permanently.
The inline comments are written to give sufficient context for LLMs
making future changes to understand why the code is structured the way
it is, reducing the risk of well-intentioned but incorrect simplification.
…cross-frame event routing widget-block.js Remove the sowsetupform jQuery trigger from sowbSetupWidgetForm. It fired on the parent document but the listener is bound to the iframe's document, making it a cross-window no-op. TinyMCE init in the iframe is already handled via the sowbBlockFormInit postMessage pathway. tinymce-field.js Pre-init listener/poll race fix: Introduce a per-field preInitEventNamespace (using a new dedicated _tinymcePreInitSeq counter) and store it as sowb-pre-init-namespace jQuery data on the field. The visibility poll now cancels the sowsetupformfield listener when it fires first, and vice-versa, preventing both paths from racing into setupTinyMCEField on the same field. Pre-init listener leak fix: clearTinyMCEFieldPendingSetup now reads sowb-pre-init-namespace back from the field and calls $field.off() with the stored namespace, so the listener is always cancelled even when setupTinyMCEField is called directly (e.g. from sortStopEvent) rather than through the pre-init path. Split _tinymceIdSeq: The single counter is replaced by _tinymceDomIdSeq (TinyMCE textarea DOM IDs) and _tinymcePreInitSeq (pre-init event namespaces), keeping the two concerns independent. sowbBlockFormInit message handler: Replace the window.sowbTinyMCEMessageListenerBound boolean guard with a stored handler reference on window._sowbTinyMCEMessageHandler, allowing the previous handler to be removed via removeEventListener before registering a fresh one (correct for HMR/re-evaluation; the boolean approach accumulated duplicate handlers). sowsetupform iframe listener: Bail early when $form is absent or empty instead of passing null to setupSiteEditorTinyMCEFields, preventing spurious re-initialization of all fields in the iframe on stale events. selectBestFormInstance connected signal: Replace document.documentElement.contains(this) with this.ownerDocument.documentElement.contains(this) so the signal is accurate in both the parent-window call path (where this is an iframe-DOM element and the closure document is the wrong document) and the iframe call path. setupTinyMCEFieldInitializer undefined ID guard: When getTinyMCEFieldEditor cannot resolve a textarea ID, treat data-initialized as stale immediately rather than forwarding undefined to tinymce.get(). admin.js sowrepeaterfieldsadded payload hygiene: Filter repeater item template rows (.siteorigin-widget-field-repeater-item-html descendants) out of $allFields before dispatching the event, so no consumer ever receives placeholder _id_ rows. Fix missing semicolon after the triggerVisibleRepeaterVisibilityFieldSetup arrow function.
Member
Adds a module-level _tinymceInitPending map that tracks editors whose TinyMCE init event has not yet fired. An entry is created when sowb-tinymce-initializing is set and removed (promise resolved) as soon as the init event fires, or after the existing 5 s safety timeout — whichever comes first. The map therefore never grows unboundedly and adds no overhead to already-initialised editors. Exposes window.sowbGetTinyMCEInitPromise(editorId) outside the IIFE. Returns a Promise<void> that resolves immediately for a ready editor, or waits for init completion for one that is still in-flight. The function is retrieved via the field element's ownerDocument.defaultView so that the correct per-frame map is consulted in Site Editor iframe contexts. This is the producer side of the TinyMCE flush-race fix. The consumer side (making the save-bridge TinyMCE flusher await this promise before calling editor.save()) lives in feature/wb-direct-block-save-bridge and requires this commit to be present before it is effective.
The _tinymceInitPending[id] entry was constructed using a shared let _resolveInitPending variable in the enclosing function scope. The Promise constructor assigned to it asynchronously, and the .resolve() method on the entry called the variable rather than the captured value. If two editor instances began initialising concurrently, the second assignment would overwrite _resolveInitPending before the first entry's .resolve() had a chance to close over its own value — causing the first editor's pending promise to be resolved (and deleted) by the second editor's resolver instead. To fix this, the shared-variable pattern was replaced with an IIFE that creates a dedicated storedResolve binding per entry. Each entry now closes over its own resolver; concurrent initialisations can no longer interfere.
…ug fixes
Widget forms rendered inside the Site Editor's editor-canvas iframe were
not correctly initialized due to several compounding issues.
compat/block-editor/widget-block.js:
- Wrong jQuery instance in sowbSetupWidgetForm. The form node returned by
sowbGetBlockForm was wrapped in the parent-document jQuery, but
sowSetupForm(), wpColorPicker, and all field plugins register against the
iframe's jQuery. Fixed by re-wrapping the form node via sowbGetElementWindow
and using the resulting formJQuery/formForms instances throughout.
- Wrong form node selected by sowbGetBlockForm. The PHP form() method renders
its own .siteorigin-widget-form-main[data-class] element inside the React
outer wrapper (also .siteorigin-widget-form-main). The outer wrapper has no
direct .siteorigin-widget-field children, so sowSetupForm found 0 fields —
breaking sections, repeaters, color pickers, and all other field setup.
Fixed by narrowing the selector to [data-class], with .first() for safety.
- Duplicate change handlers. Multiple React render cycles could call
sowbSetupWidgetForm for the same block, stacking unbounded .on('change')
listeners. Fixed by namespacing with clientId and calling .off() first.
- Added sowbGetEditorCanvasFrame() helper to centralise iframe resolution
(Site Editor canvas, post-editor canvas, window.frameElement).
- Removed window.frameElement guard from the IIFE's sowbMaybeSetupSiteEditorAssets
call — the function resolves the correct frame internally.
base/inc/fields/js/tinymce-field.js:
- _tinymceInitPending entry stranded on cleanup. clearTinyMCEFieldPendingSetup
cancelled the 5 s safety timeout — the only fallback resolver — without
resolving the associated promise, leaving sowbGetTinyMCEInitPromise() waiting
indefinitely. Fixed by resolving and deleting the pending entry before wiping
sowb-tinymce-initializing-id.
- data-initialized cleared during an in-flight init. setupTinyMCEFieldInitializer
checked hasHealthyTinyMCEEditor (returns false while TinyMCE is still
initializing), cleared data-initialized, then returned immediately because
sowb-tinymce-initializing was set. When the original init completed the field
was left unmarked, causing the next setup event to tear down a healthy editor.
Fixed by returning early when sowb-tinymce-initializing is already set.
- sowbBlockFormInit postMessage handler simplified. The sowSetupForm call
previously added to the handler is removed; sowbSetupWidgetForm already runs it
with the correct jQuery instance before the postMessage is ever sent. The handler
now covers only TinyMCE field initialization.
…acy-handler Block Editor: fix direct widget preview and save persistence
…rame-asset-clone-no-polling Feature/sowb block editor iframe asset clone no polling
Member
|
Manually merged into #2313 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes legacy SiteOrigin widget forms inside WordPress iframe editors by restoring the iframe asset/bootstrap path and hardening field initialization after the form DOM and cloned scripts mount asynchronously.
What Changed
iframe[name="editor-canvas"]) as well as the Site Editor canvas when resolving widget forms.sowbBlockFormInitmessages after form mount.sowsetupformfieldtriggers for visible repeater fields.data-tinymce-idattributes so IDs survive cloning/replacement.jQueryandPikadaywhen needed.Why
Iframe editors can mount widget form HTML and cloned admin scripts out of order. That left some field scripts present in the iframe but not actually initialized, and TinyMCE repeater editors after the first item could render as bare textareas because copied/replaced editors only kept transient jQuery data for their IDs.
This PR covers both layers of the fix: the original iframe asset delivery problem and the follow-up repeater field lifecycle problem from #2307.
Validation
node --check base/inc/fields/js/date-range-field.jsnode --check compat/block-editor/widget-block.jsnode --check tests/e2e/wb-form-field-site-editor.test.jsnpx playwright test tests/e2e/wb-form-field-site-editor.test.js -g "Test the Blog widget" --project="Google Chrome" --retries=0npm run test:e2e(9 passed)Reviewer Checklist
widgets.phpBlock Widgets screen: repeater and field initialization behavior regresses neither iframe nor non-iframe screens.LoadError,window.error,unhandledrejection, orsowbBlockFormInitmessage storm during normal add/open/copy flows.Notes
develop.--mount-dir-before-install;npm run testsstill calls the removed camelCase Playground CLI flag--mountDirBeforeInstallthroughsiteorigin-tests-common/playground/startPlayground.js.