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.