1/** 2 * MMX / BASE 3 */ 4 5const MMX = {}; 6 7MMX.variableType = (value) => { 8 const [input, type] = Object.prototype.toString.call(value).match(/\[object (\w+)\]/); 9 return type.toLowerCase(); 10}; 11 12MMX.isTruthy = (value) => { 13 return [true, 'true', 'yes', 'y', 'on', '1', 1].includes(typeof value === 'string' ? value.toLowerCase() : value); 14}; 15 16MMX.isFalsy = (value) => { 17 return [null, undefined, false, 'false', 'no', 'n', 'off', '0', 0].includes(typeof value === 'string' ? value.toLowerCase() : value); 18}; 19 20MMX.copy = (value) => { 21 if(typeof value === 'object') { 22 try { 23 return JSON.parse(JSON.stringify(value)); 24 } catch(err) { 25 return null; 26 } 27 } 28 29 return value; 30}; 31 32MMX.assign = (...args) => { 33 return Object.assign(...args.map(MMX.copy)); 34}; 35 36MMX.coerceNumber = (value, fallback = 0) => { 37 const valueType = MMX.variableType(value); 38 39 if(['null', 'undefined'].includes(valueType)) { 40 return fallback; 41 } 42 43 if(valueType === 'string' && !value.trim().length) { 44 return fallback; 45 } 46 47 return isNaN(value) ? fallback : Number(value); 48}; 49 50MMX.coerceString = (value = '', {prefix = '', suffix = '', fallback = ''} = {}) => { 51 const isValidValue = (typeof value === 'string' && value.length) || typeof value === 'number'; 52 53 if (isValidValue) { 54 return `${prefix}${value}${suffix}`; 55 } 56 57 return fallback; 58}; 59 60MMX.splitString = (value, {separator = ',', trim = true} = {}) => { 61 return String(value).split(separator).map(part => trim ? part.trim() : part); 62}; 63 64MMX.normalizeCode = (value) => { 65 const valueType = MMX.variableType(value); 66 67 if (['string', 'number', 'boolean'].includes(valueType)) { 68 return String(value).trim(); 69 } 70 71 return ''; 72}; 73 74MMX.scriptSafeJSONStringify = (data) => { 75 return JSON.stringify(data).replace(/\//gi, '\\/'); 76}; 77 78MMX.getCustomFieldValueFromRecord = (customField = '', record = {}) => { 79 const customFieldParts = customField?.split?.(':'); 80 81 if (customFieldParts?.length !== 2) { 82 return ''; 83 } 84 85 const [moduleCode, fieldCode] = customFieldParts; 86 87 return record?.CustomField_Values?.[moduleCode]?.[fieldCode]; 88}; 89 90MMX.valueIsEmpty = (value) => { 91 if (value === null) return true; 92 else if (typeof value === 'object') return Object.keys(value).length === 0 && value.constructor === Object; 93 else if (typeof value === 'undefined') return true; 94 else if (typeof value === 'string' && value.length == 0) return true; 95 96 // type is boolean, number, function, non-zero length string, etc 97 return false; 98}; 99 100MMX.arrayIsEmpty = (arr) => { 101 return !Array.isArray(arr) || arr.length === 0; 102}; 103 104MMX.cacheKeysFilters = (entities) => { 105 const values = [] 106 .concat(entities ?? []) 107 .map(entity => { 108 return MMX.coerceNumber(entity?.dt_updated ?? entity); 109 }) 110 .filter(value => value > 0); 111 112 if (!values.length) { 113 return []; 114 } 115 116 return [{ 117 name: 'cache_keys', 118 value: values 119 }]; 120}; 121 122MMX.keyToCssProp = (key = '') => { 123 return MMX.snakeToKebabCase(MMX.camelToKebabCase(key)); 124}; 125 126MMX.camelToKebabCase = (value = '') => { 127 return String(value) 128 .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // withASingleCharacterWord 129 .replace(/([a-z0-9])([A-Z])/g, '$1-$2') // commonCamelCase 130 .toLowerCase(); 131}; 132 133MMX.snakeToKebabCase = (value = '') => { 134 return String(value).replace(/_/g, '-').toLowerCase(); 135}; 136 137MMX.objectToCssShorthand = (obj, {key = 'padding_%side%'} = {}) => { 138 const first = MMX.composeUnitValue(obj?.[key.replace('%side%', 'top').replace('%corner%', 'top_left')]); 139 const second = MMX.composeUnitValue(obj?.[key.replace('%side%', 'right').replace('%corner%', 'top_right')]); 140 const third = MMX.composeUnitValue(obj?.[key.replace('%side%', 'bottom').replace('%corner%', 'bottom_right')]); 141 const fourth = MMX.composeUnitValue(obj?.[key.replace('%side%', 'left').replace('%corner%', 'bottom_left')]); 142 143 if (first === second && first === third && first === fourth) { 144 return first; 145 } 146 147 return `${first ?? 0} ${second ?? 0} ${third ?? 0} ${fourth ?? 0}`; 148}; 149 150MMX.composeUnitValue = (obj, {fallbackUnit = 'px'} = {}) => { 151 if (MMX.valueIsEmpty(obj?.value)) { 152 return undefined; 153 } 154 155 const value = MMX.coerceNumber(obj?.value); 156 157 if (value === 0) { 158 return `${value}`; 159 } 160 161 return `${value}${MMX.coerceString(obj?.unit, {fallback: fallbackUnit})}`; 162}; 163 164MMX.boxShadowObjectToCssValue = (obj) => { 165 if (!obj?.box_shadow_enabled?.value) { 166 return undefined; 167 } 168 169 const inset = obj?.box_shadow_inset?.value; 170 const offsetX = MMX.composeUnitValue(obj?.box_shadow_offset_x) ?? 0; 171 const offsetY = MMX.composeUnitValue(obj?.box_shadow_offset_y) ?? 0; 172 const blur = MMX.composeUnitValue(obj?.box_shadow_blur) ?? 0; 173 const spread = MMX.composeUnitValue(obj?.box_shadow_spread) ?? 0; 174 const color = obj?.box_shadow_color?.value; 175 176 return `${inset} ${offsetX} ${offsetY} ${blur} ${spread} ${color}`; 177}; 178 179MMX.objectToInlineStyles = (styles = {}) => { 180 return Object.entries(styles).reduce((inlineStyles, [key, value]) => { 181 const isValidValue = (typeof value === 'string' && value.length) || typeof value === 'number'; 182 183 if (isValidValue) { 184 const prop = MMX.keyToCssProp(key); 185 inlineStyles.push(`${prop}: ${value}`); 186 } 187 188 return inlineStyles; 189 }, []).join(';'); 190}; 191
192MMX.renderBreakpointStyle = ({selector = ':root', breakpoint = {}, match = {}} = {}) => { 193 if (MMX.isFalsy(breakpoint?.breakpoint_available) || MMX.valueIsEmpty(match)) { 194 return ''; 195 } 196 197 return /*html*/` 198 <style> 199 @media ${MMX.buildMediaQuery(breakpoint)} { 200 ${selector} { 201 ${MMX.objectToInlineStyles(match)} 202 } 203 } 204 </style> 205 `; 206}; 207 208MMX.getBreakpoints = () => { 209 return structuredClone(window.MMThemeBreakpoints ?? []); 210}; 211 212MMX.getBreakpoint = (code) => { 213 return MMX.getBreakpoints().find(breakpoint => String(breakpoint.code) === String(code)); 214}; 215 216MMX.buildMediaQuery = (rules = {}) => { 217 const parts = []; 218 219 const minWidth = MMX.composeUnitValue({ 220 value: rules?.breakpoint?.start?.value, 221 unit: rules?.breakpoint?.start?.unit 222 }); 223 224 const maxWidth = MMX.composeUnitValue({ 225 value: rules?.breakpoint?.end?.value, 226 unit: rules?.breakpoint?.end?.unit 227 }); 228 229 if (typeof minWidth !== 'undefined') { 230 parts.push(`(min-width: ${minWidth})`); 231 } 232 233 if (typeof maxWidth !== 'undefined') { 234 parts.push(`(max-width: ${maxWidth})`); 235 } 236 237 return parts.join(' and '); 238}; 239 240MMX.watchData = (obj, callback) => { 241 return new Proxy(obj, MMX.proxyHandler(callback)); 242}; 243 244MMX.proxyHandler = (callback) => { 245 return { 246 get(obj, prop) { 247 // Deep-watch the object (i.e. recursively bind a watcher to objects & arrays with the same callback) 248 const valueType = MMX.variableType(obj[prop]); 249 if (['object', 'array'].includes(valueType)) { 250 return MMX.watchData(obj[prop], callback); 251 } 252 253 return obj[prop]; 254 }, 255 set(obj, prop, value) { 256 obj[prop] = value; 257 if (typeof callback === 'function') { 258 callback(); 259 } 260 return true; 261 } 262 }; 263}; 264 265MMX.createElement = ({type, content, attributes, data, parent} = {}) => { 266 if (typeof type === 'undefined'){ 267 return; 268 } 269 let element = document.createElement(type); 270 271 if (typeof content === 'string') { 272 element.innerHTML = content; 273 } 274 275 if (typeof attributes === 'object') { 276 MMX.setElementAttributes(element, attributes); 277 } 278 279 if (typeof data === 'object') { 280 element.data = MMX.watchData(data, () => { 281 element.forceUpdate?.(); 282 }); 283 } 284 else if (typeof data === 'string') { 285 element.data = JSON.stringify(data); 286 } 287 288 if (typeof parent !== 'undefined') { 289 if (typeof parent === 'string' && parent === 'currentScript') { 290 const currentScript = document.scripts[document.scripts.length - 1]; 291 return currentScript.insertAdjacentElement('afterEnd', element); 292 } else if (typeof parent?.appendChild === 'function') { 293 return parent.appendChild(element); 294 } 295 } 296 297 return element; 298}; 299 300MMX.setElementAttributes = (element, attributes) => { 301 302 if (!attributes){ 303 return element; 304 } 305 306 for (const attribute in attributes) { 307 if (!Object.hasOwnProperty.call(attributes, attribute)) { 308 continue; 309 } 310 311 const value = attributes[attribute]; 312 313 if (typeof value === 'undefined') { 314 element.removeAttribute(attribute); 315 continue; 316 } 317 318 if (attribute === 'name'){ 319 element.name = value; 320 } else if (attribute === 'class'){ 321 element.className = value; 322 } else { 323 element.setAttribute(attribute, value); 324 } 325 } 326 327 return element; 328}; 329 330MMX.setElementStyles = ({element, styles = {}, suffix = ''} = {}) => { 331 if (!element) { 332 return; 333 } 334 335 for (const prop in styles) { 336 const value = typeof styles?.[prop] === 'undefined' ? '' : styles?.[prop] + suffix; 337 element.style[prop] = value; 338 } 339}; 340 341MMX.closestElement = (selector, node) => { 342 if (!node || node === document || node === window) { 343 return null; 344 } 345 346 return ( 347 node.closest?.(selector) || 348 node.parentElement?.closest?.(selector) || 349 MMX.closestElement(selector, node?.getRootNode?.()?.host) 350 ); 351}; 352 353MMX.querySelector = (selector, root = document) => { 354 if (selector === ':shadow') { 355 return root?.shadowRoot; 356 } 357 358 if (typeof root.querySelector === 'function') { 359 return root.querySelector(selector); 360 } 361 362 return null; 363}; 364 365MMX.querySelectorList = (selectors = [], root = document) => { 366 let lastElement = root; 367 368 if (!Array.isArray(selectors) || selectors.length === 0) { 369 return null; 370 } 371 372 for (const selector of selectors) { 373 const element = MMX.querySelector(selector, lastElement); 374 375 if (!element) { 376 lastElement = null; 377 break; 378 } 379 380 lastElement = element; 381 } 382 383 return lastElement; 384}; 385 386MMX.on = (events, selector, handler, { root = document, eventOptions = {} } = {}) => { 387 events = MMX.splitString(events); 388
389 events.forEach(event => { 390 root.addEventListener(event, (e) => { 391 const matched = e.target?.closest?.(selector); 392 393 if (!matched || (root !== document && !root.contains(matched))) { 394 return; 395 } 396 397 handler(e, matched); 398 }, eventOptions); 399 }); 400}; 401 402MMX.encodeEntities = (input) => { 403 return String(input ?? '') 404 .replace(/®/g, '®') 405 .replace(/™/g, 'â¢') 406 .replace(/&/g, '&') 407 .replace(/"/g, '"') 408 .replace(/</g, '<') 409 .replace(/>/g, '>') 410 .replace(/\(/g, '(') 411 .replace(/\)/g, ')') 412 .replace(/®/g, '®') 413 .replace(/â¢/g, '™'); 414}; 415 416MMX.encodeEntitiesURI = (input) => { 417 return MMX.encodeEntities(encodeURI(input)); 418}; 419 420MMX.encodeSrcset = (input) => { 421 return encodeURI(input); 422}; 423 424MMX.isEqual = (a, b) => { 425 return JSON.stringify(a) === JSON.stringify(b); 426}; 427 428MMX.setBooleanAttribute = (node, attribute_name, value, optional_enabled_values) => { 429 if (Array.isArray(optional_enabled_values) && 430 optional_enabled_values.indexOf(value) !== -1) node.setAttribute(attribute_name, value); 431 else if (MMX.isTruthy(value)) node.setAttribute(attribute_name, ''); 432 else node.removeAttribute(attribute_name); 433}; 434 435MMX.getBooleanAttribute = (node, attribute_name, optional_enabled_values) => { 436 var value; 437 438 if (!node.hasAttribute(attribute_name)) { 439 return false; 440 } 441 else { 442 value = node.getAttribute(attribute_name); 443 444 if (Array.isArray(optional_enabled_values) && 445 optional_enabled_values.indexOf(value) !== -1) return true; 446 else if (value === null || value === '') return true; 447 else return MMX.isTruthy(value); 448 } 449}; 450 451MMX.elementInViewport = (element) => { 452 var elementPosition = element.getBoundingClientRect(); 453 454 return ( 455 elementPosition.top >= 0 && 456 elementPosition.left >= 0 && 457 elementPosition.bottom <= window.innerHeight && 458 elementPosition.right <= window.innerWidth 459 ); 460}; 461 462/** 463 * Update an element's DOM to match a provided HTML string 464 * @param {Node} element 465 * @param {String} template 466 */ 467MMX.renderTemplate = (element, template) => { 468 element.innerHTML = template; 469}; 470 471MMX.debounce = (func, timeout = 100) => { 472 let timer; 473 return (...args) => { 474 clearTimeout(timer); 475 timer = setTimeout(() => { func.apply(this, args); }, timeout); 476 }; 477}; 478 479MMX.concatenateSortBy = (sortBy, customField) => { 480 if (MMX.valueIsEmpty(customField)) { 481 return sortBy; 482 } 483 else { 484 return `${sortBy}:${customField}`; 485 } 486}; 487 488class MMX_FetchQueue { 489 #id = 0; 490 #max = 3; 491 #todo = new Map(); 492 #doing = new Map(); 493 #cache = new Map(); 494 495 constructor({max} = {}) { 496 this.#max = MMX.coerceNumber(max, this.#max); 497 this.#doNext(); 498 } 499 500 request(...params) { 501 return new Promise((resolve, reject) => { 502 this.#todo.set(this.#id++, { 503 params, 504 resolve, 505 reject 506 }); 507 508 this.#doNext(); 509 }); 510 } 511 512 #doNext() { 513 if (this.#todo.size === 0 || this.#doing.size >= this.#max) { 514 return; 515 } 516 517 const [nextId, next] = this.#todo.entries().next().value; 518 this.#doing.set(nextId, next); 519 this.#todo.delete(nextId); 520 521 fetch(...next.params) 522 .then(next.resolve) 523 .catch(next.reject) 524 .finally(() => { 525 this.#doing.delete(nextId); 526 this.#doNext(); 527 }); 528 } 529 530 requestCached(...params) { 531 const key = JSON.stringify(params); 532 533 if (!this.#cache.has(key)) { 534 this.#cache.set(key, this.request(...params).catch(error => { 535 this.#cache.delete(key); 536 throw error; 537 })); 538 } 539 540 return this.#cache.get(key) 541 .then(response => { 542 return response?.clone?.() ?? response; 543 }); 544 } 545} 546 547MMX.fetchQueue = new MMX_FetchQueue(); 548 549class MMX_StyleManager { 550 #cache = new Map(); 551 552 // Main Functions 553 adoptStyleSheets(shadowRoot, resourceCodes = []) { 554 if (!shadowRoot || !(shadowRoot instanceof ShadowRoot)) { 555 return Promise.resolve(null); 556 } 557 558 const styleSheets = this.#resolveStyleSheets(resourceCodes); 559 const linkSheets = this.#resolveLinkSheets(resourceCodes); 560 561 return Promise.all([...styleSheets, ...linkSheets]) 562 .then((sheets) => { 563 shadowRoot.adoptedStyleSheets = sheets.filter(sheet => this.#isAdoptableSheet(sheet)); 564 }); 565 } 566 567 // Cache 568 #addCssToCache(key, css) { 569 if (!this.#cache.has(key)) { 570 this.#cache.set(key, this.#createSheet(css, key)); 571 } 572 573 return this.#cache.get(key); 574 } 575 576 #createSheet(css, layer = 'mmx-adopted') { 577 css = /*css*/` 578 @layer ${layer} { 579 ${css} 580 } 581 `; 582 583 const sheet = new CSSStyleSheet(); 584 return sheet.replace(css); 585 } 586 587 // Links 588 #resolveLinkSheets(resourceCodes = []) { 589 return this.#getIncludedLinkElements(resourceCodes).map((link) => { 590 return this.#getStyleSheetFromLink(link); 591 }); 592 } 593 594 #getIncludedLinkElements(resourceCodes) { 595 return this.#getAllLinkElements().filter(link => { 596 return this.#shouldIncludeElement(link, resourceCodes); 597 }); 598 } 599 600 #getAllLinkElements() { 601 const selector = 'link[href][data-resource-code]'; 602 const links = [...document.querySelectorAll(selector)]; 603 604 [...document.querySelectorAll('template.mmx-resources')].forEac
604h(template => { 605 const templateLinks = template.content.querySelectorAll(selector); 606 if (templateLinks.length) { 607 links.push(...templateLinks); 608 } 609 }); 610 611 return links; 612 } 613 614 #shouldIncludeElement(element, resourceCodes = []) { 615 return this.isAdoptableResource(element) && resourceCodes.includes(this.#getStyleSheetKey(element)); 616 } 617 618 // Styles 619 #resolveStyleSheets(resourceCodes = []) { 620 return this.#getIncludedStyleElements(resourceCodes).map((style) => { 621 return this.#getStyleSheetFromStyle(style); 622 }); 623 } 624 625 #getIncludedStyleElements(resourceCodes = []) { 626 return this.#getAllStyleElements().filter(style => { 627 return this.#shouldIncludeElement(style, resourceCodes); 628 }); 629 } 630 631 #getAllStyleElements() { 632 return [...document.querySelectorAll('style[data-resource-code]')]; 633 } 634 635 // CSSStyleSheet Building and Caching 636 isAdoptableResource(element) { 637 return element instanceof Element && element.matches?.('[data-mmx-resource-type~="adoptable"]'); 638 }; 639 640 #isAdoptableSheet(sheet) { 641 return sheet instanceof CSSStyleSheet; 642 } 643 644 #getStyleSheetKey(element) { 645 return element.dataset.resourceCode; 646 } 647 648 #getStyleSheetFromStyle(style) { 649 const key = this.#getStyleSheetKey(style); 650 651 if (!key) { 652 return null; 653 } 654 655 return this.#addCssToCache(key, style.textContent); 656 } 657 658 #getStyleSheetFromLink(link) { 659 const key = this.#getStyleSheetKey(link); 660 661 if (!(link instanceof HTMLElement) || !key) { 662 return Promise.resolve(null); 663 } 664 665 if (!this.#cache.has(key)) { 666 this.#cache.set(key, this.#fetchCssFromLink(link) 667 .then(css => this.#createSheet(css, key)) 668 .catch(error => { 669 this.#cache.delete(key); 670 throw error; 671 })); 672 } 673 674 return this.#cache.get(key); 675 } 676 677 #fetchCssFromLink(link) { 678 const options = {}; 679 680 if (link.integrity) { 681 options.integrity = link.integrity; 682 } 683 684 if (link.hasAttribute('crossorigin')) { 685 options.credentials = link.crossOrigin === 'use-credentials' ? 'include' : 'same-origin'; 686 } 687 688 return fetch(link.href, options) 689 .then(response => { 690 if (!response.ok) { 691 throw new Error(`Failed to fetch stylesheet: ${link.href} (${response.status})`); 692 } 693 return response.text(); 694 }); 695 } 696} 697 698MMX.styleManager = new MMX_StyleManager(); 699 700MMX.Runtime_JSON_API_Call = ({jsonUrl, storeCode, params = {}, cached = false} = {}) => { 701 jsonUrl = jsonUrl ?? window?.json_url; 702 storeCode = storeCode ?? window?.Store_Code; 703 704 if (!jsonUrl?.length || MMX.valueIsEmpty(storeCode)) { 705 return Promise.reject({ 706 success: false, 707 error_code: 'MMX-Runtime_JSON_API_Call-00001', 708 error_message: 'Runtime_JSON_API_Call halted. Cannot call Miva JSON API. jsonUrl and/or storeCode is not provided' 709 }); 710 } 711 712 const fetchParams = { 713 method: 'POST', 714 headers: { 715 'Content-Type': 'application/json' 716 }, 717 body: JSON.stringify(MMX.assign({Session_Type: 'runtime', Store_Code: storeCode}, params)) 718 }; 719 720 const request = cached ? MMX.fetchQueue.requestCached(jsonUrl, fetchParams) : MMX.fetchQueue.request(jsonUrl, fetchParams); 721 722 return request 723 .then(async (response) => { 724 const text = await response.text(); 725 try { 726 return JSON.parse(text); 727 } catch(err) { 728 return Promise.reject({ 729 success: false, 730 error_code: 'MMX-Runtime_JSON_API_Call-00002', 731 error_message: `Did not receive valid JSON from API. Received: "${text}"` 732 }); 733 } 734 }).then(data => { 735 if (data?.success) { 736 return Promise.resolve(data); 737 } 738 739 return Promise.reject(data); 740 }); 741}; 742 743MMX.fetchForm = (form, fetchOptions = {}) => { 744 if (!(form instanceof HTMLFormElement) || MMX.variableType(fetchOptions) !== 'object') { 745 return Promise.reject(new TypeError()); 746 } 747 748 return MMX.fetchQueue.request(form.action, { 749 method: 'POST', 750 body: new FormData(form), 751 ...fetchOptions 752 }); 753}; 754 755MMX.longMerchantUrl = (searchParams = {}, {merchantUrl, storeCode = window.Store_Code} = {}) => { 756 let url; 757 758 if (typeof merchantUrl !== 'string') { 759 if (typeof window.json_url === 'string') { 760 merchantUrl = window.json_url.replace('json.mvc', 'merchant.mvc'); 761 } else { 762 return; 763 } 764 } 765 766 try { 767 url = new URL(merchantUrl); 768 } 769 catch (error) { 770 throw new TypeError(`${error.message} when parsing 'merchantUrl'`); 771 } 772 773 if (MMX.variableType(searchParams) !== 'object') { 774 throw new TypeError("'searchParams' must be an 'Object'"); 775 } 776 777 if (['string', 'number'].includes(typeof storeCode) && !('Store_Code' in searchParams)) { 778 searchParams = { 779 Store_Code: storeCode, 780 ...searchParams 781 }; 782 } 783
784 Object.entries(searchParams).forEach(([key, value]) => { 785 url.searchParams.append(key, value); 786 }); 787 788 return url.toString(); 789}; 790 791MMX.pluralize = (singular, count, plural) => { 792 return count === 1 ? `${singular}` : plural ?? `${singular}s`; 793}; 794 795MMX.setWindowLocation = (href) => { 796 window.location.href = href; 797}; 798 799class MMX_Element extends HTMLElement { 800 801 themeResourcePattern; 802 styleResourceCodes = ['mmx-base']; 803 hideOnEmpty = false; 804 renderUniquely = false; 805 lifecycleReady = true; 806 807 static baseProps = { 808 init: { 809 isObject: true, 810 isJson: true, 811 options: [ 812 'script', 813 'innerHTML' 814 ], 815 default: null 816 } 817 }; 818 819 createElement = MMX.createElement; 820 data = this.data || {}; 821 822 /** 823 * Web Component Lifecycle Hooks 824 */ 825 826 constructor() { 827 super(); 828 } 829 830 makeComponent() { 831 this.makeShadow(); 832 this.makeStates(); 833 } 834 835 makeShadow() { 836 this.attachShadow({ 837 mode: 'open' 838 }); 839 } 840 841 makeStates() { 842 this.makeInternals(); 843 this.setInitialStates(); 844 } 845 846 connectedCallback() { 847 // Render Immediately 848 this.#adoptStyleSheets(); 849 this.renderTemplate(); 850 this.connected?.(); 851 852 // Re-Render onload to catch when component's inner DOM is not completely available at connectedCallback() 853 this.ownerDocument.addEventListener('DOMContentLoaded', () => { 854 this.renderOnFrame(); 855 }); 856 857 // Re-Render when the innerHTML / child-nodes are updated 858 this.observer = new MutationObserver((records) => { 859 const validRecords = records.filter(record => { 860 if (MMX.closestElement('.mmx-skip-mutation', record.target)) { 861 return false; 862 } 863 864 return ![...record.addedNodes, ...record.removedNodes].find(node => { 865 return MMX.closestElement('.mmx-skip-mutation', node); 866 }); 867 }); 868 869 if (!validRecords.length) { 870 return; 871 } 872 873 this.renderOnFrame(); 874 }); 875 876 this.observer.observe(this, { 877 subtree: true, 878 characterData: true, 879 childList: true 880 }); 881 } 882 883 disconnectedCallback() { 884 this.disconnected?.(); 885 this.observer?.disconnect(); 886 } 887 888 // Re-Render when attributes are modified 889 attributeChangedCallback() { 890 this.attributeChanged?.(...arguments); 891 this.debouncedRender(); 892 } 893 894 static get observedAttributes() { 895 return this.propsToAttributeNames; 896 } 897 898 static get propsToAttributeNames() { 899 if (typeof this.props !== 'object') { 900 return []; 901 } 902 903 return Object.keys(this.props).map(prop => 'data-' + prop); 904 } 905 906 /** 907 * Internals 908 */ 909 910 #internals; 911 912 makeInternals() { 913 try { 914 this.#internals = this.attachInternals(); 915 } 916 catch { 917 // already attached 918 } 919 } 920 921 /** 922 * Form 923 */ 924 925 setFormValue(...args) { 926 return this.constructor.formAssociated ? this.#internals?.setFormValue(...args) : undefined; 927 } 928 929 setValidity(...args) { 930 return this.constructor.formAssociated ? this.#internals?.setValidity(...args) : undefined; 931 } 932 933 checkValidity() { 934 return this.constructor.formAssociated ? this.#internals?.checkValidity() : undefined; 935 } 936 937 reportValidity() { 938 return this.constructor.formAssociated ? this.#internals?.reportValidity() : undefined; 939 } 940 941 get validity() { 942 return this.constructor.formAssociated ? this.#internals?.validity : undefined; 943 } 944 945 get validationMessage() { 946 return this.constructor.formAssociated ? this.#internals?.validationMessage : undefined; 947 } 948 949 /** 950 * States 951 */ 952 953 setInitialStates() { 954 this.addStates(['mmx', 'initial']); 955 } 956 957 addState(state) { 958 this.#internals?.states?.add?.(state); 959 } 960 961 addStates(states = []) { 962 states.forEach(state => { 963 this.#internals?.states?.add?.(state); 964 }); 965 } 966 967 removeState(state) { 968 this.#internals?.states?.delete?.(state); 969 } 970 971 clearStates() { 972 this.#internals?.states?.clear?.(); 973 } 974 975 /** 976 * Render Functions 977 */ 978 renderOnFrame() { 979 if (this.requestId) { 980 window.cancelAnimationFrame(this.requestId); 981 } 982 983 this.requestId = window.requestAnimationFrame(() => { 984 this.renderTemplate(); 985 }); 986 } 987 988 debouncedRender = MMX.debounce(() => { 989 this.renderOnFrame(); 990 }, 50); 991 992 renderTemplate() { 993 this.initData(); 994 this.checkDataChange(); 995 996 const element = this.shadowRoot || this; 997 const template = this.getTemplate(); 998 const renderRequired = template !== this?.lastTemplate; 999 1000 if (this.renderUniquely && !renderRequired) { 1001 return; 1002 } 1003 1004 this.beforeRender?.(); 1005 MMX.renderTemplate(element, template); 1006 this.lastTemplate = template; 1007 1008 this.addState('rendered'); 1009 1010 if (this.isLifecycleReady()) { 1011 this.addState('ready'); 1012 } 1013 1014 this.afterRender?.(); 1015 this.debouncedDispatchContentUpdated(); 1016 } 1017 1018 isLifecycleReady() { 1019 return this.lifecycleReady; 1020 } 1021 1022 setLifecycleReady(value = true) { 1023 this.lifecycleReady = Boolean(value); 1024 } 1025 1026 debouncedDispatchContentUpdated = MMX.debounce(() => { 1027 this.dispatchContentUpdated(); 1028 }, 100); 1029 1030 dispatchContentUpdated() { 1031 this.dispatchEvent(new CustomEvent('contentUpdated')); 1032 } 1033 1034 getTemplate() { 1035 if (this.hideOnEmpty && this.isEmpty() && this.slotsAreEmpty()) { 1036 return ''; 1037 } 1038 return this.renderStyles() + this.output('render'); 1039 } 1040 1041 isEmpty() { 1042 return this.textContent.trim().length === 0; 1043 } 1044 1045 slotsAreEmpty() { 1046 return !this.slotsHaveContent(); 1047 } 1048 1049 slotsHaveContent() { 1050 return Array.from(this.querySelectorAll('slot')).some(slot => { 1051 return Array.from(slot.assignedElements()).some(element => { 1052 return this.hasTextContent(element); 1053 }); 1054 }); 1055 } 1056 1057 hasTextContent(element = this) { 1058 return element?.textContent?.trim?.()?.length > 0; 1059 } 1060 1061 forceUpdate() { 1062 this.setLifecycleReady(true); 1063 1064 if (!this.lastTemplate) { 1065 return; 1066 } 1067 1068 this.lastTemplate = null; 1069 this.renderTemplate(); 1070 } 1071 1072 checkDataChange() { 1073 const currentDataString = JSON.stringify(this.data); 1074 const sameData = currentDataString === this.lastDataString; 1075 if (currentDataString !== '{}' && !sameData) { 1076 this.lastDataString = currentDataString; 1077 this.onDataChange?.(); 1078 } 1079 } 1080 1081 initData() { 1082 const init = this.loadPropertyData('init'); 1083 1084 if (MMX.variableType(init) !== 'object' || MMX.isEqual(this.data, init)) { 1085 return; 1086 } 1087 1088 this.data = init; 1089 } 1090 1091 /** 1092 * Utility / Helper Functions 1093 */ 1094 1095 on(events, selector, handler, options = {}) { 1096 return MMX.on(events, selector, handler, { 1097 root: this.shadowRoot || this, 1098 ...options 1099 }); 1100 } 1101 1102 output(name) { 1103 return typeof this[name] === 'function' ? this[name]() : ''; 1104 } 1105 1106 getPropValueWithoutDefault(name) { 1107 const prop = this.constructor.props[name] || MMX_Element.baseProps[name];
1108 const value = name in this.data ? this.data[name] : (this.getAttribute('data-' + name) || '').trim(); 1109 1110 if (typeof prop === 'undefined' || !value?.length) { 1111 return null; 1112 } 1113 1114 if (prop?.options?.length && prop.options.includes(value)) { 1115 return value; 1116 } 1117 1118 if (prop?.isBoolean) { 1119 return MMX.isTruthy(value); 1120 } 1121 1122 if (prop?.isObject && typeof value === 'object') { 1123 return value; 1124 } 1125 1126 if (prop?.isJson && typeof value === 'string') { 1127 try { 1128 return JSON.parse(value); 1129 } catch(err) { 1130 return null; 1131 } 1132 } 1133 1134 if (prop.allowAny) { 1135 if (prop.isNumeric && !isNaN(value)) { 1136 return Number(value); 1137 } 1138 1139 if (prop.isPercentage && !isNaN(value)) { 1140 return value + '%'; 1141 } 1142 1143 return value; 1144 } 1145 1146 return null; 1147 } 1148 1149 hasPropValue(name) { 1150 const propValue = this.getPropValueWithoutDefault(name); 1151 const propValueType = MMX.variableType(propValue); 1152 return propValueType !== 'null' && propValueType !== 'undefined'; 1153 } 1154 1155 getPropValue(name) { 1156 return this.getPropValueWithoutDefault(name) ?? this.constructor.props[name]?.default; 1157 } 1158 1159 inheritAttrs() { 1160 const attributeNames = this.getAttributeNames(); 1161 1162 if (!attributeNames.length) { 1163 return ''; 1164 } 1165 1166 return attributeNames.reduce((attributes, attributeName) => { 1167 const attributeValue = this.getAttribute(attributeName); 1168 1169 if (attributeName === 'part' || attributeName.indexOf('data-') === 0) { 1170 return attributes; 1171 } 1172 1173 if (attributeValue) { 1174 attributes.push(`${attributeName}="${MMX.encodeEntities(attributeValue)}"`); 1175 } else { 1176 attributes.push(attributeName); 1177 } 1178 1179 return attributes; 1180 }, []).join(' '); 1181 } 1182 1183 propExists() { 1184 return propKey in this.constructor.props || propKey in MMX_Element.baseProps; 1185 } 1186 1187 loadPropertyData(propKey) { 1188 const data = this.getPropValue(propKey); 1189 1190 if (!data) { 1191 return; 1192 } 1193 1194 if (data === 'innerHTML' ){ 1195 return this.parseData(this.innerHTML); 1196 } 1197 1198 if (data === 'script'){ 1199 let script = this.querySelector('script'); 1200 return script ? this.parseData(script.innerHTML) : {}; 1201 } 1202 1203 return this.parseData(data); 1204 } 1205 1206 parseData(data) { 1207 if (typeof data === 'object') { 1208 return data; 1209 } 1210 1211 try { 1212 return JSON.parse(data); 1213 } catch(err) { 1214 return null; 1215 } 1216 } 1217 1218 /** 1219 * Visibility Helpers 1220 */ 1221 getLoadingAttributeString() { 1222 const {loading, fetchpriority} = this.getLoadingAttributes(); 1223 return `loading="${loading}" fetchpriority="${fetchpriority}"`; 1224 } 1225 1226 getLoadingAttributes() { 1227 const isInViewport = MMX.elementInViewport(this); 1228 1229 return { 1230 loading: this.getLoading(isInViewport), 1231 fetchpriority: this.getFetchPriority(isInViewport) 1232 }; 1233 } 1234 1235 getFetchPriority(isInViewport) { 1236 return (isInViewport || MMX.elementInViewport(this)) ? 'high' : 'low'; 1237 } 1238 1239 getLoading(isInViewport) { 1240 return (isInViewport || MMX.elementInViewport(this)) ? 'eager' : 'lazy'; 1241 } 1242 1243 /** 1244 * Rendering Helpers 1245 */ 1246 renderTextProperty(property = {}, {className = '', prefix = '', field = 'normal', defaultStyle = 'paragraph-s', defaultTag = 'div'}) { 1247 if (MMX.valueIsEmpty(property?.value)) { 1248 return ''; 1249 } 1250 1251 const theme_available = property?.textsettings?.fields?.[field]?.typography_theme?.theme_available ?? false; 1252 1253 const text = MMX.createElement({ 1254 type: 'mmx-text', 1255 attributes: { 1256 class: className, 1257 'data-source': property.source, 1258 'data-theme': theme_available, 1259 'data-theme-class': property?.textsettings?.fields?.[field]?.typography_theme?.classname ?? '', 1260 'data-style': property?.textsettings?.fields?.[field]?.[`${prefix}style`]?.value || defaultStyle, 1261 'data-tag': property?.textsettings?.fields?.[field]?.[`${prefix}tag`]?.value || defaultTag, 1262 }, 1263 content: ` 1264 ${this.renderLegacyStylesTemplate(theme_available, property?.textsettings?.styles?.[field] ?? '')} 1265 ${this.renderThemeStylesheetTemplate(theme_available)} 1266 ${property.source === 'markdown' ? property.value : MMX.encodeEntities(property.value)} 1267 ` 1268 }); 1269 1270 return text.outerHTML; 1271 } 1272 1273 renderButtonProperty(property = {}, {className = '', field = 'normal', prefix = 'button_', defaultStyle = 'primary', defaultSize = 's'}) { 1274 if (MMX.valueIsEmpty(property?.value)) { 1275 return ''; 1276 } 1277 1278 const button = MMX.createElement({ 1279 type: 'mmx-button', 1280 attributes: { 1281 class: className, 1282 'data-style': property?.textsettings?.fields?.[field]?.[`${prefix}style`]?.value || defaultStyle, 1283 'data-size': property?.textsettings?.fields?.[field]?.[`${prefix}size`]?.value || defaultSize, 1284 'data-theme': property?.textsettings?.fields?.[field]?.[`${prefix}theme`]?.theme_available ?? false, 1285 'data-theme-class': property?.textsettings?.fields?.[field]?.[`${prefix}theme`]?.classname || '' 1286 },
1287 content: ` 1288 ${this.renderThemeStylesheetTemplate(property?.textsettings?.fields?.[field]?.[`${prefix}theme`]?.theme_available || false)} 1289 ${MMX.encodeEntities(property.value)} 1290 ` 1291 }); 1292 1293 return button.outerHTML; 1294 } 1295 1296 renderThemeStylesheetTemplate(theme_available) { 1297 if (!theme_available) { 1298 return ''; 1299 } 1300 1301 return /*html*/` 1302 <template data-theme-stylesheet> 1303 ${this.querySelector(':scope > template[data-theme-stylesheet]')?.content?.textContent ?? ''} 1304 </template> 1305 `; 1306 } 1307 1308 renderLegacyStylesTemplate(theme_available, styles) { 1309 if (theme_available || !styles?.length) { 1310 return ''; 1311 } 1312 1313 return /*html*/` 1314 <template data-legacy-styles> 1315 ${styles} 1316 </template> 1317 `; 1318 } 1319 1320 getStylesFromGroup(group = {}) { 1321 return Object.keys(group).reduce((styles, key) => { 1322 if (key === 'style' || key === 'button_theme' || key === 'typography_theme') { 1323 return styles; 1324 } 1325 1326 let newStyles = []; 1327 1328 if (key === 'font') { 1329 if (!MMX.valueIsEmpty(group[key]?.font?.family)) newStyles.push(`font-family: ${group[key]?.font.family}`); 1330 if (!MMX.valueIsEmpty(group[key]?.weight)) newStyles.push(`font-weight: ${group[key]?.weight}`); 1331 if (!MMX.valueIsEmpty(group[key]?.style)) newStyles.push(`font-style: ${group[key]?.style}`); 1332 } 1333 else { 1334 let value = group[key]?.value; 1335 1336 if (!MMX.valueIsEmpty(value)) { 1337 if (key === 'font_size') { 1338 value += 'px'; 1339 } 1340 1341 if (key === 'font_color') { 1342 key = 'color'; 1343 } 1344 1345 newStyles.push(`${MMX.snakeToKebabCase(key)}: ${value}`); 1346 } 1347 } 1348 1349 if (newStyles.length === 0) { 1350 return styles; 1351 } 1352 1353 if (styles.length) { 1354 styles += '; '; 1355 } 1356 1357 return `${styles}${newStyles.join(';')}`; 1358 }, ''); 1359 } 1360 1361 /** 1362 * Fragment Helpers 1363 */ 1364 renderContentIntoLightDomSlot({slotName, content} = {}) { 1365 if (typeof slotName !== 'string' || typeof content !== 'string') { 1366 return ''; 1367 } 1368 1369 const existingLightElement = this.querySelector(`[slot="${MMX.encodeEntities(slotName)}"]`); 1370 existingLightElement?.remove(); 1371 1372 const contentFragment = document.createRange().createContextualFragment(content); 1373 const lightElement = MMX.createElement({ 1374 type: 'div', 1375 attributes: { 1376 slot: slotName, 1377 class: 'mmx-skip-mutation', 1378 } 1379 }); 1380 lightElement.appendChild(contentFragment); 1381 this.appendChild(lightElement); 1382 1383 return /*html*/` 1384 <slot name="${MMX.encodeEntities(slotName)}"></slot> 1385 `; 1386 } 1387 1388 renderProductFragment({product, fragmentCode, slotName} = {}) { 1389 const fragmentContent = product?.fragments?.[fragmentCode]?.trim?.(); 1390 slotName = slotName ?? `fragment__${fragmentCode}--${product?.code}`; 1391 1392 if (MMX.valueIsEmpty(fragmentContent)) { 1393 return ''; 1394 } 1395 1396 return this.renderContentIntoLightDomSlot({ 1397 slotName, 1398 content: fragmentContent 1399 }); 1400 } 1401 1402 /** 1403 * Responsive Helper 1404 */ 1405 getResponsiveImageHeight(img) { 1406 const widthRatio = img.width / img.naturalWidth; 1407 return img.naturalHeight * widthRatio; 1408 } 1409 1410 /** 1411 * CSS Helper Functions 1412 */ 1413 1414 setSpacing(settings = {}) { 1415 MMX.setElementStyles({ 1416 element: this, 1417 styles: { 1418 marginTop: settings?.top?.value, 1419 marginBottom: settings?.bottom?.value, 1420 marginRight: settings?.right?.value, 1421 marginLeft: settings?.left?.value 1422 }, 1423 suffix: 'px' 1424 }); 1425 } 1426 1427 /** 1428 * Stylesheet Functions 1429 */ 1430 1431 #adoptStyleSheets() { 1432 return MMX.styleManager.adoptStyleSheets(this.shadowRoot, this.styleResourceCodes) 1433 .finally(() => { 1434 this.addStates(['styled', 'adopted-styles']); 1435 }); 1436 } 1437 1438 renderStyles() { 1439 return /*html*/` 1440 ${this.renderStyleElements()} 1441 ${this.renderStylesheetLinks()} 1442 <style> 1443 ${this.output('styles')} 1444 </style> 1445 `; 1446 } 1447 1448 renderStyleElements() { 1449 const mmxStyleElements = [...document.querySelectorAll('style[data-resource-code]')].filter(style => { 1450 return this.#shouldRenderStyleElement(style); 1451 }); 1452 1453 return mmxStyleElements.map(this.renderStyleElement).join('\n'); 1454 } 1455 1456 #shouldRenderStyleElement(style) { 1457 return this.shouldIncludeStyleElement(style) && !MMX.styleManager.isAdoptableResource(style); 1458 } 1459 1460 shouldIncludeStyleElement(style) { 1461 return (style.hasAttribute('data-resource-code') && this.styleResourceCodes.indexOf(style.getAttribute('data-resource-code')) !== -1); 1462 } 1463
1464 renderStyleElement(style) { 1465 return style.outerHTML; 1466 } 1467 1468 renderStylesheetLinks() { 1469 const availableSheets = [...document.querySelectorAll('link[rel="stylesheet"][href]')]; 1470 [...document.querySelectorAll('template.mmx-resources')].forEach(template => { 1471 const templateLinks = template.content.querySelectorAll('link[rel="stylesheet"][href]'); 1472 if (templateLinks.length) { 1473 availableSheets.push(...templateLinks); 1474 } 1475 }); 1476 1477 const mmxStyleSheets = availableSheets.filter(sheet => { 1478 return this.#shouldRenderStylesheetLink(sheet); 1479 }); 1480 return mmxStyleSheets.map(this.renderStylesheetLink).join('\n'); 1481 } 1482 1483 #shouldRenderStylesheetLink(sheet) { 1484 return this.shouldIncludeSheet(sheet) && !MMX.styleManager.isAdoptableResource(sheet); 1485 } 1486 1487 shouldIncludeSheet(sheet) { 1488 return ((MMX.variableType(this.themeResourcePattern) === 'regexp' && sheet.href && (this.themeResourcePattern).test(sheet.href)) || (sheet.hasAttribute('data-resource-code') && this.styleResourceCodes.indexOf(sheet.getAttribute('data-resource-code')) !== -1)); 1489 } 1490 1491 renderStylesheetLink(sheet) { 1492 return sheet.outerHTML; 1493 } 1494 1495 /** 1496 * Reveal Element Functions 1497 */ 1498 bindRevealElement() { 1499 this.addEventListener('revealElement', (e) => { 1500 this.onRevealElement(e); 1501 }); 1502 } 1503 1504 onRevealElement(e) { 1505 this.revealElementFromSelectorList(e.detail?.preview_property_selector); 1506 } 1507 1508 revealElementFromSelectorList(selectors) { 1509 const element = MMX.querySelectorList(selectors, this); 1510 1511 if (!element) { 1512 return; 1513 } 1514 1515 this.revealElement?.(element); 1516 } 1517} 1518 1519window.MMX = MMX; 1520window.MMX_FetchQueue = MMX_FetchQueue; 1521window.MMX_StyleManager = MMX_StyleManager; 1522window.MMX_Element = MMX_Element;
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.