diff --git a/blocks/accordion/accordion.css b/blocks/accordion/accordion.css new file mode 100644 index 0000000..e9b3c2c --- /dev/null +++ b/blocks/accordion/accordion.css @@ -0,0 +1,57 @@ +.accordion details { + border: 1px solid var(--dark-color); +} + +/* stylelint-disable-next-line no-descending-specificity */ +.accordion details + details { + margin-top: 16px; +} + +.accordion details summary { + position: relative; + padding: 0 16px; + padding-right: 48px; + cursor: pointer; + list-style: none; + overflow: auto; + transition: background-color 0.2s; +} + +.accordion details[open] summary { + background-color: var(--light-color); +} + +.accordion details summary:focus, +.accordion details summary:hover { + background-color: var(--dark-color); +} + +.accordion details summary::-webkit-details-marker { + display: none; +} + +.accordion details summary::after { + content: ""; + position: absolute; + top: 50%; + right: 18px; + transform: translateY(-50%) rotate(135deg); + width: 9px; + height: 9px; + border: 2px solid; + border-width: 2px 2px 0 0; + transition: transform 0.2s; +} + +.accordion details[open] summary::after { + transform: translateY(-50%) rotate(-45deg); +} + +.accordion details .accordion-item-body { + padding: 0 16px; +} + +.accordion details[open] .accordion-item-body { + border-top: 1px solid var(--dark-color); + background-color: var(--background-color); +} \ No newline at end of file diff --git a/blocks/accordion/accordion.js b/blocks/accordion/accordion.js new file mode 100644 index 0000000..53ded06 --- /dev/null +++ b/blocks/accordion/accordion.js @@ -0,0 +1,33 @@ +/* + * Accordion Block + * Recreate an accordion + * https://www.hlx.live/developer/block-collection/accordion + */ + +function hasWrapper(el) { + return !!el.firstElementChild && window.getComputedStyle(el.firstElementChild).display === 'block'; +} + +export default function decorate(block) { + [...block.children].forEach((row) => { + // decorate accordion item label + const label = row.children[0]; + const summary = document.createElement('summary'); + summary.className = 'accordion-item-label'; + summary.append(...label.childNodes); + if (!hasWrapper(summary)) { + summary.innerHTML = `

${summary.innerHTML}

`; + } + // decorate accordion item body + const body = row.children[1]; + body.className = 'accordion-item-body'; + if (!hasWrapper(body)) { + body.innerHTML = `

${body.innerHTML}

`; + } + // decorate accordion item + const details = document.createElement('details'); + details.className = 'accordion-item'; + details.append(summary, body); + row.replaceWith(details); + }); +} diff --git a/blocks/embed/embed.css b/blocks/embed/embed.css new file mode 100644 index 0000000..28c4429 --- /dev/null +++ b/blocks/embed/embed.css @@ -0,0 +1,62 @@ +.embed { + width: unset; + text-align: center; + max-width: 800px; + margin: 32px auto; +} + +.embed > div { + display: flex; + justify-content: center; +} + +.embed.embed-twitter .twitter-tweet-rendered { + margin-left: auto; + margin-right: auto; +} + +.embed .embed-placeholder { + width: 100%; + aspect-ratio: 16 / 9; + position: relative; +} + +.embed .embed-placeholder > * { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + inset: 0; +} + +.embed .embed-placeholder picture img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.embed .embed-placeholder-play button { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(3); + width: 22px; + height: 22px; + border: 2px solid; + border-radius: 20px; + padding: 0; +} + +.embed .embed-placeholder-play button::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 0; + height: 10px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 6px solid; + top: 4px; + left: 7px; +} \ No newline at end of file diff --git a/blocks/embed/embed.js b/blocks/embed/embed.js new file mode 100644 index 0000000..a695ec0 --- /dev/null +++ b/blocks/embed/embed.js @@ -0,0 +1,113 @@ +/* + * Embed Block + * Show videos and social posts directly on your page + * https://www.hlx.live/developer/block-collection/embed + */ + +const loadScript = (url, callback, type) => { + const head = document.querySelector('head'); + const script = document.createElement('script'); + script.src = url; + if (type) { + script.setAttribute('type', type); + } + script.onload = callback; + head.append(script); + return script; +}; + +const getDefaultEmbed = (url) => `
+ +
`; + +const embedYoutube = (url, autoplay) => { + const usp = new URLSearchParams(url.search); + const suffix = autoplay ? '&muted=1&autoplay=1' : ''; + let vid = usp.get('v') ? encodeURIComponent(usp.get('v')) : ''; + const embed = url.pathname; + if (url.origin.includes('youtu.be')) { + [, vid] = url.pathname.split('/'); + } + const embedHTML = `
+ +
`; + return embedHTML; +}; + +const embedVimeo = (url, autoplay) => { + const [, video] = url.pathname.split('/'); + const suffix = autoplay ? '?muted=1&autoplay=1' : ''; + const embedHTML = `
+ +
`; + return embedHTML; +}; + +const embedTwitter = (url) => { + const embedHTML = `
`; + loadScript('https://platform.twitter.com/widgets.js'); + return embedHTML; +}; + +const loadEmbed = (block, link, autoplay) => { + if (block.classList.contains('embed-is-loaded')) { + return; + } + + const EMBEDS_CONFIG = [ + { + match: ['youtube', 'youtu.be'], + embed: embedYoutube, + }, + { + match: ['vimeo'], + embed: embedVimeo, + }, + { + match: ['twitter'], + embed: embedTwitter, + }, + ]; + + const config = EMBEDS_CONFIG.find((e) => e.match.some((match) => link.includes(match))); + const url = new URL(link); + if (config) { + block.innerHTML = config.embed(url, autoplay); + block.classList = `block embed embed-${config.match[0]}`; + } else { + block.innerHTML = getDefaultEmbed(url); + block.classList = 'block embed'; + } + block.classList.add('embed-is-loaded'); +}; + +export default function decorate(block) { + const placeholder = block.querySelector('picture'); + const link = block.querySelector('a').href; + block.textContent = ''; + + if (placeholder) { + const wrapper = document.createElement('div'); + wrapper.className = 'embed-placeholder'; + wrapper.innerHTML = '
'; + wrapper.prepend(placeholder); + wrapper.addEventListener('click', () => { + loadEmbed(block, link, true); + }); + block.append(wrapper); + } else { + const observer = new IntersectionObserver((entries) => { + if (entries.some((e) => e.isIntersecting)) { + observer.disconnect(); + loadEmbed(block, link); + } + }); + observer.observe(block); + } +} diff --git a/blocks/quote/quote.css b/blocks/quote/quote.css new file mode 100644 index 0000000..1fc60e9 --- /dev/null +++ b/blocks/quote/quote.css @@ -0,0 +1,34 @@ +.quote blockquote { + margin: 0 auto; + padding: 0 32px; + max-width: 700px; +} + +.quote blockquote .quote-quotation { + font-size: 120%; +} + +.quote blockquote .quote-quotation > :first-child { + text-indent: -0.6ch; +} + +.quote blockquote .quote-quotation > :first-child::before, +.quote blockquote .quote-quotation > :last-child::after { + line-height: 0; +} + +.quote blockquote .quote-quotation > :first-child::before { + content: "“"; +} + +.quote blockquote .quote-quotation > :last-child::after { + content: "”"; +} + +.quote blockquote .quote-attribution { + text-align: right; +} + +.quote blockquote .quote-attribution > :first-child::before { + content: "—"; +} \ No newline at end of file diff --git a/blocks/quote/quote.js b/blocks/quote/quote.js new file mode 100644 index 0000000..d6c51ed --- /dev/null +++ b/blocks/quote/quote.js @@ -0,0 +1,30 @@ +function hasWrapper(el) { + return !!el.firstElementChild && window.getComputedStyle(el.firstElementChild).display === 'block'; +} + +export default async function decorate(block) { + const [quotation, attribution] = [...block.children].map((c) => c.firstElementChild); + const blockquote = document.createElement('blockquote'); + // decorate quotation + quotation.className = 'quote-quotation'; + if (!hasWrapper(quotation)) { + quotation.innerHTML = `

${quotation.innerHTML}

`; + } + blockquote.append(quotation); + // decoration attribution + if (attribution) { + attribution.className = 'quote-attribution'; + if (!hasWrapper(attribution)) { + attribution.innerHTML = `

${attribution.innerHTML}

`; + } + blockquote.append(attribution); + const ems = attribution.querySelectorAll('em'); + ems.forEach((em) => { + const cite = document.createElement('cite'); + cite.innerHTML = em.innerHTML; + em.replaceWith(cite); + }); + } + block.innerHTML = ''; + block.append(blockquote); +}