diff --git a/core/assets/vendor/ckeditor5/build/drupal/drupalLinkMedia.js b/core/assets/vendor/ckeditor5/build/drupal/drupalLinkMedia.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/assets/vendor/ckeditor5/build/drupal/drupalMedia.js b/core/assets/vendor/ckeditor5/build/drupal/drupalMedia.js index 7a2ddd13fc..9673929b67 100644 --- a/core/assets/vendor/ckeditor5/build/drupal/drupalMedia.js +++ b/core/assets/vendor/ckeditor5/build/drupal/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(()=>{var e={"ckeditor5/src/core.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/ui.js")},"ckeditor5/src/utils.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"ckeditor5/src/widget.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/widget.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function i(r){var s=t[r];if(void 0!==s)return s.exports;var a=t[r]={exports:{}};return e[r](a,a.exports,i),a.exports}i.d=(e,t)=>{for(var r in t)i.o(t,r)&&!i.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var r={};return(()=>{"use strict";i.d(r,{default:()=>v});var e=i("ckeditor5/src/core.js"),t=i("ckeditor5/src/widget.js");class s 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)),{}),r=Object.keys(e).reduce(((t,r)=>(i[r]&&(t[i[r]]=e[r]),t)),{});this.editor.model.change((e=>{this.editor.model.insertContent(function(e,t){return e.createElement("drupalMedia",t)}(e,r))}))}refresh(){const e=this.editor.model,t=e.document.selection,i=e.schema.findAllowedParent(t.getFirstPosition(),"drupalMedia");this.isEnabled=null!==i}}class a extends e.Plugin{static get requires(){return[t.Widget]}init(){this.attrs={drupalMediaAlt:"alt",drupalMediaAlign:"data-align",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=this.editor.t("Preview failed"),this.themeError=i||`\n

${this.editor.t("An error occurred while trying to preview the media. Please save your work and reload this page.")}

\n `,this._defineSchema(),this._defineConverters(),this.editor.commands.add("insertDrupalMedia",new s(this.editor))}async _fetchPreview(e,t){const i=await fetch(`${e}?${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 r=i.createContainerElement("div",{class:"drupal-media"}),s=i.createRawElement("div",{},(t=>{this.previewURL?this._fetchPreview(this.previewURL,{text:this._renderElement(e),uuid:e.getAttribute("drupalMediaEntityUuid")}).then((({label:e,preview:i})=>{t.innerHTML=i,t.setAttribute("aria-label",e)})):(t.innerHTML=this.themeError,t.setAttribute("aria-label","drupal-media"))}));return i.insert(i.createPositionAt(r,0),s),i.setCustomProperty("drupalMedia",!0,r),(0,t.toWidget)(r,i,{label:"media widget"})}}),Object.keys(this.attrs).forEach((t=>{e.attributeToAttribute({model:{key:t,name:"drupalMedia"},view:{name:"drupal-media",key:this.attrs[t]}})}))}_renderElement(e){const t=e.getAttributes();let i="{this.attrs[e[0]]&&"drupalMediaCaption"!==e[0]&&(i+=` ${this.attrs[e[0]]}="${e[1]}"`)})),i+=">",i}static get pluginName(){return"DrupalMediaEditing"}}var o=i("ckeditor5/src/ui.js");class n extends e.Plugin{init(){const e=this.editor,t=this.editor.config.get("drupalMedia");if(!t)return;const{libraryURL:i,openDialog:r,dialogSettings:s={}}=t;i&&"function"==typeof r&&e.ui.componentFactory.add("drupalMedia",(t=>{const a=e.commands.get("insertDrupalMedia"),n=new o.ButtonView(t);return n.set({label:e.t("Insert Drupal Media"),icon:'\n',tooltip:!0}),n.bind("isOn","isEnabled").to(a,"value","isEnabled"),this.listenTo(n,"execute",(()=>{r(i,(({attributes:t})=>{e.execute("insertDrupalMedia",t)}),s)})),n}))}}function l(e){return!!e&&e.is("element","drupalMedia")}function d(e){const i=e.getSelectedElement();return i&&function(e){return(0,t.isWidget)(e)&&!!e.getCustomProperty("drupalMedia")}(i)?i:null}class u extends e.Plugin{static get requires(){return[t.WidgetToolbarRepository]}static get pluginName(){return"DrupalMediaToolbar"}afterInit(){const e=this.editor,{t:i}=e;e.plugins.get(t.WidgetToolbarRepository).register("drupalMedia",{ariaLabel:i("Drupal Media toolbar"),items:e.config.get("drupalMedia.toolbar")||[],getRelatedElement:e=>d(e)})}}class c extends e.Command{refresh(){const e=this.editor.model.document.selection.getSelectedElement();this.isEnabled=!1,l(e)&&this._isMediaImage(e).then((e=>{this.isEnabled=e})),l(e)&&e.hasAttribute("drupalMediaAlt")?this.value=e.getAttribute("drupalMediaAlt"):this.value=!1}execute(e){const t=this.editor.model,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)}))}async _isMediaImage(e){const t=this.editor.config.get("drupalMedia");if(!t)return null;const{isMediaUrl:i}=t,r=new URLSearchParams({uuid:e.getAttribute("drupalMediaEntityUuid")}),s=await fetch(`${i}&${r}`);return s.ok?JSON.parse(await s.text()):null}}class h extends e.Plugin{static get pluginName(){return"MediaImageTextAlternativeEditing"}init(){this.editor.commands.add("mediaImageTextAlternative",new c(this.editor))}}function m(e){const t=e.editing.view,i=o.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 p=i("ckeditor5/src/utils.js");class g extends o.View{constructor(t){super(t);const i=this.locale.t;this.focusTracker=new p.FocusTracker,this.keystrokes=new p.KeystrokeHandler,this.labeledInput=this._createLabeledInputView(),this.saveButtonView=this._createButton(i("Save"),e.icons.check,"ck-button-save"),this.saveButtonView.type="submit",this.cancelButtonView=this._createButton(i("Cancel"),e.icons.cancel,"ck-button-cancel","cancel"),this._focusables=new o.ViewCollection,this._focusCycler=new o.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,o.injectCssTransitionDisabler)(this)}render(){super.render(),this.keystrokes.listenTo(this.element),(0,o.submitHandler)({view:this}),[this.labeledInput,this.saveButtonView,this.cancelButtonView].forEach((e=>{this._focusables.add(e),this.focusTracker.add(e.element)}))}_createButton(e,t,i,r){const s=new o.ButtonView(this.locale);return s.set({label:e,icon:t,tooltip:!0}),s.extendTemplate({attributes:{class:i}}),r&&s.delegate("execute").to(this,r),s}_createLabeledInputView(){const e=this.locale.t,t=new o.LabeledFieldView(this.locale,o.createLabeledInputText);return t.label=e("Override text alternative"),t}}class f extends e.Plugin{static get requires(){return[o.ContextualBalloon]}static get pluginName(){return"MediaImageTextAlternativeUi"}init(){this._createButton(),this._createForm()}destroy(){super.destroy(),this._form.destroy()}_createButton(){const t=this.editor,i=t.t;t.ui.componentFactory.add("mediaImageTextAlternative",(r=>{const s=t.commands.get("mediaImageTextAlternative"),a=new o.ButtonView(r);return a.set({label:i("Override media image text alternative"),icon:e.icons.lowVision,tooltip:!0}),a.bind("isVisible").to(s,"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 g(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",(()=>{d(t.selection)?this._isVisible&&function(e){const t=e.plugins.get("ContextualBalloon");if(d(e.editing.view.document.selection)){const i=m(e);t.updatePosition(i)}}(e):this._hideForm(!0)})),(0,o.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:m(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 w extends e.Plugin{static get requires(){return[h,f]}static get pluginName(){return"MediaImageTextAlternative"}}class b extends e.Plugin{static get requires(){return[a,n,u,w]}}const v={DrupalMedia:b,MediaImageTextAlternative:w,MediaImageTextAlternativeEditing:h,MediaImageTextAlternativeUi:f}})(),r=r.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(()=>{var e={"ckeditor5/src/clipboard.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/clipboard.js")},"ckeditor5/src/core.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/engine.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/engine.js")},"ckeditor5/src/typing.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/typing.js")},"ckeditor5/src/ui.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/ui.js")},"ckeditor5/src/utils.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"ckeditor5/src/widget.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/widget.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function i(r){var s=t[r];if(void 0!==s)return s.exports;var a=t[r]={exports:{}};return e[r](a,a.exports,i),a.exports}i.d=(e,t)=>{for(var r in t)i.o(t,r)&&!i.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var r={};return(()=>{"use strict";i.d(r,{default:()=>y});var e=i("ckeditor5/src/core.js"),t=i("ckeditor5/src/widget.js");class s 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)),{}),r=Object.keys(e).reduce(((t,r)=>(i[r]&&(t[i[r]]=e[r]),t)),{});this.editor.model.change((e=>{this.editor.model.insertContent(function(e,t){return e.createElement("drupalMedia",t)}(e,r))}))}refresh(){const e=this.editor.model,t=e.document.selection,i=e.schema.findAllowedParent(t.getFirstPosition(),"drupalMedia");this.isEnabled=null!==i}}class a extends e.Plugin{static get requires(){return[t.Widget]}init(){this.attrs={drupalMediaAlt:"alt",drupalMediaAlign:"data-align",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=this.editor.t("Preview failed"),this.themeError=i||`\n

${this.editor.t("An error occurred while trying to preview the media. Please save your work and reload this page.")}

\n `,this._defineSchema(),this._defineConverters(),this.editor.commands.add("insertDrupalMedia",new s(this.editor))}async _fetchPreview(e,t){const i=await fetch(`${e}?${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 r=i.createContainerElement("div",{class:"drupal-media"}),s=i.createRawElement("div",{"data-drupal-media-preview":"loading"},(t=>{this.previewURL?this._fetchPreview(this.previewURL,{text:this._renderElement(e),uuid:e.getAttribute("drupalMediaEntityUuid")}).then((({label:e,preview:i})=>{t.innerHTML=i,t.setAttribute("aria-label",e),t.setAttribute("data-drupal-media-preview","ready")})):(t.innerHTML=this.themeError,t.setAttribute("aria-label","drupal-media"),t.setAttribute("data-drupal-media-preview","unavailable"))}));return i.insert(i.createPositionAt(r,0),s),i.setCustomProperty("drupalMedia",!0,r),(0,t.toWidget)(r,i,{label:"media widget"})}}),Object.keys(this.attrs).forEach((t=>{e.attributeToAttribute({model:{key:t,name:"drupalMedia"},view:{name:"drupal-media",key:this.attrs[t]}})}))}_renderElement(e){const t=e.getAttributes();let i="{this.attrs[e[0]]&&"drupalMediaCaption"!==e[0]&&(i+=` ${this.attrs[e[0]]}="${e[1]}"`)})),i+=">",i}static get pluginName(){return"DrupalMediaEditing"}}var n=i("ckeditor5/src/ui.js");class o extends e.Plugin{init(){const e=this.editor,t=this.editor.config.get("drupalMedia");if(!t)return;const{libraryURL:i,openDialog:r,dialogSettings:s={}}=t;i&&"function"==typeof r&&e.ui.componentFactory.add("drupalMedia",(t=>{const a=e.commands.get("insertDrupalMedia"),o=new n.ButtonView(t);return o.set({label:e.t("Insert Drupal Media"),icon:'\n',tooltip:!0}),o.bind("isOn","isEnabled").to(a,"value","isEnabled"),this.listenTo(o,"execute",(()=>{r(i,(({attributes:t})=>{e.execute("insertDrupalMedia",t)}),s)})),o}))}}function l(e){return!!e&&e.is("element","drupalMedia")}function d(e){const i=e.getSelectedElement();return i&&function(e){return(0,t.isWidget)(e)&&!!e.getCustomProperty("drupalMedia")}(i)?i:null}class u extends e.Plugin{static get requires(){return[t.WidgetToolbarRepository]}static get pluginName(){return"DrupalMediaToolbar"}afterInit(){const e=this.editor,{t:i}=e;e.plugins.get(t.WidgetToolbarRepository).register("drupalMedia",{ariaLabel:i("Drupal Media toolbar"),items:e.config.get("drupalMedia.toolbar")||[],getRelatedElement:e=>d(e)})}}class c extends e.Command{refresh(){const e=this.editor.model.document.selection.getSelectedElement();this.isEnabled=!1,l(e)&&this._isMediaImage(e).then((e=>{this.isEnabled=e})),l(e)&&e.hasAttribute("drupalMediaAlt")?this.value=e.getAttribute("drupalMediaAlt"):this.value=!1}execute(e){const t=this.editor.model,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)}))}async _isMediaImage(e){const t=this.editor.config.get("drupalMedia");if(!t)return null;const{isMediaUrl:i}=t,r=new URLSearchParams({uuid:e.getAttribute("drupalMediaEntityUuid")}),s=await fetch(`${i}&${r}`);return s.ok?JSON.parse(await s.text()):null}}class m extends e.Plugin{static get pluginName(){return"MediaImageTextAlternativeEditing"}init(){this.editor.commands.add("mediaImageTextAlternative",new c(this.editor))}}function h(e){const t=e.editing.view,i=n.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 p=i("ckeditor5/src/utils.js");class g extends n.View{constructor(t){super(t);const i=this.locale.t;this.focusTracker=new p.FocusTracker,this.keystrokes=new p.KeystrokeHandler,this.labeledInput=this._createLabeledInputView(),this.saveButtonView=this._createButton(i("Save"),e.icons.check,"ck-button-save"),this.saveButtonView.type="submit",this.cancelButtonView=this._createButton(i("Cancel"),e.icons.cancel,"ck-button-cancel","cancel"),this._focusables=new n.ViewCollection,this._focusCycler=new n.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,n.injectCssTransitionDisabler)(this)}render(){super.render(),this.keystrokes.listenTo(this.element),(0,n.submitHandler)({view:this}),[this.labeledInput,this.saveButtonView,this.cancelButtonView].forEach((e=>{this._focusables.add(e),this.focusTracker.add(e.element)}))}_createButton(e,t,i,r){const s=new n.ButtonView(this.locale);return s.set({label:e,icon:t,tooltip:!0}),s.extendTemplate({attributes:{class:i}}),r&&s.delegate("execute").to(this,r),s}_createLabeledInputView(){const e=this.locale.t,t=new n.LabeledFieldView(this.locale,n.createLabeledInputText);return t.label=e("Override text alternative"),t}}class f extends e.Plugin{static get requires(){return[n.ContextualBalloon]}static get pluginName(){return"MediaImageTextAlternativeUi"}init(){this._createButton(),this._createForm()}destroy(){super.destroy(),this._form.destroy()}_createButton(){const t=this.editor,i=t.t;t.ui.componentFactory.add("mediaImageTextAlternative",(r=>{const s=t.commands.get("mediaImageTextAlternative"),a=new n.ButtonView(r);return a.set({label:i("Override media image text alternative"),icon:e.icons.lowVision,tooltip:!0}),a.bind("isVisible").to(s,"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 g(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",(()=>{d(t.selection)?this._isVisible&&function(e){const t=e.plugins.get("ContextualBalloon");if(d(e.editing.view.document.selection)){const i=h(e);t.updatePosition(i)}}(e):this._hideForm(!0)})),(0,n.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:h(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 b extends e.Plugin{static get requires(){return[m,f]}static get pluginName(){return"MediaImageTextAlternative"}}function w(e,t,i){if(t.attributes)for(const[r,s]of Object.entries(t.attributes))e.setAttribute(r,s,i);t.styles&&e.setStyle(t.styles,i),t.classes&&e.addClass(t.classes,i)}class v extends e.Plugin{init(){const e=this.editor;if(!e.plugins.has("GeneralHtmlSupport"))return;const t=e.model.schema,i=e.conversion,r=e.plugins.get("DataFilter");t.extend("drupalMedia",{allowAttributes:["htmlLinkAttributes"]}),i.for("upcast").add(function(e){return t=>{t.on("element:drupal-media",((t,i,r)=>{const s=i.viewItem.parent;function a(t,s){const a=e._consumeAllowedAttributes(t,r);a&&r.writer.setAttribute(s,a,i.modelRange)}function n(e){a(e,"htmlLinkAttributes")}s.is("element","a")&&n(s)}),{priority:"low"})}}(r)),i.for("editingDowncast").add((e=>e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{if(!i.consumable.consume(t.item,"attribute:htmlLinkAttributes:drupalMedia"))return;const r=i.mapper.toViewElement(t.item),s=function(e,t,i){const r=e.createRangeOn(t);for(const{item:e}of r.getWalker())if(e.is("element",i))return e}(i.writer,r,"a");w(i.writer,t.item.getAttribute("htmlLinkAttributes"),s)}),{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 r=i.mapper.toViewElement(t.item).parent;w(i.writer,t.item.getAttribute("htmlLinkAttributes"),r)}),{priority:"low"})))}static get pluginName(){return"DrupalMediaGeneralHtmlSupport"}}class k extends e.Plugin{static get requires(){return[a,v,o,u,b]}}i("ckeditor5/src/engine.js"),i("ckeditor5/src/typing.js"),i("ckeditor5/src/clipboard.js");(0,p.mix)(class{constructor({id:e,label:t,attributes:i,classes:r,styles:s,defaultValue:a}){this.id=e,this.set("value"),this.defaultValue=a,this.label=t,this.attributes=i,this.classes=r,this.styles=s}_createPattern(){return{attributes:this.attributes,classes:this.classes,styles:this.styles}}},p.ObservableMixin);class M extends e.Plugin{static get requires(){return["LinkEditing","DrupalMediaEditing"]}static get pluginName(){return"DrupalLinkMediaEditing"}init(){const e=this.editor;e.model.schema.extend("drupalMedia",{allowAttributes:["linkHref"]}),e.conversion.for("upcast").add((e=>{e.on("element:a",((e,t,i)=>{const r=t.viewItem,s=(a=r,Array.from(a.getChildren()).find((e=>"drupal-media"===e.name)));var a;if(!s)return;const n={attributes:["href"]};if(!i.consumable.consume(r,n))return;const o=r.getAttribute("href");if(!o)return;const l=i.convertItem(s,t.modelCursor);t.modelRange=l.modelRange,t.modelCursor=l.modelCursor;const d=t.modelCursor.nodeBefore;d&&d.is("element","drupalMedia")&&i.writer.setAttribute("linkHref",o,d)}),{priority:"high"})})),e.conversion.for("editingDowncast").add((e=>{e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{const r=i.writer;if(!i.consumable.consume(t.item,e.name))return;const s=i.mapper.toViewElement(t.item),a=Array.from(s.getChildren()).find((e=>"a"===e.name));if(a)t.attributeNewValue?r.setAttribute("href",t.attributeNewValue,a):(r.move(r.createRangeIn(a),r.createPositionAt(s,0)),r.remove(a));else{const e=Array.from(s.getChildren()).find((e=>e.getAttribute("data-drupal-media-preview"))),i=r.createContainerElement("a",{href:t.attributeNewValue});r.insert(r.createPositionAt(s,0),i),r.move(r.createRangeOn(e),r.createPositionAt(i,0))}}),{priority:"high"})})),e.conversion.for("dataDowncast").add((e=>{e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{const r=i.writer;if(!i.consumable.consume(t.item,e.name))return;const s=i.mapper.toViewElement(t.item),a=r.createContainerElement("a",{href:t.attributeNewValue});r.insert(r.createPositionBefore(s),a),r.move(r.createRangeOn(s),r.createPositionAt(a,0))}),{priority:"high"})}))}}class x extends e.Plugin{static get requires(){return["LinkEditing","LinkUI","DrupalMediaEditing"]}static get pluginName(){return"DrupalLinkMediaUi"}init(){const e=this.editor,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 e=this.editor,t=e.t;e.ui.componentFactory.add("drupalLinkMedia",(i=>{const r=new n.ButtonView(i),s=e.plugins.get("LinkUI"),a=e.commands.get("link");return r.set({isEnabled:!0,label:t("Link media"),icon:'\n',keystroke:"Ctrl+K",tooltip:!0,isToggleable:!0}),r.bind("isEnabled").to(a,"isEnabled"),r.bind("isOn").to(a,"value",(e=>!!e)),this.listenTo(r,"execute",(()=>{this._isSelectedLinkedMedia(e.model.document.selection)?s._addActionsView():s._showUI(!0)})),r}))}_isSelectedLinkedMedia(e){const t=e.getSelectedElement();return!!t&&t.is("element","drupalMedia")&&t.hasAttribute("linkHref")}}class _ extends e.Plugin{static get requires(){return[M,x]}static get pluginName(){return"DrupalLinkMedia"}}const y={DrupalMedia:k,MediaImageTextAlternative:b,MediaImageTextAlternativeEditing:m,MediaImageTextAlternativeUi:f,DrupalLinkMedia:_}})(),r=r.default})()})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupallinkmedia/drupallinkmedia.js b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupallinkmedia/drupallinkmedia.js new file mode 100644 index 0000000000..b6c26d6a78 --- /dev/null +++ b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupallinkmedia/drupallinkmedia.js @@ -0,0 +1,27 @@ +/* cspell:words drupallinkmediaediting drupallinkmediaui */ + +import { Plugin } from 'ckeditor5/src/core'; +import DrupalLinkMediaEditing from './drupallinkmediaediting'; +import DrupalLinkMediaUI from "./drupallinkmediaui"; + +/** + * @internal + */ +export default class DrupalLinkMedia extends Plugin { + /** + * @inheritdoc + */ + static get requires() { + return [ + DrupalLinkMediaEditing, + DrupalLinkMediaUI, + ]; + } + + /** + * @inheritdoc + */ + static get pluginName() { + return 'DrupalLinkMedia'; + } +} diff --git a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupallinkmedia/drupallinkmediaediting.js b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupallinkmedia/drupallinkmediaediting.js new file mode 100644 index 0000000000..44d09e4b44 --- /dev/null +++ b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupallinkmedia/drupallinkmediaediting.js @@ -0,0 +1,188 @@ +/* cspell:words drupallinkmediaediting linkediting */ + +import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting'; + +import { Plugin } from 'ckeditor5/src/core'; + +/** + * Model to view and view to model conversions for linked media elements. + * + * @internal + * + * @see https://github.com/ckeditor/ckeditor5/blob/v31.0.0/packages/ckeditor5-link/src/linkimage.js + */ +export default class DrupalLinkMediaEditing extends Plugin { + + /** + * @inheritdoc + */ + static get requires() { + return ['LinkEditing', 'DrupalMediaEditing']; + } + + /** + * @inheritdoc + */ + static get pluginName() { + return 'DrupalLinkMediaEditing'; + } + + /** + * @inheritdoc + */ + init() { + const editor = this.editor + editor.model.schema.extend('drupalMedia', { allowAttributes: ['linkHref'] }); + + editor.conversion.for('upcast').add(upcastMediaLink()); + editor.conversion.for('editingDowncast').add(editingDowncastMediaLink()); + editor.conversion.for('dataDowncast').add(dataDowncastMediaLink()); + } +} + +/** + * Returns a converter that consumes the `href` attribute if a link contains a . + * + * @return {Function} + */ +function upcastMediaLink() { + return dispatcher => { + dispatcher.on('element:a', (evt, data, conversionApi) => { + const viewLink = data.viewItem; + const mediaInLink = getFirstMedia(viewLink); + + if (!mediaInLink) { + return; + } + + // There's an inside an element - we consume it so it + // won't be picked up by the Link plugin. + const consumableAttributes = { attributes: ['href'] }; + + // Consume the `href` attribute so the default one will not convert it to + // $text attribute. + if (!conversionApi.consumable.consume(viewLink, consumableAttributes)) { + // Might be consumed by something else - i.e. other converter with + // priority=highest - a standard check. + return; + } + + const linkHref = viewLink.getAttribute('href'); + + // Missing the `href` attribute. + if (!linkHref) { + return; + } + + const conversionResult = conversionApi.convertItem(mediaInLink, data.modelCursor); + + // Set media range as conversion result. + data.modelRange = conversionResult.modelRange; + + // Continue conversion where conversion ends. + data.modelCursor = conversionResult.modelCursor; + + const modelElement = data.modelCursor.nodeBefore; + + if (modelElement && modelElement.is('element', 'drupalMedia')) { + // Set the `linkHref` attribute from element on model drupalMedia + // element. + conversionApi.writer.setAttribute('linkHref', linkHref, modelElement); + } + }, { priority: 'high' }); + }; +} + +/** + * Return a converter that adds the element to view data. + * + * @return {Function} + */ +function dataDowncastMediaLink() { + return dispatcher => { + dispatcher.on('attribute:linkHref:drupalMedia', (evt, data, conversionApi) => { + const writer = conversionApi.writer; + if (!conversionApi.consumable.consume(data.item, evt.name)) { + return; + } + + // The drupalMedia will be already converted - so it will be present in + // the view. + const mediaElement = conversionApi.mapper.toViewElement(data.item); + + // If so, update the attribute if it's defined or remove the entire link + // if the attribute is empty. But if it does not exist. Let's wrap already + // converted drupalMedia by newly created link element. + // 1. Create an empty element. + const linkElement = writer.createContainerElement('a', { href: data.attributeNewValue }); + + // 2. Insert before the element. + writer.insert(writer.createPositionBefore(mediaElement), linkElement); + + // 3. Move the drupal-media element inside the . + writer.move(writer.createRangeOn(mediaElement), writer.createPositionAt(linkElement, 0)); + }, { priority: 'high' }); + }; +} + +/** + * Return a converter that adds the element to editing view. + * + * @returns {Function} + * + * @see https://github.com/ckeditor/ckeditor5/blob/v31.0.0/packages/ckeditor5-link/src/linkimageediting.js#L180 + */ +function editingDowncastMediaLink() { + return dispatcher => { + dispatcher.on('attribute:linkHref:drupalMedia', (evt, data, conversionApi) => { + const writer = conversionApi.writer; + if (!conversionApi.consumable.consume(data.item, evt.name)) { + return; + } + + // The drupalMedia will be already converted - so it will be present in + // the view. + const mediaContainer = conversionApi.mapper.toViewElement(data.item); + const linkInMedia = Array.from(mediaContainer.getChildren()).find(child => child.name === 'a'); + + // If link already exists, instead of creating new link from scratch, + // update the existing link. This makes the UI rendering much smoother. + if (linkInMedia) { + // If attribute has a new value, update it. If new value doesn't exist, + // the link will be removed. + if (data.attributeNewValue) { + writer.setAttribute('href', data.attributeNewValue, linkInMedia); + } else { + // This is triggering elementToElement conversion for drupalMedia + // element which makes caused re-render of the media preview, making + // the media preview flicker once when media is unlinked. + // @todo ensure that this doesn't cause flickering after + // https://www.drupal.org/i/3246380 has been addressed. + writer.move(writer.createRangeIn(linkInMedia), writer.createPositionAt(mediaContainer, 0)); + writer.remove(linkInMedia); + } + } else { + const mediaPreview = Array.from(mediaContainer.getChildren()).find(child => child.getAttribute('data-drupal-media-preview')); + // 1. Create an empty element. + const linkElement = writer.createContainerElement('a', { href: data.attributeNewValue }); + + // 2. Insert inside the media container. + writer.insert(writer.createPositionAt(mediaContainer, 0), linkElement); + + // 3. Move the media preview inside the . + writer.move(writer.createRangeOn(mediaPreview), writer.createPositionAt(linkElement, 0)); + } + }, { priority: 'high' }); + }; +} + +/** + * Returns the first drupal-media element in a given view element. + * + * @param {module:engine/view/element~Element} viewElement + * + * @return {module:engine/view/element~Element|undefined} + */ +function getFirstMedia(viewElement) { + return Array.from(viewElement.getChildren()).find(child => child.name === 'drupal-media'); +} diff --git a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupallinkmedia/drupallinkmediaui.js b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupallinkmedia/drupallinkmediaui.js new file mode 100644 index 0000000000..f66ebdff66 --- /dev/null +++ b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupallinkmedia/drupallinkmediaui.js @@ -0,0 +1,100 @@ +import { Plugin } from 'ckeditor5/src/core'; +import linkIcon from '../../../../../icons/link.svg'; +import { LINK_KEYSTROKE } from "@ckeditor/ckeditor5-link/src/utils"; +import { ButtonView } from "ckeditor5/src/ui"; + +/** + * The link media UI plugin. + * + * @internal + */ +export default class DrupalLinkMediaUI extends Plugin { + /** + * @inheritdoc + */ + static get requires() { + return ['LinkEditing', 'LinkUI', 'DrupalMediaEditing']; + } + + /** + * @inheritdoc + */ + static get pluginName() { + return 'DrupalLinkMediaUi'; + } + + /** + * @inheritdoc + */ + init() { + const editor = this.editor; + const viewDocument = editor.editing.view.document; + + this.listenTo(viewDocument, 'click', (evt, data) => { + if (this._isSelectedLinkedMedia(editor.model.document.selection)) { + // Prevent browser navigation when clicking a linked media. + data.preventDefault(); + + // Block the `LinkUI` plugin when a media was clicked. In such a case, + // we'd like to display the media toolbar. + evt.stop(); + } + }, { priority: 'high' }); + this._createToolbarLinkMediaButton(); + } + + /** + * Creates a `DrupalLinkMediaUI` button view. + * + * Clicking this button shows a {@link module:link/linkui~LinkUI#_balloon} + * attached to the selection. When an media is already linked, the view shows + * {@link module:link/linkui~LinkUI#actionsView} or + * {@link module:link/linkui~LinkUI#formView} if it is not. + */ + _createToolbarLinkMediaButton() { + const editor = this.editor; + const t = editor.t; + + editor.ui.componentFactory.add('drupalLinkMedia', locale => { + const button = new ButtonView(locale); + const plugin = editor.plugins.get('LinkUI'); + const linkCommand = editor.commands.get('link'); + + button.set( { + isEnabled: true, + label: t('Link media'), + icon: linkIcon, + keystroke: LINK_KEYSTROKE, + tooltip: true, + isToggleable: true + } ); + + // Bind button to the command. + button.bind('isEnabled').to(linkCommand, 'isEnabled'); + button.bind('isOn').to(linkCommand, 'value', value => !!value); + + // Show the actionsView or formView (both from LinkUI) on button click + // depending on whether the media is already linked. + this.listenTo(button, 'execute', () => { + if (this._isSelectedLinkedMedia(editor.model.document.selection)) { + plugin._addActionsView(); + } else { + plugin._showUI(true); + } + }); + + return button; + }); + } + + /** + * Returns true if a linked media is the only selected element in the model. + * + * @param {module:engine/model/selection~Selection} selection + * @returns {Boolean} + */ + _isSelectedLinkedMedia(selection) { + const selectedModelElement = selection.getSelectedElement(); + return !!selectedModelElement && selectedModelElement.is('element', 'drupalMedia') && selectedModelElement.hasAttribute('linkHref'); + } +} diff --git a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmedia.js b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmedia.js index 1215009598..8e7f6f1bb4 100644 --- a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmedia.js +++ b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmedia.js @@ -1,4 +1,4 @@ -/* cspell:words drupalmediaediting drupalmediaui drupalmediatoolbar mediaimagetextalternative */ +/* cspell:words drupalmediaediting drupalmediageneralhtmlsupport drupalmediaui drupalmediatoolbar mediaimagetextalternative */ import { Plugin } from 'ckeditor5/src/core'; import DrupalMediaEditing from './drupalmediaediting'; @@ -6,6 +6,7 @@ import DrupalMediaUI from './drupalmediaui'; import DrupalMediaToolbar from './drupalmediatoolbar'; import MediaImageTextAlternative from './mediaimagetextalternative'; +import DrupalMediaGeneralHtmlSupport from "./drupalmediageneralhtmlsupport"; /** * @internal @@ -14,6 +15,7 @@ export default class DrupalMedia extends Plugin { static get requires() { return [ DrupalMediaEditing, + DrupalMediaGeneralHtmlSupport, DrupalMediaUI, DrupalMediaToolbar, MediaImageTextAlternative, diff --git a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js index b8106f9ccd..ff0e00a748 100644 --- a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js +++ b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js @@ -90,7 +90,7 @@ export default class DrupalMediaEditing extends Plugin { const container = viewWriter.createContainerElement('div', { class: 'drupal-media', }); - const media = viewWriter.createRawElement('div', {}, (domElement) => { + const media = viewWriter.createRawElement('div', { 'data-drupal-media-preview': 'loading' }, (domElement) => { if (this.previewURL) { this._fetchPreview(this.previewURL, { text: this._renderElement(modelElement), @@ -98,10 +98,12 @@ export default class DrupalMediaEditing extends Plugin { }).then(({ label, preview }) => { domElement.innerHTML = preview; domElement.setAttribute('aria-label', label); + domElement.setAttribute('data-drupal-media-preview', 'ready'); }); } else { domElement.innerHTML = this.themeError; domElement.setAttribute('aria-label', 'drupal-media'); + domElement.setAttribute('data-drupal-media-preview', 'unavailable'); } }); viewWriter.insert(viewWriter.createPositionAt(container, 0), media); diff --git a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmediageneralhtmlsupport.js b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmediageneralhtmlsupport.js new file mode 100644 index 0000000000..82bbeacb7d --- /dev/null +++ b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/drupalmediageneralhtmlsupport.js @@ -0,0 +1,128 @@ +import { Plugin } from 'ckeditor5/src/core'; +import { setViewAttributes } from "@ckeditor/ckeditor5-html-support/src/conversionutils"; + +/** + * Integrates Drupal Media with General HTML Support. + * + * @internal + */ +export default class DrupalMediaGeneralHtmlSupport extends Plugin { + /** + * @inheritdoc + */ + init() { + const editor = this.editor; + + // This plugin is only needed if General HTML Support plugin is loaded. + if (!editor.plugins.has('GeneralHtmlSupport')) { + return; + } + + const schema = editor.model.schema; + const conversion = editor.conversion; + const dataFilter = editor.plugins.get('DataFilter'); + + schema.extend('drupalMedia', { + allowAttributes: [ + 'htmlLinkAttributes', + ] + }); + + conversion.for('upcast').add(viewToModelDrupalMediaAttributeConverter(dataFilter)); + conversion.for('editingDowncast').add(modelToEditingViewAttributeConverter()); + conversion.for('dataDowncast').add(modelToDataViewAttributeConverter()); + } + + /** + * @inheritdoc + */ + static get pluginName() { + return 'DrupalMediaGeneralHtmlSupport'; + } +} + +/** + * View-to-model conversion helper preserving allowed attributes on the Drupal Media model. + * + * @param {module:html-support/datafilter~DataFilter} dataFilter + * The General HTML support data filter. + * + * @return {function} Returns a conversion callback. + */ +function viewToModelDrupalMediaAttributeConverter(dataFilter) { + return dispatcher => { + dispatcher.on('element:drupal-media', (evt, data, conversionApi) => { + const viewMediaElement = data.viewItem; + const viewContainerElement = viewMediaElement.parent; + + if (viewContainerElement.is('element', 'a')) { + preserveLinkAttributes(viewContainerElement); + } + + function preserveElementAttributes(viewElement, attributeName) { + const viewAttributes = dataFilter._consumeAllowedAttributes(viewElement, conversionApi); + + if (viewAttributes) { + conversionApi.writer.setAttribute(attributeName, viewAttributes, data.modelRange); + } + } + + function preserveLinkAttributes(linkElement) { + preserveElementAttributes(linkElement, 'htmlLinkAttributes'); + } + }, { priority: 'low' }); + }; +} + +/** + * Model to editing view attribute converter. + * + * @return {function} + */ +function modelToEditingViewAttributeConverter() { + return (dispatcher) => dispatcher.on('attribute:linkHref:drupalMedia', (evt, data, conversionApi) => { + if (!conversionApi.consumable.consume(data.item, 'attribute:htmlLinkAttributes:drupalMedia')) { + return; + } + + const containerElement = conversionApi.mapper.toViewElement(data.item); + const viewElement = getDescendantElement(conversionApi.writer, containerElement, 'a'); + + setViewAttributes(conversionApi.writer, data.item.getAttribute('htmlLinkAttributes'), viewElement); + }, { priority: 'low' }); +} + +/** + * Model to data view attribute converter. + * + * @return {function} + */ +function modelToDataViewAttributeConverter() { + return (dispatcher) => dispatcher.on('attribute:linkHref:drupalMedia', (evt, data, conversionApi) => { + if (!conversionApi.consumable.consume(data.item, 'attribute:htmlLinkAttributes:drupalMedia')) { + return; + } + + const mediaElement = conversionApi.mapper.toViewElement(data.item); + const linkElement = mediaElement.parent; + setViewAttributes(conversionApi.writer, data.item.getAttribute('htmlLinkAttributes'), linkElement); + }, { priority: 'low' }); +} + +/** + * Gets descendant element from a container. + * + * @param writer + * @param containerElement + * @param elementName + * @return {*} + */ +function getDescendantElement(writer, containerElement, elementName) { + const range = writer.createRangeOn(containerElement); + + for (const { item } of range.getWalker()) { + if (item.is('element', elementName)) { + return item; + } + } +} diff --git a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/index.js b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/index.js index 33db7e849c..aa8a4ad279 100644 --- a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/index.js +++ b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/index.js @@ -1,6 +1,10 @@ // cspell:ignore mediaimagetextalternative import DrupalMedia from './drupalmedia'; + +// cspell:ignore drupallinkmedia +import DrupalLinkMedia from "./drupallinkmedia/drupallinkmedia"; + // cspell:ignore mediaimagetextalternative import MediaImageTextAlternative from './mediaimagetextalternative'; import MediaImageTextAlternativeEditing from './mediaimagetextalternative/mediaimagetextalternativeediting'; @@ -14,4 +18,5 @@ export default { MediaImageTextAlternative, MediaImageTextAlternativeEditing, MediaImageTextAlternativeUi, + DrupalLinkMedia, }; diff --git a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/ui/utils.js b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/ui/utils.js index 8ba4a5e4bf..6263f86445 100644 --- a/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/ui/utils.js +++ b/core/assets/vendor/ckeditor5/ckeditor5_plugins/drupalMedia/src/ui/utils.js @@ -7,6 +7,8 @@ import { getSelectedDrupalMediaWidget } from '../utils'; * * @param editor {Editor} The editor instance. * @returns {Options} + * + * @internal */ export function getBalloonPositionData(editor) { const editingView = editor.editing.view; @@ -32,6 +34,8 @@ export function getBalloonPositionData(editor) { * to the image in the editor content, if one is selected. * * @param editor {Editor} The editor instance. + * + * @internal */ export function repositionContextualBalloon(editor) { const balloon = editor.plugins.get('ContextualBalloon'); diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 473c68c589..f887c7cea3 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -27,7 +27,7 @@ ckeditor5: version: &ckeditor5Version "31.0.0" license: &ckeditor5License name: GNU-GPL-2.0-or-later - url: https://github.com/ckeditor/ckeditor5/blob/master/LICENSE.md + url: https://github.com/ckeditor/ckeditor5/blob/v31.0.0/LICENSE.md gpl-compatible: true js: assets/vendor/ckeditor5/build/ckeditor5-dll.js: { preprocess: false, minified: true } diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml index e3674b55d5..0389ad5913 100644 --- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml +++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml @@ -252,6 +252,22 @@ ckeditor5_linkImage: - ckeditor5_link - ckeditor5_image +ckeditor5_linkMedia: + ckeditor5: + plugins: + - drupalMedia.DrupalLinkMedia + config: + # Append the "Link" button to the media balloon toolbar. + drupalMedia: + toolbar: [drupalLinkMedia] + drupal: + label: Linked Media + elements: false + conditions: + plugins: + - ckeditor5_link + - media_media + ckeditor5_list: ckeditor5: plugins: [list.List] diff --git a/core/modules/ckeditor5/ckeditor5.module b/core/modules/ckeditor5/ckeditor5.module index 3bc9ff7d14..5ae100a3e0 100644 --- a/core/modules/ckeditor5/ckeditor5.module +++ b/core/modules/ckeditor5/ckeditor5.module @@ -93,9 +93,11 @@ function ckeditor5_module_implements_alter(&$implementations, $hook) { unset($implementations['ckeditor5']); $offset = array_search('editor', array_keys($implementations)) + 1; - $media_offset = array_search('media', array_keys($implementations)) + 1; - $max = max([$offset, $media_offset]); - $implementations = array_slice($implementations, 0, $max, TRUE) + + if (array_key_exists('media', $implementations)) { + $media_offset = array_search('media', array_keys($implementations)) + 1; + $offset = max([$offset, $media_offset]); + } + $implementations = array_slice($implementations, 0, $offset, TRUE) + ['ckeditor5' => $group] + array_slice($implementations, $offset, NULL, TRUE); } diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php index 2fded27d21..97a3209250 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php @@ -2,7 +2,6 @@ namespace Drupal\Tests\ckeditor5\FunctionalJavascript; -use Drupal\Component\Utility\Html; use Drupal\editor\Entity\Editor; use Drupal\file\Entity\File; use Drupal\filter\Entity\FilterFormat; @@ -144,7 +143,7 @@ function (ConstraintViolation $v) { * * @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/architecture/editing-engine.html#conversion * - * @dataProvider imageTypes + * @dataProvider providerLinkability */ public function testLinkability(string $image_type, bool $unrestricted) { assert($image_type === 'inline' || $image_type === 'block'); @@ -159,8 +158,8 @@ public function testLinkability(string $image_type, bool $unrestricted) { // Make the test content have either a block image or an inline image. $img_tag = 'drupalimage test image'; $this->host->body->value .= $image_type === 'block' - ? '' . $img_tag . '' - : '

' . $img_tag . '

'; + ? $img_tag + : "

$img_tag

"; $this->host->save(); // Adjust the expectations accordingly. $expected_widget_class = $image_type === 'block' ? 'image' : 'image-inline'; @@ -171,10 +170,42 @@ public function testLinkability(string $image_type, bool $unrestricted) { $this->waitForEditor(); $assert_session = $this->assertSession(); - // Assert the "editingDowncast" HTML before making changes. First assert the - // widget and link exist, then assert the expected DOM structure in detail. - $drupalimage = $assert_session->waitForElementVisible('css', '.ck-content .ck-widget.' . $expected_widget_class); - $this->assertNotNull($drupalimage); + // Initial state: the image CKEditor Widget is not selected. + $drupalimage = $assert_session->waitForElementVisible('css', ".ck-content .ck-widget.$expected_widget_class"); + $this->assertNotEmpty($drupalimage); + $this->assertFalse($drupalimage->hasClass('.ck-widget_selected')); + + // Assert the "editingDowncast" HTML before making changes. + $assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class . ' > img[src*="image-test.png"][alt="drupalimage test image"]'); + + // Assert the "dataDowncast" HTML before making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertNotEmpty($xpath->query('//img[@alt="drupalimage test image"]')); + $this->assertEmpty($xpath->query('//a')); + + // Assert the link button is present and not pressed. + $link_button = $this->getEditorButton('Link'); + $this->assertSame('false', $link_button->getAttribute('aria-pressed')); + + // Tests linking images. + $drupalimage->click(); + $this->assertTrue($drupalimage->hasClass('ck-widget_selected')); + $this->assertEditorButtonEnabled('Link'); + // Assert structure of image toolbar balloon. + $this->assertVisibleBalloon('.ck-toolbar[aria-label="Image toolbar"]'); + $link_image_button = $this->getBalloonButton('Link image'); + // Click the "Link image" button. + $this->assertSame('false', $link_image_button->getAttribute('aria-pressed')); + $link_image_button->press(); + // Assert structure of link form balloon. + $balloon = $this->assertVisibleBalloon('.ck-link-form'); + $url_input = $balloon->find('css', '.ck-labeled-field-view__input-wrapper .ck-input-text'); + // Fill in link form balloon's and hit "Save". + $url_input->setValue('http://www.drupal.org/association'); + $balloon->pressButton('Save'); + + // Assert the "editingDowncast" HTML after making changes. First assert the + // link exists, then assert the expected DOM structure in detail. $assert_session->elementExists('css', '.ck-content a[href*="//www.drupal.org/association"]'); // For inline images, the link is wrapping the widget; for block images the // link lives inside the widget. (This is how it is implemented upstream, it @@ -186,11 +217,45 @@ public function testLinkability(string $image_type, bool $unrestricted) { : '.ck-content .ck-widget.' . $expected_widget_class . ' a[href*="//www.drupal.org/association"] > img[src*="image-test.png"][alt="drupalimage test image"]' ); - // Assert the "dataDowncast" HTML before making changes. - $dom = Html::load($this->getSource()); - $xpath = new \DOMXPath($dom); + // Assert the "dataDowncast" HTML after making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); $this->assertCount(1, $xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]')); + $this->assertEmpty($xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); + + // Add `class="trusted"` to the link. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertEmpty($xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); + $this->pressEditorButton('Source'); + $source_text_area = $assert_session->waitForElement('css', '.ck-source-editing-area textarea'); + $this->assertNotEmpty($source_text_area); + $new_value = str_replace('getValue()); + $source_text_area->setValue('

temp

'); + $source_text_area->setValue($new_value); + $this->pressEditorButton('Source'); + // When unrestricted, additional attributes on links should be retained. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertCount($unrestricted ? 1 : 0, $xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); + + // Save the entity whose text field is being edited. + $page->pressButton('Save'); + + // Assert the HTML the end user sees. + $assert_session->elementExists('css', $unrestricted + ? 'a[href="http://www.drupal.org/association"].trusted img[src*="image-test.png"]' + : 'a[href="http://www.drupal.org/association"] img[src*="image-test.png"]'); + + // Go back to edit the now *linked* . Everything from this + // point onwards is effectively testing "upcasting" and proving there is no + // data loss. + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + + // Assert the "dataDowncast" HTML before making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertNotEmpty($xpath->query('//img[@alt="drupalimage test image"]')); + $this->assertNotEmpty($xpath->query('//a[@href="http://www.drupal.org/association"]')); + $this->assertNotEmpty($xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]')); $this->assertCount($unrestricted ? 1 : 0, $xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); // Tests unlinking images. @@ -224,8 +289,7 @@ public function testLinkability(string $image_type, bool $unrestricted) { $assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class . ' > img[src*="image-test.png"][alt="drupalimage test image"]'); // Assert the "dataDowncast" HTML after making changes. - $dom = Html::load($this->getSource()); - $xpath = new \DOMXPath($dom); + $xpath = new \DOMXPath($this->getEditorDataAsDom()); $this->assertCount(0, $xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]')); $this->assertCount(1, $xpath->query('//img[@alt="drupalimage test image"]')); // @todo Remove the different assertion for the "inline, unrestricted" case when https://www.drupal.org/project/ckeditor5/issues/3247634 is fixed. @@ -237,7 +301,7 @@ public function testLinkability(string $image_type, bool $unrestricted) { } } - public function imageTypes(): array { + public function providerLinkability(): array { return [ 'BLOCK image, restricted' => ['block', FALSE], 'BLOCK image, unrestricted' => ['block', TRUE], @@ -246,21 +310,4 @@ public function imageTypes(): array { ]; } - /** - * Switches to source view, gets the value, switches back, and returns it. - * - * @return string - * The current CKEditor 5 source value. - */ - protected function getSource(): string { - $assert_session = $this->assertSession(); - $this->pressEditorButton('Source'); - $this->assertNotEmpty($assert_session->waitForElement('css', '.ck-source-editing-area')); - $source_value = $assert_session - ->elementExists('css', '.ck-source-editing-area textarea') - ->getValue(); - $this->pressEditorButton('Source'); - return $source_value; - } - } diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php index bb9036deb9..0d3f6fe754 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php @@ -2,7 +2,6 @@ namespace Drupal\Tests\ckeditor5\FunctionalJavascript; -use Drupal\Component\Utility\Html; use Drupal\editor\Entity\Editor; use Drupal\file\Entity\File; use Drupal\filter\Entity\FilterFormat; @@ -72,6 +71,12 @@ protected function setUp(): void { 'format' => 'test_format', 'name' => 'Test format', 'filters' => [ + 'filter_html' => [ + 'status' => TRUE, + 'settings' => [ + 'allowed_html' => '


', + ], + ], 'filter_align' => ['status' => TRUE], 'filter_caption' => ['status' => TRUE], 'media_embed' => ['status' => TRUE], @@ -85,8 +90,6 @@ protected function setUp(): void { 'items' => [ 'sourceEditing', 'link', - 'italic', - 'bold', ], ], 'plugins' => [ @@ -315,9 +318,7 @@ public function testAlt() { $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-widget.drupal-media img')); // Test that by default no alt attribute is present on the drupal-media // element. - $this->pressEditorButton('Source'); $this->assertSourceAttributeSame('alt', NULL); - $this->pressEditorButton('Source'); // Test that the preview shows the alt value from the media field's // alt text. $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-widget.drupal-media img[alt*="default alt"]')); @@ -349,11 +350,9 @@ public function testAlt() { // Test that the downcast drupal-media element now has the alt attribute // entered in the dialog. - $this->pressEditorButton('Source'); $this->assertSourceAttributeSame('alt', $who_is_zartan); // The alt field should now display the override instead of the default. - $this->pressEditorButton('Source'); $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]'); @@ -373,12 +372,10 @@ public function testAlt() { // Test that the downcast drupal-media element now has the alt attribute // entered in the dialog. - $this->pressEditorButton('Source'); $this->assertSourceAttributeSame('alt', $cobra_commander_bio); // The default value of the alt field should now display the override // instead of the value on the media image field. - $this->pressEditorButton('Source'); $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]'); @@ -399,13 +396,11 @@ public function testAlt() { // Test that the downcast drupal-media element's alt attribute now has the // empty string indicator. - $this->pressEditorButton('Source'); $this->assertSourceAttributeSame('alt', '""'); // Test that setting alt to back to an empty string within the dialog will // restore the default alt value saved in to the media image field of the // media item. - $this->pressEditorButton('Source'); $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]'); @@ -415,7 +410,6 @@ public function testAlt() { // Test that the downcast drupal-media element no longer has an alt // attribute. - $this->pressEditorButton('Source'); $this->assertSourceAttributeSame('alt', NULL); } @@ -428,21 +422,148 @@ public function testTranslationAlt() { } /** - * Tests linkability of the CKEditor widget. + * Tests linkability of the media CKEditor widget. + * + * Due to the very different HTML markup generated for the editing view and + * the data view, this is explicitly testing the "editingDowncast" and + * "dataDowncast" results. These are CKEditor 5 concepts. + * + * @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/architecture/editing-engine.html#conversion + * + * @dataProvider providerLinkability */ - public function testLinkability() { + public function testLinkability(bool $unrestricted) { + // Disable filter_html. + if ($unrestricted) { + FilterFormat::load('test_format') + ->setFilterConfig('filter_html', ['status' => FALSE]) + ->save(); + } + $page = $this->getSession()->getPage(); $this->drupalGet($this->host->toUrl('edit-form')); $this->waitForEditor(); $assert_session = $this->assertSession(); - // Select the CKEditor Widget. - $drupalmedia = $assert_session->waitForElementVisible('css', '.ck-widget.drupal-media img'); + // Initial state: the Drupal Media CKEditor Widget is not selected. + $drupalmedia = $assert_session->waitForElementVisible('css', '.ck-content .ck-widget.drupal-media'); $this->assertNotEmpty($drupalmedia); + $this->assertFalse($drupalmedia->hasClass('.ck-widget_selected')); + + // Assert the "editingDowncast" HTML before making changes. + $assert_session->elementExists('css', '.ck-content .ck-widget.drupal-media > [data-drupal-media-preview]'); + + // Assert the "dataDowncast" HTML before making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertNotEmpty($xpath->query('//drupal-media')); + $this->assertEmpty($xpath->query('//a')); + + // Assert the link button is present and not pressed. + $link_button = $this->getEditorButton('Link'); + $this->assertSame('false', $link_button->getAttribute('aria-pressed')); + + // Wait for the preview to load. + $preview = $assert_session->waitForElement('css', '.ck-content .ck-widget.drupal-media [data-drupal-media-preview="ready"]'); + $this->assertNotEmpty($preview); + + // Tests linking Drupal media. $drupalmedia->click(); + $this->assertTrue($drupalmedia->hasClass('ck-widget_selected')); + $this->assertEditorButtonEnabled('Link'); + // Assert structure of image toolbar balloon. + $this->assertVisibleBalloon('.ck-toolbar[aria-label="Drupal Media toolbar"]'); + $link_media_button = $this->getBalloonButton('Link media'); + // Click the "Link media" button. + $this->assertSame('false', $link_media_button->getAttribute('aria-pressed')); + $link_media_button->press(); + // Assert structure of link form balloon. + $balloon = $this->assertVisibleBalloon('.ck-link-form'); + $url_input = $balloon->find('css', '.ck-labeled-field-view__input-wrapper .ck-input-text'); + // Fill in link form balloon's and hit "Save". + $url_input->setValue('http://linking-embedded-media.com'); + $balloon->pressButton('Save'); + + // Assert the "editingDowncast" HTML after making changes. Assert the link + // exists, then assert the link exists. Then assert the expected DOM + // structure in detail. + $assert_session->elementExists('css', '.ck-content a[href="http://linking-embedded-media.com"]'); + $assert_session->elementExists('css', '.ck-content .drupal-media.ck-widget > a[href="http://linking-embedded-media.com"] > div[aria-label] > article > div > img[src*="image-test.png"]'); + + // Assert the "dataDowncast" HTML after making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertNotEmpty($xpath->query('//drupal-media')); + $this->assertNotEmpty($xpath->query('//a[@href="http://linking-embedded-media.com"]')); + $this->assertNotEmpty($xpath->query('//a[@href="http://linking-embedded-media.com"]/drupal-media')); + + // Add `class="trusted"` to the link. + $this->assertEmpty($xpath->query('//a[@href="http://linking-embedded-media.com" and @class="trusted"]')); + $this->pressEditorButton('Source'); + $source_text_area = $assert_session->waitForElement('css', '.ck-source-editing-area textarea'); + $this->assertNotEmpty($source_text_area); + $new_value = str_replace('getValue()); + $source_text_area->setValue('

temp

'); + $source_text_area->setValue($new_value); + $this->pressEditorButton('Source'); + + // When unrestricted, additional attributes on links should be retained. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertCount($unrestricted ? 1 : 0, $xpath->query('//a[@href="http://linking-embedded-media.com" and @class="trusted"]')); + + // Save the entity whose text field is being edited. + $page->pressButton('Save'); - // @todo Make media widgets linkable in https://www.drupal.org/project/ckeditor5/issues/3246169 and port its test coverage. + // Assert the HTML the end user sees. + + $assert_session->elementExists('css', $unrestricted + ? 'a[href="http://linking-embedded-media.com"].trusted img[src*="image-test.png"]' + : 'a[href="http://linking-embedded-media.com"] img[src*="image-test.png"]'); + + // Go back to edit the now *linked* . Everything from this + // point onwards is effectively testing "upcasting" and proving there is no + // data loss. + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + + // Assert the "dataDowncast" HTML before making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertNotEmpty($xpath->query('//drupal-media')); + $this->assertNotEmpty($xpath->query('//a[@href="http://linking-embedded-media.com"]')); + $this->assertNotEmpty($xpath->query('//a[@href="http://linking-embedded-media.com"]/drupal-media')); + + // Tests unlinking media. + $drupalmedia->click(); + $this->assertEditorButtonEnabled('Link'); + $this->assertSame('true', $this->getEditorButton('Link')->getAttribute('aria-pressed')); + // Assert structure of Drupal media toolbar balloon. + $this->assertVisibleBalloon('.ck-toolbar[aria-label="Drupal Media toolbar"]'); + $link_media_button = $this->getBalloonButton('Link media'); + $this->assertSame('true', $link_media_button->getAttribute('aria-pressed')); + $link_media_button->click(); + // Assert structure of link actions balloon. + $this->getBalloonButton('Edit link'); + $unlink_image_button = $this->getBalloonButton('Unlink'); + // Click the "Unlink" button. + $unlink_image_button->click(); + $this->assertSame('false', $this->getEditorButton('Link')->getAttribute('aria-pressed')); + + // Assert the "editingDowncast" HTML after making changes. Assert the link + // exists, then assert no link exists. Then assert the expected DOM + // structure in detail. + $assert_session->elementNotExists('css', '.ck-content a'); + $assert_session->elementExists('css', '.ck-content .drupal-media.ck-widget > div[aria-label] > article > div > img[src*="image-test.png"]'); + + // Assert the "dataDowncast" HTML after making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertNotEmpty($xpath->query('//drupal-media')); + $this->assertEmpty($xpath->query('//a')); + } + + public function providerLinkability(): array { + return [ + 'restricted' => [FALSE], + 'unrestricted' => [TRUE], + ]; } /** @@ -548,12 +669,14 @@ public function testViewMode() { * doesn't have the attribute. */ protected function assertSourceAttributeSame($attribute, $value) { - $this->assertNotEmpty($drupal_media = $this->getDrupalMediaFromSource()); + $dom = $this->getEditorDataAsDom(); + $drupal_media = (new \DOMXPath($dom))->query('//drupal-media'); + $this->assertNotEmpty($drupal_media); if ($value === NULL) { - $this->assertFalse($drupal_media->hasAttribute($attribute)); + $this->assertFalse($drupal_media[0]->hasAttribute($attribute)); } else { - $this->assertSame($value, $drupal_media->getAttribute($attribute)); + $this->assertSame($value, $drupal_media[0]->getAttribute($attribute)); } } @@ -578,22 +701,4 @@ protected function getLastPreviewRequestTransferSize() { return $this->getSession()->evaluateScript($javascript); } - /** - * Parses the element from CKEditor's "source" view. - * - * Assumes CKEditor is in source mode. - * - * @return \DOMNode|null - * The drupal-media element or NULL if it can't be found. - */ - protected function getDrupalMediaFromSource() { - $value = $this->assertSession() - ->elementExists('css', '.ck-source-editing-area textarea') - ->getValue(); - $dom = Html::load($value); - $xpath = new \DOMXPath($dom); - $list = $xpath->query('//drupal-media'); - return count($list) > 0 ? $list[0] : NULL; - } - } diff --git a/core/modules/ckeditor5/tests/src/Traits/CKEditor5TestTrait.php b/core/modules/ckeditor5/tests/src/Traits/CKEditor5TestTrait.php index c1a2cc10fa..738e0574d6 100644 --- a/core/modules/ckeditor5/tests/src/Traits/CKEditor5TestTrait.php +++ b/core/modules/ckeditor5/tests/src/Traits/CKEditor5TestTrait.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\ckeditor5\Traits; use Behat\Mink\Element\NodeElement; +use Drupal\Component\Utility\Html; /** * Provides methods to test CKEditor 5. @@ -11,6 +12,26 @@ */ trait CKEditor5TestTrait { + /** + * Gets CKEditor 5 instance data as a PHP DOMDocument. + * + * @return \DOMDocument + * The result of parsing CKEditor 5's data into a PHP DOMDocument. + * + * @see https://ckeditor.com/docs/ckeditor5/latest/api/module_editor-classic_classiceditor-ClassicEditor.html#function-getData + */ + protected function getEditorDataAsDom(): \DOMDocument { + // We cannot trust on CKEditor updating the textarea every time model + // changes. Therefore, the most reliable way to get downcasted data is to + // use the CKEditor API. + $javascript = <<getSession()->evaluateScript($javascript)); + } + /** * Waits for CKEditor to initialize. */ @@ -52,6 +73,7 @@ protected function getEditorButton($name) { */ protected function assertEditorButtonDisabled($name) { $button = $this->getEditorButton($name); + $this->assertTrue($button->hasAttribute('aria-disabled')); $this->assertTrue($button->hasClass('ck-disabled')); } @@ -63,6 +85,7 @@ protected function assertEditorButtonDisabled($name) { */ protected function assertEditorButtonEnabled($name) { $button = $this->getEditorButton($name); + $this->assertFalse($button->hasAttribute('aria-disabled')); $this->assertFalse($button->hasClass('ck-disabled')); } diff --git a/core/package.json b/core/package.json index bcce9ec738..0dbf864c57 100644 --- a/core/package.json +++ b/core/package.json @@ -37,7 +37,6 @@ "@babel/core": "^7.0.0", "@babel/preset-env": "^7.0.0", "@babel/register": "^7.7.7", - "@ckeditor/ckeditor5-dev-utils": "^25.4.5", "@ckeditor/ckeditor5-alignment": "~31.0.0", "@ckeditor/ckeditor5-basic-styles": "~31.0.0", "@ckeditor/ckeditor5-block-quote": "~31.0.0", diff --git a/core/yarn.lock b/core/yarn.lock index a0d11bd7e9..4224ee1ac2 100644 --- a/core/yarn.lock +++ b/core/yarn.lock @@ -2401,9 +2401,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001274: - version "1.0.30001278" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001278.tgz#51cafc858df77d966b17f59b5839250b24417fff" - integrity sha512-mpF9KeH8u5cMoEmIic/cr7PNS+F5LWBk0t2ekGT60lFf0Wq+n9LspAj0g3P+o7DQhD3sUdlMln4YFAWhFYn9jg== + version "1.0.30001279" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001279.tgz#eb06818da481ef5096a3b3760f43e5382ed6b0ce" + integrity sha512-VfEHpzHEXj6/CxggTwSFoZBBYGQfQv9Cf42KPlO79sWXCD1QNKWKsKzFeWL7QpZHJQYAvocqV6Rty1yJMkqWLQ== caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001271: version "1.0.30001271" @@ -3361,9 +3361,9 @@ electron-to-chromium@^1.3.878: integrity sha512-iwIP/6WoeSimzUKJIQtjtpVDsK8Ir8qQCMXsUBwg+rxJR2Uh3wTNSbxoYRfs+3UWx/9MAnPIxVZCyWkm8MT0uw== electron-to-chromium@^1.3.886: - version "1.3.890" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.890.tgz#e7143b659f73dc4d0512d1ae4baeb0fb9e7bc835" - integrity sha512-VWlVXSkv0cA/OOehrEyqjUTHwV8YXCPTfPvbtoeU2aHR21vI4Ejh5aC4AxUwOmbLbBgb6Gd3URZahoCxtBqCYQ== + version "1.3.892" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.892.tgz#0e3f5bb1de577e2e5a6dffd5a4b278c4a735cd39" + integrity sha512-YDW4yIjdfMnbRoBjRZ/aNQYmT6JgQFLwmTSDRJMQdrY4MByEzppdXp3rnJ0g4LBWcsYTUvwKKClYN1ofZ0COOQ== emoji-regex@^7.0.1: version "7.0.3" @@ -5703,13 +5703,25 @@ mime-db@1.50.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f" integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19: +mime-db@1.51.0: + version "1.51.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" + integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== + +mime-types@^2.1.12, mime-types@~2.1.19: version "2.1.33" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.33.tgz#1fa12a904472fafd068e48d9e8401f74d3f70edb" integrity sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g== dependencies: mime-db "1.50.0" +mime-types@^2.1.27: + version "2.1.34" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" + integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== + dependencies: + mime-db "1.51.0" + mime@^2.3.1: version "2.5.2" resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" @@ -8843,9 +8855,9 @@ webpack-sources@^3.2.0: integrity sha512-t6BMVLQ0AkjBOoRTZgqrWm7xbXMBzD+XDq2EZ96+vMfn3qKgsvdXZhbPZ4ElUOpdv4u+iiGe+w3+J75iy/bYGA== webpack@^5.51.1: - version "5.62.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.62.1.tgz#06f09b56a7b1bb13ed5137ad4b118358a90c9505" - integrity sha512-jNLtnWChS2CMZ7vqWtztv0G6fYB5hz11Zsadp5tE7e4/66zVDj7/KUeQZOsOl8Hz5KrLJH1h2eIDl6AnlyE12Q== + version "5.62.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.62.2.tgz#9a352423396a3d434f8c3aef19dcfd79b58fe88a" + integrity sha512-GDJymz2MEpfcLoLHQFtV72raCVsuQtlUHeeNixbYh5DkFombEhoLyto3GU8xA42VfRXR7pxrYQ75Sd+YelFe5A== dependencies: "@types/eslint-scope" "^3.7.0" "@types/estree" "^0.0.50"