diff --git a/patches/d10-4.json b/patches/d10-4.json index a6c5c61f26e0623e4851e878082735a6f69807f1..5f275609f8faaed4b8970d924946399c0f75675a 100644 --- a/patches/d10-4.json +++ b/patches/d10-4.json @@ -53,7 +53,7 @@ "#3195171 Fix view header group rendering": "https://www.drupal.org/files/issues/2022-01-05/3195171-7-fix-view-header-group-rendering.patch", "#3180227 Missing options for entity refernces in views": "https://www.drupal.org/files/issues/2020-11-02/3180227-2.patch", "#local Json API and entity reference warnings": "https://gitlab.lakedrops.com/composer/plugin/drupal-environment/-/raw/main/patches/jsonapi-entityref-warning-10.3.patch", - "#local Issue 3168966 Insert Media inline in CKEditor widget": "https://gitlab.lakedrops.com/composer/plugin/drupal-environment/-/raw/main/patches/d10/5758-2.diff", + "#local Issue 3168966 Insert Media inline in CKEditor widget": "https://gitlab.lakedrops.com/composer/plugin/drupal-environment/-/raw/main/patches/d10/5758-1.diff", "#3228298 Empty path alias exception": "https://gitlab.lakedrops.com/composer/plugin/drupal-environment/-/raw/main/patches/d10/3228298.diff?v=2", "#3227816 Hide password reset link": "https://gitlab.lakedrops.com/composer/plugin/drupal-environment/-/raw/main/patches/d10/3227816.diff", "#3249628 Comments and paragraphs": "https://www.drupal.org/files/issues/2022-12-11/1415-11.diff", diff --git a/patches/d10/5758-1.diff b/patches/d10/5758-1.diff new file mode 100644 index 0000000000000000000000000000000000000000..b44a29ebdb8a6ad060f0992f47d8c8d6e7048628 --- /dev/null +++ b/patches/d10/5758-1.diff @@ -0,0 +1,1790 @@ +diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml +index 2a2c3fd165877ef2d3790dfd63779104c425d13e..3c2f61d05bd7575d4e8b68c6c5e01c1eb37ca82c 100644 +--- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml ++++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml +@@ -742,6 +742,9 @@ media_media: + - <drupal-media> + - <drupal-media data-entity-type data-entity-uuid alt> + - <drupal-media data-view-mode> ++ - <drupal-media-inline> ++ - <drupal-media-inline data-entity-type data-entity-uuid alt> ++ - <drupal-media-inline data-view-mode> + conditions: + filter: media_embed + +@@ -756,6 +759,7 @@ ckeditor5_drupalMediaCaption: + label: Media caption + elements: + - <drupal-media data-caption> ++ - <drupal-media-inline data-caption> + conditions: + filter: filter_caption + plugins: +@@ -805,6 +809,7 @@ media_mediaAlign: + library: ckeditor5/internal.drupal.ckeditor5.mediaAlign + elements: + - <drupal-media data-align> ++ - <drupal-media-inline data-align> + conditions: + filter: filter_align + plugins: [media_media] +diff --git a/core/modules/ckeditor5/ckeditor5.module b/core/modules/ckeditor5/ckeditor5.module +index 561795fa958175d6f562f814840ab6e3de005d79..ce745000affb58f286aae24fb9162860fe26e323 100644 +--- a/core/modules/ckeditor5/ckeditor5.module ++++ b/core/modules/ckeditor5/ckeditor5.module +@@ -91,9 +91,37 @@ function ckeditor5_theme() { + 'ckeditor5_settings_toolbar' => [ + 'render element' => 'form', + ], ++ 'field__ckeditor_inline' => [ ++ 'base hook' => 'field', ++ ], ++ 'media__ckeditor_inline_embed' => [ ++ 'base hook' => 'media', ++ ], + ]; + } + ++/** ++ * Implements hook_theme_suggestions_HOOK_alter(). ++ */ ++function ckeditor5_theme_suggestions_media_alter(array &$suggestions, array $variables) { ++ if ($variables['elements']['#view_mode'] == 'ckeditor_inline') { ++ $suggestions[] = 'media__ckeditor_inline_embed'; ++ } ++} ++ ++/** ++ * Implements hook_preprocess_HOOK(). ++ */ ++function ckeditor5_preprocess_media(&$variables) { ++ if ($variables['view_mode'] == 'ckeditor_inline') { ++ foreach ($variables['content'] as &$element) { ++ if (!empty($element['#theme']) && $element['#theme'] == 'field') { ++ $element['#theme'] = 'field__ckeditor_inline'; ++ } ++ } ++ } ++} ++ + /** + * Implements hook_module_implements_alter(). + */ +diff --git a/core/modules/ckeditor5/ckeditor5.post_update.php b/core/modules/ckeditor5/ckeditor5.post_update.php +index f9b4e846c16d064d8ba9b36b64e0322ade4a779e..e2e925e63eaecfd9cf9c65b90ed21db3ef02ada3 100644 +--- a/core/modules/ckeditor5/ckeditor5.post_update.php ++++ b/core/modules/ckeditor5/ckeditor5.post_update.php +@@ -5,6 +5,8 @@ + * Post update functions for CKEditor 5. + */ + ++use Drupal\Core\Config\FileStorage; ++ + // cspell:ignore multiblock + + /** +@@ -20,3 +22,39 @@ function ckeditor5_removed_post_updates() { + 'ckeditor5_post_update_list_start_reversed' => '11.0.0', + ]; + } ++ ++/** ++ * Creates the ckeditor_inline view mode for media entities. ++ */ ++function ckeditor5_post_update_create_ckeditor_inline_view_mode() { ++ $config_path = \Drupal::service('extension.list.module')->getPath('ckeditor5') . '/config/optional'; ++ $source = new FileStorage($config_path); ++ $entity_type_manager = \Drupal::entityTypeManager(); ++ $module_handler = \Drupal::moduleHandler(); ++ /** @var \Drupal\Core\Config\StorageInterface $config_storage */ ++ $config_storage = \Drupal::service('config.storage'); ++ ++ if ($module_handler->moduleExists('media') && !$config_storage->exists('core.entity_view_mode.media.ckeditor_inline')) { ++ $entity_type_manager->getStorage('entity_view_mode') ++ ->create($source->read('core.entity_view_mode.media.ckeditor_inline')) ++ ->save(); ++ } ++} ++ ++/** ++ * Amends the filter format settings to allow the drupal-media-inline tag. ++ */ ++function ckeditor5_post_update_allow_inline_media_element() { ++ $formats = filter_formats(); ++ foreach ($formats as $format_id => $format) { ++ $config = \Drupal::configFactory()->getEditable('filter.format.' . $format_id); ++ $filters = $config->get('filters'); ++ if (\array_key_exists('media_embed', $filters) ++ && \array_key_exists('filter_html', $filters) ++ && !\str_contains($filters['filter_html']['settings']['allowed_html'], 'drupal-media-inline')) { ++ $filters['filter_html']['settings']['allowed_html'] .= ' <drupal-media-inline data-entity-type data-entity-uuid alt data-view-mode data-caption data-align>'; ++ $config->set('filters', $filters); ++ $config->save(); ++ } ++ } ++} +diff --git a/core/modules/ckeditor5/config/optional/core.entity_view_mode.media.ckeditor_inline.yml b/core/modules/ckeditor5/config/optional/core.entity_view_mode.media.ckeditor_inline.yml +new file mode 100644 +index 0000000000000000000000000000000000000000..e659b44337d5e3f03c4720bbe63f7446d6536bbf +--- /dev/null ++++ b/core/modules/ckeditor5/config/optional/core.entity_view_mode.media.ckeditor_inline.yml +@@ -0,0 +1,10 @@ ++langcode: en ++status: true ++dependencies: ++ module: ++ - media ++id: media.ckeditor_inline ++label: 'Ckeditor Inline' ++description: 'View mode used for media embedded inline' ++targetEntityType: media ++cache: true +diff --git a/core/modules/ckeditor5/css/drupalmedia.css b/core/modules/ckeditor5/css/drupalmedia.css +index e69e9c7ed6fa6f183ae267317599e687e7c8917a..7fd2b0b188fca6a36f67cf9c96ae0c1a1ac50123 100644 +--- a/core/modules/ckeditor5/css/drupalmedia.css ++++ b/core/modules/ckeditor5/css/drupalmedia.css +@@ -13,6 +13,12 @@ + min-width: 50px; + } + ++.ck .drupal-media.drupal-media-inline { ++ display: inline-block; ++ clear: initial; ++ margin: 0; ++} ++ + .ck .drupal-media [data-drupal-media-preview] { + pointer-events: none; + } +diff --git a/core/modules/ckeditor5/js/build/drupalMedia.js b/core/modules/ckeditor5/js/build/drupalMedia.js +index 766eb373081549d9e4aad090332877178bbd30a8..bd81d5269440adfea48f5a404b138267d43d670d 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())}(globalThis,(()=>(()=>{var e={"ckeditor5/src/core.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/engine.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/engine.js")},"ckeditor5/src/ui.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/ui.js")},"ckeditor5/src/utils.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"ckeditor5/src/widget.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/widget.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function i(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=(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=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var n={};return(()=>{"use strict";i.d(n,{default:()=>ne});var e=i("ckeditor5/src/core.js"),t=i("ckeditor5/src/widget.js");function a(e){return!!e&&e.is("element","drupalMedia")}function r(e){return(0,t.isWidget)(e)&&!!e.getCustomProperty("drupalMedia")}function o(e){const t=e.getSelectedElement();return a(t)?t:e.getFirstPosition().findAncestor("drupalMedia")}function s(e){const t=e.getSelectedElement();if(t&&r(t))return t;if(null===e.getFirstPosition())return null;let i=e.getFirstPosition().parent;for(;i;){if(i.is("element")&&r(i))return i;i=i.parent}return null}function l(e){const t=typeof e;return null!=e&&("object"===t||"function"===t)}function d(e){for(const t of e){if(t.hasAttribute("data-drupal-media-preview"))return t;if(t.childCount){const e=d(t.getChildren());if(e)return e}}return null}function u(e){return`drupalElementStyle${e[0].toUpperCase()+e.substring(1)}`}class c 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"),{normalizedStyles:i}=t;for(const a of Object.keys(i))for(const i of t.normalizedStyles[a])if(e[i.attributeName]&&i.attributeValue===e[i.attributeName]){const e=u(a);n[e]=i.name}}this.editor.model.change((e=>{this.editor.model.insertObject(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}}const m="METADATA_ERROR";class p extends e.Plugin{static get requires(){return[t.Widget]}constructor(e){super(e),this.attrs={drupalMediaAlt:"alt",drupalMediaEntityType:"data-entity-type",drupalMediaEntityUuid:"data-entity-uuid"},this.converterAttributes=["drupalMediaEntityUuid","drupalElementStyleViewMode","drupalMediaEntityType","drupalMediaAlt"]}init(){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. Save your work and reload this page.")}<p>\n `,this._defineSchema(),this._defineConverters(),this._defineListeners(),this.editor.commands.add("insertDrupalMedia",new c(this.editor))}upcastDrupalMediaIsImage(e){const{model:t,plugins:i}=this.editor;i.get("DrupalMediaMetadataRepository").getMetadata(e).then((i=>{e&&t.enqueueChange({isUndoable:!1},(t=>{t.setAttribute("drupalMediaIsImage",!!i.imageSourceMetadata,e)}))})).catch((i=>{e&&(console.warn(i.toString()),t.enqueueChange({isUndoable:!1},(t=>{t.setAttribute("drupalMediaIsImage",m,e)})))}))}upcastDrupalMediaType(e){this.editor.plugins.get("DrupalMediaMetadataRepository").getMetadata(e).then((t=>{e&&this.editor.model.enqueueChange({isUndoable:!1},(i=>{i.setAttribute("drupalMediaType",t.type,e)}))})).catch((t=>{e&&(console.warn(t.toString()),this.editor.model.enqueueChange({isUndoable:!1},(t=>{t.setAttribute("drupalMediaType",m,e)})))}))}async _fetchPreview(e){const t={text:this._renderElement(e),uuid:e.getAttribute("drupalMediaEntityUuid")},i=await fetch(`${this.previewUrl}?${new URLSearchParams(t)}`,{headers:{"X-Drupal-MediaPreview-CSRF-Token":this.editor.config.get("drupalMedia").previewCsrfToken}});if(i.ok){return{label:i.headers.get("drupal-media-label"),preview:await i.text()}}return{label:this.labelError,preview:this.themeError}}_defineSchema(){this.editor.model.schema.register("drupalMedia",{inheritAllFrom:"$blockObject",allowAttributes:Object.keys(this.attrs)}),this.editor.editing.view.domConverter.blockElements.push("drupal-media")}_defineConverters(){const e=this.editor.conversion,i=this.editor.plugins.get("DrupalMediaMetadataRepository");e.for("upcast").elementToElement({view:{name:"drupal-media"},model:"drupalMedia"}).add((e=>{e.on("element:drupal-media",((e,t)=>{const[n]=t.modelRange.getItems();i.getMetadata(n).then((e=>{n&&(this.upcastDrupalMediaIsImage(n),this.editor.model.enqueueChange({isUndoable:!1},(t=>{t.setAttribute("drupalMediaType",e.type,n)})))})).catch((e=>{console.warn(e.toString())}))}),{priority:"lowest"})})),e.for("dataDowncast").elementToElement({model:"drupalMedia",view:{name:"drupal-media"}}),e.for("editingDowncast").elementToElement({model:"drupalMedia",view:(e,{writer:i})=>{const n=i.createContainerElement("figure",{class:"drupal-media"});if(!this.previewUrl){const e=i.createRawElement("div",{"data-drupal-media-preview":"unavailable"});i.insert(i.createPositionAt(n,0),e)}return i.setCustomProperty("drupalMedia",!0,n),(0,t.toWidget)(n,i,{label:Drupal.t("Media widget")})}}).add((e=>{const t=(e,t,i)=>{const n=i.writer,a=t.item,r=i.mapper.toViewElement(t.item);let o=d(r.getChildren());if(o){if("ready"!==o.getAttribute("data-drupal-media-preview"))return;n.setAttribute("data-drupal-media-preview","loading",o)}else o=n.createRawElement("div",{"data-drupal-media-preview":"loading"}),n.insert(n.createPositionAt(r,0),o);this._fetchPreview(a).then((({label:e,preview:t})=>{o&&this.editor.editing.view.change((i=>{const n=i.createRawElement("div",{"data-drupal-media-preview":"ready","aria-label":e},(e=>{e.innerHTML=t}));i.insert(i.createPositionBefore(o),n),i.remove(o)}))}))};return this.converterAttributes.forEach((i=>{e.on(`attribute:${i}:drupalMedia`,t)})),e})),e.for("editingDowncast").add((e=>{e.on("attribute:drupalElementStyleAlign:drupalMedia",((e,t,i)=>{const n={left:"drupal-media-style-align-left",right:"drupal-media-style-align-right",center:"drupal-media-style-align-center"},a=i.mapper.toViewElement(t.item),r=i.writer;n[t.attributeOldValue]&&r.removeClass(n[t.attributeOldValue],a),n[t.attributeNewValue]&&i.consumable.consume(t.item,e.name)&&r.addClass(n[t.attributeNewValue],a)}))})),Object.keys(this.attrs).forEach((t=>{const i={model:{key:t,name:"drupalMedia"},view:{name:"drupal-media",key:this.attrs[t]}};e.for("dataDowncast").attributeToAttribute(i),e.for("upcast").attributeToAttribute(i)}))}_defineListeners(){this.editor.model.on("insertContent",((e,[t])=>{a(t)&&(this.upcastDrupalMediaIsImage(t),this.upcastDrupalMediaType(t))}))}_renderElement(e){const t=this.editor.model.change((t=>{const i=t.createDocumentFragment(),n=t.cloneElement(e,!1);return["linkHref"].forEach((e=>{t.removeAttribute(e,n)})),t.append(n,i),i}));return this.editor.data.stringify(t)}static get pluginName(){return"DrupalMediaEditing"}}var g=i("ckeditor5/src/ui.js");class h extends e.Plugin{init(){const e=this.editor,t=this.editor.config.get("drupalMedia");if(!t)return;const{libraryURL:i,openDialog:n,dialogSettings:a={}}=t;i&&"function"==typeof n&&e.ui.componentFactory.add("drupalMedia",(t=>{const r=e.commands.get("insertDrupalMedia"),o=new g.ButtonView(t);return o.set({label:Drupal.t("Insert Media"),icon:'<svg width="20" height="20" viewBox="0 0 20 20" 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"/><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"/></svg>\n',tooltip:!0}),o.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(o,"execute",(()=>{n(i,(({attributes:t})=>{e.execute("insertDrupalMedia",t)}),a)})),o}))}}class f extends e.Plugin{static get requires(){return[t.WidgetToolbarRepository]}static get pluginName(){return"DrupalMediaToolbar"}afterInit(){const{editor:e}=this;var i;e.plugins.get(t.WidgetToolbarRepository).register("drupalMedia",{ariaLabel:Drupal.t("Drupal Media toolbar"),items:(i=e.config.get("drupalMedia.toolbar"),i.map((e=>l(e)?e.name:e))||[]),getRelatedElement:e=>s(e)})}}class b extends e.Command{refresh(){const e=o(this.editor.model.document.selection);this.isEnabled=!!e&&e.getAttribute("drupalMediaIsImage")&&e.getAttribute("drupalMediaIsImage")!==m,this.isEnabled?this.value=e.getAttribute("drupalMediaAlt"):this.value=!1}execute(e){const{model:t}=this.editor,i=o(t.document.selection);e.newValue=e.newValue.trim(),t.change((t=>{e.newValue.length>0?t.setAttribute("drupalMediaAlt",e.newValue,i):t.removeAttribute("drupalMediaAlt",i)}))}}class w extends e.Plugin{init(){this._data=new WeakMap}getMetadata(e){if(this._data.get(e))return new Promise((t=>{t(this._data.get(e))}));const t=this.editor.config.get("drupalMedia");if(!t)return new Promise(((e,t)=>{t(new Error("drupalMedia configuration is required for parsing metadata."))}));if(!e.hasAttribute("drupalMediaEntityUuid"))return new Promise(((e,t)=>{t(new Error("drupalMedia element must have drupalMediaEntityUuid attribute to retrieve metadata."))}));const{metadataUrl:i}=t;return(async e=>{const t=await fetch(e);if(t.ok)return JSON.parse(await t.text());throw new Error("Fetching media embed metadata from the server failed.")})(`${i}&${new URLSearchParams({uuid:e.getAttribute("drupalMediaEntityUuid")})}`).then((t=>(this._data.set(e,t),t)))}static get pluginName(){return"DrupalMediaMetadataRepository"}}class y extends e.Plugin{static get requires(){return[w]}static get pluginName(){return"MediaImageTextAlternativeEditing"}init(){const{editor:e,editor:{model:t,conversion:i}}=this;t.schema.extend("drupalMedia",{allowAttributes:["drupalMediaIsImage"]}),i.for("editingDowncast").add((e=>{e.on("attribute:drupalMediaIsImage",((e,t,i)=>{const{writer:n,mapper:a}=i,r=a.toViewElement(t.item);if(t.attributeNewValue!==m){const e=Array.from(r.getChildren()).find((e=>e.getCustomProperty("drupalMediaMetadataError")));return void(e&&(n.setCustomProperty("widgetLabel",e.getCustomProperty("drupalMediaOriginalWidgetLabel"),e),n.removeElement(e)))}const o=Drupal.t("Not all functionality may be available because some information could not be retrieved."),s=new g.Template({tag:"span",children:[{tag:"span",attributes:{class:"drupal-media__metadata-error-icon","data-cke-tooltip-text":o}}]}).render(),l=n.createRawElement("div",{class:"drupal-media__metadata-error"},((e,t)=>{t.setContentOf(e,s.outerHTML)}));n.setCustomProperty("drupalMediaMetadataError",!0,l);const d=r.getCustomProperty("widgetLabel");n.setCustomProperty("drupalMediaOriginalWidgetLabel",d,l),n.setCustomProperty("widgetLabel",`${d} (${o})`,r),n.insert(n.createPositionAt(r,0),l)}),{priority:"low"})})),e.commands.add("mediaImageTextAlternative",new b(this.editor))}}function v(e){const t=e.editing.view,i=g.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 E=i("ckeditor5/src/utils.js");class M extends g.View{constructor(t){super(t),this.focusTracker=new E.FocusTracker,this.keystrokes=new E.KeystrokeHandler,this.decorativeToggle=this._decorativeToggleView(),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 g.ViewCollection,this._focusCycler=new g.FocusCycler({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.setTemplate({tag:"form",attributes:{class:["ck","ck-media-alternative-text-form","ck-vertical-form"],tabindex:"-1"},children:[{tag:"div",children:[this.decorativeToggle]},this.labeledInput,this.saveButtonView,this.cancelButtonView]}),(0,g.injectCssTransitionDisabler)(this)}render(){super.render(),this.keystrokes.listenTo(this.element),(0,g.submitHandler)({view:this}),[this.decorativeToggle,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 g.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 g.LabeledFieldView(this.locale,g.createLabeledInputText);return e.bind("class").to(this.decorativeToggle,"isOn",(e=>e?"ck-hidden":"")),e.label=Drupal.t("Alternative text override"),e}_decorativeToggleView(){const e=new g.SwitchButtonView(this.locale);return e.set({withText:!0,label:Drupal.t("Decorative image")}),e.on("execute",(()=>{e.isOn&&(this.labeledInput.fieldView.element.value=""),e.set("isOn",!e.isOn)})),e}}class k extends e.Plugin{static get requires(){return[g.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 g.ButtonView(i);return a.set({label:Drupal.t("Override media image alternative text"),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 M(e.locale),this._form.render(),this.listenTo(this._form,"submit",(()=>{e.execute("mediaImageTextAlternative",{newValue:this._form.decorativeToggle.isOn?'""':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",(()=>{s(t.selection)?this._isVisible&&function(e){const t=e.plugins.get("ContextualBalloon");if(s(e.editing.view.document.selection)){const i=v(e);t.updatePosition(i)}}(e):this._hideForm(!0)})),(0,g.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.decorativeToggle,n=e.plugins.get("DrupalMediaMetadataRepository"),r=this._form.labeledInput;this._form.disableCssTransitions(),this._isInBalloon||this._balloon.add({view:this._form,position:v(e)}),i.isOn='""'===t.value,r.fieldView.element.value=t.value||"",r.fieldView.value=r.fieldView.element.value,this._form.defaultAltText="";const o=e.model.document.selection.getSelectedElement();a(o)&&n.getMetadata(o).then((e=>{this._form.defaultAltText=e.imageSourceMetadata?e.imageSourceMetadata.alt:"",r.infoText=Drupal.t(`Leave blank to use the default alternative text: "${this._form.defaultAltText}".`)})).catch((e=>{console.warn(e.toString())})),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 D extends e.Plugin{static get requires(){return[y,k]}static get pluginName(){return"MediaImageTextAlternative"}}function C(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 A(e,t,i){if(!i.consumable.consume(t.item,e.name))return;const n=i.mapper.toViewElement(t.item);C(i.writer,t.attributeNewValue,n)}class _ extends e.Plugin{constructor(e){if(super(e),!e.plugins.has("GeneralHtmlSupport"))return;e.plugins.has("DataFilter")&&e.plugins.has("DataSchema")||console.error("DataFilter and DataSchema plugins are required for Drupal Media to integrate with General HTML Support plugin.");const{schema:t}=e.model,{conversion:i}=e,n=this.editor.plugins.get("DataFilter");this.editor.plugins.get("DataSchema").registerBlockElement({model:"drupalMedia",view:"drupal-media"}),n.on("register:drupal-media",((e,a)=>{"drupalMedia"===a.model&&(t.extend("drupalMedia",{allowAttributes:["htmlLinkAttributes","htmlAttributes"]}),i.for("upcast").add(function(e){return t=>{t.on("element:drupal-media",((t,i,n)=>{function a(t,a){const r=e.processViewAttributes(t,n);r&&n.writer.setAttribute(a,r,i.modelRange)}const r=i.viewItem,o=r.parent;a(r,"htmlAttributes"),o.is("element","a")&&a(o,"htmlLinkAttributes")}),{priority:"low"})}}(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");C(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;C(i.writer,t.item.getAttribute("htmlLinkAttributes"),n)}),{priority:"low"}),e.on("attribute:htmlAttributes:drupalMedia",A,{priority:"low"})})),e.stop())}))}static get pluginName(){return"DrupalMediaGeneralHtmlSupport"}}class x extends e.Plugin{static get requires(){return[p,_,h,f,D]}static get pluginName(){return"DrupalMedia"}}var V=i("ckeditor5/src/engine.js");function S(e){return Array.from(e.getChildren()).find((e=>"drupal-media"===e.name))}function T(e){return t=>{t.on(`attribute:${e.id}:drupalMedia`,((t,i,n)=>{const a=n.mapper.toViewElement(i.item);let r=Array.from(a.getChildren()).find((e=>"a"===e.name));if(r=!r&&a.is("element","a")?a:Array.from(a.getAncestors()).find((e=>"a"===e.name)),r){for(const[t,i]of(0,E.toMap)(e.attributes))n.writer.setAttribute(t,i,r);e.classes&&n.writer.addClass(e.classes,r);for(const t in e.styles)Object.prototype.hasOwnProperty.call(e.styles,t)&&n.writer.setStyle(t,e.styles[t],r)}}))}}function I(e,t){return e=>{e.on("element:a",((e,i,n)=>{const a=i.viewItem;if(!S(a))return;const r=new V.Matcher(t._createPattern()).match(a);if(!r)return;if(!n.consumable.consume(a,r.match))return;const o=i.modelCursor.nodeBefore;n.writer.setAttribute(t.id,!0,o)}),{priority:"high"})}}class L 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=>{e.on("element:a",((e,t,i)=>{const n=t.viewItem,a=S(n);if(!a)return;if(!i.consumable.consume(n,{attributes:["href"],name:!0}))return;const r=n.getAttribute("href");if(null===r)return;const o=i.convertItem(a,t.modelCursor);t.modelRange=o.modelRange,t.modelCursor=o.modelCursor;const s=t.modelCursor.nodeBefore;s&&s.is("element","drupalMedia")&&i.writer.setAttribute("linkHref",r,s)}),{priority:"high"})})),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"})})),this._enableManualDecorators();if(e.commands.get("link").automaticDecorators.length>0)throw new Error("The Drupal Media plugin is not compatible with automatic link decorators. To use Drupal Media, disable any plugins providing automatic link decorators.")}_enableManualDecorators(){const e=this.editor,t=e.commands.get("link");for(const i of t.manualDecorators)e.model.schema.extend("drupalMedia",{allowAttributes:i.id}),e.conversion.for("downcast").add(T(i)),e.conversion.for("upcast").add(I(0,i))}}class O 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 g.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 P extends e.Plugin{static get requires(){return[L,O]}static get pluginName(){return"DrupalLinkMedia"}}const B={get inline(){return{name:"inline",title:"In line",icon:e.icons.objectInline,modelElements:["imageInline"],isDefault:!0}},get alignLeft(){return{name:"alignLeft",title:"Left aligned image",icon:e.icons.objectLeft,modelElements:["imageBlock","imageInline"],className:"image-style-align-left"}},get alignBlockLeft(){return{name:"alignBlockLeft",title:"Left aligned image",icon:e.icons.objectBlockLeft,modelElements:["imageBlock"],className:"image-style-block-align-left"}},get alignCenter(){return{name:"alignCenter",title:"Centered image",icon:e.icons.objectCenter,modelElements:["imageBlock"],className:"image-style-align-center"}},get alignRight(){return{name:"alignRight",title:"Right aligned image",icon:e.icons.objectRight,modelElements:["imageBlock","imageInline"],className:"image-style-align-right"}},get alignBlockRight(){return{name:"alignBlockRight",title:"Right aligned image",icon:e.icons.objectBlockRight,modelElements:["imageBlock"],className:"image-style-block-align-right"}},get block(){return{name:"block",title:"Centered image",icon:e.icons.objectCenter,modelElements:["imageBlock"],isDefault:!0}},get side(){return{name:"side",title:"Side image",icon:e.icons.objectRight,modelElements:["imageBlock"],className:"image-style-side"}}},N=(()=>({full:e.icons.objectFullWidth,left:e.icons.objectBlockLeft,right:e.icons.objectBlockRight,center:e.icons.objectCenter,inlineLeft:e.icons.objectLeft,inlineRight:e.icons.objectRight,inline:e.icons.objectInline}))(),j=[{name:"imageStyle:wrapText",title:"Wrap text",defaultItem:"imageStyle:alignLeft",items:["imageStyle:alignLeft","imageStyle:alignRight"]},{name:"imageStyle:breakText",title:"Break text",defaultItem:"imageStyle:block",items:["imageStyle:alignBlockLeft","imageStyle:block","imageStyle:alignBlockRight"]}];function R(e){(0,E.logWarning)("image-style-configuration-definition-invalid",e)}const F={normalizeStyles:function(e){return(e.configuredStyles.options||[]).map((e=>function(e){e="string"==typeof e?B[e]?{...B[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}(B[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 R({style:e}),!1;{const a=[t?"imageBlock":null,i?"imageInline":null];if(!n.some((e=>a.includes(e))))return(0,E.logWarning)("image-style-missing-dependency",{style:e,missingPlugins:n.map((e=>"imageBlock"===e?"ImageBlockEditing":"ImageInlineEditing"))}),!1}return!0}(t,e)))},getDefaultStylesConfiguration:function(e,t){return e&&t?{options:["inline","alignLeft","alignRight","alignCenter","alignBlockLeft","alignBlockRight","block","side"]}:e?{options:["block","side"]}:t?{options:["inline","alignLeft","alignRight"]}:{}},getDefaultDropdownDefinitions:function(e){return e.has("ImageBlockEditing")&&e.has("ImageInlineEditing")?[...j]:[]},warnInvalidStyle:R,DEFAULT_OPTIONS:B,DEFAULT_ICONS:N,DEFAULT_DROPDOWN_DEFINITIONS:j};function U(e,t,i){for(const n of t)if(i.checkAttribute(e,n))return!0;return!1}function H(e,t,i){const n=e.getSelectedElement();if(n&&U(n,i,t))return n;let{parent:a}=e.getFirstPosition();for(;a;){if(a.is("element")&&U(a,i,t))return a;a=a.parent}return null}class $ extends e.Command{constructor(e,t){super(e),this.styles={},Object.keys(t).forEach((e=>{this.styles[e]=new Map(t[e].map((e=>[e.name,e])))})),this.modelAttributes=[];for(const e of Object.keys(t)){const t=u(e);this.modelAttributes.push(t)}}refresh(){const{editor:e}=this,t=H(e.model.document.selection,e.model.schema,this.modelAttributes);this.isEnabled=!!t,this.isEnabled?this.value=this.getValue(t):this.value=!1}getValue(e){const t={};return Object.keys(this.styles).forEach((i=>{const n=u(i);if(e.hasAttribute(n))t[i]=e.getAttribute(n);else for(const[,e]of this.styles[i])e.isDefault&&(t[i]=e.name)})),t}execute(e={}){const{editor:{model:t}}=this,{value:i,group:n}=e,a=u(n);t.change((e=>{const r=H(t.document.selection,t.schema,this.modelAttributes);!i||this.styles[n].get(i).isDefault?e.removeAttribute(a,r):e.setAttribute(a,i,r)}))}}function q(e,t){for(const i of t)if(i.name===e)return i}class W extends e.Plugin{init(){const{editor:t}=this,i=t.config.get("drupalElementStyles");this.normalizedStyles={},Object.keys(i).forEach((t=>{this.normalizedStyles[t]=i[t].map((t=>("string"==typeof t.icon&&e.icons[t.icon]&&(t.icon=e.icons[t.icon]),t.name&&(t.name=`${t.name}`),t))).filter((e=>e.isDefault||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(`${e.attributeValue} drupalElementStyles options must include attributeName and attributeValue.`),!1)))})),this._setupConversion(),t.commands.add("drupalElementStyle",new $(t,this.normalizedStyles))}_setupConversion(){const{editor:e}=this,{schema:t}=e.model;Object.keys(this.normalizedStyles).forEach((i=>{const n=u(i),a=(r=this.normalizedStyles[i],(e,t,i)=>{if(!i.consumable.consume(t.item,e.name))return;const n=q(t.attributeNewValue,r),a=q(t.attributeOldValue,r),o=i.mapper.toViewElement(t.item),s=i.writer;a&&("class"===a.attributeName?s.removeClass(a.attributeValue,o):s.removeAttribute(a.attributeName,o)),n&&("class"===n.attributeName?s.addClass(n.attributeValue,o):n.isDefault||s.setAttribute(n.attributeName,n.attributeValue,o))});var r;const o=function(e,t){const i=e.filter((e=>!e.isDefault));return(e,n,a)=>{if(!n.modelRange)return;const r=n.viewItem,o=(0,E.first)(n.modelRange.getItems());if(o&&a.schema.checkAttribute(o,t))for(const e of i)if("class"===e.attributeName)a.consumable.consume(r,{classes:e.attributeValue})&&a.writer.setAttribute(t,e.name,o);else if(a.consumable.consume(r,{attributes:[e.attributeName]}))for(const e of i)e.attributeValue===r.getAttribute(e.attributeName)&&a.writer.setAttribute(t,e.name,o)}}(this.normalizedStyles[i],n);e.editing.downcastDispatcher.on(`attribute:${n}`,a),e.data.downcastDispatcher.on(`attribute:${n}`,a);[...new Set(this.normalizedStyles[i].map((e=>e.modelElements)).flat())].forEach((e=>{t.extend(e,{allowAttributes:n})})),e.data.upcastDispatcher.on("element",o,{priority:"low"})}))}static get pluginName(){return"DrupalElementStyleEditing"}}const K=e=>e,z=(e,t)=>(e?`${e}: `:"")+t;function Z(e,t){return`drupalElementStyle:${t}:${e}`}class G extends e.Plugin{static get requires(){return[W]}init(){const{plugins:e}=this.editor,t=this.editor.config.get("drupalMedia.toolbar")||[],i=e.get("DrupalElementStyleEditing").normalizedStyles;Object.keys(i).forEach((e=>{i[e].forEach((t=>{this._createButton(t,e,i[e])}))}));t.filter(l).filter((e=>{const t=[];if(!e.display)return console.warn("dropdown configuration must include a display key specifying either listDropdown or splitButton."),!1;e.items.includes(e.defaultItem)||console.warn("defaultItem must be part of items in the dropdown configuration.");for(const i of e.items){const e=i.split(":")[1];t.push(e)}return!!t.every((e=>e===t[0]))||(console.warn("dropdown configuration should only contain buttons from one group."),!1)})).forEach((e=>{if(e.items.length>=2){const t=e.name.split(":")[1];switch(e.display){case"splitButton":this._createDropdown(e,i[t]);break;case"listDropdown":this._createListDropdown(e,i[t])}}}))}updateOptionVisibility(e,t,i,n){const{selection:a}=this.editor.model.document,r={};r[n]=e;const o=a?a.getSelectedElement():H(a,this.editor.model.schema,r),s=e.filter((function(e){for(const[t,i]of(0,E.toMap)(e.modelAttributes))if(o&&o.hasAttribute(t))return i.includes(o.getAttribute(t));return!0}));i.hasOwnProperty("model")?s.includes(t)?i.model.set({class:""}):i.model.set({class:"ck-hidden"}):s.includes(t)?i.set({class:""}):i.set({class:"ck-hidden"})}_createDropdown(e,t){const i=this.editor.ui.componentFactory;i.add(e.name,(n=>{let a;const{defaultItem:r,items:o,title:s}=e,l=o.filter((e=>{const i=e.split(":")[1];return t.find((({name:t})=>Z(t,i)===e))})).map((e=>{const t=i.create(e);return e===r&&(a=t),t}));o.length!==l.length&&F.warnInvalidStyle({dropdown:e});const d=(0,g.createDropdown)(n,g.SplitButtonView),u=d.buttonView;return(0,g.addToolbarToDropdown)(d,l),u.set({label:z(s,a.label),class:null,tooltip:!0}),u.bind("icon").toMany(l,"isOn",((...e)=>{const t=e.findIndex(K);return t<0?a.icon:l[t].icon})),u.bind("label").toMany(l,"isOn",((...e)=>{const t=e.findIndex(K);return z(s,t<0?a.label:l[t].label)})),u.bind("isOn").toMany(l,"isOn",((...e)=>e.some(K))),u.bind("class").toMany(l,"isOn",((...e)=>e.some(K)?"ck-splitbutton_flatten":null)),u.on("execute",(()=>{l.some((({isOn:e})=>e))?d.isOpen=!d.isOpen:a.fire("execute")})),d.bind("isEnabled").toMany(l,"isEnabled",((...e)=>e.some(K))),d}))}_createButton(e,t,i){const n=e.name;this.editor.ui.componentFactory.add(Z(n,t),(a=>{const r=this.editor.commands.get("drupalElementStyle"),o=new g.ButtonView(a);return o.set({label:e.title,icon:e.icon,tooltip:!0,isToggleable:!0}),o.bind("isEnabled").to(r,"isEnabled"),o.bind("isOn").to(r,"value",(e=>e&&e[t]===n)),o.on("execute",this._executeCommand.bind(this,n,t)),this.listenTo(this.editor.ui,"update",(()=>{this.updateOptionVisibility(i,e,o,t)})),o}))}getDropdownListItemDefinitions(e,t,i){const n=new E.Collection;return e.forEach((t=>{const a={type:"button",model:new g.ViewModel({group:i,commandValue:t.name,label:t.title,withText:!0,class:""})};n.add(a),this.listenTo(this.editor.ui,"update",(()=>{this.updateOptionVisibility(e,t,a,i)}))})),n}_createListDropdown(e,t){const i=this.editor.ui.componentFactory;i.add(e.name,(n=>{let a;const{defaultItem:r,items:o,title:s,defaultText:l}=e,d=e.name.split(":")[1],u=o.filter((e=>t.find((({name:t})=>Z(t,d)===e)))).map((e=>{const t=i.create(e);return e===r&&(a=t),t}));o.length!==u.length&&F.warnInvalidStyle({dropdown:e});const c=(0,g.createDropdown)(n,g.DropdownButtonView),m=c.buttonView;m.set({label:z(s,a.label),class:null,tooltip:l,withText:!0});const p=this.editor.commands.get("drupalElementStyle");return m.bind("label").to(p,"value",(e=>{if(e&&e[d])for(const i of t)if(i.name===e[d])return i.title;return l})),c.bind("isOn").to(p),c.bind("isEnabled").to(this),(0,g.addListToDropdown)(c,this.getDropdownListItemDefinitions(t,p,d)),this.listenTo(c,"execute",(e=>{this._executeCommand(e.source.commandValue,e.source.group)})),c}))}_executeCommand(e,t){this.editor.execute("drupalElementStyle",{value:e,group:t}),this.editor.editing.view.focus()}static get pluginName(){return"DrupalElementStyleUi"}}class J extends e.Plugin{static get requires(){return[W,G]}static get pluginName(){return"DrupalElementStyle"}}function X(e){const t=e.getFirstPosition().findAncestor("caption");return t&&a(t.parent)?t:null}function Q(e){for(const t of e.getChildren())if(t&&t.is("element","caption"))return t;return null}class Y extends e.Command{refresh(){const e=this.editor.model.document.selection,t=e.getSelectedElement();if(!t)return this.isEnabled=!!o(e),void(this.value=!!X(e));this.isEnabled=a(t),this.isEnabled?this.value=!!Q(t):this.value=!1}execute(e={}){const{focusCaptionOnShow:t}=e;this.editor.model.change((e=>{this.value?this._hideDrupalMediaCaption(e):this._showDrupalMediaCaption(e,t)}))}_showDrupalMediaCaption(e,t){const i=this.editor.model.document.selection,n=this.editor.plugins.get("DrupalMediaCaptionEditing"),a=o(i),r=n._getSavedCaption(a)||e.createElement("caption");e.append(r,a),t&&e.setSelection(r,"in")}_hideDrupalMediaCaption(e){const t=this.editor,i=t.model.document.selection,n=t.plugins.get("DrupalMediaCaptionEditing");let a,r=i.getSelectedElement();r?a=Q(r):(a=X(i),r=o(i)),n._saveCaption(r,a),e.setSelection(r,"on"),e.remove(a)}}class ee extends e.Plugin{static get requires(){return[]}static get pluginName(){return"DrupalMediaCaptionEditing"}constructor(e){super(e),this._savedCaptionsMap=new WeakMap}init(){const e=this.editor,t=e.model.schema;t.isRegistered("caption")?t.extend("caption",{allowIn:"drupalMedia"}):t.register("caption",{allowIn:"drupalMedia",allowContentOf:"$block",isLimit:!0}),e.commands.add("toggleMediaCaption",new Y(e)),this._setupConversion()}_setupConversion(){const e=this.editor,i=e.editing.view;var n;e.conversion.for("upcast").add(function(e){const t=(t,i,n)=>{const{viewItem:a}=i,{writer:r,consumable:o}=n;if(!i.modelRange||!o.consume(a,{attributes:["data-caption"]}))return;const s=r.createElement("caption"),l=i.modelRange.start.nodeAfter,d=e.data.processor.toView(a.getAttribute("data-caption"));n.consumable.constructor.createFrom(d,n.consumable),n.convertChildren(d,s),r.append(s,l)};return e=>{e.on("element:drupal-media",t,{priority:"low"})}}(e)),e.conversion.for("editingDowncast").elementToElement({model:"caption",view:(e,{writer:n})=>{if(!a(e.parent))return null;const r=n.createEditableElement("figcaption");return r.placeholder=Drupal.t("Enter media caption"),(0,V.enablePlaceholder)({view:i,element:r,keepOnFocus:!0}),(0,t.toWidgetEditable)(r,n)}}),e.editing.mapper.on("modelToViewPosition",(n=i,(e,t)=>{const i=t.modelPosition,r=i.parent;if(!a(r))return;const o=t.mapper.toViewElement(r);t.viewPosition=n.createPositionAt(o,i.offset+1)})),e.conversion.for("dataDowncast").add(function(e){return t=>{t.on("insert:caption",((t,i,n)=>{const{consumable:r,writer:o,mapper:s}=n;if(!a(i.item.parent)||!r.consume(i.item,"insert"))return;const l=e.model.createRangeIn(i.item),d=o.createDocumentFragment();s.bindElements(i.item,d);for(const{item:t}of Array.from(l)){const i={item:t,range:e.model.createRangeOn(t)},a=`insert:${t.name||"$text"}`;e.data.downcastDispatcher.fire(a,i,n);for(const a of t.getAttributeKeys())Object.assign(i,{attributeKey:a,attributeOldValue:null,attributeNewValue:i.item.getAttribute(a)}),e.data.downcastDispatcher.fire(`attribute:${a}`,i,n)}for(const e of o.createRangeIn(d).getItems())s.unbindViewElement(e);s.unbindViewElement(d);const u=e.data.processor.toData(d);if(u){const e=s.toViewElement(i.item.parent);o.setAttribute("data-caption",u,e)}}))}}(e))}_getSavedCaption(e){const t=this._savedCaptionsMap.get(e);return t?V.Element.fromJSON(t):null}_saveCaption(e,t){this._savedCaptionsMap.set(e,t.toJSON())}}class te extends e.Plugin{static get requires(){return[]}static get pluginName(){return"DrupalMediaCaptionUI"}init(){const{editor:t}=this,i=t.editing.view;t.ui.componentFactory.add("toggleDrupalMediaCaption",(n=>{const a=new g.ButtonView(n),r=t.commands.get("toggleMediaCaption");return a.set({label:Drupal.t("Caption media"),icon:e.icons.caption,tooltip:!0,isToggleable:!0}),a.bind("isOn","isEnabled").to(r,"value","isEnabled"),a.bind("label").to(r,"value",(e=>e?Drupal.t("Toggle caption off"):Drupal.t("Toggle caption on"))),this.listenTo(a,"execute",(()=>{t.execute("toggleMediaCaption",{focusCaptionOnShow:!0});const e=X(t.model.document.selection);if(e){const n=t.editing.mapper.toViewElement(e);i.scrollToTheSelection(),i.change((e=>{e.addClass("drupal-media__caption_highlighted",n)}))}t.editing.view.focus()})),a}))}}class ie extends e.Plugin{static get requires(){return[ee,te]}static get pluginName(){return"DrupalMediaCaption"}}const ne={DrupalMedia:x,MediaImageTextAlternative:D,MediaImageTextAlternativeEditing:y,MediaImageTextAlternativeUi:k,DrupalLinkMedia:P,DrupalMediaCaption:ie,DrupalElementStyle:J}})(),n=n.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())}(globalThis,(()=>(()=>{var e={"ckeditor5/src/core.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/engine.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/engine.js")},"ckeditor5/src/ui.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/ui.js")},"ckeditor5/src/utils.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"ckeditor5/src/widget.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/widget.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function i(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=(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=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var n={};return(()=>{"use strict";i.d(n,{default:()=>ce});var e=i("ckeditor5/src/core.js"),t=i("ckeditor5/src/widget.js"),a=i("ckeditor5/src/utils.js");function r(e,t){return void 0===t&&(t=!0),t?!!e&&(e.is("element","drupalMedia")||e.is("element","drupalMediaInline")):!!e&&e.is("element","drupalMedia")}function o(e,i){return void 0===i&&(i=!0),i?(0,t.isWidget)(e)&&(!!e.getCustomProperty("drupalMedia")||!!e.getCustomProperty("drupalMediaInline")):(0,t.isWidget)(e)&&!!e.getCustomProperty("drupalMedia")}function s(e,t){void 0===t&&(t=!0);const i=e.getSelectedElement();return r(i,t)?i:e.getFirstPosition().findAncestor(t?/drupalMedia(Inline)?/:/drupalMedia/)}function l(e,t){void 0===t&&(t=!0);const i=e.getSelectedElement();if(i&&o(i,t))return i;if(null===e.getFirstPosition())return null;let n=e.getFirstPosition().parent;for(;n;){if(n.is("element")&&o(n,t))return n;n=n.parent}return null}function d(e){const t=typeof e;return null!=e&&("object"===t||"function"===t)}function u(e){for(const t of e){if(t.hasAttribute("data-drupal-media-preview"))return t;if(t.childCount){const e=u(t.getChildren());if(e)return e}}return null}function c(e){return`drupalElementStyle${e[0].toUpperCase()+e.substring(1)}`}function m(e,t){const i=e.model.schema;return t.is("selection")?function(e,t){const i=(0,a.first)(t.getSelectedBlocks());return!i||e.isObject(i)||i.isEmpty&&"listItem"!==i.name?"drupalMedia":"drupalMediaInline"}(i,t):i.checkChild(t,"drupalMediaInline")?"drupalMediaInline":"drupalMedia"}class p 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"),{normalizedStyles:i}=t;for(const a of Object.keys(i))for(const i of t.normalizedStyles[a])if(e[i.attributeName]&&i.attributeValue===e[i.attributeName]){const e=c(a);n[e]=i.name}}const a=m(this.editor,this.editor.model.document.selection);this.editor.model.change((e=>{this.editor.model.insertObject(function(e,t,i){return e.createElement(i,t)}(e,n,a))}))}refresh(){const e=this.editor.model,t=e.document.selection,i=m(this.editor,t),n=e.schema.findAllowedParent(t.getFirstPosition(),i);this.isEnabled=null!==n}}const g="METADATA_ERROR";class h extends e.Plugin{static get requires(){return[t.Widget]}constructor(e){super(e),this.attrs={drupalMediaAlt:"alt",drupalMediaEntityType:"data-entity-type",drupalMediaEntityUuid:"data-entity-uuid"},this.converterAttributes=["drupalMediaEntityUuid","drupalElementStyleViewMode","drupalMediaEntityType","drupalMediaAlt"]}init(){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. Save your work and reload this page.")}<p>\n `,this._defineSchema(),this._defineConverters(),this._defineListeners(),this.editor.commands.add("insertDrupalMedia",new p(this.editor))}upcastDrupalMediaIsImage(e){const{model:t,plugins:i}=this.editor;i.get("DrupalMediaMetadataRepository").getMetadata(e).then((i=>{e&&t.enqueueChange({isUndoable:!1},(t=>{t.setAttribute("drupalMediaIsImage",!!i.imageSourceMetadata,e)}))})).catch((i=>{e&&(console.warn(i.toString()),t.enqueueChange({isUndoable:!1},(t=>{t.setAttribute("drupalMediaIsImage",g,e)})))}))}upcastDrupalMediaType(e){this.editor.plugins.get("DrupalMediaMetadataRepository").getMetadata(e).then((t=>{e&&this.editor.model.enqueueChange({isUndoable:!1},(i=>{i.setAttribute("drupalMediaType",t.type,e)}))})).catch((t=>{e&&(console.warn(t.toString()),this.editor.model.enqueueChange({isUndoable:!1},(t=>{t.setAttribute("drupalMediaType",g,e)})))}))}async _fetchPreview(e){const t={text:this._renderElement(e),uuid:e.getAttribute("drupalMediaEntityUuid")},i=await fetch(`${this.previewUrl}?${new URLSearchParams(t)}`,{headers:{"X-Drupal-MediaPreview-CSRF-Token":this.editor.config.get("drupalMedia").previewCsrfToken}});if(i.ok){return{label:i.headers.get("drupal-media-label"),preview:await i.text()}}return{label:this.labelError,preview:this.themeError}}_defineSchema(){const e=this.editor.model.schema;e.register("drupalMedia",{inheritAllFrom:"$blockObject",allowAttributes:Object.keys(this.attrs)}),e.register("drupalMediaInline",{inheritAllFrom:"$inlineObject",allowIn:["$block","$container","$text"],allowAttributes:Object.keys(this.attrs)}),this.editor.editing.view.domConverter.blockElements.push("drupal-media")}_defineConverters(){const e=this.editor.conversion,i=this.editor.plugins.get("DrupalMediaMetadataRepository"),n={"drupal-media":"drupalMedia","drupal-media-inline":"drupalMediaInline"};Object.keys(n).forEach((a=>{const r=n[a];e.for("upcast").elementToElement({view:{name:a},model:r}).add((e=>{e.on(`element:${a}`,((e,t)=>{const[n]=t.modelRange.getItems();i.getMetadata(n).then((e=>{n&&(this.upcastDrupalMediaIsImage(n),this.editor.model.enqueueChange({isUndoable:!1},(t=>{t.setAttribute("drupalMediaType",e.type,n)})))})).catch((e=>{console.warn(e.toString())}))}),{priority:"lowest"})})),e.for("dataDowncast").elementToElement({model:r,view:{name:a}}),e.for("editingDowncast").elementToElement({model:r,view:(e,{writer:i})=>{const n=i.createContainerElement("figure",{class:"drupal-media-inline"===a?`drupal-media ${a}`:a});if(!this.previewUrl){const e=i.createRawElement("div",{"data-drupal-media-preview":"unavailable"});i.insert(i.createPositionAt(n,0),e)}return i.setCustomProperty(r,!0,n),(0,t.toWidget)(n,i,{label:Drupal.t("Media widget")})}}).add((e=>{const t=(e,t,i)=>{const n=i.writer,a=t.item,r=i.mapper.toViewElement(t.item);let o=u(r.getChildren());if(o){if("ready"!==o.getAttribute("data-drupal-media-preview"))return;n.setAttribute("data-drupal-media-preview","loading",o)}else o=n.createRawElement("div",{"data-drupal-media-preview":"loading"}),n.insert(n.createPositionAt(r,0),o);this._fetchPreview(a).then((({label:e,preview:t})=>{o&&this.editor.editing.view.change((i=>{const n=i.createRawElement("div",{"data-drupal-media-preview":"ready","aria-label":e},(e=>{e.innerHTML=t}));i.insert(i.createPositionBefore(o),n),i.remove(o)}))}))};return this.converterAttributes.forEach((i=>{e.on(`attribute:${i}:${r}`,t)})),e})),e.for("editingDowncast").add((e=>{e.on(`attribute:drupalElementStyleAlign:${r}`,((e,t,i)=>{const n={left:"drupal-media-style-align-left",right:"drupal-media-style-align-right",center:"drupal-media-style-align-center"},a=i.mapper.toViewElement(t.item),r=i.writer;n[t.attributeOldValue]&&r.removeClass(n[t.attributeOldValue],a),n[t.attributeNewValue]&&i.consumable.consume(t.item,e.name)&&r.addClass(n[t.attributeNewValue],a)}))})),Object.keys(this.attrs).forEach((t=>{const i={model:{key:t,name:r},view:{name:a,key:this.attrs[t]}};e.for("dataDowncast").attributeToAttribute(i),e.for("upcast").attributeToAttribute(i)}))}))}_defineListeners(){this.editor.model.on("insertContent",((e,[t])=>{r(t)&&(this.upcastDrupalMediaIsImage(t),this.upcastDrupalMediaType(t))}))}_renderElement(e){const t=this.editor.model.change((t=>{const i=t.createDocumentFragment(),n=t.cloneElement(e,!1);return["linkHref"].forEach((e=>{t.removeAttribute(e,n)})),t.append(n,i),i}));return this.editor.data.stringify(t)}static get pluginName(){return"DrupalMediaEditing"}}var f=i("ckeditor5/src/ui.js");class b extends e.Plugin{init(){const e=this.editor,t=this.editor.config.get("drupalMedia");if(!t)return;const{libraryURL:i,openDialog:n,dialogSettings:a={}}=t;i&&"function"==typeof n&&e.ui.componentFactory.add("drupalMedia",(t=>{const r=e.commands.get("insertDrupalMedia"),o=new f.ButtonView(t);return o.set({label:Drupal.t("Insert Media"),icon:'<svg width="20" height="20" viewBox="0 0 20 20" 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"/><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"/></svg>\n',tooltip:!0}),o.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(o,"execute",(()=>{n(i,(({attributes:t})=>{e.execute("insertDrupalMedia",t)}),a)})),o}))}}class w 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=>d(e)?e.name:e))||[]),getRelatedElement:e=>l(e)})}}class y extends e.Command{refresh(){const e=s(this.editor.model.document.selection);this.isEnabled=!!e&&e.getAttribute("drupalMediaIsImage")&&e.getAttribute("drupalMediaIsImage")!==g,this.isEnabled?this.value=e.getAttribute("drupalMediaAlt"):this.value=!1}execute(e){const{model:t}=this.editor,i=s(t.document.selection);e.newValue=e.newValue.trim(),t.change((t=>{e.newValue.length>0?t.setAttribute("drupalMediaAlt",e.newValue,i):t.removeAttribute("drupalMediaAlt",i)}))}}class v extends e.Plugin{init(){this._data=new WeakMap}getMetadata(e){if(this._data.get(e))return new Promise((t=>{t(this._data.get(e))}));const t=this.editor.config.get("drupalMedia");if(!t)return new Promise(((e,t)=>{t(new Error("drupalMedia configuration is required for parsing metadata."))}));if(!e.hasAttribute("drupalMediaEntityUuid"))return new Promise(((e,t)=>{t(new Error("drupalMedia element must have drupalMediaEntityUuid attribute to retrieve metadata."))}));const{metadataUrl:i}=t;return(async e=>{const t=await fetch(e);if(t.ok)return JSON.parse(await t.text());throw new Error("Fetching media embed metadata from the server failed.")})(`${i}&${new URLSearchParams({uuid:e.getAttribute("drupalMediaEntityUuid")})}`).then((t=>(this._data.set(e,t),t)))}static get pluginName(){return"DrupalMediaMetadataRepository"}}class E extends e.Plugin{static get requires(){return[v]}static get pluginName(){return"MediaImageTextAlternativeEditing"}init(){const{editor:e,editor:{model:t,conversion:i}}=this;t.schema.extend("drupalMedia",{allowAttributes:["drupalMediaIsImage"]}),i.for("editingDowncast").add((e=>{e.on("attribute:drupalMediaIsImage",((e,t,i)=>{const{writer:n,mapper:a}=i,r=a.toViewElement(t.item);if(t.attributeNewValue!==g){const e=Array.from(r.getChildren()).find((e=>e.getCustomProperty("drupalMediaMetadataError")));return void(e&&(n.setCustomProperty("widgetLabel",e.getCustomProperty("drupalMediaOriginalWidgetLabel"),e),n.removeElement(e)))}const o=Drupal.t("Not all functionality may be available because some information could not be retrieved."),s=new f.Template({tag:"span",children:[{tag:"span",attributes:{class:"drupal-media__metadata-error-icon","data-cke-tooltip-text":o}}]}).render(),l=n.createRawElement("div",{class:"drupal-media__metadata-error"},((e,t)=>{t.setContentOf(e,s.outerHTML)}));n.setCustomProperty("drupalMediaMetadataError",!0,l);const d=r.getCustomProperty("widgetLabel");n.setCustomProperty("drupalMediaOriginalWidgetLabel",d,l),n.setCustomProperty("widgetLabel",`${d} (${o})`,r),n.insert(n.createPositionAt(r,0),l)}),{priority:"low"})})),e.commands.add("mediaImageTextAlternative",new y(this.editor))}}function M(e){const t=e.editing.view,i=f.BalloonPanelView.defaultPositions;return{target:t.domConverter.viewToDom(t.document.selection.getSelectedElement()),positions:[i.northArrowSouth,i.northArrowSouthWest,i.northArrowSouthEast,i.southArrowNorth,i.southArrowNorthWest,i.southArrowNorthEast]}}class k extends f.View{constructor(t){super(t),this.focusTracker=new a.FocusTracker,this.keystrokes=new a.KeystrokeHandler,this.labeledInput=this._createLabeledInputView(),this.set("defaultAltText",void 0),this.defaultAltTextView=this._createDefaultAltTextView(),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 f.ViewCollection,this._focusCycler=new f.FocusCycler({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.setTemplate({tag:"form",attributes:{class:["ck","ck-media-alternative-text-form","ck-vertical-form"],tabindex:"-1"},children:[this.defaultAltTextView,this.labeledInput,this.saveButtonView,this.cancelButtonView]}),(0,f.injectCssTransitionDisabler)(this)}render(){super.render(),this.keystrokes.listenTo(this.element),(0,f.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 f.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 f.LabeledFieldView(this.locale,f.createLabeledInputText);return e.label=Drupal.t("Alternative text override"),e}_createDefaultAltTextView(){const e=f.Template.bind(this,this);return new f.Template({tag:"div",attributes:{class:["ck-media-alternative-text-form__default-alt-text",e.if("defaultAltText","ck-hidden",(e=>!e))]},children:[{tag:"strong",attributes:{class:"ck-media-alternative-text-form__default-alt-text-label"},children:[Drupal.t("Default alternative text:")]}," ",{tag:"span",attributes:{class:"ck-media-alternative-text-form__default-alt-text-value"},children:[{text:[e.to("defaultAltText")]}]}]})}}class A extends e.Plugin{static get requires(){return[f.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 f.ButtonView(i);return a.set({label:Drupal.t("Override media image alternative text"),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 k(e.locale),this._form.render(),this.listenTo(this._form,"submit",(()=>{e.execute("mediaImageTextAlternative",{newValue:this._form.labeledInput.fieldView.element.value}),this._hideForm(!0)})),this.listenTo(this._form,"cancel",(()=>{this._hideForm(!0)})),this._form.keystrokes.set("Esc",((e,t)=>{this._hideForm(!0),t()})),this.listenTo(e.ui,"update",(()=>{l(t.selection)?this._isVisible&&function(e){const t=e.plugins.get("ContextualBalloon");if(l(e.editing.view.document.selection)){const i=M(e);t.updatePosition(i)}}(e):this._hideForm(!0)})),(0,f.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=e.plugins.get("DrupalMediaMetadataRepository"),n=this._form.labeledInput;this._form.disableCssTransitions(),this._isInBalloon||this._balloon.add({view:this._form,position:M(e)}),n.fieldView.element.value=t.value||"",n.fieldView.value=n.fieldView.element.value,this._form.defaultAltText="";const a=e.model.document.selection.getSelectedElement();r(a)&&i.getMetadata(a).then((e=>{this._form.defaultAltText=e.imageSourceMetadata?e.imageSourceMetadata.alt:""})).catch((e=>{console.warn(e.toString())})),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 D extends e.Plugin{static get requires(){return[E,A]}static get pluginName(){return"MediaImageTextAlternative"}}function C(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 _(e,t,i){if(!i.consumable.consume(t.item,e.name))return;const n=i.mapper.toViewElement(t.item);C(i.writer,t.attributeNewValue,n)}class x extends e.Plugin{constructor(e){if(super(e),!e.plugins.has("GeneralHtmlSupport"))return;e.plugins.has("DataFilter")&&e.plugins.has("DataSchema")||console.error("DataFilter and DataSchema plugins are required for Drupal Media to integrate with General HTML Support plugin.");const{schema:t}=e.model,{conversion:i}=e,n=this.editor.plugins.get("DataFilter"),a=this.editor.plugins.get("DataSchema"),r={"drupal-media":"drupalMedia","drupal-media-inline":"drupalMediaInline"};Object.keys(r).forEach((e=>{const o=r[e];a.registerBlockElement({model:o,view:e}),n.on(`register:${e}`,((a,r)=>{r.model===o&&(t.extend(o,{allowAttributes:["htmlLinkAttributes","htmlAttributes"]}),i.for("upcast").add(function(e,t,i){return t=>{t.on(`element:${i}`,((t,i,n)=>{function a(t,a){const r=e.processViewAttributes(t,n);r&&n.writer.setAttribute(a,r,i.modelRange)}const r=i.viewItem,o=r.parent;a(r,"htmlAttributes"),o.is("element","a")&&a(o,"htmlLinkAttributes")}),{priority:"low"})}}(n,0,e)),i.for("editingDowncast").add(function(e){return t=>{t.on(`attribute:linkHref:${e}`,((t,i,n)=>{if(!n.consumable.consume(i.item,`attribute:htmlLinkAttributes:${e}`))return;const a=n.mapper.toViewElement(i.item),r=function(e,t,i){const n=e.createRangeOn(t);for(const{item:e}of n.getWalker())if(e.is("element",i))return e}(n.writer,a,"a");C(n.writer,i.item.getAttribute("htmlLinkAttributes"),r)}),{priority:"low"})}}(o)),i.for("dataDowncast").add(function(e){return t=>{t.on(`attribute:linkHref:${e}`,((t,i,n)=>{if(!n.consumable.consume(i.item,`attribute:htmlLinkAttributes:${e}`))return;const a=n.mapper.toViewElement(i.item).parent;C(n.writer,i.item.getAttribute("htmlLinkAttributes"),a)}),{priority:"low"}),t.on(`attribute:htmlAttributes:${e}`,_,{priority:"low"})}}(o)),a.stop())}))}))}static get pluginName(){return"DrupalMediaGeneralHtmlSupport"}}class V extends e.Plugin{static get requires(){return[h,x,b,w,D]}static get pluginName(){return"DrupalMedia"}}var I=i("ckeditor5/src/engine.js");function S(e){return Array.from(e.getChildren()).find((e=>"drupal-media"===e.name))}function T(e){return t=>{t.on(`attribute:${e.id}:drupalMedia`,((t,i,n)=>{const r=n.mapper.toViewElement(i.item);let o=Array.from(r.getChildren()).find((e=>"a"===e.name));if(o=!o&&r.is("element","a")?r:Array.from(r.getAncestors()).find((e=>"a"===e.name)),o){for(const[t,i]of(0,a.toMap)(e.attributes))n.writer.setAttribute(t,i,o);e.classes&&n.writer.addClass(e.classes,o);for(const t in e.styles)Object.prototype.hasOwnProperty.call(e.styles,t)&&n.writer.setStyle(t,e.styles[t],o)}}))}}function L(e,t){return e=>{e.on("element:a",((e,i,n)=>{const a=i.viewItem;if(!S(a))return;const r=new I.Matcher(t._createPattern()).match(a);if(!r)return;if(!n.consumable.consume(a,r.match))return;const o=i.modelCursor.nodeBefore;n.writer.setAttribute(t.id,!0,o)}),{priority:"high"})}}class P 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=>{e.on("element:a",((e,t,i)=>{const n=t.viewItem,a=S(n);if(!a)return;if(!i.consumable.consume(n,{attributes:["href"],name:!0}))return;const r=n.getAttribute("href");if(null===r)return;const o=i.convertItem(a,t.modelCursor);t.modelRange=o.modelRange,t.modelCursor=o.modelCursor;const s=t.modelCursor.nodeBefore;s&&s.is("element","drupalMedia")&&i.writer.setAttribute("linkHref",r,s)}),{priority:"high"})})),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"})})),this._enableManualDecorators();if(e.commands.get("link").automaticDecorators.length>0)throw new Error("The Drupal Media plugin is not compatible with automatic link decorators. To use Drupal Media, disable any plugins providing automatic link decorators.")}_enableManualDecorators(){const e=this.editor,t=e.commands.get("link");for(const i of t.manualDecorators)e.model.schema.extend("drupalMedia",{allowAttributes:i.id}),e.conversion.for("downcast").add(T(i)),e.conversion.for("upcast").add(L(0,i))}}class O 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 f.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 B extends e.Plugin{static get requires(){return[P,O]}static get pluginName(){return"DrupalLinkMedia"}}const{objectFullWidth:N,objectInline:j,objectLeft:R,objectRight:F,objectCenter:$,objectBlockLeft:U,objectBlockRight:H}=e.icons,q={get inline(){return{name:"inline",title:"In line",icon:j,modelElements:["imageInline"],isDefault:!0}},get alignLeft(){return{name:"alignLeft",title:"Left aligned image",icon:R,modelElements:["imageBlock","imageInline"],className:"image-style-align-left"}},get alignBlockLeft(){return{name:"alignBlockLeft",title:"Left aligned image",icon:U,modelElements:["imageBlock"],className:"image-style-block-align-left"}},get alignCenter(){return{name:"alignCenter",title:"Centered image",icon:$,modelElements:["imageBlock"],className:"image-style-align-center"}},get alignRight(){return{name:"alignRight",title:"Right aligned image",icon:F,modelElements:["imageBlock","imageInline"],className:"image-style-align-right"}},get alignBlockRight(){return{name:"alignBlockRight",title:"Right aligned image",icon:H,modelElements:["imageBlock"],className:"image-style-block-align-right"}},get block(){return{name:"block",title:"Centered image",icon:$,modelElements:["imageBlock"],isDefault:!0}},get side(){return{name:"side",title:"Side image",icon:F,modelElements:["imageBlock"],className:"image-style-side"}}},W={full:N,left:U,right:H,center:$,inlineLeft:R,inlineRight:F,inline:j},K=[{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 z(e){(0,a.logWarning)("image-style-configuration-definition-invalid",e)}const Z={normalizeStyles:function(e){return(e.configuredStyles.options||[]).map((e=>function(e){e="string"==typeof e?q[e]?{...q[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}(q[e.name],e);"string"==typeof e.icon&&(e.icon=W[e.icon]||e.icon);return e}(e))).filter((t=>function(e,{isBlockPluginLoaded:t,isInlinePluginLoaded:i}){const{modelElements:n,name:r}=e;if(!(n&&n.length&&r))return z({style:e}),!1;{const r=[t?"imageBlock":null,i?"imageInline":null];if(!n.some((e=>r.includes(e))))return(0,a.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")?[...K]:[]},warnInvalidStyle:z,DEFAULT_OPTIONS:q,DEFAULT_ICONS:W,DEFAULT_DROPDOWN_DEFINITIONS:K};function G(e,t,i){for(const n of t)if(i.checkAttribute(e,n))return!0;return!1}function J(e,t,i){const n=e.getSelectedElement();if(n&&G(n,i,t))return n;let{parent:a}=e.getFirstPosition();for(;a;){if(a.is("element")&&G(a,i,t))return a;a=a.parent}return null}class X extends e.Command{constructor(e,t){super(e),this.styles={},Object.keys(t).forEach((e=>{this.styles[e]=new Map(t[e].map((e=>[e.name,e])))})),this.modelAttributes=[];for(const e of Object.keys(t)){const t=c(e);this.modelAttributes.push(t)}}refresh(){const{editor:e}=this,t=J(e.model.document.selection,e.model.schema,this.modelAttributes);this.isEnabled=!!t,this.isEnabled?this.value=this.getValue(t):this.value=!1}getValue(e){const t={};return Object.keys(this.styles).forEach((i=>{const n=c(i);if(e.hasAttribute(n))t[i]=e.getAttribute(n);else for(const[,e]of this.styles[i])e.isDefault&&(t[i]=e.name)})),t}execute(e={}){const{editor:{model:t}}=this,{value:i,group:n}=e,a=c(n);t.change((e=>{const r=J(t.document.selection,t.schema,this.modelAttributes);!i||this.styles[n].get(i).isDefault?e.removeAttribute(a,r):e.setAttribute(a,i,r)}))}}function Q(e,t){for(const i of t)if(i.name===e)return i}class Y extends e.Plugin{init(){const{editor:t}=this,i=t.config.get("drupalElementStyles");this.normalizedStyles={},Object.keys(i).forEach((t=>{this.normalizedStyles[t]=i[t].map((t=>("string"==typeof t.icon&&e.icons[t.icon]&&(t.icon=e.icons[t.icon]),t.name&&(t.name=`${t.name}`),t))).filter((e=>e.isDefault||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(`${e.attributeValue} drupalElementStyles options must include attributeName and attributeValue.`),!1)))})),this._setupConversion(),t.commands.add("drupalElementStyle",new X(t,this.normalizedStyles))}_setupConversion(){const{editor:e}=this,{schema:t}=e.model;Object.keys(this.normalizedStyles).forEach((i=>{const n=c(i),r=(o=this.normalizedStyles[i],(e,t,i)=>{if(!i.consumable.consume(t.item,e.name))return;const n=Q(t.attributeNewValue,o),a=Q(t.attributeOldValue,o),r=i.mapper.toViewElement(t.item),s=i.writer;a&&("class"===a.attributeName?s.removeClass(a.attributeValue,r):s.removeAttribute(a.attributeName,r)),n&&("class"===n.attributeName?s.addClass(n.attributeValue,r):n.isDefault||s.setAttribute(n.attributeName,n.attributeValue,r))});var o;const s=function(e,t){const i=e.filter((e=>!e.isDefault));return(e,n,r)=>{if(!n.modelRange)return;const o=n.viewItem,s=(0,a.first)(n.modelRange.getItems());if(s&&r.schema.checkAttribute(s,t))for(const e of i)if("class"===e.attributeName)r.consumable.consume(o,{classes:e.attributeValue})&&r.writer.setAttribute(t,e.name,s);else if(r.consumable.consume(o,{attributes:[e.attributeName]}))for(const e of i)e.attributeValue===o.getAttribute(e.attributeName)&&r.writer.setAttribute(t,e.name,s)}}(this.normalizedStyles[i],n);e.editing.downcastDispatcher.on(`attribute:${n}`,r),e.data.downcastDispatcher.on(`attribute:${n}`,r);[...new Set(this.normalizedStyles[i].map((e=>e.modelElements)).flat())].forEach((e=>{t.extend(e,{allowAttributes:n})})),e.data.upcastDispatcher.on("element",s,{priority:"low"})}))}static get pluginName(){return"DrupalElementStyleEditing"}}const ee=e=>e,te=(e,t)=>(e?`${e}: `:"")+t;function ie(e,t){return`drupalElementStyle:${t}:${e}`}class ne extends e.Plugin{static get requires(){return[Y]}init(){const{plugins:e}=this.editor,t=this.editor.config.get("drupalMedia.toolbar")||[],i=e.get("DrupalElementStyleEditing").normalizedStyles;Object.keys(i).forEach((e=>{i[e].forEach((t=>{this._createButton(t,e,i[e])}))}));t.filter(d).filter((e=>{const t=[];if(!e.display)return console.warn("dropdown configuration must include a display key specifying either listDropdown or splitButton."),!1;e.items.includes(e.defaultItem)||console.warn("defaultItem must be part of items in the dropdown configuration.");for(const i of e.items){const e=i.split(":")[1];t.push(e)}return!!t.every((e=>e===t[0]))||(console.warn("dropdown configuration should only contain buttons from one group."),!1)})).forEach((e=>{if(e.items.length>=2){const t=e.name.split(":")[1];switch(e.display){case"splitButton":this._createDropdown(e,i[t]);break;case"listDropdown":this._createListDropdown(e,i[t])}}}))}updateOptionVisibility(e,t,i,n){const{selection:r}=this.editor.model.document,o={};o[n]=e;const s=r?r.getSelectedElement():J(r,this.editor.model.schema,o),l=e.filter((function(e){for(const[t,i]of(0,a.toMap)(e.modelAttributes))if(s&&s.hasAttribute(t))return i.includes(s.getAttribute(t));return!0}));i.hasOwnProperty("model")?l.includes(t)?i.model.set({class:""}):i.model.set({class:"ck-hidden"}):l.includes(t)?i.set({class:""}):i.set({class:"ck-hidden"})}_createDropdown(e,t){const i=this.editor.ui.componentFactory;i.add(e.name,(n=>{let a;const{defaultItem:r,items:o,title:s}=e,l=o.filter((e=>{const i=e.split(":")[1];return t.find((({name:t})=>ie(t,i)===e))})).map((e=>{const t=i.create(e);return e===r&&(a=t),t}));o.length!==l.length&&Z.warnInvalidStyle({dropdown:e});const d=(0,f.createDropdown)(n,f.SplitButtonView),u=d.buttonView;return(0,f.addToolbarToDropdown)(d,l),u.set({label:te(s,a.label),class:null,tooltip:!0}),u.bind("icon").toMany(l,"isOn",((...e)=>{const t=e.findIndex(ee);return t<0?a.icon:l[t].icon})),u.bind("label").toMany(l,"isOn",((...e)=>{const t=e.findIndex(ee);return te(s,t<0?a.label:l[t].label)})),u.bind("isOn").toMany(l,"isOn",((...e)=>e.some(ee))),u.bind("class").toMany(l,"isOn",((...e)=>e.some(ee)?"ck-splitbutton_flatten":null)),u.on("execute",(()=>{l.some((({isOn:e})=>e))?d.isOpen=!d.isOpen:a.fire("execute")})),d.bind("isEnabled").toMany(l,"isEnabled",((...e)=>e.some(ee))),d}))}_createButton(e,t,i){const n=e.name;this.editor.ui.componentFactory.add(ie(n,t),(a=>{const r=this.editor.commands.get("drupalElementStyle"),o=new f.ButtonView(a);return o.set({label:e.title,icon:e.icon,tooltip:!0,isToggleable:!0}),o.bind("isEnabled").to(r,"isEnabled"),o.bind("isOn").to(r,"value",(e=>e&&e[t]===n)),o.on("execute",this._executeCommand.bind(this,n,t)),this.listenTo(this.editor.ui,"update",(()=>{this.updateOptionVisibility(i,e,o,t)})),o}))}getDropdownListItemDefinitions(e,t,i){const n=new a.Collection;return e.forEach((t=>{const a={type:"button",model:new f.ViewModel({group:i,commandValue:t.name,label:t.title,withText:!0,class:""})};n.add(a),this.listenTo(this.editor.ui,"update",(()=>{this.updateOptionVisibility(e,t,a,i)}))})),n}_createListDropdown(e,t){const i=this.editor.ui.componentFactory;i.add(e.name,(n=>{let a;const{defaultItem:r,items:o,title:s,defaultText:l}=e,d=e.name.split(":")[1],u=o.filter((e=>t.find((({name:t})=>ie(t,d)===e)))).map((e=>{const t=i.create(e);return e===r&&(a=t),t}));o.length!==u.length&&Z.warnInvalidStyle({dropdown:e});const c=(0,f.createDropdown)(n,f.DropdownButtonView),m=c.buttonView;m.set({label:te(s,a.label),class:null,tooltip:l,withText:!0});const p=this.editor.commands.get("drupalElementStyle");return m.bind("label").to(p,"value",(e=>{if(e&&e[d])for(const i of t)if(i.name===e[d])return i.title;return l})),c.bind("isOn").to(p),c.bind("isEnabled").to(this),(0,f.addListToDropdown)(c,this.getDropdownListItemDefinitions(t,p,d)),this.listenTo(c,"execute",(e=>{this._executeCommand(e.source.commandValue,e.source.group)})),c}))}_executeCommand(e,t){this.editor.execute("drupalElementStyle",{value:e,group:t}),this.editor.editing.view.focus()}static get pluginName(){return"DrupalElementStyleUi"}}class ae extends e.Plugin{static get requires(){return[Y,ne]}static get pluginName(){return"DrupalElementStyle"}}function re(e){const t=e.getFirstPosition().findAncestor("caption");return t&&r(t.parent)?t:null}function oe(e){for(const t of e.getChildren())if(t&&t.is("element","caption"))return t;return null}class se extends e.Command{refresh(){const e=this.editor.model.document.selection,t=e.getSelectedElement();if(!t)return this.isEnabled=!!s(e,!1),void(this.value=!!re(e));this.isEnabled=r(t,!1),this.isEnabled?this.value=!!oe(t):this.value=!1}execute(e={}){const{focusCaptionOnShow:t}=e;this.editor.model.change((e=>{this.value?this._hideDrupalMediaCaption(e):this._showDrupalMediaCaption(e,t)}))}_showDrupalMediaCaption(e,t){const i=this.editor.model.document.selection,n=this.editor.plugins.get("DrupalMediaCaptionEditing"),a=s(i),r=n._getSavedCaption(a)||e.createElement("caption");e.append(r,a),t&&e.setSelection(r,"in")}_hideDrupalMediaCaption(e){const t=this.editor,i=t.model.document.selection,n=t.plugins.get("DrupalMediaCaptionEditing");let a,r=i.getSelectedElement();r?a=oe(r):(a=re(i),r=s(i)),n._saveCaption(r,a),e.setSelection(r,"on"),e.remove(a)}}class le extends e.Plugin{static get requires(){return[]}static get pluginName(){return"DrupalMediaCaptionEditing"}constructor(e){super(e),this._savedCaptionsMap=new WeakMap}init(){const e=this.editor,t=e.model.schema;t.isRegistered("caption")?t.extend("caption",{allowIn:"drupalMedia"}):t.register("caption",{allowIn:"drupalMedia",allowContentOf:"$block",isLimit:!0}),e.commands.add("toggleMediaCaption",new se(e)),this._setupConversion()}_setupConversion(){const e=this.editor,i=e.editing.view;var n;e.conversion.for("upcast").add(function(e){const t=(t,i,n)=>{const{viewItem:a}=i,{writer:r,consumable:o}=n;if(!i.modelRange||!o.consume(a,{attributes:["data-caption"]}))return;const s=r.createElement("caption"),l=i.modelRange.start.nodeAfter,d=e.data.processor.toView(a.getAttribute("data-caption"));n.consumable.constructor.createFrom(d,n.consumable),n.convertChildren(d,s),r.append(s,l)};return e=>{e.on("element:drupal-media",t,{priority:"low"})}}(e)),e.conversion.for("editingDowncast").elementToElement({model:"caption",view:(e,{writer:n})=>{if(!r(e.parent))return null;const a=n.createEditableElement("figcaption");return a.placeholder=Drupal.t("Enter media caption"),(0,I.enablePlaceholder)({view:i,element:a,keepOnFocus:!0}),(0,t.toWidgetEditable)(a,n)}}),e.editing.mapper.on("modelToViewPosition",(n=i,(e,t)=>{const i=t.modelPosition,a=i.parent;if(!r(a))return;const o=t.mapper.toViewElement(a);t.viewPosition=n.createPositionAt(o,i.offset+1)})),e.conversion.for("dataDowncast").add(function(e){return t=>{t.on("insert:caption",((t,i,n)=>{const{consumable:a,writer:o,mapper:s}=n;if(!r(i.item.parent)||!a.consume(i.item,"insert"))return;const l=e.model.createRangeIn(i.item),d=o.createDocumentFragment();s.bindElements(i.item,d);for(const{item:t}of Array.from(l)){const i={item:t,range:e.model.createRangeOn(t)},a=`insert:${t.name||"$text"}`;e.data.downcastDispatcher.fire(a,i,n);for(const a of t.getAttributeKeys())Object.assign(i,{attributeKey:a,attributeOldValue:null,attributeNewValue:i.item.getAttribute(a)}),e.data.downcastDispatcher.fire(`attribute:${a}`,i,n)}for(const e of o.createRangeIn(d).getItems())s.unbindViewElement(e);s.unbindViewElement(d);const u=e.data.processor.toData(d);if(u){const e=s.toViewElement(i.item.parent);o.setAttribute("data-caption",u,e)}}))}}(e))}_getSavedCaption(e){const t=this._savedCaptionsMap.get(e);return t?I.Element.fromJSON(t):null}_saveCaption(e,t){this._savedCaptionsMap.set(e,t.toJSON())}}class de extends e.Plugin{static get requires(){return[]}static get pluginName(){return"DrupalMediaCaptionUI"}init(){const{editor:t}=this,i=t.editing.view;t.ui.componentFactory.add("toggleDrupalMediaCaption",(n=>{const a=new f.ButtonView(n),r=t.commands.get("toggleMediaCaption");return a.set({label:Drupal.t("Caption media"),icon:e.icons.caption,tooltip:!0,isToggleable:!0}),a.bind("isOn","isEnabled").to(r,"value","isEnabled"),a.bind("label").to(r,"value",(e=>e?Drupal.t("Toggle caption off"):Drupal.t("Toggle caption on"))),this.listenTo(a,"execute",(()=>{t.execute("toggleMediaCaption",{focusCaptionOnShow:!0});const e=re(t.model.document.selection);if(e){const n=t.editing.mapper.toViewElement(e);i.scrollToTheSelection(),i.change((e=>{e.addClass("drupal-media__caption_highlighted",n)}))}t.editing.view.focus()})),a}))}}class ue extends e.Plugin{static get requires(){return[le,de]}static get pluginName(){return"DrupalMediaCaption"}}const ce={DrupalMedia:V,MediaImageTextAlternative:D,MediaImageTextAlternativeEditing:E,MediaImageTextAlternativeUi:A,DrupalLinkMedia:B,DrupalMediaCaption:ue,DrupalElementStyle:ae}})(),n=n.default})())); +\ No newline at end of file +diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptioncommand.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptioncommand.js +index f1268afdab03696d93b1c76fc450f0699a8f8b13..a04133fba7731b36fb0585c3f9ea9ad5ba032883 100644 +--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptioncommand.js ++++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediacaption/drupalmediacaptioncommand.js +@@ -51,7 +51,7 @@ export default class ToggleDrupalMediaCaptionCommand extends Command { + if (!selectedElement) { + // Command should be enabled if `<drupalMedia>` element is part of the + // selection. +- this.isEnabled = !!getClosestSelectedDrupalMediaElement(selection); ++ this.isEnabled = !!getClosestSelectedDrupalMediaElement(selection, false); + // Check if the selection descends from a `<drupalMedia>` element that + // also includes a `<caption>`. + this.value = !!getMediaCaptionFromModelSelection(selection); +@@ -60,7 +60,7 @@ export default class ToggleDrupalMediaCaptionCommand extends Command { + } + + // If single element is selected, check if it's a `<drupalMedia>` element. +- this.isEnabled = isDrupalMedia(selectedElement); ++ this.isEnabled = isDrupalMedia(selectedElement, false); + + if (!this.isEnabled) { + this.value = false; +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 9b9b582482a7f90b25e75d3d32e535a9651e00ba..34aef3fa935b29f02fab048609fd0a83e789dc59 100644 +--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js ++++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js +@@ -210,6 +210,13 @@ export default class DrupalMediaEditing extends Plugin { + inheritAllFrom: '$blockObject', + allowAttributes: Object.keys(this.attrs), + }); ++ ++ schema.register('drupalMediaInline', { ++ inheritAllFrom: '$inlineObject', ++ allowIn: ['$block', '$container', '$text'], ++ allowAttributes: Object.keys(this.attrs), ++ }); ++ + // Register `<drupal-media>` as a block element in the DOM converter. This + // ensures that the DOM converter knows to handle the `<drupal-media>` as a + // block element. +@@ -227,212 +234,228 @@ export default class DrupalMediaEditing extends Plugin { + 'DrupalMediaMetadataRepository', + ); + +- conversion +- .for('upcast') +- .elementToElement({ ++ const viewToModelMap = { ++ 'drupal-media': 'drupalMedia', ++ 'drupal-media-inline': 'drupalMediaInline', ++ }; ++ ++ Object.keys(viewToModelMap).forEach((view) => { ++ const model = viewToModelMap[view]; ++ ++ conversion ++ .for('upcast') ++ .elementToElement({ ++ view: { ++ name: view, ++ }, ++ model, ++ }) ++ .add((dispatcher) => { ++ dispatcher.on( ++ `element:${view}`, ++ (evt, data) => { ++ const [modelElement] = data.modelRange.getItems(); ++ metadataRepository ++ .getMetadata(modelElement) ++ .then((metadata) => { ++ if (!modelElement) { ++ return; ++ } ++ // On upcast, get `drupalMediaIsImage` attribute value from media metadata ++ // repository. ++ this.upcastDrupalMediaIsImage(modelElement); ++ // Enqueue a model change after getting modelElement. ++ this.editor.model.enqueueChange( ++ { isUndoable: false }, ++ (writer) => { ++ writer.setAttribute( ++ 'drupalMediaType', ++ metadata.type, ++ modelElement, ++ ); ++ }, ++ ); ++ }) ++ .catch((e) => { ++ // There isn't any UI indication for errors because this should be ++ // always called after the Drupal Media has been upcast, which would ++ // already display an error in the UI. ++ console.warn(e.toString()); ++ }); ++ }, ++ // This converter needs to have the lowest priority to ensure that the ++ // model element and its attributes have already been converted. It is only used ++ // to gather metadata to make the UI tailored to the specific media entity that ++ // is being dealt with. ++ { priority: 'lowest' }, ++ ); ++ }); ++ ++ conversion.for('dataDowncast').elementToElement({ ++ model, + view: { +- name: 'drupal-media', ++ name: view, + }, +- model: 'drupalMedia', +- }) +- .add((dispatcher) => { +- dispatcher.on( +- 'element:drupal-media', +- (evt, data) => { +- const [modelElement] = data.modelRange.getItems(); +- metadataRepository +- .getMetadata(modelElement) +- .then((metadata) => { +- if (!modelElement) { +- return; +- } +- // On upcast, get `drupalMediaIsImage` attribute value from media metadata +- // repository. +- this.upcastDrupalMediaIsImage(modelElement); +- // Enqueue a model change after getting modelElement. +- this.editor.model.enqueueChange( +- { isUndoable: false }, +- (writer) => { +- writer.setAttribute( +- 'drupalMediaType', +- metadata.type, +- modelElement, +- ); +- }, +- ); +- }) +- .catch((e) => { +- // There isn't any UI indication for errors because this should be +- // always called after the Drupal Media has been upcast, which would +- // already display an error in the UI. +- console.warn(e.toString()); +- }); +- }, +- // This converter needs to have the lowest priority to ensure that the +- // model element and its attributes have already been converted. It is only used +- // to gather metadata to make the UI tailored to the specific media entity that +- // is being dealt with. +- { priority: 'lowest' }, +- ); + }); +- +- conversion.for('dataDowncast').elementToElement({ +- model: 'drupalMedia', +- view: { +- name: 'drupal-media', +- }, +- }); +- conversion +- .for('editingDowncast') +- .elementToElement({ +- model: 'drupalMedia', +- view: (modelElement, { writer }) => { +- const container = writer.createContainerElement('figure', { +- class: 'drupal-media', +- }); +- if (!this.previewUrl) { +- // If preview URL isn't available, insert empty preview element +- // which indicates that preview couldn't be loaded. +- const mediaPreview = writer.createRawElement('div', { +- 'data-drupal-media-preview': 'unavailable', ++ conversion ++ .for('editingDowncast') ++ .elementToElement({ ++ model, ++ view: (modelElement, { writer }) => { ++ const container = writer.createContainerElement('figure', { ++ class: ++ view === 'drupal-media-inline' ? `drupal-media ${view}` : view, + }); +- writer.insert(writer.createPositionAt(container, 0), mediaPreview); +- } +- writer.setCustomProperty('drupalMedia', true, container); +- +- return toWidget(container, writer, { +- label: Drupal.t('Media widget'), +- }); +- }, +- }) +- .add((dispatcher) => { +- const converter = (event, data, conversionApi) => { +- const viewWriter = conversionApi.writer; +- const modelElement = data.item; +- const container = conversionApi.mapper.toViewElement(data.item); +- +- // Search for preview container recursively from its children because +- // the preview container could be wrapped with an element such as +- // `<a>`. +- let media = getPreviewContainer(container.getChildren()); +- +- // Use pre-existing media preview container if one exists. If the +- // preview element doesn't exist, create a new element. +- if (media) { +- // Stop processing if media preview is unavailable or a preview is +- // already loading. +- if (media.getAttribute('data-drupal-media-preview') !== 'ready') { +- return; ++ if (!this.previewUrl) { ++ // If preview URL isn't available, insert empty preview element ++ // which indicates that preview couldn't be loaded. ++ const mediaPreview = writer.createRawElement('div', { ++ 'data-drupal-media-preview': 'unavailable', ++ }); ++ writer.insert( ++ writer.createPositionAt(container, 0), ++ mediaPreview, ++ ); + } ++ writer.setCustomProperty(model, true, container); + +- // Preview was ready meaning that a new preview can be loaded. +- // "Change the attribute to loading to prepare for the loading of +- // the updated preview. Preview is kept intact so that it remains +- // interactable in the UI until the new preview has been rendered. +- viewWriter.setAttribute( +- 'data-drupal-media-preview', +- 'loading', +- media, +- ); +- } else { +- media = viewWriter.createRawElement('div', { +- 'data-drupal-media-preview': 'loading', ++ return toWidget(container, writer, { ++ label: Drupal.t('Media widget'), + }); +- viewWriter.insert(viewWriter.createPositionAt(container, 0), media); +- } +- +- this._fetchPreview(modelElement).then(({ label, preview }) => { +- if (!media) { +- // Nothing to do if associated preview wrapped no longer exist. +- return; +- } +- // CKEditor 5 doesn't support async view conversion. Therefore, once +- // the promise is fulfilled, the editing view needs to be modified +- // manually. +- this.editor.editing.view.change((writer) => { +- const mediaPreview = writer.createRawElement( +- 'div', +- { 'data-drupal-media-preview': 'ready', 'aria-label': label }, +- (domElement) => { +- domElement.innerHTML = preview; +- }, ++ }, ++ }) ++ .add((dispatcher) => { ++ const converter = (event, data, conversionApi) => { ++ const viewWriter = conversionApi.writer; ++ const modelElement = data.item; ++ const container = conversionApi.mapper.toViewElement(data.item); ++ ++ // Search for preview container recursively from its children because ++ // the preview container could be wrapped with an element such as ++ // `<a>`. ++ let media = getPreviewContainer(container.getChildren()); ++ ++ // Use pre-existing media preview container if one exists. If the ++ // preview element doesn't exist, create a new element. ++ if (media) { ++ // Stop processing if media preview is unavailable or a preview is ++ // already loading. ++ if (media.getAttribute('data-drupal-media-preview') !== 'ready') { ++ return; ++ } ++ ++ // Preview was ready meaning that a new preview can be loaded. ++ // "Change the attribute to loading to prepare for the loading of ++ // the updated preview. Preview is kept intact so that it remains ++ // interactable in the UI until the new preview has been rendered. ++ viewWriter.setAttribute( ++ 'data-drupal-media-preview', ++ 'loading', ++ media, + ); +- // Insert the new preview before the previous preview element to +- // ensure that the location remains same even if it is wrapped +- // with another element. +- writer.insert(writer.createPositionBefore(media), mediaPreview); +- writer.remove(media); ++ } else { ++ media = viewWriter.createRawElement('div', { ++ 'data-drupal-media-preview': 'loading', ++ }); ++ viewWriter.insert( ++ viewWriter.createPositionAt(container, 0), ++ media, ++ ); ++ } ++ ++ this._fetchPreview(modelElement).then(({ label, preview }) => { ++ if (!media) { ++ // Nothing to do if associated preview wrapped no longer exist. ++ return; ++ } ++ // CKEditor 5 doesn't support async view conversion. Therefore, once ++ // the promise is fulfilled, the editing view needs to be modified ++ // manually. ++ this.editor.editing.view.change((writer) => { ++ const mediaPreview = writer.createRawElement( ++ 'div', ++ { 'data-drupal-media-preview': 'ready', 'aria-label': label }, ++ (domElement) => { ++ domElement.innerHTML = preview; ++ }, ++ ); ++ // Insert the new preview before the previous preview element to ++ // ensure that the location remains same even if it is wrapped ++ // with another element. ++ writer.insert(writer.createPositionBefore(media), mediaPreview); ++ writer.remove(media); ++ }); + }); ++ }; ++ ++ // List all attributes that should trigger re-rendering of the ++ // preview. ++ this.converterAttributes.forEach((attribute) => { ++ dispatcher.on(`attribute:${attribute}:${model}`, converter); + }); +- }; + +- // List all attributes that should trigger re-rendering of the +- // preview. +- this.converterAttributes.forEach((attribute) => { +- dispatcher.on(`attribute:${attribute}:drupalMedia`, converter); ++ return dispatcher; + }); + +- return dispatcher; +- }); ++ conversion.for('editingDowncast').add((dispatcher) => { ++ dispatcher.on( ++ `attribute:drupalElementStyleAlign:${model}`, ++ (evt, data, conversionApi) => { ++ const alignMapping = { ++ // This is a map of CSS classes representing Drupal element styles for alignments. ++ left: 'drupal-media-style-align-left', ++ right: 'drupal-media-style-align-right', ++ center: '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, ++ ); ++ } + +- conversion.for('editingDowncast').add((dispatcher) => { +- dispatcher.on( +- 'attribute:drupalElementStyleAlign:drupalMedia', +- (evt, data, conversionApi) => { +- const alignMapping = { +- // This is a map of CSS classes representing Drupal element styles for alignments. +- left: 'drupal-media-style-align-left', +- right: 'drupal-media-style-align-right', +- center: '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], ++ // 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, + ); +- } +- +- // 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) => { +- const attributeMapping = { +- model: { +- key: modelKey, +- name: 'drupalMedia', +- }, +- view: { +- name: 'drupal-media', +- key: this.attrs[modelKey], +- }, +- }; +- // Attributes should be rendered only in dataDowncast to avoid having +- // unfiltered data-attributes on the Drupal Media widget. +- conversion.for('dataDowncast').attributeToAttribute(attributeMapping); +- conversion.for('upcast').attributeToAttribute(attributeMapping); ++ // Set attributeToAttribute conversion for all supported attributes. ++ Object.keys(this.attrs).forEach((modelKey) => { ++ const attributeMapping = { ++ model: { ++ key: modelKey, ++ name: model, ++ }, ++ view: { ++ name: view, ++ key: this.attrs[modelKey], ++ }, ++ }; ++ // Attributes should be rendered only in dataDowncast to avoid having ++ // unfiltered data-attributes on the Drupal Media widget. ++ conversion.for('dataDowncast').attributeToAttribute(attributeMapping); ++ conversion.for('upcast').attributeToAttribute(attributeMapping); ++ }); + }); + } + +diff --git a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediageneralhtmlsupport.js b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediageneralhtmlsupport.js +index 66ca7dbee62ee9bb97f9bed0f95a2ec718903f18..d65914e909bb479bf859a0bec9f0c1c049f6fdfc 100644 +--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediageneralhtmlsupport.js ++++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediageneralhtmlsupport.js +@@ -5,18 +5,22 @@ import { setViewAttributes } from '@ckeditor/ckeditor5-html-support/src/utils'; + + /** + * View-to-model conversion helper for Drupal Media. +- * Used for preserving allowed attributes on the Drupal Media model. ++ * Used for preserving allowed attributes on the Drupal Media models. + * ++ * @param {string} model ++ * The model name (DrupalMedia or DrupalMediaInline). ++ * @param {string} view ++ * The view name (drupal-media or drupal-media-inline). + * @param {module:html-support/datafilter~DataFilter} dataFilter + * The General HTML support data filter. + * + * @return {function} + * Function that adds an event listener to upcastDispatcher. + */ +-function viewToModelDrupalMediaAttributeConverter(dataFilter) { ++function viewToModelDrupalMediaAttributeConverter(dataFilter, model, view) { + return (dispatcher) => { + dispatcher.on( +- 'element:drupal-media', ++ `element:${view}`, + (evt, data, conversionApi) => { + function preserveElementAttributes(viewElement, attributeName) { + const viewAttributes = dataFilter.processViewAttributes( +@@ -96,18 +100,21 @@ function modelToDataAttributeConverter(evt, data, conversionApi) { + /** + * Model to editing view attribute converter. + * ++ * @param {string} model ++ * The model name (DrupalMedia or DrupalMediaInline). ++ * + * @return {function} + * A function that adds an event listener to downcastDispatcher. + */ +-function modelToEditingViewAttributeConverter() { ++function modelToEditingViewAttributeConverter(model) { + return (dispatcher) => { + dispatcher.on( +- 'attribute:linkHref:drupalMedia', ++ `attribute:linkHref:${model}`, + (evt, data, conversionApi) => { + if ( + !conversionApi.consumable.consume( + data.item, +- 'attribute:htmlLinkAttributes:drupalMedia', ++ `attribute:htmlLinkAttributes:${model}`, + ) + ) { + return; +@@ -134,18 +141,21 @@ function modelToEditingViewAttributeConverter() { + /** + * Model to data view attribute converter. + * ++ * @param {string} model ++ * The model name (DrupalMedia or DrupalMediaInline). ++ * + * @return {function} + * Function that adds an event listener to downcastDispatcher. + */ +-function modelToDataViewAttributeConverter() { ++function modelToDataViewAttributeConverter(model) { + return (dispatcher) => { + dispatcher.on( +- 'attribute:linkHref:drupalMedia', ++ `attribute:linkHref:${model}`, + (evt, data, conversionApi) => { + if ( + !conversionApi.consumable.consume( + data.item, +- 'attribute:htmlLinkAttributes:drupalMedia', ++ `attribute:htmlLinkAttributes:${model}`, + ) + ) { + return; +@@ -163,7 +173,7 @@ function modelToDataViewAttributeConverter() { + ); + + dispatcher.on( +- 'attribute:htmlAttributes:drupalMedia', ++ `attribute:htmlAttributes:${model}`, + modelToDataAttributeConverter, + { priority: 'low' }, + ); +@@ -204,32 +214,45 @@ export default class DrupalMediaGeneralHtmlSupport extends Plugin { + const dataFilter = this.editor.plugins.get('DataFilter'); + const dataSchema = this.editor.plugins.get('DataSchema'); + +- // This needs to be initialized in ::constructor() to ensure this runs +- // before the General HTML Support has been initialized. +- // @see module:html-support/generalhtmlsupport~GeneralHtmlSupport +- dataSchema.registerBlockElement({ +- model: 'drupalMedia', +- view: 'drupal-media', +- }); ++ const viewToModelMap = { ++ 'drupal-media': 'drupalMedia', ++ 'drupal-media-inline': 'drupalMediaInline', ++ }; + +- dataFilter.on('register:drupal-media', (evt, definition) => { +- if (definition.model !== 'drupalMedia') { +- return; +- } ++ Object.keys(viewToModelMap).forEach((view) => { ++ const model = viewToModelMap[view]; + +- schema.extend('drupalMedia', { +- allowAttributes: ['htmlLinkAttributes', 'htmlAttributes'], ++ // This needs to be initialized in ::constructor() to ensure this runs ++ // before the General HTML Support has been initialized. ++ // @see module:html-support/generalhtmlsupport~GeneralHtmlSupport ++ dataSchema.registerBlockElement({ ++ model, ++ view, + }); + +- conversion +- .for('upcast') +- .add(viewToModelDrupalMediaAttributeConverter(dataFilter)); +- conversion +- .for('editingDowncast') +- .add(modelToEditingViewAttributeConverter()); +- conversion.for('dataDowncast').add(modelToDataViewAttributeConverter()); ++ dataFilter.on(`register:${view}`, (evt, definition) => { ++ if (definition.model !== model) { ++ return; ++ } + +- evt.stop(); ++ schema.extend(model, { ++ allowAttributes: ['htmlLinkAttributes', 'htmlAttributes'], ++ }); ++ ++ conversion ++ .for('upcast') ++ .add( ++ viewToModelDrupalMediaAttributeConverter(dataFilter, model, view), ++ ); ++ conversion ++ .for('editingDowncast') ++ .add(modelToEditingViewAttributeConverter(model)); ++ conversion ++ .for('dataDowncast') ++ .add(modelToDataViewAttributeConverter(model)); ++ ++ evt.stop(); ++ }); + }); + } + +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 bf9c6451e5330880b689017e3fcf8ade05f0f9f7..5cd6337426eb1cd499b7893dba0ccd7a9ddb921c 100644 +--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/insertdrupalmedia.js ++++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/insertdrupalmedia.js +@@ -1,15 +1,42 @@ + /* eslint-disable import/no-extraneous-dependencies */ + // cspell:ignore insertdrupalmediacommand + import { Command } from 'ckeditor5/src/core'; ++import { first } from 'ckeditor5/src/utils'; + import { groupNameToModelAttributeKey } from './utils'; + + /** + * @module drupalMedia/insertdrupalmediacommand + */ + +-function createDrupalMedia(writer, attributes) { +- const drupalMedia = writer.createElement('drupalMedia', attributes); +- return drupalMedia; ++function createDrupalMedia(writer, attributes, model) { ++ return writer.createElement(model, attributes); ++} ++ ++function determineImageTypeForInsertionAtSelection(schema, selection) { ++ const firstBlock = first(selection.getSelectedBlocks()); ++ // Insert a block media if the selection is not in/on block elements or it's ++ // on a block widget. ++ if (!firstBlock || schema.isObject(firstBlock)) { ++ return 'drupalMedia'; ++ } ++ // A block image should also be inserted into an empty block element ++ // (that is not an empty list item so the list won't get split). ++ if (firstBlock.isEmpty && firstBlock.name !== 'listItem') { ++ return 'drupalMedia'; ++ } ++ // Otherwise insert an inline media. ++ return 'drupalMediaInline'; ++} ++ ++function determineImageTypeForInsertion(editor, selectable) { ++ const schema = editor.model.schema; ++ // Try to replace the selected widget (e.g. another image). ++ if (selectable.is('selection')) { ++ return determineImageTypeForInsertionAtSelection(schema, selectable); ++ } ++ return schema.checkChild(selectable, 'drupalMediaInline') ++ ? 'drupalMediaInline' ++ : 'drupalMedia'; + } + + /** +@@ -83,9 +110,14 @@ export default class InsertDrupalMediaCommand extends Command { + } + } + ++ const insertedModel = determineImageTypeForInsertion( ++ this.editor, ++ this.editor.model.document.selection, ++ ); ++ + this.editor.model.change((writer) => { + this.editor.model.insertObject( +- createDrupalMedia(writer, modelAttributes), ++ createDrupalMedia(writer, modelAttributes, insertedModel), + ); + }); + } +@@ -93,9 +125,10 @@ export default class InsertDrupalMediaCommand extends Command { + refresh() { + const model = this.editor.model; + const selection = model.document.selection; ++ const mediaModel = determineImageTypeForInsertion(this.editor, selection); + const allowedIn = model.schema.findAllowedParent( + selection.getFirstPosition(), +- 'drupalMedia', ++ mediaModel, + ); + this.isEnabled = allowedIn !== null; + } +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 afd77f4cd9828c77c226ddda92437775f3cd8686..180fdc3f3b2b01944f749c7788a9fed36f4e14d6 100644 +--- a/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/utils.js ++++ b/core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/utils.js +@@ -7,12 +7,25 @@ import { isWidget } from 'ckeditor5/src/widget'; + * + * @param {module:engine/model/element~Element} modelElement + * The model element to be checked. ++ * @param {boolean} includeInline ++ * Optional. Whether to consider <drupal-media-inline> elements as ++ * media. Defaults to true. + * @return {boolean} + * A boolean indicating if the element is a drupalMedia element. + * + * @private + */ +-export function isDrupalMedia(modelElement) { ++export function isDrupalMedia(modelElement, includeInline) { ++ if (typeof includeInline === 'undefined') { ++ includeInline = true; ++ } ++ if (includeInline) { ++ return ( ++ !!modelElement && ++ (modelElement.is('element', 'drupalMedia') || ++ modelElement.is('element', 'drupalMediaInline')) ++ ); ++ } + return !!modelElement && modelElement.is('element', 'drupalMedia'); + } + +@@ -21,12 +34,25 @@ export function isDrupalMedia(modelElement) { + * + * @param {module:engine/view/element~Element} viewElement + * The view element. ++ * @param {boolean} includeInline ++ * Optional. Whether to consider <drupal-media-inline> elements as ++ * media. Defaults to true. + * @return {boolean} + * A boolean indicating if the element is a <drupal-media> element. + * + * @private + */ +-export function isDrupalMediaWidget(viewElement) { ++export function isDrupalMediaWidget(viewElement, includeInline) { ++ if (typeof includeInline === 'undefined') { ++ includeInline = true; ++ } ++ if (includeInline) { ++ return ( ++ isWidget(viewElement) && ++ (!!viewElement.getCustomProperty('drupalMedia') || ++ !!viewElement.getCustomProperty('drupalMediaInline')) ++ ); ++ } + return ( + isWidget(viewElement) && !!viewElement.getCustomProperty('drupalMedia') + ); +@@ -37,6 +63,9 @@ export function isDrupalMediaWidget(viewElement) { + * + * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection + * The current selection. ++ * @param {boolean} includeInline ++ * Optional. Whether to consider <drupal-media-inline> elements as ++ * media. Defaults to true. + * @return {module:engine/model/element~Element|null} + * The `drupalMedia` element which could be either the current selected an + * ancestor of the selection. Returns null if the selection has no Drupal +@@ -44,12 +73,17 @@ export function isDrupalMediaWidget(viewElement) { + * + * @private + */ +-export function getClosestSelectedDrupalMediaElement(selection) { ++export function getClosestSelectedDrupalMediaElement(selection, includeInline) { ++ if (typeof includeInline === 'undefined') { ++ includeInline = true; ++ } + const selectedElement = selection.getSelectedElement(); + +- return isDrupalMedia(selectedElement) ++ return isDrupalMedia(selectedElement, includeInline) + ? selectedElement +- : selection.getFirstPosition().findAncestor('drupalMedia'); ++ : selection ++ .getFirstPosition() ++ .findAncestor(includeInline ? /drupalMedia(Inline)?/ : /drupalMedia/); + } + + /** +@@ -57,14 +91,20 @@ export function getClosestSelectedDrupalMediaElement(selection) { + * + * @param {module:engine/model/selection~Selection} selection + * The current selection. ++ * @param {boolean} includeInline ++ * Optional. Whether to consider <drupal-media-inline> elements as ++ * media. Defaults to true. + * @return {module:engine/view/element~Element|null} + * The currently selected Drupal Media widget or null. + * + * @private + */ +-export function getClosestSelectedDrupalMediaWidget(selection) { ++export function getClosestSelectedDrupalMediaWidget(selection, includeInline) { ++ if (typeof includeInline === 'undefined') { ++ includeInline = true; ++ } + const viewElement = selection.getSelectedElement(); +- if (viewElement && isDrupalMediaWidget(viewElement)) { ++ if (viewElement && isDrupalMediaWidget(viewElement, includeInline)) { + return viewElement; + } + +@@ -76,7 +116,7 @@ export function getClosestSelectedDrupalMediaWidget(selection) { + let parent = selection.getFirstPosition().parent; + + while (parent) { +- if (parent.is('element') && isDrupalMediaWidget(parent)) { ++ if (parent.is('element') && isDrupalMediaWidget(parent, includeInline)) { + return parent; + } + +diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Media.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Media.php +index c6b9f345a98ab15a08e76a86bcf5d40cda93788a..dbaadb59a293caa2088cc32a101f67425420a366 100644 +--- a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Media.php ++++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Media.php +@@ -121,7 +121,7 @@ private function configureViewModes(EditorInterface $editor) { + 'title' => $all_view_modes[$view_mode], + 'attributeName' => 'data-view-mode', + 'attributeValue' => $view_mode, +- 'modelElements' => ['drupalMedia'], ++ 'modelElements' => ['drupalMedia', 'drupalMediaInline'], + 'modelAttributes' => [ + 'drupalMediaType' => array_keys($media_bundles), + ], +@@ -133,7 +133,7 @@ private function configureViewModes(EditorInterface $editor) { + 'title' => $all_view_modes[$view_mode], + 'attributeName' => 'data-view-mode', + 'attributeValue' => $view_mode, +- 'modelElements' => ['drupalMedia'], ++ 'modelElements' => ['drupalMedia', 'drupalMediaInline'], + 'modelAttributes' => [ + 'drupalMediaType' => $specific_bundles, + ], +@@ -196,7 +196,7 @@ public function getElementsSubset(): array { + $subset = $this->getPluginDefinition()->getElements(); + $view_mode_override_enabled = $this->getConfiguration()['allow_view_mode_override']; + if (!$view_mode_override_enabled) { +- $subset = array_diff($subset, ['<drupal-media data-view-mode>']); ++ $subset = array_diff($subset, ['<drupal-media data-view-mode>', '<drupal-media-inline data-view-mode>']); + } + return $subset; + } +diff --git a/core/modules/ckeditor5/templates/field--ckeditor-inline.html.twig b/core/modules/ckeditor5/templates/field--ckeditor-inline.html.twig +new file mode 100644 +index 0000000000000000000000000000000000000000..7f0ae06f8369db15ea4b70771cacfc9a61eff771 +--- /dev/null ++++ b/core/modules/ckeditor5/templates/field--ckeditor-inline.html.twig +@@ -0,0 +1,72 @@ ++{# ++/** ++ * @file ++ * Default theme implementation for a field. ++ * ++ * To override output, copy the "field.html.twig" from the templates directory ++ * to your theme's directory and customize it, just like customizing other ++ * Drupal templates such as page.html.twig or node.html.twig. ++ * ++ * Instead of overriding the theming for all fields, you can also just override ++ * theming for a subset of fields using ++ * @link themeable Theme hook suggestions. @endlink For example, ++ * here are some theme hook suggestions that can be used for a field_foo field ++ * on an article node type: ++ * - field--node--field-foo--article.html.twig ++ * - field--node--field-foo.html.twig ++ * - field--node--article.html.twig ++ * - field--field-foo.html.twig ++ * - field--text-with-summary.html.twig ++ * - field.html.twig ++ * ++ * Available variables: ++ * - attributes: HTML attributes for the containing element. ++ * - label_hidden: Whether to show the field label or not. ++ * - title_attributes: HTML attributes for the title. ++ * - label: The label for the field. ++ * - multiple: TRUE if a field can contain multiple items. ++ * - items: List of all the field items. Each item contains: ++ * - attributes: List of HTML attributes for each item. ++ * - content: The field item's content. ++ * - entity_type: The entity type to which the field belongs. ++ * - field_name: The name of the field. ++ * - field_type: The type of the field. ++ * - label_display: The display settings for the label. ++ * ++ * @see template_preprocess_field() ++ * ++ * @ingroup themeable ++ */ ++#} ++{% ++ set title_classes = [ ++ label_display == 'visually_hidden' ? 'visually-hidden', ++ ] ++%} ++ ++{% if label_hidden %} ++ {% if multiple %} ++ <span{{ attributes }}> ++ {% for item in items %} ++ <span{{ item.attributes }}>{{ item.content }}</span> ++ {% endfor %} ++ </span> ++ {% else %} ++ {% for item in items %} ++ <span{{ attributes }}>{{ item.content }}</span> ++ {% endfor %} ++ {% endif %} ++{% else %} ++ <span{{ attributes }}> ++ <span{{ title_attributes.addClass(title_classes) }}>{{ label }}</span> ++ {% if multiple %} ++ <span> ++ {% endif %} ++ {% for item in items %} ++ <span{{ item.attributes }}>{{ item.content }}</span> ++ {% endfor %} ++ {% if multiple %} ++ </span> ++ {% endif %} ++ </span> ++{% endif %} +diff --git a/core/modules/ckeditor5/templates/media--ckeditor-inline-embed.html.twig b/core/modules/ckeditor5/templates/media--ckeditor-inline-embed.html.twig +new file mode 100644 +index 0000000000000000000000000000000000000000..18a903f24544a90cae12c4a7d2008dd6b340fba5 +--- /dev/null ++++ b/core/modules/ckeditor5/templates/media--ckeditor-inline-embed.html.twig +@@ -0,0 +1,36 @@ ++{# ++/** ++ * @file ++ * Default theme implementation to present a media item. ++ * ++ * Available variables: ++ * - media: The media item, with limited access to object properties and ++ * methods. Only method names starting with "get", "has", or "is" and ++ * a few common methods such as "id", "label", and "bundle" are available. ++ * For example: ++ * - entity.getEntityTypeId() will return the entity type ID. ++ * - entity.hasField('field_example') returns TRUE if the entity includes ++ * field_example. (This does not indicate the presence of a value in this ++ * field.) ++ * Calling other methods, such as entity.delete(), will result in ++ * an exception. ++ * See \Drupal\Core\Entity\EntityInterface for a full list of methods. ++ * - name: Name of the media item. ++ * - content: Media content. ++ * - title_prefix: Additional output populated by modules, intended to be ++ * displayed in front of the main title tag that appears in the template. ++ * - title_suffix: Additional output populated by modules, intended to be ++ * displayed after the main title tag that appears in the template. ++ * - view_mode: View mode; for example, "teaser" or "full". ++ * - attributes: HTML attributes for the containing element. ++ * - title_attributes: Same as attributes, except applied to the main title ++ * tag that appears in the template. ++ * ++ * @see template_preprocess_media() ++ * ++ * @ingroup themeable ++ */ ++#} ++<span{{ attributes }}> ++ {{ content }} ++</span> +diff --git a/core/modules/ckeditor5/tests/src/Functional/MediaEntityMetadataApiTest.php b/core/modules/ckeditor5/tests/src/Functional/MediaEntityMetadataApiTest.php +index 8df587a4373f2e54f6e0f54f13652330f59773e1..c9ae714e1910ce4d41c91159120ab15116989fea 100644 +--- a/core/modules/ckeditor5/tests/src/Functional/MediaEntityMetadataApiTest.php ++++ b/core/modules/ckeditor5/tests/src/Functional/MediaEntityMetadataApiTest.php +@@ -115,7 +115,7 @@ protected function setUp(): void { + 'status' => TRUE, + 'weight' => -10, + 'settings' => [ +- 'allowed_html' => "<p> <br> <drupal-media data-entity-type data-entity-uuid data-view-mode alt>", ++ 'allowed_html' => "<p> <br> <drupal-media data-entity-type data-entity-uuid data-view-mode alt> <drupal-media-inline data-entity-type data-entity-uuid data-view-mode alt>", + 'filter_html_help' => TRUE, + 'filter_html_nofollow' => TRUE, + ], +diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php +index 65db1989a6497068867f2b4c9e3494c73c2351ba..40ed1bead31bc854920bd02a5ff206efe695bb58 100644 +--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php ++++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php +@@ -378,8 +378,8 @@ public function testMediaElementAllowedTags() { + $page->checkField('filters[media_embed][settings][allowed_view_modes][view_mode_1]'); + $page->checkField('filters[media_embed][settings][allowed_view_modes][view_mode_2]'); + +- $allowed_with_media = $this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt data-view-mode>'; +- $allowed_with_media_without_view_mode = $this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt>'; ++ $allowed_with_media = $this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt data-view-mode> <drupal-media-inline data-entity-type data-entity-uuid alt data-view-mode>'; ++ $allowed_with_media_without_view_mode = $this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt> <drupal-media-inline data-entity-type data-entity-uuid alt>'; + $page->clickLink('Media'); + $this->assertTrue($page->hasUncheckedField('editor[settings][plugins][media_media][allow_view_mode_override]')); + $this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $allowed_with_media_without_view_mode); +@@ -403,7 +403,7 @@ public function testMediaElementAllowedTags() { + // filter_align is enabled. + $page->checkField('filters[filter_align][status]'); + $assert_session->assertExpectedAjaxRequest(1); +- $this->assertEquals($this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt data-view-mode data-align>', $allowed_html_field->getValue()); ++ $this->assertEquals($this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt data-view-mode data-align> <drupal-media-inline data-entity-type data-entity-uuid alt data-view-mode data-align>', $allowed_html_field->getValue()); + + // Disable media embed. + $this->assertTrue($page->hasCheckedField('filters[media_embed][status]')); +diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLinkabilityTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLinkabilityTest.php +index 209b1de962c1b7f071a8cb707c6ffee04a949f81..4427a53661d33cca9a3357d1f83a267b75b5f34e 100644 +--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLinkabilityTest.php ++++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLinkabilityTest.php +@@ -41,7 +41,7 @@ public function testLinkedMediaArbitraryHtml(bool $unrestricted): void { + $filter_format->setFilterConfig('filter_html', [ + 'status' => TRUE, + 'settings' => [ +- 'allowed_html' => '<p> <br> <strong> <em> <a href data-foo> <drupal-media data-entity-type data-entity-uuid data-align data-caption alt data-view-mode> <div data-bar>', ++ 'allowed_html' => '<p> <br> <strong> <em> <a href data-foo> <drupal-media data-entity-type data-entity-uuid data-align data-caption alt data-view-mode> <div data-bar> <drupal-media-inline data-entity-type data-entity-uuid alt data-view-mode data-caption data-align>', + ], + ]); + } +diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php +index c9aca401b65cb2cc8de02b87dde466b7d4dcd7a4..6e71a6113f0fd9fcdecfa4aa971a3f22ae388b83 100644 +--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php ++++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php +@@ -98,7 +98,7 @@ public function testMediaSplitList() { + $filter_format->setFilterConfig('filter_html', [ + 'status' => TRUE, + 'settings' => [ +- 'allowed_html' => '<p> <br> <strong> <em> <a href> <drupal-media data-entity-type data-entity-uuid data-align data-caption alt data-view-mode> <ol> <ul> <li>', ++ 'allowed_html' => '<p> <br> <strong> <em> <a href> <drupal-media data-entity-type data-entity-uuid data-align data-caption alt data-view-mode> <drupal-media-inline data-entity-type data-entity-uuid data-align data-caption alt data-view-mode> <ol> <ul> <li>', + ], + ]); + $filter_format->save(); +@@ -153,7 +153,7 @@ public function testMediaArbitraryHtml() { + $filter_format->setFilterConfig('filter_html', [ + 'status' => TRUE, + 'settings' => [ +- 'allowed_html' => '<p> <br> <strong> <em> <a href> <drupal-media data-entity-type data-entity-uuid data-align data-caption alt data-foo data-view-mode> <div data-bar>', ++ 'allowed_html' => '<p> <br> <strong> <em> <a href> <drupal-media data-entity-type data-entity-uuid data-align data-caption alt data-foo data-view-mode> <drupal-media-inline data-entity-type data-entity-uuid data-align data-caption alt data-view-mode> <div data-bar>', + ], + ]); + $filter_format->save(); +@@ -688,7 +688,7 @@ public function testDrupalMediaStyleWithClass() { + $filter_format->setFilterConfig('filter_html', [ + 'status' => TRUE, + 'settings' => [ +- 'allowed_html' => '<p> <br> <h1 class> <div class> <section class> <drupal-media data-entity-type data-entity-uuid data-align data-caption data-view-mode alt class="layercake-side">', ++ 'allowed_html' => '<p> <br> <h1 class> <div class> <section class> <drupal-media data-entity-type data-entity-uuid data-align data-caption data-view-mode alt class="layercake-side"> <drupal-media-inline data-entity-type data-entity-uuid data-align data-caption data-view-mode alt>', + ], + ]); + $filter_format->save(); +@@ -1004,6 +1004,56 @@ public function testViewMode(bool $with_alignment) { + $this->assertNotEmpty($this->getBalloonButton('View Mode 4')); + } + ++ /** ++ * Tests inline embedding. ++ */ ++ public function testInlineMedia() { ++ // Reconfigure the text format to suit our needs. ++ /** @var \Drupal\filter\FilterFormatInterface $format */ ++ $format = FilterFormat::load($this->host->body->format); ++ $filter_html = $format->get('filters')['filter_html']; ++ $filter_html['settings']['allowed_html'] = $filter_html['settings']['allowed_html'] . '<ul> <li>'; ++ $format->setFilterConfig('filter_html', $filter_html); ++ $format->save(); ++ ++ // Setup the test content containing inline and normal media in the editor. ++ $assert_session = $this->assertSession(); ++ $original_value = $this->host->body->value; ++ $inline_value = \str_replace('drupal-media', 'drupal-media-inline', $original_value); ++ // @todo Captions on inline media are not supported yet. ++ $inline_value = \str_replace('data-caption="baz"', '', $inline_value); ++ $this->host->body->value = <<<END ++ <div> ++ $original_value ++ <p>This is a paragraph $inline_value with an inline media</p> ++ <ul> ++ <li>This is a list item $inline_value containing an inline media</li> ++ </ul> ++ </div> ++END; ++ $this->host->save(); ++ $this->drupalGet($this->host->toUrl('edit-form')); ++ ++ // Wait until media previews are fetched. ++ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-widget.drupal-media-inline')); ++ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-widget.drupal-media')); ++ ++ // Confirm that the inline media is contained as phrasing content inside ++ // flow content. ++ $assert_session->elementExists('css', 'p > .drupal-media-inline'); ++ $assert_session->elementExists('css', 'ul > li > .drupal-media-inline'); ++ // Also ensure that a media not embedded inside flow content is still ++ // rendered not using the inline templates. ++ $assert_session->elementExists('css', 'div > .drupal-media'); ++ ++ // Verify the rendered inline media is rendered with markup that is using ++ // the dedicated inline media and field templates. ++ $this->drupalGet($this->host->toUrl('canonical')); ++ $assert_session->elementExists('css', 'p > span.media-embedded-inline span img[src*="image-test.png"]'); ++ $assert_session->elementExists('css', 'ul > li > span.media-embedded-inline span img[src*="image-test.png"]'); ++ $assert_session->elementExists('css', 'div > figure.caption-drupal-media article.media img[src*="image-test.png"]'); ++ } ++ + /** + * For testing view modes in different scenarios. + */ +diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTestBase.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTestBase.php +index 3384ec7fa483a06b2b41e0209a1775c72327963d..0d8faf4931463d30bb8c760a76bc8f19bfdb7c45 100644 +--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTestBase.php ++++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTestBase.php +@@ -101,7 +101,7 @@ protected function setUp(): void { + 'filter_html' => [ + 'status' => TRUE, + 'settings' => [ +- 'allowed_html' => '<p> <br> <strong> <em> <a href> <drupal-media data-entity-type data-entity-uuid data-align data-view-mode data-caption alt>', ++ 'allowed_html' => '<p> <br> <strong> <em> <a href> <drupal-media data-entity-type data-entity-uuid data-align data-view-mode data-caption alt> <drupal-media-inline data-entity-type data-entity-uuid data-align data-view-mode data-caption alt>', + ], + ], + 'filter_align' => ['status' => TRUE], +diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module +index 2ba66da8085e35686628703e882f4220a5f57f50..ef79b8e9614bcb9c73c3ca37b8d75392e09242a3 100644 +--- a/core/modules/filter/filter.module ++++ b/core/modules/filter/filter.module +@@ -705,7 +705,7 @@ function _filter_autop($text) { + // to avoid messing up code. We look for matched pairs and allow basic + // nesting. For example: + // "processed <pre> ignored <script> ignored </script> ignored </pre> processed" +- $chunks = preg_split('@(<!--.*?-->|</?(?:pre|script|style|object|iframe|drupal-media|svg|!--)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE); ++ $chunks = preg_split('@(<!--.*?-->|</?(?:pre|script|style|object|iframe|drupal-media|drupal-media-inline|svg|!--)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE); + // Note: PHP ensures the array consists of alternating delimiters and literals + // and begins and ends with a literal (inserting NULL as required). + $ignore = FALSE; +diff --git a/core/modules/media/css/media.inline.css b/core/modules/media/css/media.inline.css +new file mode 100644 +index 0000000000000000000000000000000000000000..0ff40b92181fafb3edfb0d599d3e32376590f3ba +--- /dev/null ++++ b/core/modules/media/css/media.inline.css +@@ -0,0 +1,18 @@ ++/** ++ * @file ++ * Default styling for media that is embedded inline inside text, lists, etc. ++ */ ++ ++.media-embedded-inline { ++ display: inline-block; ++} ++ ++.drupal-media-inline .media-embedded-inline { ++ width: 100%; ++} ++ ++.media-embedded-inline video, ++.media-embedded-inline img, ++.media-embedded-inline audio { ++ display: inline-block; ++} +diff --git a/core/modules/media/media.libraries.yml b/core/modules/media/media.libraries.yml +index 41fc310fbb9a211b5a704715386379ef2444bc60..0e9527db1dbf30275718a68400b6c480aee5c8e4 100644 +--- a/core/modules/media/media.libraries.yml ++++ b/core/modules/media/media.libraries.yml +@@ -32,6 +32,12 @@ filter.caption: + dependencies: + - filter/caption + ++media.inline: ++ version: VERSION ++ css: ++ component: ++ css/media.inline.css: {} ++ + # Despite the name, this is actually not specific to CKEditor 4, and can be + # used by all text editor plugins. + media_embed_ckeditor_theme: +diff --git a/core/modules/media/src/Event/MediaBuildEmbedEvent.php b/core/modules/media/src/Event/MediaBuildEmbedEvent.php +new file mode 100644 +index 0000000000000000000000000000000000000000..98e5f4d5fb5e9044811810f5a013a1e838a0b6cb +--- /dev/null ++++ b/core/modules/media/src/Event/MediaBuildEmbedEvent.php +@@ -0,0 +1,95 @@ ++<?php ++ ++namespace Drupal\media\Event; ++ ++use Drupal\Component\EventDispatcher\Event; ++use Drupal\media\MediaInterface; ++ ++/** ++ * Event that gets triggered when a media is about to be embedded in ckeditor5. ++ */ ++class MediaBuildEmbedEvent extends Event { ++ ++ /** ++ * The view mode the embedded media will be rendered with. ++ * ++ * @var string ++ */ ++ protected string $viewMode; ++ ++ /** ++ * The media which will be embedded. ++ * ++ * @var \Drupal\media\MediaInterface ++ */ ++ protected MediaInterface $media; ++ ++ /** ++ * The build array of the media before it gets rendered for embedding. ++ * ++ * @var array ++ */ ++ protected array $build; ++ ++ /** ++ * The <drupal-media> DOM node that gets replaced by the embed. ++ * ++ * @var \DOMElement ++ */ ++ protected \DOMElement $node; ++ ++ /** ++ * Construct a MediaBuildEmbedEvent object. ++ * ++ * @param string $viewMode ++ * The view mode the embedded media will be rendered with. ++ * @param \Drupal\media\MediaInterface $media ++ * The media which will be embedded. ++ * @param array $build ++ * The build array of the media before it gets rendered for embedding. ++ * @param \DOMElement $node ++ * The <drupal-media> DOM node that gets replaced by the embed. ++ */ ++ public function __construct(string $viewMode, MediaInterface $media, array $build, \DOMElement $node) { ++ $this->viewMode = $viewMode; ++ $this->media = $media; ++ $this->build = $build; ++ $this->node = $node; ++ } ++ ++ /** ++ * Getter for the view mode. ++ */ ++ public function getViewMode(): string { ++ return $this->viewMode; ++ } ++ ++ /** ++ * Gets the media. ++ */ ++ public function getMedia(): MediaInterface { ++ return $this->media; ++ } ++ ++ /** ++ * Gets the build. ++ */ ++ public function getBuild(): array { ++ return $this->build; ++ } ++ ++ /** ++ * Sets the build. ++ */ ++ public function setBuild(array $build): void { ++ $this->build = $build; ++ } ++ ++ /** ++ * Gets the <drupal-media> DOM node. ++ */ ++ public function getNode(): \DOMElement { ++ return $this->node; ++ } ++ ++} +diff --git a/core/modules/media/src/Plugin/Filter/MediaEmbed.php b/core/modules/media/src/Plugin/Filter/MediaEmbed.php +index 118422bf2c28d594569aa58a806ef722e4e0c4e4..c138c2ea492cee4f1080d69afe76f007ea228091 100644 +--- a/core/modules/media/src/Plugin/Filter/MediaEmbed.php ++++ b/core/modules/media/src/Plugin/Filter/MediaEmbed.php +@@ -21,8 +21,10 @@ + use Drupal\filter\Plugin\FilterBase; + use Drupal\filter\Plugin\FilterInterface; + use Drupal\image\Plugin\Field\FieldType\ImageItem; ++use Drupal\media\Event\MediaBuildEmbedEvent; + use Drupal\media\MediaInterface; + use Symfony\Component\DependencyInjection\ContainerInterface; ++use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + + /** + * Provides a filter to embed media items using a custom tag. +@@ -118,8 +120,10 @@ class MediaEmbed extends FilterBase implements ContainerFactoryPluginInterface, + * The renderer. + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory + * The logger factory. ++ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher ++ * The event dispatcher. + */ +- public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository, EntityTypeBundleInfoInterface $bundle_info, RendererInterface $renderer, LoggerChannelFactoryInterface $logger_factory) { ++ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository, EntityTypeBundleInfoInterface $bundle_info, RendererInterface $renderer, LoggerChannelFactoryInterface $logger_factory, protected EventDispatcherInterface $eventDispatcher) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->entityRepository = $entity_repository; + $this->entityTypeManager = $entity_type_manager; +@@ -142,7 +146,8 @@ public static function create(ContainerInterface $container, array $configuratio + $container->get('entity_display.repository'), + $container->get('entity_type.bundle.info'), + $container->get('renderer'), +- $container->get('logger.factory') ++ $container->get('logger.factory'), ++ $container->get('event_dispatcher'), + ); + } + +@@ -255,6 +260,7 @@ protected function renderMedia(MediaInterface $media, $view_mode, $langcode) { + // instead of only when #access allows this media to be viewed and hence + // only when media is actually rendered. + $build[':media_embed']['#attached']['library'][] = 'media/filter.caption'; ++ $build[':media_embed']['#attached']['library'][] = 'media/media.inline'; + + return $build; + } +@@ -278,18 +284,26 @@ protected function renderMissingMediaIndicator() { + public function process($text, $langcode) { + $result = new FilterProcessResult($text); + +- if (stristr($text, '<drupal-media') === FALSE) { ++ if (stristr($text, '<drupal-media') === FALSE && stristr($text, '<drupal-media-inline') === FALSE) { + return $result; + } + + $dom = Html::load($text); + $xpath = new \DOMXPath($dom); ++ $matched_attributes = '@data-entity-type="media" and normalize-space(@data-entity-uuid)!=""'; + +- foreach ($xpath->query('//drupal-media[@data-entity-type="media" and normalize-space(@data-entity-uuid)!=""]') as $node) { ++ foreach ($xpath->query('//drupal-media[' . $matched_attributes . ']|//drupal-media-inline[' . $matched_attributes . ']') as $node) { + /** @var \DOMElement $node */ + $uuid = $node->getAttribute('data-entity-uuid'); + $view_mode_id = $node->getAttribute('data-view-mode') ?: $this->settings['default_view_mode']; + ++ // Inline media needs a dedicated view mode in order to ensure rendering ++ // of valid (flow content) HTML when rendering inside flow content like ++ // <p> tags for example. ++ if ($node->tagName == 'drupal-media-inline') { ++ $view_mode_id = 'ckeditor_inline'; ++ } ++ + // Delete the consumed attributes. + $node->removeAttribute('data-entity-type'); + $node->removeAttribute('data-entity-uuid'); +@@ -314,9 +328,14 @@ public function process($text, $langcode) { + } + } + +- $build = $media && ($view_mode || $view_mode_id === EntityDisplayRepositoryInterface::DEFAULT_DISPLAY_MODE) +- ? $this->renderMedia($media, $view_mode_id, $langcode) +- : $this->renderMissingMediaIndicator(); ++ if ($media && ($view_mode || $view_mode_id === EntityDisplayRepositoryInterface::DEFAULT_DISPLAY_MODE)) { ++ $build = $this->renderMedia($media, $view_mode_id, $langcode); ++ $event = $this->eventDispatcher->dispatch(new MediaBuildEmbedEvent($view_mode_id, $media, $build, $node)); ++ $build = $event->getBuild(); ++ } ++ else { ++ $build = $this->renderMissingMediaIndicator(); ++ } + + if (empty($build['#attributes']['class'])) { + $build['#attributes']['class'] = []; +@@ -339,6 +358,10 @@ public function process($text, $langcode) { + } + } + ++ if ($node->tagName == 'drupal-media-inline') { ++ $build['#attributes']['class'][] = 'media-embedded-inline'; ++ } ++ + $this->renderIntoDomNode($build, $node, $result); + } + +diff --git a/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php +index eb2d1d3bdcaa764666480b9567a2de63eb11071e..93da1306ee2b1223f8d49a83a1d6a1aa6c5b19fb 100644 +--- a/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php ++++ b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php +@@ -40,7 +40,7 @@ public function testBasics(array $embed_attributes, $expected_view_mode, array $ + $this->assertEqualsCanonicalizing($expected_cacheability->getCacheContexts(), $result->getCacheContexts()); + $this->assertSame($expected_cacheability->getCacheMaxAge(), $result->getCacheMaxAge()); + $this->assertSame(['library'], array_keys($result->getAttachments())); +- $this->assertSame(['media/filter.caption'], $result->getAttachments()['library']); ++ $this->assertSame(['media/filter.caption', 'media/media.inline'], $result->getAttachments()['library']); + } + + /** +@@ -194,7 +194,7 @@ public static function providerAccessUnpublished() { + ]) + ->setCacheContexts(['timezone', 'user', 'user.permissions']) + ->setCacheMaxAge(Cache::PERMANENT), +- ['library' => ['media/filter.caption']], ++ ['library' => ['media/filter.caption', 'media/media.inline']], + ], + ]; + } +@@ -428,7 +428,7 @@ public function testFilterIntegration(array $filter_ids, array $additional_attri + * Data provider for testFilterIntegration(). + */ + public static function providerFilterIntegration() { +- $default_asset_libraries = ['media/filter.caption']; ++ $default_asset_libraries = ['media/filter.caption', 'media/media.inline']; + + $caption_additional_attributes = ['data-caption' => 'Yo.']; + $caption_verification_selector = 'figure > figcaption'; +@@ -445,14 +445,14 @@ public static function providerFilterIntegration() { + $caption_additional_attributes, + $caption_verification_selector, + TRUE, +- ['filter/caption', 'media/filter.caption'], ++ ['filter/caption', 'media/filter.caption', 'media/media.inline'], + ], + '`<a>` + `data-caption`; `filter_caption` + `media_embed` ⇒ caption present, link preserved' => [ + ['filter_caption', 'media_embed'], + $caption_additional_attributes, + 'figure > a[href="https://www.drupal.org"] + figcaption', + TRUE, +- ['filter/caption', 'media/filter.caption'], ++ ['filter/caption', 'media/filter.caption', 'media/media.inline'], + '<a href="https://www.drupal.org">', + '</a>', + ], +@@ -492,14 +492,14 @@ public static function providerFilterIntegration() { + $align_additional_attributes + $caption_additional_attributes, + 'figure.align-center > figcaption', + TRUE, +- ['filter/caption', 'media/filter.caption'], ++ ['filter/caption', 'media/filter.caption', 'media/media.inline'], + ], + '`<a>` + `data-caption` + `data-align`; `filter_align` + `filter_caption` + `media_embed` ⇒ aligned caption present, link preserved' => [ + ['filter_align', 'filter_caption', 'media_embed'], + $align_additional_attributes + $caption_additional_attributes, + 'figure.align-center > a[href="https://www.drupal.org"] + figcaption', + TRUE, +- ['filter/caption', 'media/filter.caption'], ++ ['filter/caption', 'media/filter.caption', 'media/media.inline'], + '<a href="https://www.drupal.org">', + '</a>', + ], +diff --git a/core/profiles/demo_umami/config/install/editor.editor.basic_html.yml b/core/profiles/demo_umami/config/install/editor.editor.basic_html.yml +index 60cd331cc875cdb4286b0be09ff99bb9c5a7db54..08518f40f78fa4f506ed54d06549cfd066db638c 100644 +--- a/core/profiles/demo_umami/config/install/editor.editor.basic_html.yml ++++ b/core/profiles/demo_umami/config/install/editor.editor.basic_html.yml +@@ -55,6 +55,7 @@ settings: + - '<h6 id>' + - '<img src alt data-entity-type data-entity-uuid data-align data-caption width height loading>' + - '<drupal-media title>' ++ - '<drupal-media-inline title>' + media_media: + allow_view_mode_override: true + image_upload: +diff --git a/core/profiles/demo_umami/config/install/filter.format.basic_html.yml b/core/profiles/demo_umami/config/install/filter.format.basic_html.yml +index b57e2a67a7950fe32b2a422be14c9154f510ae97..8ad1363564d2631f7265d87faaa4bff5ef00c9ba 100644 +--- a/core/profiles/demo_umami/config/install/filter.format.basic_html.yml ++++ b/core/profiles/demo_umami/config/install/filter.format.basic_html.yml +@@ -40,7 +40,7 @@ filters: + status: true + weight: -10 + settings: +- allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <p> <br> <img src alt loading height width data-entity-type data-entity-uuid data-align data-caption> <drupal-media data-entity-type data-entity-uuid data-view-mode data-align data-caption alt title>' ++ allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <p> <br> <img src alt loading height width data-entity-type data-entity-uuid data-align data-caption> <drupal-media data-entity-type data-entity-uuid data-view-mode data-align data-caption alt title> <drupal-media-inline data-entity-type data-entity-uuid data-view-mode data-align data-caption alt title>' + filter_html_help: false + filter_html_nofollow: false + filter_html_image_secure: +diff --git a/core/themes/stable9/css/media/media.inline.css b/core/themes/stable9/css/media/media.inline.css +new file mode 100644 +index 0000000000000000000000000000000000000000..0ff40b92181fafb3edfb0d599d3e32376590f3ba +--- /dev/null ++++ b/core/themes/stable9/css/media/media.inline.css +@@ -0,0 +1,18 @@ ++/** ++ * @file ++ * Default styling for media that is embedded inline inside text, lists, etc. ++ */ ++ ++.media-embedded-inline { ++ display: inline-block; ++} ++ ++.drupal-media-inline .media-embedded-inline { ++ width: 100%; ++} ++ ++.media-embedded-inline video, ++.media-embedded-inline img, ++.media-embedded-inline audio { ++ display: inline-block; ++} +diff --git a/core/themes/stable9/stable9.info.yml b/core/themes/stable9/stable9.info.yml +index cace78bf8a8251b94b68a20f488824a9d9019f8b..4f79c0a60f5bb6ad85714643183a829a3aa02f04 100644 +--- a/core/themes/stable9/stable9.info.yml ++++ b/core/themes/stable9/stable9.info.yml +@@ -177,6 +177,11 @@ libraries-override: + component: + css/filter.caption.css: css/media/filter.caption.css + ++ media/media.inline: ++ css: ++ component: ++ css/media.inline.css: css/media/media.inline.css ++ + media/oembed.formatter: + css: + component: +diff --git a/core/themes/stable9/templates/content/media--ckeditor-inline-embed.html.twig b/core/themes/stable9/templates/content/media--ckeditor-inline-embed.html.twig +new file mode 100644 +index 0000000000000000000000000000000000000000..18a903f24544a90cae12c4a7d2008dd6b340fba5 +--- /dev/null ++++ b/core/themes/stable9/templates/content/media--ckeditor-inline-embed.html.twig +@@ -0,0 +1,36 @@ ++{# ++/** ++ * @file ++ * Default theme implementation to present a media item. ++ * ++ * Available variables: ++ * - media: The media item, with limited access to object properties and ++ * methods. Only method names starting with "get", "has", or "is" and ++ * a few common methods such as "id", "label", and "bundle" are available. ++ * For example: ++ * - entity.getEntityTypeId() will return the entity type ID. ++ * - entity.hasField('field_example') returns TRUE if the entity includes ++ * field_example. (This does not indicate the presence of a value in this ++ * field.) ++ * Calling other methods, such as entity.delete(), will result in ++ * an exception. ++ * See \Drupal\Core\Entity\EntityInterface for a full list of methods. ++ * - name: Name of the media item. ++ * - content: Media content. ++ * - title_prefix: Additional output populated by modules, intended to be ++ * displayed in front of the main title tag that appears in the template. ++ * - title_suffix: Additional output populated by modules, intended to be ++ * displayed after the main title tag that appears in the template. ++ * - view_mode: View mode; for example, "teaser" or "full". ++ * - attributes: HTML attributes for the containing element. ++ * - title_attributes: Same as attributes, except applied to the main title ++ * tag that appears in the template. ++ * ++ * @see template_preprocess_media() ++ * ++ * @ingroup themeable ++ */ ++#} ++<span{{ attributes }}> ++ {{ content }} ++</span> +diff --git a/core/themes/stable9/templates/field/field--ckeditor-inline.html.twig b/core/themes/stable9/templates/field/field--ckeditor-inline.html.twig +new file mode 100644 +index 0000000000000000000000000000000000000000..7f0ae06f8369db15ea4b70771cacfc9a61eff771 +--- /dev/null ++++ b/core/themes/stable9/templates/field/field--ckeditor-inline.html.twig +@@ -0,0 +1,72 @@ ++{# ++/** ++ * @file ++ * Default theme implementation for a field. ++ * ++ * To override output, copy the "field.html.twig" from the templates directory ++ * to your theme's directory and customize it, just like customizing other ++ * Drupal templates such as page.html.twig or node.html.twig. ++ * ++ * Instead of overriding the theming for all fields, you can also just override ++ * theming for a subset of fields using ++ * @link themeable Theme hook suggestions. @endlink For example, ++ * here are some theme hook suggestions that can be used for a field_foo field ++ * on an article node type: ++ * - field--node--field-foo--article.html.twig ++ * - field--node--field-foo.html.twig ++ * - field--node--article.html.twig ++ * - field--field-foo.html.twig ++ * - field--text-with-summary.html.twig ++ * - field.html.twig ++ * ++ * Available variables: ++ * - attributes: HTML attributes for the containing element. ++ * - label_hidden: Whether to show the field label or not. ++ * - title_attributes: HTML attributes for the title. ++ * - label: The label for the field. ++ * - multiple: TRUE if a field can contain multiple items. ++ * - items: List of all the field items. Each item contains: ++ * - attributes: List of HTML attributes for each item. ++ * - content: The field item's content. ++ * - entity_type: The entity type to which the field belongs. ++ * - field_name: The name of the field. ++ * - field_type: The type of the field. ++ * - label_display: The display settings for the label. ++ * ++ * @see template_preprocess_field() ++ * ++ * @ingroup themeable ++ */ ++#} ++{% ++ set title_classes = [ ++ label_display == 'visually_hidden' ? 'visually-hidden', ++ ] ++%} ++ ++{% if label_hidden %} ++ {% if multiple %} ++ <span{{ attributes }}> ++ {% for item in items %} ++ <span{{ item.attributes }}>{{ item.content }}</span> ++ {% endfor %} ++ </span> ++ {% else %} ++ {% for item in items %} ++ <span{{ attributes }}>{{ item.content }}</span> ++ {% endfor %} ++ {% endif %} ++{% else %} ++ <span{{ attributes }}> ++ <span{{ title_attributes.addClass(title_classes) }}>{{ label }}</span> ++ {% if multiple %} ++ <span> ++ {% endif %} ++ {% for item in items %} ++ <span{{ item.attributes }}>{{ item.content }}</span> ++ {% endfor %} ++ {% if multiple %} ++ </span> ++ {% endif %} ++ </span> ++{% endif %}