diff --git a/blocks/form/base-64.js b/blocks/form/base-64.js new file mode 100644 index 0000000000..619f40582d --- /dev/null +++ b/blocks/form/base-64.js @@ -0,0 +1,8 @@ +export const toBase64 = file => new Promise(async (resolve, reject) => { + const reader = new FileReader(); + await reader.readAsDataURL(file); + reader.onload = () => { + resolve(reader.result); + } + reader.onerror = reject; +}); diff --git a/blocks/form/form-fields.js b/blocks/form/form-fields.js index 2a1290c114..a39edb4a05 100644 --- a/blocks/form/form-fields.js +++ b/blocks/form/form-fields.js @@ -1,5 +1,6 @@ import { toClassName } from '../../scripts/aem.js'; +import { toBase64 } from './base-64.js'; function createFieldWrapper(fd) { const fieldWrapper = document.createElement('div'); @@ -157,6 +158,10 @@ const createInput = (fd) => { fieldWrapper.prepend(label); } + field.addEventListener('change', async function (e) { + const base64 = await toBase64(this.files[0]) + this.dataset.fileData = base64; + }) return { field, fieldWrapper }; }; diff --git a/blocks/form/form.js b/blocks/form/form.js index 6518430e05..a1809c0ceb 100644 --- a/blocks/form/form.js +++ b/blocks/form/form.js @@ -1,3 +1,4 @@ +import { toBase64 } from './base-64.js'; import createField from './form-fields.js'; async function createForm(formHref, submitHref) { @@ -33,6 +34,8 @@ function generatePayload(form) { if (field.name && field.type !== 'submit' && !field.disabled) { if (field.type === 'radio') { if (field.checked) payload[field.name] = field.value; + } else if (field.type === 'file') { + payload[field.name] = field.dataset.fileData; } else if (field.type === 'checkbox') { if (field.checked) payload[field.name] = payload[field.name] ? `${payload[field.name]},${field.value}` : field.value; } else { diff --git a/component-models.json b/component-models.json index f9a3f763a6..10f86408ac 100644 --- a/component-models.json +++ b/component-models.json @@ -1276,6 +1276,10 @@ { "name": "used-car-loan-branch-carousel", "value": "used-car-loan-branch-carousel" + }, + { + "name": "video-component-carousel", + "value": "video-component-carousel" } ] }, diff --git a/form/form-fields.js b/form/form-fields.js deleted file mode 100644 index 2a1290c114..0000000000 --- a/form/form-fields.js +++ /dev/null @@ -1,237 +0,0 @@ - -import { toClassName } from '../../scripts/aem.js'; - -function createFieldWrapper(fd) { - const fieldWrapper = document.createElement('div'); - if (fd.Style) fieldWrapper.className = fd.Style; - fieldWrapper.classList.add('field-wrapper', `${fd.Type}-wrapper`); - - fieldWrapper.dataset.fieldset = fd.Fieldset; - - return fieldWrapper; -} - -const ids = []; -function generateFieldId(fd, suffix = '') { - const slug = toClassName(`form-${fd.Name}${suffix}`); - ids[slug] = ids[slug] || 0; - const idSuffix = ids[slug] ? `-${ids[slug]}` : ''; - ids[slug] += 1; - return `${slug}${idSuffix}`; -} - -function createLabel(fd) { - const label = document.createElement('label'); - label.id = generateFieldId(fd, '-label'); - label.textContent = fd.Label || fd.Name; - label.setAttribute('for', fd.Id); - if (fd.Mandatory.toLowerCase() === 'true' || fd.Mandatory.toLowerCase() === 'x') { - label.dataset.required = true; - } - return label; -} - -function setCommonAttributes(field, fd) { - field.id = fd.Id; - field.name = fd.Name; - field.required = fd.Mandatory && (fd.Mandatory.toLowerCase() === 'true' || fd.Mandatory.toLowerCase() === 'x'); - field.placeholder = fd.Placeholder; - field.value = fd.Value; -} - -const createHeading = (fd) => { - const fieldWrapper = createFieldWrapper(fd); - - const level = fd.Style && fd.Style.includes('sub-heading') ? 3 : 2; - const heading = document.createElement(`h${level}`); - heading.textContent = fd.Value || fd.Label; - heading.id = fd.Id; - - fieldWrapper.append(heading); - - return { field: heading, fieldWrapper }; -}; - -const createPlaintext = (fd) => { - const fieldWrapper = createFieldWrapper(fd); - - const text = document.createElement('p'); - text.textContent = fd.Value || fd.Label; - text.id = fd.Id; - - fieldWrapper.append(text); - - return { field: text, fieldWrapper }; -}; - -const createSelect = async (fd) => { - const select = document.createElement('select'); - setCommonAttributes(select, fd); - const addOption = ({ text, value }) => { - const option = document.createElement('option'); - option.text = text.trim(); - option.value = value.trim(); - if (option.value === select.value) { - option.setAttribute('selected', ''); - } - select.add(option); - return option; - }; - - if (fd.Placeholder) { - const ph = addOption({ text: fd.Placeholder, value: '' }); - ph.setAttribute('disabled', ''); - } - - if (fd.Options) { - let options = []; - if (fd.Options.startsWith('https://')) { - const optionsUrl = new URL(fd.Options); - const resp = await fetch(`${optionsUrl.pathname}${optionsUrl.search}`); - const json = await resp.json(); - json.data.forEach((opt) => { - options.push({ - text: opt.Option, - value: opt.Value || opt.Option, - }); - }); - } else { - options = fd.Options.split(',').map((opt) => ({ - text: opt.trim(), - value: opt.trim().toLowerCase(), - })); - } - - options.forEach((opt) => addOption(opt)); - } - - const fieldWrapper = createFieldWrapper(fd); - fieldWrapper.append(select); - fieldWrapper.prepend(createLabel(fd)); - - return { field: select, fieldWrapper }; -}; - -const createConfirmation = (fd, form) => { - form.dataset.confirmation = new URL(fd.Value).pathname; - - return {}; -}; - -const createSubmit = (fd) => { - const button = document.createElement('button'); - button.textContent = fd.Label || fd.Name; - button.classList.add('button'); - button.type = 'submit'; - - const fieldWrapper = createFieldWrapper(fd); - fieldWrapper.append(button); - return { field: button, fieldWrapper }; -}; - -const createTextArea = (fd) => { - const field = document.createElement('textarea'); - setCommonAttributes(field, fd); - - const fieldWrapper = createFieldWrapper(fd); - const label = createLabel(fd); - field.setAttribute('aria-labelledby', label.id); - fieldWrapper.append(field); - fieldWrapper.prepend(label); - - return { field, fieldWrapper }; -}; - -const createInput = (fd) => { - const field = document.createElement('input'); - field.type = fd.Type; - setCommonAttributes(field, fd); - - const fieldWrapper = createFieldWrapper(fd); - const label = createLabel(fd); - field.setAttribute('aria-labelledby', label.id); - fieldWrapper.append(field); - if (fd.Type === 'radio' || fd.Type === 'checkbox') { - fieldWrapper.append(label); - } else { - fieldWrapper.prepend(label); - } - - return { field, fieldWrapper }; -}; - -const createFieldset = (fd) => { - const field = document.createElement('fieldset'); - setCommonAttributes(field, fd); - - if (fd.Label) { - const legend = document.createElement('legend'); - legend.textContent = fd.Label; - field.append(legend); - } - - const fieldWrapper = createFieldWrapper(fd); - fieldWrapper.append(field); - - return { field, fieldWrapper }; -}; - -const createToggle = (fd) => { - const { field, fieldWrapper } = createInput(fd); - field.type = 'checkbox'; - if (!field.value) field.value = 'on'; - field.classList.add('toggle'); - fieldWrapper.classList.add('selection-wrapper'); - - const toggleSwitch = document.createElement('div'); - toggleSwitch.classList.add('switch'); - toggleSwitch.append(field); - fieldWrapper.append(toggleSwitch); - - const slider = document.createElement('span'); - slider.classList.add('slider'); - toggleSwitch.append(slider); - slider.addEventListener('click', () => { - field.checked = !field.checked; - }); - - return { field, fieldWrapper }; -}; - -const createCheckbox = (fd) => { - const { field, fieldWrapper } = createInput(fd); - if (!field.value) field.value = 'checked'; - fieldWrapper.classList.add('selection-wrapper'); - - return { field, fieldWrapper }; -}; - -const createRadio = (fd) => { - const { field, fieldWrapper } = createInput(fd); - if (!field.value) field.value = fd.Label || 'on'; - fieldWrapper.classList.add('selection-wrapper'); - - return { field, fieldWrapper }; -}; - -const FIELD_CREATOR_FUNCTIONS = { - select: createSelect, - heading: createHeading, - plaintext: createPlaintext, - 'text-area': createTextArea, - toggle: createToggle, - submit: createSubmit, - confirmation: createConfirmation, - fieldset: createFieldset, - checkbox: createCheckbox, - radio: createRadio, -}; - -export default async function createField(fd, form) { - fd.Id = fd.Id || generateFieldId(fd); - const type = fd.Type.toLowerCase(); - const createFieldFunc = FIELD_CREATOR_FUNCTIONS[type] || createInput; - const fieldElements = await createFieldFunc(fd, form); - - return fieldElements.fieldWrapper; -} diff --git a/form/form.css b/form/form.css deleted file mode 100644 index e625b42652..0000000000 --- a/form/form.css +++ /dev/null @@ -1,170 +0,0 @@ -.form .field-wrapper { - display: grid; - grid-auto-flow: row; - align-items: center; - } - - .form fieldset { - display: grid; - grid-auto-flow: row; - margin: 0; - border: none; - padding: 0; - } - - .form form > .field-wrapper + .field-wrapper, - .form form fieldset .field-wrapper + .field-wrapper { - margin-top: 24px; - } - - .form form > .selection-wrapper + .selection-wrapper, - .form form fieldset .selection-wrapper + .selection-wrapper { - margin-top: 0.25em; - } - - @media (width >= 600px) { - .form fieldset { - grid-template-columns: repeat(2, auto); - gap: 0.25em 24px; - } - - .form form > .selection-wrapper + .selection-wrapper, - .form form fieldset .field-wrapper + .field-wrapper, - .form form fieldset .selection-wrapper + .selection-wrapper { - margin-top: 0; - } - } - - @media (width >= 900px) { - .form fieldset { - grid-template-columns: repeat(3, auto); - } - } - - .form label, - .form fieldset > legend { - margin-bottom: 0.25em; - font-size: var(--body-font-size-s); - font-weight: 700; - } - - .form .selection-wrapper label { - margin: 0; - font-weight: normal; - } - - .form input, - .form select, - .form textarea { - box-sizing: border-box; - display: block; - width: 100%; - margin: 0; - padding: 0.5em; - border-radius: 4px; - border: 1px solid var(--dark-color); - background-color: var(--background-color); - color: var(--text-color); - font-size: var(--body-font-size-s); - transition: border-color 0.2s; - } - - .form textarea { - resize: vertical; - } - - .form input:hover, - .form select:hover, - .form textarea:hover { - border: 1px solid var(--text-color); - } - - .form input:focus, - .form select:focus, - .form textarea:focus { - outline: 2px solid var(--link-color); - outline-offset: 2px; - } - - .form .selection-wrapper input { - width: max-content; - } - - @media (width >= 600px) { - .form input, - .form select, - .form textarea { - max-width: 50vw; - } - - .form .button { - max-width: max-content; - } - } - - @media (width >= 900px) { - .form input, - .form select, - .form textarea { - max-width: 33vw; - } - } - - .form .field-wrapper.selection-wrapper { - grid-auto-flow: column; - justify-content: start; - gap: 1ch; - } - - .form label[data-required]::after { - content: '*'; - color: firebrick; - margin-inline-start: 1ch; - } - - .form .toggle-wrapper .switch { - position: relative; - display: inline-block; - width: 52px; - height: 28px; - } - - .form .toggle-wrapper input { - opacity: 0; - width: 52px; - height: 28px; - } - - .form .toggle-wrapper .slider { - position: absolute; - cursor: pointer; - inset: 0; - border-radius: 28px; - background-color: var(--dark-color); - transition: background-color 0.2s; - } - - .form .toggle-wrapper .slider::before { - content: ''; - position: absolute; - width: 24px; - height: 24px; - top: 2px; - left: 2px; - border-radius: 50%; - background-color: var(--background-color); - transition: transform 0.2s; - } - - .form .toggle-wrapper input:checked + .slider { - background-color: var(--link-color); - } - - .form .toggle-wrapper input:focus + .slider { - outline: 2px solid var(--link-color); - outline-offset: 2px; - } - - .form .toggle-wrapper input:checked + .slider::before { - transform: translateX(24px); - } \ No newline at end of file diff --git a/form/form.js b/form/form.js deleted file mode 100644 index bf700ce687..0000000000 --- a/form/form.js +++ /dev/null @@ -1,102 +0,0 @@ -import createField from './form-fields.js'; - -async function createForm(formHref, submitHref) { - const { pathname } = new URL(formHref); - const resp = await fetch(pathname); - const json = await resp.json(); - - const form = document.createElement('form'); - form.dataset.action = submitHref; - - const fields = await Promise.all(json.data.map((fd) => createField(fd, form))); - fields.forEach((field) => { - if (field) { - form.append(field); - } - }); - - // group fields into fieldsets - const fieldsets = form.querySelectorAll('fieldset'); - fieldsets.forEach((fieldset) => { - form.querySelectorAll(`[data-fieldset="${fieldset.name}"`).forEach((field) => { - fieldset.append(field); - }); - }); - - return form; -} - -function generatePayload(form) { - const payload = {}; - - [...form.elements].forEach((field) => { - if (field.name && field.type !== 'submit' && !field.disabled) { - if (field.type === 'radio') { - if (field.checked) payload[field.name] = field.value; - } else if (field.type === 'checkbox') { - if (field.checked) payload[field.name] = payload[field.name] ? `${payload[field.name]},${field.value}` : field.value; - } else { - payload[field.name] = field.value; - } - } - }); - return payload; -} - -async function handleSubmit(form) { - if (form.getAttribute('data-submitting') === 'true') return; - - const submit = form.querySelector('button[type="submit"]'); - try { - form.setAttribute('data-submitting', 'true'); - submit.disabled = true; - - // create payload - const payload = generatePayload(form); - const response = await fetch(form.dataset.action, { - method: 'POST', - body: JSON.stringify({ data: payload }), - headers: { - 'Content-Type': 'application/json', - }, - }); - if (response.ok) { - if (form.dataset.confirmation) { - window.location.href = form.dataset.confirmation; - } - } else { - const error = await response.text(); - throw new Error(error); - } - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - } finally { - form.setAttribute('data-submitting', 'false'); - submit.disabled = false; - } -} - -export default async function decorate(block) { - const links = [...block.querySelectorAll('a')].map((a) => a.href); - const formLink = links.find((link) => link.startsWith(window.location.origin) && link.endsWith('.json')); - const submitLink = links.find((link) => link !== formLink); - if (!formLink || !submitLink) return; - - const form = await createForm(formLink, submitLink); - block.replaceChildren(form); - - form.addEventListener('submit', (e) => { - e.preventDefault(); - const valid = form.checkValidity(); - if (valid) { - handleSubmit(form); - } else { - const firstInvalidEl = form.querySelector(':invalid:not(fieldset)'); - if (firstInvalidEl) { - firstInvalidEl.focus(); - firstInvalidEl.scrollIntoView({ behavior: 'smooth' }); - } - } - }); -} \ No newline at end of file