1/* 2 * Copyright 2022 Adobe. All rights reserved. 3 * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. You may obtain a copy 5 * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 * 7 * Unless required by applicable law or agreed to in writing, software distributed under 8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 * OF ANY KIND, either express or implied. See the License for the specific language 10 * governing permissions and limitations under the License. 11 */ 12 13import { loadBlock } from './load.js'; 14 15/** 16 * log RUM if part of the sample. 17 * @param {string} checkpoint identifies the checkpoint in funnel 18 * @param {Object} data additional data for RUM sample 19 */ 20export function sampleRUM(checkpoint, data = {}) { 21 sampleRUM.defer = sampleRUM.defer || []; 22 const defer = (fnname) => { 23 sampleRUM[fnname] = sampleRUM[fnname] 24 || ((...args) => sampleRUM.defer.push({ fnname, args })); 25 }; 26 sampleRUM.drain = sampleRUM.drain 27 || ((dfnname, fn) => { 28 sampleRUM[dfnname] = fn; 29 sampleRUM.defer 30 .filter(({ fnname }) => dfnname === fnname) 31 .forEach(({ fnname, args }) => sampleRUM[fnname](...args)); 32 }); 33 sampleRUM.on = (chkpnt, fn) => { sampleRUM.cases[chkpnt] = fn; }; 34 defer('observe'); 35 defer('cwv'); 36 try { 37 window.hlx = window.hlx || {}; 38 if (!window.hlx.rum) { 39 const usp = new URLSearchParams(window.location.search); 40 const weight = (usp.get('rum') === 'on') ? 1 : 100; // with parameter, weight is 1. Defaults to 100. 41 // eslint-disable-next-line no-bitwise 42 const hashCode = (s) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0); 43 const id = `${hashCode(window.location.href)}-${new Date().getTime()}-${Math.random().toString(16).substr(2, 14)}`; 44 const random = Math.random(); 45 const isSelected = (random * weight < 1); 46 const urlSanitizers = { 47 full: () => window.location.href, 48 origin: () => window.location.origin, 49 path: () => window.location.href.replace(/\?.*$/, ''), 50 }; 51 // eslint-disable-next-line object-curly-newline 52 window.hlx.rum = { weight, id, random, isSelected, sampleRUM, sanitizeURL: urlSanitizers[window.hlx.RUM_MASK_URL || 'path'] }; 53 } 54 const { weight, id } = window.hlx.rum; 55 if (window.hlx && window.hlx.rum && window.hlx.rum.isSelected) { 56 const sendPing = (pdata = data) => { 57 // eslint-disable-next-line object-curly-newline, max-len 58 const body = JSON.stringify({ weight, id, referer: window.hlx.rum.sanitizeURL(), checkpoint, ...data }); 59 const url = `https://rum.hlx.page/.rum/${weight}`; 60 61 navigator.sendBeacon(url, body); 62 63 console.debug(`ping:${checkpoint}`, pdata); 64 }; 65 sampleRUM.cases = sampleRUM.cases || { 66 cwv: () => sampleRUM.cwv(data) || true, 67 lazy: () => { 68 // use classic script to avoid CORS issues 69 const script = document.createElement('script'); 70 script.src = 'https://rum.hlx.page/.rum/@adobe/helix-rum-enhancer@^1/src/index.js'; 71 document.head.appendChild(script); 72 return true; 73 }, 74 }; 75 sendPing(data); 76 if (sampleRUM.cases[checkpoint]) { sampleRUM.cases[checkpoint](); } 77 } 78 } catch (error) { 79 // something went wrong 80 console.error('Failed to sample RUM', error); 81 } 82} 83 84/** 85 * Retrieves the content of metadata tags. 86 * @param {string} name The metadata name (or property) 87 * @returns {string} The metadata value(s) 88 */ 89export function getMetadata(name) { 90 const attr = name && name.includes(':') ? 'property' : 'name'; 91 const meta = [...document.head.querySelectorAll(`meta[${attr}="${name}"]`)].map((m) => m.content).join(', '); 92 return meta || ''; 93} 94 95/** 96 * Sanitizes a string for use as class name. 97 * @param {string} name The unsanitized string 98 * @returns {string} The class name 99 */ 100export function toClassName(name) { 101 return typeof name === 'string' 102 ? name.toLowerCase().replace(/[^0-9a-z]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') 103 : ''; 104} 105 106/** 107 * Sanitizes a string for use as a js property name. 108 * @param {string} name The unsanitized string 109 * @returns {string} The camelCased name 110 */ 111export function toCamelCase(name) { 112 return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); 113} 114 115/** 116 * Gets placeholders object. 117 * @param {string} [prefix] Location of placeholders 118 * @returns {object} Window placeholders object 119 */ 120export async function fetchPlaceholders(prefix = 'default') { 121 window.placeholders = window.placeholders || {}; 122 const loaded = window.placeholders[`${prefix}-loaded`]; 123 if (!loaded) { 124 window.placeholders[`${prefix}-loaded`] = new Promise((resolve, reject) => { 125 fetch(`${prefix === 'default' ? '' : prefix}/placeholders.json`) 126 .then((resp) => { 127 if (resp.ok) { 128 return resp.json(); 129 } 130 throw new Error(`${resp.status}: ${resp.statusText}`); 131 }).then((json) => { 132 const placeholders = {}; 133 json.data 134 .filter((placeholder) => placeholder.Key) 135 .forEach((placeholder) => { 136 placeholders[toCamelCase(placeholder.Key)] = placeholder.Text; 137 }); 138 window.placeholders[prefix] = placeholders; 139 resolve(); 140 }).catch((error) => { 141 // error loading placeholders 142 window.placeholders[prefix] = {}; 143 reject(error); 144 }); 145 }); 146 } 147 await window.placeholders[`${prefix}-loaded`]; 148 return window.placeholders[prefix]; 149} 150 151/** 152 * Extracts the config from a block. 153 * @param {Element} block The block element 154 * @returns {object} The block config 155 */ 156export function readBlockConfig(block) { 157 const config = {}; 158 block.querySelectorAll(':scope > div').forEach((row) => {
159 if (row.children) { 160 const cols = [...row.children]; 161 if (cols[1]) { 162 const col = cols[1]; 163 const name = toClassName(cols[0].textContent); 164 let value = ''; 165 if (col.querySelector('a')) { 166 const as = [...col.querySelectorAll('a')]; 167 if (as.length === 1) { 168 value = as[0].href; 169 } else { 170 value = as.map((a) => a.href); 171 } 172 } else if (col.querySelector('img')) { 173 const imgs = [...col.querySelectorAll('img')]; 174 if (imgs.length === 1) { 175 value = imgs[0].src; 176 } else { 177 value = imgs.map((img) => img.src); 178 } 179 } else if (col.querySelector('p')) { 180 const ps = [...col.querySelectorAll('p')]; 181 if (ps.length === 1) { 182 value = ps[0].textContent; 183 } else { 184 value = ps.map((p) => p.textContent); 185 } 186 } else value = row.children[1].textContent; 187 config[name] = value; 188 } 189 } 190 }); 191 return config; 192} 193 194/** 195 * Updates all section status in a container element. 196 * @param {Element} main The container element 197 */ 198export function updateSectionsStatus(main) { 199 const sections = [...main.querySelectorAll(':scope > div.section')]; 200 for (let i = 0; i < sections.length; i += 1) { 201 const section = sections[i]; 202 const status = section.dataset.sectionStatus; 203 if (status !== 'loaded') { 204 const loadingBlock = section.querySelector('.block[data-block-status="initialized"], .block[data-block-status="loading"]'); 205 if (loadingBlock) { 206 section.dataset.sectionStatus = 'loading'; 207 break; 208 } else { 209 section.dataset.sectionStatus = 'loaded'; 210 section.style.display = null; 211 } 212 } 213 } 214} 215 216/** 217 * Builds a block DOM Element from a two dimensional array, string, or object 218 * @param {string} blockName name of the block 219 * @param {*} content two dimensional array or string or object of content 220 */ 221export function buildBlock(blockName, content) { 222 const table = Array.isArray(content) ? content : [[content]]; 223 const blockEl = document.createElement('div'); 224 // build image block nested div structure 225 blockEl.classList.add(blockName); 226 table.forEach((row) => { 227 const rowEl = document.createElement('div'); 228 row.forEach((col) => { 229 const colEl = document.createElement('div'); 230 const vals = col.elems ? col.elems : [col]; 231 vals.forEach((val) => { 232 if (val) { 233 if (typeof val === 'string') { 234 colEl.innerHTML += val; 235 } else { 236 colEl.appendChild(val); 237 } 238 } 239 }); 240 rowEl.appendChild(colEl); 241 }); 242 blockEl.appendChild(rowEl); 243 }); 244 return (blockEl); 245} 246 247/** 248 * Returns a picture element with webp and fallbacks 249 * @param {string} src The image URL 250 * @param {string} [alt] The image alternative text 251 * @param {boolean} [eager] Set loading attribute to eager 252 * @param {Array} [breakpoints] Breakpoints and corresponding params (eg. width) 253 * @returns {Element} The picture element 254 */ 255export function createOptimizedPicture(src, alt = '', eager = false, breakpoints = [{ media: '(min-width: 600px)', width: '2000' }, { width: '750' }]) { 256 const url = new URL(src, document.baseURI); 257 const picture = document.createElement('picture'); 258 const { pathname } = url; 259 const ext = pathname.substring(pathname.lastIndexOf('.') + 1); 260 261 // webp 262 breakpoints.forEach((br) => { 263 const source = document.createElement('source'); 264 if (br.media) source.setAttribute('media', br.media); 265 source.setAttribute('type', 'image/webp'); 266 source.setAttribute('srcset', `${pathname}?width=${br.width}&format=webp&optimize=medium`); 267 picture.appendChild(source); 268 }); 269 270 // fallback 271 breakpoints.forEach((br, i) => { 272 if (i < breakpoints.length - 1) { 273 const source = document.createElement('source'); 274 if (br.media) source.setAttribute('media', br.media); 275 source.setAttribute('srcset', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); 276 picture.appendChild(source); 277 } else { 278 const img = document.createElement('img'); 279 img.setAttribute('loading', eager ? 'eager' : 'lazy'); 280 img.setAttribute('alt', alt); 281 picture.appendChild(img); 282 img.setAttribute('src', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); 283 } 284 }); 285 286 return picture; 287} 288
289/** 290 * Normalizes all headings within a container element. 291 * @param {Element} el The container element 292 * @param {string} allowedHeadings The list of allowed headings (h1 ... h6) 293 */ 294export function normalizeHeadings(el, allowedHeadings) { 295 const allowed = allowedHeadings.map((h) => h.toLowerCase()); 296 el.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((tag) => { 297 const h = tag.tagName.toLowerCase(); 298 if (allowed.indexOf(h) === -1) { 299 // current heading is not in the allowed list -> try first to "promote" the heading 300 let level = parseInt(h.charAt(1), 10) - 1; 301 while (allowed.indexOf(`h${level}`) === -1 && level > 0) { 302 level -= 1; 303 } 304 if (level === 0) { 305 // did not find a match -> try to "downgrade" the heading 306 while (allowed.indexOf(`h${level}`) === -1 && level < 7) { 307 level += 1; 308 } 309 } 310 if (level !== 7) { 311 tag.outerHTML = `<h${level} id="${tag.id}">${tag.textContent}</h${level}>`; 312 } 313 } 314 }); 315} 316 317/** 318 * Load LCP block and/or wait for LCP in default content. 319 */ 320export async function waitForLCP(lcpBlocks) { 321 const block = document.querySelector('.block'); 322 const hasLCPBlock = (block && lcpBlocks.includes(block.dataset.blockName)); 323 if (hasLCPBlock) await loadBlock(block); 324 325 document.body.style.display = null; 326 const lcpCandidate = document.querySelector('main img'); 327 await new Promise((resolve) => { 328 if (lcpCandidate && !lcpCandidate.complete) { 329 lcpCandidate.setAttribute('loading', 'eager'); 330 lcpCandidate.addEventListener('load', resolve); 331 lcpCandidate.addEventListener('error', resolve); 332 } else { 333 resolve(); 334 } 335 }); 336} 337 338export function getEnvironment() { 339 const { hostname } = window.location; 340 341 if (hostname.includes('localhost')) { 342 return 'dev'; 343 } if (hostname.includes('hlx.page')) { 344 return 'staging'; 345 } if (hostname.includes('hlx.live')) { 346 return 'production'; 347 } 348 return 'unknown'; 349} 350 351/** 352 * Setup block utils. 353 */ 354export function setup() { 355 window.hlx = window.hlx || {}; 356 window.hlx.RUM_MASK_URL = 'full'; 357 window.hlx.codeBasePath = ''; 358 window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse') === 'on'; 359 360 const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]'); 361 if (scriptEl) { 362 try { 363 [window.hlx.codeBasePath] = new URL(scriptEl.src).pathname.split('/scripts/scripts.js'); 364 } catch (error) { 365 // eslint-disable-next-line no-console 366 console.log(error); 367 } 368 } 369} 370 371/** 372 * Auto initializiation. 373 */ 374function init() { 375 setup(); 376 sampleRUM('top'); 377 378 window.addEventListener('load', () => sampleRUM('load')); 379 380 window.addEventListener('unhandledrejection', (event) => { 381 sampleRUM('error', { source: event.reason.sourceURL, target: event.reason.line }); 382 }); 383 384 window.addEventListener('error', (event) => { 385 sampleRUM('error', { source: event.filename, target: event.lineno }); 386 }); 387} 388 389init();
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.