diff --git a/.eslintrc.js b/.eslintrc.js index 3784baf64..4d66705fd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,6 +15,7 @@ module.exports = { extends: '@adobe/helix', env: { browser: true, + mocha: true, }, rules: { // allow reassigning param @@ -23,6 +24,14 @@ module.exports = { 'import/extensions': ['error', { js: 'always', }], + 'no-unused-expressions': 0, + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, }, parser: '@babel/eslint-parser', parserOptions: { diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 000000000..a6afaa573 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,4 @@ +name: "Express CodeQL Config" + +paths-ignore: + - node_modules diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index dc943e990..157188a61 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,7 @@ -Please always provide the [JIRA issue(s)](https://jira.corp.adobe.com/secure/RapidBoard.jspa?rapidView=34618) your PR is for, as well as test URLs where your change can be observed (before and after): +Describe your specific features or fixes -Fix +Resolves: [MWPW-NUMBER](https://jira.corp.adobe.com/browse/MWPW-NUMBER) Test URLs: -- Before: https://main--express-website--adobe.hlx.page/express/ -- After: https://--express-website--adobe.hlx.page/express/ +- Before: https://main--express--adobecom.hlx.page/express/ +- After: https://--express--adobecom.hlx.page/express/ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..728002338 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,66 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main", "stage" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main", "stage" ] + schedule: + - cron: '18 6 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + config-file: ./.github/codeql/codeql-config.yml + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 8f022e3e8..8c1ad5823 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -1,18 +1,40 @@ name: Tests and Linting - -on: [pull_request] - +on: + push: + branches: [ "main", "stage" ] + pull_request: + types: [opened, synchronize, reopened, edited] + branches: [ "main", "stage" ] jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [14.x] steps: - - uses: actions/checkout@v3 - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: '14' - - run: npm install - - run: npm run lint - - run: npm test - env: - CI: true + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install XVFB + run: sudo apt-get install xvfb + + - name: Install dependencies + run: npm ci + + - name: Run linters + run: npm run lint + + - name: Run the tests + run: xvfb-run -a npm test + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: coverage/lcov.info diff --git a/express/blocks/app-banner/app-banner.js b/express/blocks/app-banner/app-banner.js index 4aa4a998f..634579069 100644 --- a/express/blocks/app-banner/app-banner.js +++ b/express/blocks/app-banner/app-banner.js @@ -202,5 +202,11 @@ export default async function decorate($block) { watchFloatingButtonState($block); }, 1000); } + + const blockLinks = $block.querySelectorAll('a'); + if (blockLinks && blockLinks.length > 0) { + const linksPopulated = new CustomEvent('linkspopulated', { detail: blockLinks }); + document.dispatchEvent(linksPopulated); + } } } diff --git a/express/blocks/app-store-blade/app-store-blade.js b/express/blocks/app-store-blade/app-store-blade.js index c3a663021..1c6a90125 100644 --- a/express/blocks/app-store-blade/app-store-blade.js +++ b/express/blocks/app-store-blade/app-store-blade.js @@ -12,7 +12,11 @@ import { createOptimizedPicture, - createTag, fetchPlaceholders, getIcon, getIconElement, getMetadata, + createTag, + fetchPlaceholders, + getIcon, + getIconElement, + getMetadata, } from '../../scripts/scripts.js'; /** @@ -233,4 +237,9 @@ export default async function decorate($block) { $block.innerHTML = ''; decorateBlade($block, payload); + const blockLinks = $block.querySelectorAll('a'); + if (blockLinks && blockLinks.length > 0) { + const linksPopulated = new CustomEvent('linkspopulated', { detail: blockLinks }); + document.dispatchEvent(linksPopulated); + } } diff --git a/express/blocks/app-store-highlight/app-store-highlight.js b/express/blocks/app-store-highlight/app-store-highlight.js index 19016a570..1196fc4bd 100644 --- a/express/blocks/app-store-highlight/app-store-highlight.js +++ b/express/blocks/app-store-highlight/app-store-highlight.js @@ -273,4 +273,10 @@ export default async function decorate($block) { decorateGallery($block, payload); decorateAppStoreIcon($block, payload); initScrollAnimation($block); + + const blockLinks = $block.querySelectorAll('a'); + if (blockLinks && blockLinks.length > 0) { + const linksPopulated = new CustomEvent('linkspopulated', { detail: blockLinks }); + document.dispatchEvent(linksPopulated); + } } diff --git a/express/blocks/browse-by-category/browse-by-category.css b/express/blocks/browse-by-category/browse-by-category.css index 78aad68dd..ab0c90934 100644 --- a/express/blocks/browse-by-category/browse-by-category.css +++ b/express/blocks/browse-by-category/browse-by-category.css @@ -1,197 +1,197 @@ main .browse-by-category-wrapper.fullwidth { - margin: 0; - max-width: fit-content; - } - - main .browse-by-category { - max-width: max-content; - } - - main .browse-by-category .carousel-container .carousel-platform { - align-items: start; - } - - main .browse-by-category.card .carousel-container .carousel-platform { - gap: 14px; - margin: 6px 0 0 0; - } - - main .browse-by-category .carousel-container .button.carousel-arrow { - position: absolute; - top: 46px; - } - - main .browse-by-category .browse-by-category-heading-section { - display: flex; - justify-content: space-between; - flex-direction: column; - padding-left: 28px; - padding-right: 28px; - margin-left: auto; - margin-right: auto; - margin-bottom: 10px; - } - - main .browse-by-category .browse-by-category-heading-section .browse-by-category-heading { - text-align: left; - font-size: 28px; - line-height: 30px; - } - - main .browse-by-category .browse-by-category-heading-section .browse-by-category-link-wrapper { - margin: 8px 0 0; - } - - main .browse-by-category .browse-by-category-link { - font-size: 16px; - line-height: 22px; - display: flex; - padding: 0; - white-space: nowrap; - width: max-content; - } - - main .browse-by-category .browse-by-category-link::after { - display: flex; - width: 6px; - height: 6px; - border-top-width: 0; - border-left-width: 0; - border-bottom-width: 2px; - border-right-width: 2px; - border-style: solid; - border-color: var(--color-info-accent); - transform-origin: 75% 75%; - transform: rotate(-45deg); - content: ""; - margin-top: 5px; - margin-left: 5px; - margin-right: 2.25px; - } - - main .browse-by-category:not(.card) .browse-by-category-card { - position: relative; - display: flex; - gap: 8px; - flex-direction: column; - align-items: center; - min-width: 123px; - margin: 0; - padding: 10px 8px 0 8px; - } - - main .browse-by-category.card .browse-by-category-card { - position: relative; - display: flex; - flex-direction: column; - } - - main .browse-by-category .browse-by-category-card-link { - position: absolute; - height: 100%; - width: 100%; - } - - main .browse-by-category:not(.card) .browse-by-category-card-link:hover ~ .browse-by-category-image-wrapper img { - transform: scale(1.1) matrix(1, -0.07, 0.05, 1, 0, 0); - } - - main .browse-by-category.card .browse-by-category-card-link:hover ~ .browse-by-category-image-wrapper img { - transform: scale(1.1) matrix(1, -0.01, 0.01, 1, 0, 0); - } - - main .browse-by-category .browse-by-category-image-wrapper { - background-color: var(--color-gray-200); - min-height: 90px; - width: 148px; - border-radius: 90px; - display: flex; - justify-content: center; - align-items: center; - pointer-events: none; - } - - main .browse-by-category:not(.card) .browse-by-category-image-wrapper img { - display: block; - object-fit: cover; - width: 80px; - height: 80px; - transform: matrix(1, -0.07, 0.07, 1, 0, 0); - box-shadow: 0 0 6px #0000001f; - border-radius: 8px; - opacity: 1; - transition: transform 0.2s ease-in-out; - } - - main .browse-by-category .browse-by-category-image-wrapper .browse-by-category-image-shadow { - position: absolute; - width: 76px; - height: 76px; - transform: matrix(0.97, -0.22, 0.22, 0.97, -6, 0); - background: var(--color-gray-300) 0 0 no-repeat padding-box; - border-radius: 10px; - opacity: 1; - } - - main .browse-by-category.card .browse-by-category-image-shadow-wrapper { - display: block; - object-fit: cover; - transform: matrix(1, -0.07, 0.07, 1, 0, 0); - box-shadow: 0 0 6px #0000001f; - border-radius: 8px; - opacity: 1; - transition: transform 0.2s ease-in-out; - } - - main .browse-by-category.card .browse-by-category-image-wrapper { - background-color: #f8f8f8; - min-height: 90px; - height: 116px; - width: 182px; - border-radius: 10px; - display: flex; - justify-content: center; - align-items: center; - pointer-events: none; - } - - main .browse-by-category.card .browse-by-category-image-wrapper img { - display: block; - object-fit: contain; - max-width: 110px; - max-height: 97px; - transform: matrix(1, -0.01, 0.01, 1, 0, 0); - box-shadow: 0 0 6px #0000001f; - border-radius: 8px; - opacity: 1; - transition: transform 0.2s ease-in-out; - } - - main .browse-by-category.card .browse-by-category-image-wrapper .browse-by-category-image-shadow { - position: absolute; - width: 100%; - height: 100%; - transform: matrix(0.90, -0.18, 0.20, 0.90, -14, 0); - background: #d6d6d6e5; - } - - main .browse-by-category:not(.card) .browse-by-category-card-title { - margin: 0; - font-size: 18px; - line-height: 24px; - font-weight: 700; - max-width: 148px; - } - - main .browse-by-category.card .browse-by-category-card-title { - margin: 6px 0; - font-size: 16px; - line-height: 24px; - font-weight: 700; - align-self: start; - } - - @media (min-width: 900px) { + margin: 0; + max-width: fit-content; +} + +main .browse-by-category { + max-width: max-content; +} + +main .browse-by-category .carousel-container .carousel-platform { + align-items: start; +} + +main .browse-by-category.card .carousel-container .carousel-platform { + gap: 14px; + margin: 6px 0 0 0; +} + +main .browse-by-category .carousel-container .button.carousel-arrow { + position: absolute; + top: 46px; +} + +main .browse-by-category .browse-by-category-heading-section { + display: flex; + justify-content: space-between; + flex-direction: column; + padding-left: 28px; + padding-right: 28px; + margin-left: auto; + margin-right: auto; + margin-bottom: 10px; +} + +main .browse-by-category .browse-by-category-heading-section .browse-by-category-heading { + text-align: left; + font-size: 28px; + line-height: 30px; +} + +main .browse-by-category .browse-by-category-heading-section .browse-by-category-link-wrapper { + margin: 8px 0 0; +} + +main .browse-by-category .browse-by-category-link { + font-size: 16px; + line-height: 22px; + display: flex; + padding: 0; + white-space: nowrap; + width: max-content; +} + +main .browse-by-category .browse-by-category-link::after { + display: flex; + width: 6px; + height: 6px; + border-top-width: 0; + border-left-width: 0; + border-bottom-width: 2px; + border-right-width: 2px; + border-style: solid; + border-color: var(--color-info-accent); + transform-origin: 75% 75%; + transform: rotate(-45deg); + content: ""; + margin-top: 5px; + margin-left: 5px; + margin-right: 2.25px; +} + +main .browse-by-category:not(.card) .browse-by-category-card { + position: relative; + display: flex; + gap: 8px; + flex-direction: column; + align-items: center; + min-width: 123px; + margin: 0; + padding: 10px 8px 0 8px; +} + +main .browse-by-category.card .browse-by-category-card { + position: relative; + display: flex; + flex-direction: column; +} + +main .browse-by-category .browse-by-category-card-link { + position: absolute; + height: 100%; + width: 100%; +} + +main .browse-by-category:not(.card) .browse-by-category-card-link:hover ~ .browse-by-category-image-wrapper img { + transform: scale(1.1) matrix(1, -0.07, 0.05, 1, 0, 0); +} + +main .browse-by-category.card .browse-by-category-card-link:hover ~ .browse-by-category-image-wrapper img { + transform: scale(1.1) matrix(1, -0.01, 0.01, 1, 0, 0); +} + +main .browse-by-category .browse-by-category-image-wrapper { + background-color: var(--color-gray-200); + min-height: 90px; + width: 148px; + border-radius: 90px; + display: flex; + justify-content: center; + align-items: center; + pointer-events: none; +} + +main .browse-by-category:not(.card) .browse-by-category-image-wrapper img { + display: block; + object-fit: cover; + width: 80px; + height: 80px; + transform: matrix(1, -0.07, 0.07, 1, 0, 0); + box-shadow: 0 0 6px #0000001f; + border-radius: 8px; + opacity: 1; + transition: transform 0.2s ease-in-out; +} + +main .browse-by-category .browse-by-category-image-wrapper .browse-by-category-image-shadow { + position: absolute; + width: 76px; + height: 76px; + transform: matrix(0.97, -0.22, 0.22, 0.97, -6, 0); + background: var(--color-gray-300) 0 0 no-repeat padding-box; + border-radius: 10px; + opacity: 1; +} + +main .browse-by-category.card .browse-by-category-image-shadow-wrapper { + display: block; + object-fit: cover; + transform: matrix(1, -0.07, 0.07, 1, 0, 0); + box-shadow: 0 0 6px #0000001f; + border-radius: 8px; + opacity: 1; + transition: transform 0.2s ease-in-out; +} + +main .browse-by-category.card .browse-by-category-image-wrapper { + background-color: #f8f8f8; + min-height: 90px; + height: 116px; + width: 182px; + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + pointer-events: none; +} + +main .browse-by-category.card .browse-by-category-image-wrapper img { + display: block; + object-fit: contain; + max-width: 110px; + max-height: 97px; + transform: matrix(1, -0.01, 0.01, 1, 0, 0); + box-shadow: 0 0 6px #0000001f; + border-radius: 8px; + opacity: 1; + transition: transform 0.2s ease-in-out; +} + +main .browse-by-category.card .browse-by-category-image-wrapper .browse-by-category-image-shadow { + position: absolute; + width: 100%; + height: 100%; + transform: matrix(0.90, -0.18, 0.20, 0.90, -14, 0); + background: #d6d6d6e5; +} + +main .browse-by-category:not(.card) .browse-by-category-card-title { + margin: 0; + font-size: 18px; + line-height: 24px; + font-weight: 700; + max-width: 148px; +} + +main .browse-by-category.card .browse-by-category-card-title { + margin: 6px 0; + font-size: 16px; + line-height: 24px; + font-weight: 700; + align-self: start; +} + +@media (min-width: 900px) { main .section .browse-by-category-wrapper { padding: 0 28px; } @@ -199,9 +199,9 @@ main .browse-by-category-wrapper.fullwidth { main .section .browse-by-category-wrapper { max-width: none; } - + main .browse-by-category .browse-by-category-heading-section { - flex-direction: row; - padding: 0; - } + flex-direction: row; + padding: 0; } +} diff --git a/express/blocks/browse-by-category/browse-by-category.js b/express/blocks/browse-by-category/browse-by-category.js index 328b268ec..fc5b2c08b 100644 --- a/express/blocks/browse-by-category/browse-by-category.js +++ b/express/blocks/browse-by-category/browse-by-category.js @@ -21,10 +21,7 @@ export function decorateHeading(block, payload) { const viewAllButtonWrapper = createTag('p', { class: 'browse-by-category-link-wrapper' }); if (payload.viewAllLink.href !== '') { - const viewAllButton = createTag('a', { - class: 'browse-by-category-link', - href: payload.viewAllLink.href, - }); + const viewAllButton = createTag('a', { class: 'browse-by-category-link', href: payload.viewAllLink.href }); viewAllButton.textContent = payload.viewAllLink.text; viewAllButtonWrapper.append(viewAllButton); } @@ -40,9 +37,7 @@ export function decorateCategories(block, payload) { payload.categories.forEach((categoryCard) => { const category = createTag('div', { class: 'browse-by-category-card' }); const categoryImageWrapper = createTag('div', { class: 'browse-by-category-image-wrapper' }); - const categoryImageShadowWrapper = createTag('div', { - class: 'browse-by-category-image-shadow-wrapper', - }); + const categoryImageShadowWrapper = createTag('div', { class: 'browse-by-category-image-shadow-wrapper' }); const categoryImageShadow = createTag('div', { class: 'browse-by-category-image-shadow' }); const categoryImage = categoryCard.image; const categoryTitle = createTag('h4', { class: 'browse-by-category-card-title' }); @@ -64,13 +59,9 @@ export default async function decorate(block) { const headingDiv = rows.shift(); const payload = { - heading: headingDiv.querySelector('h4') - ? headingDiv.querySelector('h4').textContent.trim() - : '', + heading: headingDiv.querySelector('h4') ? headingDiv.querySelector('h4').textContent.trim() : '', viewAllLink: { - text: headingDiv.querySelector('a.button') - ? headingDiv.querySelector('a.button').textContent.trim() - : '', + text: headingDiv.querySelector('a.button') ? headingDiv.querySelector('a.button').textContent.trim() : '', href: headingDiv.querySelector('a.button') ? headingDiv.querySelector('a.button').href : '', }, categories: [], @@ -79,12 +70,8 @@ export default async function decorate(block) { rows.forEach((row) => { payload.categories.push({ image: row.querySelector('picture'), - text: row.querySelector('a.button') - ? row.querySelector('a.button').textContent.trim() - : 'missing category text', - link: row.querySelector('a.button') - ? row.querySelector('a.button').href - : 'missing category link', + text: row.querySelector('a.button') ? row.querySelector('a.button').textContent.trim() : 'missing category text', + link: row.querySelector('a.button') ? row.querySelector('a.button').href : 'missing category link', }); }); diff --git a/express/blocks/bubble-ui-button/bubble-ui-button.js b/express/blocks/bubble-ui-button/bubble-ui-button.js index a652f6ea0..b7ac92fc9 100644 --- a/express/blocks/bubble-ui-button/bubble-ui-button.js +++ b/express/blocks/bubble-ui-button/bubble-ui-button.js @@ -351,7 +351,7 @@ function initNotchDragAction($wrapper, data) { }, { passive: true }); $notch.addEventListener('touchmove', (e) => { - $toolBox.style.maxHeight = `${initialHeight - (e.changedTouches[0].clientY - touchStart)}px`; + $toolBox.style.maxHeight = `${(initialHeight + 24) - (e.changedTouches[0].clientY - touchStart)}px`; }, { passive: true }); $notch.addEventListener('touchend', (e) => { @@ -395,6 +395,8 @@ export async function createMultiFunctionButton($block, data, audience) { .then(((result) => result)); $buttonWrapper.classList.add('multifunction'); await buildBubblesToolBox($block, $buttonWrapper, data); + + return $buttonWrapper; } export default async function decorate($block) { @@ -405,6 +407,11 @@ export default async function decorate($block) { } const data = await collectFloatingButtonData(); - await createMultiFunctionButton($block, data, audience); + const blockWrapper = await createMultiFunctionButton($block, data, audience); + const blockLinks = blockWrapper.querySelectorAll('a'); + if (blockLinks && blockLinks.length > 0) { + const linksPopulated = new CustomEvent('linkspopulated', { detail: blockLinks }); + document.dispatchEvent(linksPopulated); + } } } diff --git a/express/blocks/carousel-card-mobile/carousel-card-mobile.js b/express/blocks/carousel-card-mobile/carousel-card-mobile.js index 9bc291610..2de392485 100644 --- a/express/blocks/carousel-card-mobile/carousel-card-mobile.js +++ b/express/blocks/carousel-card-mobile/carousel-card-mobile.js @@ -10,12 +10,8 @@ * governing permissions and limitations under the License. */ -import { - buildStaticFreePlanWidget, - createTag, - fetchRelevantRows, -} from '../../scripts/scripts.js'; - +import { createTag, fetchRelevantRows } from '../../scripts/scripts.js'; +import { buildStaticFreePlanWidget } from '../../scripts/utils/free-plan.js'; import buildPaginatedCarousel from '../shared/paginated-carousel.js'; import { buildAppStoreBadge } from '../shared/app-store-badge.js'; diff --git a/express/blocks/columns/columns.css b/express/blocks/columns/columns.css index 3b292ef64..8da54c676 100644 --- a/express/blocks/columns/columns.css +++ b/express/blocks/columns/columns.css @@ -86,19 +86,6 @@ main .columns h1.columns-heading-x-long { font-size: var(--heading-font-size-m); } -@media (min-width: 1200px) { - main .columns h1.columns-heading-long { - font-size: var(--heading-font-size-xxl); - } - main .columns h1.columns-heading-very-long { - font-size: var(--heading-font-size-xl); - } - - main .columns h1.columns-heading-x-long { - font-size: var(--heading-font-size-l); - } -} - main .columns h2 { font-size: var(--heading-font-size-xl); line-height: 1.08; @@ -108,12 +95,6 @@ main .columns h2.columns-heading-very-long { font-size: var(--heading-font-size-l); } -@media (min-width: 1200px) { - main .columns h2.columns-heading-very-long { - font-size: var(--heading-font-size-xl); - } -} - main .columns h3 { font-size: var(--heading-font-size-l); line-height: 1.11; @@ -358,16 +339,6 @@ main .columns-video .column-picture .column-video-overlay svg { height: 66px; } -@media (min-width: 900px) { - main .columns-video .column-picture { - position: initial; - } - - main .columns-video .column-picture .column-video-overlay { - width: 50%; - } -} - main .columns.highlight > div { padding: 0; } @@ -472,7 +443,257 @@ main .columns.offer .column .columns-iconlist .columns-iconlist-description p { font-size: var(--body-font-size-m); } +main .columns .column { + padding-left: 0; + padding-right: 0; +} + +main .columns.fullsize .column-picture, +main .columns.fullsize .hero-animation-overlay { + order: -1; +} + +main .columns.fullsize > div > div p { + margin: 16px 0; +} + +main .columns.fullsize > div > div .button-container { + margin: 16px 0; +} + +main .columns.fullsize > div > div .button-container a { + margin: 16px 0 0 0; +} + +main .columns.fullsize .has-brand > div p:first-child { + margin: 0; +} + +main .columns .column { + padding-left: 32px; + padding-right: 32px; +} + +main .columns.fullsize .column { + padding-left: 24px; + padding-right: 24px; +} + +main .columns .column.column-picture { + padding-left: 8px; + padding-right: 8px; +} + +body.no-brand-header main .columns.columns-marquee:not(.center):not(.fullsize) .brand.icon { + transform: none; + left: 0; + padding-left: 32px; +} + +main .columns.fullsize .hero-animation-overlay { + padding-top: 4px; + padding-bottom: 4px; +} + +main .columns.fullsize .has-brand > div:not(.column-picture):not(.hero-animation-overlay) { + padding-top: 16px; +} + +main .columns.highlight .column.column-picture { + padding-left: 0px; + padding-right: 0px; +} + +/* Custom color variant */ + +main .columns.custom-color .img-wrapper { + height: 345px; + width: 345px; +} + +main .columns.custom-color .color-svg-img { + height: 63%; + width: 46%; +} + +main .columns.custom-color .img-wrapper { + display: flex; + justify-content: center; + align-items: center; + border-radius: 20px; +} + +main .columns.columns.custom-color > div { + padding: 80px 0 0 0; + gap: 15px; +} + +main .columns.custom-color .column.text { + padding: 0; + max-width: 345px; +} + +main .columns.custom-color h2, +main .columns.custom-color p { + hyphens: manual; + text-align: center; +} + +main .columns.custom-color h2 { + font-size: 36px; +} + +main .collapsible-card-wrapper + .columns-wrapper .columns > div { + display: flex; + flex-direction: column-reverse; + font-size: 20px; + line-height: 28px; + text-align: center; +} + +/* Japanese font sizing styles */ +:lang(ja) main .columns h2.columns-heading-long { + font-size: var(--heading-font-size-l); +} + +:lang(ja) main .columns h2.columns-heading-very-long { + font-size: var(--heading-font-size-l); +} + +:lang(ja) main .columns h2.columns-heading-x-long { + font-size: var(--heading-font-size-m); +} + +@media (max-width: 600px) { + :lang(ja) main .columns h2 { + font-size: var(--heading-font-size-l); + } + + :lang(ja) main .columns h2.columns-heading-long { + font-size: var(--heading-font-size-l); + } + + :lang(ja) main .columns h2.columns-heading-very-long { + font-size: var(--heading-font-size-m); + } + + :lang(ja) main .columns h2.columns-heading-x-long { + font-size: var(--heading-font-size-m); + } +} + +@media (min-width: 900px) and (max-width: 1200px) { + :lang(ja) main .columns h1{ + font-size: var(--heading-font-size-xl); + } + + :lang(ja) main .columns h1.columns-heading-long { + font-size: var(--heading-font-size-xl); + } + + :lang(ja) main .columns h1.columns-heading-very-long { + font-size: var(--heading-font-size-l); + } + + :lang(ja) main .columns h1.columns-heading-x-long { + font-size: var(--heading-font-size-m); + } +} + @media (min-width: 900px) { + body.no-brand-header main .columns.columns-marquee:not(.center):not(.fullsize) .brand.icon { + padding-left: 0; + } + + main .columns-fullsize-center-container .columns.fullsize .has-brand > div:not(.column-picture):not(.hero-animation-overlay) { + padding-top: 0; + } + + main .columns .column { + padding-left: 32px; + padding-right: 32px; + } + + main .columns .column:first-child:not(.column-picture):not(.hero-animation-overlay):not('.text') { + padding-right: 16px; + } + + main .columns .column:nth-child(2):not(.column-picture):not(.hero-animation-overlay):not('.text') { + padding-left: 16px; + } + + main .columns .column:nth-child(2):not(.column-picture):not(.hero-animation-overlay):not('.text') { + padding-left: 16px; + } + + main .columns.highlight .column:nth-child(2):not(.column-picture):not(.hero-animation-overlay) { + padding-left: 32px; + } + + main .columns.custom-color .column { + padding-right: 15px; + min-width: 388px; + } + + main .columns.fullsize .column-picture, + main .columns.fullsize .hero-animation-overlay { + padding-top: 0; + padding-bottom: 0; + order: unset; + } + + main .columns.fullsize > div > div { + display: block; + } + + main .columns.fullsize > div > div p { + margin: 16px 0; + } + + main .columns.fullsize > div > div .button-container { + margin: 0; + margin-top: 40px; + } + + main .columns.fullsize > div > div .button-container a { + margin: 0; + } + + main .columns.fullsize .has-brand > div p:first-child { + margin: 16px 0; + } + + main .columns .column { + padding-left: 32px; + padding-right: 32px; + } + + main .columns .column.column-picture { + padding-left: 8px; + padding-right: 8px; + } + + main .columns .column:nth-child(2):not(.column-picture):not(.hero-animation-overlay) { + padding-left: 16px; + } + + main .collapsible-card-wrapper + .columns-wrapper .columns > div { + flex-direction: unset; + } + + main .columns.custom-color .img-wrapper { + height: 418px; + width: 418px; + } + + main .columns.custom-color h2 { + font-size: 45px; + } + + main .columns.custom-color h2, main .columns.custom-color p { + text-align: left; + } + body.no-brand-header main .columns.columns-marquee .brand.icon { position: unset; top: unset; @@ -490,7 +711,7 @@ main .columns.offer .column .columns-iconlist .columns-iconlist-description p { main .columns.top > div, main .columns.offer > div { - align-items: flex-start; + align-items: flex-start; } main .columns-container > div, @@ -532,8 +753,10 @@ main .columns.offer .column .columns-iconlist .columns-iconlist-description p { margin: 0; } - main .columns.width-2-columns .column-picture, - main .columns.width-2-columns .hero-animation-overlay, + main .columns.width-2-columns .column-picture { + order: 0; + } + main .columns.highlight.width-2-columns .column-picture { order: 0; } @@ -559,10 +782,27 @@ main .columns.offer .column .columns-iconlist .columns-iconlist-description p { main .columns.offer > div:first-child { padding-bottom: 0; } + + main .columns-video .column-picture { + position: initial; + } + + main .columns-video .column-picture .column-video-overlay { + width: 50%; + } } -@media (min-width: 1200px) { +main .columns .button-container.free-plan-container.stacked .free-plan-widget { + margin-top: 0; + padding: 16px 0; + background-color: transparent; +} + +main .columns.center.button-container.free-plan-container.stacked .free-plan-widget { + padding-left: 24px; +} +@media (min-width: 1200px) { main .columns-container > div, main .columns-dark-container > div, main .columns-center-container > div, @@ -575,7 +815,7 @@ main .columns.offer .column .columns-iconlist .columns-iconlist-description p { max-width: 1122px; } - main .columns:not(.fullsize) .column:not(.column-picture):not(.hero-animation-overlay) { + main .columns:not(.fullsize) .column:not(.column-picture):not(.hero-animation-overlay):not(.text) { padding: 40px; } @@ -618,170 +858,20 @@ main .columns.offer .column .columns-iconlist .columns-iconlist-description p { main .columns.offer .column:not(:first-child) { padding-top: 0; } -} - -main .columns .column { - padding-left: 0; - padding-right: 0; -} - -main .columns.fullsize .column-picture, -main .columns.fullsize .hero-animation-overlay { - order: -1; -} - -main .columns.fullsize > div > div p { - margin: 16px 0; -} - -main .columns.fullsize > div > div .button-container { - margin: 16px 0; -} - -main .columns.fullsize > div > div .button-container a { - margin: 16px 0 0 0; -} - -main .columns.fullsize .has-brand > div p:first-child { - margin: 0; -} - -main .columns .column { - padding-left: 32px; - padding-right: 32px; -} - -main .columns.fullsize .column { - padding-left: 24px; - padding-right: 24px; -} - -main .columns .column.column-picture { - padding-left: 8px; - padding-right: 8px; -} - -body.no-brand-header main .columns.columns-marquee:not(.center):not(.fullsize) .brand.icon { - transform: none; - left: 0; - padding-left: 32px; -} - -main .columns.fullsize .hero-animation-overlay { - padding-top: 4px; - padding-bottom: 4px; -} - -main .columns.fullsize .has-brand > div:not(.column-picture):not(.hero-animation-overlay) { - padding-top: 16px; -} - -main .columns.highlight .column.column-picture { - padding-left: 0px; - padding-right: 0px; -} - -@media (min-width: 900px) { - body.no-brand-header main .columns.columns-marquee:not(.center):not(.fullsize) .brand.icon { - padding-left: 0; - } - - main .columns-fullsize-center-container .columns.fullsize .has-brand > div:not(.column-picture):not(.hero-animation-overlay) { - padding-top: 0; - } - - main .columns .column { - padding-left: 32px; - padding-right: 32px; - } - main .columns .column:first-child:not(.column-picture):not(.hero-animation-overlay) { - padding-right: 16px; + main .columns.custom-color .column.text { + max-width: unset; } - main .columns .column:nth-child(2):not(.column-picture):not(.hero-animation-overlay) { - padding-left: 16px; + main .columns.custom-color .img-wrapper { + height: 500px; + width: 500px; } - main .columns .column:nth-child(2):not(.column-picture):not(.hero-animation-overlay) { - padding-left: 16px; + main .columns.columns.custom-color > div { + gap: 25px; } - main .columns.highlight .column:nth-child(2):not(.column-picture):not(.hero-animation-overlay) { - padding-left: 32px; - } - - main .columns.fullsize .column-picture, - main .columns.fullsize .hero-animation-overlay { - padding-top: 0; - padding-bottom: 0; - order: unset; - } - - main .columns.fullsize > div > div { - display: block; - } - - main .columns.fullsize > div > div p { - margin: 16px 0; - } - - main .columns.fullsize > div > div .button-container { - margin: 0; - margin-top: 40px; - } - - main .columns.fullsize > div > div .button-container a { - margin: 0; - } - - main .columns.fullsize .has-brand > div p:first-child { - margin: 16px 0; - } - - main .columns .column { - padding-left: 32px; - padding-right: 32px; - } - - main .columns .column.column-picture { - padding-left: 8px; - padding-right: 8px; - } - - main .columns .column:nth-child(2):not(.column-picture):not(.hero-animation-overlay) { - padding-left: 16px; - } -} - -main .collapsible-card-wrapper + .columns-wrapper .columns > div { - display: flex; - flex-direction: column-reverse; - font-size: 20px; - line-height: 28px; - text-align: center; -} - -@media (min-width: 900px) { - main .collapsible-card-wrapper + .columns-wrapper .columns > div { - flex-direction: unset; - } -} - -/* Japanese font sizing styles */ -:lang(ja) main .columns h2.columns-heading-long { - font-size: var(--heading-font-size-l); -} - -:lang(ja) main .columns h2.columns-heading-very-long { - font-size: var(--heading-font-size-l); -} - -:lang(ja) main .columns h2.columns-heading-x-long { - font-size: var(--heading-font-size-m); -} - -@media (min-width: 1200px) { :lang(ja) main .columns h1{ font-size: var(--heading-font-size-xxl); } @@ -809,50 +899,19 @@ main .collapsible-card-wrapper + .columns-wrapper .columns > div { :lang(ja) main .columns h2.columns-heading-x-long { font-size: var(--heading-font-size-m); } -} - -main .columns .button-container.free-plan-container.stacked .free-plan-widget { - margin-top: 0; - padding: 16px 0; - background-color: transparent; -} - -main .columns.center.button-container.free-plan-container.stacked .free-plan-widget { - padding-left: 24px; -} - -@media (max-width: 600px) { - :lang(ja) main .columns h2 { - font-size: var(--heading-font-size-l); - } - - :lang(ja) main .columns h2.columns-heading-long { - font-size: var(--heading-font-size-l); - } - - :lang(ja) main .columns h2.columns-heading-very-long { - font-size: var(--heading-font-size-m); - } - - :lang(ja) main .columns h2.columns-heading-x-long { - font-size: var(--heading-font-size-m); - } -} -@media (min-width: 900px) and (max-width: 1200px) { - :lang(ja) main .columns h1{ + main .columns h2.columns-heading-very-long { font-size: var(--heading-font-size-xl); } - :lang(ja) main .columns h1.columns-heading-long { + main .columns h1.columns-heading-long { + font-size: var(--heading-font-size-xxl); + } + main .columns h1.columns-heading-very-long { font-size: var(--heading-font-size-xl); } - :lang(ja) main .columns h1.columns-heading-very-long { + main .columns h1.columns-heading-x-long { font-size: var(--heading-font-size-l); } - - :lang(ja) main .columns h1.columns-heading-x-long { - font-size: var(--heading-font-size-m); - } } diff --git a/express/blocks/columns/columns.js b/express/blocks/columns/columns.js index 5171be415..350de0be5 100644 --- a/express/blocks/columns/columns.js +++ b/express/blocks/columns/columns.js @@ -17,9 +17,9 @@ import { addAnimationToggle, toClassName, getIconElement, - addFreePlanWidget, addHeaderSizing, } from '../../scripts/scripts.js'; +import { addFreePlanWidget } from '../../scripts/utils/free-plan.js'; import { displayVideoModal, @@ -215,7 +215,9 @@ export default function decorate($block) { }); $cell.classList.add('column'); - if ($cell.firstElementChild && $cell.firstElementChild.tagName === 'PICTURE') { + const childEls = [...$cell.children]; + const isPictureColumn = childEls.every((el) => ['BR', 'PICTURE'].includes(el.tagName)) && childEls.length > 0; + if (isPictureColumn) { $cell.classList.add('column-picture'); } @@ -260,4 +262,20 @@ export default function decorate($block) { $button.classList.add('dark'); }); } + + // variant for the colors pages + if ($block.classList.contains('custom-color')) { + const [primaryColor, accentColor] = $rows[1].querySelector('div').textContent.trim().split(','); + const [textCol, svgCol] = Array.from(($rows[0].querySelectorAll('div'))); + const svgId = svgCol.textContent.trim(); + const svg = createTag('div', { class: 'img-wrapper' }); + + svgCol.remove(); + $rows[1].remove(); + textCol.classList.add('text'); + svg.innerHTML = ` '`; + svg.style.backgroundColor = primaryColor; + svg.style.fill = accentColor; + $rows[0].append(svg); + } } diff --git a/express/blocks/commerce-cta/commerce-cta.js b/express/blocks/commerce-cta/commerce-cta.js index 6b71f6589..7fc8b79b1 100644 --- a/express/blocks/commerce-cta/commerce-cta.js +++ b/express/blocks/commerce-cta/commerce-cta.js @@ -12,9 +12,6 @@ import { createTag, - getOffer, - getCurrency, - formatPrice, addPublishDependencies, // eslint-disable-next-line import/no-unresolved } from '../../scripts/scripts.js'; @@ -23,6 +20,7 @@ import { buildPlans, // eslint-disable-next-line import/no-unresolved } from '../pricing/pricing.js'; +import { getOffer, formatPrice, getCurrency } from '../../scripts/utils/pricing.js'; async function decorateCommerceCTA($block) { const $rows = Array.from($block.children); diff --git a/express/blocks/cta-carousel/cta-carousel.css b/express/blocks/cta-carousel/cta-carousel.css new file mode 100644 index 000000000..55709e69b --- /dev/null +++ b/express/blocks/cta-carousel/cta-carousel.css @@ -0,0 +1,312 @@ +.section .cta-carousel-wrapper { + max-width: none; +} + +.cta-carousel { + padding: 0 28px; + max-width: 1440px; + margin: auto; +} + +.cta-carousel .cta-carousel-heading-section { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + margin: auto auto 32px; +} + +.cta-carousel .cta-carousel-heading-section h2 { + text-align: left; + max-width: 800px; + margin-bottom: 8px; + font-size: var(--heading-font-size-m); +} +.cta-carousel .cta-carousel-heading-section p { + text-align: left; + margin: 0; + max-width: 800px; +} + +.cta-carousel .carousel-container .carousel-platform { + align-items: unset; +} + +.cta-carousel .card { + margin: 0 8px; + position: relative; +} + +.cta-carousel .card.coming-soon { + opacity: 0.5; + pointer-events: none; +} + +.cta-carousel .card:first-of-type { + margin-left: 0; +} + +.cta-carousel .card .card-sleeve { + position: relative; + height: 140px; + width: 200px; + border-radius: 8px; + overflow: hidden; +} + +.cta-carousel.gen-ai:not(.quick-action) .card .card-sleeve { + height: 200px; + width: unset; +} + +.cta-carousel.quick-action .card.gen-ai-action .card-sleeve { + width: 511px; + display: flex; +} + +.cta-carousel.gen-ai .card .card-sleeve .gen-ai-input-form { + background-color: var(--color-info-accent-light); + border-radius: 8px; + box-sizing: border-box; + margin: auto; +} + +.cta-carousel.gen-ai .gen-ai-input-form { + width: 305px; + height: 200px; + padding: 24px; +} + +.cta-carousel.quick-action .card.gen-ai-action .card-sleeve .gen-ai-input-form { + margin-left: -24px; + width: 335px; + padding: 10px 16px 10px 40px; + height: 100%; +} + +.cta-carousel.gen-ai .card.gen-ai-action .card-sleeve .gen-ai-input-form .gen-ai-form-wrapper { + background: white; + padding: 12px; + gap: 8px; + height: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + box-sizing: border-box; + border-radius: 8px; + border: 2px solid var(--color-info-accent); +} + +.cta-carousel.gen-ai .gen-ai-input { + box-sizing: border-box; + border-radius: 8px; + border: none; + font-family: var(--body-font-family); + font-size: var(--body-font-size-m); + resize: none; + height: 100%; + width: 100%; +} + +.cta-carousel.gen-ai .gen-ai-input:active, +.cta-carousel.gen-ai .gen-ai-input:focus-visible { + border: none; + outline: none; +} + +.cta-carousel.gen-ai .card .card-sleeve .gen-ai-input-form .gen-ai-input::placeholder { + font-style: italic; + font-family: var(--body-font-family); + font-size: var(--body-font-size-m); +} + +.cta-carousel.gen-ai .card .gen-ai-input-form .gen-ai-submit { + left: 40px; + bottom: 40px; + color: var(--color-white); + background-color: var(--color-info-accent); + border-style: none; + font-family: var(--body-font-family); + font-size: var(--body-font-size-s); + font-weight: 700; + padding: 10px 1.5em 10px 1.5em; + border-radius: 22px; + cursor: pointer; + transition: background-color .2s; +} + +.cta-carousel.quick-action .card.gen-ai-action .card-sleeve .gen-ai-input-form .gen-ai-submit { + left: 56px; + bottom: 24px; +} + +.cta-carousel.gen-ai .gen-ai-input-form .gen-ai-submit:not(:disabled):hover { + background-color: var(--color-info-accent-hover); +} + +.cta-carousel.gen-ai .card .card-sleeve .gen-ai-input-form .gen-ai-submit:disabled { + background-color: var(--color-gray-200); + color: var(--color-gray-500); +} + +.cta-carousel .card .card-sleeve .media-wrapper { + position: relative; + height: 100%; + width: 200px; +} + +.cta-carousel .card .card-sleeve .links-wrapper { + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; + top: 0; + height: 100%; + width: 200px; + background-color: #00000050; + opacity: 0; + z-index: 1; + transition: opacity 0.2s; +} + +.cta-carousel .card:hover .card-sleeve .links-wrapper { + opacity: 1; +} + +.cta-carousel .card .card-sleeve .links-wrapper a { + margin: 4px auto; + width: max-content; +} + +.cta-carousel .card .card-sleeve .links-wrapper a.clickable-overlay { + padding: 0; + border-radius: 0; + border: none; + background-color: #00000020; + position: absolute; + height: 100%; + width: 100%; +} + +.cta-carousel .card .card-sleeve .links-wrapper a:nth-of-type(1):not(.clickable-overlay) { + border: solid 2px var(--color-white); + background-color: var(--color-white); + color: var(--body-color); +} + +.cta-carousel .card .card-sleeve .links-wrapper a:nth-of-type(2) { + background-color: transparent; + border: solid 2px var(--color-white); +} + +.cta-carousel .card .card-sleeve picture, +.cta-carousel .card .card-sleeve video { + position: absolute; + display: block; + height: 100%; + width: 100%; + object-fit: cover; +} + +.cta-carousel .card .card-sleeve picture img { + position: absolute; + display: block; + height: 100%; + width: 100%; + border-radius: 8px; +} + +.cta-carousel.gen-ai:not(.quick-action) .card .card-sleeve { + width: auto; +} + +.cta-carousel.gen-ai:not(.quick-action) .card .card-sleeve .media-wrapper, +.cta-carousel.gen-ai:not(.quick-action) .card .card-sleeve .links-wrapper { + width: 100%; +} + +.cta-carousel.gen-ai:not(.quick-action) .card .card-sleeve picture, +.cta-carousel.gen-ai:not(.quick-action) .card .card-sleeve picture img, +.cta-carousel.gen-ai:not(.quick-action) .card .card-sleeve video { + position: static; + width: auto; +} + +.cta-carousel .card .card-sleeve .media-wrapper img.icon { + position: absolute; + display: block; + height: 14px; + width: 14px; + padding: 8px; + overflow: visible;; + border-radius: 24px; + background-color: var(--color-white); + box-shadow: 0 0 10px #00000029; + bottom: 12px; + left: 12px; + z-index: 1; +} + +.cta-carousel.gen-ai:not(.quick-action) .cta-carousel-cards .text-wrapper { + position: absolute; + top: 8px; + left: 16px; + color: var(--color-white); + transition: opacity .2s; + opacity: 0; +} + +.cta-carousel.gen-ai:not(.quick-action) .card:not(.gen-ai-action):hover .text-wrapper { + opacity: 1; + z-index: 1; + pointer-events: none; +} + +.cta-carousel .card .text-wrapper .cta-card-text { + font-size: var(--body-font-size-s); + font-weight: 700; + text-align: left; + margin: 8px 0 0; +} + +.cta-carousel .card .text-wrapper .subtext { + font-size: var(--body-font-size-s); + text-align: left; + margin: 0; + opacity: 0; + transition: opacity 0.2s; +} + +.cta-carousel .card:hover .text-wrapper .subtext { + opacity: 1; +} + +.cta-carousel .card .text-wrapper .tag { + font-size: 12px; + padding: 0 4px; + margin-left: 6px; + font-weight: 700; + background-color: #DEDEF9; + color: var(--color-info-accent); + border-radius: 4px; +} + +.cta-carousel.create .carousel-container .carousel-fader-left a, +.cta-carousel.create .carousel-container .carousel-fader-right a, +.cta-carousel.quick-action .carousel-container .carousel-fader-left a, +.cta-carousel.quick-action .carousel-container .carousel-fader-right a { + position: absolute; + top: 64px; +} + +@media (min-width: 900px) { + .cta-carousel .cta-carousel-heading-section { + flex-direction: row; + align-items: flex-end; + } + + .cta-carousel .carousel-container .carousel-fader-left, + .cta-carousel .carousel-container .carousel-fader-right{ + background: unset; + } +} diff --git a/express/blocks/cta-carousel/cta-carousel.js b/express/blocks/cta-carousel/cta-carousel.js new file mode 100644 index 000000000..c36d466aa --- /dev/null +++ b/express/blocks/cta-carousel/cta-carousel.js @@ -0,0 +1,239 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { createTag, fetchPlaceholders, transformLinkToAnimation } from '../../scripts/scripts.js'; + +import { buildCarousel } from '../shared/carousel.js'; + +function sanitizeInput(string) { + const charMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=', + }; + + return string.replace(/[&<>"'`=/]/g, (s) => charMap[s]); +} + +function decorateTextWithTag(textSource) { + const text = createTag('p', { class: 'cta-card-text' }); + const tagText = textSource.match(/\[(.*?)]/); + + if (tagText) { + const [fullText, tagTextContent] = tagText; + const $tag = createTag('span', { class: 'tag' }); + text.textContent = textSource.replace(fullText, '').trim(); + text.dataset.text = text.textContent.toLowerCase(); + $tag.textContent = tagTextContent; + text.append($tag); + } else { + text.textContent = textSource; + text.dataset.text = text.textContent.toLowerCase(); + } + return text; +} + +export function decorateHeading(block, payload) { + const headingSection = createTag('div', { class: 'cta-carousel-heading-section' }); + const headingTextWrapper = createTag('div', { class: 'text-wrapper' }); + const heading = createTag('h2', { class: 'cta-carousel-heading' }); + + heading.textContent = payload.heading; + headingSection.append(headingTextWrapper); + headingTextWrapper.append(heading); + + if (payload.subHeadings.length > 0) { + payload.subHeadings.forEach((p) => { + headingTextWrapper.append(p); + }); + } + + if (payload.viewAllLink.href !== '') { + const viewAllButton = createTag('a', { + class: 'cta-carousel-link', + href: payload.viewAllLink.href, + }); + viewAllButton.textContent = payload.viewAllLink.text; + headingSection.append(viewAllButton); + } + + block.append(headingSection); +} + +function handleGenAISubmit(form, link) { + const btn = form.querySelector('.gen-ai-submit'); + const input = form.querySelector('.gen-ai-input'); + + btn.disabled = true; + const genAILink = link.replace('%7B%7Bprompt-text%7D%7D', sanitizeInput(input.value).replaceAll(' ', '+')); + if (genAILink !== '') window.location.assign(genAILink); +} + +function buildGenAIForm(ctaObj) { + const genAIForm = createTag('form', { class: 'gen-ai-input-form' }); + const formWrapper = createTag('div', { class: 'gen-ai-form-wrapper' }); + const genAIInput = createTag('textarea', { + class: 'gen-ai-input', + placeholder: ctaObj.subtext || '', + }); + const genAISubmit = createTag('button', { + class: 'gen-ai-submit', + type: 'submit', + disabled: true, + }); + + genAIForm.append(formWrapper); + formWrapper.append(genAIInput, genAISubmit); + + genAISubmit.textContent = ctaObj.ctaLinks[0].textContent; + genAISubmit.disabled = genAIInput.value === ''; + + genAIInput.addEventListener('keyup', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleGenAISubmit(genAIForm, ctaObj.ctaLinks[0].href); + } else { + genAISubmit.disabled = genAIInput.value === ''; + } + }, { passive: true }); + + genAIForm.addEventListener('submit', (e) => { + e.preventDefault(); + handleGenAISubmit(genAIForm, ctaObj.ctaLinks[0].href); + }); + + return genAIForm; +} + +export async function decorateCards(block, payload) { + const cards = createTag('div', { class: 'cta-carousel-cards' }); + const placeholders = await fetchPlaceholders(); + + payload.actions.forEach((cta, index) => { + const card = createTag('div', { class: 'card' }); + const cardSleeve = createTag('div', { class: 'card-sleeve' }); + const linksWrapper = createTag('div', { class: 'links-wrapper' }); + const mediaWrapper = createTag('div', { class: 'media-wrapper' }); + const textWrapper = createTag('div', { class: 'text-wrapper' }); + + cardSleeve.append(mediaWrapper, linksWrapper); + card.append(cardSleeve, textWrapper); + + if (cta.image) mediaWrapper.append(cta.image); + + if (cta.videoLink) { + const video = transformLinkToAnimation(cta.videoLink, true); + mediaWrapper.append(video); + } + + if (cta.icon) mediaWrapper.append(cta.icon); + + if (mediaWrapper.children.length === 0) { + mediaWrapper.remove(); + } + + // determine if Gen AI gets inserted after mediaWrapper has been concluded + const hasGenAIForm = (block.classList.contains('gen-ai') && block.classList.contains('quick-action') && index === 0) + || (block.classList.contains('gen-ai') && mediaWrapper.children.length === 0); + + if (cta.ctaLinks.length > 0) { + if (hasGenAIForm) { + const genAIForm = buildGenAIForm(cta); + card.classList.add('gen-ai-action'); + cardSleeve.append(genAIForm); + linksWrapper.remove(); + } + + if ((block.classList.contains('quick-action') || block.classList.contains('gen-ai')) && cta.ctaLinks.length === 1) { + cta.ctaLinks[0].textContent = ''; + cta.ctaLinks[0].classList.add('clickable-overlay'); + cta.ctaLinks[0].removeAttribute('title'); + } + + cta.ctaLinks.forEach((a) => { + if (a.href && a.href.match('adobesparkpost.app.link')) { + const btnUrl = new URL(a.href); + if (placeholders['search-branch-links']?.replace(/\s/g, '').split(',').includes(`${btnUrl.origin}${btnUrl.pathname}`)) { + btnUrl.searchParams.set('search', cta.text); + btnUrl.searchParams.set('q', cta.text); + btnUrl.searchParams.set('category', 'templates'); + btnUrl.searchParams.set('searchCategory', 'templates'); + a.href = decodeURIComponent(btnUrl.toString()); + } + a.removeAttribute('title'); + } + linksWrapper.append(a); + }); + } else { + card.classList.add('coming-soon'); + } + + if (cta.text) { + textWrapper.append(decorateTextWithTag(cta.text)); + } + + if (cta.subtext && !hasGenAIForm) { + const subtext = createTag('p', { class: 'subtext' }); + subtext.textContent = cta.subtext; + textWrapper.append(subtext); + } + + cards.append(card); + }); + + block.append(cards); +} + +function constructPayload(block) { + const rows = Array.from(block.children); + block.innerHTML = ''; + const headingDiv = rows.shift(); + + const payload = { + heading: headingDiv.querySelector('h2, h3, h4, h5, h6')?.textContent?.trim(), + subHeadings: headingDiv.querySelectorAll('p:not(.button-container)'), + viewAllLink: { + text: headingDiv.querySelector('a.button')?.textContent?.trim(), + href: headingDiv.querySelector('a.button')?.href, + }, + actions: [], + }; + + rows.forEach((row) => { + const ctaObj = { + image: row.querySelector(':scope > div:nth-of-type(1) picture'), + videoLink: row.querySelector(':scope > div:nth-of-type(1) a'), + icon: row.querySelector(':scope > div:nth-of-type(1) img.icon'), + text: row.querySelector(':scope > div:nth-of-type(2) p:not(.button-container), :scope > div:nth-of-type(2) > *:last-of-type')?.textContent.trim(), + subtext: row.querySelector(':scope > div:nth-of-type(2) p:not(.button-container) em')?.textContent.trim(), + ctaLinks: row.querySelectorAll(':scope > div:nth-of-type(2) a'), + }; + + payload.actions.push(ctaObj); + }); + + return payload; +} + +export default async function decorate(block) { + const payload = constructPayload(block); + + decorateHeading(block, payload); + await decorateCards(block, payload); + buildCarousel('', block.querySelector('.cta-carousel-cards'), false); + document.dispatchEvent(new CustomEvent('linkspopulated', { detail: block.querySelectorAll('.links-wrapper a') })); +} diff --git a/express/blocks/feature-grid-desktop/feature-grid-desktop.css b/express/blocks/feature-grid-desktop/feature-grid-desktop.css new file mode 100644 index 000000000..1dd1b105f --- /dev/null +++ b/express/blocks/feature-grid-desktop/feature-grid-desktop.css @@ -0,0 +1,500 @@ +/* This block is desktop only */ +.feature-grid-desktop.block { + position: relative; + display: none; + flex-flow: column; + align-items: center; +} + +.feature-grid-desktop.block.dark .grid-item, +.feature-grid-desktop.block.dark .grid-item a { + color: var(--color-white); +} + +.feature-grid-desktop .heading { + text-align: left; + align-self: flex-start; +} + +.feature-grid-desktop .heading p { + margin: 16px 0; +} + +.feature-grid-desktop .grid-container { + display: grid; + overflow: hidden; + height: 2344px; + margin-top: 28px; + grid-template-columns: repeat(6, 110px); + grid-template-rows: repeat(12, 374px); + grid-gap: 20px; +} + +.feature-grid-desktop .grid-container:has(+ .load-more-div) { + height: 2568px; +} + +.feature-grid-desktop .grid-item { + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-start; + container-type: size; + color: var(--body-color); + font-weight: normal; + align-items: flex-start; + border-radius: 20px; + padding: 2rem 2.5rem 2rem 1.5rem; + background-image: linear-gradient(150deg, #D9418A, #D94F4B, #E9A840, #D9418A, #D94F4B, #E9A840); + background-repeat: no-repeat; + background-attachment: fixed; + background-size: 400% 400%; + -webkit-animation: GridGradient 85s ease infinite; + -moz-animation: GridGradient 85s ease infinite; + animation: GridGradient 85s ease infinite; +} + +.feature-grid-desktop .cta { + color: var(--body-color); + margin-bottom: 24px; +} + +.feature-grid-desktop .grid-item .cta:hover { + text-decoration: underline; +} + +.feature-grid-desktop .grid-item p { + text-align: left; + margin: 16px 0; + line-height: 1.3; +} + +.feature-grid-desktop .grid-item img, +.feature-grid-desktop .grid-item video { + align-self: center; + object-fit: contain; + flex-grow: 1; + overflow: auto; +} + +.feature-grid-desktop .grid-item h2 { + margin-top: 1rem; + font-size: var(--heading-font-size-m); + text-align: left; +} + +.feature-grid-desktop .grid-item:nth-of-type(n+5) h2 { + line-height: 1.1; +} + +.feature-grid-desktop .grid-item:nth-of-type(n+5) { + padding-right: 1rem; + padding-bottom: 1rem; +} + +.feature-grid-desktop .grid-item.item-1 h2, +.feature-grid-desktop .grid-item.item-4 h2 { + font-size: var(--heading-font-size-xxl); +} + +.feature-grid-desktop .grid-item.item-2 h2, +.feature-grid-desktop .grid-item.item-3 h2 { + font-size: var(--heading-font-size-xl); +} + +.feature-grid-desktop:not(.expanded) .load-more-div { + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; + height: 200px; + width: 100vw; + align-items: center; + bottom: 0; + left: 50%; + transform: translateX(-50%); + z-index: 1; + background: linear-gradient(#ffffff00, #FCFAFF, #FCFAFF) +} + +.feature-grid-desktop.expanded .load-more-div { + margin-top: 80px; +} + +.feature-grid-desktop .load-more-button { + display: flex; + height: 40px; + padding: 16px; + color: var(--color-gray-800); + align-items: center; + cursor: pointer; + border: 2px solid #242424; + border-radius: 100px; + background-color: transparent; + font-size: var(--body-font-size-m); + font-family: var(--body-font-family); + font-weight: bold; +} + +.feature-grid-desktop .load-more-chev { + display: flex; + color: var(--color-gray-800); + width: 6px; + height: 6px; + border-top-width: 0; + border-left-width: 0; + border-bottom-width: 2px; + border-right-width: 2px; + border-style: solid; + transform-origin: 75% 75%; + transform: rotate(45deg); + margin-left: 8px; + transition: transform 0.2s; + cursor: pointer; +} + +.feature-grid-desktop.expanded .load-more-chev { + transform: rotate(-135deg); + margin: -4px 0 0 6px; +} + +@container (width > 600px) { + .feature-grid-desktop .grid-item h2, + .feature-grid-desktop .grid-item p { + max-width: 530px; + } +} + +@container (height < 400px) { + .feature-grid-desktop img, + .feature-grid-desktop video { + max-height: 164px + } +} + +@media (min-width: 900px) { + /* To be removed after wrapper deprecation */ + .section > .feature-grid-desktop-wrapper { + max-width: 760px; + } + + .feature-grid-desktop.block { + display: flex; + } + + .feature-grid-desktop.expanded .grid-container { + height: 3885px; + } + + .feature-grid-desktop.expanded .grid-container { + height: 4708px; + } + + .feature-grid-desktop h1 { + font-size: var(--heading-font-size-xl); + } + + .feature-grid-desktop .heading { + max-width: 630px; + } + + .feature-grid-desktop .grid-item:nth-of-type(n+5) { + padding-top: 1rem; + } + + .feature-grid-desktop .grid-item:nth-of-type(n+5) p { + margin: 1rem 0; + } + + .feature-grid-desktop .grid-item.item-1 { + grid-column: 1 / 7; + grid-row: 1 / 3; + } + + .feature-grid-desktop .grid-item.item-2 { + grid-column: 1 / 4; + grid-row: 3 / 5; + } + + .feature-grid-desktop .grid-item.item-3 { + grid-column: 4 / 7; + grid-row: 3 / 5; + } + + .feature-grid-desktop .grid-item.item-4 { + grid-column: 1 / 7; + grid-row: 5 / 7; + } + + .feature-grid-desktop .grid-item.item-5 { + grid-column: 1 / 4; + grid-row: 11 / 12; + } + + .feature-grid-desktop .grid-item.item-6 { + grid-column: 1 / 7; + grid-row: 7 / 9; + } + + .feature-grid-desktop .grid-item.item-6 img, + .feature-grid-desktop .grid-item.item-6 video { + max-height: 578px; + } + + .feature-grid-desktop .grid-item.item-7 { + grid-column: 3 / 7; + grid-row: 10 / 11; + } + + .feature-grid-desktop .grid-item.item-8 { + grid-column: 4 / 7; + grid-row: 11 / 12; + } + + .feature-grid-desktop .grid-item.item-9 { + grid-column: 1 / 3; + grid-row: 9 / 11; + } + + .feature-grid-desktop .grid-item.item-9 img { + height: 553px; + width: auto; + } + + .feature-grid-desktop .grid-item.item-10 { + grid-column: 1 / 4; + grid-row: 12 / 13; + } + + .feature-grid-desktop .grid-item.item-11 { + grid-column: 4 / 7; + grid-row: 12 / 13; + } + + .feature-grid-desktop .grid-item.item-12 { + grid-column: 3 / 7; + grid-row: 9 / 10; + } + + @container (width < 250px) { + .feature-grid-desktop .grid-item h2 { + margin-top: 1rem; + } + } +} + +@media (min-width: 1200px) { + /* To be removed after wrapper deprecation */ + .section > .feature-grid-desktop-wrapper { + max-width: 1020px; + } + + .feature-grid-desktop .grid-container { + grid-template-columns: repeat(8, 110px); + grid-template-rows: repeat(8, 410px); + } + + .feature-grid-desktop .grid-container { + height: 1700px + } + + .feature-grid-desktop .grid-container:has(+ .load-more-div) { + height: 1920px; + } + + .feature-grid-desktop.expanded .grid-container { + height: 3420px; + } + + .feature-grid-desktop .grid-item.item-1 { + grid-column: 1 / 6; + grid-row: 1 / 3; + } + + .feature-grid-desktop .grid-item.item-1 img, + .feature-grid-desktop .grid-item.item-1 video { + max-height: 100%; + } + + .feature-grid-desktop .grid-item.item-2 { + grid-column: 6 / 9; + grid-row: 1 / 3; + } + + .feature-grid-desktop .grid-item.item-3 { + grid-column: 1 / 4; + grid-row: 3 / 5; + } + + .feature-grid-desktop .grid-item.item-4 { + grid-column: 4 / 9; + grid-row: 3 / 5; + } + + .feature-grid-desktop .grid-item.item-5 { + grid-column: 5 / 7; + grid-row: 8 / 9; + } + + .feature-grid-desktop .grid-item.item-5 h2, + .feature-grid-desktop .grid-item.item-5 p { + max-width: 260px; + } + + .feature-grid-desktop .grid-item.item-6 { + grid-column: 1 / 7; + grid-row: 5 / 7; + } + + .feature-grid-desktop .grid-item.item-6 img, + .feature-grid-desktop .grid-item.item-6 video { + max-height: 548px; + max-width: 100%; + margin-bottom: 4rem; + } + + .feature-grid-desktop .grid-item.item-6 p, + .feature-grid-desktop .grid-item.item-6 h2 { + max-width: 260px; + } + + .feature-grid-desktop .grid-item.item-7 { + grid-column: 1 / 5; + grid-row: 8 / 9; + } + + .feature-grid-desktop .grid-item.item-8 { + grid-column: 7 / 9; + grid-row: 8 / 9; + } + + .feature-grid-desktop .grid-item.item-9 { + grid-column: 7 / 9; + grid-row: 5 / 7; + } + + .feature-grid-desktop .grid-item.item-10 { + grid-column: 1 / 3; + grid-row: 7 / 8; + } + + .feature-grid-desktop .grid-item.item-11 { + grid-column: 3 / 5; + grid-row: 7 / 8; + } + + .feature-grid-desktop .grid-item.item-12 { + grid-column: 5 / 9; + grid-row: 7 / 8; + } +} + +@media (min-width: 1440px) { + /* To be removed after wrapper deprecation */ + .section > .feature-grid-desktop-wrapper { + max-width: 1255px; + } + + .feature-grid-desktop .grid-container { + grid-template-columns: repeat(17, 55px); + grid-template-rows: repeat(7, 400px); + } + + .feature-grid-desktop .grid-container { + height: 1740px + } + + .feature-grid-desktop .grid-container:has(+ .load-more-div) { + height: 2028px; + } + + .feature-grid-desktop.expanded .grid-container { + height: 2920px; + } + + .feature-grid-desktop .grid-item:nth-of-type(-n+4) { + padding-left: 48px; + padding-right: 48px; + line-height: 1; + } + + .feature-grid-desktop .grid-item.item-1 { + grid-column: 1 / 11; + grid-row: 1 / 3; + } + + .feature-grid-desktop .grid-item.item-2 { + grid-column: 11 / 18; + grid-row: 1 / 3; + } + + .feature-grid-desktop .grid-item.item-3 { + grid-column: 1 / 8; + grid-row: 3 / 5; + } + + .feature-grid-desktop .grid-item.item-4 { + grid-column: 8 / 18; + grid-row: 3 / 5; + } + + .feature-grid-desktop .grid-item.item-5 { + grid-column: 1 / 5; + grid-row: 5 / 6; + } + + .feature-grid-desktop .grid-item.item-6 { + grid-column: 5 / 14; + grid-row: 5 / 7; + } + + .feature-grid-desktop .grid-item.item-6 { + grid-column: 5 / 14; + grid-row: 5 / 7; + } + + .feature-grid-desktop .grid-item.item-7 { + grid-column: 14 / 18; + grid-row: 5 / 6; + } + + .feature-grid-desktop .grid-item.item-8 { + grid-column: 1 / 5; + grid-row: 6 / 7; + } + + .feature-grid-desktop .grid-item.item-9 { + grid-column: 14 / 18; + grid-row: 6 / 8; + } + + .feature-grid-desktop .grid-item.item-10 { + grid-column: 1 / 5; + grid-row: 7 / 8; + } + .feature-grid-desktop .grid-item.item-11 { + grid-column: 5 / 9; + grid-row: 7 / 8; + } + + .feature-grid-desktop .grid-item.item-12 { + grid-column: 9 / 14; + grid-row: 7 / 8; + } +} + +@-webkit-keyframes GridGradient { + 0%{background-position:0% 50%} + 50%{background-position:100% 50%} + 100%{background-position:0% 50%} +} +@-moz-keyframes GridGradient { + 0%{background-position:0% 50%} + 50%{background-position:100% 50%} + 100%{background-position:0% 50%} +} +@keyframes GridGradient { + 0%{background-position:0% 50%} + 50%{background-position:100% 50%} + 100%{background-position:0% 50%} +} diff --git a/express/blocks/feature-grid-desktop/feature-grid-desktop.js b/express/blocks/feature-grid-desktop/feature-grid-desktop.js new file mode 100644 index 000000000..4d9515a2e --- /dev/null +++ b/express/blocks/feature-grid-desktop/feature-grid-desktop.js @@ -0,0 +1,135 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { createTag } from '../../scripts/scripts.js'; +import { isVideoLink } from '../shared/video.js'; + +function renderImageOrVideo(media) { + let updatedMedia; + if (media.tagName.toUpperCase() === 'PICTURE') { + updatedMedia = media.querySelector('img'); + } else if (!media?.href) { + return null; + } else if (isVideoLink(media?.href)) { + const attributes = { class: 'hero-animation-background' }; + ['playsinline', 'autoplay', 'loop', 'muted'].forEach((p) => { + attributes[p] = ''; + }); + updatedMedia = createTag('video', attributes); + updatedMedia.src = media.href; + } + return updatedMedia; +} + +function renderGridNode({ + media, + title, + subText, + cta, +}, index) { + const gridItem = createTag('a', { class: `grid-item item-${index + 1}` }); + const updatedMedia = renderImageOrVideo(media); + gridItem.href = cta?.href; + + if (title) gridItem.append(title); + if (subText) gridItem.append(subText); + if (cta) { + cta.classList.add('cta'); + cta.classList.remove('button'); + gridItem.append(cta); + } + + if (index < 4) { + gridItem.append(updatedMedia); + } else { + gridItem.prepend(updatedMedia); + } + return gridItem; +} + +const decorateLoadMoreSection = (block, loadMoreInfo) => { + const loadMoreWrapper = createTag('div', { class: 'load-more-div' }); + const loadMoreButton = createTag('button', { class: 'load-more-button' }); + const loadMoreText = createTag('span', { class: 'load-more-text' }); + const toggleChev = createTag('div', { class: 'load-more-chev' }); + + if (loadMoreInfo.color) loadMoreWrapper.style.background = loadMoreInfo.color; + [loadMoreText.textContent] = loadMoreInfo.text; + loadMoreButton.append(loadMoreText, toggleChev); + loadMoreWrapper.append(loadMoreButton); + block.append(loadMoreWrapper); + + loadMoreButton.addEventListener('click', () => { + block.classList.toggle('expanded'); + if (block.classList.contains('expanded') && loadMoreInfo.color) { + [, loadMoreText.textContent] = loadMoreInfo.text; + loadMoreWrapper.style.background = 'none'; + } else if (loadMoreInfo.color) { + [loadMoreText.textContent] = loadMoreInfo.text; + loadMoreWrapper.style.background = loadMoreInfo.color; + } + }); +}; + +const getGradient = (rows) => { + const gradientText = rows.pop().textContent.split('|').map((item) => item.trim()); + // eslint-disable-next-line no-useless-escape + const regex = /linear-gradient\(([^\)]+)\)/; + const gradientColorRow = rows.findIndex((row) => row.textContent.match(regex)); + const loadMore = { text: gradientText }; + + if (gradientColorRow !== -1) { + loadMore.color = rows[gradientColorRow].textContent; + rows.splice(gradientColorRow, 1); + } + return loadMore; +}; + +export default function decorate(block) { + const inputRows = block.querySelectorAll(':scope > div > div'); + block.innerHTML = ''; + const rows = Array.from(inputRows); + const heading = rows.shift(); + const loadMoreSection = rows.length > 4 ? getGradient(rows) : ''; + const gridProps = rows.map((row) => { + const subText = row.querySelector('p'); + const media = row.querySelector('p:last-of-type > a, p:last-of-type > picture'); + const title = row.querySelector('h2'); + const cta = row.querySelector('a'); + return { + media, + title, + subText, + cta, + }; + }); + + if (gridProps.length > 12) { + throw new Error( + `Authoring issue: Feature Grid Fixed block should have 12 children. Received: ${gridProps.length}`, + ); + } + + const gridContainer = createTag('div', { class: 'grid-container' }); + const gridItems = gridProps.map((props, index) => renderGridNode(props, index)); + heading.classList.add('heading'); + + gridItems.forEach((gridItem) => { + gridContainer.append(gridItem); + }); + + block.append(heading, gridContainer); + + if (gridProps.length > 4) { + decorateLoadMoreSection(block, loadMoreSection); + } +} diff --git a/express/blocks/floating-button/floating-button.js b/express/blocks/floating-button/floating-button.js index 85108ff8a..5c1b9ab56 100644 --- a/express/blocks/floating-button/floating-button.js +++ b/express/blocks/floating-button/floating-button.js @@ -25,11 +25,17 @@ export default async function decorate($block) { const $parentSection = $block.closest('.section'); const data = await collectFloatingButtonData($block); - await createFloatingButton( + const blockWrapper = await createFloatingButton( $block, $parentSection ? audience : null, data, ); + + const blockLinks = blockWrapper.querySelectorAll('a'); + if (blockLinks && blockLinks.length > 0) { + const linksPopulated = new CustomEvent('linkspopulated', { detail: blockLinks }); + document.dispatchEvent(linksPopulated); + } } else { $block.parentElement.remove(); } diff --git a/express/blocks/fragment/fragment.js b/express/blocks/fragment/fragment.js index 82b170b86..a56c04f4e 100644 --- a/express/blocks/fragment/fragment.js +++ b/express/blocks/fragment/fragment.js @@ -50,8 +50,13 @@ export default async function decorate(block) { const fragmentSection = fragment.querySelector(':scope .section'); if (fragmentSection) { - if (fragmentSection.dataset.audience) { - block.closest('.section').dataset.audience = fragmentSection.dataset.audience; + const audience = fragmentSection.dataset?.audience; + if (audience) { + if (audience !== document.body.dataset?.device) { + block.closest('.section').remove(); + return; + } + block.closest('.section').dataset.audience = audience; } block.closest('.section').classList.add(...fragmentSection.classList); diff --git a/express/blocks/fullscreen-marquee-desktop/fullscreen-marquee-desktop.css b/express/blocks/fullscreen-marquee-desktop/fullscreen-marquee-desktop.css new file mode 100755 index 000000000..7f6aceaaf --- /dev/null +++ b/express/blocks/fullscreen-marquee-desktop/fullscreen-marquee-desktop.css @@ -0,0 +1,236 @@ +main .fullscreen-marquee-desktop-container { + min-height: 460px; + box-sizing: border-box; + padding: 0 0 160px 0; + position: relative; + height: 100%; +} + +main .section > div.fullscreen-marquee-desktop-wrapper { + max-width: none; +} + +main .fullscreen-marquee-desktop { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; + padding-bottom: 40px; + padding-top: 80px; +} + +main .fullscreen-marquee-desktop.has-background { + padding-bottom: 160px; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-heading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + max-width: 1160px; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-heading h1 { + font-size: 80px; + line-height: 90px; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-heading p { + justify-content: center; + max-width: 680px; + margin: 10px 0; + font-size: 18px; + line-height: 30px; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-heading .button-container { + display: flex; + align-items: center; +} + +main .fullscreen-marquee-desktop-heading h1.budoux { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +main .fullscreen-marquee-desktop-heading h1 em { + font-style: normal; + background: linear-gradient(320deg, #7C84F3, #FF4DD2, #FF993B, #FF4DD2, #7C84F3, #FF4DD2, #FF993B); + background-size: 400% 400%; + -webkit-animation: buttonGradient 45s ease infinite; + -moz-animation: buttonGradient 45s ease infinite; + animation: buttonGradient 45s ease infinite; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-background { + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 50%; + z-index: -5; + opacity: 0; + transition: .5s opacity ease; + transform: translateX(-50%); +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-background img { + max-width: none; + height: 100%; + width: 100%; + object-fit: cover; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-frame { + margin-top: 40px; + position: relative; + width: 950px; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-background { + position: absolute; + top: -15px; + left: -15px; + width: calc(100% + 30px); + height: calc(100% + 30px); + z-index: -5; + background-color: #f3f3f3; + border-radius: 42px; + transform-style: preserve-3d; + transition: transform 1s cubic-bezier(0.22, 0.78, 0.24, 0.98); +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app { + transform-style: preserve-3d; + transition: transform 1s cubic-bezier(0.22, 0.78, 0.24, 0.98); +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-image img { + width: 950px; + border-radius: 24px; +} + +main .fullscreen-marquee-desktop-app-frame-highlight { + position: absolute; + top: -2px; + left: -2px; + width: calc(100% + 4px); + height: calc(100% + 4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + opacity: 0; + cursor: pointer; + border-radius: 24px; + transition: opacity .1s ease-in-out; +} + +main .fullscreen-marquee-desktop-app-frame-highlight:hover { + background-color: rgba(255, 255, 255, 0.3); + opacity: 1; + pointer-events: initial; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-content-container { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 92px; + left: 320px; + width: 582px; + height: 440px; +} + +main .fullscreen-marquee-desktop.video .fullscreen-marquee-desktop-app-content-container { + height: 334px; + border-radius: 20px; + overflow: hidden; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-content { + display: inline-flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-frame .fullscreen-marquee-desktop-app { + display: block; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-frame .fullscreen-marquee-desktop-app img { + vertical-align: middle; + max-height: 100%; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-editor { + position: absolute; + top: 58px; + left: 66px; + width: 196px; + transition: transform 0.5s ease-out; + transform: translate3d(0px, 0px, 15px); +} + +main .fullscreen-marquee-desktop.image .fullscreen-marquee-desktop-app-editor { + top: 46px; + left: 52px; + width: 220px; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-editor img { + border-radius: initial; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-thumbnail-container { + position: absolute; + top: 66px; + left: 80px; + width: 40px; + height: 40px; + overflow: hidden; + border-radius: 5px; + transform: translate3d(0px, 0px, 15px); + z-index: 10; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-thumbnail-container video { + height: 100%; + width: 100%; + object-fit: cover; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-frames-container { + display: flex; + position: absolute; + width: 274px; + top: 470px; + left: 342px; + height: 41px; + overflow: hidden; + border-radius: 5px; +} + +main .fullscreen-marquee-desktop .fullscreen-marquee-desktop-app-frames-container video { + width: 10%; + height: 100%; + opacity: 0; + object-fit: cover; + transition: .5s opacity ease-in; +} + +@media (max-width: 1200px) { + main .fullscreen-marquee-desktop { + display: none; + } +} diff --git a/express/blocks/fullscreen-marquee-desktop/fullscreen-marquee-desktop.js b/express/blocks/fullscreen-marquee-desktop/fullscreen-marquee-desktop.js new file mode 100755 index 000000000..f12f68f27 --- /dev/null +++ b/express/blocks/fullscreen-marquee-desktop/fullscreen-marquee-desktop.js @@ -0,0 +1,177 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { + createOptimizedPicture, + createTag, + fetchPlaceholders, + transformLinkToAnimation, +} from '../../scripts/scripts.js'; +import { addFreePlanWidget } from '../../scripts/utils/free-plan.js'; + +function buildContent(content) { + const contentLink = content.querySelector('a'); + let formattedContent = content; + + if (contentLink && contentLink.href.endsWith('.mp4')) { + const video = new URL(contentLink.textContent.trim()); + const looping = ['true', 'yes', 'on'].includes(video.searchParams.get('loop')); + formattedContent = transformLinkToAnimation(contentLink, looping); + } else { + const contentImage = content.querySelector('picture'); + + if (contentImage) { + formattedContent = contentImage; + } + } + + return formattedContent; +} + +function buildHeading(block, heading) { + heading.classList.add('fullscreen-marquee-desktop-heading'); + return heading; +} + +function buildBackground(block, background) { + background.classList.add('fullscreen-marquee-desktop-background'); + + window.addEventListener('scroll', () => { + const progress = (window.scrollY * 100) / block.offsetHeight; + let opacityValue = ((progress - 10) / 1000) * 40; + + if (opacityValue > 0.6) { + opacityValue = 0.6; + } + + background.style = `opacity: ${opacityValue}`; + }); + + return background; +} + +async function buildApp(block, content) { + const appBackground = createTag('div', { class: 'fullscreen-marquee-desktop-app-background' }); + const appFrame = createTag('div', { class: 'fullscreen-marquee-desktop-app-frame' }); + const app = createTag('div', { class: 'fullscreen-marquee-desktop-app' }); + const contentContainer = createTag('div', { class: 'fullscreen-marquee-desktop-app-content-container' }); + const cta = block.querySelector('.button-container a'); + let appImage; + let editor; + let variant; + + if (block.classList.contains('video')) { + variant = 'video'; + + if (content) { + const thumbnailContainer = createTag('div', { class: 'fullscreen-marquee-desktop-app-thumbnail-container' }); + const thumbnail = content.cloneNode(true); + + thumbnailContainer.append(thumbnail); + app.append(thumbnailContainer); + + content.addEventListener('loadedmetadata', () => { + const framesContainer = createTag('div', { class: 'fullscreen-marquee-desktop-app-frames-container' }); + function createFrame(current, total) { + const frame = createTag('video', { src: `${content.currentSrc}#t=${current}` }); + framesContainer.append(frame); + + frame.addEventListener('loadedmetadata', () => { + frame.style.opacity = '1'; + + if (current < total) { + const newFrameCount = current + 1; + createFrame(newFrameCount, total); + } + }); + } + + createFrame(1, 10); + app.append(framesContainer); + }); + + thumbnail.addEventListener('loadedmetadata', () => { + thumbnail.currentTime = Math.floor(thumbnail.duration) / 2; + thumbnail.pause(); + }); + } + } else { + variant = 'image'; + } + + await fetchPlaceholders().then((placeholders) => { + appImage = createOptimizedPicture(placeholders[`fullscreen-marquee-desktop-${variant}-app`]); + editor = createOptimizedPicture(placeholders[`fullscreen-marquee-desktop-${variant}-editor`]); + + appImage.classList.add('fullscreen-marquee-desktop-app-image'); + }); + + editor.classList.add('fullscreen-marquee-desktop-app-editor'); + content.classList.add('fullscreen-marquee-desktop-app-content'); + + window.addEventListener('mousemove', (e) => { + const rotateX = ((e.clientX * 10) / (window.innerWidth / 2) - 10); + const rotateY = -((e.clientY * 10) / (window.innerHeight / 2) - 10); + + app.style.transform = `rotateX(${rotateY}deg) rotateY(${rotateX}deg) translate3d(${rotateX}px, 0px, 0px)`; + appBackground.style.transform = `rotateX(${rotateY}deg) rotateY(${rotateX}deg) translate3d(${rotateX}px, 0px, -50px)`; + }, { passive: true }); + + contentContainer.append(content); + app.append(appImage); + app.append(contentContainer); + appFrame.append(app); + appFrame.append(appBackground); + app.append(editor); + + if (cta) { + cta.classList.add('xlarge'); + + const highlightCta = cta.cloneNode(true); + const appHighlight = createTag('a', { + class: 'fullscreen-marquee-desktop-app-frame-highlight', + href: cta.href, + }); + + await addFreePlanWidget(cta.parentElement); + + appHighlight.append(highlightCta); + appFrame.append(appHighlight); + } + + return appFrame; +} + +export default async function decorate(block) { + const rows = Array.from(block.children); + const heading = rows[0] ? rows[0].querySelector('div') : null; + const background = rows[2] ? rows[2].querySelector('picture') : null; + let content = rows[1] ?? null; + + block.innerHTML = ''; + + if (content) { + content = buildContent(content); + } + + if (background) { + block.classList.add('has-background'); + block.append(buildBackground(block, background)); + } + + if (heading) { + block.append(buildHeading(block, heading)); + } + + if (content) { + block.append(await buildApp(block, content)); + } +} diff --git a/express/blocks/fullscreen-marquee/fullscreen-marquee.js b/express/blocks/fullscreen-marquee/fullscreen-marquee.js index 3660df3cc..5fb3f5996 100644 --- a/express/blocks/fullscreen-marquee/fullscreen-marquee.js +++ b/express/blocks/fullscreen-marquee/fullscreen-marquee.js @@ -14,8 +14,8 @@ import { createTag, transformLinkToAnimation, createOptimizedPicture, - addFreePlanWidget, } from '../../scripts/scripts.js'; +import { addFreePlanWidget } from '../../scripts/utils/free-plan.js'; function styleBackgroundWithScroll($section) { const $background = createTag('div', { class: 'marquee-background' }); diff --git a/express/blocks/hero-animation/hero-animation.js b/express/blocks/hero-animation/hero-animation.js index 130bf6715..8cd4ede95 100644 --- a/express/blocks/hero-animation/hero-animation.js +++ b/express/blocks/hero-animation/hero-animation.js @@ -12,13 +12,13 @@ import { addAnimationToggle, - addFreePlanWidget, createTag, toClassName, getLocale, addHeaderSizing, // eslint-disable-next-line import/no-unresolved } from '../../scripts/scripts.js'; +import { addFreePlanWidget } from '../../scripts/utils/free-plan.js'; import { isVideoLink, diff --git a/express/blocks/hero-color/hero-color.css b/express/blocks/hero-color/hero-color.css new file mode 100644 index 000000000..f2c196bf8 --- /dev/null +++ b/express/blocks/hero-color/hero-color.css @@ -0,0 +1,140 @@ +/* Future proofing wrapper deprecation. To be removed after milo migration */ +main .section .hero-color-wrapper { + max-width: unset; +} + +main .hero-color { + max-width: unset; +} + +main .hero-color .content-container .text { + display: none; +} + +main .hero-color .content-container { + min-height: 200px; + max-width: 1440px; + 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; + justify-content: center; + background-color: white; +} + +main .hero-color .text-container .text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 15px; + text-align: left; + color: var(--color-black); +} + +main .hero-color .content-container h2, +main .hero-color .text-container h2 { + margin: 0; + font-size: var(--heading-font-size-xl); + text-align: left; +} + +main .hero-color .content-container p, +main .hero-color .text-container p { + margin: 0; + font-size: var(--body-font-size-l); +} + +main .hero-color .content-container .text .button-container, +main .hero-color .text-container .text .button-container { + margin: 0; +} + +main .hero-color .content-container .text .button-container .button, +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 .content-container .color-svg-img { + height: 100%; + width: 200%; + margin: 0 0 0 15px; +} + +main .hero-color.light-bg { + color: var(--color-black); +} + +main .hero-color.dark-bg { + 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 .text-container { + display: none; + } + + main .hero-color .content-container { + min-height: 336px; + display: flex; + gap: 60px; + } + + main .hero-color .content-container .color-svg { + position: relative; + } + + main .hero-color .content-container .color-svg-img { + margin: 0; + width: 1424px; + height: 336px; + overflow: hidden; + position: absolute; + left: 0; + } + + main .hero-color .content-container .text { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + margin: 20px; + text-align: left; + min-width: 486px; + max-width: 486px; + } + + main .hero-color.dark-bg .button, + main .hero-color.dark-bg .button:hover { + background-color: var(--color-white); + color: var(--color-black); + border-color: var(--color-white); + } + + main .hero-color.light-bg .button, + main .hero-color.light-bg .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 000000000..54cf6c9c1 --- /dev/null +++ b/express/blocks/hero-color/hero-color.js @@ -0,0 +1,192 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + createTag, +} from '../../scripts/scripts.js'; + +function isDarkOverlayReadable(colorString) { + let r; + let g; + let b; + + if (colorString.match(/^rgb/)) { + const colorValues = colorString.match( + /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/, + ); + [r, g, b] = colorValues.slice(1); + } else { + const hexToRgb = +`0x${colorString + .slice(1) + .replace(colorString.length < 5 ? /./g : '', '$&$&')}`; + // eslint-disable-next-line no-bitwise + r = (hexToRgb >> 16) & 255; + // eslint-disable-next-line no-bitwise + g = (hexToRgb >> 8) & 255; + // eslint-disable-next-line no-bitwise + b = hexToRgb & 255; + } + + const hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)); + return hsp > 140; +} + +function changeTextColorAccordingToBg( + primaryColor, + block, +) { + block.classList.add(isDarkOverlayReadable(primaryColor) ? 'light-bg' : 'dark-bg'); +} + +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 heroColorContentContainer = block.querySelector('.content-container'); + heroColorContentContainer.append(svgWrapper); +} + +function copyTextBlock(block, text) { + const title = block.querySelector('h2'); + const cta = block.querySelector('.button-container'); + const descriptions = block.querySelectorAll('p:not(:last-of-type)'); + const descriptionContainer = createTag('div', { class: 'description-container' }); + + Array.from(descriptions).forEach((textDescription) => { + descriptionContainer.append(textDescription); + }); + + text.classList.add('text'); + text.append(title, descriptionContainer, cta); +} + +function cloneTextForSmallerMediaQueries(text) { + const clonedTextBlock = text.cloneNode(true); + clonedTextBlock.classList.add('text-container'); + const textContent = clonedTextBlock.children[0]; + + copyTextBlock(clonedTextBlock, textContent); + + return clonedTextBlock; +} + +function groupTextElements(text, block) { + const button = block.querySelector('.button'); + + copyTextBlock(block, text); + button.style.border = 'none'; +} + +function decorateText(block) { + const text = block.firstElementChild; + const heroColorContentContainer = block.querySelector('.content-container'); + const smallMediaQueryBlock = cloneTextForSmallerMediaQueries(text); + + groupTextElements(text, block); + heroColorContentContainer.append(text); + block.append(smallMediaQueryBlock); +} + +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 { primaryColor, secondaryColor } = extractColorElements(colors); + + block.style.backgroundColor = primaryColor; + + changeTextColorAccordingToBg(primaryColor, block); + + return { secondaryColor }; +} + +function getContentContainerHeight() { + const contentContainer = document.querySelector('.content-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); +} + +function resizeSvgOnMediaQueryChange() { + const mediaQuery = window.matchMedia('(min-width: 900px)'); + mediaQuery.addEventListener('change', (event) => { + const height = getContentContainerHeight(); + const svg = document.querySelector('.color-svg-img'); + if (event.matches) { + svg.style.height = `${height}px`; + } else { + svg.style.height = '200px'; + } + }); +} + +export default function decorate(block) { + const heroColorContentContainer = createTag('div', { + class: 'content-container', + }); + + block.append(heroColorContentContainer); + + // text + decorateText(block); + + // colors + const { secondaryColor } = decorateColors(block); + + // svg + displaySvgWithObject(block, secondaryColor); + resizeSvgOnLoad(); + resizeSvgOnMediaQueryChange(); +} diff --git a/express/blocks/how-to-steps-carousel/how-to-steps-carousel.css b/express/blocks/how-to-steps-carousel/how-to-steps-carousel.css old mode 100644 new mode 100755 index 1215440ea..eee5a8a8b --- a/express/blocks/how-to-steps-carousel/how-to-steps-carousel.css +++ b/express/blocks/how-to-steps-carousel/how-to-steps-carousel.css @@ -3,10 +3,6 @@ main .how-to-steps-carousel-container { padding: 0; } -main .how-to-steps-carousel-container > picture { - width: 100%; -} - main .how-to-steps-carousel-container > picture > img { width: 100%; margin-bottom: 56px; @@ -30,7 +26,7 @@ main .how-to-steps-carousel-container > div p.button-container a { } main .how-to-steps-carousel-container > div p.button-container { - margin: 16px 0; + margin: 16px 0; } main .how-to-steps-carousel-container > div > h2 { @@ -71,7 +67,7 @@ main .how-to-steps-carousel .tips .tip.active { 100% { opacity:1; } } -@-moz-keyframes fadeIn { +@-moz-keyframes fadeIn { 0% { opacity:0; } 100% { opacity:1; } } @@ -120,8 +116,8 @@ main .how-to-steps-carousel .tip-number { main .how-to-steps-carousel .tip-number:focus { outline: none; box-shadow: - 0 0 0 2px var(--color-white), - 0 0 0 4px var(--color-info-accent ); + 0 0 0 2px var(--color-white), + 0 0 0 4px var(--color-info-accent ); } main .how-to-steps-carousel .tip-number.active { @@ -155,14 +151,14 @@ main .how-to-steps-carousel-container .icon { height: 56px; } -@media (min-width: 600px) { - main .how-to-steps-carousel-container > div { - margin: 0 88px; - padding: 80px 88px; - } +main .how-to-steps-carousel-container > picture > img { + margin-bottom: 80px; + width: calc(100% - 40px); +} +@media (min-width: 600px) { main .how-to-steps-carousel-container > picture > img { - margin-bottom: 80px; + margin: 0; object-position: right; object-fit: cover; min-height: 100%; @@ -174,9 +170,7 @@ main .how-to-steps-carousel-container .icon { margin-top: 64px; margin-bottom: 80px; } -} -@media (min-width: 900px) { main .how-to-steps-carousel-container { display: flex; justify-content: center; @@ -184,32 +178,18 @@ main .how-to-steps-carousel-container .icon { } main .how-to-steps-carousel-container > div { - margin: 0 120px 0 20px; - padding: 80px 70px; - width: 50%; - height: 100%; max-width: 600px; + padding: 80px 88px; } +} - main .how-to-steps-carousel-container > picture > img { - margin: 0; - } - - main .how-to-steps-carousel-container.no-cover > div { - margin: 0 0 0 20px; - } - - main .how-to-steps-carousel-container.no-cover > picture { - width: unset; - } - - main .how-to-steps-carousel-container.no-cover > picture > img { - object-fit: contain; +@media (min-width: 900px) { + main .how-to-steps-carousel-container > div { + margin: 0 80px; } main .how-to-steps-carousel-container > div p.button-container { - margin: 0; - margin-top: 40px; + margin: 40px 0 0; } } @@ -217,4 +197,4 @@ main .how-to-steps-carousel-container .icon { main .how-to-steps-carousel-container.no-cover > div { margin: 0 120px 0 20px; } -} \ No newline at end of file +} diff --git a/express/blocks/how-to-steps-carousel/how-to-steps-carousel.js b/express/blocks/how-to-steps-carousel/how-to-steps-carousel.js old mode 100644 new mode 100755 index deac601f8..398f321b0 --- a/express/blocks/how-to-steps-carousel/how-to-steps-carousel.js +++ b/express/blocks/how-to-steps-carousel/how-to-steps-carousel.js @@ -1,5 +1,5 @@ /* - * Copyright 2021 Adobe. All rights reserved. + * Copyright 2023 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -13,17 +13,18 @@ /* eslint-disable import/named, import/extensions */ import { + createOptimizedPicture, createTag, -// eslint-disable-next-line import/no-unresolved + fetchPlaceholders, } from '../../scripts/scripts.js'; let rotationInterval; let fixedImageSize = false; function reset(block) { - const window = block.ownerDocument.defaultView; + const howToWindow = block.ownerDocument.defaultView; - window.clearInterval(rotationInterval); + howToWindow.clearInterval(rotationInterval); rotationInterval = null; const container = block.parentElement.parentElement; @@ -35,36 +36,30 @@ function reset(block) { fixedImageSize = false; } -function activate(block, target) { - if (!fixedImageSize) { - // trick to fix the image height when vw > 900 and avoid image resize when toggling the tips - - // get viewport width - const window = block.ownerDocument.defaultView; - const document = block.ownerDocument; - - const { documentElement } = document; - const vw = Math.max( - documentElement && documentElement.clientWidth ? documentElement.clientWidth : 0, - window && window.innerWidth ? window.innerWidth : 0, - ); - - if (vw >= 900) { - const container = block.parentElement.parentElement; - const picture = container.querySelector('picture'); - const img = picture.querySelector('img'); - const panelHeight = block.parentElement.offsetHeight; - const imgHeight = img.naturalHeight; - if (imgHeight < panelHeight) { - container.classList.add('no-cover'); - } else { - picture.style.height = `${panelHeight || imgHeight}px`; - } - } +const loadImage = (img) => new Promise((resolve) => { + if (img.complete && img.naturalHeight !== 0) resolve(); + else { + img.onload = () => { + resolve(); + }; + } +}); +function setPictureHeight(block, override) { + if (!fixedImageSize || override) { + // trick to fix the image height when vw > 900 and avoid image resize when toggling the tips + const container = block.parentElement.parentElement; + const picture = container.querySelector('picture'); + const img = picture.querySelector('img'); + const panelHeight = block.parentElement.offsetHeight; + const imgHeight = img.naturalHeight; + picture.style.height = `${panelHeight || imgHeight}px`; fixedImageSize = true; } +} +function activate(block, target) { + setPictureHeight(block); // de-activate all block.querySelectorAll('.tip, .tip-number').forEach((item) => { item.classList.remove('active'); @@ -76,10 +71,10 @@ function activate(block, target) { block.querySelectorAll(`.tip-${i}`).forEach((elem) => elem.classList.add('active')); } -function initRotation(window, document) { - if (window && !rotationInterval) { - rotationInterval = window.setInterval(() => { - document.querySelectorAll('.tip-numbers').forEach((numbers) => { +function initRotation(howToWindow, howToDocument) { + if (howToWindow && !rotationInterval) { + rotationInterval = howToWindow.setInterval(() => { + 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) { @@ -92,17 +87,7 @@ function initRotation(window, document) { } } -export default function decorate(block) { - const window = block.ownerDocument.defaultView; - const document = block.ownerDocument; - - // move first image of container outside of div for styling - const section = block.closest('.section'); - const picture = section.querySelector('picture'); - const parent = picture.parentElement; - section.prepend(picture); - parent.remove(); - +function buildHowToStepsCarousel(section, picture, block, howToDocument, rows, howToWindow) { // join wrappers together section.querySelectorAll('.default-content-wrapper').forEach((wrapper, i) => { if (i === 0) { @@ -118,9 +103,6 @@ export default function decorate(block) { } }); - const howto = block; - const rows = Array.from(howto.children); - const heading = section.querySelector('h2, h3, h4'); const includeSchema = block.classList.contains('schema'); @@ -132,7 +114,7 @@ export default function decorate(block) { const schema = { '@context': 'http://schema.org', '@type': 'HowTo', - name: (heading && heading.textContent.trim()) || document.title, + name: (heading && heading.textContent.trim()) || howToDocument.title, step: [], }; @@ -180,7 +162,7 @@ export default function decorate(block) { number.addEventListener('click', (e) => { if (rotationInterval) { - window.clearTimeout(rotationInterval); + howToWindow.clearTimeout(rotationInterval); } let { target } = e; @@ -208,31 +190,135 @@ export default function decorate(block) { if (includeSchema) { const $schema = createTag('script', { type: 'application/ld+json' }); $schema.innerHTML = JSON.stringify(schema); - const $head = document.head; + const $head = howToDocument.head; $head.append($schema); } - if (window) { - window.addEventListener('resize', () => { + if (howToWindow) { + howToWindow.addEventListener('resize', () => { reset(block); activate(block, block.querySelector('.tip-number.tip-1')); - initRotation(window, document); + initRotation(howToWindow, howToDocument); }); } - const img = picture.querySelector('img'); - const run = () => { - // slgiht delay to allow panel to size correctly - window.setTimeout(() => { - activate(block, block.querySelector('.tip-number.tip-1')); - initRotation(window, document); - }, 200); - }; + // slgiht delay to allow panel to size correctly + howToWindow.setTimeout(() => { + activate(block, block.querySelector('.tip-number.tip-1')); + initRotation(howToWindow, howToDocument); + }, 100); +} - if (!img.complete) { - img.addEventListener('load', run); - img.addEventListener('error', run); +function roundedImage(x, y, width, height, radius, ctx) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); +} + +function layerTemplateImage(canvas, ctx, templateImg) { + templateImg.style.objectFit = 'contain'; + + return new Promise((outerResolve) => { + let prevWidth; + const drawImage = (centerX, centerY, maxWidth, maxHeight) => new Promise((resolve) => { + const obs = new ResizeObserver((changes) => { + for (const change of changes) { + if (change.contentRect.width === prevWidth) return; + prevWidth = change.contentRect.width; + if (prevWidth <= maxWidth && change.contentRect.height <= maxHeight) { + ctx.save(); + roundedImage(centerX - (templateImg.width / 2), centerY - (templateImg.height / 2), + templateImg.width, templateImg.height, 7, ctx); + ctx.clip(); + ctx.drawImage(templateImg, 0, 0, templateImg.naturalWidth, + templateImg.naturalHeight, centerX - (templateImg.width / 2), + centerY - (templateImg.height / 2), templateImg.width, templateImg.height); + ctx.restore(); + obs.disconnect(); + resolve(); + } + } + }); + obs.observe(templateImg); + templateImg.style.maxWidth = `${maxWidth}px`; + templateImg.style.maxHeight = `${maxHeight}px`; + }); + + // start and end areas were directly measured and transferred from the spec image + drawImage(1123, 600, 986, 652) + .then(() => drawImage(1816, 479, 312, 472)) + .then(() => outerResolve()); + }); +} + +export default async function decorate(block) { + const howToWindow = block.ownerDocument.defaultView; + const howToDocument = block.ownerDocument; + const image = block.classList.contains('image'); + + // move first image of container outside of div for styling + const section = block.closest('.section'); + const howto = block; + const rows = Array.from(howto.children); + let picture; + + if (image) { + const canvasWidth = 2000; + const canvasHeight = 1072; + + const placeholderImgUrl = createTag('div'); + const placeholders = await fetchPlaceholders(); + const url = placeholders['how-to-steps-carousel-image-app']; + const eagerLoad = document.querySelector('.block') === block; + const backgroundPic = createOptimizedPicture(url, 'template in express', eagerLoad); + const backgroundPicImg = backgroundPic.querySelector('img', { alt: 'template in express' }); + + if (placeholderImgUrl) { + backgroundPic.appendChild(backgroundPicImg); + placeholderImgUrl.remove(); + } + + const templateDiv = rows.shift(); + const templateImg = templateDiv.querySelector(':scope picture > img'); + + templateImg.style.visibility = 'hidden'; + templateImg.style.position = 'absolute'; + backgroundPicImg.style.width = `${canvasWidth}px`; + if (window.screen.width < 600) backgroundPicImg.style.height = `${window.screen.width * 0.536}px`; + picture = backgroundPic; + section.prepend(picture); + + loadImage(backgroundPicImg).then(() => { + backgroundPicImg.width = canvasWidth; + const canvas = createTag('canvas', { width: canvasWidth, height: canvasHeight }); + const ctx = canvas.getContext('2d'); + ctx.drawImage(backgroundPicImg, 0, 0, canvasWidth, canvasHeight); + const sources = backgroundPic.querySelectorAll(':scope > source'); + sources.forEach((source) => source.remove()); + return loadImage(templateImg).then(() => { + layerTemplateImage(canvas, ctx, templateImg).then(() => { + templateDiv.remove(); + const img = createTag('img'); + img.src = canvas.toDataURL('image/png'); + backgroundPic.append(img); + backgroundPicImg.remove(); + setPictureHeight(block, true); + }); + }); + }); } else { - run(); + picture = section.querySelector('picture'); + const parent = picture.parentElement; + parent.remove(); + section.prepend(picture); } + buildHowToStepsCarousel(section, picture, block, howToDocument, rows, howToWindow); } diff --git a/express/blocks/icon-list/icon-list.css b/express/blocks/icon-list/icon-list.css index e9417cc1e..930383b09 100644 --- a/express/blocks/icon-list/icon-list.css +++ b/express/blocks/icon-list/icon-list.css @@ -10,9 +10,8 @@ main .icon-list { main .icon-list > div { display: flex; - margin-bottom: 32px; width: 350px; - margin: 16px; + margin: 16px 16px 32px; } main .icon-list > div.icon-list-column { @@ -20,8 +19,9 @@ main .icon-list > div.icon-list-column { width: unset; } -main .icon-list p { +main .icon-list .icon-list-description p { margin: 8px 0; + text-align: left; } main .icon-list .icon-list-image { @@ -76,7 +76,7 @@ main .section.icon-list-two-column-container h2 { main .icon-list.two-column { margin: 0; margin-top: 40px; - padding: 0; + padding: 0; } main .icon-list > div.icon-list-column { @@ -137,7 +137,7 @@ main .icon-list.highlight .icon-list-regular .icon-list-image { main .icon-list .icon-list-regular .icon-list-description { flex: 1 1 245px; margin-left: 24px; - align-self: top; + align-self: start; } main .icon-list.highlight .icon-list-regular .icon-list-image img, @@ -147,7 +147,7 @@ main .icon-list.highlight .icon-list-regular .icon-list-image svg { height: 72px; } -main .section.icon-list-two-column-container > div { +main .section.icon-list-two-column-container > div.icon-list-wrapper { max-width: unset; padding: 60px 32px; } @@ -170,12 +170,12 @@ main .icon-list-center-container ~ .columns-highlight-container { } main .icon-list > div.icon-list-column { - padding: 0 32px; + padding: 0 32px; } main .section.icon-list-two-column-container > div { max-width: 1122px; - } + } } @media (min-width:1200px) { diff --git a/express/blocks/library-config/library-config.css b/express/blocks/library-config/library-config.css new file mode 100644 index 000000000..429321010 --- /dev/null +++ b/express/blocks/library-config/library-config.css @@ -0,0 +1,329 @@ +/* +Common styles for the block library +*/ + +.sk-library { + width: 360px; + height: 360px; + background: #303030; + color: #FFF; + overflow: hidden; + font-family: 'Adobe Clean', adobe-clean, 'Trebuchet MS', sans-serif; + font-size: 14px; +} + +/* Header */ +.sk-library-header { + display: flex; + background: rgb(0 0 0 / 35%); + flex-flow: wrap; +} + +.sk-library-title { + display: flex; +} + +.section p.sk-library-title-text { + text-transform: capitalize; + margin: 0; + line-height: 36px; +} + +.sk-library-logo { + text-indent: -1000px; + overflow: hidden; + border: none; + background: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-size: 18px; + width: 36px; + height: 36px; + background-position: center; +} + +.allow-back .sk-library-logo { + background-color: transparent; + border: 0; + background-image: url('../../../express/icons/spectrum-back.svg'); + background-size: 50%; + background-repeat: no-repeat; + background-position: 50%; + width: 36px; + height: 36px; + cursor: pointer; +} + +.sk-library-back:hover { + background-color: rgb(52 52 52 / 50%); +} + +.sk-library.allow-back .sk-library-back { + display: block; +} + +.sk-library-search { + display: flex; + position: relative; + flex: 1 1 100%; +} + +.sk-library-search.is-hidden { + display: none; +} + +.sk-library-search::before { + position: absolute; + content: ''; + background: url('../../../express/icons/spectrum-search.svg') no-repeat; + width: 20px; + height: 20px; + margin-left: 11px; + top: 11px; + opacity: 0.6; +} + +input.sk-library-search-input { + width: 100%; + padding-left: 35px; + background: rgb(0 0 0 / 100%); + color: #FFF; + font-family: inherit; + font-size: 14px; + border: none; + height: 35px; + padding-right: 42px; + border-top: 1px solid rgba(100 100 100 / 20%); + border-bottom: 1px solid rgba(100 100 100 / 20%); +} + +input.sk-library-search-input:focus { + outline-width: 0; +} + +.sk-library-search-clear { + background: url('../../../express/icons/spectrum-close.svg') no-repeat; + width: 16px; + height: 30px; + margin-top: 10px; + margin-right: 27px; + position: absolute; + right: 0; + top: 0; + cursor: pointer; +} + +.sk-library-search-clear.is-hidden { + visibility: hidden; +} + +/* Content */ +.content-type { + margin: 0; + padding: 0 12px; + line-height: 36px; + text-transform: capitalize; +} + + +.sk-library li button { + border: none; + background-color: transparent; + background-image: url('../../../express/icons/spectrum-copy.svg'); + background-position: 50%; + background-repeat: no-repeat; + width: 44px; + height: 44px; + display: none; + position: absolute; + right: 0; + top: 0; +} + +.sk-library li p.item-title { + padding: 0; + margin: 0; + font-size: unset; + line-height: unset; +} + +.sk-library button.preview-group { +width: 32px; +height: 32px; +border-radius: 4px; +top: 6px; +right: 32px; +text-indent: -1000px; +overflow: hidden; +background-color: transparent; +background-image: url('../../../express/icons/spectrum-preview.svg'); +background-position: 50%; +background-repeat: no-repeat; +} + +.sk-library button.preview-group:hover { +background-color: rgb(0 0 0 / 15%); +} + +.sk-library p.heading { +font-weight: 700; +padding: 0 36px; +} + +.con-container { +position: relative; +height: calc(100% - 36px); +} + +.sk-library ul.sk-library-list, +.sk-library ul.con-type-list { +list-style: none; +padding: 0; +margin: 0; +overflow: scroll; +position: absolute; +inset: 0; +transition: transform .2s ease-in-out; +} + +.sk-library ul.con-type-list { +transform: translateX(360px); +} + +.sk-library ul.con-type-list.inset { +transform: translateX(0); +} + +.sk-library ul.inset { +transform: translateX(-360px); +} + +.sk-library li { +line-height: 44px; +position: relative; +border-bottom: 1px solid #676767; +} + +/* +* Fixes block list getting cut off with search +* Margin height equal to search bar height +*/ +.sk-library ul.con-blocks-list.inset { + margin-bottom: 39px; +} + +.sk-library .block-group::after, +.sk-library .content-type::after { + content: ''; + border: solid #777; + border-width: 0 3px 3px 0; + display: inline-block; + padding: 3px; + position: absolute; + right: 14px; + top: 17px; + transition: transform .2s ease-in-out; + transform: rotate(-45deg); +} + +.sk-library .block-group.is-open::after { + transform: rotate(45deg); +} + +.sk-library li:hover button { + display: unset; +} + +.sk-library li button.copied, +.sk-library li button.copied:hover { + background-color: rgb(54 127 61 / 50%); +} + +.sk-library .block-group { + padding: 0 12px; + background: rgb(0 0 0 / 50%); + font-weight: 700; +} + +.sk-library .block-group.is-hidden { + display: none; +} + +.sk-library .content-type:hover, +.sk-library .block-group:hover, +.sk-library button:hover { + cursor: pointer; + background-color: rgb(255 255 255 / 5%); +} + +.sk-library .block-group-list { + display: none; + list-style: none; + padding: 0; + margin: 0; +} + +.sk-library .block-group.is-open + .block-group-list { + display: block; +} + +.sk-library .block-group-list li.is-hidden { + display: none; +} + +.sk-library .block-group-list li:hover { + background-color: rgb(255 255 255 / 5%); +} + +.sk-library .block-group-list p { + margin: 0; + padding: 0 24px; + font-size: unset; + line-height: unset; +} + +.sk-library li.placeholder { + padding: 0 12px; +} + +.sk-library li.icon-item { + display: flex; + gap: 12px; + padding: 0 12px; +} + +.in-page { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + + +ul.con-type-list.con-assets-list { + display: flex; + align-items: start; + width: 360px; + flex-wrap: wrap; +} + +.con-assets-list li { + flex: 0 0 50%; + height: 180px; + display: flex; + align-items: center; + padding: 24px; + box-sizing: border-box; +} + +.con-assets-list li:hover { + cursor: pointer; + background: #85858545; +} + +.con-assets-list li:nth-child(odd) { + border-right: 1px solid #676767; +} + +.con-personalization_tags-list .item-title::first-letter { + text-transform: capitalize; +} diff --git a/express/blocks/library-config/library-config.js b/express/blocks/library-config/library-config.js new file mode 100644 index 000000000..d7259abc0 --- /dev/null +++ b/express/blocks/library-config/library-config.js @@ -0,0 +1,152 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + createTag, +} from '../../scripts/scripts.js'; + +const LIBRARY_PATH = '/docs/library/library.json'; + +async function loadBlocks(content, list, query) { + const { default: blocks } = await import('./lists/blocks.js'); + blocks(content, list, query); +} + +function addSearch(content, list) { + const skLibrary = list.closest('.sk-library'); + const header = skLibrary.querySelector('.sk-library-header'); + let search = skLibrary.querySelector('.sk-library-search'); + if (!search) { + search = createTag('div', { class: 'sk-library-search' }); + const searchInput = createTag('input', { class: 'sk-library-search-input', placeholder: 'Search...' }); + const clear = createTag('div', { class: 'sk-library-search-clear is-hidden' }); + searchInput.addEventListener('input', (e) => { + const query = e.target.value; + if (query === '') { + clear.classList.add('is-hidden'); + } else { + clear.classList.remove('is-hidden'); + } + loadBlocks(content, list, query); + }); + clear.addEventListener('click', (e) => { + e.target.classList.add('is-hidden'); + e.target.closest('.sk-library-search').querySelector('.sk-library-search-input').value = ''; + loadBlocks(content, list); + }); + search.append(searchInput); + search.append(clear); + header.append(search); + } else { + search.classList.remove('is-hidden'); + } +} + +async function loadList(type, content, list) { + list.innerHTML = ''; + const query = list.closest('.sk-library').querySelector('.sk-library-search-input')?.value; + addSearch(content, list); + loadBlocks(content, list, query); +} + +async function fetchLibrary(domain) { + const library = `${domain}${LIBRARY_PATH}`; + try { + const resp = await fetch(library); + if (!resp.ok) return null; + return resp.json(); + } catch { + return null; + } +} + +function createList(libraries) { + const container = createTag('div', { class: 'con-container' }); + + const libraryList = createTag('ul', { class: 'sk-library-list' }); + container.append(libraryList); + + Object.keys(libraries).forEach((type) => { + if (!libraries[type] || libraries[type].length === 0) return; + + const item = createTag('li', { class: 'content-type' }); + item.innerText = type.replace('_', ' '); + libraryList.append(item); + + const list = document.createElement('ul'); + list.classList.add('con-type-list', `con-${type}-list`); + container.append(list); + + item.addEventListener('click', (e) => { + const skLibrary = e.target.closest('.sk-library'); + skLibrary.querySelector('.sk-library-title-text').textContent = type.replace('_', ' '); + libraryList.classList.add('inset'); + list.classList.add('inset'); + skLibrary.classList.add('allow-back'); + loadList(type, libraries[type], list); + }); + }); + + return container; +} + +function createHeader() { + const nav = createTag('button', { class: 'sk-library-logo' }); + nav.innerText = 'Franklin Library'; + const title = createTag('div', { class: 'sk-library-title' }); + title.append(nav); + const libraryText = createTag('p', { class: 'sk-library-title-text' }); + libraryText.innerText = 'Pick a library'; + title.append(libraryText); + const header = createTag('div', { class: 'sk-library-header' }); + header.append(title); + + nav.addEventListener('click', (e) => { + const skLibrary = e.target.closest('.sk-library'); + skLibrary.querySelector('.sk-library-search')?.classList.add('is-hidden'); + skLibrary.querySelector('.sk-library-title-text').textContent = 'Pick a library'; + const insetEls = skLibrary.querySelectorAll('.inset'); + insetEls.forEach((el) => { + el.classList.remove('inset'); + }); + skLibrary.classList.remove('allow-back'); + }); + return header; +} + +function detectContext() { + if (window.self === window.top) { + document.body.classList.add('in-page'); + } +} + +export default async function init(el) { + el.querySelector('div').remove(); + detectContext(); + + // Get the data + const base = await fetchLibrary(window.location.origin); + const library = { + blocks: base.data, + }; + + // Create the UI + const skLibrary = createTag('div', { class: 'sk-library' }); + + const header = createHeader(); + skLibrary.append(header); + + const list = createList(library); + skLibrary.append(list); + + el.append(skLibrary); +} diff --git a/express/blocks/library-config/library-utils.js b/express/blocks/library-config/library-utils.js new file mode 100644 index 000000000..e5da43277 --- /dev/null +++ b/express/blocks/library-config/library-utils.js @@ -0,0 +1,16 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +export default function createCopy(blob) { + // eslint-disable-next-line no-undef + const data = [new ClipboardItem({ [blob.type]: blob })]; + navigator.clipboard.write(data); +} diff --git a/express/blocks/library-config/lists/blocks.js b/express/blocks/library-config/lists/blocks.js new file mode 100644 index 000000000..f780c2419 --- /dev/null +++ b/express/blocks/library-config/lists/blocks.js @@ -0,0 +1,147 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { createTag } from '../../../scripts/scripts.js'; +import createCopy from '../library-utils.js'; + +function getAuthorName(block) { + const blockSib = block.previousElementSibling; + if (!blockSib) return null; + if (['H2', 'H3'].includes(blockSib.nodeName)) { + return blockSib.textContent; + } + return null; +} + +function getBlockName(block) { + const classes = block.className.split(' '); + const name = classes.shift(); + return classes.length > 0 ? `${name} (${classes.join(', ')})` : name; +} + +function getTable(block, name, path) { + const url = new URL(path); + block.querySelectorAll('img') + .forEach((img) => { + const srcSplit = img.src.split('/'); + const mediaPath = srcSplit.pop(); + img.src = `${url.origin}/${mediaPath}`; + const { + width, + height, + } = img; + const ratio = width > 200 ? 200 / width : 1; + img.width = width * ratio; + img.height = height * ratio; + }); + const rows = [...block.children]; + const maxCols = rows.reduce((cols, row) => ( + row.children.length > cols ? row.children.length : cols), 0); + const table = document.createElement('table'); + table.setAttribute('border', 1); + const headerRow = document.createElement('tr'); + headerRow.append(createTag('th', { colspan: maxCols }, name)); + table.append(headerRow); + rows.forEach((row) => { + const tr = document.createElement('tr'); + [...row.children].forEach((col) => { + const td = document.createElement('td'); + if (row.children.length < maxCols) { + td.setAttribute('colspan', maxCols); + } + td.innerHTML = col.innerHTML; + tr.append(td); + }); + table.append(tr); + }); + return table.outerHTML; +} + +function isMatchingBlock(pageBlock, query) { + const tagsString = getBlockName(pageBlock); + if (!query || !tagsString) return false; + const searchTokens = query.split(' '); + return searchTokens.every((token) => tagsString.toLowerCase() + .includes(token.toLowerCase())); +} + +export default async function loadBlocks(blocks, list, query) { + list.textContent = ''; + blocks.forEach(async (block) => { + const titleText = createTag('p', { class: 'item-title' }, block.name); + const title = createTag('li', { class: 'block-group' }, titleText); + if (query) { + title.classList.add('is-hidden'); + } + const previewButton = createTag('button', { class: 'preview-group' }, 'Preview'); + title.append(previewButton); + list.append(title); + + const blockList = createTag('ul', { class: 'block-group-list' }); + list.append(blockList); + + title.addEventListener('click', () => { + title.classList.toggle('is-open'); + }); + + previewButton.addEventListener('click', (e) => { + e.stopPropagation(); + window.open(block.path, '_blockpreview'); + }); + + const resp = await fetch(`${block.path}.plain.html`); + if (!resp.ok) return; + + const html = await resp.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const pageBlocks = doc.body.querySelectorAll('div[class]'); + let matchingBlockFound = false; + pageBlocks.forEach((pageBlock) => { + // don't display the library-metadata block used to set the block search tags + if (pageBlock.className === 'library-metadata') { + return; + } + const item = document.createElement('li'); + const name = document.createElement('p'); + name.textContent = getAuthorName(pageBlock) || getBlockName(pageBlock); + const copy = document.createElement('button'); + copy.addEventListener('click', (e) => { + let table = getTable(pageBlock, getBlockName(pageBlock), block.path); + table = pageBlock.className === 'section-metadata' + ? `${table} ---` + : table; + e.target.classList.add('copied'); + setTimeout(() => { + e.target.classList.remove('copied'); + }, 3000); + const blob = new Blob([table], { type: 'text/html' }); + createCopy(blob); + }); + item.append(name, copy); + + if (query) { + if (isMatchingBlock(pageBlock, query)) { + matchingBlockFound = true; + } else { + item.classList.add('is-hidden'); + } + } + + blockList.append(item); + }); + if (query && matchingBlockFound) { + title.classList.remove('is-hidden'); + title.classList.add('is-open'); + } + }); +} diff --git a/express/blocks/link-list/link-list.js b/express/blocks/link-list/link-list.js index d9b5ad543..50bd085c8 100644 --- a/express/blocks/link-list/link-list.js +++ b/express/blocks/link-list/link-list.js @@ -73,6 +73,7 @@ export default async function decorate($block) { } if (window.location.href.includes('/express/templates/')) { - import('../../scripts/ckg-link-list.js'); + const { default: updateAsyncBlocks } = await import('../../scripts/ckg-link-list.js'); + await updateAsyncBlocks(); } } diff --git a/express/blocks/make-a-project/make-a-project.js b/express/blocks/make-a-project/make-a-project.js index c63b9f999..6ea9105c4 100644 --- a/express/blocks/make-a-project/make-a-project.js +++ b/express/blocks/make-a-project/make-a-project.js @@ -10,11 +10,8 @@ * governing permissions and limitations under the License. */ -import { - createTag, - addFreePlanWidget, -} from '../../scripts/scripts.js'; - +import { createTag } from '../../scripts/scripts.js'; +import { addFreePlanWidget } from '../../scripts/utils/free-plan.js'; import { buildCarousel } from '../shared/carousel.js'; export default function decorate($block) { diff --git a/express/blocks/marquee/marquee.css b/express/blocks/marquee/marquee.css new file mode 100644 index 000000000..7fae36314 --- /dev/null +++ b/express/blocks/marquee/marquee.css @@ -0,0 +1,411 @@ +body.no-desktop-brand-header header { + background-image: url(/express/icons/cc-express-logo.svg); + height: auto; +} + +.marquee-container, +.marquee-wide-container { + padding-top: 0; +} + +.section .marquee-wrapper { + position: relative; + max-width: unset; + padding: 0; +} + +.marquee { + text-align: left; + padding: 0; + margin: auto; + width: 100%; + box-sizing: border-box; +} + +.marquee.appear { + opacity: 1; + height: unset; +} + +.marquee p:first-child { /* p with icon above heading */ + margin-bottom: 8px; +} + +.marquee .marquee-foreground { + padding: 28px 20px; + position: relative; + box-sizing: border-box; + width: 100%; + max-width: 1440px; + margin: auto; +} + +.marquee .marquee-foreground > p:first-child img { + height: 32px; + width: unset; +} + +.marquee .marquee-foreground > :first-child { + margin-top: 0; +} + +.marquee .marquee-foreground > :last-child { + margin-bottom: 0; +} + +.marquee .content-wrapper h1 { + font-size: clamp(var(--heading-font-size-l), 4vw, var(--heading-font-size-xxl)); + margin: 0 0 8px 0; +} + +.marquee .content-wrapper h2 { + font-size: clamp(var(--heading-font-size-s), 3vw, var(--heading-font-size-l)); + margin: 0 0 8px 0; + text-align: left; +} + +.marquee p { + font-size: var(--body-font-size-m); + margin: 16px 0; +} + +.marquee p > a { + color: var(--color-black); + text-decoration: underline; +} + +.marquee p.button-container { + text-align: left; + margin: 16px 0; +} + +.marquee p.button-container a.button:any-link { + margin: 0; + white-space: nowrap; +} + +.marquee .marquee-background { + box-sizing: border-box; +} + +.marquee .background-wrapper { + position: relative; +} + +.marquee video.marquee-background { + z-index: -1; + object-fit: cover; + width: 100%; + min-height: 200px; + max-height: 500px; + object-position: center; +} + +.marquee.white-text, +.marquee.white-text a:any-link { + color: var(--color-white); +} + +.marquee.wide .marquee-background { + min-height: unset; + max-height: unset; +} + +.marquee.wide video.marquee-background { + max-height: 200px; + min-height: 200px; + object-position: 75% 100%; +} + +.marquee .hero-shadow { + display: none; +} + +.marquee .hero-shadow picture { + display: flex; +} + +.marquee .button-container.button-inline { + display: inline-block; + margin-right: 16px; +} + +.marquee .content-wrapper .button-container.free-plan-container { + display: flex; + justify-content: left; + align-items: flex-start; + flex-wrap: wrap; +} + +.marquee .button-container.button-inline.free-plan-container .free-plan-widget { + color: var(--body-color); + background: unset; + margin: unset; +} + +.marquee.dark .button-container.button-inline.free-plan-container .free-plan-widget { + color: var(--color-white); +} + +.marquee .button-container.button-inline.free-plan-container + .button-container.button-inline > a { + margin-left: 0; +} + +.marquee.wide .free-plan-widget { + margin: 0; + background-color: transparent; + padding-left: 0; +} + +.marquee p.button-container a.secondary::before { + display: inline-block; + width: 18px; + height: 20px; + margin-right: 10px; + vertical-align: sub; + top: 1px; + position: relative; + content: url(/express/icons/play.svg); +} + +.marquee p.underline a { + font-weight: normal; + text-decoration: underline; +} + +.marquee .reduce-motion-wrapper { + z-index: 9; + position: absolute; + white-space: nowrap; + right: 16px; + bottom: 16px; + height: 24px; + cursor: pointer; +} + +.marquee .reduce-motion-wrapper span { + position: absolute; + margin-right: 8px; + right: 100%; + top: 50%; + transform: translateY(-50%); + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; +} + +.marquee .reduce-motion-wrapper .icon { + height: 100%; + width: 100%; +} + +.marquee.reduce-motion .reduce-motion-wrapper .icon:nth-of-type(1), +.marquee .reduce-motion-wrapper .icon:nth-of-type(2) { + display: inline-block; +} + +.marquee.reduce-motion .reduce-motion-wrapper .icon:nth-of-type(2), +.marquee .reduce-motion-wrapper .icon:nth-of-type(1) { + display: none; +} + +.marquee.reduce-motion .reduce-motion-wrapper:hover span.play-animation-text, +.marquee:not(.reduce-motion) .reduce-motion-wrapper:hover span.pause-animation-text { + opacity: 1; +} + +@media (min-width: 600px) { + .marquee .marquee-foreground .content-wrapper { + display: block; + } + + .marquee.wide { + position: relative; + margin: 0 auto; + } + + .marquee.wide.fullwidth { + max-width: unset; + } + + .marquee.wide h1 { + font-size: var(--heading-font-size-l); + } + + .marquee.wide p { + font-size: var(--body-font-size-m); + position: relative; + margin: 12px 0; + } + + .marquee.wide .marquee-foreground > p:first-child img.icon { + height: 30px; + } + + .marquee .marquee-foreground > :first-child { + margin-top: 0; + } + + .marquee .marquee-foreground > :last-child { + margin-bottom: 0; + } +} + +@media (min-width: 900px) { + .marquee .background-wrapper { + position: unset; + } + + .marquee .marquee-background, + .marquee video.marquee-background { + position: absolute; + height: 100%; + max-height: unset; + } + + .marquee.wide video.marquee-background { + position: absolute; + top: 0; + left: 0; + bottom: auto; + right: auto; + height: 100%; + width: 100%; + z-index: unset; + object-fit: cover; + max-height: unset; + min-height: 250px; + object-position: center; + } + + .marquee.dark, + .marquee.dark a:any-link { + color: var(--color-white); + } + + body.no-desktop-brand-header header, body.no-brand-header header { + height: auto; + background-image: unset; + } + + .marquee .marquee-foreground { + min-height: max-content; + display: flex; + flex-direction: column; + justify-content: center; + } + + .marquee.wide .marquee-foreground { + box-sizing: border-box; + padding: 32px 20px; + min-height: 250px; + } + + .marquee .marquee-foreground .content-wrapper { + max-width: 46%; + } + + .marquee .hero-shadow { + display: flex; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-15%); + z-index: 2; + } + + .marquee p.button-container a.secondary { + background: transparent; + border-color: var(--color-black); + } + + .marquee.dark p.button-container a.secondary { + border-color: var(--color-white); + } + + .marquee p.button-container a.secondary:hover, .marquee p.button-container a.secondary:active { + background: rgba(255, 255, 255, 0.1); + border-color: var(--color-black); + } + + .marquee.dark p.button-container a.secondary:hover, .marquee.dark p.button-container a.secondary:active { + background: rgba(255, 255, 255, 0.1); + border-color: var(--color-white); + } + + .marquee p.button-container a.secondary::before { + filter: sepia(0%) saturate(1%) hue-rotate(172deg) brightness(110%) contrast(101%); + } + + .marquee.dark p.button-container a.secondary::before { + filter: invert(100%) sepia(0%) saturate(1%) hue-rotate(172deg) brightness(110%) contrast(101%); + } + + .marquee.wide .button-container.free-plan-container { + flex-direction: row; + align-items: center; + } + + .marquee.wide .button-container.free-plan-container a.button { + margin-right: 24px; + } + + .marquee.wide .button-container.free-plan-container.stacked { + flex-direction: column; + align-items: flex-start; + } + + .marquee .content-wrapper .button-container.free-plan-container { + align-items: center; + } + + .marquee.wide .button-container.free-plan-container .free-plan-widget { + margin-top: 0; + padding: 16px 24px; + } + + .marquee.wide .button-container.free-plan-container.stacked .free-plan-widget { + padding-left: 0; + } +} + +@media (min-width: 1200px) { + .marquee:not(.wide) .marquee-foreground { + min-height: 400px; + padding: 56px 20px; + } +} + +@media (min-width: 1400px) { + .marquee .marquee-foreground > p:first-child img.icon.icon-adobe-express { + height: 65px; + } + + .marquee p { + font-size: var(--body-font-size-l); + } +} + +@media (min-width: 1450px) { + .marquee .hero-shadow { + transform: translate(-15%, -2px); + } +} + +/* Japanese font sizing styles */ + +:lang(ja) .marquee h1 { + font-size: var(--heading-font-size-l); +} + +:lang(ja) .marquee h1.heading-long { + font-size: var(--heading-font-size-l); +} + +:lang(ja) .marquee h1.heading-very-long { + font-size: var(--heading-font-size-m); +} + +:lang(ja) .marquee h1.heading-x-long { + font-size: var(--heading-font-size-m); +} diff --git a/express/blocks/marquee/marquee.js b/express/blocks/marquee/marquee.js new file mode 100644 index 000000000..fdbec5ee3 --- /dev/null +++ b/express/blocks/marquee/marquee.js @@ -0,0 +1,363 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + addAnimationToggle, + createTag, + toClassName, + getLocale, + addHeaderSizing, + getIconElement, + fetchPlaceholders, +} from '../../scripts/scripts.js'; +import { addFreePlanWidget } from '../../scripts/utils/free-plan.js'; + +const breakpointConfig = [ + { + typeHint: 'default', + minWidth: 0, + }, + { + typeHint: 'mobile', + minWidth: 0, + }, + { + typeHint: 'desktop', + minWidth: 400, + }, + { + typeHint: 'hd', + minWidth: 1440, + }, +]; + +function getBreakpoint(animations) { + let breakpoint = 'default'; + breakpointConfig.forEach((bp) => { + if ((window.innerWidth > bp.minWidth) && animations[bp.typeHint]) breakpoint = bp.typeHint; + }); + return breakpoint; +} + +function buildReduceMotionSwitch(block) { + if (!block.querySelector('.reduce-motion-wrapper')) { + const reduceMotionIconWrapper = createTag('div', { class: 'reduce-motion-wrapper' }); + const videoWrapper = block.querySelector('.background-wrapper'); + const video = videoWrapper.querySelector('video'); + + if (block.classList.contains('dark')) { + reduceMotionIconWrapper.append(getIconElement('play-video-light'), getIconElement('pause-video-light')); + } else { + reduceMotionIconWrapper.append(getIconElement('play-video'), getIconElement('pause-video')); + } + + videoWrapper.append(reduceMotionIconWrapper); + + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + if (mediaQuery.matches) { + localStorage.setItem('reduceMotion', 'on'); + } + const initialValue = localStorage.getItem('reduceMotion') === 'on'; + + if (video) { + if (initialValue) { + block.classList.add('reduce-motion'); + video.currentTime = Math.floor(video.duration) / 2; + video.pause(); + } else { + video.muted = true; + video.play(); + } + } + + mediaQuery.addEventListener('change', (e) => { + const broswerValue = localStorage.getItem('reduceMotion') === 'on'; + if (broswerValue === e.matches) return; + localStorage.setItem('reduceMotion', e.matches ? 'on' : 'off'); + + if (localStorage.getItem('reduceMotion') === 'on') { + block.classList.add('reduce-motion'); + block.querySelector('video')?.pause(); + } else { + block.classList.remove('reduce-motion'); + block.querySelector('video')?.play(); + } + }); + + reduceMotionIconWrapper.addEventListener('click', () => { + localStorage.setItem('reduceMotion', localStorage.getItem('reduceMotion') === 'on' ? 'off' : 'on'); + + if (localStorage.getItem('reduceMotion') === 'on') { + block.classList.add('reduce-motion'); + block.querySelector('video')?.pause(); + } else { + block.classList.remove('reduce-motion'); + block.querySelector('video')?.play(); + } + }, { passive: true }); + + reduceMotionIconWrapper.addEventListener('mouseenter', async () => { + const placeholders = await fetchPlaceholders(); + const reduceMotionTextExist = block.querySelector('.play-animation-text') && block.querySelector('.pause-animation-text'); + + if (!reduceMotionTextExist) { + const play = createTag('span', { class: 'play-animation-text' }); + const pause = createTag('span', { class: 'pause-animation-text' }); + play.textContent = placeholders['play-animation'] || 'play animation'; + pause.textContent = placeholders['pause-animation'] || 'pause animation'; + + reduceMotionIconWrapper.prepend(play, pause); + } + }, { passive: true }); + } +} + +function createAnimation(animations) { + const attribs = { + class: 'marquee-background', + playsinline: '', + autoplay: '', + muted: '', + }; + + Object.keys(animations).forEach((k) => { + animations[k].active = false; + }); + + const breakpoint = getBreakpoint(animations); + const animation = animations[breakpoint]; + + if (animation === undefined) return null; + + if (animation.params.loop) { + attribs.loop = ''; + } + attribs.poster = animation.poster; + attribs.title = animation.title; + const { source } = animation; + animation.active = true; + + // replace anchor with video element + const video = createTag('video', attribs); + if (source) { + video.innerHTML = ``; + } + return video; +} + +function adjustLayout(animations, parent) { + const breakpoint = getBreakpoint(animations); + const animation = animations[breakpoint]; + + if (animation && !animation.active) { + const newVideo = createAnimation(animations); + if (newVideo) { + parent.replaceChild(newVideo, parent.querySelector('video')); + newVideo.addEventListener('canplay', () => { + if (!localStorage.getItem('reduceMotion') === 'on') { + newVideo.muted = true; + newVideo.play(); + } + }); + } + } +} + +async function transformToVideoLink(cell, a) { + const { isVideoLink, displayVideoModal } = await import('../shared/video.js'); + a.addEventListener('click', (e) => { + e.preventDefault(); + }); + a.setAttribute('rel', 'nofollow'); + const title = a.textContent.trim(); + + // gather video urls from all links in cell + const vidUrls = []; + [...cell.querySelectorAll(':scope a')] + .filter((link) => isVideoLink(link.href)) + .forEach((link) => { + vidUrls.push(link.href); + if (link !== a) { + if (link.classList.contains('button')) { + // remove button with container + link.closest('.button-container').remove(); + } else { + // remove link only + link.remove(); + } + } + }); + a.addEventListener('click', (e) => { + e.preventDefault(); + displayVideoModal(vidUrls, title); + }); + + a.addEventListener('keyup', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + displayVideoModal(vidUrls, title); + } + }); + + // autoplay if hash matches title + if (toClassName(title) === window.location.hash.substring(1)) { + displayVideoModal(vidUrls, title); + } +} + +export default async function decorate(block) { + const possibleBreakpoints = breakpointConfig.map((bp) => bp.typeHint); + const possibleOptions = ['shadow', 'background']; + const animations = {}; + const rows = [...block.children]; + + rows.forEach((div, index) => { + let rowType = 'animation'; + let typeHint; + if (index + 1 === rows.length) rowType = 'content'; + if ([...div.children].length > 1) typeHint = div.children[0].textContent.trim().toLowerCase(); + if (typeHint && possibleOptions.includes(typeHint)) { + rowType = 'option'; + } else if (!typeHint || (typeHint && !possibleBreakpoints.includes(typeHint))) { + typeHint = 'default'; + } + + if (rowType === 'animation') { + if (typeHint !== 'default') block.classList.add(`has-${typeHint}-animation`); + let source; + let videoParameters = {}; + const a = div.querySelector('a'); + const poster = div.querySelector('img'); + if (a) { + const url = new URL(a.href); + const params = new URLSearchParams(url.search); + videoParameters = { + loop: params.get('loop') !== 'false', + }; + const id = url.hostname.includes('hlx.blob.core') ? url.pathname.split('/')[2] : url.pathname.split('media_')[1].split('.')[0]; + source = `./media_${id}.mp4`; + } + let optimizedPosterSrc; + if (poster) { + const srcURL = new URL(poster.src); + const srcUSP = new URLSearchParams(srcURL.search); + srcUSP.set('format', 'webply'); + srcUSP.set('width', typeHint === 'mobile' ? 750 : 4080); + optimizedPosterSrc = `${srcURL.pathname}?${srcUSP.toString()}`; + } + + animations[typeHint] = { + source, + poster: optimizedPosterSrc || '', + title: (poster && poster.getAttribute('alt')) || '', + params: videoParameters, + }; + + div.remove(); + } + + if (rowType === 'content') { + const videoWrapper = createTag('div', { class: 'background-wrapper' }); + const video = createAnimation(animations); + let bg; + if (video) { + bg = videoWrapper; + videoWrapper.append(video); + div.prepend(videoWrapper); + video.addEventListener('canplay', () => { + buildReduceMotionSwitch(block); + }); + + window.addEventListener('resize', () => { + adjustLayout(animations, videoWrapper); + }, { passive: true }); + adjustLayout(animations, videoWrapper); + } else { + bg = createTag('div'); + bg.classList.add('marquee-background'); + div.prepend(bg); + } + + const marqueeForeground = createTag('div', { class: 'marquee-foreground' }); + bg.nextElementSibling.classList.add('content-wrapper'); + marqueeForeground.append(bg.nextElementSibling); + div.append(marqueeForeground); + + div.querySelectorAll(':scope p:empty').forEach((p) => { + if (p.innerHTML.trim() === '') { + p.remove(); + } + }); + + // check for video link + const videoLink = [...div.querySelectorAll('a')].find((a) => a.href.includes('youtu') + || a.href.includes('vimeo') + || /.*\/media_.*(mp4|webm|m3u8)$/.test(new URL(a.href).pathname)); + if (videoLink) { + transformToVideoLink(div, videoLink); + } + + const contentButtons = [...div.querySelectorAll('a.button.accent')]; + const buttonAsLink = contentButtons[2]; + const secondaryButton = contentButtons[1]; + buttonAsLink?.classList.remove('button'); + secondaryButton?.classList.add('secondary'); + secondaryButton?.classList.add('xlarge'); + const buttonContainers = [...div.querySelectorAll('p.button-container')]; + buttonContainers.forEach((button) => { + button.classList.add('button-inline'); + }); + } + + if (rowType === 'option') { + if (typeHint === 'shadow') { + const shadow = (div.querySelector('picture')) ? div.querySelector('picture') : createTag('img', { src: '/express/blocks/marquee/shadow.png' }); + div.innerHTML = ''; + div.appendChild(shadow); + div.classList.add('hero-shadow'); + } + if (typeHint === 'background') { + const color = div.children[1].textContent.trim().toLowerCase(); + if (color) block.style.background = color; + const lightness = ( + parseInt(color.substring(1, 2), 16) + + parseInt(color.substring(3, 2), 16) + + parseInt(color.substring(5, 2), 16)) / 3; + if (lightness < 200) block.classList.add('white-text'); + div.remove(); + } + } + }); + + if (block.classList.contains('shadow') && !block.querySelector('.hero-shadow')) { + const shadowDiv = createTag('div', { class: 'hero-shadow' }); + const shadow = createTag('img', { src: '/express/blocks/marquee/shadow.png' }); + shadowDiv.appendChild(shadow); + block.appendChild(shadowDiv); + } + + const button = block.querySelector('.button'); + if (button) { + button.classList.add('xlarge'); + await addFreePlanWidget(button.parentElement); + } + + if (block.classList.contains('wide')) { + addAnimationToggle(block); + } + + if (getLocale(window.location) === 'jp') { + addHeaderSizing(block); + } + + block.classList.add('appear'); +} diff --git a/express/blocks/marquee/shadow.png b/express/blocks/marquee/shadow.png new file mode 100644 index 000000000..213ef3219 Binary files /dev/null and b/express/blocks/marquee/shadow.png differ diff --git a/express/blocks/multifunction-button/multifunction-button.js b/express/blocks/multifunction-button/multifunction-button.js index 1e6bdb69d..184a73eb8 100644 --- a/express/blocks/multifunction-button/multifunction-button.js +++ b/express/blocks/multifunction-button/multifunction-button.js @@ -42,7 +42,7 @@ function initNotchDragAction($wrapper, data) { const $toolBox = $wrapper.querySelector('.toolbox'); const $lottie = $wrapper.querySelector('.floating-button-lottie'); let touchStart = 0; - const initialHeight = $toolBox.offsetHeight; + const initialHeight = $toolBox.clientHeight; $notch.addEventListener('touchstart', (e) => { $body.style.overflow = 'hidden'; $toolBox.style.transition = 'none'; @@ -50,7 +50,7 @@ function initNotchDragAction($wrapper, data) { }, { passive: true }); $notch.addEventListener('touchmove', (e) => { - $toolBox.style.maxHeight = `${initialHeight - (e.changedTouches[0].clientY - touchStart)}px`; + $toolBox.style.maxHeight = `${(initialHeight + 88) - (e.changedTouches[0].clientY - touchStart)}px`; }, { passive: true }); $notch.addEventListener('touchend', (e) => { @@ -106,6 +106,11 @@ export default async function decorate($block) { } const data = await collectFloatingButtonData(); - await createMultiFunctionButton($block, data, audience); + const blockWrapper = await createMultiFunctionButton($block, data, audience); + const blockLinks = blockWrapper.querySelectorAll('a'); + if (blockLinks && blockLinks.length > 0) { + const linksPopulated = new CustomEvent('linkspopulated', { detail: blockLinks }); + document.dispatchEvent(linksPopulated); + } } } diff --git a/express/blocks/plans-comparison/plans-comparison.js b/express/blocks/plans-comparison/plans-comparison.js index e5755799a..d69f8f958 100644 --- a/express/blocks/plans-comparison/plans-comparison.js +++ b/express/blocks/plans-comparison/plans-comparison.js @@ -14,9 +14,9 @@ import { createTag, fixIcons, getIconElement, - getOffer, fetchPlainBlockFromFragment, } from '../../scripts/scripts.js'; +import { getOffer } from '../../scripts/utils/pricing.js'; async function fetchPlan(planUrl) { if (!window.pricingPlans) { diff --git a/express/blocks/pricing-columns/pricing-columns.js b/express/blocks/pricing-columns/pricing-columns.js index 99f3463dd..1e26c5571 100644 --- a/express/blocks/pricing-columns/pricing-columns.js +++ b/express/blocks/pricing-columns/pricing-columns.js @@ -17,9 +17,9 @@ import { createTag, fetchPlaceholders, getHelixEnv, - getOffer, // eslint-disable-next-line import/no-unresolved } from '../../scripts/scripts.js'; +import { getOffer } from '../../scripts/utils/pricing.js'; function replaceUrlParam(url, paramName, paramValue) { const params = url.searchParams; diff --git a/express/blocks/pricing-hub/pricing-hub.js b/express/blocks/pricing-hub/pricing-hub.js index 72a6c731f..970bd8b53 100644 --- a/express/blocks/pricing-hub/pricing-hub.js +++ b/express/blocks/pricing-hub/pricing-hub.js @@ -16,9 +16,9 @@ import { getHelixEnv, getLottie, lazyLoadLottiePlayer, - getOffer, getIconElement, fetchPlaceholders, } from '../../scripts/scripts.js'; +import { getOffer } from '../../scripts/utils/pricing.js'; /* eslint-disable import/named, import/extensions */ import { diff --git a/express/blocks/pricing-summary/pricing-summary.css b/express/blocks/pricing-summary/pricing-summary.css new file mode 100644 index 000000000..c653d8a89 --- /dev/null +++ b/express/blocks/pricing-summary/pricing-summary.css @@ -0,0 +1,137 @@ +main .pricing-summary { + padding: 90px 0 20px 0; + width: auto; +} + +main .pricing-summary .pricing-container { + margin: 0 10px 0 10px; +} + +main .pricing-summary .pricing-container .columns-container { + display: flex; + flex-direction: column; + justify-content: center; + gap: 20px; + align-items: center; +} + +main .pricing-summary .pricing-column { + background-color: var(--color-white); + border-radius: 20px; + min-width: 370px; + width: 100%; + max-width: 420px; + min-height: 345px; + display: flex; + flex-direction: column; + align-items: center; + padding: 23px 0 0 0; + justify-items: center; + justify-content: center; +} + +main .pricing-summary .pricing-header { + display: flex; + flex-direction: row-reverse; + align-items: center; + justify-content: center; + gap: 10px; + font-size: var(--heading-font-size-l); +} + +main .pricing-summary .pricing-description { + max-width: 330px; + display: flex; + flex-direction: column; + margin: 17px 0 0 0; +} + +main .pricing-summary .pricing-description .details-cta, +main .pricing-summary .pricing-description .details-cta:hover { + background-color: transparent; + color: var(--color-info-accent); + border: none; + margin: 0; +} + +main .pricing-summary .pricing-description > p, +main .pricing-summary .pricing-plan > p { + font-size: var(--body-font-size-s); +} + +main .pricing-summary .pricing-plan { + display: flex; + flex-direction: column; + justify-content: center; + margin: 10px 0 30px 0; + max-width: 330px; + gap: 15px; +} + +main .pricing-summary .pricing-price { + display: flex; + justify-content: center; + font-size: var(--heading-font-size-xs); + font-weight: 900; +} + +main .pricing-summary .pricing-price > strong { + font-size: var(--heading-font-size-s); + font-weight: 900; +} + +main .pricing-summary .pricing-description > p, +main .pricing-summary .pricing-plan > p { + margin: 0; + line-height: 22px; +} + +main .pricing-summary .spacer { + flex: 1; +} + +main .pricing-summary .pricing-column .button-container { + margin: 0 0 10px 0; +} + +main .pricing-summary .pricing-column .button-container .details-cta { + padding: 0; + font-size: var(--body-font-size-s); + box-shadow: none; +} + +main .pricing-summary .pricing-column .button-container .dark { + border: 2px solid var(--color-black); +} + +main .pricing-summary > div:first-of-type > div { + display: flex; + flex-direction: column; + gap: 16px; + margin: 0 0 60px 0; +} + +main .pricing-summary > div:first-of-type > div > h2 { + font-size: var(--heading-font-size-xl); +} + +main .pricing-summary > div:first-of-type > div > p { + margin: 0; +} + +main .pricing-summary > div:last-of-type > div { + font-size: var(--body-font-size-xs); + margin: 18px 0 0 0; +} + +@media (min-width: 1200px) { + main .section .pricing-summary-wrapper { + max-width: 100%; + } + + main .pricing-summary .pricing-container .columns-container { + display: flex; + flex-direction: row; + align-items: stretch; + } +} diff --git a/express/blocks/pricing-summary/pricing-summary.js b/express/blocks/pricing-summary/pricing-summary.js new file mode 100644 index 000000000..2e98b3bea --- /dev/null +++ b/express/blocks/pricing-summary/pricing-summary.js @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { createTag } from '../../scripts/scripts.js'; +import { getOffer } from '../../scripts/utils/pricing.js'; + +async function fetchPlan(planUrl) { + if (!window.pricingPlans) { + window.pricingPlans = {}; + } + + let plan = window.pricingPlans[planUrl]; + + if (!plan) { + plan = {}; + const link = new URL(planUrl); + const params = link.searchParams; + + plan.url = planUrl; + plan.country = 'us'; + plan.language = 'en'; + plan.price = '9.99'; + plan.currency = 'US'; + plan.symbol = '$'; + + if (planUrl.includes('/sp/')) { + plan.offerId = 'FREE0'; + plan.frequency = 'monthly'; + plan.name = 'Free'; + plan.stringId = 'free-trial'; + } else { + plan.offerId = params.get('items[0][id]'); + plan.frequency = null; + plan.name = 'Premium'; + plan.stringId = '3-month-trial'; + } + + if (plan.offerId === '70C6FDFC57461D5E449597CC8F327CF1' || plan.offerId === 'CFB1B7F391F77D02FE858C43C4A5C64F') { + plan.frequency = 'Monthly'; + } else if (plan.offerId === 'E963185C442F0C5EEB3AE4F4AAB52C24' || plan.offerId === 'BADDACAB87D148A48539B303F3C5FA92') { + plan.frequency = 'Annual'; + } else { + plan.frequency = null; + } + + const countryOverride = new URLSearchParams(window.location.search).get('country'); + const offer = await getOffer(plan.offerId, countryOverride); + + if (offer) { + plan.currency = offer.currency; + plan.price = offer.unitPrice; + plan.formatted = `${offer.unitPriceCurrencyFormatted}`; + plan.country = offer.country; + plan.vatInfo = offer.vatInfo; + plan.language = offer.lang; + plan.rawPrice = offer.unitPriceCurrencyFormatted.match(/[\d\s,.+]+/g); + plan.prefix = offer.prefix ?? ''; + plan.suffix = offer.suffix ?? ''; + plan.formatted = plan.formatted.replace(plan.rawPrice[0], `${plan.prefix}${plan.rawPrice[0]}${plan.suffix}`); + } + } + + return plan; +} + +function handleHeader(column) { + column.classList.add('pricing-column'); + + const title = column.querySelector('h2'); + const icon = column.querySelector('img'); + const header = createTag('div', { class: 'pricing-header' }); + + header.append(title, icon); + + return header; +} + +function handlePrice(column) { + const price = column.querySelector('[title="{{pricing}}"]'); + const priceContainer = price?.parentNode; + const plan = priceContainer?.nextElementSibling; + + const priceText = createTag('div', { class: 'pricing-price' }); + const pricePlan = createTag('div', { class: 'pricing-plan' }); + + pricePlan.append(priceText, plan); + + fetchPlan(price?.href).then((response) => { + priceText.innerHTML = response.formatted; + }); + + priceContainer?.remove(); + + return pricePlan; +} + +function handleCtas(column) { + const ctaContainers = column.querySelectorAll('.button-container'); + + const ctas = column.querySelectorAll('a'); + ctas[0]?.classList.add(ctas[1] ? 'details-cta' : 'cta', 'xlarge'); + ctas[1]?.classList.add('cta', 'xlarge'); + + ctaContainers.forEach((container) => { + container.querySelector('em')?.children[0]?.classList.add('secondary', 'dark'); + }); + + return ctaContainers[ctaContainers.length - 1]; +} + +function handleDescription(column) { + const description = createTag('div', { class: 'pricing-description' }); + const texts = [...column.children]; + + texts.pop(); + + description.append(...texts); + + return description; +} + +export default function decorate(block) { + const pricingContainer = block.children[1]; + + pricingContainer.classList.add('pricing-container'); + + const columnsContainer = createTag('div', { class: 'columns-container' }); + + const columns = Array.from(pricingContainer.children); + + columns.forEach((column) => { + const header = handleHeader(column); + const pricePlan = handlePrice(column); + const cta = handleCtas(column); + const description = handleDescription(column); + const spacer = createTag('div', { class: 'spacer' }); + + column.append(header, description, spacer, pricePlan, cta); + columnsContainer.append(column); + }); + + pricingContainer.append(columnsContainer); +} diff --git a/express/blocks/pricing/pricing.js b/express/blocks/pricing/pricing.js index 4311e21ea..b2c7aa49c 100644 --- a/express/blocks/pricing/pricing.js +++ b/express/blocks/pricing/pricing.js @@ -15,10 +15,10 @@ import { addPublishDependencies, createTag, getHelixEnv, - getOffer, getIcon, // eslint-disable-next-line import/no-unresolved } from '../../scripts/scripts.js'; +import { getOffer } from '../../scripts/utils/pricing.js'; function replaceUrlParam(url, paramName, paramValue) { const params = url.searchParams; diff --git a/express/blocks/promotion/promotion.js b/express/blocks/promotion/promotion.js index 093228ae8..04614027f 100644 --- a/express/blocks/promotion/promotion.js +++ b/express/blocks/promotion/promotion.js @@ -18,8 +18,6 @@ import { fixIcons, toClassName, createOptimizedPicture, - trackBranchParameters, -// eslint-disable-next-line import/no-unresolved } from '../../scripts/scripts.js'; const PROMOTION_FOLDER = 'express/promotions'; @@ -95,6 +93,5 @@ export default async function decorate($block) { if ($links) { const linksPopulated = new CustomEvent('linkspopulated', { detail: $links }); document.dispatchEvent(linksPopulated); - trackBranchParameters($links); } } diff --git a/express/blocks/puf/puf.css b/express/blocks/puf/puf.css index 955cd33e2..29c41d5ab 100644 --- a/express/blocks/puf/puf.css +++ b/express/blocks/puf/puf.css @@ -1,355 +1,462 @@ main .puf-container > div { - max-width: none; + max-width: none; } main .puf.block .carousel-platform { - align-items: flex-start; + align-items: flex-start; } main .puf.block .carousel-container { - overflow: hidden; - transition: max-height 0.3s, min-height 0.3s; - max-width: 600px; + overflow: visible; + transition: max-height 0.3s, min-height 0.3s; + max-width: 600px; } main .puf.block .carousel-container .carousel-fader-left, main .puf.block .carousel-container .carousel-fader-right { - align-items: flex-start; + align-items: flex-start; } main .puf.block .carousel-container a.button.carousel-arrow { - margin-top: calc(100vh / 2); - position: sticky; + margin-top: 50vh; + position: sticky; } main .block.puf { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; } main .block.puf p { - margin: 0; + margin: 0; } main .block.puf .puf-card-container { - width: 100%; - display: flex; - justify-content: center; - margin: 20px 0; + width: 100%; + display: flex; + justify-content: center; + border-radius: 20px; } main .block.puf .puf-card { - border: 1px var(--color-gray-100) solid; - box-shadow: 0 0 20px #0000001A; - border-radius: 20px; - box-sizing: border-box; - padding: 40px 30px; - width: 325px; + border-radius: 20px; + box-sizing: border-box; + padding: 32px 30px 16px 30px; + width: 325px; + background-color: var(--color-white); } main .block.puf .puf-card .puf-card-top { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin-bottom: 10px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-bottom: 10px; } main .block.puf .puf-card .puf-card-top h3 { - display: flex; - align-items: center; - justify-content: center; - font-size: var(--heading-font-size-l); - line-height: var(--heading-line-height); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--heading-font-size-l); + line-height: var(--heading-line-height); +} + +main .block.puf .puf-card h3 { + width: 100%; } main .block.puf .puf-card .puf-card-top h2 { - display: flex; - font-size: var(--heading-font-size-m); - font-weight: var(--heading-font-weight); - margin: 20px 0 0 0; + display: flex; + font-size: var(--heading-font-size-m); + font-weight: var(--heading-font-weight); + margin: 20px 0 0 0; } main .block.puf .puf-card .puf-card-top h2 strong { - font-size: var(--heading-font-size-xxl); - font-weight: 800; - margin-top: -10px; + font-size: var(--heading-font-size-xxl); + font-weight: 800; + margin-top: -10px; } main .block.puf .puf-card .puf-card-top h2.jpy strong { - margin-right: 10px; + margin-right: 10px; } main .block.puf .puf-card .puf-card-top h3 svg { - margin-right: 10px; - width: 32px; - height: 32px; + margin-right: 10px; + width: 32px; + height: 32px; } main .block.puf .puf-card .puf-card-top a { - margin-top: 20px; - width: 100%; - box-sizing: border-box; + margin-top: 20px; + width: 100%; + box-sizing: border-box; } main .block.puf .puf-card .puf-card-top p { - font-size: var(--body-font-size-m); - margin-bottom: 20px; + font-size: var(--body-font-size-m); + margin-bottom: 20px; + text-align: left; } - main .block.puf .puf-card .puf-card-top .puf-card-plans { - display: flex; - justify-content: space-between; - align-items: center; - margin: 0 auto 20px; + display: flex; + justify-content: space-between; + align-items: center; + margin: 0 auto; } main .block.puf .puf-card .puf-card-top .puf-card-plans > div { - color: var(--color-info-accent); - text-align: right; + color: var(--color-info-accent); + text-align: right; } main .block.puf .puf-card .puf-card-top .puf-card-plans > div span { - color: var(--color-gray-800); -} - -main .block.puf .puf-card .puf-card-top .puf-card-plans > div.strong { - -webkit-text-stroke-width: .6px; - -webkit-text-stroke-color: var(--color-info-accent); + color: var(--color-gray-800); } main .block.puf .puf-card .puf-card-top .puf-card-plans > div.strong span { + -webkit-text-stroke-width: 0.6px; -webkit-text-stroke-color: var(--color-gray-800); } main .block.puf .puf-card .puf-card-top .puf-card-switch { - position: relative; - display: inline-block; - width: 58px; - height: 31px; - margin: 0 10px; + position: relative; + display: inline-block; + width: 58px; + height: 31px; + margin: 0 10px; + min-width: 58px; } main .block.puf .puf-card .puf-card-top .puf-card-switch input { - opacity: 0; - width: 0; - height: 0; + opacity: 0; + width: 0; + height: 0; } main .block.puf .puf-card .puf-card-top .puf-card-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--color-info-accent); - -webkit-transition: .4s; - transition: .4s; - border-radius: 34px; + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--color-info-accent); + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 34px; } main .block.puf .puf-card .puf-card-top .puf-card-slider::before { - position: absolute; - content: ""; - height: 23px; - width: 23px; - left: 4px; - bottom: 4px; - background-color: white; - -webkit-transition: .4s; - transition: .4s; - border-radius: 50%; -} - -main .block.puf .puf-card .puf-card-top .puf-card-switch input:checked + .puf-card-slider { - background-color: var(--color-info-accent); -} - -main .block.puf .puf-card .puf-card-top .puf-card-switch input:focus + .puf-card-slider { - box-shadow: 0 0 1px #2196F3; -} - -main .block.puf .puf-card .puf-card-top .puf-card-switch input:checked + .puf-card-slider:before { - -webkit-transform: translateX(26px); - -ms-transform: translateX(26px); - transform: translateX(26px); + position: absolute; + content: ""; + height: 23px; + width: 23px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 50%; +} + +main + .block.puf + .puf-card + .puf-card-top + .puf-card-switch + input:checked + + .puf-card-slider { + background-color: var(--color-info-accent); +} + +main + .block.puf + .puf-card + .puf-card-top + .puf-card-switch + input:focus + + .puf-card-slider { + box-shadow: 0 0 1px #2196f3; +} + +main + .block.puf + .puf-card + .puf-card-top + .puf-card-switch + input:checked + + .puf-card-slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); } main .block.puf .puf-card .puf-card-bottom { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; } main .block.puf .puf-card .puf-card-bottom h3 { - font-size: var(--body-font-size-l); - font-weight: 600; - margin-top: 10px; - margin-bottom: 40px; + font-size: var(--body-font-size-xl); + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; } main .block.puf .puf-card .puf-card-bottom p { - font-size: var(--body-font-size-m); - margin-bottom: 40px; + font-size: var(--body-font-size-l); + margin-bottom: 40px; } main .block.puf .puf-card .puf-card-bottom svg { - display: none; + display: none; } main .block.puf .puf-card .puf-card-bottom h6 { - font-weight: var(--body-font-weight); - font-size: var(--body-font-size-s); - margin: 0 0 40px 0; + font-weight: var(--body-font-weight); + font-size: var(--body-font-size-s); + margin: 0; } main .puf .puf-right { - width: 375px; + width: 375px; +} + +main .block.puf .puf-card .puf-sup { + font-size: 0.5em; +} + +main .block.puf .puf-card .puf-text-and-sup-wrapper { + display: inline; +} + +main + .block.puf + .puf-card-banner.recommended + ~ .puf-card-bottom + .puf-highlighted-text, +main .block.puf .puf-highlighted-text { + background-color: var(--color-info-accent-light); + width: 100%; + border-radius: 12px; + margin-top: 0; + box-sizing: border-box; + padding: 10px; +} + +main .block.puf .puf-pricing-footer { + max-width: 1200px; + width: 100%; +} + +main .block.puf .puf-pricing-footer .puf-highlighted-text { + padding: 16px 0; +} + +main .block.puf .puf-card-banner.recommended { + color: white; + font-size: var(--body-font-size-xl); + font-weight: 700; +} + +main .block.puf .puf-card-container:not(:has(.recommended)) { + flex: 1; + align-self: stretch; + border-top: 1px #e5e5e5 solid; + border-left: 1px #e5e5e5 solid; + border-bottom: 1px #e5e5e5 solid; + margin: 70px -16px 0 0; + padding: 0 0 8px 0; + border-radius: 20px 0 0 20px; +} + +main .block.puf .puf-card-container:has(.recommended) { + background: linear-gradient(156deg, #ff477b 0%, #5c5ce0 52%, #318fff 100%); + box-shadow: 0px 0px 24px #00000029; + padding: 48px 8px 8px 8px; + margin-top: 20px; + height: auto; + z-index: 0; +} + +main .block.puf .puf-card .puf-card-banner { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 0px; + left: 0; + width: 100%; + height: 50px; + border-radius: 20px 20px 0 0; } @media (min-width: 900px) { - main .puf.block { - margin-bottom: 100px; - } - main .puf.block .carousel-container { - max-width: none; - max-height: none !important; - } - - main .block.puf .puf-card-container { - display: block; - width: auto; - padding: 30px 0 0 0; - } - - main .block.puf .carousel-platform { - max-height: unset; - margin: 0 auto; - } - - main .block.puf .puf-card { - position: relative; - width: 610px; - margin: 0; - padding: 40px 60px; - } - - main .block.puf .puf-card.puf-left { - border-radius: 20px 0 0 20px; - box-shadow: none; - border-top: 1px #E5E5E5 solid; - border-left: 1px #E5E5E5 solid; - border-bottom: 1px #E5E5E5 solid; - border-right: none; - margin-left: 20px; - min-height: 1200px; - } - - main .block.puf .puf-card.puf-right { - border-radius: 0 0 20px 20px; - margin-right: 20px; - min-height: 1260px; - } - - main .block.puf .puf-card .puf-card-banner { - display: flex; - align-items: center; - justify-content: center; - position: absolute; - top: -50px; - left: 0; - width: 100%; - height: 50px; - background-color: var(--color-premium); - border-radius: 20px 20px 0 0; - } - - main .block.puf .puf-card .puf-card-top { - justify-content: flex-start; - min-height: 320px; - } - - main .block.puf .puf-card .puf-card-top h2 { - margin: 10px 0 0 0; - } - - main .block.puf .puf-card .puf-vat-info { - height: 24px; - line-height: 1; - } - - main .block.puf .puf-card .puf-card-top p { - text-align: left; - } - - main .block.puf .puf-card .puf-card-top p:last-child { - min-height: 75px; - } - - main .block.puf .puf-card .puf-card-top .puf-card-plans { - min-height: 40px; - } - - main .block.puf .puf-card .puf-card-bottom { - align-items: flex-start; - } - - main .block.puf .puf-card .puf-card-bottom p { - display: flex; - align-items: center; - margin-bottom: 24px; - text-align: left; - font-size: 18px; - } - - main .block.puf .puf-card .puf-card-bottom h6 { - text-align: left; - } - - main .block.puf .puf-card .puf-card-bottom svg { - display: inline-block; - margin-right: 16px; - width: 44px; - height: 44px; - flex: 0 0 44px - } - - main .block.puf .puf-card .puf-card-bottom .puf-list-item { - position: relative; - } - - main .block.puf .puf-card .puf-card-bottom .puf-list-item::before { - position: absolute; - display: block; - content: ''; - left: -30px; - top: 50%; - transform: translateY(-50%); - width: 13px; - height: 11.5px; - background-color: var(--color-info-accent); - -webkit-mask-image: url('/express/blocks/puf/check.svg'); - mask-image: url('/express/blocks/puf/check.svg'); - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - -webkit-mask-size: contain; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - } + main .puf.block .carousel-container { + max-width: none; + max-height: none !important; + } + + main .block.puf .puf-card-container { + width: auto; + margin-top: 70px; + padding-top: 0px; + } + + main .puf.block .carousel-container .carousel-fader-right { + right: -3rem; + } + + main .puf .puf-card-container:has(.recommended) { + padding: 48px 0 0 0; + padding: 48px 8px 8px 8px; + } + + main .block.puf .carousel-container { + margin-bottom: 0; + } + + main .block.puf .carousel-platform { + max-height: unset; + margin: 0 auto; + padding: 14px 24px 0 24px; + margin-top: -14px; + } + + main + .block.puf + .carousel-container:has(+ .puf-pricing-footer) + .carousel-platform { + padding-bottom: 24px; + } + + main .block.puf .puf-card { + position: relative; + width: 610px; + margin: 0; + padding: 40px 60px; + } + + main .block.puf .puf-card.puf-left { + border-radius: 20px; + border-right: none; + min-height: 1200px; + } + + main .block.puf .puf-card-container:not(:has(.recommended)) .puf-card { + padding-left: 60px; + padding-right: 76px; + } + + main .block.puf .puf-card.puf-right { + min-height: 1260px; + } + + main .block.puf .puf-card .puf-card-banner { + top: -50px; + } + + main .block.puf .puf-card .puf-card-top { + justify-content: flex-start; + min-height: 320px; + } + + main .block.puf .puf-card .puf-card-top h2 { + margin: 10px 0 0 0; + } + + main .block.puf .puf-card .puf-vat-info { + height: 24px; + line-height: 1; + } + + main .block.puf .puf-card .puf-card-top p { + text-align: left; + } + + main .block.puf .puf-card .puf-card-top p:last-child { + min-height: 75px; + } + + main .block.puf .puf-card .puf-card-top .puf-card-plans { + min-height: 40px; + } + + main .block.puf .puf-card .puf-card-bottom { + align-items: flex-start; + } + + main .block.puf .puf-card .puf-card-bottom p { + display: flex; + align-items: center; + margin-bottom: 24px; + text-align: left; + } + + main .block.puf .puf-card .puf-card-bottom h6 { + text-align: left; + } + + main .block.puf .puf-card .puf-card-bottom svg { + display: inline-block; + margin-right: 16px; + width: 44px; + height: 44px; + flex: 0 0 44px; + } + + main .block.puf .puf-card .puf-card-bottom .puf-list-item { + position: relative; + } + + main .block.puf .puf-card .puf-card-bottom .puf-list-item::before { + position: absolute; + display: block; + content: ""; + left: -30px; + top: 50%; + transform: translateY(-50%); + width: 13px; + height: 11.5px; + background-color: var(--color-info-accent); + -webkit-mask-image: url("/express/blocks/puf/check.svg"); + mask-image: url("/express/blocks/puf/check.svg"); + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + -webkit-mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } } @media (max-width: 899px) { + main .section.puf-container { + padding-bottom: 0; + } + main .block.puf .carousel-container { position: relative; overflow: hidden; + margin-bottom: 0; + padding: 0 12px 12px 0; } + + main .block.puf .puf-card .puf-card-top .puf-card-plans { + margin-top: 16px; + } + main .block.puf .carousel-platform { display: block; position: static; @@ -358,31 +465,69 @@ main .puf .puf-right { width: 100%; min-height: 100%; } + + main .block.puf .puf-card .puf-card-bottom p { + margin-bottom: 24px; + } + main .block.puf .carousel-container.slide-2-selected .carousel-fader-right, main .block.puf .carousel-container.slide-1-selected .carousel-fader-left { opacity: 0; pointer-events: none; } - main .block.puf .carousel-container.slide-1-selected .carousel-fader-right, + + main .block.puf .carousel-container.slide-1-selected .carousel-fader-right { + right: 1rem; + opacity: 1; + pointer-events: auto; + } + main .block.puf .carousel-container.slide-2-selected .carousel-fader-left { + left: 0.7rem; opacity: 1; pointer-events: auto; } + main .block.puf .carousel-container .carousel-platform .puf-card-container { position: absolute; + max-width: fit-content; transition: left 0.5s; transform: translateX(-50%); right: auto; top: 0; left: 50%; left: calc(50%); + border-right: 1px #e5e5e5 solid; + border-radius: 20px; + margin: 20px 0 0 0; + box-shadow: 0px 0px 20px #00000029; } - main .block.puf .carousel-container.slide-2-selected .carousel-platform .puf-card-container:first-child { + + main + .block.puf + .puf-card-banner.recommended + ~ .puf-card-bottom + .puf-highlighted-text { + display: flex; + align-items: center; + justify-content: center; + } + + main + .block.puf + .carousel-container.slide-2-selected + .carousel-platform + .puf-card-container:first-child { left: -150%; left: calc(-150%); } - main .block.puf .carousel-container.slide-1-selected .carousel-platform .puf-card-container:last-child { + + main + .block.puf + .carousel-container.slide-1-selected + .carousel-platform + .puf-card-container:last-child { left: 150%; left: calc(150%); } -} \ No newline at end of file +} diff --git a/express/blocks/puf/puf.js b/express/blocks/puf/puf.js index 4be38b1d0..b1cc781b0 100644 --- a/express/blocks/puf/puf.js +++ b/express/blocks/puf/puf.js @@ -13,9 +13,9 @@ import { addPublishDependencies, createTag, getHelixEnv, - getOffer, // eslint-disable-next-line import/no-unresolved } from '../../scripts/scripts.js'; +import { getOffer } from '../../scripts/utils/pricing.js'; import { buildCarousel } from '../shared/carousel.js'; @@ -136,7 +136,10 @@ async function fetchPlan(planUrl) { plan.currency = 'US'; plan.symbol = '$'; - if (planUrl.includes('/sp/')) { + // TODO: Remove '/sp/ once confirmed with stakeholders + const allowedHosts = ['new.express.adobe.com', 'express.adobe.com', 'adobesparkpost.app.link']; + const { host } = new URL(planUrl); + if (allowedHosts.includes(host) || planUrl.includes('/sp/')) { plan.offerId = 'FREE0'; plan.frequency = 'monthly'; plan.name = 'Free'; @@ -287,6 +290,8 @@ function decorateCard($block, cardClass) { if (!$cardBanner.textContent.trim()) { $cardBanner.style.display = 'none'; + } else { + $cardBanner.classList.add('recommended'); } $cardHeader.prepend($cardHeaderSvg); @@ -325,6 +330,7 @@ function updatePUFCarousel($block) { const $carouselPlatform = $block.querySelector('.carousel-platform'); let $leftCard = $block.querySelector('.puf-left'); let $rightCard = $block.querySelector('.puf-right'); + let priceSet = $block.querySelector('.puf-pricing-header').textContent; $carouselContainer.classList.add('slide-1-selected'); const slideFunctionality = () => { $carouselPlatform.scrollLeft = $carouselPlatform.offsetWidth; @@ -339,9 +345,10 @@ function updatePUFCarousel($block) { } else { $carouselContainer.classList.remove('slide-1-selected'); $carouselContainer.classList.add('slide-2-selected'); - $carouselContainer.style.minHeight = `${$rightCard.clientHeight + 40}px`; + $carouselContainer.style.minHeight = `${$rightCard.clientHeight + 110}px`; } }; + $leftArrow.addEventListener('click', () => changeSlide(0)); $rightArrow.addEventListener('click', () => changeSlide(1)); $block.addEventListener('keyup', (e) => { @@ -376,28 +383,148 @@ function updatePUFCarousel($block) { }; $block.addEventListener('touchstart', startTouch, false); $block.addEventListener('touchmove', moveTouch, false); + const mediaQuery = window.matchMedia('(min-width: 900px)'); + mediaQuery.onchange = () => { + $carouselContainer.style.minHeight = `${$leftCard.clientHeight + 40}px`; + }; }; + const waitForCardsToLoad = setInterval(() => { - if (!$leftCard && !$rightCard) { - $leftCard = $block.querySelector('.puf-left'); - $rightCard = $block.querySelector('.puf-right'); - } else { + if ($leftCard && $rightCard && priceSet) { clearInterval(waitForCardsToLoad); slideFunctionality(); + } else { + $leftCard = $block.querySelector('.puf-left'); + $rightCard = $block.querySelector('.puf-right'); + priceSet = $block.querySelector('.puf-pricing-header').textContent; } }, 400); } +function wrapTextAndSup($block) { + const supTags = $block.getElementsByTagName('sup'); + Array.from(supTags).forEach((supTag) => { + supTag.classList.add('puf-sup'); + }); + + const $listItems = $block.querySelectorAll('.puf-list-item'); + $listItems.forEach(($listItem) => { + const $childNodes = $listItem.childNodes; + + const filteredChildren = Array.from($childNodes).filter((node) => { + const isSvg = node.tagName && node.tagName.toLowerCase() === 'svg'; + const isTextNode = node.nodeType === Node.TEXT_NODE; + return !isSvg && (isTextNode || node.nodeType === Node.ELEMENT_NODE); + }); + + const filteredChildrenExceptFirstText = filteredChildren.slice(1); + + const $textAndSupWrapper = createTag('div', { class: 'puf-text-and-sup-wrapper' }); + $textAndSupWrapper.append(...filteredChildrenExceptFirstText); + $listItem.append($textAndSupWrapper); + }); +} + +function highlightText($block) { + const $highlightRegex = /^\(\(.*\)\)$/; + const $blockElements = Array.from($block.querySelectorAll('*')); + + if (!$blockElements.some(($element) => $highlightRegex.test($element.textContent))) { + return; + } + + const $highlightedElements = $blockElements + .filter(($element) => $highlightRegex.test($element.textContent)); + + $highlightedElements.forEach(($element) => { + $element.classList.add('puf-highlighted-text'); + $element.textContent = $element.textContent.replace(/^\(\(/, '').replace(/\)\)$/, ''); + }); +} + +function alignP($block) { + const cardLeftQuery = '.puf-card.puf-left > .puf-card-top > p:last-of-type'; + const cardRightQuery = '.puf-card.puf-right > .puf-card-top > p:last-of-type'; + + const card1 = $block.querySelector(cardLeftQuery); + const card2 = $block.querySelector(cardRightQuery); + + const adjustHeight = () => { + card1.style.height = 'auto'; + card2.style.height = 'auto'; + + const maxHeight = Math.max( + card1.getBoundingClientRect().height, + card2.getBoundingClientRect().height, + ); + + card1.style.height = `${maxHeight}px`; + card2.style.height = `${maxHeight}px`; + }; + + const ro = new ResizeObserver(() => adjustHeight()); + + ro.observe(card1); + ro.observe(card2); + + adjustHeight(); +} + +function alignHighlights($block) { + const cardLeftTitle = '.puf-card.puf-left > .puf-card-bottom > h3'; + const cardRightTitle= '.puf-card.puf-right > .puf-card-bottom > h3'; + + const cardLeft = $block.querySelector(cardLeftTitle); + const cardRight= $block.querySelector(cardRightTitle); + + const adjustHeight = () => { + cardLeft.style.height = 'auto'; + cardRight.style.height = 'auto'; + + const maxHeightTitle = Math.max( + cardLeft.getBoundingClientRect().height, + cardRight.getBoundingClientRect().height + ); + + + cardLeft.style.height = `${maxHeightTitle}px`; + cardRight.style.height = `${maxHeightTitle}px`; + }; + + const ro = new ResizeObserver(() => adjustHeight()); + + ro.observe(cardLeft); + ro.observe(cardRight); + + adjustHeight(); +} + + +function decorateFooter($block) { + if ($block?.children?.[3]) { + const $footer = createTag('div', { class: 'puf-pricing-footer' }); + $footer.append($block.children[3]); + return $footer; + } else { + return ''; + } +} + export default function decorate($block) { const $leftCard = decorateCard($block, 'puf-left'); const $rightCard = decorateCard($block, 'puf-right'); + const $footer = decorateFooter($block); $block.innerHTML = ''; - - $block.append($leftCard); - $block.append($rightCard); + $block.append($leftCard, $rightCard); buildCarousel('.puf-card-container', $block); updatePUFCarousel($block); addPublishDependencies('/express/system/offers-new.json'); + wrapTextAndSup($block); + alignP($block); + highlightText($block); + alignHighlights($block); + + $block.append($footer); } diff --git a/express/blocks/search-marquee/autocomplete-api-v3.js b/express/blocks/search-marquee/autocomplete-api-v3.js deleted file mode 100644 index d0119b00d..000000000 --- a/express/blocks/search-marquee/autocomplete-api-v3.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2023 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -const url = 'https://adobesearch-atc.adobe.io/uss/v3/autocomplete'; -const experienceId = 'default-templates-autocomplete-v1'; -const scopeEntities = [ - 'HzTemplate', - // 'HzTextLockup', 'Icon', 'Photo', 'DesignAsset', 'Background' -]; -const wlLocales = ['en-US', 'fr-FR', 'de-DE', 'ja-JP']; -const emptyRes = { queryResults: [{ items: [] }] }; -export default async function fetchAPI({ limit = 5, textQuery, locale = 'en-US' }) { - if (!textQuery || !wlLocales.includes(locale)) { - return []; - } - - const res = await fetch(url, { - method: 'POST', - headers: { - 'x-api-key': 'projectx_marketing_web', - 'content-type': 'application/json', - }, - body: JSON.stringify({ - experienceId, - textQuery, - locale, - queries: [ - { - limit, - id: experienceId, - scope: { - entities: scopeEntities, - }, - }, - ], - }), - }) - .then((response) => response.json()) - .then((response) => (response.queryResults?.[0]?.items ? response : emptyRes)) - .catch((err) => { - // eslint-disable-next-line no-console - console.error('Autocomplete API Error: ', err); - return emptyRes; - }); - return res.queryResults[0].items; -} diff --git a/express/blocks/search-marquee/search-marquee.css b/express/blocks/search-marquee/search-marquee.css old mode 100644 new mode 100755 diff --git a/express/blocks/search-marquee/search-marquee.js b/express/blocks/search-marquee/search-marquee.js old mode 100644 new mode 100755 index d53adb997..cbe676543 --- a/express/blocks/search-marquee/search-marquee.js +++ b/express/blocks/search-marquee/search-marquee.js @@ -11,16 +11,17 @@ */ import { - buildStaticFreePlanWidget, createTag, fetchPlaceholders, getIconElement, getLocale, getMetadata, } from '../../scripts/scripts.js'; +import { buildStaticFreePlanWidget } from '../../scripts/utils/free-plan.js'; import { buildCarousel } from '../shared/carousel.js'; import fetchAllTemplatesMetadata from '../../scripts/all-templates-metadata.js'; +import BlockMediator from '../../scripts/block-mediator.js'; function handlelize(str) { return str.normalize('NFD') @@ -49,6 +50,12 @@ function logSearch(form, url = 'https://main--express-website--adobe.hlx.page/ex } } +function wordExistsInString(word, inputString) { + const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regexPattern = new RegExp(`(?:^|\\s|[.,!?()'"\\-])${escapedWord}(?:$|\\s|[.,!?()'"\\-])`, 'i'); + return regexPattern.test(inputString); +} + function initSearchFunction(block) { const searchBarWrapper = block.querySelector('.search-bar-wrapper'); @@ -62,6 +69,22 @@ function initSearchFunction(block) { clearBtn.style.display = 'none'; + const searchBarWatcher = new IntersectionObserver((entries) => { + if (!entries[0].isIntersecting) { + BlockMediator.set('stickySearchBar', { + element: searchBarWrapper.cloneNode(true), + loadSearchBar: true, + }); + } else { + BlockMediator.set('stickySearchBar', { + element: searchBarWrapper.cloneNode(true), + loadSearchBar: false, + }); + } + }, { rootMargin: '0px', threshold: 1 }); + + searchBarWatcher.observe(searchBarWrapper); + searchBar.addEventListener('click', (e) => { e.stopPropagation(); searchDropdown.classList.remove('hidden'); @@ -86,39 +109,61 @@ function initSearchFunction(block) { } }, { passive: true }); + const trimInput = (tasks, input) => { + let alteredInput = input; + tasks[0][1].sort((a, b) => b.length - a.length).forEach((word) => { + alteredInput = alteredInput.toLowerCase().replace(word.toLowerCase(), ''); + }); + + return alteredInput.trim(); + }; + + const findTask = (map) => Object.entries(map).filter((task) => task[1].some((word) => { + const searchValue = searchBar.value.toLowerCase(); + return wordExistsInString(word.toLowerCase(), searchValue); + })).sort((a, b) => b[0].length - a[0].length); + const redirectSearch = async () => { const placeholders = await fetchPlaceholders(); - const taskMap = JSON.parse(placeholders['task-name-mapping']); + const taskMap = placeholders['task-name-mapping'] ? JSON.parse(placeholders['task-name-mapping']) : {}; + const taskXMap = placeholders['x-task-name-mapping'] ? JSON.parse(placeholders['x-task-name-mapping']) : {}; const format = getMetadata('placeholder-format'); - let currentTasks = ''; - let searchInput = searchBar.value.toLowerCase() || getMetadata('topics'); + const currentTasks = { + xCore: '', + content: '', + }; + let searchInput = searchBar.value || getMetadata('topics'); - const tasksFoundInInput = Object.entries(taskMap).filter((task) => task[1].some((word) => { - const searchValue = searchBar.value.toLowerCase(); - return searchValue.indexOf(word.toLowerCase()) >= 0; - })).sort((a, b) => b[0].length - a[0].length); + const tasksFoundInInput = findTask(taskMap); + const tasksXFoundInInput = findTask(taskXMap); if (tasksFoundInInput.length > 0) { - tasksFoundInInput[0][1].sort((a, b) => b.length - a.length).forEach((word) => { - searchInput = searchInput.toLowerCase().replace(word.toLowerCase(), ''); - }); + searchInput = trimInput(tasksFoundInInput, searchInput); + [[currentTasks.xCore]] = tasksFoundInInput; + } - searchInput = searchInput.trim(); - [[currentTasks]] = tasksFoundInInput; + if (tasksXFoundInInput.length > 0) { + searchInput = trimInput(tasksXFoundInInput, searchInput); + [[currentTasks.content]] = tasksXFoundInInput; } const locale = getLocale(window.location); const urlPrefix = locale === 'us' ? '' : `/${locale}`; const topicUrl = searchInput ? `/${searchInput}` : ''; - const taskUrl = `/${handlelize(currentTasks.toLowerCase())}`; + const taskUrl = `/${handlelize(currentTasks.xCore.toLowerCase())}`; + const taskXUrl = `/${handlelize(currentTasks.content.toLowerCase())}`; const targetPath = `${urlPrefix}/express/templates${taskUrl}${topicUrl}`; + const targetPathX = `${urlPrefix}/express/templates${taskXUrl}${topicUrl}`; const allTemplatesMetadata = await fetchAllTemplatesMetadata(); const pathMatch = (e) => e.url === targetPath; - if (allTemplatesMetadata.some(pathMatch)) { + const pathMatchX = (e) => e.url === targetPathX; + if (allTemplatesMetadata.some(pathMatchX) && document.body.dataset.device !== 'mobile') { + window.location = `${window.location.origin}${targetPathX}`; + } else if (allTemplatesMetadata.some(pathMatch) && document.body.dataset.device !== 'desktop') { window.location = `${window.location.origin}${targetPath}`; } else { - const searchUrlTemplate = `/express/templates/search?tasks=${currentTasks}&phformat=${format}&topics=${searchInput || "''"}&q=${searchInput || "''"}`; + const searchUrlTemplate = `/express/templates/search?tasks=${currentTasks.xCore}&tasksx=${currentTasks.content}&phformat=${format}&topics=${searchInput || "''"}&q=${searchInput || "''"}`; window.location = `${window.location.origin}${urlPrefix}${searchUrlTemplate}`; } }; @@ -173,7 +218,7 @@ function initSearchFunction(block) { } }; - import('./use-input-autocomplete.js').then(({ default: useInputAutocomplete }) => { + import('../../scripts/autocomplete-api-v3.js').then(({ default: useInputAutocomplete }) => { const { inputHandler } = useInputAutocomplete( suggestionsListUIUpdateCB, { throttleDelay: 300, debounceDelay: 500, limit: 7 }, ); @@ -192,8 +237,6 @@ async function decorateSearchFunctions(block) { enterKeyHint: placeholders.search ?? 'Search', }); - // Tasks Dropdown - searchForm.append(searchBar); const searchIcon = getIconElement('search'); searchIcon.loading = 'lazy'; @@ -205,8 +248,6 @@ async function decorateSearchFunctions(block) { block.append(searchBarWrapper); } -// function downloadBackgroundImg(block) {} - function decorateBackground(block) { const supportedImgFormat = ['jpeg', 'jpg', 'webp', 'png', 'svg']; const supportedVideoFormat = ['mp4']; @@ -285,7 +326,7 @@ async function buildSearchDropdown(block) { trendsContainer.append(trendsWrapper); } - suggestionsTitle.textContent = placeholders['search-suggestions-title']; + suggestionsTitle.textContent = placeholders['search-suggestions-title'] ?? ''; suggestionsContainer.append(suggestionsTitle, suggestionsList); const freePlanTags = await buildStaticFreePlanWidget(); @@ -331,6 +372,7 @@ export default async function decorate(block) { document.dispatchEvent(linksPopulated); } if (window.location.href.includes('/express/templates/')) { - import('../../scripts/ckg-link-list.js'); + const { default: updateAsyncBlocks } = await import('../../scripts/ckg-link-list.js'); + updateAsyncBlocks(); } } diff --git a/express/blocks/shared/carousel.js b/express/blocks/shared/carousel.js index 385389a52..7c591a431 100644 --- a/express/blocks/shared/carousel.js +++ b/express/blocks/shared/carousel.js @@ -92,6 +92,10 @@ export function buildCarousel(selector = ':scope > *', $parent, infinityScrollEn if (x > 15) clearInterval(refreshArrows); }, 200); + $parent.closest('.block')?.addEventListener('carouselloaded', () => { + toggleControls(); + }, { passive: true }); + // Scroll the carousel by clicking on the controls const moveCarousel = (increment) => { $platform.scrollLeft -= increment; diff --git a/express/blocks/shared/floating-cta.css b/express/blocks/shared/floating-cta.css index bed7af1f6..12f0857ac 100644 --- a/express/blocks/shared/floating-cta.css +++ b/express/blocks/shared/floating-cta.css @@ -53,7 +53,7 @@ main .floating-button { background-color: var(--color-gray-200); transition: background-color .3s, padding .3s, margin-bottom 0.6s ease-out, bottom .3s; z-index: 2; - max-width: 100%; + max-width: 100vw; pointer-events: auto; margin-bottom: 24px; } diff --git a/express/blocks/shared/masonry.js b/express/blocks/shared/masonry.js index db772e50e..e033a85f4 100644 --- a/express/blocks/shared/masonry.js +++ b/express/blocks/shared/masonry.js @@ -9,10 +9,9 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { - createTag, -} from '../../scripts/scripts.js'; +import { createTag } from '../../scripts/scripts.js'; +// todo: remove this.needBackwardCompatibility() when template-list is deprecated function nodeIsBefore(node, otherNode) { // eslint-disable-next-line no-bitwise const forward = node.compareDocumentPosition(otherNode) @@ -22,8 +21,8 @@ function nodeIsBefore(node, otherNode) { // eslint-disable-next-line import/prefer-default-export export class Masonry { - constructor($block, cells) { - this.$block = $block; + constructor(wrapper, cells) { + this.wrapper = wrapper; this.cells = cells; this.columns = []; this.nextColumn = null; @@ -33,53 +32,58 @@ export class Masonry { this.fillToHeight = 0; } + needBackwardCompatibility() { + return this.wrapper.classList.contains('template-list'); + } + // set up fresh grid if necessary setupColumns() { + const block = this.needBackwardCompatibility() ? this.wrapper : this.wrapper.parentElement; let result = 1; let colWidth = 264; - if (this.$block.classList.contains('sixcols')) { + if (block.classList.contains('sixcols')) { colWidth = 175; } - if (this.$block.classList.contains('fullwidth')) { + if (block.classList.contains('fullwidth')) { colWidth = 185; } - const width = this.$block.offsetWidth; + const width = this.wrapper.offsetWidth; if (!width) { return 0; } else if (window.innerWidth >= 900) { - if (this.$block.classList.contains('sm-view')) { - colWidth = 172; + if (block.classList.contains('sm-view')) { + colWidth = 176; } - if (this.$block.classList.contains('md-view')) { - colWidth = 270; + if (block.classList.contains('md-view')) { + colWidth = 256; } - if (this.$block.classList.contains('lg-view')) { - colWidth = 364; + if (block.classList.contains('lg-view')) { + colWidth = 340; } } else if (window.innerWidth >= 600) { - if (this.$block.classList.contains('sm-view')) { + if (block.classList.contains('sm-view')) { colWidth = 172; } - if (this.$block.classList.contains('md-view')) { + if (block.classList.contains('md-view')) { colWidth = 240; } - if (this.$block.classList.contains('lg-view')) { + if (block.classList.contains('lg-view')) { colWidth = 364; } } else { - if (this.$block.classList.contains('sm-view')) { + if (block.classList.contains('sm-view')) { colWidth = 120; } - if (this.$block.classList.contains('md-view')) { + if (block.classList.contains('md-view')) { colWidth = 172; } - if (this.$block.classList.contains('lg-view')) { + if (block.classList.contains('lg-view')) { colWidth = 340; } } @@ -96,18 +100,18 @@ export class Masonry { } let numCols = Math.floor(width / colWidth); if (numCols < 1) numCols = 1; - if (numCols !== this.$block.querySelectorAll('.masonry-col').length) { - this.$block.querySelectorAll('.masonry-col').forEach((col) => { + if (numCols !== this.wrapper.querySelectorAll('.masonry-col').length) { + this.wrapper.querySelectorAll('.masonry-col').forEach((col) => { col.remove(); }); this.columns = []; for (let i = 0; i < numCols; i += 1) { - const $column = createTag('div', { class: 'masonry-col' }); + const colEl = createTag('div', { class: 'masonry-col' }); this.columns.push({ outerHeight: 0, - $column, + colEl, }); - this.$block.appendChild($column); + this.wrapper.appendChild(colEl); } result = 2; } @@ -133,10 +137,10 @@ export class Masonry { if (this.fillToHeight) { if (this.fillToHeight - minOuterHeight >= height - 50) { - // console.log(this.fillToHeight, minOuterHeight, height, $cell); + // console.log(this.fillToHeight, minOuterHeight, height, cell); this.nextColumn = this.columns.find((col) => col.outerHeight === minOuterHeight); } else { - // console.log(this.fillToHeight, minOuterHeight, height, $cell); + // console.log(this.fillToHeight, minOuterHeight, height, cell); this.fillToHeight = 0; [this.nextColumn] = this.columns; // console.log('no more fill mode'); @@ -146,18 +150,18 @@ export class Masonry { } // add cell to next column - addCell($cell) { + addCell(cell) { let mediaHeight = 0; let mediaWidth = 0; let calculatedHeight = 0; - const img = $cell.querySelector('img'); + const img = cell.querySelector('img'); if (img) { mediaHeight = img.naturalHeight; mediaWidth = img.naturalWidth; calculatedHeight = ((this.columnWidth) / mediaWidth) * mediaHeight; } - const video = $cell.querySelector('video'); + const video = cell.querySelector('video'); if (video) { mediaHeight = video.videoHeight; mediaWidth = video.videoWidth; @@ -165,62 +169,64 @@ export class Masonry { } if (this.debug) { // eslint-disable-next-line no-console - console.log($cell.offsetHeight, calculatedHeight, $cell); + console.log(cell.offsetHeight, calculatedHeight, cell); } const column = this.getNextColumn(calculatedHeight); - column.$column.append($cell); - $cell.classList.add('appear'); + column.colEl.append(cell); + cell.classList.add('appear'); column.outerHeight += calculatedHeight; - if (!calculatedHeight && $cell.classList.contains('placeholder') && $cell.style.height) { - column.outerHeight += +$cell.style.height.split('px')[0] + 20; + if (!calculatedHeight && cell.classList.contains('placeholder') && cell.style.height) { + column.outerHeight += +cell.style.height.split('px')[0] + 20; } - const $btnC = $cell.querySelector(':scope > div:nth-of-type(2)'); + const $btnC = cell.querySelector(':scope > div:nth-of-type(2)'); if ($btnC) $btnC.classList.add('button-container'); - /* set tab index and event listeners */ - if (this.cells[0] === $cell) { - /* first cell focus handler */ - $cell.addEventListener('focus', (event) => { - if (event.relatedTarget) { - const backward = nodeIsBefore(event.target, event.relatedTarget); - if (backward) this.cells[this.cells.length - 1].focus(); - } - }); - /* first cell blur handler */ - $cell.addEventListener('blur', (event) => { - if (!event.relatedTarget.classList.contains('template')) { - const forward = nodeIsBefore(event.target, event.relatedTarget); - if (forward) { - if (this.cells.length > 1) { - this.cells[1].focus(); - } + if (this.needBackwardCompatibility()) { + /* set tab index and event listeners */ + if (this.cells[0] === cell) { + /* first cell focus handler */ + cell.addEventListener('focus', (event) => { + if (event.relatedTarget) { + const backward = nodeIsBefore(event.target, event.relatedTarget); + if (backward) this.cells[this.cells.length - 1].focus(); } - } - }); - } else { - /* all other cells get custom blur handler and no tabindex */ - $cell.setAttribute('tabindex', '-1'); - $cell.addEventListener('blur', (event) => { - if (event.relatedTarget) { - const forward = nodeIsBefore(event.target, event.relatedTarget); - const backward = !forward; - const index = this.cells.indexOf($cell); - if (forward) { - if (index < this.cells.length - 1) { - this.cells[index + 1].focus(); + }); + /* first cell blur handler */ + cell.addEventListener('blur', (event) => { + if (!event.relatedTarget.classList.contains('template')) { + const forward = nodeIsBefore(event.target, event.relatedTarget); + if (forward) { + if (this.cells.length > 1) { + this.cells[1].focus(); + } } } - if (backward) { - if (index > 0) { - this.cells[index - 1].focus(); + }); + } else { + /* all other cells get custom blur handler and no tabindex */ + cell.setAttribute('tabindex', '-1'); + cell.addEventListener('blur', (event) => { + if (event.relatedTarget) { + const forward = nodeIsBefore(event.target, event.relatedTarget); + const backward = !forward; + const index = this.cells.indexOf(cell); + if (forward) { + if (index < this.cells.length - 1) { + this.cells[index + 1].focus(); + } + } + if (backward) { + if (index > 0) { + this.cells[index - 1].focus(); + } } } - } - }); + }); + } } } @@ -243,37 +249,43 @@ export class Masonry { while (workList.length > 0) { for (let i = 0; i < 5 && i < workList.length; i += 1) { - const $cell = workList[i]; - const $image = $cell.querySelector(':scope picture > img'); - if ($image) $image.setAttribute('loading', 'eager'); + const cell = workList[i]; + const image = cell.querySelector(':scope picture > img'); + if (image) image.setAttribute('loading', 'eager'); } - const $cell = workList[0]; - const $image = $cell.querySelector(':scope picture > img'); - if ($image && !$image.complete) { + const cell = workList[0]; + const image = cell.querySelector(':scope picture > img'); + if (image && !image.complete) { // continue when image is loaded - $image.addEventListener('load', () => { + image.addEventListener('load', () => { this.draw(workList); }); return; } - const $video = $cell.querySelector('video'); - if ($video && $video.readyState === 0) { - // continue when video is loaded - $video.addEventListener('loadedmetadata', () => { - this.draw(workList); - }); - return; + + if (this.needBackwardCompatibility()) { + const video = cell.querySelector('video'); + if (video && video.readyState === 0) { + video.addEventListener('loadedmetadata', () => { + this.draw(workList); + }); + + return; + } } - this.addCell($cell); + + this.addCell(cell); // remove already processed cell workList.shift(); } if (workList.length > 0) { // draw rest this.draw(workList); + } else if (this.needBackwardCompatibility()) { + this.wrapper.classList.add('template-list-complete'); } else { - this.$block.classList.add('template-list-complete'); + this.wrapper.parentElement.classList.add('template-x-complete'); } } } diff --git a/express/blocks/template-list/template-list.css b/express/blocks/template-list/template-list.css index 27b74b33d..6c9086513 100644 --- a/express/blocks/template-list/template-list.css +++ b/express/blocks/template-list/template-list.css @@ -85,21 +85,6 @@ main .template-list.fullwidth > .masonry-col { max-width: 175px; } -main .template-list.fullwidth.sm-view > .masonry-col { - max-width: 170px; - text-align: center; -} - -main .template-list.fullwidth.md-view > .masonry-col { - max-width: 258.5px; - text-align: center; -} - -main .template-list.fullwidth.lg-view > .masonry-col { - max-width: 352px; - text-align: center; -} - main .template-list .template-title { margin: 0 12px 20px 12px; } @@ -612,12 +597,12 @@ main .template-list.fullwidth.lg-view > .masonry-col { } main .template-list.fullwidth.md-view > .masonry-col { - max-width: 165.5px; + max-width: 144px; text-align: center; } main .template-list.fullwidth.sm-view > .masonry-col { - max-width: 106.33px; + max-width: 101px; text-align: center; } @@ -2028,7 +2013,7 @@ main .template-list-fullwidth-apipowered-container nav ol.templates-breadcrumbs } main .template-list.fullwidth.lg-view > .masonry-col { - max-width: 328px; + max-width: 320px; text-align: center; } @@ -2166,7 +2151,7 @@ main .template-list-fullwidth-apipowered-container nav ol.templates-breadcrumbs } main .template-list.fullwidth.md-view > .masonry-col { - max-width: 258.5px; + max-width: 240px; text-align: center; } @@ -2398,7 +2383,7 @@ main .template-list-fullwidth-apipowered-container nav ol.templates-breadcrumbs } main .template-list.fullwidth.md-view > .masonry-col { - max-width: 250px; + max-width: 233px; } main .section.template-list-horizontal-container > div { diff --git a/express/blocks/template-list/template-list.js b/express/blocks/template-list/template-list.js index a245a9341..476e47dd0 100644 --- a/express/blocks/template-list/template-list.js +++ b/express/blocks/template-list/template-list.js @@ -214,7 +214,7 @@ async function processResponse(props) { } if (templateFetched) { - return templateFetched.map((template) => { + return templateFetched.filter((template) => !!template.branchURL).map((template) => { const $template = createTag('div'); const imgWrapper = createTag('div'); @@ -1663,14 +1663,13 @@ export async function decorateTemplateList($block, props) { const $parent = $block.closest('.section'); const $titleRow = templates.shift(); $titleRow.classList.add('template-title'); - $titleRow.querySelectorAll(':scope a') - .forEach(($a) => { - $a.className = 'template-title-link'; - const p = $a.closest('p'); - if (p) { - p.classList.remove('button-container'); - } - }); + $titleRow.querySelectorAll(':scope a').forEach(($a) => { + $a.className = 'template-title-link'; + const p = $a.closest('p'); + if (p) { + p.classList.remove('button-container'); + } + }); if ($parent && $parent.classList.contains('toc-container')) { const $tocCollidingArea = createTag('div', { class: 'toc-colliding-area' }); diff --git a/express/blocks/template-x/breadcrumbs.js b/express/blocks/template-x/breadcrumbs.js new file mode 100644 index 000000000..42e63df24 --- /dev/null +++ b/express/blocks/template-x/breadcrumbs.js @@ -0,0 +1,152 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { + fetchPlaceholders, + getMetadata, + titleCase, + createTag, +} from '../../scripts/scripts.js'; +import fetchAllTemplatesMetadata from '../../scripts/all-templates-metadata.js'; + +function sanitize(str) { + return str?.replaceAll(/[$@%'"]/g, ''); +} + +function getCrumbsForSearch(templatesUrl, allTemplatesMetadata, taskCategories) { + const { search, origin } = window.location; + const { tasksx, q } = new Proxy(new URLSearchParams(search), { + get: (searchParams, prop) => searchParams.get(prop), + }); + const tasks = sanitize(tasksx); + const crumbs = []; + // won't have meaningful short-title with no tasks and no q + if (!tasks && !sanitize(q)) { + return crumbs; + } + const shortTitle = getMetadata('short-title'); + if (!shortTitle?.trim()) { + return crumbs; + } + + const lastCrumb = createTag('li'); + lastCrumb.textContent = shortTitle; + crumbs.push(lastCrumb); + if (!tasks) { + return crumbs; + } + + const taskUrl = `${templatesUrl}${tasks}`; + const foundTaskPage = allTemplatesMetadata + .some((t) => t.url === taskUrl.replace(origin, '')); + + if (foundTaskPage) { + const taskCrumb = createTag('li'); + const taskAnchor = createTag('a', { href: taskUrl }); + taskCrumb.append(taskAnchor); + const translatedTasks = Object.entries(taskCategories) + .find(([_, t]) => t === tasks || t === tasks.replace(/-/g, ' ')) + ?.[0]?.toLowerCase() ?? tasks; + taskAnchor.textContent = titleCase(translatedTasks); + crumbs.unshift(taskCrumb); + } + + return crumbs; +} + +function getCrumbsForSEOPage(templatesUrl, allTemplatesMetadata, taskCategories, segments) { + const { origin } = window.location; + const tasks = getMetadata('tasks-x'); + const translatedTasks = Object.entries(taskCategories) + .find(([_, t]) => t === tasks || t === tasks.replace(/-/g, ' ')) + ?.[0]?.toLowerCase() ?? tasks; + // we might have an inconsistent trailing slash problem + let builtUrl = templatesUrl.replace('templates/', 'templates'); + const crumbs = []; + segments + .slice(0, segments.length - 1) + .forEach((currSeg) => { + const seg = sanitize(currSeg); + if (!seg) return; + builtUrl = `${builtUrl}/${seg}`; + // at least translate tasks seg + const translatedSeg = seg === tasks ? translatedTasks : seg; + const segmentCrumb = createTag('li'); + if (allTemplatesMetadata.some((t) => t.url === builtUrl.replace(origin, ''))) { + const segmentLink = createTag('a', { href: builtUrl }); + segmentLink.textContent = titleCase(translatedSeg); + segmentCrumb.append(segmentLink); + } else { + segmentCrumb.textContent = titleCase(translatedSeg); + } + crumbs.push(segmentCrumb); + }); + const shortTitle = getMetadata('short-title'); + if (!shortTitle?.trim()) { + return crumbs; + } + const lastCrumb = createTag('li'); + lastCrumb.textContent = shortTitle; + crumbs.push(lastCrumb); + return crumbs; +} + +async function renderBreadcrumbs(children, breadcrumbs, placeholders, templatesUrl) { + if (!children || children === '/') { + return; + } + const taskCategories = JSON.parse(placeholders['task-categories']); + const allTemplatesMetadata = await fetchAllTemplatesMetadata(); + const isSearchPage = children.startsWith('/search?') || getMetadata('template-search-page') === 'Y'; + const crumbs = isSearchPage + ? getCrumbsForSearch(templatesUrl, allTemplatesMetadata, taskCategories) + : getCrumbsForSEOPage(templatesUrl, allTemplatesMetadata, taskCategories, children.split('/')); + + crumbs.forEach((c) => { + breadcrumbs.append(c); + }); +} + +// returns null if no breadcrumbs +// returns breadcrumbs as an li element +export default async function getBreadcrumbs() { + if (!document.querySelector('.search-marquee')) { + return null; + } + const { origin, pathname } = window.location; + const regex = /(.*?\/express\/)templates(.*)/; + const matches = pathname.match(regex); + if (!matches) { + return null; + } + const placeholders = await fetchPlaceholders(); + const [, homePath, children] = matches; + const breadcrumbs = createTag('ol', { class: 'templates-breadcrumbs' }); + + const homeCrumb = createTag('li'); + const homeUrl = `${origin}${homePath}`; + const homeAnchor = createTag('a', { href: homeUrl }); + homeAnchor.textContent = titleCase(placeholders.express || '') || 'Home'; + homeCrumb.append(homeAnchor); + breadcrumbs.append(homeCrumb); + + const templatesCrumb = createTag('li'); + const templatesUrl = `${homeUrl}templates/`; + const templatesAnchor = createTag('a', { href: templatesUrl }); + templatesAnchor.textContent = titleCase(placeholders.templates || '') || 'Templates'; + templatesCrumb.append(templatesAnchor); + breadcrumbs.append(templatesCrumb); + + const nav = createTag('nav', { 'aria-label': 'Breadcrumb' }); + nav.append(breadcrumbs); + renderBreadcrumbs(children, breadcrumbs, placeholders, templatesUrl); + return nav; +} diff --git a/express/blocks/template-x/template-rendering.js b/express/blocks/template-x/template-rendering.js new file mode 100755 index 000000000..87abf8a01 --- /dev/null +++ b/express/blocks/template-x/template-rendering.js @@ -0,0 +1,391 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +/* eslint-disable no-underscore-dangle */ +import { createTag, getIconElement } from '../../scripts/scripts.js'; + +function containsVideo(pages) { + return pages.some((page) => !!page?.rendition?.video?.thumbnail?.componentId); +} + +function isVideo(iterator) { + return iterator.current().rendition?.video?.thumbnail?.componentId; +} + +function getTemplateTitle(template) { + return template['dc:title']['i-default']; +} + +function extractRenditionLinkHref(template) { + return template._links?.['http://ns.adobe.com/adobecloud/rel/rendition']?.href; +} + +function extractComponentLinkHref(template) { + return template._links?.['http://ns.adobe.com/adobecloud/rel/component']?.href; +} + +function extractImageThumbnail(page) { + return page.rendition.image?.thumbnail; +} + +function getImageThumbnailSrc(renditionLinkHref, componentLinkHref, page) { + const thumbnail = extractImageThumbnail(page); + const { + mediaType, + componentId, + width, + height, + hzRevision, + } = thumbnail; + if (mediaType === 'image/webp') { + // webp only supported by componentLink + return componentLinkHref.replace( + '{&revision,component_id}', + `&revision=${hzRevision || 0}&component_id=${componentId}`, + ); + } + + return renditionLinkHref.replace( + '{&page,size,type,fragment}', + `&size=${Math.max(width, height)}&type=${mediaType}&fragment=id=${componentId}`, + ); +} + +const videoMetadataType = 'application/vnd.adobe.ccv.videometadata'; + +async function getVideoUrls(renditionLinkHref, componentLinkHref, page) { + const videoThumbnail = page.rendition?.video?.thumbnail; + const { componentId } = videoThumbnail; + const preLink = renditionLinkHref.replace( + '{&page,size,type,fragment}', + `&type=${videoMetadataType}&fragment=id=${componentId}`, + ); + const backupPosterSrc = getImageThumbnailSrc(renditionLinkHref, componentLinkHref, page); + try { + const response = await fetch(preLink); + if (!response.ok) { + throw new Error(response.statusText); + } + const { renditionsStatus: { state }, posterframe, renditions } = await response.json(); + if (state !== 'COMPLETED') throw new Error('Video not ready'); + + const mp4Rendition = renditions.find((r) => r.videoContainer === 'MP4'); + if (!mp4Rendition?.url) throw new Error('No MP4 rendition found'); + + return { src: mp4Rendition.url, poster: posterframe?.url || backupPosterSrc }; + } catch (err) { + // use componentLink as backup + return { + src: componentLinkHref.replace( + '{&revision,component_id}', + `&revision=0&component_id=${componentId}`, + ), + poster: backupPosterSrc, + }; + } +} + +function renderShareWrapper(branchUrl, placeholders) { + const text = placeholders['tag-copied'] ?? 'Copied to clipboard'; + const wrapper = createTag('div', { class: 'share-icon-wrapper' }); + const shareIcon = getIconElement('share-arrow'); + const tooltip = createTag('div', { + class: 'shared-tooltip', + 'aria-label': text, + role: 'tooltip', + tabindex: '-1', + }); + let timeoutId = null; + shareIcon.addEventListener('click', async () => { + await navigator.clipboard.writeText(branchUrl); + tooltip.classList.add('display-tooltip'); + + const rect = tooltip.getBoundingClientRect(); + const tooltipRightEdgePos = rect.left + rect.width; + if (tooltipRightEdgePos > window.innerWidth) { + tooltip.classList.add('flipped'); + } + + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + tooltip.classList.remove('display-tooltip'); + tooltip.classList.remove('flipped'); + }, 2500); + }); + + const checkmarkIcon = getIconElement('checkmark-green'); + tooltip.append(checkmarkIcon); + tooltip.append(text); + wrapper.append(shareIcon); + wrapper.append(tooltip); + return wrapper; +} + +function renderCTA(placeholders, branchUrl) { + const btnTitle = placeholders['edit-this-template'] ?? 'Edit this template'; + const btnEl = createTag('a', { + href: branchUrl, + title: btnTitle, + class: 'button accent small', + }); + btnEl.textContent = btnTitle; + return btnEl; +} + +function getPageIterator(pages) { + return { + i: 0, + next() { + this.i = (this.i + 1) % pages.length; + }, + reset() { + this.i = 0; + }, + current() { + return pages[this.i]; + }, + all() { + return pages; + }, + }; +} +async function renderRotatingMedias(wrapper, + pages, + { templateTitle, renditionLinkHref, componentLinkHref }) { + const pageIterator = getPageIterator(pages); + let imgTimeoutId; + + const constructVideo = async () => { + if (!containsVideo(pages)) return null; + const { src, poster } = await getVideoUrls( + renditionLinkHref, + componentLinkHref, + pageIterator.current(), + ); + const video = createTag('video', { + muted: true, + playsinline: '', + title: templateTitle, + poster, + class: 'unloaded hidden', + }); + const videoSource = createTag('source', { + src, + type: 'video/mp4', + }); + + video.append(videoSource); + + return video; + }; + + const constructImg = () => createTag('img', { + src: '', + alt: templateTitle, + class: 'hidden', + }); + + const img = constructImg(); + if (img) wrapper.prepend(img); + + const video = await constructVideo(); + if (video) wrapper.prepend(video); + + const dispatchImgEndEvent = () => { + img.dispatchEvent(new CustomEvent('imgended', { detail: this })); + }; + + const playImage = () => { + img.classList.remove('hidden'); + img.src = getImageThumbnailSrc(renditionLinkHref, componentLinkHref, pageIterator.current()); + + imgTimeoutId = setTimeout(dispatchImgEndEvent, 2000); + }; + + const playVideo = async () => { + if (video) { + const videoSource = video.querySelector('source'); + video.classList.remove('hidden'); + const { src, poster } = await getVideoUrls( + renditionLinkHref, + componentLinkHref, + pageIterator.current(), + ); + video.poster = poster; + videoSource.src = src; + video.load(); + video.muted = true; + video.play().catch((e) => { + if (e instanceof DOMException && e.name === 'AbortError') { + // ignore + } else { + throw e; + } + }); + } + }; + + const playMedia = () => { + if (isVideo(pageIterator)) { + if (img) img.classList.add('hidden'); + playVideo(); + } else { + if (video) video.classList.add('hidden'); + playImage(); + } + }; + + const cleanup = () => { + if (video) { + video.pause(); + video.currentTime = 0; + } + + if (imgTimeoutId) { + clearTimeout(imgTimeoutId); + } + + pageIterator.reset(); + }; + + if (video) { + video.addEventListener('ended', () => { + if (pageIterator.all().length > 1) { + pageIterator.next(); + playMedia(); + } + }); + } + + if (img) { + img.addEventListener('imgended', () => { + if (pageIterator.all().length > 1) { + pageIterator.next(); + playMedia(); + } + }); + } + + return { cleanup, hover: playMedia }; +} + +function renderMediaWrapper(template, placeholders) { + const mediaWrapper = createTag('div', { class: 'media-wrapper' }); + + // TODO: reduce memory with LRU cache or memoization with ttl + let renderedMedia = null; + + const templateTitle = getTemplateTitle(template); + const renditionLinkHref = extractRenditionLinkHref(template); + const componentLinkHref = extractComponentLinkHref(template); + const { branchUrl } = template.customLinks; + const templateInfo = { + templateTitle, + branchUrl, + renditionLinkHref, + componentLinkHref, + }; + + const enterHandler = async (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!renderedMedia) { + renderedMedia = await renderRotatingMedias(mediaWrapper, template.pages, templateInfo); + mediaWrapper.append(renderShareWrapper(branchUrl, placeholders)); + } + renderedMedia.hover(); + }; + const leaveHandler = () => { + if (renderedMedia) renderedMedia.cleanup(); + }; + + return { mediaWrapper, enterHandler, leaveHandler }; +} + +function renderHoverWrapper(template, placeholders) { + const btnContainer = createTag('div', { class: 'button-container' }); + + const { mediaWrapper, enterHandler, leaveHandler } = renderMediaWrapper(template, placeholders); + + btnContainer.append(mediaWrapper); + btnContainer.addEventListener('mouseenter', enterHandler); + btnContainer.addEventListener('mouseleave', leaveHandler); + + const cta = renderCTA(placeholders, template.customLinks.branchUrl); + btnContainer.append(cta); + + return btnContainer; +} + +function getStillWrapperIcons(template, placeholders) { + let planIcon = null; + if (template.licensingCategory === 'free') { + planIcon = createTag('span', { class: 'free-tag' }); + planIcon.append(placeholders.free ?? 'Free'); + } else { + planIcon = getIconElement('premium'); + } + let videoIcon = ''; + if (!containsVideo(template.pages) && template.pages.length > 1) { + videoIcon = getIconElement('multipage-static-badge'); + } + + if (containsVideo(template.pages) && template.pages.length === 1) { + videoIcon = getIconElement('video-badge'); + } + + if (containsVideo(template.pages) && template.pages.length > 1) { + videoIcon = getIconElement('multipage-video-badge'); + } + if (videoIcon) videoIcon.classList.add('media-type-icon'); + return { planIcon, videoIcon }; +} + +function renderStillWrapper(template, placeholders) { + const stillWrapper = createTag('div', { class: 'still-wrapper' }); + + const templateTitle = getTemplateTitle(template); + const renditionLinkHref = extractRenditionLinkHref(template); + const componentLinkHref = extractComponentLinkHref(template); + + const thumbnailImageHref = getImageThumbnailSrc( + renditionLinkHref, + componentLinkHref, + template.pages[0], + ); + + const imgWrapper = createTag('div', { class: 'image-wrapper' }); + + const img = createTag('img', { + src: thumbnailImageHref, + alt: templateTitle, + }); + imgWrapper.append(img); + + const { planIcon, videoIcon } = getStillWrapperIcons(template, placeholders); + img.onload = (e) => { + if (e.eventPhase >= Event.AT_TARGET) { + imgWrapper.append(planIcon); + imgWrapper.append(videoIcon); + } + }; + + stillWrapper.append(imgWrapper); + return stillWrapper; +} + +export default function renderTemplate(template, placeholders) { + const tmpltEl = createTag('div'); + tmpltEl.append(renderStillWrapper(template, placeholders)); + tmpltEl.append(renderHoverWrapper(template, placeholders)); + + return tmpltEl; +} diff --git a/express/blocks/template-x/template-search-api-v3.js b/express/blocks/template-x/template-search-api-v3.js new file mode 100755 index 000000000..672436e8b --- /dev/null +++ b/express/blocks/template-x/template-search-api-v3.js @@ -0,0 +1,215 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +/* eslint-disable no-underscore-dangle */ +import { fetchPlaceholders, getLanguage } from '../../scripts/scripts.js'; +import { memoize } from '../../scripts/utils.js'; + +function extractFilterTerms(input) { + if (!input || typeof input !== 'string') { + return []; + } + return input + .split('AND') + .map((t) => t + .trim() + .toLowerCase()); +} +function extractLangs(locales) { + return locales.toLowerCase().split(' or ').map((l) => l.trim()); +} +function formatFilterString(filters) { + const { + animated, + locales, + behaviors, + premium, + tasks, + topics, + } = filters; + let str = ''; + if (premium && premium !== 'all') { + str += `&filters=licensingCategory==${premium.toLowerCase() === 'false' ? 'free' : 'premium'}`; + } + if (animated && animated !== 'all' && !behaviors) { + str += `&filters=behaviors==${animated.toLowerCase() === 'false' ? 'still' : 'animated'}`; + } + if (behaviors) { + extractFilterTerms(behaviors).forEach((behavior) => { + str += `&filters=behaviors==${behavior.split(',').map((b) => b.trim()).join(',')}`; + }); + } + extractFilterTerms(tasks).forEach((task) => { + str += `&filters=pages.task.name==${task.split(',').map((t) => t.trim()).join(',')}`; + }); + extractFilterTerms(topics).forEach((topic) => { + str += `&filters=topics==${topic.split(',').map((t) => t.trim()).join(',')}`; + }); + // locale needs backward compatibility with old api + if (locales) { + const langFilter = extractLangs(locales).map((l) => getLanguage(l)).join(','); + str += `&filters=language==${langFilter}`; + } + + return str; +} + +const memoizedFetch = memoize( + (url, headers) => fetch(url, headers).then((r) => (r.ok ? r.json() : null)), { ttl: 30 * 1000 }, +); + +async function fetchSearchUrl({ + limit, start, filters, sort, q, collectionId, +}) { + const base = 'https://www.adobe.com/express-search-api-v3'; + const collectionIdParam = `collectionId=${collectionId}`; + const queryType = 'search'; + const queryParam = `&queryType=${queryType}`; + const filterStr = formatFilterString(filters); + const limitParam = limit || limit === 0 ? `&limit=${limit}` : ''; + const startParam = start ? `&start=${start}` : ''; + const sortParam = { + 'Most Relevant': '', + 'Most Viewed': '&orderBy=-remixCount', + 'Rare & Original': '&orderBy=remixCount', + 'Newest to Oldest': '&orderBy=-availabilityDate', + 'Oldest to Newest': '&orderBy=availabilityDate', + }[sort] || sort || ''; + const qParam = q && q !== '{{q}}' ? `&q=${q}` : ''; + const url = encodeURI( + `${base}?${collectionIdParam}${queryParam}${qParam}${limitParam}${startParam}${sortParam}${filterStr}`, + ); + + const headers = {}; + + const langs = extractLangs(filters.locales); + let prefLang; + if (langs.length > 0) { + [prefLang] = langs; + headers['x-express-pref-lang'] = getLanguage(prefLang); + headers['x-express-ims-region-code'] = prefLang.toUpperCase(); + } + const res = await memoizedFetch(url, { headers }); + if (!res) return res; + if (langs.length > 1) { + res.items = [ + ...res.items.filter(({ language }) => language === getLanguage(prefLang)), + ...res.items.filter(({ language }) => language !== getLanguage(prefLang))]; + } + return res; +} + +async function getFallbackMsg(tasks = '') { + const placeholders = await fetchPlaceholders(); + const fallbackTextTemplate = tasks && tasks !== "''" ? placeholders['templates-fallback-with-tasks'] : placeholders['templates-fallback-without-tasks']; + + if (fallbackTextTemplate) { + return tasks ? fallbackTextTemplate.replaceAll('{{tasks}}', tasks.toString()) : fallbackTextTemplate; + } + + return `Sorry we couldn't find any results for what you searched for, try some of these popular ${ + tasks ? ` ${tasks.toString()} ` : ''}templates instead.`; +} + +async function fetchTemplatesNoToolbar(props) { + const { filters, limit } = props; + const langs = extractLangs(filters.locales); + if (langs.length <= 1) { + return { response: await fetchSearchUrl(props) }; + } + const [prefLangPromise, backupLangPromise] = [ + fetchSearchUrl({ + ...props, + filters: { + ...filters, + locales: langs[0], + }, + }), + fetchSearchUrl({ + ...props, + filters: { + ...filters, + locales: langs.slice(1).join(' or '), + }, + })]; + const prefLangRes = await prefLangPromise; + if (!prefLangRes) return { response: prefLangRes }; + if (prefLangRes.items?.length >= limit) return { response: prefLangRes }; + + const backupLangRes = await backupLangPromise; + const mergedItems = [...prefLangRes.items, ...backupLangRes.items].slice(0, limit); + return { + response: { + metadata: { + totalHits: mergedItems.length, + start: '0', + limit, + }, + items: mergedItems, + }, + }; +} + +async function fetchTemplatesWithToolbar(props) { + let response = await fetchSearchUrl(props); + + if (response?.metadata?.totalHits > 0) { + return { response }; + } + const { filters: { tasks, locales } } = props; + if (tasks) { + response = await fetchSearchUrl({ ...props, filters: { tasks, locales, premium: 'false' }, q: '' }); + if (response?.metadata?.totalHits > 0) { + return { response, fallbackMsg: await getFallbackMsg(tasks) }; + } + } + response = await fetchSearchUrl({ ...props, filters: { locales, premium: 'false' }, q: '' }); + if (response?.metadata?.totalHits > 0) { + return { response, fallbackMsg: await getFallbackMsg() }; + } + // ultimate fallback in case no fallback locale is authored + response = await fetchSearchUrl({ ...props, filters: {}, q: '' }); + return { response, fallbackMsg: await getFallbackMsg() }; +} + +function isValidBehaviors(behaviors) { + const collectivelyExhausiveBehaviors = ['animated', 'video', 'still']; + return behaviors.some((b) => collectivelyExhausiveBehaviors.includes(b)) + && (!behaviors.includes('still') || !(behaviors.includes('video') || behaviors.includes('animated'))); +} + +export function isValidTemplate(template) { + return !!(template.status === 'approved' + && template.customLinks?.branchUrl + && template['dc:title']?.['i-default'] + && 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 + && isValidBehaviors(template.behaviors)); +} + +export async function fetchTemplatesCategoryCount(props, tasks) { + const res = await fetchSearchUrl({ + ...props, + limit: 0, + filters: { + ...props.filters, + tasks, + }, + }); + return res?.metadata?.totalHits || 0; +} + +export async function fetchTemplates(props) { + // different strategies w/o toolBar + if (props.toolBar) return fetchTemplatesWithToolbar(props); + return fetchTemplatesNoToolbar(props); +} diff --git a/express/blocks/template-x/template-x.css b/express/blocks/template-x/template-x.css new file mode 100755 index 000000000..512256e85 --- /dev/null +++ b/express/blocks/template-x/template-x.css @@ -0,0 +1,2826 @@ +.template-x-horizontal { + padding-top: 0; +} + +.template-x-wrapper.fullwidth { + max-width: none; + padding: 0; +} + +.template-x > .default-content-wrapper, +.template-x > .default-content-wrapper { + padding: 0 15px; +} + +.template-x .default-content-wrapper h2, +.template-x .default-content-wrapper p { + margin: 0; + text-align: left; +} + +.template-x-horizontal-fullwidth-mini-spreadsheet-powered-container h2 { + margin: 20px 12px 0 12px; + text-align: left; +} + +.template-x-horizontal-fullwidth-mini-spreadsheet-powered-container p { + font-weight: 800; + font-size: var(--body-font-size-m); + margin: 16px 12px 12px 12px; + text-align: left; +} + +.template-x.template-x-complete .template-x-inner-wrapper { + min-height: unset; +} + +.template-x-horizontal-fullwidth-collaboration-container .default-content-wrapper h2, +.template-x-horizontal-fullwidth-collaboration-container .default-content-wrapper h3, +.template-x-horizontal-fullwidth-collaboration-container .default-content-wrapper p { + text-align: left; + padding: 0 28px; + margin: 12px 0; +} + +.template-x-horizontal-fullwidth-collaboration-container .default-content-wrapper p { + font-size: 16px; + line-height: 24px; +} + +.template-x.horizontal .template-tabs { + margin-top: 24px; + display: flex; + overflow: auto; + max-width: 100%; +} + +.template-x.horizontal .template-tabs button.template-tab-button { + background: none; + border: none; + padding: 8px 4px 6px 4px; + margin: 0 16px; + font-family: var(--body-font-family); + cursor: pointer; + font-size: var(--body-font-size-s); + border-bottom: 2px solid transparent; + white-space: nowrap; +} + +.template-x.horizontal .template-tabs button.template-tab-button:first-of-type { + margin-left: 0; +} + +.template-x.horizontal .template-tabs button.template-tab-button.active, +.template-x.horizontal .template-tabs button.template-tab-button:hover { + border-bottom: 2px solid var(--color-black); +} + +.template-x.horizontal .template-tabs button.template-tab-button.active { + font-weight: 700; +} + +.template-x-wrapper .template-title.horizontal .text-wrapper * { + text-align: left; +} + +.template-x-wrapper .template-title.horizontal .text-wrapper p { + margin-bottom: 0; +} + +.template-x-wrapper .template-title.horizontal .text-wrapper > div { + width: 100%; +} + +.template-x-inner-wrapper { + display: flex; + justify-content: center; + margin: 20px auto 0 auto; + font-size: var(--body-font-size-m); + font-weight: var(--body-font-weight); + text-align: left; +} + +.template-x.sixcols .template-x-inner-wrapper, +.template-x.fullwidth .template-x-inner-wrapper { + flex-direction: row; + justify-content: center; +} + +.template-x.horizontal .template-x-inner-wrapper { + margin-top: 0; + transition: opacity 0.2s; +} + +.template-x.fullwidth.holiday .template-x-inner-wrapper { + max-height: 0; + position: relative; + transition: max-height 0.5s, padding-top 0.5s; + overflow: hidden; + margin-right: 56px; + margin-left: 56px; +} + +.template-x.fullwidth.holiday.expanded .template-x-inner-wrapper { + max-height: 300px; + margin-left: 56px; + margin-right: 56px; +} + +.template-x.fullwidth.holiday.animated video { + position: absolute; + top: 0; + left: 0; + object-fit: cover; + height: 100%; + width: 100vw; + z-index: 0; + pointer-events: none; + opacity: 0; + transition: opacity 0.2s; +} + +.template-x.fullwidth.holiday.expanded.animated video { + opacity: 1; + transition: opacity 1s; + padding: 0; +} + +.template-x.fullwidth.sm-view .template-x-inner-wrapper, +.template-x.fullwidth.md-view .template-x-inner-wrapper, +.template-x.fullwidth.lg-view .template-x-inner-wrapper { + padding: 0; +} + +.template-x.with-categories-list .template-x-inner-wrapper { + min-height: 700px; +} + +.template-x.fullwidth.lg-view > .template-x-inner-wrapper > .masonry-col { + max-width: 352px; + text-align: center; +} + +.template-x-wrapper .template-title { + margin: 0 12px 20px 12px; +} + +.template-x-container .template-x-wrapper:not(:has(.toolbar-wrapper)) { + padding-top: 60px; +} + +.template-x-wrapper .template-title.with-link > div { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: flex-start; +} + +.template-x-wrapper .template-title.horizontal > div > * { + text-align: left; + margin: 0; +} + +.template-x-wrapper .template-title.horizontal h2 { + font-size: var(--heading-font-size-m); +} + +.template-x-wrapper .template-title.horizontal.holiday .template-title,picture,a,h4,p { + + z-index: 1; +} + +.template-x-wrapper .template-title.horizontal p { + margin-top: 8px; + width: fit-content; + font-size: var(--body-font-size-m); +} + +.template-x-wrapper .template-link, +.template-x-wrapper .template-title-link { + text-decoration: none; + padding-left: 16px; + padding-top: 6px; + display: flex; +} + +.template-x-wrapper .toggle-button { + display: flex; +} + +.holiday.template-x-wrapper .toggle-button-chev { + display: flex; + width: 6px; + height: 6px; + border-top-width: 0; + border-left-width: 0; + border-bottom-width: 2px; + border-right-width: 2px; + border-style: solid; + border-color: var(--color-info-accent); + transform-origin: 75% 75%; + transform: rotate( -45deg ); + content: ""; + margin-top: 5px; + margin-left: 5px; + margin-right: 2.25px; + transition: transform 0.2s; +} + +.template-x.horizontal.holiday.expanded .toggle-button .toggle-button-chev { + transform: rotate(-135deg); +} + +.template-x-wrapper .template-title-link { + padding: 0; + white-space: nowrap; +} + +.template-x .template p { + margin: 0; +} + +.template-x .template { + display: flex; + flex-direction: column; + width: 240px; + margin: 0 0 32px 0; + justify-content: flex-end; + -webkit-column-break-inside: avoid; + page-break-inside: avoid; + break-inside: avoid; + text-decoration: unset; + position: relative; + border-radius: 7px; +} + +.template-x .template:focus { + outline: none; + box-shadow: 0 0 0 2px var(--color-white), 0 0 0 4px var(--color-info-accent); +} + + +.template-x .template > div:first-child > a:any-link { + padding-left: 0; +} + +.template-x .template img, .template-x .template video { + border-radius: 12px; +} + +.template-x .template img:not(.icon), +.template-x .template video { + width: 100%; + height: 100%; + pointer-events: none; + object-fit: cover; + display: block; +} + +.template-x .template .media-wrapper img.hidden, +.template-x .template .media-wrapper video.hidden { + opacity: 0; + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 0; + z-index: -1; +} + +.template-x .template .media-wrapper video { + clip-path: inset(0 0 3px 0); +} + +.template-x.large .template { + width: 100%; + margin: 0; + border: 0; +} + +.template-x.large .template > div { + margin: 0; +} + +.template-x.large .template img { + width: 100%; +} + +.template-x + .button-container > a { + margin: 0; +} + +.template-x .template:not(.placeholder) .icon-free-badge { + height: 24px; + display: flex; + border-radius: 1000px; + align-items: center; + font-weight: 500; + width: max-content; + top: 8px; + right: 8px; + color: var(--color-white); + font-size: 12px; + line-height: 16px; + padding: 0 12px; + background: #000000B3; +} + +/* horizontal template-x */ +.template-x-horizontal > div, +.template-x-horizontal-fullwidth-collaboration-container > div { + max-width: none; + padding: 0; +} + +.template-x.horizontal { + display: block; + margin-left: auto; + margin-right: auto; + min-height: 220px; +} + +.template-x.horizontal.holiday { + min-height: 54px; +} + +.template-x.horizontal.fullwidth:not(.holiday) { + max-width: none; + padding-right: 16px; + display: block; + margin-left: auto; +} + +.template-x.horizontal.fullwidth.tabbed { + max-width: 1440px; + margin: auto; +} + +.template-x.horizontal.fullwidth.mini { + + padding: unset; +} + +.template-x.horizontal .template { + min-height: 200px; + width: max-content; + margin: 24px 12px; +} + +.template-x.horizontal .template .still-wrapper img, +.template-x.horizontal .template .still-wrapper video { + height: 200px; + width: auto; +} + +.template-x-wrapper .template-title.horizontal h4 { + font-size: 22px; + line-height: 26px; +} + +.template-x-wrapper.holiday .template-title.horizontal h4 { + text-align: left; + margin-top: 14px; + margin-bottom: 14px; + font-size: 16px; +} + +.template-x-wrapper .template-title.horizontal .template:not(.placeholder) .template-link { + display: none; +} + +.template-x.horizontal.mini .template { + min-height: unset; + max-height: 100px; + max-width: 200px; + margin: 0 4px; +} + +.template-x.horizontal.mini .template.placeholder { + height: 100px; + width: 60px; + margin: 0 4px 0 12px; +} + +.template-x.horizontal.mini .template.placeholder > div:first-of-type { + height: unset; + width: unset; + display: block; + padding-top: 16px; +} + +.template-x.horizontal.mini .template.placeholder svg { + height: 18px; + width: 18px; +} + +.template-x.horizontal.mini .template.placeholder .template-link { + font-size: 12px; +} + +.template-x.horizontal.mini .template img, +.template-x.horizontal.mini .template video { + object-fit: cover; + height: 100px; +} + +.template-x.horizontal.mini .template .icon-free-badge { + height: 14px; + font-size: 8px; + padding: 0 5px; + right: 4px; + top: 4px; +} + +.template-x.sixcols .template, +.template-x.fullwidth .template { + position: relative; + display: inline-flex; + width: 165px; + margin: 5px; +} + +.template-x.fullwidth.lg-view .template, +.template-x.fullwidth.md-view .template, +.template-x.fullwidth.sm-view .template { + width: 92%; +} + +.template-x.horizontal.fullwidth .template { + width: auto; +} + +.template-x.horizontal .template:not(.placeholder) img, +.template-x.horizontal .template:not(.placeholder) video, +.template-x.sixcols .template:not(.placeholder) img, +.template-x.sixcols .template:not(.placeholder) video, +.template-x.fullwidth .template:not(.placeholder) img, +.template-x.fullwidth .template:not(.placeholder) video { + transition: transform .3s ease-in-out; + will-change: transform; +} + +.template-x.sixcols .template:not(.placeholder) .button-container, +.template-x.fullwidth .template:not(.placeholder) .button-container { + position: absolute; + width: 120%; + border-radius: 20px; + min-height: 120%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 0; + text-align: center; + transition: opacity .3s ease-in-out; + will-change: opacity; + pointer-events: none; + z-index: 1; +} + +.template-x.horizontal .template:not(.placeholder) .button-container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 0; + text-align: center; + transition: opacity .3s ease-in-out; + will-change: opacity; + border-radius: 12px; + min-height: unset; + box-sizing: border-box; + height: 120%; + width: 120%; +} + +.template-x.horizontal .template:not(.placeholder) .template-link, +.template-x.sixcols .template:not(.placeholder) .template-link, +.template-x.fullwidth .template:not(.placeholder) .template-link { + opacity: 0; + transition: opacity .4s ease-in-out .2s; + will-change: opacity; +} + +.template-x.horizontal .template:hover:not(.placeholder) .button-container, +.template-x.horizontal .template:hover:not(.placeholder) .template-link, +.template-x.sixcols .template:hover:not(.placeholder) .button-container, +.template-x.sixcols .template:hover:not(.placeholder) .template-link, +.template-x.fullwidth .template:hover:not(.placeholder) .button-container, +.template-x.fullwidth .template:hover:not(.placeholder) .template-link { + opacity: 1; + pointer-events: initial; + z-index: 2; +} + +.template-x.horizontal .template:not(.placeholder) .template-link, +.template-x.sixcols .template:not(.placeholder) .template-link, +.template-x.fullwidth .template:not(.placeholder) .template-link { + text-decoration: none; + border-radius: 18px; + padding: 5px 1.2em 6px 1.2em; + text-align: center; + font-size: var(--body-font-size-s); + font-style: normal; + font-weight: 600; + line-height: var(--body-line-height); + cursor: pointer; + color: var(--color-black); + background-color: var(--color-white); + margin: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + height: fit-content; +} + +.template-x.horizontal.mini .template:not(.placeholder) .template-link { + padding: 2px 4px; + font-size: 8px; +} + +.template-x.horizontal .template:not(.placeholder) .template-link::after, +.template-x.sixcols .template:not(.placeholder) .template-link::after, +.template-x.fullwidth .template:not(.placeholder) .template-link::after { + display: none; +} + +.template-x .template.placeholder { + box-sizing: border-box; + display: flex; + position: relative; + justify-content: flex-start; + align-items: center; + border-radius: 14px; + border: 2px dashed var(--color-gray-300); + height: 242px; + width: 200px; + margin: 21px auto 53px; + padding: 0; +} + +.template-x.horizontal .template.placeholder, +.template-x.horizontal.fullwidth .template.placeholder { + margin: 0 11px; +} + +.template-x.sixcols .template.placeholder, +.template-x.fullwidth .template.placeholder { + width: 145px; + margin: 15px auto; +} + +.template-x.fullwidth.lg-view .template.placeholder, +.template-x.fullwidth.md-view .template.placeholder, +.template-x.fullwidth.sm-view .template.placeholder { + margin: 21px auto; + max-width: 80%; + max-height: 30vh; + min-height: 120px; + z-index: 0; +} + +.template-x.fullwidth.lg-view .template.placeholder { + width: 335px; +} + +.template-x.fullwidth.md-view .template.placeholder { + width: 165.5px; +} + +.template-x.fullwidth.sm-view .template.placeholder { + width: 106.33px; +} + +.template-x .template.placeholder div:nth-of-type(2) { + margin: 0; + position: absolute; + padding: 0 20px; + bottom: 10%; +} + +.template-x .template.placeholder .template-link { + font-size: var(--body-font-size-s); + color: currentColor; + text-align: center; + line-height: 1; + padding: 0; + width: fit-content; +} + +.template-x .template.placeholder > div:first-of-type { + width: 100%; + height: 95%; + display: flex; + justify-content: center; + align-items: center; +} + +.template-x.sm-view .template.placeholder > div:first-of-type { + height: 60%; +} + +.template-x .template.placeholder > div:first-of-type > img, +.template-x .template.placeholder > div:first-of-type > svg { + width: 44px; + height: 44px; +} + +.template-x.holiday.light-text svg { + fill: var(--color-white); +} + +.template-x.holiday.dark-text svg { + fill: var(--color-gray-800); +} + +.template-x.fullwidth.sm-view .template.placeholder > div:first-of-type > img, +.template-x.fullwidth.sm-view .template.placeholder > div:first-of-type > svg { + width: 22px; + height: 22px; +} + + +.template-x .template.placeholder .template-link::after { + display: none; +} + +.template-x .template.placeholder.wide > div:first-of-type { + height: 65%; +} + +.template-x .template.placeholder.wide > div:first-of-type > img, +.template-x .template.placeholder.wide > div:first-of-type > svg { + width: 24px; + height: 24px; +} + +.template-x .template.placeholder div:nth-of-type(2) { + min-height: 28px; +} + +.template-x-inner-wrapper.flex-masonry .template { + opacity: 0; + transition: opacity .3s; +} + +.template-x-inner-wrapper.flex-masonry .template.appear { + opacity: 1; +} + +.template-x { + position: relative; +} + +.template-x .load-more { + position: absolute; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + bottom: 0; + width: 100vw; + left: 50%; + transform: translateX(-50%); + height: 240px; + background: linear-gradient(#ffffff00, #ffffff, #ffffff); +} + +.template-x.horizontal .load-more { + display: none; +} + +.template-x .load-more-button { + width: 88px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + background-color: var(--color-black); + color: var(--color-white); + font-size: 36px; + border-radius: 36px; + border: none; + transition: background-color 0.2s; +} + +.template-x .load-more-button:hover { + background-color: var(--color-gray-700); +} + +.template-x .load-more-button.disabled { + pointer-events: none; + opacity: 0.5; +} + +.template-x .load-more-button > .icon-plus-icon { + height: 22px; + width: 22px; +} + +.template-x .load-more-text { + margin-top: 8px; + font-weight: 700; + font-size: 18px; +} + +.template-x.fullwidth.lg-view > .template-x-inner-wrapper > .masonry-col { + max-width: 335px; + text-align: center; +} + +.template-x.fullwidth.md-view > .template-x-inner-wrapper > .masonry-col { + max-width: 156px; + text-align: center; +} + +.template-x.fullwidth.sm-view > .template-x-inner-wrapper > .masonry-col { + max-width: 100px; + text-align: center; +} + +.template-x .toolbar-wrapper { + padding: 0; + background: var(--color-gray-100); + position: sticky; + z-index: 1; + top: 0; +} + +.template-x .api-templates-toolbar { + position: relative; + z-index: 1; + margin: 16px 0 28px 0; + padding: 16px; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + transition: box-shadow 0.2s, margin 0.2s; + gap: 16px; +} + +.template-x .toolbar-wrapper.with-box-shadow { + box-shadow: 0 4px 8px 2px rgb(102 102 102 / 10%); +} + +.template-x .api-templates-toolbar .wrapper-content-search { + display: flex; + align-items: center; + gap: 16px; + position: relative; + flex-basis: 100%; +} + +.template-x .api-templates-toolbar h2 { + font-size: 18px; + line-height: 24px; + margin-top: 0; +} + +.template-x .search-dropdown-container { + background: #FFFFFF; + top: calc(100% + 6px); + border-radius: 12px; + box-shadow: 0 0 20px #00000029; + position: absolute; + width: 100%; + box-sizing: border-box; + left: 0; + z-index: 3; +} + +main .template-x .search-dropdown-container .dropdown-title { + margin: 0; + font-size: 14px; + color: var(--color-gray-500); +} + +main .template-x .search-dropdown-container .trends-container, +main .template-x .search-dropdown-container .suggestions-container { + padding: 20px 28px 0; + text-align: left; +} + +main .template-x .search-dropdown-container .trends-container .from-scratch-link { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; +} + +main .template-x .search-dropdown-container .trends-container ul, +main .template-x .search-dropdown-container .suggestions-container ul { + padding-left: 0; + list-style: none; + max-height: 216px; + overflow: auto; +} + +main .template-x .search-dropdown-container .trends-container .trends-wrapper li, +main .template-x .search-dropdown-container .suggestions-container li { + margin-bottom: 16px; +} + +main .template-x .search-dropdown-container .suggestions-container li { + cursor: pointer; +} + +main .template-x .search-dropdown-container .suggestions-container li:hover { + color: var(--color-info-accent); +} + +main .template-x .search-dropdown-container .trends-container .trends-wrapper li a { + color: var(--color-black); +} + +main .template-x .search-dropdown-container .suggestions-container { + min-width: 300px; +} + +main .template-x .search-dropdown-container .free-plans-container .free-plan-widget { + width: 100%; + border-radius: 0 0 12px 12px; + flex-direction: row; + align-items: center; + gap: 10px; + margin: 0; + justify-content: center; +} + +main .template-x .search-bar-wrapper { + position: absolute; + left: 0; + width: 100%; + max-width: 560px; + margin: 0; + height: max-content; + transition: max-width 0.5s; +} + +.template-x .search-bar-wrapper .search-bar { + box-sizing: border-box; + margin: 0; + font-family: var(--body-font-family); + font-weight: 400; + font-size: 16px; + padding: 10px 40px; + border: var(--color-gray-200) 2px solid; + border-radius: 100px; + max-width: 560px; + width: 100%; + height: 41px; + transition: max-width 0.5s, padding-right 0.5s, padding-left 0.5s; +} + +.template-x .search-bar-wrapper .search-bar::placeholder { + transition: color 0.5s; +} + +.template-x .search-bar-wrapper .search-bar:focus-visible, +.template-x .search-bar-wrapper .search-bar:focus { + border: #444444 2px solid; + outline: none; +} + +main .template-x .search-bar-wrapper .search-bar:disabled { + background-color: var(--color-gray-100); +} + +main .template-x .search-bar-wrapper .icon.icon-search { + position: absolute; + z-index: 1; + height: 16px; + width: 16px; + pointer-events: none; + top: 50%; + transform: translateY(-50%); + transition: left 0.5s; + left: 16px; +} + +.template-x .search-bar-wrapper .icon.icon-search-clear { + position: absolute; + z-index: 1; + height: 16px; + width: 16px; + cursor: pointer; + top: 50%; + transform: translateY(-50%); + right: 40px; +} + +main .template-x .api-templates-toolbar .search-bar-wrapper { + display: none; +} + +.template-x .api-templates-toolbar .search-bar-wrapper.show { + display: block; +} + +.template-x .api-templates-toolbar .search-bar-wrapper.collapsed .icon-search { + left: 12px; +} + +.template-x .api-templates-toolbar .search-bar-wrapper .icon-search-clear { + right: 12px; +} + +.template-x .api-templates-toolbar .search-dropdown-container { + background: #FFFFFF; + top: calc(100% + 6px); + border-radius: 12px; + box-shadow: 0 0 20px #00000029; + position: absolute; + width: 100%; + box-sizing: border-box; + left: 0; + z-index: 3; +} + +main .template-x .api-templates-toolbar .search-bar-wrapper .hidden, +main .template-x .api-templates-toolbar .search-bar-wrapper.collapsed .search-dropdown-container { + display: none; +} + +.template-x .search-bar-wrapper .icon-search-clear, +.template-x .search-bar-wrapper.sticky-search-bar.collapsed .icon-search-clear, +.template-x .search-bar-wrapper.sticky-search-bar.collapsed .search-dropdown { + display: none; +} + +.template-x .api-templates-toolbar .search-bar-wrapper.collapsed { + position: relative; + max-width: 42px; +} + +.template-x .api-templates-toolbar .search-bar-wrapper.collapsed .search-bar { + padding: 0; + max-width: 42px; +} + +.template-x .api-templates-toolbar .search-bar-wrapper.collapsed .search-bar::placeholder, +.template-x .api-templates-toolbar .search-bar-wrapper.collapsed .search-bar { + color: transparent; +} + +.template-x .api-templates-toolbar .wrapper-functions { + display: flex; + justify-content: space-between; + flex-grow: 1; + align-items: center; + gap: 24px; +} + +.template-x .api-templates-toolbar .wrapper-functions .icon { + display: block; + padding-right: 6px; + height: 18px; + width: 18px; +} + +.template-x .api-templates-toolbar .wrapper-functions .views .icon { + height: 22px; + width: 22px; + padding-right: unset; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container, +.template-x .api-templates-toolbar .functions-drawer { + display: flex; + align-items: center; + gap: 16px; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container { + display: none; +} + +.template-x .api-templates-toolbar .wrapper-functions .current-option { + font-family: var(--body-font-family); + font-size: 14px; + line-height: 24px; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .function-wrapper, +.template-x .api-templates-toolbar .functions-drawer .function-sort { + cursor: pointer; + user-select: none; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .function-wrapper { + background-color: var(--color-gray-200); + padding: 4px 8px; + border-radius: 8px; + width: 100vw; + max-width: 160px; + position: relative; + transition: background-color 0.2s; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .function-wrapper:hover { + background-color: var(--color-gray-300); +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .options-wrapper { + position: absolute; + box-sizing: border-box; + z-index: 1; + width: 100%; + display: none; + flex-direction: column; + align-items: stretch; + top: 0; + left: 0; + background: var(--color-white); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 3px 6px #00000016; +} + +.template-x .api-templates-toolbar .functions-drawer .function-sort .options-wrapper { + position: absolute; + box-sizing: border-box; + z-index: 1; + width: 88%; + display: none; + flex-direction: column; + align-items: stretch; + bottom: -190px; + right: 50%; + transform: translateX(50%); + background: var(--color-white); + padding: 12px; + border-radius: 8px; + box-shadow: 0 3px 6px #00000016; +} + +.template-x .api-templates-toolbar .functions-drawer .function-sort .options-wrapper .icon { + padding-right: 4px; + height: 18px; + width: 18px; +} + +.template-x .api-templates-toolbar .function-wrapper.opened .options-wrapper { + display: flex; +} + +.template-x .api-templates-toolbar .function-wrapper.opened .button-wrapper .icon.icon-drop-down-arrow { + transform: rotate(180deg); +} + +.template-x .api-templates-toolbar .functions-drawer .category-list-wrapper .category-list li { + margin-bottom: 0; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .options-wrapper .option-button, +.template-x .api-templates-toolbar .functions-drawer .function-wrapper .options-wrapper .option-button, +.template-x .api-templates-toolbar .functions-drawer .category-list-wrapper .category-list li > a { + font-size: 14px; + font-family: var(--body-font-family); + line-height: 18px; + cursor: pointer; + display: flex; + align-items: center; + text-align: left; + padding: 7px 16px 7px 8px; + word-break: keep-all; + transition: background-color 0.2s, color 0.2s; +} + +.template-x .api-templates-toolbar .functions-drawer .category-list-wrapper .category-list li.active > a { + font-weight: 600; + color: var(--color-info-accent); +} + +.template-x .api-templates-toolbar .functions-drawer .category-list-wrapper .category-list li > a > span { + margin-left: 4px; +} + + +.template-x .api-templates-toolbar .function-wrapper .options-wrapper .option-button.active { + font-weight: 700; + color: var(--color-info-accent); +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .options-wrapper .option-button:hover, +.template-x .api-templates-toolbar .functions-drawer .function-wrapper .options-wrapper .option-button:hover, +.template-x .api-templates-toolbar .functions-drawer .function-wrapper .options-wrapper .option-button.active +{ + background-color: var(--color-gray-200); +} + +.template-x .api-templates-toolbar .functions-drawer .function-wrapper .options-wrapper .option-button:hover, +.template-x .api-templates-toolbar .functions-drawer .function-wrapper .options-wrapper .option-button.active +{ + border-radius: 8px; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .options-wrapper .option-button .icon, +.template-x .api-templates-toolbar .functions-drawer .function-wrapper .options-wrapper .option-button .icon, +.template-x .api-templates-toolbar .functions-drawer .category-list-wrapper .category-list li .icon{ + height: 18px; + width: 18px; + padding-right: 6px; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .options-wrapper .option-button:hover .option-radio, +.template-x .api-templates-toolbar .functions-drawer .function-wrapper .options-wrapper .option-button:hover .option-radio { + border: #4646C6 6px solid; + max-height: 4px; + max-width: 4px; + border-radius: 50%; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .options-wrapper .option-button:active .option-radio, +.template-x .api-templates-toolbar .functions-drawer .function-wrapper .options-wrapper .option-button:active .option-radio { + border: #3D3DB4 6px solid; + max-height: 4px; + max-width: 4px; + border-radius: 50%; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .options-wrapper .option-button .option-radio:focus-visible, +.template-x .api-templates-toolbar .functions-drawer .function-wrapper .options-wrapper .option-button .option-radio:focus-visible { + outline-offset: 4px; +} + +.template-x .api-templates-toolbar .wrapper-functions .functions-container .options-wrapper .option-button.active .option-radio, +.template-x .api-templates-toolbar .functions-drawer .function-wrapper .options-wrapper .option-button.active .option-radio { + border: #5c5ce0 6px solid; + max-height: 4px; + max-width: 4px; + border-radius: 50%; +} + +.template-x .api-templates-toolbar .views { + display: flex; + gap: 8px; + min-width: 82px; +} + +.template-x .api-templates-toolbar .views .view-toggle-button { + opacity: 0.16; + height: 22px; + transition: opacity 0.2s; + cursor: pointer; +} + +.template-x .api-templates-toolbar .views .view-toggle-button:hover { + opacity: 0.27; +} + +.template-x .api-templates-toolbar .views .view-toggle-button.active { + opacity: 1; +} + +.template-x .api-templates-toolbar .button-wrapper { + display: flex; + align-items: center; + position: relative; + padding-right: 24px; +} + +.template-x .api-templates-toolbar .button-wrapper .icon.icon-drop-down-arrow { + height: 10px; + width: 10px; + position: absolute; + right: 3px; + transform: rotate(0deg); + z-index: 2; + padding-right: 0; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile-inner-wrapper .function-wrapper .icon.icon-drop-down-arrow { + transform: rotate(180deg); +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile-inner-wrapper .function-wrapper.collapsed .icon.icon-drop-down-arrow { + transform: rotate(0deg); +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile { + position: fixed; + box-sizing: border-box; + overflow-x: hidden; + z-index: 4; + top: 0; + right: 0; + height: 100%; + width: 80vw; + background: var(--color-white); + padding: 12px 24px; + display: flex; + flex-direction: column; + text-align: left; + transition: transform 0.5s; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile.scrollable::before { + content: ''; + position: absolute; + z-index: 1; + width: 100vw; + bottom: 76px; + left: 0; + height: 88px; + background: linear-gradient(#ffffff00, #ffffff90, #ffffff); +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile .filter-drawer-mobile-inner-wrapper { + text-align: right; + height: 100%; + overflow: auto; + padding-bottom: 76px; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile .filter-drawer-mobile-inner-wrapper::-webkit-scrollbar { + display: none; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile .close-drawer { + margin-left: auto; + cursor: pointer; + height: 18px; + width: 18px; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-container-mobile .drawer-background { + z-index: 3; + position: fixed; + background: var(--color-gray-600); + top: 0; + left: 0; + height: 100%; + width: 100vw; + transition: opacity 0.5s; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-container-mobile .drawer-background.hidden { + display: none; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-container-mobile .drawer-background.transparent { + opacity: 0; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-button-mobile-wrapper, +.template-x .api-templates-toolbar .functions-drawer .button-wrapper { + font-family: var(--body-font-family); + cursor: pointer; + display: flex; + gap: 4px; + align-items: center; +} + +.template-x .api-templates-toolbar .filter-drawer-mobile .button-wrapper { + margin-bottom: 8px; +} + +.template-x .api-templates-toolbar .filter-drawer-mobile .button-wrapper span { + font-size: 14px; + line-height: 24px; + font-weight: 700; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-button-mobile-wrapper .icon, +.template-x .api-templates-toolbar .functions-drawer .button-wrapper .icon { + height: 18px; + width: 18px; + display: block; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-button-mobile-wrapper .icon.icon-drop-down-arrow, +.template-x .api-templates-toolbar .functions-drawer .button-wrapper .icon.icon-drop-down-arrow { + height: 10px; + width: 10px; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile .function-wrapper { + padding: 12px 0; + border-top: var(--color-gray-200) 1px solid; + overflow: hidden; + transition: max-height 0.2s; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile .function-wrapper:first-of-type { + border-top: none; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile .options-wrapper { + display: flex; + flex-direction: column; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile .options-wrapper .option-button:before { + margin-right: 16px; +} + +.template-x .api-templates-toolbar .apply-filter-button-wrapper { + position: fixed; + bottom: 0; + left: 0; + z-index: 5; + display: flex; + align-items: center; + justify-content: center; + width: 100vw; + height: 76px; + background: var(--color-gray-200); + transition: opacity 0.2s; +} + +.template-x .api-templates-toolbar .apply-filter-button-wrapper.hidden { + display: none; +} + +.template-x .api-templates-toolbar .apply-filter-button-wrapper.transparent { + opacity: 0; +} + +.template-x .api-templates-toolbar .apply-filter-button { + width: 276px; + height: 32px; + font-size: 16px; + border-radius: 100px; + display: flex; + align-items: center; + justify-content: center; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile.hidden { + display: none; +} + +.template-x .api-templates-toolbar .functions-drawer .filter-drawer-mobile.retracted { + transform: translateX(100%); +} + +.template-x .category-list-wrapper .category-list-heading { + margin-bottom: 4px; + height: 24px; + padding: 4px 7px; + border-radius: 8px; + transition: background-color .2s, color .2s; + overflow: hidden; + display: flex; + font-weight: 700; + font-size: var(--body-font-size-s); + align-items: center; + width: max-content; +} + +.template-x .category-list-wrapper .category-list-heading .icon { + height: 18px; + width: 18px; + padding-right: 8px; +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper { + padding: 12px 0; + border-top: var(--color-gray-200) 1px solid; + overflow: auto; + transition: max-height 0.5s; +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper.collapsed { + overflow: hidden; +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper::-webkit-scrollbar { + display: none; +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper .category-list-toggle-wrapper { + margin-bottom: 8px; + cursor: pointer; + display: flex; + gap: 4px; + align-items: center; + position: relative; + padding-right: 16px; +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper .category-list-toggle-wrapper > .icon { + height: 22px; + width: 22px; +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper .category-list-toggle { + font-family: var(--body-font-family); + font-size: 14px; + line-height: 24px; + font-weight: 700; +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper .category-list-toggle-wrapper .icon-drop-down-arrow { + height: 10px; + width: 10px; + position: absolute; + right: 3px; + transform: rotate(180deg); +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper .category-list-toggle-wrapper.collapsed .icon-drop-down-arrow { + transform: rotate(0deg); +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper .category-list { + margin: 0; + list-style: none; + padding-left: unset; +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper .category-list li > .category-list-template-count { + font-family: var(--body-font-family); + margin-left: 2px; +} + +.template-x.with-categories-list .api-templates-toolbar .category-list-wrapper .category-list li a { + color: var(--color-black); + font-weight: 400; + font-size: 14px; + line-height: 24px; + cursor: pointer; +} + +.template-x.with-categories-list .api-templates-toolbar .lottie-wrapper { + position: fixed; + z-index: 2; + bottom: 76px; + right: 0; +} + +.template-x.with-categories-list .api-templates-toolbar .lottie-wrapper .lottie { + height: 54px; + width: 54px; + pointer-events: none; +} + +.template-x.with-categories-list .category-list-wrapper { + display: none; + padding-top: 32px; + text-align: left; + overflow: hidden; + height: max-content; + max-height: 1000px; + min-width: max-content; + transition: max-height 0.5s; +} + +.template-x.with-categories-list .toolbar-wrapper .category-list-wrapper { + display: block; +} + +.template-x.with-categories-list .toolbar-wrapper .category-list-wrapper .category-list-heading { + display: none; +} + +.template-x.with-categories-list .category-list-wrapper::-webkit-scrollbar { + display: none; +} + +.template-x.with-categories-list .category-list-toggle-wrapper { + margin-bottom: 16px; + display: flex; + gap: 4px; + align-items: center; + position: relative; + padding-right: 16px; +} + +.template-x.with-categories-list .category-list-toggle-wrapper .icon.icon-template-free { + height: 18px; + width: 18px; +} + +.template-x.with-categories-list .category-list-toggle { + font-family: var(--body-font-family); + font-size: 16px; + line-height: 24px; + font-weight: 700; + white-space: nowrap; +} + +.template-x.with-categories-list .category-list-resize { + font-family: var(--body-font-family); + color: var(--color-gray-500); + position: relative; + display: flex; + align-items: center; + cursor: pointer; + background: var(--color-white); +} + +.template-x.with-categories-list .category-list-resize:after { + position: absolute; + width: 100%; + height: 80px; + background: linear-gradient(#ffffff00, #ffffff90, #ffffff); + bottom: 100%; +} + +.template-x.with-categories-list .category-list-wrapper ul > li:hover { + background-color: var(--color-gray-200); + color: var(--color-info-accent); + border-radius: 8px; +} + +@media (min-width: 900px) { + .template-x.with-categories-list .category-list-wrapper { + text-align: left; + padding-top: 32px; + } +} + +/* Carousel styles (Template-x specific) */ + +/* hide controls in mobile-breakpoint if they scrolled using drag */ +.template-x.horizontal .carousel-container.controls-hidden .carousel-fader-left, +.template-x.horizontal .carousel-container.controls-hidden .carousel-fader-right { + opacity: 0; + pointer-events: none; +} + +.template-x.horizontal .carousel-container.controls-hidden .carousel-fader-left a.button.carousel-arrow, +.template-x.horizontal .carousel-container.controls-hidden .carousel-fader-right a.button.carousel-arrow { + pointer-events: none; +} + +/* Remove max-width on mobile breakpoints */ +.section.template-x-horizontal-container { + padding-left: 0; + padding-right: 0; +} + +.template-x-wrapper.horizontal { + padding-left: 0; + padding-right: 0; +} + +/* re-add max-width on elements that are not carousel */ +.template-x-wrapper.horizontal > *:not(.template-x.horizontal), +.template-x-wrapper.horizontal:not(.holiday) > .template-x.horizontal > *:not(.carousel-container) { + max-width: unset; + padding-left: 15px; + padding-right: 15px; + margin-right: auto; + margin-left: auto; + margin-bottom: 24px; + margin-block: auto; +} + +main .template-x-wrapper.horizontal > .template-x.horizontal.tabbed > .template-title { + margin-bottom: 0; +} + +.template-x.horizontal .carousel-container { + margin-left: 0; + margin-right: 0; +} + +.template-x.horizontal .carousel-platform { + scroll-padding: 12px; +} + +.template-x.horizontal.fullwidth.holiday .carousel-container .carousel-fader-right { + background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + transform: translate3d(0, 0, 0); + height: 232px; + width: 170px; + right: -2px; +} + +.template-x.horizontal.fullwidth.holiday .carousel-container .carousel-fader-left { + background: linear-gradient(to left, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + transform: translate3d(0, 0, 0); + height: 232px; + width: 170px; + left: -4px +} + +.template-x.horizontal .carousel-container .carousel-fader-right { + background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); +} + +.template-x.horizontal .carousel-container .carousel-fader-left { + background: linear-gradient(to left, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); +} + +main .template-x.horizontal.tabbed .carousel-container .carousel-fader-right, +main .template-x.horizontal.tabbed .carousel-container .carousel-fader-left { + background: none; +} + + +.template-x.horizontal.holiday .template-x-inner-wrapper .carousel-container { + max-width: 1090px; +} + +.template-x.horizontal.holiday.animated video ~ .template-x-inner-wrapper { + padding: 0; + margin: 0; +} + +.template-x.horizontal.holiday.animated video ~ .template-x-inner-wrapper .carousel-container { + max-width: none; +} + +.template-x.mini { + min-height: unset; +} + +.template-x-horizontal-fullwidth-mini-container .template-x .template-title { + margin-bottom: 8px; +} + +.template-x-horizontal-fullwidth-mini-container.toc-container .template-x .template-title h2 { + -webkit-hyphens: none; /* safari */ + hyphens: none; + display: inline; +} + +.template-x-horizontal-fullwidth-mini-container.toc-container .template-x .template-title .toc-slot { + display: inline; + width: 116px; + height: 44px; + float: right; +} + +.template-x-horizontal-fullwidth-mini-container .template-x .template-title p:last-of-type { + margin-top: 16px; + font-weight: 800; +} + +.template-x-horizontal-fullwidth-mini-container .template-x .template-title p:first-of-type { + margin-bottom: 8px; +} + +.template-x-horizontal-fullwidth-mini-container .template-x .template-title p:last-of-type { + margin-top: 16px; + font-weight: 800; +} + +.template-x-horizontal-fullwidth-mini-container .template-title .icon { + height: unset; + width: 167px; +} + +.template-x-horizontal-fullwidth-mini-container .template-x .carousel-fader-left, +.template-x-horizontal-fullwidth-mini-container .template-x .carousel-fader-right { + display: none; +} + +.template-x-horizontal-fullwidth-mini-spreadsheet-powered-container > div, +.template-x-horizontal-fullwidth-mini-container > div { + max-width: unset; + padding: 0; +} + +.template-x.collaboration { + margin-bottom: 40px; +} + +.template-x.collaboration .template-title > div { + display: block; + position: relative; +} + +.template-x.collaboration .template-title .social-links { + display: flex; + gap: 12px; +} + +.template-x.collaboration .template-title .social-links a.template-title-link { + height: 22px; + width: 22px; +} + +.template-x.collaboration .template-title .social-links a.template-title-link svg { + height: 100%; + width: 100%; +} + +.template-x.collaboration .template-title .social-links a.template-title-link:after { + content: unset; +} + +.template-x.collaboration .template-title > div > h3 { + scroll-margin: 72px; + display: flex; + gap: 8px; + font-size: 22px; + line-height: 25px; + color: var(--color-black); +} + +.template-x.collaboration .template-title p:first-of-type { + max-width: 600px; + margin: 16px 0; +} + +.template-x.collaboration .template-title p:last-of-type { + position: absolute; + bottom: 0; + right: 0; +} + +.template-x.collaboration .template-title > div > h3 .collaboration-anchor { + display: inline-block; + position: relative; + height: 24px; + width: 24px; + min-width: 24px; + background-image: url('/express/icons/link.svg'); + background-repeat: no-repeat; + cursor: pointer; +} + +.template-x.collaboration .template-title > div > h3 .collaboration-anchor .clipboard-tag { + background-color: #268e6c; + color: var(--color-white); + white-space: nowrap; + font-size: 12px; + padding: 2px 8px; + border-radius: 6px; + position: absolute; + right: 0; + bottom: 32px; + opacity: 0; + transition: left 0.2s ease-in-out, opacity 0.2s ease-in-out; +} + +.template-x.collaboration .template-title > div > h3 .collaboration-anchor.copied .clipboard-tag { + opacity: 1; +} + +.template-x.collaboration .carousel-container { + margin-left: 6px; +} + +.template-x.horizontal.holiday { + position: relative; + overflow-x: hidden; + background: var(--color-black); +} + +.template-x.horizontal.holiday.animated { + color: var(--color-white); + padding-top: 0; +} + +.template-x.horizontal.holiday.light-text, +.template-x.horizontal.holiday.light-text .toggle-button a, +.template-x.horizontal.holiday.light-text .template-link { + color: var(--color-white); +} + +.template-x.horizontal.holiday.light-text .toggle-button .toggle-button-chev { + border-color: var(--color-white); +} + +.template-x.horizontal.holiday.dark-text, +.template-x.horizontal.holiday.dark-text .toggle-button a, +.template-x.horizontal.holiday.dark-text .template-link { + color: var(--color-gray-800); +} + +.template-x.horizontal.holiday.dark-text .toggle-button .toggle-button-chev { + border-color: var(--color-gray-800); +} + +.template-x.horizontal.holiday p, +.template-x.horizontal.holiday picture { + z-index: 1; +} + +.template-x.horizontal.holiday .toggle-bar { + flex-direction: row; + justify-content: space-between; + gap: 14px; + margin: auto; + align-items: center; +} + +.template-x.horizontal.holiday .toggle-bar p { + font-size: 16px; + margin: 0; +} + +.template-x.horizontal.holiday .toggle-bar img { + display: block; + max-height: 32px; + width: 84px; + min-width: 84px; +} + +.template-x.horizontal.holiday .toggle-button { + align-self: center; + transition: margin 0.4s; +} + +.template-x.horizontal.holiday.mobile .toggle-button { + display: flex; + transform: translate(-50%); + margin-left: 192px; + padding: 0 0 16px 0; + width: 110px; +} + +.template-x.horizontal.holiday.expanded.mobile .toggle-button { + margin-left: 50%; + transform: translate(-50%); +} + +.template-x.horizontal.holiday .toggle-button a:hover { + text-decoration: underline; +} + +.template-x.horizontal.holiday .toggle-button .toggle-button-chev { + display: flex; + width: 6px; + height: 6px; + border-top-width: 0; + border-left-width: 0; + border-bottom-width: 2px; + border-right-width: 2px; + border-style: solid; + transform-origin: 75% 75%; + transform: rotate(45deg); + margin-top: 5px; + margin-left: 5px; + margin-right: 2.25px; + transition: transform 0.2s; + cursor: pointer; +} + +.template-x.horizontal.holiday .toggle-bar-top { + display: flex; + justify-content: space-between; + gap: 22px; + align-items: center; +} + +.template-x.horizontal.holiday .toggle-bar-bottom { + display: flex; + justify-content: flex-end; + gap: 32px; + z-index: 1; +} + +.template-x.horizontal.holiday .template-x-wrapper, +.template-x.holiday, +.template-x.holiday .carousel-container { + max-width: none; + z-index: 1; +} + +.template-x.horizontal.holiday .template-x-wrapper { + max-height: 0; + position: relative; + transition: max-height 0.5s, padding-top 0.5s; + overflow: hidden; +} + +.template-x.horizontal.holiday.expanded .template-x-wrapper { + padding-top: 16px; +} + +.template-x.holiday .carousel-container .carousel-platform { + scroll-padding: initial; + max-height: 200px; + padding: 20px 0; +} + +.template-x-container .template-x-wrapper.horizontal.holiday > .template-x.horizontal > .template-title { + margin: 0; + padding: 0 30px 0 30px; +} + +.template-x-horizontal-holiday-container.expanded .animation-background { + opacity: 1; + transition: opacity 1s; +} + +.template-x.horizontal.holiday .toggle-bar { + max-width: 925px; + cursor: pointer; +} + +.template-x .template .still-wrapper .image-wrapper { + position: relative; +} + +.template-x .template .still-wrapper .image-wrapper .free-tag { + position: absolute; + height: 24px; + display: flex; + border-radius: 1000px; + align-items: center; + font-weight: 500; + width: max-content; + top: 8px; + right: 8px; + color: var(--color-white); + font-size: 12px; + line-height: 16px; + padding: 0 12px; + background: #000000B3; +} + +.template-x .template .still-wrapper .image-wrapper .icon-premium { + position: absolute; + width: 30px; + height: 30px; + bottom: 8px; + right: 8px; +} + +.template-x .template .still-wrapper .image-wrapper .media-type-icon { + position: absolute; + width: 20px; + height: 20px; + background-color: white; + bottom: 8px; + left: 8px; + border-radius: 50%; + box-shadow: 0 0 5px #00000033; +} + +.template-x .template .still-wrapper .image-wrapper .remix-cnt { + position: absolute; + top: 8px; + left: 8px; + background: #000000B3; + opacity: 0.7; + color: white; + border-radius: 8px; + text-align: center; + padding: 2px 6px; + font-size: var(--body-font-size-s); +} + +.template-x .template .still-wrapper .creator-span { + line-height: 20px; + font-size: var(--body-font-size-s); +} + +.template-x .template:not(.placeholder) .button-container { + opacity: 0; + background-color: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 8px; + box-shadow: 0 0 6px #00000029; +} + +.template-x .template .button-container:hover { + z-index: 99; +} + + +.template-x.horizontal .template:not(.placeholder):first-of-type .button-container { + margin-left: 12px; +} + +.template-x .template .button-container .media-wrapper { + position: relative; + height: calc(100% - 34px); + width: 100%; +} + +.template-x.horizontal .template .button-container .media-wrapper { + flex-basis: 100%; +} + +.template-x .template .button-container a { + margin: 6px 6px 0; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.template-x.horizontal .template .button-container a { + min-height: 16px; +} + +.template-x .template .button-container .media-wrapper .icon-share-arrow { + cursor: pointer; + pointer-events: auto; + width: 12px; + height: 12px; + padding: 4px; + background-color: white; + overflow: visible; +} + +.template-x .template .button-container .media-wrapper .share-icon-wrapper { + position: absolute; + top: 8px; + right: 8px; + border-radius: 50%; + height: 20px; +} + +.template-x .template .button-container .media-wrapper .share-icon-wrapper .shared-tooltip { + visibility: hidden; + position: absolute; + display: flex; + align-items: center; + gap: 4px; + width: 130px; + background-color: #33AB84; + color: white; + text-align: center; + border-radius: 6px; + padding: 6px; + top: 50%; + transform: translateY(-50%); + left: calc(100% + 12px); + font-size: var(--body-font-size-s); +} + +.template-x .template .button-container .media-wrapper .share-icon-wrapper .shared-tooltip .icon-checkmark-green { + width: 10px; + height: 10px; + border-radius: 30px; + background-color: var(--color-white); + padding: 2px; +} + +.template-x .template .button-container .media-wrapper .share-icon-wrapper .shared-tooltip.display-tooltip { + visibility: visible; +} + +.template-x .template .button-container .media-wrapper .share-icon-wrapper .shared-tooltip.flipped { + left: unset; + right: calc(100% + 12px); +} + +.template-x .template .button-container .media-wrapper .share-icon-wrapper .shared-tooltip::after { + content: " "; + position: absolute; + top: 50%; + right: 100%; + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent #33AB84 transparent transparent; +} + +.template-x .template .button-container .media-wrapper .share-icon-wrapper .shared-tooltip.flipped::after { + right: unset; + left: 100%; + transform: rotate(180deg); +} + +.template-x .template-x-fallback-msg-wrapper { + font-size: 22px; + text-align: center; + padding: 0 28px; +} + +nav ol.templates-breadcrumbs { + list-style: none; + margin: 0; + padding: 0; + display: flex; +} + +nav ol.templates-breadcrumbs li { + display: flex; + padding: 5px 10px 5px 0; + color: var(--color-gray-500); +} + +nav ol.templates-breadcrumbs li a { + color: var(--color-black); + font-weight: 400; +} + +nav ol.templates-breadcrumbs li:not(:first-child):before { + content: '/'; + color: var(--color-black); + padding: 0 10px 0 0; +} + +@media (max-width: 600px) { + .template-x-wrapper.horizontal { + margin-left: 0; + margin-right: 0; + max-width: unset; + } + + .template-x.horizontal.holiday .template-x-inner-wrapper .carousel-container { + margin-right: 16px; + margin-left: 16px; + } + + .template-x.horizontal.holiday .template-x-inner-wrapper .carousel-container { + margin-left: 16px; + } + + .template-x.fullwidth.holiday.expanded .template-x-inner-wrapper { + margin-left: 16px; + margin-right: 16px; + } +} + +@media (max-width: 900px) { + .template-x.horizontal.holiday .toggle-bar p { + display: none; + } + + .template-x.horizontal.holiday .toggle-bar-bottom { + margin-left: 16px; + } + + .template-x.holiday.mobile .carousel-container { + margin-bottom: 0; + } + + .template-x.horizontal.holiday .toggle-bar h4 { + margin: 0; + } + + .template-x.horizontal.holiday .toggle-bar { + margin-top: 10px; + } +} + +@media (max-width: 1200px) { + .template-x.horizontal.holiday .toggle-bar p { + font-size: 14px; + } + + .template-x.horizontal.holiday .toggle-bar h4 { + font-size: 15px; + } + + .template-x-wrapper.holiday .template-title-link { + font-size: var(--body-font-size-s); + } +} + +@media (min-width: 600px) { + .template-x .toolbar-wrapper { + border-radius: 12px; + } + + .template-x.with-categories-list .category-list-wrapper .category-list-toggle-wrapper { + padding: 4px 8px; + } + + .template-x.with-categories-list .category-list-wrapper .category-list-toggle-wrapper .icon { + transition: transform 0.2s; + } + + .template-x.with-categories-list .category-list-wrapper:hover .category-list-toggle-wrapper .icon { + transform: rotate(180deg); + } + + .template-x.horizontal.holiday .carousel-container .carousel-fader-left, + .template-x.horizontal.holiday .carousel-container .carousel-fader-right { + background: initial; + } + + .template-x.with-categories-list .template-x.fullwidth { + width: calc(100% - 172px); + } + + .template-x.with-categories-list .template-x.fullwidth.sm-view { + max-width: calc(100% - 172px); + } + + .template-x.with-categories-list .template-x.fullwidth.md-view { + max-width: calc(100% - 260px); + } + + .template-x.with-categories-list .template-x.fullwidth.lg-view { + max-width: calc(100% - 337px); + } + + .template-x.fullwidth.lg-view .template.placeholder { + width: 354px; + } + + .template-x.fullwidth.md-view .template.placeholder { + width: 227.33px; + } + + .template-x.fullwidth.sm-view .template.placeholder { + width: 165px; + } + + .template-x-wrapper.fullwidth:not(.horizontal), + .template-x > .default-content-wrapper, + .template-x-wrapper.fullwidth:not(.horizontal), + .template-x:not(.horizontal) > .default-content-wrapper { + max-width: none; + padding: 0 28px; + } + + .template-x.fullwidth.lg-view > .template-x-inner-wrapper > .masonry-col { + max-width: 328px; + text-align: center; + } + + .template-x.fullwidth.md-view > .template-x-inner-wrapper > .masonry-col { + max-width: 200px; + text-align: center; + } + + .template-x.fullwidth.sm-view > .template-x-inner-wrapper > .masonry-col { + max-width: 163px; + text-align: center; + } + + .template-x.fullwidth.lg-view .template.placeholder, + .template-x.fullwidth.md-view .template.placeholder, + .template-x.fullwidth.sm-view .template.placeholder { + max-height: unset; + } + + .template-x.sm-view .template.placeholder > div:first-of-type { + height: 95%; + } + + .template-x .search-bar-wrapper .task-dropdown-list { + right: 0; + } + + .template-x .api-templates-toolbar .search-bar-wrapper .task-dropdown-list { + left: 0; + } +} + +@media (min-width: 900px) { + .template-x.fullwidth.md-view .template.placeholder { + width: 258.5px; + } + + .template-x.fullwidth.sm-view > .template-x-inner-wrapper > .masonry-col { + max-width: 170px; + text-align: center; + } + + .template-x-container > div, + .template-x-fourcols-container > div, + .template-x-horizontal-container > div { + max-width: 900px; + } + + .template-x-wrapper.sixcols { + max-width: 748px; + } + + .template-x.sixcols > .template-x-inner-wrapper > .masonry-col, + .template-x.fullwidth > .template-x-inner-wrapper > .masonry-col { + max-width: 187px; + } + + .template-x .template-title.with-link.horizontal { + display: flex; + justify-content: space-between; + align-items: flex-end; + } + + .template-x-wrapper .template-title.with-link > div { + flex-direction: row; + justify-content: space-between; + } + + .template-x-wrapper .template-x:not(.holiday) .template-title.horizontal .view-all-link-wrapper { + margin: 0; + } + + .template-x-wrapper .template-title.horizontal > div > * { + text-align: left; + } + + .template-x.sixcols > .template-x-inner-wrapper > .masonry-col, + .template-x.fullwidth > .template-x-inner-wrapper > .masonry-col { + max-width: 187px; + } + + .template-x.fullwidth.md-view > .template-x-inner-wrapper > .masonry-col { + max-width: 260px; + text-align: center; + } + + .template-x .template { + width: 240px; + margin-left: 20px; + margin-right: 20px; + } + + .template-x.horizontal.fullwidth .template:not(.placeholder) { + margin: 24px 11px 18px 11px; + } + + .template-x.sixcols .template.placeholder, + .template-x.fullwidth .template.placeholder { + width: 145px; + margin: 21px auto; + } + + .columns .template-x-inner-wrapper { + margin-top: 0; + margin-left: 40px; + } + + .template-x .template:not(.placeholder) .icon-free-badge { + display: none; + } + + .template-x.horizontal .carousel-container { + margin-left: 12px; + margin-right: 12px; + max-width: none; + display: inline-block; + } + + .section.template-x-horizontal-container { + padding-left: 32px; + padding-right: 32px; + } + + .template-x.horizonta:not(.holiday) { + max-width: 800px; + } + + .template-x .api-templates-toolbar h2 { + font-size: 22px; + line-height: 24px; + width: max-content; + } + + .template-x .api-templates-toolbar .wrapper-functions { + flex-grow: 0; + } + + .template-x .api-templates-toolbar .wrapper-content-search .search-bar-wrapper { + width: 480px; + } + + .template-x .search-bar-wrapper .search-dropdown .search-dropdown-heading { + display: unset; + } + + .template-x .search-bar-wrapper .search-dropdown .free-plan-widget { + gap: 32px; + } + + .template-x.collaboration .carousel-container { + margin-left: 0; + } + + .template-x.horizontal.holiday .toggle-button { + display: flex; + } + + .template-x.horizontal.holiday.mobile .toggle-button { + margin-left: 50%; + } + + .template-x.horizontal:not(.fullwidth) { + max-width: 800px; + } +} + +@media (min-width: 1200px) { + .template-x.fullwidth.with-categories-list .template-x-inner-wrapper { + padding-left: 40px; + } + + .template-x.with-categories-list .category-list-wrapper { + display: unset; + background: transparent; + position: absolute; + left: 0; + top: 120px; + height: calc(100% - 64px); + max-height: unset; + overflow: hidden; + min-width: unset; + max-width: 32px; + z-index: 1; + } + + .template-x.with-categories-list .category-list-wrapper ul { + position: relative; + list-style: none; + max-height: 800px; + overflow: hidden; + transition: max-height 0.2s; + margin: 0; + padding-left: 0; + } + + .template-x.with-categories-list .category-list-wrapper ul > li { + margin-bottom: 4px; + border-radius: 8px; + transition: background-color .2s, color .2s; + } + + .template-x.with-categories-list .category-list-wrapper ul > li { + padding: 4px 8px; + } + + .template-x.with-categories-list .category-list-wrapper ul > li:hover { + background-color: var(--color-gray-200); + color: var(--color-info-accent); + } + + .template-x.with-categories-list .category-list-wrapper ul > li.active > a { + color: var(--color-info-accent); + font-weight: 600; + } + + .template-x.with-categories-list .category-list-wrapper ul > li .icon { + height: 18px; + width: 18px; + min-width: 18px; + display: block; + padding-right: 6px; + } + + .template-x.with-categories-list .category-list-wrapper ul > li > a { + font-family: var(--body-font-family); + color: var(--color-black); + font-weight: 400; + font-size: 14px; + line-height: 24px; + cursor: pointer; + user-select: none; + display: flex; + align-items: center; + white-space: nowrap; + } + + .template-x.with-categories-list .category-list-wrapper ul > li > a > .category-list-template-count { + font-family: var(--body-font-family); + margin-left: 2px; + font-size: 14px; + } + + .template-x.with-categories-list .category-list::before { + content: ''; + z-index: 0; + position: absolute; + height: 100%; + transition: opacity 0.2s; + background: linear-gradient(180deg, #FFFFFF00 0%, #FFFFFF 59%); + } + + .template-x.with-categories-list .category-list-wrapper:not(:hover) .category-list::before { + width: 100%; + } + + .template-x.with-categories-list .category-list-wrapper:hover .category-list::before { + width: 0; + } + + .template-x.with-categories-list .category-list-wrapper:hover::before { + content: ''; + opacity: 1; + z-index: -1; + position: absolute; + height: calc(100% - 120px); + width: 100%; + transition: opacity 0.2s; + background: linear-gradient(90deg, #fff 50%, #ffffff9B 88%, #ffffff00 100%); + } + + .template-x.with-categories-list .category-list-wrapper:hover { + min-width: max-content; + padding-right: 160px; + overflow: visible; + } + + .template-x.fullwidth.with-categories-list .template-x-inner-wrapper { + min-height: 600px; + padding-left: 56px; + } + + .template-x.horizontal .carousel-container.controls-hidden .carousel-fader-left, + .template-x.horizontal .carousel-container.controls-hidden .carousel-fader-right { + opacity: unset; + } + + .template-x.horizontal .carousel-container.controls-hidden .carousel-fader-left a.button.carousel-arrow, + .template-x.horizontal .carousel-container.controls-hidden .carousel-fader-right a.button.carousel-arrow { + pointer-events: auto; + } + + .template-x.horizontal .carousel-container .carousel-fader-left.arrow-hidden, + .template-x.horizontal .carousel-container .carousel-fader-right.arrow-hidden { + opacity: 0; + pointer-events: none; + } + + .template-x.horizontal .carousel-container .carousel-fader-left.arrow-hidden a.button.carousel-arrow, + .template-x.horizontal .carousel-container .carousel-fader-right.arrow-hidden a.button.carousel-arrow { + pointer-events: none; + } + + .template-x.horizontal .carousel-container.controls-hidden .carousel-platform { + scroll-snap-type: x mandatory; + } + + .template-x.horizontal .carousel-container { + margin-left: auto; + margin-right: auto; + max-width: 470px; + display: block; + } + + .template-x.horizontal.fullwidth .carousel-container { + margin-bottom: 20px; + max-width: none; + display: block; + } + + .template-x.horizontal .carousel-container .carousel-fader-left { + background: linear-gradient(to left, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + } + + .template-x.horizontal.fullwidth .carousel-container .carousel-fader-left { + width: 150px; + background: linear-gradient(to left, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + } + + .template-x.horizontal .carousel-container .carousel-fader-right { + background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + } + + .template-x.horizontal.fullwidth .carousel-container .carousel-fader-left { + background: linear-gradient(to left, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + width: 150px; + } + + .template-x .api-templates-toolbar { + border-radius: 12px; + } + + .template-x .api-templates-toolbar .wrapper-functions .functions-container { + display: flex; + } + + .template-x .api-templates-toolbar .functions-drawer { + display: none; + } + + .template-x .api-templates-toolbar .wrapper-content-search { + flex-basis: unset; + padding-right: 16px; + } + + .template-x .api-templates-toolbar .functions-drawer .function-sort:hover .options-wrapper { + display: flex; + } + + .template-x .api-templates-toolbar .functions-drawer .function-sort:hover .button-wrapper .icon.icon-drop-down-arrow { + transform: rotate(180deg); + } + + .template-x-horizontal-holiday-container { + padding-bottom: 0; + } + + .template-x-horizontal-holiday-container .toggle-bar { + max-width: 1024px; + } + + .template-x-horizontal-holiday-container .mobile-only { + display: none; + } + + .template-x-horizontal-holiday-container .toggle-bar-bottom { + display: flex; + } + + .template-x-horizontal-holiday-container .template-x-wrapper.expanded { + padding-top: 0; + } + + .template-x .template-x-fallback-msg-wrapper { + padding-left: 60px; + text-align: left; + } + + .template-x-wrapper.fourcols { + max-width: 1200px; + } + + .template-x-wrapper.sixcols { + max-width: 1122px; + } + + .template-x-wrapper.fullwidth { + max-width: none; + } + + /* template-x inside columns */ + + .columns .template-x-inner-wrapper { + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + } + + .columns .template-x .template { + justify-content: flex-start; + min-width: 132px; + } + + .columns .template-x .template-link { + font-size: 0.875rem; + font-weight: 400; + } + + .template-x.fullwidth.with-categories-list .template-x-inner-wrapper { + padding-left: 40px; + padding-right: 0; + } + + .template-x.with-categories-list .category-list-wrapper ul { + position: relative; + list-style: none; + max-height: 800px; + overflow: hidden; + transition: max-height 0.2s; + padding-left: 0; + } + + .template-x.with-categories-list .category-list-wrapper ul > li { + margin-bottom: 4px; + border-radius: 8px; + transition: background-color .2s, color .2s; + } + + .template-x.with-categories-list .category-list-wrapper ul > li { + padding: 4px 8px; + } + + .template-x.with-categories-list .category-list-wrapper ul > li.active > a { + color: var(--color-info-accent); + font-weight: 600; + } + + .template-x.with-categories-list .category-list-wrapper ul > li .icon { + height: 18px; + width: 18px; + min-width: 18px; + display: block; + padding-right: 6px; + } + + .template-x.with-categories-list .category-list-wrapper ul > li > a { + font-family: var(--body-font-family); + color: var(--color-black); + font-weight: 400; + font-size: 14px; + line-height: 24px; + cursor: pointer; + user-select: none; + display: flex; + align-items: center; + white-space: nowrap; + } + + .template-x.with-categories-list .category-list-wrapper ul > li > a > .category-list-template-count { + font-family: var(--body-font-family); + margin-left: 2px; + font-size: 14px; + } + + .template-x.horizontal .carousel-container.controls-hidden .carousel-fader-left, + .template-x.horizontal .carousel-container.controls-hidden .carousel-fader-right { + opacity: unset; + } + + .template-x.horizontal .carousel-container.controls-hidden .carousel-fader-left a.button.carousel-arrow, + .template-x.horizontal .carousel-container.controls-hidden .carousel-fader-right a.button.carousel-arrow { + pointer-events: auto; + } + + .template-x.horizontal .carousel-container .carousel-fader-left.arrow-hidden, + .template-x.horizontal .carousel-container .carousel-fader-right.arrow-hidden { + opacity: 0; + pointer-events: none; + } + + .template-x.horizontal .carousel-container .carousel-fader-left.arrow-hidden a.button.carousel-arrow, + .template-x.horizontal .carousel-container .carousel-fader-right.arrow-hidden a.button.carousel-arrow { + pointer-events: none; + } + + .template-x.horizontal .carousel-container.controls-hidden .carousel-platform { + scroll-snap-type: x mandatory; + } + + .template-x.horizontal.fullwidth .carousel-container { + margin-bottom: 20px; + max-width: none; + display: block; + } + + .template-x.horizontal .carousel-container .carousel-fader-left { + background: linear-gradient(to left, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + } + + .template-x.horizontal.fullwidth .carousel-container .carousel-fader-left { + width: 150px; + background: linear-gradient(to left, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + } + + .template-x.horizontal.holiday .carousel-container .carousel-fader-right { + background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + width: 170px; + } + + .template-x.horizontal.fullwidth.tabbed .carousel-container .carousel-fader-left, + .template-x.horizontal.fullwidth.tabbed .carousel-container .carousel-fader-right { + background: none; + } + + .template-x .api-templates-toolbar { + border-radius: 12px; + } + + .template-x .api-templates-toolbar .wrapper-functions .functions-container { + display: flex; + } + + .template-x .api-templates-toolbar .functions-drawer { + display: none; + } + + .template-x .api-templates-toolbar .wrapper-content-search { + flex-basis: unset; + padding-right: 16px; + height: 41px; + } + + .template-x .api-templates-toolbar .functions-drawer .function-sort:hover .options-wrapper { + display: flex; + } + + .template-x .api-templates-toolbar .functions-drawer .function-sort:hover .button-wrapper .icon.icon-drop-down-arrow { + transform: rotate(180deg); + } + + .template-x-wrapper.horizontal { + max-width: 1024px; + } + + .template-x.horizontal.fullwidth .template-x-inner-wrapper > div, + .template-x-wrapper.horizontal.fullwidth { + max-width: none; + } + + .template-x.horizontal:not(.holiday) { + max-width: 1090px; + } + + .template-x-horizontal-fullwidth-mini-container .template-x.mini { + display: none; + } + + .template-x.collaboration .template-title > div > h3 { + gap: 12px; + } + + .template-x.collaboration .template-title > div > h3 .collaboration-anchor .clipboard-tag { + right: unset; + bottom: unset; + top: 50%; + left: 48px; + transform: translateY(-50%); + } + + .template-x.collaboration .template-title > div > h3 .collaboration-anchor.copied .clipboard-tag { + left: 28px; + } + + .template-x-wrapper.holiday .toggle-bar { + max-width: 1090px; + } + + .template-x.holiday .template-title .toggle-bar-bottom { + display: flex; + flex-grow: 1; + } + + .template-x.horizontal.holiday.expanded .template-x-wrapper { + padding-top: 0; + } +} diff --git a/express/blocks/template-x/template-x.js b/express/blocks/template-x/template-x.js new file mode 100755 index 000000000..361102810 --- /dev/null +++ b/express/blocks/template-x/template-x.js @@ -0,0 +1,1651 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +/* eslint-disable import/named, import/extensions */ + +import { + addSearchQueryToHref, + createOptimizedPicture, + createTag, + decorateMain, + fetchPlaceholders, + getIconElement, + getLanguage, + getLocale, + getLottie, + getMetadata, + lazyLoadLottiePlayer, + titleCase, + toClassName, + transformLinkToAnimation, +} from '../../scripts/scripts.js'; +import { Masonry } from '../shared/masonry.js'; +import { buildCarousel } from '../shared/carousel.js'; +import { fetchTemplates, isValidTemplate, fetchTemplatesCategoryCount } from './template-search-api-v3.js'; +import fetchAllTemplatesMetadata from '../../scripts/all-templates-metadata.js'; +import renderTemplate from './template-rendering.js'; + +function wordStartsWithVowels(word) { + return word.match('^[aieouâêîôûäëïöüàéèùœAIEOUÂÊÎÔÛÄËÏÖÜÀÉÈÙŒ].*'); +} + +function logSearch(form, url = '/express/search-terms-log') { + if (form) { + const input = form.querySelector('input'); + fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + data: { + keyword: input.value, + locale: getLocale(window.location), + timestamp: Date.now(), + audience: document.body.dataset.device, + }, + }), + }); + } +} + +function camelize(str) { + return str.replace(/^\w|[A-Z]|\b\w/g, (word, index) => (index === 0 ? word.toLowerCase() : word.toUpperCase())).replace(/\s+/g, ''); +} + +function handlelize(str) { + return str.normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') // Remove accents + .replace(/(\W+|\s+)/g, '-') // Replace space and other characters by hyphen + .replace(/--+/g, '-') // Replaces multiple hyphens by one hyphen + .replace(/(^-+|-+$)/g, '') // Remove extra hyphens from beginning or end of the string + .toLowerCase(); // To lowercase +} + +function isDarkOverlayReadable(colorString) { + let r; + let g; + let b; + + if (colorString.match(/^rgb/)) { + const colorValues = colorString.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); + [r, g, b] = colorValues.slice(1); + } else { + const hexToRgb = +(`0x${colorString.slice(1).replace(colorString.length < 5 ? /./g : '', '$&$&')}`); + // eslint-disable-next-line no-bitwise + r = (hexToRgb >> 16) & 255; + // eslint-disable-next-line no-bitwise + g = (hexToRgb >> 8) & 255; + // eslint-disable-next-line no-bitwise + b = hexToRgb & 255; + } + + const hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)); + + return hsp > 127.5; +} + +async function fetchAndRenderTemplates(props) { + const [placeholders, { response, fallbackMsg }] = await Promise.all( + [fetchPlaceholders(), fetchTemplates(props)], + ); + if (!response || !response.items || !Array.isArray(response.items)) { + return { templates: null }; + } + + if ('_links' in response) { + // eslint-disable-next-line no-underscore-dangle + const nextQuery = response._links.next.href; + const starts = new URLSearchParams(nextQuery).get('start').split(','); + props.start = starts.join(','); + } else { + props.start = ''; + } + + props.total = response.metadata.totalHits; + + return { + fallbackMsg, + templates: response.items + .filter((item) => isValidTemplate(item)) + .map((template) => renderTemplate(template, placeholders)), + }; +} + +async function processContentRow(block, props) { + const templateTitle = createTag('div', { class: 'template-title' }); + const textWrapper = createTag('div', { class: 'text-wrapper' }); + textWrapper.innerHTML = props.contentRow.outerHTML; + templateTitle.append(textWrapper); + + const aTags = templateTitle.querySelectorAll(':scope a'); + + if (aTags.length > 0) { + templateTitle.classList.add('with-link'); + aTags.forEach((aTag) => { + aTag.className = 'template-title-link'; + + const p = aTag.closest('p'); + if (p) { + templateTitle.append(p); + p.className = 'view-all-link-wrapper'; + } + }); + } + + block.prepend(templateTitle); + + if (props.orientation.toLowerCase() === 'horizontal') templateTitle.classList.add('horizontal'); +} + +async function formatHeadingPlaceholder(props) { + // special treatment for express/ root url + const placeholders = await fetchPlaceholders(); + const locale = getLocale(window.location); + const lang = getLanguage(locale); + const templateCount = lang === 'es-ES' ? props.total.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ') : props.total.toLocaleString(lang); + let toolBarHeading = getMetadata('toolbar-heading') ? props.templateStats : placeholders['template-placeholder']; + + if (getMetadata('template-search-page') === 'Y' + && placeholders['template-search-heading-singular'] + && placeholders['template-search-heading-plural']) { + toolBarHeading = props.total === 1 ? placeholders['template-search-heading-singular'] : placeholders['template-search-heading-plural']; + } + + if (toolBarHeading) { + toolBarHeading = toolBarHeading + .replace('{{quantity}}', props.fallbackMsg ? '0' : templateCount) + .replace('{{Type}}', titleCase(getMetadata('short-title') || getMetadata('q') || getMetadata('topics'))) + .replace('{{type}}', getMetadata('short-title') || getMetadata('q') || getMetadata('topics')); + if (locale === 'fr') { + toolBarHeading.split(' ').forEach((word, index, words) => { + if (index + 1 < words.length) { + if (word === 'de' && wordStartsWithVowels(words[index + 1])) { + words.splice(index, 2, `d'${words[index + 1].toLowerCase()}`); + toolBarHeading = words.join(' '); + } + } + }); + } + } + + return toolBarHeading; +} + +function constructProps(block) { + const props = { + templates: [], + filters: { + locales: 'en', + topics: '', + }, + renditionParams: { + format: 'jpg', + size: 151, + }, + tailButton: '', + limit: 70, + total: 0, + start: '', + collectionId: 'urn:aaid:sc:VA6C2:25a82757-01de-4dd9-b0ee-bde51dd3b418', + sort: '', + masonry: undefined, + headingTitle: null, + headingSlug: null, + viewAllLink: null, + holidayIcon: null, + backgroundColor: '#000B1D', + backgroundAnimation: null, + textColor: '#FFFFFF', + }; + + Array.from(block.children).forEach((row) => { + const cols = row.querySelectorAll('div'); + const key = cols[0].querySelector('strong')?.textContent.trim().toLowerCase(); + if (cols.length === 1) { + [props.contentRow] = cols; + } else if (cols.length === 2) { + const value = cols[1].textContent.trim(); + + if (key && value) { + // FIXME: facebook-post + if (['tasks', 'topics', 'locales', 'behaviors'].includes(key) || (['premium', 'animated'].includes(key) && value.toLowerCase() !== 'all')) { + props.filters[camelize(key)] = value; + } else if (['yes', 'true', 'on', 'no', 'false', 'off'].includes(value.toLowerCase())) { + props[camelize(key)] = ['yes', 'true', 'on'].includes(value.toLowerCase()); + } else if (key === 'collection id') { + props[camelize(key)] = value.replaceAll('\\:', ':'); + } else { + props[camelize(key)] = value; + } + } + } else if (cols.length === 3) { + if (key === 'template stats' && ['yes', 'true', 'on'].includes(cols[1].textContent.trim().toLowerCase())) { + props[camelize(key)] = cols[2].textContent.trim(); + } + } else if (cols.length === 4) { + if (key === 'blank template') { + cols[0].remove(); + props.templates.push(row); + } + } else if (cols.length === 5) { + if (key === 'holiday block' && ['yes', 'true', 'on'].includes(cols[1].textContent.trim().toLowerCase())) { + const backgroundColor = cols[3].textContent.trim().toLowerCase(); + const holidayIcon = cols[2].querySelector('picture'); + const backgroundAnimation = cols[4].querySelector('a'); + + props.holidayBlock = true; + props.holidayIcon = holidayIcon || null; + if (backgroundColor) { + props.backgroundColor = backgroundColor; + } + props.backgroundAnimation = backgroundAnimation || null; + props.textColor = isDarkOverlayReadable(backgroundColor) ? 'dark-text' : 'light-text'; + } + } + }); + + return props; +} + +function populateTemplates(block, props, templates) { + for (let tmplt of templates) { + const isPlaceholder = tmplt.querySelector(':scope > div:first-of-type > img[src*=".svg"], :scope > div:first-of-type > svg'); + const linkContainer = tmplt.querySelector(':scope > div:nth-of-type(2)'); + const rowWithLinkInFirstCol = tmplt.querySelector(':scope > div:first-of-type > a'); + const innerWrapper = block.querySelector('.template-x-inner-wrapper'); + + if (innerWrapper && linkContainer) { + const link = linkContainer.querySelector(':scope a'); + if (link) { + if (isPlaceholder) { + const aTag = createTag('a', { + href: link.href ? addSearchQueryToHref(link.href) : '#', + }); + + aTag.append(...tmplt.children); + tmplt.remove(); + tmplt = aTag; + // convert A to SPAN + const newLink = createTag('span', { class: 'template-link' }); + newLink.append(link.textContent.trim()); + + linkContainer.innerHTML = ''; + linkContainer.append(newLink); + } + innerWrapper.append(tmplt); + } + } + + if (rowWithLinkInFirstCol && !tmplt.querySelector('img')) { + props.tailButton = rowWithLinkInFirstCol; + rowWithLinkInFirstCol.remove(); + } + + if (tmplt.children.length === 3) { + // look for options in last cell + const overlayCell = tmplt.querySelector(':scope > div:last-of-type'); + const option = overlayCell.textContent.trim(); + if (option) { + if (isPlaceholder) { + // add aspect ratio to template + const sep = option.includes(':') ? ':' : 'x'; + const ratios = option.split(sep).map((e) => +e); + props.placeholderFormat = ratios; + if (block.classList.contains('horizontal')) { + const height = block.classList.contains('mini') ? 100 : 200; + if (ratios[1]) { + const width = (ratios[0] / ratios[1]) * height; + tmplt.style = `width: ${width}px`; + if (width / height > 1.3) { + tmplt.classList.add('tall'); + } + } + } else { + const width = block.classList.contains('sixcols') || block.classList.contains('fullwidth') ? 165 : 200; + if (ratios[1]) { + const height = (ratios[1] / ratios[0]) * width; + tmplt.style = `height: ${height - 21}px`; + if (width / height > 1.3) { + tmplt.classList.add('wide'); + } + } + } + } else { + // add icon to 1st cell + const $icon = getIconElement(toClassName(option)); + $icon.setAttribute('title', option); + tmplt.children[0].append($icon); + } + } + overlayCell.remove(); + } + + if (!tmplt.querySelectorAll(':scope > div > *').length) { + // remove empty row + tmplt.remove(); + } + tmplt.classList.add('template'); + + if (isPlaceholder) { + tmplt.classList.add('placeholder'); + } + } +} + +function updateLoadMoreButton(block, props, loadMore) { + if (props.start === '') { + loadMore.style.display = 'none'; + } else { + loadMore.style.removeProperty('display'); + } +} + +async function decorateNewTemplates(block, props, options = { reDrawMasonry: false }) { + const { templates: newTemplates } = await fetchAndRenderTemplates(props); + const loadMore = block.parentElement.querySelector('.load-more'); + + props.templates = props.templates.concat(newTemplates); + populateTemplates(block, props, newTemplates); + + const newCells = Array.from(block.querySelectorAll('.template:not(.appear)')); + + if (options.reDrawMasonry) { + props.masonry.cells = [props.masonry.cells[0]].concat(newCells); + } else { + props.masonry.cells = props.masonry.cells.concat(newCells); + } + props.masonry.draw(newCells); + + if (loadMore) { + updateLoadMoreButton(block, props, loadMore); + } +} + +async function decorateLoadMoreButton(block, props) { + const placeholders = await fetchPlaceholders(); + const loadMoreDiv = createTag('div', { class: 'load-more' }); + const loadMoreButton = createTag('button', { class: 'load-more-button' }); + const loadMoreText = createTag('p', { class: 'load-more-text' }); + loadMoreDiv.append(loadMoreButton, loadMoreText); + loadMoreText.textContent = placeholders['load-more'] ?? ''; + block.append(loadMoreDiv); + loadMoreButton.append(getIconElement('plus-icon')); + + loadMoreButton.addEventListener('click', async () => { + loadMoreButton.classList.add('disabled'); + const scrollPosition = window.scrollY; + await decorateNewTemplates(block, props); + window.scrollTo({ + top: scrollPosition, + left: 0, + behavior: 'smooth', + }); + loadMoreButton.classList.remove('disabled'); + }); + + return loadMoreDiv; +} + +async function fetchBlueprint(pathname) { + if (window.spark.bluePrint) { + return (window.spark.bluePrint); + } + + const bpPath = pathname.substr(pathname.indexOf('/', 1)) + .split('.')[0]; + const resp = await fetch(`${bpPath}.plain.html`); + const body = await resp.text(); + const $main = createTag('main'); + $main.innerHTML = body; + await decorateMain($main); + + window.spark.bluePrint = $main; + return ($main); +} + +async function attachFreeInAppPills(block) { + const freeInAppText = await fetchPlaceholders().then((json) => json['free-in-app']); + + const templateLinks = block.querySelectorAll('a.template'); + for (const templateLink of templateLinks) { + if (!block.classList.contains('apipowered') + && templateLink.querySelectorAll('.icon-premium').length <= 0 + && !templateLink.classList.contains('placeholder') + && !templateLink.querySelector('.icon-free-badge') + && freeInAppText) { + const $freeInAppBadge = createTag('span', { class: 'icon icon-free-badge' }); + $freeInAppBadge.textContent = freeInAppText; + templateLink.querySelector('div').append($freeInAppBadge); + } + } +} + +function makeTemplateFunctions(placeholders) { + const functions = { + premium: { + placeholders: JSON.parse(placeholders['template-filter-premium'] ?? '{}'), + elements: {}, + icons: placeholders['template-filter-premium-icons']?.replace(/\s/g, '')?.split(',') + || ['template-premium-and-free', 'template-free', 'template-premium'], + }, + animated: { + placeholders: JSON.parse(placeholders['template-filter-animated'] ?? '{}'), + elements: {}, + icons: placeholders['template-filter-animated-icons']?.replace(/\s/g, '')?.split(',') + || ['template-static-and-animated', 'template-static', 'template-animated'], + }, + sort: { + placeholders: JSON.parse(placeholders['template-x-sort'] ?? '{}'), + elements: {}, + icons: placeholders['template-x-sort-icons']?.replace(/\s/g, '')?.split(',') + || ['sort', 'visibility-on', 'visibility-off', 'order-dsc', 'order-asc'], + }, + }; + + Object.entries(functions).forEach((entry) => { + entry[1].elements.wrapper = createTag('div', { + class: `function-wrapper function-${entry[0]}`, + 'data-param': entry[0], + }); + + entry[1].elements.wrapper.subElements = { + button: { + wrapper: createTag('div', { class: `button-wrapper button-wrapper-${entry[0]}` }), + subElements: { + iconHolder: createTag('span', { class: 'icon-holder' }), + textSpan: createTag('span', { class: `current-option current-option-${entry[0]}` }), + chevIcon: getIconElement('drop-down-arrow'), + }, + }, + options: { + wrapper: createTag('div', { class: `options-wrapper options-wrapper-${entry[0]}` }), + subElements: Object.entries(entry[1].placeholders).map((option, subIndex) => { + const icon = getIconElement(entry[1].icons[subIndex]); + const optionButton = createTag('div', { class: 'option-button', 'data-value': option[1] }); + [optionButton.textContent] = option; + optionButton.prepend(icon); + return optionButton; + }), + }, + }; + + const $span = entry[1].elements.wrapper.subElements.button.subElements.textSpan; + [[$span.textContent]] = Object.entries(entry[1].placeholders); + }); + + return functions; +} + +function updateFilterIcon(block) { + const functionWrapper = block.querySelectorAll('.function-wrapper'); + const optionsWrapper = block.querySelectorAll('.options-wrapper'); + + functionWrapper.forEach((wrap, index) => { + const iconHolder = wrap.querySelector('.icon-holder'); + const activeOption = optionsWrapper[index].querySelector('.option-button.active'); + if (iconHolder && activeOption) { + const activeIcon = activeOption.querySelector('.icon'); + if (activeIcon) { + iconHolder.innerHTML = activeIcon.outerHTML; + } + } + }); +} + +function decorateFunctionsContainer(block, functions, placeholders) { + const functionsContainer = createTag('div', { class: 'functions-container' }); + const functionContainerMobile = createTag('div', { class: 'functions-drawer' }); + + Object.values(functions).forEach((filter) => { + const filterWrapper = filter.elements.wrapper; + + Object.values(filterWrapper.subElements).forEach((part) => { + const innerWrapper = part.wrapper; + + Object.values(part.subElements).forEach((innerElement) => { + if (innerElement) { + innerWrapper.append(innerElement); + } + }); + + filterWrapper.append(innerWrapper); + }); + functionContainerMobile.append(filterWrapper.cloneNode({ deep: true })); + functionsContainer.append(filterWrapper); + }); + + // restructure drawer for mobile design + const filterContainer = createTag('div', { class: 'filter-container-mobile' }); + const mobileFilterButtonWrapper = createTag('div', { class: 'filter-button-mobile-wrapper' }); + const mobileFilterButton = createTag('span', { class: 'filter-button-mobile' }); + const drawer = createTag('div', { class: 'filter-drawer-mobile hidden retracted' }); + const drawerInnerWrapper = createTag('div', { class: 'filter-drawer-mobile-inner-wrapper' }); + const drawerBackground = createTag('div', { class: 'drawer-background hidden transparent' }); + const $closeButton = getIconElement('search-clear'); + const applyButtonWrapper = createTag('div', { class: 'apply-filter-button-wrapper hidden transparent' }); + const applyButton = createTag('a', { class: 'apply-filter-button button gradient', href: '#' }); + + $closeButton.classList.add('close-drawer'); + applyButton.textContent = placeholders['apply-filters']; + + functionContainerMobile.children[0] + .querySelector('.current-option-premium') + .textContent = `${placeholders.free} ${placeholders['versus-shorthand']} ${placeholders.premium}`; + + functionContainerMobile.children[1] + .querySelector('.current-option-animated') + .textContent = `${placeholders.static} ${placeholders['versus-shorthand']} ${placeholders.animated}`; + + drawerInnerWrapper.append( + functionContainerMobile.children[0], + functionContainerMobile.children[1], + ); + + drawer.append($closeButton, drawerInnerWrapper); + + const buttonsInDrawer = drawer.querySelectorAll('.button-wrapper'); + const optionsInDrawer = drawer.querySelectorAll('.options-wrapper'); + + [buttonsInDrawer, optionsInDrawer].forEach((category) => { + category.forEach((element) => { + element.classList.add('in-drawer'); + const heading = element.querySelector('.current-option'); + const iconHolder = element.querySelector('.icon-holder'); + if (heading) { + heading.className = 'filter-mobile-option-heading'; + } + if (iconHolder) { + iconHolder.remove(); + } + }); + }); + + mobileFilterButtonWrapper.append(getIconElement('scratch-icon-22'), mobileFilterButton); + applyButtonWrapper.append(applyButton); + filterContainer.append( + mobileFilterButtonWrapper, + drawer, + applyButtonWrapper, + drawerBackground, + ); + functionContainerMobile.prepend(filterContainer); + + mobileFilterButton.textContent = placeholders.filter; + const sortButton = functionContainerMobile.querySelector('.current-option-sort'); + if (sortButton) { + sortButton.textContent = placeholders.sort; + sortButton.className = 'filter-mobile-option-heading'; + } + + return { mobile: functionContainerMobile, desktop: functionsContainer }; +} + +function updateLottieStatus(block) { + const drawer = block.querySelector('.filter-drawer-mobile'); + const inWrapper = drawer.querySelector('.filter-drawer-mobile-inner-wrapper'); + const lottieArrows = drawer.querySelector('.lottie-wrapper'); + if (lottieArrows) { + if (inWrapper.scrollHeight - inWrapper.scrollTop === inWrapper.offsetHeight) { + lottieArrows.style.display = 'none'; + drawer.classList.remove('scrollable'); + } else { + lottieArrows.style.removeProperty('display'); + drawer.classList.add('scrollable'); + } + } +} + +async function decorateCategoryList(block, props) { + const placeholders = await fetchPlaceholders(); + const locale = getLocale(window.location); + const mobileDrawerWrapper = block.querySelector('.filter-drawer-mobile'); + const drawerWrapper = block.querySelector('.filter-drawer-mobile-inner-wrapper'); + const categories = placeholders['x-task-categories'] ? JSON.parse(placeholders['x-task-categories']) : {}; + const categoryIcons = placeholders['task-category-icons']?.replace(/\s/g, '')?.split(','); + const categoriesDesktopWrapper = createTag('div', { class: 'category-list-wrapper' }); + const categoriesToggleWrapper = createTag('div', { class: 'category-list-toggle-wrapper' }); + const categoriesToggle = getIconElement('drop-down-arrow'); + const categoriesListHeading = createTag('div', { class: 'category-list-heading' }); + const categoriesList = createTag('ul', { class: 'category-list' }); + + categoriesListHeading.append(getIconElement('template-search'), placeholders['jump-to-category']); + categoriesToggleWrapper.append(categoriesToggle); + categoriesDesktopWrapper.append(categoriesToggleWrapper, categoriesListHeading, categoriesList); + + Object.entries(categories).forEach((category, index) => { + const format = `${props.placeholderFormat[0]}:${props.placeholderFormat[1]}`; + const targetTasks = category[1]; + const currentTasks = props.filters.tasks ? props.filters.tasks : "''"; + const currentTopic = props.filters.topics || props.q; + + const listItem = createTag('li'); + if (category[1] === currentTasks) { + listItem.classList.add('active'); + } + + let icon; + if (categoryIcons[index] && categoryIcons[index] !== '') { + icon = categoryIcons[index]; + } else { + icon = 'template-static'; + } + + const iconElement = getIconElement(icon); + const urlPrefix = locale === 'us' ? '' : `/${locale}`; + const a = createTag('a', { + 'data-tasks': targetTasks, + href: `${urlPrefix}/express/templates/search?tasks=${targetTasks}&tasksx=${targetTasks}&phformat=${format}&topics=${currentTopic || "''"}&q=${currentTopic || ''}`, + }); + [a.textContent] = category; + + a.prepend(iconElement); + listItem.append(a); + categoriesList.append(listItem); + }); + + const categoriesMobileWrapper = categoriesDesktopWrapper.cloneNode({ deep: true }); + const mobileCategoriesToggle = createTag('span', { class: 'category-list-toggle' }); + mobileCategoriesToggle.textContent = placeholders['jump-to-category'] ?? ''; + categoriesMobileWrapper.querySelector('.category-list-toggle-wrapper > .icon')?.replaceWith(mobileCategoriesToggle); + const lottieArrows = createTag('a', { class: 'lottie-wrapper' }); + mobileDrawerWrapper.append(lottieArrows); + drawerWrapper.append(categoriesMobileWrapper); + lottieArrows.innerHTML = getLottie('purple-arrows', '/express/icons/purple-arrows.json'); + lazyLoadLottiePlayer(); + + block.prepend(categoriesDesktopWrapper); + block.classList.add('with-categories-list'); + + const toggleButton = categoriesMobileWrapper.querySelector('.category-list-toggle-wrapper'); + toggleButton.append(getIconElement('drop-down-arrow')); + toggleButton.addEventListener('click', () => { + const listWrapper = toggleButton.parentElement; + toggleButton.classList.toggle('collapsed'); + if (toggleButton.classList.contains('collapsed')) { + if (listWrapper.classList.contains('desktop-only')) { + listWrapper.classList.add('collapsed'); + listWrapper.style.maxHeight = '40px'; + } else { + listWrapper.classList.add('collapsed'); + listWrapper.style.maxHeight = '24px'; + } + } else { + listWrapper.classList.remove('collapsed'); + listWrapper.style.maxHeight = '1000px'; + } + + setTimeout(() => { + if (!listWrapper.classList.contains('desktop-only')) { + updateLottieStatus(block); + } + }, 510); + }, { passive: true }); + + lottieArrows.addEventListener('click', () => { + drawerWrapper.scrollBy({ + top: 300, + behavior: 'smooth', + }); + }, { passive: true }); + + drawerWrapper.addEventListener('scroll', () => { + updateLottieStatus(block); + }, { passive: true }); +} + +function closeDrawer(toolBar) { + const drawerBackground = toolBar.querySelector('.drawer-background'); + const drawer = toolBar.querySelector('.filter-drawer-mobile'); + const applyButton = toolBar.querySelector('.apply-filter-button-wrapper'); + + drawer.classList.add('retracted'); + drawerBackground.classList.add('transparent'); + applyButton.classList.add('transparent'); + + setTimeout(() => { + drawer.classList.add('hidden'); + drawerBackground.classList.add('hidden'); + applyButton.classList.add('hidden'); + }, 500); +} + +function updateOptionsStatus(block, props, toolBar) { + const wrappers = toolBar.querySelectorAll('.function-wrapper'); + const waysOfSort = { + 'Most Relevant': '', + 'Most Viewed': '&orderBy=-remixCount', + 'Rare & Original': '&orderBy=remixCount', + 'Newest to Oldest': '&orderBy=-availabilityDate', + 'Oldest to Newest': '&orderBy=availabilityDate', + }; + + wrappers.forEach((wrapper) => { + const currentOption = wrapper.querySelector('.current-option'); + const options = wrapper.querySelectorAll('.option-button'); + + options.forEach((option) => { + const paramType = wrapper.dataset.param; + const paramValue = option.dataset.value; + const filterValue = props.filters[paramType] ? props.filters[paramType] : 'remove'; + const sortValue = waysOfSort[props[paramType]] || props[paramType]; + + if (filterValue === paramValue || sortValue === paramValue) { + if (currentOption) { + currentOption.textContent = option.textContent; + } + + options.forEach((o) => { + if (option !== o) { + o.classList.remove('active'); + } + }); + + option.classList.add('active'); + } + }); + + updateFilterIcon(block); + }); +} + +function initDrawer(block, props, toolBar) { + const filterButton = toolBar.querySelector('.filter-button-mobile-wrapper'); + const drawerBackground = toolBar.querySelector('.drawer-background'); + const drawer = toolBar.querySelector('.filter-drawer-mobile'); + const closeDrawerBtn = toolBar.querySelector('.close-drawer'); + const applyButton = toolBar.querySelector('.apply-filter-button-wrapper'); + + const functionWrappers = drawer.querySelectorAll('.function-wrapper'); + + let currentFilters; + + filterButton.addEventListener('click', () => { + currentFilters = { ...props.filters }; + drawer.classList.remove('hidden'); + drawerBackground.classList.remove('hidden'); + applyButton.classList.remove('hidden'); + updateLottieStatus(block); + + setTimeout(() => { + drawer.classList.remove('retracted'); + drawerBackground.classList.remove('transparent'); + applyButton.classList.remove('transparent'); + functionWrappers.forEach((wrapper) => { + const button = wrapper.querySelector('.button-wrapper'); + if (button) { + button.style.maxHeight = `${button.nextElementSibling.offsetHeight}px`; + } + }); + }, 100); + }, { passive: true }); + + [drawerBackground, closeDrawerBtn].forEach((el) => { + el.addEventListener('click', async () => { + props.filters = { ...currentFilters }; + closeDrawer(toolBar); + updateOptionsStatus(block, props, toolBar); + }, { passive: true }); + }); + + drawer.classList.remove('hidden'); + functionWrappers.forEach((wrapper) => { + const button = wrapper.querySelector('.button-wrapper'); + let maxHeight; + if (button) { + const wrapperMaxHeightGrabbed = setInterval(() => { + if (wrapper.offsetHeight > 0) { + maxHeight = `${wrapper.offsetHeight}px`; + wrapper.style.maxHeight = maxHeight; + clearInterval(wrapperMaxHeightGrabbed); + } + }, 200); + + button.addEventListener('click', (e) => { + e.stopPropagation(); + const btnWrapper = wrapper.querySelector('.button-wrapper'); + if (btnWrapper) { + const minHeight = `${btnWrapper.offsetHeight - 8}px`; + wrapper.classList.toggle('collapsed'); + wrapper.style.maxHeight = wrapper.classList.contains('collapsed') ? minHeight : maxHeight; + } + }, { passive: true }); + } + }); + + drawer.classList.add('hidden'); +} + +function updateQuery(functionWrapper, props, option) { + const paramType = functionWrapper.dataset.param; + const paramValue = option.dataset.value; + + if (paramType === 'sort') { + props.sort = paramValue; + } else { + const filtersObj = props.filters; + + if (paramType in filtersObj) { + if (paramValue === 'remove') { + delete filtersObj[paramType]; + } else { + filtersObj[paramType] = `${paramValue}`; + } + } else if (paramValue !== 'remove') { + filtersObj[paramType] = `${paramValue}`; + } + + props.filters = filtersObj; + } +} + +async function redrawTemplates(block, existingProps, props, toolBar) { + if (JSON.stringify(props) === JSON.stringify(existingProps)) return; + const heading = toolBar.querySelector('h2'); + const currentTotal = props.total.toLocaleString('en-US'); + props.templates = [props.templates[0]]; + props.start = ''; + block.querySelectorAll('.template:not(.placeholder)').forEach((card) => { + card.remove(); + }); + + await decorateNewTemplates(block, props, { reDrawMasonry: true }); + + heading.textContent = heading.textContent.replace(`${currentTotal}`, props.total.toLocaleString('en-US')); + updateOptionsStatus(block, props, toolBar); + if (block.querySelectorAll('.template').length <= 0) { + const $viewButtons = toolBar.querySelectorAll('.view-toggle-button'); + $viewButtons.forEach((button) => { + button.classList.remove('active'); + }); + ['sm-view', 'md-view', 'lg-view'].forEach((className) => { + block.classList.remove(className); + }); + } +} + +async function initFilterSort(block, props, toolBar) { + const buttons = toolBar.querySelectorAll('.button-wrapper'); + const applyFilterButton = toolBar.querySelector('.apply-filter-button'); + let existingProps = { ...props, filters: { ...props.filters } }; + + if (buttons.length > 0) { + buttons.forEach((button) => { + const wrapper = button.parentElement; + const currentOption = wrapper.querySelector('span.current-option'); + const optionsList = button.nextElementSibling; + const options = optionsList.querySelectorAll('.option-button'); + + button.addEventListener('click', () => { + existingProps = { ...props, filters: { ...props.filters } }; + if (!button.classList.contains('in-drawer')) { + buttons.forEach((b) => { + if (button !== b) { + b.parentElement.classList.remove('opened'); + } + }); + + wrapper.classList.toggle('opened'); + } + }, { passive: true }); + + options.forEach((option) => { + const updateOptions = () => { + buttons.forEach((b) => { + b.parentElement.classList.remove('opened'); + }); + + if (currentOption) { + currentOption.textContent = option.textContent; + } + + options.forEach((o) => { + if (option !== o) { + o.classList.remove('active'); + } + }); + option.classList.add('active'); + }; + + option.addEventListener('click', async (e) => { + e.stopPropagation(); + updateOptions(); + updateQuery(wrapper, props, option); + updateFilterIcon(block); + + if (!button.classList.contains('in-drawer')) { + await redrawTemplates(block, existingProps, props, toolBar); + } + }, { passive: true }); + }); + + document.addEventListener('click', (e) => { + const { target } = e; + if (target !== wrapper && !wrapper.contains(target) && !button.classList.contains('in-drawer')) { + wrapper.classList.remove('opened'); + } + }, { passive: true }); + }); + + if (applyFilterButton) { + applyFilterButton.addEventListener('click', async (e) => { + e.preventDefault(); + await redrawTemplates(block, existingProps, props, toolBar); + closeDrawer(toolBar); + }); + } + + // sync current filter & sorting method with toolbar current options + updateOptionsStatus(block, props, toolBar); + } +} + +function getPlaceholderWidth(block) { + let width; + if (window.innerWidth >= 900) { + if (block.classList.contains('sm-view')) { + width = 165; + } + + if (block.classList.contains('md-view')) { + width = 258.5; + } + + if (block.classList.contains('lg-view')) { + width = 352; + } + } else if (window.innerWidth >= 600) { + if (block.classList.contains('sm-view')) { + width = 165; + } + + if (block.classList.contains('md-view')) { + width = 227.33; + } + + if (block.classList.contains('lg-view')) { + width = 352; + } + } else { + if (block.classList.contains('sm-view')) { + width = 106.33; + } + + if (block.classList.contains('md-view')) { + width = 165.5; + } + + if (block.classList.contains('lg-view')) { + width = 335; + } + } + + return width; +} + +function toggleMasonryView(block, props, button, toggleButtons) { + const templatesToView = block.querySelectorAll('.template:not(.placeholder)'); + const blockWrapper = block.closest('.template-x-wrapper'); + + if (!button.classList.contains('active') && templatesToView.length > 0) { + toggleButtons.forEach((b) => { + if (b !== button) { + b.classList.remove('active'); + } + }); + + ['sm-view', 'md-view', 'lg-view'].forEach((className) => { + if (className !== `${button.dataset.view}-view`) { + block.classList.remove(className); + blockWrapper.classList.remove(className); + } + }); + button.classList.add('active'); + block.classList.add(`${button.dataset.view}-view`); + blockWrapper.classList.add(`${button.dataset.view}-view`); + + props.masonry.draw(); + } + + const placeholder = block.querySelector('.template.placeholder'); + const ratios = props.placeholderFormat; + const width = getPlaceholderWidth(block); + + if (ratios[1]) { + const height = (ratios[1] / ratios[0]) * width; + placeholder.style = `height: ${height - 21}px`; + if (width / height > 1.3) { + placeholder.classList.add('wide'); + } + } +} + +function initViewToggle(block, props, toolBar) { + const toggleButtons = toolBar.querySelectorAll('.view-toggle-button '); + block.classList.add('sm-view'); + block.parentElement.classList.add('sm-view'); + toggleButtons[0].classList.add('active'); + + toggleButtons.forEach((button) => { + button.addEventListener('click', () => { + toggleMasonryView(block, props, button, toggleButtons); + }, { passive: true }); + }); +} + +function initToolbarShadow(block, toolbar) { + const toolbarWrapper = toolbar.parentElement; + document.addEventListener('scroll', () => { + if (toolbarWrapper.getBoundingClientRect().top <= 0) { + toolbarWrapper.classList.add('with-box-shadow'); + } else { + toolbarWrapper.classList.remove('with-box-shadow'); + } + }); +} + +async function decorateToolbar(block, props) { + const placeholders = await fetchPlaceholders(); + const sectionHeading = createTag('h2'); + const tBarWrapper = createTag('div', { class: 'toolbar-wrapper' }); + const tBar = createTag('div', { class: 'api-templates-toolbar' }); + const contentWrapper = createTag('div', { class: 'wrapper-content-search' }); + const functionsWrapper = createTag('div', { class: 'wrapper-functions' }); + + if (props.templateStats) { + sectionHeading.textContent = await formatHeadingPlaceholder(props) || ''; + } + + block.prepend(tBarWrapper); + tBarWrapper.append(tBar); + tBar.append(contentWrapper, functionsWrapper); + contentWrapper.append(sectionHeading); + + if (tBar) { + const viewsWrapper = createTag('div', { class: 'views' }); + + const smView = createTag('a', { class: 'view-toggle-button small-view', 'data-view': 'sm' }); + smView.append(getIconElement('small_grid')); + const mdView = createTag('a', { class: 'view-toggle-button medium-view', 'data-view': 'md' }); + mdView.append(getIconElement('medium_grid')); + const lgView = createTag('a', { class: 'view-toggle-button large-view', 'data-view': 'lg' }); + lgView.append(getIconElement('large_grid')); + + const functionsObj = makeTemplateFunctions(placeholders); + const functions = decorateFunctionsContainer(block, functionsObj, placeholders); + + viewsWrapper.append(smView, mdView, lgView); + functionsWrapper.append(viewsWrapper, functions.desktop); + + tBar.append(contentWrapper, functionsWrapper, functions.mobile); + + initDrawer(block, props, tBar); + initFilterSort(block, props, tBar); + initViewToggle(block, props, tBar); + initToolbarShadow(block, tBar); + } +} + +function initExpandCollapseToolbar(block, templateTitle, toggle, link) { + const chev = block.querySelector('.toggle-button-chev'); + templateTitle.addEventListener('click', () => block.classList.toggle('expanded')); + chev.addEventListener('click', (e) => { + e.stopPropagation(); + block.classList.toggle('expanded'); + }); + + toggle.addEventListener('click', () => block.classList.toggle('expanded')); + link.addEventListener('click', (e) => e.stopPropagation()); + + setTimeout(() => { + if (!block.matches(':hover')) { + block.classList.toggle('expanded'); + } + }, 3000); +} + +function decorateHoliday(block, props) { + const mobileViewport = window.innerWidth < 901; + const templateTitle = block.querySelector('.template-title'); + const toggleBar = templateTitle.querySelector('div'); + const heading = templateTitle.querySelector('h4'); + const subheading = templateTitle.querySelector('p'); + const link = templateTitle.querySelector('.template-title-link'); + const linkWrapper = link.closest('p'); + const toggle = createTag('div', { class: 'expanded toggle-button' }); + const topElements = createTag('div', { class: 'toggle-bar-top' }); + const bottomElements = createTag('div', { class: 'toggle-bar-bottom' }); + const toggleChev = createTag('div', { class: 'toggle-button-chev' }); + const carouselFaderLeft = block.querySelector('.carousel-fader-left'); + const carouselFaderRight = block.querySelector('.carousel-fader-right'); + + if (props.holidayIcon) topElements.append(props.holidayIcon); + if (props.backgroundAnimation) { + const animation = transformLinkToAnimation(props.backgroundAnimation); + block.classList.add('animated'); + block.prepend(animation); + } + + if (props.backgroundColor) { + if (props.backgroundAnimation) { + carouselFaderRight.style.backgroundImage = 'none'; + carouselFaderLeft.style.backgroundImage = 'none'; + } else { + carouselFaderRight.style.backgroundImage = `linear-gradient(to right, rgba(0, 255, 255, 0), ${props.backgroundColor}`; + carouselFaderLeft.style.backgroundImage = `linear-gradient(to left, rgba(0, 255, 255, 0), ${props.backgroundColor}`; + } + } + + block.classList.add('expanded', props.textColor); + toggleBar.classList.add('toggle-bar'); + topElements.append(heading); + toggle.append(link, toggleChev); + linkWrapper.remove(); + bottomElements.append(subheading); + toggleBar.append(topElements, bottomElements); + block.style.backgroundColor = props.backgroundColor; + + if (mobileViewport) { + block.classList.add('mobile'); + block.append(toggle); + } else { + toggleBar.append(toggle); + } + + initExpandCollapseToolbar(block, templateTitle, toggle, link); +} + +async function decorateTemplates(block, props) { + const locale = getLocale(window.location); + const innerWrapper = block.querySelector('.template-x-inner-wrapper'); + + let rows = block.children.length; + if ((rows === 0 || block.querySelectorAll('img').length === 0) && locale !== 'us') { + const i18nTexts = block.firstElementChild + // author defined localized edit text(s) + && (block.firstElementChild.querySelector('p') + // multiple lines in separate p tags + ? Array.from(block.querySelectorAll('p')) + .map((p) => p.textContent.trim()) + // single text directly in div + : [block.firstElementChild.textContent.trim()]); + block.innerHTML = ''; + const tls = Array.from(block.closest('main').querySelectorAll('.template-x')); + const i = tls.indexOf(block); + + const bluePrint = await fetchBlueprint(window.location.pathname); + + const $bpBlocks = bluePrint.querySelectorAll('.template-x'); + if ($bpBlocks[i] && $bpBlocks[i].className === block.className) { + block.innerHTML = $bpBlocks[i].innerHTML; + } else if ($bpBlocks.length > 1 && $bpBlocks[i].className !== block.className) { + for (let x = 0; x < $bpBlocks.length; x += 1) { + if ($bpBlocks[x].className === block.className) { + block.innerHTML = $bpBlocks[x].innerHTML; + break; + } + } + } else { + block.remove(); + } + + if (i18nTexts && i18nTexts.length > 0) { + const [placeholderText] = i18nTexts; + let [, templateText] = i18nTexts; + if (!templateText) { + templateText = placeholderText; + } + block.querySelectorAll('a') + .forEach((aTag, index) => { + aTag.textContent = index === 0 ? placeholderText : templateText; + }); + } + + const heroPicture = document.querySelector('.hero-bg'); + + if (!heroPicture && bluePrint) { + const bpHeroImage = bluePrint.querySelector('div:first-of-type img'); + if (bpHeroImage) { + const heroSection = document.querySelector('main .hero'); + const $heroDiv = document.querySelector('main .hero > div'); + + if (heroSection && !$heroDiv) { + const p = createTag('p'); + const pic = createTag('picture', { class: 'hero-bg' }); + pic.appendChild(bpHeroImage); + p.append(pic); + heroSection.classList.remove('hero-noimage'); + $heroDiv.prepend(p); + } + } + } + } + + const templates = Array.from(innerWrapper.children); + + rows = templates.length; + let breakpoints = [{ width: '400' }]; + + if (rows > 6 && !block.classList.contains('horizontal')) { + innerWrapper.classList.add('masonry'); + } + + if (rows === 1) { + block.classList.add('large'); + breakpoints = [{ + media: '(min-width: 600px)', + width: '2000', + }, { width: '750' }]; + } + + block.querySelectorAll(':scope picture > img').forEach((img) => { + const { src, alt } = img; + img.parentNode.replaceWith(createOptimizedPicture(src, alt, true, breakpoints)); + }); + + // find the edit link and turn the template DIV into the A + // A + // +- DIV + // +- PICTURE + // +- DIV + // +- SPAN + // +- "Edit this template" + // + // make copy of children to avoid modifying list while looping + + populateTemplates(block, props, templates); + if (props.orientation.toLowerCase() !== 'horizontal') { + if (rows > 6 || block.classList.contains('sixcols') || block.classList.contains('fullwidth')) { + /* flex masonry */ + + if (innerWrapper) { + const cells = Array.from(innerWrapper.children); + innerWrapper.classList.remove('masonry'); + innerWrapper.classList.add('flex-masonry'); + props.masonry = new Masonry(innerWrapper, cells); + } else { + block.remove(); + } + + props.masonry.draw(); + window.addEventListener('resize', () => { + props.masonry.draw(); + }); + } else { + block.classList.add('template-x-complete'); + } + } + + await attachFreeInAppPills(block); + + const templateLinks = block.querySelectorAll('.template .button-container > a, a.template.placeholder'); + const linksPopulated = new CustomEvent('linkspopulated', { detail: templateLinks }); + document.dispatchEvent(linksPopulated); +} + +async function appendCategoryTemplatesCount(block, props) { + const categories = block.querySelectorAll('ul.category-list > li'); + // FIXME: props already contain start: 70 at this time + const tempProps = JSON.parse(JSON.stringify(props)); + tempProps.limit = 0; + const lang = getLanguage(getLocale(window.location)); + + for (const li of categories) { + const anchor = li.querySelector('a'); + if (anchor) { + const countSpan = createTag('span', { class: 'category-list-template-count' }); + // eslint-disable-next-line no-await-in-loop + const cnt = await fetchTemplatesCategoryCount(props, anchor.dataset.tasks); + countSpan.textContent = `(${cnt.toLocaleString(lang)})`; + anchor.append(countSpan); + } + } +} + +async function decorateBreadcrumbs(block) { + // breadcrumbs are desktop-only + if (document.body.dataset.device !== 'desktop') return; + const { default: getBreadcrumbs } = await import('./breadcrumbs.js'); + const breadcrumbs = await getBreadcrumbs(); + if (breadcrumbs) block.prepend(breadcrumbs); +} + +function importSearchBar(block, blockMediator) { + blockMediator.subscribe('stickySearchBar', (e) => { + const parent = block.querySelector('.api-templates-toolbar .wrapper-content-search'); + if (parent) { + const existingStickySearchBar = parent.querySelector('.search-bar-wrapper'); + if (e.newValue.loadSearchBar && !existingStickySearchBar) { + const searchWrapper = e.newValue.element; + parent.prepend(searchWrapper); + searchWrapper.classList.add('show'); + searchWrapper.classList.add('collapsed'); + + const searchDropdown = searchWrapper.querySelector('.search-dropdown-container'); + const searchForm = searchWrapper.querySelector('.search-form'); + const searchBar = searchWrapper.querySelector('input.search-bar'); + const clearBtn = searchWrapper.querySelector('.icon-search-clear'); + const trendsContainer = searchWrapper.querySelector('.trends-container'); + const suggestionsContainer = searchWrapper.querySelector('.suggestions-container'); + const suggestionsList = searchWrapper.querySelector('.suggestions-list'); + + searchBar.addEventListener('click', (event) => { + event.stopPropagation(); + searchWrapper.classList.remove('collapsed'); + setTimeout(() => { + searchDropdown.classList.remove('hidden'); + }, 500); + }, { passive: true }); + + searchBar.addEventListener('keyup', () => { + if (searchBar.value !== '') { + clearBtn.style.display = 'inline-block'; + trendsContainer.classList.add('hidden'); + suggestionsContainer.classList.remove('hidden'); + } else { + clearBtn.style.display = 'none'; + trendsContainer.classList.remove('hidden'); + suggestionsContainer.classList.add('hidden'); + } + }, { passive: true }); + + document.addEventListener('click', (event) => { + const { target } = event; + if (target !== searchWrapper && !searchWrapper.contains(target)) { + searchWrapper.classList.add('collapsed'); + searchDropdown.classList.add('hidden'); + searchBar.value = ''; + suggestionsList.innerHTML = ''; + trendsContainer.classList.remove('hidden'); + suggestionsContainer.classList.add('hidden'); + clearBtn.style.display = 'none'; + } + }, { passive: true }); + + const redirectSearch = async () => { + const placeholders = await fetchPlaceholders(); + const taskMap = placeholders['task-name-mapping'] ? JSON.parse(placeholders['task-name-mapping']) : {}; + + const format = getMetadata('placeholder-format'); + let currentTasks = ''; + let searchInput = searchBar.value.toLowerCase() || getMetadata('topics'); + + const tasksFoundInInput = Object.entries(taskMap) + .filter((task) => task[1].some((word) => { + const searchValue = searchBar.value.toLowerCase(); + return searchValue.indexOf(word.toLowerCase()) >= 0; + })).sort((a, b) => b[0].length - a[0].length); + + if (tasksFoundInInput.length > 0) { + tasksFoundInInput[0][1].sort((a, b) => b.length - a.length).forEach((word) => { + searchInput = searchInput.toLowerCase().replace(word.toLowerCase(), ''); + }); + + searchInput = searchInput.trim(); + [[currentTasks]] = tasksFoundInInput; + } + + const locale = getLocale(window.location); + const urlPrefix = locale === 'us' ? '' : `/${locale}`; + const topicUrl = searchInput ? `/${searchInput}` : ''; + const taskUrl = `/${handlelize(currentTasks.toLowerCase())}`; + const targetPath = `${urlPrefix}/express/templates${taskUrl}${topicUrl}`; + const allTemplatesMetadata = await fetchAllTemplatesMetadata(); + const pathMatch = (event) => event.url === targetPath; + if (allTemplatesMetadata.some(pathMatch)) { + window.location = `${window.location.origin}${targetPath}`; + } else { + const searchUrlTemplate = `/express/templates/search?tasks=${currentTasks}&phformat=${format}&topics=${searchInput || "''"}&q=${searchInput || "''"}`; + window.location = `${window.location.origin}${urlPrefix}${searchUrlTemplate}`; + } + }; + + searchForm.addEventListener('submit', async (event) => { + event.preventDefault(); + searchBar.disabled = true; + logSearch(event.currentTarget); + await redirectSearch(); + }); + + clearBtn.addEventListener('click', () => { + searchBar.value = ''; + suggestionsList.innerHTML = ''; + trendsContainer.classList.remove('hidden'); + suggestionsContainer.classList.add('hidden'); + clearBtn.style.display = 'none'; + }, { passive: true }); + + const suggestionsListUIUpdateCB = (suggestions) => { + suggestionsList.innerHTML = ''; + const searchBarVal = searchBar.value.toLowerCase(); + if (suggestions && !(suggestions.length <= 1 && suggestions[0]?.query === searchBarVal)) { + suggestions.forEach((item) => { + const li = createTag('li', { tabindex: 0 }); + const valRegEx = new RegExp(searchBar.value, 'i'); + li.innerHTML = item.query.replace(valRegEx, `${searchBarVal}`); + li.addEventListener('click', () => { + if (item.query === searchBar.value) return; + searchBar.value = item.query; + searchBar.dispatchEvent(new Event('input')); + }); + + suggestionsList.append(li); + }); + } + }; + + import('../../scripts/autocomplete-api-v3.js').then(({ default: useInputAutocomplete }) => { + const { inputHandler } = useInputAutocomplete( + suggestionsListUIUpdateCB, { throttleDelay: 300, debounceDelay: 500, limit: 7 }, + ); + searchBar.addEventListener('input', inputHandler); + }); + } + + if (e.newValue.loadSearchBar && existingStickySearchBar) { + existingStickySearchBar.classList.add('show'); + } + + if (!e.newValue.loadSearchBar && existingStickySearchBar) { + existingStickySearchBar.classList.remove('show'); + } + } + }); +} + +function wordExistsInString(word, inputString) { + const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regexPattern = new RegExp(`(?:^|\\s|[.,!?()'"\\-])${escapedWord}(?:$|\\s|[.,!?()'"\\-])`, 'i'); + return regexPattern.test(inputString); +} + +async function getTaskNameInMapping(text) { + const placeholders = await fetchPlaceholders(); + const taskMap = placeholders['x-task-name-mapping'] ? JSON.parse(placeholders['x-task-name-mapping']) : {}; + return Object.entries(taskMap) + .filter((task) => task[1].some((word) => { + const searchValue = text.toLowerCase(); + return wordExistsInString(word.toLowerCase(), searchValue); + })) + .sort((a, b) => b[0].length - a[0].length); +} + +function renderFallbackMsgWrapper(block, { fallbackMsg }) { + let fallbackMsgWrapper = block.querySelector('.template-x-fallback-msg-wrapper'); + if (!fallbackMsgWrapper) { + fallbackMsgWrapper = createTag('div', { class: 'template-x-fallback-msg-wrapper' }); + block.append(fallbackMsgWrapper); + } + if (!fallbackMsg) { + fallbackMsgWrapper.textContent = ''; + } else { + fallbackMsgWrapper.textContent = fallbackMsg; + } +} + +async function buildTemplateList(block, props, type = []) { + if (type?.length > 0) { + type.forEach((typeName) => { + block.parentElement.classList.add(typeName); + block.classList.add(typeName); + }); + } + + if (!props.templateStats) { + await processContentRow(block, props); + } + + const { templates, fallbackMsg } = await fetchAndRenderTemplates(props); + + if (templates?.length > 0) { + props.fallbackMsg = fallbackMsg; + renderFallbackMsgWrapper(block, props); + const blockInnerWrapper = createTag('div', { class: 'template-x-inner-wrapper' }); + block.append(blockInnerWrapper); + props.templates = props.templates.concat(templates); + props.templates.forEach((template) => { + blockInnerWrapper.append(template); + }); + + await decorateTemplates(block, props); + } else { + // fixme: better error message. + block.innerHTML = 'Oops. Our templates delivery got stolen. Please try refresh the page.'; + } + + if (templates && props.tabs) { + block.classList.add('tabbed'); + const tabs = props.tabs.split(','); + const templatesWrapper = block.querySelector('.template-x-inner-wrapper'); + const textWrapper = block.querySelector('.template-title .text-wrapper > div'); + const tabsWrapper = createTag('div', { class: 'template-tabs' }); + + const promises = []; + + for (const tab of tabs) { + promises.push(getTaskNameInMapping(tab)); + } + + const tasksFoundInInput = await Promise.all(promises); + if (tasksFoundInInput.length === tabs.length) { + tasksFoundInInput.forEach((taskObj, index) => { + if (taskObj.length === 0) return; + const tabBtn = createTag('button', { class: 'template-tab-button' }); + tabBtn.textContent = tabs[index]; + tabsWrapper.append(tabBtn); + + const [[task]] = taskObj; + + if (props.filters.tasks === task) { + tabBtn.classList.add('active'); + } + + tabBtn.addEventListener('click', async () => { + templatesWrapper.style.opacity = 0; + + if (tasksFoundInInput) { + const { + templates: newTemplates, + fallbackMsg: newFallbackMsg, + } = await fetchAndRenderTemplates({ + ...props, + start: '', + filters: { + ...props.filters, + tasks: task, + }, + }); + if (newTemplates?.length > 0) { + props.fallbackMsg = newFallbackMsg; + renderFallbackMsgWrapper(block, props); + + templatesWrapper.innerHTML = ''; + props.templates = newTemplates; + props.templates.forEach((template) => { + templatesWrapper.append(template); + }); + + await decorateTemplates(block, props); + buildCarousel(':scope > .template', templatesWrapper, false); + templatesWrapper.style.opacity = 1; + } + + tabsWrapper.querySelectorAll('.template-tab-button').forEach((btn) => { + if (btn !== tabBtn) btn.classList.remove('active'); + }); + tabBtn.classList.add('active'); + } + }, { passive: true }); + }); + } + + textWrapper.append(tabsWrapper); + } + + // templates are either finished rendering or API has crashed at this point. + + if (templates && props.loadMoreTemplates) { + const loadMore = await decorateLoadMoreButton(block, props); + if (loadMore) { + updateLoadMoreButton(block, props, loadMore); + } + } + + if (templates && props.toolBar) { + await decorateToolbar(block, props); + await decorateCategoryList(block, props); + appendCategoryTemplatesCount(block, props); + } + + if (props.toolBar && props.searchBar) { + import('../../scripts/block-mediator.js').then(({ default: blockMediator }) => { + importSearchBar(block, blockMediator); + }); + } + + await decorateBreadcrumbs(block); + + if (templates && props.orientation && props.orientation.toLowerCase() === 'horizontal') { + const innerWrapper = block.querySelector('.template-x-inner-wrapper'); + if (innerWrapper) { + buildCarousel(':scope > .template', innerWrapper, false); + } else { + block.remove(); + } + } + + if (props.holidayBlock) { + decorateHoliday(block, props); + } +} + +function determineTemplateXType(props) { + // todo: build layers of aspects based on props conditions - i.e. orientation -> style -> use case + const type = []; + + // orientation aspect + if (props.orientation && props.orientation.toLowerCase() === 'horizontal') type.push('horizontal'); + + // style aspect + if (props.width && props.width.toLowerCase() === 'full') type.push('fullwidth'); + if (props.width && props.width.toLowerCase() === 'sixcols') type.push('sixcols'); + if (props.width && props.width.toLowerCase() === 'fourcols') type.push('fourcols'); + if (props.mini) type.push('mini'); + + // use case aspect + if (props.holidayBlock) type.push('holiday'); + + return type; +} + +export default async function decorate(block) { + const props = constructProps(block); + block.innerHTML = ''; + await buildTemplateList(block, props, determineTemplateXType(props)); +} diff --git a/express/blocks/toggle-bar/toggle-bar.css b/express/blocks/toggle-bar/toggle-bar.css new file mode 100644 index 000000000..ff79dee06 --- /dev/null +++ b/express/blocks/toggle-bar/toggle-bar.css @@ -0,0 +1,174 @@ +.section .toggle-bar-wrapper { + margin: 0 auto; + padding: 0 16px; + max-width: max-content; +} + +.block.toggle-bar { + display: none; +} + +.block.toggle-bar.float { + margin-top: -72px; +} + +.block.toggle-bar.sticky { + min-height: 147px; + position: relative; + z-index: 9; +} + +.block.toggle-bar div[data-align="center"] { + margin: auto; +} + +.block.toggle-bar div[data-align="center"] * { + text-align: center; +} + +.block.toggle-bar.dark > div:first-of-type * { + color: var(--color-white); +} + +.block.toggle-bar > div:nth-of-type(2) { + position: relative; + display: flex; + background: var(--color-white); + border-radius: 100px; + box-shadow: 0 0 10px #00000029; + box-sizing: border-box; + padding: 6px; + margin-top: 32px; + margin-bottom: 16px; + height: 70px; + overflow: auto clip; + transition: top 0.2s; +} + +.block.toggle-bar.sticky.sticking > div:nth-of-type(2) { + position: fixed; + top: 0; + left: 50%; + transform: translate(-50%, 0); +} + +.block.toggle-bar.sticky.sticking.bumped-by-gnav > div:nth-of-type(2) { + top: 66px; +} + +.block.toggle-bar.sticky.sticking.hidden > div:nth-of-type(2) { + top: -120px; +} + +.block.toggle-bar > div:nth-of-type(2)::-webkit-scrollbar { + display: none; +} + +.block.toggle-bar > div:nth-of-type(2) { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.block.toggle-bar > div { + display: flex; + align-items: center; + height: 100%; +} + +.block.toggle-bar > div > div { + border-left: 2px solid #e8e8e8; +} + +.block.toggle-bar > div > div:nth-of-type(1) { + border-left: unset; +} + +.block.toggle-bar ul { + padding: 0; + margin: 0; + height: 100%; + display: flex; + justify-content: center; +} + +.toggle-bar .toggle-bar-button { + position: relative; + display: inline-block; + list-style-type: none; + background-color: transparent; + font-size: var(--body-font-size-s); + margin: 0 28px; + cursor: pointer; + user-select: none; + border: none; + font-family: var(--body-font-family); + color: black; +} + +.toggle-bar .toggle-bar-button:after { + content: ''; + position: absolute; + left: 50%; + bottom: -13px; + transform: translate(-50%); + height: 4px; + width: 100%; + max-height: 0; + opacity: 0; + background-color: var(--color-info-accent); + border-radius: 100px 100px 0 0; + transition: max-height .2s, opacity .2s; +} + +.toggle-bar .toggle-bar-button.active:after, +.toggle-bar .toggle-bar-button:hover:after { + max-height: 4px; + opacity: 1; +} + +.toggle-bar .toggle-bar-button .text-wrapper { + display: flex; + align-items: center; + gap: 8px; + white-space: nowrap; + transition: color .3s, font-weight .3s; +} + +.toggle-bar .toggle-bar-button.active .text-wrapper { + font-weight: 700; + color: var(--color-info-accent); +} + +.toggle-bar .toggle-bar-button .icons-wrapper .icon { + height: 18px; + width: 18px; +} + +.toggle-bar .toggle-bar-button.active .icons-wrapper .icon:first-of-type, +.toggle-bar .toggle-bar-button .icons-wrapper .icon:nth-of-type(2) { + display: none; +} + +.toggle-bar .toggle-bar-button .icons-wrapper .icon:first-of-type, +.toggle-bar .toggle-bar-button.active .icons-wrapper .icon:nth-of-type(2) { + display: unset; +} + +.toggle-bar .tag { + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + font-size: 12px; + padding: 2px 4px; + font-weight: 700; + background-color: #DEDEF9; + color: var(--color-info-accent); + border-radius: 4px; +} + +@media (min-width: 900px) { + .block.toggle-bar { + display: block; + } +} diff --git a/express/blocks/toggle-bar/toggle-bar.js b/express/blocks/toggle-bar/toggle-bar.js new file mode 100644 index 000000000..481e73e75 --- /dev/null +++ b/express/blocks/toggle-bar/toggle-bar.js @@ -0,0 +1,183 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { createTag } from '../../scripts/scripts.js'; + +function decorateButton(block, toggle) { + const button = createTag('button', { class: 'toggle-bar-button' }); + const iconsWrapper = createTag('div', { class: 'icons-wrapper' }); + const textWrapper = createTag('div', { class: 'text-wrapper' }); + const icons = toggle.querySelectorAll('img'); + + const tagText = toggle.textContent.trim().match(/\[(.*?)\]/); + + if (tagText) { + const [fullText, tagTextContent] = tagText; + const tag = createTag('span', { class: 'tag' }); + textWrapper.textContent = toggle.textContent.trim().replace(fullText, '').trim(); + button.dataset.text = textWrapper.textContent.toLowerCase(); + tag.textContent = tagTextContent; + textWrapper.append(tag); + } else { + textWrapper.textContent = toggle.textContent.trim(); + button.dataset.text = textWrapper.textContent.toLowerCase(); + } + + if (icons.length > 0) { + icons.forEach((icon) => { + iconsWrapper.append(icon); + }); + } + + button.append(iconsWrapper, textWrapper); + toggle.parentNode.replaceChild(button, toggle); +} + +function awakeNestedCarousels(section) { + const carousels = section.querySelectorAll('.carousel-container'); + + if (carousels.length === 0) return; + + Array.from(carousels).forEach((c) => { + c.closest('.block')?.dispatchEvent(new CustomEvent('carouselloaded')); + }); +} + +function initButton(block, sections, index, props) { + const enclosingMain = block.closest('main'); + + if (enclosingMain) { + const buttons = block.querySelectorAll('.toggle-bar-button'); + + buttons[index].addEventListener('click', () => { + const activeButton = block.querySelector('button.active'); + props.activeTab = buttons[index].dataset.text; + + localStorage.setItem('createIntent', buttons[index].dataset.text); + if (activeButton !== buttons[index]) { + activeButton.classList.remove('active'); + buttons[index].classList.add('active'); + + sections.forEach((section) => { + if (buttons[index].dataset.text === section.dataset.toggle.toLowerCase()) { + section.style.display = 'block'; + props.activeSection = section; + awakeNestedCarousels(section); + } else { + section.style.display = 'none'; + } + }); + } + + if (!block.classList.contains('sticky')) { + window.scrollTo({ + top: Math.round(window.scrollY + block.getBoundingClientRect().top) - 24, + behavior: 'smooth', + }); + } + }); + + if (index === 0) { + buttons[index].classList.add('active'); + props.activeTab = buttons[index].dataset.text; + [props.activeSection] = sections; + } + } +} + +function syncWithStoredIntent(block) { + const buttons = block.querySelectorAll('button'); + const createIntent = localStorage.getItem('createIntent'); + + if (createIntent) { + const targetBtn = Array.from(buttons).find((btn) => btn.dataset.text === createIntent); + if (targetBtn) targetBtn.click(); + } +} + +function initGNavObserver(block) { + const gNav = document.querySelector('header.feds-header-wrapper'); + if (gNav) { + const config = { attributes: true, childList: false, subtree: false }; + + const callback = (mutationList) => { + for (const mutation of mutationList) { + if (mutation.type === 'attributes') { + if (gNav.classList.contains('feds-header-wrapper--scrolled') + && !gNav.classList.contains('feds-header-wrapper--retracted') + && block.classList.contains('sticking') + && !block.classList.contains('hidden')) { + block.classList.add('bumped-by-gnav'); + } else { + block.classList.remove('bumped-by-gnav'); + } + } + } + }; + + const observer = new MutationObserver(callback); + + // Start observing the target node for configured mutations + observer.observe(gNav, config); + } +} + +function initStickyBehavior(block, props) { + const toggleBar = block.querySelector('div:nth-of-type(2)'); + if (toggleBar) { + document.addEventListener('scroll', () => { + const blockRect = block.getBoundingClientRect(); + const sectionRect = props.activeSection.getBoundingClientRect(); + + if (sectionRect.bottom < 0) { + block.classList.add('hidden'); + } else if (blockRect.top < -45) { + block.classList.remove('hidden'); + block.classList.add('sticking'); + } else if (blockRect.top >= -45) { + block.classList.remove('sticking'); + block.classList.remove('hidden'); + } + }, { passive: true }); + } + + window.addEventListener('feds.events.experience.loaded', () => { + initGNavObserver(block); + }); +} + +export default function decorate(block) { + const props = { activeTab: '', activeSection: null }; + const enclosingMain = block.closest('main'); + if (enclosingMain) { + const sections = enclosingMain.querySelectorAll('[data-toggle]'); + const toggles = block.querySelectorAll('li'); + + toggles.forEach((toggle, index) => { + decorateButton(block, toggle); + initButton(block, sections, index, props); + }); + + if (sections) { + sections.forEach((section, index) => { + if (index > 0) { + section.style.display = 'none'; + } + }); + } + + syncWithStoredIntent(block); + + if (block.classList.contains('sticky')) { + initStickyBehavior(block, props); + } + } +} diff --git a/express/icons/adobe-firefly.svg b/express/icons/adobe-firefly.svg new file mode 100644 index 000000000..e62f619cc --- /dev/null +++ b/express/icons/adobe-firefly.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/express/icons/ai.svg b/express/icons/ai.svg new file mode 100644 index 000000000..9b7bc21a4 --- /dev/null +++ b/express/icons/ai.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/express/icons/ccx-sheet_44.svg b/express/icons/ccx-sheet_44.svg index 2f0790f9c..c7c8d42e0 100644 --- a/express/icons/ccx-sheet_44.svg +++ b/express/icons/ccx-sheet_44.svg @@ -298,6 +298,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -996,5 +1184,19 @@ + + + + + + + + + + + + + + diff --git a/express/icons/checkmark-green.svg b/express/icons/checkmark-green.svg new file mode 100644 index 000000000..96dccc012 --- /dev/null +++ b/express/icons/checkmark-green.svg @@ -0,0 +1,3 @@ + + + diff --git a/express/icons/color-sprite.svg b/express/icons/color-sprite.svg new file mode 100644 index 000000000..1acbb056e --- /dev/null +++ b/express/icons/color-sprite.svg @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/express/icons/content-calendar.svg b/express/icons/content-calendar.svg new file mode 100644 index 000000000..7c726be2c --- /dev/null +++ b/express/icons/content-calendar.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/express/icons/document-accent.svg b/express/icons/document-accent.svg new file mode 100644 index 000000000..d69559f7c --- /dev/null +++ b/express/icons/document-accent.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/express/icons/document.svg b/express/icons/document.svg new file mode 100644 index 000000000..c9316e8ec --- /dev/null +++ b/express/icons/document.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/express/icons/export-pdf-2.svg b/express/icons/export-pdf-2.svg new file mode 100644 index 000000000..fd6ebf483 --- /dev/null +++ b/express/icons/export-pdf-2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/express/icons/free-icon.svg b/express/icons/free-icon.svg new file mode 100644 index 000000000..dbed0e9a7 --- /dev/null +++ b/express/icons/free-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/express/icons/icon-quickaction-black.svg b/express/icons/icon-quickaction-black.svg new file mode 100644 index 000000000..0b03bf8b1 --- /dev/null +++ b/express/icons/icon-quickaction-black.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/express/icons/image-accent.svg b/express/icons/image-accent.svg new file mode 100644 index 000000000..64c3bb239 --- /dev/null +++ b/express/icons/image-accent.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/express/icons/image-wire.svg b/express/icons/image-wire.svg new file mode 100644 index 000000000..2e4cbc153 --- /dev/null +++ b/express/icons/image-wire.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/express/icons/infographic.svg b/express/icons/infographic.svg new file mode 100644 index 000000000..50adbf64a --- /dev/null +++ b/express/icons/infographic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/express/icons/multipage-static-badge.svg b/express/icons/multipage-static-badge.svg new file mode 100755 index 000000000..df7e3f680 --- /dev/null +++ b/express/icons/multipage-static-badge.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/express/icons/multipage-video-badge.svg b/express/icons/multipage-video-badge.svg new file mode 100755 index 000000000..987744121 --- /dev/null +++ b/express/icons/multipage-video-badge.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/express/icons/news-letter.svg b/express/icons/news-letter.svg new file mode 100644 index 000000000..23f111ff2 --- /dev/null +++ b/express/icons/news-letter.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/express/icons/organizer.svg b/express/icons/organizer.svg new file mode 100644 index 000000000..08544eaa2 --- /dev/null +++ b/express/icons/organizer.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/express/icons/pause-video-light.svg b/express/icons/pause-video-light.svg new file mode 100644 index 000000000..4e3d38059 --- /dev/null +++ b/express/icons/pause-video-light.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/express/icons/pause-video.svg b/express/icons/pause-video.svg new file mode 100644 index 000000000..5a5ae9f84 --- /dev/null +++ b/express/icons/pause-video.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/express/icons/play-button.svg b/express/icons/play-button.svg new file mode 100755 index 000000000..5fa4d9741 --- /dev/null +++ b/express/icons/play-button.svg @@ -0,0 +1,7 @@ + + + + diff --git a/express/icons/play-video-light.svg b/express/icons/play-video-light.svg new file mode 100644 index 000000000..de45fd227 --- /dev/null +++ b/express/icons/play-video-light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/express/icons/play-video.svg b/express/icons/play-video.svg index 2e9cae6dc..8129f9d38 100644 --- a/express/icons/play-video.svg +++ b/express/icons/play-video.svg @@ -1,4 +1,6 @@ - - - + + + + + diff --git a/express/icons/poster-2.svg b/express/icons/poster-2.svg new file mode 100644 index 000000000..4754f60bc --- /dev/null +++ b/express/icons/poster-2.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/express/icons/premium-icon.svg b/express/icons/premium-icon.svg new file mode 100644 index 000000000..624d6fc98 --- /dev/null +++ b/express/icons/premium-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/express/icons/presentation.svg b/express/icons/presentation.svg new file mode 100644 index 000000000..094f1ecb5 --- /dev/null +++ b/express/icons/presentation.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/express/icons/promote-accent.svg b/express/icons/promote-accent.svg new file mode 100644 index 000000000..f735992a0 --- /dev/null +++ b/express/icons/promote-accent.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/express/icons/promote.svg b/express/icons/promote.svg new file mode 100644 index 000000000..c98bc3ebe --- /dev/null +++ b/express/icons/promote.svg @@ -0,0 +1,3 @@ + + + diff --git a/express/icons/quick-action-accent.svg b/express/icons/quick-action-accent.svg new file mode 100644 index 000000000..411546580 --- /dev/null +++ b/express/icons/quick-action-accent.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/express/icons/report.svg b/express/icons/report.svg new file mode 100644 index 000000000..9e486a651 --- /dev/null +++ b/express/icons/report.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/express/icons/share-arrow.svg b/express/icons/share-arrow.svg new file mode 100755 index 000000000..e00305314 --- /dev/null +++ b/express/icons/share-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/express/icons/spectrum-back.svg b/express/icons/spectrum-back.svg new file mode 100644 index 000000000..4b9237d9f --- /dev/null +++ b/express/icons/spectrum-back.svg @@ -0,0 +1,11 @@ + + + + + S Back 18 N + + diff --git a/express/icons/spectrum-close.svg b/express/icons/spectrum-close.svg new file mode 100644 index 000000000..f08f15305 --- /dev/null +++ b/express/icons/spectrum-close.svg @@ -0,0 +1,11 @@ + + + + + S Close 18 N + + diff --git a/express/icons/spectrum-copy.svg b/express/icons/spectrum-copy.svg new file mode 100644 index 000000000..ce8da52c3 --- /dev/null +++ b/express/icons/spectrum-copy.svg @@ -0,0 +1,31 @@ + + + + + S Copy 18 N + + + + + + + + + + + + + + + + + + + + + + diff --git a/express/icons/spectrum-preview.svg b/express/icons/spectrum-preview.svg new file mode 100644 index 000000000..4a6608d1e --- /dev/null +++ b/express/icons/spectrum-preview.svg @@ -0,0 +1,12 @@ + + + + + S Preview 18 N + + + diff --git a/express/icons/spectrum-search.svg b/express/icons/spectrum-search.svg new file mode 100644 index 000000000..7dda8a2ce --- /dev/null +++ b/express/icons/spectrum-search.svg @@ -0,0 +1,11 @@ + + + + + S Search 18 N + + diff --git a/express/icons/star-accent.svg b/express/icons/star-accent.svg new file mode 100644 index 000000000..7aac3b368 --- /dev/null +++ b/express/icons/star-accent.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/express/icons/star-wire.svg b/express/icons/star-wire.svg new file mode 100644 index 000000000..333c12819 --- /dev/null +++ b/express/icons/star-wire.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/express/icons/template-free-accent.svg b/express/icons/template-free-accent.svg old mode 100644 new mode 100755 diff --git a/express/icons/template-search.svg b/express/icons/template-search.svg index aab15ce2f..c1bd91315 100644 --- a/express/icons/template-search.svg +++ b/express/icons/template-search.svg @@ -10,4 +10,4 @@ - \ No newline at end of file + diff --git a/express/icons/thumbup-accent.svg b/express/icons/thumbup-accent.svg new file mode 100644 index 000000000..c3af25781 --- /dev/null +++ b/express/icons/thumbup-accent.svg @@ -0,0 +1,3 @@ + + + diff --git a/express/icons/thumbup.svg b/express/icons/thumbup.svg new file mode 100644 index 000000000..e85bb2fcb --- /dev/null +++ b/express/icons/thumbup.svg @@ -0,0 +1,3 @@ + + + diff --git a/express/icons/video-accent.svg b/express/icons/video-accent.svg new file mode 100644 index 000000000..d991c4888 --- /dev/null +++ b/express/icons/video-accent.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/express/icons/video-badge.svg b/express/icons/video-badge.svg new file mode 100755 index 000000000..93f563ba1 --- /dev/null +++ b/express/icons/video-badge.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/express/icons/video-creation.svg b/express/icons/video-creation.svg new file mode 100644 index 000000000..1a67ea38a --- /dev/null +++ b/express/icons/video-creation.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/express/icons/video.svg b/express/icons/video.svg new file mode 100644 index 000000000..e9b86dd69 --- /dev/null +++ b/express/icons/video.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/express/icons/worksheet.svg b/express/icons/worksheet.svg new file mode 100644 index 000000000..c392df3ba --- /dev/null +++ b/express/icons/worksheet.svg @@ -0,0 +1,4 @@ + + + + diff --git a/express/scripts/all-templates-metadata.js b/express/scripts/all-templates-metadata.js index bb3552d66..542ec0dc1 100644 --- a/express/scripts/all-templates-metadata.js +++ b/express/scripts/all-templates-metadata.js @@ -36,35 +36,8 @@ export default async function fetchAllTemplatesMetadata() { sheet = `${urlPrefix}/express/templates/default/metadata.json?limit=100000`; } - let resp = await memoizedFetchUrl(sheet); + const resp = await memoizedFetchUrl(sheet); allTemplatesMetadata = resp?.data; - - // TODO: remove the > 1 logic after publishing of the split metadata sheet - if (!(allTemplatesMetadata && allTemplatesMetadata.length > 1)) { - resp = await memoizedFetchUrl('/express/templates/content.json?sheet=seo-templates&limit=100000'); - allTemplatesMetadata = resp?.data?.map((p) => ({ - ...p, - // TODO: backward compatibility. Remove when we move away from helix-seo-templates - url: p.path, - title: p.metadataTitle, - description: p.metadataDescription, - 'short-title': p.shortTitle, - ckgid: p.ckgID, - 'hero-title': p.heroAnimationTitle, - 'hero-text': p.heroAnimationText, - locales: p.templateLocale, - premium: p.templatePremium, - animated: p.templateAnimated, - tasks: p.templateTasks, - topics: p.templateTopics, - 'placeholder-format': p.placeholderFormat, - 'create-link': p.createLink, - 'create-text': p.createText, - 'top-templates-title': p.topTemplatesTitle, - 'top-templates': p.topTemplates, - 'top-templates-text': p.topTemplatesText, - })) || []; - } } catch (err) { allTemplatesMetadata = []; } diff --git a/express/scripts/api-v3-controller.js b/express/scripts/api-v3-controller.js index 6eca0a3e0..ebf69d166 100644 --- a/express/scripts/api-v3-controller.js +++ b/express/scripts/api-v3-controller.js @@ -16,19 +16,19 @@ const endpoints = { dev: { cdn: 'https://uss-templates-dev.adobe.io/uss/v3/query', url: 'https://uss-templates-dev.adobe.io/uss/v3/query', - token: 'cd1823ed-0104-492f-ba91-25f4195d5f6c', + token: atob('Y2QxODIzZWQtMDEwNC00OTJmLWJhOTEtMjVmNDE5NWQ1ZjZj'), }, stage: { cdn: 'https://www.stage.adobe.com/ax-uss-api/', url: 'https://uss-templates-stage.adobe.io/uss/v3/query', - token: 'db7a3d14-5aaa-4a3d-99c3-52a0f0dbb459', - key: 'express-ckg-stage', + token: atob('ZGI3YTNkMTQtNWFhYS00YTNkLTk5YzMtNTJhMGYwZGJiNDU5'), + key: atob('ZXhwcmVzcy1ja2ctc3RhZ2U='), }, prod: { cdn: 'https://www.adobe.com/ax-uss-api/', url: 'https://uss-templates.adobe.io/uss/v3/query', - token: '2e0199f4-c4e2-4025-8229-df4ca5397605', - key: 'template-list-linklist-facet', + token: atob('MmUwMTk5ZjQtYzRlMi00MDI1LTgyMjktZGY0Y2E1Mzk3NjA1'), + key: atob('dGVtcGxhdGUtbGlzdC1saW5rbGlzdC1mYWNldA=='), }, }; diff --git a/express/blocks/search-marquee/use-input-autocomplete.js b/express/scripts/autocomplete-api-v3.js old mode 100644 new mode 100755 similarity index 57% rename from express/blocks/search-marquee/use-input-autocomplete.js rename to express/scripts/autocomplete-api-v3.js index d49525a05..4ba578d93 --- a/express/blocks/search-marquee/use-input-autocomplete.js +++ b/express/scripts/autocomplete-api-v3.js @@ -10,9 +10,50 @@ * governing permissions and limitations under the License. */ -import { getLocale, getLanguage } from '../../scripts/scripts.js'; -import { memoize, throttle, debounce } from '../../scripts/utils.js'; -import fetchAPI from './autocomplete-api-v3.js'; +import { getLocale, getLanguage } from './scripts.js'; +import { memoize, throttle, debounce } from './utils.js'; + +const url = 'https://adobesearch-atc.adobe.io/uss/v3/autocomplete'; +const experienceId = 'default-templates-autocomplete-v1'; +const scopeEntities = ['HzTemplate']; +const wlLocales = ['en-US', 'fr-FR', 'de-DE', 'ja-JP']; +const emptyRes = { queryResults: [{ items: [] }] }; + +async function fetchAPI({ limit = 5, textQuery, locale = 'en-US' }) { + if (!textQuery || !wlLocales.includes(locale)) { + return []; + } + + const res = await fetch(url, { + method: 'POST', + headers: { + 'x-api-key': atob('cHJvamVjdHhfbWFya2V0aW5nX3dlYg=='), + 'content-type': 'application/json', + }, + body: JSON.stringify({ + experienceId, + textQuery, + locale, + queries: [ + { + limit, + id: experienceId, + scope: { + entities: scopeEntities, + }, + }, + ], + }), + }) + .then((response) => response.json()) + .then((response) => (response.queryResults?.[0]?.items ? response : emptyRes)) + .catch((err) => { + // eslint-disable-next-line no-console + console.error('Autocomplete API Error: ', err); + return emptyRes; + }); + return res.queryResults[0].items; +} const memoizedFetchAPI = memoize(fetchAPI, { key: (options) => options.textQuery, diff --git a/express/scripts/ckg-link-list.js b/express/scripts/ckg-link-list.js index fd01c0027..38b66032e 100644 --- a/express/scripts/ckg-link-list.js +++ b/express/scripts/ckg-link-list.js @@ -33,10 +33,7 @@ async function fetchLinkList() { window.linkLists.ckgData = response.queryResults[0].facets[0].buckets.map((ckgItem) => { let formattedTasks; if (getMetadata('template-search-page') === 'Y') { - const params = new Proxy(new URLSearchParams(window.location.search), { - get: (searchParams, prop) => searchParams.get(prop), - }); - formattedTasks = titleCase(params.tasks).replace(/[$@%"]/g, ''); + return {}; } else { formattedTasks = titleCase(getMetadata('tasks')).replace(/[$@%"]/g, ''); } @@ -69,18 +66,22 @@ function matchCKGResult(ckgData, pageData) { return sameLocale && ckgMatch && taskMatch; } +const defaultRegex = /\/express\/templates\/default/; function replaceLinkPill(linkPill, data) { const clone = linkPill.cloneNode(true); if (data) { clone.innerHTML = clone.innerHTML.replace('/express/templates/default', data.url); clone.innerHTML = clone.innerHTML.replaceAll('Default', data.altShortTitle || data['short-title']); } + if (defaultRegex.test(clone.innerHTML)) { + return null; + } return clone; } async function updateSEOLinkList(container, linkPill, list) { - const templatePages = await fetchAllTemplatesMetadata(); container.innerHTML = ''; + const templatePages = await fetchAllTemplatesMetadata(); if (list && templatePages) { list.forEach((d) => { @@ -88,7 +89,7 @@ async function updateSEOLinkList(container, linkPill, list) { const templatePageData = templatePages.find((p) => { const targetLocale = /^[a-z]{2}$/.test(p.url.split('/')[1]) ? p.url.split('/')[1] : 'us'; const isLive = p.live === 'Y'; - const titleMatch = p['short-title'].toLowerCase() === d.childSibling.toLowerCase(); + const titleMatch = p['short-title']?.toLowerCase() === d.childSibling?.toLowerCase(); const localeMatch = currentLocale === targetLocale; return isLive && titleMatch && localeMatch; @@ -96,7 +97,7 @@ async function updateSEOLinkList(container, linkPill, list) { if (templatePageData) { const clone = replaceLinkPill(linkPill, templatePageData); - container.append(clone); + if (clone) container.append(clone); } }); } @@ -136,17 +137,12 @@ async function updateLinkList(container, linkPill, list) { if (list && templatePages) { list.forEach((d) => { - const topics = getMetadata('topics') !== '" "' ? `${getMetadata('topics').replace(/[$@%"]/g, '')}` : ''; const templatePageData = templatePages.find((p) => p.live === 'Y' && matchCKGResult(d, p)); - const topicsQuery = `${topics ?? topics} ${d.displayValue}`.split(' ') - .filter((item, i, allItems) => i === allItems.indexOf(item)) - .join(' ').trim(); let displayText = formatLinkPillText(d); const locale = getLocale(window.location); const urlPrefix = locale === 'us' ? '' : `/${locale}`; const localeColumnString = locale === 'us' ? 'EN' : locale.toUpperCase(); - let hideUntranslatedPill = false; if (pillsMapping) { const alternateText = pillsMapping.find((row) => window.location.pathname === `${urlPrefix}${row['Express SEO URL']}` && d.ckgID === row['CKG Pill ID']); @@ -157,21 +153,11 @@ async function updateLinkList(container, linkPill, list) { templatePageData.altShortTitle = displayText; } } - - hideUntranslatedPill = !hasAlternateTextForLocale && locale !== 'us'; } if (templatePageData) { const clone = replaceLinkPill(linkPill, templatePageData); - pageLinks.push(clone); - } else if (d.ckgID && !hideUntranslatedPill) { - const currentTasks = getMetadata('tasks') ? getMetadata('tasks').replace(/[$@%"]/g, '') : ' '; - const searchParams = `tasks=${currentTasks}&phformat=${getMetadata('placeholder-format')}&topics=${topicsQuery}&ckgid=${d.ckgID}`; - const clone = linkPill.cloneNode(true); - - clone.innerHTML = clone.innerHTML.replace('/express/templates/default', `${urlPrefix}/express/templates/search?${searchParams}`); - clone.innerHTML = clone.innerHTML.replaceAll('Default', displayText); - searchLinks.push(clone); + if (clone) pageLinks.push(clone); } }); @@ -195,7 +181,7 @@ async function updateLinkList(container, linkPill, list) { linkListData.forEach((d) => { const templatePageData = templatePages.find((p) => p.live === 'Y' && p.shortTitle === d.childSibling); const clone = replaceLinkPill(linkPill, templatePageData); - container.append(clone); + if (clone) container.append(clone); }); } } @@ -241,7 +227,8 @@ async function lazyLoadSEOLinkList() { const topTemplatesData = topTemplates.split(', ').map((cs) => ({ childSibling: cs })); await updateSEOLinkList(topTemplatesContainer, topTemplatesTemplate, topTemplatesData); - topTemplatesContainer.style.visibility = 'visible'; + const hiddenDiv = seoNav.querySelector('div[style="visibility: hidden;"]'); + if (hiddenDiv) hiddenDiv.style.visibility = 'visible'; } else { topTemplatesContainer.innerHTML = ''; } @@ -272,6 +259,7 @@ async function lazyLoadSearchMarqueeLinklist() { } await updateLinkList(linkListContainer, linkListTemplate, linkListData); + searchMarquee.dispatchEvent(new CustomEvent('carouselloaded')); linkListContainer.parentElement.classList.add('appear'); } } @@ -291,7 +279,7 @@ function hideAsyncBlocks() { } } -(async function updateAsyncBlocks() { +export default async function updateAsyncBlocks() { hideAsyncBlocks(); // TODO: integrate memoization const showSearchMarqueeLinkList = getMetadata('show-search-marquee-link-list'); @@ -300,4 +288,4 @@ function hideAsyncBlocks() { } await lazyLoadLinklist(); await lazyLoadSEOLinkList(); -}()); +} diff --git a/express/scripts/content-replace.js b/express/scripts/content-replace.js index 8085fedd6..424998588 100644 --- a/express/scripts/content-replace.js +++ b/express/scripts/content-replace.js @@ -14,28 +14,28 @@ import { fetchPlaceholders, getMetadata, titleCase, - createTag, getHelixEnv, } from './scripts.js'; -import fetchAllTemplatesMetadata from './all-templates-metadata.js'; -async function replaceDefaultPlaceholders(template) { - template.innerHTML = template.innerHTML.replaceAll('https://www.adobe.com/express/templates/default-create-link', getMetadata('create-link') || '/'); +async function replaceDefaultPlaceholders(block, components) { + block.innerHTML = block.innerHTML.replaceAll('https://www.adobe.com/express/templates/default-create-link', components.link); - if (getMetadata('tasks') === '') { + if (components.tasks === '') { const placeholders = await fetchPlaceholders(); - template.innerHTML = template.innerHTML.replaceAll('default-create-link-text', placeholders['start-from-scratch'] || ''); + block.innerHTML = block.innerHTML.replaceAll('default-create-link-text', placeholders['start-from-scratch'] || ''); } else { - template.innerHTML = template.innerHTML.replaceAll('default-create-link-text', getMetadata('create-text') || ''); + block.innerHTML = block.innerHTML.replaceAll('default-create-link-text', getMetadata('create-text') || ''); } } async function getReplacementsFromSearch() { + // FIXME: tasks and tasksx split to be removed after mobile GA const params = new Proxy(new URLSearchParams(window.location.search), { get: (searchParams, prop) => searchParams.get(prop), }); const { tasks, + tasksx, phformat, topics, q, @@ -45,17 +45,26 @@ async function getReplacementsFromSearch() { } const placeholders = await fetchPlaceholders(); const categories = JSON.parse(placeholders['task-categories']); + const xCategories = JSON.parse(placeholders['x-task-categories']); if (!categories) { return null; } const tasksPair = Object.entries(categories).find((cat) => cat[1] === tasks); + const xTasksPair = Object.entries(xCategories).find((cat) => cat[1] === tasksx); const sanitizedTasks = tasks === "''" ? '' : tasks; const sanitizedTopics = topics === "''" ? '' : topics; const sanitizedQuery = q === "''" ? '' : q; - const translatedTasks = tasksPair ? tasksPair[0].toLowerCase() : tasks; + + let translatedTasks; + if (document.body.dataset.device === 'desktop') { + translatedTasks = xTasksPair?.[1] ? xTasksPair[0].toLowerCase() : tasksx; + } else { + translatedTasks = tasksPair?.[1] ? tasksPair[0].toLowerCase() : tasks; + } return { '{{queryTasks}}': sanitizedTasks || '', '{{QueryTasks}}': titleCase(sanitizedTasks || ''), + '{{queryTasksX}}': tasksx || '', '{{translatedTasks}}': translatedTasks || '', '{{TranslatedTasks}}': titleCase(translatedTasks || ''), '{{placeholderRatio}}': phformat || '', @@ -76,78 +85,7 @@ function replaceBladesInStr(str, replacements) { }); } -// for backwards compatibility -// TODO: remove this func after all content is updated -// legacy json -> metadata & dom blades -await (async function updateLegacyContent() { - const searchMarquee = document.querySelector('.search-marquee'); - if (searchMarquee) { - // not legacy - return; - } - const legacyAllTemplatesMetadata = await fetchAllTemplatesMetadata(); - const data = legacyAllTemplatesMetadata.find((p) => p.url === window.location.pathname); - if (!data) return; - if (['yes', 'true', 'on', 'Y'].includes(getMetadata('template-search-page'))) { - const replacements = await getReplacementsFromSearch(); - if (!replacements) return; - for (const key of Object.keys(data)) { - data[key] = replaceBladesInStr(data[key], replacements); - } - } - - const heroAnimation = document.querySelector('.hero-animation.wide'); - const templateList = document.querySelector('.template-list.fullwidth.apipowered'); - - const head = document.querySelector('head'); - Object.keys(data).forEach((metadataKey) => { - const existingMetadataTag = head.querySelector(`meta[name=${metadataKey}]`); - if (existingMetadataTag) { - existingMetadataTag.setAttribute('content', data[metadataKey]); - } else { - head.append(createTag('meta', { name: `${metadataKey}`, content: data[metadataKey] })); - } - }); - - if (heroAnimation) { - if (data.heroAnimationTitle) { - heroAnimation.innerHTML = heroAnimation.innerHTML.replace('Default template title', data.heroAnimationTitle); - } - - if (data.heroAnimationText) { - heroAnimation.innerHTML = heroAnimation.innerHTML.replace('Default template text', data.heroAnimationText); - } - } - - if (templateList) { - const regex = /default-[a-zA-Z_-]+/g; - const replacements = { - 'default-title': data.shortTitle || '', - 'default-tasks': data.templateTasks || '', - 'default-topics': data.templateTopics || '', - 'default-locale': data.templateLocale || 'en', - 'default-premium': data.templatePremium || '', - 'default-animated': data.templateAnimated || '', - 'default-format': data.placeholderFormat || '', - }; - templateList.innerHTML = templateList.innerHTML.replaceAll(regex, (match) => { - if (match in replacements) { - return replacements[match]; - } - return match; - }).replaceAll('https://www.adobe.com/express/templates/default-create-link', data.createLink || '/'); - - if (data.templateTasks === '') { - const placeholders = await fetchPlaceholders(); - templateList.innerHTML = templateList.innerHTML.replaceAll('default-create-link-text', placeholders['start-from-scratch'] || ''); - } else { - templateList.innerHTML = templateList.innerHTML.replaceAll('default-create-link-text', data.createText || ''); - } - } -}()); - -// searchbar -> metadata blades -await (async function updateMetadataForTemplates() { +async function updateMetadataForTemplates() { if (!['yes', 'true', 'on', 'Y'].includes(getMetadata('template-search-page'))) { return; } @@ -155,12 +93,16 @@ await (async function updateMetadataForTemplates() { if (head) { const replacements = await getReplacementsFromSearch(); if (!replacements) return; - head.innerHTML = replaceBladesInStr(head.innerHTML, replacements); + const title = head.getElementsByTagName('title')[0]; + title.innerText = replaceBladesInStr(title.innerText, replacements); + [...head.getElementsByTagName('meta')].forEach((meta) => { + meta.setAttribute('content', replaceBladesInStr(meta.getAttribute('content'), replacements)); + }); } -}()); +} // metadata -> dom blades -(function autoUpdatePage() { +function autoUpdatePage() { const wl = ['{{heading_placeholder}}', '{{type}}', '{{quantity}}']; // FIXME: deprecate wl const main = document.querySelector('main'); @@ -172,10 +114,10 @@ await (async function updateMetadataForTemplates() { } return match; }); -}()); +} // cleanup remaining dom blades -(async function updateNonBladeContent() { +async function updateNonBladeContent() { const heroAnimation = document.querySelector('.hero-animation.wide'); const templateList = document.querySelector('.template-list.fullwidth.apipowered'); const templateX = document.querySelector('.template-x'); @@ -193,11 +135,17 @@ await (async function updateMetadataForTemplates() { } if (templateList) { - await replaceDefaultPlaceholders(templateList); + await replaceDefaultPlaceholders(templateList, { + link: getMetadata('create-link') || '/', + tasks: getMetadata('tasks'), + }); } if (templateX) { - await replaceDefaultPlaceholders(templateX); + await replaceDefaultPlaceholders(templateX, { + link: getMetadata('create-link-x') || getMetadata('create-link') || '/', + tasks: getMetadata('tasks-x'), + }); } if (seoNav) { @@ -215,9 +163,9 @@ await (async function updateMetadataForTemplates() { if (browseByCat && !['yes', 'true', 'on', 'Y'].includes(getMetadata('show-browse-by-category'))) { browseByCat.remove(); } -}()); +} -(function validatePage() { +function validatePage() { const env = getHelixEnv(); const title = document.querySelector('title'); if ((env && env.name !== 'stage') && getMetadata('live') === 'N') { @@ -231,4 +179,11 @@ await (async function updateMetadataForTemplates() { if (env && env.name !== 'stage' && window.location.pathname.endsWith('/express/templates/default')) { window.location.replace('/404'); } -}()); +} + +export default async function replaceContent() { + await updateMetadataForTemplates(); + autoUpdatePage(); + await updateNonBladeContent(); + validatePage(); +} diff --git a/express/scripts/gnav.js b/express/scripts/gnav.js index 4cd8f7e4c..4bdbec7b2 100644 --- a/express/scripts/gnav.js +++ b/express/scripts/gnav.js @@ -150,10 +150,12 @@ async function loadFEDS() { window.addEventListener('adobePrivacy:PrivacyCustom', handleConsentSettings); const isMegaNav = window.location.pathname.startsWith('/express') - || window.location.pathname.startsWith('/education'); + || window.location.pathname.startsWith('/in/express') + || window.location.pathname.startsWith('/education') + || window.location.pathname.startsWith('/drafts'); const fedsExp = isMegaNav - ? `adobe-express/ax-gnav${isHomepage ? '-homepage' : ''}-beta` - : 'cc-express/cc-express-gnav'; + ? 'adobe-express/ax-gnav-x' + : 'adobe-express/ax-gnav-x-row'; async function buildBreadCrumbArray() { if (isHomepage || getMetadata('hide-breadcrumbs') === 'true') { @@ -249,7 +251,7 @@ async function loadFEDS() { } } - /* switch all links if lower envs */ + /* switch all links if lower env */ const env = getHelixEnv(); if (env && env.spark) { // eslint-disable-next-line no-console diff --git a/express/scripts/instrument.js b/express/scripts/instrument.js index a75a78903..fdb7d5303 100644 --- a/express/scripts/instrument.js +++ b/express/scripts/instrument.js @@ -18,12 +18,133 @@ import { getLanguage, getMetadata, checkTesting, - trackBranchParameters, + fetchPlaceholders, // eslint-disable-next-line import/no-unresolved } from './scripts.js'; import BlockMediator from './block-mediator.js'; +function getPlacement(btn) { + const parentBlock = btn.closest('.block'); + let placement = 'outside-blocks'; + + if (parentBlock) { + const blockName = parentBlock.dataset.blockName || parentBlock.classList[0]; + const sameBlocks = btn.closest('main')?.querySelectorAll(`.${blockName}`); + + if (sameBlocks && sameBlocks.length > 1) { + sameBlocks.forEach((b, i) => { + if (b === parentBlock) { + placement = `${blockName}-${i + 1}`; + } + }); + } else { + placement = blockName; + } + + if (['template-list', 'template-x'].includes(blockName) && btn.classList.contains('placeholder')) { + placement = 'blank-template-cta'; + } + } + + return placement; +} + +async function trackBranchParameters($links) { + const placeholders = await fetchPlaceholders(); + const rootUrl = new URL(window.location.href); + const rootUrlParameters = rootUrl.searchParams; + + const { experiment } = window.hlx; + const { referrer } = window.document; + const experimentStatus = experiment ? experiment.status.toLocaleLowerCase() : null; + const templateSearchTag = getMetadata('short-title'); + const pageUrl = window.location.pathname; + const sdid = rootUrlParameters.get('sdid'); + const mv = rootUrlParameters.get('mv'); + const mv2 = rootUrlParameters.get('mv2'); + const sKwcId = rootUrlParameters.get('s_kwcid'); + const efId = rootUrlParameters.get('ef_id'); + const promoId = rootUrlParameters.get('promoid'); + const trackingId = rootUrlParameters.get('trackingid'); + const cgen = rootUrlParameters.get('cgen'); + + $links.forEach(($a) => { + if ($a.href && $a.href.match('adobesparkpost.app.link')) { + const btnUrl = new URL($a.href); + const urlParams = btnUrl.searchParams; + const placement = getPlacement($a); + + if (templateSearchTag + && placeholders['search-branch-links']?.replace(/\s/g, '').split(',').includes(`${btnUrl.origin}${btnUrl.pathname}`)) { + urlParams.set('search', templateSearchTag); + urlParams.set('q', templateSearchTag); + urlParams.set('category', 'templates'); + urlParams.set('searchCategory', 'templates'); + } + + if (referrer) { + urlParams.set('referrer', referrer); + } + + if (pageUrl) { + urlParams.set('url', pageUrl); + } + + if (sdid) { + urlParams.set('sdid', sdid); + } + + if (mv) { + urlParams.set('mv', mv); + } + + if (mv2) { + urlParams.set('mv2', mv2); + } + + if (efId) { + urlParams.set('efid', efId); + } + + if (sKwcId) { + const sKwcIdParameters = sKwcId.split('!'); + + if (typeof sKwcIdParameters[2] !== 'undefined' && sKwcIdParameters[2] === '3') { + urlParams.set('customer_placement', 'Google%20AdWords'); + } + + if (typeof sKwcIdParameters[8] !== 'undefined' && sKwcIdParameters[8] !== '') { + urlParams.set('keyword', sKwcIdParameters[8]); + } + } + + if (promoId) { + urlParams.set('promoid', promoId); + } + + if (trackingId) { + urlParams.set('trackingid', trackingId); + } + + if (cgen) { + urlParams.set('cgen', cgen); + } + + if (experimentStatus === 'active') { + urlParams.set('expid', `${experiment.id}-${experiment.selectedVariant}`); + } + + if (placement) { + urlParams.set('ctaid', placement); + } + + btnUrl.search = urlParams.toString(); + $a.href = decodeURIComponent(btnUrl.toString()); + } + }); +} + // this saves on file size when this file gets minified... const w = window; const d = document; @@ -73,8 +194,8 @@ if (useAlloy) { (window.spark && window.spark.hostname === 'www.stage.adobe.com') || martech === 'alloy-qa' ) - ? '0f6221fd-db23-4376-8ad7-8dc7c799032f' - : 'b2e000b1-98ab-4ade-8c4f-5823d84cf015' + ? '8d2805dd-85bf-4748-82eb-f99fdad117a6' + : '2cba807b-7430-41ae-9aac-db2b0da742d5' ), }, target: checkTesting(), @@ -1060,7 +1181,8 @@ loadScript(martechURL, () => { } // Tracking any link or links that is added after page loaded. - document.addEventListener('linkspopulated', (e) => { + document.addEventListener('linkspopulated', async (e) => { + await trackBranchParameters(e.detail); e.detail.forEach(($link) => { $link.addEventListener('click', () => { trackButtonClick($link); diff --git a/express/scripts/scripts.js b/express/scripts/scripts.js index 9a60e6174..fa3d9aba6 100644 --- a/express/scripts/scripts.js +++ b/express/scripts/scripts.js @@ -195,13 +195,24 @@ export function toClassName(name) { : ''; } -export function createTag(name, attrs) { - const el = document.createElement(name); - if (typeof attrs === 'object') { - for (const [key, value] of Object.entries(attrs)) { - el.setAttribute(key, value); +export function createTag(tag, attributes, html) { + const el = document.createElement(tag); + if (html) { + if (html instanceof HTMLElement + || html instanceof SVGElement + || html instanceof DocumentFragment) { + el.append(html); + } else if (Array.isArray(html)) { + el.append(...html); + } else { + el.insertAdjacentHTML('beforeend', html); } } + if (attributes) { + Object.entries(attributes).forEach(([key, val]) => { + el.setAttribute(key, val); + }); + } return el; } @@ -347,6 +358,20 @@ export function getIcon(icons, alt, size = 44) { 'star', 'star-half', 'star-empty', + 'pricing-gen-ai', + 'pricing-features', + 'pricing-import', + 'pricing-motion', + 'pricing-stock', + 'pricing-one-click', + 'pricing-collaborate', + 'pricing-premium-plan', + 'pricing-sync', + 'pricing-brand', + 'pricing-calendar', + 'pricing-fonts', + 'pricing-libraries', + 'pricing-cloud', ]; const size22Icons = [ @@ -376,13 +401,14 @@ export function getIconElement(icons, size, alt, additionalClassName) { return ($div.firstElementChild); } -export function transformLinkToAnimation($a) { +export function transformLinkToAnimation($a, $videoLooping = true) { if (!$a || !$a.href.endsWith('.mp4')) { return null; } const params = new URL($a.href).searchParams; const attribs = {}; - ['playsinline', 'autoplay', 'loop', 'muted'].forEach((p) => { + const dataAttr = $videoLooping ? ['playsinline', 'autoplay', 'loop', 'muted'] : ['playsinline', 'autoplay', 'muted']; + dataAttr.forEach((p) => { if (params.get(p) !== 'false') attribs[p] = ''; }); // use closest picture as poster @@ -441,7 +467,7 @@ export function readBlockConfig($block) { if ($cols[1]) { const $value = $cols[1]; const name = toClassName($cols[0].textContent.trim()); - let value = ''; + let value; if ($value.querySelector('a')) { const $as = [...$value.querySelectorAll('a')]; if ($as.length === 1) { @@ -464,11 +490,23 @@ export function readBlockConfig($block) { return config; } +function removeIrrelevantSections(main) { + main.querySelectorAll(':scope > div').forEach((section) => { + const sectionMeta = section.querySelector('div.section-metadata'); + if (sectionMeta) { + const meta = readBlockConfig(sectionMeta); + if (meta.audience && meta.audience !== document.body.dataset?.device) { + section.remove(); + } + } + }); +} + /** * Decorates all sections in a container element. * @param {Element} $main The container element */ -export function decorateSections($main) { +function decorateSections($main) { let noAudienceFound = false; $main.querySelectorAll(':scope > div').forEach((section) => { const wrappers = []; @@ -496,6 +534,8 @@ export function decorateSections($main) { section.classList.add(...meta.style.split(', ').map(toClassName)); } else if (key === 'anchor') { section.id = toClassName(meta.anchor); + } else if (key === 'background') { + section.style.background = meta.background; } else { section.dataset[key] = meta[key]; } @@ -556,119 +596,6 @@ export function getCookie(cname) { return ''; } -function getCountry() { - let country = new URLSearchParams(window.location.search).get('country'); - if (!country) { - country = getCookie('international'); - } - if (!country) { - country = getLocale(window.location); - } - if (country === 'uk') country = 'gb'; - return (country.split('_')[0]); -} - -export function getCurrency(locale) { - const loc = locale || getCountry(); - const currencies = { - ar: 'ARS', - at: 'EUR', - au: 'AUD', - be: 'EUR', - bg: 'EUR', - br: 'BRL', - ca: 'CAD', - ch: 'CHF', - cl: 'CLP', - co: 'COP', - cr: 'USD', - cy: 'EUR', - cz: 'EUR', - de: 'EUR', - dk: 'DKK', - ec: 'USD', - ee: 'EUR', - es: 'EUR', - fi: 'EUR', - fr: 'EUR', - gb: 'GBP', - gr: 'EUR', - gt: 'USD', - hk: 'HKD', - hu: 'EUR', - id: 'IDR', - ie: 'EUR', - il: 'ILS', - in: 'INR', - it: 'EUR', - jp: 'JPY', - kr: 'KRW', - lt: 'EUR', - lu: 'EUR', - lv: 'EUR', - mt: 'EUR', - mx: 'MXN', - my: 'MYR', - nl: 'EUR', - no: 'NOK', - nz: 'AUD', - pe: 'PEN', - ph: 'PHP', - pl: 'EUR', - pt: 'EUR', - ro: 'EUR', - ru: 'RUB', - se: 'SEK', - sg: 'SGD', - si: 'EUR', - sk: 'EUR', - th: 'THB', - tw: 'TWD', - us: 'USD', - ve: 'USD', - za: 'USD', - ae: 'USD', - bh: 'BHD', - eg: 'EGP', - jo: 'JOD', - kw: 'KWD', - om: 'OMR', - qa: 'USD', - sa: 'SAR', - ua: 'USD', - dz: 'USD', - lb: 'LBP', - ma: 'USD', - tn: 'USD', - ye: 'USD', - am: 'USD', - az: 'USD', - ge: 'USD', - md: 'USD', - tm: 'USD', - by: 'USD', - kz: 'USD', - kg: 'USD', - tj: 'USD', - uz: 'USD', - bo: 'USD', - do: 'USD', - hr: 'EUR', - ke: 'USD', - lk: 'USD', - mo: 'HKD', - mu: 'USD', - ng: 'USD', - pa: 'USD', - py: 'USD', - sv: 'USD', - tt: 'USD', - uy: 'USD', - vn: 'USD', - }; - return currencies[loc]; -} - export function getLanguage(locale) { const langs = { us: 'en-US', @@ -740,16 +667,6 @@ function convertGlobToRe(glob) { return (new RegExp(reString)); } -function getCurrencyDisplay(currency) { - if (currency === 'JPY') { - return 'name'; - } - if (['SEK', 'DKK', 'NOK'].includes(currency)) { - return 'code'; - } - return 'symbol'; -} - export async function fetchRelevantRows(path) { if (!window.relevantRows) { try { @@ -777,60 +694,6 @@ export async function fetchRelevantRows(path) { return null; } -export function formatPrice(price, currency) { - const locale = ['USD', 'TWD'].includes(currency) - ? 'en-GB' // use en-GB for intl $ symbol formatting - : getLanguage(getCountry()); - const currencyDisplay = getCurrencyDisplay(currency); - return new Intl.NumberFormat(locale, { - style: 'currency', - currency, - currencyDisplay, - }).format(price) - .replace('SAR', 'SR'); // custom currency symbol for SAR -} - -export async function getOffer(offerId, countryOverride) { - let country = getCountry(); - if (countryOverride) country = countryOverride; - if (!country) country = 'us'; - let currency = getCurrency(country); - if (!currency) { - country = 'us'; - currency = 'USD'; - } - const resp = await fetch('/express/system/offers-new.json'); - if (!resp.ok) return {}; - const json = await resp.json(); - const upperCountry = country.toUpperCase(); - let offer = json.data.find((e) => (e.o === offerId) && (e.c === upperCountry)); - if (!offer) offer = json.data.find((e) => (e.o === offerId) && (e.c === 'US')); - - if (offer) { - // console.log(offer); - const lang = getLanguage(getLocale(window.location)).split('-')[0]; - const unitPrice = offer.p; - const unitPriceCurrencyFormatted = formatPrice(unitPrice, currency); - const commerceURL = `https://commerce.adobe.com/checkout?cli=spark&co=${country}&items%5B0%5D%5Bid%5D=${offerId}&items%5B0%5D%5Bcs%5D=0&rUrl=https%3A%2F%express.adobe.com%2Fsp%2F&lang=${lang}`; - const vatInfo = offer.vat; - const prefix = offer.pre; - const suffix = offer.suf; - - return { - country, - currency, - unitPrice, - unitPriceCurrencyFormatted, - commerceURL, - lang, - vatInfo, - prefix, - suffix, - }; - } - return {}; -} - export function addBlockClasses($block, classNames) { const $rows = Array.from($block.children); $rows.forEach(($row) => { @@ -848,20 +711,25 @@ export function addBlockClasses($block, classNames) { // } function decorateHeaderAndFooter() { - const $header = document.querySelector('header'); + const header = document.querySelector('header'); - $header.addEventListener('click', (event) => { + header.addEventListener('click', (event) => { if (event.target.id === 'feds-topnav') { const root = window.location.href.split('/express/')[0]; window.location.href = `${root}/express/`; } }); - $header.innerHTML = '
'; - - document.querySelector('footer').innerHTML = ` - - `; + const headerMeta = getMeta('header'); + if (headerMeta !== 'off') header.innerHTML = '
'; + else header.remove(); + const footerMeta = getMeta('footer'); + const footer = document.querySelector('footer'); + if (footerMeta !== 'off') { + footer.innerHTML = ` + + `; + } else footer.remove(); } /** @@ -960,7 +828,7 @@ export function decorateBlock(block) { * Decorates all blocks in a container element. * @param {Element} main The container element */ -export function decorateBlocks(main) { +function decorateBlocks(main) { main .querySelectorAll('div.section > div > div') .forEach((block) => decorateBlock(block)); @@ -1649,7 +1517,7 @@ export async function fixIcons(block = document) { }); } -export function unwrapBlock($block) { +function unwrapBlock($block) { const $section = $block.parentNode; const $elems = [...$section.children]; const $blockSection = createTag('div'); @@ -1673,6 +1541,9 @@ export function unwrapBlock($block) { if (!$postBlockSection.hasChildNodes()) { $postBlockSection.remove(); } + + // fixme: technically $section can become empty too after unwrapping. + // This function currently leaves empty section after the generation of relevant rows } export function normalizeHeadings(block, allowedHeadings) { @@ -1811,11 +1682,11 @@ async function buildAutoBlocks($main) { const relevantRowsData = await fetchRelevantRows(window.location.pathname); if (relevantRowsData) { - const $relevantRowsSection = createTag('div'); - const $fragment = buildBlock('fragment', '/express/fragments/relevant-rows-default-v2'); - $relevantRowsSection.dataset.audience = 'mobile'; - $relevantRowsSection.append($fragment); - $main.insertBefore($relevantRowsSection, $main.firstElementChild.nextSibling); + const relevantRowsSection = createTag('div'); + const fragment = buildBlock('fragment', '/express/fragments/relevant-rows-default-v2'); + relevantRowsSection.dataset.audience = 'mobile'; + relevantRowsSection.append(fragment); + $main.prepend(relevantRowsSection); window.relevantRowsLoaded = true; } } @@ -1845,26 +1716,12 @@ async function buildAutoBlocks($main) { if (!window.floatingCtasLoaded) { const floatingCTAData = await fetchFloatingCta(window.location.pathname); const validButtonVersion = ['floating-button', 'multifunction-button', 'bubble-ui-button', 'floating-panel']; - let desktopButton; - let mobileButton; - - if (floatingCTAData) { - const buttonTypes = { - desktop: floatingCTAData.desktop, - mobile: floatingCTAData.mobile, - }; - - desktopButton = validButtonVersion.includes(buttonTypes.desktop) ? buildBlock(buttonTypes.desktop, 'desktop') : null; - mobileButton = validButtonVersion.includes(buttonTypes.mobile) ? buildBlock(buttonTypes.mobile, 'mobile') : null; - - [desktopButton, mobileButton].forEach((button) => { - if (button) { - button.classList.add('spreadsheet-powered'); - if ($lastDiv) { - $lastDiv.append(button); - } - } - }); + const device = document.body.dataset?.device; + const blockName = floatingCTAData?.[device]; + if (validButtonVersion.includes(blockName) && $lastDiv) { + const button = buildBlock(blockName, device); + button.classList.add('spreadsheet-powered'); + $lastDiv.append(button); } window.floatingCtasLoaded = true; @@ -1908,19 +1765,20 @@ function splitSections($main) { function setTheme() { let theme = getMeta('theme'); if (!theme && (window.location.pathname.startsWith('/express') - || window.location.pathname.startsWith('/education'))) { + || window.location.pathname.startsWith('/education') + || window.location.pathname.startsWith('/drafts'))) { // mega nav, suppress brand header theme = 'no-brand-header'; } - const $body = document.body; + const { body } = document; if (theme) { let themeClass = toClassName(theme); /* backwards compatibility can be removed again */ if (themeClass === 'nobrand') themeClass = 'no-desktop-brand-header'; - $body.classList.add(themeClass); - if (themeClass === 'blog') $body.classList.add('no-brand-header'); + body.classList.add(themeClass); + if (themeClass === 'blog') body.classList.add('no-brand-header'); } - $body.dataset.device = navigator.userAgent.includes('Mobile') ? 'mobile' : 'desktop'; + body.dataset.device = navigator.userAgent.includes('Mobile') ? 'mobile' : 'desktop'; } function decorateLinkedPictures($main) { @@ -2121,25 +1979,25 @@ export function createOptimizedPicture(src, alt = '', eager = false, breakpoints * @param {Element} main The main element */ function decoratePictures(main) { - main.querySelectorAll('img[src*="/media_"').forEach((img, i) => { + main.querySelectorAll('img[src*="/media_"]').forEach((img, i) => { const newPicture = createOptimizedPicture(img.src, img.alt, !i); const picture = img.closest('picture'); if (picture) picture.parentElement.replaceChild(newPicture, picture); }); } -export async function decorateMain($main) { - await buildAutoBlocks($main); - splitSections($main); - decorateSections($main); - decorateButtons($main); - decorateBlocks($main); - decorateMarqueeColumns($main); - await fixIcons($main); - decoratePictures($main); - decorateLinkedPictures($main); - decorateSocialIcons($main); - makeRelativeLinks($main); +export async function decorateMain(main) { + await buildAutoBlocks(main); + splitSections(main); + decorateSections(main); + decorateButtons(main); + decorateBlocks(main); + decorateMarqueeColumns(main); + await fixIcons(main); + decoratePictures(main); + decorateLinkedPictures(main); + decorateSocialIcons(main); + makeRelativeLinks(main); } const usp = new URLSearchParams(window.location.search); @@ -2316,26 +2174,29 @@ function decorateLegalCopy(main) { /** * loads everything needed to get to LCP. */ -async function loadEager() { +async function loadEager(main) { setTheme(); - const main = document.querySelector('main'); if (main) { const language = getLanguage(getLocale(window.location)); const langSplits = language.split('-'); langSplits.pop(); const htmlLang = langSplits.join('-'); document.documentElement.setAttribute('lang', htmlLang); + + removeIrrelevantSections(main); } if (!window.hlx.lighthouse) await decorateTesting(); // for backward compatibility // TODO: remove the href check after we tag content with sheet-powered if (getMetadata('sheet-powered') === 'Y' || window.location.href.includes('/express/templates/')) { - await import('./content-replace.js'); + const { default: replaceContent } = await import('./content-replace.js'); + await replaceContent(); } if (getMetadata('template-search-page') === 'Y') { - await import('./template-redirect.js'); + const { default: redirect } = await import('./template-redirect.js'); + await redirect(); } if (main) { @@ -2348,14 +2209,11 @@ async function loadEager() { displayOldLinkWarning(); wordBreakJapanese(); - const lcpBlocks = ['columns', 'hero-animation', 'hero-3d', 'template-list', 'floating-button', 'fullscreen-marquee', 'collapsible-card', 'search-marquee']; - const blocks = document.querySelectorAll('.block'); - const firstVisualBlock = Array.from(blocks).find((b) => { - const { audience } = b.closest('.section')?.dataset || {}; - return audience === document.body.dataset.device; - }); - const hasLCPBlock = (firstVisualBlock && lcpBlocks.includes(firstVisualBlock.getAttribute('data-block-name'))); - if (hasLCPBlock) await loadBlock(firstVisualBlock, true); + const lcpBlocks = ['columns', 'hero-animation', 'hero-3d', 'template-list', 'template-x', 'floating-button', 'fullscreen-marquee', 'fullscreen-marquee-desktop', 'collapsible-card', 'search-marquee']; + if (getMetadata('show-relevant-rows') === 'yes') lcpBlocks.push('fragment'); + const block = document.querySelector('.block'); + const hasLCPBlock = (block && lcpBlocks.includes(block.getAttribute('data-block-name'))); + if (hasLCPBlock) await loadBlock(block, true); document.querySelector('body').classList.add('appear'); @@ -2396,71 +2254,17 @@ function removeMetadata() { }); } -export async function buildStaticFreePlanWidget() { - const placeholders = await fetchPlaceholders(); - const widget = createTag('div', { class: 'free-plan-widget' }); - for (let i = 1; i < 3; i += 1) { - const checkMarkColor = i % 2 !== 0 ? '#c457f0' : '#f06dad'; - const tagText = placeholders[`free-plan-check-${i}`]; - const tagWrapper = createTag('div'); - const checkMarkDiv = createTag('div', { style: `background-color: ${checkMarkColor}` }); - const textDiv = createTag('div'); - checkMarkDiv.append(getIconElement('checkmark')); - textDiv.textContent = tagText; - tagWrapper.append(checkMarkDiv, textDiv); - widget.append(tagWrapper); - } - return widget; -} - -export async function addFreePlanWidget(elem) { - if (elem && ['yes', 'true'].includes(getMetadata('show-free-plan').toLowerCase())) { - const placeholders = await fetchPlaceholders(); - const widget = await buildStaticFreePlanWidget(); - - document.addEventListener('planscomparisonloaded', () => { - const $learnMoreButton = createTag('a', { - class: 'learn-more-button', - href: '#plans-comparison-container', - }); - const lottieWrapper = createTag('span', { class: 'lottie-wrapper' }); - - $learnMoreButton.textContent = placeholders['learn-more']; - lottieWrapper.innerHTML = getLottie('purple-arrows', '/express/icons/purple-arrows.json'); - $learnMoreButton.append(lottieWrapper); - lazyLoadLottiePlayer(); - widget.append($learnMoreButton); - - $learnMoreButton.addEventListener('click', (e) => { - e.preventDefault(); - // temporarily disabling smooth scroll for accurate location - const $html = document.querySelector('html'); - $html.style.scrollBehavior = 'unset'; - const $plansComparison = document.querySelector('.plans-comparison-container'); - $plansComparison.scrollIntoView(); - $html.style.removeProperty('scroll-behavior'); - }); - }); - - elem.append(widget); - elem.classList.add('free-plan-container'); - } -} - /** * loads everything that doesn't need to be delayed. */ -async function loadLazy() { - const main = document.querySelector('main'); - +async function loadLazy(main) { // post LCP actions go here sampleRUM('lcp'); - loadBlocks(main); + loadBlocks(main).then(() => addPromotion()); loadCSS('/express/styles/lazy-styles.css'); scrollToHash(); resolveFragments(); - addPromotion(); removeMetadata(); addFavIcon('/express/icons/cc-express.svg'); if (!window.hlx.lighthouse) loadMartech(); @@ -2483,8 +2287,9 @@ async function decoratePage() { window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse') === 'on'; window.hlx.init = true; - await loadEager(); - loadLazy(); + const main = document.querySelector('main'); + await loadEager(main); + loadLazy(main); loadGnav(); if (window.location.hostname.endsWith('hlx.page') || window.location.hostname === ('localhost')) { import('../../tools/preview/preview.js'); @@ -2550,65 +2355,6 @@ function registerPerformanceLogger() { } } -export function trackBranchParameters($links) { - const rootUrl = new URL(window.location.href); - const rootUrlParameters = rootUrl.searchParams; - - const sdid = rootUrlParameters.get('sdid'); - const mv = rootUrlParameters.get('mv'); - const sKwcId = rootUrlParameters.get('s_kwcid'); - const efId = rootUrlParameters.get('ef_id'); - const promoId = rootUrlParameters.get('promoid'); - const trackingId = rootUrlParameters.get('trackingid'); - const cgen = rootUrlParameters.get('cgen'); - - if (sdid || mv || sKwcId || efId || promoId || trackingId || cgen) { - $links.forEach(($a) => { - if ($a.href && $a.href.match('adobesparkpost.app.link')) { - const buttonUrl = new URL($a.href); - const urlParams = buttonUrl.searchParams; - - if (sdid) { - urlParams.set('~campaign_id', sdid); - } - - if (mv) { - urlParams.set('~customer_campaign', mv); - } - - if (sKwcId) { - const sKwcIdParameters = sKwcId.split('!'); - - if (typeof sKwcIdParameters[2] !== 'undefined' && sKwcIdParameters[2] === '3') { - urlParams.set('~customer_placement', 'Google%20AdWords'); - } - - if (typeof sKwcIdParameters[8] !== 'undefined' && sKwcIdParameters[8] !== '') { - urlParams.set('~keyword', sKwcIdParameters[8]); - } - } - - if (promoId) { - urlParams.set('~ad_id', promoId); - } - - if (trackingId) { - urlParams.set('~keyword_id', trackingId); - } - - if (cgen) { - urlParams.set('~customer_keyword', cgen); - } - - urlParams.set('~feature', 'paid%20advertising'); - - buttonUrl.search = urlParams.toString(); - $a.href = buttonUrl.toString(); - } - }); - } -} - if (window.name.includes('performance')) registerPerformanceLogger(); export function getMobileOperatingSystem() { @@ -2637,16 +2383,3 @@ export function titleCase(str) { } return splitStr.join(' '); } - -export function arrayToObject(arr) { - return arr.reduce( - (acc, curr) => { - const key = curr[0]; - [, acc[key]] = curr; - - return acc; - }, - - {}, - ); -} diff --git a/express/scripts/template-redirect.js b/express/scripts/template-redirect.js index 6afda30f2..99daa4235 100644 --- a/express/scripts/template-redirect.js +++ b/express/scripts/template-redirect.js @@ -18,17 +18,20 @@ async function existsTemplatePage(url) { return allTemplatesMetadata.some((e) => e.url === url); } -(async function redirectToExistingPage() { +export default async function redirectToExistingPage() { // TODO: check if the search query points to an existing page. If so, redirect. - const params = new Proxy(new URLSearchParams(window.location.search), { + const { topics, tasks, tasksx } = new Proxy(new URLSearchParams(window.location.search), { get: (searchParams, prop) => searchParams.get(prop), }); - if (params.topics) { - const targetPath = `/express/templates/${params.tasks}`.concat(params.topics ? `/${params.topics}` : ''); - const locale = getLocale(window.location); - const pathToMatch = locale === 'us' ? targetPath : `/${locale}${targetPath}`; - if (await existsTemplatePage(pathToMatch)) { - window.location.replace(`${window.location.origin}${pathToMatch}`); - } + const sanitizedTopics = topics && topics !== "''" ? `/${topics}` : ''; + const sanitizedTasks = tasks && tasks !== "''" ? `/${tasks}` : ''; + const sanitizedTasksX = tasksx && tasksx !== "''" ? `/${tasksx}` : ''; + const slash = !(sanitizedTasks || sanitizedTasksX) && !sanitizedTopics ? '/' : ''; + const targetPath = `/express/templates${slash}${sanitizedTasks || sanitizedTasksX}${sanitizedTopics}`; + const locale = getLocale(window.location); + const pathToMatch = locale === 'us' ? targetPath : `/${locale}${targetPath}`; + if (await existsTemplatePage(pathToMatch)) { + window.location.assign(`${window.location.origin}${pathToMatch}`); + document.body.style.display = 'none'; // hide the page until the redirect happens } -}()); +} diff --git a/express/scripts/utils/free-plan.js b/express/scripts/utils/free-plan.js new file mode 100644 index 000000000..fdacc2d98 --- /dev/null +++ b/express/scripts/utils/free-plan.js @@ -0,0 +1,64 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { + fetchPlaceholders, + createTag, + getMetadata, + getLottie, + lazyLoadLottiePlayer, +} from '../scripts.js'; + +export async function buildStaticFreePlanWidget() { + const placeholders = await fetchPlaceholders(); + const widget = createTag('div', { class: 'free-plan-widget' }); + for (let i = 1; i < 3; i += 1) { + const tagText = placeholders[`free-plan-check-${i}`]; + const textDiv = createTag('div'); + textDiv.textContent = tagText; + widget.append(textDiv); + } + return widget; +} + +export async function addFreePlanWidget(elem) { + if (elem && ['yes', 'true', 'y', 'on'].includes(getMetadata('show-free-plan').toLowerCase())) { + const placeholders = await fetchPlaceholders(); + const widget = await buildStaticFreePlanWidget(); + + document.addEventListener('planscomparisonloaded', () => { + const $learnMoreButton = createTag('a', { + class: 'learn-more-button', + href: '#plans-comparison-container', + }); + const lottieWrapper = createTag('span', { class: 'lottie-wrapper' }); + + $learnMoreButton.textContent = placeholders['learn-more']; + lottieWrapper.innerHTML = getLottie('purple-arrows', '/express/icons/purple-arrows.json'); + $learnMoreButton.append(lottieWrapper); + lazyLoadLottiePlayer(); + widget.append($learnMoreButton); + + $learnMoreButton.addEventListener('click', (e) => { + e.preventDefault(); + // temporarily disabling smooth scroll for accurate location + const $html = document.querySelector('html'); + $html.style.scrollBehavior = 'unset'; + const $plansComparison = document.querySelector('.plans-comparison-container'); + $plansComparison.scrollIntoView(); + $html.style.removeProperty('scroll-behavior'); + }); + }); + + elem.append(widget); + elem.classList.add('free-plan-container'); + } +} diff --git a/express/scripts/utils/pricing.js b/express/scripts/utils/pricing.js new file mode 100644 index 000000000..44a14da3f --- /dev/null +++ b/express/scripts/utils/pricing.js @@ -0,0 +1,197 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { getLocale, getLanguage, getCookie } from '../scripts.js'; + +function getCurrencyDisplay(currency) { + if (currency === 'JPY') { + return 'name'; + } + if (['SEK', 'DKK', 'NOK'].includes(currency)) { + return 'code'; + } + return 'symbol'; +} + +function getCountry() { + let country = new URLSearchParams(window.location.search).get('country'); + if (!country) { + country = getCookie('international'); + } + if (!country) { + country = getLocale(window.location); + } + if (country === 'uk') country = 'gb'; + return (country.split('_')[0]); +} + +export function formatPrice(price, currency) { + const customSymbols = { + SAR: 'SR', + CA: 'CAD', + }; + const locale = ['USD', 'TWD'].includes(currency) + ? 'en-GB' // use en-GB for intl $ symbol formatting + : getLanguage(getCountry()); + const currencyDisplay = getCurrencyDisplay(currency); + let formattedPrice = new Intl.NumberFormat(locale, { + style: 'currency', + currency, + currencyDisplay, + }).format(price); + + Object.entries(customSymbols).forEach(([symbol, replacement]) => { + formattedPrice = formattedPrice.replace(symbol, replacement); + }); + + return formattedPrice; +} + +export function getCurrency(locale) { + const loc = locale || getCountry(); + const currencies = { + ar: 'ARS', + at: 'EUR', + au: 'AUD', + be: 'EUR', + bg: 'EUR', + br: 'BRL', + ca: 'CAD', + ch: 'CHF', + cl: 'CLP', + co: 'COP', + cr: 'USD', + cy: 'EUR', + cz: 'EUR', + de: 'EUR', + dk: 'DKK', + ec: 'USD', + ee: 'EUR', + es: 'EUR', + fi: 'EUR', + fr: 'EUR', + gb: 'GBP', + gr: 'EUR', + gt: 'USD', + hk: 'HKD', + hu: 'EUR', + id: 'IDR', + ie: 'EUR', + il: 'ILS', + in: 'INR', + it: 'EUR', + jp: 'JPY', + kr: 'KRW', + lt: 'EUR', + lu: 'EUR', + lv: 'EUR', + mt: 'EUR', + mx: 'MXN', + my: 'MYR', + nl: 'EUR', + no: 'NOK', + nz: 'AUD', + pe: 'PEN', + ph: 'PHP', + pl: 'EUR', + pt: 'EUR', + ro: 'EUR', + ru: 'RUB', + se: 'SEK', + sg: 'SGD', + si: 'EUR', + sk: 'EUR', + th: 'THB', + tw: 'TWD', + us: 'USD', + ve: 'USD', + za: 'USD', + ae: 'USD', + bh: 'BHD', + eg: 'EGP', + jo: 'JOD', + kw: 'KWD', + om: 'OMR', + qa: 'USD', + sa: 'SAR', + ua: 'USD', + dz: 'USD', + lb: 'LBP', + ma: 'USD', + tn: 'USD', + ye: 'USD', + am: 'USD', + az: 'USD', + ge: 'USD', + md: 'USD', + tm: 'USD', + by: 'USD', + kz: 'USD', + kg: 'USD', + tj: 'USD', + uz: 'USD', + bo: 'USD', + do: 'USD', + hr: 'EUR', + ke: 'USD', + lk: 'USD', + mo: 'HKD', + mu: 'USD', + ng: 'USD', + pa: 'USD', + py: 'USD', + sv: 'USD', + tt: 'USD', + uy: 'USD', + vn: 'USD', + }; + return currencies[loc]; +} + +export async function getOffer(offerId, countryOverride) { + let country = getCountry(); + if (countryOverride) country = countryOverride; + if (!country) country = 'us'; + let currency = getCurrency(country); + if (!currency) { + country = 'us'; + currency = 'USD'; + } + const resp = await fetch('/express/system/offers-new.json'); + if (!resp.ok) return {}; + const json = await resp.json(); + const upperCountry = country.toUpperCase(); + let offer = json.data.find((e) => (e.o === offerId) && (e.c === upperCountry)); + if (!offer) offer = json.data.find((e) => (e.o === offerId) && (e.c === 'US')); + + if (offer) { + const lang = getLanguage(getLocale(window.location)).split('-')[0]; + const unitPrice = offer.p; + const unitPriceCurrencyFormatted = formatPrice(unitPrice, currency); + const commerceURL = `https://commerce.adobe.com/checkout?cli=spark&co=${country}&items%5B0%5D%5Bid%5D=${offerId}&items%5B0%5D%5Bcs%5D=0&rUrl=https%3A%2F%express.adobe.com%2Fsp%2F&lang=${lang}`; + const vatInfo = offer.vat; + const prefix = offer.pre; + const suffix = offer.suf; + + return { + country, + currency, + unitPrice, + unitPriceCurrencyFormatted, + commerceURL, + lang, + vatInfo, + prefix, + suffix, + }; + } + return {}; +} diff --git a/express/styles/styles.css b/express/styles/styles.css index dd0b0d254..9b607e1a7 100644 --- a/express/styles/styles.css +++ b/express/styles/styles.css @@ -21,7 +21,6 @@ --color-black: #000; --color-brand-title: #000B1D; --color-info-accent: #5C5CE0; - --color-info-accent-disabled: #897DF2; --color-info-accent-hover: #4646C6; --color-info-accent-down: #3D3DB4; --color-info-accent-reverse: #eeeefc; @@ -55,6 +54,7 @@ --body-font-size-xl: 1.25rem; /* 20px */ --body-font-size-l: 1.125rem; + --body-font-fluid-size-l: 1.125em; /* 18px */ --body-font-size-m: 1rem; /* 16px */ @@ -73,6 +73,7 @@ --heading-font-size-xl: 2.8125rem; /* 45px */ --heading-font-size-l: 2.25rem; + --heading-font-fluid-size-l: 2.25rem; /* 36px */ --heading-font-size-m: 1.75rem; /* 28px */ @@ -208,15 +209,6 @@ a.button.primaryCTA:any-link { border-color: var(--color-info-accent); } -a.button.disabled:any-link, -a.button.accent.disabled:any-link, -a.button.primaryCTA.disabled:any-link { - color: var(--color-white); - background-color: var(--color-info-accent-disabled); - border-color: var(--color-info-accent-disabled); - cursor: not-allowed; -} - a.button:any-link:hover, a.button.accent:any-link:hover, a.button.primaryCTA:any-link:hover { @@ -544,10 +536,6 @@ main .section:last-of-type>.default-content-wrapper { main p { font-size: var(--body-font-size-l); } - - main .section div.promotion { - margin-bottom: 0; - } } @media (min-width:900px) { @@ -891,10 +879,6 @@ main .block.template-list[data-block-status='initialized']>div { display: none; } -main .template-list.sixcols { - min-height: 700px; -} - main .columns.columns-marquee { min-height: 650px; } @@ -958,11 +942,11 @@ main .block.center .free-plan-widget { main .block .free-plan-widget>div { display: flex; flex-direction: row; + align-items: center; } main .block .free-plan-widget>div>div:first-child { position: relative; - margin-top: 5px; margin-right: 6px; width: 14px; height: 14px; @@ -980,14 +964,14 @@ main .block .free-plan-widget .icon.icon-checkmark { } @media (min-width: 900px) { - main .block .button-container.free-plan-container { width: max-content; } main .block.fullscreen-marquee .button-container.free-plan-container { - width: 400px; - align-items: flex-start; + justify-content: center; + width: auto; + align-items: center; } main .block .button-container.free-plan-container a.button { @@ -1046,13 +1030,10 @@ main .block .free-plan-widget .icon.icon-checkmark { main .template-list-sixcols-container>div { max-width: 1122px; } -} -@media (min-width: 900px) { - main .block.fullscreen-marquee .button-container.free-plan-container { - justify-content: center; - width: auto; - align-items: center; + main .block .free-plan-bullet .free-plan-bullet-container, + main .block .free-plan-widget-placeholder { + max-width: 400px; } } diff --git a/fstab.yaml b/fstab.yaml index ca22012a2..863e1d7d9 100644 --- a/fstab.yaml +++ b/fstab.yaml @@ -1,9 +1,8 @@ mountpoints: /: - url: https://adobe.sharepoint.com/sites/CC-Express/Shared%20Documents/website + url: https://adobe.sharepoint.com/:f:/r/sites/adobecom/Express/website folders: - /express/templates/content.json: /metadata.json /express/templates/: /express/templates/default /br/express/templates/: /br/express/templates/default /de/express/templates/: /de/express/templates/default @@ -14,4 +13,18 @@ folders: /kr/express/templates/: /kr/express/templates/default /nl/express/templates/: /nl/express/templates/default /tw/express/templates/: /tw/express/templates/default - + /express/colors/: /express/colors/default + /br/express/colors/: /br/express/colors/default + /cn/express/colors/: /cn/express/colors/default + /de/express/colors/: /de/express/colors/default + /dk/express/colors/: /dk/express/colors/default + /es/express/colors/: /es/express/colors/default + /fi/express/colors/: /fi/express/colors/default + /fr/express/colors/: /fr/express/colors/default + /it/express/colors/: /it/express/colors/default + /jp/express/colors/: /jp/express/colors/default + /kr/express/colors/: /kr/express/colors/default + /nl/express/colors/: /nl/express/colors/default + /no/express/colors/: /no/express/colors/default + /se/express/colors/: /se/express/colors/default + /tw/express/colors/: /tw/express/colors/default diff --git a/helix-sitemap.yaml b/helix-sitemap.yaml index 12b0f6ce5..6bbe9ac7a 100644 --- a/helix-sitemap.yaml +++ b/helix-sitemap.yaml @@ -130,47 +130,52 @@ sitemaps: origin: https://www.adobe.com languages: default: - source: /metadata.json?sheet=sitemap-en + source: /express/templates/default/metadata.json?sheet=sitemap destination: /express/sitemap-seo-templates.xml hreflang: en alternate: /{path} brasil: - source: /metadata.json?sheet=sitemap-br + source: /br/express/templates/default/metadata.json?sheet=sitemap destination: /express/sitemap-seo-templates.xml hreflang: pt alternate: /br/{path} france: - source: /metadata.json?sheet=sitemap-fr + source: /fr/express/templates/default/metadata.json?sheet=sitemap destination: /express/sitemap-seo-templates.xml hreflang: fr alternate: /fr/{path} germany: - source: /metadata.json?sheet=sitemap-de + source: /de/express/templates/default/metadata.json?sheet=sitemap destination: /express/sitemap-seo-templates.xml hreflang: de alternate: /de/{path} italy: - source: /metadata.json?sheet=sitemap-it + source: /it/express/templates/default/metadata.json?sheet=sitemap destination: /express/sitemap-seo-templates.xml hreflang: it alternate: /it/{path} japan: - source: /metadata.json?sheet=sitemap-jp + source: /jp/express/templates/default/metadata.json?sheet=sitemap destination: /express/sitemap-seo-templates.xml hreflang: ja alternate: /jp/{path} korea: - source: /metadata.json?sheet=sitemap-kr + source: /kr/express/templates/default/metadata.json?sheet=sitemap destination: /express/sitemap-seo-templates.xml hreflang: ko alternate: /kr/{path} spain: - source: /metadata.json?sheet=sitemap-es + source: /es/express/templates/default/metadata.json?sheet=sitemap destination: /express/sitemap-seo-templates.xml hreflang: es alternate: /es/{path} taiwan: - source: /metadata.json?sheet=sitemap-tw + source: /tw/express/templates/default/metadata.json?sheet=sitemap destination: /express/sitemap-seo-templates.xml hreflang: zh-Hant alternate: /tw/{path} + netherlands: + source: /nl/express/templates/default/metadata.json?sheet=sitemap + destination: /express/sitemap-seo-templates.xml + hreflang: nl + alternate: /nl/{path} diff --git a/test/blocks/pricing-summary/mocks/body.html b/test/blocks/pricing-summary/mocks/body.html new file mode 100644 index 000000000..2bc3abd43 --- /dev/null +++ b/test/blocks/pricing-summary/mocks/body.html @@ -0,0 +1,130 @@ +
+
+
+

+ Find the Adobe Express plan that’s right for you +

+

+ Adobe Express is a free all-in-one app to grow your unique brand with + easy-to-use tools & creative inspiration. +

+
+
+
+
+

Free

+

+ free-icon +

+

+ Make standout social content, flyers, logos, banners, and more. Free use + forever. +

+

+ Learn more +

+

+ {{pricing}} +

+

Free forever. No credit card required.

+

+ Start for free +

+
+
+

Premium

+

+ premium-icon +

+

Access to all premium features and design assets.

+

30-day free trial.

+

+ {{pricing}} +

+

Billed monthly. Cancel anytime.

+

+ Start-30-day free trial +

+
+
+

Ultra Premium

+

+ creative-cloud +

+

Access to all ultra premium features and design assets.

+

30-day free trial.

+

+ {{pricing}} +

+

Billed monthly. Cancel anytime.

+

+ Start-30-day free trial +

+
+
+
+
+ *Billing begins when your free trial ends. Cancel before free trial ends + and you won’t be charged. Subscription automatically renews until you + cancel. Cancel anytime. +
+
+
diff --git a/test/blocks/pricing-summary/pricing-summary.test.js b/test/blocks/pricing-summary/pricing-summary.test.js new file mode 100644 index 000000000..c4ad90b98 --- /dev/null +++ b/test/blocks/pricing-summary/pricing-summary.test.js @@ -0,0 +1,71 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; + +const { default: decorate } = await import('../../../express/blocks/pricing-summary/pricing-summary.js'); +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); + +describe('Pricing Summary', () => { + before(() => { + window.isTestEnv = true; + }); + + it('Pricing Summary exists', () => { + const pricingSummary = document.querySelector('.pricing-summary'); + decorate(pricingSummary); + expect(pricingSummary).to.exist; + }); + + it('Columns to be 2 or 3', () => { + const columns = document.querySelectorAll('.pricing-column'); + expect(columns).to.exist; + expect(columns.length).to.be.oneOf([2, 3]); + }); + + it('Columns have correct elements', () => { + expect(document.querySelector('.pricing-header')).to.exist; + expect(document.querySelector('.pricing-description')).to.exist; + expect(document.querySelector('.pricing-plan')).to.exist; + expect(document.querySelector('.pricing-price')).to.exist; + }); + + it('Should have a free plan with correct details', () => { + const freePlan = document.querySelector('#free'); + expect(freePlan).to.exist; + + const freeIcon = document.querySelector('.icon-free-icon'); + expect(freeIcon).to.exist; + + const firstColumn = document.querySelectorAll('.pricing-column')[0]; + const freeDescription = firstColumn.querySelector('.pricing-description'); + expect(freeDescription.textContent).to.include('Free use'); + }); + + it('Should have a premium plan with correct details', () => { + const premiumPlan = document.querySelector('#premium'); + expect(premiumPlan).to.exist; + + const premiumIcon = document.querySelector('.icon-premium-icon'); + expect(premiumIcon).to.exist; + + const secondColumn = document.querySelectorAll('.pricing-column')[1]; + const premiumDescription = secondColumn.querySelector('.pricing-description'); + expect(premiumDescription.textContent).to.include('premium features'); + }); + + it('Should contain the correct billing notice', () => { + const billingNotice = document.querySelector('.pricing-summary > div:last-child div'); + expect(billingNotice.textContent.includes('Billing begins when your free trial ends')).to.be.true; + }); +}); diff --git a/test/unit/blocks/video-metadata/video-metadata.test.js b/test/unit/blocks/video-metadata/video-metadata.test.js index 60e89b992..a5ffcfc12 100644 --- a/test/unit/blocks/video-metadata/video-metadata.test.js +++ b/test/unit/blocks/video-metadata/video-metadata.test.js @@ -9,7 +9,6 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -/* eslint-env mocha */ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; diff --git a/tools/sidekick/config.json b/tools/sidekick/config.json index 1fe382134..143864e53 100644 --- a/tools/sidekick/config.json +++ b/tools/sidekick/config.json @@ -1,14 +1,12 @@ { "project": "Express", - "outerHost": "express-website--adobe.hlx.live", "host": "www.adobe.com", "byocdn": true, - "pushDownSelector": "#feds-header", "plugins": [ { "id": "metadata", "title": "Meta Data Inspector", - "url": "https://main--express-website--adobe.hlx.page/tools/metadata/inspector.html", + "url": "https://main--express--adobecom.hlx.page/tools/metadata/inspector.html", "environments": [ "edit" ], "excludePaths": [ "/**" ], "includePaths": [ "**metadata.xlsx**" ] @@ -16,7 +14,25 @@ { "id": "templates", "title": "Templates", - "url": "https://main--express-website--adobe.hlx.page/tools/templates/picker.html", + "url": "https://main--express--adobecom.hlx.page/tools/templates/picker.html", + "environments": [ "edit" ], + "excludePaths": [ "/**" ], + "includePaths": [ "**.docx**" ] + }, + { + "id": "library", + "title": "Library", + "environments": [ "edit" ], + "isPalette": true, + "paletteRect": "top: auto; bottom: 25px; left: 25px; height: 388px; width: 360px;", + "url": "https://main--express--adobecom.hlx.live/docs/library/", + "excludePaths": [ "/**" ], + "includePaths": [ "**.docx**" ] + }, + { + "id": "docs", + "title": "Docs", + "url": "https://main--express--adobecom.hlx.live/docs/", "environments": [ "edit" ], "excludePaths": [ "/**" ], "includePaths": [ "**.docx**" ] diff --git a/web-test-runner.config.mjs b/web-test-runner.config.mjs index 7201c6c94..c2b502137 100644 --- a/web-test-runner.config.mjs +++ b/web-test-runner.config.mjs @@ -17,7 +17,7 @@ function customReporter() { export default { coverageConfig: { include: [ - '**/express/blocks**', + '**/express/blocks/**', '**/express/scripts/**', ], exclude: [