Skip to content

Create block library + icon picker #7

Description

@helms-charity
  1. See this page for an example https://main--meusensia--mrvengenharia.aem.page/tools/sidekick/library.html?plugin=blocks
  2. create a /library directory in AEM
  3. In it, create a Sheet called "blocks", and another sheet called "helix-icons". The helix-icons sheet will only have 1 line which is this
Image

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

  1. Create /tools/sidekick/config.json with these contents (Not tested on AEM 6.5)
{
  "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"
    }
  ]
}
  1. create /tools/sidekick/library-utils.js with these contents:
/** 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);
}
  1. create /tools/sidekick/plugins/icons/icons.js with this
/*
 * 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,
};
  1. Create icons.css with this
.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;
}
  1. Finally, you'll need to have a local copy of /tools/sidekick/library/index.js
    https://github.com/helms-charity/mrvcopy/blob/main/tools/sidekick/library/index.js

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions