From 5af0755375bb214436b7e56ae6110e9a039447df Mon Sep 17 00:00:00 2001 From: Benjamin Franzke <ben@bnf.dev> Date: Tue, 20 Feb 2024 15:39:17 +0100 Subject: [PATCH] [BUGFIX] Preserve ordering of prefixed CKEditor5 CSS stylesheets When CKEditor5 `contentsCss` configuration options references multiple CSS, respective `fetch()` requests may finish in non-sequential order, depending on server load and file size, causing intended CSS ordering to break. Ensure sequential ordering by waiting for all promises to be settled via `Promise.allSettled()`. It is guaranteed to return the promise status and values "in the order of the promises passed, regardless of completion order": https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled#return_value Releases: main, 12.4 Resolves: #103152 Related: #100768 Change-Id: I5954b438cc6b8c6d74c81fda0227e963d7d89cf6 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83094 Tested-by: core-ci <typo3@b13.com> Tested-by: Benjamin Franzke <ben@bnf.dev> Reviewed-by: Benjamin Franzke <ben@bnf.dev> --- .../TypeScript/rte_ckeditor/ckeditor5.ts | 30 ++++++++++++------- .../Resources/Public/JavaScript/ckeditor5.js | 4 +-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts b/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts index 267b353218c7..e7319180f543 100644 --- a/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts +++ b/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts @@ -79,11 +79,7 @@ export class CKEditor5Element extends LitElement { public connectedCallback(): void { super.connectedCallback(); - if (Array.isArray(this.options.contentsCss)) { - for (const url of this.options.contentsCss) { - this.prefixAndLoadContentsCss(url, this.getAttribute('id')); - } - } + this.prefixAndLoadContentsCss(); } public disconnectedCallback() { @@ -243,13 +239,28 @@ export class CKEditor5Element extends LitElement { .filter(plugin => !overriddenPlugins.includes(plugin as PluginConstructor<Editor>)); } - private async prefixAndLoadContentsCss(url: string, fieldId: string): Promise<void> { + private async prefixAndLoadContentsCss(): Promise<void> { + if (!Array.isArray(this.options.contentsCss)) { + return; + } + const styleSheetStates = await Promise.allSettled( + this.options.contentsCss.map(url => this.prefixContentsCss(url, this.getAttribute('id'))) + ); + const styleSheets = styleSheetStates + .map(state => state.status === 'fulfilled' ? state.value : null) + .filter(v => v !== null); + styleSheets.forEach(styleSheet => this.styleSheets.set(styleSheet, true)); + document.adoptedStyleSheets = [...document.adoptedStyleSheets, ...styleSheets]; + } + + private async prefixContentsCss(url: string, fieldId: string): Promise<CSSStyleSheet> { let content: string; try { const response = await new AjaxRequest(url).get(); content = await response.resolve(); - } catch { - return; + } catch (e) { + console.error(`Failed to fetch CSS content for CKEditor5 prefixing: "${url}"`, e); + throw new Error(); } // Prefix custom stylesheets with id of the container element and a required `.ck-content` selector // see https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html @@ -260,8 +271,7 @@ export class CKEditor5Element extends LitElement { await styleSheet.replace( prefixedCss ); - this.styleSheets.set(styleSheet, true); - document.adoptedStyleSheets = [...document.adoptedStyleSheets, styleSheet]; + return styleSheet; } private applyEditableElementStyles(editor: Editor, width: string|number|undefined, height: string|number|undefined): void { diff --git a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js index facb997c5707..160cf758cf0d 100644 --- a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js +++ b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js @@ -10,7 +10,7 @@ * * The TYPO3 project - inspiring people to share! */ -var __decorate=function(e,t,o,r){var i,n=arguments.length,s=n<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,o,r);else for(var l=e.length-1;l>=0;l--)(i=e[l])&&(s=(n<3?i(s):n>3?i(t,o,s):i(t,o))||s);return n>3&&s&&Object.defineProperty(t,o,s),s};import{html,LitElement}from"lit";import{customElement,property,query}from"lit/decorators.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import{prefixAndRebaseCss}from"@typo3/rte-ckeditor/css-prefixer.js";import{ClassicEditor}from"@ckeditor/ckeditor5-editor-classic";const defaultPlugins=[{module:"@ckeditor/ckeditor5-block-quote",exports:["BlockQuote"]},{module:"@ckeditor/ckeditor5-essentials",exports:["Essentials"]},{module:"@ckeditor/ckeditor5-find-and-replace",exports:["FindAndReplace"]},{module:"@ckeditor/ckeditor5-heading",exports:["Heading"]},{module:"@ckeditor/ckeditor5-indent",exports:["Indent"]},{module:"@ckeditor/ckeditor5-link",exports:["Link"]},{module:"@ckeditor/ckeditor5-list",exports:["List"]},{module:"@ckeditor/ckeditor5-paragraph",exports:["Paragraph"]},{module:"@ckeditor/ckeditor5-clipboard",exports:["PastePlainText"]},{module:"@ckeditor/ckeditor5-paste-from-office",exports:["PasteFromOffice"]},{module:"@ckeditor/ckeditor5-remove-format",exports:["RemoveFormat"]},{module:"@ckeditor/ckeditor5-table",exports:["Table","TableToolbar","TableProperties","TableCellProperties","TableCaption"]},{module:"@ckeditor/ckeditor5-typing",exports:["TextTransformation"]},{module:"@ckeditor/ckeditor5-source-editing",exports:["SourceEditing"]},{module:"@ckeditor/ckeditor5-alignment",exports:["Alignment"]},{module:"@ckeditor/ckeditor5-style",exports:["Style"]},{module:"@ckeditor/ckeditor5-html-support",exports:["GeneralHtmlSupport"]},{module:"@ckeditor/ckeditor5-basic-styles",exports:["Bold","Italic","Subscript","Superscript","Strikethrough","Underline"]},{module:"@ckeditor/ckeditor5-special-characters",exports:["SpecialCharacters","SpecialCharactersEssentials"]},{module:"@ckeditor/ckeditor5-horizontal-line",exports:["HorizontalLine"]}];let CKEditor5Element=class extends LitElement{constructor(){super(...arguments),this.options={},this.formEngine={},this.styleSheets=new Map}connectedCallback(){if(super.connectedCallback(),Array.isArray(this.options.contentsCss))for(const e of this.options.contentsCss)this.prefixAndLoadContentsCss(e,this.getAttribute("id"))}disconnectedCallback(){super.disconnectedCallback(),document.adoptedStyleSheets=document.adoptedStyleSheets.filter((e=>!this.styleSheets.has(e))),this.styleSheets.clear()}async firstUpdated(){if(!(this.target instanceof HTMLElement))throw new Error("No rich-text content target found.");const{importModules:e,removeImportModules:t,width:o,height:r,readOnly:i,debug:n,toolbar:s,placeholder:l,htmlSupport:d,wordCount:a,typo3link:c,removePlugins:p,...u}=this.options;"extraPlugins"in u&&delete u.extraPlugins,"contentsCss"in u&&delete u.contentsCss;const m=await this.resolvePlugins(defaultPlugins,e,t),h={...u,toolbar:s,plugins:m,placeholder:l,wordCount:a,typo3link:c||null,removePlugins:p||[]};void 0!==d&&(h.htmlSupport=convertPseudoRegExp(d)),ClassicEditor.create(this.target,h).then((e=>{if(this.applyEditableElementStyles(e,o,r),this.handleWordCountPlugin(e,a),this.applyReadOnly(e,i),e.plugins.has("SourceEditing")){const t=e.plugins.get("SourceEditing");e.model.document.on("change:data",(()=>{t.isSourceEditingMode||e.updateSourceElement(),this.target.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0}))}))}n&&import("@ckeditor/ckeditor5-inspector").then((({default:t})=>t.attach(e,{isCollapsed:!0})))}))}createRenderRoot(){return this}render(){return html` +var __decorate=function(e,t,o,r){var i,n=arguments.length,s=n<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,o,r);else for(var l=e.length-1;l>=0;l--)(i=e[l])&&(s=(n<3?i(s):n>3?i(t,o,s):i(t,o))||s);return n>3&&s&&Object.defineProperty(t,o,s),s};import{html,LitElement}from"lit";import{customElement,property,query}from"lit/decorators.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import{prefixAndRebaseCss}from"@typo3/rte-ckeditor/css-prefixer.js";import{ClassicEditor}from"@ckeditor/ckeditor5-editor-classic";const defaultPlugins=[{module:"@ckeditor/ckeditor5-block-quote",exports:["BlockQuote"]},{module:"@ckeditor/ckeditor5-essentials",exports:["Essentials"]},{module:"@ckeditor/ckeditor5-find-and-replace",exports:["FindAndReplace"]},{module:"@ckeditor/ckeditor5-heading",exports:["Heading"]},{module:"@ckeditor/ckeditor5-indent",exports:["Indent"]},{module:"@ckeditor/ckeditor5-link",exports:["Link"]},{module:"@ckeditor/ckeditor5-list",exports:["List"]},{module:"@ckeditor/ckeditor5-paragraph",exports:["Paragraph"]},{module:"@ckeditor/ckeditor5-clipboard",exports:["PastePlainText"]},{module:"@ckeditor/ckeditor5-paste-from-office",exports:["PasteFromOffice"]},{module:"@ckeditor/ckeditor5-remove-format",exports:["RemoveFormat"]},{module:"@ckeditor/ckeditor5-table",exports:["Table","TableToolbar","TableProperties","TableCellProperties","TableCaption"]},{module:"@ckeditor/ckeditor5-typing",exports:["TextTransformation"]},{module:"@ckeditor/ckeditor5-source-editing",exports:["SourceEditing"]},{module:"@ckeditor/ckeditor5-alignment",exports:["Alignment"]},{module:"@ckeditor/ckeditor5-style",exports:["Style"]},{module:"@ckeditor/ckeditor5-html-support",exports:["GeneralHtmlSupport"]},{module:"@ckeditor/ckeditor5-basic-styles",exports:["Bold","Italic","Subscript","Superscript","Strikethrough","Underline"]},{module:"@ckeditor/ckeditor5-special-characters",exports:["SpecialCharacters","SpecialCharactersEssentials"]},{module:"@ckeditor/ckeditor5-horizontal-line",exports:["HorizontalLine"]}];let CKEditor5Element=class extends LitElement{constructor(){super(...arguments),this.options={},this.formEngine={},this.styleSheets=new Map}connectedCallback(){super.connectedCallback(),this.prefixAndLoadContentsCss()}disconnectedCallback(){super.disconnectedCallback(),document.adoptedStyleSheets=document.adoptedStyleSheets.filter((e=>!this.styleSheets.has(e))),this.styleSheets.clear()}async firstUpdated(){if(!(this.target instanceof HTMLElement))throw new Error("No rich-text content target found.");const{importModules:e,removeImportModules:t,width:o,height:r,readOnly:i,debug:n,toolbar:s,placeholder:l,htmlSupport:a,wordCount:d,typo3link:c,removePlugins:p,...u}=this.options;"extraPlugins"in u&&delete u.extraPlugins,"contentsCss"in u&&delete u.contentsCss;const m=await this.resolvePlugins(defaultPlugins,e,t),h={...u,toolbar:s,plugins:m,placeholder:l,wordCount:d,typo3link:c||null,removePlugins:p||[]};void 0!==a&&(h.htmlSupport=convertPseudoRegExp(a)),ClassicEditor.create(this.target,h).then((e=>{if(this.applyEditableElementStyles(e,o,r),this.handleWordCountPlugin(e,d),this.applyReadOnly(e,i),e.plugins.has("SourceEditing")){const t=e.plugins.get("SourceEditing");e.model.document.on("change:data",(()=>{t.isSourceEditingMode||e.updateSourceElement(),this.target.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0}))}))}n&&import("@ckeditor/ckeditor5-inspector").then((({default:t})=>t.attach(e,{isCollapsed:!0})))}))}createRenderRoot(){return this}render(){return html` <textarea id="${this.formEngine.id}" name="${this.formEngine.name}" @@ -18,4 +18,4 @@ var __decorate=function(e,t,o,r){var i,n=arguments.length,s=n<3?t:null===r?r=Obj rows="18" data-formengine-validation-rules="${this.formEngine.validationRules}" >${this.formEngine.value}</textarea> - `}async resolvePlugins(e,t,o){const r=normalizeImportModules(o||[]),i=normalizeImportModules([...e,...t||[]]).map((e=>{const{module:t}=e;let{exports:o}=e;for(const e of r)e.module===t&&(o=o.filter((t=>!e.exports.includes(t))));return{module:t,exports:o}})),n=await Promise.all(i.map((async e=>{try{return{module:await import(e.module),exports:e.exports}}catch(t){return console.error(`Failed to load CKEditor5 module ${e.module}`,t),{module:null,exports:[]}}}))),s=[];n.forEach((({module:e,exports:t})=>{for(const o of t)o in e?s.push(e[o]):console.error(`CKEditor5 plugin export "${o}" not available in`,e)}));const l=s.filter((e=>e.overrides?.length>0)).map((e=>e.overrides)).flat(1);return s.filter((e=>!l.includes(e)))}async prefixAndLoadContentsCss(e,t){let o;try{const t=await new AjaxRequest(e).get();o=await t.resolve()}catch{return}const r=prefixAndRebaseCss(o,e,`#${t} .ck-content`),i=new CSSStyleSheet;await i.replace(r),this.styleSheets.set(i,!0),document.adoptedStyleSheets=[...document.adoptedStyleSheets,i]}applyEditableElementStyles(e,t,o){const r=e.editing.view,i={"min-height":o,"min-width":t};Object.keys(i).forEach((e=>{const t=i[e];if(!t)return;let o;o="number"!=typeof t&&Number.isNaN(Number(o))?t:`${t}px`,r.change((t=>{t.setStyle(e,o,r.document.getRoot())}))}))}handleWordCountPlugin(e,t){if(e.plugins.has("WordCount")&&(t?.displayWords||t?.displayCharacters)){const t=e.plugins.get("WordCount");this.renderRoot.appendChild(t.wordCountContainer)}}applyReadOnly(e,t){t&&e.enableReadOnlyMode("typo3-lock")}};__decorate([property({type:Object})],CKEditor5Element.prototype,"options",void 0),__decorate([property({type:Object,attribute:"form-engine"})],CKEditor5Element.prototype,"formEngine",void 0),__decorate([query("textarea")],CKEditor5Element.prototype,"target",void 0),CKEditor5Element=__decorate([customElement("typo3-rte-ckeditor-ckeditor5")],CKEditor5Element);export{CKEditor5Element};function walkObj(e,t){if("object"==typeof e){if(Array.isArray(e))return e.map((e=>t(e)??walkObj(e,t)));const o={};for(const[r,i]of Object.entries(e))o[r]=t(i)??walkObj(i,t);return o}return e}function convertPseudoRegExp(e){return walkObj(e,(e=>{if("object"==typeof e&&"pattern"in e&&"string"==typeof e.pattern){const t=e;return new RegExp(t.pattern,t.flags||void 0)}return null}))}function normalizeImportModules(e){return e.map((e=>"string"==typeof e?{module:e,exports:["default"]}:e))} \ No newline at end of file + `}async resolvePlugins(e,t,o){const r=normalizeImportModules(o||[]),i=normalizeImportModules([...e,...t||[]]).map((e=>{const{module:t}=e;let{exports:o}=e;for(const e of r)e.module===t&&(o=o.filter((t=>!e.exports.includes(t))));return{module:t,exports:o}})),n=await Promise.all(i.map((async e=>{try{return{module:await import(e.module),exports:e.exports}}catch(t){return console.error(`Failed to load CKEditor5 module ${e.module}`,t),{module:null,exports:[]}}}))),s=[];n.forEach((({module:e,exports:t})=>{for(const o of t)o in e?s.push(e[o]):console.error(`CKEditor5 plugin export "${o}" not available in`,e)}));const l=s.filter((e=>e.overrides?.length>0)).map((e=>e.overrides)).flat(1);return s.filter((e=>!l.includes(e)))}async prefixAndLoadContentsCss(){if(!Array.isArray(this.options.contentsCss))return;const e=(await Promise.allSettled(this.options.contentsCss.map((e=>this.prefixContentsCss(e,this.getAttribute("id")))))).map((e=>"fulfilled"===e.status?e.value:null)).filter((e=>null!==e));e.forEach((e=>this.styleSheets.set(e,!0))),document.adoptedStyleSheets=[...document.adoptedStyleSheets,...e]}async prefixContentsCss(e,t){let o;try{const t=await new AjaxRequest(e).get();o=await t.resolve()}catch(t){throw console.error(`Failed to fetch CSS content for CKEditor5 prefixing: "${e}"`,t),new Error}const r=prefixAndRebaseCss(o,e,`#${t} .ck-content`),i=new CSSStyleSheet;return await i.replace(r),i}applyEditableElementStyles(e,t,o){const r=e.editing.view,i={"min-height":o,"min-width":t};Object.keys(i).forEach((e=>{const t=i[e];if(!t)return;let o;o="number"!=typeof t&&Number.isNaN(Number(o))?t:`${t}px`,r.change((t=>{t.setStyle(e,o,r.document.getRoot())}))}))}handleWordCountPlugin(e,t){if(e.plugins.has("WordCount")&&(t?.displayWords||t?.displayCharacters)){const t=e.plugins.get("WordCount");this.renderRoot.appendChild(t.wordCountContainer)}}applyReadOnly(e,t){t&&e.enableReadOnlyMode("typo3-lock")}};__decorate([property({type:Object})],CKEditor5Element.prototype,"options",void 0),__decorate([property({type:Object,attribute:"form-engine"})],CKEditor5Element.prototype,"formEngine",void 0),__decorate([query("textarea")],CKEditor5Element.prototype,"target",void 0),CKEditor5Element=__decorate([customElement("typo3-rte-ckeditor-ckeditor5")],CKEditor5Element);export{CKEditor5Element};function walkObj(e,t){if("object"==typeof e){if(Array.isArray(e))return e.map((e=>t(e)??walkObj(e,t)));const o={};for(const[r,i]of Object.entries(e))o[r]=t(i)??walkObj(i,t);return o}return e}function convertPseudoRegExp(e){return walkObj(e,(e=>{if("object"==typeof e&&"pattern"in e&&"string"==typeof e.pattern){const t=e;return new RegExp(t.pattern,t.flags||void 0)}return null}))}function normalizeImportModules(e){return e.map((e=>"string"==typeof e?{module:e,exports:["default"]}:e))} \ No newline at end of file -- GitLab