@@ -6,7 +6,7 @@ import { getOptionalPlugin, reportInitEnd, reportInitStart } from "@coremedia/ck
66import type { Logger } from "@coremedia/ckeditor5-logging" ;
77import { LoggerProvider } from "@coremedia/ckeditor5-logging" ;
88import ModelBoundSubscriptionPlugin from "./ModelBoundSubscriptionPlugin" ;
9- import { editingDowncastXlinkHref , preventUpcastImageSrc } from "./converters" ;
9+ import { editingDowncastXlinkHref , preventUpcastImageSrc , upcastContentImageAsInline } from "./converters" ;
1010import {
1111 openImageInTabCommandName ,
1212 registerOpenImageInTabCommand ,
@@ -62,6 +62,20 @@ export default class ContentImageEditingPlugin extends Plugin {
6262 // src attribute for the editing view asynchronously.
6363 // If not prevented, the src-attribute from GRS would be written to the model.
6464 this . editor . conversion . for ( "upcast" ) . add ( preventUpcastImageSrc ( ) ) ;
65+
66+ // Force content images to become imageInline when dropped inside a
67+ // paragraph (inline context). This prevents ImageBlockEditing from
68+ // splitting the host paragraph. Runs at high priority, before
69+ // ImageBlockEditing's normal-priority converter.
70+ this . editor . conversion . for ( "upcast" ) . add ( upcastContentImageAsInline ( ) ) ;
71+
72+ // When a content image is dragged and dropped between block elements,
73+ // CKEditor's image upcasting may create imageBlock instead of imageInline
74+ // (because the drop target is a block-level position). The post-fixer
75+ // registered here converts any such imageBlock back to a paragraph
76+ // containing an imageInline, which is the correct model representation
77+ // for CoreMedia content images.
78+ ContentImageEditingPlugin . #setupImageBlockToInlinePostFixer( this . editor ) ;
6579 }
6680
6781 static #setupXlinkHrefConversion( editor : Editor , modelAttributeName : string , dataAttributeName : string ) : void {
@@ -103,6 +117,86 @@ export default class ContentImageEditingPlugin extends Plugin {
103117 . add ( editingDowncastXlinkHref ( editor , modelElementName , ContentImageEditingPlugin . #logger) ) ;
104118 }
105119
120+ /**
121+ * Ensures that content images dropped between block elements remain as
122+ * `imageInline` in the model.
123+ *
124+ * When dragging a content image and dropping it between two paragraphs,
125+ * CKEditor's image upcast pipeline detects a block-level drop position and
126+ * creates an `imageBlock` model element instead of `imageInline`. Because
127+ * `xlink-href` was not allowed on `imageBlock`, the attribute was silently
128+ * dropped and GHS captured `data-xlink-href` into `htmlImgAttributes`,
129+ * making the image disappear from the editing view.
130+ *
131+ * This method:
132+ * 1. Extends the `imageBlock` schema to allow `xlink-href` so that the
133+ * existing `attributeToAttribute` upcast converter sets the attribute
134+ * correctly (and GHS no longer captures it).
135+ * 2. Registers a model post-fixer that converts every `imageBlock` carrying
136+ * a `xlink-href` attribute into a `<paragraph>` containing an
137+ * `imageInline`, which is the correct CoreMedia RichText representation.
138+ *
139+ * @param editor - Editor instance
140+ */
141+ static #setupImageBlockToInlinePostFixer( editor : Editor ) : void {
142+ const { model } = editor ;
143+ const { schema } = model ;
144+
145+ if ( ! schema . isRegistered ( "imageBlock" ) ) {
146+ return ;
147+ }
148+
149+ // Allow xlink-href on imageBlock so the upcast can set the attribute
150+ // (preventing GHS from capturing data-xlink-href into htmlImgAttributes).
151+ schema . extend ( "imageBlock" , {
152+ allowAttributes : [ ContentImageEditingPlugin . XLINK_HREF_MODEL_ATTRIBUTE_NAME ] ,
153+ } ) ;
154+
155+ const xlinkHrefAttr = ContentImageEditingPlugin . XLINK_HREF_MODEL_ATTRIBUTE_NAME ;
156+ const imageInlineName = ContentImageEditingPlugin . IMAGE_INLINE_MODEL_ELEMENT_NAME ;
157+
158+ model . document . registerPostFixer ( ( writer ) => {
159+ let changed = false ;
160+
161+ for ( const change of model . document . differ . getChanges ( ) ) {
162+ if ( change . type === "insert" && change . name === "imageBlock" ) {
163+ const item = change . position . nodeAfter ;
164+ if ( ! item || ! item . is ( "element" , "imageBlock" ) ) {
165+ continue ;
166+ }
167+
168+ const xlinkHref = item . getAttribute ( xlinkHrefAttr ) ;
169+ if ( ! xlinkHref ) {
170+ continue ;
171+ }
172+
173+ // Replace the imageBlock with a paragraph containing an imageInline.
174+ const paragraph = writer . createElement ( "paragraph" ) ;
175+ const imageInline = writer . createElement ( imageInlineName , {
176+ [ xlinkHrefAttr ] : xlinkHref ,
177+ } ) ;
178+ writer . append ( imageInline , paragraph ) ;
179+ writer . insert ( paragraph , item , "before" ) ;
180+ writer . remove ( item ) ;
181+ changed = true ;
182+ }
183+
184+ // When a content image is moved (drag-and-drop) out of a paragraph
185+ // that contained only that image, the source paragraph becomes empty.
186+ // CKEditor does not remove it automatically, so we clean it up here.
187+ if ( change . type === "remove" && change . name === "imageInline" ) {
188+ const parent = change . position . parent ;
189+ if ( parent . is ( "element" , "paragraph" ) && parent . childCount === 0 && parent . parent !== null ) {
190+ writer . remove ( parent ) ;
191+ changed = true ;
192+ }
193+ }
194+ }
195+
196+ return changed ;
197+ } ) ;
198+ }
199+
106200 /**
107201 * Register `imageInline` model elements for subscription cleanup
108202 * on model changes.
0 commit comments