diff --git a/blocks/content-image-teaser/_content-image-teaser.json b/blocks/content-image-teaser/_content-image-teaser.json new file mode 100644 index 0000000..ce6e4de --- /dev/null +++ b/blocks/content-image-teaser/_content-image-teaser.json @@ -0,0 +1,93 @@ +{ + "definitions": [ + { + "title": "Content/Image Teaser", + "id": "content-image-teaser", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Content/Image Teaser", + "model": "content-image-teaser", + "filter": "content-image-teaser" + } + } + } + } + } + ], + "models": [ + { + "id": "content-image-teaser", + "fields": [ + { + "component": "text", + "name": "teaser-pretitle", + "value": "", + "label": "Pretitle" + }, + { + "component": "text", + "name": "teaser-title", + "value": "", + "label": "Title" + }, + { + "component": "select", + "name": "teaser-titleType", + "label": "Title Type", + "options": [ + { "name": "H2", "value": "h2" }, + { "name": "H3", "value": "h3" }, + { "name": "H4", "value": "h4" }, + { "name": "H5", "value": "h5" }, + { "name": "H6", "value": "h6" } + ], + "value": "h2" + }, + { + "component": "richtext", + "name": "teaser-description", + "value": "", + "label": "Text" + }, + { + "component": "reference", + "name": "teaser-image", + "label": "Image", + "valueType": "string", + "multi": false + }, + { + "component": "text", + "name": "teaser-imageAlt", + "label": "Image Alt Text", + "value": "" + }, + { + "component": "select", + "name": "teaser-align", + "label": "Text Box Alignment", + "options": [ + { "name": "Left", "value": "left" }, + { "name": "Center", "value": "center" }, + { "name": "Right", "value": "right" } + ], + "value": "left" + }, + { + "component": "boolean", + "name": "teaser-coralLine", + "label": "Enable Coral Accent Line", + "value": false + }, + { + "...": "../../models/partials/_button.json#/fields" + } + ] + } + ], + "filters": [] +} + diff --git a/blocks/content-image-teaser/content-image-teaser.css b/blocks/content-image-teaser/content-image-teaser.css new file mode 100644 index 0000000..3dc5114 --- /dev/null +++ b/blocks/content-image-teaser/content-image-teaser.css @@ -0,0 +1,86 @@ +.content-image-teaser { + display: flex; + flex-direction: row; + align-items: stretch; + gap: var(--spacing-l); + margin: var(--spacing-l) 0; +} + +/* Alignment modifiers */ +.content-image-teaser.align-right { + flex-direction: row-reverse; +} + +.content-image-teaser .teaser-image, +.content-image-teaser .teaser-text { + flex: 1 1 50%; +} + +.content-image-teaser .teaser-image picture, +.content-image-teaser .teaser-image img { + width: 100%; + height: auto; + display: block; +} + +.content-image-teaser .teaser-text { + display: flex; + flex-direction: column; + justify-content: center; + padding: var(--spacing-m); +} + +/* Coral accent line */ +.content-image-teaser.coral-line .teaser-text { + border-left: 4px solid rgb(255, 127, 80); + padding-left: calc(var(--spacing-m) - 4px); +} + +.content-image-teaser.align-right.coral-line .teaser-text { + border-left: none; + border-right: 4px solid rgb(255, 127, 80); + padding-left: var(--spacing-m); + padding-right: calc(var(--spacing-m) - 4px); +} + +.content-image-teaser .teaser-pretitle { + font-size: var(--body-font-size-s); + font-weight: var(--font-medium); + margin: 0 0 var(--spacing-xs) 0; + text-transform: uppercase; + color: rgb(var(--text-color)); +} + +.content-image-teaser .teaser-title { + font-size: var(--heading-font-size-l); + font-weight: var(--font-bold); + margin: 0 0 var(--spacing-s) 0; +} + +.content-image-teaser .teaser-description { + font-size: var(--body-font-size-m); + margin: 0 0 var(--spacing-m) 0; +} + +.content-image-teaser .teaser-button .button { + margin-top: var(--spacing-s); +} + +@media (width < 900px) { + .content-image-teaser { + flex-direction: column; + } + + .content-image-teaser.align-right { + flex-direction: column; + } + + .content-image-teaser.coral-line .teaser-text, + .content-image-teaser.align-right.coral-line .teaser-text { + border-left: none; + border-right: none; + border-top: 4px solid rgb(255, 127, 80); + padding: var(--spacing-m) 0 0 0; + } +} + diff --git a/blocks/content-image-teaser/content-image-teaser.js b/blocks/content-image-teaser/content-image-teaser.js new file mode 100644 index 0000000..626738b --- /dev/null +++ b/blocks/content-image-teaser/content-image-teaser.js @@ -0,0 +1,140 @@ +import { setBlockItemOptions, moveClassToTargetedChild } from '../../scripts/utils.js'; +import { renderButton } from '../../components/button/button.js'; +import { createOptimizedPicture } from '../../scripts/aem.js'; +import { moveInstrumentation } from '../../scripts/scripts.js'; + +/** + * Decorates the content/image teaser block + * Expected columns order in the authoring table: + * 0 Pretitle + * 1 Title + * 2 Title type (h2-h6) + * 3 Text (richtext) + * 4 Image path / URL + * 5 Image alt text + * 6 Alignment (left|right) – optional, defaults to left + * 7 Coral line (true|false) – optional, defaults to false + * 8 CTA link + * 9 CTA label + * 10 CTA target (e.g., _blank) + */ +export default function decorate(block) { + // Parse authoring values to an object + const blockItemsOptions = []; + const blockItemMap = [ + { name: 'pretitle' }, + { name: 'title' }, + { name: 'description' }, + { name: 'image' }, + { name: 'align' }, + { name: 'coralLine' }, + { name: 'link' }, + { name: 'label' }, + { name: 'target' }, + ]; + + setBlockItemOptions(block, blockItemMap, blockItemsOptions); + const config = blockItemsOptions[0] || {}; + + // Wrapper element + const wrapper = document.createElement('div'); + wrapper.className = 'content-image-teaser'; + + // Alignment + const alignment = (config.align || 'left').toLowerCase(); + wrapper.classList.add(`align-${alignment}`); + + // Coral accent line + if (['true', 'yes', 'on'].includes((config.coralLine || '').toLowerCase())) { + wrapper.classList.add('coral-line'); + } + + /* ---------------- Image Column ---------------- */ + const imageDiv = document.createElement('div'); + imageDiv.className = 'teaser-image'; + + if (config.image) { + // Attempt to preserve Universal Editor instrumentation attributes + let originalImgEl; + const imageCell = block.children[4]; + if (imageCell) { + // Look for an existing or element provided by the author + originalImgEl = imageCell.querySelector('picture, img'); + } + + const optimizedPic = createOptimizedPicture( + config.image, + config.imageAlt || '', + false, + [{ media: '(min-width: 900px)' }], + ); + + if (originalImgEl) { + moveInstrumentation(originalImgEl, optimizedPic); + } + + imageDiv.appendChild(optimizedPic); + } else { + const img = document.createElement('img'); + img.src = 'https://placehold.co/600x400'; + img.alt = config.imageAlt || 'Placeholder Image'; + imageDiv.appendChild(img); + } + + /* ---------------- Text Column ---------------- */ + const textDiv = document.createElement('div'); + textDiv.className = 'teaser-text'; + + // Pretitle + if (config.pretitle) { + const preEl = document.createElement('p'); + preEl.className = 'teaser-pretitle'; + preEl.textContent = config.pretitle; + textDiv.appendChild(preEl); + } + + // Title with dynamic heading level + const headingLevel = ['h2', 'h3', 'h4', 'h5', 'h6'].includes((config.titleType || '').toLowerCase()) + ? config.titleType.toLowerCase() + : 'h2'; + + const titleEl = document.createElement(headingLevel); + titleEl.className = 'teaser-title'; + titleEl.textContent = config.title || ''; + textDiv.appendChild(titleEl); + + // Description / rich text – basic handling (as plain text) + if (config.description) { + const descEl = document.createElement('p'); + descEl.className = 'teaser-description'; + descEl.textContent = config.description; + textDiv.appendChild(descEl); + } + + // CTA button + if (config.label) { + const btnWrapper = document.createElement('div'); + btnWrapper.className = 'teaser-button'; + + const button = renderButton({ + link: config.link, + label: config.label, + target: config.target, + block, + }); + + btnWrapper.appendChild(button); + moveClassToTargetedChild(block, button); + textDiv.appendChild(btnWrapper); + } + + /* ---------------- Assemble ---------------- */ + // Order depends on alignment – for visual order we rely on flex-direction, + // but on mobile they stack. + wrapper.appendChild(imageDiv); + wrapper.appendChild(textDiv); + + // Clean original block and inject new DOM + block.textContent = ''; + block.appendChild(wrapper); +} diff --git a/component-definition.json b/component-definition.json index f58cade..020880b 100644 --- a/component-definition.json +++ b/component-definition.json @@ -229,6 +229,22 @@ } } }, + { + "title": "Content/Image Teaser", + "id": "content-image-teaser", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Content/Image Teaser", + "model": "content-image-teaser", + "filter": "content-image-teaser" + } + } + } + } + }, { "title": "Custom Button", "id": "custom-button", diff --git a/component-filters.json b/component-filters.json index ff802ed..35dbb28 100644 --- a/component-filters.json +++ b/component-filters.json @@ -26,7 +26,8 @@ "video", "gallery", "event-teaser", - "framed-grid" + "framed-grid", + "content-image-teaser" ] }, { diff --git a/component-models.json b/component-models.json index 97e7276..7da9315 100644 --- a/component-models.json +++ b/component-models.json @@ -454,6 +454,172 @@ } ] }, + { + "id": "content-image-teaser", + "fields": [ + { + "component": "text", + "name": "teaser-pretitle", + "value": "", + "label": "Pretitle" + }, + { + "component": "text", + "name": "teaser-title", + "value": "", + "label": "Title" + }, + { + "component": "select", + "name": "teaser-titleType", + "label": "Title Type", + "options": [ + { + "name": "H2", + "value": "h2" + }, + { + "name": "H3", + "value": "h3" + }, + { + "name": "H4", + "value": "h4" + }, + { + "name": "H5", + "value": "h5" + }, + { + "name": "H6", + "value": "h6" + } + ], + "value": "h2" + }, + { + "component": "richtext", + "name": "teaser-description", + "value": "", + "label": "Text" + }, + { + "component": "reference", + "name": "teaser-image", + "label": "Image", + "valueType": "string", + "multi": false + }, + { + "component": "text", + "name": "teaser-imageAlt", + "label": "Image Alt Text", + "value": "" + }, + { + "component": "select", + "name": "teaser-align", + "label": "Text Box Alignment", + "options": [ + { + "name": "Left", + "value": "left" + }, + { + "name": "Center", + "value": "center" + }, + { + "name": "Right", + "value": "right" + } + ], + "value": "left" + }, + { + "component": "boolean", + "name": "teaser-coralLine", + "label": "Enable Coral Accent Line", + "value": false + }, + { + "component": "aem-content", + "name": "custombutton-link", + "label": "Link" + }, + { + "component": "text", + "name": "custombutton-link-text", + "label": "Text" + }, + { + "component": "text", + "name": "custombutton-link-title", + "label": "Title" + }, + { + "component": "select", + "name": "custombutton-link-target", + "label": "Link Target", + "options": [ + { + "name": "Same Window", + "value": "" + }, + { + "name": "New Tab", + "value": "_blank" + } + ] + }, + { + "component": "select", + "name": "custombutton-type", + "label": "Button Type", + "options": [ + { + "name": "Link", + "value": "link" + }, + { + "name": "Download", + "value": "download" + }, + { + "name": "Email", + "value": "email" + }, + { + "name": "Telephone", + "value": "telephone" + } + ] + }, + { + "component": "select", + "name": "custombutton-style", + "label": "Button Style", + "options": [ + { + "name": "Primary", + "value": "primary" + }, + { + "name": "Secondary", + "value": "secondary" + }, + { + "name": "Tertiary", + "value": "tertiary" + }, + { + "name": "Text", + "value": "text" + } + ] + } + ] + }, { "id": "custom-button", "fields": [ diff --git a/components/button/button.js b/components/button/button.js index 0b14388..4d562aa 100644 --- a/components/button/button.js +++ b/components/button/button.js @@ -1,17 +1,80 @@ -export function renderButton({ - linkButton, linkText, linkTitle, linkTarget, linkType, linkStyle, -}) { - if (linkTarget !== '') linkButton.target = linkTarget; - if (linkText !== '') linkButton.textContent = linkText; - if (linkTitle !== '') linkButton.title = linkTitle; +/* + * Universal button renderer utilised by multiple blocks. + * + * The function historically supported two slightly different + * calling signatures. In order to keep backward-compatibility with + * older blocks (e.g. `custom-button`) _and_ support the simplified + * signature used by newer blocks (e.g. `teaser`, `event-teaser`, + * `content-image-teaser`) we normalise the incoming parameter object. + * + * Supported params (camel-cased to mirror model field names): + * 1. legacy signature + * { + * linkButton: , + * linkText: string, + * linkTitle: string, + * linkTarget: string, + * linkType: string, // telephone | email | download | undefined + * linkStyle: string // primary | secondary | text ... + * } + * 2. simplified signature + * { + * link: string, // href + * label: string, // visible text + * title: string, + * target: string, + * type: string, + * style: string, + * block: HTMLElement // optional, ignored here + * } + */ +export function renderButton(params = {}) { + // Map simplified signature keys → legacy equivalents when required. + const normalised = { + linkButton: params.linkButton, + linkText: params.linkText ?? params.label ?? '', + linkTitle: params.linkTitle ?? params.title ?? '', + linkTarget: params.linkTarget ?? params.target ?? '', + linkType: params.linkType ?? params.type ?? '', + linkStyle: params.linkStyle ?? params.style ?? '', + href: params.link ?? '', + }; + + // Ensure we have an element to work with. + let { linkButton } = normalised; + if (!linkButton) { + linkButton = document.createElement('a'); + linkButton.classList.add('button'); + // eslint-disable-next-line prefer-destructuring + linkButton.href = normalised.href; + } + + // Apply standard attributes / text. + if (normalised.linkTarget) linkButton.target = normalised.linkTarget; + if (normalised.linkText) linkButton.textContent = normalised.linkText; + if (normalised.linkTitle) linkButton.title = normalised.linkTitle; + + // Handle special link types. let { href } = linkButton; - if (linkType === 'telephone') href = `tel:${href}`; - if (linkType === 'email') href = `mailto:${href}`; - if (linkType === 'download') linkButton.download = ''; + switch (normalised.linkType) { + case 'telephone': + href = `tel:${href}`; + break; + case 'email': + href = `mailto:${href}`; + break; + case 'download': + linkButton.download = ''; + break; + default: + break; + } - if (linkStyle !== '') linkButton.classList.add(linkStyle); + // Add styling class when provided. + if (normalised.linkStyle) linkButton.classList.add(normalised.linkStyle); + // Finalise href. linkButton.href = href; return linkButton; diff --git a/models/_section.json b/models/_section.json index 94c06a5..780d50b 100644 --- a/models/_section.json +++ b/models/_section.json @@ -69,6 +69,7 @@ "gallery", "event-teaser", "framed-grid" + ,"content-image-teaser" ] } ]