From 302a0d59171733524497d258fd0ff4b1da32dfd2 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 26 Sep 2023 15:28:12 -0400 Subject: [PATCH 001/133] lots of little changes to test blocks for phase 1b --- arrow-right.svg | 3 ++ blocks/cards/cards.css | 1 + blocks/cards/icon.css | 12 +++-- blocks/contact-us/contact-us.css | 75 ++++++++++++++++++++++++++++++++ blocks/contact-us/contact-us.js | 7 +++ blocks/hero/default-hero.css | 33 +++++++++----- icons/arrow-right.svg | 3 ++ icons/health.svg | 8 ++++ icons/patient.svg | 10 +++++ icons/plant.svg | 15 +++++++ icons/right-arrow.svg | 10 ++--- scripts/scripts.js | 15 +++++-- styles/styles.css | 70 ++++++++++++++++++++++------- tools/sidekick/config.json | 19 ++++++++ 14 files changed, 241 insertions(+), 40 deletions(-) create mode 100644 arrow-right.svg create mode 100644 blocks/contact-us/contact-us.css create mode 100644 blocks/contact-us/contact-us.js create mode 100644 icons/arrow-right.svg create mode 100644 icons/health.svg create mode 100644 icons/patient.svg create mode 100644 icons/plant.svg create mode 100644 tools/sidekick/config.json diff --git a/arrow-right.svg b/arrow-right.svg new file mode 100644 index 00000000..131dddf5 --- /dev/null +++ b/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/blocks/cards/cards.css b/blocks/cards/cards.css index 076677ad..c1ee0ac2 100644 --- a/blocks/cards/cards.css +++ b/blocks/cards/cards.css @@ -10,6 +10,7 @@ gap: 1.75em; } + .cards.default.block > div { padding: 20px 16px; border-radius: 10px; diff --git a/blocks/cards/icon.css b/blocks/cards/icon.css index fdeed93b..ecb9b064 100644 --- a/blocks/cards/icon.css +++ b/blocks/cards/icon.css @@ -3,6 +3,10 @@ gap: 24px; } +.cards-wrapper { + max-width: var(--normal-page-width); +} + .cards.icon.block .row { display: grid; gap: 24px; @@ -14,8 +18,8 @@ padding: 20px 30px; flex-direction: column; background-color: white; - box-shadow: 0 4px 10px 0 rgba(0 0 0 / 20%); - border-radius: 10px; + text-align: center; + border-right: 1px solid grey; } .cards.icon.block .card span.icon { @@ -29,7 +33,9 @@ height: 100%; width: 100%; } - +.cards.icon.block .card:last-of-type { + border:0; +} .cards.icon.block .card h3 { margin-bottom: 1em; font-size: var(--heading-font-size-xs); diff --git a/blocks/contact-us/contact-us.css b/blocks/contact-us/contact-us.css new file mode 100644 index 00000000..ba9028ae --- /dev/null +++ b/blocks/contact-us/contact-us.css @@ -0,0 +1,75 @@ +/* Global overrides */ + +main > .section.contact-us-container { + padding: 0; +} + +main > .section.contact-us-container > .contact-us-wrapper { + padding: 0; + margin: 0; + max-width: unset; +} + +/* End overides */ + +.contact-us.block { + display: flex; + position: relative; + height: 370px; + align-items: center; + justify-content: center; + background: linear-gradient(to top, var(--bright-gray) 50%, white 0%); +} + +.contact-us.block .button-container { + padding: 20px 0 20px 0; +} + +.contact-us.block > div { + padding: 56px 16px; +} + +.contact-us.block p { + margin-bottom: 0; + font-weight: var(--font-weight-semibold); + text-align: center; + font-size: var(--body-font-size-l); + color: var(--white); +} + +.contact-us.block h3 { + color: var(--white); + font-size: var(--heading-font-size-l); +} +@media screen and (min-width: 600px) { + .contact-us.block { + height: 460px; + } + + .contact-us.block > div { + padding: 100px 16px + } + + .contact-us.block.top-gradient .gradient { + height: 80px; + } + + .contact-us.block .content { + max-width: 1000px; + text-align: center; + background-color: grey; + border-radius: 0 0 10px 10px; + padding: 20px ; + } + +} + +@media screen and (min-width: 900px) { + .contact-us.block { + height: 550px; + } + + .contact-us.block .content p { + font-size: var(--body-font-size-m); + } +} diff --git a/blocks/contact-us/contact-us.js b/blocks/contact-us/contact-us.js new file mode 100644 index 00000000..6804802c --- /dev/null +++ b/blocks/contact-us/contact-us.js @@ -0,0 +1,7 @@ +export default async function decorate(block) { + const bottomRow = document.createElement('div'); + bottomRow.classList.add('bottom-row'); + block.parentNode.append(bottomRow); + block.children[0].children[0].classList.add('content'); + block.children[0].classList.add('top-row'); +} diff --git a/blocks/hero/default-hero.css b/blocks/hero/default-hero.css index 20b5ede9..3ec09e79 100644 --- a/blocks/hero/default-hero.css +++ b/blocks/hero/default-hero.css @@ -1,6 +1,10 @@ .hero.block.default h1 { - color: var(--white); + color: var(--black-olive); +} +.hero.block.default h1 em { + color: var(--red); + font-style: normal; } .hero.block.default .image { @@ -18,7 +22,6 @@ position: relative; height: 100%; width: 100%; - clip-path: polygon(0 0, 100% 0, 100% calc(100% - 10vw), 0 100%); } .hero.block.default .image picture img { @@ -38,8 +41,21 @@ margin: 0 20px; } +.hero.block.default .content-wrapper .button-container { + text-align: left; +} + +.hero.block.default .content-wrapper .button-container span.icon { + position: relative; + top: 10px; + left: 10px; +} + .hero.block.default .content-wrapper .content { max-width: 75%; + background-color: var(--white); + border-left:10px solid var(--dark-red); + padding: 20px 20px; } .hero.block.default .content-wrapper.no-image .content { @@ -72,11 +88,10 @@ color: var(--quartz); } - .hero.block.default .content-wrapper .content p { font-size: var(--body-font-size-s); line-height: var(--line-height-s); - color: var(--white); + color: var(--black-olive); } .hero.block.default .content-wrapper.no-image .content p { @@ -86,7 +101,6 @@ color: var(--quartz); } - .hero.block.default.light-image .content-wrapper .content p { color: var(--quartz); } @@ -96,11 +110,6 @@ max-width: 550px; margin: 0 auto; } - - .hero.block.default .image picture { - clip-path: polygon(0 0, 100% 0, 100% calc(100% - 5vw), 0 100%); - } - } @media screen and (min-width: 900px) { @@ -126,6 +135,10 @@ font-size: var(--body-font-size-xxl); } + .hero.block.default .content-wrapper .content p:nth-of-type(1) { + display:none; + } + .hero.block.default .content-wrapper.no-image .content p { font-size: var(--body-font-size-l); } diff --git a/icons/arrow-right.svg b/icons/arrow-right.svg new file mode 100644 index 00000000..12fca75f --- /dev/null +++ b/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/health.svg b/icons/health.svg new file mode 100644 index 00000000..98abd5e9 --- /dev/null +++ b/icons/health.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/icons/patient.svg b/icons/patient.svg new file mode 100644 index 00000000..5872c5f6 --- /dev/null +++ b/icons/patient.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/icons/plant.svg b/icons/plant.svg new file mode 100644 index 00000000..26d748b8 --- /dev/null +++ b/icons/plant.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/icons/right-arrow.svg b/icons/right-arrow.svg index 6ecce36d..09e4a3a3 100644 --- a/icons/right-arrow.svg +++ b/icons/right-arrow.svg @@ -1,7 +1,3 @@ - - - arrow_icon - - - - \ No newline at end of file + + + diff --git a/scripts/scripts.js b/scripts/scripts.js index 4381f87d..c666a10d 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -29,7 +29,6 @@ const LCP_BLOCKS = ['hero']; // add your LCP blocks to the list function decorateSectionGradientTopper(main) { const section = main.querySelector('.section.inverted-gradient-background'); const hasInvertedGradient = section !== null; - if (!hasInvertedGradient) return; const hero = main.querySelector('& > .section.hero-container'); @@ -132,9 +131,10 @@ function buildSectionBackgroundImage(main) { const keys = Object.keys(readBlockConfig(metadata)); const bgIdx = keys.indexOf(keys.find((k) => k.match(/background-image/i))); if (bgIdx >= 0) { + const picture = metadata.children[bgIdx].children[1]; picture.querySelector('picture').classList.add('section-bg-image'); - metadata.parentElement.append(picture.cloneNode(true)); + metadata.parentElement.prepend(picture.cloneNode(true)); } }); } @@ -176,7 +176,6 @@ export function buildLayoutContainers(main) { container.append(...section.children); if (title) section.prepend(title); section.append(container); - section.querySelectorAll('.separator-wrapper').forEach((sep) => { sep.innerHTML = '
'; }); @@ -190,10 +189,17 @@ export function buildLayoutContainers(main) { function decorateSectionBackgroundImage(main) { main.querySelectorAll(':scope div > picture.section-bg-image').forEach((picture) => { const wrapper = picture.parentElement; - wrapper.classList.add('section-bg-image-wrapper'); + wrapper.classList.add('bg-image-wrapper'); wrapper.parentElement.replaceWith(wrapper); }); } +function decorateSectionButtonRow(main) { + main.querySelectorAll(':scope div > .default-content-wrapper > p.button-container').forEach((buttonContainer) => { + console.log(buttonContainer); + const wrapper = buttonContainer.parentElement; + wrapper.classList.add('button-wrapper'); + }); +} /** * Decorates the main element. @@ -209,6 +215,7 @@ export function decorateMain(main) { fixDefaultImage(main); decorateBlocks(main); buildLayoutContainers(main); + decorateSectionButtonRow(main); decorateSectionBackgroundImage(main); decorateSectionGradientTopper(main); } diff --git a/styles/styles.css b/styles/styles.css index c0060efc..96b67148 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -53,10 +53,12 @@ --bright-gray: #efefef; --medium-gray: #ddd; --gray: #979797; + --gray-cool-80: #34373F; --gray-box-shadow: rgb(0 0 0 / 20%); --silver: #d8d8d8; --quartz: #4c4948; --red: #e10; + --dark-red: #BD120A; --white: #fff; --blue: #003087; --black-olive: #3a3a3a; @@ -223,18 +225,18 @@ span { p.button-container a { display: inline-block; - padding: 8px 20px; + padding: 10px 24px; font-weight: bold; line-height: var(--line-height-l); text-decoration: none; - color: var(--black); - background-color: var(--white); - border-radius: 4px; - box-shadow: 3px 3px 7px 1px rgba(0 0 0 / 20%); + color: var(--white); + background-color: var(--dark-red); + border-bottom-left-radius: 20px; + } p.button-container a:visited { - color: var(--black); + /* color: var(--black);*/ text-decoration: none; } @@ -260,8 +262,35 @@ body.gray-gradient > main { } main .section { - padding: 32px 0; - background-color: var(--white); + padding: 12px 0; +} + +main .section.content-bump-out .default-content-wrapper { + border:0; + max-width: 1200px; + margin: -80px auto 0 ; + display: table; + width: 100%; + text-align: center; + background-color: #fff; + padding: 20px 50px; + position: relative; + max-width: 1200px; +} + +main .section .default-content-wrapper.button-wrapper { + margin: 0 auto; + border:4px dashed green; + max-width: 1200px; + text-align: right; +} +main .section.default-content-wrapper .button-container { + text-align: right; + max-width: 1200px; +} +main .section.default-content-wrapper { + text-align: right; + max-width: 1200px; } main .section.gray-background { @@ -290,7 +319,7 @@ main .section.red-title h2 { main .section[data-background-image] { position: relative; - padding-bottom: 200px; + /*padding-bottom: 200px;*/ } main .section.center .default-content-wrapper { @@ -309,10 +338,12 @@ main .section.image-boxshadow .default-content-wrapper > p.image { padding: 32px; background-color: #fff; border-radius: 10px; - box-shadow: 0 4px 10px 0 rgba(0 0 0 / 20%) } - +main .section.content-bump-out .default-content-wrapper p:first-of-type { + color: red; + margin-top: 20px; +} main .section .default-content-wrapper > p > picture > img { position: absolute; top: 0; @@ -323,6 +354,9 @@ main .section .default-content-wrapper > p > picture > img { object-position: center center; } +main .section .bg-image-wrapper { + text-align: center; +} main > .section[data-background-image] > .section-bg-image-wrapper { position: absolute; margin: 0; @@ -351,9 +385,12 @@ main > .section[data-background-image] > .section-bg-image-wrapper picture img { } main .section .default-content-wrapper span.icon { + position: relative; + top: 8px; + left: 10px; display: inline-block; - height: 30px; - width: 30px; + height: 25px; + width: 25px; } main .section .default-content-wrapper span.icon svg { @@ -395,11 +432,12 @@ main .section[data-layout="50/50"] .layout-content-wrapper > div.separator-wrapp main > .section > div { padding: 0 16px; margin: 0 auto; - max-width: var(--normal-page-width); + /* max-width: var(--normal-page-width); */ } main > .section.full-width > div { margin: 0; + text-align: center; max-width: var(--full-page-width); } @@ -586,7 +624,7 @@ main .section.inverted-gradient-background { } main > .section[data-background-image] { - padding-bottom: 300px; + /* padding-bottom: 300px; */ } @@ -659,7 +697,7 @@ main .section.inverted-gradient-background { } main .section > div { - padding: 0 40px; + /*padding: 0 40px;*/ } main .section.angled-inverted-background.hero-container .hero { diff --git a/tools/sidekick/config.json b/tools/sidekick/config.json new file mode 100644 index 00000000..b09d3339 --- /dev/null +++ b/tools/sidekick/config.json @@ -0,0 +1,19 @@ +{ + "project": "Takeda", + "host": "main--takeda-ihs--hlxsites.hlx.page", + "plugins": [ + { + "id": "asset-library", + "title": "AEM Assets Library", + "environments": [ + "edit" + ], + "url": "https://experience.adobe.com/solutions/CQ-assets-selectors/static-assets/resources/franklin/asset-selector.html", + "isPalette": true, + "includePaths": [ "**.docx**" ], + "paletteRect": "top: 50px; bottom: 10px; right: 10px; left: auto; width:400px; height: calc(100vh - 60px)" + } + ] +} + + From d8e02659af75a6816746c2abcf6e20c302d88a0a Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 26 Sep 2023 17:48:02 -0400 Subject: [PATCH 002/133] expanding hero --- blocks/hero/default-hero.css | 9 ++++++++- styles/styles.css | 7 ++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/blocks/hero/default-hero.css b/blocks/hero/default-hero.css index 3ec09e79..e56eee97 100644 --- a/blocks/hero/default-hero.css +++ b/blocks/hero/default-hero.css @@ -55,7 +55,7 @@ max-width: 75%; background-color: var(--white); border-left:10px solid var(--dark-red); - padding: 20px 20px; + padding: 20px; } .hero.block.default .content-wrapper.no-image .content { @@ -157,3 +157,10 @@ max-width: var(--normal-page-width); } } + +@media screen and (min-width: 1600px) { + .hero.block.default .content-wrapper { + padding: 120px 40px; + max-width: var(--normal-page-width); + } +} diff --git a/styles/styles.css b/styles/styles.css index 96b67148..104711ea 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -271,7 +271,12 @@ main .section.content-bump-out .default-content-wrapper { margin: -80px auto 0 ; display: table; width: 100%; - text-align: center; + text-align: center@media screen and (min-width: 1600px) { + .hero.block.default .content-wrapper { + padding: 150px 40px; + max-width: var( --new-page-width); + } +}; background-color: #fff; padding: 20px 50px; position: relative; From 2b0c697210a91664da3dc684ba3927aa4d024c36 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 26 Sep 2023 18:51:38 -0400 Subject: [PATCH 003/133] fixing icons --- blocks/cards/icon.css | 4 ++-- styles/styles.css | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/blocks/cards/icon.css b/blocks/cards/icon.css index ecb9b064..8d9f7f79 100644 --- a/blocks/cards/icon.css +++ b/blocks/cards/icon.css @@ -24,8 +24,8 @@ .cards.icon.block .card span.icon { display: inline-block; - height: 43px; - width: 43px; + height: 120px; + width: 200px; } .cards.icon.block .card span.icon svg { diff --git a/styles/styles.css b/styles/styles.css index 104711ea..6779e522 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -271,21 +271,14 @@ main .section.content-bump-out .default-content-wrapper { margin: -80px auto 0 ; display: table; width: 100%; - text-align: center@media screen and (min-width: 1600px) { - .hero.block.default .content-wrapper { - padding: 150px 40px; - max-width: var( --new-page-width); - } -}; + text-align: center; background-color: #fff; padding: 20px 50px; position: relative; - max-width: 1200px; } main .section .default-content-wrapper.button-wrapper { margin: 0 auto; - border:4px dashed green; max-width: 1200px; text-align: right; } From 75729605d3a223e1549e60d3b5bd2288d951b495 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 27 Sep 2023 13:13:27 -0400 Subject: [PATCH 004/133] Updating sections and cards --- blocks/cards/cta.css | 9 +++++++++ blocks/cards/cta.js | 7 +++++++ blocks/contact-us/contact-us.css | 8 ++++++-- blocks/hero/default-hero.css | 2 +- scripts/scripts.js | 2 +- styles/styles.css | 26 +++++++++++++++++++------- 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/blocks/cards/cta.css b/blocks/cards/cta.css index db30c4e7..63c05331 100644 --- a/blocks/cards/cta.css +++ b/blocks/cards/cta.css @@ -27,6 +27,15 @@ border-color: var(--hello-programs); } +.cards.cta.block button.primary.top-link { + display: inline-block; + padding: 0; + font-weight: bold; + line-height: var(--line-height-l); + text-decoration: none; + border: 0; +} + .cards.cta.block > ul > li div p.image { width: 230px; margin: 0 auto; diff --git a/blocks/cards/cta.js b/blocks/cards/cta.js index 33952d78..1b43533a 100644 --- a/blocks/cards/cta.js +++ b/blocks/cards/cta.js @@ -6,6 +6,13 @@ import { decorateIcons } from '../../scripts/lib-franklin.js'; */ export default async function decorate(block) { const cards = [...block.children]; + const ButtonContainer = block.querySelector('p.button-container'); + ButtonContainer.classList.remove('button-container'); + const anchor = block.querySelector('a'); + + anchor.classList.remove('button', 'primary'); + anchor.classList.add('top-link'); + console.log(anchor); const ul = document.createElement('ul'); ul.classList.add(`cards-${cards.length}`); cards.forEach((card) => { diff --git a/blocks/contact-us/contact-us.css b/blocks/contact-us/contact-us.css index ba9028ae..849c2968 100644 --- a/blocks/contact-us/contact-us.css +++ b/blocks/contact-us/contact-us.css @@ -29,6 +29,10 @@ main > .section.contact-us-container > .contact-us-wrapper { padding: 56px 16px; } +.contact-us.block .top-row .content { + padding: 40px 60px; +} + .contact-us.block p { margin-bottom: 0; font-weight: var(--font-weight-semibold); @@ -57,9 +61,9 @@ main > .section.contact-us-container > .contact-us-wrapper { .contact-us.block .content { max-width: 1000px; text-align: center; - background-color: grey; + background-color: var(--gray-neutral-80); border-radius: 0 0 10px 10px; - padding: 20px ; + padding: 40px; } } diff --git a/blocks/hero/default-hero.css b/blocks/hero/default-hero.css index e56eee97..397af1a8 100644 --- a/blocks/hero/default-hero.css +++ b/blocks/hero/default-hero.css @@ -160,7 +160,7 @@ @media screen and (min-width: 1600px) { .hero.block.default .content-wrapper { - padding: 120px 40px; + padding: 160px 0; max-width: var(--normal-page-width); } } diff --git a/scripts/scripts.js b/scripts/scripts.js index c666a10d..6306d791 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -189,7 +189,7 @@ export function buildLayoutContainers(main) { function decorateSectionBackgroundImage(main) { main.querySelectorAll(':scope div > picture.section-bg-image').forEach((picture) => { const wrapper = picture.parentElement; - wrapper.classList.add('bg-image-wrapper'); + wrapper.classList.add('section-bg-image-wrapper'); wrapper.parentElement.replaceWith(wrapper); }); } diff --git a/styles/styles.css b/styles/styles.css index 6779e522..ae27337f 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -53,7 +53,9 @@ --bright-gray: #efefef; --medium-gray: #ddd; --gray: #979797; - --gray-cool-80: #34373F; + --gray-neutral-70: #454545; + --gray-neutral-80: #333; + --gray-neutral-90: #1e1e23; --gray-box-shadow: rgb(0 0 0 / 20%); --silver: #d8d8d8; --quartz: #4c4948; @@ -108,7 +110,7 @@ --heading-font-size-xs: 20px; --heading-font-size-s: 24px; --heading-font-size-m: 26px; - --heading-font-size-l: 30px; + --heading-font-size-l: 32px; --heading-font-size-xl: 42px; /* Nav Height */ @@ -172,7 +174,7 @@ h1 { h2 { margin-bottom: 12px; - font-size: var(--heading-font-size-s); + font-size: var(--heading-font-size-l); line-height: var(--line-height-s); } @@ -268,24 +270,30 @@ main .section { main .section.content-bump-out .default-content-wrapper { border:0; max-width: 1200px; - margin: -80px auto 0 ; + margin: -60px auto 0 ; display: table; width: 100%; - text-align: center; + text-align: left; background-color: #fff; padding: 20px 50px; position: relative; } +main .section.content-bump-out.content-center .default-content-wrapper { + text-align: center; +} + main .section .default-content-wrapper.button-wrapper { margin: 0 auto; max-width: 1200px; text-align: right; } + main .section.default-content-wrapper .button-container { text-align: right; max-width: 1200px; } + main .section.default-content-wrapper { text-align: right; max-width: 1200px; @@ -356,7 +364,6 @@ main .section .bg-image-wrapper { text-align: center; } main > .section[data-background-image] > .section-bg-image-wrapper { - position: absolute; margin: 0; padding: 0; bottom: 0; @@ -374,7 +381,6 @@ main > .section[data-background-image] > .section-bg-image-wrapper picture { } main > .section[data-background-image] > .section-bg-image-wrapper picture img { - position: absolute; bottom: 0; left: 0; width: 100%; @@ -598,6 +604,12 @@ main .section.inverted-gradient-background { main > .section[data-background-image] > .section-bg-image-wrapper > picture > img { object-position: unset; } + main > .section > .default-content-wrapper > picture > img { + object-position: unset; + width: 100%; + height: 100%; + object-fit: cover; + } main .section.angled-inverted-background.hero-container .hero { clip-path: polygon(0 0, 100% 0, 100% calc(100% - 10vw), 0 100%); From 931b6271aa0388e061f726d433b4ff8d65b42c2b Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 18 Oct 2023 12:09:52 -0400 Subject: [PATCH 005/133] latest changes adding toc, and changing card styles --- blocks/cards/cta.css | 50 +++++++++++++++++++++++++++----- blocks/cards/cta.js | 16 ++++++---- blocks/cards/icon.css | 9 ++++++ blocks/contact-us/contact-us.css | 4 +++ blocks/hero/default-hero.css | 1 + blocks/hero/hero.js | 1 - blocks/toc/toc.css | 24 +++++++++++++++ blocks/toc/toc.js | 20 +++++++++++++ icons/open-window.svg | 3 ++ styles/styles.css | 10 ++++--- 10 files changed, 120 insertions(+), 18 deletions(-) create mode 100644 blocks/toc/toc.css create mode 100644 blocks/toc/toc.js create mode 100644 icons/open-window.svg diff --git a/blocks/cards/cta.css b/blocks/cards/cta.css index 63c05331..054314d1 100644 --- a/blocks/cards/cta.css +++ b/blocks/cards/cta.css @@ -11,20 +11,20 @@ } .cards.cta.block > ul > li div { - padding-top: 30px; + padding-top: 0; border-top: 3px solid white; } .cards.cta.block > ul > li.onepath div { - border-color: var(--onepath); + /* border-color: var(--onepath); */ } .cards.cta.block > ul > li.myigsource div { - border-color: var(--myigsource); + /* border-color: var(--myigsource); */ } .cards.cta.block > ul > li.hello-programs div { - border-color: var(--hello-programs); + /* border-color: var(--hello-programs); */ } .cards.cta.block button.primary.top-link { @@ -36,6 +36,25 @@ border: 0; } +.cards.cta.block .card-body .top-section { + position: relative; + margin-left:12px; +} + +.cards.cta.block .card-body p.link-container span.icon svg { + position: relative; + height: 25px; + width: 25px; +} + +.cards.cta.block .card-body p.link-container span.icon { + display: block; + width: 100%; + position: absolute; + left: 96%; + top: -5px; +} + .cards.cta.block > ul > li div p.image { width: 230px; margin: 0 auto; @@ -60,17 +79,22 @@ .cards.cta.block > ul > li h3 { font-size: var(--heading-font-size-xs); - text-align: center; + text-align: left; color: var(--gray); - margin: 12px auto; + margin: 12px 12px; } .cards.cta.block > ul > li p { text-align: center; } +.cards.cta.block > ul > li p.link-container { + text-align: left; +} + .cards.cta.block > ul > li p.button-container { - margin: 18px auto; + margin: 12px 12px; + text-align: left; } @media screen and (min-width: 900px) { @@ -96,6 +120,18 @@ .cards.cta.block > ul > li div p.image { width: 100%; } + + .cards.cta.block .card-body { + margin-right:20px; + } + + .cards.cta.block .card-body p.link-container span.icon { + display: block; + width: 100%; + position: absolute; + left: 96%; + top: -5px; + } } @media screen and (min-width: 1200px) { diff --git a/blocks/cards/cta.js b/blocks/cards/cta.js index 1b43533a..b0a8d1ce 100644 --- a/blocks/cards/cta.js +++ b/blocks/cards/cta.js @@ -6,23 +6,27 @@ import { decorateIcons } from '../../scripts/lib-franklin.js'; */ export default async function decorate(block) { const cards = [...block.children]; - const ButtonContainer = block.querySelector('p.button-container'); - ButtonContainer.classList.remove('button-container'); const anchor = block.querySelector('a'); - anchor.classList.remove('button', 'primary'); anchor.classList.add('top-link'); - console.log(anchor); const ul = document.createElement('ul'); ul.classList.add(`cards-${cards.length}`); cards.forEach((card) => { + const topSection = document.createElement('div'); + topSection.classList.add('top-section'); + const cardBody = (card.children[1]); + cardBody.classList.add('card-body'); + const ButtonContainer = card.querySelector('p.button-container'); + ButtonContainer.classList.remove('button-container'); + ButtonContainer.classList.add('link-container'); + cardBody.prepend(topSection); + const topLink = card.querySelector('p.link-container'); + topSection.append(topLink); const li = document.createElement('li'); li.classList.add('cta', card.children[0].textContent); li.append(card.children[1]); - const picture = li.querySelector('picture'); picture.closest('p').classList.add('image'); - const img = picture.querySelector('img'); const ratio = (parseInt(img.height, 10) / parseInt(img.width, 10)) * 100; picture.style.paddingBottom = `${ratio}%`; diff --git a/blocks/cards/icon.css b/blocks/cards/icon.css index 8d9f7f79..9b38baf3 100644 --- a/blocks/cards/icon.css +++ b/blocks/cards/icon.css @@ -28,6 +28,15 @@ width: 200px; } +.cards.icon.block .card .button-container span.icon { + position: relative; + top: 8px; + left: 10px; + display: inline-block; + height: 25px; + width: 25px; +} + .cards.icon.block .card span.icon svg { position: relative; height: 100%; diff --git a/blocks/contact-us/contact-us.css b/blocks/contact-us/contact-us.css index 849c2968..04e83e91 100644 --- a/blocks/contact-us/contact-us.css +++ b/blocks/contact-us/contact-us.css @@ -31,6 +31,10 @@ main > .section.contact-us-container > .contact-us-wrapper { .contact-us.block .top-row .content { padding: 40px 60px; + max-width: 1000px; + text-align: center; + background-color: var(--gray-neutral-80); + border-radius: 0 0 10px 10px; } .contact-us.block p { diff --git a/blocks/hero/default-hero.css b/blocks/hero/default-hero.css index 397af1a8..7b608e5d 100644 --- a/blocks/hero/default-hero.css +++ b/blocks/hero/default-hero.css @@ -56,6 +56,7 @@ background-color: var(--white); border-left:10px solid var(--dark-red); padding: 20px; + margin-bottom: 60px; } .hero.block.default .content-wrapper.no-image .content { diff --git a/blocks/hero/hero.js b/blocks/hero/hero.js index 30f1b949..e453121f 100644 --- a/blocks/hero/hero.js +++ b/blocks/hero/hero.js @@ -24,7 +24,6 @@ function buildProductContent(block) { const keys = Object.keys(config); const contentDiv = document.createElement('div'); contentDiv.classList.add('content'); - ['logo', 'title', 'references', 'link'].forEach((part) => { const idx = keys.indexOf(part); if (idx >= 0) { diff --git a/blocks/toc/toc.css b/blocks/toc/toc.css new file mode 100644 index 00000000..d4bcae36 --- /dev/null +++ b/blocks/toc/toc.css @@ -0,0 +1,24 @@ + +.toc ul { + display: flex; + flex-wrap: wrap; + justify-content: center; + padding-left: 0; + width: 100%; + background-color: var(--gray-neutral-80); +} + +.toc ul li { + padding: 5px 1rem; +} + +.toc ul li a { + display: flex; + align-items: center; + justify-content: center; + column-gap: 5px; + font-size: var(--body-font-size-s); + font-weight: var(--font-weight-normal); + color: var(--white); + text-decoration: none; +} diff --git a/blocks/toc/toc.js b/blocks/toc/toc.js new file mode 100644 index 00000000..a0861258 --- /dev/null +++ b/blocks/toc/toc.js @@ -0,0 +1,20 @@ + +/** + * get the name of the section and set as the id of + * the section so there is a way to anchor from toc nav to section + */ +export default async function decorate(block){ + document.querySelectorAll('.section[data-toc]').forEach((jumpTo) => { + const jumpId = jumpTo.getAttribute('data-toc'); + jumpTo.setAttribute('id', jumpId); + }); + + block.querySelectorAll('.toc ul > li > a').forEach((anchor) => { + const titleName = anchor.title; + anchor.href = `#${titleName.toLowerCase()}`; + }); + + const main = document.querySelector('main'); + const toc = document.querySelector('.toc'); + main.prepend(toc); +} diff --git a/icons/open-window.svg b/icons/open-window.svg new file mode 100644 index 00000000..3849694c --- /dev/null +++ b/icons/open-window.svg @@ -0,0 +1,3 @@ + + + diff --git a/styles/styles.css b/styles/styles.css index ae27337f..9bff5461 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -205,7 +205,7 @@ ul, li { } p { - margin-bottom: 1.5em; + margin-bottom: 1.2em; } p.reference { @@ -227,7 +227,7 @@ span { p.button-container a { display: inline-block; - padding: 10px 24px; + padding: 10px 42px; font-weight: bold; line-height: var(--line-height-l); text-decoration: none; @@ -275,12 +275,13 @@ main .section.content-bump-out .default-content-wrapper { width: 100%; text-align: left; background-color: #fff; - padding: 20px 50px; + padding: 12px 50px; position: relative; } main .section.content-bump-out.content-center .default-content-wrapper { text-align: center; + border-radius: 40px; } main .section .default-content-wrapper.button-wrapper { @@ -348,7 +349,7 @@ main .section.image-boxshadow .default-content-wrapper > p.image { main .section.content-bump-out .default-content-wrapper p:first-of-type { color: red; - margin-top: 20px; + margin-top: 16px; } main .section .default-content-wrapper > p > picture > img { position: absolute; @@ -616,6 +617,7 @@ main .section.inverted-gradient-background { } } + @media screen and (min-width: 900px) { h1 { font-size: var(--heading-font-size-xl); From 8e9c204fd64206054d50ba2260d904f40eaf2a67 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 18 Oct 2023 16:09:11 -0400 Subject: [PATCH 006/133] form demo set up --- blocks/form/form.css | 130 ++++++++++++++++++++++++++++ blocks/form/form.js | 197 +++++++++++++++++++++++++++++++++++++++++++ utils/helpers.js | 176 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 503 insertions(+) create mode 100644 blocks/form/form.css create mode 100644 blocks/form/form.js create mode 100644 utils/helpers.js diff --git a/blocks/form/form.css b/blocks/form/form.css new file mode 100644 index 00000000..1602ac63 --- /dev/null +++ b/blocks/form/form.css @@ -0,0 +1,130 @@ +main .form { + background-color: var(--color-white); + padding: var(--spacing-s); + border-radius: var(--card-border-radius-l); + filter: var(--image-filter-drop-shadow-small); + border: solid 1px var(--bg-color-grey); +} + +main .form h2 { + padding: var(--spacing-s) 0; + font-size: var(--type-heading-l-lh); +} + +main .form h3 { + padding-top: var(--spacing-s); + font-size: var(--type-heading-m-size); +} + +main .form input, +main .form textarea, +main .form select { + border: solid 1px var(--bg-color-grey); + padding: var(--spacing-xxs) var(--spacing-xs); + width: 100%; + max-width: 50rem; + box-sizing: border-box; + border-radius: var(--input-border-radius); + font-size: var(--type-body-s-size); + line-height: var(--type-body-s-lh); + font-family: var(--body-font-family); +} + +main .form textarea { + min-height: 100px; +} + +main .form input:hover, +main .form select:hover { + border-color: var(--color-font-grey); +} + +main .form label { + display: block; + padding-bottom: var(--spacing-xxs); + box-sizing: border-box; + font-size: var(--type-body-s-size); + line-height: var(--type-body-s-lh); +} + +main .form label.required::after { + content: "*"; + color: var(--color-black); + padding-left: var(--spacing-xxxs); +} + +main .form .field-wrapper { + margin-bottom: var(--spacing-m); +} + +main .form .form-checkbox-wrapper { + display: flex; + align-items: center; + margin: var(--spacing-xs) 0; +} + +main .form .form-checkbox-wrapper input[type='checkbox'] { + appearance: none; + + /* stylelint-disable */ + -webkit-appearance: none; + -moz-appearance: none; + + /* stylelint-enable */ + height: 20px; + width: 20px; + position: relative; + border: unset; + padding: unset; + margin-right: var(--spacing-xxs); + margin-left: 0; +} + +main .form .form-checkbox-wrapper input[type='checkbox']::after { + display: block; + position: absolute; + top: 0; + left: 0; + height: 20px; + width: 20px; + content: ' '; + background: url('./checkbox.svg'); + background-size: contain; +} + +main .form .form-checkbox-wrapper input[type='checkbox']:checked::after { + background: url('./checkbox-checked.svg'); + background-size: contain; + content: ' '; +} + +main .form .form-checkbox-wrapper label { + display: block; + font-size: var(--type-body-xxs-size); + line-height: var(--type-body-xxs-lh); + padding-bottom: 0; +} + +main .form-legal-wrapper p { + font-size: var(--type-body-xxs-size); + line-height: var(--type-body-xxs-lh); + font-style: italic; +} + +main .form button { + font-family: var(--body-font-family); +} + +@media screen and (min-width: 900px) { + main .form { + padding: var(--spacing-ml); + } + + main .form .field-wrapper { + display: flex; + } + + main .form label { + width: 72%; + } +} diff --git a/blocks/form/form.js b/blocks/form/form.js new file mode 100644 index 00000000..7dcb3185 --- /dev/null +++ b/blocks/form/form.js @@ -0,0 +1,197 @@ + +function createSelect(fd) { + const select = document.createElement('select'); + select.id = fd.Field; + if (fd.Placeholder) { + const ph = document.createElement('option'); + ph.textContent = fd.Placeholder; + ph.setAttribute('selected', ''); + ph.setAttribute('disabled', ''); + select.append(ph); + } + fd.Options.split(',').forEach((o) => { + const option = document.createElement('option'); + option.textContent = o.trim(); + option.value = o.trim(); + select.append(option); + }); + if (fd.Mandatory === 'x') { + select.setAttribute('required', 'required'); + } + return select; +} +console.log('inform'); +function constructPayload(form) { + const payload = {}; + [...form.elements].forEach((fe) => { + if (fe.type === 'checkbox') { + if (fe.checked) payload[fe.id] = fe.value; + } else if (fe.id) { + payload[fe.id] = fe.value; + } + }); + return payload; +} + +async function submitForm(form) { + const payload = constructPayload(form); + payload.timestamp = new Date().toJSON(); + const resp = await fetch(form.dataset.action, { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ data: payload }), + }); + await resp.text(); + return payload; +} + +function createButton(fd) { + const button = document.createElement('button'); + button.textContent = fd.Label; + button.classList.add('button'); + if (fd.Type === 'submit') { + button.addEventListener('click', async (event) => { + const form = button.closest('form'); + if (fd.Placeholder) form.dataset.action = fd.Placeholder; + if (form.checkValidity()) { + event.preventDefault(); + button.setAttribute('disabled', ''); + await submitForm(form); + const redirectTo = fd.Extra; + window.location.href = redirectTo; + } + }); + } + return button; +} + +function createHeading(fd, el) { + const heading = document.createElement(el); + heading.textContent = fd.Label; + return heading; +} + +function createInput(fd) { + const input = document.createElement('input'); + input.type = fd.Type; + input.id = fd.Field; + input.setAttribute('placeholder', fd.Placeholder); + if (fd.Mandatory === 'x') { + input.setAttribute('required', 'required'); + } + return input; +} + +function createTextArea(fd) { + const input = document.createElement('textarea'); + input.id = fd.Field; + input.setAttribute('placeholder', fd.Placeholder); + if (fd.Mandatory === 'x') { + input.setAttribute('required', 'required'); + } + return input; +} + +function createLabel(fd) { + const label = document.createElement('label'); + label.setAttribute('for', fd.Field); + label.textContent = fd.Label; + if (fd.Mandatory === 'x') { + label.classList.add('required'); + } + return label; +} + +function applyRules(form, rules) { + const payload = constructPayload(form); + rules.forEach((field) => { + const { type, condition: { key, operator, value } } = field.rule; + if (type === 'visible') { + if (operator === 'eq') { + if (payload[key] === value) { + form.querySelector(`.${field.fieldId}`).classList.remove('hidden'); + } else { + form.querySelector(`.${field.fieldId}`).classList.add('hidden'); + } + } + } + }); +} + +function fill(form) { + const { action } = form.dataset; + if (action === '/tools/bot/register-form') { + const loc = new URL(window.location.href); + form.querySelector('#owner').value = loc.searchParams.get('owner') || ''; + form.querySelector('#installationId').value = loc.searchParams.get('id') || ''; + } +} + +async function createForm(formURL) { + const { pathname } = new URL(formURL); + const resp = await fetch(pathname); + const json = await resp.json(); + const form = document.createElement('form'); + const rules = []; + // eslint-disable-next-line prefer-destructuring + form.dataset.action = pathname.split('.json')[0]; + json.data.forEach((fd) => { + fd.Type = fd.Type || 'text'; + const fieldWrapper = document.createElement('div'); + const style = fd.Style ? ` form-${fd.Style}` : ''; + const fieldId = `form-${fd.Type}-wrapper${style}`; + fieldWrapper.className = fieldId; + fieldWrapper.classList.add('field-wrapper'); + switch (fd.Type) { + case 'select': + fieldWrapper.append(createLabel(fd)); + fieldWrapper.append(createSelect(fd)); + break; + case 'heading': + fieldWrapper.append(createHeading(fd, 'h3')); + break; + case 'legal': + fieldWrapper.append(createHeading(fd, 'p')); + break; + case 'checkbox': + fieldWrapper.append(createInput(fd)); + fieldWrapper.append(createLabel(fd)); + break; + case 'text-area': + fieldWrapper.append(createLabel(fd)); + fieldWrapper.append(createTextArea(fd)); + break; + case 'submit': + fieldWrapper.append(createButton(fd)); + break; + default: + fieldWrapper.append(createLabel(fd)); + fieldWrapper.append(createInput(fd)); + } + + if (fd.Rules) { + try { + rules.push({ fieldId, rule: JSON.parse(fd.Rules) }); + } catch (e) { + // eslint-disable-next-line no-console + console.warn(`Invalid Rule ${fd.Rules}: ${e}`); + } + } + form.append(fieldWrapper); + }); + + form.addEventListener('change', () => applyRules(form, rules)); + applyRules(form, rules); + fill(form); + return (form); +} + +export default async function decorate(block) { + const form = block.querySelector('a[href$=".json"]'); + if (form) { + form.replaceWith(await createForm(form.href)); + } +} diff --git a/utils/helpers.js b/utils/helpers.js new file mode 100644 index 00000000..9bd6c090 --- /dev/null +++ b/utils/helpers.js @@ -0,0 +1,176 @@ +import createTag from './tag.js'; + +/** + * * @param {HTMLElement} element the element with the parent undesired wrapper, like

