From 1caa2e5e4302e1dcd99605bb5a419925cb1ac847 Mon Sep 17 00:00:00 2001
From: Benjamin Kott <benjamin.kott@outlook.com>
Date: Wed, 26 Oct 2022 10:01:42 +0200
Subject: [PATCH] [BUGFIX] Correct registration of toolbar link buttons in
 CKEditor 5

The CKEditor 5 has very limited support for managing links.
By default, you only get a simple input field for a string.
To bring the known and loved easy-to-use LinkBrowser to
CKEditor 5 we need to overwrite the existing linking
capabilities.

For developers and integrators, this overwrite should be
invisible as it was before with the CKEditor 4 integration.
Instead of teaching the users that they need to load custom
modules to change the link behavior, we are already migrating
this in the background for them. A typical developer can just
follow the CKEditor 5 guide to configuring the editor to its
needs and the Core is already doing the adjustments and
replacements of modules for them.

We currently overwrite the existing link and unlink commands,
but we register the toolbar buttons with a prefix. This makes
existing CKEditor configurations incompatible since the
linking buttons are missing in the toolbar.

To make migrations easier we adjust custom toolbar buttons to
match the CKEditor 5 defaults and drop the custom names for
the buttons.

In addition, we are streamlining the behavior of removing plugins.
Since we are already overwriting the "Link" plugin there is no
need to remove it if no removePlugin config is passed in the first
place. This allows us to better test the behavior and reduces the
cases we need to cover in the first place.

Resolves: #98925
Releases: main
Change-Id: I3d6d158f5ab1b21e73f86471344107a07d71b03e
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/76267
Tested-by: core-ci <typo3@b13.com>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts            | 2 +-
 Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts    | 4 ++--
 typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml      | 4 ++--
 typo3/sysext/rte_ckeditor/Configuration/RTE/Full.yaml         | 3 ++-
 .../rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js     | 2 +-
 .../Resources/Public/JavaScript/plugin/typo3-link.js          | 2 +-
 6 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts b/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts
index 2a05396eeed4..ca50fe0332f0 100644
--- a/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts
+++ b/Build/Sources/TypeScript/rte_ckeditor/ckeditor5.ts
@@ -117,7 +117,7 @@ export class CKEditor5Element extends LitElement {
           wordCount: this.options.wordCount || null,
           typo3link: this.options.typo3link || null,
           // alternative, purge from `plugins` (classes) above already (probably better)
-          removePlugins: this.options.removePlugins || ['Link'],
+          removePlugins: this.options.removePlugins || [],
         } as any;
         if (this.options.language) {
           config.language = this.options.language;
diff --git a/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts b/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts
index 244f99252370..64b1a16f9197 100644
--- a/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts
+++ b/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts
@@ -267,7 +267,7 @@ export class Typo3LinkUI extends Core.Plugin {
     });
 
     // re-uses 'Link' plugin name -> original plugin 'Link' needs to be removed during runtime
