Commit 2a2a662d authored by bnjmnm's avatar bnjmnm
Browse files

Issue #3246385 by lauriii, nod_, Wim Leers: [drupalMedia] Support captions on <drupal-media>

parent 08898695
......@@ -539,6 +539,22 @@ media_media:
conditions:
filter: media_embed
ckeditor5_drupalMediaCaption:
ckeditor5:
plugins:
- drupalMedia.DrupalMediaCaption
config:
drupalMedia:
toolbar: [toggleDrupalMediaCaption]
drupal:
label: Media caption
elements:
- <drupal-media data-caption>
conditions:
filter: filter_caption
plugins:
- media_media
media_mediaAlign:
provider: media
ckeditor5:
......
/**
* @file
* Styles for the Drupal Media in CKEditor 5.
*
* Most of these styles are written to match those in the CKEditor 5 image
* plugin to provide a consistent editing experience.
*/
.ck .drupal-media {
position: relative;
display: table;
clear: both;
min-width: 50px;
margin: 0.9em auto;
text-align: center;
}
.ck-content .drupal-media img {
display: block;
min-width: 100%;
max-width: 100%;
margin: 0 auto;
}
.ck-content .drupal-media > figcaption {
display: table-caption;
padding: 0.6em;
caption-side: bottom;
word-break: break-word;
color: hsl(0, 0%, 20%);
outline-offset: -1px;
background-color: hsl(0, 0%, 97%);
font-size: 0.75em;
}
.ck.ck-editor__editable .drupal-media__caption_highlighted {
animation: drupal-media-caption-highlight 0.6s ease-out;
}
@keyframes drupal-media-caption-highlight {
0% {
background-color: hsl(52, 100%, 50%);
}
100% {
background-color: hsl(0, 0%, 97%);
}
}
.ck .drupal-media__metadata-error {
position: absolute;
top: 8px;
......
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.drupalImage=e())}(self,(function(){return function(){var t={"ckeditor5/src/core.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/upload.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/upload.js")},"ckeditor5/src/utils.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"dll-reference CKEditor5.dll":function(t){"use strict";t.exports=CKEditor5.dll}},e={};function i(r){var n=e[r];if(void 0!==n)return n.exports;var a=e[r]={exports:{}};return t[r](a,a.exports,i),a.exports}i.d=function(t,e){for(var r in e)i.o(e,r)&&!i.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)};var r={};return function(){"use strict";i.d(r,{default:function(){return h}});var t=i("ckeditor5/src/core.js");function e(t){return t.createEmptyElement("img")}function n(t){const e=parseFloat(t);return!Number.isNaN(e)&&t===String(e)}function a(){function t(t,e,i){if(!i.consumable.consume(e.item,t.name))return;const r=i.mapper.toViewElement(e.item),n=i.writer,a=n.createContainerElement("a",{href:e.attributeNewValue});n.insert(n.createPositionBefore(r),a),n.move(n.createRangeOn(r),n.createPositionAt(a,0)),i.consumable.consume(e.item,"attribute:htmlLinkAttributes:imageBlock")&&function(t,e,i){if(e.attributes)for(const[r,n]of Object.entries(e.attributes))t.setAttribute(r,n,i);e.styles&&t.setStyle(e.styles,i),e.classes&&t.addClass(e.classes,i)}(i.writer,e.item.getAttribute("htmlLinkAttributes"),a)}return e=>{e.on("attribute:linkHref:imageBlock",t,{priority:"high"})}}class o extends t.Plugin{static get pluginName(){return"DrupalImageEditing"}init(){const{editor:t}=this,{conversion:i}=t,{schema:r}=t.model;r.isRegistered("imageInline")&&r.extend("imageInline",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),r.isRegistered("imageBlock")&&r.extend("imageBlock",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),i.for("upcast").add(function(t){function e(e,i,r){const{viewItem:n}=i,{writer:a,consumable:o,safeInsert:s,updateConversionResult:u,schema:l}=r,d=[];let c;if(o.test(n,{name:!0,attributes:"src"})){if(c=l.checkChild(i.modelCursor,"imageInline")?a.createElement("imageInline",{src:n.getAttribute("src")}):a.createElement("imageBlock",{src:n.getAttribute("src")}),t.plugins.has("ImageStyleEditing")&&o.test(n,{name:!0,attributes:"data-align"})){const t={left:"alignBlockLeft",center:"alignCenter",right:"alignBlockRight"},e={left:"alignLeft",right:"alignRight"},i=n.getAttribute("data-align"),r=c.is("element","imageBlock")?t[i]:e[i];a.setAttribute("imageStyle",r,c),d.push("data-align")}if(c.is("element","imageBlock")&&o.test(n,{name:!0,attributes:"data-caption"})){const e=a.createElement("caption"),i=t.data.processor.toView(n.getAttribute("data-caption")),o=a.createDocumentFragment();r.consumable.constructor.createFrom(i,r.consumable),r.convertChildren(i,o);for(const t of Array.from(o.getChildren()))a.append(t,e);a.append(e,c),d.push("data-caption")}o.test(n,{name:!0,attributes:"data-entity-uuid"})&&(a.setAttribute("dataEntityUuid",n.getAttribute("data-entity-uuid"),c),d.push("data-entity-uuid")),o.test(n,{name:!0,attributes:"data-entity-type"})&&(a.setAttribute("dataEntityType",n.getAttribute("data-entity-type"),c),d.push("data-entity-type")),s(c,i.modelCursor)&&(o.consume(n,{name:!0,attributes:d}),u(c,i))}}return t=>{t.on("element:img",e,{priority:"high"})}}(t)).attributeToAttribute({view:{name:"img",key:"width"},model:{key:"width",value:t=>n(t.getAttribute("width"))?`${t.getAttribute("width")}px`:`${t.getAttribute("width")}`}}).attributeToAttribute({view:{name:"img",key:"height"},model:{key:"height",value:t=>n(t.getAttribute("height"))?`${t.getAttribute("height")}px`:`${t.getAttribute("height")}`}}),i.for("downcast").add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-uuid",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityUuid",t)}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-type",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityType",t)}}()),i.for("dataDowncast").add(function(t){return e=>{e.on("insert:caption",((e,i,r)=>{const{consumable:n,writer:a,mapper:o}=r;if(!n.consume(i.item,"insert"))return;const s=t.model.createRangeIn(i.item),u=a.createDocumentFragment();o.bindElements(i.item,u);for(const{item:e}of Array.from(s)){const i={item:e,range:t.model.createRangeOn(e)},n=`insert:${e.name||"$text"}`;t.data.downcastDispatcher.fire(n,i,r);for(const n of e.getAttributeKeys())Object.assign(i,{attributeKey:n,attributeOldValue:null,attributeNewValue:i.item.getAttribute(n)}),t.data.downcastDispatcher.fire(`attribute:${n}`,i,r)}for(const t of a.createRangeIn(u).getItems())o.unbindViewElement(t);o.unbindViewElement(u);const l=t.data.processor.toData(u);if(l){const t=o.toViewElement(i.item.parent);a.setAttribute("data-caption",l,t)}}),{priority:"high"})}}(t)).elementToElement({model:"imageBlock",view:(t,{writer:i})=>e(i),converterPriority:"high"}).elementToElement({model:"imageInline",view:(t,{writer:i})=>e(i),converterPriority:"high"}).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i,o={alignLeft:"left",alignRight:"right",alignCenter:"center",alignBlockRight:"right",alignBlockLeft:"left"};if(!o[e.attributeNewValue]||!n.consume(r,t.name))return;const s=i.mapper.toViewElement(r),u=Array.from(s.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-align",o[e.attributeNewValue],u||s)}return e=>{e.on("attribute:imageStyle",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("width",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:width:imageInline",t,{priority:"high"}),e.on("attribute:width:imageBlock",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("height",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:height:imageInline",t,{priority:"high"}),e.on("attribute:height:imageBlock",t,{priority:"high"})}}()).add(a())}}class s extends t.Plugin{static get requires(){return[o]}static get pluginName(){return"DrupalImage"}}var u=s;class l extends t.Plugin{init(){const{editor:t}=this;t.plugins.get("ImageUploadEditing").on("uploadComplete",((e,{data:i,imageElement:r})=>{t.model.change((t=>{t.setAttribute("dataEntityUuid",i.dataEntityUuid,r),t.setAttribute("dataEntityType",i.dataEntityType,r)}))}))}static get pluginName(){return"DrupalImageUploadEditing"}}var d=i("ckeditor5/src/upload.js"),c=i("ckeditor5/src/utils.js");class m{constructor(t,e){this.loader=t,this.options=e}upload(){return this.loader.file.then((t=>new Promise(((e,i)=>{this._initRequest(),this._initListeners(e,i,t),this._sendRequest(t)}))))}abort(){this.xhr&&this.xhr.abort()}_initRequest(){this.xhr=new XMLHttpRequest,this.xhr.open("POST",this.options.uploadUrl,!0),this.xhr.responseType="json"}_initListeners(t,e,i){const r=this.xhr,n=this.loader,a=`Couldn't upload file: ${i.name}.`;r.addEventListener("error",(()=>e(a))),r.addEventListener("abort",(()=>e())),r.addEventListener("load",(()=>{const i=r.response;if(!i||i.error)return e(i&&i.error&&i.error.message?i.error.message:a);t({urls:{default:i.url},dataEntityUuid:i.uuid?i.uuid:"",dataEntityType:i.entity_type?i.entity_type:""})})),r.upload&&r.upload.addEventListener("progress",(t=>{t.lengthComputable&&(n.uploadTotal=t.total,n.uploaded=t.loaded)}))}_sendRequest(t){const e=this.options.headers||{},i=this.options.withCredentials||!1;Object.keys(e).forEach((t=>{this.xhr.setRequestHeader(t,e[t])})),this.xhr.withCredentials=i;const r=new FormData;r.append("upload",t),this.xhr.send(r)}}class g extends t.Plugin{static get requires(){return[d.FileRepository]}static get pluginName(){return"DrupalFileRepository"}init(){const t=this.editor.config.get("drupalImageUpload");t&&(t.uploadUrl?this.editor.plugins.get(d.FileRepository).createUploadAdapter=e=>new m(e,t):(0,c.logWarning)("simple-upload-adapter-missing-uploadurl"))}}class p extends t.Plugin{static get requires(){return[g,l]}static get pluginName(){return"DrupalImageUpload"}}var h={DrupalImage:u,DrupalImageUpload:p}}(),r=r.default}()}));
\ No newline at end of file
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.drupalImage=e())}(self,(function(){return function(){var t={"ckeditor5/src/core.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/upload.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/upload.js")},"ckeditor5/src/utils.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"dll-reference CKEditor5.dll":function(t){"use strict";t.exports=CKEditor5.dll}},e={};function i(r){var n=e[r];if(void 0!==n)return n.exports;var a=e[r]={exports:{}};return t[r](a,a.exports,i),a.exports}i.d=function(t,e){for(var r in e)i.o(e,r)&&!i.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)};var r={};return function(){"use strict";i.d(r,{default:function(){return h}});var t=i("ckeditor5/src/core.js");function e(t){return t.createEmptyElement("img")}function n(t){const e=parseFloat(t);return!Number.isNaN(e)&&t===String(e)}function a(){function t(t,e,i){if(!i.consumable.consume(e.item,t.name))return;const r=i.mapper.toViewElement(e.item),n=i.writer,a=n.createContainerElement("a",{href:e.attributeNewValue});n.insert(n.createPositionBefore(r),a),n.move(n.createRangeOn(r),n.createPositionAt(a,0)),i.consumable.consume(e.item,"attribute:htmlLinkAttributes:imageBlock")&&function(t,e,i){if(e.attributes)for(const[r,n]of Object.entries(e.attributes))t.setAttribute(r,n,i);e.styles&&t.setStyle(e.styles,i),e.classes&&t.addClass(e.classes,i)}(i.writer,e.item.getAttribute("htmlLinkAttributes"),a)}return e=>{e.on("attribute:linkHref:imageBlock",t,{priority:"high"})}}class o extends t.Plugin{static get requires(){return["ImageUtils"]}static get pluginName(){return"DrupalImageEditing"}init(){const{editor:t}=this,{conversion:i}=t,{schema:r}=t.model;r.isRegistered("imageInline")&&r.extend("imageInline",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),r.isRegistered("imageBlock")&&r.extend("imageBlock",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),i.for("upcast").add(function(t){function e(e,i,r){const{viewItem:n}=i,{writer:a,consumable:o,safeInsert:s,updateConversionResult:u,schema:l}=r,d=[];let c;if(o.test(n,{name:!0,attributes:"src"})){if(c=l.checkChild(i.modelCursor,"imageInline")?a.createElement("imageInline",{src:n.getAttribute("src")}):a.createElement("imageBlock",{src:n.getAttribute("src")}),t.plugins.has("ImageStyleEditing")&&o.test(n,{name:!0,attributes:"data-align"})){const t={left:"alignBlockLeft",center:"alignCenter",right:"alignBlockRight"},e={left:"alignLeft",right:"alignRight"},i=n.getAttribute("data-align"),r=c.is("element","imageBlock")?t[i]:e[i];a.setAttribute("imageStyle",r,c),d.push("data-align")}if(c.is("element","imageBlock")&&o.test(n,{name:!0,attributes:"data-caption"})){const e=a.createElement("caption"),i=t.data.processor.toView(n.getAttribute("data-caption")),o=a.createDocumentFragment();r.consumable.constructor.createFrom(i,r.consumable),r.convertChildren(i,o);for(const t of Array.from(o.getChildren()))a.append(t,e);a.append(e,c),d.push("data-caption")}o.test(n,{name:!0,attributes:"data-entity-uuid"})&&(a.setAttribute("dataEntityUuid",n.getAttribute("data-entity-uuid"),c),d.push("data-entity-uuid")),o.test(n,{name:!0,attributes:"data-entity-type"})&&(a.setAttribute("dataEntityType",n.getAttribute("data-entity-type"),c),d.push("data-entity-type")),s(c,i.modelCursor)&&(o.consume(n,{name:!0,attributes:d}),u(c,i))}}return t=>{t.on("element:img",e,{priority:"high"})}}(t)).attributeToAttribute({view:{name:"img",key:"width"},model:{key:"width",value:t=>n(t.getAttribute("width"))?`${t.getAttribute("width")}px`:`${t.getAttribute("width")}`}}).attributeToAttribute({view:{name:"img",key:"height"},model:{key:"height",value:t=>n(t.getAttribute("height"))?`${t.getAttribute("height")}px`:`${t.getAttribute("height")}`}}),i.for("downcast").add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-uuid",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityUuid",t)}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-type",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityType",t)}}()),i.for("dataDowncast").add(function(t){return e=>{e.on("insert:caption",((e,i,r)=>{const{consumable:n,writer:a,mapper:o}=r;if(!t.plugins.get("ImageUtils").isImage(i.item.parent)||!n.consume(i.item,"insert"))return;const s=t.model.createRangeIn(i.item),u=a.createDocumentFragment();o.bindElements(i.item,u);for(const{item:e}of Array.from(s)){const i={item:e,range:t.model.createRangeOn(e)},n=`insert:${e.name||"$text"}`;t.data.downcastDispatcher.fire(n,i,r);for(const n of e.getAttributeKeys())Object.assign(i,{attributeKey:n,attributeOldValue:null,attributeNewValue:i.item.getAttribute(n)}),t.data.downcastDispatcher.fire(`attribute:${n}`,i,r)}for(const t of a.createRangeIn(u).getItems())o.unbindViewElement(t);o.unbindViewElement(u);const l=t.data.processor.toData(u);if(l){const t=o.toViewElement(i.item.parent);a.setAttribute("data-caption",l,t)}}),{priority:"high"})}}(t)).elementToElement({model:"imageBlock",view:(t,{writer:i})=>e(i),converterPriority:"high"}).elementToElement({model:"imageInline",view:(t,{writer:i})=>e(i),converterPriority:"high"}).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i,o={alignLeft:"left",alignRight:"right",alignCenter:"center",alignBlockRight:"right",alignBlockLeft:"left"};if(!o[e.attributeNewValue]||!n.consume(r,t.name))return;const s=i.mapper.toViewElement(r),u=Array.from(s.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-align",o[e.attributeNewValue],u||s)}return e=>{e.on("attribute:imageStyle",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("width",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:width:imageInline",t,{priority:"high"}),e.on("attribute:width:imageBlock",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("height",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:height:imageInline",t,{priority:"high"}),e.on("attribute:height:imageBlock",t,{priority:"high"})}}()).add(a())}}class s extends t.Plugin{static get requires(){return[o]}static get pluginName(){return"DrupalImage"}}var u=s;class l extends t.Plugin{init(){const{editor:t}=this;t.plugins.get("ImageUploadEditing").on("uploadComplete",((e,{data:i,imageElement:r})=>{t.model.change((t=>{t.setAttribute("dataEntityUuid",i.dataEntityUuid,r),t.setAttribute("dataEntityType",i.dataEntityType,r)}))}))}static get pluginName(){return"DrupalImageUploadEditing"}}var d=i("ckeditor5/src/upload.js"),c=i("ckeditor5/src/utils.js");class m{constructor(t,e){this.loader=t,this.options=e}upload(){return this.loader.file.then((t=>new Promise(((e,i)=>{this._initRequest(),this._initListeners(e,i,t),this._sendRequest(t)}))))}abort(){this.xhr&&this.xhr.abort()}_initRequest(){this.xhr=new XMLHttpRequest,this.xhr.open("POST",this.options.uploadUrl,!0),this.xhr.responseType="json"}_initListeners(t,e,i){const r=this.xhr,n=this.loader,a=`Couldn't upload file: ${i.name}.`;r.addEventListener("error",(()=>e(a))),r.addEventListener("abort",(()=>e())),r.addEventListener("load",(()=>{const i=r.response;if(!i||i.error)return e(i&&i.error&&i.error.message?i.error.message:a);t({urls:{default:i.url},dataEntityUuid:i.uuid?i.uuid:"",dataEntityType:i.entity_type?i.entity_type:""})})),r.upload&&r.upload.addEventListener("progress",(t=>{t.lengthComputable&&(n.uploadTotal=t.total,n.uploaded=t.loaded)}))}_sendRequest(t){const e=this.options.headers||{},i=this.options.withCredentials||!1;Object.keys(e).forEach((t=>{this.xhr.setRequestHeader(t,e[t])})),this.xhr.withCredentials=i;const r=new FormData;r.append("upload",t),this.xhr.send(r)}}class g extends t.Plugin{static get requires(){return[d.FileRepository]}static get pluginName(){return"DrupalFileRepository"}init(){const t=this.editor.config.get("drupalImageUpload");t&&(t.uploadUrl?this.editor.plugins.get(d.FileRepository).createUploadAdapter=e=>new m(e,t):(0,c.logWarning)("simple-upload-adapter-missing-uploadurl"))}}class p extends t.Plugin{static get requires(){return[g,l]}static get pluginName(){return"DrupalImageUpload"}}var h={DrupalImage:u,DrupalImageUpload:p}}(),r=r.default}()}));
\ No newline at end of file
......@@ -51,7 +51,12 @@ function viewCaptionToCaptionAttribute(editor) {
'insert:caption',
(evt, data, conversionApi) => {
const { consumable, writer, mapper } = conversionApi;
if (!consumable.consume(data.item, 'insert')) {
const imageUtils = editor.plugins.get('ImageUtils');
if (
!imageUtils.isImage(data.item.parent) ||
!consumable.consume(data.item, 'insert')
) {
return;
}
......@@ -434,6 +439,10 @@ function downcastBlockImageLink() {
* @internal
*/
export default class DrupalImageEditing extends Plugin {
static get requires() {
return ['ImageUtils'];
}
/**
* @inheritdoc
*/
......
/* eslint-disable import/no-extraneous-dependencies */
/* cspell:words drupalmediacaption drupalmediacaptionediting drupalmediacaptionui */
import { Plugin } from 'ckeditor5/src/core';
import DrupalMediaCaptionEditing from './drupalmediacaption/drupalmediacaptionediting';
import DrupalMediaCaptionUI from './drupalmediacaption/drupalmediacaptionui';
/**
* @internal
*/
export default class DrupalMediaCaption extends Plugin {
static get requires() {
return [DrupalMediaCaptionEditing, DrupalMediaCaptionUI];
}
static get pluginName() {
return 'DrupalMediaCaption';
}
}
/* eslint-disable import/no-extraneous-dependencies */
/* cspell:words imagecaption */
import { Command } from 'ckeditor5/src/core';
import { isDrupalMedia } from '../utils';
/**
* Gets the caption model element from the media model selection.
*
* @param {module:engine/model/element~Element} drupalMediaModelElement
* The model element from which caption should be retrieved.
* @returns {module:engine/model/element~Element|null}
* The caption element or `null` if the selection has no child caption
* element.
*/
export function getCaptionFromDrupalMediaModelElement(drupalMediaModelElement) {
// eslint-disable-next-line no-restricted-syntax
for (const node of drupalMediaModelElement.getChildren()) {
if (!!node && node.is('element', 'caption')) {
return node;
}
}
return null;
}
/**
* The toggle Drupal Media caption command.
*
* This command either adds or removes the caption of a selected drupalMedia
* element.
*
* This is inspired by the CKEditor 5 image caption plugin.
*
* @see module:image/imagecaption~ImageCaption
*
* @extends module:core/command~Command
*
* @internal
*/
export default class ToggleDrupalMediaCaptionCommand extends Command {
/**
* @inheritDoc
*/
refresh() {
const element = this.editor.model.document.selection.getSelectedElement();
this.isEnabled = isDrupalMedia(element);
if (!this.isEnabled) {
this.value = false;
} else {
this.value = !!getCaptionFromDrupalMediaModelElement(element);
}
}
/**
* Executes the command.
*
* @example
* editor.execute('toggleMediaCaption');
*
* @param {Object} [options]
* Options for the executed command.
* @param {String} [options.focusCaptionOnShow]
* When true and the caption shows up, the selection will be moved into it
* When true: If a caption is present, the selection will be moved to that
* caption immediately.
*
* @fires execute
*/
execute(options = {}) {
const { focusCaptionOnShow } = options;
this.editor.model.change((writer) => {
if (this.value) {
this._hideDrupalMediaCaption(writer);
} else {
this._showDrupalMediaCaption(writer, focusCaptionOnShow);
}
});
}
/**
* Shows the caption of a selected drupalMedia element.
*
* This also attempts to restore the caption content from the
* `DrupalMediaEditing` caption registry. If the `focusCaptionOnShow` option
* is true, the selection is immediately moved to the caption.
*
* @param {module:engine/model/writer~Writer} writer
* The model writer.
* @param {bool} focusCaptionOnShow
* Flag indicating whether the caption should be focused.
*/
_showDrupalMediaCaption(writer, focusCaptionOnShow) {
const model = this.editor.model;
const selection = model.document.selection;
const mediaCaptionEditing = this.editor.plugins.get(
'DrupalMediaCaptionEditing',
);
const selectedMedia = selection.getSelectedElement();
const savedCaption = mediaCaptionEditing._getSavedCaption(selectedMedia);
// Try restoring the caption from the DrupalMediaCaptionEditing plugin storage.
const newCaptionElement = savedCaption || writer.createElement('caption');
writer.append(newCaptionElement, selectedMedia);
if (focusCaptionOnShow) {
writer.setSelection(newCaptionElement, 'in');
}
}
/**
* Hides the caption of a selected drupalMedia element.
*
* The content of the caption is stored in the `DrupalMediaCaptionEditing`
* caption registry to make this a reversible action.
*
* @param {module:engine/model/writer~Writer} writer
* The model writer.
*/
_hideDrupalMediaCaption(writer) {
const editor = this.editor;
const selection = editor.model.document.selection;
const mediaCaptionEditing = editor.plugins.get('DrupalMediaCaptionEditing');
const selectedMedia = selection.getSelectedElement();
if (selectedMedia) {
const captionElement =
getCaptionFromDrupalMediaModelElement(selectedMedia);
// Store the caption content so it can be restored quickly if the user
// changes their mind.
mediaCaptionEditing._saveCaption(selectedMedia, captionElement);
writer.setSelection(selectedMedia, 'on');
writer.remove(captionElement);
}
}
}
/* eslint-disable import/no-extraneous-dependencies */
/* cspell:words insertdrupalmedia JSONified drupalmediacaptioncommand downcasted */
import { Plugin } from 'ckeditor5/src/core';
import { Element, enablePlaceholder } from 'ckeditor5/src/engine';
import { toWidgetEditable } from 'ckeditor5/src/widget';
import { isDrupalMedia } from '../utils';
import ToggleDrupalMediaCaptionCommand from './drupalmediacaptioncommand';
/**
* A view to model converter for Drupal Media caption.
*
* This upcasts the `data-caption` attribute from `<drupal-media>` elements into
* a `<caption>` model element. This is converted into a model element instead of
* a model attribute in order to leverage CKEditor 5 built-in editing.
*
* @param {module:core/editor/editor~Editor} editor
* Editor on which this converter will be used.
* @return {function}
* A function that attaches converter to the dispatcher.
*/
function viewToModelCaption(editor) {
const converter = (evt, data, conversionApi) => {
const { viewItem } = data;
const { writer, consumable } = conversionApi;
if (
!data.modelRange ||
!consumable.consume(viewItem, { attributes: ['data-caption'] })
) {
return;
}
const caption = writer.createElement('caption');
const drupalMedia = data.modelRange.start.nodeAfter;
// Parse HTML from data-caption attribute and upcast it to model fragment.
const viewFragment = editor.data.processor.toView(
viewItem.getAttribute('data-caption'),
);
const modelFragment = writer.createDocumentFragment();
// Consumable must know about those newly parsed view elements.
conversionApi.consumable.constructor.createFrom(
viewFragment,
conversionApi.consumable,
);
conversionApi.convertChildren(viewFragment, modelFragment);
// Insert caption model nodes into the caption.
// eslint-disable-next-line no-restricted-syntax
for (const child of Array.from(modelFragment.getChildren())) {
writer.append(child, caption);
}
// Insert the caption element into drupalMedia, as a last child.
writer.append(caption, drupalMedia);
};
return (dispatcher) => {
dispatcher.on('element:drupal-media', converter, { priority: 'low' });
};
}
/**
* Gets mapper function for repositioning the `<figcaption>` element.
*
* @param {module:engine/view/view~View} editingView
* The editing view.
* @return {function}
* A mapper callback that moves `<figcaption>` element after the Drupal Media
* preview.
*/
function mapModelPositionToView(editingView) {
return (evt, data) => {
const modelPosition = data.modelPosition;
const parent = modelPosition.parent;
if (!isDrupalMedia(parent)) {
return;
}
const viewElement = data.mapper.toViewElement(parent);
data.viewPosition = editingView.createPositionAt(
viewElement,
modelPosition.offset + 1,
);
};
}
/**
* A model to view converter for Drupal Media caption.
*
* This downcasts the `<caption>` model element into `data-caption` attribute in
* the view.
*
* @param {module:core/editor/editor~Editor} editor
* Editor on which this converter will be used.
* @return {function}
* A function that attaches converter to the dispatcher.
*/
function modelCaptionToCaptionAttribute(editor) {
return (dispatcher) => {
dispatcher.on('insert:caption', (evt, data, conversionApi) => {
const { consumable, writer, mapper } = conversionApi;
if (
!isDrupalMedia(data.item.parent) ||
!consumable.consume(data.item, 'insert')
) {
return;
}
const range = editor.model.createRangeIn(data.item);
const viewDocumentFragment = writer.createDocumentFragment();
// Bind caption model element to the detached view document fragment so
// all content of the caption will be downcasted into that document
// fragment.
mapper.bindElements(data.item, viewDocumentFragment);
// eslint-disable-next-line no-restricted-syntax
for (const { item } of Array.from(range)) {
const itemData = {
item,
range: editor.model.createRangeOn(item),
};
// The following lines are extracted from
// DowncastDispatcher._convertInsertWithAttributes().
const eventName = `insert:${item.name || '$text'}`;
editor.data.downcastDispatcher.fire(eventName, itemData, conversionApi);
// eslint-disable-next-line no-restricted-syntax
for (const key of item.getAttributeKeys()) {
Object.assign(itemData, {
attributeKey: key,
attributeOldValue: null,
attributeNewValue: itemData.item.getAttribute(key),
});
editor.data.downcastDispatcher.fire(
`attribute:${key}`,
itemData,
conversionApi,
);
}
}
// Unbind all the view elements that were downcasted to the document
// fragment.
// eslint-disable-next-line no-restricted-syntax
for (const child of writer
.createRangeIn(viewDocumentFragment)
.getItems()) {
mapper.unbindViewElement(child);
}
mapper.unbindViewElement(viewDocumentFragment);
// Stringify view document fragment to HTML string.
const captionText = editor.data.processor.toData(viewDocumentFragment);
if (captionText) {
const imageViewElement = mapper.toViewElement(data.item.parent);
writer.setAttribute('data-caption', captionText, imageViewElement);
}
});
};
}
/**
* The Drupal Media caption editing plugin.
*
* @extends module:core/plugin~Plugin
*
* @internal
*/
export default class DrupalMediaCaptionEditing extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [];
}
/**
* @inheritDoc
*/
static get pluginName() {
return 'DrupalMediaCaptionEditing';
}
/**
* @inheritDoc
*/
constructor(editor) {
super(editor);
/**
* A map of saved Drupal Media captions and related model elements.
*
* @member {WeakMap.<module:engine/model/element~Element,Object>}
*
* @see _saveCaption
*/
this._savedCaptionsMap = new WeakMap();
}