diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
index b7b654681899f2d5b7bc3b92a9043b79ee535605..8a588ee09ea01a79d70d43bdcd39c3b2b7d373b7 100644
--- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
+++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
@@ -518,6 +518,49 @@ media_media:
     conditions:
       filter: media_embed
 
+media_mediaAlign:
+  provider: media
+  ckeditor5:
+    plugins:
+      - drupalMedia.DrupalElementStyle
+    config:
+      drupalElementStyles:
+        options:
+          - name: 'alignRight'
+            title: 'Right aligned media'
+            icon: 'objectRight'
+            attributeName: 'data-align'
+            attributeValue: 'right'
+            modelElements: [ 'drupalMedia' ]
+          - name: 'alignLeft'
+            title: 'Left aligned media'
+            icon: 'objectLeft'
+            attributeName: 'data-align'
+            attributeValue: 'left'
+            modelElements: [ 'drupalMedia' ]
+          - name: 'alignCenter'
+            title: 'Centered media'
+            icon: 'objectCenter'
+            attributeName: 'data-align'
+            attributeValue: 'center'
+            modelElements: ['drupalMedia']
+      drupalMedia:
+        toolbar:
+          - name: 'drupalMedia:align'
+            items:
+              - 'drupalElementStyle:alignLeft'
+              - 'drupalElementStyle:alignCenter'
+              - 'drupalElementStyle:alignRight'
+            defaultItem: 'drupalElementStyle:alignCenter'
+  drupal:
+    label: Media align
+    library: ckeditor5/drupal.ckeditor5.mediaAlign
+    elements:
+      - <drupal-media data-align>
+    conditions:
+      filter: filter_align
+      plugins: [media_media]
+
 media_library_mediaLibrary:
   provider: media_library
   ckeditor5:
diff --git a/core/modules/ckeditor5/ckeditor5.libraries.yml b/core/modules/ckeditor5/ckeditor5.libraries.yml
index d25a96a0ca19b7c62d125e5efa9905795bb724f5..c0ce9cf5edfc671a22b238b4105856c8db6a433a 100644
--- a/core/modules/ckeditor5/ckeditor5.libraries.yml
+++ b/core/modules/ckeditor5/ckeditor5.libraries.yml
@@ -57,6 +57,13 @@ drupal.ckeditor5.media:
     - core/ckeditor5
     - core/drupal
 
+drupal.ckeditor5.mediaAlign:
+  css:
+    theme:
+      css/media-alignment.css: { }
+  dependencies:
+    - ckeditor5/drupal.ckeditor5.media
+
 ie11.user.warnings:
   js:
     js/ie11.user.warnings.js: { }