-    editor.ui.componentFactory.add('Typo3Link', locale => {
+    editor.ui.componentFactory.add('link', locale => {
       const linkButton = new UI.ButtonView(locale);
       linkButton.isEnabled = true;
       linkButton.label = t('Link');
@@ -280,7 +280,7 @@ export class Typo3LinkUI extends Core.Plugin {
       this.listenTo(linkButton, 'execute', () => this.showUI());
       return linkButton;
     });
-    editor.ui.componentFactory.add('Typo3Unlink', locale => {
+    editor.ui.componentFactory.add('unlink', locale => {
       const unlinkButton = new UI.ButtonView(locale);
       unlinkButton.isEnabled = true;
       unlinkButton.label = t( 'Unlink');
diff --git a/typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml b/typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml
index 8c29a5ced498..adf9e370b77b 100644
--- a/typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml
+++ b/typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml
@@ -25,8 +25,8 @@ editor:
         - alignment
         - '|'
         - findAndReplace
-        - Typo3Link
-        - Typo3Unlink
+        - link
+        - unlink
         - SoftHyphen
         - '|'
         - removeFormat
diff --git a/typo3/sysext/rte_ckeditor/Configuration/RTE/Full.yaml b/typo3/sysext/rte_ckeditor/Configuration/RTE/Full.yaml
index 54ac53e17c93..dcad060137f2 100644
--- a/typo3/sysext/rte_ckeditor/Configuration/RTE/Full.yaml
+++ b/typo3/sysext/rte_ckeditor/Configuration/RTE/Full.yaml
@@ -39,7 +39,8 @@ editor:
         - find
         - selectAll
         - '|'
-        - Link
+        - link
+        - unlink
         - SoftHyphen
         - insertTable
         - tableColumn
diff --git a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js
index 6d5d5d4a25e4..ba61730cdaf2 100644
--- a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js
+++ b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/ckeditor5.js
@@ -19,4 +19,4 @@ var __decorate=function(t,e,o,i){var n,r=arguments.length,s=r<3?e:null===i?i=Obj
         data-formengine-validation-rules="${this.formEngine.validationRules}"
         >${this.formEngine.value}</textarea>
       <div class="${this.elements.wordCount.className}"></div>
-    `}firstUpdated(){if(!(this.target instanceof HTMLElement))throw new Error("No rich-text content target found.");const t=(this.options.importModules||[]).map((t=>import(t)));Promise.all(t).then((t=>{const e=t.filter((t=>t.default)).map((t=>t.default)),o=CKEditor5.builtinPlugins.concat(e),i=[].concat(...o.filter((t=>t.overrides?.length>0)).map((t=>t.overrides))),n=o.filter((t=>!i.includes(t)));let r={toolbar:this.options.toolbar,plugins:n,wordCount:this.options.wordCount||null,typo3link:this.options.typo3link||null,removePlugins:this.options.removePlugins||["Link"]};this.options.language&&(r.language=this.options.language),this.options.style&&(r.style=this.options.style),this.options.table&&(r.table=this.options.table),this.options.heading&&(r.heading=this.options.heading),this.options.alignment&&(r.alignment=this.options.alignment),CKEditor5.create(this.target,r).then((t=>{this.applyEditableElementStyles(t),this.applyWordCountWidget(t),this.applyReadOnly(t)}))}))}applyEditableElementStyles(t){const e=t.editing.view,o={"min-height":this.options.height,"min-width":this.options.width};Object.keys(o).forEach((t=>{let i=o[t];i&&(isFinite(i)&&!Number.isNaN(parseFloat(i))&&(i+="px"),e.change((o=>{o.setStyle(t,i,e.document.getRoot())})))}))}applyWordCountWidget(t){const e=t.plugins.get("WordCount");this.querySelector(this.elements.wordCount.selector).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};
\ No newline at end of file
+    `}firstUpdated(){if(!(this.target instanceof HTMLElement))throw new Error("No rich-text content target found.");const t=(this.options.importModules||[]).map((t=>import(t)));Promise.all(t).then((t=>{const e=t.filter((t=>t.default)).map((t=>t.default)),o=CKEditor5.builtinPlugins.concat(e),i=[].concat(...o.filter((t=>t.overrides?.length>0)).map((t=>t.overrides))),n=o.filter((t=>!i.includes(t)));let r={toolbar:this.options.toolbar,plugins:n,wordCount:this.options.wordCount||null,typo3link:this.options.typo3link||null,removePlugins:this.options.removePlugins||[]};this.options.language&&(r.language=this.options.language),this.options.style&&(r.style=this.options.style),this.options.table&&(r.table=this.options.table),this.options.heading&&(r.heading=this.options.heading),this.options.alignment&&(r.alignment=this.options.alignment),CKEditor5.create(this.target,r).then((t=>{this.applyEditableElementStyles(t),this.applyWordCountWidget(t),this.applyReadOnly(t)}))}))}applyEditableElementStyles(t){const e=t.editing.view,o={"min-height":this.options.height,"min-width":this.options.width};Object.keys(o).forEach((t=>{let i=o[t];i&&(isFinite(i)&&!Number.isNaN(parseFloat(i))&&(i+="px"),e.change((o=>{o.setStyle(t,i,e.document.getRoot())})))}))}applyWordCountWidget(t){const e=t.plugins.get("WordCount");this.querySelector(this.elements.wordCount.selector).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};
\ No newline at end of file
diff --git a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/plugin/typo3-link.js b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/plugin/typo3-link.js
index ef2d5622dd80..10dad6e5338b 100644
--- a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/plugin/typo3-link.js
+++ b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/plugin/typo3-link.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import{UI,Core,Engine,Typing,Link,LinkUtils,Widget,Utils}from"@typo3/ckeditor5-bundle.js";import{DoubleClickObserver}from"@typo3/rte-ckeditor/observer/double-click-observer.js";import{default as modalObject}from"@typo3/backend/modal.js";const linkIcon='<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m11.077 15 .991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184z"/></svg>',unlinkIcon='<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m11.077 15 .991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184zm4.919 10.562-1.414 1.414a.75.75 0 1 1-1.06-1.06l1.414-1.415-1.415-1.414a.75.75 0 0 1 1.061-1.06l1.414 1.414 1.414-1.415a.75.75 0 0 1 1.061 1.061l-1.414 1.414 1.414 1.415a.75.75 0 0 1-1.06 1.06l-1.415-1.414z"/></svg>';export const LINK_ALLOWED_ATTRIBUTES=["href","title","class","target","rel"];export function addLinkPrefix(e){return"link"+(e.charAt(0).toUpperCase()+e.slice(1))}export class Typo3LinkCommand extends Core.Command{refresh(){const e=this.editor.model,t=e.document.selection,i=t.getSelectedElement()||Utils.first(t.getSelectedBlocks());LinkUtils.isLinkableElement(i,e.schema)?(this.value=i.getAttribute("linkHref"),this.isEnabled=e.schema.checkAttribute(i,"linkHref")):(this.value=t.getAttribute("linkHref"),this.isEnabled=e.schema.checkAttributeInSelection(t,"linkHref"))}execute(e,t={}){const i=this.editor.model,n=i.document.selection;i.change((o=>{if(n.isCollapsed){const r=n.getFirstPosition();if(n.hasAttribute("linkHref")){const l=Typing.findAttributeRange(r,"linkHref",n.getAttribute("linkHref"),i);o.setAttribute("linkHref",e,l);for(const[e,i]of Object.entries(t.attrs))o.setAttribute(e,i,l);o.setSelection(o.createPositionAfter(l.end.nodeBefore))}else if(""!==e){const l=Utils.toMap(n.getAttributes());l.set("linkHref",e);for(const[e,i]of Object.entries(t.attrs))l.set(e,i);const{end:s}=i.insertContent(o.createText(e,l),r);o.setSelection(s)}o.removeSelectionAttribute("linkHref")}else{const t=i.schema.getValidRanges(n.getRanges(),"linkHref"),r=[];for(const e of n.getSelectedBlocks())i.schema.checkAttribute(e,"linkHref")&&r.push(o.createRangeOn(e));const l=r.slice();for(const e of t)this.isRangeToUpdate(e,r)&&l.push(e);for(const t of l)o.setAttribute("linkHref",e,t)}}))}isRangeToUpdate(e,t){for(const i of t)if(i.containsRange(e))return!1;return!0}}export class Typo3UnlinkCommand extends Core.Command{refresh(){const e=this.editor.model,t=e.document.selection,i=t.getSelectedElement();LinkUtils.isLinkableElement(i,e.schema)?(this.value=i.getAttribute("linkHref"),this.isEnabled=e.schema.checkAttribute(i,"linkHref")):(this.value=t.getAttribute("linkHref"),this.isEnabled=e.schema.checkAttributeInSelection(t,"linkHref"))}execute(){const e=this.editor.model,t=e.document.selection;e.change((i=>{const n=t.isCollapsed?[Typing.findAttributeRange(t.getFirstPosition(),"linkHref",t.getAttribute("linkHref"),e)]:e.schema.getValidRanges(t.getRanges(),"linkHref");for(const e of n)i.removeAttribute("linkHref",e),i.removeAttribute("linkTarget",e),i.removeAttribute("linkClass",e),i.removeAttribute("linkTitle",e),i.removeAttribute("linkRel",e)}))}}export class Typo3LinkEditing extends Core.Plugin{init(){const e=this.editor;window.editor=e,e.model.schema.extend("$text",{allowAttributes:["linkTitle","linkClass","linkTarget","linkRel"]}),e.conversion.for("downcast").attributeToElement({model:"linkTitle",view:(e,{writer:t})=>{const i=t.createAttributeElement("a",{title:e},{priority:5});return t.setCustomProperty("linkTitle",!0,i),i}}),e.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{title:!0}},model:{key:"linkTitle",value:e=>e.getAttribute("title")}}),e.conversion.for("downcast").attributeToElement({model:"linkClass",view:(e,{writer:t})=>{const i=t.createAttributeElement("a",{class:e},{priority:5});return t.setCustomProperty("linkClass",!0,i),i}}),e.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{title:!0}},model:{key:"linkClass",value:e=>e.getAttribute("class")}}),e.conversion.for("downcast").attributeToElement({model:"linkTarget",view:(e,{writer:t})=>{const i=t.createAttributeElement("a",{target:e},{priority:5});return t.setCustomProperty("linkTarget",!0,i),i}}),e.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{title:!0}},model:{key:"linkTarget",value:e=>e.getAttribute("target")}}),e.conversion.for("downcast").attributeToElement({model:"linkRel",view:(e,{writer:t})=>{const i=t.createAttributeElement("a",{rel:e},{priority:5});return t.setCustomProperty("linkRel",!0,i),i}}),e.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{title:!0}},model:{key:"linkRel",value:e=>e.getAttribute("rel")}}),e.commands.add("link",new Typo3LinkCommand(e)),e.commands.add("unlink",new Typo3UnlinkCommand(e))}}Typo3LinkEditing.pluginName="Typo3LinkEditing";export class Typo3LinkUI extends Core.Plugin{init(){const e=this.editor;e.editing.view.addObserver(DoubleClickObserver),e.editing.view.addObserver(Engine.ClickObserver),this.createToolbarLinkButtons()}createToolbarLinkButtons(){const e=this.editor,t=e.commands.get("link"),i=e.commands.get("unlink"),n=e.t;e.keystrokes.set(LinkUtils.LINK_KEYSTROKE,((e,i)=>{i(),t.isEnabled&&this.showUI()})),e.ui.componentFactory.add("Typo3Link",(e=>{const i=new UI.ButtonView(e);return i.isEnabled=!0,i.label=n("Link"),i.icon=linkIcon,i.keystroke=LinkUtils.LINK_KEYSTROKE,i.tooltip=!0,i.isToggleable=!0,i.bind("isEnabled").to(t,"isEnabled"),i.bind("isOn").to(t,"value",(e=>!!e)),this.listenTo(i,"execute",(()=>this.showUI())),i})),e.ui.componentFactory.add("Typo3Unlink",(e=>{const t=new UI.ButtonView(e);return t.isEnabled=!0,t.label=n("Unlink"),t.icon=unlinkIcon,t.tooltip=!0,t.isToggleable=!0,t.bind("isEnabled").to(i,"isEnabled"),t.bind("isOn").to(i,"value",(e=>!!e)),this.listenTo(t,"execute",(()=>i.execute())),t}))}showUI(){const e=this.getSelectedLinkElement();this.openLinkBrowser(this.editor,e)}getSelectedLinkElement(){const e=this.editor.editing.view,t=e.document.selection,i=t.getSelectedElement();if(t.isCollapsed||i&&Widget.isWidget(i))return this.findLinkElementAncestor(t.getFirstPosition());{const i=t.getFirstRange().getTrimmed(),n=this.findLinkElementAncestor(i.start),o=this.findLinkElementAncestor(i.end);return n&&n==o&&e.createRangeIn(n).getTrimmed().isEqual(i)?n:null}}findLinkElementAncestor(e){return e.getAncestors().find((e=>LinkUtils.isLinkElement(e)))}openLinkBrowser(e,t){let i="";t&&(i+="&P[curUrl][url]="+encodeURIComponent(t.getAttribute("href")),["target","class","title","rel"].forEach((e=>{const n=t.getAttribute(e);n&&(i+="&P[curUrl]["+e+"]="+encodeURIComponent(n))}))),this.openElementBrowser(e,"Link",this.makeUrlFromModulePath(e,e.config.get("typo3link")?.routeUrl,i))}makeUrlFromModulePath(e,t,i){return t+(-1===t.indexOf("?")?"?":"&")+"&contentsLanguage=en&editorId=123"+(i||"")}openElementBrowser(e,t,i){modalObject.advanced({type:modalObject.types.iframe,title:t,content:i,size:modalObject.sizes.large,callback:t=>{t.userData.ckeditor=e,t.querySelector(".t3js-modal-body")?.setAttribute("id","123")}})}}Typo3LinkUI.pluginName="Typo3LinkUI";export default class Typo3Link extends Core.Plugin{}Typo3Link.pluginName="Typo3Link",Typo3Link.requires=[Link.LinkEditing,Link.AutoLink,Typo3LinkEditing,Typo3LinkUI],Typo3Link.overrides=[Link.Link];
\ No newline at end of file
+import{UI,Core,Engine,Typing,Link,LinkUtils,Widget,Utils}from"@typo3/ckeditor5-bundle.js";import{DoubleClickObserver}from"@typo3/rte-ckeditor/observer/double-click-observer.js";import{default as modalObject}from"@typo3/backend/modal.js";const linkIcon='<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m11.077 15 .991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184z"/></svg>',unlinkIcon='<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m11.077 15 .991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184zm4.919 10.562-1.414 1.414a.75.75 0 1 1-1.06-1.06l1.414-1.415-1.415-1.414a.75.75 0 0 1 1.061-1.06l1.414 1.414 1.414-1.415a.75.75 0 0 1 1.061 1.061l-1.414 1.414 1.414 1.415a.75.75 0 0 1-1.06 1.06l-1.415-1.414z"/></svg>';export const LINK_ALLOWED_ATTRIBUTES=["href","title","class","target","rel"];export function addLinkPrefix(e){return"link"+(e.charAt(0).toUpperCase()+e.slice(1))}export class Typo3LinkCommand extends Core.Command{refresh(){const e=this.editor.model,t=e.document.selection,i=t.getSelectedElement()||Utils.first(t.getSelectedBlocks());LinkUtils.isLinkableElement(i,e.schema)?(this.value=i.getAttribute("linkHref"),this.isEnabled=e.schema.checkAttribute(i,"linkHref")):(this.value=t.getAttribute("linkHref"),this.isEnabled=e.schema.checkAttributeInSelection(t,"linkHref"))}execute(e,t={}){const i=this.editor.model,n=i.document.selection;i.change((o=>{if(n.isCollapsed){const r=n.getFirstPosition();if(n.hasAttribute("linkHref")){const l=Typing.findAttributeRange(r,"linkHref",n.getAttribute("linkHref"),i);o.setAttribute("linkHref",e,l);for(const[e,i]of Object.entries(t.attrs))o.setAttribute(e,i,l);o.setSelection(o.createPositionAfter(l.end.nodeBefore))}else if(""!==e){const l=Utils.toMap(n.getAttributes());l.set("linkHref",e);for(const[e,i]of Object.entries(t.attrs))l.set(e,i);const{end:s}=i.insertContent(o.createText(e,l),r);o.setSelection(s)}o.removeSelectionAttribute("linkHref")}else{const t=i.schema.getValidRanges(n.getRanges(),"linkHref"),r=[];for(const e of n.getSelectedBlocks())i.schema.checkAttribute(e,"linkHref")&&r.push(o.createRangeOn(e));const l=r.slice();for(const e of t)this.isRangeToUpdate(e,r)&&l.push(e);for(const t of l)o.setAttribute("linkHref",e,t)}}))}isRangeToUpdate(e,t){for(const i of t)if(i.containsRange(e))return!1;return!0}}export class Typo3UnlinkCommand extends Core.Command{refresh(){const e=this.editor.model,t=e.document.selection,i=t.getSelectedElement();LinkUtils.isLinkableElement(i,e.schema)?(this.value=i.getAttribute("linkHref"),this.isEnabled=e.schema.checkAttribute(i,"linkHref")):(this.value=t.getAttribute("linkHref"),this.isEnabled=e.schema.checkAttributeInSelection(t,"linkHref"))}execute(){const e=this.editor.model,t=e.document.selection;e.change((i=>{const n=t.isCollapsed?[Typing.findAttributeRange(t.getFirstPosition(),"linkHref",t.getAttribute("linkHref"),e)]:e.schema.getValidRanges(t.getRanges(),"linkHref");for(const e of n)i.removeAttribute("linkHref",e),i.removeAttribute("linkTarget",e),i.removeAttribute("linkClass",e),i.removeAttribute("linkTitle",e),i.removeAttribute("linkRel",e)}))}}export class Typo3LinkEditing extends Core.Plugin{init(){const e=this.editor;window.editor=e,e.model.schema.extend("$text",{allowAttributes:["linkTitle","linkClass","linkTarget","linkRel"]}),e.conversion.for("downcast").attributeToElement({model:"linkTitle",view:(e,{writer:t})=>{const i=t.createAttributeElement("a",{title:e},{priority:5});return t.setCustomProperty("linkTitle",!0,i),i}}),e.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{title:!0}},model:{key:"linkTitle",value:e=>e.getAttribute("title")}}),e.conversion.for("downcast").attributeToElement({model:"linkClass",view:(e,{writer:t})=>{const i=t.createAttributeElement("a",{class:e},{priority:5});return t.setCustomProperty("linkClass",!0,i),i}}),e.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{title:!0}},model:{key:"linkClass",value:e=>e.getAttribute("class")}}),e.conversion.for("downcast").attributeToElement({model:"linkTarget",view:(e,{writer:t})=>{const i=t.createAttributeElement("a",{target:e},{priority:5});return t.setCustomProperty("linkTarget",!0,i),i}}),e.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{title:!0}},model:{key:"linkTarget",value:e=>e.getAttribute("target")}}),e.conversion.for("downcast").attributeToElement({model:"linkRel",view:(e,{writer:t})=>{const i=t.createAttributeElement("a",{rel:e},{priority:5});return t.setCustomProperty("linkRel",!0,i),i}}),e.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{title:!0}},model:{key:"linkRel",value:e=>e.getAttribute("rel")}}),e.commands.add("link",new Typo3LinkCommand(e)),e.commands.add("unlink",new Typo3UnlinkCommand(e))}}Typo3LinkEditing.pluginName="Typo3LinkEditing";export class Typo3LinkUI extends Core.Plugin{init(){const e=this.editor;e.editing.view.addObserver(DoubleClickObserver),e.editing.view.addObserver(Engine.ClickObserver),this.createToolbarLinkButtons()}createToolbarLinkButtons(){const e=this.editor,t=e.commands.get("link"),i=e.commands.get("unlink"),n=e.t;e.keystrokes.set(LinkUtils.LINK_KEYSTROKE,((e,i)=>{i(),t.isEnabled&&this.showUI()})),e.ui.componentFactory.add("link",(e=>{const i=new UI.ButtonView(e);return i.isEnabled=!0,i.label=n("Link"),i.icon=linkIcon,i.keystroke=LinkUtils.LINK_KEYSTROKE,i.tooltip=!0,i.isToggleable=!0,i.bind("isEnabled").to(t,"isEnabled"),i.bind("isOn").to(t,"value",(e=>!!e)),this.listenTo(i,"execute",(()=>this.showUI())),i})),e.ui.componentFactory.add("unlink",(e=>{const t=new UI.ButtonView(e);return t.isEnabled=!0,t.label=n("Unlink"),t.icon=unlinkIcon,t.tooltip=!0,t.isToggleable=!0,t.bind("isEnabled").to(i,"isEnabled"),t.bind("isOn").to(i,"value",(e=>!!e)),this.listenTo(t,"execute",(()=>i.execute())),t}))}showUI(){const e=this.getSelectedLinkElement();this.openLinkBrowser(this.editor,e)}getSelectedLinkElement(){const e=this.editor.editing.view,t=e.document.selection,i=t.getSelectedElement();if(t.isCollapsed||i&&Widget.isWidget(i))return this.findLinkElementAncestor(t.getFirstPosition());{const i=t.getFirstRange().getTrimmed(),n=this.findLinkElementAncestor(i.start),o=this.findLinkElementAncestor(i.end);return n&&n==o&&e.createRangeIn(n).getTrimmed().isEqual(i)?n:null}}findLinkElementAncestor(e){return e.getAncestors().find((e=>LinkUtils.isLinkElement(e)))}openLinkBrowser(e,t){let i="";t&&(i+="&P[curUrl][url]="+encodeURIComponent(t.getAttribute("href")),["target","class","title","rel"].forEach((e=>{const n=t.getAttribute(e);n&&(i+="&P[curUrl]["+e+"]="+encodeURIComponent(n))}))),this.openElementBrowser(e,"Link",this.makeUrlFromModulePath(e,e.config.get("typo3link")?.routeUrl,i))}makeUrlFromModulePath(e,t,i){return t+(-1===t.indexOf("?")?"?":"&")+"&contentsLanguage=en&editorId=123"+(i||"")}openElementBrowser(e,t,i){modalObject.advanced({type:modalObject.types.iframe,title:t,content:i,size:modalObject.sizes.large,callback:t=>{t.userData.ckeditor=e,t.querySelector(".t3js-modal-body")?.setAttribute("id","123")}})}}Typo3LinkUI.pluginName="Typo3LinkUI";export default class Typo3Link extends Core.Plugin{}Typo3Link.pluginName="Typo3Link",Typo3Link.requires=[Link.LinkEditing,Link.AutoLink,Typo3LinkEditing,Typo3LinkUI],Typo3Link.overrides=[Link.Link];
\ No newline at end of file
-- 
GitLab