PluginProbe
Extendify / 3.2.0
Extendify v3.2.0
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / workflows / theme / tools / update-header-style.js

update-header-style.js in Extendify 3.2.0, at src/Agent/workflows/theme/tools/update-header-style.js

124 lines 3.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { fetchBlockCodeById } from '@agent/lib/block-code';
2 import { ensureCoreBlocksRegistered } from '@agent/lib/register-blocks';
3 import apiFetch from '@wordpress/api-fetch';
4 import { parse, serialize } from '@wordpress/blocks';
5
6 const PART_ATTR = 'data-extendify-part';
7 const PART_ID_ATTR = 'data-extendify-part-block-id';
8 const PART_SLUG_ATTR = 'data-extendify-part-slug';
9
10 // The compositions style.css documents. Anything else has no theme rules: a
11 // pill with no glass has no background, dark with no scrim is white on white.
12 export const HEADER_STATES = {
13 plain: [],
14 sticky: ['ext-header-sticky'],
15 overlay: ['ext-header-overlay'],
16 'overlay-dark': ['ext-header-overlay', 'ext-header--dark'],
17 glass: ['ext-header-sticky', 'ext-header-glass'],
18 'glass-dark': ['ext-header-sticky', 'ext-header-glass', 'ext-header--dark'],
19 pill: [
20 'ext-header-sticky',
21 'ext-header-overlay',
22 'ext-header-glass',
23 'ext-header-sticky--floating-pill',
24 ],
25 'pill-dark': [
26 'ext-header-sticky',
27 'ext-header-overlay',
28 'ext-header-glass',
29 'ext-header-sticky--floating-pill',
30 'ext-header--dark',
31 ],
32 };
33
34 const OWNED = [...new Set(Object.values(HEADER_STATES).flat())];
35
36 export const headerBlockEl = () =>
37 [...document.querySelectorAll(`[${PART_ATTR}="header"]`)].find(
38 (el) => !el.parentElement?.closest(`[${PART_ATTR}="header"]`),
39 ) ?? null;
40
41 export const isExtendableHeader = () => {
42 const el = headerBlockEl();
43 if (!el) return false;
44 const classes = String(el.className).split(/\s+/);
45 return OWNED.some((cls) => classes.includes(cls));
46 };
47
48 const stateOf = (classes) => {
49 const on = OWNED.filter((cls) => classes.includes(cls));
50 const match = Object.entries(HEADER_STATES).find(
51 ([, want]) =>
52 want.length === on.length && want.every((cls) => on.includes(cls)),
53 );
54 return match?.[0] ?? null;
55 };
56
57 // Null for a header the theme never composed, so the model is not told a state
58 // that would silently rewrite the other classes.
59 export const currentHeaderState = () => {
60 const el = headerBlockEl();
61 return el ? stateOf([...el.classList]) : null;
62 };
63
64 export const nextClasses = (current, state) => {
65 const want = HEADER_STATES[state];
66 if (!want) return [...current];
67 return [...current.filter((cls) => !OWNED.includes(cls)), ...want];
68 };
69
70 export default async ({ state } = {}) => {
71 const el = headerBlockEl();
72 const blockId = el?.getAttribute(PART_ID_ATTR);
73 const partSlug = el?.getAttribute(PART_SLUG_ATTR);
74 if (!blockId || !partSlug) {
75 return { changed: false, message: 'This page has no header to change.' };
76 }
77
78 await ensureCoreBlocksRegistered();
79 const code = await fetchBlockCodeById(blockId, {
80 kind: 'template-part',
81 partSlug,
82 });
83 const blocks = code ? parse(code) : [];
84 if (blocks.length !== 1 || !blocks[0].name) {
85 return { changed: false, message: 'The header block could not be read.' };
86 }
87
88 const block = blocks[0];
89 const current = String(block.attributes?.className ?? '')
90 .split(/\s+/)
91 .filter(Boolean);
92 const next = nextClasses(current, state);
93 if (stateOf(next) === stateOf(current)) {
94 return { changed: false, headerState: stateOf(next) };
95 }
96
97 const { applied = [] } = await apiFetch({
98 path: '/extendify/v1/agent/update-blocks',
99 method: 'POST',
100 data: {
101 partSlug,
102 operations: [
103 {
104 op: 'edit',
105 blockId,
106 block: serialize([
107 {
108 ...block,
109 attributes: { ...block.attributes, className: next.join(' ') },
110 },
111 ]),
112 },
113 ],
114 },
115 });
116 if (!applied.length) {
117 return { changed: false, message: 'The header change was not saved.' };
118 }
119
120 // Without this the change shows only after a reload.
121 for (const cls of OWNED) el.classList.toggle(cls, next.includes(cls));
122 return { changed: true, headerState: stateOf(next) };
123 };
124