diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml index 894d6c4b49f61557d4f3a166e1a52129567f84e0..f9a4b34fb93186279e9557312a08552ed9f631da 100644 --- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml +++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml @@ -539,6 +539,22 @@ media_media: conditions: filter: media_embed +ckeditor5_drupalMediaCaption: + ckeditor5: + plugins: + - drupalMedia.DrupalMediaCaption + config: + drupalMedia: + toolbar: [toggleDrupalMediaCaption] + drupal: + label: Media caption + elements: + - <drupal-media data-caption> + conditions: + filter: filter_caption + plugins: + - media_media + media_mediaAlign: provider: media ckeditor5: diff --git a/core/modules/ckeditor5/css/drupalmedia.css b/core/modules/ckeditor5/css/drupalmedia.css index 45a817af2bc05f01fc0a104a7438cc60c19b8ac2..60a90ef43a2e3864e6d02bba356591cea3a60955 100644 --- a/core/modules/ckeditor5/css/drupalmedia.css +++ b/core/modules/ckeditor5/css/drupalmedia.css @@ -1,6 +1,51 @@ +/** + * @file + * Styles for the Drupal Media in CKEditor 5. + * + * Most of these styles are written to match those in the CKEditor 5 image + * plugin to provide a consistent editing experience. + */ + .ck .drupal-media { position: relative; + display: table; + clear: both; + min-width: 50px; + margin: 0.9em auto; + text-align: center; } + +.ck-content .drupal-media img { + display: block; + min-width: 100%; + max-width: 100%; + margin: 0 auto; +} + +.ck-content .drupal-media > figcaption { + display: table-caption; + padding: 0.6em; + caption-side: bottom; + word-break: break-word; + color: hsl(0, 0%, 20%); + outline-offset: -1px; + background-color: hsl(0, 0%, 97%); + font-size: 0.75em; +} +.ck.ck-editor__editable .drupal-media__caption_highlighted { + animation: drupal-media-caption-highlight 0.6s ease-out; +} + +@keyframes drupal-media-caption-highlight { + 0% { + background-color: hsl(52, 100%, 50%); + } + + 100% { + background-color: hsl(0, 0%, 97%); + } +} + .ck .drupal-media__metadata-error { position: absolute; top: 8px; diff --git a/core/modules/ckeditor5/js/build/drupalImage.js b/core/modules/ckeditor5/js/build/drupalImage.js index d99b5f2d11e54c8ff18c96e9a8be02f5444f9184..3181eb5bb02c33a18e3ebb147562bc7cf6fa55bc 100644 --- a/core/modules/ckeditor5/js/build/drupalImage.js +++ b/core/modules/ckeditor5/js/build/drupalImage.js @@ -1 +1 @@ -!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 function(){var t={"ckeditor5/src/core.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/upload.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/upload.js")},"ckeditor5/src/utils.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"dll-reference CKEditor5.dll":function(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=function(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=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)};var r={};return function(){"use strict";i.d(r,{default:function(){return 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 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(!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"}}var 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"}}var 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 function(){var t={"ckeditor5/src/core.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/upload.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/upload.js")},"ckeditor5/src/utils.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"dll-reference CKEditor5.dll":function(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=function(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=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)};var r={};return function(){"use strict";i.d(r,{default:function(){return 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"}}var 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"}}var h={DrupalImage:u,DrupalImageUpload:p}}(),r=r.default}()})); \ No newline at end of file diff --git a/core/modules/ckeditor5/js/build/drupalMedia.js b/core/modules/ckeditor5/js/build/drupalMedia.js index 1a1ca696908f5ebdada0250b38197e6026708860..35e9b0cbca9f56509c79f9695ce265c0962608d9 100644 --- a/core/modules/ckeditor5/js/build/drupalMedia.js +++ b/core/modules/ckeditor5/js/build/drupalMedia.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.CKEditor5=t():(e.CKEditor5=e.CKEditor5||{},e.CKEditor5.drupalMedia=t())}(self,(function(){return function(){var e={"ckeditor5/src/core.js":function(e,t,i){e.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":function(e,t,i){e.exports=i("dll-reference CKEditor5.dll")("./src/ui.js")},"ckeditor5/src/utils.js":function(e,t,i){e.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"ckeditor5/src/widget.js":function(e,t,i){e.exports=i("dll-reference CKEditor5.dll")("./src/widget.js")},"dll-reference CKEditor5.dll":function(e){"use strict";e.exports=CKEditor5.dll}},t={};function i(a){var n=t[a];if(void 0!==n)return n.exports;var r=t[a]={exports:{}};return e[a](r,r.exports,i),r.exports}i.d=function(e,t){for(var a in t)i.o(t,a)&&!i.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var a={};return function(){"use strict";i.d(a,{default:function(){return X}});var e=i("ckeditor5/src/core.js"),t=i("ckeditor5/src/widget.js");class n extends e.Command{execute(e){const t=this.editor.plugins.get("DrupalMediaEditing"),i=Object.entries(t.attrs).reduce(((e,[t,i])=>(e[i]=t,e)),{}),a=Object.keys(e).reduce(((t,a)=>(i[a]&&(t[i[a]]=e[a]),t)),{});if(this.editor.plugins.has("DrupalElementStyleEditing")){const t=this.editor.plugins.get("DrupalElementStyleEditing");for(const i of t.normalizedStyles)if(e[i.attributeName]&&i.attributeValue===e[i.attributeName]){a.drupalElementStyle=i.name;break}}this.editor.model.change((e=>{this.editor.model.insertContent(function(e,t){return e.createElement("drupalMedia",t)}(e,a))}))}refresh(){const e=this.editor.model,t=e.document.selection,i=e.schema.findAllowedParent(t.getFirstPosition(),"drupalMedia");this.isEnabled=null!==i}}function r(e){return!!e&&e.is("element","drupalMedia")}function l(e){const i=e.getSelectedElement();return i&&function(e){return(0,t.isWidget)(e)&&!!e.getCustomProperty("drupalMedia")}(i)?i:null}function o(e){const t=typeof e;return null!=e&&("object"===t||"function"===t)}function s(e){for(const t of e){if(t.hasAttribute("data-drupal-media-preview"))return t;if(t.childCount){const e=s(t.getChildren());if(e)return e}}return null}class d extends e.Plugin{static get requires(){return[t.Widget]}init(){this.attrs={drupalMediaAlt:"alt",drupalMediaCaption:"data-caption",drupalMediaEntityType:"data-entity-type",drupalMediaEntityUuid:"data-entity-uuid",drupalMediaViewMode:"data-view-mode"};const e=this.editor.config.get("drupalMedia");if(!e)return;const{previewURL:t,themeError:i}=e;this.previewUrl=t,this.labelError=Drupal.t("Preview failed"),this.themeError=i||`\n <p>${Drupal.t("An error occurred while trying to preview the media. Please save your work and reload this page.")}<p>\n `,this._defineSchema(),this._defineConverters(),this.editor.commands.add("insertDrupalMedia",new n(this.editor))}async _fetchPreview(e){const t={text:this._renderElement(e),uuid:e.getAttribute("drupalMediaEntityUuid")},i=await fetch(`${this.previewUrl}?${new URLSearchParams(t)}`,{headers:{"X-Drupal-MediaPreview-CSRF-Token":this.editor.config.get("drupalMedia").previewCsrfToken}});if(i.ok){return{label:i.headers.get("drupal-media-label"),preview:await i.text()}}return{label:this.labelError,preview:this.themeError}}_defineSchema(){this.editor.model.schema.register("drupalMedia",{allowWhere:"$block",isObject:!0,isContent:!0,allowAttributes:Object.keys(this.attrs)})}_defineConverters(){const e=this.editor.conversion;e.for("upcast").elementToElement({view:{name:"drupal-media"},model:"drupalMedia"}),e.for("dataDowncast").elementToElement({model:"drupalMedia",view:{name:"drupal-media"}}),e.for("editingDowncast").elementToElement({model:"drupalMedia",view:(e,{writer:i})=>{const a=i.createContainerElement("div",{class:"drupal-media"});if(!this.previewUrl){const e=i.createRawElement("div",{"data-drupal-media-preview":"unavailable"});i.insert(i.createPositionAt(a,0),e)}return i.setCustomProperty("drupalMedia",!0,a),(0,t.toWidget)(a,i,{label:Drupal.t("Media widget")})}}).add((e=>{const t=(e,t,i)=>{const a=i.writer,n=t.item,r=i.mapper.toViewElement(t.item);let l=s(r.getChildren());if(l){if("ready"!==l.getAttribute("data-drupal-media-preview"))return;a.setAttribute("data-drupal-media-preview","loading",l)}else l=a.createRawElement("div",{"data-drupal-media-preview":"loading"}),a.insert(a.createPositionAt(r,0),l);this._fetchPreview(n).then((({label:e,preview:t})=>{l&&this.editor.editing.view.change((i=>{const a=i.createRawElement("div",{"data-drupal-media-preview":"ready","aria-label":e},(e=>{e.innerHTML=t}));i.insert(i.createPositionBefore(l),a),i.remove(l)}))}))};return e.on("attribute:drupalMediaEntityUuid:drupalMedia",t),e.on("attribute:drupalMediaViewMode:drupalMedia",t),e.on("attribute:drupalMediaEntityType:drupalMedia",t),e.on("attribute:drupalMediaAlt:drupalMedia",t),e})),e.for("editingDowncast").add((e=>{e.on("attribute:drupalElementStyle:drupalMedia",((e,t,i)=>{const a={alignLeft:"drupal-media-style-align-left",alignRight:"drupal-media-style-align-right",alignCenter:"drupal-media-style-align-center"},n=i.mapper.toViewElement(t.item),r=i.writer;a[t.attributeOldValue]&&r.removeClass(a[t.attributeOldValue],n),a[t.attributeNewValue]&&i.consumable.consume(t.item,e.name)&&r.addClass(a[t.attributeNewValue],n)}))})),Object.keys(this.attrs).forEach((t=>{const i={model:{key:t,name:"drupalMedia"},view:{name:"drupal-media",key:this.attrs[t]}};e.for("dataDowncast").attributeToAttribute(i),e.for("upcast").attributeToAttribute(i)}))}_renderElement(e){const t=e.getAttributes();let i="<drupal-media";return Array.from(t).forEach((e=>{this.attrs[e[0]]&&"drupalMediaCaption"!==e[0]&&(i+=` ${this.attrs[e[0]]}="${e[1]}"`)})),i+="></drupal-media>",i}static get pluginName(){return"DrupalMediaEditing"}}var u=i("ckeditor5/src/ui.js");class c extends e.Plugin{init(){const e=this.editor,t=this.editor.config.get("drupalMedia");if(!t)return;const{libraryURL:i,openDialog:a,dialogSettings:n={}}=t;i&&"function"==typeof a&&e.ui.componentFactory.add("drupalMedia",(t=>{const r=e.commands.get("insertDrupalMedia"),l=new u.ButtonView(t);return l.set({label:Drupal.t("Insert Drupal Media"),icon:'<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M19.1873 4.86414L10.2509 6.86414V7.02335H10.2499V15.5091C9.70972 15.1961 9.01793 15.1048 8.34069 15.3136C7.12086 15.6896 6.41013 16.8967 6.75322 18.0096C7.09631 19.1226 8.3633 19.72 9.58313 19.344C10.6666 19.01 11.3484 18.0203 11.2469 17.0234H11.2499V9.80173L18.1803 8.25067V14.3868C17.6401 14.0739 16.9483 13.9825 16.2711 14.1913C15.0513 14.5674 14.3406 15.7744 14.6836 16.8875C15.0267 18.0004 16.2937 18.5978 17.5136 18.2218C18.597 17.8877 19.2788 16.8982 19.1773 15.9011H19.1803V8.02687L19.1873 8.0253V4.86414Z" fill="black"/><path fill-rule="evenodd" clip-rule="evenodd" d="M13.5039 0.743652H0.386932V12.1603H13.5039V0.743652ZM12.3379 1.75842H1.55289V11.1454H1.65715L4.00622 8.86353L6.06254 10.861L9.24985 5.91309L11.3812 9.22179L11.7761 8.6676L12.3379 9.45621V1.75842ZM6.22048 4.50869C6.22048 5.58193 5.35045 6.45196 4.27722 6.45196C3.20398 6.45196 2.33395 5.58193 2.33395 4.50869C2.33395 3.43546 3.20398 2.56543 4.27722 2.56543C5.35045 2.56543 6.22048 3.43546 6.22048 4.50869Z" fill="black"/></svg>\n',tooltip:!0}),l.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(l,"execute",(()=>{a(i,(({attributes:t})=>{e.execute("insertDrupalMedia",t)}),n)})),l}))}}class m extends e.Plugin{static get requires(){return[t.WidgetToolbarRepository]}static get pluginName(){return"DrupalMediaToolbar"}afterInit(){const{editor:e}=this;var i;e.plugins.get(t.WidgetToolbarRepository).register("drupalMedia",{ariaLabel:Drupal.t("Drupal Media toolbar"),items:(i=e.config.get("drupalMedia.toolbar"),i.map((e=>o(e)?e.name:e))||[]),getRelatedElement:e=>l(e)})}}const g="METADATA_ERROR";class p extends e.Command{refresh(){const e=this.editor.model.document.selection.getSelectedElement();this.isEnabled=r(e)&&e.getAttribute("drupalMediaIsImage")&&e.getAttribute("drupalMediaIsImage")!==g,r(e)&&e.hasAttribute("drupalMediaAlt")?this.value=e.getAttribute("drupalMediaAlt"):this.value=!1}execute(e){const{model:t}=this.editor,i=t.document.selection.getSelectedElement();e.newValue=e.newValue.trim(),t.change((t=>{e.newValue.length>0?t.setAttribute("drupalMediaAlt",e.newValue,i):t.removeAttribute("drupalMediaAlt",i)}))}}class h extends e.Plugin{init(){this._data=new WeakMap}getMetadata(e){if(this._data.get(e))return new Promise((t=>{t(this._data.get(e))}));const t=this.editor.config.get("drupalMedia");if(!t)return new Promise(((e,t)=>{t(new Error("drupalMedia configuration is required for parsing metadata."))}));if(!e.hasAttribute("drupalMediaEntityUuid"))return new Promise(((e,t)=>{t(new Error("drupalMedia element must have drupalMediaEntityUuid attribute to retrieve metadata."))}));const{metadataUrl:i}=t;return(async e=>{const t=await fetch(e);if(t.ok)return JSON.parse(await t.text());throw new Error("Fetching media embed metadata from the server failed.")})(`${i}&${new URLSearchParams({uuid:e.getAttribute("drupalMediaEntityUuid")})}`).then((t=>(this._data.set(e,t),t)))}static get pluginName(){return"DrupalMediaMetadataRepository"}}class f extends e.Plugin{static get requires(){return[h]}static get pluginName(){return"MediaImageTextAlternativeEditing"}_upcastDrupalMediaIsImage(e){const{model:t,plugins:i}=this.editor;i.get("DrupalMediaMetadataRepository").getMetadata(e).then((i=>{e&&t.enqueueChange("transparent",(t=>{t.setAttribute("drupalMediaIsImage",!!i.imageSourceMetadata,e)}))})).catch((i=>{e&&(console.warn(i.toString()),t.enqueueChange("transparent",(t=>{t.setAttribute("drupalMediaIsImage",g,e)})))}))}init(){const{editor:e,editor:{model:t,conversion:i}}=this;t.schema.extend("drupalMedia",{allowAttributes:["drupalMediaIsImage"]}),this.listenTo(t,"insertContent",((e,[t])=>{r(t)&&this._upcastDrupalMediaIsImage(t)})),i.for("upcast").add((e=>{e.on("element:drupal-media",((e,t)=>{const[i]=t.modelRange.getItems();r(i)&&this._upcastDrupalMediaIsImage(i)}),{priority:"lowest"})})),i.for("editingDowncast").add((e=>{e.on("attribute:drupalMediaIsImage",((e,t,i)=>{const{writer:a,mapper:n}=i,r=n.toViewElement(t.item);if(t.attributeNewValue!==g){const e=Array.from(r.getChildren()).find((e=>e.getCustomProperty("drupalMediaMetadataError")));return void(e&&(a.setCustomProperty("widgetLabel",e.getCustomProperty("drupalMediaOriginalWidgetLabel"),e),a.removeElement(e)))}const l=Drupal.t("Not all functionality may be available because some information could not be retrieved."),o=new u.TooltipView;o.text=l,o.position="sw";const s=new u.Template({tag:"span",children:[{tag:"span",attributes:{class:"drupal-media__metadata-error-icon"}},o]}).render(),d=a.createRawElement("div",{class:"drupal-media__metadata-error"},((e,t)=>{t.setContentOf(e,s.outerHTML)}));a.setCustomProperty("drupalMediaMetadataError",!0,d);const c=r.getCustomProperty("widgetLabel");a.setCustomProperty("drupalMediaOriginalWidgetLabel",c,d),a.setCustomProperty("widgetLabel",`${c} (${l})`,r),a.insert(a.createPositionAt(r,0),d)}),{priority:"low"})})),e.commands.add("mediaImageTextAlternative",new p(this.editor))}}function b(e){const t=e.editing.view,i=u.BalloonPanelView.defaultPositions;return{target:t.domConverter.viewToDom(t.document.selection.getSelectedElement()),positions:[i.northArrowSouth,i.northArrowSouthWest,i.northArrowSouthEast,i.southArrowNorth,i.southArrowNorthWest,i.southArrowNorthEast]}}var w=i("ckeditor5/src/utils.js");class y extends u.View{constructor(t){super(t),this.focusTracker=new w.FocusTracker,this.keystrokes=new w.KeystrokeHandler,this.labeledInput=this._createLabeledInputView(),this.saveButtonView=this._createButton(Drupal.t("Save"),e.icons.check,"ck-button-save"),this.saveButtonView.type="submit",this.cancelButtonView=this._createButton(Drupal.t("Cancel"),e.icons.cancel,"ck-button-cancel","cancel"),this._focusables=new u.ViewCollection,this._focusCycler=new u.FocusCycler({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.setTemplate({tag:"form",attributes:{class:["ck","ck-text-alternative-form","ck-responsive-form"],tabindex:"-1"},children:[this.labeledInput,this.saveButtonView,this.cancelButtonView]}),(0,u.injectCssTransitionDisabler)(this)}render(){super.render(),this.keystrokes.listenTo(this.element),(0,u.submitHandler)({view:this}),[this.labeledInput,this.saveButtonView,this.cancelButtonView].forEach((e=>{this._focusables.add(e),this.focusTracker.add(e.element)}))}_createButton(e,t,i,a){const n=new u.ButtonView(this.locale);return n.set({label:e,icon:t,tooltip:!0}),n.extendTemplate({attributes:{class:i}}),a&&n.delegate("execute").to(this,a),n}_createLabeledInputView(){const e=new u.LabeledFieldView(this.locale,u.createLabeledInputText);return e.label=Drupal.t("Override text alternative"),e}}class v extends e.Plugin{static get requires(){return[u.ContextualBalloon]}static get pluginName(){return"MediaImageTextAlternativeUi"}init(){this._createButton(),this._createForm()}destroy(){super.destroy(),this._form.destroy()}_createButton(){const t=this.editor;t.ui.componentFactory.add("mediaImageTextAlternative",(i=>{const a=t.commands.get("mediaImageTextAlternative"),n=new u.ButtonView(i);return n.set({label:Drupal.t("Override media image text alternative"),icon:e.icons.lowVision,tooltip:!0}),n.bind("isVisible").to(a,"isEnabled"),this.listenTo(n,"execute",(()=>{this._showForm()})),n}))}_createForm(){const e=this.editor,t=e.editing.view.document;this._balloon=this.editor.plugins.get("ContextualBalloon"),this._form=new y(e.locale),this._form.render(),this.listenTo(this._form,"submit",(()=>{e.execute("mediaImageTextAlternative",{newValue:this._form.labeledInput.fieldView.element.value}),this._hideForm(!0)})),this.listenTo(this._form,"cancel",(()=>{this._hideForm(!0)})),this._form.keystrokes.set("Esc",((e,t)=>{this._hideForm(!0),t()})),this.listenTo(e.ui,"update",(()=>{l(t.selection)?this._isVisible&&function(e){const t=e.plugins.get("ContextualBalloon");if(l(e.editing.view.document.selection)){const i=b(e);t.updatePosition(i)}}(e):this._hideForm(!0)})),(0,u.clickOutsideHandler)({emitter:this._form,activator:()=>this._isVisible,contextElements:[this._balloon.view.element],callback:()=>this._hideForm()})}_showForm(){if(this._isVisible)return;const e=this.editor,t=e.commands.get("mediaImageTextAlternative"),i=this._form.labeledInput;this._form.disableCssTransitions(),this._isInBalloon||this._balloon.add({view:this._form,position:b(e)}),i.fieldView.element.value=t.value||"",i.fieldView.value=i.fieldView.element.value,this._form.labeledInput.fieldView.select(),this._form.enableCssTransitions()}_hideForm(e){this._isInBalloon&&(this._form.focusTracker.isFocused&&this._form.saveButtonView.focus(),this._balloon.remove(this._form),e&&this.editor.editing.view.focus())}get _isVisible(){return this._balloon.visibleView===this._form}get _isInBalloon(){return this._balloon.hasView(this._form)}}class E extends e.Plugin{static get requires(){return[f,v]}static get pluginName(){return"MediaImageTextAlternative"}}function M(e,t,i){if(t.attributes)for(const[a,n]of Object.entries(t.attributes))e.setAttribute(a,n,i);t.styles&&e.setStyle(t.styles,i),t.classes&&e.addClass(t.classes,i)}function k(e){return t=>{t.on("element:drupal-media",((t,i,a)=>{const n=i.viewItem.parent;n.is("element","a")&&function(t,n){const r=e._consumeAllowedAttributes(t,a);r&&a.writer.setAttribute(n,r,i.modelRange)}(n,"htmlLinkAttributes")}),{priority:"low"})}}class A extends e.Plugin{init(){const{editor:e}=this;if(!e.plugins.has("GeneralHtmlSupport"))return;const{schema:t}=e.model,{conversion:i}=e,a=e.plugins.get("DataFilter");t.extend("drupalMedia",{allowAttributes:["htmlLinkAttributes"]}),i.for("upcast").add(k(a)),i.for("editingDowncast").add((e=>e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{if(!i.consumable.consume(t.item,"attribute:htmlLinkAttributes:drupalMedia"))return;const a=i.mapper.toViewElement(t.item),n=function(e,t,i){const a=e.createRangeOn(t);for(const{item:e}of a.getWalker())if(e.is("element",i))return e}(i.writer,a,"a");M(i.writer,t.item.getAttribute("htmlLinkAttributes"),n)}),{priority:"low"}))),i.for("dataDowncast").add((e=>e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{if(!i.consumable.consume(t.item,"attribute:htmlLinkAttributes:drupalMedia"))return;const a=i.mapper.toViewElement(t.item).parent;M(i.writer,t.item.getAttribute("htmlLinkAttributes"),a)}),{priority:"low"})))}static get pluginName(){return"DrupalMediaGeneralHtmlSupport"}}class _ extends e.Plugin{static get requires(){return[d,A,c,m,E]}static get pluginName(){return"DrupalMedia"}}function C(){return e=>{e.on("element:a",((e,t,i)=>{const a=t.viewItem,n=(r=a,Array.from(r.getChildren()).find((e=>"drupal-media"===e.name)));var r;if(!n)return;if(!i.consumable.consume(a,{attributes:["href"]}))return;const l=a.getAttribute("href");if(!l)return;const o=i.convertItem(n,t.modelCursor);t.modelRange=o.modelRange,t.modelCursor=o.modelCursor;const s=t.modelCursor.nodeBefore;s&&s.is("element","drupalMedia")&&i.writer.setAttribute("linkHref",l,s)}),{priority:"high"})}}class x extends e.Plugin{static get requires(){return["LinkEditing","DrupalMediaEditing"]}static get pluginName(){return"DrupalLinkMediaEditing"}init(){const{editor:e}=this;e.model.schema.extend("drupalMedia",{allowAttributes:["linkHref"]}),e.conversion.for("upcast").add(C()),e.conversion.for("editingDowncast").add((e=>{e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{const{writer:a}=i;if(!i.consumable.consume(t.item,e.name))return;const n=i.mapper.toViewElement(t.item),r=Array.from(n.getChildren()).find((e=>"a"===e.name));if(r)t.attributeNewValue?a.setAttribute("href",t.attributeNewValue,r):(a.move(a.createRangeIn(r),a.createPositionAt(n,0)),a.remove(r));else{const e=Array.from(n.getChildren()).find((e=>e.getAttribute("data-drupal-media-preview"))),i=a.createContainerElement("a",{href:t.attributeNewValue});a.insert(a.createPositionAt(n,0),i),a.move(a.createRangeOn(e),a.createPositionAt(i,0))}}),{priority:"high"})})),e.conversion.for("dataDowncast").add((e=>{e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{const{writer:a}=i;if(!i.consumable.consume(t.item,e.name))return;const n=i.mapper.toViewElement(t.item),r=a.createContainerElement("a",{href:t.attributeNewValue});a.insert(a.createPositionBefore(n),r),a.move(a.createRangeOn(n),a.createPositionAt(r,0))}),{priority:"high"})}))}}class S extends e.Plugin{static get requires(){return["LinkEditing","LinkUI","DrupalMediaEditing"]}static get pluginName(){return"DrupalLinkMediaUi"}init(){const{editor:e}=this,t=e.editing.view.document;this.listenTo(t,"click",((t,i)=>{this._isSelectedLinkedMedia(e.model.document.selection)&&(i.preventDefault(),t.stop())}),{priority:"high"}),this._createToolbarLinkMediaButton()}_createToolbarLinkMediaButton(){const{editor:e}=this;e.ui.componentFactory.add("drupalLinkMedia",(t=>{const i=new u.ButtonView(t),a=e.plugins.get("LinkUI"),n=e.commands.get("link");return i.set({isEnabled:!0,label:Drupal.t("Link media"),icon:'<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m11.077 15 .991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184z"/></svg>\n',keystroke:"Ctrl+K",tooltip:!0,isToggleable:!0}),i.bind("isEnabled").to(n,"isEnabled"),i.bind("isOn").to(n,"value",(e=>!!e)),this.listenTo(i,"execute",(()=>{this._isSelectedLinkedMedia(e.model.document.selection)?a._addActionsView():a._showUI(!0)})),i}))}_isSelectedLinkedMedia(e){const t=e.getSelectedElement();return!!t&&t.is("element","drupalMedia")&&t.hasAttribute("linkHref")}}class V extends e.Plugin{static get requires(){return[x,S]}static get pluginName(){return"DrupalLinkMedia"}}const{objectFullWidth:I,objectInline:D,objectLeft:L,objectRight:T,objectCenter:B,objectBlockLeft:P,objectBlockRight:N}=e.icons,O={inline:{name:"inline",title:"In line",icon:D,modelElements:["imageInline"],isDefault:!0},alignLeft:{name:"alignLeft",title:"Left aligned image",icon:L,modelElements:["imageBlock","imageInline"],className:"image-style-align-left"},alignBlockLeft:{name:"alignBlockLeft",title:"Left aligned image",icon:P,modelElements:["imageBlock"],className:"image-style-block-align-left"},alignCenter:{name:"alignCenter",title:"Centered image",icon:B,modelElements:["imageBlock"],className:"image-style-align-center"},alignRight:{name:"alignRight",title:"Right aligned image",icon:T,modelElements:["imageBlock","imageInline"],className:"image-style-align-right"},alignBlockRight:{name:"alignBlockRight",title:"Right aligned image",icon:N,modelElements:["imageBlock"],className:"image-style-block-align-right"},block:{name:"block",title:"Centered image",icon:B,modelElements:["imageBlock"],isDefault:!0},side:{name:"side",title:"Side image",icon:T,modelElements:["imageBlock"],className:"image-style-side"}},R={full:I,left:P,right:N,center:B,inlineLeft:L,inlineRight:T,inline:D},j=[{name:"imageStyle:wrapText",title:"Wrap text",defaultItem:"imageStyle:alignLeft",items:["imageStyle:alignLeft","imageStyle:alignRight"]},{name:"imageStyle:breakText",title:"Break text",defaultItem:"imageStyle:block",items:["imageStyle:alignBlockLeft","imageStyle:block","imageStyle:alignBlockRight"]}];function F(e){(0,w.logWarning)("image-style-configuration-definition-invalid",e)}var U={normalizeStyles:function(e){return(e.configuredStyles.options||[]).map((e=>function(e){e="string"==typeof e?O[e]?{...O[e]}:{name:e}:function(e,t){const i={...t};for(const a in e)Object.prototype.hasOwnProperty.call(t,a)||(i[a]=e[a]);return i}(O[e.name],e);"string"==typeof e.icon&&(e.icon=R[e.icon]||e.icon);return e}(e))).filter((t=>function(e,{isBlockPluginLoaded:t,isInlinePluginLoaded:i}){const{modelElements:a,name:n}=e;if(!(a&&a.length&&n))return F({style:e}),!1;{const n=[t?"imageBlock":null,i?"imageInline":null];if(!a.some((e=>n.includes(e))))return(0,w.logWarning)("image-style-missing-dependency",{style:e,missingPlugins:a.map((e=>"imageBlock"===e?"ImageBlockEditing":"ImageInlineEditing"))}),!1}return!0}(t,e)))},getDefaultStylesConfiguration:function(e,t){return e&&t?{options:["inline","alignLeft","alignRight","alignCenter","alignBlockLeft","alignBlockRight","block","side"]}:e?{options:["block","side"]}:t?{options:["inline","alignLeft","alignRight"]}:{}},getDefaultDropdownDefinitions:function(e){return e.has("ImageBlockEditing")&&e.has("ImageInlineEditing")?[...j]:[]},warnInvalidStyle:F,DEFAULT_OPTIONS:O,DEFAULT_ICONS:R,DEFAULT_DROPDOWN_DEFINITIONS:j};function H(e,t){const i=e.getSelectedElement();return i&&t.checkAttribute(i,"drupalElementStyle")?i:e.getFirstPosition().findAncestor((e=>t.checkAttribute(e,"drupalElementStyle")))}class W extends e.Command{constructor(e,t){super(e),this._styles=new Map(t.map((e=>[e.name,e])))}refresh(){const e=this.editor,t=H(e.model.document.selection,e.model.schema);this.isEnabled=!!t,this.isEnabled&&t.hasAttribute("drupalElementStyle")?this.value=t.getAttribute("drupalElementStyle"):this.value=!1}execute(e={}){const t=this.editor.model;t.change((i=>{const a=e.value,n=H(t.document.selection,t.schema);!a||this._styles.get(a).isDefault?i.removeAttribute("drupalElementStyle",n):i.setAttribute("drupalElementStyle",a,n)}))}}function q(e,t){for(const i of t)if(i.name===e)return i}class K extends e.Plugin{init(){const t=this.editor;t.config.define("drupalElementStyles",{options:[]});const i=t.config.get("drupalElementStyles").options;this.normalizedStyles=i.map((t=>("string"==typeof t.icon&&e.icons[t.icon]&&(t.icon=e.icons[t.icon]),t))).filter((e=>e.attributeName&&e.attributeValue?e.modelElements&&Array.isArray(e.modelElements)?!!e.name||(console.warn("drupalElementStyles options must include a name."),!1):(console.warn("drupalElementStyles options must include an array of supported modelElements."),!1):(console.warn("drupalElementStyles options must include attributeName and attributeValue."),!1))),this._setupConversion(),t.commands.add("drupalElementStyle",new W(t,this.normalizedStyles))}_setupConversion(){const e=this.editor,t=e.model.schema,i=(a=this.normalizedStyles,(e,t,i)=>{if(!i.consumable.consume(t.item,e.name))return;const n=q(t.attributeNewValue,a),r=q(t.attributeOldValue,a),l=i.mapper.toViewElement(t.item),o=i.writer;r&&("class"===r.attributeName?o.removeClass(r.attributeValue,l):o.removeAttribute(r.attributeName,l)),n&&("class"===n.attributeName?o.addClass(n.attributeValue,l):o.setAttribute(n.attributeName,n.attributeValue,l))});var a;const n=function(e){const t=e.filter((e=>!e.isDefault));return(e,i,a)=>{if(!i.modelRange)return;const n=i.viewItem,r=(0,w.first)(i.modelRange.getItems());if(r&&a.schema.checkAttribute(r,"drupalElementStyle"))for(const e of t)if("class"===e.attributeName)a.consumable.consume(n,{classes:e.attributeValue})&&a.writer.setAttribute("drupalElementStyle",e.name,r);else if(a.consumable.consume(n,{attributes:[e.attributeName]}))for(const e of t)e.attributeValue===n.getAttribute(e.attributeName)&&a.writer.setAttribute("drupalElementStyle",e.name,r)}}(this.normalizedStyles);e.editing.downcastDispatcher.on("attribute:drupalElementStyle",i),e.data.downcastDispatcher.on("attribute:drupalElementStyle",i);[...new Set(this.normalizedStyles.map((e=>e.modelElements)).flat())].forEach((e=>{t.extend(e,{allowAttributes:"drupalElementStyle"})})),e.data.upcastDispatcher.on("element",n,{priority:"low"})}static get pluginName(){return"DrupalElementStyleEditing"}}const $=e=>e,z=(e,t)=>(e?`${e}: `:"")+t;function Z(e){return`drupalElementStyle:${e}`}class G extends e.Plugin{static get requires(){return[K]}init(){const e=this.editor.plugins,t=this.editor.config.get("drupalMedia.toolbar")||[],i=Object.values(e.get("DrupalElementStyleEditing").normalizedStyles);i.forEach((e=>{this._createButton(e)}));t.filter(o).forEach((e=>{this._createDropdown(e,i)}))}_createDropdown(e,t){const i=this.editor.ui.componentFactory;i.add(e.name,(a=>{let n;const{defaultItem:r,items:l,title:o}=e,s=l.filter((e=>t.find((({name:t})=>Z(t)===e)))).map((e=>{const t=i.create(e);return e===r&&(n=t),t}));l.length!==s.length&&U.warnInvalidStyle({dropdown:e});const d=(0,u.createDropdown)(a,u.SplitButtonView),c=d.buttonView;return(0,u.addToolbarToDropdown)(d,s),c.set({label:z(o,n.label),class:null,tooltip:!0}),c.bind("icon").toMany(s,"isOn",((...e)=>{const t=e.findIndex($);return t<0?n.icon:s[t].icon})),c.bind("label").toMany(s,"isOn",((...e)=>{const t=e.findIndex($);return z(o,t<0?n.label:s[t].label)})),c.bind("isOn").toMany(s,"isOn",((...e)=>e.some($))),c.bind("class").toMany(s,"isOn",((...e)=>e.some($)?"ck-splitbutton_flatten":null)),c.on("execute",(()=>{s.some((({isOn:e})=>e))?d.isOpen=!d.isOpen:n.fire("execute")})),d.bind("isEnabled").toMany(s,"isEnabled",((...e)=>e.some($))),d}))}_createButton(e){const t=e.name;this.editor.ui.componentFactory.add(Z(t),(i=>{const a=this.editor.commands.get("drupalElementStyle"),n=new u.ButtonView(i);return n.set({label:e.title,icon:e.icon,tooltip:!0,isToggleable:!0}),n.bind("isEnabled").to(a,"isEnabled"),n.bind("isOn").to(a,"value",(e=>e===t)),n.on("execute",this._executeCommand.bind(this,t)),n}))}_executeCommand(e){this.editor.execute("drupalElementStyle",{value:e}),this.editor.editing.view.focus()}static get pluginName(){return"DrupalElementStyleUi"}}class J extends e.Plugin{static get requires(){return[K,G]}static get pluginName(){return"DrupalElementStyle"}}var X={DrupalMedia:_,MediaImageTextAlternative:E,MediaImageTextAlternativeEditing:f,MediaImageTextAlternativeUi:v,DrupalLinkMedia:V,DrupalElementStyle:J}}(),a=a.default}()})); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.CKEditor5=t():(e.CKEditor5=e.CKEditor5||{},e.CKEditor5.drupalMedia=t())}(self,(function(){return function(){var e={"ckeditor5/src/core.js":function(e,t,i){e.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/engine.js":function(e,t,i){e.exports=i("dll-reference CKEditor5.dll")("./src/engine.js")},"ckeditor5/src/ui.js":function(e,t,i){e.exports=i("dll-reference CKEditor5.dll")("./src/ui.js")},"ckeditor5/src/utils.js":function(e,t,i){e.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"ckeditor5/src/widget.js":function(e,t,i){e.exports=i("dll-reference CKEditor5.dll")("./src/widget.js")},"dll-reference CKEditor5.dll":function(e){"use strict";e.exports=CKEditor5.dll}},t={};function i(n){var a=t[n];if(void 0!==a)return a.exports;var r=t[n]={exports:{}};return e[n](r,r.exports,i),r.exports}i.d=function(e,t){for(var n in t)i.o(t,n)&&!i.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var n={};return function(){"use strict";i.d(n,{default:function(){return ne}});var e=i("ckeditor5/src/core.js"),t=i("ckeditor5/src/widget.js");class a extends e.Command{execute(e){const t=this.editor.plugins.get("DrupalMediaEditing"),i=Object.entries(t.attrs).reduce(((e,[t,i])=>(e[i]=t,e)),{}),n=Object.keys(e).reduce(((t,n)=>(i[n]&&(t[i[n]]=e[n]),t)),{});if(this.editor.plugins.has("DrupalElementStyleEditing")){const t=this.editor.plugins.get("DrupalElementStyleEditing");for(const i of t.normalizedStyles)if(e[i.attributeName]&&i.attributeValue===e[i.attributeName]){n.drupalElementStyle=i.name;break}}this.editor.model.change((e=>{this.editor.model.insertContent(function(e,t){return e.createElement("drupalMedia",t)}(e,n))}))}refresh(){const e=this.editor.model,t=e.document.selection,i=e.schema.findAllowedParent(t.getFirstPosition(),"drupalMedia");this.isEnabled=null!==i}}function r(e){return!!e&&e.is("element","drupalMedia")}function o(e){const i=e.getSelectedElement();return i&&function(e){return(0,t.isWidget)(e)&&!!e.getCustomProperty("drupalMedia")}(i)?i:null}function l(e){const t=typeof e;return null!=e&&("object"===t||"function"===t)}function s(e){for(const t of e){if(t.hasAttribute("data-drupal-media-preview"))return t;if(t.childCount){const e=s(t.getChildren());if(e)return e}}return null}class d extends e.Plugin{static get requires(){return[t.Widget]}init(){this.attrs={drupalMediaAlt:"alt",drupalMediaEntityType:"data-entity-type",drupalMediaEntityUuid:"data-entity-uuid",drupalMediaViewMode:"data-view-mode"};const e=this.editor.config.get("drupalMedia");if(!e)return;const{previewURL:t,themeError:i}=e;this.previewUrl=t,this.labelError=Drupal.t("Preview failed"),this.themeError=i||`\n <p>${Drupal.t("An error occurred while trying to preview the media. Please save your work and reload this page.")}<p>\n `,this._defineSchema(),this._defineConverters(),this.editor.commands.add("insertDrupalMedia",new a(this.editor))}async _fetchPreview(e){const t={text:this._renderElement(e),uuid:e.getAttribute("drupalMediaEntityUuid")},i=await fetch(`${this.previewUrl}?${new URLSearchParams(t)}`,{headers:{"X-Drupal-MediaPreview-CSRF-Token":this.editor.config.get("drupalMedia").previewCsrfToken}});if(i.ok){return{label:i.headers.get("drupal-media-label"),preview:await i.text()}}return{label:this.labelError,preview:this.themeError}}_defineSchema(){this.editor.model.schema.register("drupalMedia",{allowWhere:"$block",isObject:!0,isContent:!0,allowAttributes:Object.keys(this.attrs)})}_defineConverters(){const e=this.editor.conversion;e.for("upcast").elementToElement({view:{name:"drupal-media"},model:"drupalMedia"}),e.for("dataDowncast").elementToElement({model:"drupalMedia",view:{name:"drupal-media"}}),e.for("editingDowncast").elementToElement({model:"drupalMedia",view:(e,{writer:i})=>{const n=i.createContainerElement("figure",{class:"drupal-media"});if(!this.previewUrl){const e=i.createRawElement("div",{"data-drupal-media-preview":"unavailable"});i.insert(i.createPositionAt(n,0),e)}return i.setCustomProperty("drupalMedia",!0,n),(0,t.toWidget)(n,i,{label:Drupal.t("Media widget")})}}).add((e=>{const t=(e,t,i)=>{const n=i.writer,a=t.item,r=i.mapper.toViewElement(t.item);let o=s(r.getChildren());if(o){if("ready"!==o.getAttribute("data-drupal-media-preview"))return;n.setAttribute("data-drupal-media-preview","loading",o)}else o=n.createRawElement("div",{"data-drupal-media-preview":"loading"}),n.insert(n.createPositionAt(r,0),o);this._fetchPreview(a).then((({label:e,preview:t})=>{o&&this.editor.editing.view.change((i=>{const n=i.createRawElement("div",{"data-drupal-media-preview":"ready","aria-label":e},(e=>{e.innerHTML=t}));i.insert(i.createPositionBefore(o),n),i.remove(o)}))}))};return e.on("attribute:drupalMediaEntityUuid:drupalMedia",t),e.on("attribute:drupalMediaViewMode:drupalMedia",t),e.on("attribute:drupalMediaEntityType:drupalMedia",t),e.on("attribute:drupalMediaAlt:drupalMedia",t),e})),e.for("editingDowncast").add((e=>{e.on("attribute:drupalElementStyle:drupalMedia",((e,t,i)=>{const n={alignLeft:"drupal-media-style-align-left",alignRight:"drupal-media-style-align-right",alignCenter:"drupal-media-style-align-center"},a=i.mapper.toViewElement(t.item),r=i.writer;n[t.attributeOldValue]&&r.removeClass(n[t.attributeOldValue],a),n[t.attributeNewValue]&&i.consumable.consume(t.item,e.name)&&r.addClass(n[t.attributeNewValue],a)}))})),Object.keys(this.attrs).forEach((t=>{const i={model:{key:t,name:"drupalMedia"},view:{name:"drupal-media",key:this.attrs[t]}};e.for("dataDowncast").attributeToAttribute(i),e.for("upcast").attributeToAttribute(i)}))}_renderElement(e){const t=e.getAttributes();let i="<drupal-media";return Array.from(t).forEach((e=>{this.attrs[e[0]]&&"drupalMediaCaption"!==e[0]&&(i+=` ${this.attrs[e[0]]}="${e[1]}"`)})),i+="></drupal-media>",i}static get pluginName(){return"DrupalMediaEditing"}}var u=i("ckeditor5/src/ui.js");class c extends e.Plugin{init(){const e=this.editor,t=this.editor.config.get("drupalMedia");if(!t)return;const{libraryURL:i,openDialog:n,dialogSettings:a={}}=t;i&&"function"==typeof n&&e.ui.componentFactory.add("drupalMedia",(t=>{const r=e.commands.get("insertDrupalMedia"),o=new u.ButtonView(t);return o.set({label:Drupal.t("Insert Drupal Media"),icon:'<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M19.1873 4.86414L10.2509 6.86414V7.02335H10.2499V15.5091C9.70972 15.1961 9.01793 15.1048 8.34069 15.3136C7.12086 15.6896 6.41013 16.8967 6.75322 18.0096C7.09631 19.1226 8.3633 19.72 9.58313 19.344C10.6666 19.01 11.3484 18.0203 11.2469 17.0234H11.2499V9.80173L18.1803 8.25067V14.3868C17.6401 14.0739 16.9483 13.9825 16.2711 14.1913C15.0513 14.5674 14.3406 15.7744 14.6836 16.8875C15.0267 18.0004 16.2937 18.5978 17.5136 18.2218C18.597 17.8877 19.2788 16.8982 19.1773 15.9011H19.1803V8.02687L19.1873 8.0253V4.86414Z" fill="black"/><path fill-rule="evenodd" clip-rule="evenodd" d="M13.5039 0.743652H0.386932V12.1603H13.5039V0.743652ZM12.3379 1.75842H1.55289V11.1454H1.65715L4.00622 8.86353L6.06254 10.861L9.24985 5.91309L11.3812 9.22179L11.7761 8.6676L12.3379 9.45621V1.75842ZM6.22048 4.50869C6.22048 5.58193 5.35045 6.45196 4.27722 6.45196C3.20398 6.45196 2.33395 5.58193 2.33395 4.50869C2.33395 3.43546 3.20398 2.56543 4.27722 2.56543C5.35045 2.56543 6.22048 3.43546 6.22048 4.50869Z" fill="black"/></svg>\n',tooltip:!0}),o.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(o,"execute",(()=>{n(i,(({attributes:t})=>{e.execute("insertDrupalMedia",t)}),a)})),o}))}}class m extends e.Plugin{static get requires(){return[t.WidgetToolbarRepository]}static get pluginName(){return"DrupalMediaToolbar"}afterInit(){const{editor:e}=this;var i;e.plugins.get(t.WidgetToolbarRepository).register("drupalMedia",{ariaLabel:Drupal.t("Drupal Media toolbar"),items:(i=e.config.get("drupalMedia.toolbar"),i.map((e=>l(e)?e.name:e))||[]),getRelatedElement:e=>o(e)})}}const p="METADATA_ERROR";class g extends e.Command{refresh(){const e=this.editor.model.document.selection.getSelectedElement();this.isEnabled=r(e)&&e.getAttribute("drupalMediaIsImage")&&e.getAttribute("drupalMediaIsImage")!==p,r(e)&&e.hasAttribute("drupalMediaAlt")?this.value=e.getAttribute("drupalMediaAlt"):this.value=!1}execute(e){const{model:t}=this.editor,i=t.document.selection.getSelectedElement();e.newValue=e.newValue.trim(),t.change((t=>{e.newValue.length>0?t.setAttribute("drupalMediaAlt",e.newValue,i):t.removeAttribute("drupalMediaAlt",i)}))}}class h extends e.Plugin{init(){this._data=new WeakMap}getMetadata(e){if(this._data.get(e))return new Promise((t=>{t(this._data.get(e))}));const t=this.editor.config.get("drupalMedia");if(!t)return new Promise(((e,t)=>{t(new Error("drupalMedia configuration is required for parsing metadata."))}));if(!e.hasAttribute("drupalMediaEntityUuid"))return new Promise(((e,t)=>{t(new Error("drupalMedia element must have drupalMediaEntityUuid attribute to retrieve metadata."))}));const{metadataUrl:i}=t;return(async e=>{const t=await fetch(e);if(t.ok)return JSON.parse(await t.text());throw new Error("Fetching media embed metadata from the server failed.")})(`${i}&${new URLSearchParams({uuid:e.getAttribute("drupalMediaEntityUuid")})}`).then((t=>(this._data.set(e,t),t)))}static get pluginName(){return"DrupalMediaMetadataRepository"}}class f extends e.Plugin{static get requires(){return[h]}static get pluginName(){return"MediaImageTextAlternativeEditing"}_upcastDrupalMediaIsImage(e){const{model:t,plugins:i}=this.editor;i.get("DrupalMediaMetadataRepository").getMetadata(e).then((i=>{e&&t.enqueueChange("transparent",(t=>{t.setAttribute("drupalMediaIsImage",!!i.imageSourceMetadata,e)}))})).catch((i=>{e&&(console.warn(i.toString()),t.enqueueChange("transparent",(t=>{t.setAttribute("drupalMediaIsImage",p,e)})))}))}init(){const{editor:e,editor:{model:t,conversion:i}}=this;t.schema.extend("drupalMedia",{allowAttributes:["drupalMediaIsImage"]}),this.listenTo(t,"insertContent",((e,[t])=>{r(t)&&this._upcastDrupalMediaIsImage(t)})),i.for("upcast").add((e=>{e.on("element:drupal-media",((e,t)=>{const[i]=t.modelRange.getItems();r(i)&&this._upcastDrupalMediaIsImage(i)}),{priority:"lowest"})})),i.for("editingDowncast").add((e=>{e.on("attribute:drupalMediaIsImage",((e,t,i)=>{const{writer:n,mapper:a}=i,r=a.toViewElement(t.item);if(t.attributeNewValue!==p){const e=Array.from(r.getChildren()).find((e=>e.getCustomProperty("drupalMediaMetadataError")));return void(e&&(n.setCustomProperty("widgetLabel",e.getCustomProperty("drupalMediaOriginalWidgetLabel"),e),n.removeElement(e)))}const o=Drupal.t("Not all functionality may be available because some information could not be retrieved."),l=new u.TooltipView;l.text=o,l.position="sw";const s=new u.Template({tag:"span",children:[{tag:"span",attributes:{class:"drupal-media__metadata-error-icon"}},l]}).render(),d=n.createRawElement("div",{class:"drupal-media__metadata-error"},((e,t)=>{t.setContentOf(e,s.outerHTML)}));n.setCustomProperty("drupalMediaMetadataError",!0,d);const c=r.getCustomProperty("widgetLabel");n.setCustomProperty("drupalMediaOriginalWidgetLabel",c,d),n.setCustomProperty("widgetLabel",`${c} (${o})`,r),n.insert(n.createPositionAt(r,0),d)}),{priority:"low"})})),e.commands.add("mediaImageTextAlternative",new g(this.editor))}}function b(e){const t=e.editing.view,i=u.BalloonPanelView.defaultPositions;return{target:t.domConverter.viewToDom(t.document.selection.getSelectedElement()),positions:[i.northArrowSouth,i.northArrowSouthWest,i.northArrowSouthEast,i.southArrowNorth,i.southArrowNorthWest,i.southArrowNorthEast]}}var w=i("ckeditor5/src/utils.js");class v extends u.View{constructor(t){super(t),this.focusTracker=new w.FocusTracker,this.keystrokes=new w.KeystrokeHandler,this.labeledInput=this._createLabeledInputView(),this.saveButtonView=this._createButton(Drupal.t("Save"),e.icons.check,"ck-button-save"),this.saveButtonView.type="submit",this.cancelButtonView=this._createButton(Drupal.t("Cancel"),e.icons.cancel,"ck-button-cancel","cancel"),this._focusables=new u.ViewCollection,this._focusCycler=new u.FocusCycler({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.setTemplate({tag:"form",attributes:{class:["ck","ck-text-alternative-form","ck-responsive-form"],tabindex:"-1"},children:[this.labeledInput,this.saveButtonView,this.cancelButtonView]}),(0,u.injectCssTransitionDisabler)(this)}render(){super.render(),this.keystrokes.listenTo(this.element),(0,u.submitHandler)({view:this}),[this.labeledInput,this.saveButtonView,this.cancelButtonView].forEach((e=>{this._focusables.add(e),this.focusTracker.add(e.element)}))}_createButton(e,t,i,n){const a=new u.ButtonView(this.locale);return a.set({label:e,icon:t,tooltip:!0}),a.extendTemplate({attributes:{class:i}}),n&&a.delegate("execute").to(this,n),a}_createLabeledInputView(){const e=new u.LabeledFieldView(this.locale,u.createLabeledInputText);return e.label=Drupal.t("Override text alternative"),e}}class E extends e.Plugin{static get requires(){return[u.ContextualBalloon]}static get pluginName(){return"MediaImageTextAlternativeUi"}init(){this._createButton(),this._createForm()}destroy(){super.destroy(),this._form.destroy()}_createButton(){const t=this.editor;t.ui.componentFactory.add("mediaImageTextAlternative",(i=>{const n=t.commands.get("mediaImageTextAlternative"),a=new u.ButtonView(i);return a.set({label:Drupal.t("Override media image text alternative"),icon:e.icons.lowVision,tooltip:!0}),a.bind("isVisible").to(n,"isEnabled"),this.listenTo(a,"execute",(()=>{this._showForm()})),a}))}_createForm(){const e=this.editor,t=e.editing.view.document;this._balloon=this.editor.plugins.get("ContextualBalloon"),this._form=new v(e.locale),this._form.render(),this.listenTo(this._form,"submit",(()=>{e.execute("mediaImageTextAlternative",{newValue:this._form.labeledInput.fieldView.element.value}),this._hideForm(!0)})),this.listenTo(this._form,"cancel",(()=>{this._hideForm(!0)})),this._form.keystrokes.set("Esc",((e,t)=>{this._hideForm(!0),t()})),this.listenTo(e.ui,"update",(()=>{o(t.selection)?this._isVisible&&function(e){const t=e.plugins.get("ContextualBalloon");if(o(e.editing.view.document.selection)){const i=b(e);t.updatePosition(i)}}(e):this._hideForm(!0)})),(0,u.clickOutsideHandler)({emitter:this._form,activator:()=>this._isVisible,contextElements:[this._balloon.view.element],callback:()=>this._hideForm()})}_showForm(){if(this._isVisible)return;const e=this.editor,t=e.commands.get("mediaImageTextAlternative"),i=this._form.labeledInput;this._form.disableCssTransitions(),this._isInBalloon||this._balloon.add({view:this._form,position:b(e)}),i.fieldView.element.value=t.value||"",i.fieldView.value=i.fieldView.element.value,this._form.labeledInput.fieldView.select(),this._form.enableCssTransitions()}_hideForm(e){this._isInBalloon&&(this._form.focusTracker.isFocused&&this._form.saveButtonView.focus(),this._balloon.remove(this._form),e&&this.editor.editing.view.focus())}get _isVisible(){return this._balloon.visibleView===this._form}get _isInBalloon(){return this._balloon.hasView(this._form)}}class y extends e.Plugin{static get requires(){return[f,E]}static get pluginName(){return"MediaImageTextAlternative"}}function M(e,t,i){if(t.attributes)for(const[n,a]of Object.entries(t.attributes))e.setAttribute(n,a,i);t.styles&&e.setStyle(t.styles,i),t.classes&&e.addClass(t.classes,i)}function k(e){return t=>{t.on("element:drupal-media",((t,i,n)=>{const a=i.viewItem.parent;a.is("element","a")&&function(t,a){const r=e._consumeAllowedAttributes(t,n);r&&n.writer.setAttribute(a,r,i.modelRange)}(a,"htmlLinkAttributes")}),{priority:"low"})}}class C extends e.Plugin{init(){const{editor:e}=this;if(!e.plugins.has("GeneralHtmlSupport"))return;const{schema:t}=e.model,{conversion:i}=e,n=e.plugins.get("DataFilter");t.extend("drupalMedia",{allowAttributes:["htmlLinkAttributes"]}),i.for("upcast").add(k(n)),i.for("editingDowncast").add((e=>e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{if(!i.consumable.consume(t.item,"attribute:htmlLinkAttributes:drupalMedia"))return;const n=i.mapper.toViewElement(t.item),a=function(e,t,i){const n=e.createRangeOn(t);for(const{item:e}of n.getWalker())if(e.is("element",i))return e}(i.writer,n,"a");M(i.writer,t.item.getAttribute("htmlLinkAttributes"),a)}),{priority:"low"}))),i.for("dataDowncast").add((e=>e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{if(!i.consumable.consume(t.item,"attribute:htmlLinkAttributes:drupalMedia"))return;const n=i.mapper.toViewElement(t.item).parent;M(i.writer,t.item.getAttribute("htmlLinkAttributes"),n)}),{priority:"low"})))}static get pluginName(){return"DrupalMediaGeneralHtmlSupport"}}class _ extends e.Plugin{static get requires(){return[d,C,c,m,y]}static get pluginName(){return"DrupalMedia"}}function A(){return e=>{e.on("element:a",((e,t,i)=>{const n=t.viewItem,a=(r=n,Array.from(r.getChildren()).find((e=>"drupal-media"===e.name)));var r;if(!a)return;if(!i.consumable.consume(n,{attributes:["href"]}))return;const o=n.getAttribute("href");if(!o)return;const l=i.convertItem(a,t.modelCursor);t.modelRange=l.modelRange,t.modelCursor=l.modelCursor;const s=t.modelCursor.nodeBefore;s&&s.is("element","drupalMedia")&&i.writer.setAttribute("linkHref",o,s)}),{priority:"high"})}}class D extends e.Plugin{static get requires(){return["LinkEditing","DrupalMediaEditing"]}static get pluginName(){return"DrupalLinkMediaEditing"}init(){const{editor:e}=this;e.model.schema.extend("drupalMedia",{allowAttributes:["linkHref"]}),e.conversion.for("upcast").add(A()),e.conversion.for("editingDowncast").add((e=>{e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{const{writer:n}=i;if(!i.consumable.consume(t.item,e.name))return;const a=i.mapper.toViewElement(t.item),r=Array.from(a.getChildren()).find((e=>"a"===e.name));if(r)t.attributeNewValue?n.setAttribute("href",t.attributeNewValue,r):(n.move(n.createRangeIn(r),n.createPositionAt(a,0)),n.remove(r));else{const e=Array.from(a.getChildren()).find((e=>e.getAttribute("data-drupal-media-preview"))),i=n.createContainerElement("a",{href:t.attributeNewValue});n.insert(n.createPositionAt(a,0),i),n.move(n.createRangeOn(e),n.createPositionAt(i,0))}}),{priority:"high"})})),e.conversion.for("dataDowncast").add((e=>{e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{const{writer:n}=i;if(!i.consumable.consume(t.item,e.name))return;const a=i.mapper.toViewElement(t.item),r=n.createContainerElement("a",{href:t.attributeNewValue});n.insert(n.createPositionBefore(a),r),n.move(n.createRangeOn(a),n.createPositionAt(r,0))}),{priority:"high"})}))}}class x extends e.Plugin{static get requires(){return["LinkEditing","LinkUI","DrupalMediaEditing"]}static get pluginName(){return"DrupalLinkMediaUi"}init(){const{editor:e}=this,t=e.editing.view.document;this.listenTo(t,"click",((t,i)=>{this._isSelectedLinkedMedia(e.model.document.selection)&&(i.preventDefault(),t.stop())}),{priority:"high"}),this._createToolbarLinkMediaButton()}_createToolbarLinkMediaButton(){const{editor:e}=this;e.ui.componentFactory.add("drupalLinkMedia",(t=>{const i=new u.ButtonView(t),n=e.plugins.get("LinkUI"),a=e.commands.get("link");return i.set({isEnabled:!0,label:Drupal.t("Link media"),icon:'<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m11.077 15 .991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184z"/></svg>\n',keystroke:"Ctrl+K",tooltip:!0,isToggleable:!0}),i.bind("isEnabled").to(a,"isEnabled"),i.bind("isOn").to(a,"value",(e=>!!e)),this.listenTo(i,"execute",(()=>{this._isSelectedLinkedMedia(e.model.document.selection)?n._addActionsView():n._showUI(!0)})),i}))}_isSelectedLinkedMedia(e){const t=e.getSelectedElement();return!!t&&t.is("element","drupalMedia")&&t.hasAttribute("linkHref")}}class S extends e.Plugin{static get requires(){return[D,x]}static get pluginName(){return"DrupalLinkMedia"}}const{objectFullWidth:V,objectInline:I,objectLeft:T,objectRight:L,objectCenter:P,objectBlockLeft:B,objectBlockRight:N}=e.icons,O={inline:{name:"inline",title:"In line",icon:I,modelElements:["imageInline"],isDefault:!0},alignLeft:{name:"alignLeft",title:"Left aligned image",icon:T,modelElements:["imageBlock","imageInline"],className:"image-style-align-left"},alignBlockLeft:{name:"alignBlockLeft",title:"Left aligned image",icon:B,modelElements:["imageBlock"],className:"image-style-block-align-left"},alignCenter:{name:"alignCenter",title:"Centered image",icon:P,modelElements:["imageBlock"],className:"image-style-align-center"},alignRight:{name:"alignRight",title:"Right aligned image",icon:L,modelElements:["imageBlock","imageInline"],className:"image-style-align-right"},alignBlockRight:{name:"alignBlockRight",title:"Right aligned image",icon:N,modelElements:["imageBlock"],className:"image-style-block-align-right"},block:{name:"block",title:"Centered image",icon:P,modelElements:["imageBlock"],isDefault:!0},side:{name:"side",title:"Side image",icon:L,modelElements:["imageBlock"],className:"image-style-side"}},R={full:V,left:B,right:N,center:P,inlineLeft:T,inlineRight:L,inline:I},j=[{name:"imageStyle:wrapText",title:"Wrap text",defaultItem:"imageStyle:alignLeft",items:["imageStyle:alignLeft","imageStyle:alignRight"]},{name:"imageStyle:breakText",title:"Break text",defaultItem:"imageStyle:block",items:["imageStyle:alignBlockLeft","imageStyle:block","imageStyle:alignBlockRight"]}];function F(e){(0,w.logWarning)("image-style-configuration-definition-invalid",e)}var U={normalizeStyles:function(e){return(e.configuredStyles.options||[]).map((e=>function(e){e="string"==typeof e?O[e]?{...O[e]}:{name:e}:function(e,t){const i={...t};for(const n in e)Object.prototype.hasOwnProperty.call(t,n)||(i[n]=e[n]);return i}(O[e.name],e);"string"==typeof e.icon&&(e.icon=R[e.icon]||e.icon);return e}(e))).filter((t=>function(e,{isBlockPluginLoaded:t,isInlinePluginLoaded:i}){const{modelElements:n,name:a}=e;if(!(n&&n.length&&a))return F({style:e}),!1;{const a=[t?"imageBlock":null,i?"imageInline":null];if(!n.some((e=>a.includes(e))))return(0,w.logWarning)("image-style-missing-dependency",{style:e,missingPlugins:n.map((e=>"imageBlock"===e?"ImageBlockEditing":"ImageInlineEditing"))}),!1}return!0}(t,e)))},getDefaultStylesConfiguration:function(e,t){return e&&t?{options:["inline","alignLeft","alignRight","alignCenter","alignBlockLeft","alignBlockRight","block","side"]}:e?{options:["block","side"]}:t?{options:["inline","alignLeft","alignRight"]}:{}},getDefaultDropdownDefinitions:function(e){return e.has("ImageBlockEditing")&&e.has("ImageInlineEditing")?[...j]:[]},warnInvalidStyle:F,DEFAULT_OPTIONS:O,DEFAULT_ICONS:R,DEFAULT_DROPDOWN_DEFINITIONS:j};function H(e,t){const i=e.getSelectedElement();return i&&t.checkAttribute(i,"drupalElementStyle")?i:e.getFirstPosition().findAncestor((e=>t.checkAttribute(e,"drupalElementStyle")))}class W extends e.Command{constructor(e,t){super(e),this._styles=new Map(t.map((e=>[e.name,e])))}refresh(){const e=this.editor,t=H(e.model.document.selection,e.model.schema);this.isEnabled=!!t,this.isEnabled&&t.hasAttribute("drupalElementStyle")?this.value=t.getAttribute("drupalElementStyle"):this.value=!1}execute(e={}){const t=this.editor.model;t.change((i=>{const n=e.value,a=H(t.document.selection,t.schema);!n||this._styles.get(n).isDefault?i.removeAttribute("drupalElementStyle",a):i.setAttribute("drupalElementStyle",n,a)}))}}function q(e,t){for(const i of t)if(i.name===e)return i}class $ extends e.Plugin{init(){const t=this.editor;t.config.define("drupalElementStyles",{options:[]});const i=t.config.get("drupalElementStyles").options;this.normalizedStyles=i.map((t=>("string"==typeof t.icon&&e.icons[t.icon]&&(t.icon=e.icons[t.icon]),t))).filter((e=>e.attributeName&&e.attributeValue?e.modelElements&&Array.isArray(e.modelElements)?!!e.name||(console.warn("drupalElementStyles options must include a name."),!1):(console.warn("drupalElementStyles options must include an array of supported modelElements."),!1):(console.warn("drupalElementStyles options must include attributeName and attributeValue."),!1))),this._setupConversion(),t.commands.add("drupalElementStyle",new W(t,this.normalizedStyles))}_setupConversion(){const e=this.editor,t=e.model.schema,i=(n=this.normalizedStyles,(e,t,i)=>{if(!i.consumable.consume(t.item,e.name))return;const a=q(t.attributeNewValue,n),r=q(t.attributeOldValue,n),o=i.mapper.toViewElement(t.item),l=i.writer;r&&("class"===r.attributeName?l.removeClass(r.attributeValue,o):l.removeAttribute(r.attributeName,o)),a&&("class"===a.attributeName?l.addClass(a.attributeValue,o):l.setAttribute(a.attributeName,a.attributeValue,o))});var n;const a=function(e){const t=e.filter((e=>!e.isDefault));return(e,i,n)=>{if(!i.modelRange)return;const a=i.viewItem,r=(0,w.first)(i.modelRange.getItems());if(r&&n.schema.checkAttribute(r,"drupalElementStyle"))for(const e of t)if("class"===e.attributeName)n.consumable.consume(a,{classes:e.attributeValue})&&n.writer.setAttribute("drupalElementStyle",e.name,r);else if(n.consumable.consume(a,{attributes:[e.attributeName]}))for(const e of t)e.attributeValue===a.getAttribute(e.attributeName)&&n.writer.setAttribute("drupalElementStyle",e.name,r)}}(this.normalizedStyles);e.editing.downcastDispatcher.on("attribute:drupalElementStyle",i),e.data.downcastDispatcher.on("attribute:drupalElementStyle",i);[...new Set(this.normalizedStyles.map((e=>e.modelElements)).flat())].forEach((e=>{t.extend(e,{allowAttributes:"drupalElementStyle"})})),e.data.upcastDispatcher.on("element",a,{priority:"low"})}static get pluginName(){return"DrupalElementStyleEditing"}}const K=e=>e,z=(e,t)=>(e?`${e}: `:"")+t;function Z(e){return`drupalElementStyle:${e}`}class J extends e.Plugin{static get requires(){return[$]}init(){const e=this.editor.plugins,t=this.editor.config.get("drupalMedia.toolbar")||[],i=Object.values(e.get("DrupalElementStyleEditing").normalizedStyles);i.forEach((e=>{this._createButton(e)}));t.filter(l).forEach((e=>{this._createDropdown(e,i)}))}_createDropdown(e,t){const i=this.editor.ui.componentFactory;i.add(e.name,(n=>{let a;const{defaultItem:r,items:o,title:l}=e,s=o.filter((e=>t.find((({name:t})=>Z(t)===e)))).map((e=>{const t=i.create(e);return e===r&&(a=t),t}));o.length!==s.length&&U.warnInvalidStyle({dropdown:e});const d=(0,u.createDropdown)(n,u.SplitButtonView),c=d.buttonView;return(0,u.addToolbarToDropdown)(d,s),c.set({label:z(l,a.label),class:null,tooltip:!0}),c.bind("icon").toMany(s,"isOn",((...e)=>{const t=e.findIndex(K);return t<0?a.icon:s[t].icon})),c.bind("label").toMany(s,"isOn",((...e)=>{const t=e.findIndex(K);return z(l,t<0?a.label:s[t].label)})),c.bind("isOn").toMany(s,"isOn",((...e)=>e.some(K))),c.bind("class").toMany(s,"isOn",((...e)=>e.some(K)?"ck-splitbutton_flatten":null)),c.on("execute",(()=>{s.some((({isOn:e})=>e))?d.isOpen=!d.isOpen:a.fire("execute")})),d.bind("isEnabled").toMany(s,"isEnabled",((...e)=>e.some(K))),d}))}_createButton(e){const t=e.name;this.editor.ui.componentFactory.add(Z(t),(i=>{const n=this.editor.commands.get("drupalElementStyle"),a=new u.ButtonView(i);return a.set({label:e.title,icon:e.icon,tooltip:!0,isToggleable:!0}),a.bind("isEnabled").to(n,"isEnabled"),a.bind("isOn").to(n,"value",(e=>e===t)),a.on("execute",this._executeCommand.bind(this,t)),a}))}_executeCommand(e){this.editor.execute("drupalElementStyle",{value:e}),this.editor.editing.view.focus()}static get pluginName(){return"DrupalElementStyleUi"}}class G extends e.Plugin{static get requires(){return[$,J]}static get pluginName(){return"DrupalElementStyle"}}var X=i("ckeditor5/src/engine.js");function Q(e){for(const t of e.getChildren())if(t&&t.is("element","caption"))return t;return null}class Y extends e.Command{refresh(){const e=this.editor.model.document.selection.getSelectedElement();this.isEnabled=r(e),this.isEnabled?this.value=!!Q(e):this.value=!1}execute(e={}){const{focusCaptionOnShow:t}=e;this.editor.model.change((e=>{this.value?this._hideDrupalMediaCaption(e):this._showDrupalMediaCaption(e,t)}))}_showDrupalMediaCaption(e,t){const i=this.editor.model.document.selection,n=this.editor.plugins.get("DrupalMediaCaptionEditing"),a=i.getSelectedElement(),r=n._getSavedCaption(a)||e.createElement("caption");e.append(r,a),t&&e.setSelection(r,"in")}_hideDrupalMediaCaption(e){const t=this.editor,i=t.model.document.selection,n=t.plugins.get("DrupalMediaCaptionEditing"),a=i.getSelectedElement();if(a){const t=Q(a);n._saveCaption(a,t),e.setSelection(a,"on"),e.remove(t)}}}class ee extends e.Plugin{static get requires(){return[]}static get pluginName(){return"DrupalMediaCaptionEditing"}constructor(e){super(e),this._savedCaptionsMap=new WeakMap}init(){const e=this.editor,t=e.model.schema;t.isRegistered("caption")?t.extend("caption",{allowIn:"drupalMedia"}):t.register("caption",{allowIn:"drupalMedia",allowContentOf:"$block",isLimit:!0}),e.commands.add("toggleMediaCaption",new Y(e)),this._setupConversion()}_setupConversion(){const e=this.editor,i=e.editing.view;var n;e.conversion.for("upcast").add(function(e){const t=(t,i,n)=>{const{viewItem:a}=i,{writer:r,consumable:o}=n;if(!i.modelRange||!o.consume(a,{attributes:["data-caption"]}))return;const l=r.createElement("caption"),s=i.modelRange.start.nodeAfter,d=e.data.processor.toView(a.getAttribute("data-caption")),u=r.createDocumentFragment();n.consumable.constructor.createFrom(d,n.consumable),n.convertChildren(d,u);for(const e of Array.from(u.getChildren()))r.append(e,l);r.append(l,s)};return e=>{e.on("element:drupal-media",t,{priority:"low"})}}(e)),e.conversion.for("editingDowncast").elementToElement({model:"caption",view:(e,{writer:n})=>{if(!r(e.parent))return null;const a=n.createEditableElement("figcaption");return(0,X.enablePlaceholder)({view:i,element:a,text:Drupal.t("Enter media caption"),keepOnFocus:!0}),(0,t.toWidgetEditable)(a,n)}}),e.editing.mapper.on("modelToViewPosition",(n=i,(e,t)=>{const i=t.modelPosition,a=i.parent;if(!r(a))return;const o=t.mapper.toViewElement(a);t.viewPosition=n.createPositionAt(o,i.offset+1)})),e.conversion.for("dataDowncast").add(function(e){return t=>{t.on("insert:caption",((t,i,n)=>{const{consumable:a,writer:o,mapper:l}=n;if(!r(i.item.parent)||!a.consume(i.item,"insert"))return;const s=e.model.createRangeIn(i.item),d=o.createDocumentFragment();l.bindElements(i.item,d);for(const{item:t}of Array.from(s)){const i={item:t,range:e.model.createRangeOn(t)},a=`insert:${t.name||"$text"}`;e.data.downcastDispatcher.fire(a,i,n);for(const a of t.getAttributeKeys())Object.assign(i,{attributeKey:a,attributeOldValue:null,attributeNewValue:i.item.getAttribute(a)}),e.data.downcastDispatcher.fire(`attribute:${a}`,i,n)}for(const e of o.createRangeIn(d).getItems())l.unbindViewElement(e);l.unbindViewElement(d);const u=e.data.processor.toData(d);if(u){const e=l.toViewElement(i.item.parent);o.setAttribute("data-caption",u,e)}}))}}(e))}_getSavedCaption(e){const t=this._savedCaptionsMap.get(e);return t?X.Element.fromJSON(t):null}_saveCaption(e,t){this._savedCaptionsMap.set(e,t.toJSON())}}class te extends e.Plugin{static get requires(){return[]}static get pluginName(){return"DrupalMediaCaptionUI"}init(){const{editor:t}=this,i=t.editing.view;t.ui.componentFactory.add("toggleDrupalMediaCaption",(n=>{const a=new u.ButtonView(n),r=t.commands.get("toggleMediaCaption");return a.set({label:Drupal.t("Caption media"),icon:e.icons.caption,tooltip:!0,isToggleable:!0}),a.bind("isOn","isEnabled").to(r,"value","isEnabled"),a.bind("label").to(r,"value",(e=>e?Drupal.t("Toggle caption off"):Drupal.t("Toggle caption on"))),this.listenTo(a,"execute",(()=>{t.execute("toggleMediaCaption",{focusCaptionOnShow:!0});const e=t.model.document.selection.getFirstPosition().findAncestor("caption");if(e){const n=t.editing.mapper.toViewElement(e);i.scrollToTheSelection(),i.change((e=>{e.addClass("drupal-media__caption_highlighted",n)}))}})),a}))}}class ie extends e.Plugin{static get requires(){return[ee,te]}static get pluginName(){return"DrupalMediaCaption"}}var ne={DrupalMedia:_,MediaImageTextAlternative:y,MediaImageTextAlternativeEditing:f,MediaImageTextAlternativeUi:E,DrupalLinkMedia:S,DrupalMediaCaption:ie,DrupalElementStyle:G}}(),n=n.default}()})); \ No newline at end of file diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalImage/src/drupalimageediting.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalImage/src/drupalimageediting.js index 734d2b74b2fc71bb077b92c6024c7a7641d0f3b0..731e47f78b59aca768c6bebc4cb0078ba03dee1a 100644 --- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalImage/src/drupalimageediting.js +++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalImage/src/drupalimageediting.js @@ -51,7 +51,12 @@ function viewCaptionToCaptionAttribute(editor) { 'insert:caption', (evt, data, conversionApi) => { const { consumable, writer, mapper } = conversionApi; - if (!consumable.consume(data.item, 'insert')) { + const imageUtils = editor.plugins.get('ImageUtils'); + + if ( + !imageUtils.isImage(data.item.parent) || + !consumable.consume(data.item, 'insert') + ) { return; } @@ -434,6 +439,10 @@ function downcastBlockImageLink() { * @internal */ export default class DrupalImageEditing extends Plugin { + static get requires() { + return ['ImageUtils']; + } + /** * @inheritdoc */ diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption.js new file mode 100644 index 0000000000000000000000000000000000000000..65950738c13cb31e1dce6f987b3622a484fce2e6 --- /dev/null +++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption.js @@ -0,0 +1,18 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* cspell:words drupalmediacaption drupalmediacaptionediting drupalmediacaptionui */ +import { Plugin } from 'ckeditor5/src/core'; +import DrupalMediaCaptionEditing from './drupalmediacaption/drupalmediacaptionediting'; +import DrupalMediaCaptionUI from './drupalmediacaption/drupalmediacaptionui'; + +/** + * @internal + */ +export default class DrupalMediaCaption extends Plugin { + static get requires() { + return [DrupalMediaCaptionEditing, DrupalMediaCaptionUI]; + } + + static get pluginName() { + return 'DrupalMediaCaption'; + } +} diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptioncommand.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptioncommand.js new file mode 100644 index 0000000000000000000000000000000000000000..a735b6ab86345f1f3f445fcaee3434be81d9d35d --- /dev/null +++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptioncommand.js @@ -0,0 +1,139 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* cspell:words imagecaption */ +import { Command } from 'ckeditor5/src/core'; +import { isDrupalMedia } from '../utils'; + +/** + * Gets the caption model element from the media model selection. + * + * @param {module:engine/model/element~Element} drupalMediaModelElement + * The model element from which caption should be retrieved. + * @returns {module:engine/model/element~Element|null} + * The caption element or `null` if the selection has no child caption + * element. + */ +export function getCaptionFromDrupalMediaModelElement(drupalMediaModelElement) { + // eslint-disable-next-line no-restricted-syntax + for (const node of drupalMediaModelElement.getChildren()) { + if (!!node && node.is('element', 'caption')) { + return node; + } + } + + return null; +} + +/** + * The toggle Drupal Media caption command. + * + * This command either adds or removes the caption of a selected drupalMedia + * element. + * + * This is inspired by the CKEditor 5 image caption plugin. + * + * @see module:image/imagecaption~ImageCaption + * + * @extends module:core/command~Command + * + * @internal + */ +export default class ToggleDrupalMediaCaptionCommand extends Command { + /** + * @inheritDoc + */ + refresh() { + const element = this.editor.model.document.selection.getSelectedElement(); + + this.isEnabled = isDrupalMedia(element); + + if (!this.isEnabled) { + this.value = false; + } else { + this.value = !!getCaptionFromDrupalMediaModelElement(element); + } + } + + /** + * Executes the command. + * + * @example + * editor.execute('toggleMediaCaption'); + * + * @param {Object} [options] + * Options for the executed command. + * @param {String} [options.focusCaptionOnShow] + * When true and the caption shows up, the selection will be moved into it + * When true: If a caption is present, the selection will be moved to that + * caption immediately. + * + * @fires execute + */ + execute(options = {}) { + const { focusCaptionOnShow } = options; + this.editor.model.change((writer) => { + if (this.value) { + this._hideDrupalMediaCaption(writer); + } else { + this._showDrupalMediaCaption(writer, focusCaptionOnShow); + } + }); + } + + /** + * Shows the caption of a selected drupalMedia element. + * + * This also attempts to restore the caption content from the + * `DrupalMediaEditing` caption registry. If the `focusCaptionOnShow` option + * is true, the selection is immediately moved to the caption. + * + * @param {module:engine/model/writer~Writer} writer + * The model writer. + * @param {bool} focusCaptionOnShow + * Flag indicating whether the caption should be focused. + */ + _showDrupalMediaCaption(writer, focusCaptionOnShow) { + const model = this.editor.model; + const selection = model.document.selection; + const mediaCaptionEditing = this.editor.plugins.get( + 'DrupalMediaCaptionEditing', + ); + const selectedMedia = selection.getSelectedElement(); + const savedCaption = mediaCaptionEditing._getSavedCaption(selectedMedia); + + // Try restoring the caption from the DrupalMediaCaptionEditing plugin storage. + const newCaptionElement = savedCaption || writer.createElement('caption'); + + writer.append(newCaptionElement, selectedMedia); + + if (focusCaptionOnShow) { + writer.setSelection(newCaptionElement, 'in'); + } + } + + /** + * Hides the caption of a selected drupalMedia element. + * + * The content of the caption is stored in the `DrupalMediaCaptionEditing` + * caption registry to make this a reversible action. + * + * @param {module:engine/model/writer~Writer} writer + * The model writer. + */ + _hideDrupalMediaCaption(writer) { + const editor = this.editor; + const selection = editor.model.document.selection; + const mediaCaptionEditing = editor.plugins.get('DrupalMediaCaptionEditing'); + const selectedMedia = selection.getSelectedElement(); + + if (selectedMedia) { + const captionElement = + getCaptionFromDrupalMediaModelElement(selectedMedia); + + // Store the caption content so it can be restored quickly if the user + // changes their mind. + mediaCaptionEditing._saveCaption(selectedMedia, captionElement); + writer.setSelection(selectedMedia, 'on'); + writer.remove(captionElement); + } + } +} diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptionediting.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptionediting.js new file mode 100644 index 0000000000000000000000000000000000000000..f36a93414470bb7edc95e54c12d0d1b33cf983dc --- /dev/null +++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptionediting.js @@ -0,0 +1,313 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* cspell:words insertdrupalmedia JSONified drupalmediacaptioncommand downcasted */ +import { Plugin } from 'ckeditor5/src/core'; +import { Element, enablePlaceholder } from 'ckeditor5/src/engine'; +import { toWidgetEditable } from 'ckeditor5/src/widget'; +import { isDrupalMedia } from '../utils'; +import ToggleDrupalMediaCaptionCommand from './drupalmediacaptioncommand'; + +/** + * A view to model converter for Drupal Media caption. + * + * This upcasts the `data-caption` attribute from `<drupal-media>` elements into + * a `<caption>` model element. This is converted into a model element instead of + * a model attribute in order to leverage CKEditor 5 built-in editing. + * + * @param {module:core/editor/editor~Editor} editor + * Editor on which this converter will be used. + * @return {function} + * A function that attaches converter to the dispatcher. + */ +function viewToModelCaption(editor) { + const converter = (evt, data, conversionApi) => { + const { viewItem } = data; + const { writer, consumable } = conversionApi; + if ( + !data.modelRange || + !consumable.consume(viewItem, { attributes: ['data-caption'] }) + ) { + return; + } + + const caption = writer.createElement('caption'); + const drupalMedia = data.modelRange.start.nodeAfter; + + // Parse HTML from data-caption attribute and upcast it to model fragment. + const viewFragment = editor.data.processor.toView( + viewItem.getAttribute('data-caption'), + ); + const modelFragment = writer.createDocumentFragment(); + + // Consumable must know about those newly parsed view elements. + conversionApi.consumable.constructor.createFrom( + viewFragment, + conversionApi.consumable, + ); + conversionApi.convertChildren(viewFragment, modelFragment); + + // Insert caption model nodes into the caption. + // eslint-disable-next-line no-restricted-syntax + for (const child of Array.from(modelFragment.getChildren())) { + writer.append(child, caption); + } + + // Insert the caption element into drupalMedia, as a last child. + writer.append(caption, drupalMedia); + }; + + return (dispatcher) => { + dispatcher.on('element:drupal-media', converter, { priority: 'low' }); + }; +} + +/** + * Gets mapper function for repositioning the `<figcaption>` element. + * + * @param {module:engine/view/view~View} editingView + * The editing view. + * @return {function} + * A mapper callback that moves `<figcaption>` element after the Drupal Media + * preview. + */ +function mapModelPositionToView(editingView) { + return (evt, data) => { + const modelPosition = data.modelPosition; + const parent = modelPosition.parent; + + if (!isDrupalMedia(parent)) { + return; + } + + const viewElement = data.mapper.toViewElement(parent); + data.viewPosition = editingView.createPositionAt( + viewElement, + modelPosition.offset + 1, + ); + }; +} + +/** + * A model to view converter for Drupal Media caption. + * + * This downcasts the `<caption>` model element into `data-caption` attribute in + * the view. + * + * @param {module:core/editor/editor~Editor} editor + * Editor on which this converter will be used. + * @return {function} + * A function that attaches converter to the dispatcher. + */ +function modelCaptionToCaptionAttribute(editor) { + return (dispatcher) => { + dispatcher.on('insert:caption', (evt, data, conversionApi) => { + const { consumable, writer, mapper } = conversionApi; + + if ( + !isDrupalMedia(data.item.parent) || + !consumable.consume(data.item, 'insert') + ) { + return; + } + + const range = editor.model.createRangeIn(data.item); + const viewDocumentFragment = writer.createDocumentFragment(); + + // Bind caption model element to the detached view document fragment so + // all content of the caption will be downcasted into that document + // fragment. + mapper.bindElements(data.item, viewDocumentFragment); + + // eslint-disable-next-line no-restricted-syntax + for (const { item } of Array.from(range)) { + const itemData = { + item, + range: editor.model.createRangeOn(item), + }; + + // The following lines are extracted from + // DowncastDispatcher._convertInsertWithAttributes(). + const eventName = `insert:${item.name || '$text'}`; + + editor.data.downcastDispatcher.fire(eventName, itemData, conversionApi); + + // eslint-disable-next-line no-restricted-syntax + for (const key of item.getAttributeKeys()) { + Object.assign(itemData, { + attributeKey: key, + attributeOldValue: null, + attributeNewValue: itemData.item.getAttribute(key), + }); + + editor.data.downcastDispatcher.fire( + `attribute:${key}`, + itemData, + conversionApi, + ); + } + } + + // Unbind all the view elements that were downcasted to the document + // fragment. + // eslint-disable-next-line no-restricted-syntax + for (const child of writer + .createRangeIn(viewDocumentFragment) + .getItems()) { + mapper.unbindViewElement(child); + } + + mapper.unbindViewElement(viewDocumentFragment); + + // Stringify view document fragment to HTML string. + const captionText = editor.data.processor.toData(viewDocumentFragment); + + if (captionText) { + const imageViewElement = mapper.toViewElement(data.item.parent); + + writer.setAttribute('data-caption', captionText, imageViewElement); + } + }); + }; +} + +/** + * The Drupal Media caption editing plugin. + * + * @extends module:core/plugin~Plugin + * + * @internal + */ +export default class DrupalMediaCaptionEditing extends Plugin { + /** + * @inheritDoc + */ + static get requires() { + return []; + } + + /** + * @inheritDoc + */ + static get pluginName() { + return 'DrupalMediaCaptionEditing'; + } + + /** + * @inheritDoc + */ + constructor(editor) { + super(editor); + + /** + * A map of saved Drupal Media captions and related model elements. + * + * @member {WeakMap.<module:engine/model/element~Element,Object>} + * + * @see _saveCaption + */ + this._savedCaptionsMap = new WeakMap(); + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const schema = editor.model.schema; + + // Schema configuration. + if (!schema.isRegistered('caption')) { + schema.register('caption', { + allowIn: 'drupalMedia', + allowContentOf: '$block', + isLimit: true, + }); + } else { + schema.extend('caption', { + allowIn: 'drupalMedia', + }); + } + + editor.commands.add( + 'toggleMediaCaption', + new ToggleDrupalMediaCaptionCommand(editor), + ); + + this._setupConversion(); + } + + /** + * Initializes upcasting and downcasting Drupal Media captions. + */ + _setupConversion() { + const editor = this.editor; + const view = editor.editing.view; + + // View -> model converter for the data pipeline. + editor.conversion.for('upcast').add(viewToModelCaption(editor)); + + // Model -> Editing View converter for the data pipeline. + editor.conversion.for('editingDowncast').elementToElement({ + model: 'caption', + view: (modelElement, { writer }) => { + if (!isDrupalMedia(modelElement.parent)) { + return null; + } + + const figcaptionElement = writer.createEditableElement('figcaption'); + + enablePlaceholder({ + view, + element: figcaptionElement, + text: Drupal.t('Enter media caption'), + keepOnFocus: true, + }); + + return toWidgetEditable(figcaptionElement, writer); + }, + }); + // The `<caption>` element inside the Drupal Media wrapper is by default + // placed before the preview. This rearranges the elements so that + // `<caption>` is rendered after the preview. + editor.editing.mapper.on( + 'modelToViewPosition', + mapModelPositionToView(view), + ); + + // Model -> Data converter for the data pipeline. + editor.conversion + .for('dataDowncast') + .add(modelCaptionToCaptionAttribute(editor)); + } + + /** + * Returns the saved caption of a Drupal Media model element. + * + * @param {module:engine/model/element~Element} drupalMediaModelElement + * The model element the caption should be returned for. + * @return {module:engine/model/element~Element|null} + * The model caption element or `null` if there is none. + */ + _getSavedCaption(drupalMediaModelElement) { + const jsonObject = this._savedCaptionsMap.get(drupalMediaModelElement); + + return jsonObject ? Element.fromJSON(jsonObject) : null; + } + + /** + * Saves Drupal Media element caption to allow restoring it in the future. + * + * A caption is saved every time it gets hidden and/or the type of an Drupal + * Media changes. The user should be able to restore it on demand. + * + * @param {module:engine/model/element~Element} drupalMediaModelElement + * The model element the caption is saved for. + * @param {module:engine/model/element~Element} caption + * The caption model element to be saved. + * + * @see _getSavedCaption + * @see module:engine/model/element~Element#toJSON + */ + _saveCaption(drupalMediaModelElement, caption) { + this._savedCaptionsMap.set(drupalMediaModelElement, caption.toJSON()); + } +} diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptionui.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptionui.js new file mode 100644 index 0000000000000000000000000000000000000000..d6a7a55f72aa04309bdfc993a80275ce92f83695 --- /dev/null +++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptionui.js @@ -0,0 +1,77 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { Plugin, icons } from 'ckeditor5/src/core'; +import { ButtonView } from 'ckeditor5/src/ui'; + +/** + * The caption media UI plugin. + * + * @internal + */ +export default class DrupalMediaCaptionUI extends Plugin { + /** + * @inheritdoc + */ + static get requires() { + return []; + } + + /** + * @inheritdoc + */ + static get pluginName() { + return 'DrupalMediaCaptionUI'; + } + + /** + * @inheritdoc + */ + init() { + const { editor } = this; + const editingView = editor.editing.view; + editor.ui.componentFactory.add('toggleDrupalMediaCaption', (locale) => { + const button = new ButtonView(locale); + const captionCommand = editor.commands.get('toggleMediaCaption'); + button.set({ + label: Drupal.t('Caption media'), + icon: icons.caption, + tooltip: true, + isToggleable: true, + }); + + // Bind button isOn and isEnabled properties to the command. + button.bind('isOn', 'isEnabled').to(captionCommand, 'value', 'isEnabled'); + + button + .bind('label') + .to(captionCommand, 'value', (value) => + value + ? Drupal.t('Toggle caption off') + : Drupal.t('Toggle caption on'), + ); + + this.listenTo(button, 'execute', () => { + editor.execute('toggleMediaCaption', { focusCaptionOnShow: true }); + + // If a caption is present, highlight it and scroll to the selection. + const modelCaptionElement = editor.model.document.selection + .getFirstPosition() + .findAncestor('caption'); + if (modelCaptionElement) { + const figcaptionElement = + editor.editing.mapper.toViewElement(modelCaptionElement); + + editingView.scrollToTheSelection(); + + editingView.change((writer) => { + writer.addClass( + 'drupal-media__caption_highlighted', + figcaptionElement, + ); + }); + } + }); + + return button; + }); + } +} diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js index 27029c3b2b558c0a040c641730133ecd0622b492..454de1886f9ab8cc5808d8fee6c0f083d613f50c 100644 --- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js +++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js @@ -22,7 +22,6 @@ export default class DrupalMediaEditing extends Plugin { init() { this.attrs = { drupalMediaAlt: 'alt', - drupalMediaCaption: 'data-caption', drupalMediaEntityType: 'data-entity-type', drupalMediaEntityUuid: 'data-entity-uuid', drupalMediaViewMode: 'data-view-mode', @@ -116,7 +115,7 @@ export default class DrupalMediaEditing extends Plugin { .elementToElement({ model: 'drupalMedia', view: (modelElement, { writer }) => { - const container = writer.createContainerElement('div', { + const container = writer.createContainerElement('figure', { class: 'drupal-media', }); if (!this.previewUrl) { diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/index.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/index.js index 25fb43d0cfce29951c11eb5fafb28208681e7ce7..3610787a04025865067cb53debb2a7c03a7b1b64 100644 --- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/index.js +++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/index.js @@ -1,5 +1,5 @@ /* eslint-disable import/no-extraneous-dependencies */ -// cspell:ignore mediaimagetextalternative +// cspell:ignore mediaimagetextalternative drupalmediacaption import DrupalMedia from './drupalmedia'; @@ -9,6 +9,8 @@ import DrupalLinkMedia from './drupallinkmedia/drupallinkmedia'; // cspell:ignore drupalelementstyle import DrupalElementStyle from './drupalelementstyle'; +import DrupalMediaCaption from './drupalmediacaption'; + // cspell:ignore mediaimagetextalternative import MediaImageTextAlternative from './mediaimagetextalternative'; import MediaImageTextAlternativeEditing from './mediaimagetextalternative/mediaimagetextalternativeediting'; @@ -23,5 +25,6 @@ export default { MediaImageTextAlternativeEditing, MediaImageTextAlternativeUi, DrupalLinkMedia, + DrupalMediaCaption, DrupalElementStyle, }; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php index ea7183764ecc496f3c203bb4d56a3af9b1eeb522..8e9da31cf0bb42d62e2612edff0c7d3c7222edce 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php @@ -432,44 +432,4 @@ public function testEmphasis() { $assert_session->responseContains('<p>This is a <em>test!</em></p>'); } - /** - * Ensures that images can have caption set. - */ - public function testImageCaption() { - $page = $this->getSession()->getPage(); - $assert_session = $this->assertSession(); - - // Add a node with text rendered via the Plain Text format. - $this->drupalGet('node/add'); - $page->fillField('title[0][value]', 'My test content'); - // Add image with data-caption. The foo attribute is added to be removed - // later by CKEditor to make sure CKEditor was able to downcast data. - $page->fillField('body[0][value]', '<img src="/sites/default/files/alpaca.jpg" data-caption="Alpacas <em>are</em> cute" foo="bar">'); - $page->pressButton('Save'); - - $this->createNewTextFormat($page, $assert_session); - $this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-uploadImage')); - $this->triggerKeyUp('.ckeditor5-toolbar-item-uploadImage', 'ArrowDown'); - $assert_session->assertWaitOnAjaxRequest(); - $page->clickLink('Image Upload'); - $assert_session->waitForText('Enable image uploads'); - $page->checkField('editor[settings][plugins][ckeditor5_imageUpload][status]'); - $assert_session->assertWaitOnAjaxRequest(); - $page->checkField('filters[filter_caption][status]'); - $assert_session->assertWaitOnAjaxRequest(); - $this->saveNewTextFormat($page, $assert_session); - - $this->drupalGet('node/1/edit'); - $page->selectFieldOption('body[0][format]', 'ckeditor5'); - $this->assertNotEmpty($assert_session->waitForText('Change text format?')); - $page->pressButton('Continue'); - - $this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor')); - $page->pressButton('Save'); - - $this->assertEquals('<img src="/sites/default/files/alpaca.jpg" data-caption="Alpacas <em>are</em> cute">', Node::load(1)->get('body')->value); - $assert_session->elementExists('xpath', '//figure/img[@src="/sites/default/files/alpaca.jpg" and not(@data-caption)]'); - $assert_session->responseContains('<figcaption>Alpacas <em>are</em> cute</figcaption>'); - } - } diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php index deb8733b23ae19a5de766f79cbad65f65e9e91ed..4f3301a6bc3a9929013dcee3eaa19e1135805d8b 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php @@ -5,6 +5,7 @@ use Drupal\editor\Entity\Editor; use Drupal\file\Entity\File; use Drupal\filter\Entity\FilterFormat; +use Drupal\node\Entity\Node; use Drupal\Tests\TestFileCreationTrait; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; use Drupal\ckeditor5\Plugin\Editor\CKEditor5; @@ -70,7 +71,7 @@ protected function setUp(): void { 'filter_html' => [ 'status' => TRUE, 'settings' => [ - 'allowed_html' => '<p> <br> <a href> <img src alt data-entity-uuid data-entity-type height width data-caption data-align>', + 'allowed_html' => '<p> <br> <em> <a href> <img src alt data-entity-uuid data-entity-type height width data-caption data-align>', ], ], 'filter_align' => ['status' => TRUE], @@ -86,6 +87,7 @@ protected function setUp(): void { 'uploadImage', 'sourceEditing', 'link', + 'italic', ], ], 'plugins' => [ @@ -357,6 +359,37 @@ public function testWidth(string $width): void { $this->assertSame($width, $width_from_editor); } + /** + * Ensures that images can have caption set. + */ + public function testImageCaption() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + // The foo attribute is added to be removed later by CKEditor 5 to make sure + // CKEditor 5 was able to downcast data. + $img_tag = '<img alt="drupalimage test image" data-caption="Alpacas <em>are</em> cute" foo="bar" data-entity-type="file" data-entity-uuid="' . $this->file->uuid() . '" src="' . $this->file->createFileUrl() . '">'; + $this->host->body->value = $img_tag; + $this->host->save(); + + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + + $this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor')); + $this->assertNotEmpty($figcaption = $assert_session->waitForElement('css', '.image figcaption')); + $this->assertSame('Alpacas <em>are</em> cute', $figcaption->getHtml()); + $page->pressButton('Source'); + $editor_dom = $this->getEditorDataAsDom(); + $data_caption = $editor_dom->getElementsByTagName('img')->item(0)->getAttribute('data-caption'); + $this->assertSame('Alpacas <em>are</em> cute', $data_caption); + + $page->pressButton('Save'); + + $this->assertEquals('<img src="' . $this->file->createFileUrl() . '" data-entity-uuid="' . $this->file->uuid() . '" data-entity-type="file" alt="drupalimage test image" data-caption="Alpacas <em>are</em> cute">', Node::load(1)->get('body')->value); + $assert_session->elementExists('xpath', '//figure/img[@src="' . $this->file->createFileUrl() . '" and not(@data-caption)]'); + $assert_session->responseContains('<figcaption>Alpacas <em>are</em> cute</figcaption>'); + } + /** * Data provider for ::testWidth(). * diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php index d93a7d9975b78b590bbf41897b426e7083c0b2aa..347e8d6ce73ce97e6c222da7b246d5e79fad5cd6 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php @@ -78,7 +78,7 @@ protected function setUp(): void { 'filter_html' => [ 'status' => TRUE, 'settings' => [ - 'allowed_html' => '<p> <br> <a href> <drupal-media data-entity-type data-entity-uuid data-align alt>', + 'allowed_html' => '<p> <br> <strong> <em> <a href> <drupal-media data-entity-type data-entity-uuid data-align data-caption alt>', ], ], 'filter_align' => ['status' => TRUE], @@ -94,6 +94,8 @@ protected function setUp(): void { 'items' => [ 'sourceEditing', 'link', + 'bold', + 'italic', ], ], 'plugins' => [ @@ -298,8 +300,99 @@ public function testPreviewUsesDefaultThemeAndIsClientCacheable() { * Tests caption editing in the CKEditor widget. */ public function testEditableCaption() { - // @todo Port in https://www.drupal.org/project/ckeditor5/issues/3246385 - $this->markTestSkipped('Blocked on https://www.drupal.org/project/ckeditor5/issues/3246385.'); + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + // Test that setting caption to blank string doesn't break 'Edit media' + // button. + $original_value = $this->host->body->value; + $this->host->body->value = str_replace('data-caption="baz"', 'data-caption=""', $original_value); + $this->host->save(); + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + // Wait for the media preview to load. + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-widget.drupal-media img')); + $assert_session->elementExists('css', '[data-drupal-media-preview][aria-label="Screaming hairy armadillo"]'); + $assert_session->elementContains('css', 'figcaption', ''); + $assert_session->elementAttributeContains('css', 'figcaption', 'data-placeholder', 'Enter media caption'); + + // Test if you leave the caption blank, but change another attribute, + // such as the alt text, the editable caption is still there and the edit + // button still exists. + $this->click('.ck-widget.drupal-media'); + $this->assertVisibleBalloon('[aria-label="Drupal Media toolbar"]'); + // Click the "Override media image text alternative" button. + $this->getBalloonButton('Override media image text alternative')->click(); + $this->assertVisibleBalloon('.ck-text-alternative-form'); + $alt_override_input = $page->find('css', '.ck-balloon-panel .ck-text-alternative-form input[type=text]'); + + // Fill in the alt field and submit. + $alt_override_input->setValue('Gold star for robot boy.'); + $this->getBalloonButton('Save')->click(); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.drupal-media img[alt*="Gold star for robot boy."]')); + $this->assertEquals('', $assert_session->waitForElement('css', '.drupal-media figcaption')->getText()); + $assert_session->elementAttributeContains('css', '.drupal-media figcaption', 'data-placeholder', 'Enter media caption'); + + // Restore caption in saved body value. + $original_value = $this->host->body->value; + $this->host->body->value = str_replace('data-caption=""', 'data-caption="baz"', $original_value); + $this->host->save(); + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-widget.drupal-media img')); + $this->assertNotEmpty($figcaption = $assert_session->waitForElement('css', '.drupal-media figcaption')); + $this->assertSame('baz', $figcaption->getHtml()); + + // Ensure that caption can be toggled off from the toolbar. + $this->click('.ck-widget.drupal-media'); + $this->assertVisibleBalloon('[aria-label="Drupal Media toolbar"]'); + $this->getBalloonButton('Toggle caption off')->click(); + $assert_session->assertNoElementAfterWait('css', 'figcaption'); + + // Ensure that caption can be toggled on from the toolbar. + $this->click('.ck-widget.drupal-media'); + $this->assertVisibleBalloon('[aria-label="Drupal Media toolbar"]'); + $this->getBalloonButton('Toggle caption on')->click(); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.drupal-media figcaption')); + + // Type into the widget's caption element. + $figcaption->setValue('Llamas are the most awesome ever'); + $editor_dom = $this->getEditorDataAsDom(); + $this->assertEquals('Llamas are the most awesome ever', $editor_dom->getElementsByTagName('drupal-media')->item(0)->getAttribute('data-caption')); + + // Ensure that the caption can be changed to bold. + $this->assertNotEmpty($figcaption = $assert_session->waitForElement('css', '.drupal-media figcaption')); + $this->selectTextInsideElement('.drupal-media figcaption'); + $this->assertNotEmpty($assert_session->waitForElement('css', '.drupal-media figcaption.ck-editor__nested-editable')); + $this->pressEditorButton('Bold'); + $this->assertNotEmpty($assert_session->waitForElement('css', '.drupal-media figcaption > strong')); + $this->assertEquals('<strong>Llamas are the most awesome ever</strong>', $figcaption->getHtml()); + $editor_dom = $this->getEditorDataAsDom(); + $this->assertEquals('<strong>Llamas are the most awesome ever</strong>', $editor_dom->getElementsByTagName('drupal-media')->item(0)->getAttribute('data-caption')); + + // Ensure that bold can be removed from the caption. + $this->assertNotEmpty($assert_session->waitForElement('css', '.drupal-media figcaption > strong')); + $this->selectTextInsideElement('.drupal-media figcaption > strong'); + $this->assertNotEmpty($assert_session->waitForElement('css', '.drupal-media figcaption.ck-editor__nested-editable')); + $this->pressEditorButton('Bold'); + $this->assertTrue($assert_session->waitForElementRemoved('css', '.drupal-media figcaption > strong')); + $this->assertNotEmpty($figcaption = $assert_session->waitForElement('css', '.drupal-media figcaption')); + $this->assertEquals('Llamas are the most awesome ever', $figcaption->getHtml()); + $editor_dom = $this->getEditorDataAsDom(); + $this->assertEquals('Llamas are the most awesome ever', $editor_dom->getElementsByTagName('drupal-media')->item(0)->getAttribute('data-caption')); + + // Ensure that caption can be linked. + $this->assertNotEmpty($figcaption = $assert_session->waitForElement('css', '.drupal-media figcaption')); + $this->selectTextInsideElement('.drupal-media figcaption'); + $this->assertNotEmpty($assert_session->waitForElement('css', '.drupal-media figcaption.ck-editor__nested-editable')); + $this->pressEditorButton('Link'); + $this->assertVisibleBalloon('.ck-link-form'); + $link_input = $page->find('css', '.ck-balloon-panel .ck-link-form input[type=text]'); + $link_input->setValue('https://drupal.org'); + $page->find('css', '.ck-balloon-panel .ck-link-form button[type=submit]')->click(); + $this->assertNotEmpty($assert_session->waitForElement('css', '.drupal-media figcaption > a')); + $this->assertEquals('<a class="ck-link_selected" href="https://drupal.org">Llamas are the most awesome ever</a>', $figcaption->getHtml()); + $editor_dom = $this->getEditorDataAsDom(); + $this->assertEquals('<a href="https://drupal.org">Llamas are the most awesome ever</a>', $editor_dom->getElementsByTagName('drupal-media')->item(0)->getAttribute('data-caption')); } /** @@ -673,7 +766,7 @@ public function testAlignment() { // Check that the 'content has been updated' message status appears to confirm we left the editor. $assert_session->waitForElementVisible('css', 'messages messages--status'); // Check that the class is correct in the front end. - $assert_session->elementExists('css', 'article.align-center'); + $assert_session->elementExists('css', 'figure.align-center'); // Go back to the editor to check that the alignment class still exists. $edit_url = $this->getSession()->getCurrentURL() . '/edit'; $this->drupalGet($edit_url); @@ -703,7 +796,7 @@ public function testDrupalMediaStyleWithClass() { $filter_format->setFilterConfig('filter_html', [ 'status' => TRUE, 'settings' => [ - 'allowed_html' => '<p> <br> <h1 class> <div class> <section class> <drupal-media data-entity-type data-entity-uuid data-align alt class="layercake-side">', + 'allowed_html' => '<p> <br> <h1 class> <div class> <section class> <drupal-media data-entity-type data-entity-uuid data-align data-caption alt class="layercake-side">', ], ]); $filter_format->save(); @@ -746,8 +839,8 @@ function (ConstraintViolation $v) { $assert_session->waitForElementVisible('css', 'messages messages--status'); // Ensure that the class is correct in the front end. - $assert_session->elementExists('css', 'article.layercake-side'); - $assert_session->elementNotExists('css', 'article.arbitrary-class'); + $assert_session->elementExists('css', 'figure.layercake-side'); + $assert_session->elementNotExists('css', 'figure.arbitrary-class'); } /** @@ -804,4 +897,24 @@ protected function getLastPreviewRequestTransferSize() { return $this->getSession()->evaluateScript($javascript); } + /** + * Selects text inside an element. + * + * @param string $selector + * A CSS selector for the element which contents should be selected. + */ + protected function selectTextInsideElement(string $selector): void { + $javascript = <<<JS +(function() { + const el = document.querySelector("$selector"); + const range = document.createRange(); + range.selectNodeContents(el); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); +})(); +JS; + $this->getSession()->evaluateScript($javascript); + } + } diff --git a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php index e912ab75e4805a0405c373b192e8d81ee883a68a..03d642bc964abfc1b22d0a2d6190080259f5445e 100644 --- a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php +++ b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php @@ -1068,6 +1068,7 @@ public function testEnabledPlugins() { // should be available now that the media_embed is enabled. $plugin_ids = array_keys($this->manager->getEnabledDefinitions($editor)); $expected_plugins = array_merge($default_plugins, [ + 'ckeditor5_drupalMediaCaption', 'ckeditor5_test_layercake', 'media_media', 'media_mediaAlign', diff --git a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php index 0f6eb4a625c92d2ab6b3b36e4c02e0808737a48f..42ff505fa144837117c1a45d90804514e7705bde 100644 --- a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php +++ b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php @@ -552,19 +552,12 @@ public function provider() { array_slice($basic_html_test_case['expected_ckeditor5_settings']['toolbar']['items'], 10), ), ], - 'plugins' => [ - 'ckeditor5_sourceEditing' => [ - 'allowed_tags' => array_merge( - $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], - ['<drupal-media data-caption>'], - ), - ], - ] + $basic_html_test_case['expected_ckeditor5_settings']['plugins'], + 'plugins' => $basic_html_test_case['expected_ckeditor5_settings']['plugins'], ], 'expected_superset' => $basic_html_test_case['expected_superset'], 'expected_fundamental_compatibility_violations' => $basic_html_test_case['expected_fundamental_compatibility_violations'], 'expected_messages' => array_merge($basic_html_test_case['expected_messages'], [ - "This format's HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported by this text format, the following were added to the Source Editing plugin's <em>Manually editable HTML tags</em>: <a hreflang> <blockquote cite> <ul type> <ol start type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-caption>.", + "This format's HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported by this text format, the following were added to the Source Editing plugin's <em>Manually editable HTML tags</em>: <a hreflang> <blockquote cite> <ul type> <ol start type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>.", ]), ];