PluginProbe
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News / 4.0.3
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News v4.0.3
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 2.3.5 2.3.6 2.4.0 2.4.1 2.4.10 2.4.11 2.4.12 2.4.13 2.4.14 2.4.15 2.4.16 2.4.17 2.4.18 2.4.19 2.4.2 2.4.20 2.4.21 All 88 releases
post-carousel / src / controls / saveBlockCSS.js

saveBlockCSS.js in Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News 4.0.3, at src/controls/saveBlockCSS.js

297 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { subscribe, select } from "@wordpress/data";
2 import apiFetch from "@wordpress/api-fetch";
3 import { parse, getBlockType } from "@wordpress/blocks";
4
5 const processBlocksRecursively = (blocks) => {
6 if (!blocks || !blocks.length) {
7 return { css: "", fonts: [], hasOurBlock: false, blockName: [] };
8 }
9 let combinedCSS = "";
10 let blockNames = [];
11 const fonts = [];
12 let hasOurBlock = false;
13
14 blocks.forEach((block) => {
15 const uniqueId = block.attributes?.uniqueId;
16 // Detect our custom blocks.
17 if (
18 uniqueId &&
19 (uniqueId.startsWith("sp-smart-") ||
20 uniqueId.startsWith("sp-section-") ||
21 uniqueId.startsWith("sp-social-"))
22 ) {
23 hasOurBlock = true;
24 const blockFonts = block.attributes?.fontLists ?? "";
25 if (typeof blockFonts === "string" && blockFonts.trim()) {
26 try {
27 const parsed = JSON.parse(blockFonts);
28 if (Array.isArray(parsed) && parsed.length) {
29 fonts.push(...parsed);
30 }
31 } catch (e) {
32 if (/^[A-Za-z0-9\s]+:\d+$/.test(blockFonts)) {
33 fonts.push(blockFonts);
34 }
35 }
36 }
37
38 const blockType = getBlockType(block.name);
39 if (blockType?.generateCSS) {
40 const css = blockType.generateCSS(block.attributes, block.name);
41 blockNames.push(block.name);
42 combinedCSS += `/* CSS for ${block.name} - ${uniqueId} */\n${css}\n`;
43 }
44 }
45
46 // recurse children
47 if (block.innerBlocks?.length) {
48 const {
49 css: childCSS,
50 fonts: childFonts,
51 hasOurBlock: childHas,
52 blockName: blockName,
53 } = processBlocksRecursively(block.innerBlocks);
54
55 combinedCSS += childCSS;
56 fonts.push(...childFonts);
57 blockNames.push(...blockName);
58 if (childHas) {
59 hasOurBlock = true;
60 }
61 }
62 });
63
64 const uniqueFonts = [...new Set(fonts.filter((f) => /^[A-Za-z0-9\s]+:\d+$/.test(f)))];
65 blockNames = [...new Set(blockNames.map((name) => name.split("/").pop()))];
66 return {
67 css: combinedCSS || "",
68 fonts: uniqueFonts || [],
69 hasOurBlock,
70 blockName: blockNames,
71 };
72 };
73
74 /**
75 * Recursively collect all reusable block refs (core/block with attributes.ref)
76 * Returns an array of unique ref IDs (numbers/strings).
77 * @param blocks
78 * @param set
79 */
80 const collectReusableRefs = (blocks, set = new Set()) => {
81 if (!blocks || !blocks.length) {
82 return set;
83 }
84 blocks.forEach((block) => {
85 if (block.name === "core/block" && block.attributes?.ref) {
86 set.add(block.attributes.ref);
87 }
88 if (block.innerBlocks?.length) {
89 collectReusableRefs(block.innerBlocks, set);
90 }
91 });
92 return set;
93 };
94
95 /**
96 * Main save handler:
97 * - watches saves for postType entities (except wp_block themselves)
98 * - when saving, parse content, collect reusable refs
99 * - for each ref: fetch reusable block, parse, if has our block -> POST to smart-save-block-css with slug 'wp_block'
100 * - also send the usual API for the main post if it has our block.
101 */
102 const saveBlockCSS = () => {
103 let previousSaving = false;
104
105 subscribe(() => {
106 const { __experimentalGetDirtyEntityRecords, isSavingEntityRecord, getEditedEntityRecord } = select("core");
107
108 const coreEditor = select("core/editor");
109 const dirtyEntities = __experimentalGetDirtyEntityRecords();
110
111 // detect a real save (not autosave)
112 // const isSaving = dirtyEntities.some((record) =>
113 // isSavingEntityRecord(record.kind, record.name, record.key)
114 // );
115 const isSaving = dirtyEntities.some((record) => {
116 const entity = getEditedEntityRecord(record.kind, record.name, record.key);
117 if (!entity) {
118 return false;
119 }
120 return isSavingEntityRecord(record.kind, record.name, record.key);
121 });
122
123 const isAutosavingPost = coreEditor?.isAutosavingPost ? () => coreEditor.isAutosavingPost() : () => false;
124 // console.log(!previousSaving, !isAutosavingPost(), isSaving);
125 const shouldTrigger = !previousSaving && !isAutosavingPost() && isSaving;
126
127 if (!shouldTrigger) {
128 previousSaving = isSaving;
129 return;
130 }
131 // We'll run the async work in an IIFE so subscribe() stays sync.
132 (async () => {
133 // keep track of refs processed in this save to avoid duplicate API calls.
134 const processedRefs = new Set();
135 for (const entity of dirtyEntities) {
136 // Only care about postType entities (posts/pages/templates), skip wp_block here.
137 if (entity.kind !== "postType" && entity.name !== "widget") {
138 continue;
139 }
140 const record = getEditedEntityRecord(entity.kind, entity.name, entity.key);
141 // Determine postId for main API (fallbacks like earlier).
142 let postId = record?.id || entity.name || "unknown";
143 if (entity.name === "wp_template_part" || entity.name === "wp_template" || entity.name === "widget") {
144 postId = entity.name;
145 }
146 if (entity.name === "wp_block") {
147 record.slug = "wp_block";
148 }
149 // Extract content safely (mirror your previous logic)
150 let content = "";
151 let widgetId = "";
152 if (entity.name === "widget") {
153 content = record.instance?.raw?.content || "";
154 widgetId = record?.id || "";
155 } else if (typeof record.content === "string") {
156 content = record.content;
157 } else if (typeof record.content === "function") {
158 try {
159 if (record.blocks) {
160 if (wp.blocks && wp.blocks.__unstableSerializeAndClean) {
161 content = wp.blocks.__unstableSerializeAndClean(record.blocks);
162 } else {
163 content = "";
164 }
165 } else {
166 content = "";
167 }
168 } catch (error) {
169 // eslint-disable-next-line no-console
170 console.log("Error processing content function:", error);
171 content = "";
172 }
173 } else if (record.content && typeof record.content === "object") {
174 content = record.content.raw || "";
175 } else {
176 content = "NO Content Found";
177 }
178
179 if (!content || typeof content !== "string") {
180 continue;
181 }
182
183 // Parse main content blocks.
184 let blocks = [];
185 try {
186 blocks = parse(content);
187 } catch (err) {
188 // eslint-disable-next-line no-console
189 console.log("Error parsing content blocks:", err);
190 continue;
191 }
192 // Collect all reusable refs used inside this content
193
194 const refsSet = collectReusableRefs(blocks);
195 // Main post: if it has our block -> send API (same as before)
196 const { css: mainCSS, fonts: mainFonts, blockName: blockName } = processBlocksRecursively(blocks);
197 try {
198 await apiFetch({
199 path: "/sp-smart-post/v2/smart-save-block-css",
200 method: "POST",
201 data: {
202 post_id: postId,
203 nonce: sp_smart_post_block_localize.ajaxNonce,
204 slug: record?.slug || "",
205 widget_id: widgetId, // only for widgets earlier
206 theme: record?.theme || "",
207 block_css: mainCSS || "",
208 fonts: mainFonts || [],
209 preview: false,
210 has_block: mainCSS?.length > 1,
211 block_names: blockName,
212 has_refs: refsSet?.size > 0,
213 },
214 });
215 } catch (err) {
216 // eslint-disable-next-line no-console
217 console.log("Main post CSS save error:", err);
218 }
219
220 if (!refsSet || refsSet.size === 0) {
221 continue;
222 }
223 // For each unique ref, fetch reusable block content and if it has our blocks -> send API
224 for (const refId of refsSet) {
225 // Skip if already processed in this save.
226 if (processedRefs.has(refId)) {
227 continue;
228 }
229
230 processedRefs.add(refId);
231
232 try {
233 const reusable = await apiFetch({
234 path: `/wp/v2/blocks/${refId}`,
235 });
236
237 // Support different shapes: prefer content.raw but tolerate fallback.
238 const rawContent =
239 reusable?.content?.raw ??
240 reusable?.content?.rendered ??
241 (typeof reusable?.content === "string" ? reusable.content : "");
242
243 if (!rawContent || typeof rawContent !== "string") {
244 continue;
245 }
246 let innerBlocks = [];
247 try {
248 innerBlocks = parse(rawContent);
249 } catch (err) {
250 console.log(`Error parsing reusable block (${refId}) content:`, err);
251 continue;
252 }
253
254 const {
255 css: innerCSS,
256 fonts: innerFonts,
257 hasOurBlock: innerHas,
258 blockName: blockName,
259 } = processBlocksRecursively(innerBlocks);
260
261 // Send api for the reusable block.
262 try {
263 await apiFetch({
264 path: "/sp-smart-post/v2/smart-save-block-css",
265 method: "POST",
266 data: {
267 post_id: postId,
268 ref_id: refId,
269 nonce: sp_smart_post_block_localize.ajaxNonce,
270 slug: record?.slug,
271 widget_id: "",
272 theme: record?.theme || "",
273 block_css: innerCSS || "",
274 fonts: innerFonts || [],
275 preview: false,
276 block_type: "wp_block",
277 reusable_block_ids: Array.from(refsSet),
278 has_block: innerCSS?.length > 1,
279 block_names: blockName,
280 },
281 });
282 // optional: console.log('Reusable block saved:', refId);
283 } catch (err) {
284 console.log(`Reusable block CSS save error for ref ${refId}:`, err);
285 }
286 } catch (err) {
287 console.log(`Error fetching reusable block ${refId}:`, err);
288 }
289 } // end loop refs
290 } // end loop entities
291 })(); // end IIFE
292 previousSaving = isSaving;
293 });
294 };
295
296 export default saveBlockCSS;
297