PageSourceSearch

https://dopper-v2.vercel.app/resources/utils.js

js dopper-v2.vercel.app collected 2026-10-03 06:45:14 UTC 12,381 bytes, 463 lines download raw bytes

1/* eslint-disable no-param-reassign */
2/* eslint-disable functional/no-loop-statement */
3/* eslint-disable functional/no-let */
4/* eslint-disable functional/no-conditional-statement */
5/* eslint-disable functional/immutable-data */
6/* eslint-disable @typescript-eslint/explicit-function-return-type */
7
8// Rich text helpers
9function escapeHTML(string) {
10  const htmlEscapes = {
11    '&': '&',
12    '<': '&lt;',
13    '>': '&gt;',
14    '"': '&quot;',
15    "'": '&#39;',
16  };
17
18  const reUnescapedHtml = /[&<>"']/g;
19  const reHasUnescapedHtml = RegExp(reUnescapedHtml.source);
20
21  return string && reHasUnescapedHtml.test(string)
22    ? string.replace(reUnescapedHtml, chr => htmlEscapes[chr])
23    : string;
24}
25
26const schema = {
27  marks: {
28    link: node => {
29      const attrs = { ...node.attrs };
30      const { linktype = 'url' } = node.attrs;
31
32      if (linktype === 'email') {
33        attrs.href = `mailto:${attrs.href}`;
34      }
35
36      if (attrs.anchor) {
37        attrs.href = `${attrs.href}#${attrs.anchor}`;
38        delete attrs.anchor;
39      }
40
41      if (attrs.custom) {
42        for (const key in attrs.custom) {
43          attrs[key] = attrs.custom[key];
44        }
45        delete attrs.custom;
46      }
47
48      return {
49        tag: [
50          {
51            tag: 'a',
52            attrs: attrs,
53          },
54        ],
55      };
56    },
57    bold: () => ({
58      tag: 'b',
59    }),
60  },
61};
62
63// Mini-storyblok rich text resolver: headings, paragraphs, links.
64class RichTextResolver {
65  constructor(schema) {
66    this.nodes = {};
67    this.marks = {};
68
69    if (schema) {
70      this.nodes = schema.nodes || {};
71      this.marks = schema.marks || {};
72    }
73  }
74
75  addNode(key, schema) {
76    this.nodes[key] = schema;
77  }
78
79  addMark(key, schema) {
80    this.marks[key] = schema;
81  }
82
83  getMatchingMark(item) {
84    const node = this.marks[item.type];
85    if (typeof node === 'function') {
86      return node(item);
87    }
88  }
89
90  renderTag(tags, ending) {
91    if (tags.constructor === String) {
92      return `<${tags}${ending}>`;
93    }
94
95    const all = tags.map(tag => {
96      if (tag.constructor === String) {
97        return `<${tag}${ending}>`;
98      } else {
99        let h = `<${tag.tag}`;
100        if (tag.attrs) {
101          for (const key in tag.attrs) {
102            const value = tag.attrs[key];
103            if (value !== null) {
104              h += ` ${key}="${value}"`;
105            }
106          }
107        }
108
109        return `${h}${ending}>`;
110      }
111    });
112    return all.join('');
113  }
114
115  renderOpeningTag(tags) {
116    return this.renderTag(tags, '');
117  }
118
119  renderClosingTag(tags) {
120    if (tags.constructor === String) {
121      return `</${tags}>`;
122    }
123
124    const all = tags
125      .slice(0)
126      .reverse()
127      .map(tag => {
128        if (tag.constructor === String) {
129          return `</${tag}>`;
130        } else {
131          return `</${tag.tag}>`;
132        }
133      });
134
135    return all.join('');
136  }
137
138  render(data, styles) {
139    if (data && data.content && Array.isArray(data.content)) {
140      let html = '';
141
142      data.content.forEach(node => {
143        html += this.renderNode(node);
144      });
145
146      // Rich text styles need to be passed here as we can't style
147      // the shadow DOM from the outside.
148      return `
149        ${styles}
150        ${html}
151      `;
152    }
153
154    console.warn(`The render method must receive an Object with a "content" field.`);
155    return '';
156  }
157
158  renderNode(item) {
159    let html = '';
160
161    if (item.marks) {
162      item.marks.forEach(m => {
163        const mark = this.getMatchingMark(m);
164
165        if (mark) {
166          html += this.renderOpeningTag(mark.tag);
167        }
168      });
169    }
170
171    if (item.type === 'heading') {
172      html += `<h${item.attrs.level}>`;
173      if (item.content && Array.isArray(item.content)) {
174        item.content.forEach(node => {
175          html += this.renderNode(node);
176        });
177      }
178      html += `</h${item.attrs.level}>`;
179    } else if (item.type === 'paragraph') {
180      html += `<p>`;
181      if (item.content && Array.isArray(item.content)) {
182        item.content.forEach(node => {
183          html += this.renderNode(node);
184        });
185      }
186      html += `</p>`;
187    } else if (item.type === 'text') {
188      html += escapeHTML(item.text);
189    } else if (item.type === 'link') {
190      html += `<a href="${item.attrs.href}" target="${item.attrs.target}">`;
191      if (item.content && Array.isArray(item.content)) {
192        item.content.forEach(node => {
193          html += this.renderNode(node);
194        });
195      }
196      html += `</a>`;
197    } else if (item.type === 'bullet_list') {
198      html += `<ul>`;
199      if (item.content && Array.isArray(item.content)) {
200        item.content.forEach(node => {
201          html += this.renderNode(node);
202        });
203      }
204      html += `</ul>`;
205    } else if (item.type === 'list_item') {
206      html += `<li>`;
207      if (item.content && Array.isArray(item.content)) {
208        item.content.forEach(node => {
209          html += this.renderNode(node);
210        });
211      }
212      html += `</li>`;
213    }
214
215    if (item.marks) {
216      item.marks
217        .slice(0)
218        .reverse()
219        .forEach(m => {
220          const mark = this.getMatchingMark(m);
221
222          if (mark) {
223            html += this.renderClosingTag(mark.tag);
224          }
225        });
226    }
227
228    return html;
229  }
230}
231// END: Rich text helpers
232
233class YMUtils {
234  static createTemplate(html) {
235    const template = document.createElement('template');
236    template.innerHTML = html;
237    return template;
238  }
239
240  static createStyle(css) {
241    const style = document.createElement('style');
242    style.textContent = css;
243    return style;
244  }
245
246  static appendToShadowRoot({ shadowRoot, template }) {
247    shadowRoot.appendChild(this.createTemplate(template).content.cloneNode(true));
248  }
249
250  static handleAnimation({ className, condition, container }) {
251    condition && container.classList.add(className);
252    condition &&
253      container.addEventListener(
254        'animationend',
255        () => {
256          container.classList.remove(className);
257        },
258        {
259          once: true,
260        }
261      );
262
263    !condition && container.classList.remove(className);
264  }
265
266  static createWebComponent({
267    template,
268    style,
269    connectedCallback,
270    disconnectedCallback,
271    effect,
272    deps,
273  }) {
274    return class WebComponent extends HTMLElement {
275      static get observedAttributes() {
276        return deps || [];
277      }
278      constructor() {
279        super();
280        this.attachShadow({ mode: 'open' });
281        style && this.shadowRoot.appendChild(YMUtils.createStyle(style).cloneNode(true));
282        template &&
283          this.shadowRoot.appendChild(YMUtils.createTemplate(template).content.cloneNode(true));
284        this.shadowRoot.innerHTML += '<div id="content-container"></div>';
285        this.prevDeps = [];
286      }
287      connectedCallback() {
288        typeof connectedCallback === 'function'
289          ? connectedCallback.call(this, { element: this })
290          : this.shadowRoot.getElementById('content-container')?.remove();
291      }
292      disconnectedCallback() {
293        typeof disconnectedCallback === 'function' &&
294          disconnectedCallback.call(this, { element: this });
295      }
296      attributeChangedCallback(name, prevValue, newValue) {
297        const currentDeps = deps ? deps.map(dep => this.getAttribute(dep)) : [];
298        const hasChanged = currentDeps.some((dep, index) => dep !== this.prevDeps[index]);
299        hasChanged &&
300          typeof effect === 'function' &&
301          effect.call(this, { name, prevValue, newValue, element: this });
302        this.prevDeps = currentDeps;
303      }
304      render(element) {
305        this.shadowRoot.getElementById('content-container').innerHTML = element;
306      }
307    };
308  }
309
310  static registerComponent(obj) {
311    customElements.define(obj.componentTag, obj.component);
312  }
313
314  static spawn({ componentTag, fetcher = obj => obj, parser = obj => obj, renderer = obj => obj }) {
315    return this.asyncPipe(
316      fetcher,
317      parser,
318      renderer,
319      this.registerComponent
320    )({ componentTag, href: window?.location?.href, pathname: window?.location?.pathname });
321  }
322
323  static getLocale(url) {
324    const pathname = new URL(url).pathname;
325    const pathParts = pathname.split('/').filter(part => part);
326    const supportedLocales = ['pt', 'es', 'nl', 'de', 'fr'];
327    if (pathParts.length > 0 && supportedLocales.includes(pathParts[0])) {
328      return `/${pathParts[0]}`;
329    } else {
330      return '';
331    }
332  }
333
334  static insertLocaleIntoUrl(url, locale) {
335    const urlObj = new URL(url);
336    urlObj.pathname = `${locale}${urlObj.pathname}`;
337    return urlObj.href;
338  }
339
340  static asyncPipe(...fns) {
341    return async input => {
342      return fns.reduce(async (chain, fun) => {
343        const result = await chain;
344        return fun(result);
345      }, Promise.resolve(input));
346    };
347  }
348
349  static replaceLocaleInUrl(newLocaleUrl, currentUrl) {
350    const newLocale = this.getLocale(newLocaleUrl);
351    const currentLocale = this.getLocale(currentUrl);
352    const currentUrlObj = new URL(currentUrl);
353    const newPathname = {
354      replaceCurrentLocale: () =>
355        currentUrlObj.pathname.replace(currentLocale, newLocale === 'en/' ? '' : newLocale),
356      insertNewLocale: () => `${newLocale === 'en/' ? '' : newLocale}${currentUrlObj.pathname}`,
357    }[currentLocale ? 'replaceCurrentLocale' : 'insertNewLocale']();
358    currentUrlObj.pathname = newPathname;
359    return currentUrlObj.href;
360  }
361
362  static mapAndJoin(arr, fn, separator = '') {
363    return arr.map(fn).join(separator);
364  }
365
366  static rem(px, base = 16) {
367    return `${px / base}rem`;
368  }
369
370  static getPageType(path) {
371    const locales = ['', 'en', 'pt', 'es', '/es', '/pt'];
372    const allPaths = {
373      buy: locales.map(locale => `${locale}/buy`),
374      redeem: locales.map(locale => `${locale}/redeem`),
375      use: locales.map(locale => `${locale}/use`),
376      home: locales.map(locale => `${locale === '' ? '/' : locale}`),
377    };
378    const foundKey = Object.keys(allPaths).find(key => allPaths[key].some(item => item === path));
379    return foundKey || 'other';
380  }
381
382  // Mini-storyblok renderRichText
383  static renderRichText(data, styles) {
384    const localResolver = new RichTextResolver(schema);
385    if (!localResolver) {
386      console.error(
387        'Please initialize the Storyblok SDK before calling the renderRichText function'
388      );
389      return;
390    }
391
392    if (data === '') {
393      return '';
394    } else if (!data) {
395      console.warn(`${data} is not a valid Richtext object. This might be because the value of the richtext field is empty.
396      
397    For more info about the richtext object check https://github.com/storyblok/storyblok-js#rendering-rich-text`);
398      return '';
399    }
400
401    return localResolver.render(data, styles);
402  }
403
404  static scrollToPosition(position, duration) {
405    const start = window.scrollY;
406    const change = position - start;
407    const increment = 20; // The amount of pixels to scroll in each step
408    let currentTime = 0;
409
410    function animateScroll() {
411      currentTime += increment;
412      const val = easeInOutQuad(currentTime, start, change, duration);
413      window.scrollTo(0, val);
414
415      if (currentTime < duration) {
416        window.requestAnimationFrame(animateScroll);
417      }
418    }
419
420    function easeInOutQuad(t, b, c, d) {
421      // Easing function for smooth scrolling
422      t /= d / 2;
423      if (t < 1) return (c / 2) * t * t + b;
424      t--;
425      return (-c / 2) * (t * (t - 2) - 1) + b;
426    }
427
428    window.requestAnimationFrame(animateScroll);
429  }
430
431  static scrollToElement(element, duration = 1000) {
432    const y = element.getBoundingClientRect().top + window.scrollY;
433    this.scrollToPosition(y - 30, duration);
434  }
435
436  static scrollBy(distance, duration = 500) {
437    const y = distance + window.scrollY;
438    this.scrollToPosition(y, duration);
439  }
440
441  static delayAndCall(fn, ms) {
442    return new Promise(resolve =>
443      setTimeout(() => {
444        fn();
445        resolve();
446      }, ms)
447    );
448  }
449
450  static getNavigationByDomain(domain, navigationArray) {
451    if (!domain) return {};
452    if (!(navigationArray && navigationArray.length > 0)) return {};
453
454    const domainBase = domain.replace('https://', '').replace('http://', '');
455
456    return (
457      navigationArray.find(nav => nav.Domain === domainBase) ||
458      navigationArray.find(nav => nav.Domain === 'dopper.com') ||
459      navigationArray[0] ||
460      {}
461    );
462  }
463}

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.