1(function () { 2 'use strict'; 3 4 var config = window.sstOrderHelpChatConfig || {}; 5 var topics = []; 6 var topicMap = {}; 7 var rootId = 'sst-order-help-chat'; 8 var whatsappNumber = typeof config.whatsappNumber === 'string' 9 ? config.whatsappNumber.replace(/\D+/g, '') 10 : ''; 11 var visitedTopics = []; 12 var lastLookupNumber = ''; 13 var positionStorageKey = 'sstOhcPosV1'; 14 15 function syncTopics() { 16 topics = Array.isArray(config.topics) ? config.topics : []; 17 topicMap = {}; 18 topics.forEach(function (topic) { 19 if (topic && topic.id) { 20 topicMap[String(topic.id)] = topic; 21 } 22 }); 23 } 24 25 syncTopics(); 26 27 if (!topics.length || document.getElementById(rootId)) { 28 return; 29 } 30 31 function track(name, params) { 32 try { 33 var payload = params || {}; 34 if (typeof window.gtag === 'function') { 35 window.gtag('event', name, payload); 36 } else if (window.dataLayer && typeof window.dataLayer.push === 'function') { 37 var entry = { event: name }; 38 for (var key in payload) { 39 if (Object.prototype.hasOwnProperty.call(payload, key)) { 40 entry[key] = payload[key]; 41 } 42 } 43 window.dataLayer.push(entry); 44 } 45 } catch (error) { 46 // Analytics must never break the chat. 47 } 48 try { 49 // Server-side usage counter (wp_sst_ohc_stats). The GA4 events above 50 // only reach Google if GTM forwards them; this beacon is the in-house 51 // record and works with no tag manager configuration at all. 52 if (!config.ajaxUrl) { 53 return; 54 } 55 var label = ''; 56 if (name === 'ohc_topic') { 57 label = String((params && params.topic_id) || ''); 58 } else if (name === 'ohc_feedback') { 59 label = String((params && params.topic_id) || '') + ':' + String((params && params.helpful) || ''); 60 } else if (name === 'ohc_lookup') { 61 label = String((params && params.result) || ''); 62 } else if (name === 'ohc_lang') { 63 label = String((params && params.lang) || ''); 64 } 65 var body = new FormData(); 66 body.append('action', 'sst_ohc_track'); 67 body.append('event', name); 68 if (label) { 69 body.append('label', label); 70 } 71 if (navigator.sendBeacon) { 72 navigator.sendBeacon(config.ajaxUrl, body); 73 } else if (window.fetch) { 74 window.fetch(config.ajaxUrl, { method: 'POST', body: body, keepalive: true, credentials: 'same-origin' }); 75 } 76 } catch (error) { 77 // Counting must never break the chat. 78 } 79 } 80 81 function text(value, fallback) { 82 if (typeof value === 'string' && value.trim()) { 83 return value; 84 } 85 86 return fallback || ''; 87 } 88 89 function createElement(tagName, className, content) { 90 var element = document.createElement(tagName); 91 if (className) { 92 element.className = className; 93 } 94 if (typeof content === 'string') { 95 element.textContent = content; 96 } 97 return element; 98 } 99 100 function closestElement(target, selector) { 101 if (!target) { 102 return null; 103 } 104 105 if (target.closest) { 106 return target.closest(selector); 107 } 108 109 if (target.parentElement && target.parentElement.closest) { 110 return target.parentElement.closest(selector); 111 } 112 113 return null; 114 } 115 116 function focusElement(element) { 117 if (!element || typeof element.focus !== 'function') { 118 return; 119 } 120 121 try { 122 element.focus({ preventScroll: true }); 123 } catch (error) { 124 element.focus(); 125 } 126 } 127 128 function isHexColor(value) { 129 return typeof value === 'string' && /^#[0-9a-f]{6}$/i.test(value); 130 } 131 132 function hexToRgb(hex) { 133 if (!isHexColor(hex)) { 134 return null; 135 } 136 137 return [ 138 parseInt(hex.slice(1, 3), 16), 139 parseInt(hex.slice(3, 5), 16), 140 parseInt(hex.slice(5, 7), 16) 141 ]; 142 } 143 144 function rgbToHex(rgb) { 145 return '#' + rgb.map(function (value) { 146 var channel = Math.max(0, Math.min(255, Math.round(value))); 147 return channel.toString(16).padStart(2, '0'); 148 }).join(''); 149 } 150 151 function mixHex(a, b, amount) { 152 var rgbA = hexToRgb(a); 153 var rgbB = hexToRgb(b); 154 if (!rgbA || !rgbB) { 155 return a; 156 } 157 158 return rgbToHex(rgbA.map(function (channel, index) { 159 return channel * (1 - amount) + rgbB[index] * amount; 160 })); 161 } 162 163 function luminance(hex) { 164 var rgb = hexToRgb(hex); 165 if (!rgb) { 166 return 0; 167 } 168 169 var linear = rgb.map(function (value) { 170 value = value / 255; 171 return value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4); 172 }); 173 174 return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]; 175 } 176 177 function contrastRatio(a, b) { 178 var lumA = luminance(a); 179 var lumB = luminance(b); 180 var high = Math.max(lumA, lumB); 181 var low = Math.min(lumA, lumB); 182 return (high + 0.05) / (low + 0.05); 183 } 184 185 function applyAppearance(rootElement) { 186 var appearance = config.appearance || {}; 187 var allowedPositions = { 188 'bottom-right': true, 189 'bottom-left': true, 190 'top-right': true, 191 'top-left': true 192 }; 193 var position = allowedPositions[appearance.position] ? appearance.position : 'bottom-right'; 194 195 rootElement.setAttribute('data-position', position); 196 197 if (!isHexColor(appearance.accentColor)) { 198 return; 199 } 200 201 var accent = appearance.accentColor; 202 var lightInk = '#fbf7fa'; 203 var darkInk = '#111827'; 204 var accentInk = contrastRatio(lightInk, accent) >= 4.5 ? lightInk : darkInk; 205 206 rootElement.style.setProperty('--sst-ohc-admin-accent', accent); 207 rootElement.style.setProperty('--sst-ohc-admin-accent-hover', mixHex(accent, '#000000', 0.22)); 208 rootElement.style.setProperty('--sst-ohc-admin-accent-soft', mixHex(accent, '#ffffff', 0.9)); 209 rootElement.style.setProperty('--sst-ohc-admin-accent-ink', accentInk); 210 rootElement.style.setProperty('--sst-ohc-admin-focus', mixHex(accent, '#ffffff', 0.18)); 211 } 212 213 function rememberTopic(topic) { 214 if (!topic || !topic.id) { 215 return; 216 } 217 218 for (var i = 0; i < visitedTopics.length; i++) { 219 if (visitedTopics[i].id === topic.id) { 220 return; 221 } 222 } 223 224 visitedTopics.push({ 225 id: topic.id, 226 summary: text(topic.title, text(topic.label, topic.id)) 227 }); 228 } 229 230 function buildWhatsAppText() { 231 var lines = [ 232 text(config.whatsappGreeting, 'Hello SAMURAI SWORD TOKYO support. I used the order help chat on your site, but my question is not solved yet.') 233 ]; 234 235 if (visitedTopics.length) { 236 lines.push(''); 237 lines.push(text(config.whatsappTopicsIntro, 'Questions I checked in the chat:')); 238 visitedTopics.forEach(function (topic, index) { 239 lines.push((index + 1) + '. ' + topic.summary); 240 }); 241 } else { 242 lines.push(''); 243 lines.push(text(config.whatsappNoTopics, 'I could not find my question in the chat topics.')); 244 } 245 246 if (lastLookupNumber) { 247 lines.push(''); 248 lines.push(text(config.whatsappOrderLine, 'My order number:') + ' #' + lastLookupNumber); 249 } 250 251 lines.push(''); 252 lines.push('Page: ' + window.location.href); 253 254 return lines.join('\n'); 255 } 256
257 function buildWhatsAppUrl() { 258 return 'https://wa.me/' + whatsappNumber + '?text=' + encodeURIComponent(buildWhatsAppText()); 259 } 260 261 function buildWhatsAppLink(label) { 262 var link = createElement('a', 'sst-ohc-action sst-ohc-action--whatsapp'); 263 link.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/></svg>'; 264 link.appendChild(createElement('span', '', label)); 265 link.href = buildWhatsAppUrl(); 266 link.target = '_blank'; 267 link.rel = 'noopener noreferrer'; 268 link.setAttribute('data-whatsapp', '1'); 269 return link; 270 } 271 272 function buildEscalationTrigger() { 273 var trigger = createElement('button', 'sst-ohc-escalate-trigger', text(config.escalationLabel, 'Still not solved?')); 274 trigger.type = 'button'; 275 trigger.setAttribute('data-escalate', '1'); 276 return trigger; 277 } 278 279 function resolveUrl(action) { 280 if (!action) { 281 return ''; 282 } 283 284 if (typeof action.url === 'string' && action.url) { 285 return action.url; 286 } 287 288 if (action.urlKey && config.urls && typeof config.urls[action.urlKey] === 'string') { 289 return config.urls[action.urlKey]; 290 } 291 292 return ''; 293 } 294 295 function appendBody(container, body) { 296 var paragraphs = Array.isArray(body) ? body : [body]; 297 298 paragraphs.forEach(function (paragraph) { 299 var value = text(paragraph, ''); 300 if (!value) { 301 return; 302 } 303 304 container.appendChild(createElement('p', 'sst-ohc-message__text', value)); 305 }); 306 } 307 308 function buildBotAvatar() { 309 var avatar = createElement('span', 'sst-ohc-avatar', 'S'); 310 avatar.setAttribute('aria-hidden', 'true'); 311 return avatar; 312 } 313 314 function buildBotRow(content) { 315 var row = createElement('div', 'sst-ohc-row sst-ohc-row--bot'); 316 row.appendChild(buildBotAvatar()); 317 row.appendChild(content); 318 return row; 319 } 320 321 function buildAction(action) { 322 var label = text(action && action.label, ''); 323 if (!label) { 324 return null; 325 } 326 327 if (action.type === 'topic' && action.target && topicMap[action.target]) { 328 var topicButton = createElement('button', 'sst-ohc-action', label); 329 topicButton.type = 'button'; 330 topicButton.setAttribute('data-topic-id', action.target); 331 return topicButton; 332 } 333 334 if (action.type === 'link') { 335 if (action.urlKey === 'contact' && whatsappNumber) { 336 return buildWhatsAppLink(label); 337 } 338 339 var url = resolveUrl(action); 340 if (!url) { 341 return null; 342 } 343 344 var link = createElement('a', 'sst-ohc-action sst-ohc-action--link', label); 345 link.href = url; 346 return link; 347 } 348 349 return null; 350 } 351 352 function buildChoice(topic) { 353 var button = createElement('button', 'sst-ohc-choice'); 354 var label = createElement('span', 'sst-ohc-choice__label', text(topic.label, topic.title || topic.id)); 355 var arrow = createElement('span', 'sst-ohc-choice__arrow', 'âº'); 356 357 button.type = 'button'; 358 button.setAttribute('data-topic-id', topic.id); 359 arrow.setAttribute('aria-hidden', 'true'); 360 button.appendChild(label); 361 button.appendChild(arrow); 362 363 return button; 364 } 365 366 var root = createElement('div', 'sst-order-help-chat'); 367 root.id = rootId; 368 root.setAttribute('data-open', '0'); 369 applyAppearance(root); 370 371 var launcher = createElement('button', 'sst-ohc-launcher'); 372 var launcherIcon = createElement('span', 'sst-ohc-launcher__icon'); 373 var launcherLabel = createElement('span', 'sst-ohc-launcher__label', text(config.launcherLabel, 'Order help')); 374 launcher.type = 'button'; 375 launcher.setAttribute('aria-label', text(config.launcherLabel, 'Order help')); 376 launcher.setAttribute('aria-expanded', 'false');
377 launcher.setAttribute('aria-controls', 'sst-ohc-panel'); 378 launcherIcon.setAttribute('aria-hidden', 'true'); 379 launcher.appendChild(launcherIcon); 380 launcher.appendChild(launcherLabel); 381 382 var panel = createElement('section', 'sst-ohc-panel'); 383 panel.id = 'sst-ohc-panel'; 384 panel.setAttribute('role', 'dialog'); 385 panel.setAttribute('aria-modal', 'false'); 386 panel.setAttribute('aria-labelledby', 'sst-ohc-title'); 387 panel.setAttribute('aria-hidden', 'true'); 388 389 var header = createElement('div', 'sst-ohc-header'); 390 var headingWrap = createElement('div', 'sst-ohc-heading'); 391 var headingCopy = createElement('div', 'sst-ohc-heading__copy'); 392 var title = createElement('h2', 'sst-ohc-title', text(config.title, 'Order help')); 393 title.id = 'sst-ohc-title'; 394 var subtitle = createElement('p', 'sst-ohc-subtitle'); 395 var statusDot = createElement('span', 'sst-ohc-status-dot'); 396 statusDot.setAttribute('aria-hidden', 'true'); 397 subtitle.appendChild(statusDot); 398 subtitle.appendChild(document.createTextNode(text(config.subtitle, 'SAMURAI SWORD TOKYO Support'))); 399 var closeButton = createElement('button', 'sst-ohc-close', 'Ã'); 400 closeButton.type = 'button'; 401 closeButton.setAttribute('aria-label', text(config.closeLabel, 'Close')); 402 403 headingCopy.appendChild(title); 404 headingCopy.appendChild(subtitle); 405 headingWrap.appendChild(buildBotAvatar()); 406 headingWrap.appendChild(headingCopy); 407 header.appendChild(headingWrap); 408 header.appendChild(closeButton); 409 410 var body = createElement('div', 'sst-ohc-body'); 411 412 panel.appendChild(header); 413 panel.appendChild(body); 414 root.appendChild(launcher); 415 root.appendChild(panel); 416 document.body.appendChild(root); 417 418 function setOpen(open) { 419 if (open && root.getAttribute('data-open') !== '1') { 420 track('ohc_open', {}); 421 } 422 root.setAttribute('data-open', open ? '1' : '0'); 423 launcher.setAttribute('aria-expanded', open ? 'true' : 'false'); 424 panel.setAttribute('aria-hidden', open ? 'false' : 'true'); 425 426 // Phones: freeze the page behind the sheet so nothing shifts while 427 // choosing a topic. Desktop keeps normal page scroll. 428 if (isMobileViewport()) { 429 document.documentElement.classList.toggle('sst-ohc-lock', open); 430 } else { 431 document.documentElement.classList.remove('sst-ohc-lock'); 432 } 433 434 // Auto-focus is a keyboard-navigation aid; on touch it just paints a 435 // focus ring and can nudge the scroll position. 436 var fineDesktopPointer = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches; 437 if (open && fineDesktopPointer) { 438 window.setTimeout(function () { 439 var firstChoice = body.querySelector('button, a'); 440 if (firstChoice) { 441 focusElement(firstChoice); 442 } 443 }, 30); 444 } 445 } 446 447 function contextTopicId() { 448 var cls = document.body.className || ''; 449 if (cls.indexOf('woocommerce-checkout') !== -1 || cls.indexOf('woocommerce-cart') !== -1) { 450 return 'payment-transfer-help'; 451 } 452 if (cls.indexOf('single-product') !== -1) { 453 return 'stock-price'; 454 } 455 return ''; 456 } 457 458 function buildLookupChoice() { 459 var button = createElement('button', 'sst-ohc-choice sst-ohc-choice--lookup'); 460 var label = createElement('span', 'sst-ohc-choice__label', text(config.lookupLabel, 'Check my order status')); 461 var arrow = createElement('span', 'sst-ohc-choice__arrow', 'âº'); 462 463 button.type = 'button'; 464 button.setAttribute('data-lookup', '1'); 465 arrow.setAttribute('aria-hidden', 'true'); 466 button.appendChild(label); 467 button.appendChild(arrow); 468 469 return button; 470 } 471
472 function renderStart() { 473 body.textContent = ''; 474 475 var intro = createElement('div', 'sst-ohc-message sst-ohc-message--intro'); 476 intro.appendChild(createElement('h3', 'sst-ohc-message__title', text(config.welcomeTitle, 'How can we help?'))); 477 intro.appendChild(createElement('p', 'sst-ohc-message__text', text(config.welcomeBody, config.subtitle || 'Choose a topic below.'))); 478 body.appendChild(buildBotRow(intro)); 479 480 var list = createElement('div', 'sst-ohc-choices'); 481 list.appendChild(buildLookupChoice()); 482 483 var ordered = topics.slice(); 484 var contextId = contextTopicId(); 485 if (contextId && topicMap[contextId]) { 486 ordered = [topicMap[contextId]].concat(ordered.filter(function (topic) { 487 return topic.id !== contextId; 488 })); 489 } 490 ordered.forEach(function (topic) { 491 list.appendChild(buildChoice(topic)); 492 }); 493 494 body.appendChild(list); 495 } 496 497 function renderLookupResult(wrap, data) { 498 lastLookupNumber = String(data.number || ''); 499 500 var card = createElement('div', 'sst-ohc-message sst-ohc-message--result'); 501 card.appendChild(createElement('h3', 'sst-ohc-message__title', 'Order #' + data.number)); 502 503 if (data.date) { 504 card.appendChild(createElement('p', 'sst-ohc-message__text', text(config.lookupOrderedLabel, 'Order date') + ': ' + data.date)); 505 } 506 if (data.payment) { 507 card.appendChild(createElement('p', 'sst-ohc-message__text', text(config.lookupPaymentLabel, 'Payment') + ': ' + data.payment)); 508 } 509 if (data.tracking) { 510 var trackingLine = text(config.lookupTrackingLabel, 'Tracking number') + ': ' + data.tracking + (data.carrier ? ' (' + data.carrier + ')' : ''); 511 card.appendChild(createElement('p', 'sst-ohc-message__text', trackingLine)); 512 } 513 514 var messages = config.lookupMessages || {}; 515 var statusMessage = typeof messages[data.group] === 'string' ? messages[data.group] : (messages.other || ''); 516 if (statusMessage) { 517 card.appendChild(createElement('p', 'sst-ohc-message__text sst-ohc-message__text--status', statusMessage)); 518 } 519 520 wrap.appendChild(buildBotRow(card)); 521 } 522 523 function renderLookup() { 524 rememberTopic({ id: 'order-lookup', title: text(config.lookupTitle, 'Order status lookup') }); 525 track('ohc_lookup_open', {}); 526 body.textContent = ''; 527 528 var message = createElement('div', 'sst-ohc-message'); 529 message.appendChild(createElement('h3', 'sst-ohc-message__title', text(config.lookupTitle, 'Check your order status'))); 530 appendBody(message, [text(config.lookupBody, 'Enter your order number and the email you used at checkout.')]); 531 body.appendChild(buildBotRow(message)); 532 533 var form = createElement('form', 'sst-ohc-lookup'); 534 var orderInput = createElement('input', 'sst-ohc-input'); 535 orderInput.type = 'text'; 536 orderInput.inputMode = 'numeric'; 537 orderInput.autocomplete = 'off'; 538 orderInput.required = true; 539 orderInput.placeholder = text(config.lookupOrderPlaceholder, 'Order number (e.g. 41234)'); 540 541 var emailInput = createElement('input', 'sst-ohc-input'); 542 emailInput.type = 'email'; 543 emailInput.autocomplete = 'email'; 544 emailInput.required = true; 545 emailInput.placeholder = text(config.lookupEmailPlaceholder, 'Email used at checkout'); 546 547 var submit = createElement('button', 'sst-ohc-action sst-ohc-action--link', text(config.lookupSubmit, 'Check status')); 548 submit.type = 'submit'; 549 550 var note = createElement('p', 'sst-ohc-lookup__note', ''); 551 552 form.appendChild(orderInput); 553 form.appendChild(emailInput); 554 form.appendChild(submit); 555 form.appendChild(note); 556 body.appendChild(form); 557 558 var resultWrap = createElement('div', 'sst-ohc-lookup-result'); 559 body.appendChild(resultWrap); 560 561 var escalateWrap = createElement('div', 'sst-ohc-escalate'); 562 escalateWrap.appendChild(buildEscalationTrigger()); 563 body.appendChild(escalateWrap); 564 565 var back = createElement('button', 'sst-ohc-back', text(config.homeLabel, 'Back to topics')); 566 back.type = 'button'; 567 back.setAttribute('data-back', '1'); 568 body.appendChild(back); 569 570 form.addEventListener('submit', function (event) { 571 event.preventDefault(); 572 note.textContent = ''; 573 resultWrap.textContent = ''; 574 submit.disabled = true; 575 submit.textContent = text(config.lookupChecking, 'Checkingâ¦'); 576 577 var params = new URLSearchParams(); 578 params.set('action', 'sst_ohc_lookup'); 579 params.set('order', orderInput.value); 580 params.set('email', emailInput.value); 581 582 function restoreSubmit() { 583 submit.disabled = false;
584 submit.textContent = text(config.lookupSubmit, 'Check status'); 585 } 586 587 window.fetch(config.ajaxUrl, { method: 'POST', body: params, credentials: 'same-origin' }) 588 .then(function (res) { return res.json(); }) 589 .then(function (json) { 590 restoreSubmit(); 591 if (json && json.success && json.data && json.data.number) { 592 renderLookupResult(resultWrap, json.data); 593 track('ohc_lookup', { result: 'found', status_group: json.data.group }); 594 } else { 595 note.textContent = text(config.lookupNotFound, 'We could not find an order with that number and email.'); 596 track('ohc_lookup', { result: 'not_found' }); 597 } 598 }) 599 .catch(function () { 600 restoreSubmit(); 601 note.textContent = text(config.lookupError, 'Something went wrong. Please try again in a moment.'); 602 track('ohc_lookup', { result: 'error' }); 603 }); 604 }); 605 606 window.setTimeout(function () { 607 focusElement(orderInput); 608 }, 30); 609 } 610 611 function buildContactEscalationUrl(baseUrl) { 612 try { 613 var url = new URL(baseUrl, window.location.origin); 614 url.searchParams.set('topic', 'help_chat'); 615 if (lastLookupNumber) { 616 url.searchParams.set('order', lastLookupNumber); 617 } 618 url.hash = 'contact-form'; 619 return url.toString(); 620 } catch (error) { 621 // URL() unsupported or malformed base: plain contact link still works. 622 return baseUrl; 623 } 624 } 625 626 function renderEscalation() { 627 track('ohc_escalate', {}); 628 body.textContent = ''; 629 630 var message = createElement('div', 'sst-ohc-message'); 631 message.appendChild(createElement('h3', 'sst-ohc-message__title', text(config.escalationTitle, 'Talk to our team'))); 632 appendBody(message, [ 633 text(config.escalationBody, 'Send us a message through our contact form and a member of our team will reply by email. Support hours: Mon-Fri 11:00-18:00 JST.') 634 ]); 635 body.appendChild(buildBotRow(message)); 636 637 var actions = createElement('div', 'sst-ohc-actions'); 638 if (whatsappNumber) { 639 actions.appendChild(buildWhatsAppLink(text(config.escalationCta, 'Message us on WhatsApp'))); 640 } else { 641 var contactUrl = config.urls && typeof config.urls.contact === 'string' ? config.urls.contact : ''; 642 if (contactUrl) { 643 var link = createElement('a', 'sst-ohc-action sst-ohc-action--whatsapp', text(config.escalationCta, 'Contact support')); 644 link.href = buildContactEscalationUrl(contactUrl); 645 actions.appendChild(link); 646 } 647 } 648 if (actions.children.length) { 649 body.appendChild(actions); 650 } 651 652 var hours = text(config.escalationHours, ''); 653 if (hours) { 654 body.appendChild(createElement('p', 'sst-ohc-hours', hours)); 655 } 656 657 var back = createElement('button', 'sst-ohc-back', text(config.homeLabel, 'Choose another topic')); 658 back.type = 'button'; 659 back.setAttribute('data-back', '1'); 660 body.appendChild(back); 661 } 662 663 function renderTopic(topicId) { 664 var topic = topicMap[topicId]; 665 if (!topic) { 666 renderStart(); 667 return; 668 } 669 670 rememberTopic(topic); 671 track('ohc_topic', { topic_id: topicId }); 672 body.textContent = ''; 673 674 var message = createElement('div', 'sst-ohc-message'); 675 message.appendChild(createElement('h3', 'sst-ohc-message__title', text(topic.title, topic.label))); 676 appendBody(message, topic.body); 677 body.appendChild(buildBotRow(message)); 678 679 if (Array.isArray(topic.actions) && topic.actions.length) { 680 var actions = createElement('div', 'sst-ohc-actions'); 681 topic.actions.forEach(function (action) { 682 var actionElement = buildAction(action); 683 if (actionElement) { 684 actions.appendChild(actionElement); 685 } 686 }); 687 688 if (actions.children.length) { 689 body.appendChild(actions); 690 } 691 } 692 693 var feedback = createElement('div', 'sst-ohc-feedback'); 694 feedback.appendChild(createElement('span', 'sst-ohc-feedback__prompt', text(config.feedbackPrompt, 'Did this answer your question?'))); 695 696 var yesButton = createElement('button', 'sst-ohc-feedback__btn', text(config.feedbackYes, 'Yes, thanks'));
697 yesButton.type = 'button'; 698 yesButton.addEventListener('click', function () { 699 track('ohc_feedback', { topic_id: topicId, helpful: 'yes' }); 700 feedback.textContent = ''; 701 feedback.appendChild(createElement('span', 'sst-ohc-feedback__thanks', text(config.feedbackThanks, 'Great! Feel free to check another topic.'))); 702 }); 703 704 var noButton = createElement('button', 'sst-ohc-feedback__btn sst-ohc-feedback__btn--no', text(config.feedbackNo, 'No, I need more help')); 705 noButton.type = 'button'; 706 noButton.addEventListener('click', function () { 707 track('ohc_feedback', { topic_id: topicId, helpful: 'no' }); 708 renderEscalation(); 709 }); 710 711 feedback.appendChild(yesButton); 712 feedback.appendChild(noButton); 713 body.appendChild(feedback); 714 715 var back = createElement('button', 'sst-ohc-back', text(config.homeLabel, 'Choose another topic')); 716 back.type = 'button'; 717 back.setAttribute('data-back', '1'); 718 body.appendChild(back); 719 } 720 721 function readSavedPosition() { 722 try { 723 var raw = window.localStorage.getItem(positionStorageKey); 724 if (!raw) { 725 return null; 726 } 727 var pos = JSON.parse(raw); 728 if (!pos || typeof pos.x !== 'number' || typeof pos.y !== 'number') { 729 return null; 730 } 731 return pos; 732 } catch (error) { 733 return null; 734 } 735 } 736 737 function savePosition(pos) { 738 try { 739 window.localStorage.setItem(positionStorageKey, JSON.stringify(pos)); 740 } catch (error) { 741 // Private mode / storage disabled: position just resets next visit. 742 } 743 } 744 745 function isMobileViewport() { 746 return window.matchMedia && window.matchMedia('(max-width: 32.5rem)').matches; 747 } 748 749 function clampPosition(x, y) { 750 var margin = 8; 751 var width = root.offsetWidth || 56; 752 var height = root.offsetHeight || 56; 753 var maxX = Math.max(margin, window.innerWidth - width - margin); 754 var maxY = Math.max(margin, window.innerHeight - height - margin); 755 756 // Phones: sideways drift feels wrong on small screens, so the button 757 // stays pinned to the right edge and only moves vertically. 758 if (isMobileViewport()) { 759 x = maxX; 760 } 761 762 return { 763 x: Math.min(Math.max(x, margin), maxX), 764 y: Math.min(Math.max(y, margin), maxY) 765 }; 766 } 767 768 function updatePanelOrientation() { 769 var rect = root.getBoundingClientRect(); 770 var horizontal = rect.left + rect.width / 2 <= window.innerWidth / 2 ? 'left' : 'right'; 771 var vertical = rect.top + rect.height / 2 <= window.innerHeight / 2 ? 'top' : 'bottom'; 772 root.setAttribute('data-position', vertical + '-' + horizontal); 773 } 774 775 function applyPosition(pos, persist) { 776 var clamped = clampPosition(pos.x, pos.y); 777 root.style.left = clamped.x + 'px'; 778 root.style.top = clamped.y + 'px'; 779 root.style.right = 'auto'; 780 root.style.bottom = 'auto'; 781 updatePanelOrientation(); 782 if (persist) { 783 savePosition(clamped); 784 } 785 return clamped; 786 } 787 788 var savedPosition = readSavedPosition(); 789 if (savedPosition) { 790 applyPosition(savedPosition, false); 791 } 792 793 var dragState = null; 794 var suppressClick = false; 795 796 launcher.addEventListener('pointerdown', function (event) { 797 if (typeof event.button === 'number' && event.button !== 0) { 798 return; 799 } 800 801 // Touch devices: no dragging at all â the button stays put and taps 802 // select instantly. Dragging is a desktop (mouse) feature only. 803 if (event.pointerType !== 'mouse') { 804 return; 805 } 806 807 var rect = root.getBoundingClientRect(); 808 dragState = { 809 pointerId: event.pointerId, 810 startX: event.clientX, 811 startY: event.clientY, 812 offsetX: event.clientX - rect.left, 813 offsetY: event.clientY - rect.top, 814 moved: false 815 }; 816 }); 817 818 launcher.addEventListener('pointermove', function (event) { 819 if (!dragState || event.pointerId !== dragState.pointerId) { 820 return; 821 } 822 823 if (!dragState.moved) { 824 var distance = Math.abs(event.clientX - dragState.startX) + Math.abs(event.clientY - dragState.startY); 825 if (distance < 6) { 826 return; 827 } 828 dragState.moved = true; 829 root.setAttribute('data-dragging', '1'); 830 if (launcher.setPointerCapture) { 831 try { 832 launcher.setPointerCapture(event.pointerId); 833 } catch (error) { 834 // Capture unsupported: dragging still works while pointer stays on the button. 835 } 836 } 837 } 838 839 applyPosition( 840 { 841 x: event.clientX - dragState.offsetX, 842 y: event.clientY - dragState.offsetY 843 }, 844 false 845 ); 846 }); 847 848 function endDrag(event) { 849 if (!dragState || (event && event.pointerId !== dragState.pointerId)) { 850 return; 851 } 852 853 if (dragState.moved) { 854 suppressClick = true; 855 var rect = root.getBoundingClientRect(); 856 applyPosition({ x: rect.left, y: rect.top }, true); 857 } 858 859 root.removeAttribute('data-dragging'); 860 dragState = null; 861 } 862 863 launcher.addEventListener('pointerup', endDrag);
864 launcher.addEventListener('pointercancel', endDrag); 865 866 window.addEventListener('resize', function () { 867 var pos = readSavedPosition(); 868 if (pos && root.style.left) { 869 applyPosition(pos, false); 870 } 871 }); 872 873 launcher.addEventListener('click', function () { 874 if (suppressClick) { 875 suppressClick = false; 876 return; 877 } 878 var isOpen = root.getAttribute('data-open') === '1'; 879 setOpen(!isOpen); 880 }); 881 882 closeButton.addEventListener('click', function () { 883 setOpen(false); 884 focusElement(launcher); 885 }); 886 887 body.addEventListener('click', function (event) { 888 var whatsappLink = closestElement(event.target, '[data-whatsapp]'); 889 if (whatsappLink && whatsappNumber) { 890 whatsappLink.href = buildWhatsAppUrl(); 891 track('ohc_whatsapp_click', { topics_checked: visitedTopics.length }); 892 return; 893 } 894 895 if (closestElement(event.target, '[data-escalate]')) { 896 event.preventDefault(); 897 renderEscalation(); 898 return; 899 } 900 901 if (closestElement(event.target, '[data-lookup]')) { 902 event.preventDefault(); 903 renderLookup(); 904 return; 905 } 906 907 var topicButton = closestElement(event.target, '[data-topic-id]'); 908 if (topicButton) { 909 event.preventDefault(); 910 renderTopic(topicButton.getAttribute('data-topic-id')); 911 return; 912 } 913 914 if (closestElement(event.target, '[data-back]')) { 915 event.preventDefault(); 916 renderStart(); 917 } 918 }); 919 920 document.addEventListener('keydown', function (event) { 921 if (event.key === 'Escape' && root.getAttribute('data-open') === '1') { 922 setOpen(false); 923 focusElement(launcher); 924 } 925 }); 926 927 renderStart(); 928 929 // Swap the whole flow to the visitor's language when a translation file 930 // exists. English stays the default; a failed fetch silently keeps English. 931 (function initLanguage() { 932 var urls = config.translationUrls || {}; 933 var preferred = navigator.languages && navigator.languages.length 934 ? navigator.languages 935 : [navigator.language || '']; 936 var lang = ''; 937 938 for (var i = 0; i < preferred.length; i++) { 939 var code = String(preferred[i] || '').toLowerCase().slice(0, 2); 940 if (code === 'en') { 941 return; 942 } 943 if (urls[code]) { 944 lang = code; 945 break; 946 } 947 } 948 949 if (!lang || typeof window.fetch !== 'function') { 950 return; 951 } 952 953 window.fetch(urls[lang], { credentials: 'omit' }) 954 .then(function (res) { return res.json(); }) 955 .then(function (data) { 956 if (!data || !Array.isArray(data.topics) || !data.topics.length) { 957 return; 958 } 959 960 var protectedKeys = { urls: 1, appearance: 1, whatsappNumber: 1, ajaxUrl: 1, translationUrls: 1 }; 961 for (var key in data) { 962 if (!Object.prototype.hasOwnProperty.call(data, key) || protectedKeys[key]) { 963 continue; 964 } 965 if (key === 'lookupMessages' && data[key] && typeof data[key] === 'object') { 966 config.lookupMessages = config.lookupMessages || {}; 967 for (var mk in data[key]) { 968 if (Object.prototype.hasOwnProperty.call(data[key], mk) && typeof data[key][mk] === 'string') { 969 config.lookupMessages[mk] = data[key][mk]; 970 } 971 } 972 } else { 973 config[key] = data[key]; 974 } 975 } 976 977 syncTopics(); 978 title.textContent = text(config.title, 'Order Help'); 979 subtitle.textContent = ''; 980 subtitle.appendChild(statusDot); 981 subtitle.appendChild(document.createTextNode(text(config.subtitle, 'SAMURAI SWORD TOKYO Support'))); 982 launcherLabel.textContent = text(config.launcherLabel, 'Order help'); 983 launcher.setAttribute('aria-label', text(config.launcherLabel, 'Order help')); 984 closeButton.setAttribute('aria-label', text(config.closeLabel, 'Close')); 985 renderStart(); 986 track('ohc_lang', { lang: lang }); 987 }) 988 .catch(function () { 989 // Keep English. 990 }); 991 })(); 992})();
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.