diff --git a/core/modules/ckeditor5/css/media-alignment.css b/core/modules/ckeditor5/css/media-alignment.css
new file mode 100644
index 0000000000000000000000000000000000000000..6ff5a6b16f87b74cef9f6d7c01264630c3ec9bc2
--- /dev/null
+++ b/core/modules/ckeditor5/css/media-alignment.css
@@ -0,0 +1,18 @@
+.ck-content .drupal-media-style-align-right {
+  float: right;
+  margin-left: 1.5rem;
+}
+.ck-content .drupal-media-style-align-left {
+  float: left;
+  margin-right: 1.5rem;
+}
+.ck-content .drupal-media-style-align-left,
+.ck-content .drupal-media-style-align-right {
+  clear: both;
+  max-width: 50%;
+}
+.ck-content .drupal-media-style-align-center {
+  max-width: 50%;
+  margin-right: auto;
+  margin-left: auto;
+}
diff --git a/core/modules/ckeditor5/js/build/drupalMedia.js b/core/modules/ckeditor5/js/build/drupalMedia.js
index fd1540e85d90a1ff2c747a99c39c486e565a27a5..6818484fcecca645de01304e18537e581ce0144d 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(r){var a=t[r];if(void 0!==a)return a.exports;var n=t[r]={exports:{}};return e[r](n,n.exports,i),n.exports}i.d=function(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=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var r={};return function(){"use strict";i.d(r,{default:function(){return y}});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)),{}),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 n 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=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,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"}),a=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),a),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="<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 s=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:a={}}=t;i&&"function"==typeof r&&e.ui.componentFactory.add("drupalMedia",(t=>{const n=e.commands.get("insertDrupalMedia"),o=new s.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(n,"value","isEnabled"),this.listenTo(o,"execute",(()=>{r(i,(({attributes:t})=>{e.execute("insertDrupalMedia",t)}),a)})),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;e.plugins.get(t.WidgetToolbarRepository).register("drupalMedia",{ariaLabel:Drupal.t("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{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)}))}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")}),a=await fetch(`${i}&${r}`);return a.ok?JSON.parse(await a.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=s.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 f extends s.View{constructor(t){super(t),this.focusTracker=new p.FocusTracker,this.keystrokes=new p.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 s.ViewCollection,this._focusCycler=new s.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,s.injectCssTransitionDisabler)(this)}render(){super.render(),this.keystrokes.listenTo(this.element),(0,s.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 a=new s.ButtonView(this.locale);return a.set({label:e,icon:t,tooltip:!0}),a.extendTemplate({attributes:{class:i}}),r&&a.delegate("execute").to(this,r),a}_createLabeledInputView(){const e=new s.LabeledFieldView(this.locale,s.createLabeledInputText);return e.label=Drupal.t("Override text alternative"),e}}class g extends e.Plugin{static get requires(){return[s.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 r=t.commands.get("mediaImageTextAlternative"),a=new s.ButtonView(i);return a.set({label:Drupal.t("Override media image text alternative"),icon:e.icons.lowVision,tooltip:!0}),a.bind("isVisible").to(r,"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 f(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,s.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,g]}static get pluginName(){return"MediaImageTextAlternative"}}function w(e,t,i){if(t.attributes)for(const[r,a]of Object.entries(t.attributes))e.setAttribute(r,a,i);t.styles&&e.setStyle(t.styles,i),t.classes&&e.addClass(t.classes,i)}function v(e){return t=>{t.on("element:drupal-media",((t,i,r)=>{const a=i.viewItem.parent;a.is("element","a")&&function(t,a){const n=e._consumeAllowedAttributes(t,r);n&&r.writer.setAttribute(a,n,i.modelRange)}(a,"htmlLinkAttributes")}),{priority:"low"})}}class M extends e.Plugin{init(){const{editor:e}=this;if(!e.plugins.has("GeneralHtmlSupport"))return;const{schema:t}=e.model,{conversion:i}=e,r=e.plugins.get("DataFilter");t.extend("drupalMedia",{allowAttributes:["htmlLinkAttributes"]}),i.for("upcast").add(v(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),a=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"),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 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[n,M,o,u,b]}}function _(){return e=>{e.on("element:a",((e,t,i)=>{const r=t.viewItem,a=(n=r,Array.from(n.getChildren()).find((e=>"drupal-media"===e.name)));var n;if(!a)return;if(!i.consumable.consume(r,{attributes:["href"]}))return;const s=r.getAttribute("href");if(!s)return;const o=i.convertItem(a,t.modelCursor);t.modelRange=o.modelRange,t.modelCursor=o.modelCursor;const l=t.modelCursor.nodeBefore;l&&l.is("element","drupalMedia")&&i.writer.setAttribute("linkHref",s,l)}),{priority:"high"})}}class A 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(_()),e.conversion.for("editingDowncast").add((e=>{e.on("attribute:linkHref:drupalMedia",((e,t,i)=>{const{writer:r}=i;if(!i.consumable.consume(t.item,e.name))return;const a=i.mapper.toViewElement(t.item),n=Array.from(a.getChildren()).find((e=>"a"===e.name));if(n)t.attributeNewValue?r.setAttribute("href",t.attributeNewValue,n):(r.move(r.createRangeIn(n),r.createPositionAt(a,0)),r.remove(n));else{const e=Array.from(a.getChildren()).find((e=>e.getAttribute("data-drupal-media-preview"))),i=r.createContainerElement("a",{href:t.attributeNewValue});r.insert(r.createPositionAt(a,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{writer:r}=i;if(!i.consumable.consume(t.item,e.name))return;const a=i.mapper.toViewElement(t.item),n=r.createContainerElement("a",{href:t.attributeNewValue});r.insert(r.createPositionBefore(a),n),r.move(r.createRangeOn(a),r.createPositionAt(n,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 s.ButtonView(t),r=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)?r._addActionsView():r._showUI(!0)})),i}))}_isSelectedLinkedMedia(e){const t=e.getSelectedElement();return!!t&&t.is("element","drupalMedia")&&t.hasAttribute("linkHref")}}class E extends e.Plugin{static get requires(){return[A,x]}static get pluginName(){return"DrupalLinkMedia"}}var y={DrupalMedia:k,MediaImageTextAlternative:b,MediaImageTextAlternativeEditing:m,MediaImageTextAlternativeUi:g,DrupalLinkMedia:E}}(),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 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(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 Z}});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}}class r 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 a(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 n=i.createContainerElement("div",{class:"drupal-media"}),a=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(n,0),a),i.setCustomProperty("drupalMedia",!0,n),(0,t.toWidget)(n,i,{label:"media widget"})}}),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=>{e.attributeToAttribute({model:{key:t,name:"drupalMedia"},view:{name:"drupal-media",key:this.attrs[t]}})}))}_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 l=i("ckeditor5/src/ui.js");class s 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"),s=new l.ButtonView(t);return s.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}),s.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(s,"execute",(()=>{n(i,(({attributes:t})=>{e.execute("insertDrupalMedia",t)}),a)})),s}))}}function o(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}function u(e){const t=typeof e;return null!=e&&("object"===t||"function"===t)}class c 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=>u(e)?e.name:e))||[]),getRelatedElement:e=>d(e)})}}class m extends e.Command{refresh(){const e=this.editor.model.document.selection.getSelectedElement();this.isEnabled=!1,o(e)&&this._isMediaImage(e).then((e=>{this.isEnabled=e})),o(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)}))}async _isMediaImage(e){const t=this.editor.config.get("drupalMedia");if(!t)return null;const{isMediaUrl:i}=t,n=new URLSearchParams({uuid:e.getAttribute("drupalMediaEntityUuid")}),a=await fetch(`${i}&${n}`);return a.ok?JSON.parse(await a.text()):null}}class g extends e.Plugin{static get pluginName(){return"MediaImageTextAlternativeEditing"}init(){this.editor.commands.add("mediaImageTextAlternative",new m(this.editor))}}function p(e){const t=e.editing.view,i=l.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 h=i("ckeditor5/src/utils.js");class f extends l.View{constructor(t){super(t),this.focusTracker=new h.FocusTracker,this.keystrokes=new h.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 l.ViewCollection,this._focusCycler=new l.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,l.injectCssTransitionDisabler)(this)}render(){super.render(),this.keystrokes.listenTo(this.element),(0,l.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 l.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 l.LabeledFieldView(this.locale,l.createLabeledInputText);return e.label=Drupal.t("Override text alternative"),e}}class b extends e.Plugin{static get requires(){return[l.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 l.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 f(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=p(e);t.updatePosition(i)}}(e):this._hideForm(!0)})),(0,l.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:p(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[g,b]}static get pluginName(){return"MediaImageTextAlternative"}}function y(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 v(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 E 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(v(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");y(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;y(i.writer,t.item.getAttribute("htmlLinkAttributes"),n)}),{priority:"low"})))}static get pluginName(){return"DrupalMediaGeneralHtmlSupport"}}class k extends e.Plugin{static get requires(){return[r,E,s,c,w]}static get pluginName(){return"DrupalMedia"}}function M(){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 l=n.getAttribute("href");if(!l)return;const s=i.convertItem(a,t.modelCursor);t.modelRange=s.modelRange,t.modelCursor=s.modelCursor;const o=t.modelCursor.nodeBefore;o&&o.is("element","drupalMedia")&&i.writer.setAttribute("linkHref",l,o)}),{priority:"high"})}}class A 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(M()),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 _ 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 l.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 x extends e.Plugin{static get requires(){return[A,_]}static get pluginName(){return"DrupalLinkMedia"}}const{objectFullWidth:S,objectInline:C,objectLeft:V,objectRight:D,objectCenter:L,objectBlockLeft:I,objectBlockRight:B}=e.icons,T={inline:{name:"inline",title:"In line",icon:C,modelElements:["imageInline"],isDefault:!0},alignLeft:{name:"alignLeft",title:"Left aligned image",icon:V,modelElements:["imageBlock","imageInline"],className:"image-style-align-left"},alignBlockLeft:{name:"alignBlockLeft",title:"Left aligned image",icon:I,modelElements:["imageBlock"],className:"image-style-block-align-left"},alignCenter:{name:"alignCenter",title:"Centered image",icon:L,modelElements:["imageBlock"],className:"image-style-align-center"},alignRight:{name:"alignRight",title:"Right aligned image",icon:D,modelElements:["imageBlock","imageInline"],className:"image-style-align-right"},alignBlockRight:{name:"alignBlockRight",title:"Right aligned image",icon:B,modelElements:["imageBlock"],className:"image-style-block-align-right"},block:{name:"block",title:"Centered image",icon:L,modelElements:["imageBlock"],isDefault:!0},side:{name:"side",title:"Side image",icon:D,modelElements:["imageBlock"],className:"image-style-side"}},N={full:S,left:I,right:B,center:L,inlineLeft:V,inlineRight:D,inline:C},P=[{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 O(e){(0,h.logWarning)("image-style-configuration-definition-invalid",e)}var R={normalizeStyles:function(e){return(e.configuredStyles.options||[]).map((e=>function(e){e="string"==typeof e?T[e]?{...T[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}(T[e.name],e);"string"==typeof e.icon&&(e.icon=N[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 O({style:e}),!1;{const a=[t?"imageBlock":null,i?"imageInline":null];if(!n.some((e=>a.includes(e))))return(0,h.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")?[...P]:[]},warnInvalidStyle:O,DEFAULT_OPTIONS:T,DEFAULT_ICONS:N,DEFAULT_DROPDOWN_DEFINITIONS:P};function j(e,t){const i=e.getSelectedElement();return i&&t.checkAttribute(i,"drupalElementStyle")?i:e.getFirstPosition().findAncestor((e=>t.checkAttribute(e,"drupalElementStyle")))}class F extends e.Command{constructor(e,t){super(e),this._styles=new Map(t.map((e=>[e.name,e])))}refresh(){const e=this.editor,t=j(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=j(t.document.selection,t.schema);!n||this._styles.get(n).isDefault?i.removeAttribute("drupalElementStyle",a):i.setAttribute("drupalElementStyle",n,a)}))}}function H(e,t){for(const i of t)if(i.name===e)return i}class U 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 F(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=H(t.attributeNewValue,n),r=H(t.attributeOldValue,n),l=i.mapper.toViewElement(t.item),s=i.writer;r&&("class"===r.attributeName?s.removeClass(r.attributeValue,l):s.removeAttribute(r.attributeName,l)),a&&("class"===a.attributeName?s.addClass(a.attributeValue,l):s.setAttribute(a.attributeName,a.attributeValue,l))});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,h.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 W=e=>e,K=(e,t)=>(e?`${e}: `:"")+t;function z(e){return`drupalElementStyle:${e}`}class q extends e.Plugin{static get requires(){return[U]}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(u).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:s,title:o}=e,d=s.filter((e=>t.find((({name:t})=>z(t)===e)))).map((e=>{const t=i.create(e);return e===r&&(a=t),t}));s.length!==d.length&&R.warnInvalidStyle({dropdown:e});const u=(0,l.createDropdown)(n,l.SplitButtonView),c=u.buttonView;return(0,l.addToolbarToDropdown)(u,d),c.set({label:K(o,a.label),class:null,tooltip:!0}),c.bind("icon").toMany(d,"isOn",((...e)=>{const t=e.findIndex(W);return t<0?a.icon:d[t].icon})),c.bind("label").toMany(d,"isOn",((...e)=>{const t=e.findIndex(W);return K(o,t<0?a.label:d[t].label)})),c.bind("isOn").toMany(d,"isOn",((...e)=>e.some(W))),c.bind("class").toMany(d,"isOn",((...e)=>e.some(W)?"ck-splitbutton_flatten":null)),c.on("execute",(()=>{d.some((({isOn:e})=>e))?u.isOpen=!u.isOpen:a.fire("execute")})),u.bind("isEnabled").toMany(d,"isEnabled",((...e)=>e.some(W))),u}))}_createButton(e){const t=e.name;this.editor.ui.componentFactory.add(z(t),(i=>{const n=this.editor.commands.get("drupalElementStyle"),a=new l.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 $ extends e.Plugin{static get requires(){return[U,q]}static get pluginName(){return"DrupalElementStyle"}}var Z={DrupalMedia:k,MediaImageTextAlternative:w,MediaImageTextAlternativeEditing:g,MediaImageTextAlternativeUi:b,DrupalLinkMedia:x,DrupalElementStyle:$}}(),n=n.default}()}));
\ No newline at end of file
diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle.js
new file mode 100644
index 0000000000000000000000000000000000000000..9bfa5d571b45d7b87b3f63f8f5ace2d98ba7942c
--- /dev/null
+++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle.js
@@ -0,0 +1,49 @@
+/* eslint-disable import/no-extraneous-dependencies */
+/* cspell:words drupalelementstyle drupalelementstyleui drupalelementstyleediting imagestyle drupalmediatoolbar drupalmediaediting */
+import { Plugin } from 'ckeditor5/src/core';
+import DrupalElementStyleUi from './drupalelementstyle/drupalelementstyleui';
+import DrupalElementStyleEditing from './drupalelementstyle/drupalelementstyleediting';
+
+/**
+ * @module drupalMedia/drupalelementstyle
+ */
+
+/**
+ * The Drupal Element Style plugin.
+ *
+ * This plugin is internal and it is currently only used for providing
+ * `data-align` support to `<drupal-media>`. However, this plugin isn't tightly
+ * coupled to `<drupal-media>` or `data-align`. The intent is to make this
+ * plugin a starting point for adding `data-align` support to other elements,
+ * because the `FilterAlign` filter plugin PHP code also does not limit itself
+ * to a specific HTML element. This could be also used for other filters to
+ * provide same authoring experience as `FilterAlign` without the need for
+ * additional JavaScript code.
+ *
+ * To be able to change element styles in the UI, the model element needs to
+ * have a toolbar where the element style buttons can be displayed.
+ *
+ * This plugin is inspired by the CKEditor 5 Image Style plugin.
+ *
+ * @see module:image/imagestyle~ImageStyle
+ * @see core/modules/ckeditor5/css/media-alignment.css
+ * @see module:drupalMedia/drupalmediaediting~DrupalMediaEditing
+ * @see module:drupalMedia/drupalmediatoolbar~DrupalMediaToolbar
+ *
+ * @internal
+ */
+export default class DrupalElementStyle extends Plugin {
+  /**
+   * @inheritDoc
+   */
+  static get requires() {
+    return [DrupalElementStyleEditing, DrupalElementStyleUi];
+  }
+
+  /**
+   * @inheritdoc
+   */
+  static get pluginName() {
+    return 'DrupalElementStyle';
+  }
+}
diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle/drupalelementstylecommand.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle/drupalelementstylecommand.js
new file mode 100644
index 0000000000000000000000000000000000000000..bffb290f55e5b64193fb41727b18c3bfc9661477
--- /dev/null
+++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle/drupalelementstylecommand.js
@@ -0,0 +1,111 @@
+/* eslint-disable import/no-extraneous-dependencies */
+/* cspell:words documentselection */
+import { Command } from 'ckeditor5/src/core';
+
+/**
+ * @module drupalMedia/drupalelementstyle/drupalelementstylecommand
+ */
+
+/**
+ * Gets closest element that has drupalElementStyle attribute in schema.
+ *
+ * @param {module:engine/model/documentselection~DocumentSelection} selection
+ *   The current document selection.
+ * @param {module:engine/model/schema~Schema} schema
+ *   The model schema.
+ *
+ * @return {null|module:engine/model/element~Element}
+ *   The closest element that supports element styles.
+ */
+function getClosestElementWithElementStyleAttribute(selection, schema) {
+  const selectedElement = selection.getSelectedElement();
+
+  return selectedElement &&
+    schema.checkAttribute(selectedElement, 'drupalElementStyle')
+    ? selectedElement
+    : selection
+        .getFirstPosition()
+        .findAncestor((element) =>
+          schema.checkAttribute(element, 'drupalElementStyle'),
+        );
+}
+
+/**
+ * The Drupal Element style command.
+ *
+ * This is used to apply Drupal Element style option to supported model elements.
+ *
+ * @extends module:core/command~Command
+ *
+ * @internal
+ */
+export default class DrupalElementStyleCommand extends Command {
+  /**
+   * Constructs a new object.
+   *
+   * @param {module:core/editor/editor~Editor} editor
+   *   The editor instance.
+   * @param {Drupal.CKEditor5~DrupalElementStyle[]} styles
+   *   All available Drupal Element Styles.
+   */
+  constructor(editor, styles) {
+    super(editor);
+    this._styles = new Map(
+      styles.map((style) => {
+        return [style.name, style];
+      }),
+    );
+  }
+
+  /**
+   * @inheritDoc
+   */
+  refresh() {
+    const editor = this.editor;
+    const element = getClosestElementWithElementStyleAttribute(
+      editor.model.document.selection,
+      editor.model.schema,
+    );
+
+    this.isEnabled = !!element;
+
+    if (!this.isEnabled) {
+      this.value = false;
+    } else if (element.hasAttribute('drupalElementStyle')) {
+      this.value = element.getAttribute('drupalElementStyle');
+    } else {
+      this.value = false;
+    }
+  }
+
+  /**
+   * Executes the command and applies the style to the selected model element.
+   *
+   * @example
+   *    editor.execute('drupalElementStyle', { value: 'alignLeft' });
+   *
+   * @param {Object} options
+   *   The command options.
+   * @param {string} options.value
+   *   The name of the style as configured in the Drupal Element style
+   *   configuration.
+   */
+  execute(options = {}) {
+    const editor = this.editor;
+    const model = editor.model;
+
+    model.change((writer) => {
+      const requestedStyle = options.value;
+      const element = getClosestElementWithElementStyleAttribute(
+        model.document.selection,
+        model.schema,
+      );
+
+      if (!requestedStyle || this._styles.get(requestedStyle).isDefault) {
+        writer.removeAttribute('drupalElementStyle', element);
+      } else {
+        writer.setAttribute('drupalElementStyle', requestedStyle, element);
+      }
+    });
+  }
+}
diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle/drupalelementstyleediting.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle/drupalelementstyleediting.js
new file mode 100644
index 0000000000000000000000000000000000000000..269c0a3e65addb16ac4b28e6fe7c7c14b31bd609
--- /dev/null
+++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle/drupalelementstyleediting.js
@@ -0,0 +1,300 @@
+/* eslint-disable import/no-extraneous-dependencies */
+/* cspell:words drupalelementstylecommand */
+import { Plugin, icons } from 'ckeditor5/src/core';
+import { first } from 'ckeditor5/src/utils';
+import DrupalElementStyleCommand from './drupalelementstylecommand';
+
+/**
+ * @module drupalMedia/drupalelementstyle/drupalelementstyleediting
+ */
+
+/**
+ * Gets style definition by name.
+ *
+ * @param {string} name
+ *   The name of the style definition.
+ * @param styles
+ *   The styles to search from.
+ * @return {Drupal.CKEditor5~DrupalElementStyle}
+ */
+function getStyleDefinitionByName(name, styles) {
+  // eslint-disable-next-line no-restricted-syntax
+  for (const style of styles) {
+    if (style.name === name) {
+      return style;
+    }
+  }
+}
+
+/**
+ * Returns a model-to-view converted for Drupal Element styles.
+ *
+ * This model to view converter supports downcasting model to either a CSS class
+ * or attribute.
+ *
+ * Note that only one style can be applied to a single model element.
+ */
+function modelToViewStyleAttribute(styles) {
+  return (evt, data, conversionApi) => {
+    if (!conversionApi.consumable.consume(data.item, evt.name)) {
+      return;
+    }
+
+    // Check if there is a style associated with given value.
+    const newStyle = getStyleDefinitionByName(data.attributeNewValue, styles);
+    const oldStyle = getStyleDefinitionByName(data.attributeOldValue, styles);
+
+    const viewElement = conversionApi.mapper.toViewElement(data.item);
+    const viewWriter = conversionApi.writer;
+
+    if (oldStyle) {
+      if (oldStyle.attributeName === 'class') {
+        viewWriter.removeClass(oldStyle.attributeValue, viewElement);
+      } else {
+        viewWriter.removeAttribute(oldStyle.attributeName, viewElement);
+      }
+    }
+
+    if (newStyle) {
+      if (newStyle.attributeName === 'class') {
+        viewWriter.addClass(newStyle.attributeValue, viewElement);
+      } else {
+        viewWriter.setAttribute(
+          newStyle.attributeName,
+          newStyle.attributeValue,
+          viewElement,
+        );
+      }
+    }
+  };
+}
+
+/**
+ * Returns a view-to-model converter for Drupal Element styles.
+ *
+ * This view to model converted supports styles that are configured to use
+ * either CSS class or an attribute.
+ *
+ * Note that only one style can be applied to each model element.
+ */
+function viewToModelStyleAttribute(styles) {
+  // Convert only non–default styles.
+  const nonDefaultStyles = styles.filter((style) => !style.isDefault);
+
+  return (evt, data, conversionApi) => {
+    if (!data.modelRange) {
+      return;
+    }
+
+    const viewElement = data.viewItem;
+    const modelElement = first(data.modelRange.getItems());
+
+    // Run this converter only if a model element has been found from the model.
+    if (!modelElement) {
+      return;
+    }
+
+    // Stop conversion early if the drupalElementStyle attribute isn't allowed
+    // for the element.
+    if (
+      !conversionApi.schema.checkAttribute(modelElement, 'drupalElementStyle')
+    ) {
+      return;
+    }
+
+    // Convert styles with CSS classes one by one.
+    // eslint-disable-next-line no-restricted-syntax
+    for (const style of nonDefaultStyles) {
+      // Try to consume class corresponding with the style.
+      if (style.attributeName === 'class') {
+        if (
+          conversionApi.consumable.consume(viewElement, {
+            classes: style.attributeValue,
+          })
+        ) {
+          // And convert this style to model attribute.
+          conversionApi.writer.setAttribute(
+            'drupalElementStyle',
+            style.name,
+            modelElement,
+          );
+        }
+      } else if (
+        conversionApi.consumable.consume(viewElement, {
+          attributes: [style.attributeName],
+        })
+      ) {
+        // eslint-disable-next-line no-restricted-syntax
+        for (const style of nonDefaultStyles) {
+          if (
+            style.attributeValue ===
+            viewElement.getAttribute(style.attributeName)
+          ) {
+            conversionApi.writer.setAttribute(
+              'drupalElementStyle',
+              style.name,
+              modelElement,
+            );
+          }
+        }
+      }
+    }
+  };
+}
+
+/**
+ * The Drupal Element Style editing plugin.
+ *
+ * Additional Drupal Element styles can be defined with `drupalElementStyles`
+ * configuration key.
+ *
+ * @example
+ *    config:
+ *      drupalElementStyles:
+ *         options:
+ *           - name: 'side'
+ *             icon: 'objectBlockRight'
+ *             title: 'Side image'
+ *             attributeName: 'class'
+ *             attributeValue: 'image-side'
+ *             modelElement: ['drupalMedia']
+ *
+ * @see Drupal.CKEditor5~DrupalElementStyle
+ *
+ * @extends module:core/plugin~Plugin
+ *
+ * @internal
+ */
+export default class DrupalElementStyleEditing extends Plugin {
+  /**
+   * @inheritDoc
+   */
+  init() {
+    const editor = this.editor;
+
+    // Ensure that the drupalElementStyles.options exists always.
+    editor.config.define('drupalElementStyles', { options: [] });
+    const stylesConfig = editor.config.get('drupalElementStyles').options;
+
+    /**
+     * The Drupal Element Styles.
+     *
+     * @typedef {Object} Drupal.CKEditor5~DrupalElementStyle
+     *
+     * @prop {string} name
+     *   The name of the style used for identifying the button.
+     * @prop {string} title
+     *   The title of the style displayed in the UI.
+     * @prop {string} attributeName
+     *   The name of the attribute in view.
+     * @prop {string} attributeValue
+     *   The value of the attribute in view.
+     * @prop {string[]} modelElements
+     *   A list of model elements that the style can be attached to.
+     * @prop {string} [icon]
+     *   An icon for the style button. This needs to either refer to an icon in
+     *   the CKEditor 5 core icons, or this can be the XML content of the icon.
+     *
+     * @type {Drupal.CKEditor5~DrupalElementStyle[]}
+     */
+    this.normalizedStyles = stylesConfig
+      .map((style) => {
+        // Allow defining style icon as a string that is referring to the
+        // CKEditor 5 default icons.
+        if (typeof style.icon === 'string') {
+          if (icons[style.icon]) {
+            style.icon = icons[style.icon];
+          }
+        }
+        return style;
+      })
+      .filter((style) => {
+        if (!style.attributeName || !style.attributeValue) {
+          console.warn(
+            'drupalElementStyles options must include attributeName and attributeValue.',
+          );
+          return false;
+        }
+        if (!style.modelElements || !Array.isArray(style.modelElements)) {
+          console.warn(
+            'drupalElementStyles options must include an array of supported modelElements.',
+          );
+          return false;
+        }
+
+        if (!style.name) {
+          console.warn('drupalElementStyles options must include a name.');
+          return false;
+        }
+
+        return true;
+      });
+
+    this._setupConversion();
+
+    editor.commands.add(
+      'drupalElementStyle',
+      new DrupalElementStyleCommand(editor, this.normalizedStyles),
+    );
+  }
+
+  /**
+   * Sets up conversion for Drupal Element Styles.
+   *
+   * @see modelToViewStyleAttribute()
+   * @see viewToModelStyleAttribute()
+   *
+   * @private
+   */
+  _setupConversion() {
+    const editor = this.editor;
+    const schema = editor.model.schema;
+
+    const modelToViewConverter = modelToViewStyleAttribute(
+      this.normalizedStyles,
+    );
+    const viewToModelConverter = viewToModelStyleAttribute(
+      this.normalizedStyles,
+    );
+
+    editor.editing.downcastDispatcher.on(
+      'attribute:drupalElementStyle',
+      modelToViewConverter,
+    );
+    editor.data.downcastDispatcher.on(
+      'attribute:drupalElementStyle',
+      modelToViewConverter,
+    );
+
+    // Allow drupalElementStyle on all model elements that have associated
+    // styles.
+    const modelElements = [
+      ...new Set(
+        this.normalizedStyles
+          .map((style) => {
+            return style.modelElements;
+          })
+          .flat(),
+      ),
+    ];
+    modelElements.forEach((modelElement) => {
+      schema.extend(modelElement, { allowAttributes: 'drupalElementStyle' });
+    });
+
+    // View to model converter that runs on all elements.
+    editor.data.upcastDispatcher.on(
+      'element',
+      viewToModelConverter,
+      // This needs to be set as low priority to ensure this runs always after
+      // the element has been converted to a model element.
+      { priority: 'low' },
+    );
+  }
+
+  /**
+   * @inheritDoc
+   */
+  static get pluginName() {
+    return 'DrupalElementStyleEditing';
+  }
+}
diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle/drupalelementstyleui.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle/drupalelementstyleui.js
new file mode 100644
index 0000000000000000000000000000000000000000..6fa1fc93a788df39306d487e87729f0944ac0433
--- /dev/null
+++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalelementstyle/drupalelementstyleui.js
@@ -0,0 +1,286 @@
+/* eslint-disable import/no-extraneous-dependencies */
+/* cspell:words drupalelementstyleediting splitbutton imagestyle componentfactory */
+import { Plugin } from 'ckeditor5/src/core';
+import utils from '@ckeditor/ckeditor5-image/src/imagestyle/utils';
+import {
+  addToolbarToDropdown,
+  ButtonView,
+  createDropdown,
+  SplitButtonView,
+} from 'ckeditor5/src/ui';
+import DrupalElementStyleEditing from './drupalelementstyleediting';
+
+import { isObject } from '../utils';
+
+/**
+ * @module drupalMedia/drupalelementstyle/drupalelementstyleui
+ */
+
+/**
+ * Returns the first argument it receives.
+ *
+ * @param {*} value
+ *   Any value to be returned by this function.
+ * @return {*}
+ *   Any value passed as the first argument.
+ */
+const identity = (value) => {
+  return value;
+};
+
+/**
+ * Gets the dropdown title.
+ *
+ * @param {string} dropdownTitle
+ *   The dropdown title.
+ * @param {string} buttonTitle
+ *   The button title.
+ * @return {string}
+ *   The generated dropdown title.
+ */
+const getDropdownButtonTitle = (dropdownTitle, buttonTitle) => {
+  return (dropdownTitle ? `${dropdownTitle}: ` : '') + buttonTitle;
+};
+
+/**
+ * Gets the UI Component name.
+ *
+ * This is used for getting unique component names for registering the UI
+ * components in the component factory.
+ *
+ * @param {string} name
+ *   The name of the component.
+ * @return {string}
+ *   The UI component name.
+ *
+ * @see module:ui/componentfactory~ComponentFactory
+ */
+function getUIComponentName(name) {
+  return `drupalElementStyle:${name}`;
+}
+
+/**
+ * The Drupal Element Style UI plugin.
+ *
+ * @extends module:core/plugin~Plugin
+ *
+ * @internal
+ */
+export default class DrupalElementStyleUi extends Plugin {
+  /**
+   * @inheritDoc
+   */
+  static get requires() {
+    return [DrupalElementStyleEditing];
+  }
+
+  /**
+   * @inheritDoc
+   */
+  init() {
+    const plugins = this.editor.plugins;
+    const toolbarConfig = this.editor.config.get('drupalMedia.toolbar') || [];
+
+    const definedStyles = Object.values(
+      plugins.get('DrupalElementStyleEditing').normalizedStyles,
+    );
+
+    definedStyles.forEach((styleConfig) => {
+      this._createButton(styleConfig);
+    });
+
+    /**
+     * A Drupal Element Style dropdown definition.
+     *
+     * @example
+     *    config:
+     *       drupalMedia:
+     *         toolbar:
+     *           - name: 'drupalMedia:alignment'
+     *             title: 'Custom title for the dropdown'
+     *             items:
+     *               - 'drupalElementStyle:alignLeft'
+     *               - 'drupalElementStyle:alignCenter'
+     *               - 'drupalElementStyle:alignRight'
+     *             defaultItem: 'drupalElementStyle:alignCenter'
+     *
+     * @typedef {Object} Drupal.CKEditor5~drupalElementStyleDropdownDefinition
+     *
+     * @prop {string} name
+     *   The name of the dropdown used for identifying the dropdown.
+     * @prop {string[]} items
+     *   The items displayed in the dropdown. These must be styles defined in
+     *   `drupalElementStyles.options`.
+     * @prop {string} defaultItem
+     *   The default item of the dropdown. This must be a style defined in
+     *   `drupalElementStyles.options`.
+     * @prop {string} [title]
+     *   The title of the dropdown.
+     *
+     * @see module:drupalMedia/drupalelementstyle/drupalelementstyleediting:DrupalElementStyleEditing
+     */
+    const definedDropdowns = toolbarConfig.filter(isObject);
+
+    definedDropdowns.forEach((dropdownConfig) => {
+      this._createDropdown(dropdownConfig, definedStyles);
+    });
+  }
+
+  /**
+   * Creates a dropdown and stores it in the component factory.
+   *
+   * @param {Drupal.CKEditor5~drupalElementStyleDropdownDefinition} dropdownConfig
+   *   The dropdown configuration.
+   * @param {Drupal.CKEditor5~DrupalElementStyle[]} definedStyles
+   *   A list of defined styles.
+   *
+   * @see module:ui/componentfactory~ComponentFactory
+   *
+   * @private
+   */
+  _createDropdown(dropdownConfig, definedStyles) {
+    const factory = this.editor.ui.componentFactory;
+
+    factory.add(dropdownConfig.name, (locale) => {
+      let defaultButton;
+
+      const { defaultItem, items, title } = dropdownConfig;
+      const buttonViews = items
+        .filter((itemName) =>
+          definedStyles.find(
+            ({ name }) => getUIComponentName(name) === itemName,
+          ),
+        )
+        .map((buttonName) => {
+          const button = factory.create(buttonName);
+
+          if (buttonName === defaultItem) {
+            defaultButton = button;
+          }
+
+          return button;
+        });
+
+      if (items.length !== buttonViews.length) {
+        utils.warnInvalidStyle({ dropdown: dropdownConfig });
+      }
+
+      const dropdownView = createDropdown(locale, SplitButtonView);
+      const splitButtonView = dropdownView.buttonView;
+
+      addToolbarToDropdown(dropdownView, buttonViews);
+
+      splitButtonView.set({
+        label: getDropdownButtonTitle(title, defaultButton.label),
+        class: null,
+        tooltip: true,
+      });
+
+      // If style is selected, show the currently selected style as the default
+      // button of the split button.
+      splitButtonView.bind('icon').toMany(buttonViews, 'isOn', (...areOn) => {
+        const index = areOn.findIndex(identity);
+
+        return index < 0 ? defaultButton.icon : buttonViews[index].icon;
+      });
+
+      // If style is selected, use the label of the selected style as the
+      // default label of the split button.
+      splitButtonView.bind('label').toMany(buttonViews, 'isOn', (...areOn) => {
+        const index = areOn.findIndex(identity);
+
+        return getDropdownButtonTitle(
+          title,
+          index < 0 ? defaultButton.label : buttonViews[index].label,
+        );
+      });
+
+      // If one of the style is selected, render the split button as selected.
+      splitButtonView
+        .bind('isOn')
+        .toMany(buttonViews, 'isOn', (...areOn) => areOn.some(identity));
+
+      // If one of the styles is selected, add a CSS class to the split button
+      // which modifies the styles to indicate that the splitbutton default
+      // option is currently selected.
+      splitButtonView
+        .bind('class')
+        .toMany(buttonViews, 'isOn', (...areOn) =>
+          areOn.some(identity) ? 'ck-splitbutton_flatten' : null,
+        );
+
+      splitButtonView.on('execute', () => {
+        if (!buttonViews.some(({ isOn }) => isOn)) {
+          defaultButton.fire('execute');
+        } else {
+          dropdownView.isOpen = !dropdownView.isOpen;
+        }
+      });
+
+      dropdownView
+        .bind('isEnabled')
+        .toMany(buttonViews, 'isEnabled', (...areEnabled) =>
+          areEnabled.some(identity),
+        );
+
+      return dropdownView;
+    });
+  }
+
+  /**
+   * Creates a button and stores it in the editor component factory.
+   *
+   * @param {Drupal.CKEditor5~DrupalElementStyle} buttonConfig
+   *   The button configuration.
+   *
+   * @see module:ui/componentfactory~ComponentFactory
+   *
+   * @private
+   */
+  _createButton(buttonConfig) {
+    const buttonName = buttonConfig.name;
+
+    this.editor.ui.componentFactory.add(
+      getUIComponentName(buttonName),
+      (locale) => {
+        const command = this.editor.commands.get('drupalElementStyle');
+        const view = new ButtonView(locale);
+
+        view.set({
+          label: buttonConfig.title,
+          icon: buttonConfig.icon,
+          tooltip: true,
+          isToggleable: true,
+        });
+
+        view.bind('isEnabled').to(command, 'isEnabled');
+        view.bind('isOn').to(command, 'value', (value) => value === buttonName);
+        view.on('execute', this._executeCommand.bind(this, buttonName));
+
+        return view;
+      },
+    );
+  }
+
+  /**
+   * Executes the Drupal Element Style command.
+   *
+   * @param {string} name
+   *   The name of the style that should be applied.
+   *
+   * @see module:drupalMedia/drupalelementstyle/drupalelementstylecommand~DrupalElementStyleCommand
+   *
+   * @private
+   */
+  _executeCommand(name) {
+    this.editor.execute('drupalElementStyle', { value: name });
+    this.editor.editing.view.focus();
+  }
+
+  /**
+   * @inheritDoc
+   */
+  static get pluginName() {
+    return 'DrupalElementStyleUi';
+  }
+}
diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmedia.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmedia.js
index c295fc15b3e274eabd08cae2be29cb9161085f0d..d5d871792fb33830110d4b2a289a23243f69dc3f 100644
--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmedia.js
+++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmedia.js
@@ -22,4 +22,11 @@ export default class DrupalMedia extends Plugin {
       MediaImageTextAlternative,
     ];
   }
+
+  /**
+   * @inheritdoc
+   */
+  static get pluginName() {
+    return 'DrupalMedia';
+  }
 }
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 8133f4314107b2a4637bcd39226fa1a43bee2045..9efe0d1a0d5e87c882e4dc68073964a977179d57 100644
--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js
+++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js
@@ -1,11 +1,15 @@
 /* eslint-disable import/no-extraneous-dependencies */
-/* cspell:words insertdrupalmedia */
+/* cspell:words insertdrupalmedia drupalmediaediting */
 
 import { Plugin } from 'ckeditor5/src/core';
 import { toWidget, Widget } from 'ckeditor5/src/widget';
 
 import InsertDrupalMediaCommand from './insertdrupalmedia';
 
+/**
+ * @module drupalMedia/drupalmediaediting
+ */
+
 /**
  * @internal
  */
@@ -17,7 +21,6 @@ export default class DrupalMediaEditing extends Plugin {
   init() {
     this.attrs = {
       drupalMediaAlt: 'alt',
-      drupalMediaAlign: 'data-align',
       drupalMediaCaption: 'data-caption',
       drupalMediaEntityType: 'data-entity-type',
       drupalMediaEntityUuid: 'data-entity-uuid',
@@ -124,6 +127,47 @@ export default class DrupalMediaEditing extends Plugin {
       },
     });
 
+    conversion.for('editingDowncast').add((dispatcher) => {
+      dispatcher.on(
+        'attribute:drupalElementStyle:drupalMedia',
+        (evt, data, conversionApi) => {
+          const alignMapping = {
+            alignLeft: 'drupal-media-style-align-left',
+            alignRight: 'drupal-media-style-align-right',
+            alignCenter: 'drupal-media-style-align-center',
+          };
+          const viewElement = conversionApi.mapper.toViewElement(data.item);
+          const viewWriter = conversionApi.writer;
+
+          // If the prior value is alignment related, it should be removed
+          // whether or not the module property is consumed.
+          if (alignMapping[data.attributeOldValue]) {
+            viewWriter.removeClass(
+              alignMapping[data.attributeOldValue],
+              viewElement,
+            );
+          }
+
+          // If the new value is not alignment related, do not proceed.
+          if (!alignMapping[data.attributeNewValue]) {
+            return;
+          }
+
+          // The model property is already consumed, do not proceed.
+          if (!conversionApi.consumable.consume(data.item, evt.name)) {
+            return;
+          }
+
+          // Add the alignment class in the view that corresponds to the value
+          // of the model's drupalElementStyle property.
+          viewWriter.addClass(
+            alignMapping[data.attributeNewValue],
+            viewElement,
+          );
+        },
+      );
+    });
+
     // Set attributeToAttribute conversion for all supported attributes.
     Object.keys(this.attrs).forEach((modelKey) => {
       conversion.attributeToAttribute({
diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediatoolbar.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediatoolbar.js
index 4da4b6ecae5881bba385f9e4584e8602dddf32ee..3a7ecb461c18bad36bcd8076110c595cbce20ea0 100644
--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediatoolbar.js
+++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediatoolbar.js
@@ -1,8 +1,29 @@
 /* eslint-disable import/no-extraneous-dependencies */
+/* cspell:words drupalmediatoolbar */
 import { Plugin } from 'ckeditor5/src/core';
 import { WidgetToolbarRepository } from 'ckeditor5/src/widget';
 
-import { getSelectedDrupalMediaWidget } from './utils';
+import { getSelectedDrupalMediaWidget, isObject } from './utils';
+
+/**
+ * @module drupalMedia/drupalmediatoolbar
+ */
+
+/**
+ * Convert dropdown definitions to keys registered in the ComponentFactory.
+ *
+ * The registration process should be handled by the plugin which handles the UI
+ * of a particular feature.
+ *
+ * @param {Array.<string|Object>} config
+ *   The drupalMedia.toolbar configuration.
+ *
+ * @return {string[]}
+ *   A normalized toolbar item list.
+ */
+function normalizeDeclarativeConfig(config) {
+  return config.map((item) => (isObject(item) ? item.name : item));
+}
 
 /**
  * @internal
@@ -17,12 +38,14 @@ export default class DrupalMediaToolbar extends Plugin {
   }
 
   afterInit() {
-    const editor = this.editor;
+    const { editor } = this;
     const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository);
 
     widgetToolbarRepository.register('drupalMedia', {
       ariaLabel: Drupal.t('Drupal Media toolbar'),
-      items: editor.config.get('drupalMedia.toolbar') || [],
+      items:
+        normalizeDeclarativeConfig(editor.config.get('drupalMedia.toolbar')) ||
+        [],
       // Get the selected image or an image containing the figcaption with the selection inside.
       getRelatedElement: (selection) => getSelectedDrupalMediaWidget(selection),
     });
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 dfdc8cc22dd2eec18335da0d8d0e0e6d07c14633..25fb43d0cfce29951c11eb5fafb28208681e7ce7 100644
--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/index.js
+++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/index.js
@@ -6,6 +6,9 @@ import DrupalMedia from './drupalmedia';
 // cspell:ignore drupallinkmedia
 import DrupalLinkMedia from './drupallinkmedia/drupallinkmedia';
 
+// cspell:ignore drupalelementstyle
+import DrupalElementStyle from './drupalelementstyle';
+
 // cspell:ignore mediaimagetextalternative
 import MediaImageTextAlternative from './mediaimagetextalternative';
 import MediaImageTextAlternativeEditing from './mediaimagetextalternative/mediaimagetextalternativeediting';
@@ -20,4 +23,5 @@ export default {
   MediaImageTextAlternativeEditing,
   MediaImageTextAlternativeUi,
   DrupalLinkMedia,
+  DrupalElementStyle,
 };
diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/insertdrupalmedia.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/insertdrupalmedia.js
index 205d42caa4c3cbe168ce5a5e86acee0f0f3f8d88..0f985e3b86dba79823938dfe0d8222858ffa27f7 100644
--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/insertdrupalmedia.js
+++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/insertdrupalmedia.js
@@ -55,6 +55,25 @@ export default class InsertDrupalMediaCommand extends Command {
       {},
     );
 
+    // Check if there's Drupal Element Style matching the default attributes on
+    // the media.
+    // @see module:drupalMedia/drupalelementstyle/drupalelementstyleediting~DrupalElementStyleEditing
+    if (this.editor.plugins.has('DrupalElementStyleEditing')) {
+      const elementStyleEditing = this.editor.plugins.get(
+        'DrupalElementStyleEditing',
+      );
+      // eslint-disable-next-line no-restricted-syntax
+      for (const style of elementStyleEditing.normalizedStyles) {
+        if (
+          attributes[style.attributeName] &&
+          style.attributeValue === attributes[style.attributeName]
+        ) {
+          modelAttributes.drupalElementStyle = style.name;
+          break;
+        }
+      }
+    }
+
     this.editor.model.change((writer) => {
       this.editor.model.insertContent(
         createDrupalMedia(writer, modelAttributes),
diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/utils.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/utils.js
index 3289b8931536a942f02d9978fee12a282f151282..5041e3880819c55f0b87f79d0fd41d2ed0629bae 100644
--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/utils.js
+++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/utils.js
@@ -49,3 +49,19 @@ export function getSelectedDrupalMediaWidget(selection) {
 
   return null;
 }
+
+/**
+ * Checks if value is a JavaScript object.
+ *
+ * This will return true for any type of JavaScript object. (e.g. arrays,
+ * functions, objects, regexes, new Number(0), and new String(''))
+ *
+ * @param value
+ *   Value to check.
+ * @return {boolean}
+ *   True if value is an object, else false.
+ */
+export function isObject(value) {
+  const type = typeof value;
+  return value != null && (type === 'object' || type === 'function');
+}
diff --git a/core/modules/ckeditor5/tests/modules/ckeditor5_test/ckeditor5_test.ckeditor5.yml b/core/modules/ckeditor5/tests/modules/ckeditor5_test/ckeditor5_test.ckeditor5.yml
index 52217fc674478462e48e61ad75e52f9254f0913d..413dc0630b15baa40d97b00795fe3edd78689bd5 100644
--- a/core/modules/ckeditor5/tests/modules/ckeditor5_test/ckeditor5_test.ckeditor5.yml
+++ b/core/modules/ckeditor5/tests/modules/ckeditor5_test/ckeditor5_test.ckeditor5.yml
@@ -2,6 +2,18 @@
 ckeditor5_test_layercake:
   ckeditor5:
     plugins: []
+    config:
+      drupalElementStyles:
+        options:
+          - name: 'layerCakeSide'
+            title: 'Media aligned to side'
+            icon: '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path opacity=".5" d="M2 3h16v1.5H2zm0 12h16v1.5H2z"/><path d="M18.003 7v5.5a1 1 0 0 1-1 1H8.996a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h8.007a1 1 0 0 1 1 1zm-1.506.5H9.5V12h6.997V7.5z"/></svg>'
+            attributeName: 'class'
+            attributeValue: 'layercake-side'
+            modelElements: ['drupalMedia']
+      drupalMediaStyles:
+        toolbar:
+          - drupalElementStyle:layerCakeSide
   drupal:
     label: TEST — Layercake
     library: ckeditor5_test/layercake
@@ -14,3 +26,4 @@ ckeditor5_test_layercake:
       - <h1 class>
       - <div class>
       - <section class>
+      - <drupal-media class="layercake-side">
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php
index a0b8de84c8cecb3611ffb8bde42a32320cae7c50..80b0636fcfcd17475ee0d265db746ac864adf8ec 100644
--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php
@@ -34,13 +34,6 @@ class CKEditor5AllowedTagsTest extends CKEditor5TestBase {
    */
   protected $allowedElements = '<br> <p> <h2> <h3> <h4> <h5> <h6> <strong> <em>';
 
-  /**
-   * The element that must be allowed when media embed is enabled.
-   *
-   * @var string
-   */
-  protected $mediaElement = '<drupal-media data-entity-type data-entity-uuid alt>';
-
   /**
    * The default allowed elements when updating a non-CKEditor 5 editor.
    *
@@ -379,7 +372,7 @@ public function testMediaElementAllowedTags() {
     $assert_session->assertWaitOnAjaxRequest();
     $assert_session->responseContains('Media types selectable in the Media Library');
 
-    $allowed_with_media = $this->allowedElements . ' ' . $this->mediaElement;
+    $allowed_with_media = $this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt>';
     $assert_session->responseContains('Media types selectable in the Media Library');
     $this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $allowed_with_media);
     $this->saveNewTextFormat($page, $assert_session);
@@ -395,6 +388,12 @@ public function testMediaElementAllowedTags() {
     // field.
     $this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $allowed_with_media);
 
+    // Ensure that data-align attribute is added to <drupal-media> when
+    // filter_align is enabled.
+    $page->checkField('filters[filter_align][status]');
+    $assert_session->assertWaitOnAjaxRequest();
+    $this->assertEquals($this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt data-align>', $allowed_html_field->getValue());
+
     // Disable media embed.
     $this->assertTrue($page->hasCheckedField('filters[media_embed][status]'));
     $page->uncheckField('filters[media_embed][status]');
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLibraryTest.php
index 70b97129f436e6998e01620741c59105b9d39026..d1c0c71e4b11cb99e0a5482e56b1fced99f53455 100644
--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLibraryTest.php
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLibraryTest.php
@@ -3,7 +3,6 @@
 namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
 
 use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
-use Drupal\Component\Utility\Html;
 use Drupal\editor\Entity\Editor;
 use Drupal\file\Entity\File;
 use Drupal\filter\Entity\FilterFormat;
@@ -147,6 +146,7 @@ function (ConstraintViolation $v) {
    * Tests using drupalMedia button to embed media into CKEditor 5.
    */
   public function testButton() {
+    $media_preview_selector = '.ck-content .ck-widget.drupal-media .media';
     $this->drupalGet('/node/add/blog');
     $this->waitForEditor();
     $this->pressEditorButton('Insert Drupal Media');
@@ -168,28 +168,44 @@ public function testButton() {
     $assert_session->elementExists('css', '.js-media-library-item')->click();
     $assert_session->pageTextContains('1 of 1 item selected');
     $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
-    $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-content .ck-widget.drupal-media .media', 1000));
-    $this->pressEditorButton('Source');
-    $value = $assert_session->elementExists('css', '.ck-source-editing-area textarea')->getValue();
-    $dom = Html::load($value);
-    $xpath = new \DOMXPath($dom);
+    $this->assertNotEmpty($assert_session->waitForElementVisible('css', $media_preview_selector, 1000));
+    $xpath = new \DOMXPath($this->getEditorDataAsDom());
     $drupal_media = $xpath->query('//drupal-media')[0];
     $expected_attributes = [
       'data-entity-type' => 'media',
       'data-entity-uuid' => $this->media->uuid(),
-      'data-align' => 'center',
     ];
     foreach ($expected_attributes as $name => $expected) {
       $this->assertSame($expected, $drupal_media->getAttribute($name));
     }
-    $this->pressEditorButton('Source');
-    $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-content .ck-widget.drupal-media .media', 1000));
     $this->assertEditorButtonEnabled('Undo');
     $this->pressEditorButton('Undo');
-    $this->assertEmpty($assert_session->waitForElementVisible('css', '.ck-content .ck-widget.drupal-media .media', 1000));
+    $this->assertEmpty($assert_session->waitForElementVisible('css', $media_preview_selector, 1000));
     $this->assertEditorButtonDisabled('Undo');
     $this->pressEditorButton('Redo');
     $this->assertEditorButtonEnabled('Undo');
+
+    // Ensure that data-align attribute is set by default when media is inserted
+    // while filter_align is enabled.
+    FilterFormat::load('test_format')
+      ->setFilterConfig('filter_align', ['status' => TRUE])
+      ->save();
+    $this->drupalGet('/node/add/blog');
+    $this->waitForEditor();
+    $this->pressEditorButton('Insert Drupal Media');
+    $assert_session->waitForElement('css', '.js-media-library-item')->click();
+    $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
+    $this->assertNotEmpty($assert_session->waitForElementVisible('css', $media_preview_selector, 1000));
+    $xpath = new \DOMXPath($this->getEditorDataAsDom());
+    $drupal_media = $xpath->query('//drupal-media')[0];
+    $expected_attributes = [
+      'data-entity-type' => 'media',
+      'data-entity-uuid' => $this->media->uuid(),
+      'data-align' => 'center',
+    ];
+    foreach ($expected_attributes as $name => $expected) {
+      $this->assertSame($expected, $drupal_media->getAttribute($name));
+    }
   }
 
   /**
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php
index 8fb14f18d9da4e5533e9da4b297aacdcc11b6c95..8a2835d0dbd8d5f0ea89a5dcdf63a280ea3304b5 100644
--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php
@@ -13,6 +13,8 @@
 use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
 use Symfony\Component\Validator\ConstraintViolation;
 
+// cspell:ignore layercake
+
 /**
  * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media
  * @group ckeditor5
@@ -54,6 +56,8 @@ class MediaTest extends WebDriverTestBase {
     'node',
     'text',
     'media_test_embed',
+    'media_library',
+    'ckeditor5_test',
   ];
 
   /**
@@ -74,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 alt>',
+            'allowed_html' => '<p> <br> <a href> <drupal-media data-entity-type data-entity-uuid data-align alt>',
           ],
         ],
         'filter_align' => ['status' => TRUE],
@@ -641,12 +645,110 @@ public function previewAccessProvider() {
    * Tests alignment integration.
    *
    * Tests that alignment is reflected onto the CKEditor Widget wrapper, that
-   * the EditorMediaDialog allows altering the alignment and that the changes
+   * the media style toolbar allows altering the alignment and that the changes
    * are reflected on the widget and downcast drupal-media tag.
    */
   public function testAlignment() {
-    // @todo Port in https://www.drupal.org/project/ckeditor5/issues/3246385
-    $this->markTestSkipped('Blocked on https://www.drupal.org/project/ckeditor5/issues/3246385.');
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+    $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'));
+    // Edit the source of the image through the UI.
+    $page->pressButton('Source');
+
+    $editor_dom = $this->getEditorDataAsDom();
+    $drupal_media_element = $editor_dom->getElementsByTagName('drupal-media')
+      ->item(0);
+    $drupal_media_element->setAttribute('data-align', 'center');
+    $textarea = $page->find('css', '.ck-source-editing-area > textarea');
+    // Set the value of the source code to the updated HTML that has the
+    // `data-align` attribute.
+    $textarea->setValue($editor_dom->C14N());
+    $page->pressButton('Source');
+
+    // Assert the alignment class exists after editing downcast.
+    $assert_session->elementExists('css', '.ck-widget.drupal-media.drupal-media-style-align-center');
+    $page->pressButton('Save');
+    // 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');
+    // Go back to the editor to check that the alignment class still exists.
+    $edit_url = $this->getSession()->getCurrentURL() . '/edit';
+    $this->drupalGet($edit_url);
+    $this->waitForEditor();
+    $assert_session->elementExists('css', '.ck-widget.drupal-media.drupal-media-style-align-center');
+  }
+
+  /**
+   * Tests Drupal Media Style with a CSS class.
+   */
+  public function testDrupalMediaStyleWithClass() {
+    $editor = Editor::load('test_format');
+    $editor->setSettings([
+      'toolbar' => [
+        'items' => [
+          'sourceEditing',
+          'simpleBox',
+        ],
+      ],
+      'plugins' => [
+        'ckeditor5_sourceEditing' => [
+          'allowed_tags' => [],
+        ],
+      ],
+    ]);
+    $filter_format = $editor->getFilterFormat();
+    $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">',
+      ],
+    ]);
+    $filter_format->save();
+    $editor->save();
+
+    $this->assertSame([], array_map(
+      function (ConstraintViolation $v) {
+        return (string) $v->getMessage();
+      },
+      iterator_to_array(CKEditor5::validatePair(
+        Editor::load('test_format'),
+        FilterFormat::load('test_format')
+      ))
+    ));
+
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+    $this->drupalGet($this->host->toUrl('edit-form'));
+    $this->waitForEditor();
+
+    $page->pressButton('Source');
+    $editor_dom = $this->getEditorDataAsDom();
+    $drupal_media_element = $editor_dom->getElementsByTagName('drupal-media')->item(0);
+
+    // Add `layercake-side` class which is used in `ckeditor5_test_layercake`,
+    // as well as an arbitrary class to compare behavior between these.
+    $drupal_media_element->setAttribute('class', 'layercake-side arbitrary-class');
+    $textarea = $page->find('css', '.ck-source-editing-area > textarea');
+    $textarea->setValue($editor_dom->C14N());
+    $page->pressButton('Source');
+
+    // Ensure that the `layercake-side` class is retained.
+    $this->assertNotEmpty($assert_session->waitForElement('css', '.ck-widget.drupal-media.layercake-side'));
+
+    // Ensure that the `arbitrary-class` class is removed.
+    $assert_session->elementNotExists('css', '.ck-widget.drupal-media.arbitrary-class');
+    $page->pressButton('Save');
+
+    // Check that the 'content has been updated' message status appears to confirm we left the editor.
+    $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');
   }
 
   /**
diff --git a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
index f50aa30bad1d04407159e5ce93d25865d1d0b30e..d638a4c3acb72739ef4387867df0b40d50d3d212 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
@@ -937,11 +937,13 @@ public function testEnabledPlugins() {
     $expected_plugins = array_merge($default_plugins, [
       'ckeditor5_test_layercake',
       'media_media',
+      'media_mediaAlign',
     ]);
     sort($expected_plugins);
     $this->assertSame($expected_plugins, $plugin_ids);
     $expected_libraries = array_merge($default_libraries, [
       'ckeditor5/drupal.ckeditor5.media',
+      'ckeditor5/drupal.ckeditor5.mediaAlign',
       'ckeditor5_test/layercake',
     ]);
     sort($expected_libraries);
@@ -970,6 +972,7 @@ public function testEnabledPlugins() {
     $this->assertSame(array_values($expected_plugins), $plugin_ids);
     $expected_libraries = array_merge($default_libraries, [
       'ckeditor5/drupal.ckeditor5.media',
+      'ckeditor5/drupal.ckeditor5.mediaAlign',
       'ckeditor5_test/layercake',
       'core/ckeditor5.table',
     ]);
diff --git a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
index e79413205d6d158753fc05841d2b0119290d07ef..abe7693f9801402aae278012176174b73355bbff 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
@@ -556,7 +556,7 @@ public function provider() {
           'ckeditor5_sourceEditing' => [
             'allowed_tags' => array_merge(
               $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
-              ['<drupal-media data-align data-caption>'],
+              ['<drupal-media data-caption>'],
             ),
           ],
         ] + $basic_html_test_case['expected_ckeditor5_settings']['plugins'],
@@ -564,7 +564,7 @@ public function provider() {
       '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>: &lt;a hreflang&gt; &lt;blockquote cite&gt; &lt;ul type&gt; &lt;ol start type&gt; &lt;h2 id&gt; &lt;h3 id&gt; &lt;h4 id&gt; &lt;h5 id&gt; &lt;h6 id&gt; &lt;drupal-media data-align data-caption&gt;.",
+        "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>: &lt;a hreflang&gt; &lt;blockquote cite&gt; &lt;ul type&gt; &lt;ol start type&gt; &lt;h2 id&gt; &lt;h3 id&gt; &lt;h4 id&gt; &lt;h5 id&gt; &lt;h6 id&gt; &lt;drupal-media data-caption&gt;.",
       ]),
     ];