diff --git a/blocks/carousel/card.css b/blocks/carousel/card.css index d1ce8ca..253d8f6 100644 --- a/blocks/carousel/card.css +++ b/blocks/carousel/card.css @@ -1,148 +1,147 @@ main .card { - height: 100%; - background: var(--background-color); - margin: 0; - position: relative; - box-shadow: 0 1px 15px rgba(0 0 0 / 40%); - transition: all .3s; - display: flex; - align-items: stretch; - align-content: stretch; - width: 100%; - flex-direction: column; - justify-content: space-between; - margin-bottom: 30px; - } - - main .carousel .card { - margin: 0; - } - - main .card:hover { - z-index: 1; - transform: scale(1.07); - transition: all .3s; - } - + height: 100%; + background: var(--background-color); + margin: 0; + position: relative; + box-shadow: 0 1px 15px rgba(0 0 0 / 40%); + transition: all .3s; + display: flex; + align-items: stretch; + align-content: stretch; + width: 100%; + flex-direction: column; + justify-content: space-between; + margin-bottom: 30px; +} + +main .carousel .card { + margin: 0; +} + +main .card:hover { + z-index: 1; + transform: scale(1.07); + transition: all .3s; +} + +main .card-thumb { + height: 200px; + min-height: 200px; + overflow: hidden; +} + +main .card-thumb a { + display: block; +} + +main .card-thumb img { + height: 200px; + width: 100%; + min-width: auto; + object-fit: cover; + transition: all .5s ease-in-out; +} + +main .card-caption { + display: flex; + flex-direction: column; + padding: 10px 20px 20px; + color: #12141f; + height: 100%; + min-height: auto; +} + +main .card-caption h3 { + font-size: 22px; + font-weight: normal; + text-align: left; + min-height: 49px; + margin-top: 14px; + margin-bottom: 15px; +} + +main .card-caption h3 a { + color: inherit; +} + +main .card-caption .card-description { + font-size: var(--body-font-size-s); + text-align: left; + margin-bottom: 10px; + margin-top: 0; +} + +main .card-caption .c2a { + text-align: center; + margin-top: auto; +} + +main .card-caption .button.primary { + border: 1px solid var(--background-color-green); + color: var(--text-color); + padding-bottom: 10px; + padding-top: 11px; + background: none; + border-radius: 0; + font-size: 18px; + max-width: unset; + width: unset; + white-space: break-spaces; + word-break: break-word; +} + +main .card-caption .c2a .compare-button { + color: var(--text-light-gray); + letter-spacing: .5px; + align-items: center; + float: right; + display: none; +} + +.product-finder .card-caption .c2a .compare-button, +.product-compare main .card-caption .c2a .compare-button { + display: flex; +} + +main .card-caption .c2a .compare-checkbox { + width: 18px; + height: 18px; + margin-left: 10px; + vertical-align: middle; + border: 2px solid var(--dark-border-color-gray); + cursor: pointer; +} + +main .card-caption .c2a .compare-checkbox.selected { + background: var(--background-color-green); + border-color: var(--background-color-green); +} + +main .card-type { + display: none; +} + +main .card a .icon.icon-chevron-right-outline { + margin-left: 5px; + vertical-align: text-top; +} + +main .card a .icon.icon-chevron-right-outline svg { + height: 18px; + width: 18px; +} + +main .card a .icon.icon-chevron-right-outline svg path { + stroke-width: 15; +} + +@media only screen and (max-width: 767px) { main .card-thumb { - height: 200px; - min-height: 200px; - overflow: hidden; + height: 150px; + min-height: 150px; } - - main .card-thumb a { - display: block; - } - + main .card-thumb img { - height: 200px; - width: 100%; - min-width: auto; - object-fit: cover; - transition: all .5s ease-in-out; - } - - main .card-caption { - display: flex; - flex-direction: column; - padding: 10px 20px 20px; - color: #12141f; - height: 100%; - min-height: auto; - } - - main .card-caption h3 { - font-size: 22px; - font-weight: normal; - text-align: left; - min-height: 49px; - margin-top: 14px; - margin-bottom: 15px; - } - - main .card-caption h3 a { - color: inherit; - } - - main .card-caption .card-description { - font-size: var(--body-font-size-s); - text-align: left; - margin-bottom: 10px; - margin-top: 0; - } - - main .card-caption .c2a { - text-align: center; - margin-top: auto; - } - - main .card-caption .button.primary { - border: 1px solid var(--background-color-green); - color: var(--text-color); - padding-bottom: 10px; - padding-top: 11px; - background: none; - border-radius: 0; - font-size: 18px; - max-width: unset; - width: unset; - white-space: break-spaces; - word-break: break-word; - } - - main .card-caption .c2a .compare-button { - color: var(--text-light-gray); - letter-spacing: .5px; - align-items: center; - float: right; - display: none; - } - - .product-finder .card-caption .c2a .compare-button, - .product-compare main .card-caption .c2a .compare-button { - display: flex; - } - - main .card-caption .c2a .compare-checkbox { - width: 18px; - height: 18px; - margin-left: 10px; - vertical-align: middle; - border: 2px solid var(--dark-border-color-gray); - cursor: pointer; - } - - main .card-caption .c2a .compare-checkbox.selected { - background: var(--background-color-green); - border-color: var(--background-color-green); - } - - main .card-type { - display: none; - } - - main .card a .icon.icon-chevron-right-outline { - margin-left: 5px; - vertical-align: text-top; - } - - main .card a .icon.icon-chevron-right-outline svg { - height: 18px; - width: 18px; - } - - main .card a .icon.icon-chevron-right-outline svg path { - stroke-width: 15; - } - - @media only screen and (max-width: 767px) { - main .card-thumb { - height: 150px; - min-height: 150px; - } - - main .card-thumb img { - height: 150px; - } + height: 150px; } - \ No newline at end of file +} diff --git a/blocks/carousel/card.js b/blocks/carousel/card.js index e15b642..608840c 100644 --- a/blocks/carousel/card.js +++ b/blocks/carousel/card.js @@ -1,46 +1,193 @@ -import { createOptimizedPicture } from '../../scripts/lib-franklin.js'; -import { a, li as liHelper, div as divHelper } from '../../scripts/dom-helpers.js'; - -// prettier-ignore -export default function decorate(block) { - /* change to ul, li */ - const ul = document.createElement('ul'); - [...block.children].forEach((row) => { - const wrappingDiv = divHelper({ class: 'cards-card-wrapper' }, ...row.children); - [...wrappingDiv.children].forEach((div) => { - if (div.children.length === 1 && div.querySelector('picture')) { - div.className = 'cards-card-image'; - } else { - div.className = 'cards-card-body'; - } - }); - - const li = liHelper(wrappingDiv); - - ul.append(li); +/* eslint-disable no-unused-expressions */ +/* eslint-disable import/prefer-default-export */ +/* eslint-disable no-alert */ + +import { + decorateIcons, loadCSS, createOptimizedPicture, fetchPlaceholders, toCamelCase, +} from '../../scripts/lib-franklin.js'; +import { isGatedResource, summariseDescription } from '../../scripts/scripts.js'; +import { + a, div, h3, p, i, span, +} from '../../scripts/dom-helpers.js'; +import { createCompareBannerInterface } from '../../templates/compare-items/compare-banner.js'; +import { + MAX_COMPARE_ITEMS, + getTitleFromNode, + getSelectedItems, + updateCompareButtons, +} from '../../scripts/compare-helpers.js'; + +let placeholders = {}; + +export async function handleCompareProducts(e) {F + const { target } = e; + const clickedItemTitle = getTitleFromNode(target); + const selectedItemTitles = getSelectedItems(); + + // get or create compare banner + const compareBannerInterface = await createCompareBannerInterface({ + currentCompareItemsCount: selectedItemTitles.length, }); - ul.querySelectorAll('img').forEach((img) => img.closest('picture').replaceWith(createOptimizedPicture(img.src, img.alt, false, [{ width: '750' }]))); - block.textContent = ''; - block.append(ul); - - if (block.classList.contains('image-link') || block.classList.contains('who-we-are')) { - block.querySelectorAll('li').forEach((li) => { - const link = li.querySelector('a'); - li.querySelectorAll('picture').forEach((picture) => { - const pictureClone = picture.cloneNode(true); - const newLink = a({ href: link.href }, pictureClone); - picture.parentNode.replaceChild(newLink, picture); + + compareBannerInterface.getOrRenderBanner(); + + if (selectedItemTitles.includes(clickedItemTitle)) { + const deleteIndex = selectedItemTitles.indexOf(clickedItemTitle); + if (deleteIndex !== -1) { + selectedItemTitles.splice(deleteIndex, 1); + } + } else if (selectedItemTitles.length >= MAX_COMPARE_ITEMS) { + alert(`You can only select up to ${MAX_COMPARE_ITEMS} products.`); + return; + } else { + selectedItemTitles.push(clickedItemTitle); + } + + updateCompareButtons(selectedItemTitles); + compareBannerInterface.refreshBanner(); +} + +class Card { + constructor(config = {}) { + this.cssFiles = []; + this.defaultStyling = true; + this.defaultImage = '/images/default-card-thumbnail.webp'; + this.defaultButtonText = 'Read More'; + this.useDefaultButtonText = false; + this.showImageThumbnail = true; + this.imageBlockReady = false; + this.thumbnailLink = true; + this.titleLink = true; + this.descriptionLength = 75; + this.c2aLinkStyle = false; + this.c2aLinkConfig = false; + this.c2aLinkIconFull = false; + + // Apply overwrites + Object.assign(this, config); + + if (this.defaultStyling) { + this.cssFiles.push('/blocks/card/card.css'); + } + } + + renderItem(item) { + const cardTitle = item.h1 && item.h1 !== '0' ? item.h1 : item.title; + + let itemImage = this.defaultImage; + if (item.thumbnail && item.thumbnail !== '0') { + itemImage = item.thumbnail; + } else if (item.image && item.image !== '0') { + itemImage = item.image; + } + const thumbnailBlock = this.imageBlockReady + ? item.imageBlock : createOptimizedPicture(itemImage, item.title, 'lazy', [{ width: '800' }]); + + let cardLink = item.path; + if (isGatedResource(item)) { + cardLink = item.gatedURL; + } else if (item.redirectPath && item.redirectPath !== '0') { + cardLink = item.redirectPath; + } + + const buttonText = !this.useDefaultButtonText && item.cardC2A && item.cardC2A !== '0' + ? item.cardC2A : this.defaultButtonText; + let c2aLinkBlock = a({ href: cardLink, 'aria-label': buttonText, class: 'button primary' }, buttonText); + if (this.c2aLinkConfig) { + c2aLinkBlock = a(this.c2aLinkConfig, buttonText); + } + if (item.c2aLinkConfig) { + c2aLinkBlock = a(item.c2aLinkConfig, buttonText); + } + if (this.c2aLinkStyle) { + c2aLinkBlock.classList.remove('button', 'primary'); + c2aLinkBlock.append( + this.c2aLinkIconFull + ? i({ class: 'fa fa-chevron-circle-right', 'aria-hidden': true }) + : span({ class: 'icon icon-chevron-right-outline', 'aria-hidden': true }), + ); + decorateIcons(c2aLinkBlock); + } + + const c2aBlock = div({ class: 'c2a' }, + p({ class: 'button-container' }, + c2aLinkBlock, + ), + ); + if ( + item.specifications + && item.specifications !== '0' + ) { + c2aBlock.append(div({ class: 'compare-button' }, + `${placeholders.compare || 'Compare'} (`, + span({ class: 'compare-count' }, '0'), + ')', + span({ + class: 'compare-checkbox', + onclick: handleCompareProducts, + 'data-identifier': item.identifier, + 'data-title': cardTitle, + 'data-path': cardLink, + 'data-thumbnail': itemImage, + 'data-specifications': item.specifications, + 'data-familyID': item.familyID, + }), + )); + } + + let cardDescription = ''; + if (item.cardDescription && item.cardDescription !== '0') { + cardDescription = summariseDescription(item.cardDescription, this.descriptionLength); + } else if (item.description && item.description !== '0') { + cardDescription = summariseDescription(item.description, this.descriptionLength); + } + + return ( + div({ class: 'card' }, + this.showImageThumbnail ? div({ class: 'card-thumb' }, + this.thumbnailLink ? a({ href: cardLink }, + thumbnailBlock, + ) : thumbnailBlock, + ) : '', + item.badgeText ? div({ class: 'badge' }, item.badgeText) : '', + div({ class: 'card-caption' }, + item.type ? div({ class: 'card-type' }, item.type) : '', + h3( + this.titleLink ? a({ href: cardLink }, cardTitle) : cardTitle, + ), + cardDescription ? p({ class: 'card-description' }, cardDescription) : '', + c2aBlock, + ), + ) + ); + } + + async loadCSSFiles() { + let defaultCSSPromise; + if (Array.isArray(this.cssFiles) && this.cssFiles.length > 0) { + defaultCSSPromise = new Promise((resolve) => { + this.cssFiles.forEach((cssFile) => { + loadCSS(cssFile, (e) => resolve(e)); + }); }); - }); - } else if (block.classList.contains('image-only')) { - block.querySelectorAll('li').forEach((li) => { - const link = li.querySelector('a'); - const picture = li.querySelector('picture'); - const pictureClone = picture.cloneNode(true); - const newLink = a({ href: link.href }, pictureClone); - picture.parentNode.replaceChild(newLink, picture); - const cardBody = li.querySelector('.cards-card-body'); - cardBody.parentNode.removeChild(cardBody); - }); + } + this.cssFiles && (await defaultCSSPromise); } } + +/** + * Create and render default card. + * @param {Object} item required - rendered item in JSON + * @param {Object} config optional - config object for + * customizing the rendering and behaviour + */ +export async function createCard(config = {}) { + placeholders = await fetchPlaceholders(); + + config.defaultButtonText = config.defaultButtonText + ? (placeholders[toCamelCase(config.defaultButtonText)] || config.defaultButtonText) + : placeholders.readMore; + const card = new Card(config); + await card.loadCSSFiles(); + return card; +} diff --git a/blocks/tabs/tabs.css b/blocks/tabs/tabs.css index f59e83c..cd2984a 100644 --- a/blocks/tabs/tabs.css +++ b/blocks/tabs/tabs.css @@ -1,79 +1,79 @@ .tabs { - margin-top: 20px; - } - - .tabs .section > div { - width: 100%; - } - + margin-top: 20px; +} + +.tabs .section > div { + width: 100%; +} + +.tabs h2 { + font-size: 42px; + color: #5f696b; + font-family: var(--ff-proxima-regular); +} + +.tabs h3 { + font-family: var(--ff-proxima-xbold); + font-weight: 700; +} + +.tabs .tab-pane { + display: none; + border: 1px solid var(--border-color-gray); + background-color: var(--background-color); +} + +.tabs .tab-pane.active { + display: block; +} + +.tabs .tab-pane > div { + padding-left: 50px; + padding-right: 50px; +} + +.tabs .tab-pane .columns { + width: 100%; +} + +.tabs ul.tabs-nav { + display: flex; + flex-wrap: wrap; + padding: 0; + margin: 0; +} + +.tabs li.tabs-nav-item { + display: flex; + list-style: none; + flex-grow: 1; + padding: 10px 20px; + text-align: center; + align-items: center; + justify-content: center; + color: var(--text-white); + border: 1px solid var(--text-white); + border-bottom-color: var(--dark-gray-background-color); + background-color: var(--border-color-gray); + cursor: pointer; +} + +.tabs li.tabs-nav-item.active { + background-color: var(--dark-gray-background-color); +} + +@media only screen and (max-width: 767px) { .tabs h2 { - font-size: 42px; - color: #5f696b; - font-family: var(--ff-proxima-regular); - } - - .tabs h3 { - font-family: var(--ff-proxima-xbold); - font-weight: 700; - } - - .tabs .tab-pane { - display: none; - border: 1px solid var(--border-color-gray); - background-color: var(--background-color); - } - - .tabs .tab-pane.active { - display: block; + font-size: 36px; } - + .tabs .tab-pane > div { - padding-left: 50px; - padding-right: 50px; + padding-left: 20px; + padding-right: 20px; } - - .tabs .tab-pane .columns { - width: 100%; - } - - .tabs ul.tabs-nav { - display: flex; - flex-wrap: wrap; - padding: 0; - margin: 0; - } - + .tabs li.tabs-nav-item { - display: flex; - list-style: none; - flex-grow: 1; - padding: 10px 20px; - text-align: center; - align-items: center; - justify-content: center; - color: var(--text-white); - border: 1px solid var(--text-white); - border-bottom-color: var(--dark-gray-background-color); - background-color: var(--border-color-gray); - cursor: pointer; - } - - .tabs li.tabs-nav-item.active { - background-color: var(--dark-gray-background-color); - } - - @media only screen and (max-width: 767px) { - .tabs h2 { - font-size: 36px; - } - - .tabs .tab-pane > div { - padding-left: 20px; - padding-right: 20px; - } - - .tabs li.tabs-nav-item { - width: 100% !important; - } + width: 100% !important; } +} \ No newline at end of file diff --git a/blocks/tabs/tabs.js b/blocks/tabs/tabs.js index 9c41745..76433f8 100644 --- a/blocks/tabs/tabs.js +++ b/blocks/tabs/tabs.js @@ -1,61 +1,61 @@ import { - div, li, ul, - } from '../../scripts/dom-helpers.js'; - import { processEmbedFragment } from '../../scripts/scripts.js'; - - const classActive = 'active'; - - function handleTabClick(e, idx) { - e.preventDefault(); - const { target } = e; - [...target.closest('.tabs-nav').children].forEach((nav) => nav.classList.remove(classActive)); - target.closest('.tabs-nav-item').classList.add(classActive); - const panes = target.closest('.tabs').querySelectorAll('.tab-pane'); - [...panes].forEach((pane) => pane.classList.remove(classActive)); - panes[idx].classList.add(classActive); - } - - function buildNav(block) { - const titles = block.querySelectorAll('.tabs > div > div:first-child'); - const elemWidth = Math.floor(100 / titles.length); - const navList = ul({ class: 'tabs-nav' }); - [...titles].forEach((title, idx) => { - const tabTitle = title.textContent; - const listItem = li( - { - class: 'tabs-nav-item', - style: `width: ${elemWidth}%;`, - onclick: (e) => { handleTabClick(e, idx); }, - 'aria-label': tabTitle, - }, - div(tabTitle), - ); - navList.append(listItem); - }); - navList.querySelector('li').classList.add(classActive); - return navList; - } - - async function buildTabs(block) { - const tabPanes = block.querySelectorAll('.tabs > div > div:last-child'); - const tabList = div({ class: 'tabs-list' }); - const decoratedPanes = await Promise.all([...tabPanes].map(async (pane) => { - pane.classList.add('tab-pane'); - const decoratedPane = await processEmbedFragment(pane); - return decoratedPane; - })); - decoratedPanes.forEach((pane) => { tabList.append(pane); }); - tabList.querySelector('.tab-pane').classList.add(classActive); - return tabList; - } - - export default async function decorate(block) { - const nav = buildNav(block); - const tabs = await buildTabs(block); - block.innerHTML = ''; - - block.append(nav); - block.append(tabs); - - return block; - } \ No newline at end of file + div, li, ul, +} from '../../scripts/dom-helpers.js'; +import { processEmbedFragment } from '../../scripts/scripts.js'; + +const classActive = 'active'; + +function handleTabClick(e, idx) { + e.preventDefault(); + const { target } = e; + [...target.closest('.tabs-nav').children].forEach((nav) => nav.classList.remove(classActive)); + target.closest('.tabs-nav-item').classList.add(classActive); + const panes = target.closest('.tabs').querySelectorAll('.tab-pane'); + [...panes].forEach((pane) => pane.classList.remove(classActive)); + panes[idx].classList.add(classActive); +} + +function buildNav(block) { + const titles = block.querySelectorAll('.tabs > div > div:first-child'); + const elemWidth = Math.floor(100 / titles.length); + const navList = ul({ class: 'tabs-nav' }); + [...titles].forEach((title, idx) => { + const tabTitle = title.textContent; + const listItem = li( + { + class: 'tabs-nav-item', + style: `width: ${elemWidth}%;`, + onclick: (e) => { handleTabClick(e, idx); }, + 'aria-label': tabTitle, + }, + div(tabTitle), + ); + navList.append(listItem); + }); + navList.querySelector('li').classList.add(classActive); + return navList; +} + +async function buildTabs(block) { + const tabPanes = block.querySelectorAll('.tabs > div > div:last-child'); + const tabList = div({ class: 'tabs-list' }); + const decoratedPanes = await Promise.all([...tabPanes].map(async (pane) => { + pane.classList.add('tab-pane'); + const decoratedPane = await processEmbedFragment(pane); + return decoratedPane; + })); + decoratedPanes.forEach((pane) => { tabList.append(pane); }); + tabList.querySelector('.tab-pane').classList.add(classActive); + return tabList; +} + +export default async function decorate(block) { + const nav = buildNav(block); + const tabs = await buildTabs(block); + block.innerHTML = ''; + + block.append(nav); + block.append(tabs); + + return block; +} diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index 80b042e..2e44743 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -359,6 +359,26 @@ export function decorateSections(main) { if (key === 'style') { const styles = meta.style.split(',').map((style) => toClassName(style.trim())); styles.forEach((style) => section.classList.add(style)); + } else if (key === 'background') { + const { background } = meta; + if (background.startsWith('http')) { + const url = new URL(background, window.location.href); + const { pathname } = url; + const backgroundImages = []; + const exts = ['webply', pathname.substring(pathname.lastIndexOf('.') + 1)]; + if (imageMediaQuery.matches) { + exts.forEach((ext) => backgroundImages.push(`url(${pathname}?width=2000&format=${ext}&optimize=medium)`)); + } else { + exts.forEach((ext) => backgroundImages.push(`url(${pathname}?width=750&format=${ext}&optimize=medium)`)); + } + section.style.backgroundImage = backgroundImages.join(', '); + } else { + section.style.background = background; + } + } else if (key === 'name') { + // section.id = toClassName(meta[key]); + section.dataset[toCamelCase(key)] = toClassName(meta[key]); + section.title = meta[key]; } else { section.dataset[toCamelCase(key)] = meta[key]; } @@ -456,16 +476,18 @@ function getBlockConfig(block) { * @param {Element} block The block element */ export async function loadBlock(block) { - const status = block.dataset.blockStatus; + const status = block.getAttribute('data-block-status'); if (status !== 'loading' && status !== 'loaded') { - block.dataset.blockStatus = 'loading'; - const { blockName, cssPath, jsPath } = getBlockConfig(block); + block.setAttribute('data-block-status', 'loading'); + const blockName = block.getAttribute('data-block-name'); try { - const cssLoaded = loadCSS(cssPath); + const cssLoaded = new Promise((resolve) => { + loadCSS(`${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.css`, resolve); + }); const decorationComplete = new Promise((resolve) => { (async () => { try { - const mod = await import(jsPath); + const mod = await import(`../blocks/${blockName}/${blockName}.js`); if (mod.default) { await mod.default(block); } @@ -481,10 +503,9 @@ export async function loadBlock(block) { // eslint-disable-next-line no-console console.log(`failed to load block ${blockName}`, error); } - block.dataset.blockStatus = 'loaded'; + block.setAttribute('data-block-status', 'loaded'); } } - /** * Loads JS and CSS for all blocks in a container element. * @param {Element} main The container element @@ -588,6 +609,7 @@ export function decorateTemplateAndTheme() { * Decorates paragraphs containing a single link as buttons. * @param {Element} element container element */ + export function decorateButtons(element) { element.querySelectorAll('a').forEach((a) => { a.title = a.title || a.textContent; @@ -596,13 +618,15 @@ export function decorateButtons(element) { const twoup = a.parentElement.parentElement; if (!a.querySelector('img')) { if (up.childNodes.length === 1 && (up.tagName === 'P' || up.tagName === 'DIV')) { - a.className = 'button primary'; // default up.classList.add('button-container'); } if (up.childNodes.length === 1 && up.tagName === 'STRONG' && twoup.childNodes.length === 1 && twoup.tagName === 'P') { a.className = 'button primary'; twoup.classList.add('button-container'); + const btnBorder = document.createElement('span'); + btnBorder.className = 'button-border'; + a.append(btnBorder); } if (up.childNodes.length === 1 && up.tagName === 'EM' && twoup.childNodes.length === 1 && twoup.tagName === 'P') { @@ -613,7 +637,6 @@ export function decorateButtons(element) { } }); } - /** * Load LCP block and/or wait for LCP in default content. */ diff --git a/scripts/scripts.js b/scripts/scripts.js index 71ba69b..a60eacb 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -1,7 +1,5 @@ import { sampleRUM, - buildBlock, - loadHeader, loadFooter, decorateButtons, decorateIcons, @@ -10,14 +8,86 @@ import { decorateTemplateAndTheme, waitForLCP, loadBlocks, + toClassName, + getMetadata, loadCSS, + loadBlock, + loadHeader, + decorateBlock, + buildBlock, + readBlockConfig, + toCamelCase, } from './lib-franklin.js'; import { - a, div, p, + a, div, domEl, p, } from './dom-helpers.js'; +/** + * to add/remove a template, just add/remove it in the list below + */ +const TEMPLATE_LIST = [ + 'application-note', + 'news', + 'publication', + 'blog', + 'event', + 'about-us', + 'newsroom', + 'landing-page', +]; + const LCP_BLOCKS = []; // add your LCP blocks to the list +let LAST_SCROLL_POSITION = 0; +let LAST_STACKED_HEIGHT = 0; +let STICKY_ELEMENTS; +let PREV_STICKY_ELEMENTS; +const mobileDevice = window.matchMedia('(max-width: 991px)'); + +export function loadScript(url, callback, type, async, forceReload) { + let script = document.querySelector(`head > script[src="${url}"]`); + if (forceReload && script) { + script.remove(); + script = null; + } + + if (!script) { + const head = document.querySelector('head'); + script = document.createElement('script'); + script.src = url; + if (async) { + script.async = true; + } + if (type) { + script.setAttribute('type', type); + } + script.onload = callback; + head.append(script); + } else if (typeof callback === 'function') { + callback('noop'); + } + + return script; +} + +/** + * Summarises the description to maximum character count without cutting words. + * @param {string} description Description to be summarised + * @param {number} charCount Max character count + * @returns summarised string + */ +export function summariseDescription(description, charCount) { + let result = description; + if (result.length > charCount) { + result = result.substring(0, charCount); + const lastSpaceIndex = result.lastIndexOf(' '); + if (lastSpaceIndex !== -1) { + result = result.substring(0, lastSpaceIndex); + } + } + return `${result}…`; +} + /** * Builds hero block and prepends to main in a new section. * @param {Element} main The container element @@ -33,6 +103,157 @@ function buildHeroBlock(main) { } } +/** + * Decorate blocks in an embed fragment. + */ +function decorateEmbeddedBlocks(container) { + container + .querySelectorAll('div.section > div') + .forEach(decorateBlock); +} + + +/** + * Parse video links and build the markup + */ +export function isVideo(url) { + let isV = false; + const hostnames = ['vids.moleculardevices.com', 'vidyard.com']; + [...hostnames].forEach((hostname) => { + if (url.hostname.includes(hostname)) { + isV = true; + } + }); + return isV; +} + +export function embedVideo(link, url, type) { + const videoId = url.pathname.substring(url.pathname.lastIndexOf('/') + 1).replace('.html', ''); + const observer = new IntersectionObserver((entries) => { + if (entries.some((e) => e.isIntersecting)) { + observer.disconnect(); + loadScript('https://play.vidyard.com/embed/v4.js', null, null, null, true); + link.parentElement.innerHTML = ``; + } + }); + observer.observe(link.parentElement); +} + +export function videoButton(container, button, url) { + const videoId = url.pathname.split('/').at(-1).trim(); + const overlay = div({ id: 'overlay' }, div({ + class: 'vidyard-player-embed', 'data-uuid': videoId, 'dava-v': '4', 'data-type': 'lightbox', 'data-autoplay': '2', + })); + + container.prepend(overlay); + button.addEventListener('click', (e) => { + e.preventDefault(); + loadScript('https://play.vidyard.com/embed/v4.js', () => { + // eslint-disable-next-line no-undef + VidyardV4.api.getPlayersByUUID(videoId)[0].showLightbox(); + }); + }); +} + +export function decorateExternalLink(link) { + if (!link.href) return; + + const url = new URL(link.href); + + const internalLinks = [ + 'https://view.ceros.com', + 'https://share.vidyard.com', + 'https://main--moleculardevices--hlxsites.hlx.page', + 'https://main--moleculardevices--hlxsites.hlx.live', + 'http://molecular-devices.myshopify.com', + 'http://moldev.com', + 'http://go.pardot.com', + 'http://pi.pardot.com', + 'https://drift.me', + ]; + + if (url.origin === window.location.origin + || url.host.endsWith('moleculardevices.com') + || internalLinks.includes(url.origin) + || !url.protocol.startsWith('http') + || link.closest('.languages-dropdown') + || link.querySelector('.icon')) { + return; + } + + const acceptedTags = ['STRONG', 'EM', 'SPAN', 'H2']; + const invalidChildren = Array.from(link.children) + .filter((child) => !acceptedTags.includes(child.tagName)); + + if (invalidChildren.length > 0) { + return; + } + + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); + + const heading = link.querySelector('h2'); + const externalLinkIcon = domEl('i', { class: 'fa fa-external-link' }); + if (!heading) { + link.appendChild(externalLinkIcon); + } else { + heading.appendChild(externalLinkIcon); + } +} + +export function decorateLinks(main) { + main.querySelectorAll('a').forEach((link) => { + const url = new URL(link.href); + // decorate video links + if (isVideo(url) && !link.closest('.block.hero-advanced') && !link.closest('.block.hero')) { + const closestButtonContainer = link.closest('.button-container'); + if (link.closest('.block.cards') || (closestButtonContainer && closestButtonContainer.querySelector('strong,em'))) { + videoButton(link.closest('div'), link, url); + } else { + const up = link.parentElement; + const isInlineBlock = (link.closest('.block.vidyard') && !link.closest('.block.vidyard').classList.contains('lightbox')); + const type = (up.tagName === 'EM' || isInlineBlock) ? 'inline' : 'lightbox'; + const wrapper = div({ class: 'video-wrapper' }, div({ class: 'video-container' }, a({ href: link.href }, link.textContent))); + if (link.href !== link.textContent) wrapper.append(p({ class: 'video-title' }, link.textContent)); + up.innerHTML = wrapper.outerHTML; + embedVideo(up.querySelector('a'), url, type); + } + } + + // decorate RFQ page links with pid parameter + if (url.pathname.startsWith('/quote-request') && !url.searchParams.has('pid') && getMetadata('family-id')) { + url.searchParams.append('pid', getMetadata('family-id')); + link.href = url.toString(); + } + + if (url.pathname.endsWith('.pdf')) { + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); + } + + // decorate external links + decorateExternalLink(link); + }); +} + +function decorateParagraphs(main) { + [...main.querySelectorAll('p > picture')].forEach((picturePar) => { + picturePar.parentElement.classList.add('picture'); + }); + [...main.querySelectorAll('ol > li > em:only-child')].forEach((captionList) => { + captionList.parentElement.parentElement.classList.add('text-caption'); + }); +} + + /** * load fonts.css and set a session storage flag */ @@ -98,14 +319,7 @@ async function loadEager(doc) { } } -/** - * Decorate blocks in an embed fragment. - */ -function decorateEmbeddedBlocks(container) { - container - .querySelectorAll('div.section > div') - .forEach(decorateBlock); -} + export async function fetchFragment(path, plain = true) { const response = await fetch(path + (plain ? '.plain.html' : '')); @@ -123,49 +337,7 @@ export async function fetchFragment(path, plain = true) { return text; } -export function decorateLinks(main) { - main.querySelectorAll('a').forEach((link) => { - const url = new URL(link.href); - // decorate video links - if (isVideo(url) && !link.closest('.block.hero-advanced') && !link.closest('.block.hero')) { - const closestButtonContainer = link.closest('.button-container'); - if (link.closest('.block.cards') || (closestButtonContainer && closestButtonContainer.querySelector('strong,em'))) { - videoButton(link.closest('div'), link, url); - } else { - const up = link.parentElement; - const isInlineBlock = (link.closest('.block.vidyard') && !link.closest('.block.vidyard').classList.contains('lightbox')); - const type = (up.tagName === 'EM' || isInlineBlock) ? 'inline' : 'lightbox'; - const wrapper = div({ class: 'video-wrapper' }, div({ class: 'video-container' }, a({ href: link.href }, link.textContent))); - if (link.href !== link.textContent) wrapper.append(p({ class: 'video-title' }, link.textContent)); - up.innerHTML = wrapper.outerHTML; - embedVideo(up.querySelector('a'), url, type); - } - } - - // decorate RFQ page links with pid parameter - if (url.pathname.startsWith('/quote-request') && !url.searchParams.has('pid') && getMetadata('family-id')) { - url.searchParams.append('pid', getMetadata('family-id')); - link.href = url.toString(); - } - - if (url.pathname.endsWith('.pdf')) { - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener noreferrer'); - } - // decorate external links - decorateExternalLink(link); - }); -} - -function decorateParagraphs(main) { - [...main.querySelectorAll('p > picture')].forEach((picturePar) => { - picturePar.parentElement.classList.add('picture'); - }); - [...main.querySelectorAll('ol > li > em:only-child')].forEach((captionList) => { - captionList.parentElement.parentElement.classList.add('text-caption'); - }); -} /** * Loads everything that doesn't need to be delayed.