+ * * @param {targetSelector} string selector of the target element + * result: removed the undesired wrapper + */ +export function removeOuterElementLayer(element, targetSelector) { + const targetElement = element.querySelector(targetSelector); + if (targetElement) { + const parent = targetElement.parentNode; + if (parent) (parent).replaceWith(targetElement); + } +} + +/** + * * @param {HTMLElement} element the elemen/block with mutilple child + * * that you want to combine that into single div only + * result: single div with all children elements + * e.g. input:
+ * *

+ * *

+ * *
+ * * output:
+ * *
+ * *


+ * *

+ * *
+ */ +export function combineChildrenToSingleDiv(element) { + const targetChildren = element.querySelectorAll(':scope > div'); + if (targetChildren.length === 0) { return; } + + const singleDiv = document.createElement('div'); + targetChildren.forEach((targetChild) => { + const children = Array.from(targetChild.childNodes); + children.forEach((childElement) => { + singleDiv.appendChild(childElement); + }); + targetChild.remove(); + }); + + element.append(singleDiv); +} + +/** + * * @param {HTMLElement} element + * * @param {string} targetTag, like 'ul' or 'div' + * * @param {string} className + * result: return the new element with inner content of the element, desired tag and css class + */ +export function changeTag(element, targetTag, className) { + const newElClass = className || ''; + const innerContent = element.innerHTML; + const newTagElement = createTag(targetTag, { class: newElClass }, innerContent); + + return newTagElement; +} + +/** + * * @param {string} url the href of a link element + * result: return `_self` or `_blank` if the link has the same host + */ +export function returnLinkTarget(url) { + const currentHost = window.location.host; + const urlObject = new URL(url); + const isSameHost = urlObject.host === currentHost; + + // take in pathname that should be opened in new tab, in redirects excel + const redirectExternalPaths = ['/history', '/chat']; + const redirectToExternalPath = redirectExternalPaths.includes(urlObject.pathname); + + if (!isSameHost || redirectToExternalPath) { + return '_blank'; + } + return '_self'; +} + +// as the blocks are loaded in aysnchronously, we don't have a specific timing +// that the all blocks are loaded -> cannot use a single observer to +// observe all blocks, so use functions here in blocks instead +// eslint-disable-next-line max-len +const requireRevealWrapper = ['slide-reveal-up', 'slide-reveal-up-slow']; + +export function addRevealWrapperToAnimationTarget(element) { + const revealWrapper = createTag('div', { class: 'slide-reveal-wrapper' }); + const parent = element.parentNode; + // Insert the wrapper before the element + parent.insertBefore(revealWrapper, element); + revealWrapper.appendChild(element); +} + +// eslint-disable-next-line max-len +export function addAnimatedClassToElement(targetSelector, animatedClass, delayTime, targetSelectorWrapper) { + const target = targetSelectorWrapper.querySelector(targetSelector); + if (target) { + target.classList.add(animatedClass); + if (delayTime) target.style.transitionDelay = `${delayTime}s`; + if (requireRevealWrapper.indexOf(animatedClass) !== -1) { + addRevealWrapperToAnimationTarget(target); + } + } +} + +// eslint-disable-next-line max-len +export function addAnimatedClassToMultipleElements(targetSelector, animatedClass, delayTime, targetSelectorWrapper, staggerTime) { + const targets = targetSelectorWrapper.querySelectorAll(targetSelector); + if (targets) { + targets.forEach((target, i) => { + target.classList.add(animatedClass); + if (delayTime) target.style.transitionDelay = `${delayTime * (i + 1)}s`; + if (staggerTime) target.style.transitionDelay = `${delayTime + staggerTime * (i + 1)}s`; + if (requireRevealWrapper.indexOf(animatedClass) !== -1) { + addRevealWrapperToAnimationTarget(target); + } + }); + } +} + +export function addInviewObserverToTriggerElement(triggerElement) { + const observerOptions = { + threshold: 0.25, // show when is 25% in view + }; + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('in-view'); + observer.unobserve(entry.target); + } + }); + }, observerOptions); + observer.observe(triggerElement); +} + +// eslint-disable-next-line max-len +export function addInViewAnimationToSingleElement(targetElement, animatedClass, triggerElement, delayTime) { + // if it's HTML element + if (targetElement.nodeType === 1) { + targetElement.classList.add(animatedClass); + if (requireRevealWrapper.indexOf(animatedClass) !== -1) { + addRevealWrapperToAnimationTarget(targetElement); + } + } + // if it's string only, which should be a selector + if (targetElement.nodeType === 3) { + addAnimatedClassToElement(targetElement, animatedClass, triggerElement, delayTime); + } + const trigger = triggerElement || targetElement; + addInviewObserverToTriggerElement(trigger); +} + +export function addInViewAnimationToMultipleElements(animatedItems, triggerElement, staggerTime) { + // set up animation class + animatedItems.forEach((el, i) => { + const delayTime = staggerTime ? i * staggerTime : null; + if (Object.prototype.hasOwnProperty.call(el, 'selector')) { + addAnimatedClassToElement(el.selector, el.animatedClass, delayTime, triggerElement); + } + if (Object.prototype.hasOwnProperty.call(el, 'selectors')) { + // eslint-disable-next-line max-len + addAnimatedClassToMultipleElements(el.selectors, el.animatedClass, el.staggerTime, triggerElement); + } + }); + + // add `.in-view` to triggerElement, so the elements inside will start animating + addInviewObserverToTriggerElement(triggerElement); +} + +export default { + removeOuterElementLayer, + changeTag, + returnLinkTarget, + addInViewAnimationToSingleElement, + addInViewAnimationToMultipleElements, + addInviewObserverToTriggerElement, +}; From 71757a289912f11af3ea9f822f76129292ce009f Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 18 Oct 2023 16:11:19 -0400 Subject: [PATCH 007/133] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c975b4c..673d01c8 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ Takeda Integrated Health Systems ## Environments --Preview: https://main--takeda-ihs--hlxsites.hlx.page/ +-Preview: https://phase-two-redo--takeda-ihs--hlxsites.hlx.page/ --Live: https://main--takeda-ihs--hlxsites.hlx.live/ +-Live: https://phase-two-redo--takeda-ihs--hlxsites.hlx.live/ ## Installation From 8a01265f0082faae34949b31dfcd3cb5abdff88f Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 30 Oct 2023 08:54:09 -0400 Subject: [PATCH 008/133] updates from display changes for phase 1b lots of display layer changes --- blocks/form/decorators/attachments.js | 170 +++++++ blocks/form/decorators/index.js | 19 + blocks/form/decorators/recaptcha.js | 54 +++ blocks/form/decorators/repeat.js | 108 +++++ blocks/form/form.css | 324 +++++++++---- blocks/form/form.js | 491 ++++++++++++++------ blocks/form/formatting.js | 37 ++ blocks/form/rules/RuleCompiler.js | 54 +++ blocks/form/rules/RuleEngine.js | 254 ++++++++++ blocks/form/rules/index.js | 40 ++ blocks/form/rules/parser/Formula.js | 39 ++ blocks/form/rules/parser/Lexer.js | 258 ++++++++++ blocks/form/rules/parser/Parser.js | 222 +++++++++ blocks/form/rules/parser/Runtime.js | 16 + blocks/form/rules/parser/Tokens.js | 24 + blocks/form/rules/parser/TreeInterpreter.js | 152 ++++++ blocks/form/rules/parser/functions.js | 116 +++++ blocks/form/rules/parser/utils.js | 29 ++ tools/importer/forms/ui/css/styles.css | 316 +++++++++++++ tools/importer/forms/ui/index.html | 71 +++ tools/importer/forms/ui/js/script.js | 165 +++++++ tools/importer/forms/ui/svg/attachments.svg | 1 + tools/importer/forms/ui/svg/marketo.svg | 1 + tools/importer/forms/ui/svg/pdf.svg | 45 ++ tools/importer/forms/ui/svg/recaptcha.svg | 6 + tools/sidekick/config.json | 7 + utils/tag.js | 23 + 27 files changed, 2811 insertions(+), 231 deletions(-) create mode 100644 blocks/form/decorators/attachments.js create mode 100644 blocks/form/decorators/index.js create mode 100644 blocks/form/decorators/recaptcha.js create mode 100644 blocks/form/decorators/repeat.js create mode 100644 blocks/form/formatting.js create mode 100644 blocks/form/rules/RuleCompiler.js create mode 100644 blocks/form/rules/RuleEngine.js create mode 100644 blocks/form/rules/index.js create mode 100644 blocks/form/rules/parser/Formula.js create mode 100644 blocks/form/rules/parser/Lexer.js create mode 100644 blocks/form/rules/parser/Parser.js create mode 100644 blocks/form/rules/parser/Runtime.js create mode 100644 blocks/form/rules/parser/Tokens.js create mode 100644 blocks/form/rules/parser/TreeInterpreter.js create mode 100644 blocks/form/rules/parser/functions.js create mode 100644 blocks/form/rules/parser/utils.js create mode 100644 tools/importer/forms/ui/css/styles.css create mode 100644 tools/importer/forms/ui/index.html create mode 100644 tools/importer/forms/ui/js/script.js create mode 100644 tools/importer/forms/ui/svg/attachments.svg create mode 100644 tools/importer/forms/ui/svg/marketo.svg create mode 100644 tools/importer/forms/ui/svg/pdf.svg create mode 100644 tools/importer/forms/ui/svg/recaptcha.svg create mode 100644 utils/tag.js diff --git a/blocks/form/decorators/attachments.js b/blocks/form/decorators/attachments.js new file mode 100644 index 00000000..24b2a7c3 --- /dev/null +++ b/blocks/form/decorators/attachments.js @@ -0,0 +1,170 @@ +function isFileAllowed(file, allowedTypes = '') { + if (!file) { + throw new Error('File object is required.'); + } + const extensionRegex = /(?:\.([^.]+))?$/; + const fileExtension = extensionRegex.exec(file.name)[1]; + const fileType = file.type; + return !allowedTypes || allowedTypes.includes(fileType) || allowedTypes.includes(fileExtension); +} + +function getFileList(files) { + const dataTransfer = new DataTransfer(); + files.forEach((file) => dataTransfer.items.add(file)); + return dataTransfer.files; +} + +function getFileDesription(file) { + const description = document.createElement('div'); + description.className = 'field-description file-description'; + const span = document.createElement('span'); + span.innerText = `${file.name} ${(file.size / (1024 * 1024)).toFixed(2)}mb`; + description.append(span); + return description; +} + +function updateIndex(elements = []) { + elements.forEach((element, index) => { + element.dataset.index = index; + }); +} + +function updateMessage(messages, message) { + const li = document.createElement('li'); + li.innerText = message; + messages.append(li); +} + +function clearMessages(messages) { + messages.innerHTML = ''; +} + +function validateType(files, allowedTypes = '') { + const allowedFiles = []; + const disallowedFiles = []; + files.forEach((file) => { + (isFileAllowed(file, allowedTypes) ? allowedFiles : disallowedFiles).push(file); + }); + return { allowedFiles, disallowedFiles }; +} + +function validateSize(files, maxSize = 200) { + const withinSizeFiles = []; + const exceedSizeFiles = []; + files.forEach((file) => { + const size = (file.size / (1024 * 1024)).toFixed(2); // in mb + (size < maxSize ? withinSizeFiles : exceedSizeFiles).push(file); + }); + return { withinSizeFiles, exceedSizeFiles }; +} + +function validateLimit(files, attachedFiles, multiple = false, max = -1) { + let filesToAttach = []; + let filesToReject = []; + if (!multiple) { + filesToAttach = files.splice(0, attachedFiles.length ? 0 : 1); + } else { + filesToAttach = files.splice(0, max === -1 ? Infinity : max - attachedFiles.length); + } + filesToReject = files; + return { filesToAttach, filesToReject }; +} + +export async function transformFileDOM(formDef, form) { + const wrappers = form.querySelectorAll('.form-file-wrapper'); + [...wrappers].forEach((wrapper) => { + const attachedFiles = []; + const input = wrapper.querySelector('input'); + const max = (parseInt(input.max, 10) || -1); + const template = input.cloneNode(true); + const multiple = input.hasAttribute('multiple'); + const messages = document.createElement('ul'); + const fileDescriptions = wrapper.getElementsByClassName('file-description'); + const validate = (files = []) => { + clearMessages(messages); + const { allowedFiles, disallowedFiles } = validateType(files); + disallowedFiles.forEach((file) => updateMessage(messages, `${file.name} - This type of file is not allowed.`)); + const { withinSizeFiles, exceedSizeFiles } = validateSize(allowedFiles); + exceedSizeFiles.forEach((file) => updateMessage(messages, `${file.name} - File exceeds size limit.`)); + // eslint-disable-next-line max-len + const { filesToAttach, filesToReject } = validateLimit(withinSizeFiles, attachedFiles, multiple, max); + if (filesToReject.length > 0) { + updateMessage(messages, 'Maximum number of files reached.'); + } + return filesToAttach; + }; + const attachFiles = (files = []) => { + const filesToAttach = validate(files); + filesToAttach.forEach((file) => { + const description = getFileDesription(file); + const button = document.createElement('button'); + button.type = 'button'; + button.onclick = () => { + const index = parseInt(description.dataset.index, 10); + description.remove(); + attachedFiles.splice(index, 1); + input.files = getFileList(attachedFiles); + updateIndex([...fileDescriptions]); + clearMessages(messages); + }; + description.append(button); + wrapper.append(description); + attachedFiles.push(file); + }); + updateIndex([...fileDescriptions]); + input.files = getFileList(attachedFiles); + }; + const dropArea = document.createElement('div'); + dropArea.className = 'field-dropregion'; + dropArea.innerHTML = `

${input.getAttribute('placeholder')}

`; + dropArea.ondragover = (event) => event.preventDefault(); + dropArea.ondrop = (event) => { + attachFiles([...event.dataTransfer.files]); + event.preventDefault(); + }; + const button = document.createElement('button'); + button.type = 'button'; + button.innerText = 'Select files'; + button.onclick = () => { + const fileInput = template.cloneNode(true); + fileInput.onchange = () => attachFiles([...fileInput.files]); + fileInput.click(); + }; + dropArea.append(button); + wrapper.insertBefore(dropArea, input); + wrapper.append(messages); // for validation messages. + }); +} + +export async function transformFileRequest(request, form) { + const fileFields = form.querySelectorAll('input[type="file"]'); + const { body, url } = request; + const attachments = Object.fromEntries([...fileFields].map((fe) => [fe.name, fe.files])); + if (attachments && Object.keys(attachments).length > 0) { + const newHeaders = {}; + const newbody = new FormData(); + const oldbody = JSON.parse(body); + const fileNames = Object.keys(attachments); + let hasAttachments = false; + Object.entries(attachments).forEach(([name, files]) => { + if (files.length > 0) hasAttachments = true; + [...files].forEach((file) => newbody.append(name, file)); + }); + if (hasAttachments) { + Object.entries(oldbody).forEach(([k, v]) => { + if (typeof v === 'object') { + newbody.append(k, JSON.stringify(v)); + } else { + newbody.append(k, v); + } + }); + newbody.append('fileFields', JSON.stringify(fileNames)); + return { + body: newbody, + headers: newHeaders, + url, + }; + } + } + return request; +} diff --git a/blocks/form/decorators/index.js b/blocks/form/decorators/index.js new file mode 100644 index 00000000..5cb15163 --- /dev/null +++ b/blocks/form/decorators/index.js @@ -0,0 +1,19 @@ +import { applyRuleEngine } from '../rules/index.js'; +import { transformFileDOM, transformFileRequest } from './attachments.js'; +import { transformCaptchaDOM, transformCaptchaRequest } from './recaptcha.js'; +import transferRepeatableDOM from './repeat.js'; + +export const transformers = [ + transformFileDOM, + transformCaptchaDOM, + transferRepeatableDOM, + applyRuleEngine, +]; + +export const asyncTransformers = [ +]; + +export const requestTransformers = [ + transformCaptchaRequest, + transformFileRequest, +]; diff --git a/blocks/form/decorators/recaptcha.js b/blocks/form/decorators/recaptcha.js new file mode 100644 index 00000000..e3698453 --- /dev/null +++ b/blocks/form/decorators/recaptcha.js @@ -0,0 +1,54 @@ +let SITE_KEY = '6LcB318mAAAAAO6smzDd-TtD1-AWlidsHsCXcJHy'; + +function loadScript(url) { + const head = document.querySelector('head'); + let script = head.querySelector(`script[src="${url}"]`); + if (!script) { + script = document.createElement('script'); + script.src = url; + script.async = true; + head.append(script); + return script; + } + return script; +} + +export async function transformCaptchaDOM(formDef, form) { + SITE_KEY = formDef.find((field) => field.Name === 'googleRecaptcha')?.Value; + const button = form.querySelector('button[type="submit"]'); + if (SITE_KEY && button) { + const obs = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + loadScript(`https://www.google.com/recaptcha/api.js?render=${SITE_KEY}`); + obs.disconnect(); + } + }); + }); + obs.observe(button); + } +} + +export async function transformCaptchaRequest(request) { + const { grecaptcha } = window; + const { body, headers, url } = request; + return new Promise((resolve) => { + if (grecaptcha) { + grecaptcha.ready(() => { + grecaptcha.execute(SITE_KEY, { action: 'submit' }).then(async (token) => { + const newbody = { + data: JSON.parse(body).data, + token, + }; + resolve({ + body: JSON.stringify(newbody), + headers, + url, + }); + }); + }); + } else { + resolve(request); + } + }); +} diff --git a/blocks/form/decorators/repeat.js b/blocks/form/decorators/repeat.js new file mode 100644 index 00000000..c3b5a997 --- /dev/null +++ b/blocks/form/decorators/repeat.js @@ -0,0 +1,108 @@ +const getId = (function getId() { + const ids = {}; + return (name) => { + ids[name] = ids[name] || 0; + const idSuffix = ids[name] ? `-${ids[name]}` : ''; + ids[name] += 1; + return `${name}${idSuffix}`; + }; +}()); + +function update(fieldset, index, labelTemplate) { + const legend = fieldset.querySelector(':scope>.field-label').firstChild; + const text = labelTemplate.replace('#', index + 1); + if (legend) { + legend.textContent = text; + } + fieldset.id = getId(fieldset.name); + fieldset.setAttribute('data-index', index); + if (index > 0) { + fieldset.querySelectorAll('.field-wrapper').forEach((f) => { + const [label, input, description] = ['label', 'input,select,button,textarea', 'description'] + .map((x) => f.querySelector(x)); + input.id = getId(input.name); + if (label) { + label.htmlFor = input.id; + } + if (description) { + input.setAttribute('aria-describedby', `${input.Id}-description`); + description.id = `${input.id}-description`; + } + }); + } +} + +function createButton(label, icon) { + const button = document.createElement('button'); + button.className = `item-${icon}`; + button.type = 'button'; + const text = document.createElement('span'); + text.textContent = label; + button.append(document.createElement('i'), text); + return button; +} + +function insertRemoveButton(fieldset, wrapper, form) { + const removeButton = createButton('Remove', 'remove'); + removeButton.addEventListener('click', () => { + fieldset.remove(); + wrapper.querySelector('.item-add').setAttribute('data-visible', 'true'); + wrapper.querySelectorAll('[data-repeatable="true"]').forEach((el, index) => { + update(el, index, wrapper['#repeat-template-label']); + }); + const event = new CustomEvent('item:remove', { + detail: { item: { name: fieldset.name, id: fieldset.id } }, + bubbles: false, + }); + form.dispatchEvent(event); + }); + const legend = fieldset.querySelector(':scope>.field-label'); + legend.append(removeButton); +} + +const add = (wrapper, form) => (e) => { + const { currentTarget } = e; + const { parentElement } = currentTarget; + const fieldset = parentElement['#repeat-template']; + const max = parentElement.getAttribute('data-max'); + const min = parentElement.getAttribute('data-min'); + const childCount = parentElement.children.length - 1; + const newFieldset = fieldset.cloneNode(true); + newFieldset.setAttribute('data-index', childCount); + update(newFieldset, childCount, parentElement['#repeat-template-label']); + if (childCount >= +min) { + insertRemoveButton(newFieldset, wrapper, form); + } + if (+max <= childCount + 1) { + e.currentTarget.setAttribute('data-visible', 'false'); + } + currentTarget.insertAdjacentElement('beforebegin', newFieldset); + const event = new CustomEvent('item:add', { + detail: { item: { name: newFieldset.name, id: newFieldset.id } }, + bubbles: false, + }); + form.dispatchEvent(event); +}; + +export default function transferRepeatableDOM(formDef, form) { + form.querySelectorAll('[data-repeatable="true"]').forEach((el) => { + const div = document.createElement('div'); + div.setAttribute('data-min', el.dataset.min); + div.setAttribute('data-max', el.dataset.max); + el.insertAdjacentElement('beforebegin', div); + div.append(el); + const addLabel = 'Add'; + const addButton = createButton(addLabel, 'add'); + addButton.addEventListener('click', add(div, form)); + div['#repeat-template'] = el.cloneNode(true); + div['#repeat-template-label'] = el.querySelector(':scope>.field-label').textContent; + if (+el.min === 0) { + el.remove(); + } else { + update(el, 0, div['#repeat-template-label']); + el.setAttribute('data-index', 0); + } + div.append(addButton); + div.className = 'form-repeat-wrapper'; + }); +} diff --git a/blocks/form/form.css b/blocks/form/form.css index 1602ac63..64c85aac 100644 --- a/blocks/form/form.css +++ b/blocks/form/form.css @@ -1,130 +1,290 @@ +:root { + --background-color-primary: #fff; + --label-color: #666; + --border-color: #818a91; + --button-primary-color: #5F8DDA; + --button-primary-hover-color: #035fe6; + --form-font-size-m: 22px; + --form-font-size-s: 18px; + --form-font-size-xs: 16px; + --form-background-color: var(--background-color-primary); + --form-padding: 3%; + --form-columns: 1; + --form-field-horz-gap: 40px; + --form-field-vert-gap: 20px; + --form-invalid-border-color: #ff5f3f; + --form-input-padding: 0.75rem 0.6rem; + --form-input-font-size: 1rem; + --form-input-disable-color: var(--label-color); + --form-input-border-size: 1px; + --form-input-border-color: var(--border-color); + --form-input-background-color: var(--background-color-primary); + --form-paragraph-color: var(--label-color); + --form-paragraph-margin: 0 0 0.9rem; + --form-paragraph-font-style: none; + --form-paragraph-font-size: var(--form-font-size-s); + --form-label-color: var(--label-color); + --form-label-font-size: var(--form-font-size-s); + --form-label-font-weight: 400; + --form-title-font-weight: 600; + --form-fieldset-border: 0; + --form-fieldset-marign: 0; + --form-fieldset-legend-color: var(--form-label-color); + --form-fieldset-legend-font-size: var(--form-label-font-size); + --form-fieldset-legend-font-weight: var(--form-title-font-weight); + --form-fieldset-legend-border: none; + --form-fieldset-legend-padding: 0; + --form-button-color: var(--background-color-primary); + --form-button-font-size: var(--form-font-size-s); + --form-button-background-color: var(--button-primary-color); + --form-button-background-hover-color: var(--button-primary-hover-color); + --form-button-border: 2px solid transparent; + --form-button-padding:15px 50px; + --form-upload-color: var(--background-color-primary); + --form-upload-font-size: var(--form-font-size-xs); + --form-upload-background-color: var(--mt-global-color-base-primary); + --form-submit-width: 100%; + --form-width: 100%; +} + +form output { + display: block; + font-weight: 700; + font-size: 1.625rem; +} + +form [data-visible="false"] { + display: none !important; +} + +main .form-container { + background-color: var(--mt-background-color-primary); + padding: var(--form-padding); + width: var(--form-width); + margin: var(--nav-height) auto; +} + main .form { - background-color: var(--color-white); - padding: var(--spacing-s); - border-radius: var(--card-border-radius-l); - filter: var(--image-filter-drop-shadow-small); - border: solid 1px var(--bg-color-grey); + background-color: var(--form-background-color); +} + +main .form > div:not(:first-child) { + display: none; +} + +main .form form { + display: flex; + flex-wrap: wrap; + gap: var(--form-field-vert-gap) var(--form-field-horz-gap); + align-items: start; +} + +main .form form fieldset { + border: var(--form-fieldset-border); + margin: var(--form-fieldset-marign); + width: 100%; } -main .form h2 { - padding: var(--spacing-s) 0; - font-size: var(--type-heading-l-lh); +main .form form fieldset fieldset { + padding: 0; } -main .form h3 { - padding-top: var(--spacing-s); - font-size: var(--type-heading-m-size); +main .form .field-description { + color: var(--form-label-color); + font-size: var(--form-font-size-xs); } -main .form input, -main .form textarea, -main .form select { - border: solid 1px var(--bg-color-grey); - padding: var(--spacing-xxs) var(--spacing-xs); +main .form input, main .form textarea, main .form select { + background-color: var(--form-input-background-color); + border: var(--form-input-border-size) solid var(--form-input-border-color); width: 100%; - max-width: 50rem; - box-sizing: border-box; - border-radius: var(--input-border-radius); - font-size: var(--type-body-s-size); - line-height: var(--type-body-s-lh); - font-family: var(--body-font-family); + height: 42px; + color: var(--form-label-color); + padding: var(--form-input-padding); + font-size: var(--form-input-font-size); + max-width: unset; +} + +main .form input[type='file'] { + border: none; + padding-inline-start:0; +} + +main .form input[type='checkbox'], +main .form input[type='radio'] { + width: 16px; + height: 16px; + flex: none; + margin: 0; } main .form textarea { min-height: 100px; } -main .form input:hover, -main .form select:hover { - border-color: var(--color-font-grey); +main .form input:hover, main .form select:hover { + border-color: rgb(90 92 96); +} + +main .form fieldset legend { + font-weight: var(--form-fieldset-legend-font-weight); + font-size: var(--form-fieldset-legend-font-size); + color: var(--form-fieldset-legend-color); + border-bottom: var(--form-fieldset-legend-border); + width: 100%; + padding: var(--form-fieldset-legend-padding); + margin-bottom: 10px; +} + +main .innovate form > fieldset > legend { + text-align: center; } main .form label { - display: block; - padding-bottom: var(--spacing-xxs); - box-sizing: border-box; - font-size: var(--type-body-s-size); - line-height: var(--type-body-s-lh); + font-weight: var(--form-label-font-weight); + font-size: var(--form-label-font-size); + color: var(--form-label-color); } main .form label.required::after { content: "*"; - color: var(--color-black); - padding-left: var(--spacing-xxxs); + color: var(--form-label-color); + padding-inline-start: 5px; } -main .form .field-wrapper { - margin-bottom: var(--spacing-m); +main .form form p, +main .form form .field-wrapper { + margin: var(--form-field-gap); + color: var(--form-label-color); + flex: 1 0 calc(100%/var(--form-columns) - var(--form-field-horz-gap)); } -main .form .form-checkbox-wrapper { +main .form form p { + font-size: var(--form-paragraph-font-size); + font-style: var(--form-paragraph-font-style); + color: var(--form-paragraph-color); + margin: var(--form-paragraph-margin); +} + +main .form form .form-checkbox-wrapper, main .form form .form-radio-wrapper { display: flex; - align-items: center; - margin: var(--spacing-xs) 0; + align-items: baseline; + margin: 0; + gap: 8px; +} + +main .form form fieldset > .form-radio-wrapper:first-of-type, +main .form form fieldset > .form-checkbox-wrapper:first-of-type { + margin: var(--form-field-gap); } -main .form .form-checkbox-wrapper input[type='checkbox'] { - appearance: none; +main .form .form-radio-wrapper label, +main .form .form-checkbox-wrapper label { + font-weight: var(--mt-font-weight-regular); + flex-basis: calc(100% - 28px); +} - /* stylelint-disable */ - -webkit-appearance: none; - -moz-appearance: none; +input::file-selector-button { + color: var(--form-upload-color); + background: var(--form-upload-background-color); + font-size: var(--form-upload-font-size); + text-align: center; +} - /* stylelint-enable */ - height: 20px; - width: 20px; - position: relative; - border: unset; - padding: unset; - margin-right: var(--spacing-xxs); - margin-left: 0; +main .form button { + color: var(--form-button-color); + background: var(--form-button-background-color); + border: var(--form-button-border); + padding: var(--form-button-padding); + font-size: var(--form-button-font-size); + font-weight: var(--mt-font-weight-bold); + border-radius: unset; + width: 100%; } -main .form .form-checkbox-wrapper input[type='checkbox']::after { - display: block; - position: absolute; - top: 0; - left: 0; - height: 20px; - width: 20px; - content: ' '; - background: url('./checkbox.svg'); - background-size: contain; +main .form button:hover { + background: var(--form-button-background-hover-color); } -main .form .form-checkbox-wrapper input[type='checkbox']:checked::after { - background: url('./checkbox-checked.svg'); - background-size: contain; - content: ' '; +main .form-submit-wrapper{ + width: var(--form-submit-width); } -main .form .form-checkbox-wrapper label { - display: block; - font-size: var(--type-body-xxs-size); - line-height: var(--type-body-xxs-lh); - padding-bottom: 0; +main .form input:disabled, +main .form textarea:disabled, +main .form select:disabled, +main .form button:disabled { + background-color: var(--form-input-disable-color); } -main .form-legal-wrapper p { - font-size: var(--type-body-xxs-size); - line-height: var(--type-body-xxs-lh); - font-style: italic; +main .form-file-wrapper input[type="file"] { + display: none; } -main .form button { - font-family: var(--body-font-family); +main .form-file-wrapper .field-dropregion { + background: rgb(0 0 0 / 2%); + border: 1px dashed var(--mt-global-color-silver); + border-radius: 4px; + margin: 11px 0 8px; + padding: 32px; + text-align: center; +} + +main .form-file-wrapper .file-description button { + --form-button-padding: 15px; + + background: url('/icons/delete.svg') no-repeat; + width: unset; + border: unset; + text-align: center; } -@media screen and (min-width: 900px) { - main .form { - padding: var(--spacing-ml); +@media (min-width: 576px) { + :root { + --form-width: 540px; + } +} + +@media (min-width: 768px) { + :root { + --form-width: 740px; + } + + main .form button { + width: unset; + } +} + +@media (min-width: 1200px) { + :root { + --form-width: 990px; + } +} + +main form .form-fieldset-wrapper { + display: flex; + padding: 0; + flex-direction: column; +} + +/* main form .form-fieldset-wrapper > .field-wrapper{ + flex: 1 0 auto; +} */ + +@media (min-width: 1200px), (min-width: 992px) { + :root { + --form-columns: 2; } - main .form .field-wrapper { - display: flex; + main .form form .form-checkbox-wrapper, + main .form form .form-textarea-wrapper, + main .form form .form-file-wrapper, + main .form form .form-fieldset-wrapper { + flex: 1 0 100%; } - main .form label { - width: 72%; + main .form form .form-fieldset-wrapper { + flex-flow: row wrap; + gap: 10px 15px; } } diff --git a/blocks/form/form.js b/blocks/form/form.js index 7dcb3185..8302f1e9 100644 --- a/blocks/form/form.js +++ b/blocks/form/form.js @@ -1,7 +1,190 @@ +import { readBlockConfig } from '../../scripts/lib-franklin.js'; -function createSelect(fd) { +function generateUnique() { + return new Date().valueOf() + Math.random(); +} + +const formatFns = await (async function imports() { + try { + const formatters = await import('./formatting.js'); + return formatters.default; + } catch (e) { + // eslint-disable-next-line no-console + console.log('Formatting library not found. Formatting will not be supported'); + } + return {}; +}()); + +function constructPayload(form) { + const payload = { __id__: generateUnique() }; + [...form.elements].forEach((fe) => { + if (fe.name) { + if (fe.type === 'radio') { + if (fe.checked) payload[fe.name] = fe.value; + } else if (fe.type === 'checkbox') { + if (fe.checked) payload[fe.name] = payload[fe.name] ? `${payload[fe.name]},${fe.value}` : fe.value; + } else if (fe.type !== 'file') { + payload[fe.name] = fe.value; + } + } + }); + return { payload }; +} + +async function submissionFailure(error, form) { + alert(error); // TODO define error mechansim + form.setAttribute('data-submitting', 'false'); + form.querySelector('button[type="submit"]').disabled = false; +} + +async function prepareRequest(form, transformer) { + const { payload } = constructPayload(form); + console.log(payload); + const headers = { + 'Content-Type': 'application/json', + }; + const body = JSON.stringify({ data: payload }); + console.log(body); + const url = form.dataset.submit || form.dataset.action; + if (typeof transformer === 'function') { + return transformer({ headers, body, url }, form); + } + return { headers, body, url }; +} + +async function submitForm(form, transformer) { + try { + const { headers, body, url } = await prepareRequest(form, transformer); + + const response = await fetch(url, { + method: 'POST', + headers, + body, + }); + console.log(response); + if (response.ok) { + /* window.location.href = form.dataset?.redirect || 'thankyou'; */ + } else { + const error = await response.text(); + throw new Error(error); + } + } catch (error) { + submissionFailure(error, form); + } +} + +async function handleSubmit(form, transformer) { + if (form.getAttribute('data-submitting') !== 'true') { + form.setAttribute('data-submitting', 'true'); + await submitForm(form, transformer); + } +} + +function setPlaceholder(element, fd) { + if (fd.Placeholder) { + element.setAttribute('placeholder', fd.Placeholder); + } +} + +const constraintsDef = Object.entries({ + 'email|text': [['Max', 'maxlength'], ['Min', 'minlength']], + 'number|range|date': ['Max', 'Min', 'Step'], + file: ['Accept', 'Multiple'], + fieldset: [['Max', 'data-max'], ['Min', 'data-min']], +}).flatMap(([types, constraintDef]) => types.split('|') + .map((type) => [type, constraintDef.map((cd) => (Array.isArray(cd) ? cd : [cd, cd]))])); + +const constraintsObject = Object.fromEntries(constraintsDef); + +function setConstraints(element, fd) { + const constraints = constraintsObject[fd.Type]; + if (constraints) { + constraints + .filter(([nm]) => fd[nm]) + .forEach(([nm, htmlNm]) => { + element.setAttribute(htmlNm, fd[nm]); + }); + } +} + +function createLabel(fd, tagName = 'label') { + const label = document.createElement(tagName); + label.setAttribute('for', fd.Id); + label.className = 'field-label'; + label.textContent = fd.Label || ''; + if (fd.Tooltip) { + label.title = fd.Tooltip; + } + return label; +} + +function createHelpText(fd) { + const div = document.createElement('div'); + div.className = 'field-description'; + div.setAttribute('aria-live', 'polite'); + div.innerText = fd.Description; + div.id = `${fd.Id}-description`; + return div; +} + +function createFieldWrapper(fd, tagName = 'div') { + const fieldWrapper = document.createElement(tagName); + const nameStyle = fd.Name ? ` form-${fd.Name}` : ''; + const fieldId = `form-${fd.Type}-wrapper${nameStyle}`; + fieldWrapper.className = fieldId; + if (fd.Fieldset) { + fieldWrapper.dataset.fieldset = fd.Fieldset; + } + if (fd.Mandatory.toLowerCase() === 'true') { + fieldWrapper.dataset.required = ''; + } + if (fd.Visible?.toLowerCase() === 'false') { + fieldWrapper.dataset.visible = 'false'; + } + fieldWrapper.classList.add('field-wrapper'); + fieldWrapper.append(createLabel(fd)); + return fieldWrapper; +} + +function createButton(fd) { + const wrapper = createFieldWrapper(fd); + const button = document.createElement('button'); + button.textContent = fd.Label; + button.type = fd.Type; + button.classList.add('button'); + button.dataset.redirect = fd.Extra || ''; + button.id = fd.Id; + button.name = fd.Name; + wrapper.replaceChildren(button); + return wrapper; +} +function createSubmit(fd) { + const wrapper = createButton(fd); + return wrapper; +} + +function createInput(fd) { + const input = document.createElement('input'); + input.type = fd.Type; + setPlaceholder(input, fd); + setConstraints(input, fd); + return input; +} + +const withFieldWrapper = (element) => (fd) => { + const wrapper = createFieldWrapper(fd); + wrapper.append(element(fd)); + return wrapper; +}; + +const createTextArea = withFieldWrapper((fd) => { + const input = document.createElement('textarea'); + setPlaceholder(input, fd); + return input; +}); + +const createSelect = withFieldWrapper((fd) => { const select = document.createElement('select'); - select.id = fd.Field; if (fd.Placeholder) { const ph = document.createElement('option'); ph.textContent = fd.Placeholder; @@ -15,183 +198,193 @@ function createSelect(fd) { option.value = o.trim(); select.append(option); }); - if (fd.Mandatory === 'x') { - select.setAttribute('required', 'required'); - } return select; -} -console.log('inform'); -function constructPayload(form) { - const payload = {}; - [...form.elements].forEach((fe) => { - if (fe.type === 'checkbox') { - if (fe.checked) payload[fe.id] = fe.value; - } else if (fe.id) { - payload[fe.id] = fe.value; - } - }); - return payload; -} - -async function submitForm(form) { - const payload = constructPayload(form); - payload.timestamp = new Date().toJSON(); - const resp = await fetch(form.dataset.action, { - method: 'POST', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: payload }), - }); - await resp.text(); - return payload; +}); + +function createRadio(fd) { + const wrapper = createFieldWrapper(fd); + wrapper.insertAdjacentElement('afterbegin', createInput(fd)); + return wrapper; } -function createButton(fd) { - const button = document.createElement('button'); - button.textContent = fd.Label; - button.classList.add('button'); - if (fd.Type === 'submit') { - button.addEventListener('click', async (event) => { - const form = button.closest('form'); - if (fd.Placeholder) form.dataset.action = fd.Placeholder; - if (form.checkValidity()) { - event.preventDefault(); - button.setAttribute('disabled', ''); - await submitForm(form); - const redirectTo = fd.Extra; - window.location.href = redirectTo; - } - }); +const createOutput = withFieldWrapper((fd) => { + const output = document.createElement('output'); + output.name = fd.Name; + output.id = fd.Id; + const displayFormat = fd['Display Format']; + if (displayFormat) { + output.dataset.displayFormat = displayFormat; } - return button; + const formatFn = formatFns[displayFormat] || ((x) => x); + output.innerText = formatFn(fd.Value); + return output; +}); + +function createHidden(fd) { + const input = document.createElement('input'); + input.type = 'hidden'; + input.id = fd.Id; + input.name = fd.Name; + input.value = fd.Value; + return input; } -function createHeading(fd, el) { - const heading = document.createElement(el); - heading.textContent = fd.Label; - return heading; +function createLegend(fd) { + return createLabel(fd, 'legend'); } -function createInput(fd) { - const input = document.createElement('input'); - input.type = fd.Type; - input.id = fd.Field; - input.setAttribute('placeholder', fd.Placeholder); - if (fd.Mandatory === 'x') { - input.setAttribute('required', 'required'); +function createFieldSet(fd) { + const wrapper = createFieldWrapper(fd, 'fieldset'); + wrapper.id = fd.Id; + wrapper.name = fd.Name; + wrapper.replaceChildren(createLegend(fd)); + if (fd.Repeatable && fd.Repeatable.toLowerCase() === 'true') { + setConstraints(wrapper, fd); + wrapper.dataset.repeatable = 'true'; } - return input; + return wrapper; } -function createTextArea(fd) { - const input = document.createElement('textarea'); - input.id = fd.Field; - input.setAttribute('placeholder', fd.Placeholder); - if (fd.Mandatory === 'x') { - input.setAttribute('required', 'required'); - } - return input; +function groupFieldsByFieldSet(form) { + const fieldsets = form.querySelectorAll('fieldset'); + fieldsets?.forEach((fieldset) => { + const fields = form.querySelectorAll(`[data-fieldset="${fieldset.name}"`); + fields?.forEach((field) => { + fieldset.append(field); + }); + }); } -function createLabel(fd) { - const label = document.createElement('label'); - label.setAttribute('for', fd.Field); - label.textContent = fd.Label; - if (fd.Mandatory === 'x') { - label.classList.add('required'); +function createPlainText(fd) { + const paragraph = document.createElement('p'); + const nameStyle = fd.Name ? `form-${fd.Name}` : ''; + paragraph.className = nameStyle; + paragraph.dataset.fieldset = fd.Fieldset ? fd.Fieldset : ''; + paragraph.textContent = fd.Label; + return paragraph; +} + +export const getId = (function getId() { + const ids = {}; + return (name) => { + ids[name] = ids[name] || 0; + const idSuffix = ids[name] ? `-${ids[name]}` : ''; + ids[name] += 1; + return `${name}${idSuffix}`; + }; +}()); + +const fieldRenderers = { + radio: createRadio, + checkbox: createRadio, + textarea: createTextArea, + select: createSelect, + button: createButton, + submit: createSubmit, + output: createOutput, + hidden: createHidden, + fieldset: createFieldSet, + plaintext: createPlainText, +}; + +function renderField(fd) { + const renderer = fieldRenderers[fd.Type]; + let field; + if (typeof renderer === 'function') { + field = renderer(fd); + } else { + field = createFieldWrapper(fd); + field.append(createInput(fd)); } - return label; + if (fd.Description) { + field.append(createHelpText(fd)); + } + return field; } -function applyRules(form, rules) { - const payload = constructPayload(form); - rules.forEach((field) => { - const { type, condition: { key, operator, value } } = field.rule; - if (type === 'visible') { - if (operator === 'eq') { - if (payload[key] === value) { - form.querySelector(`.${field.fieldId}`).classList.remove('hidden'); - } else { - form.querySelector(`.${field.fieldId}`).classList.add('hidden'); - } - } +async function applyTransformation(formDef, form) { + try { + const { requestTransformers, transformers } = await import('./decorators/index.js'); + if (transformers) { + transformers.forEach( + (fn) => fn.call(this, formDef, form), + ); } - }); -} -function fill(form) { - const { action } = form.dataset; - if (action === '/tools/bot/register-form') { - const loc = new URL(window.location.href); - form.querySelector('#owner').value = loc.searchParams.get('owner') || ''; - form.querySelector('#installationId').value = loc.searchParams.get('id') || ''; + const transformRequest = async (request, fd) => requestTransformers?.reduce( + (promise, transformer) => promise.then((modifiedRequest) => transformer(modifiedRequest, fd)), + Promise.resolve(request), + ); + return transformRequest; + } catch (e) { + // eslint-disable-next-line no-console + console.log('no custom decorators found.'); } + return (req) => req; +} + +async function fetchData(url) { + const resp = await fetch(url); + const json = await resp.json(); + return json.data.map((fd) => ({ + ...fd, + Id: fd.Id || getId(fd.Name), + Value: fd.Value || '', + })); +} + +async function fetchForm(pathname) { + // get the main form + const jsonData = await fetchData(pathname); + console.log(jsonData); + return jsonData; } async function createForm(formURL) { const { pathname } = new URL(formURL); - const resp = await fetch(pathname); - const json = await resp.json(); + const data = await fetchForm(pathname); const form = document.createElement('form'); - const rules = []; - // eslint-disable-next-line prefer-destructuring - form.dataset.action = pathname.split('.json')[0]; - json.data.forEach((fd) => { - fd.Type = fd.Type || 'text'; - const fieldWrapper = document.createElement('div'); - const style = fd.Style ? ` form-${fd.Style}` : ''; - const fieldId = `form-${fd.Type}-wrapper${style}`; - fieldWrapper.className = fieldId; - fieldWrapper.classList.add('field-wrapper'); - switch (fd.Type) { - case 'select': - fieldWrapper.append(createLabel(fd)); - fieldWrapper.append(createSelect(fd)); - break; - case 'heading': - fieldWrapper.append(createHeading(fd, 'h3')); - break; - case 'legal': - fieldWrapper.append(createHeading(fd, 'p')); - break; - case 'checkbox': - fieldWrapper.append(createInput(fd)); - fieldWrapper.append(createLabel(fd)); - break; - case 'text-area': - fieldWrapper.append(createLabel(fd)); - fieldWrapper.append(createTextArea(fd)); - break; - case 'submit': - fieldWrapper.append(createButton(fd)); - break; - default: - fieldWrapper.append(createLabel(fd)); - fieldWrapper.append(createInput(fd)); + data.forEach((fd) => { + const el = renderField(fd); + const input = el.querySelector('input,textarea,select'); + if (fd.Mandatory && fd.Mandatory.toLowerCase() === 'true') { + input.setAttribute('required', 'required'); } - - if (fd.Rules) { - try { - rules.push({ fieldId, rule: JSON.parse(fd.Rules) }); - } catch (e) { - // eslint-disable-next-line no-console - console.warn(`Invalid Rule ${fd.Rules}: ${e}`); + if (input) { + input.id = fd.Id; + input.name = fd.Name; + if (input.type !== 'file') { + input.value = fd.Value; + if (input.type === 'radio' || input.type === 'checkbox') { + input.checked = fd.Checked === 'true'; + } + } + if (fd.Description) { + input.setAttribute('aria-describedby', `${fd.Id}-description`); } } - form.append(fieldWrapper); + form.append(el); }); - - form.addEventListener('change', () => applyRules(form, rules)); - applyRules(form, rules); - fill(form); - return (form); + groupFieldsByFieldSet(form); + const transformRequest = await applyTransformation(data, form); + // eslint-disable-next-line prefer-destructuring + form.dataset.action = pathname?.split('.json')[0]; + form.addEventListener('submit', (e) => { + e.preventDefault(); + e.submitter.setAttribute('disabled', ''); + handleSubmit(form, transformRequest); + }); + return form; } export default async function decorate(block) { - const form = block.querySelector('a[href$=".json"]'); - if (form) { - form.replaceWith(await createForm(form.href)); + const formLink = block.querySelector('a[href$=".json"]'); + console.log(formLink); + if (formLink) { + const form = await createForm(formLink.href); + formLink.replaceWith(form); + + const config = readBlockConfig(block); + Object.entries(config).forEach(([key, value]) => { if (value) form.dataset[key] = value; }); } } diff --git a/blocks/form/formatting.js b/blocks/form/formatting.js new file mode 100644 index 00000000..d5405c92 --- /dev/null +++ b/blocks/form/formatting.js @@ -0,0 +1,37 @@ +const decimalSymbol = '.'; +const groupingSymbol = ','; +const minFractionDigits = '00'; + +function toString(num) { + const [integer, fraction] = num.toString().split('.'); + const formattedInteger = integer.replace(/(\d)(?=(\d\d\d)+(?!\d))/g, `$1${groupingSymbol}`); + let formattedFraction = fraction || minFractionDigits; + const lengthFraction = formattedFraction.length; + if (lengthFraction < minFractionDigits) { + formattedFraction += Array(minFractionDigits - lengthFraction).fill(0).join(''); + } + return `${formattedInteger}${decimalSymbol}${fraction || '00'}`; +} + +function currency(num, currencySymbol = '$') { + return `${currencySymbol} ${toString(num)}`; +} + +function year(num) { + if (num < 1) { + return `${num * 12} months`; + } if (num === 1) { + return `${num} year`; + } + return `${num} years`; +} + +function percent(num) { + return `${(num * 100).toFixed(2).replace(/(\.0*$)|0*$/, '')}%`; +} + +export default { + currency, + year, + '%': percent, +}; diff --git a/blocks/form/rules/RuleCompiler.js b/blocks/form/rules/RuleCompiler.js new file mode 100644 index 00000000..d30818af --- /dev/null +++ b/blocks/form/rules/RuleCompiler.js @@ -0,0 +1,54 @@ +const cellNameRegex = /^\$?[A-Z]+\$?(\d+)$/; + +function visitor(nameMap, fields, bExcelFormula) { + return function visit(n) { + if (n.type === 'Field') { + const name = n?.name; + let field; + if (bExcelFormula) { + const match = cellNameRegex.exec(name); + if (match?.[1]) { + field = nameMap[match[1]]; + } + if (!field) { + // eslint-disable-next-line no-console + console.log(`Unknown column used in excel formula ${n.name}`); + } else { + n.name = field.name; + fields.add(field.id); + } + } else { + fields.add(name); + } + } if (n.type === 'Function') { + n.name = n.name.toLowerCase(); + } else if (n.type === 'Subexpression') { + return visit({ + type: 'Field', + name: n.children[1].name, + }, n.children[0].name); + } + return { + ...n, + children: n.children?.map((c) => visit(c)), + }; + }; +} + +function updateCellNames(ast, rowNumberFieldMap, bExcelFormula = true) { + const fields = new Set(); + const newAst = visitor(rowNumberFieldMap, fields, bExcelFormula)(ast); + return [newAst, Array.from(fields)]; +} + +export default function transformRule({ prop, expression }, fieldToCellMap, formula) { + const biSExcelFormula = expression.startsWith('='); + const updatedExpression = biSExcelFormula ? expression.slice(1) : expression; + const ast = formula.compile(updatedExpression); + const [newAst, deps] = updateCellNames(ast, fieldToCellMap, biSExcelFormula); + return { + prop, + deps, + ast: newAst, + }; +} diff --git a/blocks/form/rules/RuleEngine.js b/blocks/form/rules/RuleEngine.js new file mode 100644 index 00000000..ff24cf7a --- /dev/null +++ b/blocks/form/rules/RuleEngine.js @@ -0,0 +1,254 @@ +/* eslint-disable max-classes-per-file */ +import Formula from './parser/Formula.js'; +import transformRule from './RuleCompiler.js'; +import formatFns from '../formatting.js'; + +function stripTags(input, allowd) { + const allowed = ((`${allowd || ''}`) + .toLowerCase() + .match(/<[a-z][a-z0-9]*>/g) || []) + .join(''); // making sure the allowed arg is a string containing only tags in lowercase () + const tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi; + const comments = //gi; + return input.replace(comments, '') + .replace(tags, ($0, $1) => (allowed.indexOf(`<${$1.toLowerCase()}>`) > -1 ? $0 : '')); +} + +export function sanitizeHTML(input) { + return stripTags(input, ''); +} + +function coerceValue(val) { + if (val === 'true') return true; + if (val === 'false') return false; + return val; +} + +const isFieldset = (e) => e.tagName === 'FIELDSET'; + +const isRepeatableFieldset = (e) => isFieldset(e) && e.getAttribute('data-repeatable') === 'true'; + +const isDataElement = (element) => element.tagName !== 'BUTTON' && !isFieldset(element) && element.name; + +function getValue(fe) { + if (fe.type === 'checkbox' || fe.type === 'radio') { + if (fe.checked) return coerceValue(fe.value); + } else if (fe.tagName === 'OUTPUT') { + return fe.dataset.value; + } else if (fe.name) { + return coerceValue(fe.value); + } + return undefined; +} + +function constructData(elements) { + const payload = {}; + elements.filter(isDataElement) + .forEach((fe) => { + payload[fe.name] = getValue(fe); + }); + return payload; +} + +function getFieldsetPayload(form, fieldsetName) { + let fieldsets = form.elements[fieldsetName]; + if (!(fieldsets instanceof RadioNodeList)) { + fieldsets = [fieldsets]; + } + const payload = {}; + fieldsets.forEach((fe, i) => { + [...fe.elements].filter(isDataElement).forEach((e) => { + payload[e.name] = payload[e.name] || []; + payload[e.name][i] = getValue(e); + }); + }); + return payload; +} + +function constructPayload(form) { + const elements = [...form.elements]; + const payload = constructData(elements); + const fieldsetNames = [...elements.filter(isRepeatableFieldset) + .reduce((names, x) => { + names.add(x.name); + return names; + }, new Set())]; + return fieldsetNames.reduce((currPayload, x) => { + const fieldsetPayload = getFieldsetPayload(form, x); + return { + ...currPayload, + ...fieldsetPayload, + }; + }, payload); +} + +export default class RuleEngine { + rulesOrder = {}; + + constructor(formRules, fieldIdMap, formTag) { + this.formTag = formTag; + this.data = constructPayload(formTag); + this.formula = new Formula(); + const newRules = formRules.map(([fieldId, fieldRules]) => [ + fieldId, + fieldRules.map((rule) => transformRule(rule, fieldIdMap, this.formula)), + ]); + + this.formRules = Object.fromEntries(newRules); + this.dependencyTree = newRules.reduce((fields, [fieldId, rules]) => { + fields[fieldId] = fields[fieldId] || { deps: {} }; + rules.forEach(({ prop, deps }) => { + deps.forEach((dep) => { + fields[dep] = fields[dep] || { deps: {} }; + fields[dep].deps[prop] = fields[dep].deps[prop] || []; + fields[dep].deps[prop].push(fieldId); + }); + }); + return fields; + }, {}); + } + + listRules(fieldId) { + const arr = {}; + let index = 0; + const stack = [fieldId]; + do { + const el = stack.pop(); + arr[el] = index; + index += 1; + if (this.dependencyTree[el]?.deps.Value) { + stack.push(...this.dependencyTree[el].deps.Value); + } + // eslint-disable-next-line no-loop-func + ['Visible', 'Label'].forEach((prop) => { + this.dependencyTree[el]?.deps[prop]?.forEach((field) => { + arr[field] = index; + index += 1; + }); + }); + // @todo add label deps as well. + } while (stack.length > 0); + return Object.entries(arr).sort((a, b) => a[1] - b[1]).map((_) => _[0]).slice(1); + } + + updateValue(fieldId, value) { + const element = document.getElementById(fieldId); + if (!(element instanceof NodeList)) { + this.data[element.name] = coerceValue(value); + const { displayFormat } = element.dataset; + if (element.tagName === 'OUTPUT') { + const formatFn = formatFns[displayFormat] || ((x) => x); + element.value = formatFn(value); + element.dataset.value = value; + } else { + element.value = value; + } + if (element.type === 'range') { + element.dispatchEvent(new CustomEvent('input', { bubbles: false })); + } + } + } + + // eslint-disable-next-line class-methods-use-this + updateVisible(fieldId, value) { + const element = document.getElementById(fieldId); + let wrapper = element; + if (!isFieldset(element)) { + wrapper = element.closest('.field-wrapper'); + } + wrapper.dataset.visible = value; + } + + // eslint-disable-next-line class-methods-use-this + updateLabel(fieldId, value) { + const element = document.getElementById(fieldId); + const label = element.closest('.field-wrapper').querySelector('.field-label'); + label.innerHTML = sanitizeHTML(value); + } + + setData(field) { + const fieldName = field.name; + if (field.type === 'checkbox') { + this.data[fieldName] = field.checked ? coerceValue(field.value) : undefined; + } else { + this.data[fieldName] = coerceValue(field.value); + } + } + + applyRules(rules) { + rules.forEach((fId) => { + this.formRules[fId]?.forEach((rule) => { + const newValue = this.formula.evaluate(rule.ast, this.data); + const handler = this[`update${rule.prop}`]; + if (handler instanceof Function) { + handler.apply(this, [fId, newValue]); + } + }); + }); + } + + getRules(id) { + if (!this.rulesOrder[id]) { + this.rulesOrder[id] = this.listRules(id); + } + return this.rulesOrder[id]; + } + + enable() { + this.formTag.addEventListener('input', (e) => { + const field = e.target; + const valid = e.target.checkValidity(); + if (valid) { + let fieldId = field.id; + let rules = []; + const fieldset = field.closest('fieldset'); + if (fieldset && fieldset.getAttribute('data-repeatable') === 'true') { + this.data = { + ...this.data, + ...getFieldsetPayload(this.formTag, fieldset.name), + }; + fieldId = field.name; + } else { + this.setData(field); + } + if (field.type === 'radio') { + const radios = this.formTag.elements[field.name]; + if (radios instanceof RadioNodeList) { + rules = [...radios].flatMap((f) => this.getRules(f.id)); + } + } else { + rules = this.getRules(fieldId); + } + this.applyRules(rules); + } + }); + + this.formTag.addEventListener('item:add', (e) => { + const fieldsetName = e.detail.item.name; + let fieldset = this.formTag.elements[fieldsetName]; + if (fieldset instanceof RadioNodeList) { + fieldset = fieldset.item(0); + } + this.data = { + ...this.data, + ...getFieldsetPayload(this.formTag, fieldsetName), + }; + const rules = [...fieldset.elements].map((fd) => this.getRules(fd.name)).flat(); + this.applyRules(rules); + }); + + this.formTag.addEventListener('item:remove', (e) => { + const fieldsetName = e.detail.item.name; + let fieldset = this.formTag.elements[fieldsetName]; + if (fieldset instanceof RadioNodeList) { + fieldset = fieldset.item(0); + } + this.data = { + ...this.data, + ...getFieldsetPayload(this.formTag, fieldsetName), + }; + const rules = [...fieldset.elements].map((fd) => this.getRules(fd.name)).flat(); + this.applyRules(rules); + }); + } +} diff --git a/blocks/form/rules/index.js b/blocks/form/rules/index.js new file mode 100644 index 00000000..7e56ffd9 --- /dev/null +++ b/blocks/form/rules/index.js @@ -0,0 +1,40 @@ +export function getRules(fd) { + const entries = [ + ['Value', fd?.['Value Expression']], + ['Visible', fd?.['Visible Expression']], + ['Label', fd?.['Label Expression']], + ]; + return entries.filter((e) => e[1]).map(([prop, expression]) => ({ + prop, + expression, + })); +} + +function extractRules(data) { + return data + .reduce(({ fieldIdMap, rules }, fd, index) => { + const currentRules = getRules(fd); + return { + fieldIdMap: { + ...fieldIdMap, + [index + 2]: { name: fd.Name, id: fd.Id }, + }, + rules: currentRules.length ? rules.concat([[fd.Id, currentRules]]) : rules, + }; + }, { fieldIdMap: {}, rules: [] }); +} + +export async function applyRuleEngine(form, formTag) { + try { + const RuleEngine = (await import('./RuleEngine.js')).default; + + const formData = extractRules(form); + const { fieldIdMap, rules } = formData; + + const ruleEngine = new RuleEngine(rules, fieldIdMap, formTag); + ruleEngine.enable(); + } catch (e) { + // eslint-disable-next-line no-console + console.log('unable to apply rules ', e); + } +} diff --git a/blocks/form/rules/parser/Formula.js b/blocks/form/rules/parser/Formula.js new file mode 100644 index 00000000..d74dbdf7 --- /dev/null +++ b/blocks/form/rules/parser/Formula.js @@ -0,0 +1,39 @@ +import Parser from './Parser.js'; +import TreeInterpreter from './TreeInterpreter.js'; +import Runtime from './Runtime.js'; + +export default class Formula { + constructor(customFunctions) { + this.debug = []; + this.runtime = new Runtime(this.debug, customFunctions); + } + + compile(stream) { + let ast; + try { + const parser = new Parser(); + ast = parser.parse(stream, this.debug); + } catch (e) { + this.debug.push(e.toString()); + throw e; + } + return ast; + } + + evaluate(node, data) { + // This needs to be improved. Both the interpreter and runtime depend on + // each other. The runtime needs the interpreter to support exprefs. + // There's likely a clean way to avoid the cyclic dependency. + this.runtime.interpreter = new TreeInterpreter( + this.runtime, + this.debug, + ); + + try { + return this.runtime.interpreter.search(node, data); + } catch (e) { + this.debug.push(e.message || e.toString()); + throw e; + } + } +} diff --git a/blocks/form/rules/parser/Lexer.js b/blocks/form/rules/parser/Lexer.js new file mode 100644 index 00000000..72ce6cfb --- /dev/null +++ b/blocks/form/rules/parser/Lexer.js @@ -0,0 +1,258 @@ +import tokenDefinitions from './Tokens.js'; + +const { + TOK_ADD, + TOK_COMMA, + TOK_CONCATENATE, + TOK_DIVIDE, + TOK_EQ, + TOK_GT, + TOK_GTE, + TOK_LITERAL, + TOK_LPAREN, + TOK_LT, + TOK_LTE, + TOK_MULTIPLY, + TOK_NE, + TOK_NUMBER, + TOK_QUOTEDIDENTIFIER, + TOK_RPAREN, + TOK_SUBTRACT, + TOK_UNARY_MINUS, + TOK_UNQUOTEDIDENTIFIER, + TOK_SHEET_ACCESS, +} = tokenDefinitions; + +const basicTokens = { + '!': TOK_SHEET_ACCESS, + ',': TOK_COMMA, + '(': TOK_LPAREN, + ')': TOK_RPAREN, +}; + +const operatorStartToken = { + '<': true, + '>': true, + '=': true, +}; + +const skipChars = { + ' ': true, + '\t': true, + '\n': true, +}; + +function isNum(ch) { + return (ch >= '0' && ch <= '9') || (ch === '.'); +} + +function isAlphaNum(ch) { + return (ch >= 'a' && ch <= 'z') + || (ch >= 'A' && ch <= 'Z') + || (ch >= '0' && ch <= '9') + || ch === '_'; +} + +function isIdentifier(stream, pos) { + const ch = stream[pos]; + // $ is special -- it's allowed to be part of an identifier if it's the first character + if (ch === '$') { + return stream.length > pos && isAlphaNum(stream[pos + 1]); + } + // return whether character 'isAlpha' + return (ch >= 'a' && ch <= 'z') + || (ch >= 'A' && ch <= 'Z') + || ch === '_'; +} + +export default class Lexer { + constructor(debug = []) { + this.debug = debug; + } + + tokenize(stream) { + const tokens = []; + this.current = 0; + let start; + let identifier; + let token; + while (this.current < stream.length) { + const prev = tokens.length ? tokens.slice(-1)[0].type : null; + + if (isIdentifier(stream, this.current)) { + start = this.current; + identifier = this.consumeUnquotedIdentifier(stream); + tokens.push({ + type: TOK_UNQUOTEDIDENTIFIER, + value: identifier, + start, + }); + } else if (basicTokens[stream[this.current]] !== undefined) { + tokens.push({ + type: basicTokens[stream[this.current]], + value: stream[this.current], + start: this.current, + }); + this.current += 1; + } else if (stream[this.current] === '-' + && ![TOK_NUMBER, TOK_RPAREN, TOK_UNQUOTEDIDENTIFIER, TOK_QUOTEDIDENTIFIER].includes(prev)) { + token = { type: TOK_UNARY_MINUS, value: '-', start: this.current }; + this.current += 1; + tokens.push(token); + } else if (isNum(stream[this.current])) { + token = this.consumeNumber(stream); + tokens.push(token); + } else if (stream[this.current] === "'") { + start = this.current; + identifier = this.consumeQuotedIdentifier(stream); + tokens.push({ + type: TOK_QUOTEDIDENTIFIER, + value: identifier, + start, + }); + } else if (stream[this.current] === '"') { + start = this.current; + identifier = this.consumeRawStringLiteral(stream); + tokens.push({ + type: TOK_LITERAL, + value: identifier, + start, + }); + } else if (operatorStartToken[stream[this.current]] !== undefined) { + tokens.push(this.consumeOperator(stream)); + } else if (skipChars[stream[this.current]] !== undefined) { + // Ignore whitespace. + this.current += 1; + } else if (stream[this.current] === '&') { + tokens.push({ type: TOK_CONCATENATE, value: '&', start: this.current }); + this.current += 1; + } else if (stream[this.current] === '+') { + tokens.push({ type: TOK_ADD, value: '+', start: this.current }); + this.current += 1; + } else if (stream[this.current] === '-') { + tokens.push({ type: TOK_SUBTRACT, value: '-', start: this.current }); + this.current += 1; + } else if (stream[this.current] === '*') { + tokens.push({ type: TOK_MULTIPLY, value: '*', start: this.current }); + this.current += 1; + } else if (stream[this.current] === '/') { + tokens.push({ type: TOK_DIVIDE, value: '/', start: this.current }); + this.current += 1; + } else { + const error = new Error(`Unknown character:${stream[this.current]}`); + error.name = 'LexerError'; + throw error; + } + } + return tokens; + } + + consumeUnquotedIdentifier(stream) { + const start = this.current; + this.current += 1; + while (this.current < stream.length && isAlphaNum(stream[this.current])) { + this.current += 1; + } + return stream.slice(start, this.current); + } + + consumeQuotedIdentifier(stream) { + const start = this.current; + this.current += 1; + const maxLength = stream.length; + let foundNonAlpha = !isIdentifier(stream, start + 1); + while (stream[this.current] !== "'" && this.current < maxLength) { + // You can escape a quote and you can escape an escape. + let { current } = this; + if (!isAlphaNum(stream[current])) foundNonAlpha = true; + if (stream[current] === '\\' && (stream[current + 1] === '\\' + || stream[current + 1] === "'")) { + current += 2; + } else { + current += 1; + } + this.current = current; + } + this.current += 1; + const val = stream.slice(start, this.current); + // Check for unnecessary double quotes. + // json-formula uses double quotes to escape characters that don't belong in names names. + // e.g. 'purchase-order'.address + // If we find a quoted entity with spaces or all legal characters, issue a warning + try { + if (!foundNonAlpha || val.includes(' ')) { + this.debug.push(`Suspicious quotes: ${val}`); + this.debug.push(`Did you intend a literal? '${val.replace(/'/g, '')}'?`); + } + // eslint-disable-next-line no-empty + } catch (e) { } + return val.substring(1, val.length - 1); + } + + consumeRawStringLiteral(stream) { + const start = this.current; + this.current += 1; + const maxLength = stream.length; + while (stream[this.current] !== '"' && this.current < maxLength) { + // You can escape a single quote and you can escape an escape. + let { current } = this; + if (stream[current] === '\\' && (stream[current + 1] === '\\' + || stream[current + 1] === '"')) { + current += 2; + } else { + current += 1; + } + this.current = current; + } + this.current += 1; + const literal = stream.slice(start + 1, this.current - 1); + return literal.replaceAll('\\"', '"'); + } + + consumeNumber(stream) { + const start = this.current; + this.current += 1; + const maxLength = stream.length; + while (isNum(stream[this.current]) && this.current < maxLength) { + this.current += 1; + } + const n = stream.slice(start, this.current); + let value; + if (n.includes('.')) { + value = parseFloat(n); + } else { + value = parseInt(n, 10); + } + return { type: TOK_NUMBER, value, start }; + } + + consumeOperator(stream) { + const start = this.current; + const startingChar = stream[start]; + this.current += 1; + if (startingChar === '<') { + if (stream[this.current] === '=') { + this.current += 1; + return { type: TOK_LTE, value: '<=', start }; + } + if (stream[this.current] === '>') { + this.current += 1; + return { type: TOK_NE, value: '<>', start }; + } + return { type: TOK_LT, value: '<', start }; + } + if (startingChar === '>') { + if (stream[this.current] === '=') { + this.current += 1; + return { type: TOK_GTE, value: '>=', start }; + } + return { type: TOK_GT, value: '>', start }; + } + // startingChar is '=' + if (stream[this.current] === '=') { + this.current += 1; + return { type: TOK_EQ, value: '==', start }; + } + return { type: TOK_EQ, value: '=', start }; + } +} diff --git a/blocks/form/rules/parser/Parser.js b/blocks/form/rules/parser/Parser.js new file mode 100644 index 00000000..e8bde657 --- /dev/null +++ b/blocks/form/rules/parser/Parser.js @@ -0,0 +1,222 @@ +import Lexer from './Lexer.js'; +import tokenDefinitions from './Tokens.js'; + +const { + TOK_EOF, + TOK_ADD, + TOK_COMMA, + TOK_CONCATENATE, + TOK_DIVIDE, + TOK_EQ, + TOK_FIELD, + TOK_GT, + TOK_GTE, + TOK_LITERAL, + TOK_LPAREN, + TOK_LT, + TOK_LTE, + TOK_MULTIPLY, + TOK_NE, + TOK_NUMBER, + TOK_QUOTEDIDENTIFIER, + TOK_RPAREN, + TOK_SUBTRACT, + TOK_UNARY_MINUS, + TOK_UNQUOTEDIDENTIFIER, + TOK_SHEET_ACCESS, +} = tokenDefinitions; + +const bindingPower = { + [TOK_EOF]: 0, + [TOK_UNQUOTEDIDENTIFIER]: 0, + [TOK_QUOTEDIDENTIFIER]: 0, + [TOK_RPAREN]: 0, + [TOK_NUMBER]: 0, + [TOK_FIELD]: 0, + [TOK_COMMA]: 0, + [TOK_CONCATENATE]: 5, + [TOK_ADD]: 6, + [TOK_SUBTRACT]: 6, + [TOK_MULTIPLY]: 7, + [TOK_DIVIDE]: 7, + [TOK_EQ]: 5, + [TOK_GT]: 5, + [TOK_LT]: 5, + [TOK_GTE]: 5, + [TOK_LTE]: 5, + [TOK_NE]: 5, + [TOK_UNARY_MINUS]: 30, + [TOK_SHEET_ACCESS]: 40, + [TOK_LPAREN]: 60, +}; + +export default class Parser { + parse(expression, debug) { + this.loadTokens(expression, debug); + this.index = 0; + const ast = this.expression(0); + if (this.lookahead(0) !== TOK_EOF) { + const t = this.lookaheadToken(0); + const error = new Error( + `Unexpected token type: ${t.type}, value: ${t.value}`, + ); + error.name = 'ParserError'; + throw error; + } + return ast; + } + + loadTokens(expression, debug) { + const lexer = new Lexer(debug); + const tokens = lexer.tokenize(expression); + tokens.push({ type: TOK_EOF, value: '', start: expression.length }); + this.tokens = tokens; + } + + expression(rbp) { + const leftToken = this.lookaheadToken(0); + this.advance(); + let left = this.nud(leftToken); + let currentToken = this.lookahead(0); + while (rbp < bindingPower[currentToken]) { + this.advance(); + left = this.led(currentToken, left); + currentToken = this.lookahead(0); + } + return left; + } + + lookahead(number) { + return this.tokens[this.index + number].type; + } + + lookaheadToken(number) { + return this.tokens[this.index + number]; + } + + advance() { + this.index += 1; + } + + // eslint-disable-next-line consistent-return + nud(token) { + let right; + let expression; + let node; + let args; + switch (token.type) { + case TOK_LITERAL: + return { type: 'Literal', value: token.value }; + case TOK_NUMBER: + return { type: 'Number', value: token.value }; + case TOK_UNQUOTEDIDENTIFIER: + return { type: 'Field', name: token.value }; + case TOK_QUOTEDIDENTIFIER: + node = { type: 'Field', name: token.value }; + if (this.lookahead(0) === TOK_LPAREN) { + throw new Error('Quoted identifier not allowed for function names.'); + } + return node; + case TOK_UNARY_MINUS: + right = this.expression(bindingPower.UnaryMinus); + return { type: 'UnaryMinusExpression', children: [right] }; + case TOK_FIELD: + return { type: TOK_FIELD }; + case TOK_LPAREN: + args = []; + while (this.lookahead(0) !== TOK_RPAREN) { + expression = this.expression(0); + args.push(expression); + } + this.match(TOK_RPAREN); + return args[0]; + default: + this.errorToken(token); + } + } + + // eslint-disable-next-line consistent-return + led(tokenName, left) { + let right; + let name; + let args; + let expression; + let node; + let rbp; + switch (tokenName) { + case TOK_SHEET_ACCESS: + rbp = bindingPower.Sheet; + right = this.parseSheetRHS(rbp); + return { type: 'Subexpression', children: [left, right] }; + case TOK_CONCATENATE: + right = this.expression(bindingPower.Concatenate); + return { type: 'ConcatenateExpression', children: [left, right] }; + case TOK_ADD: + right = this.expression(bindingPower.Add); + return { type: 'AddExpression', children: [left, right] }; + case TOK_SUBTRACT: + right = this.expression(bindingPower.Subtract); + return { type: 'SubtractExpression', children: [left, right] }; + case TOK_MULTIPLY: + right = this.expression(bindingPower.Multiply); + return { type: 'MultiplyExpression', children: [left, right] }; + case TOK_DIVIDE: + right = this.expression(bindingPower.Divide); + return { type: 'DivideExpression', children: [left, right] }; + case TOK_LPAREN: + name = left.name; + args = []; + while (this.lookahead(0) !== TOK_RPAREN) { + expression = this.expression(0); + if (this.lookahead(0) === TOK_COMMA) { + this.match(TOK_COMMA); + } + args.push(expression); + } + this.match(TOK_RPAREN); + node = { type: 'Function', name, children: args }; + return node; + case TOK_EQ: + case TOK_NE: + case TOK_GT: + case TOK_GTE: + case TOK_LT: + case TOK_LTE: + return this.parseComparator(left, tokenName); + default: + this.errorToken(this.lookaheadToken(0)); + } + } + + match(tokenType) { + if (this.lookahead(0) === tokenType) { + this.advance(); + } else { + const t = this.lookaheadToken(0); + const error = new Error(`Expected ${tokenType}, got: ${t.type}`); + error.name = 'ParserError'; + throw error; + } + } + + // eslint-disable-next-line class-methods-use-this + errorToken(token) { + const error = new Error(`Invalid token (${token.type}): "${token.value}"`); + error.name = 'ParserError'; + throw error; + } + + parseComparator(left, comparator) { + const right = this.expression(bindingPower[comparator]); + return { type: 'Comparator', name: comparator, children: [left, right] }; + } + + // eslint-disable-next-line consistent-return + parseSheetRHS(rbp) { + const lookahead = this.lookahead(0); + const exprTokens = [TOK_UNQUOTEDIDENTIFIER]; + if (exprTokens.indexOf(lookahead) >= 0) { + return this.expression(rbp); + } + } +} diff --git a/blocks/form/rules/parser/Runtime.js b/blocks/form/rules/parser/Runtime.js new file mode 100644 index 00000000..efc54eea --- /dev/null +++ b/blocks/form/rules/parser/Runtime.js @@ -0,0 +1,16 @@ +import functions from './functions.js'; + +export default class Runtime { + constructor(debug, toNumber) { + this.toNumber = toNumber; + this.functionTable = functions(debug); + } + + callFunction(name, resolvedArgs, data, interpreter) { + // this check will weed out 'valueOf', 'toString' etc + if (!Object.prototype.hasOwnProperty.call(this.functionTable, name)) throw new Error(`Unknown function: ${name}()`); + + const functionEntry = this.functionTable[name]; + return functionEntry.func.call(this, resolvedArgs, data, interpreter); + } +} diff --git a/blocks/form/rules/parser/Tokens.js b/blocks/form/rules/parser/Tokens.js new file mode 100644 index 00000000..2df2ecec --- /dev/null +++ b/blocks/form/rules/parser/Tokens.js @@ -0,0 +1,24 @@ +export default { + TOK_EOF: 'EOF', + TOK_ADD: 'Add', + TOK_COMMA: 'Comma', + TOK_CONCATENATE: 'Concatenate', + TOK_DIVIDE: 'Divide', + TOK_EQ: 'EQ', + TOK_FIELD: 'Field', + TOK_GT: 'GT', + TOK_GTE: 'GTE', + TOK_LITERAL: 'Literal', + TOK_LPAREN: 'Lparen', + TOK_LT: 'LT', + TOK_LTE: 'LTE', + TOK_MULTIPLY: 'Multiply', + TOK_NE: 'NE', + TOK_NUMBER: 'Number', + TOK_QUOTEDIDENTIFIER: 'QuotedIdentifier', + TOK_RPAREN: 'Rparen', + TOK_SUBTRACT: 'Subtract', + TOK_UNARY_MINUS: 'UnaryMinus', + TOK_UNQUOTEDIDENTIFIER: 'UnquotedIdentifier', + TOK_SHEET_ACCESS: 'Sheet', +}; diff --git a/blocks/form/rules/parser/TreeInterpreter.js b/blocks/form/rules/parser/TreeInterpreter.js new file mode 100644 index 00000000..188a6d38 --- /dev/null +++ b/blocks/form/rules/parser/TreeInterpreter.js @@ -0,0 +1,152 @@ +import tokenDefinitions from './Tokens.js'; +import { + getToNumber, +} from './utils.js'; + +const { + TOK_EQ, + TOK_GT, + TOK_LT, + TOK_GTE, + TOK_LTE, + TOK_NE, +} = tokenDefinitions; + +export default class TreeInterpreter { + constructor(runtime, debug) { + this.runtime = runtime; + this.debug = debug; + this.toNumber = getToNumber(debug); + } + + search(node, value) { + return this.visit(node, value); + } + + visit(n, v) { + const visitFunctions = { + Field: (node, value) => { + // we used to check isObject(value) here -- but it is possible for an array-based + // object to have properties. So we'll allow the child check on objects and arrays. + if (value !== null) { + let field = value[node.name]; + // fields can be objects with overridden methods. e.g. valueOf + // so don't resolve to a function... + if (typeof field === 'function') field = undefined; + if (field === undefined) { + try { + this.debug.push(`Failed to find: '${node.name}'`); + const available = Object.keys(value).map((a) => `'${a}'`).toString(); + if (available.length) this.debug.push(`Available fields: ${available}`); + // eslint-disable-next-line no-empty + } catch (e) { } + return null; + } + return field; + } + return null; + }, + + Comparator: (node, value) => { + const first = this.visit(node.children[0], value); + const second = this.visit(node.children[1], value); + + if (node.name === TOK_EQ) return first === second; + if (node.name === TOK_NE) return first !== second; + if (node.name === TOK_GT) return first > second; + if (node.name === TOK_GTE) return first >= second; + if (node.name === TOK_LT) return first < second; + if (node.name === TOK_LTE) return first <= second; + throw new Error(`Unknown comparator: ${node.name}`); + }, + + Identity: (_node, value) => value, + + AddExpression: (node, value) => { + const first = this.visit(node.children[0], value); + const second = this.visit(node.children[1], value); + return this.applyOperator(first, second, '+'); + }, + + ConcatenateExpression: (node, value) => { + let first = this.visit(node.children[0], value); + let second = this.visit(node.children[1], value); + first = first.toString(); + second = second.toString(); + return this.applyOperator(first, second, '&'); + }, + + SubtractExpression: (node, value) => { + const first = this.visit(node.children[0], value); + const second = this.visit(node.children[1], value); + return this.applyOperator(first, second, '-'); + }, + + MultiplyExpression: (node, value) => { + const first = this.visit(node.children[0], value); + const second = this.visit(node.children[1], value); + return this.applyOperator(first, second, '*'); + }, + + DivideExpression: (node, value) => { + const first = this.visit(node.children[0], value); + const second = this.visit(node.children[1], value); + return this.applyOperator(first, second, '/'); + }, + + UnaryMinusExpression: (node, value) => { + const first = this.visit(node.children[0], value); + return first * -1; + }, + + Literal: (node) => node.value, + + Number: (node) => node.value, + + Function: (node, value) => { + // Special case for if() + // we need to make sure the results are called only after the condition is evaluated + // Otherwise we end up with both results invoked -- which could include side effects + // For "if", the last parameter to callFunction is false (bResolved) to indicate there's + // no point in validating the argument type. + if (node.name === 'if') return this.runtime.callFunction(node.name, node.children, value, this, false); + const resolvedArgs = node.children.map((child) => this.visit(child, value)); + return this.runtime.callFunction(node.name, resolvedArgs, value, this); + }, + + }; + const fn = n && visitFunctions[n.type]; + if (!fn) throw new Error(`Unknown/missing node type ${(n && n.type) || ''}`); + return fn(n, v); + } + + applyOperator(first, second, operator) { + if (Array.isArray(first) && Array.isArray(second)) { + // balance the size of the arrays + const shorter = first.length < second.length ? first : second; + const diff = Math.abs(first.length - second.length); + shorter.length += diff; + shorter.fill(null, shorter.length - diff); + const result = []; + for (let i = 0; i < first.length; i += 1) { + result.push(this.applyOperator(first[i], second[i], operator)); + } + return result; + } + + if (Array.isArray(first)) return first.map((a) => this.applyOperator(a, second, operator)); + if (Array.isArray(second)) return second.map((a) => this.applyOperator(first, a, operator)); + + if (operator === '*') return this.toNumber(first, this.debug) * this.toNumber(second, this.debug); + if (operator === '&') return first.toString() + second.toString(); + if (operator === '+') { + return this.toNumber(first, this.debug) + this.toNumber(second, this.debug); + } + if (operator === '-') return this.toNumber(first, this.debug) - this.toNumber(second, this.debug); + if (operator === '/') { + const result = first / second; + return Number.isFinite(result) ? result : null; + } + throw new Error(`Unknown operator: ${operator}`); + } +} diff --git a/blocks/form/rules/parser/functions.js b/blocks/form/rules/parser/functions.js new file mode 100644 index 00000000..c9dd8549 --- /dev/null +++ b/blocks/form/rules/parser/functions.js @@ -0,0 +1,116 @@ +import { + getValueOf, getToNumber, +} from './utils.js'; + +export default function functions(debug) { + const toNumber = getToNumber(debug); + const fnMap = { + and: { + func: (resolvedArgs) => { + let result = !!getValueOf(resolvedArgs[0]); + resolvedArgs.slice(1).forEach((arg) => { + result = result && !!getValueOf(arg); + }); + return result; + }, + }, + + false: { + func: () => false, + }, + + if: { + func: (unresolvedArgs, data, interpreter) => { + const conditionNode = unresolvedArgs[0]; + const leftBranchNode = unresolvedArgs[1]; + const rightBranchNode = unresolvedArgs[2]; + const condition = interpreter.visit(conditionNode, data); + if (getValueOf(condition)) { + return interpreter.visit(leftBranchNode, data); + } + return interpreter.visit(rightBranchNode, data); + }, + }, + + not: { + func: (resolveArgs) => !getValueOf(resolveArgs[0]), + }, + + or: { + func: (resolvedArgs) => { + let result = !!getValueOf(resolvedArgs[0]); + resolvedArgs.slice(1).forEach((arg) => { + result = result || !!getValueOf(arg); + }); + return result; + }, + }, + + true: { + func: () => true, + }, + + power: { + func: (args) => { + const base = toNumber(args[0]); + const power = toNumber(args[1]); + return base ** power; + }, + }, + + round: { + func: (args) => { + const num = toNumber(args[0]); + const digits = toNumber(args[1]); + const precision = 10 ** digits; + return Math.round(num * precision) / precision; + }, + }, + + ceiling: { + func: (args) => { + const num = toNumber(args[0]); + const significance = toNumber(args[1]); + if (num === 0 || significance === 0) { + return 0; + } + return Math.ceil(num / significance) * significance; + }, + }, + + min: { + func: (args) => { + // flatten the args into a single array + const array = args.reduce((prev, cur) => { + if (Array.isArray(cur)) prev.push(...cur); + else prev.push(cur); + return prev; + }, []); + + const first = array.find((r) => r !== null); + if (array.length === 0 || first === undefined) return null; + // use the first value to determine the comparison type + const isNumber = !Number.isNaN(parseInt(first, 10)); + const compare = isNumber + ? (prev, cur) => { + const current = toNumber(cur); + return prev <= current ? prev : current; + } + : (prev, cur) => { + const current = toString(cur); + return prev.localeCompare(current) === 1 ? current : prev; + }; + + return array.reduce(compare, isNumber ? toNumber(first) : toString(first)); + }, + }, + + sum: { + func: (args) => args.reduce((sum, x) => { + if (Array.isArray(x)) return sum + fnMap.sum.func(x); + return sum + toNumber(x); + }, 0), + }, + }; + return fnMap; +} diff --git a/blocks/form/rules/parser/utils.js b/blocks/form/rules/parser/utils.js new file mode 100644 index 00000000..5f7f4ea6 --- /dev/null +++ b/blocks/form/rules/parser/utils.js @@ -0,0 +1,29 @@ +export function getValueOf(a) { + if (a === null || a === undefined) return a; + if (Array.isArray(a)) { + return a.map((i) => getValueOf(i)); + } + return a.valueOf(); +} + +const defaultStringToNumber = ((str) => { + const n = +str; + return Number.isNaN(n) ? 0 : n; +}); + +export function getToNumber(debug = []) { + return (value) => { + const n = getValueOf(value); // in case it's an object that implements valueOf() + if (n === null) return null; + if (Array.isArray(n)) { + debug.push('Converted array to zero'); + return 0; + } + const type = typeof n; + if (type === 'number') return n; + if (type === 'string') return defaultStringToNumber(n, debug); + if (type === 'boolean') return n ? 1 : 0; + debug.push('Converted object to zero'); + return 0; + }; +} diff --git a/tools/importer/forms/ui/css/styles.css b/tools/importer/forms/ui/css/styles.css new file mode 100644 index 00000000..36462f07 --- /dev/null +++ b/tools/importer/forms/ui/css/styles.css @@ -0,0 +1,316 @@ +.form-preview { + --form-columns: 1; +} +.container { + height: 89%; + display: flex; + flex-wrap: wrap; + line-height: normal; + font-size: 16px; +} +.container hr { + margin-top: 0; + border-bottom: 2px solid gray; +} + +.status { + display: flex; + align-items: center; +} + +.left { + max-width: 20%; + flex-basis: 20%; + padding: 24px; + border-right: 2px solid lightgray; +} + +.right { + flex-basis: 78%; + padding: 20px; +} + +.right .header { + display: flex; + justify-content: space-between; +} + +.icon { + width: 40px; + height: 40px; + display: block; +} +.recaptcha-icon { + background: url(../svg/recaptcha.svg) no-repeat; +} +.marketo-icon { + margin-top: -12px; + background: url(../svg/marketo.svg) no-repeat; +} + +.attachment-icon { + background: url(../svg/attachments.svg) no-repeat; +} + +.pdf-icon { + background: url(../svg/pdf.svg) no-repeat; +} + +form label { + width: 150px; +} + +.left :is(input, select) { + width: 100%; + padding: 12px 20px; + margin: 8px 0; + display: inline-block; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +button:disabled { + background-color: #cccccc; + color: #666666; + cursor: not-allowed; +} + +button { + background-color: #4CAF50; + color: white; + padding: 14px 20px; + margin: 8px 0; + border: none; + border-radius: 4px; + cursor: pointer; + height: 45px; +} + +.cards-container { + display: flex; + flex-wrap: wrap; + flex-direction: column; +} +.cards-container a { + text-decoration: none; +} +.card { + background-color: white; + color: #4a4a4a; + width: 100%; + position: relative; + border-top: 1px solid #dbdbdb; + cursor: pointer; +} +.card.selected { + border-bottom: 4px solid green; + border-top: 4px solid green; +} +.card .card-content .follow-info { + padding: 0 0 1rem; + display: flex; +} +.card .card-content .follow-info h2 { + text-align: center; + width: 50%; + margin: 0; + box-sizing: border-box; +} +.card .card-content .follow-info h2 div { + text-decoration: none; + display: flex; + flex-direction: column; + border-radius: 0.8rem; + transition: background-color 100ms ease-in-out; +} +.card .card-content .follow-info h2 div span { + color: #1c9eff; + font-weight: bold; +} +.card .card-content .follow-info h2 div small { + color: #afafaf; + font-size: 0.85rem; + font-weight: normal; +} +.card-header-title { + align-items: center; + color: #363636; + display: flex; + padding: 0 0.75rem; +} + +.card-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.features { + display: flex; +} + +.title { + color: #4CAF50; + font-weight: bold; +} + +.card-footer { + border-top: 1px solid #dbdbdb; + align-items: stretch; + display: flex; +} +card-footer-item:not(:last-child) { + border-right: 1px solid #dbdbdb; +} + +.card-footer-item { + align-items: center; + display: flex; + flex-basis: 0; + flex-grow: 1; + flex-shrink: 0; + padding: 0.75rem; +} + +.ribbon span { + display: block; + padding: 15px; + background-color: #db9c34; + box-shadow: 0 5px 10px rgba(0,0,0,.1); + color: #fff; + font-weight: bold +} + +.ribbon[data-pdf="true"] span { + background-color: #3498db; +} + +.toolbar { + display: flex; + justify-content: space-between; + gap: 20px; +} + +.smartfill button, +.toolbar button { + width: auto; +} +.stats { + display: flex; + flex-wrap: wrap; + gap: 20px; +} + +.stats > div { + text-align: center; + min-width: 110px; +} + +.stats span { + font-weight: 700; + font-size: 35px; + color: #1c9eff; +} + +.smartfill { + display: flex; + flex-wrap: wrap; + gap: 20px; + align-items: center; +} + +input[type=file] { + padding-top: 10px; +} + +input::file-selector-button { + color: dodgerblue !important; + background-color: #fff; +} + +#smartprefill:checked ~ .smartprefill-container { + display: block; +} + +#smartprefill:checked ~ .stats-container { + display: none; +} +.smartprefill-container { + display: none; +} + +.error { + color: red; +} + +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider { + background-color: #4CAF50; +} + +input:focus + .slider { + box-shadow: 0 0 1px #4CAF50; +} + +input:checked + .slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +.switchcontainer { + display: flex; + align-items: center; + gap: 20px; +} + +#editor { + width: 100%; + height: 500px; +} + +.hide { + display: none; +} + +iframe { + width: 50%; + height: 100%; +} \ No newline at end of file diff --git a/tools/importer/forms/ui/index.html b/tools/importer/forms/ui/index.html new file mode 100644 index 00000000..d23751f2 --- /dev/null +++ b/tools/importer/forms/ui/index.html @@ -0,0 +1,71 @@ + + + + + + + + + Forms Importer + + + + +
+
+
+
+

Configuration

+ +
+ + +
+ Include plain text + +
+
+ Use iframe + +
+ +
+
+
Status:
+
Click Scan to Start
+
+
+ +
+
+

Preview Form

+
+
+ JSON View + +
+ +
+
+
+
+
+ Scan & Select the form. +
+
+
+
+
+ + + + diff --git a/tools/importer/forms/ui/js/script.js b/tools/importer/forms/ui/js/script.js new file mode 100644 index 00000000..145cc3c9 --- /dev/null +++ b/tools/importer/forms/ui/js/script.js @@ -0,0 +1,165 @@ +/* + * 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 { generateFormRendition } from '../../../../../blocks/form/form.js'; + +/* eslint-disable no-undef */ + +const FORM_IMPORTER = 'https://g7ory75qdb.execute-api.ap-south-1.amazonaws.com/vega-services/importer'; + +const scanFormEl = document.querySelector('form'); +const domainEl = document.querySelector('#domainURL'); +const includePlainText = document.querySelector('#includePlainText'); +const startBtn = document.querySelector('#startBtn'); +const copyAction = document.querySelector('#copyAction'); +const msgEl = document.querySelector('#msg'); +const switchView = document.querySelector('#switchView'); +const cardsContainer = document.getElementById('cards-container'); +const formPreview = document.querySelector('.form-preview'); +const jsonPreview = document.querySelector('.json-preview'); +let forms = []; +let editor; +let article; +let selectedForm; + +function convertToCSV(fields, divider = '\t') { + if (fields && fields.length > 0) { + const keys = Object.keys(fields?.[0]); + const th = keys.join(divider); + const rows = fields + .map((field) => Object.values(field).join(divider)) + .join('\n'); + return `${th}\n${rows}`; + } + return 'table is empty'; +} + +function updateStatus(msg, completed = false, error = false) { + startBtn.disabled = !completed; + msgEl.textContent = msg; + if (error) { + msgEl.classList.add('error'); + } else { + msgEl.classList.remove('error'); + } +} + +function cleanUp() { + cardsContainer.innerHTML = ''; +} + +function cardTemplate(form, index) { + return `
+
+
${form.name}
+
+ ${form?.stats?.attachmentsUsed ? '' : ''} + ${form?.stats?.recaptchaUsed ? '' : ''} + ${form?.stats?.isMarketToForm ? '' : ''} +
+
+
+ +
+
+
+
`; +} + +function renderCards() { + cleanUp(); + const formCards = forms.map(cardTemplate).join(''); + cardsContainer.innerHTML += formCards; +} + +function loadForm(index) { + const formEl = document.createElement('form'); + selectedForm = forms[index]; + generateFormRendition(selectedForm?.data, formEl); + formPreview.replaceChildren(formEl); + // Set JSON data to the editor + editor.session.setValue(JSON.stringify(selectedForm, null, 2)); + copyAction.disabled = false; +} +async function previewForm(event) { + if (article) { + article.classList.toggle('selected'); + } + article = event?.target?.closest('article'); + if (article) { + article.classList.toggle('selected'); + const { index } = article.dataset; + loadForm(index); + } +} + +function setupJSONView() { + editor = ace.edit('editor'); + editor.setTheme('ace/theme/monokai'); + editor.session.setMode('ace/mode/json'); +} + +async function scanNow() { + const valid = scanFormEl.checkValidity(); + if (valid) { + cleanUp(); + updateStatus('Scan Initiated...'); + const domain = domainEl.value; + const payload = { url: domain, options: { includePlainText: includePlainText.checked } }; + try { + const response = await fetch(FORM_IMPORTER, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + if (response.ok) { + forms = await response.json(); + renderCards(forms); + updateStatus('Scanning Completed', true, false); + } else { + const msg = await response.text(); + updateStatus(`Failed to scan ${msg}`, true, true); + } + } catch (e) { + updateStatus(e.message, true, true); + } + startBtn.disabled = false; + } +} + +function copyToClipboard() { + if (selectedForm) { + const data = convertToCSV(selectedForm.data); + navigator.clipboard.writeText(data).then(() => { + // eslint-disable-next-line no-alert + alert('Copied to clipboard'); + }) + .catch(() => { + // eslint-disable-next-line no-alert + alert('Issue in copying to clipboard use json view'); + }); + } +} + +copyAction.addEventListener('click', copyToClipboard); +startBtn.addEventListener('click', scanNow); +cardsContainer.addEventListener('click', previewForm); +switchView.addEventListener('click', () => { + formPreview.classList.toggle('hide'); + jsonPreview.classList.toggle('hide'); +}); +setupJSONView(); diff --git a/tools/importer/forms/ui/svg/attachments.svg b/tools/importer/forms/ui/svg/attachments.svg new file mode 100644 index 00000000..c44f5cae --- /dev/null +++ b/tools/importer/forms/ui/svg/attachments.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tools/importer/forms/ui/svg/marketo.svg b/tools/importer/forms/ui/svg/marketo.svg new file mode 100644 index 00000000..017b7965 --- /dev/null +++ b/tools/importer/forms/ui/svg/marketo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tools/importer/forms/ui/svg/pdf.svg b/tools/importer/forms/ui/svg/pdf.svg new file mode 100644 index 00000000..637e1062 --- /dev/null +++ b/tools/importer/forms/ui/svg/pdf.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/importer/forms/ui/svg/recaptcha.svg b/tools/importer/forms/ui/svg/recaptcha.svg new file mode 100644 index 00000000..929c0bab --- /dev/null +++ b/tools/importer/forms/ui/svg/recaptcha.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/tools/sidekick/config.json b/tools/sidekick/config.json index b09d3339..1bf0f51b 100644 --- a/tools/sidekick/config.json +++ b/tools/sidekick/config.json @@ -12,7 +12,14 @@ "isPalette": true, "includePaths": [ "**.docx**" ], "paletteRect": "top: 50px; bottom: 10px; right: 10px; left: auto; width:400px; height: calc(100vh - 60px)" + }, + { + "id": "form-editor", + "title": "Form Editor", + "event": "editform", + "environments": ["preview"] } + ] } diff --git a/utils/tag.js b/utils/tag.js new file mode 100644 index 00000000..b264213e --- /dev/null +++ b/utils/tag.js @@ -0,0 +1,23 @@ +/** + * Create an element with ID, class, children, and attributes + * @param {String} tag the tag nav of the element + * @param {Object} attributes the attributes of the tag + * @param {HTMLElement} html the content of the element + * @returns {HTMLElement} the element created + */ +export default function createTag(tag, attributes, html) { + const el = document.createElement(tag); + if (html) { + if (html instanceof HTMLElement) { + el.append(html); + } else { + el.insertAdjacentHTML('beforeend', html); + } + } + if (attributes) { + Object.keys(attributes).forEach((key) => { + el.setAttribute(key, attributes[key]); + }); + } + return el; +} From 3651ed46cb16bda314859793642d86d1348b978b Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 6 Nov 2023 08:28:52 -0500 Subject: [PATCH 009/133] lots of lint changes --- blocks/cards/cta.css | 4 ++-- blocks/cards/icon.css | 3 +++ blocks/contact-us/contact-us.css | 3 ++- blocks/hero/default-hero.css | 1 + blocks/toc/toc.js | 4 +--- scripts/scripts.js | 2 -- styles/styles.css | 31 +++++++++++++--------------- tools/importer/forms/ui/js/script.js | 2 -- 8 files changed, 23 insertions(+), 27 deletions(-) diff --git a/blocks/cards/cta.css b/blocks/cards/cta.css index 054314d1..aa5a2f4f 100644 --- a/blocks/cards/cta.css +++ b/blocks/cards/cta.css @@ -81,7 +81,7 @@ font-size: var(--heading-font-size-xs); text-align: left; color: var(--gray); - margin: 12px 12px; + margin: 12px; } .cards.cta.block > ul > li p { @@ -93,7 +93,7 @@ } .cards.cta.block > ul > li p.button-container { - margin: 12px 12px; + margin: 12px; text-align: left; } diff --git a/blocks/cards/icon.css b/blocks/cards/icon.css index 9b38baf3..04244ccf 100644 --- a/blocks/cards/icon.css +++ b/blocks/cards/icon.css @@ -1,3 +1,4 @@ + .cards.icon.block { display: grid; gap: 24px; @@ -42,9 +43,11 @@ height: 100%; width: 100%; } + .cards.icon.block .card:last-of-type { border:0; } + .cards.icon.block .card h3 { margin-bottom: 1em; font-size: var(--heading-font-size-xs); diff --git a/blocks/contact-us/contact-us.css b/blocks/contact-us/contact-us.css index 04e83e91..21977081 100644 --- a/blocks/contact-us/contact-us.css +++ b/blocks/contact-us/contact-us.css @@ -22,7 +22,7 @@ main > .section.contact-us-container > .contact-us-wrapper { } .contact-us.block .button-container { - padding: 20px 0 20px 0; + padding: 20px; } .contact-us.block > div { @@ -49,6 +49,7 @@ main > .section.contact-us-container > .contact-us-wrapper { color: var(--white); font-size: var(--heading-font-size-l); } + @media screen and (min-width: 600px) { .contact-us.block { height: 460px; diff --git a/blocks/hero/default-hero.css b/blocks/hero/default-hero.css index 7b608e5d..9c08124f 100644 --- a/blocks/hero/default-hero.css +++ b/blocks/hero/default-hero.css @@ -2,6 +2,7 @@ .hero.block.default h1 { color: var(--black-olive); } + .hero.block.default h1 em { color: var(--red); font-style: normal; diff --git a/blocks/toc/toc.js b/blocks/toc/toc.js index a0861258..932ed6c9 100644 --- a/blocks/toc/toc.js +++ b/blocks/toc/toc.js @@ -1,9 +1,8 @@ - /** * get the name of the section and set as the id of * the section so there is a way to anchor from toc nav to section */ -export default async function decorate(block){ +export default async function decorate(block) { document.querySelectorAll('.section[data-toc]').forEach((jumpTo) => { const jumpId = jumpTo.getAttribute('data-toc'); jumpTo.setAttribute('id', jumpId); @@ -13,7 +12,6 @@ export default async function decorate(block){ const titleName = anchor.title; anchor.href = `#${titleName.toLowerCase()}`; }); - const main = document.querySelector('main'); const toc = document.querySelector('.toc'); main.prepend(toc); diff --git a/scripts/scripts.js b/scripts/scripts.js index 6306d791..50000b3e 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -131,7 +131,6 @@ function buildSectionBackgroundImage(main) { const keys = Object.keys(readBlockConfig(metadata)); const bgIdx = keys.indexOf(keys.find((k) => k.match(/background-image/i))); if (bgIdx >= 0) { - const picture = metadata.children[bgIdx].children[1]; picture.querySelector('picture').classList.add('section-bg-image'); metadata.parentElement.prepend(picture.cloneNode(true)); @@ -195,7 +194,6 @@ function decorateSectionBackgroundImage(main) { } function decorateSectionButtonRow(main) { main.querySelectorAll(':scope div > .default-content-wrapper > p.button-container').forEach((buttonContainer) => { - console.log(buttonContainer); const wrapper = buttonContainer.parentElement; wrapper.classList.add('button-wrapper'); }); diff --git a/styles/styles.css b/styles/styles.css index 9bff5461..d5b02cbf 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -238,7 +238,6 @@ p.button-container a { } p.button-container a:visited { - /* color: var(--black);*/ text-decoration: none; } @@ -279,6 +278,10 @@ main .section.content-bump-out .default-content-wrapper { position: relative; } +main .section.center .default-content-wrapper { + text-align: center; +} + main .section.content-bump-out.content-center .default-content-wrapper { text-align: center; border-radius: 40px; @@ -326,11 +329,6 @@ main .section.red-title h2 { main .section[data-background-image] { position: relative; - /*padding-bottom: 200px;*/ -} - -main .section.center .default-content-wrapper { - text-align: center; } main .section .default-content-wrapper > p > picture { @@ -351,6 +349,7 @@ main .section.content-bump-out .default-content-wrapper p:first-of-type { color: red; margin-top: 16px; } + main .section .default-content-wrapper > p > picture > img { position: absolute; top: 0; @@ -364,6 +363,7 @@ main .section .default-content-wrapper > p > picture > img { main .section .bg-image-wrapper { text-align: center; } + main > .section[data-background-image] > .section-bg-image-wrapper { margin: 0; padding: 0; @@ -437,7 +437,6 @@ main .section[data-layout="50/50"] .layout-content-wrapper > div.separator-wrapp main > .section > div { padding: 0 16px; margin: 0 auto; - /* max-width: var(--normal-page-width); */ } main > .section.full-width > div { @@ -598,6 +597,14 @@ main .section.inverted-gradient-background { padding: 0 24px; } + main > .section > .default-content-wrapper > picture > img { + object-position: unset; + width: 100%; + height: 100%; + object-fit: cover; + } + + main > .section[data-background-image] > .section-bg-image-wrapper { height: 20%; } @@ -605,12 +612,6 @@ main .section.inverted-gradient-background { main > .section[data-background-image] > .section-bg-image-wrapper > picture > img { object-position: unset; } - main > .section > .default-content-wrapper > picture > img { - object-position: unset; - width: 100%; - height: 100%; - object-fit: cover; - } main .section.angled-inverted-background.hero-container .hero { clip-path: polygon(0 0, 100% 0, 100% calc(100% - 10vw), 0 100%); @@ -708,10 +709,6 @@ main .section.inverted-gradient-background { grid-template-columns: 3fr 1fr; } - main .section > div { - /*padding: 0 40px;*/ - } - main .section.angled-inverted-background.hero-container .hero { clip-path: polygon(0 0, 100% 0, 100% calc(100% - 5vw), 0 100%); } diff --git a/tools/importer/forms/ui/js/script.js b/tools/importer/forms/ui/js/script.js index 145cc3c9..846e2483 100644 --- a/tools/importer/forms/ui/js/script.js +++ b/tools/importer/forms/ui/js/script.js @@ -10,8 +10,6 @@ * governing permissions and limitations under the License. */ -import { generateFormRendition } from '../../../../../blocks/form/form.js'; - /* eslint-disable no-undef */ const FORM_IMPORTER = 'https://g7ory75qdb.execute-api.ap-south-1.amazonaws.com/vega-services/importer'; From 47a48e3a91c0cf49fe20f5ba3e678f97ff5a5d7f Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 1 Dec 2023 09:17:38 -0500 Subject: [PATCH 010/133] matching designs for these images to the designs --- blocks/cards/icon.css | 4 ++-- styles/styles.css | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/blocks/cards/icon.css b/blocks/cards/icon.css index 04244ccf..f541d45a 100644 --- a/blocks/cards/icon.css +++ b/blocks/cards/icon.css @@ -25,8 +25,8 @@ .cards.icon.block .card span.icon { display: inline-block; - height: 120px; - width: 200px; + height: 70px; + width: 70px; } .cards.icon.block .card .button-container span.icon { diff --git a/styles/styles.css b/styles/styles.css index d5b02cbf..a86a050c 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -227,8 +227,8 @@ span { p.button-container a { display: inline-block; - padding: 10px 42px; - font-weight: bold; + padding: 8px 42px; + font-weight: normal; line-height: var(--line-height-l); text-decoration: none; color: var(--white); @@ -284,13 +284,13 @@ main .section.center .default-content-wrapper { main .section.content-bump-out.content-center .default-content-wrapper { text-align: center; - border-radius: 40px; + } main .section .default-content-wrapper.button-wrapper { margin: 0 auto; max-width: 1200px; - text-align: right; + text-align: center; } main .section.default-content-wrapper .button-container { @@ -633,7 +633,7 @@ main .section.inverted-gradient-background { } main > .section > div { - padding: 0 90px; + padding: 20px 90px; } main > .section[data-background-image] { From e5bb5da54fd37fbef74265c10fe5710f5cec53fb Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 19 Jan 2024 11:09:29 -0500 Subject: [PATCH 011/133] adding Sagar's changes --- blocks/disclaimer-modal/disclaimer-modal.css | 87 ++++++++++++++------ blocks/footer/footer.css | 3 +- scripts/scripts.js | 8 +- styles/styles.css | 4 +- 4 files changed, 73 insertions(+), 29 deletions(-) diff --git a/blocks/disclaimer-modal/disclaimer-modal.css b/blocks/disclaimer-modal/disclaimer-modal.css index 3a9804ef..0f2ed714 100644 --- a/blocks/disclaimer-modal/disclaimer-modal.css +++ b/blocks/disclaimer-modal/disclaimer-modal.css @@ -1,19 +1,15 @@ .disclaimer-modal-container { - background-color: var(--black); width: 100vw; height: 100vh; position: fixed; - top: 0; - left: 0; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); z-index: 1000; - font-family: var(--body-font-family); - padding: 0; } .disclaimer-modal-container .disclaimer-modal-wrapper { - position: relative; display: flex; - background-color: var(--black); width: 100%; height: 100%; align-items: center; @@ -21,21 +17,40 @@ } .disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal { - width: 90%; - max-width: 600px; + margin-top:-400px; + width:60vw; background-color: var(--white); + border: 4px solid #fff; + border-radius: 10px; +} + +.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .close-button { + text-align: end; + padding: 10px; +} + +.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .close-button .close { + width: 32px; + height: 32px; + display: inline-block; + background-image: url("../../icons/close.svg"); + background-repeat: no-repeat; + background-size: contain; + cursor: pointer; } .disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .title h2 { - color: var(--white); - padding: 30px 38px; + color: var(--text-emph); + padding: 20px 20px 10px 40px; margin: 0; - background-color: #898989; + background-color: var(--white); + text-align: center; } .disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .content p { - padding: 30px 38px 40px; + padding: 0 38px 32px 40px; margin: 0; + text-align: center; } .disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section { @@ -51,30 +66,52 @@ margin-bottom: 0; } -.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .agree p { +.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .agree, +.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .leave +{ + width: 100%; + background-color: var(--dark-red); + border-radius: 0 0 0 16px; + overflow: hidden; +} + +.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .leave a { + text-decoration: none; + height: 100%; + display: block; +} + +.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .agree p, +.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .leave a p { cursor: pointer; font-weight: var(--font-weight-semibold); + display: flex; + align-items: center; + justify-content: center; text-align: center; - background-color: var(--red); color: var(--white); padding: 12px 18px; - border-radius: 4px; box-shadow: 3px 3px 7px 1px #0003; + height: 100%; } -.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .leave a p { - text-decoration: underline; - color: var(--black); - font-weight: var(--font-weight-semibold); -} - -.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .agree p:hover { +.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .agree:hover, +.disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .leave:hover +{ background-color: #c00; } @media screen and (min-width: 600px) { .disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section { flex-direction: row; - justify-content: space-evenly; + justify-content: center; + align-items: stretch; + gap: 40px; } -} + + .disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .agree, + .disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section .leave + { + max-width: 200px; + } +} \ No newline at end of file diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index 750baf90..19d209fc 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -16,8 +16,7 @@ footer .footer { } .footer.block p > picture { - display: inline-block; - position: relative; + display:flex; height: 36px; width: auto; } diff --git a/scripts/scripts.js b/scripts/scripts.js index 50000b3e..732a8cfd 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -51,6 +51,9 @@ async function decorateDisclaimerModal() { const modal = tmp.querySelector('.disclaimer-modal'); const config = readBlockConfig(modal); modal.innerHTML = ` +
+ +

${config.title}

${config.content}

@@ -59,7 +62,7 @@ async function decorateDisclaimerModal() {
`; const disclaimerContainer = document.createElement('div'); - disclaimerContainer.className = 'disclaimer-modal-container'; + disclaimerContainer.className = 'section disclaimer-modal-container'; const disclaimerWrapper = document.createElement('div'); disclaimerWrapper.className = 'disclaimer-modal-wrapper'; disclaimerWrapper.appendChild(modal); @@ -74,6 +77,9 @@ async function decorateDisclaimerModal() { disclaimerContainer.remove(); }); main.append(disclaimerContainer); + modal.querySelector('.close').addEventListener('click', function() { + document.querySelector('.disclaimer-modal-container').style.display = 'none'; + }); } } } diff --git a/styles/styles.css b/styles/styles.css index a86a050c..2520186d 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -577,7 +577,9 @@ main .section.inverted-gradient-background { height: 1px; overflow: hidden; } - +main .disclaimer-modal-container { + background-color: rgb(0 0 0 / 50%); +} @media screen and (max-width: 299px) { :root { --nav-height: 75px; From b2697ab16830739cd8f5d887d750486f7350a25c Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 19 Jan 2024 11:50:04 -0500 Subject: [PATCH 012/133] updating footer and align section headings center --- blocks/footer/footer.css | 46 ++++++++++++++++++++++++++++++++++------ styles/styles.css | 1 + 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index 19d209fc..5a478539 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -6,22 +6,56 @@ footer { } -footer .footer { +footer .footer.block { max-width: 1200px; margin: auto; } -.footer.block a { - color: inherit; +footer .footer.block > div { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 20px; +} + +footer .footer.block > div > div:has(picture) { + border-bottom: 2px solid var(--silver); + padding-bottom: 20px; + width: 100%; } -.footer.block p > picture { - display:flex; +footer .footer.block > div > div:has(p>a) { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0; +} + +.footer.block picture { + display: flex; height: 36px; width: auto; } +.footer.block a { + color: inherit; + text-decoration: none; +} + +.footer.block a:hover { + text-decoration: underline; + opacity: 0.8; +} + .footer.block > div > div > p { - font-size: var(--body-font-size-xxs); + font-size: var(--body-font-size-xs); margin-bottom: 14px; + text-align: left; } + +@media screen and (min-width: 600px) { + footer .footer.block > div > div:has(p>a){ + flex-direction: row; + gap: 30px; + } +} \ No newline at end of file diff --git a/styles/styles.css b/styles/styles.css index 2520186d..916ac9d1 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -348,6 +348,7 @@ main .section.image-boxshadow .default-content-wrapper > p.image { main .section.content-bump-out .default-content-wrapper p:first-of-type { color: red; margin-top: 16px; + text-align: center; } main .section .default-content-wrapper > p > picture > img { From b93a7be97a023a97585d11d048706bf04e28281c Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 19 Jan 2024 11:56:11 -0500 Subject: [PATCH 013/133] removing whitespace in padding white bg was showing under contact us banner --- styles/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/styles.css b/styles/styles.css index 916ac9d1..92b5ab78 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -636,7 +636,7 @@ main .disclaimer-modal-container { } main > .section > div { - padding: 20px 90px; + padding: 0 90px; } main > .section[data-background-image] { From 6ad12d1ab5094f217310cf37f39bdb863f91cd52 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 19 Jan 2024 12:36:14 -0500 Subject: [PATCH 014/133] Update styles.css --- styles/styles.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/styles/styles.css b/styles/styles.css index 92b5ab78..5324a564 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -388,6 +388,8 @@ main > .section[data-background-image] > .section-bg-image-wrapper picture img { width: 100%; object-fit: cover; object-position: center; + /* temporary until there are right sized images */ + height: 600px; } main .section .default-content-wrapper span.icon { From 6b15ed40aea2524208c76a56a5b33475340a6489 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 19 Jan 2024 13:12:01 -0500 Subject: [PATCH 015/133] resize heros, temp removing top padding on sections its crating white space --- styles/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/styles.css b/styles/styles.css index 5324a564..0d5676fb 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -263,7 +263,7 @@ body.gray-gradient > main { } main .section { - padding: 12px 0; + padding: 0; } main .section.content-bump-out .default-content-wrapper { From 34d4b2c2ca2d8e36bdba48c4ee4c5033117c8be0 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 19 Jan 2024 13:26:20 -0500 Subject: [PATCH 016/133] adding space between sections --- styles/styles.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/styles/styles.css b/styles/styles.css index 0d5676fb..82241fe9 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -263,7 +263,11 @@ body.gray-gradient > main { } main .section { - padding: 0; + +} +main .section .cards-container{ + + padding: 12px 0; } main .section.content-bump-out .default-content-wrapper { @@ -698,6 +702,7 @@ main .disclaimer-modal-container { main > .section[data-background-image] > .section-bg-image-wrapper { height: 30%; + padding-top: 40px; } main .section[data-layout="25/75"] .layout-content-wrapper { From b17219c6e1b589dbdacb0cc90f9f05534a9e4b14 Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 22 Jan 2024 12:22:20 -0500 Subject: [PATCH 017/133] center align content section titles center align content section titles --- styles/styles.css | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/styles/styles.css b/styles/styles.css index 82241fe9..db249385 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -263,11 +263,10 @@ body.gray-gradient > main { } main .section { - +padding:12px 0; } -main .section .cards-container{ - - padding: 12px 0; + main .section.cards-container { + padding-bottom: 20px; } main .section.content-bump-out .default-content-wrapper { @@ -282,13 +281,8 @@ main .section.content-bump-out .default-content-wrapper { position: relative; } -main .section.center .default-content-wrapper { - text-align: center; -} - main .section.content-bump-out.content-center .default-content-wrapper { text-align: center; - } main .section .default-content-wrapper.button-wrapper { From 4f26d1ecc107b9bbb7fd8cda6db6153f6e8cab03 Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 22 Jan 2024 16:17:45 -0500 Subject: [PATCH 018/133] for free trial cards added a border The default cards with a border per the new designs --- blocks/cards/cards.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blocks/cards/cards.css b/blocks/cards/cards.css index c1ee0ac2..87fd88c0 100644 --- a/blocks/cards/cards.css +++ b/blocks/cards/cards.css @@ -50,6 +50,8 @@ @media screen and (min-width: 600px) { .cards.default.block > div { padding: 20px 32px; + border-radius: 8px; + box-shadow: 0 4px 10px 0 rgba(0 0 0 / 20%); } } From 849e370236d0c1966740ca41e8879226eb248fc4 Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 22 Jan 2024 16:37:09 -0500 Subject: [PATCH 019/133] Update cards.css --- blocks/cards/cards.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blocks/cards/cards.css b/blocks/cards/cards.css index 87fd88c0..0c6f7113 100644 --- a/blocks/cards/cards.css +++ b/blocks/cards/cards.css @@ -13,7 +13,8 @@ .cards.default.block > div { padding: 20px 16px; - border-radius: 10px; + border-radius: 8px; + box-shadow: 0 4px 10px 0 rgba(0 0 0 / 20%); background-color: rgba(255 255 255 / 66%); } From 0c698caafff2a390ef88d1c3d9b873045c196300 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 23 Jan 2024 13:41:46 -0500 Subject: [PATCH 020/133] issue 155 update style for floating image block just removed the border. --- blocks/floating-images/floating-images.css | 1 - 1 file changed, 1 deletion(-) diff --git a/blocks/floating-images/floating-images.css b/blocks/floating-images/floating-images.css index 30ca35cd..7c3e8079 100644 --- a/blocks/floating-images/floating-images.css +++ b/blocks/floating-images/floating-images.css @@ -29,7 +29,6 @@ } .block.floating-images .content { - border-left: 3px solid var(--red); padding: 0 24px; } From 41e0bc38e13f5460761254405bd2c4baf05a3a31 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 24 Jan 2024 12:03:25 -0500 Subject: [PATCH 021/133] Floating image tune up and such Mostly aligning floating image section to the new designs and also some tweaks to the cards.... --- blocks/cards/icon.css | 14 +++++++++++++- blocks/floating-images/floating-images.css | 10 ++++++++++ blocks/floating-images/floating-images.js | 3 ++- styles/styles.css | 16 +++++++++++----- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/blocks/cards/icon.css b/blocks/cards/icon.css index f541d45a..074a96f4 100644 --- a/blocks/cards/icon.css +++ b/blocks/cards/icon.css @@ -20,7 +20,7 @@ flex-direction: column; background-color: white; text-align: center; - border-right: 1px solid grey; + border-bottom: 1px solid grey; } .cards.icon.block .card span.icon { @@ -54,12 +54,21 @@ font-weight: var(--font-weight-bold); line-height: var(--line-height-s); } +@media screen and (min-width: 600px) { + .cards.icon.block .card { + + } +} @media screen and (min-width: 900px) { .cards.icon.block .row { grid-template-columns: repeat(2, 1fr); } + .cards.icon.block .card { + border-right: 1px solid grey; + border-bottom:0; + } main .section[data-layout] .cards.icon.block .row { grid-template-columns: 1fr; } @@ -70,4 +79,7 @@ main .section[data-layout] .cards.icon.block .row { grid-template-columns: repeat(3, 1fr); } + .cards.icon.block .card { + + } } diff --git a/blocks/floating-images/floating-images.css b/blocks/floating-images/floating-images.css index 7c3e8079..f726da0a 100644 --- a/blocks/floating-images/floating-images.css +++ b/blocks/floating-images/floating-images.css @@ -1,9 +1,13 @@ /* styles for the floating images auto block */ +.section.floating-images-container.grey > div { + background-color: var(--brand--secondary-subtle); +} .block.floating-images > div { display: flex; flex-direction: column-reverse; + align-items: start; gap: 1em; } @@ -31,6 +35,11 @@ .block.floating-images .content { padding: 0 24px; } +.block.floating-images .content h3 { + color:red; + font-size: var(--body-font-size-l); + font-weight: var(--font-weight-normal); +} .block.floating-images .content ul { margin-left: 1em; @@ -47,6 +56,7 @@ .block.floating-images > div { flex-direction: row; align-items: center; + padding:20px 0; } .block.floating-images.image-left > div { diff --git a/blocks/floating-images/floating-images.js b/blocks/floating-images/floating-images.js index 396033aa..758e6a1f 100644 --- a/blocks/floating-images/floating-images.js +++ b/blocks/floating-images/floating-images.js @@ -2,7 +2,8 @@ export default function decorate(block) { const container = block.querySelector(':scope > div'); container.children[0].classList.add('content'); container.children[1].classList.add('image'); - + const kids = block.children[1]; + console.log(kids); const picture = block.querySelector('picture'); const img = block.querySelector('img'); diff --git a/styles/styles.css b/styles/styles.css index db249385..4e8d2054 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -50,26 +50,32 @@ :root { /* Colors */ + /* grey color palette */ --bright-gray: #efefef; --medium-gray: #ddd; --gray: #979797; --gray-neutral-70: #454545; + --brand--secondary-subtle: #edf2fa; --gray-neutral-80: #333; --gray-neutral-90: #1e1e23; --gray-box-shadow: rgb(0 0 0 / 20%); + --gray-gradient-light: #edeff0; + --gray-gradient-dark: #9da8af; /* Gradient breakpoint is defined below */ + + /* general colors */ --silver: #d8d8d8; --quartz: #4c4948; - --red: #e10; + --red: #e1242a; --dark-red: #BD120A; --white: #fff; --blue: #003087; --black-olive: #3a3a3a; --black: black; --cadet: #5B6770; /* gray-blue */ - --gray-gradient-light: #edeff0; - --gray-gradient-dark: #9da8af; /* Gradient breakpoint is defined below */ + /* Brand Colors */ + --takeda-red-50: #e1242a; --gammagard-liq: #003087; --gammagard-sd: #35a4d8; --hyqvia: #500778; @@ -272,7 +278,7 @@ padding:12px 0; main .section.content-bump-out .default-content-wrapper { border:0; max-width: 1200px; - margin: -60px auto 0 ; + margin: -81px auto 0 ; display: table; width: 100%; text-align: left; @@ -632,7 +638,7 @@ main .disclaimer-modal-container { } p { - font-size: var(--body-font-size-l); + font-size: var(--body-font-size-s); } main > .section > div { From 0967b78b831b15f06a1ef0738bc3d529c6e7fc16 Mon Sep 17 00:00:00 2001 From: Sagar Date: Thu, 25 Jan 2024 12:06:48 +0530 Subject: [PATCH 022/133] fixed few lint issues and adjusted disclaimer postion --- blocks/disclaimer-modal/disclaimer-modal.css | 9 ++++++--- blocks/floating-images/floating-images.css | 1 + styles/styles.css | 10 ++++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/blocks/disclaimer-modal/disclaimer-modal.css b/blocks/disclaimer-modal/disclaimer-modal.css index 0f2ed714..1bf0ac7d 100644 --- a/blocks/disclaimer-modal/disclaimer-modal.css +++ b/blocks/disclaimer-modal/disclaimer-modal.css @@ -12,13 +12,12 @@ display: flex; width: 100%; height: 100%; - align-items: center; + align-items: flex-start; justify-content: center; + padding-top: 40px; } .disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal { - margin-top:-400px; - width:60vw; background-color: var(--white); border: 4px solid #fff; border-radius: 10px; @@ -102,6 +101,10 @@ } @media screen and (min-width: 600px) { + .disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal{ + width: 70%; + } + .disclaimer-modal-container .disclaimer-modal-wrapper .disclaimer-modal .button-section { flex-direction: row; justify-content: center; diff --git a/blocks/floating-images/floating-images.css b/blocks/floating-images/floating-images.css index f726da0a..2e7e17c3 100644 --- a/blocks/floating-images/floating-images.css +++ b/blocks/floating-images/floating-images.css @@ -4,6 +4,7 @@ background-color: var(--brand--secondary-subtle); } + .block.floating-images > div { display: flex; flex-direction: column-reverse; diff --git a/styles/styles.css b/styles/styles.css index 4e8d2054..9c450ff8 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -49,8 +49,6 @@ } :root { - /* Colors */ - /* grey color palette */ --bright-gray: #efefef; --medium-gray: #ddd; --gray: #979797; @@ -60,7 +58,7 @@ --gray-neutral-90: #1e1e23; --gray-box-shadow: rgb(0 0 0 / 20%); --gray-gradient-light: #edeff0; - --gray-gradient-dark: #9da8af; /* Gradient breakpoint is defined below */ + --gray-gradient-dark: #9da8af; /* general colors */ --silver: #d8d8d8; @@ -271,7 +269,8 @@ body.gray-gradient > main { main .section { padding:12px 0; } - main .section.cards-container { + +main .section.cards-container { padding-bottom: 20px; } @@ -392,6 +391,7 @@ main > .section[data-background-image] > .section-bg-image-wrapper picture img { width: 100%; object-fit: cover; object-position: center; + /* temporary until there are right sized images */ height: 600px; } @@ -584,9 +584,11 @@ main .section.inverted-gradient-background { height: 1px; overflow: hidden; } + main .disclaimer-modal-container { background-color: rgb(0 0 0 / 50%); } + @media screen and (max-width: 299px) { :root { --nav-height: 75px; From 5dabf2d66df759ae74302126436f8f53e16a7bdb Mon Sep 17 00:00:00 2001 From: dan Date: Sun, 28 Jan 2024 20:27:17 -0500 Subject: [PATCH 023/133] added adoibe launch library for testing --- scripts/delayed.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/delayed.js b/scripts/delayed.js index 920b4ad8..f124db1b 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -1,7 +1,13 @@ // eslint-disable-next-line import/no-cycle -import { sampleRUM } from './lib-franklin.js'; +import { sampleRUM, loadScript } from './lib-franklin.js'; // Core Web Vitals RUM collection sampleRUM('cwv'); // add more delayed functionality here + +const loadAdobeDTM = async () => { + await loadScript('https://assets.adobedtm.com/8fee56b0a165/a42c27096959/launch-c6342f10fc46-development.min.js'); +}; + +await loadAdobeDTM(); From 879b1473f8b54941250ceb41150756265f08277b Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 29 Jan 2024 13:03:04 -0500 Subject: [PATCH 024/133] changing launch link development seems to work --- scripts/delayed.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/delayed.js b/scripts/delayed.js index f124db1b..9f19a5bb 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -7,7 +7,7 @@ sampleRUM('cwv'); // add more delayed functionality here const loadAdobeDTM = async () => { - await loadScript('https://assets.adobedtm.com/8fee56b0a165/a42c27096959/launch-c6342f10fc46-development.min.js'); + await loadScript('https://assets.adobedtm.com/8fee56b0a165/4f6b3b702502/launch-9b9e958a7168-development.min.js'); }; await loadAdobeDTM(); From 62ac9d4ba02ccf0347556750f7e2b18da7413855 Mon Sep 17 00:00:00 2001 From: Sagar Date: Tue, 30 Jan 2024 17:42:45 +0530 Subject: [PATCH 025/133] updated styles for product card --- blocks/cards/product.css | 88 ++++++++++++--------------------------- blocks/cards/product.js | 32 +++++--------- icons/download-button.svg | 43 +++++++++++++++++++ 3 files changed, 79 insertions(+), 84 deletions(-) create mode 100644 icons/download-button.svg diff --git a/blocks/cards/product.css b/blocks/cards/product.css index 888f4602..663e08bc 100644 --- a/blocks/cards/product.css +++ b/blocks/cards/product.css @@ -1,87 +1,51 @@ -.cards.product.block > ul { +.cards.product.block { display: grid; grid-template-columns: 1fr; gap: 1em; } -.cards.product.block > ul > li > a { - display: block; - padding: 20px 20px 10px; - text-decoration: none; - border-radius: 10px; +.cards.product.block > .card { + display: grid; + grid-template-columns: 25% 75%; + padding: 1em; + gap: 1em; box-shadow: 0 4px 10px 0 rgba(0 0 0 / 20%); - background-color: var(--white); -} - -.cards.product.block > ul > li hr { - border-top: 3px solid white; -} - -.cards.product.block > ul > li .logo { - padding-top: 10px; - max-width: 240px; + border-radius: 8px; } -.cards.product.block > ul > li picture { - display: block; - position: relative; +.cards.product.block .card img { width: 100%; - height: 100%; -} - -.cards.product.block > ul > li.gammagard-liquid hr { - border-color: var(--gammagard-liq); -} - -.cards.product.block > ul > li.gammagard-sd hr { - border-color: var(--gammagard-sd); -} - -.cards.product.block > ul > li.hyqvia hr { - border-color: var(--hyqvia); + object-fit: contain; } -.cards.product.block > ul > li.cuvitru hr { - border-color: var(--cuvitru); +.cards.product.block .card .details { + display: flex; + flex-direction: column; + justify-content: center; + padding: 10px; } -.cards.product.block > ul > li.flexbumin hr { - border-color: var(--flexbumin); +.cards.product.block .card .details .button-container{ + align-self: flex-end; } -.cards.product.block > ul > li.glassia hr { - border-color: var(--glassia); +.cards.product.block .card .details .button-container > a { + background-color: unset; } -.cards.product.block > ul > li.aralast hr { - border-color: var(--aralast); +.cards.product.block .card .details .icon.icon-download-button { + display: block; + width: 35px; + height: 35px; } -.cards.product.block > ul > li .logo picture img { - position: absolute; - top: 0; - left: 0; +.cards.product.block .card .details .icon.icon-download-button > svg { width: 100%; height: 100%; - object-position: center center; - object-fit: contain; -} - -.cards.product.block > ul > li > a p { - margin-top: 10px; - margin-bottom: 0; - font-size: var(--body-font-size-s); - text-align: right; } @media screen and (min-width: 600px) { - .cards.product.block > ul > li .logo { - margin-left: 45px; - } -} - -@media screen and (min-width: 900px) { - main .section[data-layout] .cards.product.block > ul > li .logo { - margin-left: 1em; + .cards.product.block { + grid-template-columns: 1fr 1fr; } -} +} \ No newline at end of file diff --git a/blocks/cards/product.js b/blocks/cards/product.js index ce575429..6f729c07 100644 --- a/blocks/cards/product.js +++ b/blocks/cards/product.js @@ -1,31 +1,19 @@ -import { decorateIcons } from '../../scripts/lib-franklin.js'; - /** - * Builds the Icons variation of the cards block. + * Builds the Profile variation of the cards block. * @param {HTMLDivElement} block */ export default async function decorate(block) { - const ul = document.createElement('ul'); [...block.children].forEach((card) => { - const li = document.createElement('li'); - li.classList.add('product', card.children[0].textContent); - const picture = card.children[1].querySelector('picture'); + card.classList.add('card'); - const img = picture.querySelector('img'); - const ratio = (parseInt(img.height, 10) / parseInt(img.width, 10)) * 100; - picture.style.paddingBottom = `${ratio}%`; + card.children[0].classList.add('image'); + card.children[1].classList.add('details'); - const link = card.children[1].querySelector('a'); - link.innerHTML = ` -
- -

Learn More

- `; - li.append(link); - ul.append(li); + let p = card.children[1].querySelector('p'); + if (!p) { + p = document.createElement('p'); + p.innerHTML = card.children[1].innerHTML; + card.children[1].replaceChildren(p); + } }); - block.replaceChildren(ul); - await decorateIcons(block); } diff --git a/icons/download-button.svg b/icons/download-button.svg new file mode 100644 index 00000000..a010a1fd --- /dev/null +++ b/icons/download-button.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 70eaf4db12669e373c632170e9bbcbdc7f952c4b Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 30 Jan 2024 12:39:00 -0500 Subject: [PATCH 026/133] adding launch link for new environment --- scripts/delayed.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/delayed.js b/scripts/delayed.js index 9f19a5bb..e2b5252f 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -7,7 +7,10 @@ sampleRUM('cwv'); // add more delayed functionality here const loadAdobeDTM = async () => { - await loadScript('https://assets.adobedtm.com/8fee56b0a165/4f6b3b702502/launch-9b9e958a7168-development.min.js'); + await loadScript('https://assets.adobedtm.com/8fee56b0a165/0dcea4176083/launch-868c1997ca51-development.min.js'); }; await loadAdobeDTM(); + + + \ No newline at end of file From 3fb2eb1e721ec10ec2487c7b94c0e57f57795f96 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 30 Jan 2024 12:43:10 -0500 Subject: [PATCH 027/133] removing script tag removing script tag --- scripts/delayed.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/delayed.js b/scripts/delayed.js index e2b5252f..5e7c393e 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -11,6 +11,3 @@ const loadAdobeDTM = async () => { }; await loadAdobeDTM(); - - - \ No newline at end of file From e3662d8ab644dd9ec541865202b38aef36fd3913 Mon Sep 17 00:00:00 2001 From: Sagar Date: Tue, 6 Feb 2024 20:45:48 +0530 Subject: [PATCH 028/133] updated code for header block as per new design --- blocks/header/header.css | 366 ++++++++++----------------------------- blocks/header/header.js | 251 ++++++++++++--------------- scripts/scripts.js | 11 ++ styles/styles.css | 13 +- 4 files changed, 219 insertions(+), 422 deletions(-) diff --git a/blocks/header/header.css b/blocks/header/header.css index a69cc5cb..e74af732 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -1,91 +1,54 @@ - -header { - background-color: white; -} - .header.block nav { - position: relative; display: grid; - height: var(--nav-height); - z-index: 1000; - overflow-y: scroll; grid-template: - 'logo . hamburger' 75px - 'brand brand brand' min-content - 'sections sections sections' min-content - 'utility utility utility' min-content + "logo . hamburger" 75px + "sections sections sections" min-content + "contact-us contact-us contact-us" min-content / min-content 1fr min-content; - justify-content: space-between; + padding: 20px; + align-items: center; background-color: var(--white); - box-shadow: 2px 2px 10px 1px rgba(0 0 0 / 15%); } -.header.block nav[aria-expanded="true"] { - height: 100vh; +.header.block nav a { + text-decoration: none; + color: var(--quartz); + font-weight: bold; } -.header.block .nav-logo { +.header.block.nav-logo { grid-area: logo; } -.header.block .nav-logo a { - position: relative; - display: flex; - margin: 0 15px; - height: 100%; - align-items: center; -} - .header.block .nav-logo img { padding: 15px 0; height: auto; width: 105px; } -.header.block .nav-brand { - grid-area: brand; - display: flex; - height: 25px; - align-items: center; - justify-content: center; - font-size: var(--body-font-size-xs); - font-weight: var(--font-weight-semibold); - line-height: var(--line-height-m); - text-transform: uppercase; -} - -.header.block .nav-brand a:hover { - color: var(--quartz); - text-decoration: none; -} - .header.block .nav-hamburger { - display: flex; grid-area: hamburger; - align-items: center; - justify-content: center; + overflow: hidden; } .header.block .nav-hamburger .nav-hamburger-icon { - display: block; - margin: 0 15px; background-color: transparent; + cursor: pointer; } -.header.block .nav-hamburger .icon-hamburger, -.header.block nav[aria-expanded="true"] .nav-hamburger .icon-close { - display: block; +.header.block .nav-hamburger .icon { + height: 25px; + width: 25px; } -/* stylelint-disable-next-line no-descending-specificity */ -.header.block .nav-hamburger .icon-close, -.header.block nav[aria-expanded="true"] .nav-hamburger .icon-hamburger { - display: none; +.header.block nav[aria-expanded="false"] .nav-hamburger .icon.icon-hamburger, +.header.block nav[aria-expanded="true"] .nav-hamburger .icon.icon-close { + display: block; } -.header.block .nav-hamburger .icon { - height: 25px; - width: 25px; +.header.block nav[aria-expanded="false"] .nav-hamburger .icon.icon-close, +.header.block nav[aria-expanded="true"] .nav-hamburger .icon.icon-hamburger { + display: none; } .header.block .nav-hamburger .icon svg { @@ -93,42 +56,38 @@ header { width: 100%; } -.header.block .nav-sections { +.header.block nav .nav-sections { display: none; - grid-area: sections; - background-color: white; } -.header.block .nav-utility { - display: none; - grid-area: utility; - background-color: var(--silver); -} - -.header.block nav[aria-expanded="true"] .nav-sections, -.header.block nav[aria-expanded="true"] .nav-utility { +.header.block nav[aria-expanded="true"] .nav-sections { + grid-area: sections; display: block; } .header.block .nav-sections > ul { - display: block; - padding: 10px 0; - text-align: center; + display: flex; + padding: 20px 0; + margin: 0; + flex-direction: column; + justify-content: space-around; + gap: 20px; } -.header.block .nav-sections > ul > li { - position: relative; - padding: 15px 0; +.header.block .nav-sections > ul > li > ul { + display: none; } -/* stylelint-disable-next-line no-descending-specificity */ .header.block .nav-sections > ul > li > a { display: flex; - font-weight: var(--font-weight-semibold); - line-height: var(--line-height-m); align-items: center; - justify-content: center; - column-gap: 5px; + justify-content: space-between; + padding: 10px 0; + border-bottom: 1px solid var(--gray); +} + +.header.block .nav-sections > ul > li.nav-drop > a.active-arrow-down::after { + background-image: url(images/down-arrow.png); } .header.block .nav-sections > ul > li.nav-drop > a > .icon { @@ -146,248 +105,105 @@ header { height: 100%; } -.header.block .nav-sections > ul > li.nav-drop[aria-expanded="true"] > a > span { - transform: rotate(180deg); -} - -.header.block .nav-sections > ul > li.separator { - display: none; +.header.block .nav-sections > ul > li.nav-drop > a > span { + transition: transform 0.3s ease-in-out; + transform: rotate(0); } -.header.block .nav-sections > ul > li > ul { - display: none; +.header.block .nav-sections > ul > li.nav-drop[aria-expanded="true"] > a > span { + transform: rotate(180deg); } .header.block .nav-sections > ul > li.nav-drop[aria-expanded="true"] > ul { display: block; } -.header.block .nav-sections > ul > li > ul > li { - padding: 12px 0; +.header.block .nav-sections > ul > li > ul > li.active > a { + text-decoration: underline; } -/* stylelint-disable-next-line no-descending-specificity */ .header.block .nav-sections > ul > li > ul > li > a { display: flex; - align-items: center; - justify-content: center; - column-gap: 5px; - font-size: var(--body-font-size-s); - font-weight: var(--font-weight-normal); -} - -.header.block .nav-sections > ul > li > ul > li.show-all > a { - font-weight: var(--font-weight-bold); + justify-content: flex-start; + padding: 20px 20px 0; } -/* stylelint-disable-next-line no-descending-specificity */ -.header.block .nav-sections > ul > li > ul > li > a > .icon { - display: inline-block; - position: relative; - height: 14px; - width: 14px; -} - -.header.block .nav-sections > ul > li > hr { +.header.block nav .nav-contact-us { display: none; } -/* stylelint-disable-next-line no-descending-specificity */ -.header.block .nav-utility > ul > li { - padding: 20px 0; - margin: 0 auto; - max-width: 200px; - font-size: var(--body-font-size-s); - font-weight: var(--font-weight-semibold); - text-align: center; - text-transform: uppercase; - border-bottom: 1px solid var(--gray); +.header.block nav[aria-expanded="true"] .nav-contact-us { + grid-area: contact-us; + display: flex; + justify-content: center; } -.header.block .nav-utility > ul > li:last-child { - border-bottom: none; +.header.block .nav-contact-us > p { + margin: 0; } -/* stylelint-disable-next-line no-descending-specificity */ -.header.block nav a { +.header.block .nav-contact-us > p > a { + display: inline-block; + padding: 8px 42px; + font-weight: normal; text-decoration: none; - color: var(--quartz); + color: var(--white); + background-color: var(--dark-red); + border-bottom-left-radius: 20px; + white-space: nowrap; } -/* stylelint-disable-next-line no-descending-specificity */ -.header.block nav a:visited { - color: var(--quartz); -} - -@media screen and (max-width: 299px) { - .header.block .nav-brand { - display: none; - max-width: 150px; - margin: 0 auto; - text-align: center; - } - - .header.block nav[aria-expanded="true"] .nav-brand { - display: flex; - } -} +@media (min-width: 900px) { -@media screen and (min-width: 600px) { .header.block nav { - overflow-y: visible; grid-template: - 'utility utility utility' 50px - 'logo brand hamburger' 75px - 'sections sections sections' min-content - / min-content 1fr min-content; + "logo sections contact-us" 75px + ". . hamburger" min-content + / min-content 1fr min-content; + padding: 20px 90px; } - .header.block .nav-brand { - align-items: center; - justify-content: flex-start; - height: 100%; - } - - .header.block .nav-hamburger .nav-hamburger-icon .icon { - height: 35px; - width: 35px; - } - - .header.block nav[aria-expanded="false"] .nav-utility { - display: block; - } - - .header.block .nav-utility ul { - display: flex; - height: 50px; - justify-content: flex-end; + .header.block .nav-hamburger { + display: none; } - .header.block .nav-utility > ul > li { + .header.block .nav-sections { display: flex; - margin: unset; - max-width: unset; - padding: 0 25px; - align-items: center; - border-bottom: none; - text-transform: none; - } - - .header.block .nav-utility a { - font-size: var(--body-font-size-s); - font-weight: var(--font-weight-semibold); - color: var(--quartz); - text-decoration: none; - } - - .header.block .nav-utility a:visited { - color: var(--quartz); - } -} - -@media screen and (min-width: 900px) { - .header.block nav[aria-expanded="true"] { - height: var(--nav-height); - } - - .header.block nav { - grid-template: - 'utility utility utility' 50px - 'logo brand sections' 75px - / min-content min-content 1fr; - } - - /* stylelint-disable-next-line no-descending-specificity */ - .header.block nav a:hover { - text-decoration: underline; - color: var(--red); + justify-content: center; } .header.block .nav-sections > ul { - display: flex; - padding: 0 15px; - height: 100%; - align-items: center; - justify-content: space-around; + flex-direction: row; + width: 100%; } - .header.block .nav-sections > ul > li { - display: flex; - height: 100%; + .header.block .nav-sections > ul > li > a { padding: 0; - align-items: center; - } - - .header.block .nav-sections > ul > li.active { - border-bottom: 3px solid var(--red); - } - - .header.block .nav-sections > ul > li > ul { - position: absolute; - padding: 0 20px; - top: calc(80%); - left: -45px; - background-color: white; - box-shadow: 0 5px 8px 0 rgba(0 0 0 / 30%); - z-index: 1000; - } - - .header.block .nav-sections > ul > li.nav-drop:not([data-touch-click="true"]):hover > ul { - display: block; - } - - .header.block .nav-sections > ul > li > ul > li { - padding: 6px 0; - } - - .header.block .nav-sections > ul > li > ul > li > a { - padding: 6px 0; - line-height: var(--line-height-m); - justify-content: flex-start; - white-space: nowrap; - } - - .header.block .nav-sections > ul > li.nav-drop:not([data-touch-click="true"]):hover > a > span { - transform: rotate(180deg); - } - - .header.block .nav-sections > ul > li.separator { - display: list-item; - padding: 0; - height: 35px; - border-left: 1px solid var(--silver) - } - - .header.block .nav-brand { - width: 180px; + border-bottom: none; } - .header.block .nav-hamburger { + .header.block .nav-sections > ul > li.nav-drop > a > .icon { display: none; } - .header.block nav[aria-expanded="true"] .nav-utility, - .header.block nav[aria-expanded="false"] .nav-utility { - display: block; - justify-content: flex-end; + .header.block .nav-sections > ul > li.nav-drop[aria-expanded="true"] > ul { + position: absolute; + left: 0; + width: 100vw; + display: flex; + opacity: 1; + background-color: var(--gray-neutral-80); + top: 86px; + justify-content: space-evenly; } -} -@media screen and (min-width: 1200px) { - .header.block .nav-brand { - align-items: center; - justify-content: flex-start; - width: unset; - white-space: nowrap; - font-size: var(--body-font-size-m); + .header.block .nav-sections > ul > li.nav-drop[aria-expanded="true"] > ul > li > a { + padding: 10px 0; + color: var(--gray); } - .header.block .nav-sections > ul { - column-gap: 25px; - justify-content: flex-end; + .header.block .nav-sections > ul > li > ul > li.active > a { + border-bottom: 5px solid var(--white); } - .header.block .nav-sections > ul > li > ul > li.hide { - display: none; - } } diff --git a/blocks/header/header.js b/blocks/header/header.js index dfe5887f..4aa2c739 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -1,110 +1,79 @@ -import { getMetadata, decorateIcons, decorateSections } from '../../scripts/lib-franklin.js'; +import { + getMetadata, + decorateIcons, + decorateSections, +} from '../../scripts/lib-franklin.js'; -// media query match that indicates mobile/tablet width const isDesktop = window.matchMedia('(min-width: 900px)'); -function closeOnEscape(e) { - if (e.code === 'Escape') { - const nav = document.getElementById('nav'); - const navSections = nav.querySelector('.nav-sections'); - const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]'); - if (navSectionExpanded && isDesktop.matches) { - // eslint-disable-next-line no-use-before-define - toggleAllNavSections(navSections); - navSectionExpanded.focus(); - } else if (!isDesktop.matches) { - // eslint-disable-next-line no-use-before-define - toggleMenu(nav, navSections); - nav.querySelector('button').focus(); - } +function setAttributes(element, attributes) { + for (const key in attributes) { + element.setAttribute(key, attributes[key]); } } -function openOnKeydown(e) { - const navDrop = e.currentTarget.closest('.nav-drop'); - if (navDrop && (e.code === 'Enter' || e.code === 'Space')) { - const dropExpanded = navDrop.getAttribute('aria-expanded') === 'true'; - // eslint-disable-next-line no-use-before-define - toggleAllNavSections(navDrop.closest('.nav-sections')); - navDrop.setAttribute('aria-expanded', dropExpanded ? 'false' : 'true'); - } +function createElemWithClass(type, ...classNames) { + const elem = document.createElement(type); + elem.classList.add(...classNames); + return elem; } -/** - * Toggles all nav sections - * @param {Element} sections The container element - * @param {Boolean} expanded Whether the element should be expanded or collapsed - */ function toggleAllNavSections(sections, expanded = false) { - sections.querySelectorAll('.nav-sections > ul > li.nav-drop').forEach((section) => { - section.setAttribute('aria-expanded', expanded); - section.removeAttribute('data-touch-click'); - }); + sections + .querySelectorAll('.nav-sections > ul > li.nav-drop') + .forEach((section) => { + setAttributes(section, { + 'aria-expanded': expanded, + 'data-touch-click': null, + }); + }); } -/** - * Toggles the entire nav - * @param {Element} nav The container element - * @param {Element} navSections The nav sections within the container element - * @param {*} forceExpanded Optional param to force nav expand behavior when not null - */ function toggleMenu(nav, navSections, forceExpanded = null) { const button = nav.querySelector('.nav-hamburger button'); - const expanded = forceExpanded !== null ? !forceExpanded : nav.getAttribute('aria-expanded') === 'true'; + const expanded = forceExpanded !== null + ? !forceExpanded + : nav.getAttribute('aria-expanded') === 'true'; - document.body.style.overflowY = (expanded || isDesktop.matches) ? '' : 'hidden'; - nav.setAttribute('aria-expanded', expanded ? 'false' : 'true'); + document.body.style.overflowY = expanded || isDesktop.matches ? '' : 'hidden'; + setAttributes(nav, { 'aria-expanded': expanded ? 'false' : 'true' }); toggleAllNavSections(navSections); - button.setAttribute('aria-label', expanded ? 'Open navigation' : 'Close navigation'); - // enable menu collapse on escape keypress - if (!expanded || isDesktop.matches) { - // collapse menu on escape press - window.addEventListener('keydown', closeOnEscape); - } else { - window.removeEventListener('keydown', closeOnEscape); - } + setAttributes(button, { + 'aria-label': expanded ? 'Open navigation' : 'Close navigation', + }); } -/** - * Builds the logo Div. - * @returns {HTMLDivElement} - */ function buildLogo() { - const logo = document.createElement('div'); - logo.classList.add('nav-logo'); - logo.innerHTML = ` -
- - - `; + const logo = createElemWithClass('div', 'nav-logo'); + const link = document.createElement('a'); + setAttributes(link, { href: '/', rel: 'noopener', tabindex: '0' }); + const img = document.createElement('img'); + setAttributes(img, { + alt: 'Takeda Logo', + class: 'logo', + src: '/styles/images/logo.png', + loading: 'lazy', + height: '274', + width: '815', + }); + link.appendChild(img); + logo.appendChild(link); return logo; } -/** - * Builds the hamburger menu Div. - * @returns {HTMLDivElement} - */ -function buildBrand() { - const brand = document.createElement('div'); - brand.classList.add('nav-brand'); - brand.innerHTML = 'Takeda\'s Integrated Health Systems Team'; - return brand; -} - -/** - * Builds the hamburger menu Div. - * @returns {HTMLDivElement} - */ function buildHamburger() { - const hamburger = document.createElement('div'); - hamburger.classList.add('nav-hamburger'); - - hamburger.innerHTML = ` - - `; + const hamburger = createElemWithClass('div', 'nav-hamburger'); + const button = document.createElement('button'); + setAttributes(button, { + class: 'nav-hamburger-icon', + 'aria-controls': 'nav', + 'aria-label': 'Open navigation', + tabindex: '0', + }); + const iconHamburger = createElemWithClass('span', 'icon', 'icon-hamburger'); + const iconClose = createElemWithClass('span', 'icon', 'icon-close'); + button.append(iconHamburger, iconClose); + hamburger.appendChild(button); return hamburger; } @@ -122,71 +91,65 @@ function buildSections(sections) { const active = section.querySelector('a'); if (active) { const url = new URL(active.href); - if (window.location.pathname.startsWith(url.pathname)) section.classList.add('active'); + if (window.location.pathname === url.pathname) { + section.classList.add('active'); + } } const submenu = section.querySelector('ul'); if (submenu) { - const icon = submenu.querySelector('span.icon-right-arrow'); - if (icon) { - const li = icon.closest('li'); - li.classList.add('hide', 'show-all'); - } - const anchor = section.querySelector('a'); anchor.append(expander.cloneNode()); section.classList.add('nav-drop'); - section.setAttribute('aria-expanded', 'false'); + + const submenuLinks = submenu.querySelectorAll('li > a'); + const isCurrentPath = Array.from(submenuLinks).some((link) => { + const isMatch = window.location.pathname === new URL(link.href).pathname + && link.hash === window.location.hash; + if (isMatch) { + link.parentElement.classList.add('active'); + link.href = link.hash; + } + return isMatch; + }); + section.setAttribute('aria-expanded', isCurrentPath ? 'true' : 'false'); + + submenuLinks.forEach((link) => { + if (window.location.pathname === new URL(link.href).pathname) { + link.href = link.hash; + } + link.addEventListener('click', (e) => { + // remove active class from all links + submenuLinks.forEach((l) => l.parentElement.classList.remove('active')); + if (link.hash && window.location.pathname === new URL(link.href).pathname) { + link.parentElement.classList.add('active'); + e.stopPropagation(); + } + }); + }); anchor.setAttribute('tabindex', '0'); anchor.setAttribute('role', 'button'); anchor.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); const expanded = section.getAttribute('aria-expanded') === 'true'; - if (e.pointerType !== 'mouse' || !isDesktop.matches) { - e.preventDefault(); - e.stopPropagation(); - toggleAllNavSections(sections); + toggleAllNavSections(sections); + if (!isDesktop.matches) { section.setAttribute('aria-expanded', !expanded); - const all = section.querySelector('.show-all'); - if (all) { - all.classList.remove('hide'); - } - if (e.pointerType !== 'mouse') { - section.setAttribute('data-touch-click', 'true'); - } + } else { + section.setAttribute('aria-expanded', true); } - }); - section.addEventListener('pointerenter', (e) => { - if (e.pointerType === 'mouse') { - toggleAllNavSections(sections); - section.setAttribute('aria-expanded', 'true'); - const all = section.querySelector('.show-all'); - if (all) { - all.classList.add('hide'); - } + const all = section.querySelector('.show-all'); + if (all) { + all.classList.remove('hide'); } - }); - section.addEventListener('pointerleave', (e) => { - if (e.pointerType === 'mouse') { - toggleAllNavSections(sections); - section.setAttribute('aria-expanded', 'false'); - const all = section.querySelector('.show-all'); - if (all) { - all.classList.remove('hide'); - } + if (e.pointerType !== 'mouse') { + section.setAttribute('data-touch-click', 'true'); } }); - // enable nav dropdown keyboard accessibility - anchor.addEventListener('keydown', openOnKeydown); } }); - sections.querySelectorAll(':scope > ul > li').forEach((li) => { - if (li.textContent.match(/^---/)) { - li.classList.add('separator'); - li.textContent = ''; - } - }); - return sections; } @@ -197,7 +160,7 @@ function buildSections(sections) { export default async function decorate(block) { // fetch nav content const navMeta = getMetadata('nav'); - const navPath = navMeta ? new URL(navMeta).pathname : '/nav'; + const navPath = navMeta ? new URL(navMeta).pathname : '/drafts/phase-two-redo/nav'; const resp = await fetch(`${navPath}.plain.html`); if (resp.ok) { @@ -210,12 +173,13 @@ export default async function decorate(block) { wrapper.classList.replace('default-content-wrapper', `nav-${clazz}`); }); - block.innerHTML = ` - - `; + const navWrapper = createElemWithClass('div', 'nav-wrapper'); + const navElement = createElemWithClass('nav', 'nav'); + navElement.id = 'nav'; + navElement.setAttribute('aria-expanded', isDesktop.matches); + + navWrapper.appendChild(navElement); + block.appendChild(navWrapper); const nav = block.querySelector('nav'); const sections = buildSections(html.querySelector('.nav-sections')); @@ -226,25 +190,22 @@ export default async function decorate(block) { toggleMenu(nav, sections); }); + const contactus = html.querySelector('.nav-contact-us'); + const utility = html.querySelector('.nav-utility'); // Order maintains tabindex keyboard nav nav.append(buildLogo()); - nav.append(buildBrand()); nav.append(hamburger); nav.append(sections); - nav.append(utility); + nav.append(contactus); - isDesktop.addEventListener('change', () => toggleMenu(nav, sections, isDesktop.matches)); - document.body.addEventListener('click', () => { - toggleAllNavSections(sections); - }); await decorateIcons(block); // Add a link to the Author guide, if anywhere in the Author Guide if (window.location.pathname.startsWith('/author-guide')) { const li = document.createElement('li'); - li.innerHTML = 'Author Guide'; + li.innerHTML = 'Author Guide'; utility.querySelector('ul').prepend(li); } } diff --git a/scripts/scripts.js b/scripts/scripts.js index 732a8cfd..0b8ec971 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -205,6 +205,16 @@ function decorateSectionButtonRow(main) { }); } +function decorateSectionIDs(main) { + main.querySelectorAll(':scope .section').forEach((section) => { + const id = section.getAttribute('data-id'); + if (id) { + section.id = id.toLowerCase().replaceAll(' ', '-'); + } + }); + +} + /** * Decorates the main element. * @param {Element} main The main element @@ -222,6 +232,7 @@ export function decorateMain(main) { decorateSectionButtonRow(main); decorateSectionBackgroundImage(main); decorateSectionGradientTopper(main); + decorateSectionIDs(main); } /** diff --git a/styles/styles.css b/styles/styles.css index 9c450ff8..cfa72ac6 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -58,7 +58,7 @@ --gray-neutral-90: #1e1e23; --gray-box-shadow: rgb(0 0 0 / 20%); --gray-gradient-light: #edeff0; - --gray-gradient-dark: #9da8af; + --gray-gradient-dark: #9da8af; /* general colors */ --silver: #d8d8d8; @@ -255,6 +255,15 @@ body.appear { header { height: var(--nav-height); + position: fixed; + top: 0; + left: 0; + width: 100vw; + z-index: 1000; +} + +main { + padding-top: var(--nav-height); } body.gray-gradient > main { @@ -391,7 +400,7 @@ main > .section[data-background-image] > .section-bg-image-wrapper picture img { width: 100%; object-fit: cover; object-position: center; - + /* temporary until there are right sized images */ height: 600px; } From 883fb3d9e8a251cc0569e45c4606cdaef4e56faa Mon Sep 17 00:00:00 2001 From: Sagar Date: Tue, 6 Feb 2024 21:01:45 +0530 Subject: [PATCH 029/133] fixes for eslint errors --- blocks/floating-images/floating-images.js | 2 -- blocks/form/form.js | 6 ------ blocks/header/header.js | 3 ++- scripts/scripts.js | 3 +-- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/blocks/floating-images/floating-images.js b/blocks/floating-images/floating-images.js index 758e6a1f..f4f57d7a 100644 --- a/blocks/floating-images/floating-images.js +++ b/blocks/floating-images/floating-images.js @@ -2,8 +2,6 @@ export default function decorate(block) { const container = block.querySelector(':scope > div'); container.children[0].classList.add('content'); container.children[1].classList.add('image'); - const kids = block.children[1]; - console.log(kids); const picture = block.querySelector('picture'); const img = block.querySelector('img'); diff --git a/blocks/form/form.js b/blocks/form/form.js index 8302f1e9..ee4b8537 100644 --- a/blocks/form/form.js +++ b/blocks/form/form.js @@ -32,19 +32,16 @@ function constructPayload(form) { } async function submissionFailure(error, form) { - alert(error); // TODO define error mechansim form.setAttribute('data-submitting', 'false'); form.querySelector('button[type="submit"]').disabled = false; } async function prepareRequest(form, transformer) { const { payload } = constructPayload(form); - console.log(payload); const headers = { 'Content-Type': 'application/json', }; const body = JSON.stringify({ data: payload }); - console.log(body); const url = form.dataset.submit || form.dataset.action; if (typeof transformer === 'function') { return transformer({ headers, body, url }, form); @@ -61,7 +58,6 @@ async function submitForm(form, transformer) { headers, body, }); - console.log(response); if (response.ok) { /* window.location.href = form.dataset?.redirect || 'thankyou'; */ } else { @@ -336,7 +332,6 @@ async function fetchData(url) { async function fetchForm(pathname) { // get the main form const jsonData = await fetchData(pathname); - console.log(jsonData); return jsonData; } @@ -379,7 +374,6 @@ async function createForm(formURL) { export default async function decorate(block) { const formLink = block.querySelector('a[href$=".json"]'); - console.log(formLink); if (formLink) { const form = await createForm(formLink.href); formLink.replaceWith(form); diff --git a/blocks/header/header.js b/blocks/header/header.js index 4aa2c739..92ccd758 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -7,6 +7,7 @@ import { const isDesktop = window.matchMedia('(min-width: 900px)'); function setAttributes(element, attributes) { + // eslint-disable-next-line for (const key in attributes) { element.setAttribute(key, attributes[key]); } @@ -205,7 +206,7 @@ export default async function decorate(block) { // Add a link to the Author guide, if anywhere in the Author Guide if (window.location.pathname.startsWith('/author-guide')) { const li = document.createElement('li'); - li.innerHTML = 'Author Guide'; + li.innerHTML = 'Author Guide'; utility.querySelector('ul').prepend(li); } } diff --git a/scripts/scripts.js b/scripts/scripts.js index 0b8ec971..e2133e73 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -77,7 +77,7 @@ async function decorateDisclaimerModal() { disclaimerContainer.remove(); }); main.append(disclaimerContainer); - modal.querySelector('.close').addEventListener('click', function() { + modal.querySelector('.close').addEventListener('click', () => { document.querySelector('.disclaimer-modal-container').style.display = 'none'; }); } @@ -212,7 +212,6 @@ function decorateSectionIDs(main) { section.id = id.toLowerCase().replaceAll(' ', '-'); } }); - } /** From 519931eac646db64933dd5ea72d31ddd36093d26 Mon Sep 17 00:00:00 2001 From: Sagar Date: Tue, 6 Feb 2024 21:08:52 +0530 Subject: [PATCH 030/133] fix for few eslint issues --- blocks/cards/icon.css | 9 +---- blocks/floating-images/floating-images.css | 1 - blocks/header/header.css | 47 +++++++++++----------- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/blocks/cards/icon.css b/blocks/cards/icon.css index 074a96f4..512239bd 100644 --- a/blocks/cards/icon.css +++ b/blocks/cards/icon.css @@ -54,21 +54,18 @@ font-weight: var(--font-weight-bold); line-height: var(--line-height-s); } -@media screen and (min-width: 600px) { - .cards.icon.block .card { - - } -} @media screen and (min-width: 900px) { .cards.icon.block .row { grid-template-columns: repeat(2, 1fr); } + .cards.icon.block .card { border-right: 1px solid grey; border-bottom:0; } + main .section[data-layout] .cards.icon.block .row { grid-template-columns: 1fr; } @@ -79,7 +76,5 @@ main .section[data-layout] .cards.icon.block .row { grid-template-columns: repeat(3, 1fr); } - .cards.icon.block .card { - } } diff --git a/blocks/floating-images/floating-images.css b/blocks/floating-images/floating-images.css index 2e7e17c3..2e35053e 100644 --- a/blocks/floating-images/floating-images.css +++ b/blocks/floating-images/floating-images.css @@ -2,7 +2,6 @@ .section.floating-images-container.grey > div { background-color: var(--brand--secondary-subtle); - } .block.floating-images > div { diff --git a/blocks/header/header.css b/blocks/header/header.css index e74af732..821419ad 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -56,6 +56,21 @@ width: 100%; } +.header.block .nav-contact-us > p { + margin: 0; +} + +.header.block .nav-contact-us > p > a { + display: inline-block; + padding: 8px 42px; + font-weight: normal; + text-decoration: none; + color: var(--white); + background-color: var(--dark-red); + border-bottom-left-radius: 20px; + white-space: nowrap; +} + .header.block nav .nav-sections { display: none; } @@ -87,7 +102,7 @@ } .header.block .nav-sections > ul > li.nav-drop > a.active-arrow-down::after { - background-image: url(images/down-arrow.png); + background-image: url('images/down-arrow.png'); } .header.block .nav-sections > ul > li.nav-drop > a > .icon { @@ -118,16 +133,16 @@ display: block; } -.header.block .nav-sections > ul > li > ul > li.active > a { - text-decoration: underline; -} - .header.block .nav-sections > ul > li > ul > li > a { display: flex; justify-content: flex-start; padding: 20px 20px 0; } +.header.block .nav-sections > ul > li > ul > li.active > a { + text-decoration: underline; +} + .header.block nav .nav-contact-us { display: none; } @@ -138,23 +153,9 @@ justify-content: center; } -.header.block .nav-contact-us > p { - margin: 0; -} -.header.block .nav-contact-us > p > a { - display: inline-block; - padding: 8px 42px; - font-weight: normal; - text-decoration: none; - color: var(--white); - background-color: var(--dark-red); - border-bottom-left-radius: 20px; - white-space: nowrap; -} @media (min-width: 900px) { - .header.block nav { grid-template: "logo sections contact-us" 75px @@ -197,13 +198,13 @@ justify-content: space-evenly; } + .header.block .nav-sections > ul > li > ul > li.active > a { + border-bottom: 5px solid var(--white); + } + .header.block .nav-sections > ul > li.nav-drop[aria-expanded="true"] > ul > li > a { padding: 10px 0; color: var(--gray); } - .header.block .nav-sections > ul > li > ul > li.active > a { - border-bottom: 5px solid var(--white); - } - } From 47be5126c789e4e43f518440ba76d31bf8b630fd Mon Sep 17 00:00:00 2001 From: Sagar Date: Wed, 7 Feb 2024 17:07:18 +0530 Subject: [PATCH 031/133] updated styles for footer new design --- blocks/footer/footer.css | 80 +++++++++++++++++++++++++++------------- blocks/footer/footer.js | 22 ++++++----- blocks/header/header.js | 7 +--- scripts/utils.js | 5 +++ 4 files changed, 72 insertions(+), 42 deletions(-) create mode 100644 scripts/utils.js diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index 5a478539..96147173 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -5,57 +5,85 @@ footer { background-color: var(--bright-gray); } - footer .footer.block { max-width: 1200px; margin: auto; } -footer .footer.block > div { +.footer.block .content-wrapper { display: flex; flex-direction: column; - align-items: flex-start; gap: 20px; } -footer .footer.block > div > div:has(picture) { - border-bottom: 2px solid var(--silver); - padding-bottom: 20px; +.footer.block .content-wrapper p { + font-size: var(--body-font-size-xs); + margin-bottom: 14px; + text-align: left; width: 100%; } -footer .footer.block > div > div:has(p>a) { +.footer.block .content-wrapper .footer-contact-us { + background-color: var(--gray-neutral-80); + border-radius: 0 0 10px 10px; + padding: 40px 60px; +} + +.footer.block .content-wrapper .footer-contact-us * { + color: var(--white); +} + +.footer.block .content-wrapper .footer-contact-us h3 { + color: var(--white); + font-size: var(--heading-font-size-l); + padding-bottom: 30px; +} + +.footer.block .content-wrapper .footer-contact-us p { + font-weight: var(--font-weight-semibold); + font-size: var(--body-font-size-l); + text-align: center; + padding-bottom: 30px; +} + +.footer.block .content-wrapper .footer-contact-us a { + display: inline-block; + padding: 8px 42px; + font-weight: normal; + line-height: var(--line-height-l); + text-decoration: none; + color: var(--white); + background-color: var(--dark-red); + border-bottom-left-radius: 20px; +} + +.footer.block .content-wrapper .footer-logo { display: flex; - flex-direction: column; align-items: flex-start; - gap: 0; + flex-direction: column; } -.footer.block picture { +.footer.block .content-wrapper .footer-logo p:has(picture) { + border-bottom: 2px solid var(--silver); +} + +.footer.block .content-wrapper .footer-logo img { + width: 105px; + height: auto; +} + +.footer.block .content-wrapper .footer-links { display: flex; - height: 36px; - width: auto; + flex-direction: column; } -.footer.block a { +.footer.block .content-wrapper .footer-links p > a { color: inherit; text-decoration: none; } -.footer.block a:hover { +.footer.block .content-wrapper .footer-links p > a:hover { text-decoration: underline; opacity: 0.8; } -.footer.block > div > div > p { - font-size: var(--body-font-size-xs); - margin-bottom: 14px; - text-align: left; -} - -@media screen and (min-width: 600px) { - footer .footer.block > div > div:has(p>a){ - flex-direction: row; - gap: 30px; - } -} \ No newline at end of file diff --git a/blocks/footer/footer.js b/blocks/footer/footer.js index 145775a2..a6bf17a0 100644 --- a/blocks/footer/footer.js +++ b/blocks/footer/footer.js @@ -1,4 +1,5 @@ -import { decorateIcons, getMetadata } from '../../scripts/lib-franklin.js'; +import { decorateSections, getMetadata } from '../../scripts/lib-franklin.js'; +import { createElemWithClass } from '../../scripts/utils.js'; /** * loads and decorates the footer @@ -6,7 +7,7 @@ import { decorateIcons, getMetadata } from '../../scripts/lib-franklin.js'; */ export default async function decorate(block) { const footerMeta = getMetadata('footer'); - const footerPath = footerMeta ? new URL(footerMeta).pathname : '/footer'; + const footerPath = footerMeta ? new URL(footerMeta).pathname : '/drafts/phase-two-redo/footer'; const resp = await fetch( `${footerPath}.plain.html`, window.location.pathname.endsWith('/footer') ? { cache: 'reload' } : {}, @@ -16,13 +17,14 @@ export default async function decorate(block) { const footer = document.createElement('div'); // decorate footer DOM footer.innerHTML = await resp.text(); - - // size the footer image - const image = footer.querySelector('picture img'); - image.width = '100'; - image.height = '36'; - - decorateIcons(footer); - block.append(footer); + decorateSections(footer); + const contentWrapper = createElemWithClass('div', 'content-wrapper'); + footer.querySelectorAll('.section[data-section]').forEach((section) => { + const clazz = section.getAttribute('data-section'); + const wrapper = section.children[0]; + wrapper.classList.replace('default-content-wrapper', `footer-${clazz}`); + contentWrapper.append(wrapper); + }); + block.replaceChildren(contentWrapper); } } diff --git a/blocks/header/header.js b/blocks/header/header.js index 92ccd758..d1b50653 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -3,6 +3,7 @@ import { decorateIcons, decorateSections, } from '../../scripts/lib-franklin.js'; +import { createElemWithClass } from '../../scripts/utils.js'; const isDesktop = window.matchMedia('(min-width: 900px)'); @@ -13,12 +14,6 @@ function setAttributes(element, attributes) { } } -function createElemWithClass(type, ...classNames) { - const elem = document.createElement(type); - elem.classList.add(...classNames); - return elem; -} - function toggleAllNavSections(sections, expanded = false) { sections .querySelectorAll('.nav-sections > ul > li.nav-drop') diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 00000000..b1f4256c --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,5 @@ +export function createElemWithClass(type, ...classNames) { + const elem = document.createElement(type); + elem.classList.add(...classNames); + return elem; +} From b1abe8f5904db5b6b3b209d29f3dd70dda6035c9 Mon Sep 17 00:00:00 2001 From: Sagar Date: Wed, 7 Feb 2024 17:43:52 +0530 Subject: [PATCH 032/133] updated background --- blocks/footer/footer.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index 96147173..121d9cd7 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -1,8 +1,8 @@ footer { margin: 0; - padding: 2rem; + padding: 0 2rem 2rem; text-align: center; - background-color: var(--bright-gray); + background: linear-gradient(to top, var(--bright-gray) 75%, white 0%); } footer .footer.block { From d9968572a017ce189c69432dea57858d8dfa213c Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 7 Feb 2024 08:34:36 -0500 Subject: [PATCH 033/133] icon card tweaks --- blocks/cards/icon.css | 10 +++++++++- styles/styles.css | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/blocks/cards/icon.css b/blocks/cards/icon.css index 074a96f4..99aa1d38 100644 --- a/blocks/cards/icon.css +++ b/blocks/cards/icon.css @@ -16,7 +16,8 @@ .cards.icon.block .card { display: flex; - padding: 20px 30px; + padding-right:20px; + margin:20px 0; flex-direction: column; background-color: white; text-align: center; @@ -27,6 +28,7 @@ display: inline-block; height: 70px; width: 70px; + margin-bottom:1em; } .cards.icon.block .card .button-container span.icon { @@ -50,10 +52,16 @@ .cards.icon.block .card h3 { margin-bottom: 1em; + mar font-size: var(--heading-font-size-xs); font-weight: var(--font-weight-bold); line-height: var(--line-height-s); } + +.cards.icon.block .card p { + margin-bottom:0; + +} @media screen and (min-width: 600px) { .cards.icon.block .card { diff --git a/styles/styles.css b/styles/styles.css index 9c450ff8..90017d1d 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -276,7 +276,7 @@ main .section.cards-container { main .section.content-bump-out .default-content-wrapper { border:0; - max-width: 1200px; + max-width: 1040px; margin: -81px auto 0 ; display: table; width: 100%; From 74b8547a0600f345f7f9f8b3c2c33b633f6bd532 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 7 Feb 2024 12:56:07 -0500 Subject: [PATCH 034/133] adding new launch env --- blocks/form/form.css | 4 ++-- blocks/hero/default-hero.css | 2 +- scripts/delayed.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blocks/form/form.css b/blocks/form/form.css index 64c85aac..3cd54d03 100644 --- a/blocks/form/form.css +++ b/blocks/form/form.css @@ -247,7 +247,7 @@ main .form-file-wrapper .file-description button { @media (min-width: 768px) { :root { - --form-width: 740px; + --form-width: 800px; } main .form button { @@ -257,7 +257,7 @@ main .form-file-wrapper .file-description button { @media (min-width: 1200px) { :root { - --form-width: 990px; + --form-width: 1200px; } } diff --git a/blocks/hero/default-hero.css b/blocks/hero/default-hero.css index 9c08124f..1210bb12 100644 --- a/blocks/hero/default-hero.css +++ b/blocks/hero/default-hero.css @@ -155,7 +155,7 @@ @media screen and (min-width: 1280px) { .hero.block.default .content-wrapper { - padding: 80px 40px; + padding: 80px 80px; max-width: var(--normal-page-width); } } diff --git a/scripts/delayed.js b/scripts/delayed.js index 5e7c393e..d554b665 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -7,7 +7,7 @@ sampleRUM('cwv'); // add more delayed functionality here const loadAdobeDTM = async () => { - await loadScript('https://assets.adobedtm.com/8fee56b0a165/0dcea4176083/launch-868c1997ca51-development.min.js'); + await loadScript('https://assets.adobedtm.com/8fee56b0a165/0dcea4176083/launch-5b9d43d0b93e-development.min.js'); }; await loadAdobeDTM(); From 437f70f7aaf57c1d85c5a1187295f919821eeed1 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 7 Feb 2024 13:29:29 -0500 Subject: [PATCH 035/133] added new launch env --- scripts/delayed.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/delayed.js b/scripts/delayed.js index d554b665..5e7c393e 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -7,7 +7,7 @@ sampleRUM('cwv'); // add more delayed functionality here const loadAdobeDTM = async () => { - await loadScript('https://assets.adobedtm.com/8fee56b0a165/0dcea4176083/launch-5b9d43d0b93e-development.min.js'); + await loadScript('https://assets.adobedtm.com/8fee56b0a165/0dcea4176083/launch-868c1997ca51-development.min.js'); }; await loadAdobeDTM(); From e8979c5998012024c1cdee54df68f9d842870b34 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 7 Feb 2024 15:20:38 -0500 Subject: [PATCH 036/133] updating launch code and adding assets library --- scripts/delayed.js | 2 +- tools/config.json | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tools/config.json diff --git a/scripts/delayed.js b/scripts/delayed.js index 5e7c393e..d554b665 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -7,7 +7,7 @@ sampleRUM('cwv'); // add more delayed functionality here const loadAdobeDTM = async () => { - await loadScript('https://assets.adobedtm.com/8fee56b0a165/0dcea4176083/launch-868c1997ca51-development.min.js'); + await loadScript('https://assets.adobedtm.com/8fee56b0a165/0dcea4176083/launch-5b9d43d0b93e-development.min.js'); }; await loadAdobeDTM(); diff --git a/tools/config.json b/tools/config.json new file mode 100644 index 00000000..8ff29a19 --- /dev/null +++ b/tools/config.json @@ -0,0 +1,17 @@ +{ + "project": "Takeda", + "host": "https://phase-two-redo--takeda-ihs--hlxsites.hlx.page/", + "plugins": [ + { + "id": "asset-library", + "title": "AEM Assets Library", + "environments": [ + "edit" + ], + "url": "https://experience.adobe.com/solutions/CQ-assets-selectors/static-assets/resources/franklin/asset-selector.html", + "isPalette": true, + "includePaths": [ "**.docx**" ], + "paletteRect": "top: 50px; bottom: 10px; right: 10px; left: auto; width:400px; height: calc(100vh - 60px)" + } + ] +} \ No newline at end of file From 84faf8727052d90d2974af00e7d6a3fd9dd4a88d Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 8 Feb 2024 07:09:51 -0500 Subject: [PATCH 037/133] https://github.com/hlxsites/takeda-ihs/issues/169 --- blocks/header/header.css | 2 +- styles/styles.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blocks/header/header.css b/blocks/header/header.css index 821419ad..e72fd2a3 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -85,7 +85,7 @@ padding: 20px 0; margin: 0; flex-direction: column; - justify-content: space-around; + justify-content: center; gap: 20px; } diff --git a/styles/styles.css b/styles/styles.css index 6911f346..4d773ddc 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -610,7 +610,7 @@ main .disclaimer-modal-container { @media screen and (min-width: 600px) { :root { - --nav-height: 125px; + --nav-height: 80px; } main > .section > div { From 6924d1f1dbd46f6b60b5f74be529f11817c93a7b Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 8 Feb 2024 09:16:44 -0500 Subject: [PATCH 038/133] bunch of floating image content container tweaks bunch of tweaks --- blocks/floating-images/floating-images.css | 2 +- styles/styles.css | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blocks/floating-images/floating-images.css b/blocks/floating-images/floating-images.css index 2e35053e..c3e9a9ef 100644 --- a/blocks/floating-images/floating-images.css +++ b/blocks/floating-images/floating-images.css @@ -55,7 +55,7 @@ @media screen and (min-width: 600px) { .block.floating-images > div { flex-direction: row; - align-items: center; + align-items: start; padding:20px 0; } diff --git a/styles/styles.css b/styles/styles.css index 4d773ddc..d7fac8f3 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -285,13 +285,13 @@ main .section.cards-container { main .section.content-bump-out .default-content-wrapper { border:0; - max-width: 1040px; + max-width: 1030px; margin: -81px auto 0 ; display: table; width: 100%; text-align: left; background-color: #fff; - padding: 12px 50px; + padding: 12px 91px; position: relative; } @@ -657,7 +657,7 @@ main .disclaimer-modal-container { } main > .section[data-background-image] { - /* padding-bottom: 300px; */ + padding-bottom: 81px; } From 7adcc905f728a4548b123d2efdbe7de0c7cc496a Mon Sep 17 00:00:00 2001 From: Sagar Date: Thu, 8 Feb 2024 21:32:59 +0530 Subject: [PATCH 039/133] updated styles for footer links alignment --- blocks/footer/footer.css | 15 +++++++++++++-- styles/styles.css | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index 121d9cd7..1d019cf0 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -1,6 +1,6 @@ footer { margin: 0; - padding: 0 2rem 2rem; + padding: 0 20px 20px; text-align: center; background: linear-gradient(to top, var(--bright-gray) 75%, white 0%); } @@ -20,7 +20,6 @@ footer .footer.block { font-size: var(--body-font-size-xs); margin-bottom: 14px; text-align: left; - width: 100%; } .footer.block .content-wrapper .footer-contact-us { @@ -61,10 +60,14 @@ footer .footer.block { display: flex; align-items: flex-start; flex-direction: column; + margin-top: 30px; } .footer.block .content-wrapper .footer-logo p:has(picture) { border-bottom: 2px solid var(--silver); + width: 100%; + padding-bottom: 30px; + margin-bottom: 30px; } .footer.block .content-wrapper .footer-logo img { @@ -75,6 +78,7 @@ footer .footer.block { .footer.block .content-wrapper .footer-links { display: flex; flex-direction: column; + align-items: center; } .footer.block .content-wrapper .footer-links p > a { @@ -87,3 +91,10 @@ footer .footer.block { opacity: 0.8; } +@media (min-width: 900px) { + footer .footer.block .content-wrapper .footer-links { + flex-direction: row; + align-items: flex-start; + gap: 40px; + } +} diff --git a/styles/styles.css b/styles/styles.css index d7fac8f3..bbf15eb3 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -86,7 +86,7 @@ --hello-programs: #808080; /* Font Colors */ - --default-text-color: var(--black-olive); + --default-text-color: var(--gray-neutral-80); /* Fonts */ --font-family-opensans: 'Open Sans', 'Open Sans Fallback', 'Arial', sans-serif; From ac7406aab310a33fd6be6edf6447e1f9ba6acdd0 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 9 Feb 2024 06:12:07 -0500 Subject: [PATCH 040/133] removing GA --- scripts/third-party.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/third-party.js b/scripts/third-party.js index fe3c838c..60e04cc2 100644 --- a/scripts/third-party.js +++ b/scripts/third-party.js @@ -1,8 +1,8 @@ -const GTM_SCRIPT = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': - new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], - j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= - 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); -})(window,document,'script','dataLayer','GTM-P49296M');`; +//* const GTM_SCRIPT = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': +//* new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], +//* j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= +//* 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); +//* })(window,document,'script','dataLayer','GTM-P49296M');`; *// function createInlineScript(innerHTML, parent) { const script = document.createElement('script'); From f8e48cbcbd65be9b77d240537ad237e3baaa8146 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 9 Feb 2024 06:28:44 -0500 Subject: [PATCH 041/133] remove gtm --- scripts/third-party.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/third-party.js b/scripts/third-party.js index 60e04cc2..1b21dd65 100644 --- a/scripts/third-party.js +++ b/scripts/third-party.js @@ -1,8 +1,8 @@ -//* const GTM_SCRIPT = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': -//* new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], -//* j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= -//* 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); -//* })(window,document,'script','dataLayer','GTM-P49296M');`; *// +const GTM_SCRIPT = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': + new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], + j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= + 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); +})(window,document,'script','dataLayer','GTM-P49296M');`; function createInlineScript(innerHTML, parent) { const script = document.createElement('script'); @@ -14,6 +14,6 @@ function createInlineScript(innerHTML, parent) { // eslint-disable-next-line import/prefer-default-export export function integrateMartech() { if (window.location.href.match(/^https?:\/\/(localhost|127.0.0.1)/) === null) { - createInlineScript(GTM_SCRIPT, document.body); + createInlineScript(document.body); } } From 4e740d7816db3996a7be5e24b210e2dc3f8643b7 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 9 Feb 2024 06:31:03 -0500 Subject: [PATCH 042/133] remove const GTM --- scripts/third-party.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scripts/third-party.js b/scripts/third-party.js index 1b21dd65..392b19d5 100644 --- a/scripts/third-party.js +++ b/scripts/third-party.js @@ -1,9 +1,3 @@ -const GTM_SCRIPT = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': - new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], - j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= - 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); -})(window,document,'script','dataLayer','GTM-P49296M');`; - function createInlineScript(innerHTML, parent) { const script = document.createElement('script'); script.type = 'text/partytown'; From f5f274b7d60e23269bb824376315905432e30341 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 9 Feb 2024 06:35:25 -0500 Subject: [PATCH 043/133] more gtm errors --- scripts/third-party.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/third-party.js b/scripts/third-party.js index 392b19d5..1b21dd65 100644 --- a/scripts/third-party.js +++ b/scripts/third-party.js @@ -1,3 +1,9 @@ +const GTM_SCRIPT = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': + new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], + j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= + 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); +})(window,document,'script','dataLayer','GTM-P49296M');`; + function createInlineScript(innerHTML, parent) { const script = document.createElement('script'); script.type = 'text/partytown'; From 1a092a297862b23d497a84249eac42ff0dcd2d4c Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 9 Feb 2024 10:59:38 -0500 Subject: [PATCH 044/133] nav tune up, space above and space between lis --- blocks/header/header.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blocks/header/header.css b/blocks/header/header.css index e72fd2a3..7212ddaa 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -86,7 +86,7 @@ margin: 0; flex-direction: column; justify-content: center; - gap: 20px; + gap: 48px; } .header.block .nav-sections > ul > li > ul { @@ -161,7 +161,7 @@ "logo sections contact-us" 75px ". . hamburger" min-content / min-content 1fr min-content; - padding: 20px 90px; + padding: 0 90px; } .header.block .nav-hamburger { From b2669544a0edaa0cafbba61b65a79228319e259b Mon Sep 17 00:00:00 2001 From: Sagar Date: Tue, 13 Feb 2024 17:32:47 +0530 Subject: [PATCH 045/133] top nav style changes as per new designs --- blocks/header/header.css | 59 +++++++++++++++++++------------- blocks/header/header.js | 74 ++++++++++++++++++++-------------------- styles/styles.css | 4 +-- 3 files changed, 75 insertions(+), 62 deletions(-) diff --git a/blocks/header/header.css b/blocks/header/header.css index 7212ddaa..d7171027 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -1,11 +1,11 @@ .header.block nav { display: grid; grid-template: - "logo . hamburger" 75px + "logo . hamburger" 65px "sections sections sections" min-content "contact-us contact-us contact-us" min-content / min-content 1fr min-content; - padding: 20px; + padding: 0 20px; align-items: center; background-color: var(--white); } @@ -13,7 +13,7 @@ .header.block nav a { text-decoration: none; color: var(--quartz); - font-weight: bold; + font-weight: normal; } .header.block.nav-logo { @@ -23,7 +23,7 @@ .header.block .nav-logo img { padding: 15px 0; height: auto; - width: 105px; + width: 99px; } .header.block .nav-hamburger { @@ -63,12 +63,12 @@ .header.block .nav-contact-us > p > a { display: inline-block; padding: 8px 42px; - font-weight: normal; text-decoration: none; color: var(--white); background-color: var(--dark-red); border-bottom-left-radius: 20px; white-space: nowrap; + line-height: 19px; } .header.block nav .nav-sections { @@ -86,7 +86,7 @@ margin: 0; flex-direction: column; justify-content: center; - gap: 48px; + gap: 16px; } .header.block .nav-sections > ul > li > ul { @@ -97,22 +97,23 @@ display: flex; align-items: center; justify-content: space-between; - padding: 10px 0; + padding: 12px 0; border-bottom: 1px solid var(--gray); + font-size: 22px; } -.header.block .nav-sections > ul > li.nav-drop > a.active-arrow-down::after { +.header.block .nav-sections > ul > li.top-nav > a.active-arrow-down::after { background-image: url('images/down-arrow.png'); } -.header.block .nav-sections > ul > li.nav-drop > a > .icon { +.header.block .nav-sections > ul > li.top-nav > a > .icon { display: inline-block; position: relative; height: 24px; width: 24px; } -.header.block .nav-sections > ul > li.nav-drop .icon svg { +.header.block .nav-sections > ul > li.top-nav .icon svg { position: absolute; top: 0; left: 0; @@ -120,26 +121,27 @@ height: 100%; } -.header.block .nav-sections > ul > li.nav-drop > a > span { +.header.block .nav-sections > ul > li.top-nav > a > span { transition: transform 0.3s ease-in-out; transform: rotate(0); } -.header.block .nav-sections > ul > li.nav-drop[aria-expanded="true"] > a > span { +.header.block .nav-sections > ul > li.top-nav[aria-expanded="true"] > a > span { transform: rotate(180deg); } -.header.block .nav-sections > ul > li.nav-drop[aria-expanded="true"] > ul { +.header.block .nav-sections > ul > li.top-nav[aria-expanded="true"] > ul { display: block; } -.header.block .nav-sections > ul > li > ul > li > a { +.header.block .nav-sections > ul > li.top-nav > ul > li.sub-nav > a { display: flex; justify-content: flex-start; - padding: 20px 20px 0; + padding: 10px 20px 0; + font-size: 18px; } -.header.block .nav-sections > ul > li > ul > li.active > a { +.header.block .nav-sections > ul > li.top-nav > ul > li.sub-nav.active > a { text-decoration: underline; } @@ -151,6 +153,7 @@ grid-area: contact-us; display: flex; justify-content: center; + margin: 20px; } @@ -158,7 +161,7 @@ @media (min-width: 900px) { .header.block nav { grid-template: - "logo sections contact-us" 75px + "logo sections contact-us" 65px ". . hamburger" min-content / min-content 1fr min-content; padding: 0 90px; @@ -178,33 +181,43 @@ width: 100%; } - .header.block .nav-sections > ul > li > a { + .header.block .nav-sections > ul > li.top-nav > a { padding: 0; border-bottom: none; + font-size: 14px; + font-weight: bold; } - .header.block .nav-sections > ul > li.nav-drop > a > .icon { + .header.block .nav-sections > ul > li.top-nav > a > .icon { display: none; } - .header.block .nav-sections > ul > li.nav-drop[aria-expanded="true"] > ul { + .header.block .nav-sections > ul > li.top-nav[aria-expanded="true"] > ul { position: absolute; left: 0; width: 100vw; display: flex; opacity: 1; background-color: var(--gray-neutral-80); - top: 86px; + top: 65px; justify-content: space-evenly; } - .header.block .nav-sections > ul > li > ul > li.active > a { + .header.block .nav-sections > ul > li.top-nav > ul > li.sub-nav.active > a { border-bottom: 5px solid var(--white); } - .header.block .nav-sections > ul > li.nav-drop[aria-expanded="true"] > ul > li > a { + .header.block .nav-sections > ul > li.top-nav[aria-expanded="true"] > ul > li.sub-nav > a { padding: 10px 0; color: var(--gray); + font-size: 14px; + font-weight: bold; } } + +@media (min-width: 1200px) { +.header.block nav { + padding: 0 140px; +} +} diff --git a/blocks/header/header.js b/blocks/header/header.js index d1b50653..74d8bff3 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -16,7 +16,7 @@ function setAttributes(element, attributes) { function toggleAllNavSections(sections, expanded = false) { sections - .querySelectorAll('.nav-sections > ul > li.nav-drop') + .querySelectorAll('.nav-sections > ul > li.top-nav') .forEach((section) => { setAttributes(section, { 'aria-expanded': expanded, @@ -83,65 +83,65 @@ function buildSections(sections) { const expander = document.createElement('span'); expander.classList.add('icon', 'icon-chevron-down'); - sections.querySelectorAll(':scope > ul > li').forEach((section) => { - const active = section.querySelector('a'); + sections.querySelectorAll(':scope > ul > li').forEach((topmenu) => { + topmenu.classList.add('top-nav'); + const active = topmenu.querySelector('a'); if (active) { const url = new URL(active.href); if (window.location.pathname === url.pathname) { - section.classList.add('active'); + topmenu.classList.add('active'); } } - const submenu = section.querySelector('ul'); + const submenu = topmenu.querySelector('ul'); if (submenu) { - const anchor = section.querySelector('a'); - anchor.append(expander.cloneNode()); - section.classList.add('nav-drop'); + const topanchor = topmenu.querySelector('a'); + topanchor.append(expander.cloneNode()); const submenuLinks = submenu.querySelectorAll('li > a'); - const isCurrentPath = Array.from(submenuLinks).some((link) => { - const isMatch = window.location.pathname === new URL(link.href).pathname - && link.hash === window.location.hash; + const isCurrentPath = Array.from(submenuLinks).some((subanchor) => { + const isMatch = window.location.pathname === new URL(subanchor.href).pathname + && subanchor.hash === window.location.hash; if (isMatch) { - link.parentElement.classList.add('active'); - link.href = link.hash; + subanchor.parentElement.classList.add('active'); + subanchor.href = subanchor.hash; } return isMatch; }); - section.setAttribute('aria-expanded', isCurrentPath ? 'true' : 'false'); + topmenu.setAttribute('aria-expanded', isCurrentPath ? 'true' : 'false'); - submenuLinks.forEach((link) => { - if (window.location.pathname === new URL(link.href).pathname) { - link.href = link.hash; + submenuLinks.forEach((subanchor) => { + subanchor.parentElement.classList.add('sub-nav'); + if (window.location.pathname === new URL(subanchor.href).pathname) { + subanchor.href = subanchor.hash; } - link.addEventListener('click', (e) => { + subanchor.addEventListener('click', (e) => { // remove active class from all links submenuLinks.forEach((l) => l.parentElement.classList.remove('active')); - if (link.hash && window.location.pathname === new URL(link.href).pathname) { - link.parentElement.classList.add('active'); + if (subanchor.hash && window.location.pathname === new URL(subanchor.href).pathname) { + subanchor.parentElement.classList.add('active'); e.stopPropagation(); + toggleMenu(document.getElementById('nav'), sections, false); } }); }); - anchor.setAttribute('tabindex', '0'); - anchor.setAttribute('role', 'button'); - anchor.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - const expanded = section.getAttribute('aria-expanded') === 'true'; - toggleAllNavSections(sections); + topanchor.setAttribute('tabindex', '0'); + topanchor.setAttribute('role', 'button'); + topanchor.addEventListener('click', (e) => { if (!isDesktop.matches) { - section.setAttribute('aria-expanded', !expanded); - } else { - section.setAttribute('aria-expanded', true); - } - const all = section.querySelector('.show-all'); - if (all) { - all.classList.remove('hide'); - } - if (e.pointerType !== 'mouse') { - section.setAttribute('data-touch-click', 'true'); + e.preventDefault(); + e.stopPropagation(); + const expanded = topmenu.getAttribute('aria-expanded') === 'true'; + toggleAllNavSections(sections); + topmenu.setAttribute('aria-expanded', !expanded); + const all = topmenu.querySelector('.show-all'); + if (all) { + all.classList.remove('hide'); + } + if (e.pointerType !== 'mouse') { + topmenu.setAttribute('data-touch-click', 'true'); + } } }); } diff --git a/styles/styles.css b/styles/styles.css index bbf15eb3..ab1e8f99 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -118,7 +118,7 @@ --heading-font-size-xl: 42px; /* Nav Height */ - --nav-height: 100px; + --nav-height: 65px; /* Line heights */ --line-height-xs: 1.15em; @@ -610,7 +610,7 @@ main .disclaimer-modal-container { @media screen and (min-width: 600px) { :root { - --nav-height: 80px; + --nav-height: 65px; } main > .section > div { From 94e196fb8233af8d802046949880a36f1e31e7fe Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 13 Feb 2024 12:17:54 -0500 Subject: [PATCH 046/133] trying to add gotham font --- fonts/GothamSSm-Book_Web.woff2 | Bin 0 -> 59494 bytes head.html | 1 + styles/lazy-styles.css | 1 + styles/styles.css | 17 ++++++++++++----- 4 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 fonts/GothamSSm-Book_Web.woff2 diff --git a/fonts/GothamSSm-Book_Web.woff2 b/fonts/GothamSSm-Book_Web.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1d9ade9e078160fd76599a5613e82f58794677d1 GIT binary patch literal 59494 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 + \ No newline at end of file diff --git a/styles/lazy-styles.css b/styles/lazy-styles.css index f82796c6..11aa6efd 100644 --- a/styles/lazy-styles.css +++ b/styles/lazy-styles.css @@ -2,3 +2,4 @@ @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400;1,600;1,700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Oswald:wght@400;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap'); +@import url('/fonts/GothamSSm-Book_Web.woff2'); diff --git a/styles/styles.css b/styles/styles.css index bbf15eb3..b7c36164 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -52,7 +52,7 @@ --bright-gray: #efefef; --medium-gray: #ddd; --gray: #979797; - --gray-neutral-70: #454545; + --gray-neutral-70: #333333; --brand--secondary-subtle: #edf2fa; --gray-neutral-80: #333; --gray-neutral-90: #1e1e23; @@ -88,8 +88,16 @@ /* Font Colors */ --default-text-color: var(--gray-neutral-80); + @font-face { + font-family: 'Gotham'; + src: url('../fonts/GothamSSm-Book_Web.woff2') format("woff2"); + font-weight: 400; + font-style: normal; + font-stretch: normal; + } + /* Fonts */ - --font-family-opensans: 'Open Sans', 'Open Sans Fallback', 'Arial', sans-serif; + --font-family-opensans: 'Gotham', 'Gotham Fallback', 'Arial', sans-serif; --body-font-family: var(--font-family-opensans); --heading-font-family: var(--font-family-opensans); @@ -114,7 +122,7 @@ --heading-font-size-xs: 20px; --heading-font-size-s: 24px; --heading-font-size-m: 26px; - --heading-font-size-l: 32px; + --heading-font-size-l: 36px; --heading-font-size-xl: 42px; /* Nav Height */ @@ -167,7 +175,7 @@ sub { h1, h2, h3 { font-family: var(--heading-font-family); font-weight: var(--font-weight-bold); - color: var(--black); + color: var(--gray-neutral-80); } h1 { @@ -713,7 +721,6 @@ main .disclaimer-modal-container { main > .section[data-background-image] > .section-bg-image-wrapper { height: 30%; - padding-top: 40px; } main .section[data-layout="25/75"] .layout-content-wrapper { From 272e02598a64ad544630944bd478df3139440fd4 Mon Sep 17 00:00:00 2001 From: gargadobe <81246588+gargadobe@users.noreply.github.com> Date: Tue, 13 Feb 2024 23:05:20 +0530 Subject: [PATCH 047/133] Updated form block (#170) --- blocks/form/constant.js | 8 + blocks/form/file.js | 162 +++++++ blocks/form/form.css | 285 +++++++++++-- blocks/form/form.js | 586 +++++++++++++------------- blocks/form/integrations/recaptcha.js | 54 +++ blocks/form/layout/repeat.js | 100 +++++ blocks/form/layout/wizard.js | 117 +++++ blocks/form/submit.js | 102 +++++ blocks/form/transform.js | 275 ++++++++++++ blocks/form/util.js | 197 +++++++++ 10 files changed, 1555 insertions(+), 331 deletions(-) create mode 100644 blocks/form/constant.js create mode 100644 blocks/form/file.js create mode 100644 blocks/form/integrations/recaptcha.js create mode 100644 blocks/form/layout/repeat.js create mode 100644 blocks/form/layout/wizard.js create mode 100644 blocks/form/submit.js create mode 100644 blocks/form/transform.js create mode 100644 blocks/form/util.js diff --git a/blocks/form/constant.js b/blocks/form/constant.js new file mode 100644 index 00000000..92fa3b6e --- /dev/null +++ b/blocks/form/constant.js @@ -0,0 +1,8 @@ +export const fileAttachmentText = 'Select files'; + +export const errorMessages = { + accept: 'The specified file type not supported.', + maxFileSize: 'File too large. Reduce size and try again.', + maxItems: 'Specify a number of items equal to or less than $0.', + minItems: 'Specify a number of items equal to or greater than $0.', +}; diff --git a/blocks/form/file.js b/blocks/form/file.js new file mode 100644 index 00000000..f0d5aa41 --- /dev/null +++ b/blocks/form/file.js @@ -0,0 +1,162 @@ +import { updateOrCreateInvalidMsg } from './util.js'; +import { fileAttachmentText, errorMessages } from './constant.js'; + +const fileSizeRegex = /^(\d*\.?\d+)(\\?(?=[KMGT])([KMGT])(?:i?B)?|B?)$/i; +function sizeToBytes(size, symbol) { + const sizes = { + KB: 1, MB: 2, GB: 3, TB: 4, + }; + const i = 1024 ** sizes[symbol]; + return Math.round(size * i); +} +function getFileSizeInBytes(str) { + let retVal = 0; + if (typeof str === 'string') { + const matches = fileSizeRegex.exec(str.trim()); + if (matches != null) { + retVal = sizeToBytes(parseFloat(matches[1]), (matches[2] || 'kb').toUpperCase()); + } + } + return retVal; +} +function matchMediaType(mediaType, accepts) { + return !mediaType || accepts.some((accept) => { + const trimmedAccept = accept.trim(); + const prefixAccept = trimmedAccept.split('/')[0]; + const suffixAccept = trimmedAccept.split('.')[1]; + return ((trimmedAccept.includes('*') && mediaType.startsWith(prefixAccept)) + || (trimmedAccept.includes('.') && mediaType.endsWith(suffixAccept)) + || (trimmedAccept === mediaType)); + }); +} +function maxFileSize(constraint, files) { + const sizeLimit = typeof constraint === 'string' ? getFileSizeInBytes(constraint) : constraint; + let isError = false; + Array.from(files).forEach((file) => { + if (file.size > sizeLimit) { + isError = true; + } + }); + return isError; +} +function acceptCheck(constraint, value) { + if (!constraint || constraint.length === 0 || value === null || value === undefined) { + return true; + } + const tempFiles = Array.from(value); + const invalidFile = tempFiles.some((file) => !matchMediaType(file.type, constraint)); + return !invalidFile; +} +function fileValidation({ wrapper, input, files }) { + const multiple = input.hasAttribute('multiple'); + const acceptedFile = (input.getAttribute('accept') || '').split(','); + const minItems = (parseInt(input.dataset.minItems, 10) || 1); + const maxItems = (parseInt(input.dataset.maxItems, 10) || -1); + const fileSize = `${input.dataset.maxFileSize || '2'}MB`; + if (!acceptCheck(acceptedFile, files)) { + updateOrCreateInvalidMsg(input, (wrapper.dataset.accept || errorMessages.accept)); + } else if (maxFileSize(fileSize, files)) { + updateOrCreateInvalidMsg(input, (wrapper.dataset.maxFileSize || errorMessages.maxFileSize)); + } else if (multiple && maxItems !== -1 && files.length > maxItems) { + updateOrCreateInvalidMsg(input, (wrapper.dataset.maxItems || errorMessages.maxItems.replace(/\$0/, maxItems))); + } else if (multiple && minItems !== 1 && files.length < minItems) { + updateOrCreateInvalidMsg(input, (wrapper.dataset.minItems || errorMessages.minItems.replace(/\$0/, minItems))); + } else { + updateOrCreateInvalidMsg(input, ''); + } +} + +function getFiles(files) { + const dataTransfer = new DataTransfer(); + files.forEach((file) => dataTransfer.items.add(file)); + return dataTransfer.files; +} + +function updateButtonIndex(elements = []) { + elements.forEach((element, index) => { + const button = element.querySelector('button'); + button.dataset.index = index; + }); +} + +function addFileInFileList(fileList, file, index) { + const description = `
+ ${file.name} ${(file.size / (1024 * 1024)).toFixed(2)}mb + +
`; + fileList.innerHTML += description; +} +function clearFileList(fileList) { + fileList.innerHTML = ''; +} + +function createAttachButton(input) { + const button = document.createElement('button'); + button.type = 'button'; + button.innerHTML = fileAttachmentText; + button.onclick = () => { + input.click(); + }; + return button; +} + +function attachFiles(event, { + input, fileList, allFiles, wrapper, +}) { + const multiple = input.hasAttribute('multiple'); + if (!multiple) { + allFiles.splice(0, allFiles.length); + } + const files = event.files || event.target.files; + Array.from(files).forEach((file) => allFiles.push(file)); + fileValidation({ wrapper, input, files: allFiles }); + clearFileList(fileList); + allFiles.forEach((file, index) => addFileInFileList(fileList, file, index)); + input.files = getFiles(allFiles); +} + +function attachChangeEvent({ + input, fileList, allFiles, wrapper, +}) { + input.addEventListener('change', (event) => { + if (!event?.detail?.stopRendering) { + attachFiles(event, { + input, fileList, allFiles, wrapper, + }); + } + }); +} + +function attachRemoveFileEvent({ + input, fileList, allFiles, wrapper, +}) { + fileList.addEventListener('click', (event) => { + if (event.target.tagName === 'BUTTON') { + const index = parseInt(event.target.dataset.index, 10); + allFiles.splice(index, 1); + fileValidation({ wrapper, input, files: allFiles }); + const deletedFile = Array.from(fileList.children)[index]; + fileList.removeChild(deletedFile); + updateButtonIndex(Array.from(fileList.children)); + const dupEvent = new CustomEvent('change', { bubbles: true, detail: { stopRendering: true } }); + input.dispatchEvent(dupEvent); + input.files = getFiles(allFiles); + } + }); +} + +export default async function decorate(wrapper, field) { + const allFiles = []; + const input = wrapper.querySelector('input'); + const fileList = document.createElement('div'); + fileList.setAttribute('id', `${field.id}-fileList`); + const AttachButton = createAttachButton(input); + attachChangeEvent({ + input, fileList, allFiles, wrapper, + }); + attachRemoveFileEvent({ + input, fileList, allFiles, wrapper, + }); + wrapper.insertBefore(AttachButton, input); + wrapper.append(fileList); +} diff --git a/blocks/form/form.css b/blocks/form/form.css index 3cd54d03..7a23097b 100644 --- a/blocks/form/form.css +++ b/blocks/form/form.css @@ -2,20 +2,22 @@ --background-color-primary: #fff; --label-color: #666; --border-color: #818a91; + --form-error-color: #ff5f3f; --button-primary-color: #5F8DDA; + --button-secondary-color: #666; --button-primary-hover-color: #035fe6; --form-font-size-m: 22px; --form-font-size-s: 18px; --form-font-size-xs: 16px; --form-background-color: var(--background-color-primary); - --form-padding: 3%; - --form-columns: 1; + --form-padding: 0; + --form-columns: 12; --form-field-horz-gap: 40px; --form-field-vert-gap: 20px; --form-invalid-border-color: #ff5f3f; --form-input-padding: 0.75rem 0.6rem; --form-input-font-size: 1rem; - --form-input-disable-color: var(--label-color); + --form-input-disable-color: #ebebe4; --form-input-border-size: 1px; --form-input-border-color: var(--border-color); --form-input-background-color: var(--background-color-primary); @@ -42,9 +44,14 @@ --form-button-padding:15px 50px; --form-upload-color: var(--background-color-primary); --form-upload-font-size: var(--form-font-size-xs); - --form-upload-background-color: var(--mt-global-color-base-primary); + --form-upload-background-color: var(--background-color-primary); --form-submit-width: 100%; - --form-width: 100%; + --form-width: 95%; + --form-wizard-background-color: #f2f2f2; + --form-wizard-border-color: #757575; + --form-wizard-number-color: var(--button-primary-color); + --form-max-width: 100%; + --form-grid-gap: 0 30px; } form output { @@ -58,7 +65,7 @@ form [data-visible="false"] { } main .form-container { - background-color: var(--mt-background-color-primary); + background-color: var(--form-background-color); padding: var(--form-padding); width: var(--form-width); margin: var(--nav-height) auto; @@ -73,28 +80,44 @@ main .form > div:not(:first-child) { } main .form form { - display: flex; - flex-wrap: wrap; - gap: var(--form-field-vert-gap) var(--form-field-horz-gap); - align-items: start; + display: grid; + grid-template-columns: repeat(var(--form-columns), minmax(0, 1fr)); + gap: var(--form-grid-gap); + padding: var(--form-padding); + max-width: var(--form-max-width); } main .form form fieldset { + display: grid; + align-content: flex-start; + gap: var(--form-fieldset-grid-gap); + grid-template-columns: repeat(var(--form-fieldset-columns), minmax(0, 1fr)); border: var(--form-fieldset-border); - margin: var(--form-fieldset-marign); - width: 100%; + margin: var(--form-fieldset-margin); + padding: var(--form-fieldset-padding); } main .form form fieldset fieldset { padding: 0; } +main .form form .field-wrapper { + grid-column: span 12; +} + main .form .field-description { color: var(--form-label-color); font-size: var(--form-font-size-xs); } -main .form input, main .form textarea, main .form select { +main .form form .field-invalid { + color: var(--form-error-color); + font-weight: 700; +} + +main .form input, +main .form textarea, +main .form select { background-color: var(--form-input-background-color); border: var(--form-input-border-size) solid var(--form-input-border-color); width: 100%; @@ -103,6 +126,9 @@ main .form input, main .form textarea, main .form select { padding: var(--form-input-padding); font-size: var(--form-input-font-size); max-width: unset; + margin-top: .5rem; + margin-bottom: 1rem; + border-radius: 0.25rem; } main .form input[type='file'] { @@ -116,6 +142,8 @@ main .form input[type='radio'] { height: 16px; flex: none; margin: 0; + position: static; + z-index: 1; } main .form textarea { @@ -136,10 +164,6 @@ main .form fieldset legend { margin-bottom: 10px; } -main .innovate form > fieldset > legend { - text-align: center; -} - main .form label { font-weight: var(--form-label-font-weight); font-size: var(--form-label-font-size); @@ -180,7 +204,6 @@ main .form form fieldset > .form-checkbox-wrapper:first-of-type { main .form .form-radio-wrapper label, main .form .form-checkbox-wrapper label { - font-weight: var(--mt-font-weight-regular); flex-basis: calc(100% - 28px); } @@ -197,11 +220,14 @@ main .form button { border: var(--form-button-border); padding: var(--form-button-padding); font-size: var(--form-button-font-size); - font-weight: var(--mt-font-weight-bold); border-radius: unset; width: 100%; } +main form .form-file-wrapper > button{ + display: flex; +} + main .form button:hover { background: var(--form-button-background-hover-color); } @@ -223,7 +249,7 @@ main .form-file-wrapper input[type="file"] { main .form-file-wrapper .field-dropregion { background: rgb(0 0 0 / 2%); - border: 1px dashed var(--mt-global-color-silver); + border: 1px dashed var(--form-input-border-color); border-radius: 4px; margin: 11px 0 8px; padding: 32px; @@ -233,58 +259,225 @@ main .form-file-wrapper .field-dropregion { main .form-file-wrapper .file-description button { --form-button-padding: 15px; - background: url('/icons/delete.svg') no-repeat; + background: url('./icons/delete.svg') no-repeat; width: unset; border: unset; text-align: center; } -@media (min-width: 576px) { - :root { - --form-width: 540px; - } +/** Wizard Styling */ + +main .wizard { + counter-reset: wizard-step-counter; } -@media (min-width: 768px) { - :root { - --form-width: 800px; - } +main .wizard fieldset legend { + font-weight: normal; +} + +main .form form .wizard > fieldset { + counter-increment: wizard-step-counter; +} + +main .wizard > :not(.current-wizard-step) { + display: none; +} + +main .wizard > .current-wizard-step > legend { + font-weight: 600; +} + +main .wizard .form-wizard-button-wrapper { + display: flex; + justify-content: end; + gap: 20px; + padding-right: 30px; + +} + +main .wizard .form-wizard-button-wrapper .form-button-wrapper { + flex: none; +} + +main .wizard .form-wizard-button-wrapper button { + border: 3px solid var(--button-secondary-color); + background-color: transparent; + color: var(--button-secondary-color); + text-align: center; +} + +main .wizard .form-submit-wrapper, +main .wizard .form-wizard-button-skip, +main .wizard > .current-wizard-step.form-panel-wrapper:first-of-type ~ .form-wizard-button-wrapper > .form-wizard-button-prev, +main .wizard > .current-wizard-step.form-panel-wrapper:last-of-type ~ .form-wizard-button-wrapper > .form-wizard-button-next { + display: none; +} + +main .wizard .current-wizard-step.field-wrapper:last-of-type ~ .form-wizard-button-wrapper > .form-submit-wrapper { + display: inline-block; +} +@media (width >= 600px) { main .form button { width: unset; } } -@media (min-width: 1200px) { - :root { - --form-width: 1200px; +@media(width >= 992px) { + main .wizard.left { + --wizard-left-titlebar-width: 360px; + --wizard-left-form-padding: 30px; + --wizard-left-titlebar-padding: var(--wizard-left-form-padding); + --wizard-left-form-padding-bottom: 160px; + --form-columns: 1; + --fieldset-coulmns: 1; + + display: grid; + grid-template-columns: repeat(var(--form-columns), minmax(0, 1fr)); + border: 2px solid var(--form-wizard-border-color); + background-color: var( --form-wizard-background-color); + gap: unset; } -} -main form .form-fieldset-wrapper { - display: flex; - padding: 0; - flex-direction: column; -} + /* wizard title on left */ + main .wizard.left .current-wizard-step { + display: grid; + grid-template-columns: repeat(var( --fieldset-coulmns), minmax(0, 1fr)); + border-left: 2px solid var(--form-wizard-border-color); + margin-left: var(--wizard-left-titlebar-width); + background-color: var(--background-color-primary); + padding: var(--wizard-left-form-padding); + padding-bottom: var(--wizard-left-form-padding-bottom); + } -/* main form .form-fieldset-wrapper > .field-wrapper{ - flex: 1 0 auto; -} */ + main .wizard.left > fieldset { + width: unset; + } + + main .wizard.left > fieldset:not(.current-wizard-step) { + position: absolute; + display: unset; + } + + main .wizard.left > fieldset:not(.current-wizard-step) > :not(legend) { + display: none; + } + + main .wizard.left > fieldset > legend::before { + content: counter(wizard-step-counter); + display: inline-block; + border: 2px solid var(--form-wizard-number-color); + color: var(--form-wizard-number-color); + border-radius: 50%; + width: 35px; + height: 35px; + margin-right: 10px; + text-align: center; + line-height: 2; + } + + main .wizard.left > fieldset > legend { + display: block !important; + position: absolute; + font-size: 1.2rem; + margin-top: calc(var(--wizard-step-index) * 85px); + width: var(--wizard-left-titlebar-width); + padding: var(--wizard-left-titlebar-padding) var(--wizard-left-titlebar-padding) 0; + margin-bottom: 0; + } + + main .wizard.left > .current-wizard-step > legend { + margin-left: calc(-1*var(--wizard-left-titlebar-width) - var(--wizard-left-form-padding)); + margin-top: calc((var(--wizard-step-index) * 85px) - var(--wizard-left-titlebar-padding)); + } + + main .wizard.left > .current-wizard-step > legend::before { + background-color: var(--form-wizard-number-color); + color: var(--background-color-primary); + } + + main .wizard.left .form-wizard-button-wrapper { + margin-top: -160px; + margin-left: var(--wizard-left-titlebar-width); + } + + main .form form .field-wrapper.col-11 { + grid-column: span 11; + } + + main .form form .field-wrapper.col-10 { + grid-column: span 10; + } + + main .form form .field-wrapper.col-9 { + grid-column: span 9; + } + + main .form form .field-wrapper.col-8 { + grid-column: span 8; + } -@media (min-width: 1200px), (min-width: 992px) { - :root { - --form-columns: 2; + main .form form .field-wrapper.col-7 { + grid-column: span 7; + } + + main .form form .field-wrapper.col-6 { + grid-column: span 6; + } + + main .form form .field-wrapper.col-5 { + grid-column: span 5; + } + + main .form form .field-wrapper.col-4 { + grid-column: span 4; + } + + main .form form .field-wrapper.col-3 { + grid-column: span 3; + } + + main .form form .field-wrapper.col-2 { + grid-column: span 2; + } + + main .form form .field-wrapper.col-1 { + grid-column: span 1; + } +} + +@media (width >= 1200px) { + main .wizard.left { + --fieldset-coulmns: 1; } main .form form .form-checkbox-wrapper, main .form form .form-textarea-wrapper, main .form form .form-file-wrapper, - main .form form .form-fieldset-wrapper { + main .form form .form-panel-wrapper { flex: 1 0 100%; } - main .form form .form-fieldset-wrapper { + main .form form .form-panel-wrapper { flex-flow: row wrap; gap: 10px 15px; } } + +main form .form-panel-wrapper { + display: flex; + padding: 0; + flex-direction: column; +} + +main .form .form-message.success-message { + color: #0f5132; + background-color: #d1e7dd; + border-color: #badbcc; +} + +main .form .form-message.error-message { + color: #842029; + background-color: #f8d7da; + border-color: #f5c2c7; +} \ No newline at end of file diff --git a/blocks/form/form.js b/blocks/form/form.js index ee4b8537..5607e7c5 100644 --- a/blocks/form/form.js +++ b/blocks/form/form.js @@ -1,99 +1,45 @@ -import { readBlockConfig } from '../../scripts/lib-franklin.js'; +import { + createButton, createFieldWrapper, createLabel, getHTMLRenderType, + createHelpText, + getId, + stripTags, + checkValidation, +} from './util.js'; +import GoogleReCaptcha from './integrations/recaptcha.js'; + +import fileDecorate from './file.js'; +import DocBaseFormToAF from './transform.js'; +import handleSubmit from './submit.js'; + +export const DELAY_MS = 0; +let captchaField; +let afModule; -function generateUnique() { - return new Date().valueOf() + Math.random(); -} - -const formatFns = await (async function imports() { - try { - const formatters = await import('./formatting.js'); - return formatters.default; - } catch (e) { - // eslint-disable-next-line no-console - console.log('Formatting library not found. Formatting will not be supported'); - } - return {}; -}()); - -function constructPayload(form) { - const payload = { __id__: generateUnique() }; - [...form.elements].forEach((fe) => { - if (fe.name) { - if (fe.type === 'radio') { - if (fe.checked) payload[fe.name] = fe.value; - } else if (fe.type === 'checkbox') { - if (fe.checked) payload[fe.name] = payload[fe.name] ? `${payload[fe.name]},${fe.value}` : fe.value; - } else if (fe.type !== 'file') { - payload[fe.name] = fe.value; - } - } - }); - return { payload }; -} - -async function submissionFailure(error, form) { - form.setAttribute('data-submitting', 'false'); - form.querySelector('button[type="submit"]').disabled = false; -} - -async function prepareRequest(form, transformer) { - const { payload } = constructPayload(form); - const headers = { - 'Content-Type': 'application/json', - }; - const body = JSON.stringify({ data: payload }); - const url = form.dataset.submit || form.dataset.action; - if (typeof transformer === 'function') { - return transformer({ headers, body, url }, form); - } - return { headers, body, url }; -} - -async function submitForm(form, transformer) { - try { - const { headers, body, url } = await prepareRequest(form, transformer); - - const response = await fetch(url, { - method: 'POST', - headers, - body, - }); - if (response.ok) { - /* window.location.href = form.dataset?.redirect || 'thankyou'; */ - } else { - const error = await response.text(); - throw new Error(error); - } - } catch (error) { - submissionFailure(error, form); - } -} - -async function handleSubmit(form, transformer) { - if (form.getAttribute('data-submitting') !== 'true') { - form.setAttribute('data-submitting', 'true'); - await submitForm(form, transformer); - } -} +const withFieldWrapper = (element) => (fd) => { + const wrapper = createFieldWrapper(fd); + wrapper.append(element(fd)); + return wrapper; +}; function setPlaceholder(element, fd) { - if (fd.Placeholder) { - element.setAttribute('placeholder', fd.Placeholder); + if (fd.placeHolder) { + element.setAttribute('placeholder', fd.placeHolder); } } const constraintsDef = Object.entries({ - 'email|text': [['Max', 'maxlength'], ['Min', 'minlength']], - 'number|range|date': ['Max', 'Min', 'Step'], - file: ['Accept', 'Multiple'], - fieldset: [['Max', 'data-max'], ['Min', 'data-min']], + 'password|tel|email|text': [['maxLength', 'maxlength'], ['minLength', 'minlength'], 'pattern'], + 'number|range|date': [['maximum', 'Max'], ['minimum', 'Min'], 'step'], + file: ['accept', 'Multiple'], + fieldset: [['maxOccur', 'data-max'], ['minOccur', 'data-min']], }).flatMap(([types, constraintDef]) => types.split('|') .map((type) => [type, constraintDef.map((cd) => (Array.isArray(cd) ? cd : [cd, cd]))])); const constraintsObject = Object.fromEntries(constraintsDef); function setConstraints(element, fd) { - const constraints = constraintsObject[fd.Type]; + const renderType = getHTMLRenderType(fd); + const constraints = constraintsObject[renderType]; if (constraints) { constraints .filter(([nm]) => fd[nm]) @@ -103,76 +49,14 @@ function setConstraints(element, fd) { } } -function createLabel(fd, tagName = 'label') { - const label = document.createElement(tagName); - label.setAttribute('for', fd.Id); - label.className = 'field-label'; - label.textContent = fd.Label || ''; - if (fd.Tooltip) { - label.title = fd.Tooltip; - } - return label; -} - -function createHelpText(fd) { - const div = document.createElement('div'); - div.className = 'field-description'; - div.setAttribute('aria-live', 'polite'); - div.innerText = fd.Description; - div.id = `${fd.Id}-description`; - return div; -} - -function createFieldWrapper(fd, tagName = 'div') { - const fieldWrapper = document.createElement(tagName); - const nameStyle = fd.Name ? ` form-${fd.Name}` : ''; - const fieldId = `form-${fd.Type}-wrapper${nameStyle}`; - fieldWrapper.className = fieldId; - if (fd.Fieldset) { - fieldWrapper.dataset.fieldset = fd.Fieldset; - } - if (fd.Mandatory.toLowerCase() === 'true') { - fieldWrapper.dataset.required = ''; - } - if (fd.Visible?.toLowerCase() === 'false') { - fieldWrapper.dataset.visible = 'false'; - } - fieldWrapper.classList.add('field-wrapper'); - fieldWrapper.append(createLabel(fd)); - return fieldWrapper; -} - -function createButton(fd) { - const wrapper = createFieldWrapper(fd); - const button = document.createElement('button'); - button.textContent = fd.Label; - button.type = fd.Type; - button.classList.add('button'); - button.dataset.redirect = fd.Extra || ''; - button.id = fd.Id; - button.name = fd.Name; - wrapper.replaceChildren(button); - return wrapper; -} -function createSubmit(fd) { - const wrapper = createButton(fd); - return wrapper; -} - function createInput(fd) { const input = document.createElement('input'); - input.type = fd.Type; + input.type = getHTMLRenderType(fd); setPlaceholder(input, fd); setConstraints(input, fd); return input; } -const withFieldWrapper = (element) => (fd) => { - const wrapper = createFieldWrapper(fd); - wrapper.append(element(fd)); - return wrapper; -}; - const createTextArea = withFieldWrapper((fd) => { const input = document.createElement('textarea'); setPlaceholder(input, fd); @@ -181,48 +65,52 @@ const createTextArea = withFieldWrapper((fd) => { const createSelect = withFieldWrapper((fd) => { const select = document.createElement('select'); - if (fd.Placeholder) { - const ph = document.createElement('option'); - ph.textContent = fd.Placeholder; - ph.setAttribute('selected', ''); + select.required = fd.required; + select.title = fd.tooltip ?? ''; + select.readOnly = fd.readOnly; + select.multiple = fd.type === 'string[]' || fd.type === 'boolean[]' || fd.type === 'number[]'; + let ph; + if (fd.placeholder) { + ph = document.createElement('option'); + ph.textContent = fd.placeholder; ph.setAttribute('disabled', ''); + ph.setAttribute('value', ''); select.append(ph); } - fd.Options.split(',').forEach((o) => { + let optionSelected = false; + + const addOption = (label, value) => { const option = document.createElement('option'); - option.textContent = o.trim(); - option.value = o.trim(); + option.textContent = label?.trim(); + option.value = value?.trim() || label?.trim(); + if (fd.value === option.value || (Array.isArray(fd.value) && fd.value.includes(option.value))) { + option.setAttribute('selected', ''); + optionSelected = true; + } select.append(option); - }); - return select; -}); + return option; + }; -function createRadio(fd) { - const wrapper = createFieldWrapper(fd); - wrapper.insertAdjacentElement('afterbegin', createInput(fd)); - return wrapper; -} + const options = fd?.enum || []; + const optionNames = fd?.enumNames ?? options; + options.forEach((value, index) => addOption(optionNames?.[index], value)); -const createOutput = withFieldWrapper((fd) => { - const output = document.createElement('output'); - output.name = fd.Name; - output.id = fd.Id; - const displayFormat = fd['Display Format']; - if (displayFormat) { - output.dataset.displayFormat = displayFormat; + if (ph && optionSelected === false) { + ph.setAttribute('selected', ''); } - const formatFn = formatFns[displayFormat] || ((x) => x); - output.innerText = formatFn(fd.Value); - return output; + return select; }); -function createHidden(fd) { - const input = document.createElement('input'); - input.type = 'hidden'; - input.id = fd.Id; - input.name = fd.Name; - input.value = fd.Value; - return input; +function createRadioOrCheckbox(fd) { + const wrapper = createFieldWrapper(fd); + const input = createInput(fd); + const [value, uncheckedValue] = fd.enum || []; + input.value = value; + if (typeof uncheckedValue !== 'undefined') { + input.dataset.uncheckedValue = uncheckedValue; + } + wrapper.insertAdjacentElement('afterbegin', input); + return wrapper; } function createLegend(fd) { @@ -230,61 +118,178 @@ function createLegend(fd) { } function createFieldSet(fd) { - const wrapper = createFieldWrapper(fd, 'fieldset'); - wrapper.id = fd.Id; - wrapper.name = fd.Name; - wrapper.replaceChildren(createLegend(fd)); - if (fd.Repeatable && fd.Repeatable.toLowerCase() === 'true') { - setConstraints(wrapper, fd); - wrapper.dataset.repeatable = 'true'; + const wrapper = createFieldWrapper(fd, 'fieldset', createLegend); + wrapper.id = fd.id; + wrapper.name = fd.name; + if (fd.fieldType === 'panel') { + wrapper.classList.add('form-panel-wrapper'); } return wrapper; } -function groupFieldsByFieldSet(form) { - const fieldsets = form.querySelectorAll('fieldset'); - fieldsets?.forEach((fieldset) => { - const fields = form.querySelectorAll(`[data-fieldset="${fieldset.name}"`); - fields?.forEach((field) => { - fieldset.append(field); +function setConstraintsMessage(field, messages = {}) { + Object.keys(messages).forEach((key) => { + field.dataset[`${key}ErrorMessage`] = messages[key]; + }); +} + +function createRadioOrCheckboxGroup(fd) { + const wrapper = createFieldSet({ ...fd }); + const type = fd.fieldType.split('-')[0]; + fd.enum.forEach((value, index) => { + const label = typeof fd.enumNames[index] === 'object' ? fd.enumNames[index].value : fd.enumNames[index]; + const id = getId(fd.name); + const field = createRadioOrCheckbox({ + name: fd.name, + id, + label: { value: label }, + fieldType: type, + enum: [value], + required: fd.required, }); + field.classList.remove('field-wrapper', `form-${fd.name}`); + const input = field.querySelector('input'); + input.id = id; + input.dataset.fieldType = fd.fieldType; + input.name = fd.id; // since id is unique across radio/checkbox group + input.checked = Array.isArray(fd.value) ? fd.value.includes(value) : value === fd.value; + if ((index === 0 && type === 'radio') || type === 'checkbox') { + input.required = fd.required; + } + wrapper.appendChild(field); }); + wrapper.dataset.required = fd.required; + setConstraintsMessage(wrapper, fd.constraintMessages); + return wrapper; } function createPlainText(fd) { const paragraph = document.createElement('p'); - const nameStyle = fd.Name ? `form-${fd.Name}` : ''; - paragraph.className = nameStyle; - paragraph.dataset.fieldset = fd.Fieldset ? fd.Fieldset : ''; - paragraph.textContent = fd.Label; - return paragraph; + if (fd.richText) { + paragraph.innerHTML = stripTags(fd.value); + } else { + paragraph.textContent = fd.value; + } + const wrapper = createFieldWrapper(fd); + wrapper.id = fd.id; + wrapper.replaceChildren(paragraph); + return wrapper; } -export const getId = (function getId() { - const ids = {}; - return (name) => { - ids[name] = ids[name] || 0; - const idSuffix = ids[name] ? `-${ids[name]}` : ''; - ids[name] += 1; - return `${name}${idSuffix}`; - }; -}()); +function createFileField(fd) { + const field = createFieldWrapper(fd); + field.append(createInput(fd)); + fileDecorate(field, fd); + return field; +} const fieldRenderers = { - radio: createRadio, - checkbox: createRadio, - textarea: createTextArea, - select: createSelect, + 'drop-down': createSelect, + 'plain-text': createPlainText, + checkbox: createRadioOrCheckbox, button: createButton, - submit: createSubmit, - output: createOutput, - hidden: createHidden, - fieldset: createFieldSet, - plaintext: createPlainText, + multiline: createTextArea, + panel: createFieldSet, + radio: createRadioOrCheckbox, + 'radio-group': createRadioOrCheckboxGroup, + 'checkbox-group': createRadioOrCheckboxGroup, + file: createFileField, +}; + +async function fetchForm(pathname) { + // get the main form + const resp = await fetch(pathname); + const json = await resp.json(); + return json; +} + +function colSpanDecorator(field, element) { + const colSpan = field['Column Span']; + if (colSpan && element) { + element.classList.add(`col-${colSpan}`); + } +} + +const handleFocus = (input, field) => { + const editValue = input.getAttribute('edit-value'); + input.type = field.type; + input.value = editValue; }; +const handleFocusOut = (input) => { + const displayValue = input.getAttribute('display-value'); + input.type = 'text'; + input.value = displayValue; +}; + +function inputDecorator(field, element) { + const input = element?.querySelector('input,textarea,select'); + if (input) { + input.id = field.id; + input.name = field.name; + input.tooltip = field.tooltip; + input.readOnly = field.readOnly; + input.autocomplete = field.autoComplete ?? 'off'; + input.disabled = field.enabled === false; + const fieldType = getHTMLRenderType(field); + if (['number', 'date'].includes(fieldType) && field.displayFormat !== undefined) { + field.type = fieldType; + input.setAttribute('edit-value', field.value ?? ''); + input.setAttribute('display-value', field.displayValue ?? ''); + input.type = 'text'; + input.value = field.displayValue ?? ''; + input.addEventListener('focus', () => handleFocus(input, field)); + input.addEventListener('blur', () => handleFocusOut(input)); + } else if (input.type !== 'file') { + input.value = field.value ?? ''; + if (input.type === 'radio' || input.type === 'checkbox') { + input.value = field?.enum?.[0] ?? 'on'; + input.checked = field.value === input.value; + } + } else { + input.multiple = field.type === 'file[]'; + } + if (field.required) { + input.setAttribute('required', 'required'); + } + if (field.description) { + input.setAttribute('aria-describedby', `${field.id}-description`); + } + if (field.minItems) { + input.dataset.minItems = field.minItems; + } + if (field.maxItems) { + input.dataset.maxItems = field.maxItems; + } + if (field.maxFileSize && !Number.isNaN(Number(field.maxFileSize))) { + input.dataset.maxFileSize = field.maxFileSize; + } + setConstraintsMessage(element, field.constraintMessages); + element.dataset.required = field.required; + } +} + +const layoutDecorators = [ + [(panel) => { + const { ':type': type = '' } = panel; + return type.endsWith('wizard'); + }, 'wizard'], +]; + +async function applyLayout(panel, element) { + const result = layoutDecorators.find(([predicate]) => predicate(panel)); + if (result) { + const module = await import(`./layout/${result[1]}.js`); + if (module && module.default) { + const layoutFn = module.default; + await layoutFn(element); + } + } +} + function renderField(fd) { - const renderer = fieldRenderers[fd.Type]; + const fieldType = fd?.fieldType?.replace('-input', '') ?? 'text'; + const renderer = fieldRenderers[fieldType]; let field; if (typeof renderer === 'function') { field = renderer(fd); @@ -292,93 +297,104 @@ function renderField(fd) { field = createFieldWrapper(fd); field.append(createInput(fd)); } - if (fd.Description) { + if (fd.description) { field.append(createHelpText(fd)); + field.dataset.description = fd.description; // In case overriden by error message } return field; } -async function applyTransformation(formDef, form) { - try { - const { requestTransformers, transformers } = await import('./decorators/index.js'); - if (transformers) { - transformers.forEach( - (fn) => fn.call(this, formDef, form), - ); +export async function generateFormRendition(panel, container) { + const { items = [] } = panel; + const promises = []; + items.forEach((field) => { + field.value = field.value ?? ''; + const { fieldType } = field; + if (fieldType === 'captcha') { + captchaField = field; + } else { + const element = renderField(field); + if (field.fieldType !== 'radio-group' && field.fieldType !== 'checkbox-group') { + inputDecorator(field, element); + } + colSpanDecorator(field, element); + container.append(element); + if (field?.fieldType === 'panel') { + promises.push(generateFormRendition(field, element)); + } } + }); - const transformRequest = async (request, fd) => requestTransformers?.reduce( - (promise, transformer) => promise.then((modifiedRequest) => transformer(modifiedRequest, fd)), - Promise.resolve(request), - ); - return transformRequest; - } catch (e) { - // eslint-disable-next-line no-console - console.log('no custom decorators found.'); - } - return (req) => req; + await Promise.all(promises); + await applyLayout(panel, container); } -async function fetchData(url) { - const resp = await fetch(url); - const json = await resp.json(); - return json.data.map((fd) => ({ - ...fd, - Id: fd.Id || getId(fd.Name), - Value: fd.Value || '', - })); -} +function enableValidation(form) { + form.querySelectorAll('input,textarea,select').forEach((input) => { + input.addEventListener('invalid', (event) => { + checkValidation(event.target); + }); + }); -async function fetchForm(pathname) { - // get the main form - const jsonData = await fetchData(pathname); - return jsonData; + form.addEventListener('change', (event) => { + const { validity } = event.target; + if (validity.valid) { + // only to remove the error message + checkValidation(event.target); + } + }); } -async function createForm(formURL) { - const { pathname } = new URL(formURL); - const data = await fetchForm(pathname); +export async function createForm(formDef, data) { + const { action: formPath } = formDef; const form = document.createElement('form'); - data.forEach((fd) => { - const el = renderField(fd); - const input = el.querySelector('input,textarea,select'); - if (fd.Mandatory && fd.Mandatory.toLowerCase() === 'true') { - input.setAttribute('required', 'required'); - } - if (input) { - input.id = fd.Id; - input.name = fd.Name; - if (input.type !== 'file') { - input.value = fd.Value; - if (input.type === 'radio' || input.type === 'checkbox') { - input.checked = fd.Checked === 'true'; - } - } - if (fd.Description) { - input.setAttribute('aria-describedby', `${fd.Id}-description`); - } - } - form.append(el); - }); - groupFieldsByFieldSet(form); - const transformRequest = await applyTransformation(data, form); - // eslint-disable-next-line prefer-destructuring - form.dataset.action = pathname?.split('.json')[0]; - form.addEventListener('submit', (e) => { - e.preventDefault(); - e.submitter.setAttribute('disabled', ''); - handleSubmit(form, transformRequest); + form.dataset.action = formPath; + form.noValidate = true; + await generateFormRendition(formDef, form); + + let captcha; + if (captchaField) { + const siteKey = captchaField?.properties?.['fd:captcha']?.config?.siteKey; + captcha = new GoogleReCaptcha(siteKey, captchaField.id); + captcha.loadCaptcha(form); + } + + enableValidation(form); + + if (afModule) { + window.setTimeout(async () => { + afModule.loadRuleEngine(formDef, form, captcha, generateFormRendition, data); + }, DELAY_MS); + } + + form.addEventListener('submit', (event) => { + handleSubmit(event, form); }); + return form; } export default async function decorate(block) { - const formLink = block.querySelector('a[href$=".json"]'); - if (formLink) { - const form = await createForm(formLink.href); - formLink.replaceWith(form); - - const config = readBlockConfig(block); - Object.entries(config).forEach(([key, value]) => { if (value) form.dataset[key] = value; }); + let container = block.querySelector('a[href$=".json"]'); + let formDef; + let pathname; + if (container) { + ({ pathname } = new URL(container.href)); + formDef = await fetchForm(pathname); + } else { + container = block.querySelector('pre'); + const codeEl = container?.querySelector('code'); + const content = codeEl?.textContent; + if (content) { + formDef = JSON.parse(content?.replace(/\x83\n|\n|\s\s+/g, '')); + } + } + if (formDef) { + const { data } = formDef; + const transform = new DocBaseFormToAF(); + const afFormDef = transform.transform(formDef); + const form = await createForm(afFormDef, data); + form.dataset.action = pathname?.split('.json')[0]; + container.replaceWith(form); } } diff --git a/blocks/form/integrations/recaptcha.js b/blocks/form/integrations/recaptcha.js new file mode 100644 index 00000000..e9483bb1 --- /dev/null +++ b/blocks/form/integrations/recaptcha.js @@ -0,0 +1,54 @@ +export default class GoogleReCaptcha { + id; + + siteKey; + + loadPromise; + + constructor(siteKey, id) { + this.siteKey = siteKey; + this.id = id; + } + + #loadScript(url) { + if (!this.loadPromise) { + this.loadPromise = new Promise((resolve, reject) => { + const head = document.head || document.querySelector('head'); + const script = document.createElement('script'); + script.src = url; + script.async = true; + script.onload = () => resolve(window.grecaptcha); + script.onerror = () => reject(new Error(`Failed to load script ${url}`)); + head.append(script); + }); + } + } + + loadCaptcha(form) { + if (form && this.siteKey) { + const submit = form.querySelector('button[type="submit"]'); + const obs = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + this.#loadScript(`https://www.google.com/recaptcha/api.js?render=${this.siteKey}`); + obs.disconnect(); + } + }); + }); + obs.observe(submit); + } + } + + async getToken() { + if (!this.siteKey) { + return null; + } + return new Promise((resolve) => { + const { grecaptcha } = window; + grecaptcha.ready(async () => { + const token = await grecaptcha.execute(this.siteKey, { action: 'submit' }); + resolve(token); + }); + }); + } +} diff --git a/blocks/form/layout/repeat.js b/blocks/form/layout/repeat.js new file mode 100644 index 00000000..ce12b02b --- /dev/null +++ b/blocks/form/layout/repeat.js @@ -0,0 +1,100 @@ +import { getId } from '../util.js'; + +function update(fieldset, index, labelTemplate) { + const legend = fieldset.querySelector(':scope>.field-label').firstChild; + const text = labelTemplate.replace('#', index + 1); + if (legend) { + legend.textContent = text; + } + fieldset.id = getId(fieldset.name); + fieldset.setAttribute('data-index', index); + if (index > 0) { + fieldset.querySelectorAll('.field-wrapper').forEach((f) => { + const [label, input, description] = ['label', 'input,select,button,textarea', 'description'] + .map((x) => f.querySelector(x)); + input.id = getId(input.name); + if (label) { + label.htmlFor = input.id; + } + if (description) { + input.setAttribute('aria-describedby', `${input.Id}-description`); + description.id = `${input.id}-description`; + } + }); + } +} + +function createButton(label, icon) { + const button = document.createElement('button'); + button.className = `item-${icon}`; + button.type = 'button'; + const text = document.createElement('span'); + text.textContent = label; + button.append(document.createElement('i'), text); + return button; +} + +function insertRemoveButton(fieldset, wrapper, form) { + const removeButton = createButton('Remove', 'remove'); + removeButton.addEventListener('click', () => { + fieldset.remove(); + wrapper.querySelector('.item-add').setAttribute('data-visible', 'true'); + wrapper.querySelectorAll('[data-repeatable="true"]').forEach((el, index) => { + update(el, index, wrapper['#repeat-template-label']); + }); + const event = new CustomEvent('item:remove', { + detail: { item: { name: fieldset.name, id: fieldset.id } }, + bubbles: false, + }); + form.dispatchEvent(event); + }); + const legend = fieldset.querySelector(':scope>.field-label'); + legend.append(removeButton); +} + +const add = (wrapper, form) => (e) => { + const { currentTarget } = e; + const { parentElement } = currentTarget; + const fieldset = parentElement['#repeat-template']; + const max = parentElement.getAttribute('data-max'); + const min = parentElement.getAttribute('data-min'); + const childCount = parentElement.children.length - 1; + const newFieldset = fieldset.cloneNode(true); + newFieldset.setAttribute('data-index', childCount); + update(newFieldset, childCount, parentElement['#repeat-template-label']); + if (childCount >= +min) { + insertRemoveButton(newFieldset, wrapper, form); + } + if (+max <= childCount + 1) { + e.currentTarget.setAttribute('data-visible', 'false'); + } + currentTarget.insertAdjacentElement('beforebegin', newFieldset); + const event = new CustomEvent('item:add', { + detail: { item: { name: newFieldset.name, id: newFieldset.id } }, + bubbles: false, + }); + form.dispatchEvent(event); +}; + +export default function transferRepeatableDOM(form) { + form.querySelectorAll('[data-repeatable="true"]').forEach((el) => { + const div = document.createElement('div'); + div.setAttribute('data-min', el.dataset.min); + div.setAttribute('data-max', el.dataset.max); + el.insertAdjacentElement('beforebegin', div); + div.append(el); + const addLabel = 'Add'; + const addButton = createButton(addLabel, 'add'); + addButton.addEventListener('click', add(div, form)); + div['#repeat-template'] = el.cloneNode(true); + div['#repeat-template-label'] = el.querySelector(':scope>.field-label').textContent; + if (+el.min === 0) { + el.remove(); + } else { + update(el, 0, div['#repeat-template-label']); + el.setAttribute('data-index', 0); + } + div.append(addButton); + div.className = 'form-repeat-wrapper'; + }); +} diff --git a/blocks/form/layout/wizard.js b/blocks/form/layout/wizard.js new file mode 100644 index 00000000..39ad0c14 --- /dev/null +++ b/blocks/form/layout/wizard.js @@ -0,0 +1,117 @@ +import { createButton } from '../util.js'; + +export class WizardLayout { + inputFields = 'input,textarea,select'; + + constructor(includePrevBtn = true, includeNextBtn = true) { + this.includePrevBtn = includePrevBtn; + this.includeNextBtn = includeNextBtn; + } + + // eslint-disable-next-line class-methods-use-this + getSteps(panel) { + return [...panel.children].filter((step) => step.tagName.toLowerCase() === 'fieldset'); + } + + assignIndexToSteps(panel) { + const steps = this.getSteps(panel); + steps.forEach((step, index) => { + step.dataset.index = index; + step.style.setProperty('--wizard-step-index', index); + }); + } + + // eslint-disable-next-line class-methods-use-this + getEligibleSibling(current, forward = true) { + const direction = forward ? 'nextElementSibling' : 'previousElementSibling'; + + for (let sibling = current[direction]; sibling; sibling = sibling[direction]) { + if (sibling.dataset.hidden !== 'true') { + return sibling; + } + } + return null; + } + + /** + * @param {FormElement | Fieldset} container + * @returns return false, if there are invalid fields + */ + validateContainer(container) { + const fieldElements = [...container.querySelectorAll(this.inputFields)]; + const isValid = fieldElements.reduce((valid, fieldElement) => { + const isFieldValid = fieldElement.checkValidity(); + return valid && isFieldValid; + }, true); + + if (!isValid) { + container.querySelector(':invalid')?.focus(); + } + return isValid; + } + + navigate(panel, forward = true) { + const current = panel.querySelector('.current-wizard-step'); + + let valid = true; + if (forward) { + valid = this.validateContainer(current); + } + const navigateTo = valid ? this.getEligibleSibling(current, forward) : current; + + if (navigateTo && current !== navigateTo) { + current.classList.remove('current-wizard-step'); + navigateTo.classList.add('current-wizard-step'); + const event = new CustomEvent('wizard:navigate', { + detail: { + prevStep: { id: current.id, index: +current.dataset.index }, + currStep: { id: navigateTo.id, index: +navigateTo.dataset.index }, + }, + bubbles: false, + }); + panel.dispatchEvent(event); + } + } + + addButton(wrapper, panel, buttonDef, forward = true) { + const button = createButton(buttonDef); + button.classList.add(buttonDef.id); + button.addEventListener('click', () => this.navigate(panel, forward)); + wrapper.append(button); + } + + applyLayout(panel) { + const wrapper = document.createElement('div'); + wrapper.className = 'form-wizard-button-wrapper'; + if (this.includePrevBtn) { + this.addButton(wrapper, panel, { + label: { value: 'Back' }, fieldType: 'button', name: 'back', id: 'form-wizard-button-prev', + }, false); + } + + if (this.includeNextBtn) { + this.addButton(wrapper, panel, { + label: { value: 'NEXT' }, fieldType: 'button', name: 'next', id: 'form-wizard-button-next', + }); + } + + const submitBtn = panel.querySelector('.form-submit-wrapper'); + if (submitBtn) { + wrapper.append(submitBtn); + } + this.assignIndexToSteps(panel); + panel.append(wrapper); + panel.querySelector('fieldset')?.classList.add('current-wizard-step'); + panel.classList.add('wizard'); + panel.classList.add('left'); + } +} + +const layout = new WizardLayout(); + +export default function wizardLayout(panel) { + layout.applyLayout(panel); +} + +export const navigate = layout.navigate.bind(layout); +export const validateContainer = layout.validateContainer.bind(layout); diff --git a/blocks/form/submit.js b/blocks/form/submit.js new file mode 100644 index 00000000..89154f66 --- /dev/null +++ b/blocks/form/submit.js @@ -0,0 +1,102 @@ +function submitSuccess(e, form) { + const { payload } = e; + if (payload?.body?.redirectUrl) { + window.location.assign(encodeURI(payload.body.redirectUrl)); + } else { + let thankYouMessage = form.querySelector('.form-message.success-message'); + if (!thankYouMessage) { + thankYouMessage = document.createElement('div'); + thankYouMessage.className = 'form-message success-message'; + } + thankYouMessage.innerHTML = 'Thanks for your submission'; + form.prepend(thankYouMessage); + if (thankYouMessage.scrollIntoView) { + thankYouMessage.scrollIntoView({ behavior: 'smooth' }); + } + form.reset(); + } + form.querySelector('button[type="submit"]').disabled = false; +} + +function submitFailure(error, form) { + let errorMessage = form.querySelector('.form-message.error-message'); + if (!errorMessage) { + errorMessage = document.createElement('div'); + errorMessage.className = 'form-message error-message'; + } + errorMessage.innerHTML = 'Some error occured while submitting the form'; // TODO: translation + form.prepend(errorMessage); + errorMessage.scrollIntoView({ behavior: 'smooth' }); + form.setAttribute('data-submitting', 'false'); + form.querySelector('button[type="submit"]').disabled = false; +} + +function generateUnique() { + return new Date().valueOf() + Math.random(); +} + +function constructPayload(form) { + const payload = { __id__: generateUnique() }; + [...form.elements].forEach((fe) => { + if (fe.name) { + if (fe.type === 'radio') { + if (fe.checked) payload[fe.name] = fe.value; + } else if (fe.type === 'checkbox') { + if (fe.checked) payload[fe.name] = payload[fe.name] ? `${payload[fe.name]},${fe.value}` : fe.value; + } else if (fe.type !== 'file') { + payload[fe.name] = fe.value; + } + } + }); + return { payload }; +} + +async function prepareRequest(form) { + const { payload } = constructPayload(form); + const headers = { + 'Content-Type': 'application/json', + }; + const body = JSON.stringify({ data: payload }); + const url = form.dataset.submit || form.dataset.action; + return { headers, body, url }; +} + +async function submitForm(form) { + try { + const { headers, body, url } = await prepareRequest(form); + const response = await fetch(url, { + method: 'POST', + headers, + body, + }); + if (response.ok) { + submitSuccess(response, form); + } else { + const error = await response.text(); + throw new Error(error); + } + } catch (error) { + submitFailure(error, form); + } +} + +export default async function handleSubmit(e, form) { + e.preventDefault(); + const valid = form.checkValidity(); + if (valid) { + e.submitter.setAttribute('disabled', ''); + if (form.getAttribute('data-submitting') !== 'true') { + form.setAttribute('data-submitting', 'true'); + + // hide error message in case it was shown before + form.querySelectorAll('.form-message.show').forEach((el) => el.classList.remove('show')); + await submitForm(form); + } + } else { + const firstInvalidEl = form.querySelector(':invalid:not(fieldset)'); + if (firstInvalidEl) { + firstInvalidEl.focus(); + firstInvalidEl.scrollIntoView({ behavior: 'smooth' }); + } + } +} diff --git a/blocks/form/transform.js b/blocks/form/transform.js new file mode 100644 index 00000000..e0f5bcac --- /dev/null +++ b/blocks/form/transform.js @@ -0,0 +1,275 @@ +/* eslint-disable no-param-reassign */ + +import { getId } from './util.js'; + +/* eslint-disable class-methods-use-this */ +const PROPERTY = 'property'; + +export default class DocBaseFormToAF { + panelMap = new Map(); + + errors = []; + + fieldPropertyMapping = { + Default: 'default', + Step: 'step', + Pattern: 'pattern', + Value: 'value', + Placeholder: 'placeholder', + Field: 'name', + Name: 'name', + ReadOnly: 'readOnly', + Description: 'description', + Type: 'fieldType', + Label: 'label.value', + Mandatory: 'required', + Options: 'enum', + OptionNames: 'enumNames', + }; + + fieldMapping = new Map([ + ['text', 'text-input'], + ['number', 'number-input'], + ['datetime-local', 'date-input'], + ['file', 'file-input'], + ['select', 'drop-down'], + ['radio-group', 'radio-group'], + ['checkbox-group', 'checkbox-group'], + ['plain-text', 'plain-text'], + ['checkbox', 'checkbox'], + ['textarea', 'multiline-input'], + ['fieldset', 'panel'], + ['button', 'button'], + ]); + + /** + * @param {string} formPath + */ + async getForm(formPath) { + if (!formPath) { + throw new Error('formPath is required'); + } + const resp = await fetch(formPath); + const json = await resp.json(); + return json; + } + + #initFormDef(name) { + return { + name, + adaptiveform: '0.10.0', + metadata: { + grammar: 'json-formula-1.0.0', + version: '1.0.0', + }, + properties: {}, + items: [], + }; + } + + #initField() { + return { + constraintMessages: { + required: 'Please fill in this field.', + }, + }; + } + + /** + * @param {{ total?: number; + * offset?: number; limit?: number; data: any; ":type"?: string; adaptiveform?: any; }} exData + * + * @return {{formDef: any, excelData: any}} response + */ + transform(exData, { name } = { name: 'Form' }) { + this.errors = []; + // if its adaptive form json just return it. + if (exData?.adaptiveform) { + return { formDef: exData, excelData: null }; + } + if (!exData || !exData.data) { + throw new Error('Unable to retrieve the form details from json'); + } + const formDef = this.#initFormDef(name); + + this.panelMap.set('root', formDef); + + exData.data.forEach((/** @type {{ [s: string]: any; } | ArrayLike} */ item) => { + if (item.Name || item.Field) { + // eslint-disable-next-line no-unused-vars + const source = Object.fromEntries(Object.entries(item).filter(([_, v]) => (v != null && v !== ''))); + let field = { ...source, ...this.#initField() }; + this.#transformFieldNames(field); + field.id = field.id || getId(field.name); + field.value = field.Value || ''; + if (this.#isProperty(field)) { + this.#handleProperites(formDef, field); + } else { + if (field?.fieldType === 'fieldset') { + this.panelMap.set(field?.name, field); + delete field?.constraintMessages; + } + field = this.#handleField(field); + this.#addToParent(field); + } + } + }); + + return formDef; + } + + /** + * @param {any} formDef Headless Form definition + * @param {any} field + */ + #handleProperites(formDef, field) { + formDef.properties[field.name] = field.default; + } + + #handleCheckboxAndRadio(field) { + if (field?.fieldType === 'checkbox-group' || field?.fieldType === 'radio-group') { + if (!field.enum) { + field.enum = ['yes']; + } + } + } + + /** + * Transform flat field to Crispr Field + * @param {any} field + * @returns + */ + #handleField(field) { + this.#transformFieldType(field); + this.#transformFlatToHierarchy(field); + this.#handleCheckboxAndRadio(field); + this.#handleMultiValues(field, 'enum'); + this.#handleMultiValues(field, 'enumNames'); + + this.#handleFranklinSpecialCases(field); + this.#handlePanel(field); + this.#handleSpecialButtons(field); + return field; + } + + #handleSpecialButtons(field) { + if (field?.fieldType === 'submit' || field?.fieldType === 'reset') { + field.buttonType = field.fieldType; + field.fieldType = 'button'; + field.properties = field.properties || {}; + field.properties['fd:buttonType'] = field.buttonType; + } + } + + /** + * Convert flat field to hierarchy based on dot notation. + * @param {any} item Flat field Definition + * @returns + */ + #transformFlatToHierarchy(item) { + Object.keys(item).forEach((key) => { + if (key.includes('.')) { + let temp = item; + const keys = key.split('.'); + keys.forEach((k, i, values) => { + if (i === values.length - 1) { + temp[k] = item[key]; + } else { + temp[k] = temp[k] != null ? temp[k] : {}; + temp = temp[k]; + } + }); + delete item[key]; + } + }); + } + + /** + * Transform CRISPR fieldType to HTML Input Type. + * @param {any} field FieldJson + */ + #transformFieldType(field) { + if (this.fieldMapping.has(field?.fieldType)) { + field.fieldType = this.fieldMapping.get(field?.fieldType); + } + } + + /** + * Convert Field names from Franklin Form to crispr def. + * @param {any} field Form Def received from excel + */ + #transformFieldNames(field) { + Object.keys(this.fieldPropertyMapping).forEach((key) => { + if (field[key]) { + field[this.fieldPropertyMapping[key]] = field[key]; + delete field[key]; + } + }); + } + + /** + * handle multivalues field i.e. comma seprator to array. + * @param {{ [x: string]: any; }} item + * @param {string | number} key + */ + #handleMultiValues(item, key) { + let values; + if (item && item[key] && typeof item[key] === 'string') { + values = item[key]?.split(',').map((value) => value.trim()); + item[key] = values; + } + } + + /** + * Handle special use cases of Franklin. + * @param {{ required: string | boolean; }} item + */ + #handleFranklinSpecialCases(item) { + // Franklin Mandatory uses x for true. + item.required = (item.required === 'x' || item.required === 'true' || item.required === true); + + if (item.Max || item.Min) { + if (item.fieldType === 'number' || item.fieldType === 'date') { + item.maximum = item.Max; + item.minimum = item.Min; + } else { + item.maxLength = item.Max; + item.minLength = item.Min; + } + delete item.Max; + delete item.Min; + } + } + + /** + * Handle Panel related transformation. + * @param {*} field + */ + #handlePanel(field) { + if (field?.fieldType === 'panel') { + // Ignore name if type is not defined on panel. + /* if (typeof field?.type === 'undefined') { + field.name = null; + } */ + } + } + + /** + * @param {any} field FieldJson + */ + #isProperty(field) { + return field && field.fieldType === PROPERTY; + } + + /** + * Add the field to its relevant parent items. + * @param {Object} field + */ + #addToParent(field) { + const parent = field?.Fieldset || 'root'; + const parentField = this.panelMap.get(this.panelMap.has(parent) ? parent : 'root'); + parentField.items = parentField.items || []; + parentField.items.push(field); + delete field?.parent; + } +} diff --git a/blocks/form/util.js b/blocks/form/util.js new file mode 100644 index 00000000..b872c7f5 --- /dev/null +++ b/blocks/form/util.js @@ -0,0 +1,197 @@ +// create a string containing head tags from h1 to h5 +const headings = Array.from({ length: 5 }, (_, i) => ``).join(''); +const allowedTags = `${headings}