diff --git a/blocks/cards/cards.js b/blocks/cards/cards.js index a3e1bfad..2bdf63a6 100644 --- a/blocks/cards/cards.js +++ b/blocks/cards/cards.js @@ -1,4 +1,4 @@ -import { createOptimizedPicture } from '../../scripts/lib-franklin.js'; +import { createOptimizedPicture } from '../../scripts/scripts.js'; export default function decorate(block) { /* change to ul, li */ diff --git a/blocks/library-config/library-config.js b/blocks/library-config/library-config.js deleted file mode 100644 index d28be4f4..00000000 --- a/blocks/library-config/library-config.js +++ /dev/null @@ -1,154 +0,0 @@ -import { createTag } from '../../scripts/scripts.js'; - -const validListTypes = ['blocks', 'sections', 'buttons', 'templates', 'icons']; - -const LIBRARY_PATH = '/block-library/library.json'; - -async function executeList(name, content, list) { - const { default: listFn } = await import(`./lists/${name}.js`); - listFn(content, list); -} - -async function loadListContent(type, content, list) { - list.innerHTML = ''; - if (validListTypes.includes(type)) { - executeList(type, content, list); - } else { - console.log(`Library type not supported: ${type}`); // eslint-disable-line no-console - } -} - -async function fetchLibrary(domain) { - const { searchParams } = new URL(window.location.href); - const suppliedLibrary = searchParams.get('library'); - const library = suppliedLibrary || `${domain}${LIBRARY_PATH}`; - const resp = await fetch(library); - if (!resp.ok) return null; - return resp.json(); -} - -async function fetchSuppliedLibrary() { - const { searchParams } = new URL(window.location.href); - const repo = searchParams.get('repo'); - const owner = searchParams.get('owner'); - if (!repo || !owner) return null; - return fetchLibrary(`https://main--${repo}--${owner}.hlx.live`); -} - -async function fetchAssetsData(path) { - if (!path) return null; - const resp = await fetch(path); - if (!resp.ok) return null; - - const json = await resp.json(); - return json.entities.map((entity) => entity.links[0].href); -} - -async function combineLibraries(base, supplied) { - if (!base) { - return {}; - } - - const library = Object.entries(base).reduce((prev, [name, item]) => ({ - ...prev, - [name]: [...(item.data || [])], - }), {}); - - const url = new URL(window.location.href); - const assetsPath = url.searchParams.get('assets'); - library.assets = await fetchAssetsData(assetsPath); - - if (supplied) { - Object.entries(supplied).forEach(([name, item]) => { - const { data } = item; - if (data?.length > 0) { - library[name].push(...data); - } - }); - } - - return library; -} - -function createHeader() { - const nav = createTag('button', { class: 'sk-library-logo' }, 'Franklin Library'); - const title = createTag('div', { class: 'sk-library-title' }, nav); - title.append(createTag('p', { class: 'sk-library-title-text' }, 'Pick a library')); - const header = createTag('div', { class: 'sk-library-header' }, title); - - nav.addEventListener('click', (e) => { - const skLibrary = e.target.closest('.sk-library'); - skLibrary.querySelector('.sk-library-title-text').textContent = 'Pick a library'; - const insetEls = skLibrary.querySelectorAll('.inset'); - insetEls.forEach((el) => { - el.classList.remove('inset'); - }); - skLibrary.classList.remove('allow-back'); - }); - return header; -} - -function createLibraryList(libraries) { - const container = createTag('div', { class: 'con-container' }); - - const libraryList = createTag('ul', { class: 'sk-library-list' }); - container.append(libraryList); - - Object.entries(libraries).forEach(([type, lib]) => { - if (!libraries[type] || libraries[type].length === 0) return; - - const item = createTag('li', { class: 'content-type' }, type); - libraryList.append(item); - - const list = document.createElement('ul'); - list.classList.add('con-type-list', `con-${type}-list`); - container.append(list); - - item.addEventListener('click', (e) => { - const skLibrary = e.target.closest('.sk-library'); - skLibrary.querySelector('.sk-library-title-text').textContent = type; - libraryList.classList.add('inset'); - list.classList.add('inset'); - skLibrary.classList.add('allow-back'); - loadListContent(type, lib, list); - }); - }); - - return container; -} - -function detectContext() { - if (window.self === window.top) { - document.body.classList.add('in-page'); - } -} - -export default async function init(el) { - el.querySelector('div').remove(); - detectContext(); - - // Get the data - const [base, supplied] = await Promise.allSettled([ - fetchLibrary(window.location.origin), - fetchSuppliedLibrary(), - ]); - const libraries = await combineLibraries(base.value, supplied.value); - - // Create the UI - const skLibrary = createTag('div', { class: 'sk-library' }); - - const header = createHeader(); - skLibrary.append(header); - - const list = createLibraryList(libraries); - skLibrary.append(list); - el.attachShadow({ mode: 'open' }); - - el.shadowRoot.append(skLibrary); - - // add styles - const link = document.createElement('link'); - link.setAttribute('rel', 'stylesheet'); - link.setAttribute('href', '/blocks/library-config/library-config.css'); - el.shadowRoot.appendChild(link); -} diff --git a/blocks/library-config/library-utils.js b/blocks/library-config/library-utils.js deleted file mode 100644 index 22a1bf5b..00000000 --- a/blocks/library-config/library-utils.js +++ /dev/null @@ -1,103 +0,0 @@ -import { createTag } from '../../scripts/scripts.js'; - -export function writeToClipboard(blob) { - const data = [new ClipboardItem({ [blob.type]: blob })]; - navigator.clipboard.write(data); -} - -export function appendListGroup(list, listData) { - const titleText = createTag('p', { class: 'item-title' }, listData.name); - const title = createTag('li', { class: 'block-group' }, titleText); - const previewButton = createTag('button', { class: 'preview-group' }, 'Preview'); - title.append(previewButton); - list.append(title); - - const groupList = createTag('ul', { class: 'block-group-list' }); - list.append(groupList); - - title.addEventListener('click', () => { - title.classList.toggle('is-open'); - }); - - previewButton.addEventListener('click', (e) => { - e.stopPropagation(); - window.open(listData.path, '_blockpreview'); - }); - - return groupList; -} - -export function createGroupItem(itemName, onCopy = () => undefined) { - if (itemName) { - const item = document.createElement('li'); - const name = document.createElement('p'); - name.textContent = itemName; - const copy = document.createElement('button'); - copy.addEventListener('click', (e) => { - const copyContent = onCopy(); - const copyButton = e.target; - copyButton.classList.toggle('copied'); - const blob = new Blob([copyContent], { type: 'text/html' }); - writeToClipboard(blob); - setTimeout(() => { - copyButton.classList.toggle('copied'); - }, 3000); - }); - item.append(name, copy); - return item; - } - return undefined; -} - -export async function fetchListDocument(listData) { - try { - const resp = await fetch(`${listData.path}.plain.html`); - if (!resp.ok) { - return null; - } - const html = await resp.text(); - const parser = new DOMParser(); - return parser.parseFromString(html, 'text/html'); - } catch (e) { - return null; - } -} - -export function decorateImages(block, path, imageAltText) { - const url = new URL(path); - block.querySelectorAll('img').forEach((img) => { - const srcSplit = img.src.split('/'); - const mediaPath = srcSplit.pop(); - img.src = `${url.origin}/${mediaPath}`; - const { width, height } = img; - const ratio = width > 200 ? 200 / width : 1; - img.width = width * ratio; - img.height = height * ratio; - img.alt = img.alt || imageAltText; - }); -} - -export function createTable(block, name, path) { - decorateImages(block, path, 'table image'); - const rows = [...block.children]; - const maxCols = rows.reduce((cols, row) => ( - row.children.length > cols ? row.children.length : cols), 0); - const table = document.createElement('table'); - table.setAttribute('border', 1); - const headerRow = document.createElement('tr'); - headerRow.append(createTag('th', { colspan: maxCols }, name)); - table.append(headerRow); - rows.forEach((row) => { - const tr = document.createElement('tr'); - [...row.children].forEach((col) => { - const td = document.createElement('td'); - if (row.children.length < maxCols) { - td.setAttribute('colspan', maxCols); - } - td.innerHTML = col.innerHTML; - tr.append(td); - }); - table.append(tr); - }); - return table.outerHTML; -} diff --git a/blocks/library-config/lists/assets.js b/blocks/library-config/lists/assets.js deleted file mode 100644 index 1ea288f9..00000000 --- a/blocks/library-config/lists/assets.js +++ /dev/null @@ -1,19 +0,0 @@ -import { writeToClipboard } from '../library-utils.js'; -import { createTag } from '../../../scripts/scripts.js'; - -function buildLink(href) { - return createTag('a', { href }, href).outerHTML; -} - -export default async function assetsList(content, list) { - content.forEach((href) => { - const img = createTag('img', { src: href }); - const li = createTag('li', { class: 'asset-item' }, img); - list.append(li); - img.addEventListener('click', () => { - const html = href.endsWith('.svg') ? buildLink(href) : img.outerHTML; - const blob = new Blob([html], { type: 'text/html' }); - writeToClipboard(blob); - }); - }); -} diff --git a/blocks/library-config/lists/blocks.js b/blocks/library-config/lists/blocks.js deleted file mode 100644 index f47cb111..00000000 --- a/blocks/library-config/lists/blocks.js +++ /dev/null @@ -1,37 +0,0 @@ -import { - appendListGroup, - createGroupItem, - fetchListDocument, - createTable, -} from '../library-utils.js'; - -function getAuthorName(block) { - const blockSib = block.previousElementSibling; - if (!blockSib) return null; - if (['H2', 'H3'].includes(blockSib.nodeName)) { - return blockSib.textContent; - } - return null; -} - -function getBlockName(block) { - const classes = block.className.split(' '); - const name = classes.shift(); - return classes.length > 0 ? `${name} (${classes.join(', ')})` : name; -} - -export default function loadBlocks(blocks, list) { - blocks.forEach(async (block) => { - const blockGroup = appendListGroup(list, block); - const blockDoc = await fetchListDocument(block); - const pageBlocks = blockDoc.body.querySelectorAll('div[class]'); - pageBlocks.forEach((pageBlock) => { - const blockName = getAuthorName(pageBlock) || getBlockName(pageBlock); - const blockItem = createGroupItem( - blockName, - () => createTable(pageBlock, getBlockName(pageBlock), block.path), - ); - blockGroup.append(blockItem); - }); - }); -} diff --git a/blocks/library-config/lists/buttons.js b/blocks/library-config/lists/buttons.js deleted file mode 100644 index bece11fa..00000000 --- a/blocks/library-config/lists/buttons.js +++ /dev/null @@ -1,38 +0,0 @@ -import { appendListGroup, createGroupItem, fetchListDocument } from '../library-utils.js'; - -function getButtonName(button) { - return button.textContent; -} - -function getButtonHTML(button) { - const span = document.createElement('span'); - span.innerHTML = button.closest('p').outerHTML; - const iconSpan = span.querySelector('span.icon'); - if (iconSpan) { - const iconClass = [...iconSpan.classList.values()].find((className) => className.startsWith('icon-')); - const iconName = iconClass.substring(iconClass.indexOf('-') + 1); - iconSpan.outerHTML = ` :${iconName}:`; - } - return span.innerHTML; -} - -export default function loadButtons(buttons, list) { - buttons.forEach(async (button) => { - const buttonGroup = appendListGroup(list, button); - const buttonDoc = await fetchListDocument(button); - if (buttonDoc === null) { - return; - } - const pageButtons = buttonDoc.body.querySelectorAll('a'); - pageButtons.forEach((pageButton) => { - const buttonName = getButtonName(pageButton); - const buttonItem = createGroupItem( - buttonName, - () => getButtonHTML(pageButton), - ); - if (buttonItem) { - buttonGroup.append(buttonItem); - } - }); - }); -} diff --git a/blocks/library-config/lists/icons.js b/blocks/library-config/lists/icons.js deleted file mode 100644 index f4571616..00000000 --- a/blocks/library-config/lists/icons.js +++ /dev/null @@ -1,35 +0,0 @@ -import { - appendListGroup, - createGroupItem, - fetchListDocument, -} from '../library-utils.js'; - -function getIconName(span) { - const heading = span.closest('p').previousElementSibling; - if (heading && ['H2', 'H3'].includes(heading.nodeName)) { - return heading.textContent; - } - return null; -} - -function createIcon(element) { - return `:${element.className.split('icon-').pop()}:`; -} - -export default function loadIcons(icons, list) { - icons.forEach(async (iconItem) => { - const iconGroup = appendListGroup(list, iconItem); - const iconDoc = await fetchListDocument(iconItem); - const pageIcons = iconDoc.body.querySelectorAll(':scope > div span.icon'); - pageIcons.forEach((iconSpan) => { - const iconName = getIconName(iconSpan); - const iconElement = createGroupItem( - iconName, - () => createIcon(iconSpan), - ); - if (iconElement) { - iconGroup.append(iconElement); - } - }); - }); -} diff --git a/blocks/library-config/lists/placeholders.js b/blocks/library-config/lists/placeholders.js deleted file mode 100644 index 570f890d..00000000 --- a/blocks/library-config/lists/placeholders.js +++ /dev/null @@ -1,27 +0,0 @@ -import { createTag } from '../../../scripts/scripts.js'; -import { writeToClipboard } from '../library-utils.js'; - -async function fetchPlaceholders(path) { - const resp = await fetch(path); - if (!resp.ok) return []; - const json = await resp.json(); - return json.data || []; -} - -export default async function placeholderList(content, list) { - const placeholders = await fetchPlaceholders(content[0].path); - placeholders.forEach((placeholder) => { - const titleText = createTag('p', { class: 'item-title' }, placeholder.value); - const title = createTag('li', { class: 'placeholder' }, titleText); - const copy = createTag('button', { class: 'copy' }); - copy.addEventListener('click', (e) => { - e.target.classList.add('copied'); - setTimeout(() => { e.target.classList.remove('copied'); }, 3000); - const formatted = `{{${placeholder.key}}}`; - const blob = new Blob([formatted], { type: 'text/plain' }); - writeToClipboard(blob); - }); - title.append(copy); - list.append(title); - }); -} diff --git a/blocks/library-config/lists/sections.js b/blocks/library-config/lists/sections.js deleted file mode 100644 index 9c4fedad..00000000 --- a/blocks/library-config/lists/sections.js +++ /dev/null @@ -1,64 +0,0 @@ -import { - appendListGroup, - createGroupItem, - fetchListDocument, - createTable, - decorateImages, -} from '../library-utils.js'; -import { readBlockConfig } from '../../../scripts/lib-franklin.js'; - -function getAuthorName(sectionMeta) { - const sibling = sectionMeta.parentElement.previousElementSibling; - if (sibling) { - const heading = sibling.querySelector('h2'); - return heading?.textContent; - } - return undefined; -} - -function getSectionName(section) { - const sectionMeta = section.querySelector('div.section-metadata'); - let sectionName; - if (sectionMeta) { - const meta = readBlockConfig(sectionMeta); - Object.keys(meta).forEach((key) => { - if (key === 'style') { - sectionName = getAuthorName(sectionMeta) || meta.style; - } - }); - } - return sectionName; -} - -function createSection(section, path) { - decorateImages(section, path, 'section-image'); - let output = '---'; - [...section.children].forEach((row) => { - if (row.nodeName === 'DIV') { - const blockName = row.classList[0]; - output = output.concat(createTable(row, blockName, path)); - } else { - output = output.concat(row.outerHTML); - } - }); - output = output.concat('---'); - return output; -} - -export default function loadSections(sections, list) { - sections.forEach(async (section) => { - const sectionGroup = appendListGroup(list, section); - const sectionDoc = await fetchListDocument(section); - const pageSections = sectionDoc.body.querySelectorAll(':scope > div'); - pageSections.forEach((pageSection) => { - const sectionName = getSectionName(pageSection); - const sectionItem = createGroupItem( - sectionName, - () => createSection(pageSection, section.path), - ); - if (sectionItem) { - sectionGroup.append(sectionItem); - } - }); - }); -} diff --git a/blocks/library-config/lists/templates.js b/blocks/library-config/lists/templates.js deleted file mode 100644 index d7eb3508..00000000 --- a/blocks/library-config/lists/templates.js +++ /dev/null @@ -1,51 +0,0 @@ -import { - appendListGroup, - createGroupItem, - fetchListDocument, - createTable, - decorateImages, -} from '../library-utils.js'; - -function createSection(section, path) { - decorateImages(section, path, 'section-image'); - let output = ''; - [...section.children].forEach((row) => { - if (row.nodeName === 'DIV') { - const blockName = row.classList[0]; - output = output.concat(createTable(row, blockName, path)); - } else { - output = output.concat(row.outerHTML); - } - }); - return output; -} - -function createTemplate(template, path) { - decorateImages(template, path, 'template-image'); - let output = ''; - [...template.children].forEach((row, i) => { - if (row.nodeName === 'DIV') { - if (i > 0) output = output.concat('---'); - output = output.concat(createSection(row, path)); - } else { - output = output.concat(row.outerHTML); - } - }); - return output; -} - -export default function loadTemplates(templates, list) { - templates.forEach(async (template) => { - const templateGroup = appendListGroup(list, template); - const templateDoc = await fetchListDocument(template); - const pageTemplate = templateDoc.body; - const templateName = template.name; - const templateItem = createGroupItem( - templateName, - () => createTemplate(pageTemplate, template.path), - ); - if (templateItem) { - templateGroup.append(templateItem); - } - }); -} diff --git a/blocks/stat/stat.css b/blocks/stat/stat.css index ed6a5295..18547c18 100644 --- a/blocks/stat/stat.css +++ b/blocks/stat/stat.css @@ -3,6 +3,11 @@ div.stat-container { padding: 0; } +div.stat-container .button-container a.tertiary { + color: var(--neutral-white); + border-bottom: 2px solid var(--neutral-white); +} + div.stat-container .stat-wrapper { width: 100%; margin: 0; diff --git a/scripts/csp.json b/scripts/csp.json index fbc046b0..900972fc 100644 --- a/scripts/csp.json +++ b/scripts/csp.json @@ -42,6 +42,7 @@ ], "connect-src": [ "'self'", + "https://*.hlx.page", "https://rum.hlx.page/", "*.mktoweb.com", "*.mktoresp.com", @@ -109,6 +110,7 @@ ], "style-src": [ "'self'", + "*.hlx.live", "'unsafe-inline'", "*.mktoweb.com", "*.marketo.com", diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index 8cc14357..f0121916 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -422,45 +422,7 @@ export async function loadBlocks(main) { } } -/** - * Returns a picture element with webp and fallbacks - * @param {string} src The image URL - * @param {boolean} eager load image eager - * @param {Array} breakpoints breakpoints and corresponding params (eg. width) - */ -export function createOptimizedPicture(src, alt = '', eager = false, breakpoints = [{ media: '(min-width: 600px)', width: '2000' }, { width: '750' }]) { - const url = new URL(src, window.location.href); - const picture = document.createElement('picture'); - const { pathname } = url; - const ext = pathname.substring(pathname.lastIndexOf('.') + 1); - - // webp - breakpoints.forEach((br) => { - const source = document.createElement('source'); - if (br.media) source.setAttribute('media', br.media); - source.setAttribute('type', 'image/webp'); - source.setAttribute('srcset', `${pathname}?width=${br.width}&format=webply&optimize=medium`); - picture.appendChild(source); - }); - - // fallback - breakpoints.forEach((br, i) => { - if (i < breakpoints.length - 1) { - const source = document.createElement('source'); - if (br.media) source.setAttribute('media', br.media); - source.setAttribute('srcset', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); - picture.appendChild(source); - } else { - const img = document.createElement('img'); - img.setAttribute('loading', eager ? 'eager' : 'lazy'); - img.setAttribute('alt', alt); - picture.appendChild(img); - img.setAttribute('src', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); - } - }); - - return picture; -} +// createOptimizedPicture moved to scripts.js /** * Normalizes all headings within a container element. diff --git a/scripts/scripts.js b/scripts/scripts.js index 261958ee..7994321e 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -12,7 +12,6 @@ import { waitForLCP, loadBlocks, loadCSS, - createOptimizedPicture, readBlockConfig, } from './lib-franklin.js'; @@ -28,6 +27,77 @@ export function locationCheck(keyword) { return window.location.pathname.includes(keyword); } +/** + * Added 2023-06-26 per new sidekick instructions + * Returns the true origin of the current page in the browser. + * If the page is running in an iframe with srcdoc, the ancestor origin is returned. + * @returns {String} The true origin + */ + +export function getOrigin() { + const { location } = window; + return location.href === 'about:srcdoc' ? window.parent.location.origin : location.origin; +} + +/** + * Returns the true of the current page in the browser.mac + * If the page is running in a iframe with srcdoc, + * the ancestor origin + the path query param is returned. + * @returns {String} The href of the current page or the href of the block running in the library + */ + +export function getHref() { + if (window.location.href !== 'about:srcdoc') return window.location.href; + + const { location: parentLocation } = window.parent; + const urlParams = new URLSearchParams(parentLocation.search); + return `${parentLocation.origin}${urlParams.get('path')}`; +} + +/** moved from lib-franklin.js per https://github.com/adobe/franklin-sidekick-library#considerations-when-building-blocks-for-the-library + * Returns a picture element with webp and fallbacks + * @param {string} src The image URL + * @param {boolean} eager load image eager + * @param {Array} breakpoints breakpoints and corresponding params (eg. width) + */ + +export function createOptimizedPicture(src, alt = '', eager = false, breakpoints = [{ + media: '(min-width: 600px)', + width: '2000', +}, { width: '750' }]) { + const url = new URL(src, getHref()); + const picture = document.createElement('picture'); + const { pathname } = url; + const ext = pathname.substring(pathname.lastIndexOf('.') + 1); + + // webp + breakpoints.forEach((br) => { + const source = document.createElement('source'); + if (br.media) source.setAttribute('media', br.media); + source.setAttribute('type', 'image/webp'); + source.setAttribute('srcset', `${pathname}?width=${br.width}&format=webply&optimize=medium`); + picture.appendChild(source); + }); + + // fallback + breakpoints.forEach((br, i) => { + if (i < breakpoints.length - 1) { + const source = document.createElement('source'); + if (br.media) source.setAttribute('media', br.media); + source.setAttribute('srcset', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); + picture.appendChild(source); + } else { + const img = document.createElement('img'); + img.setAttribute('loading', eager ? 'eager' : 'lazy'); + img.setAttribute('alt', alt); + picture.appendChild(img); + img.setAttribute('src', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); + } + }); + + return picture; +} + /** * Helper function to create DOM elements * @param {string} tag DOM element to be created diff --git a/styles/styles.css b/styles/styles.css index 1a8b1070..6ba8a1f5 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -1761,7 +1761,7 @@ body.quick-links main .section > div { } /* All except desktop */ -@media only screen and (max-width: 1200px) { +@media only screen and (max-width: 1200px) { html { scroll-padding-top: calc(var(--nav-height-mobile) + var(--spacer-element-03)); } @@ -1833,7 +1833,7 @@ body.quick-links main .section > div { letter-spacing: var(--letter-spacing-1-75); } - h4, + h4, code, h5 { font-family: var(--sans-serif-font-regular); diff --git a/test/scripts/block-utils.test.js b/test/scripts/block-utils.test.js deleted file mode 100644 index b18a0410..00000000 --- a/test/scripts/block-utils.test.js +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable no-unused-expressions */ -/* global describe before it */ - -import { readFile } from '@web/test-runner-commands'; -import { expect } from '@esm-bundle/chai'; -import sinon from 'sinon'; - -let blockUtils; - -document.body.innerHTML = await readFile({ path: './dummy.html' }); -document.head.innerHTML = await readFile({ path: './head.html' }); - -describe('Utils methods', () => { - before(async () => { - blockUtils = await import('../../scripts/lib-franklin.js'); - document.body.innerHTML = await readFile({ path: './body.html' }); - }); - - it('Sanitizes class name', async () => { - expect(blockUtils.toClassName('Hello world')).to.equal('hello-world'); - expect(blockUtils.toClassName(null)).to.equal(''); - }); - - it('Extracts metadata', async () => { - expect(blockUtils.getMetadata('description')).to.equal('Lorem ipsum dolor sit amet.'); - expect(blockUtils.getMetadata('og:title')).to.equal('Foo'); - }); - - it('Loads CSS', async () => { - // loads a css file and calls callback - const load = await new Promise((resolve) => { - blockUtils.loadCSS('/test/scripts/test.css', (e) => resolve(e)); - }); - expect(load).to.equal('load'); - expect(getComputedStyle(document.body).color).to.equal('rgb(255, 0, 0)'); - - // does nothing if css already loaded - const noop = await new Promise((resolve) => { - blockUtils.loadCSS('/test/scripts/test.css', (e) => resolve(e)); - }); - expect(noop).to.equal('noop'); - - // calls callback in case of error - const error = await new Promise((resolve) => { - blockUtils.loadCSS('/test/scripts/nope.css', (e) => resolve(e)); - }); - expect(error).to.equal('error'); - }); - - it('Collects RUM data', async () => { - const sendBeacon = sinon.stub(navigator, 'sendBeacon'); - // turn on RUM - window.history.pushState({}, '', `${window.location.href}&rum=on`); - delete window.hlx; - - // sends checkpoint beacon - await blockUtils.sampleRUM('test', { foo: 'bar' }); - expect(sendBeacon.called).to.be.true; - sendBeacon.resetHistory(); - - // sends cwv beacon - await blockUtils.sampleRUM('cwv', { foo: 'bar' }); - expect(sendBeacon.called).to.be.true; - - // test error handling - sendBeacon.throws(); - await blockUtils.sampleRUM('error', { foo: 'bar' }); - - sendBeacon.restore(); - }); - - it('Creates optimized picture', async () => { - const $picture = blockUtils.createOptimizedPicture('/test/scripts/mock.png'); - expect($picture.querySelector(':scope source[type="image/webp"]')).to.exist; // webp - expect($picture.querySelector(':scope source:not([type="image/webp"])')).to.exist; // fallback - expect($picture.querySelector(':scope img').src).to.include('format=png&optimize=medium'); // default - }); - - it('Normalizes headings', async () => { - const numHeadings = document.querySelectorAll('h1, h2, h3, h4, h5, h6').length; - blockUtils.normalizeHeadings(document.querySelector('main'), ['h1', 'h2', 'h3']); - expect(document.querySelectorAll('h1, h2, h3, h4, h5, h6').length).to.equal(numHeadings); - expect(document.querySelectorAll('h4, h5, h6').length).to.equal(0); - }); -}); - -describe('Sections and blocks', () => { - it('Decorates sections', async () => { - blockUtils.decorateSections(document.querySelector('main')); - expect(document.querySelectorAll('main .section').length).to.equal(2); - }); - - it('Decorates blocks', async () => { - blockUtils.decorateBlocks(document.querySelector('main')); - expect(document.querySelectorAll('main .block').length).to.equal(1); - }); - - it('Loads blocks', async () => { - await blockUtils.loadBlocks(document.querySelector('main')); - document.querySelectorAll('main .block').forEach(($block) => { - expect($block.dataset.blockStatus).to.equal('loaded'); - }); - }); - - it('Updates section status', async () => { - blockUtils.updateSectionsStatus(document.querySelector('main')); - document.querySelectorAll('main .section').forEach(($section) => { - expect($section.dataset.sectionStatus).to.equal('loaded'); - }); - - // test section with block still loading - const $section = document.querySelector('main .section'); - delete $section.dataset.sectionStatus; - $section.querySelector(':scope .block').dataset.blockStatus = 'loading'; - blockUtils.updateSectionsStatus(document.querySelector('main')); - expect($section.dataset.sectionStatus).to.equal('loading'); - }); - - it('Reads block config', async () => { - document.querySelector('main .section > div').innerHTML += await readFile({ path: './config.html' }); - const cfg = blockUtils.readBlockConfig(document.querySelector('main .config')); - expect(cfg).to.deep.include({ - 'prop-0': 'Plain text', - 'prop-1': 'Paragraph', - 'prop-2': ['First paragraph', 'Second paragraph'], - 'prop-3': 'https://www.adobe.com/', - 'prop-4': ['https://www.adobe.com/', 'https://www.hlx.live/'], - }); - }); -}); diff --git a/tools/sidekick/config.json b/tools/sidekick/config.json index 9060a5e8..1a88e5de 100644 --- a/tools/sidekick/config.json +++ b/tools/sidekick/config.json @@ -6,9 +6,7 @@ "id": "library", "title": "Library", "environments": [ "edit" ], - "isPalette": true, - "paletteRect": "top: auto; bottom: 20px; left: 20px; height: 398px; width: 360px;", - "url": "/block-library/library", + "url": "/tools/sidekick/library.html", "includePaths": [ "**.docx**" ] }, { @@ -16,7 +14,9 @@ "excludePaths": [ "**/drafts/**", - "**%2Fdrafts**" + "**%2Fdrafts**", + "**/block-library/**", + "**%2Fblock-library**" ] }, { @@ -26,4 +26,4 @@ "url": "/tools/tagger/index.html" } ] -} \ No newline at end of file +} diff --git a/blocks/library-config/library-config.css b/tools/sidekick/library-config.css similarity index 100% rename from blocks/library-config/library-config.css rename to tools/sidekick/library-config.css diff --git a/tools/sidekick/library-utils.js b/tools/sidekick/library-utils.js new file mode 100644 index 00000000..1c0f7975 --- /dev/null +++ b/tools/sidekick/library-utils.js @@ -0,0 +1,239 @@ +/** Needed for icons + * Create an element with the given id and classes. + * @param {string} tagName the tag + * @param {string[]|string} classes the class or classes to add + * @param {object} props any other attributes to add to the element + * @param {string|Element} html content to add + * @returns the element + */ +export function createElement(tagName, classes, props, html) { + const elem = document.createElement(tagName); + if (classes) { + const classesArr = (typeof classes === 'string') ? [classes] : classes; + elem.classList.add(...classesArr); + } + if (props) { + Object.keys(props).forEach((propName) => { + elem.setAttribute(propName, props[propName]); + }); + } + if (html) { // added for templates feature + const appendEl = (el) => { + if (el instanceof HTMLElement || el instanceof SVGElement) { + elem.append(el); + } else { + elem.insertAdjacentHTML('beforeend', el); + } + }; + + if (Array.isArray(html)) { + html.forEach(appendEl); + } else { + appendEl(html); + } + } + + return elem; +} + +/** + * Sanitizes a name for use as class name. + * @param {string} name The unsanitized name + * @returns {string} The class name + */ +export function toClassName(name) { + return typeof name === 'string' + ? name.toLowerCase().replace(/[^0-9a-z]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') + : ''; +} + +/** + * Find and return the library metadata info. + * @param {Element} elem the page/block element containing the library metadata block + */ +export function getLibraryMetadata(elem) { + const config = {}; + const libMeta = elem.querySelector('div.library-metadata'); + if (libMeta) { + libMeta.querySelectorAll(':scope > div').forEach((row) => { + if (row.children) { + const cols = [...row.children]; + if (cols[1]) { + const name = toClassName(cols[0].textContent); + const value = row.children[1].textContent; + config[name] = value; + } + } + }); + } + + return config; +} + +export async function fetchBlockPage(path) { + if (!window.blocks) { + window.blocks = {}; + } + if (!window.blocks[path]) { + const resp = await fetch(`${path}.plain.html`); + if (!resp.ok) return ''; + + const html = await resp.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + window.blocks[path] = doc; + } + + return window.blocks[path]; +} + +export function renderScaffolding() { + return createElement('div', 'block-library', {}, createElement('sp-split-view', '', { + 'primary-size': '350', + dir: 'ltr', + 'splitter-pos': '250', + resizable: '', + }, [ + createElement('div', 'menu', {}, [ + createElement('div', 'list-container'), + ]), + createElement('div', 'content'), + ])); +} + +/** + * Copies to the clipboard + * @param {Blob} blob The data + */ +export async function createCopy(blob) { + // eslint-disable-next-line no-undef + const data = [new ClipboardItem({ [blob.type]: blob })]; + await navigator.clipboard.write(data); +} + +export function processMarkup(pageBlock, path) { + const copy = pageBlock.cloneNode(true); + const url = new URL(path); + copy.querySelectorAll('img').forEach((img) => { + const srcSplit = img.src.split('/'); + const mediaPath = srcSplit.pop(); + img.src = `${url.origin}/${mediaPath}`; + const { width, height } = img; + const ratio = width > 450 ? 450 / width : 1; + img.width = width * ratio; + img.height = height * ratio; + }); + + copy.querySelector('div.library-metadata')?.remove(); + + return copy; +} + +export async function copyBlock(pageBlock, path, container) { + const processed = processMarkup(pageBlock, path); + const blob = new Blob([processed.innerHTML], { type: 'text/html' }); + try { + await createCopy(blob); + // Show toast + container.dispatchEvent(new CustomEvent('Toast', { detail: { message: 'Copied Block' } })); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err.message); + container.dispatchEvent(new CustomEvent('Toast', { detail: { message: err.message, variant: 'negative' } })); + } +} + +// Need to copy this here since only the Blocks feature uses it from helix source +export function initSplitFrame(content) { + const contentContainer = content.querySelector('.content'); + if (contentContainer.querySelector('sp-split-view')) { + // already initialized + return; + } + + contentContainer.append(createElement('sp-split-view', '', { + vertical: '', + resizable: '', + 'primary-size': '2600', + 'secondary-min': '200', + 'splitter-pos': '250', + }, [ + createElement('div', 'view', {}, [ + createElement('div', 'action-bar', {}, [ + createElement('sp-action-group', '', { compact: '', selects: 'single', selected: 'desktop' }, [ + createElement('sp-action-button', '', { value: 'mobile' }, [ + createElement('sp-icon-device-phone', '', { slot: 'icon' }), + 'Mobile', + ]), + createElement('sp-action-button', '', { value: 'tablet' }, [ + createElement('sp-icon-device-tablet', '', { slot: 'icon' }), + 'Tablet', + ]), + createElement('sp-action-button', '', { value: 'desktop' }, [ + createElement('sp-icon-device-desktop', '', { slot: 'icon' }), + 'Desktop', + ]), + ]), + createElement('sp-divider', '', { size: 's' }), + ]), + createElement('div', 'frame-view', {}), + ]), + createElement('div', 'details-container', {}, [ + createElement('div', 'action-bar', {}, [ + createElement('h3', 'block-title'), + createElement('div', 'actions', {}, createElement('sp-button', 'copy-button', {}, 'Copy Block')), + ]), + createElement('sp-divider', '', { size: 's' }), + createElement('div', 'details'), + ]), + ])); + + const actionGroup = content.querySelector('sp-action-group'); + actionGroup.selected = 'desktop'; + + const frameView = content.querySelector('.frame-view'); + const mobileViewButton = content.querySelector('sp-action-button[value="mobile"]'); + mobileViewButton?.addEventListener('click', () => { + frameView.style.width = '480px'; + }); + + const tabletViewButton = content.querySelector('sp-action-button[value="tablet"]'); + tabletViewButton?.addEventListener('click', () => { + frameView.style.width = '768px'; + }); + + const desktopViewButton = content.querySelector('sp-action-button[value="desktop"]'); + desktopViewButton?.addEventListener('click', () => { + frameView.style.width = '100%'; + }); +} + +export async function renderPreview(pageBlock, path, previewContainer) { + const frame = createElement('iframe'); + + const blockPageResp = await fetch(path); + if (!blockPageResp.ok) { + return; + } + + const html = await blockPageResp.text(); + const parser = new DOMParser(); + const blockPage = parser.parseFromString(html, 'text/html'); + + const blockPageDoc = blockPage.documentElement; + const blockPageMain = blockPageDoc.querySelector('main'); + + blockPageDoc.querySelector('header').style.display = 'none'; + blockPageDoc.querySelector('footer').style.display = 'none'; + blockPageMain.replaceChildren(processMarkup(pageBlock, path)); + + frame.srcdoc = blockPageDoc.outerHTML; + frame.style.display = 'block'; + + frame.addEventListener('load', () => { + // todo + }); + + previewContainer.innerHTML = ''; + previewContainer.append(frame); +} diff --git a/tools/sidekick/library.html b/tools/sidekick/library.html new file mode 100644 index 00000000..ec94a036 --- /dev/null +++ b/tools/sidekick/library.html @@ -0,0 +1,53 @@ + + + +
+ + + + +