From f5c527488b8af7d62b52743fcb48e7ac612d380e Mon Sep 17 00:00:00 2001 From: Dirk Rudolph Date: Sun, 3 Mar 2024 22:54:03 +0100 Subject: [PATCH] feat: in-context editable blocks (#5) * feat: support in-context authoring for blocks * fix: preserve rte instrumentation * fix: decorate richtext after decorating blocks on reload * fix: move editor-support-rte.js scripts to boilerplate * chore: short hand for moving instrumentation * fix: remove moved attribute --- blocks/cards/cards.js | 11 +++++--- scripts/aem.js | 12 +++++++++ scripts/editor-support-rte.js | 47 ++++++++++++++++++++++++----------- scripts/editor-support.js | 2 +- scripts/scripts.js | 34 +++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 19 deletions(-) diff --git a/blocks/cards/cards.js b/blocks/cards/cards.js index ab62dbadd..084c4e4f1 100644 --- a/blocks/cards/cards.js +++ b/blocks/cards/cards.js @@ -1,13 +1,12 @@ import { createOptimizedPicture } from '../../scripts/aem.js'; +import { moveInstrumentation } from '../../scripts/scripts.js'; export default function decorate(block) { /* change to ul, li */ const ul = document.createElement('ul'); [...block.children].forEach((row) => { const li = document.createElement('li'); - [...row.attributes].forEach(({ nodeName, nodeValue }) => { - li.setAttribute(nodeName, nodeValue); - }); + moveInstrumentation(row, 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'; @@ -15,7 +14,11 @@ export default function decorate(block) { }); ul.append(li); }); - ul.querySelectorAll('img').forEach((img) => img.closest('picture').replaceWith(createOptimizedPicture(img.src, img.alt, false, [{ width: '750' }]))); + ul.querySelectorAll('img').forEach((img) => { + const optimizedPic = createOptimizedPicture(img.src, img.alt, false, [{ width: '750' }]); + moveInstrumentation(img, optimizedPic.querySelector('img')); + img.closest('picture').replaceWith(optimizedPic); + }); block.textContent = ''; block.append(ul); } diff --git a/scripts/aem.js b/scripts/aem.js index b1e048f95..abf2b4d56 100644 --- a/scripts/aem.js +++ b/scripts/aem.js @@ -634,10 +634,22 @@ function decorateBlock(block) { if ((!firstChild && cellText) || (firstChild && !firstChild.tagName.match(/^(P(RE)?|H[1-6]|(U|O)L|TABLE)$/))) { const paragraph = document.createElement('p'); + [...cell.attributes] + // move the instrumentation from the cell to the new paragraph, also keep the class + // in case the content is a buttton and the cell the button-container + .filter(({ nodeName }) => nodeName === 'class' + || nodeName.startsWith('data-aue') + || nodeName.startsWith('data-richtext')) + .forEach(({ nodeName, nodeValue }) => { + paragraph.setAttribute(nodeName, nodeValue); + cell.removeAttribute(nodeName); + }); paragraph.append(...cell.childNodes); cell.replaceChildren(paragraph); } }); + // eslint-disable-next-line no-use-before-define + decorateButtons(block); } } diff --git a/scripts/editor-support-rte.js b/scripts/editor-support-rte.js index 0136b1203..bd73106a9 100644 --- a/scripts/editor-support-rte.js +++ b/scripts/editor-support-rte.js @@ -1,23 +1,29 @@ -// group editable texts in single wrappers if applicable -// -// this script should execute after script.js by editor-support.js +/* eslint-disable no-cond-assign */ +/* eslint-disable import/prefer-default-export */ + +// group editable texts in single wrappers if applicable. +// this script should execute after script.js but before the the universal editor cors script +// and any block being loaded -// eslint-disable-next-line import/prefer-default-export export function decorateRichtext(container = document) { function deleteInstrumentation(element) { delete element.dataset.richtextResource; delete element.dataset.richtextProp; delete element.dataset.richtextFilter; + delete element.dataset.richtextLabel; } let element; - // eslint-disable-next-line no-cond-assign - while (element = container.querySelector('[data-richtext-resource]')) { - const { richtextResource, richtextProp, richtextFilter } = element.dataset; + while (element = container.querySelector('[data-richtext-prop]:not(div)')) { + const { + richtextResource, + richtextProp, + richtextFilter, + richtextLabel, + } = element.dataset; deleteInstrumentation(element); const siblings = []; let sibling = element; - // eslint-disable-next-line no-cond-assign while (sibling = sibling.nextElementSibling) { if (sibling.dataset.richtextResource === richtextResource && sibling.dataset.richtextProp === richtextProp) { @@ -25,16 +31,29 @@ export function decorateRichtext(container = document) { siblings.push(sibling); } else break; } - const orphanElements = document.querySelectorAll(`[data-richtext-id="${richtextResource}"][data-richtext-prop="${richtextProp}"]`); + + let orphanElements; + if (richtextResource && richtextProp) { + orphanElements = document.querySelectorAll(`[data-richtext-id="${richtextResource}"][data-richtext-prop="${richtextProp}"]`); + } else { + const editable = element.closest('[data-aue-resource]'); + if (editable) { + orphanElements = editable.querySelectorAll(`[data-richtext-prop="${richtextProp}"]`); + } else { + console.warn(`Editable parent not found or richtext property ${richtextProp}`); + return; + } + } + if (orphanElements.length) { - // eslint-disable-next-line no-console console.warn('Found orphan elements of a richtext, that were not consecutive siblings of ' - + 'the first paragraph.', orphanElements); - orphanElements.forEach((el) => deleteInstrumentation(el)); + + 'the first paragraph', orphanElements); + orphanElements.forEach((orphanElement) => deleteInstrumentation(orphanElement)); } else { const group = document.createElement('div'); - group.dataset.aueResource = richtextResource; - group.dataset.aueProp = richtextProp; + if (richtextResource) group.dataset.aueResource = richtextResource; + if (richtextProp) group.dataset.aueProp = richtextProp; + if (richtextLabel) group.dataset.aueLabel = richtextLabel; if (richtextFilter) group.dataset.aueFilter = richtextFilter; group.dataset.aueBehavior = 'component'; group.dataset.aueType = 'richtext'; diff --git a/scripts/editor-support.js b/scripts/editor-support.js index 6b6856649..4bdae906f 100644 --- a/scripts/editor-support.js +++ b/scripts/editor-support.js @@ -50,8 +50,8 @@ async function applyChanges(event) { block.insertAdjacentElement('afterend', newBlock); decorateButtons(newBlock); decorateIcons(newBlock); - decorateRichtext(newBlock); decorateBlock(newBlock); + decorateRichtext(newBlock); await loadBlock(newBlock); block.remove(); newBlock.style.display = null; diff --git a/scripts/scripts.js b/scripts/scripts.js index 9614b3256..e42988cd9 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -14,6 +14,40 @@ import { const LCP_BLOCKS = []; // add your LCP blocks to the list +/** + * Moves all the attributes from a given elmenet to another given element. + * @param {Element} from the element to copy attributes from + * @param {Element} to the element to copy attributes to + */ +export function moveAttributes(from, to, attributes) { + if (!attributes) { + // eslint-disable-next-line no-param-reassign + attributes = [...from.attributes].map(({ nodeName }) => nodeName); + } + attributes.forEach((attr) => { + const value = from.getAttribute(attr); + if (value) { + to.setAttribute(attr, value); + from.removeAttribute(attr); + } + }); +} + +/** + * Move instrumentation attributes from a given element to another given element. + * @param {Element} from the element to copy attributes from + * @param {Element} to the element to copy attributes to + */ +export function moveInstrumentation(from, to) { + moveAttributes( + from, + to, + [...from.attributes] + .map(({ nodeName }) => nodeName) + .filter((attr) => attr.startsWith('data-aue-') || attr.startsWith('data-richtext-')), + ); +} + /** * load fonts.css and set a session storage flag */