PageSourceSearch

https://samuraiswordtokyo.com/wp-content/plugins/sst-order-help-chat/assets/order-help-chat.js?ver=1786013796

js samuraiswordtokyo.com collected 2026-10-06 04:17:32 UTC 33,292 bytes, 992 lines download raw bytes

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.