PageSourceSearch

https://site.fr/_static/ad2521ddc1b5acb4/scripts/features/support/support-utils.js

js site.fr collected 2026-10-02 04:06:57 UTC 19,680 bytes, 594 lines download raw bytes

1import { formatUrlTitle } from '../../client-utils.js';
2import {
3  slugifyLegacyUrl,
4  slugifyLegacyArticleUrl,
5  normalizePath,
6  getI18nCore,
7  getCanonicalLocale,
8  getCanonicalContext,
9  getTranslationTemplate,
10  resolveTemplateText,
11  setSafeHtml,
12} from '../../client-utils.js';
13import { toArray, toText } from '../../value-utils.js';
14import '../../../shared/support-core.js';
15
16const globalScope = typeof window !== 'undefined' ? window : globalThis;
17
18const supportCore = globalThis.SUPPORT_CORE || {};
19
20const SUPPORT_SEARCH_CANONICAL_PATH = supportCore.SUPPORT_SEARCH_CANONICAL_PATH || '/support/search';
21const SUPPORT_ARTICLE_CANONICAL_PATH = supportCore.SUPPORT_ARTICLE_CANONICAL_PATH || '/support/article';
22
23const getPathsContext = () => {
24  const canonicalLocale = getCanonicalLocale();
25  const canonicalContext = getCanonicalContext(canonicalLocale);
26  const core = canonicalContext?.i18nCore || getI18nCore();
27  const paths = canonicalContext?.context?.pathsData || null;
28  const urlLocaleRaw = canonicalContext?.context?.urlLocaleRaw || canonicalLocale || core?.DEFAULT_LOCALE || 'en';
29  return {
30    core,
31    pathsData: paths,
32    urlLocaleRaw,
33    defaultLocale: core?.DEFAULT_LOCALE || 'en',
34  };
35};
36
37const normalizePathname = (value) => {
38  if (typeof supportCore.normalizePathname === 'function') {
39    return supportCore.normalizePathname(value, normalizePath);
40  }
41  const raw = toText(value).trim();
42  const [withoutQuery = ''] = raw.split('?');
43  const [withoutHash = ''] = withoutQuery.split('#');
44  return normalizePath(withoutHash || '');
45};
46
47const resolveSupportSearchBasePath = (form = globalScope.document?.querySelector('[data-support-search-form]')) => {
48  // The server localizes form actions even when client path translations stay unloaded.
49  const formAction = form?.getAttribute('action');
50  if (formAction) {
51    return normalizePathname(formAction);
52  }
53  const pathContext = getPathsContext();
54  if (typeof supportCore.resolveSupportSearchBasePath === 'function') {
55    const resolved = supportCore.resolveSupportSearchBasePath({
56      core: pathContext.core,
57      pathsData: pathContext.pathsData,
58      urlLocaleRaw: pathContext.urlLocaleRaw,
59      defaultLocale: pathContext.defaultLocale,
60      fallback: SUPPORT_SEARCH_CANONICAL_PATH,
61    });
62    return normalizePath(resolved || SUPPORT_SEARCH_CANONICAL_PATH);
63  }
64  return normalizePath(SUPPORT_SEARCH_CANONICAL_PATH);
65};
66
67const resolveSupportSearchPath = (query, form) => {
68  const slug = encodeURIComponent(String(query || '').trim());
69  return slug ? `${resolveSupportSearchBasePath(form)}/${slug}` : '';
70};
71
72const getSupportSearchTermFromPath = (pathname = globalScope.location?.pathname || '') => {
73  const basePath = resolveSupportSearchBasePath() || SUPPORT_SEARCH_CANONICAL_PATH;
74  if (typeof supportCore.getSupportSearchTermFromPath === 'function') {
75    return supportCore.getSupportSearchTermFromPath(pathname, basePath, {
76      normalizePathnameFn: normalizePathname,
77    });
78  }
79  return '';
80};
81
82const normalizeSupportSubcategories = (value) => {
83  if (typeof supportCore.normalizeSupportSubcategories === 'function') {
84    return supportCore.normalizeSupportSubcategories(value);
85  }
86  return toArray(value)
87    .map((subcategory) => {
88      if (!subcategory || subcategory.id === undefined || subcategory.id === null) {
89        return null;
90      }
91      const articlesRaw = toArray(subcategory.articles || subcategory.helpdesk_articles);
92      const articles = articlesRaw
93        .map((article) => {
94          if (!article || article.id === undefined || article.id === null) {
95            return null;
96          }
97          return {
98            id: article.id,
99            title: article.title || article.name || '',
100          };
101        })
102        .filter(Boolean);
103      return {
104        id: subcategory.id,
105        name: subcategory.name || '',
106        intro: subcategory.intro || '',
107        articles,
108      };
109    })
110    .filter(Boolean);
111};
112
113const bindSupportSearchForm = (form) => {
114  if (!form) {
115    return null;
116  }
117  const input = form.querySelector('[data-support-search-input]');
118  const button = form.querySelector('[data-support-search-button]');
119  if (!(input instanceof HTMLInputElement)) {
120    return null;
121  }
122
123  const handleChange = () => {
124    const value = input.value.trim();
125    if (value) {
126      button?.removeAttribute('disabled');
127      button?.classList.remove('pointer-events-none');
128    } else {
129      button?.setAttribute('disabled', '');
130      button?.classList.add('pointer-events-none');
131    }
132  };
133
134  input.addEventListener('input', handleChange);
135  handleChange();
136
137  form.addEventListener('submit', (event) => {
138    if (event) {
139      event.preventDefault();
140    }
141    const value = input.value.trim();
142    if (!value) {
143      return;
144    }
145    const href = resolveSupportSearchPath(value, form);
146    if (href) {
147      window.location.assign(href);
148    }
149  });
150
151  return { input, button, handleChange };
152};
153
154const buildSupportCategorySlugMap = (categories, formatter, i18nCore) => {
155  if (typeof supportCore.buildSupportCategorySlugMap !== 'function') {
156    return new Map();
157  }
158  return supportCore.buildSupportCategorySlugMap(categories, {
159    getName: (category) => formatUrlTitle(category?.name, formatter),
160    slugify: i18nCore?.slugifyLegacyUrl || slugifyLegacyUrl,
161  });
162};
163
164const resolveSupportCategoryHref = (slug, canonicalContext) => {
165  const slugValue = toText(slug).trim().replace(/^\/+/, '');
166  if (!slugValue || !canonicalContext || typeof supportCore.resolveSupportCategoryHref !== 'function') {
167    return '#';
168  }
169  const pathsData = canonicalContext.context?.pathsData;
170  const urlLocaleRaw = canonicalContext.context?.urlLocaleRaw;
171  const defaultLocale = canonicalContext.i18nCore?.DEFAULT_LOCALE || 'en';
172  const resolved = supportCore.resolveSupportCategoryHref(slugValue, {
173    core: canonicalContext.i18nCore,
174    pathsData,
175    urlLocaleRaw,
176    defaultLocale,
177    fallback: '#',
178  });
179  if (resolved && resolved !== '#') {
180    return resolved;
181  }
182  const currentPath = normalizePathname(globalScope.location?.pathname || '');
183  const fallbackBase =
184    typeof supportCore.resolveSupportBasePath === 'function'
185      ? supportCore.resolveSupportBasePath({
186          core: canonicalContext.i18nCore,
187          pathsData,
188          urlLocaleRaw,
189          defaultLocale,
190          fallback: currentPath || supportCore.SUPPORT_CANONICAL_PATH || '/support',
191        })
192      : currentPath || supportCore.SUPPORT_CANONICAL_PATH || '/support';
193  const normalizedBase = normalizePath(fallbackBase || currentPath || supportCore.SUPPORT_CANONICAL_PATH || '/support');
194  const basePath = normalizedBase === '/' ? '' : normalizedBase.replace(/\/+$/g, '');
195  return `${basePath}/${slugValue}`;
196};
197
198const resolveSupportArticleBasePath = (canonicalContext) => {
199  if (!canonicalContext || typeof supportCore.resolveSupportArticleBasePath !== 'function') {
200    return SUPPORT_ARTICLE_CANONICAL_PATH;
201  }
202  const resolved = supportCore.resolveSupportArticleBasePath({
203    core: canonicalContext.i18nCore,
204    pathsData: canonicalContext.context?.pathsData,
205    urlLocaleRaw: canonicalContext.context?.urlLocaleRaw,
206    defaultLocale: canonicalContext.i18nCore?.DEFAULT_LOCALE || 'en',
207    fallback: SUPPORT_ARTICLE_CANONICAL_PATH,
208  });
209  return normalizePath(resolved || SUPPORT_ARTICLE_CANONICAL_PATH);
210};
211
212const resolveSupportArticleId = ({
213  attrValue,
214  pathname = globalScope.location?.pathname || '',
215  canonicalContext,
216} = {}) => {
217  if (typeof supportCore.resolveSupportArticleIdFromPath === 'function') {
218    return supportCore.resolveSupportArticleIdFromPath({
219      attrValue,
220      pathname,
221      basePath: resolveSupportArticleBasePath(canonicalContext),
222      slugify: slugifyLegacyArticleUrl,
223      normalizePathnameFn: normalizePathname,
224    });
225  }
226  return null;
227};
228
229const hasObjectEntries = (value) =>
230  Boolean(value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length);
231
232const hasSupportArticleFormatterData = (canonicalContext) =>
233  hasObjectEntries(canonicalContext?.context?.articlesData) && hasObjectEntries(canonicalContext?.context?.pathsData);
234
235const resolveCanonicalArticleTitle = (id, canonicalContext, fallbackTitle) => {
236  if (!canonicalContext || typeof supportCore.resolveCanonicalSupp
236ortArticleTitle !== 'function') {
237    return fallbackTitle || '';
238  }
239  return supportCore.resolveCanonicalSupportArticleTitle(id, canonicalContext.context, fallbackTitle, {
240    core: canonicalContext.i18nCore,
241  });
242};
243
244const resolveArticleCanonicalTitle = (article, canonicalContext, fallbackTitle) => {
245  const rawExplicitTitle = toText(article?.canonicalTitle || '');
246  const explicitTitle = canonicalContext?.i18nCore?.replaceApplicationHostToken
247    ? canonicalContext.i18nCore.replaceApplicationHostToken(rawExplicitTitle, canonicalContext.context?.root)
248    : rawExplicitTitle;
249  return explicitTitle || resolveCanonicalArticleTitle(article?.id, canonicalContext, fallbackTitle);
250};
251
252const buildArticleHref = (id, canonicalTitle, canonicalContext) => {
253  if (!canonicalContext || typeof supportCore.buildSupportArticleHref !== 'function') {
254    return '#';
255  }
256  return supportCore.buildSupportArticleHref(id, canonicalTitle, canonicalContext.context, {
257    core: canonicalContext.i18nCore,
258  });
259};
260
261const reInitEmbla = (section) => {
262  const embla = section?.__supportEmbla;
263  if (embla && typeof embla.reInit === 'function') {
264    embla.reInit();
265  }
266};
267
268const renderSupportArticleCards = ({
269  items,
270  formatter,
271  canonicalContext,
272  list,
273  templateItem,
274  section,
275  emptySection,
276  imageResolver,
277  imageRenderer,
278  imageSizes,
279  safeAttr,
280  limit,
281} = {}) => {
282  if (!list || !templateItem) {
283    return false;
284  }
285
286  const arr = Array.isArray(items) ? items : [];
287  const limited = Number.isFinite(limit) ? arr.slice(0, limit) : arr;
288
289  if (!limited.length) {
290    if (section) {
291      section.setAttribute('hidden', '');
292    }
293    list.replaceChildren();
294    if (emptySection) {
295      emptySection.removeAttribute('hidden');
296    }
297    reInitEmbla(section);
298    return true;
299  }
300
301  if (section) {
302    section.removeAttribute('hidden');
303  }
304  if (emptySection) {
305    emptySection.setAttribute('hidden', '');
306  }
307
308  const resolveImage =
309    typeof imageResolver === 'function' ? imageResolver : (article) => article?.coverImage || article?.image;
310  const sanitize = typeof safeAttr === 'function' ? safeAttr : (value) => value;
311
312  const fragment = document.createDocumentFragment();
313
314  limited.forEach((article) => {
315    if (!article || article.id === undefined || article.id === null) {
316      return;
317    }
318    const titleText = formatter.toText(article.title || article.name || '');
319    if (!titleText) {
320      return;
321    }
322    const canonicalTitle = resolveArticleCanonicalTitle(article, canonicalContext, titleText);
323    const href = buildArticleHref(article.id, canonicalTitle, canonicalContext);
324    if (!href || href === '#') {
325      return;
326    }
327
328    const card = templateItem.cloneNode(true);
329    if (!(card instanceof Element)) {
330      return;
331    }
332
333    const cardRoot = card.querySelector('[data-support-article-card]');
334    if (cardRoot) {
335      cardRoot.setAttribute('data-id', String(article.id));
336    }
337
338    const titleEl = card.querySelector('[data-support-article-title]');
339    if (titleEl) {
340      titleEl.textContent = titleText;
341    }
342
343    card.querySelectorAll('[data-support-article-link]').forEach((link) => {
344      link.setAttribute('href', sanitize(href));
345    });
346
347    const arrow = card.querySelector('[data-support-article-arrow]');
348    if (arrow) {
349      arrow.setAttribute('href', sanitize(href));
350      arrow.setAttribute('aria-label', titleText);
351    }
352
353    const tagEl = card.querySelector('[data-support-article-tag]');
354    if (tagEl) {
355      const tagText = formatter.toText(article.tag || '');
356      tagEl.textContent = tagText ? `#${tagText.replace(/^#+/, '')}` : '';
357    }
358
359    const imageEl = card.querySelector('[data-support-article-image]');
360    if (imageEl) {
361      const imageUrl = resolveImage(article);
362      if (imageUrl) {
363        const safeImageUrl = sanitize(imageUrl);
364        if (imageSizes) {
365          imageEl.setAttribute('sizes', imageSizes);
366        }
367        const rendered =
368          typeof imageRenderer === 'function'
369            ? imageRenderer(imageEl, { sourceUrl: safeImageUrl, altText: titleText, article })
370            : false;
371        if (!rendered) {
372          imageEl.setAttribute('src', safeImageUrl);
373          imageEl.setAttribute('alt', titleText);
374        }
375      } else {
376        imageEl.remove();
377      }
378    }
379
380    fragment.appendChild(card);
381  });
382
383  list.replaceChildren(fragment);
384  reInitEmbla(section);
385
386  return true;
387};
388
389const buildFaqAnswerHtml = (answer, formatter) => {
390  const raw = answer === undefined || answer === null ? '' : String(answer);
391  if (!raw) {
392    return '';
393  }
394  if (raw.includes('%newParagraph')) {
395    return raw
396      .split(/\n?%newParagraph\n?/i)
397      .map((chunk) => chunk.trim())
398      .filter(Boolean)
399      .map((chunk) => `<p>${formatter.toHtml(chunk)}</p>`)
400      .join('');
401  }
402  return formatter.toHtml(raw);
403};
404
405const buildFaqItemHtml = (faq, formatter) => {
406  if (!faq) {
407    return '';
408  }
409  const question = formatter.toText(faq.question || '');
410  const answerHtml = buildFaqAnswerHtml(faq.answer, formatter);
411  if (!question && !answerHtml) {
412    return '';
413  }
414  const i18nCore = getI18nCore();
415  const safeQuestion = i18nCore?.escapeHtml ? i18nCore.escapeHtml(question || '') : question || '';
416  return `
417      <div class="[&_a]:text-pink pb-5 leading-6.5 md:flex [&_a]:hover:underline [&_a]:underline-offset-2 [&_a]:focus:outline-none [&_a]:focus:underline [&_a]:focus-visible:underline">
418        <h5 class="mb-2 font-bold md:w-1/3 md:pr-3">${safeQuestion}</h5>
419        <div class="space-y-7 md:w-2/3 md:pl-3">${answerHtml}</div>
420      </div>
421    `;
422};
423
424const bindFaqToggleLabels = (faqSection) => {
425  if (!faqSection) {
426    return;
427  }
428  faqSection.querySelectorAll('[data-support-faq-toggle-label]').forEach((label) => {
429    if (!(label instanceof Element)) {
430      return;
431    }
432    if (label.dataset.supportFaqBound === 'true') {
433      return;
434    }
435    label.dataset.supportFaqBound = 'true';
436    label.addEventListener('click', () => {
437      const targetId = label.getAttribute('data-scroll-target');
438      if (!targetId) {
439        return;
440      }
441      requestAnimationFrame(() => {
442        const target = document.getElementById(targetId);
443        target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
444      });
445    });
446  });
447};
448
449const renderFaqSection = ({ faqs, formatter, locale, faqSection, faqContent, translationKeys = {} }) => {
450  if (!faqSection || !faqContent) {
451    return false;
452  }
453  const list = toArray(faqs);
454  if (!list.length) {
455    faqSection.setAttribute('hidden', '');
456    faqContent.replaceChildren();
457    return true;
458  }
459
460  faqSection.removeAttribute('hidden');
461
462  const titleTemplate = getTranslationTemplate(translationKeys.title || 'backend.frontend.other.quickAnswers.title');
463  const checkOtherTemplate = getTranslationTemplate(
464    translationKeys.checkOther || 'backend.frontend.other.quickAnswers.needMore',
465  );
466  const showMoreTemplate = getTranslationTemplate(
467    translationKeys.showMore || 'backend.frontend.other.quickAnswers.showMore',
468  );
469
470  const title = resolveTemplateText(titleTemplate, locale);
471  const checkOther = resolveTemplateText(checkOtherTemplate, locale);
472  const showMore = resolveTemplateText(showMoreTemplate, locale);
473
474  const i18nCore = getI18nCore();
475  const safeText = (value) => (i18nCore?.escapeHtml ? i18nCore.escapeHtml(value || '') : value || '');
476  const safeTitle = safeText(title);
477  const safeCheckOther = safeText(checkOther);
478  const safeShowMore = safeText(showMore);
479
480  const initialVisible = 2;
481  const initialExpansion = 3;
482  const chunkSize = 3;
483  const faqToggleId = 'support-faq-toggle';
484  const firstExpansionId = `${faqToggleId}-content`;
485
486  const initialFaqs = list.slice(0, initialVisible);
487  const firstExpansion = list.slice(initialVisible, initialVisible + initialExpansion);
488  const remainingFaqs = list.slice(initialVisible + initialExpansion);
489  const extraChunks = Array.from({ length: Math.ceil(remainingFaqs.length / chunkSize) }, (_, idx) =>
490    remainingFaqs.slice(idx * chunkSize, (idx + 1) * chunkSize),
491  );
492
493  const renderMore = (chunkIdx) => {
494    const inputId = `${faqToggleId}-more-${chunkIdx}`;
495    const items = extraChunks[chunkIdx] || [];
496    const itemsHtml = items
497      .map((item) => buildFaqItemHtml(item, formatter))
498      .filter(Boolean)
499      .join('');
500    if (!itemsHtml) {
501      return '';
502    }
503    const nextBlock =
504      chunkIdx + 1 < extraChunks.length ? `<div class="list-none">${renderMore(chunkIdx + 1)}</div>` : '';
505    return `
506        <input id="${inputId}" type="checkbox" class="peer sr-only" />
507        <div class="border-t-background flex justify-center border-t pt-5 peer-checked:hidden">
508          <label
509            class="shadow-button border-yellow bg-yellow shadow-yellow/20 hover:bg-yellow-light focus-visible:bg-yellow-light focus-visible:ring-yellow/20 focus-visible:ring-4 pointer-events-auto mb-4 cursor-pointer rounded-md border border-solid px-3 py-1.5 text-sm font-semibold text-foreground"
510            for="${inputId}"
511          >
512            ${safeShowMore}
513          </label>
514        </div>
515        <div class="hidden peer-checked:block">
516          <div class="space-y-5 divide-y border-t pt-5">${itemsHtml}</div>
517          ${nextBlock}
518        </div>
519      `;
520  };
521
522  const initialHtml = initialFaqs
523    .map((item) => buildFaqItemHtml(item, formatter))
524    .filter(Boolean)
525    .join('');
526  const firstExpansionHtml = firstExpansion
527    .map((item) => buildFaqItemHtml(item, formatter))
528    .filter(Boolean)
529    .join('');
530  const extraHtml = extraChunks.length ? `<div class="list-none">${renderMore(0)}</div>` : '';
531
532  const headerToggle =
533    list.length > initialVisible
534      ? `
535          <div class="md:w-2/3 md:pl-3">
536            <label
537              class="inline-block text-pink font-bold underline-offset-2 hover:underline focus-visible:underline py-2 cursor-pointer"
538              data-support-faq-toggle-label
539              data-scroll-target="${firstExpansionId}"
540              for="${faqToggleId}"
541            >
542              ${safeCheckOther}
543            </label>
544          </div>
545        `
546      : '';
547
548  const expansionBlock = firstExpansionHtml
549    ? `
550        <div id="${firstExpansionId}" class="scroll-mt-header hidden peer-checked:block">
551          <div class="space-y-5 divide-y border-t pt-5">${firstExpansionHtml}</div>
552          ${extraHtml}
553        </div>
554      `
555    : '';
556
557  setSafeHtml(
558    faqContent,
559    `
560      <input id="${faqToggleId}" type="checkbox" class="peer sr-only" />
561
562      <div class="mb-5 md:flex peer-checked:[&_label]:hidden">
563        <h2 class="mb-5 text-2xl/7 font-extrabold md:w-1/3 md:pr-3 md:text-3xl/9.5">${safeTitle}</h2>
564        ${headerToggle}
565      </div>
566
567      <div class="space-y-5 divide-y">${initialHtml}</div>
568      ${expansionBlock}
569    `,
570  );
571
572  bindFaqToggleLabels(faqSection);
573
574  return true;
575};
576
577export {
578  resolveSupportSearchPath,
579  resolveSupportSearchBasePath,
580  getSupportSearchTermFromPath,
581  normalizeSupportSubcategories,
582  bindSupportSearchForm,
583  buildSupportCategorySlugMap,
584  resolveSupportCategoryHref,
585  resolveSupportArticleBasePath,
586  resolveSupportArticleId,
587  hasSupportArticleFormatterData,
588  resolveCanonicalArticleTitle,
589  resolveArticleCanonicalTitle,
590  buildArticleHref,
591  renderSupportArticleCards,
592  renderFaqSection,
593  bindFaqToggleLabels,
594};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.