PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.7
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.7
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 / scripts / handler.js

handler.js in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.7, at assets/js/tools/editor/scripts/handler.js

143 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * editor/get-scripts + editor/set-scripts — typed read/patch over a BLOCK's
3 * `spectraCustomJS` attribute (the per-block JS store). `spectraCustomJS` in
4 * post_content is the single source of truth for page behaviour — Spectra Pro's
5 * BlockJsCompiler renders it once at wp_footer (spectra-blocks-pro #165),
6 * superseding the removed per-page `_spectra_blocks_page_scripts` meta.
7 *
8 * SESSION-SCOPED BY DESIGN (the apply_change philosophy): reads come from
9 * `core/block-editor` getBlockAttributes (the live editing session, incl.
10 * unsaved edits) and writes go through updateBlockAttributes — the block's JS
11 * updates in the session immediately, is DISCARDED if the user discards the
12 * session, and persists only on Save. A REST write here would race the open
13 * editor's copy and mutate before save — never do that.
14 *
15 * Target a block by `clientId` (default = the page root container, the first
16 * top-level block — page-wide behaviour). The stored code is raw JS; the
17 * renderer wraps it in an IIFE and resolves the `_current_block_` token to the
18 * block's scope class.
19 */
20 (function () {
21 // Block-editor store access comes from the ONE shared source
22 // (editor/shared/editor-shared-utils.js): window in the browser, require()
23 // under jest — so the editor handlers can't drift.
24 function sharedEditorUtils() {
25 if (typeof window !== 'undefined' && window.zipwpEditorShared) return window.zipwpEditorShared;
26 if (typeof require === 'function') {
27 try { return require('../shared/editor-shared-utils.js'); } catch (e) { return null; }
28 }
29 return null;
30 }
31 function blockEditorSelect() { var u = sharedEditorUtils(); return u && u.blockEditorSelect ? u.blockEditorSelect() : null; }
32 function blockEditorDispatch() { var u = sharedEditorUtils(); return u && u.blockEditorDispatch ? u.blockEditorDispatch() : null; }
33 function rootClientId(sel) { var u = sharedEditorUtils(); return u && u.rootClientId ? u.rootClientId(sel) : null; }
34 function currentPostId() { var u = sharedEditorUtils(); return u && u.currentPostId ? u.currentPostId() : null; }
35
36 // The target block: an explicit clientId, else the page root container.
37 function resolveClientId(sel, args) {
38 var cid = args && typeof args.clientId === 'string' && args.clientId !== '' ? args.clientId : null;
39 return cid || rootClientId(sel);
40 }
41
42 // A block's current spectraCustomJS ('' when unset or the block is gone).
43 function customJsOf(sel, clientId) {
44 var attrs = sel && sel.getBlockAttributes ? sel.getBlockAttributes(clientId) : null;
45 return attrs && typeof attrs.spectraCustomJS === 'string' ? attrs.spectraCustomJS : '';
46 }
47
48 function handleGetScripts(args) {
49 var sel = blockEditorSelect();
50 if (!sel || !sel.getBlockAttributes) {
51 return { success: false, error: 'editor_unavailable: core/block-editor store not present (is the block editor open?)' };
52 }
53 var clientId = resolveClientId(sel, args);
54 if (!clientId) {
55 return { success: false, error: 'no_block: the page has no blocks to read JS from' };
56 }
57 return {
58 success: true,
59 data: {
60 post_id: currentPostId(),
61 client_id: clientId,
62 code: customJsOf(sel, clientId),
63 },
64 };
65 }
66
67 // Throws on code the block renderer would reject — typed and loud beats a
68 // silent drop. `<script>` tags never belong in the store (it wraps the raw JS).
69 function validateCode(code) {
70 if (typeof code !== 'string' || code === '') {
71 throw new Error('invalid_input: code is required (the raw JS source, no <script> tags)');
72 }
73 if (/<\/?script/i.test(code)) {
74 throw new Error('invalid_input: code must be the raw JS source only, with no <script> tags (the store wraps it)');
75 }
76 }
77
78 // `code` REPLACES the block's spectraCustomJS; `append: true` adds to the
79 // existing JS instead (read first with editor/get-scripts).
80 function handleSetScripts(args) {
81 var sel = blockEditorSelect();
82 var dis = blockEditorDispatch();
83 if (!sel || !sel.getBlockAttributes || !dis || !dis.updateBlockAttributes) {
84 return { success: false, error: 'editor_unavailable: core/block-editor store not present (is the block editor open?)' };
85 }
86
87 try { validateCode(args ? args.code : undefined); } catch (e) {
88 return { success: false, error: String(e && e.message ? e.message : e) };
89 }
90
91 var clientId = resolveClientId(sel, args);
92 if (!clientId) {
93 return { success: false, error: 'no_block: the page has no blocks to attach JS to' };
94 }
95 var attrs = sel.getBlockAttributes(clientId);
96 if (!attrs) {
97 return { success: false, error: 'unknown_block: no block with clientId ' + clientId + ' in the live tree (read the outline / get-context first)' };
98 }
99
100 var existing = typeof attrs.spectraCustomJS === 'string' ? attrs.spectraCustomJS : '';
101 var next = (args.append === true && existing !== '') ? existing + '\n' + args.code : args.code;
102 dis.updateBlockAttributes(clientId, { spectraCustomJS: next });
103
104 return {
105 success: true,
106 data: {
107 client_id: clientId,
108 code: next,
109 note: 'Session-scoped: the block\'s JS updates now; persists when the user saves the page.',
110 },
111 };
112 }
113
114 // Register once the bridge is ready (same retry pattern as get-context /
115 // apply-change). The react-manager glob auto-enqueues this file.
116 function initHandler() {
117 if (window.zipwpMcp && window.zipwpMcp.registerTool) {
118 window.zipwpMcp.registerTool(
119 'editor/get-scripts',
120 async function (args) { return handleGetScripts(args); },
121 { previewMode: 'client' }
122 );
123 window.zipwpMcp.registerTool(
124 'editor/set-scripts',
125 async function (args) { return handleSetScripts(args); },
126 { previewMode: 'client' }
127 );
128 } else {
129 setTimeout(initHandler, 100);
130 }
131 }
132
133 initHandler();
134
135 // Test-only surface (Node/CommonJS) — inert in the browser bundle.
136 if (typeof module !== 'undefined' && module.exports) {
137 module.exports = {
138 handleGetScripts: handleGetScripts,
139 handleSetScripts: handleSetScripts,
140 };
141 }
142 })();
143