| 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 |
|