PluginProbe
Extendify / trunk
Extendify vtrunk
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
← All changes | src/AutoLaunch/functions/nav.js +146 -40 3.0.6 → trunk View file →
@@ -23,8 +23,24 @@
23 23 method: 'POST',
24 24 data: { content },
25 25 });
26 26
27 +const navLink = (attributes) =>
28 + `<!-- wp:navigation-link ${JSON.stringify(attributes)} /-->`;
29 +
30 +const pluginPageLink = (page) =>
31 + navLink({
32 + label: page.title?.rendered ?? page.name,
33 + id: page.id,
34 + type: page.type,
35 + url: page.link,
36 + kind: page.id ? 'post-type' : 'custom',
37 + isTopLevelLink: true,
38 + });
39 +
40 +const anchorLink = ({ label, url }) =>
41 + navLink({ label, type: 'custom', url, kind: 'custom', isTopLevelLink: true });
42 +
27 43 export const addSectionLinksToNav = async (
28 44 navigationId,
29 45 homePatterns = [],
30 46 pluginPages = [],
@@ -47,12 +63,9 @@
47 63 const lookup =
48 64 Object.values(pageNames).find(({ alias }) =>
49 65 alias.includes(patternType),
50 66 ) || {};
51 - return {
52 - label: pattern.navLabel ?? lookup.title,
53 - slug: pattern.navSlug ?? lookup.slug,
54 - };
67 + return { label: lookup.title, slug: lookup.slug };
55 68 };
56 69
57 70 const sectionPatterns = homePatterns.filter((pattern) => {
58 71 const { slug } = resolve(pattern);
@@ -70,34 +83,13 @@
70 83 const url = pages.includes(slug)
71 84 ? `${window.extSharedData.homeUrl}/${slug}`
72 85 : `${window.extSharedData.homeUrl}/#${slug}`;
73 86
74 - const attributes = JSON.stringify({
75 - label,
76 - type: 'custom',
77 - url,
78 - kind: 'custom',
79 - isTopLevelLink: true,
80 - });
81 -
82 - return `<!-- wp:navigation-link ${attributes} /-->`;
87 + return anchorLink({ label, url });
83 88 });
84 89
85 - const pluginPagesNavigationLinks = pluginPages.map(
86 - ({ title, id, type, link }) => {
87 - const attributes = JSON.stringify({
88 - label: title.rendered,
89 - id,
90 - type,
91 - url: link,
92 - kind: id ? 'post-type' : 'custom',
93 - isTopLevelLink: true,
94 - });
90 + const pluginPagesNavigationLinks = pluginPages.map(pluginPageLink);
95 91
96 - return `<!-- wp:navigation-link ${attributes} /-->`;
97 - },
98 - );
99 -
100 92 // When an ordered slug list is provided, interleave plugin pages by slug
101 93 // so e.g. "shop" lands where the design preview placed it.
102 94 let navigationLinks;
103 95 if (orderedSlugs.length) {
@@ -130,8 +122,42 @@
130 122
131 123 await updateNavigation(navigationId, navigationLinks);
132 124 };
133 125
126 +// Full-page single-page: the design build's page list IS the menu, in order —
127 +// the same list the preview nav used — and the BE baked matching #section
128 +// anchors into the page HTML. Build straight from it; no pattern derivation.
129 +export const addSectionLinksFromDesign = async (
130 + navigationId,
131 + designPages = [],
132 + pluginPages = [],
133 +) => {
134 + const { homeUrl } = window.extSharedData;
135 + const pluginBySlug = new Map(pluginPages.map((page) => [page.slug, page]));
136 +
137 + const sectionLink = ({ slug, name }) =>
138 + anchorLink({ label: name, url: `${homeUrl}/#${slug}` });
139 +
140 + const seen = new Set();
141 + const links = [];
142 + for (const { slug, name } of designPages) {
143 + if (!slug || slug === 'home' || seen.has(slug)) continue;
144 + seen.add(slug);
145 + const pluginPage = pluginBySlug.get(slug);
146 + links.push(
147 + pluginPage ? pluginPageLink(pluginPage) : sectionLink({ slug, name }),
148 + );
149 + }
150 + // An active plugin page the design didn't place still belongs in the nav.
151 + for (const page of pluginPages) {
152 + if (!page.slug || seen.has(page.slug)) continue;
153 + seen.add(page.slug);
154 + links.push(pluginPageLink(page));
155 + }
156 +
157 + await updateNavigation(navigationId, links.join(''));
158 +};
159 +
134 160 export const addPageLinksToNav = async (
135 161 navigationId,
136 162 allPages,
137 163 createdPages,
@@ -157,9 +183,18 @@
157 183 Object.values(pageNames).find((p) => p.alias?.includes(slug))?.navOrder ??
158 184 Object.keys(pageNames).length + 1
159 185 );
160 186 };
161 - const mergedPages = [...filteredCreatedPages, ...pluginPages];
187 + const seen = new Set();
188 + const mergedPages = [...filteredCreatedPages, ...pluginPages].filter(
189 + (page) => {
190 + const slug = getSlug(page);
191 + if (!slug) return true;
192 + if (seen.has(slug)) return false;
193 + seen.add(slug);
194 + return true;
195 + },
196 + );
162 197
163 198 let finalPages;
164 199 if (orderedSlugs.length) {
165 200 const indexOf = (p) => orderedSlugs.indexOf(getSlug(p));
@@ -190,21 +225,10 @@
190 225 })()
191 226 : sortedPages;
192 227 }
193 228
194 - const pageLinks = finalPages.map(({ id, title, link, type }) => {
195 - const attributes = JSON.stringify({
196 - label: title.rendered,
197 - id,
198 - type,
199 - url: link,
200 - kind: id ? 'post-type' : 'custom',
201 - isTopLevelLink: true,
202 - });
229 + const pageLinks = finalPages.map(pluginPageLink);
203 230
204 - return `<!-- wp:navigation-link ${attributes} /-->`;
205 - });
206 -
207 231 const topLevelLinks = pageLinks.slice(0, 5).join('');
208 232 const submenuLinks = pageLinks.slice(5);
209 233 // We want a max of 6 top-level links, but if 7+, then move the last
210 234 // two+ to a submenu.
@@ -238,5 +262,87 @@
238 262 // biome-ignore lint: don't want to refactor and test this regex now
239 263 /(<!--\s*wp:navigation\b[^>]*>)([^]*?)(<!--\s*\/wp:navigation\s*-->)/gi,
240 264 `<!-- wp:navigation ${newAttributes} /-->`,
241 265 );
266 +};
267 +
268 +const getNavExtrasBlock = (launchDecisions) => {
269 + switch (launchDecisions?.navExtras) {
270 + case 'button': {
271 + const label =
272 + launchDecisions?.navButtonLabel || __('Get Started', 'extendify-local');
273 + return `<!-- wp:buttons {"className":"ext-nav-extras-btn"} -->
274 +<div class="wp-block-buttons ext-nav-extras-btn"><!-- wp:button {"className":"is-style-ext-preset\u002d\u002dbutton\u002d\u002dnatural-1\u002d\u002dbutton-1","style":{"spacing":{"padding":{"left":"20px","right":"20px","top":"8px","bottom":"8px"}},"typography":{"lineHeight":1.6}},"fontSize":"small"} -->
275 +<div class="wp-block-button is-style-ext-preset--button--natural-1--button-1"><a class="wp-block-button__link has-small-font-size has-custom-font-size wp-element-button" href="#extendify-navbar-cta" style="padding-top:8px;padding-right:20px;padding-bottom:8px;padding-left:20px;line-height:1.6">${label}</a></div>
276 +<!-- /wp:button --></div>
277 +<!-- /wp:buttons -->`;
278 + }
279 + case 'phone-number':
280 + return `<!-- wp:group {"className":"ext-nav-extras-phone","style":{"spacing":{"blockGap":"6px"}},"layout":{"type":"flex","flexWrap":"nowrap","verticalAlignment":"center"}} -->
281 +<div class="wp-block-group ext-nav-extras-phone"><!-- wp:image {"sizeSlug":"large","style":{"layout":{"selfStretch":"fixed","flexSize":"21px"},"color":{"duotone":"var:preset|duotone|primary-foreground"},"spacing":{"margin":{"bottom":"6px"}}}} -->
282 +<figure class="wp-block-image size-large" style="margin-bottom:6px"><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iIzAwMDAwMCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0yMjQsMTU0LjhsLTQ3LjA5LTIxLjExLS4xOC0uMDhhMTkuOTQsMTkuOTQsMCwwLDAtMTksMS43NSwxMy4wOCwxMy4wOCwwLDAsMC0xLjEyLjg0bC0yMi4zMSwxOWMtMTMtNy4wNS0yNi40My0yMC4zNy0zMy40OS0zMy4yMWwxOS4wNi0yMi42NmExMS43NiwxMS43NiwwLDAsMCwuODUtMS4xNSwyMCwyMCwwLDAsMCwxLjY2LTE4LjgzLDEuNDIsMS40MiwwLDAsMS0uMDgtLjE4TDEwMS4yLDMyQTIwLjA2LDIwLjA2LDAsMCwwLDgwLjQyLDIwLjE1LDYwLjI3LDYwLjI3LDAsMCwwLDI4LDgwYzAsODEuNjEsNjYuMzksMTQ4LDE0OCwxNDhhNjAuMjcsNjAuMjcsMCwwLDAsNTkuODUtNTIuNDJBMjAuMDYsMjAuMDYsMCwwLDAsMjI0LDE1NC44Wk0xNzYsMjA0QTEyNC4xNSwxMjQuMTUsMCwwLDEsNTIsODAsMzYuMjksMzYuMjksMCwwLDEsODAuNDgsNDQuNDZsMTguODIsNDJMODAuMTQsMTA5LjI4YTEyLDEyLDAsMCwwLS44NiwxLjE2QTIwLDIwLDAsMCwwLDc4LDEzMC4wOGM5LjQyLDE5LjI4LDI4LjgzLDM4LjU2LDQ4LjMxLDQ4QTIwLDIwLDAsMCwwLDE0NiwxNzYuNjNhMTEuNjMsMTEuNjMsMCwwLDAsMS4xMS0uODVsMjIuNDMtMTkuMDcsNDIsMTguODFBMzYuMjksMzYuMjksMCwwLDEsMTc2LDIwNFoiPjwvcGF0aD48L3N2Zz4=" alt=""/></figure>
283 +<!-- /wp:image -->
284 +
285 +<!-- wp:paragraph {"className":"no-underline","style":{"elements":{"link":{"color":{"text":"var:preset|color|primary"}}},"typography":{"fontSize":"18px","fontStyle":"normal","fontWeight":"700","textDecoration":"none"}},"textColor":"primary"} -->
286 +<p class="no-underline has-primary-color has-text-color has-link-color" style="font-size:18px;font-style:normal;font-weight:700;text-decoration:none"><a href="tel:206-555-0100" data-type="tel" data-id="tel:206-555-0100">206-555-0100</a></p>
287 +<!-- /wp:paragraph --></div>
288 +<!-- /wp:group -->`;
289 + case 'social-icons':
290 + return `<!-- wp:social-links {"iconColor":"foreground","iconColorValue":"var(--wp--preset--color--foreground)","size":"has-small-icon-size","className":"is-style-logos-only ext-hidden tablet:ext-flex ext-nav-extras-social","style":{"spacing":{"blockGap":"1rem"}},"layout":{"type":"flex","flexWrap":"nowrap","justifyContent":"right"}} -->
291 +<ul class="wp-block-social-links has-small-icon-size has-icon-color is-style-logos-only ext-hidden tablet:ext-flex ext-nav-extras-social"><!-- wp:social-link {"url":"https://www.instagram.com/","service":"instagram"} /-->
292 +
293 +<!-- wp:social-link {"url":"https://www.facebook.com/","service":"facebook"} /-->
294 +
295 +<!-- wp:social-link {"url":"https://x.com/","service":"x"} /--></ul>
296 +<!-- /wp:social-links -->`;
297 + default:
298 + return null;
299 + }
300 +};
301 +
302 +// Woo auto-inserts these after the nav, but WP suppresses them when AutoLaunch
303 +// saves the header directly — insert them ourselves so they render. fontSize
304 +// matches Extendable's nav (`small`).
305 +export const injectWooCommerceIcons = (headerCode) => {
306 + // The fetched header can already carry them: WP materializes hooked blocks
307 + // into REST responses once Woo is active (e.g. on a re-launch).
308 + if (/wp:woocommerce\/(mini-cart|customer-account)/.test(headerCode)) {
309 + return headerCode;
310 + }
311 +
312 + // After updateNavAttributes the nav block is always self-closing.
313 + const navBlock = /<!--\s*wp:navigation\b[^>]*?\/-->/i;
314 + if (!navBlock.test(headerCode)) return headerCode;
315 +
316 + const icons = [
317 + '<!-- wp:woocommerce/customer-account {"displayStyle":"icon_only","iconStyle":"line","iconClass":"wc-block-customer-account__account-icon","fontSize":"small"} /-->',
318 + '<!-- wp:woocommerce/mini-cart {"fontSize":"small"} /-->',
319 + ].join('\n');
320 +
321 + return headerCode.replace(navBlock, (nav) => `${nav}\n${icons}`);
322 +};
323 +
324 +export const injectNavExtras = (headerCode, launchDecisions) => {
325 + const navExtras = launchDecisions?.navExtras;
326 + if (!navExtras || navExtras === 'none') return headerCode;
327 +
328 + const block = getNavExtrasBlock(launchDecisions);
329 + if (!block) return headerCode;
330 +
331 + // Strip any existing ext-nav-extras-* block so we don't stack with what the
332 + // header may already ship with.
333 + const stripped = headerCode.replace(
334 + /<!--\s*wp:([\w-]+)\b[^>]*ext-nav-extras-[\w-]+[^>]*-->[\s\S]*?<!--\s*\/wp:\1\s*-->\s*/g,
335 + '',
336 + );
337 +
338 + const markerIdx = stripped.indexOf('ext-nav-extras');
339 + if (markerIdx === -1) return stripped;
340 +
341 + const groupCloseMatch = stripped
342 + .slice(markerIdx)
343 + .match(/<!--\s*\/wp:group\s*-->/);
344 + if (!groupCloseMatch) return stripped;
345 +
346 + const insertAt = markerIdx + groupCloseMatch.index;
347 + return `${stripped.slice(0, insertAt)}${block}\n${stripped.slice(insertAt)}`;
242 348 };