diff --git a/core/assets/vendor/ckeditor5/style/style.js b/core/assets/vendor/ckeditor5/style/style.js new file mode 100644 index 000000000000..a47a10cdcfcd --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/style.js @@ -0,0 +1,5 @@ +!function(e){const t=e.en=e.en||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Block styles","Multiple styles":"Multiple styles",Styles:"Styles","Text styles":"Text styles"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})), +/*! + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */(()=>{var e={529:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var s=n(609),o=n.n(s)()((function(e){return e[1]}));o.push([e.id,".ck.ck-dropdown.ck-style-dropdown.ck-style-dropdown_multiple-active>.ck-button>.ck-button__label{font-style:italic}",""]);const i=o},945:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var s=n(609),o=n.n(s)()((function(e){return e[1]}));o.push([e.id,":root{--ck-style-panel-columns:3}.ck.ck-style-panel .ck-style-grid{display:grid;grid-template-columns:repeat(var(--ck-style-panel-columns),auto);justify-content:start}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button{display:flex;flex-direction:column;justify-content:space-between}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button .ck-style-grid__button__preview{align-content:center;align-items:center;display:flex;flex-basis:100%;flex-grow:1;justify-content:flex-start}:root{--ck-style-panel-button-width:120px;--ck-style-panel-button-height:80px;--ck-style-panel-button-shadow-color:rgba(0,0,0,.1);--ck-style-panel-button-shadow:0px 0px 6px var(--ck-style-panel-button-shadow-color);--ck-style-panel-button-label-background:#e6e6e6;--ck-style-panel-button-hover-label-background:#ccc;--ck-style-panel-button-hover-border-color:#b3b3b3}.ck.ck-style-panel .ck-style-grid{column-gap:var(--ck-spacing-large);row-gap:var(--ck-spacing-large)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button{--ck-color-button-default-hover-background:var(--ck-color-base-background);--ck-color-button-default-active-background:var(--ck-color-base-background);height:var(--ck-style-panel-button-height);padding:0;width:var(--ck-style-panel-button-width)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:not(:focus){border:1px solid var(--ck-color-base-border);box-shadow:var(--ck-style-panel-button-shadow)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button .ck-button__label{background:var(--ck-style-panel-button-label-background);flex-shrink:0;height:22px;line-height:22px;overflow:hidden;padding:0 var(--ck-spacing-medium);text-overflow:ellipsis;width:100%}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button .ck-style-grid__button__preview{border:2px solid var(--ck-color-base-background);opacity:.9;overflow:hidden;padding:var(--ck-spacing-medium);width:100%}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled{--ck-color-button-default-disabled-background:var(--ck-color-base-foreground)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled:not(:focus){border-color:var(--ck-style-panel-button-label-background);box-shadow:none}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled .ck-button__label{background:var(--ck-style-panel-button-label-background)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled .ck-style-grid__button__preview{border-color:var(--ck-color-base-foreground);filter:saturate(.3);opacity:.4}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on{--ck-color-button-on-background:var(--ck-color-base-background);--ck-color-button-on-hover-background:var(--ck-color-base-background);--ck-color-button-on-active-background:var(--ck-color-base-background);--ck-style-panel-button-shadow-color:rgba(25,140,240,.1);border-color:var(--ck-color-base-active)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on .ck-button__label{background:var(--ck-color-base-active);color:var(--ck-color-base-background)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on:hover{border-color:var(--ck-color-base-active-focus)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on:hover .ck-button__label{background:var(--ck-color-base-active-focus)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:hover:not(.ck-disabled):not(.ck-on){border-color:var(--ck-style-panel-button-hover-border-color)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:hover:not(.ck-disabled):not(.ck-on) .ck-style-grid__button__preview{opacity:1}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:hover:not(.ck-disabled):not(.ck-on) .ck-button__label{background:var(--ck-style-panel-button-hover-label-background)}",""]);const i=o},561:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var s=n(609),o=n.n(s)()((function(e){return e[1]}));o.push([e.id,".ck.ck-style-panel .ck-style-panel__style-group>.ck-label{margin:var(--ck-spacing-large) 0}.ck.ck-style-panel .ck-style-panel__style-group:first-child>.ck-label{margin-top:0}",""]);const i=o},662:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var s=n(609),o=n.n(s)()((function(e){return e[1]}));o.push([e.id,":root{--ck-style-panel-max-height:470px}.ck.ck-style-panel{max-height:var(--ck-style-panel-max-height);overflow-y:auto;padding:var(--ck-spacing-large)}",""]);const i=o},609:e=>{"use strict";e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n=e(t);return t[2]?"@media ".concat(t[2]," {").concat(n,"}"):n})).join("")},t.i=function(e,n,s){"string"==typeof e&&(e=[[null,e,""]]);var o={};if(s)for(var i=0;i{"use strict";var s,o=function(){return void 0===s&&(s=Boolean(window&&document&&document.all&&!window.atob)),s},i=function(){var e={};return function(t){if(void 0===e[t]){var n=document.querySelector(t);if(window.HTMLIFrameElement&&n instanceof window.HTMLIFrameElement)try{n=n.contentDocument.head}catch(e){n=null}e[t]=n}return e[t]}}(),l=[];function r(e){for(var t=-1,n=0;n{e.exports=n(79)("./src/core.js")},273:(e,t,n)=>{e.exports=n(79)("./src/ui.js")},209:(e,t,n)=>{e.exports=n(79)("./src/utils.js")},79:e=>{"use strict";e.exports=CKEditor5.dll}},t={};function n(s){var o=t[s];if(void 0!==o)return o.exports;var i=t[s]={id:s,exports:{}};return e[s](i,i.exports,n),i.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var s in t)n.o(t,s)&&!n.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nc=void 0;var s={};(()=>{"use strict";n.r(s),n.d(s,{Style:()=>V,StyleEditing:()=>x,StyleUI:()=>w});var e=n(704),t=n(273),o=n(209);const i=["caption","colgroup","dd","dt","figcaption","legend","li","optgroup","option","rp","rt","summary","tbody","td","tfoot","th","thead","tr"];class l extends t.ButtonView{constructor(e,t){super(e),this.styleDefinition=t,this.previewView=this._createPreview(),this.set({label:t.name,class:"ck-style-grid__button",withText:!0}),this.extendTemplate({attributes:{role:"option"}}),this.children.add(this.previewView,0)}_createPreview(){const{element:e,classes:n}=this.styleDefinition,s=new t.View(this.locale);return s.setTemplate({tag:"div",attributes:{class:["ck","ck-reset_all-excluded","ck-style-grid__button__preview","ck-content"]},children:[{tag:this._isPreviewable(e)?e:"div",attributes:{class:n},children:[{text:"AaBbCcDdEeFfGgHhIiJj"}]}]}),s}_isPreviewable(e){return!i.includes(e)}}var r=n(62),c=n.n(r),a=n(945),d={injectType:"singletonStyleTag",attributes:{"data-cke":!0},insert:"head",singleton:!0};c()(a.Z,d);a.Z.locals;class u extends t.View{constructor(e,t){super(e),this.set("activeStyles",[]),this.set("enabledStyles",[]),this.children=this.createCollection(),this.children.delegate("execute").to(this);for(const n of t){const t=new l(e,n);this.children.add(t)}this.on("change:activeStyles",(()=>{for(const e of this.children)e.isOn=this.activeStyles.includes(e.styleDefinition.name)})),this.on("change:enabledStyles",(()=>{for(const e of this.children)e.isEnabled=this.enabledStyles.includes(e.styleDefinition.name)})),this.setTemplate({tag:"div",attributes:{class:["ck","ck-style-grid"],role:"listbox"},children:this.children})}}var k=n(561),b={injectType:"singletonStyleTag",attributes:{"data-cke":!0},insert:"head",singleton:!0};c()(k.Z,b);k.Z.locals;class h extends t.View{constructor(e,n,s){super(e),this.labelView=new t.LabelView(e),this.labelView.text=n,this.gridView=new u(e,s),this.setTemplate({tag:"div",attributes:{class:["ck","ck-style-panel__style-group"],role:"group","aria-labelledby":this.labelView.id},children:[this.labelView,this.gridView]})}}var y=n(662),g={injectType:"singletonStyleTag",attributes:{"data-cke":!0},insert:"head",singleton:!0};c()(y.Z,g);y.Z.locals;class p extends t.View{constructor(e,n){super(e);const s=e.t;this.focusTracker=new o.FocusTracker,this.keystrokes=new o.KeystrokeHandler,this.children=this.createCollection(),this.blockStylesGroupView=new h(e,s("Block styles"),n.block),this.inlineStylesGroupView=new h(e,s("Text styles"),n.inline),this.set("activeStyles",[]),this.set("enabledStyles",[]),this._focusables=new t.ViewCollection,this._focusCycler=new t.FocusCycler({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:["arrowup","arrowleft"],focusNext:["arrowdown","arrowright"]}}),n.block.length&&this.children.add(this.blockStylesGroupView),n.inline.length&&this.children.add(this.inlineStylesGroupView),this.blockStylesGroupView.gridView.delegate("execute").to(this),this.inlineStylesGroupView.gridView.delegate("execute").to(this),this.blockStylesGroupView.gridView.bind("activeStyles","enabledStyles").to(this),this.inlineStylesGroupView.gridView.bind("activeStyles","enabledStyles").to(this),this.setTemplate({tag:"div",attributes:{class:["ck","ck-style-panel"]},children:this.children})}render(){super.render();[...this.blockStylesGroupView.gridView.children,...this.inlineStylesGroupView.gridView.children].forEach((e=>{this._focusables.add(e),this.focusTracker.add(e.element)})),this.keystrokes.listenTo(this.element)}focus(){this._focusCycler.focusFirst()}focusLast(){this._focusCycler.focusLast()}}function f(e,t=[]){const n={block:[],inline:[]};for(const s of t){const t=[],o=[];for(const n of e.getDefinitionsForView(s.element))n.isBlock?t.push(n.model):o.push(n.model);t.length?n.block.push({...s,modelElements:t,isBlock:!0}):n.inline.push({...s,ghsAttributes:o})}return n}var v=n(529),m={injectType:"singletonStyleTag",attributes:{"data-cke":!0},insert:"head",singleton:!0};c()(v.Z,m);v.Z.locals;class w extends e.Plugin{static get pluginName(){return"StyleUI"}init(){const e=this.editor,n=f(e.plugins.get("DataSchema"),e.config.get("style.definitions"));e.ui.componentFactory.add("style",(s=>{const o=s.t,i=(0,t.createDropdown)(s),l=new p(s,n),r=e.commands.get("style");return i.bind("isEnabled").to(r),i.panelView.children.add(l),i.buttonView.withText=!0,i.buttonView.bind("label").to(r,"value",(e=>e.length>1?o("Multiple styles"):1===e.length?e[0]:o("Styles"))),i.bind("class").to(r,"value",(e=>{const t=["ck-style-dropdown"];return e.length>1&&t.push("ck-style-dropdown_multiple-active"),t.join(" ")})),l.delegate("execute").to(i),i.on("execute",(t=>{e.execute("style",{styleName:t.source.styleDefinition.name}),e.editing.view.focus()})),l.bind("activeStyles").to(r,"value"),l.bind("enabledStyles").to(r,"enabledStyles"),i}))}}class _ extends e.Command{constructor(e,t){super(e),this.set("value",[]),this.set("enabledStyles",[]),this._styleDefinitions=t}refresh(){const e=this.editor.model,t=e.document.selection,n=new Set,s=new Set;for(const o of this._styleDefinitions.inline)for(const i of o.ghsAttributes){e.schema.checkAttributeInSelection(t,i)&&s.add(o.name);S(this._getValueFromFirstAllowedNode(i),o.classes)&&n.add(o.name)}const i=(0,o.first)(t.getSelectedBlocks());if(i){const t=i.getAncestors({includeSelf:!0,parentFirst:!0});for(const o of t){if(e.schema.isLimit(o))break;if(e.schema.checkAttribute(o,"htmlAttributes"))for(const e of this._styleDefinitions.block){if(!e.modelElements.includes(o.name))continue;s.add(e.name);S(o.getAttribute("htmlAttributes"),e.classes)&&n.add(e.name)}}}this.enabledStyles=Array.from(s).sort(),this.isEnabled=this.enabledStyles.length>0,this.value=this.isEnabled?Array.from(n).sort():[]}execute({styleName:e,forceValue:t}){if(!this.enabledStyles.includes(e))return void(0,o.logWarning)("style-command-executed-with-incorrect-style-name");const n=this.editor.model,s=n.document.selection,i=this.editor.plugins.get("GeneralHtmlSupport"),l=[...this._styleDefinitions.inline,...this._styleDefinitions.block].find((({name:t})=>t==e)),r=void 0===t?!this.value.includes(l.name):t;n.change((()=>{let e;e=l.isBlock?function(e,t,n){const s=new Set;for(const o of e){const e=o.getAncestors({includeSelf:!0,parentFirst:!0});for(const o of e){if(n.isLimit(o))break;if(t.includes(o.name)){s.add(o);break}}}return s}(s.getSelectedBlocks(),l.modelElements,n.schema):[s];for(const t of e)r?i.addModelHtmlClass(l.element,l.classes,t):i.removeModelHtmlClass(l.element,l.classes,t)}))}_getValueFromFirstAllowedNode(e){const t=this.editor.model,n=t.schema,s=t.document.selection;if(s.isCollapsed)return s.getAttribute(e);for(const t of s.getRanges())for(const s of t.getItems())if(n.checkAttribute(s,e))return s.getAttribute(e);return null}}function S(e,t){return!(!e||!e.classes)&&t.every((t=>e.classes.includes(t)))}class x extends e.Plugin{static get pluginName(){return"StyleEditing"}static get requires(){return["GeneralHtmlSupport"]}init(){const e=this.editor,t=f(e.plugins.get("DataSchema"),e.config.get("style.definitions"));e.commands.add("style",new _(e,t)),this._configureGHSDataFilter(t)}_configureGHSDataFilter({block:e,inline:t}){const n=this.editor.plugins.get("DataFilter");n.loadAllowedConfig(e.map(T)),n.loadAllowedConfig(t.map(T))}}function T({element:e,classes:t}){return{name:e,classes:t}}class V extends e.Plugin{static get pluginName(){return"Style"}static get requires(){return[x,w]}}})(),(window.CKEditor5=window.CKEditor5||{}).style=s})(); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/ar.js b/core/assets/vendor/ckeditor5/style/translations/ar.js new file mode 100644 index 000000000000..93fad6b5ffb6 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/ar.js @@ -0,0 +1 @@ +!function(t){const s=t.ar=t.ar||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"أنماط الكتل","Multiple styles":"أنماط متعددة",Styles:"الأنماط","Text styles":"أنماط النصوص"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/bg.js b/core/assets/vendor/ckeditor5/style/translations/bg.js new file mode 100644 index 000000000000..91e98237aeec --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/bg.js @@ -0,0 +1 @@ +!function(t){const s=t.bg=t.bg||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Блокови стилове","Multiple styles":"Множество стилове",Styles:"Стилове","Text styles":"Текстови стилове"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/bn.js b/core/assets/vendor/ckeditor5/style/translations/bn.js new file mode 100644 index 000000000000..9ec8fa1e2bbf --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/bn.js @@ -0,0 +1 @@ +!function(t){const n=t.bn=t.bn||{};n.dictionary=Object.assign(n.dictionary||{},{"Block styles":"ব্লক স্টাইল","Multiple styles":"একাধিক স্টাইল",Styles:"স্টাইলস","Text styles":"টেস্কট স্টাইল"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/ca.js b/core/assets/vendor/ckeditor5/style/translations/ca.js new file mode 100644 index 000000000000..cc8ecb1b8ff0 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/ca.js @@ -0,0 +1 @@ +!function(s){const t=s.ca=s.ca||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estils de bloc","Multiple styles":"Estils múltiples",Styles:"Estils","Text styles":"Estils de text"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/cs.js b/core/assets/vendor/ckeditor5/style/translations/cs.js new file mode 100644 index 000000000000..a1eb1690d2f4 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/cs.js @@ -0,0 +1 @@ +!function(t){const s=t.cs=t.cs||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Styly bloků","Multiple styles":"Více stylů",Styles:"Styly","Text styles":"Styly textu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/da.js b/core/assets/vendor/ckeditor5/style/translations/da.js new file mode 100644 index 000000000000..a8b1356cdf7f --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/da.js @@ -0,0 +1 @@ +!function(t){const e=t.da=t.da||{};e.dictionary=Object.assign(e.dictionary||{},{"Block styles":"Blokstile","Multiple styles":"Flere stile",Styles:"Stile","Text styles":"Tekststile"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/de.js b/core/assets/vendor/ckeditor5/style/translations/de.js new file mode 100644 index 000000000000..df2dd650d6e8 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/de.js @@ -0,0 +1 @@ +!function(e){const t=e.de=e.de||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Block-Stile","Multiple styles":"Mehrere Stile",Styles:"Stile","Text styles":"Text-Stile"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/el.js b/core/assets/vendor/ckeditor5/style/translations/el.js new file mode 100644 index 000000000000..393f7ba372c7 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/el.js @@ -0,0 +1 @@ +!function(t){const s=t.el=t.el||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Στυλ για μπλοκ","Multiple styles":"Πολλαπλά στυλ",Styles:"Στυλ","Text styles":"Στυλ για κείμενο"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/en-au.js b/core/assets/vendor/ckeditor5/style/translations/en-au.js new file mode 100644 index 000000000000..10c98586ed5a --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/en-au.js @@ -0,0 +1 @@ +!function(s){const t=s["en-au"]=s["en-au"]||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Block styles","Multiple styles":"Multiple styles",Styles:"Styles","Text styles":"Text styles"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/es.js b/core/assets/vendor/ckeditor5/style/translations/es.js new file mode 100644 index 000000000000..4135e93c53f4 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/es.js @@ -0,0 +1 @@ +!function(s){const t=s.es=s.es||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estilos de bloque","Multiple styles":"Múltiples estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/et.js b/core/assets/vendor/ckeditor5/style/translations/et.js new file mode 100644 index 000000000000..5d89f84547ac --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/et.js @@ -0,0 +1 @@ +!function(i){const t=i.et=i.et||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Ploki stiilid","Multiple styles":"Mitu stiili",Styles:"Stiilid","Text styles":"Teksti stiilid"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/fi.js b/core/assets/vendor/ckeditor5/style/translations/fi.js new file mode 100644 index 000000000000..417f4e170e94 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/fi.js @@ -0,0 +1 @@ +!function(t){const i=t.fi=t.fi||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Lohkotyylit","Multiple styles":"Useita tyylejä",Styles:"Tyylit","Text styles":"Tekstityylit"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/fr.js b/core/assets/vendor/ckeditor5/style/translations/fr.js new file mode 100644 index 000000000000..6b71b0569bfc --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/fr.js @@ -0,0 +1 @@ +!function(t){const e=t.fr=t.fr||{};e.dictionary=Object.assign(e.dictionary||{},{"Block styles":"Styles de bloc","Multiple styles":"Styles multiples",Styles:"Styles","Text styles":"Styles de texte"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/gl.js b/core/assets/vendor/ckeditor5/style/translations/gl.js new file mode 100644 index 000000000000..4f325f104299 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/gl.js @@ -0,0 +1 @@ +!function(s){const t=s.gl=s.gl||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estilos de bloque","Multiple styles":"Múltiples estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/he.js b/core/assets/vendor/ckeditor5/style/translations/he.js new file mode 100644 index 000000000000..074d954b4528 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/he.js @@ -0,0 +1 @@ +!function(t){const s=t.he=t.he||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"סגנונות בלוקים","Multiple styles":"סגנונות מרובים",Styles:"סגנונות","Text styles":"עיצוב טקסט"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/hi.js b/core/assets/vendor/ckeditor5/style/translations/hi.js new file mode 100644 index 000000000000..f66d7a4e75e2 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/hi.js @@ -0,0 +1 @@ +!function(i){const t=i.hi=i.hi||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"ब्लॉक स्टाइल्स","Multiple styles":"कई स्टाइल्स",Styles:"स्टाइल्स","Text styles":"टेक्स्ट स्टाइल्स"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/hr.js b/core/assets/vendor/ckeditor5/style/translations/hr.js new file mode 100644 index 000000000000..2ae2f8c70059 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/hr.js @@ -0,0 +1 @@ +!function(i){const t=i.hr=i.hr||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blok stilovi","Multiple styles":"Više stilova",Styles:"Stilovi","Text styles":"Tekstualni stilovi"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/hu.js b/core/assets/vendor/ckeditor5/style/translations/hu.js new file mode 100644 index 000000000000..02de59f58adf --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/hu.js @@ -0,0 +1 @@ +!function(s){const t=s.hu=s.hu||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blokkstílusok","Multiple styles":"Többféle stílus",Styles:"Stílusok","Text styles":"Szövegstílusok"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/id.js b/core/assets/vendor/ckeditor5/style/translations/id.js new file mode 100644 index 000000000000..60908c8907bf --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/id.js @@ -0,0 +1 @@ +!function(a){const t=a.id=a.id||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Gaya blok","Multiple styles":"Banyak gaya",Styles:"Gaya","Text styles":"Gaya teks"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/it.js b/core/assets/vendor/ckeditor5/style/translations/it.js new file mode 100644 index 000000000000..ed8671f22f74 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/it.js @@ -0,0 +1 @@ +!function(i){const t=i.it=i.it||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Stili per blocchi","Multiple styles":"Stili multipli",Styles:"Stili","Text styles":"Stili per testi"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/ja.js b/core/assets/vendor/ckeditor5/style/translations/ja.js new file mode 100644 index 000000000000..037872263210 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/ja.js @@ -0,0 +1 @@ +!function(t){const s=t.ja=t.ja||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"ブロックスタイル","Multiple styles":"複数のスタイル",Styles:"スタイル","Text styles":"テキストスタイル"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/ko.js b/core/assets/vendor/ckeditor5/style/translations/ko.js new file mode 100644 index 000000000000..d40b801b0546 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/ko.js @@ -0,0 +1 @@ +!function(t){const s=t.ko=t.ko||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"블록 스타일","Multiple styles":"다중 스타일",Styles:"스타일","Text styles":"텍스트 스타일"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/lt.js b/core/assets/vendor/ckeditor5/style/translations/lt.js new file mode 100644 index 000000000000..8772e3931c74 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/lt.js @@ -0,0 +1 @@ +!function(i){const t=i.lt=i.lt||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blokuoti stilius","Multiple styles":"Daug stilių",Styles:"Stiliai","Text styles":"Teksto stiliai"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/lv.js b/core/assets/vendor/ckeditor5/style/translations/lv.js new file mode 100644 index 000000000000..21f954db04dd --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/lv.js @@ -0,0 +1 @@ +!function(i){const t=i.lv=i.lv||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Bloka stili","Multiple styles":"Vairāki stili",Styles:"Stili","Text styles":"Teksta stili"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/ms.js b/core/assets/vendor/ckeditor5/style/translations/ms.js new file mode 100644 index 000000000000..4310f2c021fc --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/ms.js @@ -0,0 +1 @@ +!function(s){const a=s.ms=s.ms||{};a.dictionary=Object.assign(a.dictionary||{},{"Block styles":"Gaya blok","Multiple styles":"Gaya berbilang",Styles:"Gaya","Text styles":"Gaya teks"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/nl.js b/core/assets/vendor/ckeditor5/style/translations/nl.js new file mode 100644 index 000000000000..55087d59bc8f --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/nl.js @@ -0,0 +1 @@ +!function(e){const t=e.nl=e.nl||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blok stijlen","Multiple styles":"Meerdere stijlen",Styles:"Stijlen","Text styles":"Tekst stijlen"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/no.js b/core/assets/vendor/ckeditor5/style/translations/no.js new file mode 100644 index 000000000000..1ee6bc2576fe --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/no.js @@ -0,0 +1 @@ +!function(t){const i=t.no=t.no||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Blokkstiler","Multiple styles":"Multiple stiler",Styles:"Stiler","Text styles":"Tekststiler"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/pl.js b/core/assets/vendor/ckeditor5/style/translations/pl.js new file mode 100644 index 000000000000..53e79ada91e7 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/pl.js @@ -0,0 +1 @@ +!function(t){const e=t.pl=t.pl||{};e.dictionary=Object.assign(e.dictionary||{},{"Block styles":"Style tekstu blokowego","Multiple styles":"Wiele stylów",Styles:"Style","Text styles":"Style tekstu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/pt-br.js b/core/assets/vendor/ckeditor5/style/translations/pt-br.js new file mode 100644 index 000000000000..442b3da01988 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/pt-br.js @@ -0,0 +1 @@ +!function(t){const s=t["pt-br"]=t["pt-br"]||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Estilos de bloco","Multiple styles":"Múltiplos estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/pt.js b/core/assets/vendor/ckeditor5/style/translations/pt.js new file mode 100644 index 000000000000..ebc0a534b07a --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/pt.js @@ -0,0 +1 @@ +!function(s){const t=s.pt=s.pt||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estilos de blocos","Multiple styles":"Vários estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/ro.js b/core/assets/vendor/ckeditor5/style/translations/ro.js new file mode 100644 index 000000000000..cf6c754d1cbe --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/ro.js @@ -0,0 +1 @@ +!function(t){const i=t.ro=t.ro||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Stiluri pentru blocuri","Multiple styles":"Stiluri multiple",Styles:"Stiluri","Text styles":"Stiluri pentru text"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/ru.js b/core/assets/vendor/ckeditor5/style/translations/ru.js new file mode 100644 index 000000000000..08a4e2aee000 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/ru.js @@ -0,0 +1 @@ +!function(t){const s=t.ru=t.ru||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Блочные стили","Multiple styles":"Несколько стилей",Styles:"Стили","Text styles":"Стиль текста"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/sk.js b/core/assets/vendor/ckeditor5/style/translations/sk.js new file mode 100644 index 000000000000..96d02cb631bd --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/sk.js @@ -0,0 +1 @@ +!function(t){const l=t.sk=t.sk||{};l.dictionary=Object.assign(l.dictionary||{},{"Block styles":"Štýly bloku","Multiple styles":"Viacero štýlov",Styles:"Štýly","Text styles":"Štýly textu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/sr-latn.js b/core/assets/vendor/ckeditor5/style/translations/sr-latn.js new file mode 100644 index 000000000000..44cd2cc6646b --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/sr-latn.js @@ -0,0 +1 @@ +!function(t){const i=t["sr-latn"]=t["sr-latn"]||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Blok stilovi","Multiple styles":"Više stilova",Styles:"Stilovi","Text styles":"Stilovi teksta"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/sr.js b/core/assets/vendor/ckeditor5/style/translations/sr.js new file mode 100644 index 000000000000..485c4e3d5081 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/sr.js @@ -0,0 +1 @@ +!function(s){const t=s.sr=s.sr||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Блок стилови","Multiple styles":"Више стилова",Styles:"Стилови","Text styles":"Стилови текста"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/sv.js b/core/assets/vendor/ckeditor5/style/translations/sv.js new file mode 100644 index 000000000000..27d4d9eb7fee --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/sv.js @@ -0,0 +1 @@ +!function(t){const s=t.sv=t.sv||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Blockstilar","Multiple styles":"Flera stilar",Styles:"Stilar","Text styles":"Texttyper"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/th.js b/core/assets/vendor/ckeditor5/style/translations/th.js new file mode 100644 index 000000000000..d2af76bb83e4 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/th.js @@ -0,0 +1 @@ +!function(t){const s=t.th=t.th||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"รูปแบบบล็อก","Multiple styles":"มีหลายรูปแบบ",Styles:"รูปแบบ","Text styles":"รูปแบบข้อความ"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/tr.js b/core/assets/vendor/ckeditor5/style/translations/tr.js new file mode 100644 index 000000000000..dc79d240f939 --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/tr.js @@ -0,0 +1 @@ +!function(t){const i=t.tr=t.tr||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Blok stilleri","Multiple styles":"Birden fazla stil",Styles:"Stiller","Text styles":"Metin stilleri"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/uk.js b/core/assets/vendor/ckeditor5/style/translations/uk.js new file mode 100644 index 000000000000..bdd86ff2cd1d --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/uk.js @@ -0,0 +1 @@ +!function(t){const s=t.uk=t.uk||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Стилі блоку","Multiple styles":"Кілька стилів",Styles:"Стилі","Text styles":"Стилі тексту"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/vi.js b/core/assets/vendor/ckeditor5/style/translations/vi.js new file mode 100644 index 000000000000..be0388e8db4e --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/vi.js @@ -0,0 +1 @@ +!function(i){const t=i.vi=i.vi||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Kiểu của khối","Multiple styles":"Nhiều kiểu",Styles:"Kiểu","Text styles":"Kiểu văn bản"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/zh-cn.js b/core/assets/vendor/ckeditor5/style/translations/zh-cn.js new file mode 100644 index 000000000000..e8d2ad492d0d --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/zh-cn.js @@ -0,0 +1 @@ +!function(t){const n=t["zh-cn"]=t["zh-cn"]||{};n.dictionary=Object.assign(n.dictionary||{},{"Block styles":"块级样式","Multiple styles":"多样式",Styles:"样式","Text styles":"文本样式"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/assets/vendor/ckeditor5/style/translations/zh.js b/core/assets/vendor/ckeditor5/style/translations/zh.js new file mode 100644 index 000000000000..bb265efe617a --- /dev/null +++ b/core/assets/vendor/ckeditor5/style/translations/zh.js @@ -0,0 +1 @@ +!function(t){const s=t.zh=t.zh||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"區塊樣式","Multiple styles":"多重樣式",Styles:"樣式","Text styles":"文字樣式"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); \ No newline at end of file diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 34131a2554ae..8bcb611deb16 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -302,6 +302,19 @@ ckeditor5.codeBlock: - core/ckeditor5 - core/ckeditor5.translations +ckeditor5.style: + remote: https://github.com/ckeditor/ckeditor5 + version: "35.0.1" + license: + name: GNU-GPL-2.0-or-later + url: https://github.com/ckeditor/ckeditor5/blob/v35.0.1/LICENSE.md + gpl-compatible: true + js: + assets/vendor/ckeditor5/style/style.js: { minified: true } + dependencies: + - core/ckeditor5 + - core/ckeditor5.translations + ckeditor5.translations: # No sensible version can be specified, since the translations may change at # any time. diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml index 8ed9c5733c35..a8115170d792 100644 --- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml +++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml @@ -62,6 +62,27 @@ ckeditor5_heading: -
-
+ckeditor5_style: + ckeditor5: + plugins: [style.Style] + drupal: + label: Style + library: core/ckeditor5.style + admin_library: ckeditor5/admin.style + class: Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style + toolbar_items: + style: + label: Style + # This plugin is able to add any configured class on any tag that can be + # created by some other CKEditor 5 plugin. Hence it indicates it allows all + # classes on all tags. Its subset then restricts this to a concrete set of + # tags, and a concrete set of classes. + # @todo Update in https://www.drupal.org/project/drupal/issues/3280124 + # @see \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style::getElementsSubset() + # @see \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator + elements: + - <$any-html5-element class> + ckeditor5_arbitraryHtmlSupport: ckeditor5: plugins: [htmlSupport.GeneralHtmlSupport] diff --git a/core/modules/ckeditor5/ckeditor5.libraries.yml b/core/modules/ckeditor5/ckeditor5.libraries.yml index cbe1d455ccd7..61e1d9e63cd6 100644 --- a/core/modules/ckeditor5/ckeditor5.libraries.yml +++ b/core/modules/ckeditor5/ckeditor5.libraries.yml @@ -177,6 +177,17 @@ admin.sourceEditing: theme: css/source-editing.admin.css: { } +admin.style: + js: + js/ckeditor5.style.admin.js: { } + css: + theme: + css/style.admin.css: { } + dependencies: + - core/jquery + - core/drupal + - core/drupal.vertical-tabs + admin.table: css: theme: diff --git a/core/modules/ckeditor5/ckeditor5.module b/core/modules/ckeditor5/ckeditor5.module index 1eb0293a8535..5be20aab8a7b 100644 --- a/core/modules/ckeditor5/ckeditor5.module +++ b/core/modules/ckeditor5/ckeditor5.module @@ -133,19 +133,6 @@ function ckeditor5_form_filter_format_form_alter(array &$form, FormStateInterfac if (isset($form['filters']['settings']['filter_html']['allowed_html'])) { $filter_allowed_html = &$form['filters']['settings']['filter_html']['allowed_html']; - if (isset($form['editor']['settings']['subform']['plugins']['ckeditor5_sourceEditing']['allowed_tags'])) { - $source_allowed_tags = &$form['editor']['settings']['subform']['plugins']['ckeditor5_sourceEditing']['allowed_tags']; - // @todo if this triggers the callback via keyboard navigation such as - // tab, focus should move to the next element, not to the rebuilt - // "allowed tags" field - // https://www.drupal.org/project/ckeditor5/issues/3231321. - $source_allowed_tags['#ajax'] = [ - 'callback' => '_update_ckeditor5_html_filter', - 'trigger_as' => ['name' => 'editor_configure'], - 'event' => 'change', - ]; - } - $filter_allowed_html['#value_callback'] = [CKEditor5::class, 'getGeneratedAllowedHtmlValue']; // Set readonly and add the form-disabled wrapper class as using #disabled // or the disabled attribute will prevent the new values from being @@ -214,6 +201,7 @@ function ckeditor5_form_filter_format_form_alter(array &$form, FormStateInterfac 'checkbox', 'select', 'radios', + 'textarea', ]; if (isset($plugins_config_form['#type']) && in_array($plugins_config_form['#type'], $field_types) && !isset($plugins_config_form['#ajax'])) { $plugins_config_form['#ajax'] = [ @@ -267,17 +255,22 @@ function ckeditor5_form_filter_format_form_alter(array &$form, FormStateInterfac function ckeditor5_filter_format_edit_form_submit(array $form, FormStateInterface $form_state) { $limit_allowed_html_tags = isset($form['filters']['settings']['filter_html']['allowed_html']); $manually_editable_tags = $form_state->getValue(['editor', 'settings', 'plugins', 'ckeditor5_sourceEditing', 'allowed_tags']); - if ($limit_allowed_html_tags && is_array($manually_editable_tags)) { - // When "Manually editable tags" and "limit allowed HTML tags" are both - // configured, the former informs the value of the latter. This dependent + $styles = $form_state->getValue(['editor', 'settings', 'plugins', 'ckeditor5_style', 'styles']); + if ($limit_allowed_html_tags && is_array($manually_editable_tags) || is_array($styles)) { + // When "Manually editable tags", "Style" and "limit allowed HTML tags" are + // all configured, the latter is dependent on the others. This dependent // value is typically updated via AJAX, but it's possible for "Manually // editable tags" to update without triggering the AJAX rebuild. That value // is recalculated here on save to ensure it happens even if the AJAX // rebuild doesn't happen. - $manually_editable_tags_restrictions = HTMLRestrictions::fromString(implode($manually_editable_tags)); + $manually_editable_tags_restrictions = HTMLRestrictions::fromString(implode($manually_editable_tags ?? [])); + $styles_restrictions = HTMLRestrictions::fromString(implode($styles ? array_column($styles, 'element') : [])); $format = $form_state->get('ckeditor5_validated_pair')->getFilterFormat(); $allowed_html = HTMLRestrictions::fromTextFormat($format); - $combined_tags_string = $manually_editable_tags_restrictions->merge($allowed_html)->toFilterHtmlAllowedTagsString(); + $combined_tags_string = $allowed_html + ->merge($manually_editable_tags_restrictions) + ->merge($styles_restrictions) + ->toFilterHtmlAllowedTagsString(); $form_state->setValue(['filters', 'filter_html', 'settings', 'allowed_html'], $combined_tags_string); } } diff --git a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml index c58dce3fed5b..7b358f1e2adb 100644 --- a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml +++ b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml @@ -73,6 +73,7 @@ ckeditor5.plugin.ckeditor5_imageResize: constraints: NotNull: [] +# Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing ckeditor5.plugin.ckeditor5_sourceEditing: type: mapping label: Source Editing @@ -134,3 +135,35 @@ ckeditor5.plugin.media_media: label: 'Allow view mode override' constraints: NotNull: [] + +# Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style +ckeditor5.plugin.ckeditor5_style: + type: mapping + label: Style + mapping: + styles: + type: sequence + label: 'Styles' + constraints: + NotBlank: + message: "Enable at least one style, otherwise disable the Style plugin." + UniqueLabelInList: + labelKey: label + sequence: + type: mapping + label: 'Style' + mapping: + label: + type: label + label: 'Style label' + element: + type: ckeditor5.element + constraints: + # Validate that this contains exactly 1 attribute (class) and >=1 class attr value. + CKEditor5Element: + requiredAttributes: + - + attributeName: class + minAttributeValueCount: 1 + StyleSensibleElement: [] + label: 'Style tag + classes' diff --git a/core/modules/ckeditor5/css/style.admin.css b/core/modules/ckeditor5/css/style.admin.css new file mode 100644 index 000000000000..6cb5b6bd5303 --- /dev/null +++ b/core/modules/ckeditor5/css/style.admin.css @@ -0,0 +1,30 @@ +.ckeditor5-toolbar-button-style { + display: flex; + align-items: center; + justify-content: space-between; + width: 110px; + color: #000; +} +.ckeditor5-toolbar-button-style::before { + margin-left: 10px; + content: "Style"; + font-size: 14px; +} +[dir="rtl"] .ckeditor5-toolbar-button-style::before { + margin-right: 10px; + margin-left: 0; +} +.ckeditor5-toolbar-button-style::after { + display: inline-block; + width: 7px; + height: 7px; + margin-right: 10px; + content: ""; + transform: rotate(135deg); + border-width: 2px 2px 0 0; + border-style: solid; +} +[dir="rtl"] .ckeditor5-toolbar-button-style::after { + margin-right: 0; + margin-left: 10px; +} diff --git a/core/modules/ckeditor5/js/build/ckeditor5.types.jsdoc b/core/modules/ckeditor5/js/build/ckeditor5.types.jsdoc index 188458461d85..64d6266a285b 100644 --- a/core/modules/ckeditor5/js/build/ckeditor5.types.jsdoc +++ b/core/modules/ckeditor5/js/build/ckeditor5.types.jsdoc @@ -1942,6 +1942,54 @@ * @typedef {module:special-characters/ui/specialcharactersnavigationview} module:special-characters/ui/specialcharactersnavigationview~SpecialCharactersNavigationView */ +/** + * Declared in file @ckeditor/ckeditor5-style/src/style.js + * + * @typedef {module:style/style} module:style/style~Style + */ + +/** + * Declared in file @ckeditor/ckeditor5-style/src/stylecommand.js + * + * @typedef {module:style/stylecommand} module:style/stylecommand~StyleCommand + */ + +/** + * Declared in file @ckeditor/ckeditor5-style/src/styleediting.js + * + * @typedef {module:style/styleediting} module:style/styleediting~StyleEditing + */ + +/** + * Declared in file @ckeditor/ckeditor5-style/src/styleui.js + * + * @typedef {module:style/styleui} module:style/styleui~StyleUI + */ + +/** + * Declared in file @ckeditor/ckeditor5-style/src/ui/stylegridbuttonview.js + * + * @typedef {module:style/ui/stylegridbuttonview} module:style/ui/stylegridbuttonview~StyleGridButtonView + */ + +/** + * Declared in file @ckeditor/ckeditor5-style/src/ui/stylegridview.js + * + * @typedef {module:style/ui/stylegridview} module:style/ui/stylegridview~StyleGridView + */ + +/** + * Declared in file @ckeditor/ckeditor5-style/src/ui/stylegroupview.js + * + * @typedef {module:style/ui/stylegroupview} module:style/ui/stylegroupview~StyleGroupView + */ + +/** + * Declared in file @ckeditor/ckeditor5-style/src/ui/stylepanelview.js + * + * @typedef {module:style/ui/stylepanelview} module:style/ui/stylepanelview~StylePanelView + */ + /** * Declared in file @ckeditor/ckeditor5-table/src/commands/insertcolumncommand.js * diff --git a/core/modules/ckeditor5/js/ckeditor5.style.admin.es6.js b/core/modules/ckeditor5/js/ckeditor5.style.admin.es6.js new file mode 100644 index 000000000000..f44521613513 --- /dev/null +++ b/core/modules/ckeditor5/js/ckeditor5.style.admin.es6.js @@ -0,0 +1,39 @@ +/** + * @file + * CKEditor 5 Style admin behavior. + */ + +(function ($, Drupal) { + /** + * Provides the summary for the "style" plugin settings vertical tab. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches summary behavior to the plugin settings vertical tab. + */ + Drupal.behaviors.ckeditor5StyleSettingsSummary = { + attach() { + $('[data-ckeditor5-plugin-id="ckeditor5_style"]').drupalSetSummary( + (context) => { + const stylesElement = document.querySelector( + '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]', + ); + const styleCount = stylesElement.value + .split('\n') + // Minimum length is 5: "p.z|Z" is the shortest possible style definition. + .filter((line) => line.trim().length >= 5).length; + + if (styleCount === 0) { + return Drupal.t('No styles configured'); + } + return Drupal.formatPlural( + styleCount, + 'One style configured', + '@count styles configured', + ); + }, + ); + }, + }; +})(jQuery, Drupal); diff --git a/core/modules/ckeditor5/js/ckeditor5.style.admin.js b/core/modules/ckeditor5/js/ckeditor5.style.admin.js new file mode 100644 index 000000000000..ffdf5db5c468 --- /dev/null +++ b/core/modules/ckeditor5/js/ckeditor5.style.admin.js @@ -0,0 +1,24 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, Drupal) { + Drupal.behaviors.ckeditor5StyleSettingsSummary = { + attach() { + $('[data-ckeditor5-plugin-id="ckeditor5_style"]').drupalSetSummary(context => { + const stylesElement = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]'); + const styleCount = stylesElement.value.split('\n').filter(line => line.trim().length >= 5).length; + + if (styleCount === 0) { + return Drupal.t('No styles configured'); + } + + return Drupal.formatPlural(styleCount, 'One style configured', '@count styles configured'); + }); + } + + }; +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/modules/ckeditor5/src/HTMLRestrictions.php b/core/modules/ckeditor5/src/HTMLRestrictions.php index 4057271530be..bf81037fef9d 100644 --- a/core/modules/ckeditor5/src/HTMLRestrictions.php +++ b/core/modules/ckeditor5/src/HTMLRestrictions.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\DiffArray; use Drupal\filter\FilterFormatInterface; use Drupal\filter\Plugin\Filter\FilterHtml; use Drupal\filter\Plugin\FilterInterface; +use Masterminds\HTML5\Elements; /** * Represents a set of HTML restrictions. @@ -66,6 +67,7 @@ final class HTMLRestrictions { * @var string[] */ private const WILDCARD_ELEMENT_METHODS = [ + '$any-html5-element' => 'getHtml5ElementList', '$text-container' => 'getTextContainerElementList', ]; @@ -1188,6 +1190,16 @@ final class HTMLRestrictions { ]; } + /** + * Gets a list of all known HTML5 elements. + * + * @return string[] + * An array of HTML5 element tags. + */ + private static function getHtml5ElementList(): array { + return array_keys(Elements::$html5); + } + /** * Computes the tags that match the provided wildcard. * diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor4To5Upgrade/Core.php b/core/modules/ckeditor5/src/Plugin/CKEditor4To5Upgrade/Core.php index 25e132222a18..14b3c74eebb0 100644 --- a/core/modules/ckeditor5/src/Plugin/CKEditor4To5Upgrade/Core.php +++ b/core/modules/ckeditor5/src/Plugin/CKEditor4To5Upgrade/Core.php @@ -6,6 +6,7 @@ namespace Drupal\ckeditor5\Plugin\CKEditor4To5Upgrade; use Drupal\ckeditor5\HTMLRestrictions; use Drupal\ckeditor5\Plugin\CKEditor4To5UpgradePluginInterface; +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style; use Drupal\Core\Plugin\PluginBase; use Drupal\filter\FilterFormatInterface; @@ -58,10 +59,11 @@ use Drupal\filter\FilterFormatInterface; * "language", * }, * cke5_plugin_elements_subset_configuration = { - * "ckeditor5_heading", - * "ckeditor5_alignment", - * "ckeditor5_list", - * "media_media", + * "ckeditor5_heading", + * "ckeditor5_alignment", + * "ckeditor5_list", + * "ckeditor5_style", + * "media_media", * } * ) * @@ -163,8 +165,7 @@ class Core extends PluginBase implements CKEditor4To5UpgradePluginInterface { // @see \Drupal\ckeditor\Plugin\CKEditorPlugin\StylesCombo case 'Styles': - // @todo Change in https://www.drupal.org/project/ckeditor5/issues/3222797 - return NULL; + return ['style']; // @see \Drupal\ckeditor5\Plugin\CKEditor5Plugin\specialCharacters case 'SpecialChar': @@ -190,8 +191,17 @@ class Core extends PluginBase implements CKEditor4To5UpgradePluginInterface { switch ($cke4_plugin_id) { // @see \Drupal\ckeditor\Plugin\CKEditorPlugin\StylesCombo case 'stylescombo': - // @todo Change in https://www.drupal.org/project/ckeditor5/issues/3222797 - return NULL; + if (!isset($cke4_plugin_settings['styles'])) { + $styles = []; + } + else { + [$styles] = Style::parseStylesFormValue($cke4_plugin_settings['styles']); + } + return [ + 'ckeditor5_style' => [ + 'styles' => $styles, + ], + ]; // @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Language case 'language': @@ -284,6 +294,10 @@ class Core extends PluginBase implements CKEditor4To5UpgradePluginInterface { $configuration['allow_view_mode_override'] = !empty($restrictions['allowed']['drupal-media']['data-view-mode']); return $configuration; + case 'ckeditor5_style': + // @see mapCKEditor4SettingsToCKEditor5Configuration() + return NULL; + default: throw new \OutOfBoundsException(); } diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php index 3adfaa3040ba..6e9ea5bf2807 100644 --- a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php +++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php @@ -40,12 +40,12 @@ class SourceEditing extends CKEditor5PluginDefault implements CKEditor5PluginCon * {@inheritdoc} */ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { - // Match the config schema structure at ckeditor5.plugin.ckeditor5_heading. + // Match the config schema structure at + // ckeditor5.plugin.ckeditor5_sourceEditing. $form_value = $form_state->getValue('allowed_tags'); - if (!is_array($form_value)) { - $config_value = HTMLRestrictions::fromString($form_value)->toCKEditor5ElementsArray(); - $form_state->setValue('allowed_tags', $config_value); - } + assert(is_string($form_value)); + $config_value = HTMLRestrictions::fromString($form_value)->toCKEditor5ElementsArray(); + $form_state->setValue('allowed_tags', $config_value); } /** diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Style.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Style.php new file mode 100644 index 000000000000..89e2d23c9416 --- /dev/null +++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Style.php @@ -0,0 +1,185 @@ + $this->t('Styles'), + '#type' => 'textarea', + '#description' => $this->t('A list of classes that will be provided in the "Style" dropdown. Enter one or more classes on each line in the format: element.classA.classB|Label. Example: h1.title|Title. Advanced example: h1.fancy.title|Fancy title.
These styles should be available in your theme\'s CSS file.'), + ]; + if (!empty($this->configuration['styles'])) { + $as_selectors = ''; + foreach ($this->configuration['styles'] as $style) { + [$tag, $classes] = self::getTagAndClasses(HTMLRestrictions::fromString($style['element'])); + $as_selectors .= sprintf("%s.%s|%s\n", $tag, implode('.', $classes), $style['label']); + } + $form['styles']['#default_value'] = $as_selectors; + } + + return $form; + } + + /** + * Gets the tag and classes for a parsed style element. + * + * @param \Drupal\ckeditor5\HTMLRestrictions $style_element + * A parsed style element. + * + * @return array + * An array containing two values: + * - a HTML tag name + * - a list of classes + * + * @internal + */ + public static function getTagAndClasses(HTMLRestrictions $style_element): array { + $tag = array_keys($style_element->getAllowedElements())[0]; + $classes = array_keys($style_element->getAllowedElements()[$tag]['class']); + return [$tag, $classes]; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + // Match the config schema structure at ckeditor5.plugin.ckeditor5_style. + $form_value = $form_state->getValue('styles'); + [$styles, $unparseable_lines] = self::parseStylesFormValue($form_value); + if (!empty($unparseable_lines)) { + $line_numbers = array_keys($unparseable_lines); + $form_state->setError($form['styles'], $this->formatPlural( + count($unparseable_lines), + 'Line @line-number does not contain a valid value. Enter a valid CSS selector containing one or more classes, followed by a pipe symbol and a label.', + 'Lines @line-numbers do not contain a valid value. Enter a valid CSS selector containing one or more classes, followed by a pipe symbol and a label.', + [ + '@line-number' => reset($line_numbers), + '@line-numbers' => implode(', ', $line_numbers), + ] + )); + } + $form_state->setValue('styles', $styles); + } + + /** + * Parses the line-based (for form) style configuration. + * + * @param string $form_value + * A string containing >=1 lines with on each line a CSS selector targeting + * 1 tag with >=1 classes, a pipe symbol and a label. An example of a single + * line: `p.foo.bar|Foo bar paragraph`. + * + * @return array + * The parsed equivalent: a list of arrays with each containing: + * - label: the label after the pipe symbol, with whitespace trimmed + * - element: the CKEditor 5 element equivalent of the tag + classes + * + * @internal + * This method is public only to allow the CKEditor 4 to 5 upgrade path to + * reuse this logic. Mark this private in https://www.drupal.org/i/3239012. + * + * @see \Drupal\ckeditor5\Plugin\CKEditor4To5Upgrade\Core::mapCKEditor4SettingsToCKEditor5Configuration() + */ + public static function parseStylesFormValue(string $form_value): array { + $unparseable_lines = []; + + $lines = explode("\n", $form_value); + $styles = []; + foreach ($lines as $index => $line) { + if (empty(trim($line))) { + continue; + } + + // Parse the line. + [$selector, $label] = array_map('trim', explode('|', $line)); + + // Validate the selector. + $selector_matches = []; + // @see https://www.w3.org/TR/CSS2/syndata.html#:~:text=In%20CSS%2C%20identifiers%20(including%20element,hyphen%20followed%20by%20a%20digit + if (!preg_match('/^([a-z][0-9a-zA-Z\-]*)((\.[a-zA-Z0-9\x{00A0}-\x{FFFF}\-_]+)+)$/u', $selector, $selector_matches)) { + $unparseable_lines[$index + 1] = $line; + continue; + } + + // Parse selector into tag + classes and normalize. + $tag = $selector_matches[1]; + $classes = array_filter(explode('.', $selector_matches[2])); + $normalized = HTMLRestrictions::fromString(sprintf('<%s class="%s">', $tag, implode(' ', $classes))); + + $styles[] = [ + 'label' => $label, + 'element' => $normalized->toCKEditor5ElementsArray()[0], + ]; + } + return [$styles, $unparseable_lines]; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['styles'] = $form_state->getValue('styles'); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'styles' => [], + ]; + } + + /** + * {@inheritdoc} + */ + public function getElementsSubset(): array { + return array_column($this->configuration['styles'], 'element'); + } + + /** + * {@inheritdoc} + */ + public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array { + $definitions = []; + foreach ($this->configuration['styles'] as $style) { + [$tag, $classes] = self::getTagAndClasses(HTMLRestrictions::fromString($style['element'])); + // Transform configured styles to the configuration structure expected by + // the CKEditor 5 Style plugin. + $definitions[] = [ + 'name' => $style['label'], + 'element' => $tag, + 'classes' => $classes, + ]; + } + return [ + 'style' => [ + 'definitions' => $definitions, + ], + ]; + } + +} diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php index a7933a122e93..fe2d7992cd01 100644 --- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php +++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php @@ -331,10 +331,26 @@ class CKEditor5PluginManager extends DefaultPluginManager implements CKEditor5Pl $subset = $this->getPlugin($id, $editor)->getElementsSubset(); $subset_restrictions = HTMLRestrictions::fromString(implode($subset)); $defined_restrictions = HTMLRestrictions::fromString(implode($defined_elements)); - $subset_violations = $subset_restrictions->diff($defined_restrictions)->toCKEditor5ElementsArray(); - if (!empty($subset_violations)) { - throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: "%s".', $id, implode(' ', $subset_violations))); + // Determine max supported elements by resolving wildcards in the + // restrictions defined by the plugin. + $max_supported = $defined_restrictions; + if (!$defined_restrictions->getWildcardSubset()->allowsNothing()) { + $concrete_tags_to_use_to_resolve_wildcards = $subset_restrictions->extractPlainTagsSubset(); + $max_supported = $max_supported->merge($concrete_tags_to_use_to_resolve_wildcards) + ->diff($concrete_tags_to_use_to_resolve_wildcards); } + $not_in_max_supported = $subset_restrictions->diff($max_supported); + if (!$not_in_max_supported->allowsNothing()) { + // If the editor is still being configured, the configuration may + // not yet be valid. + if ($editor->isNew()) { + $subset = []; + } + else { + throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: "%s".', $id, implode(' ', $not_in_max_supported->toCKEditor5ElementsArray()))); + } + } + // Also detect what is technically a valid subset, but has lost the // ability to create tags that are still in the subset. This points to // a bug in the plugin's ::getElementsSubset() logic. @@ -346,6 +362,7 @@ class CKEditor5PluginManager extends DefaultPluginManager implements CKEditor5Pl if (!$missing_creatable_for_subset->allowsNothing()) { throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did return a subset ("%s") but the following tags can no longer be created: "%s".', $id, implode($subset_restrictions->toCKEditor5ElementsArray()), implode($missing_creatable_for_subset->toCKEditor5ElementsArray()))); } + $defined_elements = $subset; } } diff --git a/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php b/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php index 3bc43daf7eeb..eb3c6df06e2d 100644 --- a/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php +++ b/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php @@ -623,6 +623,7 @@ class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface { $submitted_editor->setSettings($settings); $eventual_editor_and_format_for_plugin_settings_visibility = $this->getEventualEditorWithPrimedFilterFormat($form_state, $submitted_editor); $settings['plugins'] = []; + $default_configurations = []; foreach ($this->ckeditor5PluginManager->getDefinitions() as $plugin_id => $definition) { if (!$definition->isConfigurable()) { continue; @@ -636,6 +637,12 @@ class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface { // @see editor_image_upload_settings_form() $default_configuration = $plugin->defaultConfiguration(); $configuration_stored_out_of_band = empty($default_configuration); + // If this plugin is configurable but has not yet had user interaction, + // the default configuration will still be active and may trigger + // validation errors. Do not trigger those validation errors until the + // form is actually saved, to allow the user to first configure other + // CKEditor 5 functionality. + $default_configurations[$plugin_id] = $default_configuration; if ($form_state->hasValue(['plugins', $plugin_id])) { $subform = $form['plugins'][$plugin_id]; @@ -672,6 +679,31 @@ class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface { $eventual_editor_and_format = $this->getEventualEditorWithPrimedFilterFormat($form_state, $submitted_editor); $violations = CKEditor5::validatePair($eventual_editor_and_format, $eventual_editor_and_format->getFilterFormat()); foreach ($violations as $violation) { + $property_path_parts = explode('.', $violation->getPropertyPath()); + + // Special case: AJAX updates that do not submit the form (that cannot + // result in configuration being saved). + if ($form_state->getSubmitHandlers() === ['editor_form_filter_admin_format_editor_configure']) { + // Ensure that plugins' validation constraints do not immediately + // trigger a validation error: the user may choose to configure other + // CKEditor 5 aspects first. + if ($property_path_parts[0] === 'settings' && $property_path_parts[1] === 'plugins') { + $plugin_id = $property_path_parts[2]; + // This CKEditor 5 plugin settings form was just added: the user has + // not yet had a chance to configure it. + if (!$form_state->hasValue(['plugins', $plugin_id])) { + continue; + } + // This CKEditor 5 plugin settings form was added recently, the user + // is triggering AJAX rebuilds of the configuration UI because they're + // configuring other functionality first. Only require these to be + // valid at form submission time. + if ($form_state->getValue(['plugins', $plugin_id]) === $default_configurations[$plugin_id]) { + continue; + } + } + } + $form_item_name = static::mapPairViolationPropertyPathsToFormNames($violation->getPropertyPath(), $form); // When adding a toolbar item, it is possible that not all conditions for // using it have been met yet. FormBuilder refuses to rebuild forms when a @@ -816,6 +848,11 @@ class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface { */ protected static function createEphemeralPairedEditor(EditorInterface $editor, FilterFormatInterface $filter_format): EditorInterface { $paired_editor = clone $editor; + // If the editor is still being configured, the configuration may not yet be + // valid. Explicitly mark the ephemeral paired editor as new to allow other + // code to treat this accordingly. + // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getProvidedElements() + $paired_editor->enforceIsNew(TRUE); $reflector = new \ReflectionObject($paired_editor); $property = $reflector->getProperty('filterFormat'); $property->setAccessible(TRUE); diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraint.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraint.php index b9c3dc6062af..a80feafd4252 100644 --- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraint.php +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraint.php @@ -23,4 +23,25 @@ class CKEditor5ElementConstraint extends Constraint { */ public $message = 'The following tag is not valid HTML: %provided_element.'; + /** + * Violation message when a required attribute is missing. + * + * @var string + */ + public $missingRequiredAttributeMessage = 'The following tag is missing the required attribute @required_attribute_name: @provided_element.'; + + /** + * Violation message when a required attribute does not allow enough values. + * + * @var string + */ + public $requiredAttributeMinValuesMessage = 'The following tag does not have the minimum of @min_attribute_value_count allowed values for the required attribute @required_attribute_name: @provided_element.'; + + /** + * Validation constraint option to impose attributes to be specified. + * + * @var null|array + */ + public $requiredAttributes = NULL; + } diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php index 285e57bc607b..c8a361449f10 100644 --- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php @@ -32,6 +32,41 @@ class CKEditor5ElementConstraintValidator extends ConstraintValidator { ->setParameter('%provided_element', $element) ->addViolation(); } + + // The optional "requiredAttributes" constraint property allows more + // detailed validation. + if (isset($constraint->requiredAttributes)) { + $allowed_elements = $parsed->getAllowedElements(); + $tag = array_keys($allowed_elements)[0]; + $attribute_restrictions = $allowed_elements[$tag]; + assert(is_array($constraint->requiredAttributes)); + foreach ($constraint->requiredAttributes as $required_attribute) { + // Validate attributeName. + $required_attribute_name = $required_attribute['attributeName']; + if (!is_array($attribute_restrictions) || !isset($attribute_restrictions[$required_attribute_name])) { + $this->context->buildViolation($constraint->missingRequiredAttributeMessage) + ->setParameter('@provided_element', $element) + ->setParameter('@required_attribute_name', $required_attribute_name) + ->addViolation(); + continue; + } + + $attribute_values = $attribute_restrictions[$required_attribute_name]; + + // Validate minAttributeValueCount if specified. + if (isset($required_attribute['minAttributeValueCount'])) { + $min_attribute_value_count = $required_attribute['minAttributeValueCount']; + if (!is_array($attribute_values) || count($attribute_values) < $min_attribute_value_count) { + $this->context->buildViolation($constraint->requiredAttributeMinValuesMessage) + ->setParameter('@provided_element', $element) + ->setParameter('@required_attribute_name', $required_attribute_name) + ->setParameter('@min_attribute_value_count', $min_attribute_value_count) + ->addViolation(); + continue; + } + } + } + } } } diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php index cb8ce30579b9..1cac8c16b15c 100644 --- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php @@ -6,6 +6,7 @@ namespace Drupal\ckeditor5\Plugin\Validation\Constraint; use Drupal\ckeditor5\HTMLRestrictions; use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition; +use Drupal\ckeditor5\Plugin\CKEditor5PluginElementsSubsetInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\editor\EditorInterface; use Drupal\filter\FilterFormatInterface; @@ -199,32 +200,36 @@ class FundamentalCompatibilityConstraintValidator extends ConstraintValidator im foreach ($non_creatable_tags->toCKEditor5ElementsArray() as $non_creatable_tag) { // Find the plugin which has a non-creatable tag. $needle = HTMLRestrictions::fromString($non_creatable_tag); - $matching_plugins = array_filter($enabled_definitions, function (CKEditor5PluginDefinition $d) use ($needle) { + $matching_plugins = array_filter($enabled_definitions, function (CKEditor5PluginDefinition $d) use ($needle, $text_editor) { if (!$d->hasElements()) { return FALSE; } - $haystack = HTMLRestrictions::fromString(implode($d->getElements())); - return !$haystack->intersect($needle)->allowsNothing(); + $haystack = new HTMLRestrictions($this->pluginManager->getProvidedElements([$d->id()], $text_editor, FALSE, FALSE)); + return !$haystack->extractPlainTagsSubset()->intersect($needle)->allowsNothing(); }); assert(count($matching_plugins) === 1); $plugin_definition = reset($matching_plugins); assert($plugin_definition instanceof CKEditor5PluginDefinition); // Compute which attributes it would be able to create on this tag. - $matching_elements = array_filter($plugin_definition->getElements(), function (string $element) use ($needle) { - $haystack = HTMLRestrictions::fromString($element); - return !$haystack->intersect($needle)->allowsNothing(); - }); - $attributes_on_tag = HTMLRestrictions::fromString(implode($matching_elements)); + $provided_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements([$plugin_definition->id()], $text_editor, FALSE, FALSE)); + $attributes_on_tag = $provided_elements->intersect( + new HTMLRestrictions(array_fill_keys(array_keys($needle->getAllowedElements()), TRUE)) + ); $violation = $this->context->buildViolation($constraint->nonCreatableTagMessage) ->setParameter('@non_creatable_tag', $non_creatable_tag) ->setParameter('%plugin', $plugin_definition->label()) ->setParameter('@attributes_on_tag', implode(', ', $attributes_on_tag->toCKEditor5ElementsArray())); + // If this plugin has a configurable subset, associate the violation + // with the property path pointing to this plugin's settings form. + if (is_a($plugin_definition->getClass(), CKEditor5PluginElementsSubsetInterface::class, TRUE)) { + $violation->atPath(sprintf('settings.plugins.%s', $plugin_definition->id())); + } // If this plugin is associated with a toolbar item, associate the // violation with the property path pointing to the active toolbar item. - if ($plugin_definition->hasToolbarItems()) { + elseif ($plugin_definition->hasToolbarItems()) { $toolbar_items = $plugin_definition->getToolbarItems(); $active_toolbar_items = array_intersect( $text_editor->getSettings()['toolbar']['items'], diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PluginManagerDependentValidatorTrait.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PluginManagerDependentValidatorTrait.php index ae52ece32d48..12d4ec8a4da1 100644 --- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PluginManagerDependentValidatorTrait.php +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PluginManagerDependentValidatorTrait.php @@ -4,7 +4,11 @@ declare(strict_types = 1); namespace Drupal\ckeditor5\Plugin\Validation\Constraint; +// cspell:ignore enableable + +use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition; use Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface; +use Drupal\editor\EditorInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -40,4 +44,49 @@ trait PluginManagerDependentValidatorTrait { ); } + /** + * Gets all other enabled CKEditor 5 plugin definitions. + * + * @param \Drupal\editor\EditorInterface $text_editor + * A Text Editor config entity configured to use CKEditor 5. + * @param string $except + * A CKEditor 5 plugin ID to exclude: all enabled plugins other than this + * one are returned. + * + * @return \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition[] + * A list of CKEditor 5 plugin definitions keyed by plugin ID. + */ + private function getOtherEnabledPlugins(EditorInterface $text_editor, string $except): array { + $enabled_plugins = $this->pluginManager->getEnabledDefinitions($text_editor); + unset($enabled_plugins[$except]); + return $enabled_plugins; + } + + /** + * Gets all disabled CKEditor 5 plugin definitions the user can enable. + * + * @param \Drupal\editor\EditorInterface $text_editor + * A Text Editor config entity configured to use CKEditor 5. + * + * @return \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition[] + * A list of CKEditor 5 plugin definitions keyed by plugin ID. + */ + private function getEnableableDisabledPlugins(EditorInterface $text_editor) { + $disabled_plugins = array_diff_key( + $this->pluginManager->getDefinitions(), + $this->pluginManager->getEnabledDefinitions($text_editor) + ); + // Only consider plugins that can be explicitly enabled by the user: plugins + // that have a toolbar item and do not have conditions. Those are the only + // plugins that are truly available for the site builder to enable without + // other consequences. + // In the future, we may choose to expand this, but it will require complex + // infrastructure to generate messages that explain which of the conditions + // are already fulfilled and which are not. + $enableable_disabled_plugins = array_filter($disabled_plugins, function (CKEditor5PluginDefinition $definition) { + return $definition->hasToolbarItems() && !$definition->hasConditions(); + }); + return $enableable_disabled_plugins; + } + } diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PrecedingConstraintAwareValidatorTrait.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PrecedingConstraintAwareValidatorTrait.php new file mode 100644 index 000000000000..0e6ae75076cc --- /dev/null +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/PrecedingConstraintAwareValidatorTrait.php @@ -0,0 +1,61 @@ +context instanceof ExecutionContext); + $earlier_constraints = iterator_to_array($this->getPrecedingConstraints($current_constraint)); + $earlier_violations = array_filter( + iterator_to_array($this->context->getViolations()), + function (ConstraintViolationInterface $violation) use ($earlier_constraints) { + return in_array($violation->getConstraint(), $earlier_constraints); + } + ); + return !empty($earlier_violations); + } + + /** + * Gets the constraints preceding the given constraint in the current context. + * + * @param \Symfony\Component\Validator\Constraint $needle + * The constraint to find the preceding constraints for. + * + * @return iterable + * The preceding constraints. + */ + private function getPrecedingConstraints(Constraint $needle): iterable { + assert($this->context instanceof ExecutionContext); + $constraints = $this->context->getMetadata()->getConstraints(); + if (!in_array($needle, $constraints)) { + throw new \OutOfBoundsException(); + } + foreach ($constraints as $constraint) { + if ($constraint != $needle) { + yield $constraint; + } + } + } + +} diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php index 0dcb00259448..f0ff31e5c5c4 100644 --- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php @@ -4,8 +4,9 @@ declare(strict_types = 1); namespace Drupal\ckeditor5\Plugin\Validation\Constraint; +// cspell:ignore enableable + use Drupal\ckeditor5\HTMLRestrictions; -use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\Validator\Constraint; @@ -38,25 +39,15 @@ class SourceEditingRedundantTagsConstraintValidator extends ConstraintValidator } $text_editor = $this->createTextEditorObjectFromContext(); - $enabled_plugins = $this->pluginManager->getEnabledDefinitions($text_editor); - $disabled_plugins = array_diff_key($this->pluginManager->getDefinitions(), $enabled_plugins); - // Only consider plugins that can be explicitly enabled by the user: plugins - // that have a toolbar item and do not have conditions. Those are the only - // plugins that are truly available for the site builder to enable without - // other consequences. - // In the future, we may choose to expand this, but it will require complex - // infrastructure to generate messages that explain which of the conditions - // are already fulfilled and which are not. - $disabled_plugins = array_filter($disabled_plugins, function (CKEditor5PluginDefinition $definition) { - return $definition->hasToolbarItems() && !$definition->hasConditions(); - }); - unset($enabled_plugins['ckeditor5_sourceEditing']); + + $other_enabled_plugins = $this->getOtherEnabledPlugins($text_editor, 'ckeditor5_sourceEditing'); + $enableable_disabled_plugins = $this->getEnableableDisabledPlugins($text_editor); // An array of tags enabled by every plugin other than Source Editing. - $enabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enabled_plugins), $text_editor, FALSE)); - $disabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($disabled_plugins), $text_editor, FALSE)); - $enabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enabled_plugins), $text_editor, FALSE, TRUE)); - $disabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($disabled_plugins), $text_editor, FALSE, TRUE)); + $enabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE)); + $disabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE)); + $enabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE, TRUE)); + $disabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE, TRUE)); // The single element for which source editing is enabled, which we are // checking now. @@ -85,7 +76,7 @@ class SourceEditingRedundantTagsConstraintValidator extends ConstraintValidator foreach ([$enabled_plugin_overlap, $disabled_plugin_overlap] as $overlap) { $checking_enabled = $overlap === $enabled_plugin_overlap; if (!$overlap->allowsNothing()) { - $plugins_to_check_against = $checking_enabled ? $enabled_plugins : $disabled_plugins; + $plugins_to_check_against = $checking_enabled ? $other_enabled_plugins : $enableable_disabled_plugins; $plain_tags_to_check_against = $checking_enabled ? $enabled_plugin_plain_tags : $disabled_plugin_plain_tags; $tags_plugin_report = $this->pluginsSupplyingTagsMessage($overlap, $plugins_to_check_against, $enabled_plugin_elements); $message = $checking_enabled ? $constraint->enabledPluginsMessage : $constraint->availablePluginsMessage; diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraint.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraint.php new file mode 100644 index 000000000000..68d473dd7e8d --- /dev/null +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraint.php @@ -0,0 +1,44 @@ +@tag is not an HTML5 tag.'; + + /** + * When a Style is defined with classes supported by an enabled plugin. + * + * @var string + */ + public $conflictingEnabledPluginMessage = 'A style must only specify classes not supported by other plugins. The @classes classes on @tag are already supported by the enabled %plugin plugin.'; + + /** + * When a Style is defined with classes supported by a disabled plugin. + * + * @var string + */ + public $conflictingDisabledPluginMessage = 'A style must only specify classes not supported by other plugins. The @classes classes on @tag are supported by the %plugin plugin. Remove this style and enable that plugin instead.'; + +} diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraintValidator.php new file mode 100644 index 000000000000..4be7d8ba1402 --- /dev/null +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraintValidator.php @@ -0,0 +1,165 @@ +hasViolationsForPrecedingConstraints($constraint)) { + return; + } + + $text_editor = $this->createTextEditorObjectFromContext(); + + // The single tag for which a style is specified, which we are checking now. + $style_element = HTMLRestrictions::fromString($element); + assert(count($style_element->getAllowedElements()) === 1); + [$tag, $classes] = Style::getTagAndClasses($style_element); + + // Ensure the tag is in the range supported by the Style plugin. + $superset = HTMLRestrictions::fromString('<$any-html5-element class>'); + $supported_range = $superset->merge($style_element->extractPlainTagsSubset()); + if (!$style_element->diff($supported_range)->allowsNothing()) { + $this->context->buildViolation($constraint->nonHtml5TagMessage) + ->setParameter('@tag', sprintf("<%s>", $tag)) + ->addViolation(); + return; + } + + // Get the list of tags enabled by every plugin other than Style. + $other_enabled_plugins = $this->getOtherEnabledPlugins($text_editor, 'ckeditor5_style'); + $enableable_disabled_plugins = $this->getEnableableDisabledPlugins($text_editor); + + $other_enabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE)); + $disabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE)); + + // Next, validate that the classes specified for this style are not + // supported by an enabled plugin. + if (self::intersectionWithClasses($style_element, $other_enabled_plugin_elements)) { + $this->context->buildViolation($constraint->conflictingEnabledPluginMessage) + ->setParameter('@tag', sprintf("<%s>", $tag)) + ->setParameter('@classes', implode(", ", $classes)) + ->setParameter('%plugin', $this->findStyleConflictingPluginLabel($style_element)) + ->addViolation(); + } + // Next, validate that the classes specified for this style are not + // supported by a disabled plugin. + elseif (self::intersectionWithClasses($style_element, $disabled_plugin_elements)) { + $this->context->buildViolation($constraint->conflictingDisabledPluginMessage) + ->setParameter('@tag', sprintf("<%s>", $tag)) + ->setParameter('@classes', implode(", ", $classes)) + ->setParameter('%plugin', $this->findStyleConflictingPluginLabel($style_element)) + ->addViolation(); + } + } + + /** + * Checks if there is an intersection on allowed 'class' attribute values. + * + * @param \Drupal\ckeditor5\HTMLRestrictions $a + * One set of HTML restrictions. + * @param \Drupal\ckeditor5\HTMLRestrictions $b + * Another set of HTML restrictions. + * + * @return bool + * Whether there is an intersection. + */ + private static function intersectionWithClasses(HTMLRestrictions $a, HTMLRestrictions $b): bool { + // Compute the intersection, but first resolve wildcards, by merging + // tags of the other operand. Because only tags are merged, this cannot + // introduce a 'class' attribute intersection. + // For example: a plugin may support `<$text-container class="foo">`. On its + // own that would not trigger an intersection, but when resolved into + // concrete tags it could. + $tags_from_a = array_diff(array_keys($a->getConcreteSubset()->getAllowedElements()), ['*']); + $tags_from_b = array_diff(array_keys($b->getConcreteSubset()->getAllowedElements()), ['*']); + $a = $a->merge(new HTMLRestrictions(array_fill_keys($tags_from_b, FALSE))); + $b = $b->merge(new HTMLRestrictions(array_fill_keys($tags_from_a, FALSE))); + $intersection = $a->intersect($b); + + // Leverage the "GHS configuration" representation to easily find whether + // there is an intersection for classes. Other implementations are possible. + $intersection_as_ghs_config = $intersection->toGeneralHtmlSupportConfig(); + $ghs_config_classes = array_column($intersection_as_ghs_config, 'classes'); + return !empty($ghs_config_classes); + } + + /** + * Finds the plugin with elements that conflict with the style element. + * + * @param \Drupal\ckeditor5\HTMLRestrictions $needle + * A style definition element: a single tag, plus the 'class' attribute, + * plus >=1 allowed 'class' attribute values. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The label of the plugin that is conflicting with this style. + * + * @throws \OutOfBoundsException + * When a $needle is provided which does not exist among the other plugins. + */ + private function findStyleConflictingPluginLabel(HTMLRestrictions $needle): TranslatableMarkup { + foreach ($this->pluginManager->getDefinitions() as $id => $definition) { + // We're looking to find the other plugin, not this one. + if ($id === 'ckeditor5_style') { + continue; + } + + assert($definition instanceof CKEditor5PluginDefinition); + if (!$definition->hasElements()) { + continue; + } + + $haystack = HTMLRestrictions::fromString(implode($definition->getElements())); + if ($id === 'ckeditor5_sourceEditing') { + // The Source Editing plugin's allowed elements are based on stored + // config. This differs from all other plugins, which establish allowed + // elements as part of their definition. Because of this, the $haystack + // is calculated differently for Source Editing. + $text_editor = $this->createTextEditorObjectFromContext(); + $editor_plugins = $text_editor->getSettings()['plugins']; + if (!empty($editor_plugins['ckeditor5_sourceEditing'])) { + $source_tags = $editor_plugins['ckeditor5_sourceEditing']['allowed_tags']; + $haystack = HTMLRestrictions::fromString(implode($source_tags)); + } + } + if (self::intersectionWithClasses($needle, $haystack)) { + return $definition->label(); + } + } + + throw new \OutOfBoundsException(); + } + +} diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/UniqueLabelInListConstraint.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/UniqueLabelInListConstraint.php new file mode 100644 index 000000000000..5b479f5373a6 --- /dev/null +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/UniqueLabelInListConstraint.php @@ -0,0 +1,42 @@ +labelKey); + $label_frequencies = array_count_values($labels); + + foreach ($label_frequencies as $label => $frequency) { + if ($frequency > 1) { + $this->context->buildViolation($constraint->message) + ->setParameter('%label', $label) + ->addViolation(); + } + } + } + +} diff --git a/core/modules/ckeditor5/src/SmartDefaultSettings.php b/core/modules/ckeditor5/src/SmartDefaultSettings.php index fe55ab1ad7ca..b5f29697fde2 100644 --- a/core/modules/ckeditor5/src/SmartDefaultSettings.php +++ b/core/modules/ckeditor5/src/SmartDefaultSettings.php @@ -153,6 +153,15 @@ final class SmartDefaultSettings { [$upgraded_settings, $messages] = $this->createSettingsFromCKEditor4($old_editor->getSettings(), HTMLRestrictions::fromTextFormat($old_editor->getFilterFormat())); $editor->setSettings($upgraded_settings); $editor->setImageUploadSettings($old_editor->getImageUploadSettings()); + // *Before* determining which elements are still needed for this text + // format, ensure that all already enabled plugins that are configurable + // have valid settings. + // For all already enabled plugins, find the ones that are configurable, + // and add their default settings. For enabled plugins with element + // subsets, compute the appropriate settings to achieve the subset that + // matches the original text format restrictions. + $this->addDefaultSettingsForEnabledConfigurablePlugins($editor); + $this->computeSubsetSettingForEnabledPluginsWithSubsets($editor, $text_format); } // Add toolbar items based on HTML tags and attributes. @@ -224,6 +233,9 @@ final class SmartDefaultSettings { // and add their default settings. For enabled plugins with element subsets, // compute the appropriate settings to achieve the subset that matches the // original text format restrictions. + // Note: if switching from CKEditor 4, this will already have happened for + // plugins that were already enabled in CKEditor 4. It's harmless to compute + // this again. $this->addDefaultSettingsForEnabledConfigurablePlugins($editor); $this->computeSubsetSettingForEnabledPluginsWithSubsets($editor, $text_format); @@ -780,12 +792,13 @@ final class SmartDefaultSettings { * The text editor config entity to update. * * @return array|null - * NULL when nothing happened, otherwise an array with three values: + * NULL when nothing happened, otherwise an array with four values: * 1. a description (for use in a message) of which CKEditor 5 plugins were * enabled to match the HTML tags allowed by the text format. * 2. a description (for use in a message) of which CKEditor 5 plugins were * enabled to match the HTML attributes allowed by the text format. - * 3. the unsupported elements, in an HTMLRestrictions value object + * 3. the unsupported elements, in an HTMLRestrictions value object. + * 4. the list of enabled plugin labels. */ private function addToolbarItemsToMatchHtmlElementsInFormat(FilterFormatInterface $format, EditorInterface $editor): ?array { $html_restrictions_needed_elements = $format->getHtmlRestrictions(); @@ -797,11 +810,9 @@ final class SmartDefaultSettings { $enabled_definitions = $this->pluginManager->getEnabledDefinitions($editor); $disabled_definitions = array_diff_key($all_definitions, $enabled_definitions); $enabled_plugins = array_keys($enabled_definitions); - $provided_elements = $this->pluginManager->getProvidedElements($enabled_plugins); + $provided_elements = $this->pluginManager->getProvidedElements($enabled_plugins, $editor); $provided = new HTMLRestrictions($provided_elements); $needed = HTMLRestrictions::fromTextFormat($format); - $still_needed = $needed->diff($provided); - // Plugins only supporting cannot create the tag. For that, they // must support plain too. With this being the case, break down what // is needed based on what is currently provided. @@ -812,11 +823,12 @@ final class SmartDefaultSettings { $provided_plain_tags = new HTMLRestrictions( $this->pluginManager->getProvidedElements($enabled_plugins, NULL, FALSE, TRUE) ); + + // Determine the still needed plain tags, the still needed attributes, and + // the union of both. $still_needed_plain_tags = $needed->extractPlainTagsSubset()->diff($provided_plain_tags); - $still_needed_attributes = $still_needed->diff($still_needed_plain_tags); - // Merging $still_needed_plain_tags with $still_needed_attributes must - // always equal $still_needed. - assert($still_needed_plain_tags->merge($still_needed_attributes)->diff($still_needed)->allowsNothing()); + $still_needed_attributes = $needed->diff($provided)->diff($still_needed_plain_tags); + $still_needed = $still_needed_plain_tags->merge($still_needed_attributes); if (!$still_needed->allowsNothing()) { // Select plugins for supporting the still needed plain tags. @@ -889,6 +901,7 @@ final class SmartDefaultSettings { NULL, NULL, $still_needed, + NULL, ]; } } diff --git a/core/modules/ckeditor5/tests/modules/ckeditor5_plugin_elements_subset/ckeditor5_plugin_elements_subset.ckeditor5.yml b/core/modules/ckeditor5/tests/modules/ckeditor5_plugin_elements_subset/ckeditor5_plugin_elements_subset.ckeditor5.yml index c5218e7e2f5e..97952121f3eb 100644 --- a/core/modules/ckeditor5/tests/modules/ckeditor5_plugin_elements_subset/ckeditor5_plugin_elements_subset.ckeditor5.yml +++ b/core/modules/ckeditor5/tests/modules/ckeditor5_plugin_elements_subset/ckeditor5_plugin_elements_subset.ckeditor5.yml @@ -7,3 +7,5 @@ ckeditor5_plugin_elements_subset_sneakySuperset: elements: - - + - + - <$any-html5-element class> diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php index 64cb3d675032..07a26cc22112 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php @@ -48,7 +48,7 @@ class CKEditor5AllowedTagsTest extends CKEditor5TestBase { * * @var string */ - protected $defaultElementsAfterUpdatingToCkeditor5 = '