From 7759f11348009b6adad799dc0bc717e9fc98c569 Mon Sep 17 00:00:00 2001 From: Tushar <72930233+tusharmane-tm@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:56:50 +0530 Subject: [PATCH 1/2] feat: Introduce AEM Universal Editor support for rich text and real-time content updates. --- scripts/editor-support-rte.js | 67 +++++++++++++++++++ scripts/editor-support.js | 119 ++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 scripts/editor-support-rte.js create mode 100644 scripts/editor-support.js diff --git a/scripts/editor-support-rte.js b/scripts/editor-support-rte.js new file mode 100644 index 0000000..dc0187e --- /dev/null +++ b/scripts/editor-support-rte.js @@ -0,0 +1,67 @@ +/* eslint-disable no-console */ +/* 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 + +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; + while (element = container.querySelector('[data-richtext-prop]:not(div)')) { + const { + richtextResource, + richtextProp, + richtextFilter, + richtextLabel, + } = element.dataset; + deleteInstrumentation(element); + const siblings = []; + let sibling = element; + while (sibling = sibling.nextElementSibling) { + if (sibling.dataset.richtextResource === richtextResource + && sibling.dataset.richtextProp === richtextProp) { + deleteInstrumentation(sibling); + siblings.push(sibling); + } else break; + } + + 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(`:scope > :not([data-aue-resource]) [data-richtext-prop="${richtextProp}"]`); + } else { + console.warn(`Editable parent not found or richtext property ${richtextProp}`); + return; + } + } + + if (orphanElements.length) { + console.warn('Found orphan elements of a richtext, that were not consecutive siblings of ' + + 'the first paragraph', orphanElements); + orphanElements.forEach((orphanElement) => deleteInstrumentation(orphanElement)); + } else { + const group = document.createElement('div'); + if (richtextResource) { + group.dataset.aueResource = richtextResource; + group.dataset.aueBehavior = 'component'; + } + if (richtextProp) group.dataset.aueProp = richtextProp; + if (richtextLabel) group.dataset.aueLabel = richtextLabel; + if (richtextFilter) group.dataset.aueFilter = richtextFilter; + group.dataset.aueType = 'richtext'; + element.replaceWith(group); + group.append(element, ...siblings); + } + } +} diff --git a/scripts/editor-support.js b/scripts/editor-support.js new file mode 100644 index 0000000..a2d45f9 --- /dev/null +++ b/scripts/editor-support.js @@ -0,0 +1,119 @@ +import { + decorateBlock, + decorateBlocks, + decorateButtons, + decorateIcons, + decorateSections, + loadBlock, + loadScript, + loadSections, +} from './aem.js'; +import { decorateRichtext } from './editor-support-rte.js'; +import { decorateMain } from './scripts.js'; + +async function applyChanges(event) { + // redecorate default content and blocks on patches (in the properties rail) + const { detail } = event; + + const resource = detail?.request?.target?.resource // update, patch components + || detail?.request?.target?.container?.resource // update, patch, add to sections + || detail?.request?.to?.container?.resource; // move in sections + if (!resource) return false; + const updates = detail?.response?.updates; + if (!updates.length) return false; + const { content } = updates[0]; + if (!content) return false; + + // load dompurify + await loadScript(`${window.hlx.codeBasePath}/scripts/dompurify.min.js`); + + const sanitizedContent = window.DOMPurify.sanitize(content, { USE_PROFILES: { html: true } }); + const parsedUpdate = new DOMParser().parseFromString(sanitizedContent, 'text/html'); + const element = document.querySelector(`[data-aue-resource="${resource}"]`); + + if (element) { + if (element.matches('main')) { + const newMain = parsedUpdate.querySelector(`[data-aue-resource="${resource}"]`); + newMain.style.display = 'none'; + element.insertAdjacentElement('afterend', newMain); + decorateMain(newMain); + decorateRichtext(newMain); + await loadSections(newMain); + element.remove(); + newMain.style.display = null; + // eslint-disable-next-line no-use-before-define + attachEventListners(newMain); + return true; + } + + const block = element.parentElement?.closest('.block[data-aue-resource]') || element?.closest('.block[data-aue-resource]'); + if (block) { + const blockResource = block.getAttribute('data-aue-resource'); + const newBlock = parsedUpdate.querySelector(`[data-aue-resource="${blockResource}"]`); + if (newBlock) { + newBlock.style.display = 'none'; + block.insertAdjacentElement('afterend', newBlock); + decorateButtons(newBlock); + decorateIcons(newBlock); + decorateBlock(newBlock); + decorateRichtext(newBlock); + await loadBlock(newBlock); + block.remove(); + newBlock.style.display = null; + return true; + } + } else { + // sections and default content, may be multiple in the case of richtext + const newElements = parsedUpdate.querySelectorAll(`[data-aue-resource="${resource}"],[data-richtext-resource="${resource}"]`); + if (newElements.length) { + const { parentElement } = element; + if (element.matches('.section')) { + const [newSection] = newElements; + newSection.style.display = 'none'; + element.insertAdjacentElement('afterend', newSection); + decorateButtons(newSection); + decorateIcons(newSection); + decorateRichtext(newSection); + decorateSections(parentElement); + decorateBlocks(parentElement); + await loadSections(parentElement); + element.remove(); + newSection.style.display = null; + } else { + element.replaceWith(...newElements); + decorateButtons(parentElement); + decorateIcons(parentElement); + decorateRichtext(parentElement); + } + return true; + } + } + } + + return false; +} + +function attachEventListners(main) { + [ + 'aue:content-patch', + 'aue:content-update', + 'aue:content-add', + 'aue:content-move', + 'aue:content-remove', + 'aue:content-copy', + ].forEach((eventType) => main?.addEventListener(eventType, async (event) => { + event.stopPropagation(); + const applied = await applyChanges(event); + if (!applied) window.location.reload(); + })); +} + +attachEventListners(document.querySelector('main')); + +// decorate rich text +// this has to happen after decorateMain(), and everythime decorateBlocks() is called +decorateRichtext(); +// in cases where the block decoration is not done in one synchronous iteration we need to listen +// for new richtext-instrumented elements. this happens for example when using experimentation. +const observer = new MutationObserver(() => decorateRichtext()); +observer.observe(document, { attributeFilter: ['data-richtext-prop'], subtree: true }); From 8b3c15a6eb23a5364984cb14efe9fd2a82e86d5c Mon Sep 17 00:00:00 2001 From: Tushar <72930233+tusharmane-tm@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:23:26 +0530 Subject: [PATCH 2/2] feat: Add distinct `dev` scripts for DA, GDrive, and UE AEM environments. --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 399daaa..fdf8ea8 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "version": "1.3.0", "description": "Starter project for Adobe Helix", "scripts": { - "dev": "aem up --url=https://main--acme-services-gdrive--initialyze.aem.page/", + "dev:da": "aem up --url=https://main--acme-services--initialyze.aem.page/", + "dev:gdrive": "aem up --url=https://main--acme-services-gdrive--initialyze.aem.page/", + "dev:ue": "aem up --url=https://main--acme-services-ue--initialyze.aem.page/", "lint:js": "eslint .", "lint:css": "stylelint \"blocks/**/*.css\" \"styles/*.css\"", "lint": "npm run lint:js && npm run lint:css",