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 '<': '<', 13 '>': '>', 14 '"': '"', 15 "'": ''', 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.