Commit 4ad6f750 authored by Ben Mullins's avatar Ben Mullins
Browse files

Issue #3269868 by lauriii, ravi.shankar, andregp, Wim Leers: [drupalImage]...

Issue #3269868 by lauriii, ravi.shankar, andregp, Wim Leers: [drupalImage] Some Image attributes are lost in edge cases where image upcasts into inline image
parent a3822199
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.drupalImage=e())}(self,(function(){return(()=>{var t={"ckeditor5/src/core.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/upload.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/upload.js")},"ckeditor5/src/utils.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"dll-reference CKEditor5.dll":t=>{"use strict";t.exports=CKEditor5.dll}},e={};function i(r){var n=e[r];if(void 0!==n)return n.exports;var a=e[r]={exports:{}};return t[r](a,a.exports,i),a.exports}i.d=(t,e)=>{for(var r in e)i.o(e,r)&&!i.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var r={};return(()=>{"use strict";i.d(r,{default:()=>h});var t=i("ckeditor5/src/core.js");function e(t){return t.createEmptyElement("img")}function n(t){const e=parseFloat(t);return!Number.isNaN(e)&&t===String(e)}function a(){function t(t,e,i){if(!i.consumable.consume(e.item,t.name))return;const r=i.mapper.toViewElement(e.item),n=i.writer,a=n.createContainerElement("a",{href:e.attributeNewValue});n.insert(n.createPositionBefore(r),a),n.move(n.createRangeOn(r),n.createPositionAt(a,0)),i.consumable.consume(e.item,"attribute:htmlLinkAttributes:imageBlock")&&function(t,e,i){if(e.attributes)for(const[r,n]of Object.entries(e.attributes))t.setAttribute(r,n,i);e.styles&&t.setStyle(e.styles,i),e.classes&&t.addClass(e.classes,i)}(i.writer,e.item.getAttribute("htmlLinkAttributes"),a)}return e=>{e.on("attribute:linkHref:imageBlock",t,{priority:"high"})}}class o extends t.Plugin{static get requires(){return["ImageUtils"]}static get pluginName(){return"DrupalImageEditing"}init(){const{editor:t}=this,{conversion:i}=t,{schema:r}=t.model;r.isRegistered("imageInline")&&r.extend("imageInline",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),r.isRegistered("imageBlock")&&r.extend("imageBlock",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),i.for("upcast").add(function(t){function e(e,i,r){const{viewItem:n}=i,{writer:a,consumable:o,safeInsert:s,updateConversionResult:u,schema:l}=r,d=[];let c;if(o.test(n,{name:!0,attributes:"src"})){if(c=l.checkChild(i.modelCursor,"imageInline")?a.createElement("imageInline",{src:n.getAttribute("src")}):a.createElement("imageBlock",{src:n.getAttribute("src")}),t.plugins.has("ImageStyleEditing")&&o.test(n,{name:!0,attributes:"data-align"})){const t={left:"alignBlockLeft",center:"alignCenter",right:"alignBlockRight"},e={left:"alignLeft",right:"alignRight"},i=n.getAttribute("data-align"),r=c.is("element","imageBlock")?t[i]:e[i];a.setAttribute("imageStyle",r,c),d.push("data-align")}if(c.is("element","imageBlock")&&o.test(n,{name:!0,attributes:"data-caption"})){const e=a.createElement("caption"),i=t.data.processor.toView(n.getAttribute("data-caption")),o=a.createDocumentFragment();r.consumable.constructor.createFrom(i,r.consumable),r.convertChildren(i,o);for(const t of Array.from(o.getChildren()))a.append(t,e);a.append(e,c),d.push("data-caption")}o.test(n,{name:!0,attributes:"data-entity-uuid"})&&(a.setAttribute("dataEntityUuid",n.getAttribute("data-entity-uuid"),c),d.push("data-entity-uuid")),o.test(n,{name:!0,attributes:"data-entity-type"})&&(a.setAttribute("dataEntityType",n.getAttribute("data-entity-type"),c),d.push("data-entity-type")),s(c,i.modelCursor)&&(o.consume(n,{name:!0,attributes:d}),u(c,i))}}return t=>{t.on("element:img",e,{priority:"high"})}}(t)).attributeToAttribute({view:{name:"img",key:"width"},model:{key:"width",value:t=>n(t.getAttribute("width"))?`${t.getAttribute("width")}px`:`${t.getAttribute("width")}`}}).attributeToAttribute({view:{name:"img",key:"height"},model:{key:"height",value:t=>n(t.getAttribute("height"))?`${t.getAttribute("height")}px`:`${t.getAttribute("height")}`}}),i.for("downcast").add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-uuid",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityUuid",t)}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-type",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityType",t)}}()),i.for("dataDowncast").add(function(t){return e=>{e.on("insert:caption",((e,i,r)=>{const{consumable:n,writer:a,mapper:o}=r;if(!t.plugins.get("ImageUtils").isImage(i.item.parent)||!n.consume(i.item,"insert"))return;const s=t.model.createRangeIn(i.item),u=a.createDocumentFragment();o.bindElements(i.item,u);for(const{item:e}of Array.from(s)){const i={item:e,range:t.model.createRangeOn(e)},n=`insert:${e.name||"$text"}`;t.data.downcastDispatcher.fire(n,i,r);for(const n of e.getAttributeKeys())Object.assign(i,{attributeKey:n,attributeOldValue:null,attributeNewValue:i.item.getAttribute(n)}),t.data.downcastDispatcher.fire(`attribute:${n}`,i,r)}for(const t of a.createRangeIn(u).getItems())o.unbindViewElement(t);o.unbindViewElement(u);const l=t.data.processor.toData(u);if(l){const t=o.toViewElement(i.item.parent);a.setAttribute("data-caption",l,t)}}),{priority:"high"})}}(t)).elementToElement({model:"imageBlock",view:(t,{writer:i})=>e(i),converterPriority:"high"}).elementToElement({model:"imageInline",view:(t,{writer:i})=>e(i),converterPriority:"high"}).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i,o={alignLeft:"left",alignRight:"right",alignCenter:"center",alignBlockRight:"right",alignBlockLeft:"left"};if(!o[e.attributeNewValue]||!n.consume(r,t.name))return;const s=i.mapper.toViewElement(r),u=Array.from(s.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-align",o[e.attributeNewValue],u||s)}return e=>{e.on("attribute:imageStyle",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("width",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:width:imageInline",t,{priority:"high"}),e.on("attribute:width:imageBlock",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("height",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:height:imageInline",t,{priority:"high"}),e.on("attribute:height:imageBlock",t,{priority:"high"})}}()).add(a())}}class s extends t.Plugin{static get requires(){return[o]}static get pluginName(){return"DrupalImage"}}const u=s;class l extends t.Plugin{init(){const{editor:t}=this;t.plugins.get("ImageUploadEditing").on("uploadComplete",((e,{data:i,imageElement:r})=>{t.model.change((t=>{t.setAttribute("dataEntityUuid",i.dataEntityUuid,r),t.setAttribute("dataEntityType",i.dataEntityType,r)}))}))}static get pluginName(){return"DrupalImageUploadEditing"}}var d=i("ckeditor5/src/upload.js"),c=i("ckeditor5/src/utils.js");class m{constructor(t,e){this.loader=t,this.options=e}upload(){return this.loader.file.then((t=>new Promise(((e,i)=>{this._initRequest(),this._initListeners(e,i,t),this._sendRequest(t)}))))}abort(){this.xhr&&this.xhr.abort()}_initRequest(){this.xhr=new XMLHttpRequest,this.xhr.open("POST",this.options.uploadUrl,!0),this.xhr.responseType="json"}_initListeners(t,e,i){const r=this.xhr,n=this.loader,a=`Couldn't upload file: ${i.name}.`;r.addEventListener("error",(()=>e(a))),r.addEventListener("abort",(()=>e())),r.addEventListener("load",(()=>{const i=r.response;if(!i||i.error)return e(i&&i.error&&i.error.message?i.error.message:a);t({urls:{default:i.url},dataEntityUuid:i.uuid?i.uuid:"",dataEntityType:i.entity_type?i.entity_type:""})})),r.upload&&r.upload.addEventListener("progress",(t=>{t.lengthComputable&&(n.uploadTotal=t.total,n.uploaded=t.loaded)}))}_sendRequest(t){const e=this.options.headers||{},i=this.options.withCredentials||!1;Object.keys(e).forEach((t=>{this.xhr.setRequestHeader(t,e[t])})),this.xhr.withCredentials=i;const r=new FormData;r.append("upload",t),this.xhr.send(r)}}class g extends t.Plugin{static get requires(){return[d.FileRepository]}static get pluginName(){return"DrupalFileRepository"}init(){const t=this.editor.config.get("drupalImageUpload");t&&(t.uploadUrl?this.editor.plugins.get(d.FileRepository).createUploadAdapter=e=>new m(e,t):(0,c.logWarning)("simple-upload-adapter-missing-uploadurl"))}}class p extends t.Plugin{static get requires(){return[g,l]}static get pluginName(){return"DrupalImageUpload"}}const h={DrupalImage:u,DrupalImageUpload:p}})(),r=r.default})()}));
 No newline at end of file
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.drupalImage=e())}(self,(function(){return(()=>{var t={"ckeditor5/src/core.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/upload.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/upload.js")},"ckeditor5/src/utils.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"dll-reference CKEditor5.dll":t=>{"use strict";t.exports=CKEditor5.dll}},e={};function i(r){var n=e[r];if(void 0!==n)return n.exports;var a=e[r]={exports:{}};return t[r](a,a.exports,i),a.exports}i.d=(t,e)=>{for(var r in e)i.o(e,r)&&!i.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var r={};return(()=>{"use strict";i.d(r,{default:()=>f});var t=i("ckeditor5/src/core.js");function e(t){return t.createEmptyElement("img")}function n(t){const e=parseFloat(t);return!Number.isNaN(e)&&t===String(e)}const a=[{modelValue:"alignCenter",dataValue:"center"},{modelValue:"alignRight",dataValue:"right"},{modelValue:"alignLeft",dataValue:"left"}];function o(){function t(t,e,i){if(!i.consumable.consume(e.item,t.name))return;const r=i.mapper.toViewElement(e.item),n=i.writer,a=n.createContainerElement("a",{href:e.attributeNewValue});n.insert(n.createPositionBefore(r),a),n.move(n.createRangeOn(r),n.createPositionAt(a,0)),i.consumable.consume(e.item,"attribute:htmlLinkAttributes:imageBlock")&&function(t,e,i){if(e.attributes)for(const[r,n]of Object.entries(e.attributes))t.setAttribute(r,n,i);e.styles&&t.setStyle(e.styles,i),e.classes&&t.addClass(e.classes,i)}(i.writer,e.item.getAttribute("htmlLinkAttributes"),a)}return e=>{e.on("attribute:linkHref:imageBlock",t,{priority:"high"})}}class s extends t.Plugin{static get requires(){return["ImageUtils"]}static get pluginName(){return"DrupalImageEditing"}init(){const{editor:t}=this,{conversion:i}=t,{schema:r}=t.model;r.isRegistered("imageInline")&&r.extend("imageInline",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),r.isRegistered("imageBlock")&&r.extend("imageBlock",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),i.for("upcast").add(function(t){function e(e,i,r){const{viewItem:n}=i,{writer:o,consumable:s,safeInsert:u,updateConversionResult:l,schema:d}=r,c=[];let m;if(!s.test(n,{name:!0,attributes:"src"}))return;const p=s.test(n,{name:!0,attributes:"data-caption"});if(m=d.checkChild(i.modelCursor,"imageInline")&&!p?o.createElement("imageInline",{src:n.getAttribute("src")}):o.createElement("imageBlock",{src:n.getAttribute("src")}),t.plugins.has("ImageStyleEditing")&&s.test(n,{name:!0,attributes:"data-align"})){const t=n.getAttribute("data-align"),e=a.find((e=>e.dataValue===t));e&&(o.setAttribute("imageStyle",e.modelValue,m),c.push("data-align"))}if(p){const e=o.createElement("caption"),i=t.data.processor.toView(n.getAttribute("data-caption")),a=o.createDocumentFragment();r.consumable.constructor.createFrom(i,r.consumable),r.convertChildren(i,a);for(const t of Array.from(a.getChildren()))o.append(t,e);o.append(e,m),c.push("data-caption")}s.test(n,{name:!0,attributes:"data-entity-uuid"})&&(o.setAttribute("dataEntityUuid",n.getAttribute("data-entity-uuid"),m),c.push("data-entity-uuid")),s.test(n,{name:!0,attributes:"data-entity-type"})&&(o.setAttribute("dataEntityType",n.getAttribute("data-entity-type"),m),c.push("data-entity-type")),u(m,i.modelCursor)&&(s.consume(n,{name:!0,attributes:c}),l(m,i))}return t=>{t.on("element:img",e,{priority:"high"})}}(t)).attributeToAttribute({view:{name:"img",key:"width"},model:{key:"width",value:t=>n(t.getAttribute("width"))?`${t.getAttribute("width")}px`:`${t.getAttribute("width")}`}}).attributeToAttribute({view:{name:"img",key:"height"},model:{key:"height",value:t=>n(t.getAttribute("height"))?`${t.getAttribute("height")}px`:`${t.getAttribute("height")}`}}),i.for("downcast").add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-uuid",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityUuid",t)}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-type",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityType",t)}}()),i.for("dataDowncast").add(function(t){return e=>{e.on("insert:caption",((e,i,r)=>{const{consumable:n,writer:a,mapper:o}=r;if(!t.plugins.get("ImageUtils").isImage(i.item.parent)||!n.consume(i.item,"insert"))return;const s=t.model.createRangeIn(i.item),u=a.createDocumentFragment();o.bindElements(i.item,u);for(const{item:e}of Array.from(s)){const i={item:e,range:t.model.createRangeOn(e)},n=`insert:${e.name||"$text"}`;t.data.downcastDispatcher.fire(n,i,r);for(const n of e.getAttributeKeys())Object.assign(i,{attributeKey:n,attributeOldValue:null,attributeNewValue:i.item.getAttribute(n)}),t.data.downcastDispatcher.fire(`attribute:${n}`,i,r)}for(const t of a.createRangeIn(u).getItems())o.unbindViewElement(t);o.unbindViewElement(u);const l=t.data.processor.toData(u);if(l){const t=o.toViewElement(i.item.parent);a.setAttribute("data-caption",l,t)}}),{priority:"high"})}}(t)).elementToElement({model:"imageBlock",view:(t,{writer:i})=>e(i),converterPriority:"high"}).elementToElement({model:"imageInline",view:(t,{writer:i})=>e(i),converterPriority:"high"}).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:o}=i,s=a.find((t=>t.modelValue===e.attributeNewValue));if(!s||!n.consume(r,t.name))return;const u=i.mapper.toViewElement(r),l=Array.from(u.getChildren()).find((t=>"img"===t.name));o.setAttribute("data-align",s.dataValue,l||u)}return e=>{e.on("attribute:imageStyle",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("width",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:width:imageInline",t,{priority:"high"}),e.on("attribute:width:imageBlock",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("height",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:height:imageInline",t,{priority:"high"}),e.on("attribute:height:imageBlock",t,{priority:"high"})}}()).add(o())}}class u extends t.Plugin{static get requires(){return[s]}static get pluginName(){return"DrupalImage"}}const l=u;class d extends t.Plugin{init(){const{editor:t}=this;t.plugins.get("ImageUploadEditing").on("uploadComplete",((e,{data:i,imageElement:r})=>{t.model.change((t=>{t.setAttribute("dataEntityUuid",i.dataEntityUuid,r),t.setAttribute("dataEntityType",i.dataEntityType,r)}))}))}static get pluginName(){return"DrupalImageUploadEditing"}}var c=i("ckeditor5/src/upload.js"),m=i("ckeditor5/src/utils.js");class p{constructor(t,e){this.loader=t,this.options=e}upload(){return this.loader.file.then((t=>new Promise(((e,i)=>{this._initRequest(),this._initListeners(e,i,t),this._sendRequest(t)}))))}abort(){this.xhr&&this.xhr.abort()}_initRequest(){this.xhr=new XMLHttpRequest,this.xhr.open("POST",this.options.uploadUrl,!0),this.xhr.responseType="json"}_initListeners(t,e,i){const r=this.xhr,n=this.loader,a=`Couldn't upload file: ${i.name}.`;r.addEventListener("error",(()=>e(a))),r.addEventListener("abort",(()=>e())),r.addEventListener("load",(()=>{const i=r.response;if(!i||i.error)return e(i&&i.error&&i.error.message?i.error.message:a);t({urls:{default:i.url},dataEntityUuid:i.uuid?i.uuid:"",dataEntityType:i.entity_type?i.entity_type:""})})),r.upload&&r.upload.addEventListener("progress",(t=>{t.lengthComputable&&(n.uploadTotal=t.total,n.uploaded=t.loaded)}))}_sendRequest(t){const e=this.options.headers||{},i=this.options.withCredentials||!1;Object.keys(e).forEach((t=>{this.xhr.setRequestHeader(t,e[t])})),this.xhr.withCredentials=i;const r=new FormData;r.append("upload",t),this.xhr.send(r)}}class g extends t.Plugin{static get requires(){return[c.FileRepository]}static get pluginName(){return"DrupalFileRepository"}init(){const t=this.editor.config.get("drupalImageUpload");t&&(t.uploadUrl?this.editor.plugins.get(c.FileRepository).createUploadAdapter=e=>new p(e,t):(0,m.logWarning)("simple-upload-adapter-missing-uploadurl"))}}class h extends t.Plugin{static get requires(){return[g,d]}static get pluginName(){return"DrupalImageUpload"}}const f={DrupalImage:l,DrupalImageUpload:h}})(),r=r.default})()}));
 No newline at end of file
+45 −37
Original line number Diff line number Diff line
@@ -40,6 +40,21 @@ function modelEntityUuidToDataAttribute() {
  };
}

const alignmentMapping = [
  {
    modelValue: 'alignCenter',
    dataValue: 'center',
  },
  {
    modelValue: 'alignRight',
    dataValue: 'right',
  },
  {
    modelValue: 'alignLeft',
    dataValue: 'left',
  },
];

// Downcast `caption` model to `data-caption` attribute with its content
// downcasted to plain HTML. This is needed because CKEditor 5 uses <caption>
// element internally in various places, which differs from Drupal which uses
@@ -158,19 +173,12 @@ function modelImageStyleToDataAttribute() {
    const { item } = data;
    const { consumable, writer } = conversionApi;

    const mapping = {
      alignLeft: 'left',
      alignRight: 'right',
      alignCenter: 'center',
      alignBlockRight: 'right',
      alignBlockLeft: 'left',
    };
    const mappedAlignment = alignmentMapping.find(
      (value) => value.modelValue === data.attributeNewValue,
    );

    // Consume only for the values that can be converted into data-align.
    if (
      !mapping[data.attributeNewValue] ||
      !consumable.consume(item, evt.name)
    ) {
    if (!mappedAlignment || !consumable.consume(item, evt.name)) {
      return;
    }

@@ -181,7 +189,7 @@ function modelImageStyleToDataAttribute() {

    writer.setAttribute(
      'data-align',
      mapping[data.attributeNewValue],
      mappedAlignment.dataValue,
      imageInFigure || viewElement,
    );
  }
@@ -268,8 +276,17 @@ function viewImageToModelImage(editor) {
      return;
    }

    // Create image that's allowed in the given context.
    if (schema.checkChild(data.modelCursor, 'imageInline')) {
    const hasDataCaption = consumable.test(viewItem, {
      name: true,
      attributes: 'data-caption',
    });

    // Create image that's allowed in the given context. If the image has a
    // caption, the image must be created as a block image to ensure the caption
    // is not lost on conversion. This is based on the assumption that
    // preserving the image caption is more important to the content creator
    // than preserving the wrapping element that doesn't allow block images.
    if (schema.checkChild(data.modelCursor, 'imageInline') && !hasDataCaption) {
      image = writer.createElement('imageInline', {
        src: viewItem.getAttribute('src'),
      });
@@ -279,39 +296,30 @@ function viewImageToModelImage(editor) {
      });
    }

    // The way that image styles are handled here is naive - it assumes that the
    // image styles are configured exactly as expected by this plugin.
    // @todo Add support for custom image style configurations
    //   https://www.drupal.org/i/3270693.
    if (
      editor.plugins.has('ImageStyleEditing') &&
      consumable.test(viewItem, { name: true, attributes: 'data-align' })
    ) {
      // https://ckeditor.com/docs/ckeditor5/latest/api/module_image_imagestyle_utils.html#constant-defaultStyles
      const dataToPresentationMapBlock = {
        left: 'alignBlockLeft',
        center: 'alignCenter',
        right: 'alignBlockRight',
      };
      const dataToPresentationMapInline = {
        left: 'alignLeft',
        right: 'alignRight',
      };

      const dataAlign = viewItem.getAttribute('data-align');
      const alignment = image.is('element', 'imageBlock')
        ? dataToPresentationMapBlock[dataAlign]
        : dataToPresentationMapInline[dataAlign];
      const mappedAlignment = alignmentMapping.find(
        (value) => value.dataValue === dataAlign,
      );

      writer.setAttribute('imageStyle', alignment, image);
      if (mappedAlignment) {
        writer.setAttribute('imageStyle', mappedAlignment.modelValue, image);

        // Make sure the attribute can be consumed after successful `safeInsert`
        // operation.
        attributesToConsume.push('data-align');
      }
    }

    // Check if the view element has still unconsumed `data-caption` attribute.
    // Also, we can add caption only to block image.
    if (
      image.is('element', 'imageBlock') &&
      consumable.test(viewItem, { name: true, attributes: 'data-caption' })
    ) {
    if (hasDataCaption) {
      // Create `caption` model element. Thanks to that element the rest of the
      // `ckeditor5-plugin` converters can recognize this image as a block image
      // with a caption.
+61 −0
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@

namespace Drupal\Tests\ckeditor5\FunctionalJavascript;

use Drupal\Component\Utility\Html;
use Drupal\editor\Entity\Editor;
use Drupal\file\Entity\File;
use Drupal\filter\Entity\FilterFormat;
@@ -140,6 +141,66 @@ function (ConstraintViolation $v) {
    $this->drupalLogin($this->adminUser);
  }

  /**
   * Ensures that attributes are retained on conversion.
   */
  public function testAttributeRetentionDuringUpcasting() {
    // Run test cases in a single test to make the test run faster.
    $attributes_to_retain = [
      '-none-' => 'inline',
      'data-caption="test caption 🦙"' => 'block',
      'data-align="left"' => 'inline',
    ];

    foreach ($attributes_to_retain as $attribute_to_retain => $expected_upcast_behavior_when_wrapped_in_block_element) {
      if ($attribute_to_retain === '-none-') {
        $attribute_to_retain = '';
      }
      $img_tag = '<img ' . $attribute_to_retain . ' alt="drupalimage test image" data-entity-type="file" data-entity-uuid="' . $this->file->uuid() . '" src="' . $this->file->createFileUrl() . '" />';
      $test_cases = [
        // Plain image tag for a baseline.
        [
          $img_tag,
          $img_tag,
        ],
        // Image tag wrapped with <p>.
        [
          "<p>$img_tag</p>",
          $expected_upcast_behavior_when_wrapped_in_block_element === 'inline' ? "<p>$img_tag</p>" : $img_tag,
        ],
        // Image tag wrapped with an unallowed paragraph-like element (<div).
        // When inline is the expected upcast behavior, it will wrap in <p>
        // because it still must wrap in a paragraph-like element, and <p> is
        // available to be that element.
        [
          "<div>$img_tag</div>",
          $expected_upcast_behavior_when_wrapped_in_block_element === 'inline' ? "<p>$img_tag</p>" : $img_tag,
        ],
      ];

      foreach ($test_cases as $test_case) {
        [$markup, $expected] = $test_case;
        $this->host->body->value = $markup;
        $this->host->save();

        $this->drupalGet($this->host->toUrl('edit-form'));
        $this->waitForEditor();

        // Ensure that the image is rendered in preview.
        $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', ".ck-content .ck-widget img"));
        $editor_dom = $this->getEditorDataAsDom();
        $expected_dom = Html::load($expected);
        $xpath = new \DOMXPath($this->getEditorDataAsDom());
        $this->assertEquals($expected_dom->getElementsByTagName('body')->item(0)->C14N(), $editor_dom->getElementsByTagName('body')->item(0)->C14N());

        // Ensure the test attribute is persisted on downcast.
        if ($attribute_to_retain) {
          $this->assertNotEmpty($xpath->query("//img[@$attribute_to_retain]"));
        }
      }
    }
  }

  /**
   * Tests that arbitrary attributes are allowed via GHS.
   *