diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js index 465f75624ad444a5b9044962a27ffadc3f0dc7bc..f7deabb538504ae4c83c3790751350256125ad6e 100644 --- a/app/assets/javascripts/content_editor/extensions/image.js +++ b/app/assets/javascripts/content_editor/extensions/image.js @@ -2,6 +2,7 @@ import { Image } from '@tiptap/extension-image'; import { VueNodeViewRenderer } from '@tiptap/vue-2'; import { PARSE_HTML_PRIORITY_HIGH } from '../constants'; import ImageWrapper from '../components/wrappers/image.vue'; +import { getSourceMapAttributes } from '../services/markdown_sourcemap'; const resolveImageEl = (element) => element.nodeName === 'IMG' ? element : element.querySelector('img'); @@ -76,6 +77,7 @@ export default Image.extend({ default: false, renderHTML: () => '', }, + ...getSourceMapAttributes(resolveImageEl), }; }, parseHTML() { diff --git a/app/assets/javascripts/content_editor/extensions/playable.js b/app/assets/javascripts/content_editor/extensions/playable.js index 3ac44c28f7e92c53ab28340755499061c29843a4..24c2d6e64ee4854b4650795a2cbbc3868aef5025 100644 --- a/app/assets/javascripts/content_editor/extensions/playable.js +++ b/app/assets/javascripts/content_editor/extensions/playable.js @@ -1,6 +1,7 @@ import { Node } from '@tiptap/core'; import { VueNodeViewRenderer } from '@tiptap/vue-2'; import PlayableWrapper from '../components/wrappers/playable.vue'; +import { getSourceMapAttributes } from '../services/markdown_sourcemap'; const queryPlayableElement = (element, mediaType) => element.querySelector(mediaType); @@ -37,6 +38,7 @@ export default Node.create({ parseHTML: (element) => queryPlayableElement(element, this.options.mediaType).getAttribute('height'), }, + ...getSourceMapAttributes((element) => queryPlayableElement(element, this.options.mediaType)), }; }, diff --git a/app/assets/javascripts/content_editor/extensions/sourcemap.js b/app/assets/javascripts/content_editor/extensions/sourcemap.js index 8ac4a7d07200fde1545be44d40a52c27f79f2b76..1eb0efb2b0dd24dd8d24429b883cbadff73f3ca1 100644 --- a/app/assets/javascripts/content_editor/extensions/sourcemap.js +++ b/app/assets/javascripts/content_editor/extensions/sourcemap.js @@ -1,5 +1,5 @@ import { Extension } from '@tiptap/core'; -import { getMarkdownSource, docHasSourceMap } from '../services/markdown_sourcemap'; +import { getSourceMapAttributes } from '../services/markdown_sourcemap'; import Audio from './audio'; import Blockquote from './blockquote'; import Bold from './bold'; @@ -37,8 +37,6 @@ export default Extension.create({ name: 'sourcemap', addGlobalAttributes() { - const preserveMarkdown = () => gon.features?.preserveMarkdown; - return [ { types: [ @@ -75,30 +73,7 @@ export default Extension.create({ Video.name, ...HTMLNodes.map((htmlNode) => htmlNode.name), ], - attributes: { - /** - * The reason to add a function that returns an empty - * string in these attributes is indicate that these - * attributes shouldn’t be rendered in the ProseMirror - * view. - */ - sourceMarkdown: { - default: null, - parseHTML: (element) => (preserveMarkdown() ? getMarkdownSource(element) : null), - renderHTML: () => '', - }, - sourceMapKey: { - default: null, - parseHTML: (element) => (preserveMarkdown() ? element.dataset.sourcepos : null), - renderHTML: () => '', - }, - sourceTagName: { - default: null, - parseHTML: (element) => - preserveMarkdown() && docHasSourceMap(element) ? element.tagName.toLowerCase() : null, - renderHTML: () => '', - }, - }, + attributes: getSourceMapAttributes(), }, ]; }, diff --git a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js index d7b0d48c54f190aaad2788163264d131a497c203..b2754dc4f4fcaa3e69feea7f95285838715b76d4 100644 --- a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js +++ b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js @@ -1,3 +1,7 @@ +import { identity } from 'lodash'; + +const preserveMarkdown = () => gon.features?.preserveMarkdown; + export const docHasSourceMap = (element) => { const commentNode = element.ownerDocument.body.lastChild; return Boolean(commentNode?.nodeName === '#comment' && commentNode.textContent); @@ -24,6 +28,15 @@ const getRangeFromSourcePos = (sourcePos) => { }; }; +const getMarkdownSourceKey = (element) => { + return element.dataset.sourcepos; +}; + +const getSourceTagName = (element) => { + if (!docHasSourceMap(element)) return undefined; + return element.tagName.toLowerCase(); +}; + export const getMarkdownSource = (element) => { if (!element.dataset.sourcepos) return undefined; @@ -51,3 +64,23 @@ export const getMarkdownSource = (element) => { return undefined; } }; + +export const getSourceMapAttributes = (queryElement = identity) => { + return { + sourceMarkdown: { + default: null, + parseHTML: (el) => (preserveMarkdown() ? getMarkdownSource(queryElement(el)) : null), + renderHTML: () => '', + }, + sourceMapKey: { + default: null, + parseHTML: (el) => (preserveMarkdown() ? getMarkdownSourceKey(queryElement(el)) : null), + renderHTML: () => '', + }, + sourceTagName: { + default: null, + parseHTML: (el) => (preserveMarkdown() ? getSourceTagName(queryElement(el)) : null), + renderHTML: () => '', + }, + }; +}; diff --git a/app/assets/javascripts/content_editor/services/serializer/image.js b/app/assets/javascripts/content_editor/services/serializer/image.js index de809d0bf2e3b1f411b89cabf7bd14de71d1c74d..e0b5deab38b403d90249a3479dd254edbce0c91b 100644 --- a/app/assets/javascripts/content_editor/services/serializer/image.js +++ b/app/assets/javascripts/content_editor/services/serializer/image.js @@ -17,7 +17,7 @@ const image = preserveUnchanged({ if (realSrc.startsWith('data:') || realSrc.startsWith('blob:')) return; if (realSrc) { - if (sourceTagName && !sourceMarkdown) { + if (sourceTagName === 'img' && !sourceMarkdown) { const attrs = pickBy({ alt, title, width, height }, identity); state.write(openTag(sourceTagName, { src: realSrc, ...attrs })); return; diff --git a/spec/frontend/content_editor/services/serializer/image_spec.js b/spec/frontend/content_editor/services/serializer/image_spec.js index d20f32717ed0b7c7ef1e4777fd9c42f5cfd49568..32310ff265d6ceeee94256f45b2f121ffb0476f5 100644 --- a/spec/frontend/content_editor/services/serializer/image_spec.js +++ b/spec/frontend/content_editor/services/serializer/image_spec.js @@ -111,3 +111,17 @@ it('serializes image as an HTML tag if sourceTagName is defined', () => { '<img src="img.jpg" alt="image" title="image title">', ); }); + +it('does not serialize image as HTML if sourceTagName is not img', () => { + const imageAttrs = { src: 'img.jpg', alt: 'image', ...sourceTag('a') }; + + expect(serialize(paragraph(image(imageAttrs)))).toBe(''); + + expect(serialize(paragraph(image({ ...imageAttrs, width: 300, height: 300 })))).toBe( + '{width=300 height=300}', + ); + + expect(serialize(paragraph(image({ ...imageAttrs, title: 'image title' })))).toBe( + '', + ); +}); diff --git a/spec/support/helpers/rich_text_editor_helpers.rb b/spec/support/helpers/rich_text_editor_helpers.rb index 22cf169f6e099317e0c26321355b6d224d8a249c..8ebade8b1c27692437405c169018d974381b74d8 100644 --- a/spec/support/helpers/rich_text_editor_helpers.rb +++ b/spec/support/helpers/rich_text_editor_helpers.rb @@ -57,4 +57,22 @@ def click_edit_diagram_button def expect_drawio_editor_is_opened expect(page).to have_css('#drawio-frame', visible: :hidden) end + + def drag_element(element, dx, dy) + page.execute_script(<<-JS, element, dx, dy) + function simulateDragDrop(element, dx, dy) { + const rect = element.getBoundingClientRect(); + const events = ['mousedown', 'mousemove', 'mouseup']; + events.forEach((eventType, index) => { + const event = new MouseEvent(eventType, { + bubbles: true, + screenX: rect.left + (index ? dx : 0), + screenY: rect.top + (index ? dy : 0) + }); + element.dispatchEvent(event); + }); + } + simulateDragDrop(arguments[0], arguments[1], arguments[2]); + JS + end end diff --git a/spec/support/shared_examples/features/rich_text_editor/media_shared_examples.rb b/spec/support/shared_examples/features/rich_text_editor/media_shared_examples.rb index 9e5976d126f94a3e5d1d5aa6a6f5eb74fbdb48ef..3d51eb53dc1f38fdbacaf67477de7f028cf4bd80 100644 --- a/spec/support/shared_examples/features/rich_text_editor/media_shared_examples.rb +++ b/spec/support/shared_examples/features/rich_text_editor/media_shared_examples.rb @@ -24,4 +24,25 @@ expect_media_bubble_menu_to_be_visible end end + + describe 'resizing images' do + it 'renders correctly with an image as initial content after image is resized' do + click_attachment_button + + switch_to_content_editor + display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png' + + within content_editor_testid do + drag_element(find('[data-testid="image-resize-se"]'), -200, -200) + end + + wait_until_hidden_field_is_updated(/width=/) + switch_to_markdown_editor + + textarea_value = page.find('textarea').value + + expect(textarea_value).to start_with(' + expect(textarea_value).to end_with('/dk.png){width=260 height=182}') + end + end end