3.5. as you create one page per block, you will add that to the blocks sheet

{
"project": "Your Project Name",
"editUrlLabel": "AEM Editor",
"editUrlPattern": "{{contentSourceUrl}}{{pathname}}?cmd=open",
"plugins": [
{
"id": "library",
"title": "Biblioteca",
"environments": [
"preview",
"live",
"dev"
],
"url": "/tools/sidekick/library.html"
}
]
}
/** 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-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);
}
/*
* Copyright 2023 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { createElement } from '../../library-utils.js';
// we have to hardcode this because there is no multiple sheets feature
const ICONS_PATH = 'https://main--meusensia--mrvengenharia.aem.page/library/icons';
async function processIcons(pageBlock) {
const icons = {};
const { host } = new URL(ICONS_PATH);
const iconElements = [...pageBlock.querySelectorAll('span.icon')];
await Promise.all(iconElements.map(async (icon) => {
const iconText = icon.parentElement.nextElementSibling.textContent;
const iconName = Array.from(icon.classList)
.find((c) => c.startsWith('icon-'))
.substring(5);
// need to comment out host to run locally
const response = await fetch(`https://${host}/icons/${iconName}.svg`);
// const response = await fetch(`http://localhost:3000/icons/${iconName}.svg`);
const svg = await response.text();
icons[iconText] = { label: iconText, name: iconName, svg };
}));
return icons;
}
export async function fetchBlock(path) {
if (!window.blocks) {
window.blocks = {};
}
if (!window.icons) {
window.icons = [];
}
if (!window.blocks[path]) {
const resp = await fetch('/library/icons.plain.html');
if (!resp.ok) {
return '';
}
const html = await resp.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const icons = await processIcons(doc);
window.blocks[path] = { doc, icons };
}
return window.blocks[path];
}
/**
* Called when a user tries to load the plugin.
* This takes all icons from all sheets and puts in 1 gridContainer
* @param {HTMLElement} container The container to render the plugin in
* @param {Object} data The data contained in the plugin sheet
* @param {String} query If search is active, the current search query
*/
export async function decorate(container, inputData, query) {
const data = inputData || [{
name: 'default',
path: ICONS_PATH,
}];
container.dispatchEvent(new CustomEvent('ShowLoader'));
const gridContainer = createElement('div', 'grid-container');
const iconGrid = createElement('div', 'icon-grid');
gridContainer.append(iconGrid);
const promises = data.map(async (item) => {
const { name, path } = item;
const blockPromise = fetchBlock(path);
try {
const res = await blockPromise;
if (!res) {
throw new Error(`Ocorreu um erro ao buscar ${name}/An error occurred fetching ${name}`);
}
const keys = Object.keys(res.icons).filter((key) => {
if (!query) {
return true;
}
return key.toLowerCase().includes(query.toLowerCase());
});
keys.sort().forEach((iconText) => {
const icon = res.icons[iconText];
const card = createElement('sp-card', '', { variant: 'quiet', heading: icon.label, size: 's' });
const cardIcon = createElement('div', 'icon', { size: 's', slot: 'preview' });
cardIcon.innerHTML = icon.svg;
card.append(cardIcon);
iconGrid.append(card);
card.addEventListener('click', () => {
navigator.clipboard.writeText(`:${icon.name}:`);
// Show toast
container.dispatchEvent(new CustomEvent('Toast', { detail: { message: 'Ícone copiado/Copied Icon' } }));
});
});
} catch (e) {
// eslint-disable-next-line no-console
console.error(e.message);
container.dispatchEvent(new CustomEvent('Toast', { detail: { message: e.message, variant: 'negative' } }));
}
return blockPromise;
});
await Promise.all(promises);
// Show blocks and hide loader
container.append(gridContainer);
container.dispatchEvent(new CustomEvent('HideLoader'));
}
export default {
title: 'icons',
searchEnabled: true,
};
.icon-grid {
display: flex;
flex-wrap: wrap;
margin: 64px auto;
}
.icon-grid sp-card {
height: unset;
margin: 10px;
.icon[size="s"] svg{
width: 50px;
height: 50px;
}
}
.grid-container {
display: flex;
width: calc(100% - 128px);
margin: 0 auto;
}
3.5. as you create one page per block, you will add that to the blocks sheet

https://github.com/helms-charity/mrvcopy/blob/main/tools/sidekick/library/index.js