diff --git a/blocks/edit/prose/image-utils.js b/blocks/edit/prose/image-utils.js new file mode 100644 index 00000000..c8a3da2c --- /dev/null +++ b/blocks/edit/prose/image-utils.js @@ -0,0 +1,23 @@ +/** + * Rewrites an image src from an AEM preview/publish host to the DA preview host. + * + * Images stored in DA content may reference *.aem.page (preview) or *.aem.live + * (publish) URLs. These hosts require AEM authentication that the DA editor does + * not carry, causing 401 errors when the browser fetches them. The equivalent + * *.preview.da.live URL serves the same content and is accessible from the editor. + * + * @param {string} src - The original image src URL. + * @returns {string} The rewritten src, or the original if no rewrite was needed. + */ +export function rewriteImageSrcForEditor(src) { + try { + const url = new URL(src); + if (url.host.endsWith('.aem.page') || url.host.endsWith('.aem.live')) { + url.host = url.host.replace(/\.aem\.(page|live)$/, '.preview.da.live'); + return url.toString(); + } + } catch { + // relative or malformed src — leave unchanged + } + return src; +} diff --git a/blocks/edit/prose/index.js b/blocks/edit/prose/index.js index ef2b7419..466bdf12 100644 --- a/blocks/edit/prose/index.js +++ b/blocks/edit/prose/index.js @@ -27,6 +27,7 @@ import { COLLAB_ORIGIN, DA_ORIGIN } from '../../shared/constants.js'; import { daFetch, getAuthToken } from '../../shared/utils.js'; import { getDiffClass, checkForLocNodes, addActiveView } from './diff/diff-utils.js'; import { debounce, initDaMetadata } from '../utils/helpers.js'; +import { rewriteImageSrcForEditor } from './image-utils.js'; async function checkDoc(path) { return daFetch(path, { method: 'HEAD' }); @@ -469,6 +470,20 @@ export default async function initProse({ path, permissions, doc, daContent, wsP state, dispatchTransaction, nodeViews: { + image(node) { + const img = document.createElement('img'); + img.src = rewriteImageSrcForEditor(node.attrs.src); + if (node.attrs.alt) img.alt = node.attrs.alt; + return { + dom: img, + update(updated) { + if (updated.type.name !== 'image') return false; + img.src = rewriteImageSrcForEditor(updated.attrs.src); + if (updated.attrs.alt) img.alt = updated.attrs.alt; + return true; + }, + }; + }, diff_added(node, view, getPos) { const LocAddedView = getDiffClass('da-diff-added', getSchema, dispatchTransaction, { isUpstream: false }); return new LocAddedView(node, view, getPos); diff --git a/blocks/edit/prose/plugins/imageFocalPoint.js b/blocks/edit/prose/plugins/imageFocalPoint.js index b7c04626..547a2d5a 100644 --- a/blocks/edit/prose/plugins/imageFocalPoint.js +++ b/blocks/edit/prose/plugins/imageFocalPoint.js @@ -1,4 +1,5 @@ import { Plugin, PluginKey } from 'da-y-wrapper'; +import { rewriteImageSrcForEditor } from '../image-utils.js'; import inlinesvg from '../../../shared/inlinesvg.js'; import { openFocalPointDialog } from './focalPointDialog.js'; import { loadLibrary } from '../../da-library/helpers/helpers.js'; @@ -40,7 +41,7 @@ function shouldShowFocalPoint(tableName, blocks) { } function updateImageAttributes(img, attrs) { - img.src = attrs.src; + img.src = rewriteImageSrcForEditor(attrs.src); ['alt', 'title', 'width', 'height'].forEach((attr) => { if (attrs[attr]) { img[attr] = attrs[attr]; @@ -150,7 +151,16 @@ export default function imageFocalPoint() { if (isInTableCell(view.state, getPos())) { return new ImageWithFocalPointView(node, view, getPos); } - return null; + const img = document.createElement('img'); + updateImageAttributes(img, node.attrs); + return { + dom: img, + update(updated) { + if (updated.type.name !== 'image') return false; + updateImageAttributes(img, updated.attrs); + return true; + }, + }; }, }, }, diff --git a/test/unit/blocks/edit/prose/plugins/imageFocalPoint.test.js b/test/unit/blocks/edit/prose/plugins/imageFocalPoint.test.js index a72d5af4..00e3d9c9 100644 --- a/test/unit/blocks/edit/prose/plugins/imageFocalPoint.test.js +++ b/test/unit/blocks/edit/prose/plugins/imageFocalPoint.test.js @@ -128,7 +128,7 @@ describe('imageFocalPoint Plugin', () => { expect(icon.classList.contains('focal-point-icon-active')).to.be.true; }); - it('does not create node view for images outside table cells', () => { + it('creates a plain image node view for images outside table cells', () => { const plugin = imageFocalPoint(); const createNodeView = plugin.props.nodeViews.image; @@ -144,8 +144,9 @@ describe('imageFocalPoint Plugin', () => { const mockView = { state: mockState, dom: document.createElement('div') }; const getPos = () => 10; - const nodeView = createNodeView({}, mockView, getPos); - expect(nodeView).to.be.null; + const nodeView = createNodeView({ attrs: { src: 'https://example.com/img.jpg' } }, mockView, getPos); + expect(nodeView).to.not.be.null; + expect(nodeView.dom.tagName).to.equal('IMG'); }); it('updates node view correctly', async () => {