diff --git a/blocks/avm-report/avm-report-delayed.js b/blocks/avm-report/avm-report-delayed.js deleted file mode 100644 index 19a7d9a1..00000000 --- a/blocks/avm-report/avm-report-delayed.js +++ /dev/null @@ -1,23 +0,0 @@ -import { fetchPlaceholders } from '../../scripts/aem.js'; - -async function initGooglePlacesAPI() { - const placeholders = await fetchPlaceholders(); - const CALLBACK_FN = 'initAvmPlaces'; - const { mapsApiKey } = placeholders; - const script = document.createElement('script'); - script.type = 'text/javascript'; - script.async = true; - script.defer = true; - script.innerHTML = ` - window.${CALLBACK_FN} = function(){ - const input = document.querySelector('form input[name="avmaddress"]'); - const autocomplete = new google.maps.places.Autocomplete(input, {fields:['formatted_address'], types: ['address']}); - } - const script = document.createElement('script'); - script.src = "https://maps.googleapis.com/maps/api/js?key=${mapsApiKey}&libraries=places&callback=${CALLBACK_FN}"; - document.head.append(script); - `; - document.head.append(script); -} - -initGooglePlacesAPI(); diff --git a/blocks/avm-report/avm-report.css b/blocks/avm-report/avm-report.css index 4f463616..08ad8820 100644 --- a/blocks/avm-report/avm-report.css +++ b/blocks/avm-report/avm-report.css @@ -43,6 +43,9 @@ width: 67px; } +.avm-report.block form button[type="submit"] { + transition: all .3s ease-in; +} @media screen and (min-width: 600px) { .avm-report.block { diff --git a/blocks/avm-report/avm-report.js b/blocks/avm-report/avm-report.js index dc2a0e77..c6a7f5fc 100644 --- a/blocks/avm-report/avm-report.js +++ b/blocks/avm-report/avm-report.js @@ -1,23 +1,8 @@ -import { - showModal, -} from '../../scripts/util.js'; - -let alreadyDeferred = false; -function initGooglePlacesAPI() { - if (alreadyDeferred) { - return; - } - alreadyDeferred = true; - const script = document.createElement('script'); - script.type = 'text/partytown'; - script.innerHTML = ` - const script = document.createElement('script'); - script.type = 'module'; - script.src = '${window.hlx.codeBasePath}/blocks/avm-report/avm-report-delayed.js'; - document.head.append(script); - `; - document.head.append(script); -} +import { showModal } from '../../scripts/util.js'; + +import loadMaps from '../../scripts/google-maps/index.js'; + +let autocompleteAttached = false; export default async function decorate(block) { const form = document.createElement('form'); @@ -32,6 +17,19 @@ export default async function decorate(block) { const addressField = form.querySelector('input[name="avmaddress"]'); + addressField.addEventListener('focus', async () => { + if (!autocompleteAttached) { + loadMaps(); + await window.google.maps.importLibrary('places'); + // eslint-disable-next-line no-unused-vars + const autocomplete = new window.google.maps.places.Autocomplete(addressField, { + fields: ['formatted_address'], + types: ['address'], + }); + autocompleteAttached = true; + } + }); + form.addEventListener('submit', (e) => { e.preventDefault(); const address = addressField.value; @@ -49,5 +47,4 @@ export default async function decorate(block) { window.location = redirect; }); block.append(form); - initGooglePlacesAPI(); } diff --git a/blocks/cards/cards.css b/blocks/cards/cards.css index ff629085..72d1dded 100644 --- a/blocks/cards/cards.css +++ b/blocks/cards/cards.css @@ -6,6 +6,10 @@ width: 100%; } +.cards.block.shade-icon { + margin-bottom: 40px; +} + .cards.block .title { padding: 2em 0; } @@ -127,10 +131,40 @@ letter-spacing: var(--letter-spacing-xs); } +.cards.block.shade-icon .cards-list .cards-item .card-body p { + text-align: center; + margin-bottom: 10px; +} + .section.grey-background .cards.block .cards-list .cards-item .card-image p { background-color: var(--light-grey); } +.cards.block.shade-icon .cards-list { + gap: 70px; + max-width: 83.333%; +} + +.cards.block.shade-icon .cards-list .cards-item { + background-color: var(--light-grey); + border-top: 1px solid #000; + min-height: 274px; + align-items: center; + justify-content: center; +} + +.cards.block.shade-icon .cards-list .cards-item .card-body h4 { + text-align: center; + padding: 8px 0 20px; +} + +.cards.block.shade-icon .cards-list .cards-item .card-body .icon img { + margin: 0 auto; + display: block; + height: 35px; + width: 35px; +} + @media screen and (min-width: 600px) { .cards.block.icons { margin: 0 auto; @@ -180,4 +214,3 @@ column-gap: 20px; } } - diff --git a/blocks/embed/lite-yt-embed.css b/blocks/embed/lite-yt-embed.css new file mode 100644 index 00000000..089e1931 --- /dev/null +++ b/blocks/embed/lite-yt-embed.css @@ -0,0 +1,90 @@ +/* copied from https://github.com/paulirish/lite-youtube-embed/releases/tag/v0.2.0 */ +/* stylelint-disable */ + +lite-youtube { + background-color: #000; + position: relative; + display: block; + contain: content; + background-position: center center; + background-size: cover; + cursor: pointer; + max-width: 720px; +} + +/* gradient */ +lite-youtube::before { + content: ''; + display: block; + position: absolute; + top: 0; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==); + background-position: top; + background-repeat: repeat-x; + height: 60px; + padding-bottom: 50px; + width: 100%; + transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); +} + +/* responsive iframe with a 16:9 aspect ratio + thanks https://css-tricks.com/responsive-iframes/ +*/ +lite-youtube::after { + content: ""; + display: block; + padding-bottom: calc(100% / (16 / 9)); +} + +lite-youtube > iframe { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + border: 0; +} + +/* play button */ +lite-youtube > .lty-playbtn { + width: 68px; + height: 48px; + position: absolute; + cursor: pointer; + transform: translate3d(-50%, -50%, 0); + top: 50%; + left: 50%; + z-index: 1; + background-color: transparent; + /* YT's actual play button svg */ + background-image: url('data:image/svg+xml;utf8,'); + filter: grayscale(100%); + transition: filter .1s cubic-bezier(0, 0, 0.2, 1); + border: none; +} + +lite-youtube:hover > .lty-playbtn, +lite-youtube .lty-playbtn:focus { + filter: none; +} + +/* Post-click styles */ +lite-youtube.lyt-activated { + cursor: unset; +} + +lite-youtube.lyt-activated::before, +lite-youtube.lyt-activated > .lty-playbtn { + opacity: 0; + pointer-events: none; +} + +.lyt-visually-hidden { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} diff --git a/blocks/embed/lite-yt-embed.js b/blocks/embed/lite-yt-embed.js new file mode 100644 index 00000000..4de5f989 --- /dev/null +++ b/blocks/embed/lite-yt-embed.js @@ -0,0 +1,127 @@ +// Copied from https://github.com/paulirish/lite-youtube-embed/releases/tag/v0.2.0 +/* eslint-disable */ +/** + * A lightweight youtube embed. Still should feel the same to the user, just MUCH faster to initialize and paint. + * + * Thx to these as the inspiration + * https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html + * https://autoplay-youtube-player.glitch.me/ + * + * Once built it, I also found these: + * https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (👍👍) + * https://github.com/Daugilas/lazyYT + * https://github.com/vb/lazyframe + */ +class LiteYTEmbed extends HTMLElement { + connectedCallback() { + this.videoId = this.getAttribute('videoid'); + + let playBtnEl = this.querySelector('.lty-playbtn'); + // A label for the button takes priority over a [playlabel] attribute on the custom-element + this.playLabel = (playBtnEl && playBtnEl.textContent.trim()) || this.getAttribute('playlabel') || 'Play'; + + /** + * Lo, the youtube placeholder image! (aka the thumbnail, poster image, etc) + * + * See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md + * + * TODO: Do the sddefault->hqdefault fallback + * - When doing this, apply referrerpolicy (https://github.com/ampproject/amphtml/pull/3940) + * TODO: Consider using webp if supported, falling back to jpg + */ + if (!this.style.backgroundImage) { + this.posterUrl = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`; + // Warm the connection for the poster image + LiteYTEmbed.addPrefetch('preload', this.posterUrl, 'image'); + + this.style.backgroundImage = `url("${this.posterUrl}")`; + } + + // Set up play button, and its visually hidden label + if (!playBtnEl) { + playBtnEl = document.createElement('button'); + playBtnEl.type = 'button'; + playBtnEl.classList.add('lty-playbtn'); + this.append(playBtnEl); + } + if (!playBtnEl.textContent) { + const playBtnLabelEl = document.createElement('span'); + playBtnLabelEl.className = 'lyt-visually-hidden'; + playBtnLabelEl.textContent = this.playLabel; + playBtnEl.append(playBtnLabelEl); + } + + // On hover (or tap), warm up the TCP connections we're (likely) about to use. + this.addEventListener('pointerover', LiteYTEmbed.warmConnections, {once: true}); + + // Once the user clicks, add the real iframe and drop our play button + // TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time + // We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003 + this.addEventListener('click', e => this.addIframe()); + } + + // // TODO: Support the the user changing the [videoid] attribute + // attributeChangedCallback() { + // } + + /** + * Add a to the head + */ + static addPrefetch(kind, url, as) { + const linkEl = document.createElement('link'); + linkEl.rel = kind; + linkEl.href = url; + if (as) { + linkEl.as = as; + } + document.head.append(linkEl); + } + + /** + * Begin pre-connecting to warm up the iframe load + * Since the embed's network requests load within its iframe, + * preload/prefetch'ing them outside the iframe will only cause double-downloads. + * So, the best we can do is warm up a few connections to origins that are in the critical path. + * + * Maybe `` would work, but it's unsupported: http://crbug.com/593267 + * But TBH, I don't think it'll happen soon with Site Isolation and split caches adding serious complexity. + */ + static warmConnections() { + if (LiteYTEmbed.preconnected) return; + + // The iframe document and most of its subresources come right off youtube.com + LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com'); + // The botguard script is fetched off from google.com + LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com'); + + // Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling. + LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net'); + LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net'); + + LiteYTEmbed.preconnected = true; + } + + addIframe() { + const params = new URLSearchParams(this.getAttribute('params') || []); + params.append('autoplay', '1'); + + const iframeEl = document.createElement('iframe'); + iframeEl.width = 560; + iframeEl.height = 315; + // No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include + iframeEl.title = this.playLabel; + iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'; + iframeEl.allowFullscreen = true; + // AFAIK, the encoding here isn't necessary for XSS, but we'll do it only because this is a URL + // https://stackoverflow.com/q/64959723/89484 + iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${params.toString()}`; + this.append(iframeEl); + + this.classList.add('lyt-activated'); + + // Set focus for a11y + this.querySelector('iframe').focus(); + } +} +// Register custom element +customElements.define('lite-youtube', LiteYTEmbed); diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index c8ea6721..44381b8a 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -87,7 +87,7 @@ footer { margin-right: 10px; } -.icon-facebook svg, .icon-linkedin svg, .icon-instagram svg, .icon-youtube svg { +.icon-facebook img, .icon-linkedin img, .icon-instagram img, .icon-youtube img { height: 20px; width: 20px; margin-right: 5px; @@ -95,7 +95,7 @@ footer { } -.footer-container-flex > div:nth-child(1) svg { +.footer-container-flex > div:nth-child(1) img { position: relative; height: 68px; width: 68px; diff --git a/blocks/header/header.css b/blocks/header/header.css index c2264db7..398a5dd1 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -97,7 +97,7 @@ body.light-nav { font-family: var(--font-family-proxima); font-weight: var(--font-weight-normal); font-size: var(--body-font-size-s); - letter-spacing:var(--letter-spacing-reg); + letter-spacing: var(--letter-spacing-reg); line-height: var(--body-font-size-s); text-decoration: none; cursor: pointer; @@ -197,6 +197,7 @@ body.light-nav { } .header.block nav .nav-hamburger .nav-hamburger-icon { + position: relative; height: 24px; width: 24px; } @@ -208,9 +209,19 @@ body.light-nav { .header.block nav .nav-hamburger .close { display: none; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; } .header.block nav .nav-hamburger .open { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; filter: var(--hamburger-filter); } @@ -239,7 +250,7 @@ body.light-nav { height: var(--nav-height); background: transparent; } - + body.light-nav .header.block nav[aria-expanded="true"] { --logo-filter: invert(1); } diff --git a/blocks/header/header.js b/blocks/header/header.js index a59f69bc..d369cc32 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -2,7 +2,7 @@ import { BREAKPOINTS } from '../../scripts/scripts.js'; import { getMetadata, decorateIcons, decorateSections } from '../../scripts/aem.js'; // media query match that indicates mobile/tablet width -const isDesktop = BREAKPOINTS.medium; +const isDesktop = BREAKPOINTS.large; function closeOnEscape(e) { if (e.code === 'Escape') { @@ -230,7 +230,9 @@ export default async function decorate(block) { nav.setAttribute('aria-expanded', 'false'); // prevent mobile nav behavior on window resize toggleMenu(nav, navSections, isDesktop.matches); - isDesktop.addEventListener('change', () => toggleMenu(nav, navSections, isDesktop.matches)); + isDesktop.addEventListener('change', () => { + toggleMenu(nav, navSections, isDesktop.matches); + }); decorateIcons(nav); const navWrapper = document.createElement('div'); diff --git a/blocks/hero/search/home.css b/blocks/hero/search/home.css index 59117177..7dbedee2 100644 --- a/blocks/hero/search/home.css +++ b/blocks/hero/search/home.css @@ -18,6 +18,11 @@ display: none; } +.hero.block .content .homes .search-bar .search-country-select-parent { + position: relative; + width: 80px; +} + .hero.block .content .homes .search-bar .suggester-input { flex: 1; } @@ -45,7 +50,7 @@ justify-content: center; } -.hero.block .content .homes .search-bar .filter span.icon svg { +.hero.block .content .homes .search-bar .filter span.icon img { height: 22px; width: 22px; color: var(--black); diff --git a/blocks/hero/search/home.js b/blocks/hero/search/home.js index c73af908..069079cc 100644 --- a/blocks/hero/search/home.js +++ b/blocks/hero/search/home.js @@ -1,11 +1,10 @@ -import { decorateIcons } from '../../../scripts/aem.js'; import { build as buildCountrySelect, } from '../../shared/search-countries/search-countries.js'; function observeForm() { const script = document.createElement('script'); - script.type = 'text/partytown'; + script.type = 'module'; script.innerHTML = ` const script = document.createElement('script'); script.type = 'module'; @@ -76,6 +75,7 @@ async function buildForm() {