PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
← All changes | src/AutoLaunch/functions/nav.js +146 -40 3.0.5 → 3.2.2 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 };