PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.8
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.8
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / assets / js / tools / editor / shared / editor-shared-utils.js

editor-shared-utils.js in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.8, at assets/js/tools/editor/shared/editor-shared-utils.js

560 lines 26.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * ZipWP MCP — Vibe Editing v2: shared editor utilities.
3 *
4 * The ONE source of truth for helpers the editor handlers (get-context,
5 * apply-change, get/set-scripts, get/set-styles) need identically — so they can
6 * never drift:
7 * - currentPostId: the live post id from the editor store, with the same
8 * null-guard. apply-change uses it for the two-tab post_id guard;
9 * get-context returns it in the response.
10 * - editorSelect / editorDispatch: the null-guarded `core/editor` store
11 * select/dispatch accessors. styles read the editing session through these
12 * (getEditedPostAttribute / editPost).
13 * - currentMeta: the live post meta object from the session (or {}), the
14 * read side of the session-scoped styles write.
15 * - blockEditorSelect / blockEditorDispatch / rootClientId: the null-guarded
16 * `core/block-editor` store accessors + the page root container's clientId.
17 * get/set-scripts read/patch a block's `spectraCustomJS` attribute through
18 * these (getBlockAttributes / updateBlockAttributes).
19 * - bannedVisualAttrs / isBannedVisualAttr: the 8 GBS-banned per-block visual
20 * styling props (style, *Color(Hover), boxShadow(Hover), styleAttributes) —
21 * styling lives in `className` ONLY (the Spectra GBS JIT grammar). apply-change
22 * strips them from an incoming attrs write; get-context omits them from the
23 * authorable attr-keys it advertises. ONE list so the two can never drift
24 * (mirrors Laravel's StrictAttrValidator). The 8 props are a sanctioned
25 * constant, not an allowlist — every OTHER attr is decided by the registry.
26 * - isJsCapable: is `spectraCustomJS` actually a registered attribute on this
27 * block type? spectra-blocks-pro only registers it on spectra/spectra-pro
28 * blocks + core/image + core/heading (global-styles/helpers.js
29 * SUPPORTED_BLOCKS) — writing it anywhere else paints the session but is
30 * dropped on Save. Read LIVE off `wp.blocks.getBlockType`, not a mirrored
31 * name list, so it can't drift from whatever the plugin actually registered.
32 *
33 * Dual-mode: attaches to window.zipwpEditorShared in the browser; CommonJS
34 * export for jest.
35 *
36 * Load order: enqueued before the editor handlers in source mode
37 * (react-manager.php) and concatenated ahead of handler.js in the production
38 * grunt bundle (matches the `tools/**\/*-utils.js` glob). The handlers also
39 * resolve it lazily at call time, so a missing global degrades consistently
40 * (post id -> null = the two-tab guard no-ops, exactly as a null
41 * getCurrentPostId() would).
42 *
43 * @package
44 */
45 ( function () {
46 'use strict';
47
48 function editorSelect() {
49 return ( typeof window !== 'undefined' && window.wp && window.wp.data && window.wp.data.select )
50 ? window.wp.data.select( 'core/editor' )
51 : null;
52 }
53
54 function editorDispatch() {
55 return ( typeof window !== 'undefined' && window.wp && window.wp.data && window.wp.data.dispatch )
56 ? window.wp.data.dispatch( 'core/editor' )
57 : null;
58 }
59
60 function currentPostId() {
61 const editor = editorSelect();
62 return editor && typeof editor.getCurrentPostId === 'function'
63 ? editor.getCurrentPostId()
64 : null;
65 }
66
67 // The live post meta object from the editing session (or {} when absent) —
68 // the read side of the session-scoped scripts/styles write.
69 function currentMeta( sel ) {
70 const meta = sel && sel.getEditedPostAttribute ? sel.getEditedPostAttribute( 'meta' ) : null;
71 return meta && typeof meta === 'object' ? meta : {};
72 }
73
74 // The null-guarded `core/block-editor` store — the read/write side of a
75 // block's attributes (get/set-scripts operate on `spectraCustomJS`, an attr).
76 // Edits through it are inherently session-scoped: in-memory now, persisted on
77 // Save, discarded with the session (same model apply-change uses).
78 function blockEditorSelect() {
79 return ( typeof window !== 'undefined' && window.wp && window.wp.data && window.wp.data.select )
80 ? window.wp.data.select( 'core/block-editor' )
81 : null;
82 }
83 function blockEditorDispatch() {
84 return ( typeof window !== 'undefined' && window.wp && window.wp.data && window.wp.data.dispatch )
85 ? window.wp.data.dispatch( 'core/block-editor' )
86 : null;
87 }
88
89 // The page root container's clientId — the first top-level block — the
90 // default owner for page-wide JS when no clientId is passed. null when the
91 // tree is empty or the store is absent.
92 function rootClientId( sel ) {
93 const order = sel && sel.getBlockOrder ? sel.getBlockOrder() : null;
94 return Array.isArray( order ) && order.length > 0 ? order[ 0 ] : null;
95 }
96
97 // The 13 GBS-banned per-block visual styling props. Per-block styling lives in
98 // `className` ONLY (the Spectra GBS JIT grammar); these are never authorable
99 // attrs even when a block's registry declares them. A sanctioned constant —
100 // every OTHER attribute's validity is decided by the registry (getBlockType).
101 //
102 // HARDCODED MIRROR of config/spectra-contract.json `banned_attrs.keys` in the
103 // api-scs-credits-system repo (the SSOT the brain zod + Laravel validator read).
104 // Kept in lockstep by hand for now; a generated artifact + cross-repo CI pin is
105 // the sanctioned fix (drift here is otherwise silent — apply_change attrs bypass
106 // Laravel's validator, so this client strip is the editor's only write-point guard
107 // besides the brain denylist). Per the contract's `notes.update_ops`, the full
108 // 13-key ban applies name-agnostically on the clientId-targeted apply_change path
109 // (no block name at validation time); insert/replace BlockSpecs carry a `name` and
110 // are scope-aware (srfm/* exempt) — that scope-awareness is a separate follow-up.
111 const bannedVisualAttrs = [
112 'style', 'styleAttributes',
113 'backgroundColor', 'backgroundColorHover',
114 'boxShadow', 'boxShadowHover',
115 'textColor', 'textColorHover',
116 'numberColor', 'numberColorHover',
117 'borderHover', 'iconColorHover',
118 'backgroundGradientHover',
119 ];
120 function isBannedVisualAttr( key ) {
121 return bannedVisualAttrs.indexOf( key ) !== -1;
122 }
123
124 // True when this block type actually persists `spectraCustomJS` (the
125 // registry, not a mirrored name list — see the file header).
126 function isJsCapable( blockName ) {
127 const blocks = ( typeof window !== 'undefined' && window.wp && window.wp.blocks ) ? window.wp.blocks : null;
128 if ( ! blocks || typeof blocks.getBlockType !== 'function' || typeof blockName !== 'string' ) {
129 return false;
130 }
131 const blockType = blocks.getBlockType( blockName );
132 return !! ( blockType && blockType.attributes &&
133 Object.prototype.hasOwnProperty.call( blockType.attributes, 'spectraCustomJS' ) );
134 }
135
136 // ── Dependents (reverse edges) ──────────────────────────────────────────
137 // The editor resolves OWNERSHIP (which layer owns a property) but never
138 // DEPENDENTS ("who else relies on the resource I'm about to change"). These
139 // pure helpers compute a mutating write's blast radius from the LIVE tree —
140 // no persistent index — so a destructive/shared write can surface its impact
141 // as a structured signal (act-first: the model informs the user, it doesn't
142 // silently break things). SSOT here so every handler reads dependents identically.
143
144 // Every id a block's JS references — `getElementById('x')` or a `#x` inside a
145 // querySelector. Shared by set-scripts' WRITE-time dead-#id gate AND
146 // apply-change's DELETE-time orphan-JS surface, so the two never drift on how
147 // JS is parsed.
148 function referencedIds( code ) {
149 const ids = [];
150 if ( typeof code !== 'string' || code === '' ) {
151 return ids;
152 }
153 let m;
154 const reGid = /getElementById\(\s*['"]([A-Za-z][\w-]*)['"]\s*\)/g;
155 while ( ( m = reGid.exec( code ) ) !== null ) {
156 ids.push( m[ 1 ] );
157 }
158 const reQs = /querySelector(?:All)?\(\s*(['"])([^'"]*)\1/g;
159 while ( ( m = reQs.exec( code ) ) !== null ) {
160 // eslint-disable-next-line no-var
161 var idm,
162 reId = /#([A-Za-z][\w-]*)/g;
163 while ( ( idm = reId.exec( m[ 2 ] ) ) !== null ) {
164 ids.push( idm[ 1 ] );
165 }
166 }
167 return ids;
168 }
169
170 // Every clientId in the live tree (top-level + descendants).
171 function allClientIds( sel ) {
172 return ( sel && sel.getClientIdsWithDescendants ) ? sel.getClientIdsWithDescendants() : [];
173 }
174
175 // Blocks (excluding `excludeIds`) whose `className` carries `gsToken` — the
176 // reverse `class -> blocks` edge. Answers "editing this gs- class body also
177 // restyles N other sections" BEFORE the write.
178 function classDependents( sel, gsToken, excludeIds ) {
179 const out = [];
180 if ( ! sel || ! gsToken || ! sel.getBlockAttributes ) {
181 return out;
182 }
183 const skip = Object.create( null );
184 ( excludeIds || [] ).forEach( function ( id ) {
185 skip[ id ] = true;
186 } );
187 allClientIds( sel ).forEach( function ( cid ) {
188 if ( skip[ cid ] ) {
189 return;
190 }
191 const a = sel.getBlockAttributes( cid );
192 const cn = ( a && typeof a.className === 'string' ) ? a.className : '';
193 if ( cn.split( /\s+/ ).indexOf( gsToken ) !== -1 ) {
194 out.push( { clientId: cid, blockName: sel.getBlockName ? sel.getBlockName( cid ) : null } );
195 }
196 } );
197 return out;
198 }
199
200 // Blocks (excluding `excludeIds`) whose `spectraCustomJS` references any of
201 // `anchors` (by `#id` / getElementById) — the reverse `anchor -> scripts`
202 // edge. Answers "deleting this block orphans the JS on N others" BEFORE the
203 // delete (their getElementById(...) would return null and THROW, killing the
204 // whole per-block IIFE — not a clean no-op).
205 function anchorDependents( sel, anchors, excludeIds ) {
206 const out = [];
207 if ( ! sel || ! anchors || ! anchors.length || ! sel.getBlockAttributes ) {
208 return out;
209 }
210 const want = Object.create( null );
211 anchors.forEach( function ( x ) {
212 if ( x ) {
213 want[ x ] = true;
214 }
215 } );
216 const skip = Object.create( null );
217 ( excludeIds || [] ).forEach( function ( id ) {
218 skip[ id ] = true;
219 } );
220 allClientIds( sel ).forEach( function ( cid ) {
221 if ( skip[ cid ] ) {
222 return;
223 }
224 const a = sel.getBlockAttributes( cid );
225 const js = ( a && typeof a.spectraCustomJS === 'string' ) ? a.spectraCustomJS : '';
226 if ( ! js ) {
227 return;
228 }
229 const hits = referencedIds( js ).filter( function ( id ) {
230 return want[ id ];
231 } );
232 if ( hits.length ) {
233 out.push( { clientId: cid, blockName: sel.getBlockName ? sel.getBlockName( cid ) : null, refs: hits } );
234 }
235 } );
236 return out;
237 }
238
239 // ── GBS page-store persist (SSOT) ──────────────────────────────────────────
240 // ONE read→merge→write→render→inject path over the per-page GBS store, shared
241 // by editor/set-styles (the styling tool) AND editor/apply-change (which uses
242 // it to persist a generated section's semantic-token class bodies — the JIT
243 // cannot synthesize gs-* class bodies, so without a store write the section
244 // renders unstyled). `apiFetch` is injected by the caller (its own wp.apiFetch
245 // wrapper); NS is the GBS route root.
246 const GBS_NS = '/spectra-blocks/v1/global-styles';
247
248 // Deep-merge one incoming schema-v1 payload onto an existing one, bucket by
249 // bucket (null value = delete). Never full-replaces — importer chrome / other
250 // sections' classes in untouched buckets survive.
251 function mergePayload( existing, incoming ) {
252 const out = Object.assign( {}, existing || {} );
253 out.v = '1';
254 Object.keys( incoming || {} ).forEach( function ( bucket ) {
255 if ( bucket === 'v' ) {
256 return;
257 }
258 const inc = incoming[ bucket ];
259 if ( inc === null ) {
260 delete out[ bucket ];
261 return;
262 }
263 if ( Array.isArray( inc ) ) {
264 out[ bucket ] = inc.slice();
265 return;
266 }
267 if ( typeof inc !== 'object' ) {
268 return;
269 }
270 const base = ( out[ bucket ] && typeof out[ bucket ] === 'object' && ! Array.isArray( out[ bucket ] ) )
271 ? Object.assign( {}, out[ bucket ] )
272 : {};
273 Object.keys( inc ).forEach( function ( key ) {
274 if ( inc[ key ] === null ) {
275 delete base[ key ];
276 } else {
277 base[ key ] = inc[ key ];
278 }
279 } );
280 out[ bucket ] = base;
281 } );
282 return out;
283 }
284
285 // The block-editor canvas runs in an iframe; styles must be injected THERE.
286 function canvasDoc() {
287 const ifr = document.querySelector( 'iframe[name="editor-canvas"]' );
288 return ifr && ifr.contentDocument ? ifr.contentDocument : document;
289 }
290 function injectCss( elementId, css ) {
291 const doc = canvasDoc();
292 let el = doc.getElementById( elementId );
293 if ( ! el ) {
294 el = doc.createElement( 'style' );
295 el.id = elementId;
296 ( doc.head || doc.documentElement ).appendChild( el );
297 }
298 el.textContent = css || '';
299 }
300
301 // Tag a REST failure with which of the three GBS steps it came from so a
302 // caller can label the outcome granularly. `step` is 'read' | 'write' |
303 // 'render' — a render failure means the data WAS saved, only the live paint
304 // failed (recoverable by reload), which is a different remediation than a
305 // read/write failure where nothing persisted. The message is preserved.
306 function taggedGbsError( step, e ) {
307 const err = e instanceof Error ? e : new Error( String( e && e.message ? e.message : e ) );
308 err.gbsStep = step;
309 return err;
310 }
311
312 // Read-modify-write the per-page GBS store through the SSOT /save route (the
313 // same endpoint the importer uses) then render + inject the merged CSS so it
314 // paints live. Returns the merged payload; throws on any REST failure (tagged
315 // with `gbsStep`) so the caller can decide whether to surface or swallow it
316 // and, if surfacing, which step failed.
317 async function persistPageGbs( apiFetch, incoming, postId ) {
318 let existing;
319 try {
320 existing = await apiFetch( { path: GBS_NS + '/save?scope=page&post_id=' + postId } )
321 .then( function ( g ) {
322 return ( g && g.payload && typeof g.payload === 'object' ) ? g.payload : {};
323 } );
324 } catch ( e ) {
325 throw taggedGbsError( 'read', e );
326 }
327 const merged = mergePayload( existing, incoming );
328 try {
329 await apiFetch( {
330 path: GBS_NS + '/save',
331 method: 'POST',
332 data: { scope: 'page', post_id: postId, payload: merged, replace: true },
333 } );
334 } catch ( e ) {
335 throw taggedGbsError( 'write', e );
336 }
337 try {
338 const r = await apiFetch( {
339 path: GBS_NS + '/render',
340 method: 'POST',
341 data: { payload: merged, post_id: postId, scope: 'page' },
342 } );
343 injectCss( 'spectra-gen-custom-css-' + postId + '-inline-css', r && r.css );
344 } catch ( e ) {
345 throw taggedGbsError( 'render', e );
346 }
347 return merged;
348 }
349
350 // ── Deferred section-GBS persistence (persist on SAVE, never before) ─────────
351 // A generate_section insert carries custom `gs-` class BODIES — semantic-token
352 // CSS (var(--primary)/…) the JIT can't compile as utilities, so it can't ride
353 // the block className the way per-block styling now does. We must NOT write it
354 // to the DB immediately: that persists before the user Saves, breaking the
355 // "nothing hits the DB until Save" contract (and orphaning CSS if they discard).
356 // Instead: RENDER + inject it for a live PREVIEW now (a pure compile — NO /save,
357 // no DB write), ACCUMULATE the payload, and FLUSH it to the GBS store only when
358 // the editor completes a real Save. Never Saved → never persisted.
359
360 // Preview-only: compile the payload via the SSOT renderer and inject it into the
361 // canvas. NO /save — this never touches the DB. (The saved-meta render replaces
362 // this element on the next reload.)
363 async function previewSectionGbs( apiFetch, incoming, postId ) {
364 const r = await apiFetch( {
365 path: GBS_NS + '/render',
366 method: 'POST',
367 data: { payload: incoming, post_id: postId, scope: 'page' },
368 } );
369 injectCss( 'zipwp-gbs-pending-section-' + postId, r && r.css );
370 }
371
372 // Remove the pending-section preview <style> for a post — after Save flushes,
373 // the persisted-meta render (spectra-gen-custom-css-<postId>) paints the
374 // section, so the preview element is stale duplicate CSS.
375 function removePendingSectionStyle( postId ) {
376 const el = canvasDoc().getElementById( 'zipwp-gbs-pending-section-' + postId );
377 if ( el && el.parentNode ) {
378 el.parentNode.removeChild( el );
379 }
380 }
381
382 // Build a deferred saver. Deps are injectable (apiFetch, editorStore, subscribe,
383 // persist, preview, merge) so the accumulate → flush-on-save logic is testable
384 // without a live wp.data. `queue(incoming, postId)` merges the payload into the
385 // pending set (per post), paints the preview, and arms a ONE-TIME subscription
386 // that flushes every pending payload through the real persist the first time a
387 // NON-autosave Save completes successfully.
388 function createSectionGbsSaver( deps ) {
389 const apiFetch = deps.apiFetch;
390 const editorStore = deps.editorStore; // () => core/editor select, or the select object
391 const subscribe = deps.subscribe; // (cb) => unsubscribe
392 const persist = deps.persist || persistPageGbs;
393 const preview = deps.preview || previewSectionGbs;
394 const removePreview = deps.removePreview || removePendingSectionStyle;
395 const merge = deps.merge || mergePayload;
396 const pending = {}; // postId -> merged payload
397 let armed = false;
398 let wasSaving = false;
399
400 function resolveStore() {
401 return typeof editorStore === 'function' ? editorStore() : editorStore;
402 }
403 function flush() {
404 Object.keys( pending ).forEach( function ( postId ) {
405 const payload = pending[ postId ];
406 // Optimistically clear so a second Save with nothing new is a no-op…
407 delete pending[ postId ];
408 Promise.resolve( persist( apiFetch, payload, Number( postId ) ) ).then( function () {
409 // …persisted: the saved-meta render now owns the paint, so drop
410 // the stale preview <style> — but ONLY if nothing was re-queued
411 // for this post while the persist was in flight. A queue() during
412 // the flush re-creates the shared per-post preview element for a
413 // section that hasn't persisted yet; removing it here would leave
414 // that section unstyled until the next Save.
415 if ( ! pending[ postId ] ) {
416 removePreview( Number( postId ) );
417 }
418 } ).catch( function ( e ) {
419 // A 'render'-tagged rejection means the /save DB write SUCCEEDED
420 // and only the live paint failed (recoverable by reload). The
421 // payload IS persisted, so treat it like success: drop the stale
422 // preview and do NOT re-queue (re-queuing would rewrite an
423 // already-saved body and, via the merge below, could clobber a
424 // newer queued edit).
425 if ( e && e.gbsStep === 'render' ) {
426 if ( ! pending[ postId ] ) {
427 removePreview( Number( postId ) );
428 }
429 return;
430 }
431 // A genuine not-persisted failure (read/write): re-queue so the
432 // NEXT Save retries — a transient REST error must not permanently
433 // lose the section's styling. `payload` is the OLDER failed batch,
434 // so merge it UNDER anything queued since (2nd arg wins per key)
435 // to keep a newer regenerate from being clobbered by the stale body.
436 pending[ postId ] = merge( payload, pending[ postId ] || {} );
437 // eslint-disable-next-line no-console -- developer signal; retried on the next Save
438 console.warn( '[ZIP AI] deferred section-styles flush failed on Save (will retry next Save)', e );
439 } );
440 } );
441 }
442 function onStoreChange() {
443 const sel = resolveStore();
444 if ( ! sel || typeof sel.isSavingPost !== 'function' ) {
445 return;
446 }
447 // A real (non-autosave) save in flight.
448 const saving = sel.isSavingPost() &&
449 ! ( typeof sel.isAutosavingPost === 'function' && sel.isAutosavingPost() );
450 // Transition saving → finished: flush IF the save succeeded (degrade-open
451 // when the selector is absent — a completed save with no failure signal).
452 if ( wasSaving && ! saving ) {
453 const succeeded = typeof sel.didPostSaveRequestSucceed === 'function'
454 ? sel.didPostSaveRequestSucceed()
455 : true;
456 if ( succeeded ) {
457 flush();
458 }
459 }
460 wasSaving = saving;
461 }
462 function arm() {
463 if ( armed ) {
464 return;
465 }
466 armed = true;
467 if ( typeof subscribe === 'function' ) {
468 subscribe( onStoreChange );
469 }
470 }
471 return {
472 queue ( incoming, postId ) {
473 if ( ! incoming || ! postId ) {
474 return Promise.resolve();
475 }
476 pending[ postId ] = merge( pending[ postId ] || {}, incoming );
477 arm();
478 // Preview the MERGED payload (not just this section) so a second
479 // generate_section doesn't overwrite the single per-post preview
480 // <style> with only its own CSS — earlier sections would go unstyled
481 // until Save otherwise.
482 return Promise.resolve( preview( apiFetch, pending[ postId ], postId ) ).catch( function () {} );
483 },
484 // Test seams — the pending set + the store-change handler.
485 _pending: pending,
486 _onStoreChange: onStoreChange,
487 };
488 }
489
490 // Lazily-built browser singleton (ONE subscription per editor session), wired to
491 // the live wp.data core/editor store. `queueSectionGbsForSave` is what the
492 // apply-change section path calls.
493 let _sectionGbsSaver = null;
494 function sectionGbsSaver( apiFetch ) {
495 if ( ! _sectionGbsSaver ) {
496 _sectionGbsSaver = createSectionGbsSaver( {
497 apiFetch,
498 editorStore: editorSelect,
499 subscribe ( cb ) {
500 return ( typeof window !== 'undefined' && window.wp && window.wp.data && window.wp.data.subscribe )
501 ? window.wp.data.subscribe( cb )
502 : function () {};
503 },
504 } );
505 }
506 return _sectionGbsSaver;
507 }
508 function queueSectionGbsForSave( apiFetch, incoming, postId ) {
509 return sectionGbsSaver( apiFetch ).queue( incoming, postId );
510 }
511
512 if ( typeof window !== 'undefined' ) {
513 window.zipwpEditorShared = window.zipwpEditorShared || {};
514 window.zipwpEditorShared.mergePayload = mergePayload;
515 window.zipwpEditorShared.canvasDoc = canvasDoc;
516 window.zipwpEditorShared.injectCss = injectCss;
517 window.zipwpEditorShared.persistPageGbs = persistPageGbs;
518 window.zipwpEditorShared.previewSectionGbs = previewSectionGbs;
519 window.zipwpEditorShared.createSectionGbsSaver = createSectionGbsSaver;
520 window.zipwpEditorShared.queueSectionGbsForSave = queueSectionGbsForSave;
521 window.zipwpEditorShared.currentPostId = currentPostId;
522 window.zipwpEditorShared.editorSelect = editorSelect;
523 window.zipwpEditorShared.editorDispatch = editorDispatch;
524 window.zipwpEditorShared.currentMeta = currentMeta;
525 window.zipwpEditorShared.blockEditorSelect = blockEditorSelect;
526 window.zipwpEditorShared.blockEditorDispatch = blockEditorDispatch;
527 window.zipwpEditorShared.rootClientId = rootClientId;
528 window.zipwpEditorShared.bannedVisualAttrs = bannedVisualAttrs;
529 window.zipwpEditorShared.isBannedVisualAttr = isBannedVisualAttr;
530 window.zipwpEditorShared.isJsCapable = isJsCapable;
531 window.zipwpEditorShared.referencedIds = referencedIds;
532 window.zipwpEditorShared.classDependents = classDependents;
533 window.zipwpEditorShared.anchorDependents = anchorDependents;
534 }
535 if ( typeof module !== 'undefined' && module.exports ) {
536 module.exports = {
537 mergePayload,
538 canvasDoc,
539 injectCss,
540 persistPageGbs,
541 previewSectionGbs,
542 createSectionGbsSaver,
543 queueSectionGbsForSave,
544 currentPostId,
545 editorSelect,
546 editorDispatch,
547 currentMeta,
548 blockEditorSelect,
549 blockEditorDispatch,
550 isJsCapable,
551 referencedIds,
552 classDependents,
553 anchorDependents,
554 rootClientId,
555 bannedVisualAttrs,
556 isBannedVisualAttr,
557 };
558 }
559 }() );
560