PluginProbe
Extendify / 3.1.0
Extendify v3.1.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 / AutoLaunch / functions / nav.js

nav.js in Extendify 3.1.0, at src/AutoLaunch/functions/nav.js

303 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { pageNames } from '@shared/lib/pages';
2 import apiFetch from '@wordpress/api-fetch';
3 import { __ } from '@wordpress/i18n';
4 import { addQueryArgs } from '@wordpress/url';
5
6 export const createNavigation = async ({ content = '', title, slug }) => {
7 const existing = await apiFetch({
8 path: addQueryArgs('extendify/v1/auto-launch/get-navigation', { slug }),
9 }).catch(() => undefined);
10
11 if (existing?.id) return existing;
12
13 return await apiFetch({
14 path: 'extendify/v1/auto-launch/create-navigation',
15 method: 'POST',
16 data: { title, slug, content },
17 });
18 };
19
20 export const updateNavigation = (id, content) =>
21 apiFetch({
22 path: `wp/v2/navigation/${id}`,
23 method: 'POST',
24 data: { content },
25 });
26
27 export const addSectionLinksToNav = async (
28 navigationId,
29 homePatterns = [],
30 pluginPages = [],
31 createdPages = [],
32 { orderedSlugs = [] } = {},
33 ) => {
34 // Extract plugin page slugs for comparison
35 const pluginPageTitles = pluginPages.map(({ title }) =>
36 title?.rendered?.toLowerCase(),
37 );
38
39 const pages =
40 createdPages
41 ?.filter((page) => page?.slug !== 'home')
42 ?.map((page) => page.slug)
43 ?.filter(Boolean) ?? [];
44
45 const resolve = (pattern) => {
46 const patternType = pattern.patternTypes?.[0];
47 const lookup =
48 Object.values(pageNames).find(({ alias }) =>
49 alias.includes(patternType),
50 ) || {};
51 return {
52 label: pattern.navLabel ?? lookup.title,
53 slug: pattern.navSlug ?? lookup.slug,
54 };
55 };
56
57 const sectionPatterns = homePatterns.filter((pattern) => {
58 const { slug } = resolve(pattern);
59 return slug && !pluginPageTitles.includes(slug);
60 });
61
62 const seen = new Set();
63
64 const sectionsNavigationLinks = sectionPatterns.map((pattern) => {
65 const { label, slug } = resolve(pattern);
66 if (!slug) return '';
67 if (seen.has(slug)) return '';
68 seen.add(slug);
69
70 const url = pages.includes(slug)
71 ? `${window.extSharedData.homeUrl}/${slug}`
72 : `${window.extSharedData.homeUrl}/#${slug}`;
73
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} /-->`;
83 });
84
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 });
95
96 return `<!-- wp:navigation-link ${attributes} /-->`;
97 },
98 );
99
100 // When an ordered slug list is provided, interleave plugin pages by slug
101 // so e.g. "shop" lands where the design preview placed it.
102 let navigationLinks;
103 if (orderedSlugs.length) {
104 const bySlug = new Map();
105 sectionsNavigationLinks.forEach((link, i) => {
106 const slug = resolve(sectionPatterns[i]).slug;
107 if (slug) bySlug.set(slug, link);
108 });
109 pluginPages.forEach((page, i) => {
110 if (page.slug) bySlug.set(page.slug, pluginPagesNavigationLinks[i]);
111 });
112 const ordered = orderedSlugs
113 .map((slug) => bySlug.get(slug))
114 .filter(Boolean);
115 const placed = new Set(orderedSlugs.filter((s) => bySlug.has(s)));
116 const extras = [
117 ...sectionsNavigationLinks.filter(
118 (_, i) => !placed.has(resolve(sectionPatterns[i]).slug),
119 ),
120 ...pluginPagesNavigationLinks.filter(
121 (_, i) => !placed.has(pluginPages[i].slug),
122 ),
123 ];
124 navigationLinks = [...ordered, ...extras].join('');
125 } else {
126 navigationLinks = sectionsNavigationLinks
127 .concat(pluginPagesNavigationLinks)
128 .join('');
129 }
130
131 await updateNavigation(navigationId, navigationLinks);
132 };
133
134 export const addPageLinksToNav = async (
135 navigationId,
136 allPages,
137 createdPages,
138 pluginPages = [],
139 { orderedSlugs = [] } = {},
140 ) => {
141 // Because WP may have changed the slug and permalink (i.e., because of different languages),
142 // we are using the `originalSlug` property to match the original pages with the updated ones.
143 const findCreatedPage = ({ slug }) =>
144 createdPages.find(({ originalSlug: s }) => s === slug) || {};
145
146 const filteredCreatedPages = allPages
147 .filter((p) => findCreatedPage(p)?.id) // make sure its a page
148 .filter(({ slug }) => slug !== 'home') // exclude home page
149 .map((page) => findCreatedPage(page));
150
151 // Plugin pages use `slug`, created pages use `originalSlug`
152 const getSlug = (page) => page.originalSlug ?? page.slug;
153 const getOrder = (page) => {
154 const slug = getSlug(page);
155 return (
156 pageNames[slug]?.navOrder ??
157 Object.values(pageNames).find((p) => p.alias?.includes(slug))?.navOrder ??
158 Object.keys(pageNames).length + 1
159 );
160 };
161 const mergedPages = [...filteredCreatedPages, ...pluginPages];
162
163 let finalPages;
164 if (orderedSlugs.length) {
165 const indexOf = (p) => orderedSlugs.indexOf(getSlug(p));
166 const ordered = mergedPages
167 .filter((p) => indexOf(p) !== -1)
168 .sort((a, b) => indexOf(a) - indexOf(b));
169 const extras = mergedPages.filter((p) => indexOf(p) === -1);
170 finalPages = [...ordered, ...extras];
171 } else {
172 const contactPage = mergedPages.find((page) => {
173 const slug = getSlug(page);
174 return slug === 'contact' || pageNames.contact?.alias?.includes(slug);
175 });
176
177 const sortedPages = mergedPages
178 .filter((page) => page !== contactPage)
179 .sort((a, b) => getOrder(a) - getOrder(b));
180
181 finalPages = contactPage
182 ? (() => {
183 const index =
184 sortedPages.length === 5 ? 5 : Math.min(4, sortedPages.length);
185 return [
186 ...sortedPages.slice(0, index),
187 contactPage,
188 ...sortedPages.slice(index),
189 ];
190 })()
191 : sortedPages;
192 }
193
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 });
203
204 return `<!-- wp:navigation-link ${attributes} /-->`;
205 });
206
207 const topLevelLinks = pageLinks.slice(0, 5).join('');
208 const submenuLinks = pageLinks.slice(5);
209 // We want a max of 6 top-level links, but if 7+, then move the last
210 // two+ to a submenu.
211 const additionalLinks =
212 submenuLinks.length > 1
213 ? ` <!-- wp:navigation-submenu ${JSON.stringify({
214 // translators: "More" here is used for a navigation menu item that contains additional links.
215 label: __('More', 'extendify-local'),
216 url: '#',
217 kind: 'custom',
218 })} --> ${submenuLinks.join('')} <!-- /wp:navigation-submenu -->`
219 : submenuLinks.join(''); // only 1 link here
220
221 await updateNavigation(navigationId, topLevelLinks + additionalLinks);
222 };
223
224 const getNavAttributes = (headerCode) => {
225 try {
226 return JSON.parse(headerCode.match(/<!-- wp:navigation([\s\S]*?)-->/)[1]);
227 } catch (_e) {
228 return {};
229 }
230 };
231
232 export const updateNavAttributes = (headerCode, attributes) => {
233 const newAttributes = JSON.stringify({
234 ...getNavAttributes(headerCode),
235 ...attributes,
236 });
237 return headerCode.replace(
238 // biome-ignore lint: don't want to refactor and test this regex now
239 /(<!--\s*wp:navigation\b[^>]*>)([^]*?)(<!--\s*\/wp:navigation\s*-->)/gi,
240 `<!-- wp:navigation ${newAttributes} /-->`,
241 );
242 };
243
244 const getNavExtrasBlock = (launchDecisions) => {
245 switch (launchDecisions?.navExtras) {
246 case 'button': {
247 const label =
248 launchDecisions?.navButtonLabel || __('Get Started', 'extendify-local');
249 return `<!-- wp:buttons {"className":"ext-nav-extras-btn"} -->
250 <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"} -->
251 <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>
252 <!-- /wp:button --></div>
253 <!-- /wp:buttons -->`;
254 }
255 case 'phone-number':
256 return `<!-- wp:group {"className":"ext-nav-extras-phone","style":{"spacing":{"blockGap":"6px"}},"layout":{"type":"flex","flexWrap":"nowrap","verticalAlignment":"center"}} -->
257 <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"}}}} -->
258 <figure class="wp-block-image size-large" style="margin-bottom:6px"><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iIzAwMDAwMCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0yMjQsMTU0LjhsLTQ3LjA5LTIxLjExLS4xOC0uMDhhMTkuOTQsMTkuOTQsMCwwLDAtMTksMS43NSwxMy4wOCwxMy4wOCwwLDAsMC0xLjEyLjg0bC0yMi4zMSwxOWMtMTMtNy4wNS0yNi40My0yMC4zNy0zMy40OS0zMy4yMWwxOS4wNi0yMi42NmExMS43NiwxMS43NiwwLDAsMCwuODUtMS4xNSwyMCwyMCwwLDAsMCwxLjY2LTE4LjgzLDEuNDIsMS40MiwwLDAsMS0uMDgtLjE4TDEwMS4yLDMyQTIwLjA2LDIwLjA2LDAsMCwwLDgwLjQyLDIwLjE1LDYwLjI3LDYwLjI3LDAsMCwwLDI4LDgwYzAsODEuNjEsNjYuMzksMTQ4LDE0OCwxNDhhNjAuMjcsNjAuMjcsMCwwLDAsNTkuODUtNTIuNDJBMjAuMDYsMjAuMDYsMCwwLDAsMjI0LDE1NC44Wk0xNzYsMjA0QTEyNC4xNSwxMjQuMTUsMCwwLDEsNTIsODAsMzYuMjksMzYuMjksMCwwLDEsODAuNDgsNDQuNDZsMTguODIsNDJMODAuMTQsMTA5LjI4YTEyLDEyLDAsMCwwLS44NiwxLjE2QTIwLDIwLDAsMCwwLDc4LDEzMC4wOGM5LjQyLDE5LjI4LDI4LjgzLDM4LjU2LDQ4LjMxLDQ4QTIwLDIwLDAsMCwwLDE0NiwxNzYuNjNhMTEuNjMsMTEuNjMsMCwwLDAsMS4xMS0uODVsMjIuNDMtMTkuMDcsNDIsMTguODFBMzYuMjksMzYuMjksMCwwLDEsMTc2LDIwNFoiPjwvcGF0aD48L3N2Zz4=" alt=""/></figure>
259 <!-- /wp:image -->
260
261 <!-- 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"} -->
262 <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>
263 <!-- /wp:paragraph --></div>
264 <!-- /wp:group -->`;
265 case 'social-icons':
266 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"}} -->
267 <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"} /-->
268
269 <!-- wp:social-link {"url":"https://www.facebook.com/","service":"facebook"} /-->
270
271 <!-- wp:social-link {"url":"https://x.com/","service":"x"} /--></ul>
272 <!-- /wp:social-links -->`;
273 default:
274 return null;
275 }
276 };
277
278 export const injectNavExtras = (headerCode, launchDecisions) => {
279 const navExtras = launchDecisions?.navExtras;
280 if (!navExtras || navExtras === 'none') return headerCode;
281
282 const block = getNavExtrasBlock(launchDecisions);
283 if (!block) return headerCode;
284
285 // Strip any existing ext-nav-extras-* block so we don't stack with what the
286 // header may already ship with.
287 const stripped = headerCode.replace(
288 /<!--\s*wp:([\w-]+)\b[^>]*ext-nav-extras-[\w-]+[^>]*-->[\s\S]*?<!--\s*\/wp:\1\s*-->\s*/g,
289 '',
290 );
291
292 const markerIdx = stripped.indexOf('ext-nav-extras');
293 if (markerIdx === -1) return stripped;
294
295 const groupCloseMatch = stripped
296 .slice(markerIdx)
297 .match(/<!--\s*\/wp:group\s*-->/);
298 if (!groupCloseMatch) return stripped;
299
300 const insertAt = markerIdx + groupCloseMatch.index;
301 return `${stripped.slice(0, insertAt)}${block}\n${stripped.slice(insertAt)}`;
302 };
303