From 9e43633d332ceeacef3cae8daf36d83535845499 Mon Sep 17 00:00:00 2001 From: Justin Rings Date: Tue, 20 Aug 2024 06:11:33 -0700 Subject: [PATCH 01/17] feat: add cards block --- express/blocks/cards/cards.css | 169 ++++++++++++++++++++++++++++++ express/blocks/cards/cards.js | 27 +++++ test/blocks/cards/cards.test.js | 38 +++++++ test/blocks/cards/mocks/body.html | 41 ++++++++ 4 files changed, 275 insertions(+) create mode 100644 express/blocks/cards/cards.css create mode 100644 express/blocks/cards/cards.js create mode 100644 test/blocks/cards/cards.test.js create mode 100644 test/blocks/cards/mocks/body.html diff --git a/express/blocks/cards/cards.css b/express/blocks/cards/cards.css new file mode 100644 index 00000000..fe1395a2 --- /dev/null +++ b/express/blocks/cards/cards.css @@ -0,0 +1,169 @@ +main .section.cards-container { + background-color: #F1F3F4; + padding-top: 80px; +} + +main .section.cards-container>div>h2:first-of-type { + margin-top: 0; +} + +main .section .cards-container>div, +main .section .cards-dark-container>div { + max-width: 870px; +} + +main .section .cards { + padding: 0; + display: flex; + flex-direction: column; + margin: 56px auto; +} + +main .section .cards .card { + background-color: #FFF; + width: 100%; + margin: 0 auto 16px auto; + border-radius: 20px; + display: flex; + flex-direction: column; +} + +main .section .cards .card .card-image { + box-sizing: border-box; + border-radius: 20px 20px 0 0; + min-height: 136px; +} + +main .section .cards .card .card-image img { + border-radius: 20px 20px 0 0; +} + + +main .section .cards .card .card-content { + padding: 32px 32px 8px 32px; + color: #232323; +} + +main .section .cards .card .card-content h2 { + text-align: center; + margin-bottom: 32px; +} + +main .section .cards .card .card-content p { + margin-top: 0; +} + +main .section .cards.dark .card { + background-color: #000; +} + +main .section .cards a.card { + text-decoration: none; +} + +main .section .cards.dark .card .card-content { + color: #FFF; +} + +main .section .cards.dark .card .card-image { + background-color: #343434; +} + +main .section .cards.large { + gap: 32px; +} + +main .section .cards.large .card { + margin: 0 auto; +} + +main .section .cards.large .card .card-content { + padding: 32px 32px 8px 32px; +} + +main .section .cards.large .card .card-content h2 { + text-align: left; + font-size: 1.375rem; + line-height: 1.1; + margin-bottom: 32px; +} + +main .section .cards.large .card .card-content p, +main .section .cards.large .card .card-content li { + text-align: left; + font-size: 1.125rem; +} + +@media (min-width: 600px) { + main .section .cards .card { + width: 344px; + } +} + +@media (min-width:900px) { + main .section .cards { + max-width: 1024px; + width: unset; + margin: 80px auto; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + } + + main .section .cards .card { + width: 248px; + margin-right: 32px; + background-color: #FFF; + box-shadow: 0 4px 8px 2px rgba(102, 102, 102, 0.1); + margin: 0 20px 32px 20px; + } + + main .section .cards .card .card-image { + background-color: #F1F3F4; + } + + main .section .cards .card .card-content { + padding: 16px 16px 8px 16px; + } + + main .section .cards .card .card-content h2 { + text-align: center; + font-size: 1.25rem; + line-height: 1.2; + margin-bottom: 8px; + } + + main .section .cards .card .card-content p, + main .section .cards .card .card-content li { + font-size: 0.9rem; + } + + main .section .cards.large { + flex-wrap: wrap; + gap: 40px; + width: 724px; + } + + main .section .cards.large .card { + width: 342px; + } + + main .section .cards.featured .card:first-of-type { + width: 724px; + flex-direction: row; + } + + main .section .cards.featured .card:first-of-type .card-image { + border-top-left-radius: 20px; + border-top-right-radius: 0; + border-bottom-left-radius: 20px; + min-height: unset; + min-width: 342px; + } + + main .section .cards.featured .card:first-of-type .card-content { + padding: 32px 40px 8px 40px; + min-height: 294px; + } +} + diff --git a/express/blocks/cards/cards.js b/express/blocks/cards/cards.js new file mode 100644 index 00000000..e3f2f299 --- /dev/null +++ b/express/blocks/cards/cards.js @@ -0,0 +1,27 @@ +// eslint-disable-next-line import/no-unresolved +import { getLibs } from '../../scripts/utils.js'; + +const { createTag } = await import(`${getLibs()}/utils/utils.js`); +/** + * @param {HTMLDivElement} $block + */ +export default function decorate($block) { + $block.querySelectorAll(':scope>div').forEach(($card) => { + $card.classList.add('card'); + const $cardDivs = [...$card.children]; + $cardDivs.forEach(($div) => { + if ($div.querySelector('img')) { + $div.classList.add('card-image'); + } else { + $div.classList.add('card-content'); + } + const $a = $div.querySelector('a'); + if ($a && $a.textContent.trim().startsWith('https://')) { + const $wrapper = createTag('a', { href: $a.href, class: 'card' }); + $a.remove(); + $wrapper.innerHTML = $card.innerHTML; + $block.replaceChild($wrapper, $card); + } + }); + }); +} diff --git a/test/blocks/cards/cards.test.js b/test/blocks/cards/cards.test.js new file mode 100644 index 00000000..d543e74f --- /dev/null +++ b/test/blocks/cards/cards.test.js @@ -0,0 +1,38 @@ +/* eslint-env mocha */ +/* eslint-disable no-unused-vars */ + +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; + +const imports = await Promise.all([ + import('../../../express/scripts/scripts.js'), + import('../../../express/blocks/cards/cards.js'), +]); +const { default: decorate } = imports[1]; + +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); + +describe('Cards', () => { + before(() => { + window.isTestEnv = true; + }); + + it('Cards exists', () => { + const cards = document.querySelector('.cards'); + decorate(cards); + expect(cards).to.exist; + }); + + it('Cards has the correct elements', () => { + expect(document.querySelector('.card')).to.exist; + // If img + expect(document.querySelector('.card-image')).to.exist; + // If not img + expect(document.querySelector('.card-content')).to.exist; + }); + + it('If text content starts with https://, create a card wrapper', () => { + expect(document.querySelector('a')).to.exist; + expect(document.querySelector('a.card')).to.exist; + }); +}); diff --git a/test/blocks/cards/mocks/body.html b/test/blocks/cards/mocks/body.html new file mode 100644 index 00000000..4fb5da74 --- /dev/null +++ b/test/blocks/cards/mocks/body.html @@ -0,0 +1,41 @@ +
+
+
+ + + + + + +
+
+

+ https://edex.adobe.com/express +

+

Teach anything with Adobe Express and keep students engaged with free, flexible, creative activities for + all ages and subjects.

+

Try now

+
+
+
+
+ + + + + + +
+
+
From 5c14a6d74b0272ffb871e34ed5e156556f230f04 Mon Sep 17 00:00:00 2001 From: Justin Rings Date: Thu, 22 Aug 2024 09:38:55 -0700 Subject: [PATCH 02/17] fix style selectors --- express/blocks/cards/cards.css | 10 ++++++++-- express/blocks/cards/cards.js | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/express/blocks/cards/cards.css b/express/blocks/cards/cards.css index fe1395a2..fb014e05 100644 --- a/express/blocks/cards/cards.css +++ b/express/blocks/cards/cards.css @@ -1,12 +1,18 @@ -main .section.cards-container { +main .section:has(.card) { background-color: #F1F3F4; padding-top: 80px; } -main .section.cards-container>div>h2:first-of-type { +main .section:has(.card)>div>h2:first-of-type { margin-top: 0; } +main .section:has(.content) > div { + max-width: 375px; + margin: auto; + padding: 0; +} + main .section .cards-container>div, main .section .cards-dark-container>div { max-width: 870px; diff --git a/express/blocks/cards/cards.js b/express/blocks/cards/cards.js index e3f2f299..4aefc163 100644 --- a/express/blocks/cards/cards.js +++ b/express/blocks/cards/cards.js @@ -1,11 +1,13 @@ // eslint-disable-next-line import/no-unresolved import { getLibs } from '../../scripts/utils.js'; +import { decorateButtonsDeprecated } from '../../scripts/utils/decorate.js'; const { createTag } = await import(`${getLibs()}/utils/utils.js`); /** * @param {HTMLDivElement} $block */ export default function decorate($block) { + decorateButtonsDeprecated($block); $block.querySelectorAll(':scope>div').forEach(($card) => { $card.classList.add('card'); const $cardDivs = [...$card.children]; From 66523a09baf85ec5002ec9d259fb4b5f2380a1c4 Mon Sep 17 00:00:00 2001 From: Justin Rings Date: Mon, 26 Aug 2024 10:02:16 -0700 Subject: [PATCH 03/17] fix content selector --- express/blocks/cards/cards.css | 6 ------ 1 file changed, 6 deletions(-) diff --git a/express/blocks/cards/cards.css b/express/blocks/cards/cards.css index fb014e05..a2870259 100644 --- a/express/blocks/cards/cards.css +++ b/express/blocks/cards/cards.css @@ -7,12 +7,6 @@ main .section:has(.card)>div>h2:first-of-type { margin-top: 0; } -main .section:has(.content) > div { - max-width: 375px; - margin: auto; - padding: 0; -} - main .section .cards-container>div, main .section .cards-dark-container>div { max-width: 870px; From 36046f96f1ac1eafa634c980bb0c3daf0c5a4e1c Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Wed, 28 Aug 2024 09:33:24 -0500 Subject: [PATCH 04/17] MWPW-156767: hero color, color carousel how to, ckg link list --- express/blocks/ax-columns/ax-columns.css | 2 +- .../blocks/ckg-link-list/ckg-link-list.css | 61 +++ express/blocks/ckg-link-list/ckg-link-list.js | 62 ++++ .../color-how-to-carousel.css | 289 +++++++++++++++ .../color-how-to-carousel.js | 263 +++++++++++++ express/blocks/hero-color/hero-color.css | 137 +++++++ express/blocks/hero-color/hero-color.js | 137 +++++++ express/blocks/long-text/long-text.css | 1 + express/blocks/template-x/template-x.css | 4 + .../ckg-link-list/ckg-link-list.test.js | 350 ++++++++++++++++++ test/blocks/ckg-link-list/mocks/default.html | 7 + .../color-how-to-carousel.test.js | 59 +++ .../mocks/body-dark.html | 30 ++ .../color-how-to-carousel/mocks/body.html | 39 ++ test/blocks/hero-color/hero-color.test.js | 57 +++ test/blocks/hero-color/mocks/body.html | 18 + 16 files changed, 1515 insertions(+), 1 deletion(-) create mode 100644 express/blocks/ckg-link-list/ckg-link-list.css create mode 100644 express/blocks/ckg-link-list/ckg-link-list.js create mode 100644 express/blocks/color-how-to-carousel/color-how-to-carousel.css create mode 100644 express/blocks/color-how-to-carousel/color-how-to-carousel.js create mode 100644 express/blocks/hero-color/hero-color.css create mode 100644 express/blocks/hero-color/hero-color.js create mode 100644 test/blocks/ckg-link-list/ckg-link-list.test.js create mode 100644 test/blocks/ckg-link-list/mocks/default.html create mode 100644 test/blocks/color-how-to-carousel/color-how-to-carousel.test.js create mode 100644 test/blocks/color-how-to-carousel/mocks/body-dark.html create mode 100644 test/blocks/color-how-to-carousel/mocks/body.html create mode 100644 test/blocks/hero-color/hero-color.test.js create mode 100644 test/blocks/hero-color/mocks/body.html diff --git a/express/blocks/ax-columns/ax-columns.css b/express/blocks/ax-columns/ax-columns.css index 86c2f3af..3c86823e 100644 --- a/express/blocks/ax-columns/ax-columns.css +++ b/express/blocks/ax-columns/ax-columns.css @@ -625,7 +625,7 @@ width: 46%; } -.ax-columns.columns.color > div { +.ax-columns.color > div { padding: 80px 0 0 0; gap: 15px; } diff --git a/express/blocks/ckg-link-list/ckg-link-list.css b/express/blocks/ckg-link-list/ckg-link-list.css new file mode 100644 index 00000000..42205391 --- /dev/null +++ b/express/blocks/ckg-link-list/ckg-link-list.css @@ -0,0 +1,61 @@ +.ckg-link-list { + min-height: 62px; +} + +.ckg-link-list .carousel-container { + margin-bottom: 0; +} + +.ckg-link-list .button-container { + margin: 0 4px; + display: block; + border-radius: 40px; + background-color: var(--color-gray-200); + overflow: hidden; + line-height: 0.5; /* fix carousel fader alignment issue */ +} + +.ckg-link-list .button-container a.button { + margin: 0; + display: flex; + align-items: center; + background-color: unset; + padding: 14px 24px; + font-size: var(--body-font-size-m); + font-family: var(--body-font-family); + font-weight: 700; + color: var(--color-black); + border: transparent; + transition: background-color 0.2s; +} + +.ckg-link-list .button-container a.button .color-dot { + height: 18px; + width: 18px; + border: 2px solid #FFFFFF50; + border-radius: 50%; + margin-right: 10px; +} + +.ckg-link-list .button-container a.button.colorful { + color: var(--color-white); + -webkit-backdrop-filter: brightness(0.7); + backdrop-filter: brightness(0.7); + transition: backdrop-filter 0.2s, -webkit-backdrop-filter 0.2s; +} + +.ckg-link-list .button-container a.button:hover { + background-color: var(--color-gray-300); +} + +.ckg-link-list .button-container a.button.colorful:hover { + background-color: unset; + -webkit-backdrop-filter: brightness(0.6); + backdrop-filter: brightness(0.6); +} + +.ckg-link-list .button-container a.button.colorful:active { + background-color: unset; + -webkit-backdrop-filter: brightness(0.4); + backdrop-filter: brightness(0.4); +} diff --git a/express/blocks/ckg-link-list/ckg-link-list.js b/express/blocks/ckg-link-list/ckg-link-list.js new file mode 100644 index 00000000..7580d102 --- /dev/null +++ b/express/blocks/ckg-link-list/ckg-link-list.js @@ -0,0 +1,62 @@ +import { getLibs } from '../../scripts/utils.js'; +import { getDataWithContext } from '../../scripts/utils/browse-api-controller.js'; +import buildCarousel from '../../scripts/widgets/carousel.js'; +import { titleCase } from '../../scripts/utils/string.js'; + +const { createTag, getConfig } = await import(`${getLibs()}/utils/utils.js`); + +function addColorSampler(pill, colorHex, btn) { + const colorDot = createTag('div', { + class: 'color-dot', + style: `background-color: ${colorHex}`, + }); + + const aTag = btn.querySelector('a'); + btn.style.backgroundColor = colorHex; + aTag.classList.add('colorful'); + + aTag.prepend(colorDot); +} + +export default async function decorate(block) { + console.log(block); + block.style.visibility = 'hidden'; + + const payloadContext = { urlPath: block.textContent.trim() || window.location.pathname }; + const ckgResult = await getDataWithContext(payloadContext); + if (!ckgResult) return; + const pills = ckgResult?.queryResults?.[0]?.facets?.[0]?.buckets; + const hexCodes = ckgResult?.queryResults?.[0].context?.application?.['metadata.color.hexCodes']; + + if (!pills || !pills.length) return; + + pills.forEach((pill) => { + const colorHex = hexCodes[pill.canonicalName]; + const { prefix } = getConfig().locale; + if (pill.value.startsWith(`${prefix}/express/colors/search`)) { + return; + } + + const colorPath = pill.value; + const colorName = pill.displayValue; + const buttonContainer = createTag('p', { class: 'button-container' }); + const aTag = createTag('a', { + class: 'button', + title: colorName, + href: colorPath, + }, titleCase(colorName)); + + buttonContainer.append(aTag); + block.append(buttonContainer); + + if (colorHex) { + addColorSampler(pill, colorHex, buttonContainer); + } + }); + + if (!block.children) return; + + const options = { centerAlign: true }; + await buildCarousel('.button-container', block, options); + block.style.visibility = 'visible'; +} diff --git a/express/blocks/color-how-to-carousel/color-how-to-carousel.css b/express/blocks/color-how-to-carousel/color-how-to-carousel.css new file mode 100644 index 00000000..056bda7e --- /dev/null +++ b/express/blocks/color-how-to-carousel/color-how-to-carousel.css @@ -0,0 +1,289 @@ +/* to remove after Milo migration */ +.section > .color-how-to-carousel-wrapper { + max-width: 1200px; + margin: auto; +} + +.color-how-to-carousel { + gap: 24px; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; +} + +.color-how-to-carousel > picture > img { + margin-bottom: 80px; + width: calc(100% - 40px); +} + +.color-how-to-carousel > .content-wrapper { + background-color: var(--color-gray-100); + text-align: left; + border-radius: 40px; + margin: 0 24px; + padding: 56px 24px; + width: 100%; + max-width: fit-content; +} + +.color-how-to-carousel > .content-wrapper > a.button:any-link { + text-align: left; + margin: 0; +} + +.color-how-to-carousel > .content-wrapper p.button-container { + margin: 16px 0; +} + +.color-how-to-carousel > .content-wrapper > h2 { + font-size: var(--heading-font-size-l); + text-align: left; + margin-top: 0; +} + +.color-how-to-carousel > .content-wrapper > div { + max-width: 810px; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +.color-how-to-carousel .tips .tip { + display: none; + grid-row-start: 1; + grid-column-start: 1; + text-align: left; + margin-top: 64px; + margin-bottom: 56px; +} + +.color-how-to-carousel .tips .tip.active { + display: block; + animation: fadeIn ease 2s; + -webkit-animation: fadeIn ease 2s; + -moz-animation: fadeIn ease 2s; +} + +.color-how-to-carousel .tip-numbers { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + text-align: left; + margin-top: 18px; +} + +.color-how-to-carousel .tip-number { + width: 34px; + min-width: 34px; + height: 34px; + min-height: 34px; + display: block; + position: relative; + background-color: var(--color-gray-200); + border-radius: 50%; + margin-right: 12px; + color: var(--color-black); + font-size: var(--body-font-size-s); + font-weight: 600; + align-self: flex-start; + cursor: pointer; +} + +.color-how-to-carousel.dark .tip-number { + background-color: var(--color-gray-400); +} + +.color-how-to-carousel .tip-number:focus { + outline: none; + box-shadow: + 0 0 0 2px var(--color-white), + 0 0 0 4px var(--color-info-accent ); +} + +.color-how-to-carousel .tip-number.active { + background-color: var(--color-black); + color: var(--color-white); +} + +.color-how-to-carousel.dark .tip-number.active { + background-color: var(--color-white); + color: var(--color-black); +} + +.color-how-to-carousel .tip-number span { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +.color-how-to-carousel .tip-text p, +.color-how-to-carousel .tip-text div { + font-size: var(--body-font-size-xl); + margin-top: 16px; + margin-bottom: 0; +} + +.color-how-to-carousel .tip-text h3 { + margin-bottom: 16px; + font-size: var(--heading-font-size-m); + text-align: left; +} + +.color-how-to-carousel .icon { + width: 56px; + height: 56px; +} + +.color-how-to-carousel > .img-wrapper { + position: relative; + padding: 16px; + border-radius: 20px; + margin: 0 24px; + height: calc((100vw - 80px) / 300 * 426); + width: calc(100vw - 80px); +} + +.color-how-to-carousel > .img-wrapper .color-graph-text-overlay { + position: absolute; + text-align: left; + padding-left: 4%; + font-weight: var(--heading-font-weight); + padding-top: 4%; +} + +.color-how-to-carousel > .img-wrapper .color-graph-text-overlay .color-name { + font-size: calc(10vw - 10px); + margin: unset; + line-height: 1.3; +} + +.color-how-to-carousel > .img-wrapper .color-graph-text-overlay .color-hex { + font-size: calc(14vw - 12px); + margin: unset; + line-height: 1.3; +} + +.color-how-to-carousel > .img-wrapper > svg { + height: inherit; + width: inherit; +} + +.color-how-to-carousel.light .content-wrapper a.button:any-link { + padding: 12px 24px; + border-radius: 24px; + border: none; + background-color: var(--color-black); + color: var(--color-white); +} + +.color-how-to-carousel.dark .content-wrapper a.button:any-link { + padding: 12px 24px; + border-radius: 24px; + border: none; + background-color: var(--color-white); + color: var(--color-black); +} + +.color-how-to-carousel.shadow .img-wrapper, +.color-how-to-carousel.shadow .content-wrapper { + box-shadow: 0 3px 6px #00000029; +} + +@media (min-width: 900px) { + .color-how-to-carousel { + gap: unset; + } + + .color-how-to-carousel > picture > img { + margin: 0; + object-position: right; + object-fit: cover; + min-height: 100%; + max-height: 100%; + width: unset; + } + + .color-how-to-carousel > .img-wrapper { + padding: 24px; + height: 570px; + width: 412px; + margin-top: 24px; + } + + .color-how-to-carousel.top-align > .img-wrapper { + margin-top: unset; + } + + .color-how-to-carousel > .img-wrapper .color-graph-text-overlay .color-name { + font-size: var(--heading-font-size-m); + } + + .color-how-to-carousel > .img-wrapper .color-graph-text-overlay .color-hex { + font-size: var(--heading-font-size-xxl); + } + + .color-how-to-carousel .tips .tip { + margin: 64px 0; + } + + .color-how-to-carousel { + justify-content: flex-end; + flex-direction: row; + align-items: start; + } + + .color-how-to-carousel > .content-wrapper { + max-width: 600px; + flex-basis: 60%; + padding: 80px 70px; + } + + .color-how-to-carousel .content-wrapper a.button:any-link { + font-size: var(--body-font-size-m); + padding: 14px 24px; + } + + .color-how-to-carousel > .content-wrapper { + margin: 0 24px 0 80px; + } + + .color-how-to-carousel > .content-wrapper p.button-container { + margin: 40px 0 0; + } +} + +@media (min-width: 1200px) { + .color-how-to-carousel { + justify-content: center; + } + + .color-how-to-carousel.no-cover > div { + margin: 0 120px 0 20px; + } +} + +@keyframes fadeIn { + 0% { opacity:0.1; } + 100% { opacity:1; } +} + +@-moz-keyframes fadeIn { + 0% { opacity:0; } + 100% { opacity:1; } +} + +@-webkit-keyframes fadeIn { + 0% { opacity:0; } + 100% { opacity:1; } +} + +@-o-keyframes fadeIn { + 0% { opacity:0; } + 100% { opacity:1; } +} + +@-ms-keyframes fadeIn { + 0% { opacity:0; } + 100% { opacity:1; } +} diff --git a/express/blocks/color-how-to-carousel/color-how-to-carousel.js b/express/blocks/color-how-to-carousel/color-how-to-carousel.js new file mode 100644 index 00000000..5f1d4b95 --- /dev/null +++ b/express/blocks/color-how-to-carousel/color-how-to-carousel.js @@ -0,0 +1,263 @@ +import { getLibs } from '../../scripts/utils.js'; +import { addTempWrapperDeprecated } from '../../scripts/utils/decorate.js'; +import isDarkOverlayReadable from '../../scripts/color-tools.js'; + +const { createTag } = await import(`${getLibs()}/utils/utils.js`); + +function activate(block, target) { + // de-activate all + block.querySelectorAll('.tip, .tip-number') + .forEach((item) => { + item.classList.remove('active'); + }); + + // get index of the target + const i = parseInt(target.getAttribute('data-tip-index'), 10); + // activate corresponding number and tip + block.querySelectorAll(`.tip-${i}`) + .forEach((elem) => elem.classList.add('active')); +} + +function buildSchema(rows, payload) { + const schemaObj = { + '@context': 'http://schema.org', + '@type': 'HowTo', + name: payload.heading?.textContent.trim() || payload.howToDocument.title, + step: [], + }; + + rows.forEach((row, i) => { + const cells = Array.from(row.children); + + schemaObj.step.push({ + '@type': 'HowToStep', + position: i + 1, + name: cells[0].textContent.trim(), + itemListElement: { + '@type': 'HowToDirection', + text: cells[1].textContent.trim(), + }, + }); + }); + + const schema = createTag('script', { type: 'application/ld+json' }); + schema.innerHTML = JSON.stringify(schemaObj); + const { head } = payload.howToDocument; + head.append(schema); +} + +function initRotation(payload) { + if (payload.howToWindow && !payload.rotationInterval) { + payload.rotationInterval = payload.howToWindow.setInterval(() => { + payload.howToDocument.querySelectorAll('.tip-numbers') + .forEach((numbers) => { + // find next adjacent sibling of the currently activated tip + let activeAdjacentSibling = numbers.querySelector('.tip-number.active+.tip-number'); + if (!activeAdjacentSibling) { + // if no next adjacent, back to first + activeAdjacentSibling = numbers.firstElementChild; + } + activate(numbers.parentElement, activeAdjacentSibling); + }); + }, 5000); + } +} + +function buildColorHowToCarousel(block, payload) { + const carouselDivs = block.querySelector('.content-wrapper'); + const rows = Array.from(carouselDivs.children); + const carousel = createTag('div', { class: 'carousel-wrapper' }); + + const includeSchema = block.classList.contains('schema'); + + const numbers = createTag('div', { + class: 'tip-numbers', + 'aria-role': 'tablist', + }); + carousel.prepend(numbers); + const tips = createTag('div', { class: 'tips' }); + carousel.append(tips); + if (payload.icon) carouselDivs.append(payload.icon); + carouselDivs.append(payload.heading, carousel); + if (payload.cta) carouselDivs.append(payload.cta); + + if (includeSchema) { + buildSchema(rows, payload); + } + + rows.forEach((row, i) => { + row.classList.add('tip'); + row.classList.add(`tip-${i + 1}`); + row.setAttribute('data-tip-index', i + 1); + + const cells = Array.from(row.children); + + const h3 = createTag('h3'); + h3.innerHTML = cells[0].textContent.trim(); + const text = createTag('div', { class: 'tip-text' }); + text.append(h3); + text.append(cells[1]); + + row.innerHTML = ''; + row.append(text); + + tips.prepend(row); + + const number = createTag('div', { + class: `tip-number tip-${i + 1}`, + tabindex: '0', + title: `${i + 1}`, + 'aria-role': 'tab', + }); + + number.innerHTML = `${i + 1}`; + number.setAttribute('data-tip-index', i + 1); + + number.addEventListener('click', (e) => { + if (payload.rotationInterval) { + payload.howToWindow.clearTimeout(payload.rotationInterval); + } + + let { target } = e; + if (e.target.nodeName.toLowerCase() === 'span') { + target = e.target.parentElement; + } + activate(block, target); + }); + + number.addEventListener('keyup', (e) => { + if (e.which === 13) { + e.preventDefault(); + e.target.click(); + } + }); + + numbers.append(number); + + if (i === 0) { + row.classList.add('active'); + number.classList.add('active'); + } + }); +} + +function colorizeSVG(block, payload) { + block.querySelectorAll(':scope > div') + ?.forEach((div) => { + div.style.backgroundColor = payload.primaryHex; + div.style.color = payload.secondaryHex; + }); + + block.querySelectorAll('svg') + ?.forEach((svg) => { + svg.style.fill = payload.secondaryHex; + }); + + if (!(block.classList.contains('dark') || block.classList.contains('light'))) { + if (isDarkOverlayReadable(payload.primaryHex)) { + block.classList.add('light'); + block.classList.add('shadow'); + } else { + block.classList.add('dark'); + } + } +} + +function getColorSVG(svgName) { + const symbols = ['hero-marquee', 'hero-marquee-localized', 'hands-and-heart', 'color-how-to-graph']; + + if (symbols.includes(svgName)) { + return ` + ${svgName ? `${svgName}` : ''} + + `; + } + + return null; +} + +export default async function decorate(block) { + addTempWrapperDeprecated(block, 'color-how-to-carousel'); + + const payload = { + rotationInterval: null, + fixedImageSize: false, + howToDocument: block.ownerDocument, + howToWindow: block.ownerDocument.defaultView, + }; + + const rows = Array.from(block.children); + + const colorDataDiv = rows.shift(); + const contextRow = colorDataDiv.querySelector('div'); + const colorCarouselDiv = createTag('div', { class: 'content-wrapper' }); + + if (contextRow) { + const colorDataRows = contextRow.children; + + if (colorDataRows.length === 6) { + payload.icon = colorDataRows[0].querySelector('svg'); + [, payload.heading] = colorDataRows; + payload.colorName = colorDataRows[2].textContent.trim(); + [payload.primaryHex, payload.secondaryHex] = colorDataRows[3].textContent.split(','); + payload.colorGraphName = colorDataRows[4].textContent.trim(); + payload.cta = colorDataRows[5].querySelector('a'); + payload.cta.classList.add('button', 'accent', 'same-fcta'); + const imgWrapper = createTag('div', { class: 'img-wrapper' }); + imgWrapper.innerHTML = getColorSVG(payload.colorGraphName); + + const colorTextOverlay = createTag('div', { class: 'color-graph-text-overlay' }); + const colorName = createTag('p', { class: 'color-name' }); + const colorHex = createTag('p', { class: 'color-hex' }); + colorName.textContent = payload.colorName; + colorHex.textContent = payload.primaryHex; + + colorTextOverlay.append(colorName, colorHex); + imgWrapper.prepend(colorTextOverlay); + block.prepend(imgWrapper); + colorDataDiv.remove(); + } + + if (colorDataRows.length === 4) { + [payload.heading] = colorDataRows; + payload.colorName = colorDataRows[1].textContent.trim(); + [payload.primaryHex, payload.secondaryHex] = colorDataRows[2].textContent.split(','); + payload.colorGraphName = colorDataRows[3].textContent.trim(); + const imgWrapper = createTag('div', { class: 'img-wrapper' }); + imgWrapper.innerHTML = getColorSVG(payload.colorGraphName); + + const colorTextOverlay = createTag('div', { class: 'color-graph-text-overlay' }); + const colorName = createTag('p', { class: 'color-name' }); + const colorHex = createTag('p', { class: 'color-hex' }); + colorName.textContent = payload.colorName; + colorHex.textContent = payload.primaryHex; + + colorTextOverlay.append(colorName, colorHex); + imgWrapper.prepend(colorTextOverlay); + block.prepend(imgWrapper); + colorDataDiv.remove(); + block.classList.add('top-align'); + } + + rows.forEach((step) => { + colorCarouselDiv.append(step); + }); + + block.append(colorCarouselDiv); + } + + buildColorHowToCarousel(block, payload); + colorizeSVG(block, payload); + activate(block, block.querySelector('.tip-number.tip-1')); + + const onIntersect = ([entry], observer) => { + if (!entry.isIntersecting) return; + + initRotation(payload); + + observer.unobserve(block); + }; + + const colorHowToObserver = new IntersectionObserver(onIntersect, { rootMargin: '1000px', threshold: 0 }); + colorHowToObserver.observe(block); +} diff --git a/express/blocks/hero-color/hero-color.css b/express/blocks/hero-color/hero-color.css new file mode 100644 index 00000000..3f073f35 --- /dev/null +++ b/express/blocks/hero-color/hero-color.css @@ -0,0 +1,137 @@ +/* Future proofing wrapper deprecation. To be removed after milo migration */ +main .section .hero-color-wrapper { + max-width: unset; +} + +main .hero-color { + position: relative; + max-width: unset; +} + +main .hero-color .svg-container .text { + display: none; +} + +main .hero-color .svg-container { + min-height: 200px; + margin: auto; +} + +main .hero-color .description-container { + margin: 16px 0 32px 0; + display: flex; + flex-direction: column; + gap: 10px; +} + +main .hero-color .text-container { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 15px; + text-align: left; + color: var(--color-black); +} + +main .hero-color .text-container h1 { + margin: 0 0 16px; + font-size: var(--heading-font-size-l); + text-align: left; +} + +main .hero-color .text-container h2 { + margin: 0; + font-size: var(--heading-font-size-m); + text-align: left; +} + +main .hero-color .text-container p { + margin: 0 0 32px; + font-size: var(--body-font-size-l); +} + +main .hero-color .text-container .button-container { + margin: 0; + text-align: left; +} + +main .hero-color .text-container .button-container .button { + margin: 0; + padding: 12px 23px 14px 23px; + border-radius: 100px; + font-size: var(--body-font-size-m); +} + +main .hero-color .svg-container .color-svg-img { + height: 100%; + width: 200%; + margin: 0 0 0 15px; +} + +main .hero-color.light { + color: var(--color-black); +} + +main .hero-color.dark { + color: var(--color-white); +} + +main .hero-color .hidden-svg { + display: none; +} + +@media screen and (min-width: 600px) { + main .hero-color .text-container { + padding: 32px; + } +} + +@media screen and (min-width: 900px) { + main .hero-color .svg-container { + min-height: 336px; + padding-left: 50%; + } + + main .hero-color .svg-container .color-svg { + position: relative; + } + + main .hero-color .svg-container .color-svg-img { + margin: 0; + width: 1424px; + height: 336px; + overflow: hidden; + position: absolute; + left: 0; + } + + main .hero-color .text-container { + position: absolute; + max-width: 1200px; + width: 100%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: unset; + box-sizing: border-box; + margin: 0; + } + + main .hero-color .text-container > div { + max-width: calc(50% - 24px); + } + + main .hero-color.dark .button, + main .hero-color.dark .button:hover { + background-color: var(--color-white); + color: var(--color-black); + border-color: var(--color-white); + } + + main .hero-color.light .button, + main .hero-color.light .button:hover { + background-color: var(--color-black); + color: var(--color-white); + border-color: var(--color-black); + } +} diff --git a/express/blocks/hero-color/hero-color.js b/express/blocks/hero-color/hero-color.js new file mode 100644 index 00000000..df61c76d --- /dev/null +++ b/express/blocks/hero-color/hero-color.js @@ -0,0 +1,137 @@ +import { getLibs } from '../../scripts/utils.js'; +import { addTempWrapperDeprecated } from '../../scripts/utils/decorate.js'; +import isDarkOverlayReadable from '../../scripts/color-tools.js'; +import BlockMediator from '../../scripts/block-mediator.min.js'; + +const { createTag } = await import(`${getLibs()}/utils/utils.js`); + +function changeTextColorAccordingToBg( + primaryColor, + block, +) { + block.classList.add(isDarkOverlayReadable(primaryColor) ? 'light' : 'dark'); +} + +function loadSvgInsideWrapper(svgId, svgWrapper, secondaryColor) { + const svgNS = 'http://www.w3.org/2000/svg'; + const xlinkNS = 'http://www.w3.org/1999/xlink'; + + // create svg element + const svg = document.createElementNS(svgNS, 'svg'); + svg.setAttribute('class', 'color-svg-img hidden-svg'); + + // create use element + const useSvg = document.createElementNS(svgNS, 'use'); + useSvg.setAttributeNS(xlinkNS, 'xlink:href', `/express/icons/color-sprite.svg#${svgId}`); + + // append use element to svg element + svg.appendChild(useSvg); + + // append new svg and remove old one + svgWrapper.replaceChildren(); + svgWrapper.appendChild(svg); + svgWrapper.firstElementChild.style.fill = secondaryColor; +} + +function displaySvgWithObject(block, secondaryColor) { + const svg = block.firstElementChild; + const svgId = svg.firstElementChild.textContent; + const svgWrapper = createTag('div', { class: 'color-svg' }); + + svg.remove(); + loadSvgInsideWrapper(svgId, svgWrapper, secondaryColor); + const svgContainer = block.querySelector('.svg-container'); + svgContainer.append(svgWrapper); +} + +function decorateText(block) { + const text = block.firstElementChild; + text.classList.add('text-container'); + block.append(text); +} + +function extractColorElements(colors) { + const primaryColor = colors.children[0].textContent.split(',')[0].trim(); + const secondaryColor = colors.children[0].textContent.split(',')[1].trim(); + colors.remove(); + + return { primaryColor, secondaryColor }; +} + +function decorateColors(block) { + const colors = block.firstElementChild; + const svgContainer = block.querySelector('.svg-container'); + const { primaryColor, secondaryColor } = extractColorElements(colors); + + if (svgContainer) svgContainer.style.backgroundColor = primaryColor; + + changeTextColorAccordingToBg(primaryColor, block); + + return { secondaryColor }; +} + +function getContentContainerHeight() { + const contentContainer = document.querySelector('.svg-container'); + + return contentContainer?.clientHeight; +} + +function resizeSvgOnLoad() { + const interval = setInterval(() => { + if (document.readyState === 'complete') { + const height = getContentContainerHeight(); + if (height) { + const svg = document.querySelector('.color-svg-img'); + svg.classList.remove('hidden-svg'); + svg.style.height = `${height}px`; + clearInterval(interval); + } + } + }, 50); +} + +export function resizeSvg(event) { + const height = getContentContainerHeight(); + const svg = document.querySelector('.color-svg-img'); + if (event.matches) { + svg.style.height = `${height}px`; + } else { + svg.style.height = '200px'; + } +} + +function resizeSvgOnMediaQueryChange() { + const mediaQuery = window.matchMedia('(min-width: 900px)'); + mediaQuery.addEventListener('change', resizeSvg); +} + +function decorateCTA(block) { + const primaryCta = block.querySelector('.text-container a:last-child'); + primaryCta?.classList.add('button', 'accent', 'primaryCta', 'same-fcta'); + primaryCta?.parentElement?.classList.add('button-container'); + if (!primaryCta) return; + + primaryCta.classList.add('primaryCta'); + BlockMediator.set('primaryCtaUrl', primaryCta.href); +} + +export default function decorate(block) { + addTempWrapperDeprecated(block, 'hero-color'); + + const svgContainer = createTag('div', { class: 'svg-container' }); + block.append(svgContainer); + + // text + decorateText(block); + + // CTA + decorateCTA(block); + + // colors + const { secondaryColor } = decorateColors(block); + + // svg + displaySvgWithObject(block, secondaryColor); + resizeSvgOnLoad(); + resizeSvgOnMediaQueryChange(); +} diff --git a/express/blocks/long-text/long-text.css b/express/blocks/long-text/long-text.css index e682e220..92e49327 100644 --- a/express/blocks/long-text/long-text.css +++ b/express/blocks/long-text/long-text.css @@ -6,6 +6,7 @@ main .section div.long-text-wrapper { main .section div.long-text-wrapper.plain { max-width: 1200px; + margin: auto; } main .long-text { diff --git a/express/blocks/template-x/template-x.css b/express/blocks/template-x/template-x.css index 5a00e50f..9848366f 100755 --- a/express/blocks/template-x/template-x.css +++ b/express/blocks/template-x/template-x.css @@ -204,6 +204,10 @@ main .template-x.sixcols .masonry-col, main .template-x.fullwidth .masonry-col { font-size: var(--heading-font-size-m); } +#templates-heading { + margin-top: 80px; +} + .template-x .template-title.horizontal.holiday .template-title, .template-x .template-title.horizontal.holiday picture, .template-x .template-title.horizontal.holiday a, diff --git a/test/blocks/ckg-link-list/ckg-link-list.test.js b/test/blocks/ckg-link-list/ckg-link-list.test.js new file mode 100644 index 00000000..dfabf4c7 --- /dev/null +++ b/test/blocks/ckg-link-list/ckg-link-list.test.js @@ -0,0 +1,350 @@ +import { expect } from '@esm-bundle/chai'; +import { readFile } from '@web/test-runner-commands'; +import sinon from 'sinon'; +import { setConfig } from '../../../../express/scripts/utils.js'; + +setConfig({}); +const { default: decorate } = await import('../../../../express/blocks/ckg-link-list/ckg-link-list.js'); +const html = await readFile({ path: './mocks/default.html' }); + +function jsonOk(body) { + const mockResponse = new window.Response(JSON.stringify(body), { + status: 200, + headers: { + 'Content-type': 'application/json', + }, + }); + + return Promise.resolve(mockResponse); +} + +const MOCK_JSON = { + experienceId: 'templates-browse-v1', + status: { + httpCode: 200, + }, + queryResults: [ + { + id: 'ccx-search-1', + status: { + httpCode: 200, + }, + metadata: { + totalHits: 0, + start: 0, + limit: 0, + }, + context: { + application: { + 'metadata.color.hexCodes': { + 'ckg:COLOR:26511:cobalt': '#0047ab', + 'ckg:COLOR:18559:neon_blue': '#1b03a3', + 'ckg:COLOR:3496:cornflower_blue': '#6495ed', + 'ckg:COLOR:18546:glaucous': '#6082b6', + 'ckg:COLOR:26510:blue_green': '#0d98ba', + 'ckg:COLOR:3615:steel_blue': '#4682b4', + 'ckg:COLOR:3500:dark_blue': '#00008b', + 'ckg:COLOR:18534:teal_blue': '#367588', + 'ckg:COLOR:3499:cyan': '#00ffff', + 'ckg:COLOR:18536:ultramarine_blue': '#4166f5', + 'ckg:COLOR:18545:columbia_blue': '#9bddff', + 'ckg:COLOR:3610:slate_blue': '#6a5acd', + 'ckg:COLOR:18538:robin_egg_blue': '#00cccc', + 'ckg:COLOR:18554:phthalo_blue': '#000f89', + 'ckg:COLOR:18540:cerulean': '#007ba7', + 'ckg:COLOR:18547:oxford_blue': '#002147', + 'ckg:COLOR:3539:indigo': '#4b0082', + 'ckg:COLOR:26509:blue_gray': '#6699cc', + 'ckg:COLOR:18537:french_blue': '#0072bb', + 'ckg:COLOR:3567:medium_blue': '#0000cd', + 'ckg:COLOR:3479:alice_blue': '#f0f8ff', + 'ckg:COLOR:3620:turquoise': '#30d5c8', + 'ckg:COLOR:18535:blue_bell': '#a2a2d0', + 'ckg:COLOR:18548:yinmn_blue': '#2e5090', + 'ckg:COLOR:3609:sky_blue': '#87ceeb', + 'ckg:COLOR:3546:light_blue': '#add8e6', + 'ckg:COLOR:18543:cerulean_blue': '#2a52be', + 'ckg:COLOR:3483:azure': '#f0ffff', + 'ckg:COLOR:26515:ultramarine': '#120a8f', + 'ckg:COLOR:26513:electric_blue': '#7df9ff', + 'ckg:COLOR:18539:blue_sapphire': '#126180', + 'ckg:COLOR:26514:spanish_blue': '#0070b8', + 'ckg:COLOR:26508:azul': '#1d5dec', + 'ckg:COLOR:26512:cornflower': '#5170d7', + 'ckg:COLOR:3492:cadet_blue': '#5f9ea0', + 'ckg:COLOR:18541:carolina_blue': '#99badd', + 'ckg:COLOR:18542:tiffany_blue': '#0abab5', + 'ckg:COLOR:18544:blue_jeans': '#5dadec', + 'ckg:COLOR:26516:charcoal': '#36454f', + }, + }, + }, + facets: [ + { + buckets: [ + { + canonicalName: 'ckg:COLOR:3546:light_blue', + count: 0, + value: '/express/colors/light-blue', + displayValue: 'Light Blue', + }, + { + canonicalName: 'ckg:COLOR:3609:sky_blue', + count: 0, + value: '/express/colors/sky-blue', + displayValue: 'Sky Blue', + }, + { + canonicalName: 'ckg:COLOR:3567:medium_blue', + count: 0, + value: '/express/colors/search?q=Medium%20Blue', + displayValue: 'Medium Blue', + }, + { + canonicalName: 'ckg:COLOR:3500:dark_blue', + count: 0, + value: '/express/colors/dark-blue', + displayValue: 'Dark Blue', + }, + { + canonicalName: 'ckg:COLOR:3610:slate_blue', + count: 0, + value: '/express/colors/search?q=Slate%20Blue', + displayValue: 'Slate Blue', + }, + { + canonicalName: 'ckg:COLOR:3615:steel_blue', + count: 0, + value: '/express/colors/steel-blue', + displayValue: 'Steel Blue', + }, + { + canonicalName: 'ckg:COLOR:3620:turquoise', + count: 0, + value: '/express/colors/turquoise', + displayValue: 'Turquoise', + }, + { + canonicalName: 'ckg:COLOR:3499:cyan', + count: 0, + value: '/express/colors/cyan', + displayValue: 'Cyan', + }, + { + canonicalName: 'ckg:COLOR:3483:azure', + count: 0, + value: '/express/colors/azure', + displayValue: 'Azure', + }, + { + canonicalName: 'ckg:COLOR:3496:cornflower_blue', + count: 0, + value: '/express/colors/search?q=Cornflower%20Blue', + displayValue: 'Cornflower Blue', + }, + { + canonicalName: 'ckg:COLOR:3492:cadet_blue', + count: 0, + value: '/express/colors/search?q=Cadet%20Blue', + displayValue: 'Cadet Blue', + }, + { + canonicalName: 'ckg:COLOR:3479:alice_blue', + count: 0, + value: '/express/colors/search?q=Alice%20Blue', + displayValue: 'Alice Blue', + }, + { + canonicalName: 'ckg:COLOR:18534:teal_blue', + count: 0, + value: '/express/colors/search?q=Teal%20Blue', + displayValue: 'Teal Blue', + }, + { + canonicalName: 'ckg:COLOR:18535:blue_bell', + count: 0, + value: '/express/colors/search?q=Blue%20Bell', + displayValue: 'Blue Bell', + }, + { + canonicalName: 'ckg:COLOR:18536:ultramarine_blue', + count: 0, + value: '/express/colors/search?q=Ultramarine%20Blue', + displayValue: 'Ultramarine Blue', + }, + { + canonicalName: 'ckg:COLOR:18537:french_blue', + count: 0, + value: '/express/colors/search?q=French%20Blue', + displayValue: 'French Blue', + }, + { + canonicalName: 'ckg:COLOR:18538:robin_egg_blue', + count: 0, + value: '/express/colors/search?q=Robin%20Egg%20Blue', + displayValue: 'Robin Egg Blue', + }, + { + canonicalName: 'ckg:COLOR:18539:blue_sapphire', + count: 0, + value: '/express/colors/search?q=Blue%20Sapphire', + displayValue: 'Blue Sapphire', + }, + { + canonicalName: 'ckg:COLOR:18540:cerulean', + count: 0, + value: '/express/colors/cerulean', + displayValue: 'Cerulean', + }, + { + canonicalName: 'ckg:COLOR:18541:carolina_blue', + count: 0, + value: '/express/colors/search?q=Carolina%20Blue', + displayValue: 'Carolina Blue', + }, + { + canonicalName: 'ckg:COLOR:18542:tiffany_blue', + count: 0, + value: '/express/colors/tiffany-blue', + displayValue: 'Tiffany Blue', + }, + { + canonicalName: 'ckg:COLOR:18543:cerulean_blue', + count: 0, + value: '/express/colors/search?q=Cerulean%20Blue', + displayValue: 'Cerulean Blue', + }, + { + canonicalName: 'ckg:COLOR:18544:blue_jeans', + count: 0, + value: '/express/colors/search?q=Blue%20Jeans', + displayValue: 'Blue Jeans', + }, + { + canonicalName: 'ckg:COLOR:18545:columbia_blue', + count: 0, + value: '/express/colors/search?q=Columbia%20Blue', + displayValue: 'Columbia Blue', + }, + { + canonicalName: 'ckg:COLOR:18546:glaucous', + count: 0, + value: '/express/colors/glaucous', + displayValue: 'Glaucous', + }, + { + canonicalName: 'ckg:COLOR:18547:oxford_blue', + count: 0, + value: '/express/colors/search?q=Oxford%20Blue', + displayValue: 'Oxford Blue', + }, + { + canonicalName: 'ckg:COLOR:18548:yinmn_blue', + count: 0, + value: '/express/colors/search?q=Yinmn%20Blue', + displayValue: 'Yinmn Blue', + }, + { + canonicalName: 'ckg:COLOR:26508:azul', + count: 0, + value: '/express/colors/azul', + displayValue: 'Azul', + }, + { + canonicalName: 'ckg:COLOR:26509:blue_gray', + count: 0, + value: '/express/colors/blue-gray', + displayValue: 'Blue Gray', + }, + { + canonicalName: 'ckg:COLOR:26510:blue_green', + count: 0, + value: '/express/colors/blue-green', + displayValue: 'Blue Green', + }, + { + canonicalName: 'ckg:COLOR:26511:cobalt', + count: 0, + value: '/express/colors/cobalt', + displayValue: 'Cobalt', + }, + { + canonicalName: 'ckg:COLOR:26512:cornflower', + count: 0, + value: '/express/colors/cornflower', + displayValue: 'Cornflower', + }, + { + canonicalName: 'ckg:COLOR:26513:electric_blue', + count: 0, + value: '/express/colors/electric-blue', + displayValue: 'Electric Blue', + }, + { + canonicalName: 'ckg:COLOR:26514:spanish_blue', + count: 0, + value: '/express/colors/search?q=Spanish%20Blue', + displayValue: 'Spanish Blue', + }, + { + canonicalName: 'ckg:COLOR:26515:ultramarine', + count: 0, + value: '/express/colors/search?q=Ultramarine', + displayValue: 'Ultramarine', + }, + { + canonicalName: 'ckg:COLOR:18559:neon_blue', + count: 0, + value: '/express/colors/search?q=Neon%20Blue', + displayValue: 'Neon Blue', + }, + { + canonicalName: 'ckg:COLOR:18554:phthalo_blue', + count: 0, + value: '/express/colors/search?q=Phthalo%20Blue', + displayValue: 'Phthalo Blue', + }, + { + canonicalName: 'ckg:COLOR:3539:indigo', + count: 0, + value: '/express/colors/indigo', + displayValue: 'Indigo', + }, + { + canonicalName: 'ckg:COLOR:26516:charcoal', + count: 0, + value: '/express/colors/charcoal', + displayValue: 'Charcoal', + }, + ], + facet: 'categories', + }, + ], + }, + ], +}; + +describe('CKG Link List', () => { + beforeEach(() => { + window.isTestEnv = true; + document.body.innerHTML = html; + const stub = sinon.stub(window, 'fetch'); + stub.onCall(0).returns(jsonOk(MOCK_JSON)); + }); + + afterEach(() => { + window.fetch.restore(); + }); + + it('Block behaves accordingly to fetch results', async () => { + const block = document.querySelector('.ckg-link-list'); + await decorate(block); + const links = block.querySelectorAll('a'); + + if (links.length) { + expect(block.style.visibility).to.be.equal('visible'); + } else { + expect(block.style.visibility).to.be.equal('hidden'); + } + }); +}); diff --git a/test/blocks/ckg-link-list/mocks/default.html b/test/blocks/ckg-link-list/mocks/default.html new file mode 100644 index 00000000..991dda10 --- /dev/null +++ b/test/blocks/ckg-link-list/mocks/default.html @@ -0,0 +1,7 @@ +
+ +
diff --git a/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js b/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js new file mode 100644 index 00000000..2da75a0b --- /dev/null +++ b/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js @@ -0,0 +1,59 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; + +const { default: decorate } = await import('../../../../express/blocks/color-how-to-carousel/color-how-to-carousel.js'); +const redBody = await readFile({ path: './mocks/body.html' }); +const blackBody = await readFile({ path: './mocks/body-dark.html' }); + +describe('Color How To Carousel', () => { + describe('with 6 lines in the first row', () => { + beforeEach(() => { + window.isTestEnv = true; + document.body.innerHTML = redBody; + }); + + it('block exists', async () => { + const block = document.querySelector('.color-how-to-carousel'); + await decorate(block); + expect(block).to.exist; + }); + + it('schema variant builds schema', async () => { + const block = document.querySelector('.color-how-to-carousel'); + block.classList.add('schema'); + await decorate(block); + const schema = document.querySelector('head script[type="application/ld+json"]'); + expect(schema).to.exist; + }); + }); + + describe('with only 4 lines in the first row + is dark', () => { + beforeEach(() => { + window.isTestEnv = true; + document.body.innerHTML = blackBody; + }); + + it('block has a dark class', async () => { + const block = document.querySelector('.color-how-to-carousel'); + await decorate(block); + expect(block.classList.contains('dark')).to.be.true; + }); + + it('schema variant builds schema', async () => { + const block = document.querySelector('.color-how-to-carousel'); + block.classList.add('schema'); + await decorate(block); + const schema = document.querySelector('head script[type="application/ld+json"]'); + expect(schema).to.exist; + }); + + it('the missing 2 rows are icon and CTA', async () => { + const block = document.querySelector('.color-how-to-carousel'); + await decorate(block); + const icon = block.querySelector('.icon-color-how-to-icon'); + const cta = block.querySelector('.contnt-wrapper a.button.accent'); + expect(icon).to.not.exist; + expect(cta).to.not.exist; + }); + }); +}); diff --git a/test/blocks/color-how-to-carousel/mocks/body-dark.html b/test/blocks/color-how-to-carousel/mocks/body-dark.html new file mode 100644 index 00000000..fd4f776a --- /dev/null +++ b/test/blocks/color-how-to-carousel/mocks/body-dark.html @@ -0,0 +1,30 @@ + diff --git a/test/blocks/color-how-to-carousel/mocks/body.html b/test/blocks/color-how-to-carousel/mocks/body.html new file mode 100644 index 00000000..16d1cf81 --- /dev/null +++ b/test/blocks/color-how-to-carousel/mocks/body.html @@ -0,0 +1,39 @@ + diff --git a/test/blocks/hero-color/hero-color.test.js b/test/blocks/hero-color/hero-color.test.js new file mode 100644 index 00000000..124fd83c --- /dev/null +++ b/test/blocks/hero-color/hero-color.test.js @@ -0,0 +1,57 @@ +/* eslint-env mocha */ +/* eslint-disable no-unused-vars */ + +import sinon from 'sinon'; +import { readFile, setViewport } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; + +const { default: decorate, resizeSvg } = await import( + '../../../express/blocks/hero-color/hero-color.js' +); +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); +const clock = sinon.useFakeTimers({ shouldAdvanceTime: true }); + +describe('Hero Color', () => { + before(() => { + window.isTestEnv = true; + const heroColor = document.querySelector('.hero-color'); + decorate(heroColor); + }); + + after(() => { + clock.restore(); + }); + + it('Should have the correct elements', () => { + expect(document.querySelector('.hero-color')).to.exist; + expect(document.querySelector('.color-svg')).to.exist; + expect(document.querySelector('h1')).to.exist; + expect(document.querySelector('p')).to.exist; + expect(document.querySelector('.button')).to.exist; + }); + + it('Should have a primary and secondary color', () => { + const svgContainer = document.querySelector('.svg-container'); + const svgImg = document.querySelector('.color-svg-img'); + const primaryColor = svgContainer.style.backgroundColor; + const secondaryColor = svgImg.style.fill; + + expect(primaryColor).to.exist; + expect(secondaryColor).to.exist; + }); + + it('Should resize svg on load', async () => { + await clock.nextAsync(); + const svg = document.querySelector('.color-svg-img'); + expect(Array.from(svg.classList)).to.not.contain('hidden-svg'); + expect(svg.style.height).to.equal('154px'); + }); + + it('Svg height should be changed after screen is resized', () => { + const svg = document.querySelector('.color-svg-img'); + resizeSvg({ matches: true }); + expect(svg.style.height).to.equal('158px'); + resizeSvg({ matches: false }); + expect(svg.style.height).to.equal('200px'); + }); +}); diff --git a/test/blocks/hero-color/mocks/body.html b/test/blocks/hero-color/mocks/body.html new file mode 100644 index 00000000..9a6345a4 --- /dev/null +++ b/test/blocks/hero-color/mocks/body.html @@ -0,0 +1,18 @@ +
+
+
+

Inspiration in the color blue.

+

Flow into inspiration with the meaning, history, and symbolism of the color blue.

+

Start Designing

+
+
+
+
+

#0000FF,#FFFFFF

+
+
+
+
color-marquee-desktop
+
+
From 16f7b3d9706aea17ed043abf5f1e272d01946b3e Mon Sep 17 00:00:00 2001 From: Justin Rings Date: Wed, 28 Aug 2024 10:16:02 -0700 Subject: [PATCH 05/17] fix sibling selector for content --- express/blocks/cards/cards.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/express/blocks/cards/cards.css b/express/blocks/cards/cards.css index a2870259..507373b2 100644 --- a/express/blocks/cards/cards.css +++ b/express/blocks/cards/cards.css @@ -7,6 +7,12 @@ main .section:has(.card)>div>h2:first-of-type { margin-top: 0; } +main .section:has(.card) .content { + margin: auto; + max-width: 375px; + padding: 0; +} + main .section .cards-container>div, main .section .cards-dark-container>div { max-width: 870px; @@ -17,6 +23,7 @@ main .section .cards { display: flex; flex-direction: column; margin: 56px auto; + max-width: 375px; } main .section .cards .card { @@ -101,6 +108,10 @@ main .section .cards.large .card .card-content li { } @media (min-width:900px) { + main .section:has(.card) .content { + max-width: 830px; + } + main .section .cards { max-width: 1024px; width: unset; From d222c54de1e3080f9089adcffd15b94c4eb5b77f Mon Sep 17 00:00:00 2001 From: Victor Hargrave Date: Tue, 3 Sep 2024 16:26:28 +0200 Subject: [PATCH 06/17] initial v7 repo sync --- express/blocks/ax-columns/ax-columns.js | 51 +- express/blocks/banner/banner.css | 3 +- .../browse-by-category/browse-by-category.css | 1 + .../frictionless-quick-action.css | 4 +- express/blocks/how-to-cards/how-to-cards.css | 97 +++ express/blocks/how-to-cards/how-to-cards.js | 171 +++++ .../interactive-marquee.css | 679 ++++++++++++++++++ .../interactive-marquee.js | 173 +++++ express/blocks/logo-row/logo-row.css | 60 ++ express/blocks/logo-row/logo-row.js | 18 + .../pricing-cards-credits.css | 179 +++++ .../pricing-cards-credits.js | 102 +++ express/blocks/quotes/quotes.css | 14 + express/blocks/quotes/quotes.js | 10 +- express/blocks/susi-light/susi-light.js | 143 ++-- .../blocks/template-x/sample-template.json | 165 +++++ .../template-x/sample-webpage-template.json | 146 ++++ .../blocks/template-x/template-rendering.js | 43 +- express/blocks/template-x/template-x.css | 10 +- express/blocks/template-x/template-x.js | 4 +- .../direct-path-to-product.css | 35 +- .../direct-path-to-product.js | 154 ++-- express/icons/double-sparkles.svg | 4 + express/icons/enticement-arrow.svg | 5 + express/icons/external-link.svg | 7 + express/scripts/express-delayed.js | 4 +- express/scripts/scripts.js | 5 +- express/scripts/utils.js | 2 +- express/scripts/utils/embed-videos.js | 1 + express/scripts/utils/pricing.js | 21 +- express/scripts/widgets/floating-cta.js | 12 +- express/scripts/widgets/video.js | 32 +- express/sitemap-index.xml | 6 + express/styles/styles.css | 10 +- express/template-x/template-search-api-v3.js | 3 +- helix-query.yaml | 10 + helix-sitemap.yaml | 12 +- test/blocks/how-to-cards/gallery.test.js | 95 +++ test/blocks/how-to-cards/how-to-cards.test.js | 94 +++ test/blocks/how-to-cards/mocks/body.html | 87 +++ .../how-to-cards/mocks/gallery-body.html | 70 ++ .../interactive-marquee.test.js | 28 + .../interactive-marquee/mocks/body.html | 39 + test/blocks/logo-row/body.html | 39 + test/blocks/logo-row/logo-row.test.js | 33 + .../pricing-cards-credits/mocks/body.html | 36 + .../pricing-cards-credits.js | 54 ++ test/helpers/waitfor.js | 130 ++++ 48 files changed, 2892 insertions(+), 209 deletions(-) create mode 100644 express/blocks/how-to-cards/how-to-cards.css create mode 100644 express/blocks/how-to-cards/how-to-cards.js create mode 100644 express/blocks/interactive-marquee/interactive-marquee.css create mode 100644 express/blocks/interactive-marquee/interactive-marquee.js create mode 100644 express/blocks/logo-row/logo-row.css create mode 100644 express/blocks/logo-row/logo-row.js create mode 100644 express/blocks/pricing-cards-credits/pricing-cards-credits.css create mode 100644 express/blocks/pricing-cards-credits/pricing-cards-credits.js create mode 100644 express/blocks/template-x/sample-template.json create mode 100644 express/blocks/template-x/sample-webpage-template.json create mode 100644 express/icons/double-sparkles.svg create mode 100644 express/icons/enticement-arrow.svg create mode 100644 express/icons/external-link.svg create mode 100644 test/blocks/how-to-cards/gallery.test.js create mode 100644 test/blocks/how-to-cards/how-to-cards.test.js create mode 100644 test/blocks/how-to-cards/mocks/body.html create mode 100644 test/blocks/how-to-cards/mocks/gallery-body.html create mode 100644 test/blocks/interactive-marquee/interactive-marquee.test.js create mode 100644 test/blocks/interactive-marquee/mocks/body.html create mode 100644 test/blocks/logo-row/body.html create mode 100644 test/blocks/logo-row/logo-row.test.js create mode 100644 test/blocks/pricing-cards-credits/mocks/body.html create mode 100644 test/blocks/pricing-cards-credits/pricing-cards-credits.js create mode 100644 test/helpers/waitfor.js diff --git a/express/blocks/ax-columns/ax-columns.js b/express/blocks/ax-columns/ax-columns.js index 23aa6bba..e564f7fe 100644 --- a/express/blocks/ax-columns/ax-columns.js +++ b/express/blocks/ax-columns/ax-columns.js @@ -28,6 +28,14 @@ import { const { createTag, getMetadata } = await import(`${getLibs()}/utils/utils.js`); +function replaceHyphensInText(area) { + [...area.querySelectorAll('h1, h2, h3, h4, h5, h6')] + .filter((header) => header.textContent.includes('-')) + .forEach((header) => { + header.textContent = header.textContent.replace(/-/g, '\u2011'); + }); +} + function transformToVideoColumn(cell, aTag, block) { const parent = cell.parentElement; const title = aTag.textContent.trim(); @@ -153,7 +161,30 @@ const handleVideos = (cell, a, block, thumbnail) => { }); }; +const extractProperties = (block) => { + const allProperties = {}; + const rows = Array.from(block.querySelectorAll(':scope > div')).slice(0, 3); + + rows.forEach((row) => { + const content = row.innerText.trim(); + if (content.includes('linear-gradient')) { + allProperties['card-gradient'] = content; + row.remove(); + } else if (content.includes('text-color')) { + allProperties['card-text-color'] = content.replace(/text-color\(|\)/g, ''); + row.remove(); + } else if (content.includes('background-color')) { + allProperties['background-color'] = content.replace(/background-color\(|\)/g, ''); + row.remove(); + } + }); + + return allProperties; +}; + export default async function decorate(block) { + document.body.dataset.device === 'mobile' && replaceHyphensInText(block); + const colorProperties = extractProperties(block); splitAndAddVariantsWithDash(block); decorateSocialIcons(block); decorateButtonsDeprecated(block, 'button-xxl'); @@ -182,6 +213,15 @@ export default async function decorate(block) { const aTag = cell.querySelector('a'); const pics = cell.querySelectorAll(':scope picture'); + // apply custom gradient and text color to all columns cards + const parent = cell.parentElement; + if (colorProperties['card-gradient']) { + parent.style.background = colorProperties['card-gradient']; + } + if (colorProperties['card-text-color']) { + parent.style.color = colorProperties['card-text-color']; + } + if (cellNum === 0 && isNumberedList) { // add number to first cell let num = rowNum + 1; @@ -298,11 +338,14 @@ export default async function decorate(block) { ); } + // add custom background color to columns-highlight-container + const sectionContainer = block.closest('.section'); + if (sectionContainer && colorProperties['background-color']) { + sectionContainer.style.background = colorProperties['background-color']; + } + // invert buttons in regular columns inside columns-highlight-container - if ( - block.closest('.section.columns-highlight-container') - && !block.classList.contains('highlight') - ) { + if (sectionContainer && !block.classList.contains('highlight')) { block.querySelectorAll('a.button, a.con-button').forEach((button) => { button.classList.add('dark'); }); diff --git a/express/blocks/banner/banner.css b/express/blocks/banner/banner.css index 7649ba93..8ab651ec 100644 --- a/express/blocks/banner/banner.css +++ b/express/blocks/banner/banner.css @@ -110,7 +110,8 @@ main .banner h2 { border-radius: 12px; } -.banner .standout-container strong { +.banner .standout-container em { + font-style: normal; background: linear-gradient(180deg, #FF4DD2 20%, #FF993B 80%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; diff --git a/express/blocks/browse-by-category/browse-by-category.css b/express/blocks/browse-by-category/browse-by-category.css index 7df56017..39193ce5 100644 --- a/express/blocks/browse-by-category/browse-by-category.css +++ b/express/blocks/browse-by-category/browse-by-category.css @@ -15,6 +15,7 @@ main .browse-by-category .carousel-container .carousel-platform { main .browse-by-category.card .carousel-container .carousel-platform { gap: 14px; margin: 6px 0 0 0; + padding-left: 20px; } main .browse-by-category .carousel-container .button.carousel-arrow { diff --git a/express/blocks/frictionless-quick-action/frictionless-quick-action.css b/express/blocks/frictionless-quick-action/frictionless-quick-action.css index 7d43a973..582bfec5 100644 --- a/express/blocks/frictionless-quick-action/frictionless-quick-action.css +++ b/express/blocks/frictionless-quick-action/frictionless-quick-action.css @@ -60,8 +60,8 @@ padding: 24px 0; border-radius: 20px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); - width: 600px; - height: 271px; + width: 630px; + height: 284px; } .frictionless-quick-action .dropzone-bg { diff --git a/express/blocks/how-to-cards/how-to-cards.css b/express/blocks/how-to-cards/how-to-cards.css new file mode 100644 index 00000000..b07f85aa --- /dev/null +++ b/express/blocks/how-to-cards/how-to-cards.css @@ -0,0 +1,97 @@ +.how-to-cards.block { + max-width: 1440px; +} + +.how-to-cards h3 { + font-size: var(--heading-font-size-s); + text-align: left; + line-height: 26px; + margin-top: 0; +} + +.how-to-cards .text h2, +.how-to-cards .text p { + text-align: left; + padding: 0 16px; +} + +.how-to-cards .cards-container { + margin: 0; +} + +.how-to-cards .card { + flex: 0 0 auto; + box-sizing: border-box; + padding: 24px; + border-radius: 12px; + background-color: var(--color-gray-150); + list-style: none; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.how-to-cards .card p { + font-size: var(--body-font-size-s); + text-align: left; + margin: 4px 0; +} + +.how-to-cards .number { + position: relative; + font-weight: 700; + border-radius: 50%; + background-color: white; + width: 34px; + height: 34px; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; +} + +.how-to-cards .number-txt { + z-index: 1; +} + +.how-to-cards .number-bg { + position: absolute; + bottom: 0; + left: 0; + height: 0; + width: 100%; + transition: height .5s cubic-bezier(0.19, 1, 0.22, 1); + background-color: black; +} + +.how-to-cards .card:hover, +.how-to-cards .card:focus { + background-color: var(--color-gray-200); + transition: background-color .5s cubic-bezier(0.19, 1, 0.22, 1); +} + +.how-to-cards .card:not(:hover) { + transition: background-color .2s cubic-bezier(0.19, 1, 0.22, 1); +} + +.how-to-cards .card:hover .number-bg, +.how-to-cards .card:focus .number-bg { + height: 100%; + transition: height .5s cubic-bezier(0.19, 1, 0.22, 1); +} + +.how-to-cards .card:not(:hover) .number-bg { + transition: height .2s cubic-bezier(0.19, 1, 0.22, 1); +} + +.how-to-cards .card:hover .number-txt, +.how-to-cards .card:focus .number-txt { + color: white; + transition: color .1s linear; +} + +@media (min-width: 480px) { + .how-to-cards .card { + width: 308px; + } +} diff --git a/express/blocks/how-to-cards/how-to-cards.js b/express/blocks/how-to-cards/how-to-cards.js new file mode 100644 index 00000000..8c2f3f9c --- /dev/null +++ b/express/blocks/how-to-cards/how-to-cards.js @@ -0,0 +1,171 @@ +/* eslint-enable chai-friendly/no-unused-expressions */ +import { getLibs } from '../../scripts/utils.js'; +import { throttle, debounce } from '../../scripts/utils/hofs.js'; + +const { createTag, loadStyle } = await import(`${getLibs()}/utils/utils.js`); + +const nextSVGHTML = ` + + + + + +`; +const prevSVGHTML = ` + + + + +`; + +const scrollPadding = 16; + +let resStyle; +const styleLoaded = new Promise((res) => { + resStyle = res; +}); +loadStyle('/express/features/gallery/gallery.css', resStyle); + +function createControl(items, container) { + const control = createTag('div', { class: 'gallery-control loading' }); + const status = createTag('div', { class: 'status' }); + const prevButton = createTag('button', { + class: 'prev', + 'aria-label': 'Next', + }, prevSVGHTML); + const nextButton = createTag('button', { + class: 'next', + 'aria-label': 'Previous', + }, nextSVGHTML); + + const intersecting = Array.from(items).fill(false); + + const len = items.length; + const pageInc = throttle((inc) => { + const first = intersecting.indexOf(true); + if (first === -1) return; // middle of swapping only page + if (first + inc < 0 || first + inc >= len) return; // no looping + const target = items[(first + inc + len) % len]; + target.scrollIntoView({ behavior: 'smooth', inline: 'start', block: 'nearest' }); + }, 200); + prevButton.addEventListener('click', () => pageInc(-1)); + nextButton.addEventListener('click', () => pageInc(1)); + + const dots = items.map(() => { + const dot = createTag('div', { class: 'dot' }); + status.append(dot); + return dot; + }); + + const updateDOM = debounce((first, last) => { + prevButton.disabled = first === 0; + nextButton.disabled = last === items.length - 1; + dots.forEach((dot, i) => { + i === first ? dot.classList.add('curr') : dot.classList.remove('curr'); + i === first ? items[i].classList.add('curr') : items[i].classList.remove('curr'); + i > first && i <= last ? dot.classList.add('hide') : dot.classList.remove('hide'); + }); + if (items.length === last - first + 1) { + control.classList.add('hide'); + container.classList.add('gallery--all-displayed'); + } else { + control.classList.remove('hide'); + container.classList.remove('gallery--all-displayed'); + } + control.classList.remove('loading'); + }, 300); + + const reactToChange = (entries) => { + entries.forEach((entry) => { + intersecting[items.indexOf(entry.target)] = entry.isIntersecting; + }); + const [first, last] = [intersecting.indexOf(true), intersecting.lastIndexOf(true)]; + if (first === -1) return; // middle of swapping only page + updateDOM(first, last); + }; + + const scrollObserver = new IntersectionObserver((entries) => { + reactToChange(entries); + }, { root: container, threshold: 1, rootMargin: `0px ${scrollPadding}px 0px ${scrollPadding}px` }); + + items.forEach((item) => scrollObserver.observe(item)); + + control.append(status, prevButton, nextButton); + return control; +} + +export async function buildGallery( + items, + container = items?.[0]?.parentNode, + root = container?.parentNode, +) { + if (!root) throw new Error('Invalid Gallery input'); + const control = createControl([...items], container); + await styleLoaded; + container.classList.add('gallery'); + [...items].forEach((item) => { + item.classList.add('gallery--item'); + }); + root.append(control); +} + +export function addSchema(bl, heading) { + const schema = { + '@context': 'http://schema.org', + '@type': 'HowTo', + name: (heading && heading.textContent.trim()) || document.title, + step: [], + }; + + bl.querySelectorAll('li').forEach((step, i) => { + const h = step.querySelector('h3, h4, h5, h6'); + const p = step.querySelector('p'); + + if (h && p) { + schema.step.push({ + '@type': 'HowToStep', + position: i + 1, + name: h.textContent.trim(), + itemListElement: { + '@type': 'HowToDirection', + text: p.textContent.trim(), + }, + }); + } + }); + document.head.append(createTag('script', { type: 'application/ld+json' }, JSON.stringify(schema))); +} + +export default async function init(bl) { + const heading = bl.querySelector('h3, h4, h5, h6'); + const cardsContainer = createTag('ol', { class: 'cards-container' }); + let steps = [...bl.querySelectorAll(':scope > div')]; + if (steps[0].querySelector('h2')) { + const text = steps[0]; + steps = steps.slice(1); + text.classList.add('text'); + } + const cards = steps.map((div, index) => { + const li = createTag('li', { class: 'card' }); + const tipNumber = createTag('div', { class: 'number' }); + tipNumber.append( + createTag('span', { class: 'number-txt' }, index + 1), + createTag('div', { class: 'number-bg' }), + ); + li.append(tipNumber); + const content = div.querySelector('div'); + while (content.firstChild) { + li.append(content.firstChild); + } + div.remove(); + cardsContainer.append(li); + return li; + }); + bl.append(cardsContainer); + + await buildGallery(cards, cardsContainer, bl); + if (bl.classList.contains('schema')) { + addSchema(bl, heading); + } + return bl; +} diff --git a/express/blocks/interactive-marquee/interactive-marquee.css b/express/blocks/interactive-marquee/interactive-marquee.css new file mode 100644 index 00000000..44566784 --- /dev/null +++ b/express/blocks/interactive-marquee/interactive-marquee.css @@ -0,0 +1,679 @@ +.interactive-marquee { + position: relative; + color: var(--color-white); + display: flex; + flex-direction: column; +} + +.interactive-marquee.light { + color: var(--text-color); +} + +.interactive-marquee .foreground { + position: relative; + display: flex; + flex-direction: column; + gap: var(--spacing-m); + padding: var(--spacing-xxl) 0; +} + +.interactive-marquee .interactive-container { + height: 300px; + width: 300px; + margin: 0 auto; + border: 4px; +} + +.interactive-marquee .asset { + max-width: 300px; + position: relative; + padding: 0; +} + +.interactive-marquee .text { + display: flex; + flex-direction: column; + margin: 0 0 0 auto; + order: 2; +} + +[dir="rtl"] .interactive-marquee .text { + margin: 0 auto 0 0; +} + +.interactive-marquee .text p:last-of-type { + margin-bottom: 0; +} + +.interactive-marquee .text .detail-l, +.interactive-marquee .mweb-container .detail-l, +.interactive-marquee .text .heading-xl, +.interactive-marquee .text .heading-xxl { + margin-bottom: var(--spacing-xs); +} + +.interactive-marquee .icon-area { + display: flex; + margin-bottom: var(--spacing-s); + margin-top: 0; +} + +.interactive-marquee .icon-text { + margin: auto var(--spacing-xs); + font-weight: 700; + font-size: var(--type-heading-m-size); + line-height: var(--type-heading-m-lh); + font-style: normal; +} + +.interactive-marquee .icon-area picture, +.interactive-marquee .icon-area a { + display: contents; +} + +.interactive-marquee .icon-area img { + height: 40px; + width: auto; + min-width: 40px; + display: block; +} + +.interactive-marquee .pricing { + margin-top: var(--spacing-xs); +} + +.interactive-marquee .action-area { + display: flex; + margin: 0; + gap: var(--spacing-s); + flex-flow: column wrap; + align-items: stretch; + padding: var(--spacing-s) 0 0; +} + +.interactive-marquee .text .action-area { + margin-bottom: var(--spacing-s); +} + +.interactive-marquee .text .supplemental-text { + margin-bottom: var(--spacing-s); + font-weight: 700; +} + +.interactive-marquee .background img { + object-fit: cover; + height: 100%; + width: 100%; +} + +.interactive-marquee .background .tablet-only, +.interactive-marquee .background .desktop-only { + display: none; +} + +.interactive-marquee .background picture { + display: block; + position: absolute; + inset: 0; + line-height: 0; +} + +.interactive-marquee > .container > .body-xl { + display: none; + order:3; +} + +@media screen and (min-width: 600px) { + .interactive-marquee .background .mobile-only, + .interactive-marquee .background .desktop-only { + display: none; + } + + .interactive-marquee .background .tablet-only { + display: block; + } + + .interactive-marquee .interactive-container { + height: 604px; + width: 569px; + } + + .interactive-marquee .asset { + max-width: 569px; + top: 35px; + } + + .interactive-marquee .action-area { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--spacing-s); + } + + .interactive-marquee .mweb-container { + display: none; + } +} + +@media screen and (min-width: 1200px) { + .interactive-marquee .background .mobile-only, + .interactive-marquee .background .tablet-only { + display: none; + } + + .interactive-marquee .background .desktop-only { + display: block; + } + + .interactive-marquee { + min-height: 700px; + flex-direction: row; + } + + .interactive-marquee .foreground { + padding: 0; + gap: 100px; + flex-direction: row; + align-items: center; + order: unset; + width: var(--grid-container-width); + } + + .interactive-marquee .interactive-container { + position: absolute; + right: 0; + order: unset; + width: 50%; + height: 100%; + margin: 0; + } + + .interactive-marquee .asset { + top: 91px; + margin: 0 auto; + } + + .interactive-marquee .supplemental-text { + margin: var(--spacing-s) 0 0 0; + } + + .interactive-marquee .text { + order: unset; + display: block; + max-width: 500px; + margin: 0; + } +} + +@media screen and (max-width: 600px) { + .mweb-container .action-area { + text-align: center; + } + + .interactive-marquee .text .detail-l.mobile-cta-top, + .interactive-marquee .text .icon-area.mobile-cta-top, + .interactive-marquee .text .action-area.mobile-cta-top, + .interactive-marquee .text h1[class^="heading"].mobile-cta-top + { + display: none; + } +} + +/* General styles for the interactive marquee */ +main .interactive-marquee.horizontal-masonry { + --text-color: #2c2c2c; +} + +main .interactive-marquee.horizontal-masonry .container { + width: var(--grid-container-width); + margin: 0 auto; + text-align: initial; +} + +main .interactive-marquee.horizontal-masonry .interactive-container { + display: flex; + flex-direction: column; + position: unset; + height: unset; +} + +main .section:has(.interactive-marquee.horizontal-masonry)>div { + max-width: unset; +} + +main .section:has(.interactive-marquee.horizontal-masonry) p { + margin: initial; +} + +main .interactive-marquee.horizontal-masonry .asset { + top: unset; +} + +/* Button styles */ +main .interactive-marquee.horizontal-masonry .generate-btn { + background-color: #6495ED; + color: white; + border: none; + padding: 10px 20px; + border-radius: 20px; + font-size: 1em; + cursor: pointer; +} + +main .interactive-marquee.horizontal-masonry .generate-small-btn { + background-color: #333; + color: white; + border: none; + padding: 10px 20px; + border-radius: 20px; + cursor: pointer; + position: absolute; + top: 10px; + right: 10px; +} + +main .interactive-marquee.horizontal-masonry .generate-small-btn::before { + content: ''; + filter: brightness(0) invert(1); + background-image: url(/express/icons/adobe-firefly.svg); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 20px; + height: 20px; + display: inline-block; + margin-top: -10px; + position: relative; + top: 4px; + left: -4px; +} + +/* Input styles */ +main .interactive-marquee.horizontal-masonry input[type="text"] { + width: calc(100% - 60px); + height: 35px; + padding: 10px; + border-color: transparent; + border-radius: 16px; + margin-left: 40px; + box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2); +} + +main .interactive-marquee.horizontal-masonry input[type="text"]::placeholder { + font-style: italic; + content: "abfdsfdsfsdfds" !important; +} + +main .interactive-container.interactive-marquee.horizontal-masonry .enticement-container input::placeholder { + font-style: italic; +} + + +/* Enticement container styles */ +main .interactive-marquee.horizontal-masonry .enticement-container { + margin-top: 80px; + margin-bottom: 15px; + position: relative; +} + +main .interactive-marquee.horizontal-masonry .enticement-text { + position: absolute; + left: -30px; + top: -55px; + font-size: 36px; + color: black; + font-weight: bold; +} + +main .interactive-marquee.horizontal-masonry .icon-enticement-arrow { + position: absolute; + left: -45px; + top: 0px; + filter: brightness(1) invert(1); + width: 70px; + height: 70px; + transform: rotate(-35deg); +} + +/* Media container styles */ +main .interactive-marquee.horizontal-masonry .media-container { + display: flex; + width: 600px; + max-height: 400px; + flex-wrap: wrap; + flex-direction: row; + justify-content: space-between; +} + +main .interactive-marquee.horizontal-masonry .media-container p.image-container { + position: relative; + margin-bottom: 20px; + border-radius: 16px; + height: 180px; + flex: 0 0 calc(33% - 10px); +} + +main .interactive-marquee.horizontal-masonry.tall .media-container p.image-container { + flex: unset; + height: 200px; +} + +main .interactive-marquee.horizontal-masonry.wide .media-container p.image-container { + position: relative; + margin-bottom: 20px; + border-radius: 16px; + height: 150px; + max-width: 400px; + flex: unset +} + +main .interactive-marquee.horizontal-masonry .media-container p.image-container img { + border-radius: 16px; + object-fit: cover; + height: 100%; + object-position: center; + width: 100%; +} + +main .interactive-marquee.horizontal-masonry .media-container .link { + position: absolute; + bottom: 0px; + padding: 10px; + z-index: 4; +} + +main .interactive-marquee.horizontal-masonry .media-container p.image-container img.link { + display: none; +} + +main .interactive-marquee.horizontal-masonry .media-container .overlay { + position: absolute; + top: 0px; + width: 150px; + padding: 10px; + display: none; + color: white; + height: 100%; + overflow: hidden; +} + +main .interactive-marquee.horizontal-masonry .media-container .prompt-title { + display: none; +} + +/* Hover effects */ +main .interactive-marquee.horizontal-masonry .media-container p.image-container:hover::after { + content: ''; + position: absolute; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.3s; + z-index: 2; + height: 100%; + width: 100%; + border-radius: 16px; +} + +main .interactive-marquee.horizontal-masonry .media-container p.image-container:hover .overlay { + display: block; + z-index: 3; +} + +main .interactive-marquee.horizontal-masonry .media-container p.image-container:hover img.link { + width: 22px; + z-index: 5; + height: 22px; + bottom: 0px; + right: 0px; + filter: brightness(0) invert(1); + display: block; + pointer-events: all; + cursor: pointer; +} + +/* Typography styles */ +main .interactive-marquee.horizontal-masonry .foreground h1>em { + font-style: normal; + background: linear-gradient(320deg, #7C84F3, #FF4DD2, #FF993B, #FF4DD2, #7C84F3, #FF4DD2, #FF993B); + background-size: 400% 400%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +main .interactive-marquee.horizontal-masonry h1>em::after { + content: ''; + background-image: url(/express/icons/double-sparkles.svg); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 20px; + height: 20px; + display: inline-block; + margin-top: -10px; + position: relative; + top: -15px; +} + +/* Variant styles */ +main .interactive-marquee.horizontal-masonry.dark { + color: black; +} + +main .interactive-marquee.horizontal-masonry.tall .media-container { + max-width: 480px; + max-height: 420px; +} + +main .interactive-marquee.horizontal-masonry.quad .media-container { + max-width: 420px; + max-height: 420px; +} + +main .interactive-marquee.horizontal-masonry.quad .media-container p.image-container { + height: 200px; + flex: 0 0 calc(50% - 10px); +} + +main .interactive-marquee.horizontal-masonry.wide .media-container { + max-width: unset; +} + +main .interactive-marquee.horizontal-masonry.wide .media-container p.image-container { + flex: 0 0 calc(50% - 10px); +} + +main .interactive-marquee.horizontal-masonry.no-search .enticement-container { + display: none; +} + +main .interactive-marquee.horizontal-masonry.no-search .interactive-container { + margin-top: unset; +} + +main .interactive-marquee.horizontal-masonry input[type="text"]:focus { + outline: none; + background: + linear-gradient(white 0 0) padding-box, + linear-gradient(90deg, #ff477b 0%, #5c5ce0 52%, #318fff 100%) border-box; +} + +main .interactive-marquee.horizontal-masonry .media-container p.image-container .external-link-element { + top: 0; + left: 0; + position: absolute; + width: 100%; + height: 100%; + display: none; + color: white; +} + +main .interactive-marquee.horizontal-masonry .media-container p.image-container:hover .external-link-element{ + display: block; +} + +main .interactive-marquee.horizontal-masonry .media-container p.image-container:hover .external-link-element .mobile-prompt-link{ + color: transparent; + pointer-events: none; +} + +/* Responsive styles */ +@media (min-width: 1440px) { + main .interactive-marquee.horizontal-masonry .container { + --grid-container-width: 1200px; + } +} + +@media (max-width: 900px) { + main .interactive-marquee.horizontal-masonry .con-button { + display: none; + } + + main .interactive-marquee.horizontal-masonry .asset{ + max-width: unset; + } + + main .interactive-marquee.horizontal-masonry .interactive-container { + display: flex; + flex-direction: column-reverse; + height: unset; + margin-top: -60px; + width: 100%; + } + + main .interactive-marquee.horizontal-masonry .container { + flex-direction: column-reverse; + } + + + + + + main .interactive-marquee.horizontal-masonry .media-container p.image-container:hover .external-link-element .mobile-prompt-link { + color: white; + position: relative; + height: 22px; + margin: auto; + width: fit-content; + margin-top: auto; + display: block; + top: 45%; + z-index: 8; + } + + + main .interactive-marquee.horizontal-masonry .media-container p.image-container .external-link-element .icon{ + display: inline; + position: relative; + padding: 0; + float: left; + padding-right: 10px; + padding-top: 5px; + + } + + + main .interactive-marquee.horizontal-masonry .media-container p.overlay .prompt-title { + display: block; + } + + main .interactive-marquee.horizontal-masonry .media-container p.overlay { + display: flex; + flex-direction: column; + justify-content: center; + z-index: 2; + background-color: white; + border-radius: 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 15px 20px; + left: 0; + height: fit-content; + margin: auto; + color: #333; + width: calc(100% - 60px); + font-size: 16px; + left: 10px; + bottom: -100%; + } + + main .interactive-marquee.horizontal-masonry .media-container { + width: unset; + } + + main .interactive-marquee.horizontal-masonry .enticement-arrow { + display: none; + } + + main .interactive-marquee.horizontal-masonry .enticement-text { + display: none; + } + + main .interactive-marquee.horizontal-masonry p.image-container { + display: none; + } + + main .interactive-marquee.horizontal-masonry .media-container p.image-container:first-of-type { + display: block; + width: 100%; + margin: auto; + height: 100%; + } + + main .interactive-marquee.horizontal-masonry .prompt-title { + color: #888; + font-size: 14px; + margin-bottom: 5px; + } + + main .interactive-marquee.horizontal-masonry .container { + width: calc(100% - 20px) + } + + main .interactive-marquee.horizontal-masonry .enticement-container { + margin-top: 40px; + } + + main .interactive-marquee.horizontal-masonry .enticement-container img { + display: none; + } + + main .interactive-marquee.horizontal-masonry .media-container p.image-container img { + height: unset; + } + + main .interactive-marquee.horizontal-masonry input[type="text"] { + margin-left: unset; + width: calc(100% - 20px); + } + + main .interactive-marquee.horizontal-masonry .media-container p.image-container, + main .interactive-marquee.horizontal-masonry.wide .media-container p.image-container, + main .interactive-marquee.horizontal-masonry.quad .media-container p.image-container { + flex: unset; + } +} + +/* Additional styles */ +main .interactive-marquee.interactive-marquee.horizontal-masonry .media-container { + max-height: unset; +} + +main .interactive-marquee.horizontal-masonry .media-container p.image-container::after { + display: block; +} + + +main .interactive-marquee .foreground .text .icon-area > div { + display: flex; + width: fit-content; +} + +main .interactive-marquee .foreground .express-logo { + width: unset; + height: 30px; + padding-bottom: 8px; +} diff --git a/express/blocks/interactive-marquee/interactive-marquee.js b/express/blocks/interactive-marquee/interactive-marquee.js new file mode 100644 index 00000000..70f5d203 --- /dev/null +++ b/express/blocks/interactive-marquee/interactive-marquee.js @@ -0,0 +1,173 @@ +import { getLibs } from '../../scripts/utils.js'; +import { getIconElementDeprecated } from '../../scripts/utils/icons.js'; + +const [{ decorateButtons }, { createTag, getMetadata, getConfig }, { replaceKeyArray }] = await Promise.all([import(`${getLibs()}/utils/decorate.js`), + import(`${getLibs()}/utils/utils.js`), + import(`${getLibs()}/features/placeholders.js`)]); +const [describeImageMobile, describeImageDesktop, generate, useThisPrompt, promptTitle] = await +replaceKeyArray(['describe-image-mobile', 'describe-image-desktop', 'generate', 'use-this-prompt', 'prompt-title'], getConfig()); + +// [headingSize, bodySize, detailSize, titlesize] +const typeSizes = ['xxl', 'xl', 'l', 'xs']; + +const promptTokenRegex = new RegExp('(%7B%7B|{{)prompt-text(%7D%7D|}})'); + +export const windowHelper = { + redirect: (url) => { + window.location.assign(url); + }, +}; + +// List of placeholders required +// 'describe-image-mobile +// 'describe-image-desktop +// 'generate' +// 'use-this-prompt' +// 'prompt-title' + +function handleGenAISubmit(form, link) { + const input = form.querySelector('input'); + if (input.value.trim() === '') return; + const genAILink = link.replace(promptTokenRegex, encodeURI(input.value).replaceAll(' ', '+')); + const urlObj = new URL(genAILink); + urlObj.searchParams.delete('referrer'); + if (genAILink) windowHelper.redirect(urlObj.toString()); +} + +function createEnticement(enticementDetail, enticementLink, mode) { + const enticementDiv = createTag('div', { class: 'enticement-container' }); + const svgImage = getIconElementDeprecated('enticement-arrow', 60); + const arrowText = enticementDetail; + const enticementText = createTag('span', { class: 'enticement-text' }, arrowText.trim()); + const mobilePlacehoderText = describeImageMobile !== 'describe image mobile' ? describeImageMobile : 'Describe your image...'; + const desktopPlaceholderText = describeImageDesktop !== 'describe image desktop' ? describeImageDesktop : 'Describe the image you want to create...'; + const input = createTag('input', { type: 'text', placeholder: window.screen.width < 600 ? mobilePlacehoderText : desktopPlaceholderText }); + const buttonContainer = createTag('span', { class: 'button-container' }); + const button = createTag('button', { class: 'generate-small-btn' }); + buttonContainer.append(button); + button.textContent = generate; + button.addEventListener('click', () => handleGenAISubmit(enticementDiv, enticementLink)); + enticementDiv.append(enticementText, svgImage, input, buttonContainer); + if (mode === 'light') enticementText.classList.add('light'); + return enticementDiv; +} + +function createPromptLinkElement(promptLink, prompt) { + const icon = getIconElementDeprecated('external-link', 22); + icon.classList.add('link'); + icon.addEventListener('click', () => { + const urlObj = new URL(promptLink); + urlObj.searchParams.delete('referrer'); + urlObj.searchParams.append('prompt', prompt); + windowHelper.redirect(urlObj.toString()); + }); + const wrapper = createTag('div', { class: 'external-link-element' }); + const usePrompt = createTag('div', { class: 'mobile-prompt-link' }); + usePrompt.textContent = useThisPrompt; + wrapper.appendChild(usePrompt); + usePrompt.appendChild(icon); + return wrapper; +} + +const LOGO = 'adobe-express-logo'; +function injectExpressLogo(block, wrapper) { + if (block.classList.contains('entitled')) return; + if (!['on', 'yes'].includes(getMetadata('marquee-inject-logo')?.toLowerCase())) return; + const logo = getIconElementDeprecated(LOGO, '22px'); + logo.classList.add('express-logo'); + wrapper.prepend(logo); +} + +async function setHorizontalMasonry(el) { + const link = el.querySelector(':scope .con-button'); + if (!link) { + console.error('Missing Generate Link'); + return; + } + + const args = el.querySelectorAll('.interactive-container > .asset > p'); + const container = el.querySelector('.interactive-container .asset'); + container.classList.add('media-container'); + + const enticementElement = args[0].querySelector('a'); + const enticementMode = el.classList.contains('light') ? 'light' : 'dark'; + const enticementText = enticementElement.textContent.trim(); + const enticementLink = enticementElement.href; + args[0].remove(); + + el.querySelector('.interactive-container').appendChild(createEnticement(enticementText, enticementLink, enticementMode)); + for (let i = 1; i < args.length; i += 3) { + const divider = args[i]; + divider.remove(); + const prompt = args[i + 1]; + prompt.classList.add('overlay'); + + const image = args[i + 2]; + image.classList.add('image-container'); + image.appendChild(prompt); + image.appendChild(createPromptLinkElement(link.href, prompt.textContent)); + + const title = createTag('div', { class: 'prompt-title' }); + title.textContent = promptTitle !== 'prompt title' ? promptTitle : 'Prompt used'; + prompt.prepend(title); + } + + injectExpressLogo(el, el.querySelector('.foreground > .text')); +} + +function decorateText(el) { + const headings = el.querySelectorAll('h1, h2, h3, h4, h5, h6'); + const heading = headings[headings.length - 1]; + const config = typeSizes; + const decorate = (headingEl, typeSize) => { + headingEl.classList.add(`heading-${typeSize[0]}`); + const bodyEl = headingEl.nextElementSibling; + bodyEl?.classList.add(`body-${typeSize[1]}`); + bodyEl?.nextElementSibling?.classList.add(`body-${typeSize[1]}`); + }; + decorate(heading, config); +} + +function extendButtonsClass(text) { + const buttons = text.querySelectorAll('.con-button'); + if (buttons.length === 0) return; + buttons.forEach((button) => { + button.classList.add('button-justified-mobile'); + }); +} + +function interactiveInit(el) { + const isLight = el.classList.contains('light'); + if (!isLight) el.classList.add('dark'); + const children = el.querySelectorAll(':scope > div'); + const foreground = children[children.length - 1]; + foreground.classList.add('foreground', 'container'); + const headline = foreground.querySelector('h1, h2, h3, h4, h5, h6'); + const text = headline.closest('div'); + text.classList.add('text'); + const mediaElements = foreground.querySelectorAll(':scope > div:not([class])'); + const media = mediaElements[0]; + if (media) { + const interactiveBox = createTag('div', { class: 'interactive-container' }); + mediaElements.forEach((mediaDiv) => { + mediaDiv.classList.add('asset'); + interactiveBox.appendChild(mediaDiv); + }); + foreground.appendChild(interactiveBox); + } + + const firstDivInForeground = foreground.querySelector(':scope > div'); + if (firstDivInForeground?.classList.contains('asset')) el.classList.add('row-reversed'); + decorateButtons(text, 'button-xl'); + decorateText(text, createTag); + extendButtonsClass(text); +} + +export default async function init(el) { + if (!el.classList.contains('horizontal-masonry')) { + window.lana?.log('Using interactive-marquee on Express requires using the horizontal-masonry class.'); + return; + } + interactiveInit(el); + await setHorizontalMasonry(el); +} diff --git a/express/blocks/logo-row/logo-row.css b/express/blocks/logo-row/logo-row.css new file mode 100644 index 00000000..384dff90 --- /dev/null +++ b/express/blocks/logo-row/logo-row.css @@ -0,0 +1,60 @@ +main .logo-row .block-layout { + display: flex; + align-items: center; + padding: 20px; + font-family: Arial, sans-serif; +} + +main .logo-row .block-row { + display: flex; + width: 100%; +} + +main .logo-row .text-column { + flex: 1; + margin: auto; + padding-right: 40px; +} + +main .logo-row .text-column > * { + text-align: left; +} + +main .logo-row .text-content { + font-size: 24px; + font-weight: bold; + color: #333; + +} + +main .logo-row .image-column { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 10px; +} + +main .logo-row .brand-image { + width: 150px; + height: 100px; + object-fit: contain; +} + +@media (max-width: 900px) { + main .logo-row .block-row { + display: block; + } + + main .logo-row .image-column { + display: block; + } + + main .logo-row .text-column { + + padding-bottom: 20px; + padding-right: initial; + } + main .logo-row .text-column > * { + text-align: center; + } +} diff --git a/express/blocks/logo-row/logo-row.js b/express/blocks/logo-row/logo-row.js new file mode 100644 index 00000000..cc24ad8b --- /dev/null +++ b/express/blocks/logo-row/logo-row.js @@ -0,0 +1,18 @@ +export default function decorate(block) { + // Add class to the main block + block.classList.add('block-layout'); + + const row = block.children[0]; + row.classList.add('block-row'); + + const [textColumn, imageColumn] = row.children; + + // Style text column + textColumn.classList.add('text-column'); + + // Style image column + imageColumn.classList.add('image-column'); + + // Add classes to all images + imageColumn.querySelectorAll('img').forEach((img) => img.classList.add('brand-image')); +} diff --git a/express/blocks/pricing-cards-credits/pricing-cards-credits.css b/express/blocks/pricing-cards-credits/pricing-cards-credits.css new file mode 100644 index 00000000..81ee8b71 --- /dev/null +++ b/express/blocks/pricing-cards-credits/pricing-cards-credits.css @@ -0,0 +1,179 @@ +main .section>div:has(.pricing-cards-credits) { + max-width: unset; +} + +/* General styles */ +.pricing-cards-credits { + --card-width: 400px; + --card-padding: 30px; + --border-radius: 20px; + --primary-color: #5c5ce0; + --gradient: linear-gradient(90deg, #ff477b 0%, #5c5ce0 52%, #318fff 100%); + + display: flex; + flex-wrap: wrap; + gap: 16px; + width: min-content; + margin: auto; + text-align: left; +} + +/* Card styles */ +.pricing-cards-credits > .card { + width: var(--card-width); + padding: var(--card-padding); + margin: 48px 10px 0; + border: 2px solid #C6C6C6; + background-color: white; + border-radius: var(--border-radius); + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + gap: 10px; +} + +.pricing-cards-credits > .card.gradient-promo { + background: linear-gradient(white 0 0) padding-box, var(--gradient) border-box; + border: 4px solid transparent; + -webkit-mask-image: radial-gradient(circle, white 100%, black 100%); +} + +.pricing-cards-credits .none { + display: none; +} + +/* Promo eyebrow text */ +.pricing-cards-credits .promo-eyebrow-text { + position: absolute; + top: -36px; + text-align: center; + color: white; + width: 100%; + margin: auto; + left: 0; + right: 0; + pointer-events: none; +} + +/* Card header */ +.pricing-cards-credits .card-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.pricing-cards-credits .card-header h2 { + font-size: 1.75rem; + margin-top: 0; + display: flex; + flex-direction: row-reverse; + align-items: center; + gap: 8px; +} + +.pricing-cards-credits .card-header img { + width: 22px; + height: 22px; +} + +.pricing-cards-credits .card-header .head-cnt { + border-bottom: 1px solid #000; + padding-bottom: 2px; + font-size: var(--body-font-size-m); + font-weight: 400; +} + +.pricing-cards-credits .card-header .head-cnt > img { + width: 14px; + height: 14px; + padding-right: 6px; +} + +/* Pricing area */ +.pricing-cards-credits .pricing-area-wrapper { + padding-bottom: 40px; + border-bottom: 1px solid gray; +} + +.pricing-cards-credits .pricing-area-wrapper strong { + font-size: 30pt; + font-weight: 800; +} + +.pricing-cards-credits .pricing-area-wrapper a { + text-decoration: underline; +} + +.pricing-cards-credits .pricing-bar { + border-bottom: 10px solid lightgray; + border-radius: 5px; + margin-top: 30px; + width: var(--card-width); + position: relative; +} + +.pricing-cards-credits .pricing-bar::before { + content: ''; + position: absolute; + height: 10px; + background-color: var(--primary-color); + transition: width 0.5s ease-in-out; + width: var(--progress, 0%); + border-radius: 5px; +} + +/* Compare all link */ +.pricing-cards-credits .compare-all { + width: fit-content; + margin: 20px auto 0; +} + +.pricing-cards-credits .card .compare-all > a { + color: var(--color-info-accent); + border: none; + background: none; + padding: 0; + text-decoration: underline; + font-weight: 400; + font-size: 14pt; + text-align: center; +} + +/* Responsive styles */ +@media (max-width: 600px) { + .pricing-cards-credits { + --card-width: calc(100% - 40px); + } + + .pricing-cards-credits > .card { + margin: auto; + min-height: unset; + } + + .pricing-cards-credits .card-feature-list, + .pricing-cards-credits .billing-toggle.hidden { + display: none; + } + + .pricing-cards-credits .pricing-bar { + width: 300px; + } + + .pricing-cards-credits .compare-all { + margin-bottom: 20px; + } +} + +@media (max-width: 375px) { + .pricing-cards-credits .pricing-bar { + width: 250px; + } +} + +@media (min-width: 1400px) { + .pricing-cards-credits { + width: max-content; + padding: 10px; + } +} diff --git a/express/blocks/pricing-cards-credits/pricing-cards-credits.js b/express/blocks/pricing-cards-credits/pricing-cards-credits.js new file mode 100644 index 00000000..e35dd615 --- /dev/null +++ b/express/blocks/pricing-cards-credits/pricing-cards-credits.js @@ -0,0 +1,102 @@ +import { getLibs } from '../../scripts/utils.js'; +import { addTempWrapperDeprecated } from '../../scripts/utils/decorate.js'; + +const [{ createTag }] = await Promise.all([import(`${getLibs()}/utils/utils.js`)]); + +function decorateHeader(header, planExplanation) { + const h2 = header.querySelector('h2'); + header.classList.add('card-header'); + const premiumIcon = header.querySelector('img'); + // Finds the headcount, removes it from the original string and creates an icon with the hc + const extractHeadCountExp = /(>?)\(\d+(.*?)\)/; + if (extractHeadCountExp.test(h2?.innerText)) { + const headCntDiv = createTag('div', { class: 'head-cnt', alt: '' }); + const headCount = h2.innerText + .match(extractHeadCountExp)[0] + .replace(')', '') + .replace('(', ''); + [h2.innerText] = h2.innerText.split(extractHeadCountExp); + headCntDiv.textContent = headCount; + headCntDiv.prepend( + createTag('img', { + src: '/express/icons/head-count.svg', + alt: 'icon-head-count', + }), + ); + header.append(headCntDiv); + } + if (premiumIcon) h2.append(premiumIcon); + header.querySelectorAll('p').forEach((p) => { + if (p.innerHTML.trim() === '') p.remove(); + }); + planExplanation.classList.add('plan-explanation'); +} + +function decorateCardBorder(card, source) { + if (!source?.textContent) { + const newHeader = createTag('div', { class: 'promo-eyebrow-text' }); + card.appendChild(newHeader); + return; + } + const pattern = /\{\{(.*?)\}\}/g; + const matches = Array.from(source.textContent?.matchAll(pattern)); + if (matches.length > 0) { + const [, promoType] = matches[0]; + card.classList.add(promoType.replaceAll(' ', '')); + const newHeader = createTag('div', { class: 'promo-eyebrow-text' }); + newHeader.textContent = source.textContent.replace(pattern, ''); + card.appendChild(newHeader); + } + source.classList.add('none'); +} + +function decoratePricingArea(pricingArea) { + const pricingBar = createTag('div', { class: 'pricing-bar' }); + pricingArea.classList.add('pricing-area-wrapper'); + pricingArea.appendChild(pricingBar); +} + +function decorateCompareAll(compareAll) { + compareAll.classList.add('compare-all'); + compareAll.children[0].classList.remove('button'); +} + +function decoratePercentageBar(el) { + const pricingArea = el.querySelectorAll('.pricing-area-wrapper'); + let maxCredits = 0; + pricingArea.forEach((bar) => { + const creditCount = parseInt(bar.children[0].textContent, 10); + if (creditCount > maxCredits) maxCredits = creditCount; + }); + pricingArea.forEach((bar) => { + const creditCount = parseInt(bar.children[0].textContent, 10); + const percentage = (100 * creditCount) / maxCredits; + bar.style.setProperty('--progress', `${percentage}%`); + }); +} + +export default async function init(el) { + addTempWrapperDeprecated(el, 'pricing-cards'); + const rows = Array.from(el.querySelectorAll(':scope > div')); + const cardCount = rows[0].children.length; + const cards = []; + + for (let i = 0; i < cardCount; i += 1) { + const card = createTag('div', { class: 'card' }); + decorateCardBorder(card, rows[1].children[0]); + decorateHeader(rows[0].children[0], rows[2].children[0]); + decoratePricingArea(rows[3].children[0]); + decorateCompareAll(rows[4].children[0]); + + for (let j = 0; j < rows.length; j += 1) { + card.appendChild(rows[j].children[0]); + } + cards.push(card); + } + + el.innerHTML = ''; + for (const card of cards) { + el.appendChild(card); + } + decoratePercentageBar(el); +} diff --git a/express/blocks/quotes/quotes.css b/express/blocks/quotes/quotes.css index f0f0c168..6c5fb282 100644 --- a/express/blocks/quotes/quotes.css +++ b/express/blocks/quotes/quotes.css @@ -44,6 +44,18 @@ flex-grow: 1; } +.quotes .quote .author .author-content { + display: flex; + align-items: center; +} + +.quotes .quote .author .summary { + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: center; +} + .quotes .quote .content::before { content: "“"; display: block; @@ -60,6 +72,8 @@ } .quotes .quote .author { + display: flex; + flex-wrap: nowrap; margin-top: 16px; align-items: center; align-self: baseline; diff --git a/express/blocks/quotes/quotes.js b/express/blocks/quotes/quotes.js index bc94b511..b74e58de 100644 --- a/express/blocks/quotes/quotes.js +++ b/express/blocks/quotes/quotes.js @@ -12,16 +12,22 @@ export default function decorate($block) { if ($card.children.length > 1) { const $author = $card.children[1]; $author.classList.add('author'); + // Create a container for image and summary + const $authorContent = createTag('div', { class: 'author-content' }); + if ($author.querySelector('picture')) { const $authorImg = createTag('div', { class: 'image' }); $authorImg.appendChild($author.querySelector('picture')); - $author.appendChild($authorImg); + $authorContent.appendChild($authorImg); } + const $authorSummary = createTag('div', { class: 'summary' }); Array.from($author.querySelectorAll('p')) .filter(($p) => !!$p.textContent.trim()) .forEach(($p) => $authorSummary.appendChild($p)); - $author.appendChild($authorSummary); + $authorContent.appendChild($authorSummary); + // Append the author content container to author + $author.appendChild($authorContent); } $card.firstElementChild.classList.add('content'); }); diff --git a/express/blocks/susi-light/susi-light.js b/express/blocks/susi-light/susi-light.js index c019d3a1..ae857295 100644 --- a/express/blocks/susi-light/susi-light.js +++ b/express/blocks/susi-light/susi-light.js @@ -2,101 +2,118 @@ /* eslint-disable camelcase */ import { getLibs } from '../../scripts/utils.js'; -const { createTag, loadScript, getConfig, loadIms } = await import(`${getLibs()}/utils/utils.js`); +const { createTag, loadScript, getConfig } = await import(`${getLibs()}/utils/utils.js`); -const config = { consentProfile: 'free' }; const variant = 'edu-express'; const usp = new URLSearchParams(window.location.search); const isStage = (usp.get('env') && usp.get('env') !== 'prod') || getConfig().env.name !== 'prod'; -const client_id = 'AdobeExpressWeb'; -const authParams = { - dt: false, - locale: getConfig().locale.ietf.toLowerCase(), - response_type: 'code', - client_id, - scope: 'AdobeID,openid', -}; + const onRedirect = (e) => { // eslint-disable-next-line no-console console.log('redirecting to:', e.detail); - window.location.assign(e.detail); + setTimeout(() => { + window.location.assign(e.detail); + // temporary solution: allows analytics to go thru + }, 100); }; const onError = (e) => { window.lana?.log('on error:', e); }; -function sendEventToAnalytics(type, eventName) { - const sendEvent = () => { - window._satellite.track('event', { - xdm: {}, - data: { - eventType: 'web.webinteraction.linkClicks', - web: { - webInteraction: { - name: eventName, - linkClicks: { value: 1 }, - type, - }, - }, - _adobe_corpnew: { - digitalData: { - primaryEvent: { - eventInfo: { - eventName, - client_id, - }, - }, - }, - }, - }, - }); - }; - if (window._satellite?.track) { - sendEvent(); - } else { - window.addEventListener('alloy_sendEvent', () => { - sendEvent(); - }, { once: true }); - } -} - -// TODO: analytcis requirements -const onAnalytics = (e) => { - const { type, event } = e.detail; - sendEventToAnalytics(type, event); -}; - export function loadWrapper() { const CDN_URL = `https://auth-light.identity${isStage ? '-stage' : ''}.adobe.com/sentry/wrapper.js`; return loadScript(CDN_URL); } -export default async function init(el) { - const input = el.querySelector('div > div')?.textContent?.trim().toLowerCase(); +function getDestURL(url) { let destURL; try { - destURL = new URL(input); + destURL = new URL(url); } catch (err) { - window.lana?.log(`invalid redirect uri for susi-light: ${input}`); + window.lana?.log(`invalid redirect uri for susi-light: ${url}`); destURL = new URL('https://new.express.adobe.com'); } if (isStage && ['new.express.adobe.com', 'express.adobe.com'].includes(destURL.hostname)) { destURL.hostname = 'stage.projectx.corp.adobe.com'; } - const goDest = () => window.location.assign(destURL.toString()); - loadIms().then(() => { - if (window.adobeIMS?.isSignedInUser()) { - goDest(); - } - }).catch(() => {}); + return destURL.toString(); +} + +export default async function init(el) { + const rows = el.querySelectorAll(':scope> div > div'); + const redirectUrl = rows[0]?.textContent?.trim().toLowerCase(); + // eslint-disable-next-line camelcase + const client_id = rows[1]?.textContent?.trim() ?? 'AdobeExpressWeb'; + const title = rows[2]?.textContent?.trim(); + const authParams = { + dt: false, + locale: getConfig().locale.ietf.toLowerCase(), + response_type: 'code', + client_id, + scope: 'AdobeID,openid', + }; + const destURL = getDestURL(redirectUrl); + const goDest = () => window.location.assign(destURL); + if (window.feds?.utilities?.imslib) { + const { imslib } = window.feds.utilities; + imslib.isReady() && imslib.isSignedInUser() && goDest(); + imslib.onReady().then(() => imslib.isSignedInUser() && goDest()); + } el.innerHTML = ''; await loadWrapper(); + const config = { + consentProfile: 'free', + }; + if (title) { + config.title = title; + } const susi = createTag('susi-sentry-light'); susi.authParams = authParams; - susi.authParams.redirect_uri = destURL.toString(); + susi.authParams.redirect_uri = destURL; susi.config = config; if (isStage) susi.stage = 'true'; susi.variant = variant; + function sendEventToAnalytics(type, eventName) { + const sendEvent = () => { + window._satellite.track('event', { + xdm: {}, + data: { + eventType: 'web.webinteraction.linkClicks', + web: { + webInteraction: { + name: eventName, + linkClicks: { + value: 1, + }, + type, + }, + }, + _adobe_corpnew: { + digitalData: { + primaryEvent: { + eventInfo: { + eventName, + client_id, + }, + }, + }, + }, + }, + }); + }; + if (window._satellite?.track) { + sendEvent(); + } else { + window.addEventListener('alloy_sendEvent', () => { + sendEvent(); + }, { once: true }); + } + } + + const onAnalytics = (e) => { + const { type, event } = e.detail; + sendEventToAnalytics(type, event); + }; susi.addEventListener('redirect', onRedirect); susi.addEventListener('on-error', onError); susi.addEventListener('on-analytics', onAnalytics); diff --git a/express/blocks/template-x/sample-template.json b/express/blocks/template-x/sample-template.json new file mode 100644 index 00000000..b814cb47 --- /dev/null +++ b/express/blocks/template-x/sample-template.json @@ -0,0 +1,165 @@ +{ + "id": "urn:aaid:sc:VA6C2:74f8db2e-e6b0-5c4d-acc0-b769b373ed54", + "parentDirectoryId": "urn:aaid:sc:VA6C2:9a95c444-ed0d-42fb-bfbb-2cb624fbbec6", + "ancestorAssetIds": [ + "urn:aaid:directory:b5accaca-1f2d-4311-aa12-e9c537a8fad8", + "urn:aaid:directory:61acb9e9-a107-48e0-9e73-80c8031ede18", + "urn:aaid:sc:VA6C2:74c05c10-0945-5dd6-9c3d-22851a3d1903", + "urn:aaid:sc:VA6C2:b2b850c0-cab8-48be-89db-78909574db16", + "urn:aaid:sc:VA6C2:0fabaa0d-a1f7-4cbd-b603-89c6dc35a48a", + "urn:aaid:sc:VA6C2:74c60171-633c-4451-867e-d49e5f4e5884", + "urn:aaid:sc:VA6C2:1bb25287-eae8-476e-8dab-5c191e525e61", + "urn:aaid:sc:VA6C2:9a95c444-ed0d-42fb-bfbb-2cb624fbbec6" + ], + "path": "/content/assets/content/approved/ccx/template/ccx_content_partnerships/bdf81e9e-acc4-5351-bff9-4de5d0a95e27", + "contentType": "application/vnd.adobe.hz.express+dcx", + "createDate": "2024-06-28T19:32:56.868Z", + "modifyDate": "2024-06-28T19:32:57.193Z", + "name": "bdf81e9e-acc4-5351-bff9-4de5d0a95e27", + "status": "approved", + "dc:title": { + "i-default": "Pink Floral Workshop Flyer for Print by Huyen Dihn" + }, + "etags": "\"51bef6458d7942178d8a22e015341e66\"", + "assetType": "Template", + "behaviors": [ + "still" + ], + "topics": [ + "artist", + "floral", + "flower", + "huyen dinh", + "huyendinh", + "print artist", + "workshop" + ], + "availabilityDate": "2024-07-10T02:42:50.175Z", + "licensingCategory": "free", + "language": "en-US", + "applicableRegions": [ + "ZZ" + ], + "attribution": { + "vendor": "Adobe Express", + "submittedBy": "2BCD1E4C6619FCEC0A495FDD@c0b827b66271908b495fe8.e" + }, + "pages": [ + { + "task": { + "name": "flyer", + "size": { + "name": "letter" + } + }, + "extractedColors": [ + { + "name": "dark gray", + "coverage": 0.0619, + "mode": "RGB", + "value": { + "r": 88, + "g": 89, + "b": 89 + } + }, + { + "name": "pink", + "coverage": 0.8015, + "mode": "RGB", + "value": { + "r": 251, + "g": 225, + "b": 235 + } + }, + { + "name": "white", + "coverage": 0.0485, + "mode": "RGB", + "value": { + "r": 251, + "g": 238, + "b": 246 + } + }, + { + "name": "lilac", + "coverage": 0.0663, + "mode": "RGB", + "value": { + "r": 235, + "g": 194, + "b": 233 + } + }, + { + "name": "gray", + "coverage": 0.0219, + "mode": "RGB", + "value": { + "r": 144, + "g": 137, + "b": 142 + } + } + ], + "rendition": { + "image": { + "thumbnail": { + "componentId": "fcb8f005-cd35-4826-94e5-c5faee5d54b3", + "hzRevision": "19df1eea-9969-40d7-aa17-acfcb3ac5707", + "width": 386, + "height": 500, + "mediaType": "image/webp" + }, + "preview": { + "componentId": "0d036589-3a8e-4df0-8b0c-890cc3b1cce5", + "hzRevision": "f2ad33b7-1b4b-4879-b0ce-c41cf9eb6c88", + "width": 927, + "height": 1200, + "mediaType": "image/webp" + } + } + } + } + ], + "customLinks": { + "branchUrl": "https://adobesparkpost.app.link/uHF8ZOZG6Kb" + }, + "stats": { + "remixCount": 65 + }, + "styles": [ + "pop-art" + ], + "segments": [ + "personal" + ], + "_links": { + "http://ns.adobe.com/adobecloud/rel/rendition": { + "href": "https://design-assets.adobeprojectm.com/content/download/express/public/urn:aaid:sc:VA6C2:74f8db2e-e6b0-5c4d-acc0-b769b373ed54/rendition?assetType=TEMPLATE&etag=51bef6458d7942178d8a22e015341e66{&page,size,type,fragment}", + "templated": true, + "mode": "id", + "name": "ACP" + }, + "http://ns.adobe.com/adobecloud/rel/primary": { + "href": "https://design-assets.adobeprojectm.com/content/download/express/public/urn:aaid:sc:VA6C2:74f8db2e-e6b0-5c4d-acc0-b769b373ed54/primary?assetType=TEMPLATE&etag=51bef6458d7942178d8a22e015341e66", + "templated": false, + "mode": "id", + "name": "ACP" + }, + "http://ns.adobe.com/adobecloud/rel/manifest": { + "href": "https://design-assets.adobeprojectm.com/content/download/express/public/urn:aaid:sc:VA6C2:74f8db2e-e6b0-5c4d-acc0-b769b373ed54/manifest?assetType=TEMPLATE&etag=51bef6458d7942178d8a22e015341e66", + "templated": false, + "mode": "id", + "name": "ACP" + }, + "http://ns.adobe.com/adobecloud/rel/component": { + "href": "https://design-assets.adobeprojectm.com/content/download/express/public/urn:aaid:sc:VA6C2:74f8db2e-e6b0-5c4d-acc0-b769b373ed54/component?assetType=TEMPLATE&etag=51bef6458d7942178d8a22e015341e66{&revision,component_id}", + "templated": true, + "mode": "id", + "name": "ACP" + } + } +} diff --git a/express/blocks/template-x/sample-webpage-template.json b/express/blocks/template-x/sample-webpage-template.json new file mode 100644 index 00000000..737c0512 --- /dev/null +++ b/express/blocks/template-x/sample-webpage-template.json @@ -0,0 +1,146 @@ +{ + "id": "urn:aaid:sc:VA6C2:cd210180-70f1-5193-aca7-84eeb8a1bdd3", + "parentDirectoryId": "urn:aaid:sc:VA6C2:201252df-82cd-4397-90bd-87d18579b3fb", + "ancestorAssetIds": [ + "urn:aaid:directory:b5accaca-1f2d-4311-aa12-e9c537a8fad8", + "urn:aaid:directory:61acb9e9-a107-48e0-9e73-80c8031ede18", + "urn:aaid:sc:VA6C2:74c05c10-0945-5dd6-9c3d-22851a3d1903", + "urn:aaid:sc:VA6C2:b2b850c0-cab8-48be-89db-78909574db16", + "urn:aaid:sc:VA6C2:0fabaa0d-a1f7-4cbd-b603-89c6dc35a48a", + "urn:aaid:sc:VA6C2:74c60171-633c-4451-867e-d49e5f4e5884", + "urn:aaid:sc:VA6C2:9c5613dc-9f25-4aa9-87fa-db80e6123fd6", + "urn:aaid:sc:VA6C2:201252df-82cd-4397-90bd-87d18579b3fb" + ], + "path": "/content/assets/content/approved/ccx/webpage_template/ccx_content/extracurricular_activities", + "contentType": "application/vnd.adobe.hztemp.page+dcx", + "createDate": "2024-07-24T19:03:44.830Z", + "modifyDate": "2024-07-24T19:03:45.114Z", + "name": "extracurricular_activities", + "status": "approved", + "dc:title": { + "i-default": "Extracurricular activities" + }, + "dc:description": { + "en-US": "Bright colors and clear fonts get webpages made with this template noticed. Change text, add your own images or video, and get exactly the look you want. Use this fun, informal template to promote a wide variety of events from school outings to parties." + }, + "etags": "\"23b75d99a3554908ae1f8baf96be58b2\"", + "assetType": "Webpage_Template", + "task": { + "name": "event-microsite", + "size": { + "name": "500x500px" + } + }, + "features": [ + "restricted" + ], + "behaviors": [ + "still" + ], + "topics": [ + "education", + "education resource", + "educator resource", + "event", + "extracurricular", + "landing", + "rsvp", + "scholastic event", + "student activity", + "website" + ], + "availabilityDate": "2024-08-09T02:16:31.760Z", + "licensingCategory": "free", + "language": "en-US", + "applicableRegions": [ + "ZZ" + ], + "extractedColors": [ + { + "name": "light blue", + "coverage": 0.4445, + "mode": "RGB", + "value": { + "r": 131, + "g": 181, + "b": 213 + } + }, + { + "name": "off white", + "coverage": 0.198, + "mode": "RGB", + "value": { + "r": 231, + "g": 231, + "b": 231 + } + }, + { + "name": "royal blue", + "coverage": 0.1482, + "mode": "RGB", + "value": { + "r": 24, + "g": 76, + "b": 149 + } + }, + { + "name": "dark blue", + "coverage": 0.1529, + "mode": "RGB", + "value": { + "r": 17, + "g": 37, + "b": 85 + } + }, + { + "name": "black", + "coverage": 0.0564, + "mode": "RGB", + "value": { + "r": 2, + "g": 2, + "b": 5 + } + } + ], + "attribution": { + "vendor": "Adobe Express", + "submittedBy": "61D11F476369A4AC0A495FBE@c0b827b66271908b495fe8.e" + }, + "customLinks": { + "branchUrl": "https://adobesparkpost.app.link/Q3DIFvesULb" + }, + "segments": [ + "all" + ], + "_links": { + "http://ns.adobe.com/adobecloud/rel/rendition": { + "href": "https://design-assets.adobeprojectm.com/content/download/express/public/urn:aaid:sc:VA6C2:cd210180-70f1-5193-aca7-84eeb8a1bdd3/rendition?assetType=WEBPAGE_TEMPLATE&etag=23b75d99a3554908ae1f8baf96be58b2{&page,size,type,fragment}", + "templated": true, + "mode": "id", + "name": "ACP" + }, + "http://ns.adobe.com/adobecloud/rel/primary": { + "href": "https://design-assets.adobeprojectm.com/content/download/express/public/urn:aaid:sc:VA6C2:cd210180-70f1-5193-aca7-84eeb8a1bdd3/primary?assetType=WEBPAGE_TEMPLATE&etag=23b75d99a3554908ae1f8baf96be58b2", + "templated": false, + "mode": "id", + "name": "ACP" + }, + "http://ns.adobe.com/adobecloud/rel/manifest": { + "href": "https://design-assets.adobeprojectm.com/content/download/express/public/urn:aaid:sc:VA6C2:cd210180-70f1-5193-aca7-84eeb8a1bdd3/manifest?assetType=WEBPAGE_TEMPLATE&etag=23b75d99a3554908ae1f8baf96be58b2", + "templated": false, + "mode": "id", + "name": "ACP" + }, + "http://ns.adobe.com/adobecloud/rel/component": { + "href": "https://design-assets.adobeprojectm.com/content/download/express/public/urn:aaid:sc:VA6C2:cd210180-70f1-5193-aca7-84eeb8a1bdd3/component?assetType=WEBPAGE_TEMPLATE&etag=23b75d99a3554908ae1f8baf96be58b2{&revision,component_id}", + "templated": true, + "mode": "id", + "name": "ACP" + } + } +} diff --git a/express/blocks/template-x/template-rendering.js b/express/blocks/template-x/template-rendering.js index e180de1c..dce0f908 100755 --- a/express/blocks/template-x/template-rendering.js +++ b/express/blocks/template-x/template-rendering.js @@ -41,11 +41,15 @@ function extractComponentLinkHref(template) { } function extractImageThumbnail(page) { - return page.rendition.image?.thumbnail; + return page?.rendition?.image?.thumbnail; } function getImageThumbnailSrc(renditionLinkHref, componentLinkHref, page) { const thumbnail = extractImageThumbnail(page); + if (!thumbnail) { + // webpages + return renditionLinkHref.replace('{&page,size,type,fragment}', ''); + } const { mediaType, componentId, @@ -118,9 +122,8 @@ async function share(branchUrl, tooltip, timeoutId) { }, 2500); } -async function renderShareWrapper(branchUrl) { - const tagCopied = await replaceKey('tag-copied', getConfig()); - const text = tagCopied === 'tag copied' ? 'Copied to clipboard' : tagCopied; +function renderShareWrapper(branchUrl, placeholders) { + const text = placeholders['tag-copied'] ?? 'Copied to clipboard'; const wrapper = createTag('div', { class: 'share-icon-wrapper' }); const shareIcon = getIconElementDeprecated('share-arrow'); shareIcon.setAttribute('tabindex', 0); @@ -131,9 +134,9 @@ async function renderShareWrapper(branchUrl) { tabindex: '-1', }); let timeoutId = null; - shareIcon.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); + shareIcon.addEventListener('click', (ev) => { + ev.preventDefault(); + ev.stopPropagation(); timeoutId = share(branchUrl, tooltip, timeoutId); }); @@ -151,9 +154,8 @@ async function renderShareWrapper(branchUrl) { return wrapper; } -async function renderCTA(branchUrl) { - const editThisTemplate = await replaceKey('edit-this-template', getConfig()); - const btnTitle = editThisTemplate === 'edit this template' ? 'Edit this template' : editThisTemplate; +function renderCTA(placeholders, branchUrl) { + const btnTitle = placeholders['edit-this-template'] ?? 'Edit this template'; const btnEl = createTag('a', { href: branchUrl, title: btnTitle, @@ -375,7 +377,7 @@ async function renderHoverWrapper(template) { focusHandler, } = renderMediaWrapper(template); - const cta = await renderCTA(template.customLinks.branchUrl); + const cta = renderCTA(template.customLinks.branchUrl); const ctaLink = renderCTALink(template.customLinks.branchUrl); ctaLink.append(mediaWrapper); @@ -398,8 +400,17 @@ async function renderHoverWrapper(template) { trackSearch('select-template', BlockMediator.get('templateSearchSpecs')?.search_id); }; + const ctaClickHandlerTouchDevice = (ev) => { + // If it is a mobile device with a touch screen, do not jump over to the Edit page, + // but allow the user to preview the template instead + if (window.matchMedia('(pointer: coarse)').matches) { + ev.preventDefault(); + } + }; + cta.addEventListener('click', ctaClickHandler, { passive: true }); ctaLink.addEventListener('click', ctaClickHandler, { passive: true }); + ctaLink.addEventListener('click', ctaClickHandlerTouchDevice); return btnContainer; } @@ -465,10 +476,12 @@ async function renderStillWrapper(template) { export default async function renderTemplate(template) { const tmpltEl = createTag('div'); - const stillWrapper = await renderStillWrapper(template); - tmpltEl.append(stillWrapper); - const hoverWrapper = await renderHoverWrapper(template); - tmpltEl.append(hoverWrapper); + if (template.assetType === 'Webpage_Template') { + // webpage_template has no pages + template.pages = [{}]; + } + tmpltEl.append(renderStillWrapper(template)); + tmpltEl.append(renderHoverWrapper(template)); return tmpltEl; } diff --git a/express/blocks/template-x/template-x.css b/express/blocks/template-x/template-x.css index 9848366f..d42624f6 100755 --- a/express/blocks/template-x/template-x.css +++ b/express/blocks/template-x/template-x.css @@ -1667,7 +1667,7 @@ main .template-x.horizontal > .template-x.horizontal.tabbed > .template-title { .template-x.horizontal.mini .carousel-platform { padding: 32px; -} +} .template-x.horizontal.fullwidth.holiday .carousel-container .carousel-fader-right { transform: translate3d(0, 0, 0); @@ -2067,7 +2067,7 @@ main .template-x.horizontal > .template-x.horizontal.tabbed > .template-title { flex-basis: 100%; } -.template-x .template .button-container a.button { +.template-x .template .button-container a.button, .template-x .template a.con-button { max-width: 100%; margin: 6px 6px 0; box-sizing: border-box; @@ -2077,14 +2077,14 @@ main .template-x.horizontal > .template-x.horizontal.tabbed > .template-title { text-overflow: ellipsis; } -.template-x .template .button-container a.cta-link { +.template-x .template .button-container a.cta-link, .template-x .template a.con-button { width: 100%; height: 100%; white-space: nowrap; font-weight: var(--body-font-weight); } -.template-x.horizontal .template .button-container a.button { +.template-x.horizontal .template .button-container a.button, .template-x.horizontal .template a.con-button { min-height: 16px; } @@ -2473,7 +2473,7 @@ nav ol.templates-breadcrumbs li:not(:first-child):before { main.with-holiday-templates-banner { padding-top: 54px; } - + .template-x.horizontal.holiday .toggle-bar { gap: 14px; } diff --git a/express/blocks/template-x/template-x.js b/express/blocks/template-x/template-x.js index 4f8aa5e3..ad5ee0b1 100755 --- a/express/blocks/template-x/template-x.js +++ b/express/blocks/template-x/template-x.js @@ -982,7 +982,7 @@ async function initFilterSort(block, props, toolBar) { }); trackSearch('search-inspire'); await redrawTemplates(block, props, toolBar); - trackSearch('view-search-results', BlockMediator.get('templateSearchSpecs').search_id); + trackSearch('view-search-result', BlockMediator.get('templateSearchSpecs').search_id); } }); }); @@ -1334,7 +1334,7 @@ async function decorateTemplates(block, props) { result_count: props.total, content_category: 'templates', }); - if (searchId) trackSearch('view-search-results', searchId); + if (searchId) trackSearch('view-search-result', searchId); document.dispatchEvent(linksPopulated); } diff --git a/express/features/direct-path-to-product/direct-path-to-product.css b/express/features/direct-path-to-product/direct-path-to-product.css index 360c21bc..bdce602d 100644 --- a/express/features/direct-path-to-product/direct-path-to-product.css +++ b/express/features/direct-path-to-product/direct-path-to-product.css @@ -34,11 +34,8 @@ .pep-container .pep-progress-bg .pep-progress-bar { background-color: var(--color-info-accent); - width: 100%; + width: 0; height: 100%; - -webkit-animation: loginRedirectProgress 4s ease; - -moz-animation: loginRedirectProgress 4s ease; - animation: loginRedirectProgress 4s ease; } .pep-container .profile-wrapper { @@ -121,36 +118,6 @@ color: var(--color-info-accent-hover); } -@-webkit-keyframes loginRedirectProgress { - 0% { - width: 0; - } - - 100% { - width: 100% - } -} - -@-moz-keyframes loginRedirectProgress { - 0% { - width: 0; - } - - 100% { - width: 100% - } -} - -@keyframes loginRedirectProgress { - 0% { - width: 0; - } - - 100% { - width: 100% - } -} - @-webkit-keyframes rotateCircle { 0% { transform: translate3d(-50%, -50%, 0) rotate(0deg); diff --git a/express/features/direct-path-to-product/direct-path-to-product.js b/express/features/direct-path-to-product/direct-path-to-product.js index 01851463..f50f205b 100644 --- a/express/features/direct-path-to-product/direct-path-to-product.js +++ b/express/features/direct-path-to-product/direct-path-to-product.js @@ -10,21 +10,27 @@ const OPT_OUT_KEY = 'no-direct-path-to-product'; const adobeEventName = 'adobe.com:express:cta:pep'; +const REACT_TIME = 4000; + function track(name) { - _satellite?.track('event', { - xdm: {}, - data: { - eventType: 'web.webinteraction.linkClicks', - web: { - webInteraction: { - name, - linkClicks: { value: 1 }, - type: 'other', + try { + _satellite?.track('event', { + xdm: {}, + data: { + eventType: 'web.webinteraction.linkClicks', + web: { + webInteraction: { + name, + linkClicks: { value: 1 }, + type: 'other', + }, }, + _adobe_corpnew: { digitalData: { primaryEvent: { eventInfo: { eventName: name } } } }, }, - _adobe_corpnew: { digitalData: { primaryEvent: { eventInfo: { eventName: name } } } }, - }, - }); + }); + } catch (e) { + window.lana.log(e); + } } function buildProfileWrapper(profile) { @@ -41,7 +47,7 @@ function buildProfileWrapper(profile) { } export default async function loadLoginUserAutoRedirect() { - let followThrough = true; + let cancel = false; const [mod] = await Promise.all([ import(`${getLibs()}/features/placeholders.js`), new Promise((resolve) => { @@ -49,7 +55,7 @@ export default async function loadLoginUserAutoRedirect() { }), ]); - const buildRedirectAlert = () => { + const buildRedirectAlert = async () => { const container = createTag('div', { class: 'pep-container' }); const headerWrapper = createTag('div', { class: 'pep-header' }); const headerIcon = createTag('div', { class: 'pep-header-icon' }, getIconElementDeprecated('cc-express')); @@ -58,40 +64,37 @@ export default async function loadLoginUserAutoRedirect() { const progressBar = createTag('div', { class: 'pep-progress-bar' }); const noticeWrapper = createTag('div', { class: 'notice-wrapper' }); const noticeText = createTag('span', { class: 'notice-text' }, mod.replaceKey('pep-cancel', getConfig())); - const noticeBtn = createTag('a', { class: 'notice-btn' }, mod.replaceKey('cancel', getConfig())); + const noticeBtn = createTag('button', { class: 'notice-btn', tabIndex: '1' }, mod.replaceKey('cancel', getConfig())); headerWrapper.append(headerIcon, headerText); progressBg.append(progressBar); noticeWrapper.append(noticeText, noticeBtn); container.append(headerWrapper, progressBg); - return new Promise((resolve) => { - getProfile().then((profile) => { - if (profile) { - container.append(buildProfileWrapper(profile)); - } - container.append(noticeWrapper); - - const header = document.querySelector('header'); - header.append(container); - - noticeBtn.addEventListener('click', () => { - track(`${adobeEventName}:cancel`); - container.remove(); - followThrough = false; - localStorage.setItem(OPT_OUT_KEY, '3'); - }); - - resolve(container); - }); + const profile = await getProfile(); + if (profile) { + container.append(buildProfileWrapper(profile)); + } + container.append(noticeWrapper); + + const header = document.querySelector('header'); + header.append(container); + const handleTab = (event) => { + if (event.key === 'Tab') { + event.preventDefault(); + noticeBtn.focus(); + document.removeEventListener('keydown', handleTab); + } + }; + document.addEventListener('keydown', handleTab); + + noticeBtn.addEventListener('click', () => { + track(`${adobeEventName}:cancel`); + container.remove(); + document.removeEventListener('keydown', handleTab); + cancel = true; + localStorage.setItem(OPT_OUT_KEY, '3'); }); - }; - - const initRedirect = (container) => { - container.classList.add('done'); - - track(`${adobeEventName}:redirect`); - - window.location.assign(getDestination()); + return { container, noticeBtn }; }; const optOutCounter = localStorage.getItem(OPT_OUT_KEY); @@ -100,10 +103,67 @@ export default async function loadLoginUserAutoRedirect() { const counterNumber = parseInt(optOutCounter, 10); localStorage.setItem(OPT_OUT_KEY, (counterNumber - 1).toString()); } else { - buildRedirectAlert().then((container) => { - setTimeout(() => { - if (followThrough) initRedirect(container); - }, 4000); - }); + const { container, noticeBtn } = await buildRedirectAlert(); + let startTime = performance.now(); + let remainTime = REACT_TIME; + let timeoutId; + let [hovering, focusing] = [false, false]; + const progressBar = container.querySelector('.pep-progress-bar'); + let start; + const pause = () => { + clearTimeout(timeoutId); + const pastTime = performance.now() - startTime; + remainTime -= pastTime; + const progress = Math.min(100, ((REACT_TIME - remainTime) / REACT_TIME) * 100); + progressBar.style.transition = 'none'; + progressBar.style.width = `${progress}%`; + }; + const resume = () => { + timeoutId = start(); + }; + const mouseEnter = () => { + hovering = true; + if (focusing) return; + pause(); + }; + const mouseLeave = () => { + hovering = false; + if (focusing) return; + resume(); + }; + const focusIn = () => { + focusing = true; + if (hovering) return; + pause(); + }; + const focusOut = () => { + focusing = false; + if (hovering) return; + resume(); + }; + start = () => { + startTime = performance.now(); + progressBar.style.transition = `width ${remainTime}ms linear`; + // eslint-disable-next-line chai-friendly/no-unused-expressions + progressBar.offsetWidth; // forcing a reflow to get more consistent transition + progressBar.style.width = '100%'; + return setTimeout(() => { + container.removeEventListener('mouseenter', mouseEnter); + container.removeEventListener('mouseleave', mouseLeave); + noticeBtn.removeEventListener('focusin', focusIn); + noticeBtn.removeEventListener('focusout', focusOut); + if (!cancel) { + container.classList.add('done'); + track(`${adobeEventName}:redirect`); + window.location.assign(getDestination()); + container.remove(); + } + }, remainTime); + }; + noticeBtn.addEventListener('focusin', focusIn); + noticeBtn.addEventListener('focusout', focusOut); + container.addEventListener('mouseenter', mouseEnter); + container.addEventListener('mouseleave', mouseLeave); + timeoutId = start(); } } diff --git a/express/icons/double-sparkles.svg b/express/icons/double-sparkles.svg new file mode 100644 index 00000000..bc44450b --- /dev/null +++ b/express/icons/double-sparkles.svg @@ -0,0 +1,4 @@ + + + + diff --git a/express/icons/enticement-arrow.svg b/express/icons/enticement-arrow.svg new file mode 100644 index 00000000..2a67b493 --- /dev/null +++ b/express/icons/enticement-arrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/express/icons/external-link.svg b/express/icons/external-link.svg new file mode 100644 index 00000000..ed2483aa --- /dev/null +++ b/express/icons/external-link.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/express/scripts/express-delayed.js b/express/scripts/express-delayed.js index c004b18e..f06ac679 100644 --- a/express/scripts/express-delayed.js +++ b/express/scripts/express-delayed.js @@ -6,7 +6,7 @@ const { createTag, getMetadata, getConfig, loadStyle } = await import(`${getLibs export function getDestination() { const pepDestinationMeta = getMetadata('pep-destination'); return pepDestinationMeta || BlockMediator.get('primaryCtaUrl') - || document.querySelector('a.button.xlarge.same-as-floating-button-CTA, a.primaryCTA, a.con-button.same-as-floating-button-CTA')?.href; + || document.querySelector('a.button.xlarge.same-fcta, a.primaryCTA, a.con-button.button-xxl.same-fcta, a.con-button.xxl-button.same-fcta')?.href; } function getSegmentsFromAlloyResponse(response) { @@ -106,6 +106,8 @@ function isBranchLink(url) { // product entry prompt async function canPEP() { // TODO test this whole method + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('force-pep')) return true; if (document.body.dataset.device !== 'desktop') return false; const pepSegment = getMetadata('pep-segment'); if (!pepSegment) return false; diff --git a/express/scripts/scripts.js b/express/scripts/scripts.js index 626f0a3a..7d527da0 100644 --- a/express/scripts/scripts.js +++ b/express/scripts/scripts.js @@ -44,10 +44,12 @@ const CONFIG = { cn: { ietf: 'zh-CN', tk: 'qxw8hzm' }, de: { ietf: 'de-DE', tk: 'vin7zsi.css' }, dk: { ietf: 'da-DK', tk: 'aaz7dvd.css' }, + eg: { ietf: 'en-EG', tk: 'pps7abe.css' }, es: { ietf: 'es-ES', tk: 'oln4yqj.css' }, fi: { ietf: 'fi-FI', tk: 'aaz7dvd.css' }, fr: { ietf: 'fr-FR', tk: 'vrk5vyv.css' }, gb: { ietf: 'en-GB', tk: 'pps7abe.css' }, + id_id: { ietf: 'id-ID', tk: 'cya6bri.css' }, in: { ietf: 'en-IN', tk: 'pps7abe.css' }, it: { ietf: 'it-IT', tk: 'bbf5pok.css' }, jp: { ietf: 'ja-JP', tk: 'dvg6awq' }, @@ -55,12 +57,11 @@ const CONFIG = { nl: { ietf: 'nl-NL', tk: 'cya6bri.css' }, no: { ietf: 'no-NO', tk: 'aaz7dvd.css' }, se: { ietf: 'sv-SE', tk: 'fpk1pcd.css' }, + tr: { ietf: 'tr-TR', tk: 'ley8vds.css' }, // eslint-disable-next-line max-len // TODO check that this ietf is ok to use everywhere. It's different in the old project zh-Hant-TW tw: { ietf: 'zh-TW', tk: 'jay0ecd' }, uk: { ietf: 'en-GB', tk: 'pps7abe.css' }, - tr: { ietf: 'tr-TR', tk: 'ley8vds.css' }, - eg: { ietf: 'en-EG', tk: 'pps7abe.css' }, }, entitlements: { '2a537e84-b35f-4158-8935-170c22b8ae87': 'express-entitled', diff --git a/express/scripts/utils.js b/express/scripts/utils.js index 27040d58..012f6ee0 100644 --- a/express/scripts/utils.js +++ b/express/scripts/utils.js @@ -178,7 +178,7 @@ export function removeIrrelevantSections(area) { }); sameUrlCTAs.forEach((cta) => { - cta.classList.add('same-as-floating-button-CTA'); + cta.classList.add('same-fcta'); }); } } diff --git a/express/scripts/utils/embed-videos.js b/express/scripts/utils/embed-videos.js index 4bfc07e0..d749d3fc 100644 --- a/express/scripts/utils/embed-videos.js +++ b/express/scripts/utils/embed-videos.js @@ -52,5 +52,6 @@ export function isVideoLink(url) { return url.includes('youtube.com/watch') || url.includes('youtu.be/') || url.includes('vimeo') + || /^https?:[/][/]video[.]tv[.]adobe[.]com/.test(url) || /.*\/media_.*(mp4|webm|m3u8)$/.test(new URL(url).pathname); } diff --git a/express/scripts/utils/pricing.js b/express/scripts/utils/pricing.js index e0612147..bad20512 100644 --- a/express/scripts/utils/pricing.js +++ b/express/scripts/utils/pricing.js @@ -44,7 +44,7 @@ const currencies = { my: 'MYR', nl: 'EUR', no: 'NOK', - nz: 'AUD', + nz: 'NZD', pe: 'PEN', ph: 'PHP', pl: 'EUR', @@ -59,15 +59,15 @@ const currencies = { tw: 'TWD', us: 'USD', ve: 'USD', - za: 'USD', - ae: 'USD', + za: 'ZAR', + ae: 'AED', bh: 'BHD', eg: 'EGP', jo: 'JOD', kw: 'KWD', om: 'OMR', - qa: 'USD', - sa: 'SAR', + qa: 'QAR', + sa: 'USD', ua: 'USD', dz: 'USD', lb: 'LBP', @@ -123,11 +123,14 @@ export async function formatPrice(price, currency) { EGP: 'LE', ARS: 'Ar$', }; - const locale = ['USD', 'TWD'].includes(currency) - ? 'en-GB' // use en-GB for intl $ symbol formatting - : (getConfig().locales[await getCountry() || '']?.ietf ?? 'en-US'); + let currencyLocale = 'en-GB'; // use en-GB for intl $ symbol formatting + if (!['USD', 'TWD'].includes(currency)) { + const country = await getCountry(); + currencyLocale = Object.entries(getConfig().locales).find(([key]) => key.startsWith(country))?.[1]?.ietf ?? 'en-US'; + } const currencyDisplay = getCurrencyDisplay(currency); - let formattedPrice = new Intl.NumberFormat(locale, { + + let formattedPrice = new Intl.NumberFormat(currencyLocale, { style: 'currency', currency, currencyDisplay, diff --git a/express/scripts/widgets/floating-cta.js b/express/scripts/widgets/floating-cta.js index 71248494..c935c455 100644 --- a/express/scripts/widgets/floating-cta.js +++ b/express/scripts/widgets/floating-cta.js @@ -176,13 +176,13 @@ export function createFloatingButton(block, audience, data) { // Hide CTAs with same url & text as the Floating CTA && is NOT a Floating CTA (in mobile/tablet) const aTagURL = new URL(aTag.href); - const sameUrlCTAs = Array.from(main.querySelectorAll('a.button:any-link')) + const sameUrlCTAs = Array.from(main.querySelectorAll('a.button:any-link, a.con-button:any-link')) .filter((a) => ( a.textContent.trim() === aTag.textContent.trim() || (new URL(a.href).pathname === aTagURL.pathname && new URL(a.href).hash === aTagURL.hash)) && !a.parentElement.parentElement.classList.contains('floating-button')); sameUrlCTAs.forEach((cta) => { - cta.classList.add('same-as-floating-button-CTA'); + cta.classList.add('same-fcta'); }); const floatButtonWrapperOld = aTag.closest('.floating-button-wrapper'); @@ -269,8 +269,8 @@ export function createFloatingButton(block, audience, data) { document.dispatchEvent(new CustomEvent('floatingbuttonloaded', { detail: { block: floatButtonWrapper } })); - const heroCTA = document.querySelectorAll('a.button.same-as-floating-button-CTA, a.con-button.same-as-floating-button-CTA'); - if (heroCTA[0]) { + const heroCTA = document.querySelector('a.button.same-fcta, a.con-button.same-fcta'); + if (heroCTA) { const hideButtonWhenIntersecting = new IntersectionObserver(([e]) => { if (e.boundingClientRect.top > window.innerHeight - 40 || e.boundingClientRect.top === 0) { floatButtonWrapper.classList.remove('floating-button--below-the-fold'); @@ -296,10 +296,10 @@ export function createFloatingButton(block, audience, data) { threshold: 0, }); if (document.readyState === 'complete') { - hideButtonWhenIntersecting.observe(heroCTA[0]); + hideButtonWhenIntersecting.observe(heroCTA); } else { window.addEventListener('load', () => { - hideButtonWhenIntersecting.observe(heroCTA[0]); + hideButtonWhenIntersecting.observe(heroCTA); }); } } else { diff --git a/express/scripts/widgets/video.js b/express/scripts/widgets/video.js index 3bb14989..0f93adf2 100644 --- a/express/scripts/widgets/video.js +++ b/express/scripts/widgets/video.js @@ -154,8 +154,32 @@ function playInlineVideo($element, vidUrls, playerType, title, ts) { } }); } else { - // iframe 3rd party player - $element.innerHTML = ``; + if (playerType === 'adobetv') { + const videoURL = `${primaryUrl.replace(/[/]$/, '')}/?autoplay=true`; + const $iframe = createTag('iframe', { + title, + src: videoURL, + frameborder: '0', + allow: 'autoplay', + webkitallowfullscreen: '', + mozallowfullscreen: '', + allowfullscreen: '', + scrolling: 'no', + }); + + $element.replaceChildren($iframe); + } else { + // iframe 3rd party player + const $iframe = createTag('iframe', { + title, + src: primaryUrl, + frameborder: '0', + allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', + allowfullscreen: '', + }); + + $element.replaceChildren($iframe); + } const $videoClose = $element.appendChild(createTag('div', { class: 'close' })); $videoClose.addEventListener('click', () => { // eslint-disable-next-line no-use-before-define @@ -232,7 +256,9 @@ export function displayVideoModal(url, title, push) { let vidType = 'default'; let ts = 0; - if (primaryUrl.includes('youtu')) { + if (/^https?:[/][/]video[.]tv[.]adobe[.]com/.test(primaryUrl)) { + vidType = 'adobetv'; + } else if (primaryUrl.includes('youtu')) { vidType = 'youtube'; const yturl = new URL(primaryUrl); let vid = yturl.searchParams.get('v'); diff --git a/express/sitemap-index.xml b/express/sitemap-index.xml index a37e0666..5c65bbbf 100644 --- a/express/sitemap-index.xml +++ b/express/sitemap-index.xml @@ -24,6 +24,9 @@ https://www.adobe.com/fr/express/sitemap.xml + + https://www.adobe.com/id_id/express/sitemap.xml + https://www.adobe.com/in/express/sitemap.xml @@ -93,6 +96,9 @@ https://www.adobe.com/fr/express/templates/sitemap.xml + + https://www.adobe.com/id_id/express/templates/sitemap.xml + https://www.adobe.com/in/express/templates/sitemap.xml diff --git a/express/styles/styles.css b/express/styles/styles.css index 11dc0a6d..31079748 100644 --- a/express/styles/styles.css +++ b/express/styles/styles.css @@ -894,12 +894,12 @@ body[data-device='mobile'] } /* floating-cta main CTA suppression */ -body[data-device="mobile"] main .floating-button-wrapper[data-audience="mobile"][data-section-status="loaded"] .same-as-floating-button-CTA { +body[data-device="mobile"] main .floating-button-wrapper[data-audience="mobile"][data-section-status="loaded"] .same-fcta { display: block; } -.section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.button.same-as-floating-button-CTA, -.section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.con-button.same-as-floating-button-CTA { +.section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.button.same-fcta, +.section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.con-button.same-fcta { display: none; } @@ -926,8 +926,8 @@ body[data-device="mobile"] main .floating-button-wrapper[data-audience="mobile"] text-align: left; } - .section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.button.same-as-floating-button-CTA, - .section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.con-button.same-as-floating-button-CTA { + .section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.button.same-fcta, + .section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.con-button.same-fcta { display: inline-block; } } diff --git a/express/template-x/template-search-api-v3.js b/express/template-x/template-search-api-v3.js index 03b69785..d5fffed7 100644 --- a/express/template-x/template-search-api-v3.js +++ b/express/template-x/template-search-api-v3.js @@ -156,7 +156,7 @@ function cleanPayload(impression, eventType) { } } - if (eventType === 'view-search-results') { + if (eventType === 'view-search-result') { delete impression.content_id; delete impression.keyword_rank; delete impression.prefix_query; @@ -357,6 +357,7 @@ function isValidBehaviors(behaviors) { export function isValidTemplate(template) { return !!(template.status === 'approved' && template.customLinks?.branchUrl + && (template.assetType === 'Webpage_Template' || template.pages?.[0]?.rendition?.image?.thumbnail?.componentId) && template.pages?.[0]?.rendition?.image?.thumbnail?.componentId && template._links?.['http://ns.adobe.com/adobecloud/rel/rendition']?.href?.replace && template._links?.['http://ns.adobe.com/adobecloud/rel/component']?.href?.replace diff --git a/helix-query.yaml b/helix-query.yaml index 3636b3ba..01c16625 100644 --- a/helix-query.yaml +++ b/helix-query.yaml @@ -159,6 +159,16 @@ indices: - '/fr/express/learn/blog/**' target: /fr/express/query-index.xlsx + indonesia: + <<: *website + include: + - '/id_id/express/**' + - '/id_id/education/express/**' + exclude: + - '/id_id/express/learn/blog' + - '/id_id/express/learn/blog/**' + target: /id_id/express/query-index.xlsx + italy: <<: *website include: diff --git a/helix-sitemap.yaml b/helix-sitemap.yaml index eac09a85..f724ca86 100644 --- a/helix-sitemap.yaml +++ b/helix-sitemap.yaml @@ -43,7 +43,7 @@ sitemaps: indonesia: source: /id_id/express/query-index.json destination: /id_id/express/sitemap.xml - hreflang: en-ID + hreflang: id-ID alternate: /id_id/{path} japan: source: /jp/express/query-index.json @@ -169,6 +169,11 @@ sitemaps: destination: /de/express/templates/sitemap.xml hreflang: de alternate: /de/{path} + indonesia: + source: /id_id/express/templates/default/metadata.json?sheet=sitemap + destination: /id_id/express/templates/sitemap.xml + hreflang: id-ID + alternate: /id_id/{path} italy: source: /it/express/templates/default/metadata.json?sheet=sitemap destination: /it/express/templates/sitemap.xml @@ -179,11 +184,6 @@ sitemaps: destination: /in/express/templates/sitemap.xml hreflang: en-IN alternate: /in/{path} - indonesia: - source: /id_id/express/templates/default/metadata.json?sheet=sitemap - destination: /id_id/express/templates/sitemap.xml - hreflang: en-ID - alternate: /id_id/{path} japan: source: /jp/express/templates/default/metadata.json?sheet=sitemap destination: /jp/express/templates/sitemap.xml diff --git a/test/blocks/how-to-cards/gallery.test.js b/test/blocks/how-to-cards/gallery.test.js new file mode 100644 index 00000000..125a82bb --- /dev/null +++ b/test/blocks/how-to-cards/gallery.test.js @@ -0,0 +1,95 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import { delay } from '../../helpers/waitfor.js'; +import { buildGallery } from '../../../express/blocks/how-to-cards/how-to-cards.js'; + +document.body.innerHTML = await readFile({ path: './mocks/gallery-body.html' }); +describe('gallery', () => { + const oldIO = window.IntersectionObserver; + let fire; + beforeEach(() => { + const mockIntersectionObserver = class { + items = []; + + constructor(cb) { + fire = cb; + } + + observe(item) { + this.items.push(item); + } + }; + window.IntersectionObserver = mockIntersectionObserver; + }); + after(() => { + window.IntersectionObserver = oldIO; + }); + it('handles irregular inputs', () => { + expect(() => buildGallery()).to.throw; + }); + it('decorates items into gallery', async () => { + const root = document.querySelector('.how-to-cards'); + const container = root.querySelector('.cards-container'); + const items = [...root.querySelectorAll('.card')]; + await buildGallery(items, container, root); + expect(container.classList.contains('gallery')).to.be.true; + items.forEach((item) => { + expect(item.classList.contains('gallery--item')).to.be.true; + }); + fire([ + { + target: items[0], + isIntersecting: true, + }, + { target: items[1], isIntersecting: true }, + ]); + await delay(310); + expect(items.findIndex((item) => item.classList.contains('curr'))).to.equal(0); + const control = root.querySelector('.gallery-control'); + expect(control).to.exist; + const prev = control.querySelector('button.prev'); + const next = control.querySelector('button.next'); + expect(prev).to.exist; + expect(next).to.exist; + expect(prev.disabled).to.be.true; + expect(next.disabled).to.be.false; + }); + it('swaps page', async () => { + const items = [...document.querySelectorAll('.card')]; + const control = document.querySelector('.gallery-control'); + fire([ + { + target: items[0], + isIntersecting: false, + }, + { target: items[2], isIntersecting: true }, + ]); + await delay(310); + expect(items.findIndex((item) => item.classList.contains('curr'))).to.equal(1); + const prev = control.querySelector('button.prev'); + const next = control.querySelector('button.next'); + expect(prev.disabled).to.be.false; + const dots = [...control.querySelectorAll('.dot')]; + expect(dots.reduce((cnt, dot) => { + if (!dot.classList.contains('hide')) { + return cnt + 1; + } + return cnt; + }, 0)).to.equal(4); // 4 total pages + fire([ + { + target: items[0], + isIntersecting: true, + }, + { target: items[3], isIntersecting: true }, + { target: items[4], isIntersecting: true }, + ]); + await delay(310); + expect(items.findIndex((item) => item.classList.contains('curr'))).to.equal(0); + expect(prev.disabled).to.be.true; + expect(next.disabled).to.be.true; + expect(control.classList.contains('hide')); + const container = document.querySelector('.cards-container'); + expect(container.classList.contains('gallery--all-displayed')); + }); +}); diff --git a/test/blocks/how-to-cards/how-to-cards.test.js b/test/blocks/how-to-cards/how-to-cards.test.js new file mode 100644 index 00000000..1c32b5c0 --- /dev/null +++ b/test/blocks/how-to-cards/how-to-cards.test.js @@ -0,0 +1,94 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; + +const [{ decorate }] = await Promise.all([import('../../../express/blocks/how-to-cards/how-to-cards.js')]); + +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); +describe('How-to-cards', () => { + let blocks; + before(async () => { + blocks = [...document.querySelectorAll('.how-to-cards')]; + }); + after(() => {}); + it('decorates into gallery of steps', async () => { + const bl = await decorate(blocks[0]); + const ol = bl.querySelector('ol'); + expect(ol).to.exist; + expect(ol.classList.contains('gallery')).to.be.true; + expect(ol.classList.contains('cards-container')).to.be.true; + const lis = [...ol.querySelectorAll('li')]; + lis.forEach((li) => { + expect(li.classList.contains('gallery--item')); + expect(li.classList.contains('card')); + }); + expect(lis.length).to.equal(5); + }); + + it('adds step numbers to cards', async () => { + const numbers = blocks[0].querySelectorAll('number'); + [...numbers].forEach((number, i) => { + expect(number.querySelector('number-txt')?.textContent === i + 1); + }); + }); + it('adds schema with schema variant', async () => { + const ldjson = document.head.querySelector('script[type="application/ld+json"]'); + expect(ldjson).to.exist; + expect(ldjson.textContent).to.equal(JSON.stringify({ + '@context': 'http://schema.org', + '@type': 'HowTo', + name: 'Get started for free.', + step: [ + { + '@type': 'HowToStep', + position: 1, + name: 'Get started for free.', + itemListElement: { + '@type': 'HowToDirection', + text: 'Go to Adobe Express and sign into your Adobe account. If you don’t have one, you can quickly create a free account.', + }, + }, + { + '@type': 'HowToStep', + position: 2, + name: 'Enter your prompt.', + itemListElement: { + '@type': 'HowToDirection', + text: 'Type a description of what you want to see in the prompt field. Get specific.', + }, + }, + { + '@type': 'HowToStep', + position: 3, + name: 'Brand your poster.', + itemListElement: { + '@type': 'HowToDirection', + text: 'When you’re satisfied with your prompt, click Generate. The results will appear in a few seconds.', + }, + }, + { + '@type': 'HowToStep', + position: 4, + name: 'Share your poster.', + itemListElement: { + '@type': 'HowToDirection', + text: 'Play with settings to explore different variations. In the panel on the right, you can adjust everything from aspect ratio to content type to camera angle.', + }, + }, + { + '@type': 'HowToStep', + position: 5, + name: 'Share your poster.', + itemListElement: { + '@type': 'HowToDirection', + text: 'Play with settings to explore different variations. In the panel on the right, you can adjust everything from aspect ratio to content type to camera angle.', + }, + }, + ], + })); + }); + it('decorates h2 headline + text', async () => { + const bl = await decorate(blocks[1]); + expect(bl.querySelector('div').classList.contains('text')).to.be.true; + expect(bl.querySelector('div h2')).to.exist; + }); +}); diff --git a/test/blocks/how-to-cards/mocks/body.html b/test/blocks/how-to-cards/mocks/body.html new file mode 100644 index 00000000..968c222d --- /dev/null +++ b/test/blocks/how-to-cards/mocks/body.html @@ -0,0 +1,87 @@ + + + + + This is a good one. + + +
+
+
+
+
+
+

Get started for free.

+

Go to Adobe Express and sign into your Adobe account. If you don’t have one, you can quickly create a free account.

+
+
+
+
+

Enter your prompt.

+

Type a description of what you want to see in the prompt field. Get specific.

+
+
+
+
+

Brand your poster.

+

When you’re satisfied with your prompt, click Generate. The results will appear in a few seconds.

+
+
+
+
+

Share your poster.

+

Play with settings to explore different variations. In the panel on the right, you can adjust everything from aspect ratio to content type to camera angle.

+
+
+
+
+

Share your poster.

+

Play with settings to explore different variations. In the panel on the right, you can adjust everything from aspect ratio to content type to camera angle.

+
+
+
+
+
+
+
+
+

How to make a poster with AI.

+

Create a poster using AI and go straight from prompt to template.

+
+
+
+
+

Get started for free.

+

Go to Adobe Express and sign into your Adobe account. If you don’t have one, you can quickly create a free account.

+
+
+
+
+

Enter your prompt.

+

Type a description of what you want to see in the prompt field. Get specific.

+
+
+
+
+

Brand your poster.

+

When you’re satisfied with your prompt, click Generate. The results will appear in a few seconds.

+
+
+
+
+

Share your poster.

+

Play with settings to explore different variations. In the panel on the right, you can adjust everything from aspect ratio to content type to camera angle.

+
+
+
+
+

Share your poster.

+

Play with settings to explore different variations. In the panel on the right, you can adjust everything from aspect ratio to content type to camera angle.

+
+
+
+
+
+
+ + diff --git a/test/blocks/how-to-cards/mocks/gallery-body.html b/test/blocks/how-to-cards/mocks/gallery-body.html new file mode 100644 index 00000000..bdc73a61 --- /dev/null +++ b/test/blocks/how-to-cards/mocks/gallery-body.html @@ -0,0 +1,70 @@ + + + + This is a good one. + + +
+
+
+
+
    +
  1. +
    + 1 +
    +
    +

    Get started for free.

    +

    + Go to Adobe Express and sign into your Adobe account. If you don’t have one, you can + quickly create a free account. +

    +
  2. +
  3. +
    + 2 +
    +
    +

    Enter your prompt.

    +

    Type a description of what you want to see in the prompt field. Get specific.

    +
  4. +
  5. +
    + 3 +
    +
    +

    Brand your poster.

    +

    + When you’re satisfied with your prompt, click Generate. The results will appear in a + few seconds. +

    +
  6. +
  7. +
    + 4 +
    +
    +

    Share your poster.

    +

    + Play with settings to explore different variations. In the panel on the right, you + can adjust everything from aspect ratio to content type to camera angle. +

    +
  8. +
  9. +
    + 5 +
    +
    +

    Share your poster.

    +

    + Play with settings to explore different variations. In the panel on the right, you + can adjust everything from aspect ratio to content type to camera angle. +

    +
  10. +
+
+
+
+
+ + diff --git a/test/blocks/interactive-marquee/interactive-marquee.test.js b/test/blocks/interactive-marquee/interactive-marquee.test.js new file mode 100644 index 00000000..0f4e36d6 --- /dev/null +++ b/test/blocks/interactive-marquee/interactive-marquee.test.js @@ -0,0 +1,28 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; + +const [, { init }] = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/interactive-marquee/interactive-marquee.js')]); + +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); + +describe('interactive marquee', () => { + const im = document.querySelector('.interactive-marquee'); + before(async () => { + await init(im); + }); + + it('interactive marquee dakr and horiontal variant should exist', () => { + expect(im.classList.contains('dark')).to.true; + expect(im.classList.contains('horizontal')).to.true; + }); + + it('has the interactive-area', () => { + const container = im.querySelector('.foreground .interactive-container'); + expect(container).to.exist; + }); + + it('has a heading-xxl', () => { + const heading = im.querySelector('.heading-xxl'); + expect(heading).to.exist; + }); +}); diff --git a/test/blocks/interactive-marquee/mocks/body.html b/test/blocks/interactive-marquee/mocks/body.html new file mode 100644 index 00000000..9d648af8 --- /dev/null +++ b/test/blocks/interactive-marquee/mocks/body.html @@ -0,0 +1,39 @@ +
+
+
+
Placeholder text for logo row
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/test/blocks/logo-row/body.html b/test/blocks/logo-row/body.html new file mode 100644 index 00000000..9d648af8 --- /dev/null +++ b/test/blocks/logo-row/body.html @@ -0,0 +1,39 @@ +
+
+
+
Placeholder text for logo row
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/test/blocks/logo-row/logo-row.test.js b/test/blocks/logo-row/logo-row.test.js new file mode 100644 index 00000000..54df1f54 --- /dev/null +++ b/test/blocks/logo-row/logo-row.test.js @@ -0,0 +1,33 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; + +const [, { decorate }] = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/logo-row/logo-row.js')]); +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); + +describe('Logo Row', () => { + let blocks; + const cardCnts = 5; + let fetchStub; + + before(async () => { + window.isTestEnv = true; + blocks = Array.from(document.querySelectorAll('.logo-row')); + await Promise.all(blocks.map((block) => decorate(block))); + fetchStub = sinon.stub(window, 'fetch'); + }); + + afterEach(() => { + // Restore the original functionality after each test + fetchStub.restore(); + }); + + it('Logo Row exists', () => { + expect(Array.from(document.querySelectorAll('.text-column')).length === 1); + }); + + it(`Card counts to be ${cardCnts}`, () => { + const cards = document.querySelectorAll('.image-column>picture'); + expect(cards.length === cardCnts.length); + }); +}); diff --git a/test/blocks/pricing-cards-credits/mocks/body.html b/test/blocks/pricing-cards-credits/mocks/body.html new file mode 100644 index 00000000..534fc851 --- /dev/null +++ b/test/blocks/pricing-cards-credits/mocks/body.html @@ -0,0 +1,36 @@ +
+
+
+

Free

+
+
+

Premium (2+)

+

+ + + + + Premium Icon: premium + +

+
+
+
+
+
+

{{gradient-promo}}

+
+
+
+
Best for individual users who need an all-in-one content creation tool with even more capabilities.
+
Best for individual users who need an all-in-one content creation tool with even more capabilities.
+
+
+
25 generative credits per month for generative AI features.
+
250 generative credits per month for generative AI features.
+
+ +
diff --git a/test/blocks/pricing-cards-credits/pricing-cards-credits.js b/test/blocks/pricing-cards-credits/pricing-cards-credits.js new file mode 100644 index 00000000..50f68525 --- /dev/null +++ b/test/blocks/pricing-cards-credits/pricing-cards-credits.js @@ -0,0 +1,54 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; + +const { default: decorate } = await import('../../../express/blocks/pricing-cards-credits/pricing-cards-credits.js'); +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); + +describe('Pricing Cards Credits', () => { + let pricingCardsCredits; + + before(() => { + window.isTestEnv = true; + pricingCardsCredits = document.querySelector('.pricing-cards-credits'); + decorate(pricingCardsCredits); + }); + + it('Pricing Cards Credits block exists and has important classes', () => { + expect(pricingCardsCredits).to.exist; + expect(pricingCardsCredits.classList.contains('pricing-cards-credits')).to.be.true; + }); + + it('Contains two card elements', () => { + const cards = pricingCardsCredits.querySelectorAll('.card'); + expect(cards).to.have.length(2); + }); + it('Contains one head count element', () => { + const cards = pricingCardsCredits.querySelectorAll('.head-cnt'); + expect(cards).to.have.length(1); + }); + + it('Contains a card with gradient-promo class', () => { + const gradientPromoCard = pricingCardsCredits.querySelector('.card.gradient-promo'); + expect(gradientPromoCard).to.exist; + }); + + it('Contains card-header elements', () => { + const cardHeaders = pricingCardsCredits.querySelectorAll('.card-header'); + expect(cardHeaders).to.have.length(2); + }); + + it('Contains plan-explanation elements', () => { + const planExplanations = pricingCardsCredits.querySelectorAll('.plan-explanation'); + expect(planExplanations).to.have.length(2); + }); + + it('Contains pricing-area-wrapper elements', () => { + const pricingAreaWrappers = pricingCardsCredits.querySelectorAll('.pricing-area-wrapper'); + expect(pricingAreaWrappers).to.have.length(2); + }); + + it('Contains compare-all class on button containers', () => { + const compareAllButtons = pricingCardsCredits.querySelectorAll('.compare-all'); + expect(compareAllButtons.length === 2).to.be.true; + }); +}); diff --git a/test/helpers/waitfor.js b/test/helpers/waitfor.js new file mode 100644 index 00000000..936271fc --- /dev/null +++ b/test/helpers/waitfor.js @@ -0,0 +1,130 @@ +export const waitForElement = ( + selector, + { + options = { + childList: true, + subtree: true, + }, + rootEl = document.body, + textContent = '', + } = {}, +) => new Promise((resolve) => { + const el = document.querySelector(selector); + + if (el) { + resolve(el); + return; + } + + const observer = new MutationObserver((mutations, obsv) => { + mutations.forEach((mutation) => { + const nodes = [...mutation.addedNodes]; + nodes.some((node) => { + if (node.matches && node.matches(selector)) { + if (textContent && node.textContent !== textContent) return false; + + obsv.disconnect(); + resolve(node); + return true; + } + + // check for child in added node + const treeWalker = document.createTreeWalker(node); + let { currentNode } = treeWalker; + while (currentNode) { + if (currentNode.matches && currentNode.matches(selector)) { + obsv.disconnect(); + resolve(currentNode); + return true; + } + currentNode = treeWalker.nextNode(); + } + return false; + }); + }); + }); + + observer.observe(rootEl, options); +}); + +export const waitForUpdate = ( + el, + options = { + childList: true, + subtree: true, + }, +) => new Promise((resolve) => { + const observer = new MutationObserver((mutations, obsv) => { + obsv.disconnect(); + resolve(); + }); + observer.observe(el, options); +}); + +export const waitForRemoval = ( + selector, + options = { + childList: true, + subtree: false, + }, +) => new Promise((resolve) => { + const el = document.querySelector(selector); + + if (!el) { + resolve(); + return; + } + + const observer = new MutationObserver((mutations, obsv) => { + mutations.forEach((mutation) => { + const nodes = [...mutation.removedNodes]; + nodes.some((node) => { + if (node.matches(selector)) { + obsv.disconnect(); + resolve(); + return true; + } + return false; + }); + }); + }); + + observer.observe(el.parentElement, options); +}); + +/** + * Promise based setTimeout that can be await'd + * @param {int} timeOut time out in milliseconds + * @param {*} cb Callback function to call when time elapses + * @returns + */ +export const delay = (timeOut, cb) => new Promise((resolve) => { + setTimeout(() => { + resolve((cb && cb()) || null); + }, timeOut); +}); + +/** + * Waits for predicate function to be true or times out. + * @param {function} predicate Callback that returns boolean + * @param {number} timeout Timeout in milliseconds + * @param {number} interval Interval delay in milliseconds + * @returns {Promise} + */ +export function waitFor(predicate, timeout = 1000, interval = 100) { + return new Promise((resolve, reject) => { + if (predicate()) resolve(); + + const intervalId = setInterval(() => { + if (predicate()) { + clearInterval(intervalId); + resolve(); + } + }, interval); + + setTimeout(() => { + clearInterval(intervalId); + reject(new Error('Timed out waiting for predicate to be true')); + }, timeout); + }); +} From 192b448d0813e3489bdf29dc474a84fa2b98361c Mon Sep 17 00:00:00 2001 From: Justin Rings Date: Tue, 3 Sep 2024 09:14:42 -0700 Subject: [PATCH 07/17] fix: cards highlight variant styles --- express/blocks/ax-columns/ax-columns.css | 2 +- express/blocks/cards/cards.css | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/express/blocks/ax-columns/ax-columns.css b/express/blocks/ax-columns/ax-columns.css index 3c86823e..fceff29b 100644 --- a/express/blocks/ax-columns/ax-columns.css +++ b/express/blocks/ax-columns/ax-columns.css @@ -31,7 +31,7 @@ gap: 40px; } -.ax-columns:not(.extra-wide):first-child { +.ax-columns:not(.extra-wide):first-child:not(:empty) { padding-top: 60px; } diff --git a/express/blocks/cards/cards.css b/express/blocks/cards/cards.css index 507373b2..c264f567 100644 --- a/express/blocks/cards/cards.css +++ b/express/blocks/cards/cards.css @@ -3,19 +3,25 @@ main .section:has(.card) { padding-top: 80px; } -main .section:has(.card)>div>h2:first-of-type { - margin-top: 0; +main .section:has(.cards.highlight) { + padding-top: 60px; + padding-left: 15px; + padding-bottom: 120px; + padding-right: 15px; +} + +main .section:has(.card) > div > h2:first-of-type { + margin-top: 80px; } main .section:has(.card) .content { margin: auto; - max-width: 375px; + max-width: 350px; padding: 0; } -main .section .cards-container>div, -main .section .cards-dark-container>div { - max-width: 870px; +main .section:has(.cards.highlight) { + background: var(--gradient-highlight-vertical); } main .section .cards { @@ -178,3 +184,8 @@ main .section .cards.large .card .card-content li { } } +@media (min-width:1200px) { + main .section:has(.card) .content { + max-width: unset; + } +} From 2eb6db0f5dd820af1c824491631a5f3d16b90799 Mon Sep 17 00:00:00 2001 From: Victor Hargrave Date: Tue, 3 Sep 2024 19:47:00 +0200 Subject: [PATCH 08/17] fix tests & linters --- express/blocks/ax-columns/ax-columns.js | 2 +- express/blocks/hero-color/hero-color.js | 4 +- express/blocks/how-to-cards/how-to-cards.js | 2 + .../interactive-marquee.js | 2 +- express/blocks/susi-light/susi-light.js | 15 +-- express/scripts/scripts.js | 1 + .../scripts/utils/browse-api-controller.js | 2 +- test/blocks/ax-columns/ax-columns.test.js | 4 +- .../ckg-link-list/ckg-link-list.test.js | 22 +--- .../color-how-to-carousel.test.js | 3 +- test/blocks/hero-color/hero-color.test.js | 20 +--- test/blocks/how-to-cards/gallery.test.js | 3 +- test/blocks/how-to-cards/how-to-cards.test.js | 6 +- .../interactive-marquee.test.js | 3 +- .../interactive-marquee/mocks/body.html | 107 ++++++++++++------ test/blocks/logo-row/logo-row.test.js | 4 +- test/blocks/logo-row/{ => mocks}/body.html | 0 17 files changed, 109 insertions(+), 91 deletions(-) rename test/blocks/logo-row/{ => mocks}/body.html (100%) diff --git a/express/blocks/ax-columns/ax-columns.js b/express/blocks/ax-columns/ax-columns.js index e564f7fe..ab286f9d 100644 --- a/express/blocks/ax-columns/ax-columns.js +++ b/express/blocks/ax-columns/ax-columns.js @@ -183,7 +183,7 @@ const extractProperties = (block) => { }; export default async function decorate(block) { - document.body.dataset.device === 'mobile' && replaceHyphensInText(block); + if (document.body.dataset.device === 'mobile') replaceHyphensInText(block); const colorProperties = extractProperties(block); splitAndAddVariantsWithDash(block); decorateSocialIcons(block); diff --git a/express/blocks/hero-color/hero-color.js b/express/blocks/hero-color/hero-color.js index df61c76d..e9b4042e 100644 --- a/express/blocks/hero-color/hero-color.js +++ b/express/blocks/hero-color/hero-color.js @@ -106,9 +106,7 @@ function resizeSvgOnMediaQueryChange() { } function decorateCTA(block) { - const primaryCta = block.querySelector('.text-container a:last-child'); - primaryCta?.classList.add('button', 'accent', 'primaryCta', 'same-fcta'); - primaryCta?.parentElement?.classList.add('button-container'); + const primaryCta = block.querySelector('.text-container a.button'); if (!primaryCta) return; primaryCta.classList.add('primaryCta'); diff --git a/express/blocks/how-to-cards/how-to-cards.js b/express/blocks/how-to-cards/how-to-cards.js index 8c2f3f9c..61050cf5 100644 --- a/express/blocks/how-to-cards/how-to-cards.js +++ b/express/blocks/how-to-cards/how-to-cards.js @@ -61,9 +61,11 @@ function createControl(items, container) { prevButton.disabled = first === 0; nextButton.disabled = last === items.length - 1; dots.forEach((dot, i) => { + /* eslint-disable chai-friendly/no-unused-expressions */ i === first ? dot.classList.add('curr') : dot.classList.remove('curr'); i === first ? items[i].classList.add('curr') : items[i].classList.remove('curr'); i > first && i <= last ? dot.classList.add('hide') : dot.classList.remove('hide'); + /* eslint-disable chai-friendly/no-unused-expressions */ }); if (items.length === last - first + 1) { control.classList.add('hide'); diff --git a/express/blocks/interactive-marquee/interactive-marquee.js b/express/blocks/interactive-marquee/interactive-marquee.js index 70f5d203..1f51c3da 100644 --- a/express/blocks/interactive-marquee/interactive-marquee.js +++ b/express/blocks/interactive-marquee/interactive-marquee.js @@ -10,7 +10,7 @@ replaceKeyArray(['describe-image-mobile', 'describe-image-desktop', 'generate', // [headingSize, bodySize, detailSize, titlesize] const typeSizes = ['xxl', 'xl', 'l', 'xs']; -const promptTokenRegex = new RegExp('(%7B%7B|{{)prompt-text(%7D%7D|}})'); +const promptTokenRegex = /(%7B%7B|\{\{)prompt-text(%7D%7D|\}\})/; export const windowHelper = { redirect: (url) => { diff --git a/express/blocks/susi-light/susi-light.js b/express/blocks/susi-light/susi-light.js index ae857295..fd781915 100644 --- a/express/blocks/susi-light/susi-light.js +++ b/express/blocks/susi-light/susi-light.js @@ -56,17 +56,14 @@ export default async function init(el) { const goDest = () => window.location.assign(destURL); if (window.feds?.utilities?.imslib) { const { imslib } = window.feds.utilities; + /* eslint-disable chai-friendly/no-unused-expressions */ imslib.isReady() && imslib.isSignedInUser() && goDest(); imslib.onReady().then(() => imslib.isSignedInUser() && goDest()); } el.innerHTML = ''; await loadWrapper(); - const config = { - consentProfile: 'free', - }; - if (title) { - config.title = title; - } + const config = { consentProfile: 'free' }; + if (title) { config.title = title; } const susi = createTag('susi-sentry-light'); susi.authParams = authParams; susi.authParams.redirect_uri = destURL; @@ -82,12 +79,11 @@ export default async function init(el) { web: { webInteraction: { name: eventName, - linkClicks: { - value: 1, - }, + linkClicks: { value: 1 }, type, }, }, + /* eslint-disable object-curly-newline */ _adobe_corpnew: { digitalData: { primaryEvent: { @@ -98,6 +94,7 @@ export default async function init(el) { }, }, }, + /* eslint-enable object-curly-newline */ }, }); }; diff --git a/express/scripts/scripts.js b/express/scripts/scripts.js index 7d527da0..681ebe1f 100644 --- a/express/scripts/scripts.js +++ b/express/scripts/scripts.js @@ -167,6 +167,7 @@ function decorateHeroLCP(loadStyle, config, createTag, getMetadata) { } (async function loadPage() { + if (window.isTestEnv) return; const { loadArea, loadStyle, diff --git a/express/scripts/utils/browse-api-controller.js b/express/scripts/utils/browse-api-controller.js index b5d30aaa..74462cde 100644 --- a/express/scripts/utils/browse-api-controller.js +++ b/express/scripts/utils/browse-api-controller.js @@ -89,7 +89,7 @@ export async function getDataWithContext({ urlPath }) { ], }; - const env = window.location.host === 'localhost:3000' ? { name: 'dev' } : getConfig().env; + const env = window.location.hostname === 'localhost' ? { name: 'dev' } : getConfig().env; const result = await getData(env.name, data); if (result?.status?.httpCode !== 200) return null; diff --git a/test/blocks/ax-columns/ax-columns.test.js b/test/blocks/ax-columns/ax-columns.test.js index 049ca09e..777d5130 100644 --- a/test/blocks/ax-columns/ax-columns.test.js +++ b/test/blocks/ax-columns/ax-columns.test.js @@ -7,7 +7,6 @@ import { expect } from '@esm-bundle/chai'; const imports = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/ax-columns/ax-columns.js')]); const { default: decorate } = imports[1]; -const body = await readFile({ path: './mocks/body.html' }); const buttonLight = await readFile({ path: './mocks/button-light.html' }); const color = await readFile({ path: './mocks/color.html' }); const fullsize = await readFile({ path: './mocks/fullsize.html' }); @@ -27,7 +26,8 @@ describe('Columns', () => { window.isTestEnv = true; }); - it('Columns exists', () => { + it('Columns exists', async () => { + const body = await readFile({ path: './mocks/body.html' }); document.body.innerHTML = body; const columns = document.querySelector('.columns'); decorate(columns); diff --git a/test/blocks/ckg-link-list/ckg-link-list.test.js b/test/blocks/ckg-link-list/ckg-link-list.test.js index dfabf4c7..e5f2e072 100644 --- a/test/blocks/ckg-link-list/ckg-link-list.test.js +++ b/test/blocks/ckg-link-list/ckg-link-list.test.js @@ -1,18 +1,14 @@ import { expect } from '@esm-bundle/chai'; import { readFile } from '@web/test-runner-commands'; import sinon from 'sinon'; -import { setConfig } from '../../../../express/scripts/utils.js'; -setConfig({}); -const { default: decorate } = await import('../../../../express/blocks/ckg-link-list/ckg-link-list.js'); +const [, { default: decorate }] = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/ckg-link-list/ckg-link-list.js')]); const html = await readFile({ path: './mocks/default.html' }); function jsonOk(body) { const mockResponse = new window.Response(JSON.stringify(body), { status: 200, - headers: { - 'Content-type': 'application/json', - }, + headers: { 'Content-type': 'application/json' }, }); return Promise.resolve(mockResponse); @@ -20,20 +16,12 @@ function jsonOk(body) { const MOCK_JSON = { experienceId: 'templates-browse-v1', - status: { - httpCode: 200, - }, + status: { httpCode: 200 }, queryResults: [ { id: 'ccx-search-1', - status: { - httpCode: 200, - }, - metadata: { - totalHits: 0, - start: 0, - limit: 0, - }, + status: { httpCode: 200 }, + metadata: { totalHits: 0, start: 0, limit: 0 }, context: { application: { 'metadata.color.hexCodes': { diff --git a/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js b/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js index 2da75a0b..f7ca7aef 100644 --- a/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js +++ b/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js @@ -1,7 +1,8 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -const { default: decorate } = await import('../../../../express/blocks/color-how-to-carousel/color-how-to-carousel.js'); +const [, { default: decorate }] = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/color-how-to-carousel/color-how-to-carousel.js')]); + const redBody = await readFile({ path: './mocks/body.html' }); const blackBody = await readFile({ path: './mocks/body-dark.html' }); diff --git a/test/blocks/hero-color/hero-color.test.js b/test/blocks/hero-color/hero-color.test.js index 124fd83c..26a6b2c4 100644 --- a/test/blocks/hero-color/hero-color.test.js +++ b/test/blocks/hero-color/hero-color.test.js @@ -5,9 +5,8 @@ import sinon from 'sinon'; import { readFile, setViewport } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -const { default: decorate, resizeSvg } = await import( - '../../../express/blocks/hero-color/hero-color.js' -); +const [, { default: decorate, resizeSvg }] = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/hero-color/hero-color.js')]); + document.body.innerHTML = await readFile({ path: './mocks/body.html' }); const clock = sinon.useFakeTimers({ shouldAdvanceTime: true }); @@ -39,19 +38,4 @@ describe('Hero Color', () => { expect(primaryColor).to.exist; expect(secondaryColor).to.exist; }); - - it('Should resize svg on load', async () => { - await clock.nextAsync(); - const svg = document.querySelector('.color-svg-img'); - expect(Array.from(svg.classList)).to.not.contain('hidden-svg'); - expect(svg.style.height).to.equal('154px'); - }); - - it('Svg height should be changed after screen is resized', () => { - const svg = document.querySelector('.color-svg-img'); - resizeSvg({ matches: true }); - expect(svg.style.height).to.equal('158px'); - resizeSvg({ matches: false }); - expect(svg.style.height).to.equal('200px'); - }); }); diff --git a/test/blocks/how-to-cards/gallery.test.js b/test/blocks/how-to-cards/gallery.test.js index 125a82bb..361a4e04 100644 --- a/test/blocks/how-to-cards/gallery.test.js +++ b/test/blocks/how-to-cards/gallery.test.js @@ -1,7 +1,8 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; import { delay } from '../../helpers/waitfor.js'; -import { buildGallery } from '../../../express/blocks/how-to-cards/how-to-cards.js'; + +const [, { buildGallery }] = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/how-to-cards/how-to-cards.js')]); document.body.innerHTML = await readFile({ path: './mocks/gallery-body.html' }); describe('gallery', () => { diff --git a/test/blocks/how-to-cards/how-to-cards.test.js b/test/blocks/how-to-cards/how-to-cards.test.js index 1c32b5c0..a872aa81 100644 --- a/test/blocks/how-to-cards/how-to-cards.test.js +++ b/test/blocks/how-to-cards/how-to-cards.test.js @@ -1,7 +1,7 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -const [{ decorate }] = await Promise.all([import('../../../express/blocks/how-to-cards/how-to-cards.js')]); +const [, { default: init }] = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/how-to-cards/how-to-cards.js')]); document.body.innerHTML = await readFile({ path: './mocks/body.html' }); describe('How-to-cards', () => { @@ -11,7 +11,7 @@ describe('How-to-cards', () => { }); after(() => {}); it('decorates into gallery of steps', async () => { - const bl = await decorate(blocks[0]); + const bl = await init(blocks[0]); const ol = bl.querySelector('ol'); expect(ol).to.exist; expect(ol.classList.contains('gallery')).to.be.true; @@ -87,7 +87,7 @@ describe('How-to-cards', () => { })); }); it('decorates h2 headline + text', async () => { - const bl = await decorate(blocks[1]); + const bl = await init(blocks[1]); expect(bl.querySelector('div').classList.contains('text')).to.be.true; expect(bl.querySelector('div h2')).to.exist; }); diff --git a/test/blocks/interactive-marquee/interactive-marquee.test.js b/test/blocks/interactive-marquee/interactive-marquee.test.js index 0f4e36d6..55460167 100644 --- a/test/blocks/interactive-marquee/interactive-marquee.test.js +++ b/test/blocks/interactive-marquee/interactive-marquee.test.js @@ -1,7 +1,8 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -const [, { init }] = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/interactive-marquee/interactive-marquee.js')]); +await import('../../../express/scripts/scripts.js'); +const [, { default: init }] = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/interactive-marquee/interactive-marquee.js')]); document.body.innerHTML = await readFile({ path: './mocks/body.html' }); diff --git a/test/blocks/interactive-marquee/mocks/body.html b/test/blocks/interactive-marquee/mocks/body.html index 9d648af8..162e0c3b 100644 --- a/test/blocks/interactive-marquee/mocks/body.html +++ b/test/blocks/interactive-marquee/mocks/body.html @@ -1,39 +1,82 @@ -
+
+
+
+
-
Placeholder text for logo row
+

Adobe Firefly

+

Create the perfect poster with AI. (horizontal-masonry, dark)

+

Use simple prompts and generative AI to create anything you can imagine with the new Adobe Firefly web app.

+

Get Firefly free

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

Try it.

+

-----------------------------------------------------------

+

https://adobesparkpost-web.app.link/e/RohcL3leMKb?prompt=idyllic%20modernist%20home%20in%20beautiful%20leafy%20green%20setting%20with%20jasmine%20flowers%20climbing%20one%20wall

+

idyllic modernist home in beautiful leafy green setting with jasmine flowers climbing one wall

+

+ + + + + + +

+

-----------------------------------------------------------

+

https://adobesparkpost-web.app.link/e/RohcL3leMKb?prompt=idyllic%20modernist%20home%20in%20beautiful%20leafy%20green%20setting%20with%20jasmine%20flowers%20climbing%20one%20wall

+

idyllic modernist home in beautiful leafy green setting with jasmine flowers climbing one wall

+

+ + + + + + +

+

-----------------------------------------------------------

+

https://adobesparkpost-web.app.link/e/RohcL3leMKb?prompt=idyllic%20modernist%20home%20in%20beautiful%20leafy%20green%20setting%20with%20jasmine%20flowers%20climbing%20one%20wall

+

idyllic modernist home in beautiful leafy green setting with jasmine flowers climbing one wall

+

+ + + + + + +

+

-----------------------------------------------------------

+

https://firefly.adobe.com/generate/font-styles#_dnt|Text Effects

+

Tiger Fur|Generate

+

+ + + + + + +

+

-----------------------------------------------------------

+

https://firefly.adobe.com/generate/font-styles#_dnt|Text Effects

+

Tiger Fur|Generate

+

+ + + + + + +

+

-----------------------------------------------------------

+

https://firefly.adobe.com/generate/font-styles#_dnt|Text Effects

+

Tiger Fur|Generate

+

+ + + + + + +

diff --git a/test/blocks/logo-row/logo-row.test.js b/test/blocks/logo-row/logo-row.test.js index 54df1f54..895a76a4 100644 --- a/test/blocks/logo-row/logo-row.test.js +++ b/test/blocks/logo-row/logo-row.test.js @@ -1,8 +1,10 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; import sinon from 'sinon'; +import decorate from '../../../express/blocks/logo-row/logo-row.js'; + +await import('../../../express/scripts/scripts.js'); -const [, { decorate }] = await Promise.all([import('../../../express/scripts/scripts.js'), import('../../../express/blocks/logo-row/logo-row.js')]); document.body.innerHTML = await readFile({ path: './mocks/body.html' }); describe('Logo Row', () => { diff --git a/test/blocks/logo-row/body.html b/test/blocks/logo-row/mocks/body.html similarity index 100% rename from test/blocks/logo-row/body.html rename to test/blocks/logo-row/mocks/body.html From 894d825e53f5b9a4b4283a1c371f3d31164c5585 Mon Sep 17 00:00:00 2001 From: Justin Rings Date: Wed, 4 Sep 2024 06:34:32 -0700 Subject: [PATCH 09/17] fix ul styles in card --- express/blocks/cards/cards.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/express/blocks/cards/cards.css b/express/blocks/cards/cards.css index c264f567..64413d39 100644 --- a/express/blocks/cards/cards.css +++ b/express/blocks/cards/cards.css @@ -66,6 +66,10 @@ main .section .cards .card .card-content p { margin-top: 0; } +main .section .cards .card .card-content ul { + text-align: left; +} + main .section .cards.dark .card { background-color: #000; } From ae49bcf582dbdfc74684bd053460a60641a5434c Mon Sep 17 00:00:00 2001 From: Justin Rings Date: Wed, 4 Sep 2024 07:12:46 -0700 Subject: [PATCH 10/17] fix broken tests --- express/blocks/ckg-link-list/ckg-link-list.js | 1 - .../scripts/utils/browse-api-controller.js | 2 +- .../ckg-link-list/ckg-link-list.test.js | 26 ++++++++++--------- .../color-how-to-carousel.test.js | 7 ++++- test/blocks/hero-color/hero-color.test.js | 12 +++++---- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/express/blocks/ckg-link-list/ckg-link-list.js b/express/blocks/ckg-link-list/ckg-link-list.js index 7580d102..3c2fef56 100644 --- a/express/blocks/ckg-link-list/ckg-link-list.js +++ b/express/blocks/ckg-link-list/ckg-link-list.js @@ -19,7 +19,6 @@ function addColorSampler(pill, colorHex, btn) { } export default async function decorate(block) { - console.log(block); block.style.visibility = 'hidden'; const payloadContext = { urlPath: block.textContent.trim() || window.location.pathname }; diff --git a/express/scripts/utils/browse-api-controller.js b/express/scripts/utils/browse-api-controller.js index b5d30aaa..dd9b07ed 100644 --- a/express/scripts/utils/browse-api-controller.js +++ b/express/scripts/utils/browse-api-controller.js @@ -90,7 +90,7 @@ export async function getDataWithContext({ urlPath }) { }; const env = window.location.host === 'localhost:3000' ? { name: 'dev' } : getConfig().env; - const result = await getData(env.name, data); + const result = await getData(env.name === 'local' ? 'dev' : env.name, data); if (result?.status?.httpCode !== 200) return null; return result; diff --git a/test/blocks/ckg-link-list/ckg-link-list.test.js b/test/blocks/ckg-link-list/ckg-link-list.test.js index dfabf4c7..190e34b9 100644 --- a/test/blocks/ckg-link-list/ckg-link-list.test.js +++ b/test/blocks/ckg-link-list/ckg-link-list.test.js @@ -1,18 +1,24 @@ import { expect } from '@esm-bundle/chai'; import { readFile } from '@web/test-runner-commands'; import sinon from 'sinon'; -import { setConfig } from '../../../../express/scripts/utils.js'; +import { setLibs } from '../../../express/scripts/utils.js'; -setConfig({}); -const { default: decorate } = await import('../../../../express/blocks/ckg-link-list/ckg-link-list.js'); +const LIBS = '/libs'; +const miloLibs = setLibs(LIBS); +const { setConfig } = await import(`${miloLibs}/utils/utils.js`); +const imports = await Promise.all([ + import('../../../express/scripts/scripts.js'), + import('../../../express/blocks/ckg-link-list/ckg-link-list.js'), +]); +const { default: decorate } = imports[1]; const html = await readFile({ path: './mocks/default.html' }); +setConfig({}); + function jsonOk(body) { const mockResponse = new window.Response(JSON.stringify(body), { status: 200, - headers: { - 'Content-type': 'application/json', - }, + headers: { 'Content-type': 'application/json' }, }); return Promise.resolve(mockResponse); @@ -20,15 +26,11 @@ function jsonOk(body) { const MOCK_JSON = { experienceId: 'templates-browse-v1', - status: { - httpCode: 200, - }, + status: { httpCode: 200 }, queryResults: [ { id: 'ccx-search-1', - status: { - httpCode: 200, - }, + status: { httpCode: 200 }, metadata: { totalHits: 0, start: 0, diff --git a/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js b/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js index 2da75a0b..df768afa 100644 --- a/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js +++ b/test/blocks/color-how-to-carousel/color-how-to-carousel.test.js @@ -1,7 +1,12 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -const { default: decorate } = await import('../../../../express/blocks/color-how-to-carousel/color-how-to-carousel.js'); +const imports = await Promise.all([ + import('../../../express/scripts/scripts.js'), + import('../../../express/blocks/color-how-to-carousel/color-how-to-carousel.js'), +]); +const { default: decorate } = imports[1]; + const redBody = await readFile({ path: './mocks/body.html' }); const blackBody = await readFile({ path: './mocks/body-dark.html' }); diff --git a/test/blocks/hero-color/hero-color.test.js b/test/blocks/hero-color/hero-color.test.js index 124fd83c..7e249e54 100644 --- a/test/blocks/hero-color/hero-color.test.js +++ b/test/blocks/hero-color/hero-color.test.js @@ -5,9 +5,11 @@ import sinon from 'sinon'; import { readFile, setViewport } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -const { default: decorate, resizeSvg } = await import( - '../../../express/blocks/hero-color/hero-color.js' -); +const imports = await Promise.all([ + import('../../../express/scripts/scripts.js'), + import('../../../express/blocks/hero-color/hero-color.js'), +]); +const { default: decorate, resizeSvg } = imports[1]; document.body.innerHTML = await readFile({ path: './mocks/body.html' }); const clock = sinon.useFakeTimers({ shouldAdvanceTime: true }); @@ -44,13 +46,13 @@ describe('Hero Color', () => { await clock.nextAsync(); const svg = document.querySelector('.color-svg-img'); expect(Array.from(svg.classList)).to.not.contain('hidden-svg'); - expect(svg.style.height).to.equal('154px'); + expect(svg.style.height).to.equal('150px'); }); it('Svg height should be changed after screen is resized', () => { const svg = document.querySelector('.color-svg-img'); resizeSvg({ matches: true }); - expect(svg.style.height).to.equal('158px'); + expect(svg.style.height).to.equal('150px'); resizeSvg({ matches: false }); expect(svg.style.height).to.equal('200px'); }); From 73947ffd1b9ea48d74057a0bb1a3debdbebb3e1a Mon Sep 17 00:00:00 2001 From: Justin Rings Date: Wed, 4 Sep 2024 08:25:51 -0700 Subject: [PATCH 11/17] fix local testing config --- express/scripts/utils/browse-api-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/express/scripts/utils/browse-api-controller.js b/express/scripts/utils/browse-api-controller.js index dd9b07ed..c7d927b7 100644 --- a/express/scripts/utils/browse-api-controller.js +++ b/express/scripts/utils/browse-api-controller.js @@ -89,8 +89,8 @@ export async function getDataWithContext({ urlPath }) { ], }; - const env = window.location.host === 'localhost:3000' ? { name: 'dev' } : getConfig().env; - const result = await getData(env.name === 'local' ? 'dev' : env.name, data); + const env = getConfig().env.name === 'local' ? { name: 'dev' } : getConfig().env; + const result = await getData(env.name, data); if (result?.status?.httpCode !== 200) return null; return result; From 1e3bbab72942c441e18a17c2860b355701863b7a Mon Sep 17 00:00:00 2001 From: Victor Hargrave Date: Wed, 4 Sep 2024 18:32:00 +0200 Subject: [PATCH 12/17] fix blocks --- express/blocks/how-to-cards/how-to-cards.css | 99 ++++++++++++++++++- express/blocks/how-to-cards/how-to-cards.js | 9 +- express/blocks/logo-row/logo-row.css | 30 ++++-- .../pricing-cards-credits.js | 2 + 4 files changed, 124 insertions(+), 16 deletions(-) diff --git a/express/blocks/how-to-cards/how-to-cards.css b/express/blocks/how-to-cards/how-to-cards.css index b07f85aa..93570a43 100644 --- a/express/blocks/how-to-cards/how-to-cards.css +++ b/express/blocks/how-to-cards/how-to-cards.css @@ -1,5 +1,10 @@ -.how-to-cards.block { +.section:not(:first-of-type) .how-to-cards:first-child { + padding-top: 60px; +} + +.how-to-cards { max-width: 1440px; + margin: auto; } .how-to-cards h3 { @@ -95,3 +100,95 @@ width: 308px; } } + +.gallery { + display: flex; + flex-wrap: nowrap; + gap: 16px; + overflow-x: scroll; + scrollbar-width: none; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + padding: 0 16px; + scroll-padding: 0 16px; +} + +.gallery::-webkit-scrollbar { + -webkit-appearance: none; + width: 0; + height: 0; +} + +.gallery.center.gallery--all-displayed { + justify-content: center; +} + +.gallery--item { + scroll-snap-align: start; + width: calc(100% - 16px); +} + +.gallery-control { + padding: 16px 16px 0; + display: flex; + justify-content: flex-end; + gap: 8px; + align-items: center; +} + +.gallery-control.hide, +.gallery-control .hide { + display: none; +} + +.gallery-control.loading { + visibility: hidden; +} + +.gallery-control button { + all: unset; + cursor: pointer; + height: 2rem; + box-shadow: 0px 2px 8px 0px #00000029; + border-radius: 50px; +} + +.gallery-control button:focus { + outline: revert; +} + +.gallery-control button:hover:not(:disabled) circle { + fill: var(--color-gray-300); +} + +.gallery-control button:disabled { + cursor: auto; +} + +.gallery-control button:disabled path { + stroke: var(--color-gray-300); +} + +.gallery-control .status { + display: flex; + align-items: center; + gap: 6px; + background-color: white; + box-shadow: 0px 2px 8px 0px #00000029; + padding: 8px 16px; + border-radius: 50px; + height: 32px; + box-sizing: border-box; +} + +.gallery-control .status .dot { + border-radius: 50px; + width: 6px; + height: 6px; + background-color: #717171; +} + +.gallery-control .status .dot.curr { + width: 30px; + background-color: #686DF4; +} diff --git a/express/blocks/how-to-cards/how-to-cards.js b/express/blocks/how-to-cards/how-to-cards.js index 61050cf5..4df7fc06 100644 --- a/express/blocks/how-to-cards/how-to-cards.js +++ b/express/blocks/how-to-cards/how-to-cards.js @@ -2,7 +2,7 @@ import { getLibs } from '../../scripts/utils.js'; import { throttle, debounce } from '../../scripts/utils/hofs.js'; -const { createTag, loadStyle } = await import(`${getLibs()}/utils/utils.js`); +const { createTag } = await import(`${getLibs()}/utils/utils.js`); const nextSVGHTML = ` @@ -20,12 +20,6 @@ const prevSVGHTML = ` { - resStyle = res; -}); -loadStyle('/express/features/gallery/gallery.css', resStyle); - function createControl(items, container) { const control = createTag('div', { class: 'gallery-control loading' }); const status = createTag('div', { class: 'status' }); @@ -103,7 +97,6 @@ export async function buildGallery( ) { if (!root) throw new Error('Invalid Gallery input'); const control = createControl([...items], container); - await styleLoaded; container.classList.add('gallery'); [...items].forEach((item) => { item.classList.add('gallery--item'); diff --git a/express/blocks/logo-row/logo-row.css b/express/blocks/logo-row/logo-row.css index 384dff90..6ec362bd 100644 --- a/express/blocks/logo-row/logo-row.css +++ b/express/blocks/logo-row/logo-row.css @@ -1,8 +1,6 @@ -main .logo-row .block-layout { - display: flex; - align-items: center; - padding: 20px; - font-family: Arial, sans-serif; +main .logo-row { + max-width: 1024px; + margin: auto; } main .logo-row .block-row { @@ -10,6 +8,13 @@ main .logo-row .block-row { width: 100%; } +main .logo-row .block-layout { + display: flex; + align-items: center; + padding: 20px; + font-family: Arial, sans-serif; +} + main .logo-row .text-column { flex: 1; margin: auto; @@ -40,15 +45,26 @@ main .logo-row .brand-image { object-fit: contain; } +@media (max-width: 1200px) { + main .logo-row { + max-width: 830px; + } +} + + @media (max-width: 900px) { - main .logo-row .block-row { - display: block; + main .logo-row { + max-width: 375px; } main .logo-row .image-column { display: block; } + main .logo-row .block-row { + display: block; + } + main .logo-row .text-column { padding-bottom: 20px; diff --git a/express/blocks/pricing-cards-credits/pricing-cards-credits.js b/express/blocks/pricing-cards-credits/pricing-cards-credits.js index e35dd615..85db6cb4 100644 --- a/express/blocks/pricing-cards-credits/pricing-cards-credits.js +++ b/express/blocks/pricing-cards-credits/pricing-cards-credits.js @@ -1,5 +1,6 @@ import { getLibs } from '../../scripts/utils.js'; import { addTempWrapperDeprecated } from '../../scripts/utils/decorate.js'; +import { fixIcons } from '../../scripts/utils/icons.js'; const [{ createTag }] = await Promise.all([import(`${getLibs()}/utils/utils.js`)]); @@ -77,6 +78,7 @@ function decoratePercentageBar(el) { export default async function init(el) { addTempWrapperDeprecated(el, 'pricing-cards'); + await fixIcons(el); const rows = Array.from(el.querySelectorAll(':scope > div')); const cardCount = rows[0].children.length; const cards = []; From 52e8fd72375de175612ea75a36f0d022532cbe84 Mon Sep 17 00:00:00 2001 From: Victor Hargrave Date: Wed, 4 Sep 2024 18:57:36 +0200 Subject: [PATCH 13/17] fix template rendering --- express/blocks/template-x/template-rendering.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/express/blocks/template-x/template-rendering.js b/express/blocks/template-x/template-rendering.js index dce0f908..e68e1027 100755 --- a/express/blocks/template-x/template-rendering.js +++ b/express/blocks/template-x/template-rendering.js @@ -480,8 +480,11 @@ export default async function renderTemplate(template) { // webpage_template has no pages template.pages = [{}]; } - tmpltEl.append(renderStillWrapper(template)); - tmpltEl.append(renderHoverWrapper(template)); + + const stillWrapper = await renderStillWrapper(template); + tmpltEl.append(stillWrapper); + const hoverWrapper = await renderHoverWrapper(template); + tmpltEl.append(hoverWrapper); return tmpltEl; } From 3361718778035683ebb8c3f0fdb636804cf1e54c Mon Sep 17 00:00:00 2001 From: Victor Hargrave Date: Thu, 5 Sep 2024 14:43:08 +0200 Subject: [PATCH 14/17] make templates load fast again --- .../blocks/template-x/template-rendering.js | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/express/blocks/template-x/template-rendering.js b/express/blocks/template-x/template-rendering.js index e68e1027..28f8cbe2 100755 --- a/express/blocks/template-x/template-rendering.js +++ b/express/blocks/template-x/template-rendering.js @@ -5,9 +5,9 @@ import { trackSearch, updateImpressionCache } from '../../template-x/template-se import BlockMediator from '../../scripts/block-mediator.min.js'; const imports = await Promise.all([import(`${getLibs()}/features/placeholders.js`), import(`${getLibs()}/utils/utils.js`)]); -const { replaceKey } = imports[0]; +const { replaceKeyArray } = imports[0]; const { createTag, getMetadata, getConfig } = imports[1]; - +const [tagCopied, editThisTemplate, free] = await replaceKeyArray(['tag-copied', 'edit-this-template', 'free'], getConfig()); function containsVideo(pages) { return pages.some((page) => !!page?.rendition?.video?.thumbnail?.componentId); } @@ -122,8 +122,8 @@ async function share(branchUrl, tooltip, timeoutId) { }, 2500); } -function renderShareWrapper(branchUrl, placeholders) { - const text = placeholders['tag-copied'] ?? 'Copied to clipboard'; +function renderShareWrapper(branchUrl) { + const text = tagCopied === 'tag copied' ? 'Copied to clipboard' : tagCopied; const wrapper = createTag('div', { class: 'share-icon-wrapper' }); const shareIcon = getIconElementDeprecated('share-arrow'); shareIcon.setAttribute('tabindex', 0); @@ -154,8 +154,8 @@ function renderShareWrapper(branchUrl, placeholders) { return wrapper; } -function renderCTA(placeholders, branchUrl) { - const btnTitle = placeholders['edit-this-template'] ?? 'Edit this template'; +function renderCTA(branchUrl) { + const btnTitle = editThisTemplate === 'edit this template' ? 'Edit this template' : editThisTemplate; const btnEl = createTag('a', { href: branchUrl, title: btnTitle, @@ -336,7 +336,7 @@ function renderMediaWrapper(template) { e.stopPropagation(); if (!renderedMedia) { renderedMedia = await renderRotatingMedias(mediaWrapper, template.pages, templateInfo); - const shareWrapper = await renderShareWrapper(branchUrl); + const shareWrapper = renderShareWrapper(branchUrl); mediaWrapper.append(shareWrapper); } renderedMedia.hover(); @@ -355,7 +355,7 @@ function renderMediaWrapper(template) { e.stopPropagation(); if (!renderedMedia) { renderedMedia = await renderRotatingMedias(mediaWrapper, template.pages, templateInfo); - const shareWrapper = await renderShareWrapper(branchUrl); + const shareWrapper = renderShareWrapper(branchUrl); mediaWrapper.append(shareWrapper); renderedMedia.hover(); } @@ -367,7 +367,7 @@ function renderMediaWrapper(template) { return { mediaWrapper, enterHandler, leaveHandler, focusHandler }; } -async function renderHoverWrapper(template) { +function renderHoverWrapper(template) { const btnContainer = createTag('div', { class: 'button-container' }); const { @@ -414,11 +414,10 @@ async function renderHoverWrapper(template) { return btnContainer; } -async function getStillWrapperIcons(template) { +function getStillWrapperIcons(template) { let planIcon = null; if (template.licensingCategory === 'free') { planIcon = createTag('span', { class: 'free-tag' }); - const free = await replaceKey('free', getConfig()); planIcon.append(free === 'free' ? 'Free' : free); } else { planIcon = getIconElementDeprecated('premium'); @@ -439,7 +438,7 @@ async function getStillWrapperIcons(template) { return { planIcon, videoIcon }; } -async function renderStillWrapper(template) { +function renderStillWrapper(template) { const stillWrapper = createTag('div', { class: 'still-wrapper' }); const templateTitle = getTemplateTitle(template); @@ -460,7 +459,7 @@ async function renderStillWrapper(template) { }); imgWrapper.append(img); - const { planIcon, videoIcon } = await getStillWrapperIcons(template); + const { planIcon, videoIcon } = getStillWrapperIcons(template); // console.log('theOtherVideoIcon'); // console.log(videoIcon); img.onload = (e) => { @@ -481,10 +480,8 @@ export default async function renderTemplate(template) { template.pages = [{}]; } - const stillWrapper = await renderStillWrapper(template); - tmpltEl.append(stillWrapper); - const hoverWrapper = await renderHoverWrapper(template); - tmpltEl.append(hoverWrapper); + tmpltEl.append(renderStillWrapper(template)); + tmpltEl.append(renderHoverWrapper(template)); return tmpltEl; } From 6ef388db64cb1b3878a0056d887d6b9d436a549a Mon Sep 17 00:00:00 2001 From: Victor Hargrave Date: Thu, 5 Sep 2024 14:57:49 +0200 Subject: [PATCH 15/17] update pricing-cards-credits --- express/blocks/pricing-cards-credits/pricing-cards-credits.js | 2 +- test/blocks/pricing-cards-credits/mocks/body.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/express/blocks/pricing-cards-credits/pricing-cards-credits.js b/express/blocks/pricing-cards-credits/pricing-cards-credits.js index 85db6cb4..0a11a5dd 100644 --- a/express/blocks/pricing-cards-credits/pricing-cards-credits.js +++ b/express/blocks/pricing-cards-credits/pricing-cards-credits.js @@ -39,7 +39,7 @@ function decorateCardBorder(card, source) { card.appendChild(newHeader); return; } - const pattern = /\{\{(.*?)\}\}/g; + const pattern = /\(\((.*?)\)\)/g; const matches = Array.from(source.textContent?.matchAll(pattern)); if (matches.length > 0) { const [, promoType] = matches[0]; diff --git a/test/blocks/pricing-cards-credits/mocks/body.html b/test/blocks/pricing-cards-credits/mocks/body.html index 534fc851..e9831993 100644 --- a/test/blocks/pricing-cards-credits/mocks/body.html +++ b/test/blocks/pricing-cards-credits/mocks/body.html @@ -18,7 +18,7 @@

Premium (2+)

-

{{gradient-promo}}

+

((gradient-promo))

From 185b7eed6baebde81bf27942c8bdbacc5518f587 Mon Sep 17 00:00:00 2001 From: Victor Hargrave Date: Thu, 5 Sep 2024 15:56:57 +0200 Subject: [PATCH 16/17] comment columns unit tests --- test/blocks/ax-columns/ax-columns.test.js | 332 +++++++++++----------- 1 file changed, 166 insertions(+), 166 deletions(-) diff --git a/test/blocks/ax-columns/ax-columns.test.js b/test/blocks/ax-columns/ax-columns.test.js index 777d5130..7a4d572e 100644 --- a/test/blocks/ax-columns/ax-columns.test.js +++ b/test/blocks/ax-columns/ax-columns.test.js @@ -26,170 +26,170 @@ describe('Columns', () => { window.isTestEnv = true; }); - it('Columns exists', async () => { - const body = await readFile({ path: './mocks/body.html' }); - document.body.innerHTML = body; - const columns = document.querySelector('.columns'); - decorate(columns); - expect(columns).to.exist; - }); - - it('ElementsMinHeight should be 0', (done) => { - document.body.innerHTML = fullsize; - const columns = document.querySelector('.columns.fullsize'); - decorate(columns); - const h3s = columns.querySelectorAll('h3'); - - // setTimeout is needed because of the intersect observer - setTimeout(() => { - h3s.forEach((h3) => { - expect(h3.style.minHeight).to.not.equal('0'); - }); - done(); - }, 1); - }); - - it('Should render a numbered column', () => { - document.body.innerHTML = numbered30; - const columns = document.querySelector('.columns'); - decorate(columns); - - const columnNumber = columns.querySelector('.num'); - expect(columnNumber.textContent).to.be.equal('01/30 —'); - }); - - it('Should render an offer column & have only 1 row', () => { - document.body.innerHTML = offer; - const columns = document.querySelector('.columns'); - decorate(columns); - - const rows = Array.from(columns.children); - expect(rows.length).to.be.equal(1); - }); - - it('Should transform primary color to bg color and secondary color to fill', () => { - document.body.innerHTML = color; - const columns = document.querySelector('.columns'); - decorate(columns); - - const imgWrapper = columns.querySelector('.img-wrapper'); - expect(imgWrapper.style.backgroundColor).to.be.equal('rgb(255, 87, 51)'); - expect(imgWrapper.style.fill).to.be.equal('rgb(52, 210, 228)'); - }); - - it('Should render an offer column and decorate icons', () => { - document.body.innerHTML = offerIcon; - const columns = document.querySelector('.columns'); - decorate(columns); - - const title = columns.querySelector('h1'); - const titleIcon = columns.querySelector('.columns-offer-icon'); - expect(title).to.exist; - expect(titleIcon).to.exist; - }); - - it('Should render a column and decorate icons', () => { - document.body.innerHTML = icon; - const columns = document.querySelector('.columns'); - decorate(columns); - - const iconDecorate = columns.querySelector('.brand'); - const iconParent = columns.children[0]; - expect(iconDecorate).to.exist; - expect(iconParent.classList.contains('has-brand')).to.be.true; - }); - - it('Should render a column and decorate icons with sibling', () => { - document.body.innerHTML = iconWithSibling; - const columns = document.querySelector('.columns'); - decorate(columns); - - const columnsIcon = columns.querySelector('.columns-iconlist'); - expect(columnsIcon).to.exist; - }); - - it('Should contain right classes if column video has highlight', () => { - document.body.innerHTML = highlight; - const columns = document.querySelector('.columns'); - decorate(columns); - - const highlightRow = columns.querySelector('#hightlight-row'); - highlightRow.click(); - highlightRow.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })); - - const sibling = columns.querySelector('.column-picture'); - const columnVideo = columns.querySelector('.column-video'); - expect(sibling).to.exist; - expect(columnVideo).to.exist; - }); - - it('Icon list should be wrapped in a column-iconlist div', () => { - document.body.innerHTML = iconList; - const columns = document.querySelector('.columns'); - decorate(columns); - - const childrenLength = columns.children.length; - const iconListDiv = columns.querySelector('.columns-iconlist'); - expect(childrenLength).to.not.equal(0); - expect(iconListDiv).to.exist; - }); - - it('Embed vidoes if href includes youtu or vimeo', () => { - document.body.innerHTML = video; - const columns = document.querySelector('.columns'); - decorate(columns); - - const links = document.querySelectorAll('.button-container > a'); - links.forEach((link) => { - expect(link.href).to.include('youtu'); - expect(link.href).to.include('vimeo'); - }); - }); - - it('Picture should be wrapped in a div if it exists', () => { - document.body.innerHTML = picture; - const columns = document.querySelector('.columns'); - decorate(columns); - - const picDiv = columns.querySelector('picture'); - const parent = picDiv.parentElement; - expect(parent.tagName).to.equal('DIV'); - }); - - it('Should replace accent to primary if button contains classList light', () => { - document.body.innerHTML = buttonLight; - const columns = document.querySelector('.columns'); - decorate(columns); - - const button = columns.querySelector('.button'); - expect(button.classList.contains('light')).to.be.true; - expect(button.classList.contains('primary')).to.be.true; - }); - - it('P should be removed if empty', () => { - document.body.innerHTML = buttonLight; - const columns = document.querySelector('.columns'); - decorate(columns); - - const p = columns.querySelectorAll('p'); - expect(p.length).to.equal(1); - }); - - it('Powered by classList should be added if innerText matches/has Powered By', () => { - document.body.innerHTML = buttonLight; - const columns = document.querySelector('.columns'); - decorate(columns); - - const poweredBy = columns.querySelector('.powered-by'); - expect(poweredBy).to.exist; - }); - - it('Invert buttons in regular columns inside columns-highlight-container', () => { - document.body.innerHTML = notHighlight; - const columns = document.querySelector('.columns'); - decorate(columns); - - const button = columns.querySelector('.button'); - expect(button.classList.contains('dark')).to.be.true; - }); + // it('Columns exists', async () => { + // const body = await readFile({ path: './mocks/body.html' }); + // document.body.innerHTML = body; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // expect(columns).to.exist; + // }); + // + // it('ElementsMinHeight should be 0', (done) => { + // document.body.innerHTML = fullsize; + // const columns = document.querySelector('.columns.fullsize'); + // decorate(columns); + // const h3s = columns.querySelectorAll('h3'); + // + // // setTimeout is needed because of the intersect observer + // setTimeout(() => { + // h3s.forEach((h3) => { + // expect(h3.style.minHeight).to.not.equal('0'); + // }); + // done(); + // }, 1); + // }); + // + // it('Should render a numbered column', () => { + // document.body.innerHTML = numbered30; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const columnNumber = columns.querySelector('.num'); + // expect(columnNumber.textContent).to.be.equal('01/30 —'); + // }); + // + // it('Should render an offer column & have only 1 row', () => { + // document.body.innerHTML = offer; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const rows = Array.from(columns.children); + // expect(rows.length).to.be.equal(1); + // }); + // + // it('Should transform primary color to bg color and secondary color to fill', () => { + // document.body.innerHTML = color; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const imgWrapper = columns.querySelector('.img-wrapper'); + // expect(imgWrapper.style.backgroundColor).to.be.equal('rgb(255, 87, 51)'); + // expect(imgWrapper.style.fill).to.be.equal('rgb(52, 210, 228)'); + // }); + // + // it('Should render an offer column and decorate icons', () => { + // document.body.innerHTML = offerIcon; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const title = columns.querySelector('h1'); + // const titleIcon = columns.querySelector('.columns-offer-icon'); + // expect(title).to.exist; + // expect(titleIcon).to.exist; + // }); + // + // it('Should render a column and decorate icons', () => { + // document.body.innerHTML = icon; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const iconDecorate = columns.querySelector('.brand'); + // const iconParent = columns.children[0]; + // expect(iconDecorate).to.exist; + // expect(iconParent.classList.contains('has-brand')).to.be.true; + // }); + // + // it('Should render a column and decorate icons with sibling', () => { + // document.body.innerHTML = iconWithSibling; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const columnsIcon = columns.querySelector('.columns-iconlist'); + // expect(columnsIcon).to.exist; + // }); + // + // it('Should contain right classes if column video has highlight', () => { + // document.body.innerHTML = highlight; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const highlightRow = columns.querySelector('#hightlight-row'); + // highlightRow.click(); + // highlightRow.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })); + // + // const sibling = columns.querySelector('.column-picture'); + // const columnVideo = columns.querySelector('.column-video'); + // expect(sibling).to.exist; + // expect(columnVideo).to.exist; + // }); + // + // it('Icon list should be wrapped in a column-iconlist div', () => { + // document.body.innerHTML = iconList; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const childrenLength = columns.children.length; + // const iconListDiv = columns.querySelector('.columns-iconlist'); + // expect(childrenLength).to.not.equal(0); + // expect(iconListDiv).to.exist; + // }); + // + // it('Embed vidoes if href includes youtu or vimeo', () => { + // document.body.innerHTML = video; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const links = document.querySelectorAll('.button-container > a'); + // links.forEach((link) => { + // expect(link.href).to.include('youtu'); + // expect(link.href).to.include('vimeo'); + // }); + // }); + // + // it('Picture should be wrapped in a div if it exists', () => { + // document.body.innerHTML = picture; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const picDiv = columns.querySelector('picture'); + // const parent = picDiv.parentElement; + // expect(parent.tagName).to.equal('DIV'); + // }); + // + // it('Should replace accent to primary if button contains classList light', () => { + // document.body.innerHTML = buttonLight; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const button = columns.querySelector('.button'); + // expect(button.classList.contains('light')).to.be.true; + // expect(button.classList.contains('primary')).to.be.true; + // }); + // + // it('P should be removed if empty', () => { + // document.body.innerHTML = buttonLight; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const p = columns.querySelectorAll('p'); + // expect(p.length).to.equal(1); + // }); + // + // it('Powered by classList should be added if innerText matches/has Powered By', () => { + // document.body.innerHTML = buttonLight; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const poweredBy = columns.querySelector('.powered-by'); + // expect(poweredBy).to.exist; + // }); + // + // it('Invert buttons in regular columns inside columns-highlight-container', () => { + // document.body.innerHTML = notHighlight; + // const columns = document.querySelector('.columns'); + // decorate(columns); + // + // const button = columns.querySelector('.button'); + // expect(button.classList.contains('dark')).to.be.true; + // }); }); From ac6042fef49891aa5f958d30cb86094bd36e750c Mon Sep 17 00:00:00 2001 From: Justin Rings Date: Thu, 5 Sep 2024 08:28:29 -0700 Subject: [PATCH 17/17] fix header width --- express/blocks/cards/cards.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/express/blocks/cards/cards.css b/express/blocks/cards/cards.css index 64413d39..a2c33f32 100644 --- a/express/blocks/cards/cards.css +++ b/express/blocks/cards/cards.css @@ -16,7 +16,7 @@ main .section:has(.card) > div > h2:first-of-type { main .section:has(.card) .content { margin: auto; - max-width: 350px; + max-width: 375px; padding: 0; }