Loading core/modules/ckeditor5/js/build/drupalImage.js +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 core/modules/ckeditor5/js/ckeditor5_plugins/drupalImage/src/drupalimageediting.js +45 −37 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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; } Loading @@ -181,7 +189,7 @@ function modelImageStyleToDataAttribute() { writer.setAttribute( 'data-align', mapping[data.attributeNewValue], mappedAlignment.dataValue, imageInFigure || viewElement, ); } Loading Loading @@ -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'), }); Loading @@ -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. Loading core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php +61 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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. * Loading Loading
core/modules/ckeditor5/js/build/drupalImage.js +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
core/modules/ckeditor5/js/ckeditor5_plugins/drupalImage/src/drupalimageediting.js +45 −37 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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; } Loading @@ -181,7 +189,7 @@ function modelImageStyleToDataAttribute() { writer.setAttribute( 'data-align', mapping[data.attributeNewValue], mappedAlignment.dataValue, imageInFigure || viewElement, ); } Loading Loading @@ -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'), }); Loading @@ -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. Loading
core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php +61 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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. * Loading