Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 109 additions & 7 deletions blocks/cards/cards.css
Original file line number Diff line number Diff line change
@@ -1,27 +1,129 @@
.cards {
display: flex;
justify-content: center;
}

.cards > ul {
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(257px, 1fr));
flex: 1;
grid-template-columns: 1fr;
justify-items: center;
gap: var(--space-m);
margin: 0;
padding: 0;
}

@media (width >= 600px) {
.cards.cols-2 > ul,
.cards.cols-4 > ul,
.cards.cols-6 > ul {
grid-template-columns: repeat(2, 1fr);
}
}

@media (width >= 900px) {
.cards.cols-3 > ul,
.cards.cols-6 > ul {
grid-template-columns: repeat(3, 1fr);
}

.cards.cols-4 > ul {
grid-template-columns: repeat(4, 1fr);
}
}

.cards > ul > li {
display: flex;
flex-direction: column;
width: 100%;
min-width: 0;
border: 1px solid var(--color-border);
background-color: var(--color-bg);
}

.cards .cards-card-body {
margin: var(--space-s);
.cards li.linked {
position: relative;
cursor: pointer;
transition: box-shadow 0.2s;
}

.cards li.linked a::before {
content: '';
position: absolute;
inset: 0;
}

.cards li.linked:hover {
box-shadow: var(--shadow-elevated);
}

/* media wrapper */
.cards .media-wrapper {
position: relative;
overflow: hidden;
height: 200px;
border: 1px solid var(--color-border);
border-bottom: 0;
}

.cards .cards-card-image {
line-height: 0;
.cards .media-wrapper picture {
position: absolute;
inset: 0;
box-sizing: border-box;
}

.cards > ul > li img {
.cards .media-wrapper img {
height: 100%;
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}

@media (width >= 600px) {
.cards .media-wrapper {
height: unset;
aspect-ratio: 4 / 3;
}
}

/* body wrapper */
.cards .body-wrapper {
flex: 1;
padding: var(--space-m);
}

/* footer */
.cards footer {
padding: var(--space-m);
padding-top: 0;
}

.cards footer .button {
width: 100%;
}

.cards footer a[href]:not(.button) {
color: inherit;
font-family: var(--heading-font-family);
text-transform: uppercase;
}

.cards footer a[href]:not(.button)::after {
content: '';
display: inline-block;
height: 1em;
width: 1em;
margin-left: 1ch;
background-color: currentcolor;
mask-image: url('/icons/chevron.svg');
mask-repeat: no-repeat;
mask-size: contain;
transform: rotate(-90deg);
vertical-align: middle;
}

@media (width >= 900px) {
.cards footer .button {
width: unset;
}
}
79 changes: 71 additions & 8 deletions blocks/cards/cards.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,80 @@
import { createOptimizedPicture } from '../../scripts/aem.js';
import { createOptimizedPicture, decorateIcons } from '../../scripts/aem.js';

/**
* Returns the largest factor of the block's row count between 1 and 6.
* @param {Element} block The cards block element
* @returns {number} Grid column count to apply as a cols-N class
*/
function getGridFactor(block) {
const rows = block.children.length;
for (let n = 6; n >= 2; n -= 1) {
if (rows % n === 0) return n;
}
return rows === 1 ? 1 : 3;
}

/**
* Moves a trailing CTA out of .body-wrapper into a card footer.
* @param {HTMLElement} card The card <li> element being decorated
*/
function extractButtons(card) {
const body = card.querySelector('.body-wrapper');
if (!body) return;
const last = body.lastElementChild;
if (!last) return;

const isButtonWrapper = last.classList.contains('button-wrapper');
const isLoneLink = last.tagName === 'P'
&& last.querySelectorAll('a[href]').length === 1
&& last.textContent.trim() === last.querySelector('a[href]').textContent.trim();

if (!isButtonWrapper && !isLoneLink) return;
const footer = document.createElement('footer');
footer.append(last);
card.append(footer);
}

/**
* Marks a card as fully clickable when it contains exactly one link.
* @param {HTMLElement} card The card <li> element being decorated
*/
function linkCard(card) {
const links = [...card.querySelectorAll('a[href]')];
if (links.length !== 1) return;
card.classList.add('linked');
}

/**
* Replaces all images in the block with optimized picture elements.
* @param {Element} block The cards block element
*/
function optimizeImages(block) {
block.querySelectorAll('.media-wrapper picture > img').forEach((img) => {
img.closest('picture').replaceWith(createOptimizedPicture(img.src, img.alt, false, [{ width: '750' }]));
});
}

export default function decorate(block) {
/* change to ul, li */
if (![...block.classList].some((c) => c.startsWith('cols-'))) {
block.classList.add(`cols-${getGridFactor(block)}`);
}

const ul = document.createElement('ul');
[...block.children].forEach((row) => {
[...block.children].forEach((card) => {
const li = document.createElement('li');
while (row.firstElementChild) li.append(row.firstElementChild);
[...li.children].forEach((div) => {
if (div.children.length === 1 && div.querySelector('picture')) div.className = 'cards-card-image';
else div.className = 'cards-card-body';
[...card.children].forEach((cell) => {
const els = [...cell.children];
const isMedia = els.length > 0 && els.every((el) => el.tagName === 'PICTURE' || el.tagName === 'VIDEO');
cell.classList.add(isMedia ? 'media-wrapper' : 'body-wrapper');
li.append(cell);
});
extractButtons(li);
linkCard(li);
ul.append(li);
});
ul.querySelectorAll('picture > img').forEach((img) => img.closest('picture').replaceWith(createOptimizedPicture(img.src, img.alt, false, [{ width: '750' }])));

block.replaceChildren(ul);

optimizeImages(block);
decorateIcons(block);
}