diff --git a/blocks/edit/da-editor/da-editor.css b/blocks/edit/da-editor/da-editor.css index e9b00846..2af32b3a 100644 --- a/blocks/edit/da-editor/da-editor.css +++ b/blocks/edit/da-editor/da-editor.css @@ -554,6 +554,10 @@ table tr:first-of-type p:first-child { pointer-events: none; } +.ProseMirror img[href] { + border: #00ee 3px solid; +} + /* Palette */ .da-palettes { position: fixed; diff --git a/blocks/edit/prose/index.js b/blocks/edit/prose/index.js index 9900bddf..3886feb6 100644 --- a/blocks/edit/prose/index.js +++ b/blocks/edit/prose/index.js @@ -55,13 +55,45 @@ function addCustomMarks(marks) { .addToEnd('contextHighlightingMark', contextHighlight); } +function getImageNodeWithHref() { + // due to bug in y-prosemirror, add href to image node + // which will be converted to a wrapping tag + return { + inline: true, + attrs: { + src: { validate: 'string' }, + alt: { default: null, validate: 'string|null' }, + title: { default: null, validate: 'string|null' }, + href: { default: null, validate: 'string|null' }, + }, + group: 'inline', + draggable: true, + parseDOM: [{ + tag: 'img[src]', + getAttrs(dom) { + return { + src: dom.getAttribute('src'), + title: dom.getAttribute('title'), + alt: dom.getAttribute('alt'), + href: dom.getAttribute('href'), + }; + }, + }], + toDOM(node) { + const { src, alt, title, href } = node.attrs; + return ['img', { src, alt, title, href }]; + }, + }; +} + // Note: until getSchema() is separated in its own module, this function needs to be kept in-sync // with the getSchema() function in da-collab src/collab.js export function getSchema() { const { marks, nodes: baseNodes } = baseSchema.spec; const withLocNodes = addLocNodes(baseNodes); const withListnodes = addListNodes(withLocNodes, 'block+', 'block'); - const nodes = withListnodes.append(tableNodes({ tableGroup: 'block', cellContent: 'block+' })); + const withTableNodes = withListnodes.append(tableNodes({ tableGroup: 'block', cellContent: 'block+' })); + const nodes = withTableNodes.update('image', getImageNodeWithHref()); return new Schema({ nodes, marks: addCustomMarks(marks) }); } diff --git a/blocks/edit/prose/plugins/menu.js b/blocks/edit/prose/plugins/menu.js index 4789fcf8..b75c7948 100644 --- a/blocks/edit/prose/plugins/menu.js +++ b/blocks/edit/prose/plugins/menu.js @@ -109,6 +109,11 @@ function calculateLinkPosition(state, link, offset) { }; } +function hasImageNode(contentArr) { + if (!contentArr) return false; + return contentArr.some((node) => node.type.name === 'image'); +} + function linkItem(linkMarkType) { const label = 'Link'; @@ -120,8 +125,10 @@ function linkItem(linkMarkType) { class: 'edit-link', active(state) { return markActive(state, linkMarkType); }, enable(state) { - return state.selection.content().content.childCount <= 1 - && (!state.selection.empty || this.active(state)); + const selContent = state.selection.content(); + return selContent.content.childCount <= 1 + && (!state.selection.empty || this.active(state)) + && !hasImageNode(selContent.content?.content[0]?.content?.content); }, run(initialState, dispatch, view) { if (lastPrompt.isOpen()) { @@ -158,17 +165,35 @@ function linkItem(linkMarkType) { end = $to.pos; } + let isImage = false; + view.state.doc.nodesBetween($from.pos, $to.pos, (node) => { + if (node.type === view.state.schema.nodes.image) { + isImage = true; + fields.href.value = node.attrs.href; + fields.title.value = node.attrs.title; + } + }); + dispatch(view.state.tr .addMark(start, end, view.state.schema.marks.contextHighlightingMark.create({})) .setMeta('addToHistory', false)); const callback = (attrs) => { - const tr = view.state.tr - .setSelection(TextSelection.create(view.state.doc, start, end)); - if (fields.href.value) { - dispatch(tr.addMark(start, end, linkMarkType.create(attrs))); - } else if (this.active(view.state)) { - dispatch(tr.removeMark(start, end, linkMarkType)); + if (isImage) { + if (fields.href.value) { + dispatch(view.state.tr.setNodeAttribute(start, 'href', fields.href.value.trim())); + } + if (fields.title.value) { + dispatch(view.state.tr.setNodeAttribute(start, 'title', fields.title.value.trim())); + } + } else { + const tr = view.state.tr + .setSelection(TextSelection.create(view.state.doc, start, end)); + if (fields.href.value) { + dispatch(tr.addMark(start, end, linkMarkType.create(attrs))); + } else if (this.active(view.state)) { + dispatch(tr.removeMark(start, end, linkMarkType)); + } } view.focus(); @@ -187,15 +212,44 @@ function removeLinkItem(linkMarkType) { title: 'Remove link', label: 'Remove Link', class: 'edit-unlink', - active(state) { return markActive(state, linkMarkType); }, + isImage: false, + active(state) { + this.isImage = false; + const { $from, $to } = state.selection; + let imgHasAttrs = false; + state.doc.nodesBetween($from.pos, $to.pos, (node) => { + if (node.type === state.schema.nodes.image) { + this.isImage = true; + if (node.attrs.href || node.attrs.title) { + imgHasAttrs = true; + } else { + imgHasAttrs = false; + } + } + }); + if (this.isImage) { + if (($to.pos - $from.pos) <= 1) { + return imgHasAttrs; + } + // selection is more than just an image + return false; + } + + return markActive(state, linkMarkType); + }, enable(state) { return this.active(state); }, // eslint-disable-next-line no-unused-vars run(state, dispatch, _view) { - const { link, offset } = findExistingLink(state, linkMarkType); - const { start, end } = calculateLinkPosition(state, link, offset); - const tr = state.tr.setSelection(TextSelection.create(state.doc, start, end)) - .removeMark(start, end, linkMarkType); - dispatch(tr); + if (this.isImage) { + const { $from } = state.selection; + dispatch(state.tr.setNodeAttribute($from.pos, 'href', null).setNodeAttribute($from.pos, 'title', null)); + } else { + const { link, offset } = findExistingLink(state, linkMarkType); + const { start, end } = calculateLinkPosition(state, link, offset); + const tr = state.tr.setSelection(TextSelection.create(state.doc, start, end)) + .removeMark(start, end, linkMarkType); + dispatch(tr); + } }, }); } diff --git a/blocks/shared/prose2aem.js b/blocks/shared/prose2aem.js index 65630f7b..e78a63b1 100644 --- a/blocks/shared/prose2aem.js +++ b/blocks/shared/prose2aem.js @@ -55,7 +55,7 @@ function makePictures(editor) { const clone = img.cloneNode(true); clone.setAttribute('loading', 'lazy'); - const pic = document.createElement('picture'); + let pic = document.createElement('picture'); const srcMobile = document.createElement('source'); srcMobile.srcset = clone.src; @@ -66,6 +66,18 @@ function makePictures(editor) { pic.append(srcMobile, srcTablet, clone); + const hrefAttr = img.getAttribute('href'); + if (hrefAttr) { + const a = document.createElement('a'); + a.href = hrefAttr; + const titleAttr = img.getAttribute('title'); + if (titleAttr) { + a.title = titleAttr; + } + a.append(pic); + pic = a; + } + // Determine what to replace const imgParent = img.parentElement; const imgGrandparent = imgParent.parentElement; diff --git a/test/e2e/tests/browse.spec.js b/test/e2e/tests/browse.spec.js index bf8c858a..e6d01ad9 100644 --- a/test/e2e/tests/browse.spec.js +++ b/test/e2e/tests/browse.spec.js @@ -3,10 +3,9 @@ import ENV from '../utils/env.js'; test('Get Main Page', async ({ page }) => { await page.goto(ENV); - const html = await page.content(); - expect(html).toContain('Dark Alley'); + expect(html).toContain('Browse - DA'); await expect(page.locator('a.nx-nav-brand')).toBeVisible(); - await expect(page.locator('a.nx-nav-brand')).toContainText('Project Dark Alley'); + await expect(page.locator('a.nx-nav-brand')).toContainText('Document Authoring'); }); diff --git a/test/unit/blocks/shared/mocks/prose2aem.html b/test/unit/blocks/shared/mocks/prose2aem.html index 0bfebac2..b6281b60 100644 --- a/test/unit/blocks/shared/mocks/prose2aem.html +++ b/test/unit/blocks/shared/mocks/prose2aem.html @@ -52,3 +52,7 @@ + +
+ +
diff --git a/test/unit/blocks/shared/prose2aem.test.js b/test/unit/blocks/shared/prose2aem.test.js index 395b7997..4cd1c5bd 100644 --- a/test/unit/blocks/shared/prose2aem.test.js +++ b/test/unit/blocks/shared/prose2aem.test.js @@ -53,4 +53,10 @@ describe('aem2prose', () => { const meta = document.querySelector('.metadata'); expect(meta).to.not.exist; }); + + it('Wraps imgs with href attrs in a link tag', () => { + const pictureEl = document.querySelector('a > picture'); + const parent = pictureEl.parentElement; + expect(parent.href).to.equal('https://my.image.link/'); + }); });