From 34384bb8614d8c914063ca20adb32315d9b48902 Mon Sep 17 00:00:00 2001 From: Benjamin Franzke Date: Sun, 8 Oct 2023 10:51:51 +0200 Subject: [PATCH] [BUGFIX] Enable configuration passthrough for custom CKEditor5 plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use JavaScript object destructuring to "remove" all options from the configuration object, that are consumed by our CKEditor5 wrapper. Also cleanup RTE resource and config handling to not write unused options into the configuration array and to avoid resolving legacy (CKEditor4) resource paths which are dropped anyway. As a drive-by, dots are now substituted with '_' in RTE field IDs to avoid the following hassle: Dots are interpreted as CSS classes when the ID value is used in combination with a number sign (#) to create a CSS selector for the respective field ID. That means the selector will not match. The class selector additionally becomes invalid once there is a digit after the dot, as CSS classes need to start with strings. (Example: EXT:styleguide in_flex » tab » rte.2). Resolves: #100784 Resolves: #101437 Releases: main, 12.4 Change-Id: I076b838c03588ad6eb8ad075a9df58501f146376 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/81406 Tested-by: Benjamin Franzke Tested-by: core-ci Tested-by: Oliver Hader Reviewed-by: Oliver Hader Reviewed-by: Benjamin Franzke Reviewed-by: Benni Mack Tested-by: Benni Mack Reviewed-by: Andreas Kienast --- .../TypeScript/rte_ckeditor/ckeditor5.ts | 222 +++++++++--------- .../rte_ckeditor/plugin/typo3-link.ts | 12 +- .../core/Configuration/RTE/SysNews.yaml | 3 - .../Classes/Form/Element/RichTextElement.php | 41 +--- .../Configuration/RTE/Default.yaml | 2 - .../Resources/Public/JavaScript/ckeditor5.js | 4 +- 6 files changed, 137 insertions(+), 147 deletions(-) diff --git a/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts b/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts index 1525508d6e9a..586a65bf176e 100644 --- a/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts +++ b/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts @@ -3,9 +3,9 @@ import { customElement, property, query } from 'lit/decorators'; import AjaxRequest from '@typo3/core/ajax/ajax-request'; import { prefixAndRebaseCss } from '@typo3/rte-ckeditor/css-prefixer'; import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'; -import type { WordCount } from '@ckeditor/ckeditor5-word-count'; +import type { Editor, EditorConfig, PluginConstructor } from '@ckeditor/ckeditor5-core'; +import type { WordCount, WordCountConfig } from '@ckeditor/ckeditor5-word-count'; import type { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; -import type { Editor, PluginConstructor } from '@ckeditor/ckeditor5-core'; import type { GeneralHtmlSupportConfig } from '@ckeditor/ckeditor5-html-support'; type PluginModuleDescriptor = { @@ -13,27 +13,15 @@ type PluginModuleDescriptor = { exports: string[], }; -interface CKEditor5Config { +type CKEditor5Config = Omit & { // in TYPO3 always `items` property is used, skipping `string[]` toolbar?: { items: string[], shouldNotGroupWhenFull?: boolean }; - extraPlugins?: string[]; - removePlugins?: string[]; importModules?: Array; removeImportModules?: Array; contentsCss?: string[]; - style?: any; - heading?: any; - alignment?: any; - width?: any; - height?: any; + width?: string|number; + height?: string|number; readOnly?: boolean; - language?: any; - table?: any; - ui?: any; - htmlSupport?: GeneralHtmlSupportConfig; - - wordCount?: any; - typo3link?: any; debug?: boolean; } @@ -44,7 +32,7 @@ interface FormEngineConfig { id?: string; name?: string; value?: string; - validationRules?: any; + validationRules?: string; } const defaultPlugins: PluginModuleDescriptor[] = [ @@ -109,10 +97,101 @@ export class CKEditor5Element extends LitElement { throw new Error('No rich-text content target found.'); } - const removeImportModules: Array = normalizeImportModules(this.options.removeImportModules || []); + const { + // options handled by this wrapper + importModules, + removeImportModules, + width, + height, + readOnly, + debug, + + // options forwarded to CKEditor5 + toolbar, + placeholder, + htmlSupport, + wordCount, + typo3link, + removePlugins, + ...otherOptions + } = this.options; + + if ('extraPlugins' in otherOptions) { + // Drop CKEditor4 style extraPlugins which we do not support for CKEditor5 + // as this string-based list of plugin names works only for bundled plugins. + // `config.importModules` is used for CKEditor5 instead + delete otherOptions.extraPlugins; + } + if ('contentsCss' in otherOptions) { + // Consumed in connectedCallback + delete otherOptions.contentsCss; + } + + const plugins = await this.resolvePlugins(defaultPlugins, importModules, removeImportModules); + + const config: EditorConfig = { + ...otherOptions, + // link.defaultProtocol: 'https://' + toolbar, + plugins, + placeholder, + wordCount, + typo3link: typo3link || null, + removePlugins: removePlugins || [], + }; + + if (htmlSupport !== undefined) { + config.htmlSupport = convertPseudoRegExp(htmlSupport) as GeneralHtmlSupportConfig; + } + + ClassicEditor + .create(this.target, config) + .then((editor: ClassicEditor) => { + this.applyEditableElementStyles(editor, width, height); + this.handleWordCountPlugin(editor, wordCount); + this.applyReadOnly(editor, readOnly); + if (editor.plugins.has('SourceEditing')) { + const sourceEditingPlugin = editor.plugins.get('SourceEditing') as SourceEditing; + editor.model.document.on('change:data', (): void => { + if (!sourceEditingPlugin.isSourceEditingMode) { + editor.updateSourceElement() + } + this.target.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); + }); + } + + if (debug) { + import('@ckeditor/ckeditor5-inspector').then(({ default: CKEditorInspector }) => CKEditorInspector.attach(editor, { isCollapsed: true })); + } + }); + } + + protected createRenderRoot(): HTMLElement | ShadowRoot { + // const renderRoot = this.attachShadow({mode: 'open'}); + return this; + } + + protected render(): TemplateResult { + return html` + + `; + } + + private async resolvePlugins( + defaultPlugins: Array, + importModulesOption: Array|undefined, + removeImportModulesOption: Array|undefined + ): Promise>> { + const removeImportModules: Array = normalizeImportModules(removeImportModulesOption || []); const importModules: Array = normalizeImportModules([ ...defaultPlugins, - ...(this.options.importModules || []), + ...(importModulesOption || []), ]).map((moduleDescriptor: PluginModuleDescriptor) => { const { module } = moduleDescriptor; let { exports } = moduleDescriptor; @@ -160,81 +239,8 @@ export class CKEditor5Element extends LitElement { .flat(1); // plugins, without those that have been overridden - const plugins: Array> = declaredPlugins + return declaredPlugins .filter(plugin => !overriddenPlugins.includes(plugin as PluginConstructor)); - - const toolbar = this.options.toolbar; - - const config = { - // link.defaultProtocol: 'https://' - // @todo use complete `config` later - currently step-by-step only - toolbar, - plugins, - typo3link: this.options.typo3link || null, - removePlugins: this.options.removePlugins || [], - } as any; - if (this.options.language) { - config.language = this.options.language; - } - if (this.options.style) { - config.style = this.options.style; - } - if (this.options.wordCount) { - config.wordCount = this.options.wordCount; - } - if (this.options.table) { - config.table = this.options.table; - } - if (this.options.heading) { - config.heading = this.options.heading; - } - if (this.options.alignment) { - config.alignment = this.options.alignment; - } - if (this.options.ui) { - config.ui = this.options.ui; - } - if (this.options.htmlSupport) { - config.htmlSupport = convertPseudoRegExp(this.options.htmlSupport) as GeneralHtmlSupportConfig; - } - - ClassicEditor - .create(this.target, config) - .then((editor: ClassicEditor) => { - this.applyEditableElementStyles(editor); - this.handleWordCountPlugin(editor); - this.applyReadOnly(editor); - if (editor.plugins.has('SourceEditing')) { - const sourceEditingPlugin = editor.plugins.get('SourceEditing') as SourceEditing; - editor.model.document.on('change:data', (): void => { - if (!sourceEditingPlugin.isSourceEditingMode) { - editor.updateSourceElement() - } - this.target.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); - }); - } - - if (this.options.debug) { - import('@ckeditor/ckeditor5-inspector').then(({ default: CKEditorInspector }) => CKEditorInspector.attach(editor, { isCollapsed: true })); - } - }); - } - - protected createRenderRoot(): HTMLElement | ShadowRoot { - // const renderRoot = this.attachShadow({mode: 'open'}); - return this; - } - - protected render(): TemplateResult { - return html` - - `; } private async prefixAndLoadContentsCss(url: string, fieldId: string): Promise { @@ -258,19 +264,22 @@ export class CKEditor5Element extends LitElement { document.adoptedStyleSheets = [...document.adoptedStyleSheets, styleSheet]; } - private applyEditableElementStyles(editor: Editor): void { + private applyEditableElementStyles(editor: Editor, width: string|number|undefined, height: string|number|undefined): void { const view = editor.editing.view; - const styles: Record = { - 'min-height': this.options.height, - 'min-width': this.options.width, + const styles: Record = { + 'min-height': height, + 'min-width': width, }; Object.keys(styles).forEach((key) => { - let assignment: any = styles[key]; - if (!assignment) { + const _assignment: string|number = styles[key]; + if (!_assignment) { return; } - if (isFinite(assignment) && !Number.isNaN(parseFloat(assignment))) { - assignment += 'px'; + let assignment: string; + if (typeof _assignment === 'number' || !Number.isNaN(Number(assignment))) { + assignment = `${_assignment}px`; + } else { + assignment = _assignment } view.change((writer) => { writer.setStyle(key, assignment, view.document.getRoot()); @@ -281,8 +290,8 @@ export class CKEditor5Element extends LitElement { /** * see https://ckeditor.com/docs/ckeditor5/latest/features/word-count.html */ - private handleWordCountPlugin(editor: Editor): void { - if (editor.plugins.has('WordCount') && (this.options?.wordCount?.displayWords || this.options?.wordCount?.displayCharacters)) { + private handleWordCountPlugin(editor: Editor, wordCount: WordCountConfig|undefined): void { + if (editor.plugins.has('WordCount') && (wordCount?.displayWords || wordCount?.displayCharacters)) { const wordCountPlugin = editor.plugins.get('WordCount') as WordCount; this.renderRoot.appendChild(wordCountPlugin.wordCountContainer); } @@ -290,10 +299,9 @@ export class CKEditor5Element extends LitElement { /** * see https://ckeditor.com/docs/ckeditor5/latest/features/read-only.html - * does not work with types yet. so the editor is added with "any". */ - private applyReadOnly(editor: any): void { - if (this.options.readOnly) { + private applyReadOnly(editor: Editor, readOnly: boolean): void { + if (readOnly) { editor.enableReadOnlyMode('typo3-lock'); } } diff --git a/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts b/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts index 6b2072799bb5..3c609a6d6c8e 100644 --- a/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts +++ b/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts @@ -27,6 +27,10 @@ export function removeLinkPrefix(attribute: string): string { return attribute; } +export interface Typo3LinkConfig { + routeUrl: string; +} + export interface Typo3LinkDict { attrs?: { linkTitle?: string; @@ -689,7 +693,7 @@ export class Typo3LinkUI extends Core.Plugin { 'Link', this.makeUrlFromModulePath( editor, - (editor.config.get('typo3link') as any)?.routeUrl, + editor.config.get('typo3link')?.routeUrl, additionalParameters )); } @@ -728,5 +732,11 @@ export class Typo3Link extends Core.Plugin { static readonly overrides?: Array = [Link.Link]; } +declare module '@ckeditor/ckeditor5-core' { + interface EditorConfig { + typo3link?: Typo3LinkConfig; + } +} + // Provided for backwards compatibility export default Typo3Link; diff --git a/typo3/sysext/core/Configuration/RTE/SysNews.yaml b/typo3/sysext/core/Configuration/RTE/SysNews.yaml index 0f21c2dea01b..db8082dba3ff 100644 --- a/typo3/sysext/core/Configuration/RTE/SysNews.yaml +++ b/typo3/sysext/core/Configuration/RTE/SysNews.yaml @@ -20,6 +20,3 @@ editor: - Style - Underline - Strike - - extraPlugins: - - autolink diff --git a/typo3/sysext/rte_ckeditor/Classes/Form/Element/RichTextElement.php b/typo3/sysext/rte_ckeditor/Classes/Form/Element/RichTextElement.php index 5d50c1e3b6be..e7df8f5c8e8c 100644 --- a/typo3/sysext/rte_ckeditor/Classes/Form/Element/RichTextElement.php +++ b/typo3/sysext/rte_ckeditor/Classes/Form/Element/RichTextElement.php @@ -117,7 +117,7 @@ public function render(): array $ckeditorAttributes = GeneralUtility::implodeAttributes([ 'id' => $fieldId . 'ckeditor5', - 'options' => GeneralUtility::jsonEncodeForHtmlAttribute($ckeditorConfiguration['options'], false), + 'options' => GeneralUtility::jsonEncodeForHtmlAttribute($ckeditorConfiguration, false), 'form-engine' => GeneralUtility::jsonEncodeForHtmlAttribute([ 'id' => $fieldId, 'name' => $itemFormElementName, @@ -154,12 +154,12 @@ public function render(): array $resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html)); $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/rte-ckeditor/ckeditor5.js'); - $uiLanguage = $ckeditorConfiguration['options']['language']['ui']; + $uiLanguage = $ckeditorConfiguration['language']['ui']; if ($this->translationExists($uiLanguage)) { $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/ckeditor5/translations/' . $uiLanguage . '.js'); } - $contentLanguage = $ckeditorConfiguration['options']['language']['content']; + $contentLanguage = $ckeditorConfiguration['language']['content']; if ($this->translationExists($contentLanguage)) { $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/ckeditor5/translations/' . $contentLanguage . '.js'); } @@ -194,14 +194,10 @@ protected function getLanguageIsoCodeOfContent(): string return $contentLanguage; } - /** - * @return array{options: array, externalPlugins: array} - */ protected function resolveCkEditorConfiguration(): array { $configuration = $this->prepareConfigurationForEditor(); - $externalPlugins = []; foreach ($this->getExtraPlugins() as $extraPluginName => $extraPluginConfig) { $configName = $extraPluginConfig['configName'] ?? $extraPluginName; if (!empty($extraPluginConfig['config']) && is_array($extraPluginConfig['config'])) { @@ -211,20 +207,11 @@ protected function resolveCkEditorConfiguration(): array $configuration[$configName] = array_replace_recursive($extraPluginConfig['config'], $configuration[$configName]); } } - $configuration['extraPlugins'] = ($configuration['extraPlugins'] ?? '') . ',' . $extraPluginName; - if (isset($this->data['parameterArray']['fieldConf']['config']['placeholder'])) { - $configuration['editorplaceholder'] = (string)$this->data['parameterArray']['fieldConf']['config']['placeholder']; - } - - $externalPlugins[] = [ - 'name' => $extraPluginName, - 'resource' => $extraPluginConfig['resource'] ?? null, - ]; } - return [ - 'options' => $configuration, - 'externalPlugins' => $externalPlugins, - ]; + if (isset($this->data['parameterArray']['fieldConf']['config']['placeholder'])) { + $configuration['placeholder'] = (string)$this->data['parameterArray']['fieldConf']['config']['placeholder']; + } + return $configuration; } /** @@ -253,10 +240,8 @@ protected function getExtraPlugins(): array $pluginConfiguration[$pluginName] = [ 'configName' => $configuration['configName'] ?? $pluginName, ]; - if ($configuration['resource'] ?? null) { - $configuration['resource'] = $this->resolveUrlPath($configuration['resource']); - } unset($configuration['configName']); + // CKEditor4 style config, unused in CKEditor5 and not forwarded to the resutling plugin config unset($configuration['resource']); if ($configuration['route'] ?? null) { @@ -354,14 +339,6 @@ protected function prepareConfigurationForEditor(): array // Replace all paths $configuration = $this->replaceAbsolutePathsToRelativeResourcesPath($configuration); - // there are some places where we define an array, but it needs to be a list in order to work - if (is_array($configuration['extraPlugins'] ?? null)) { - $configuration['extraPlugins'] = implode(',', $configuration['extraPlugins']); - } - if (is_array($configuration['removeButtons'] ?? null)) { - $configuration['removeButtons'] = implode(',', $configuration['removeButtons']); - } - // unless explicitly set, the debug mode is enabled in development context if (!isset($configuration['debug'])) { $configuration['debug'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] ?? false) && Environment::getContext()->isDevelopment(); @@ -376,7 +353,7 @@ protected function prepareConfigurationForEditor(): array protected function sanitizeFieldId(string $itemFormElementName): string { - $fieldId = (string)preg_replace('/[^a-zA-Z0-9_:.-]/', '_', $itemFormElementName); + $fieldId = (string)preg_replace('/[^a-zA-Z0-9_:-]/', '_', $itemFormElementName); return htmlspecialchars((string)preg_replace('/^[^a-zA-Z]/', 'x', $fieldId)); } diff --git a/typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml b/typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml index b1334a7ffa53..174bd111d26e 100644 --- a/typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml +++ b/typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml @@ -66,5 +66,3 @@ editor: - mergeTableCells - tableProperties - tableCellProperties - - extraPlugins: diff --git a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js index 3e95cd7a98ce..b4351086dcf4 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(t,e,o,r){var i,n=arguments.length,s=n<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,o,r);else for(var l=t.length-1;l>=0;l--)(i=t[l])&&(s=(n<3?i(s):n>3?i(e,o,s):i(e,o))||s);return n>3&&s&&Object.defineProperty(e,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:["DocumentList"]},{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"]},{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 t of this.options.contentsCss)this.prefixAndLoadContentsCss(t,this.getAttribute("id"))}disconnectedCallback(){super.disconnectedCallback(),document.adoptedStyleSheets=document.adoptedStyleSheets.filter((t=>!this.styleSheets.has(t))),this.styleSheets.clear()}async firstUpdated(){if(!(this.target instanceof HTMLElement))throw new Error("No rich-text content target found.");const t=normalizeImportModules(this.options.removeImportModules||[]),e=normalizeImportModules([...defaultPlugins,...this.options.importModules||[]]).map((e=>{const{module:o}=e;let{exports:r}=e;for(const e of t)e.module===o&&(r=r.filter((t=>!e.exports.includes(t))));return{module:o,exports:r}})),o=await Promise.all(e.map((async t=>{try{return{module:await import(t.module),exports:t.exports}}catch(e){return console.error(`Failed to load CKEditor5 module ${t.module}`,e),{module:null,exports:[]}}}))),r=[];o.forEach((({module:t,exports:e})=>{for(const o of e)o in t?r.push(t[o]):console.error(`CKEditor5 plugin export "${o}" not available in`,t)}));const i=r.filter((t=>t.overrides?.length>0)).map((t=>t.overrides)).flat(1),n=r.filter((t=>!i.includes(t))),s={toolbar:this.options.toolbar,plugins:n,typo3link:this.options.typo3link||null,removePlugins:this.options.removePlugins||[]};this.options.language&&(s.language=this.options.language),this.options.style&&(s.style=this.options.style),this.options.wordCount&&(s.wordCount=this.options.wordCount),this.options.table&&(s.table=this.options.table),this.options.heading&&(s.heading=this.options.heading),this.options.alignment&&(s.alignment=this.options.alignment),this.options.ui&&(s.ui=this.options.ui),this.options.htmlSupport&&(s.htmlSupport=convertPseudoRegExp(this.options.htmlSupport)),ClassicEditor.create(this.target,s).then((t=>{if(this.applyEditableElementStyles(t),this.handleWordCountPlugin(t),this.applyReadOnly(t),t.plugins.has("SourceEditing")){const e=t.plugins.get("SourceEditing");t.model.document.on("change:data",(()=>{e.isSourceEditingMode||t.updateSourceElement(),this.target.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0}))}))}this.options.debug&&import("@ckeditor/ckeditor5-inspector").then((({default:e})=>e.attach(t,{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:["DocumentList"]},{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"]},{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` - `}async prefixAndLoadContentsCss(t,e){let o;try{const e=await new AjaxRequest(t).get();o=await e.resolve()}catch{return}const r=prefixAndRebaseCss(o,t,`#${e} .ck-content`),i=new CSSStyleSheet;await i.replace(r),this.styleSheets.set(i,!0),document.adoptedStyleSheets=[...document.adoptedStyleSheets,i]}applyEditableElementStyles(t){const e=t.editing.view,o={"min-height":this.options.height,"min-width":this.options.width};Object.keys(o).forEach((t=>{let r=o[t];r&&(isFinite(r)&&!Number.isNaN(parseFloat(r))&&(r+="px"),e.change((o=>{o.setStyle(t,r,e.document.getRoot())})))}))}handleWordCountPlugin(t){if(t.plugins.has("WordCount")&&(this.options?.wordCount?.displayWords||this.options?.wordCount?.displayCharacters)){const e=t.plugins.get("WordCount");this.renderRoot.appendChild(e.wordCountContainer)}}applyReadOnly(t){this.options.readOnly&&t.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(t,e){if("object"==typeof t){if(Array.isArray(t))return t.map((t=>e(t)??walkObj(t,e)));const o={};for(const[r,i]of Object.entries(t))o[r]=e(i)??walkObj(i,e);return o}return t}function convertPseudoRegExp(t){return walkObj(t,(t=>{if("object"==typeof t&&"pattern"in t&&"string"==typeof t.pattern){const e=t;return new RegExp(e.pattern,e.flags||void 0)}return null}))}function normalizeImportModules(t){return t.map((t=>"string"==typeof t?{module:t,exports:["default"]}:t))} \ 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(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