From 0e5ab82d416198f1ac26c5932ba8935c58431eb6 Mon Sep 17 00:00:00 2001 From: Benjamin Kott <benjamin.kott@outlook.com> Date: Fri, 6 Jan 2023 14:36:15 +0100 Subject: [PATCH] [BUGFIX] Restore text selection correctly in RTE LinkBrowser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Firefox the editor loses the text selection information after opening the Link Browser. Instead of using the references, we are now passing the selection start and end positions to the link browser to restore the text selection. Resolves: #99284 Releases: main Change-Id: I2f9e985c07c20ae59a83d22d0ff6e231ed4706e8 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77276 Tested-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Stefan Bürk <stefan@buerk.tech> Tested-by: Daniel Siepmann <coding@daniel-siepmann.de> Tested-by: core-ci <typo3@b13.com> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Benni Mack <benni@typo3.org> --- .../rte_ckeditor/plugin/typo3-link.ts | 21 +++++------ .../rte_ckeditor/rte-link-browser.ts | 35 ++++++++++--------- .../Public/JavaScript/plugin/typo3-link.js | 2 +- .../Public/JavaScript/rte-link-browser.js | 2 +- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts b/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts index 7cb55250582d..b5d74f3bd070 100644 --- a/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts +++ b/Build/Sources/TypeScript/rte_ckeditor/plugin/typo3-link.ts @@ -31,7 +31,7 @@ export class Typo3TextView extends UI.View { tag: 'span', attributes: { class: ['ck', 'ck-linktext'], - title: bind.to( 'text' ), + title: bind.to('text'), }, children: [{ text: bind.to('text') }] }); @@ -314,10 +314,9 @@ export class Typo3LinkUI extends Core.Plugin { actionsView.editButtonView.bind('isEnabled').to(linkCommand); actionsView.unlinkButtonView.bind('isEnabled').to(unlinkCommand); - // Execute unlink command after clicking on the "Edit" button. + // Open LinkBrowser after clicking on the "Edit" button. this.listenTo(actionsView, 'edit', () => { - const element = this.getSelectedLinkElement(); - this.openLinkBrowser(this.editor as EditorWithUI, element); + this.openLinkBrowser(this.editor as EditorWithUI); }); // Execute unlink command after clicking on the "Unlink" button. @@ -410,8 +409,7 @@ export class Typo3LinkUI extends Core.Plugin { private showUI(): void { if (!this.getSelectedLinkElement()) { this.showFakeVisualSelection(); - const element = this.getSelectedLinkElement(); - this.openLinkBrowser(this.editor as EditorWithUI, element); + this.openLinkBrowser(this.editor as EditorWithUI); } else { this.addActionsView(); this.balloon.showStack('main'); @@ -578,11 +576,8 @@ export class Typo3LinkUI extends Core.Plugin { return position.getAncestors().find((ancestor: any) => LinkUtils.isLinkElement(ancestor)); } - private openLinkBrowser(editor: EditorWithUI, element: any): void { - // @todo copied from existing code... improve it - if (!element) { - // return; - } + private openLinkBrowser(editor: EditorWithUI): void { + const element = this.getSelectedLinkElement(); let additionalParameters = ''; if (element) { additionalParameters += '&P[curUrl][url]=' + encodeURIComponent(element.getAttribute('href')); @@ -619,7 +614,9 @@ export class Typo3LinkUI extends Core.Plugin { size: modalObject.sizes.large, callback: (currentModal: ModalElement) => { // Add the instance to the iframe itself - currentModal.userData.ckeditor = editor; + currentModal.userData.editor = editor; + currentModal.userData.selectionStartPosition = editor.model.document.selection.getFirstPosition(); + currentModal.userData.selectionEndPosition = editor.model.document.selection.getLastPosition(); // @todo: is this used at all? // should maybe be a regular modal attribute then diff --git a/Build/Sources/TypeScript/rte_ckeditor/rte-link-browser.ts b/Build/Sources/TypeScript/rte_ckeditor/rte-link-browser.ts index 71d7f328c6c6..673517535aa2 100644 --- a/Build/Sources/TypeScript/rte_ckeditor/rte-link-browser.ts +++ b/Build/Sources/TypeScript/rte_ckeditor/rte-link-browser.ts @@ -14,9 +14,9 @@ import LinkBrowser from '@typo3/backend/link-browser'; import Modal from '@typo3/backend/modal'; import RegularEvent from '@typo3/core/event/regular-event'; -import {Engine} from '@typo3/ckeditor5-bundle'; -import {Typo3LinkCommand, Typo3LinkDict, LINK_ALLOWED_ATTRIBUTES, addLinkPrefix} from '@typo3/rte-ckeditor/plugin/typo3-link'; -import type {EditorWithUI} from '@ckeditor/ckeditor5-core/src/editor/editorwithui'; +import { Typo3LinkDict, LINK_ALLOWED_ATTRIBUTES, addLinkPrefix } from '@typo3/rte-ckeditor/plugin/typo3-link'; +import type { EditorWithUI } from '@ckeditor/ckeditor5-core/src/editor/editorwithui'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; /** * Module: @typo3/rte-ckeditor/rte-link-browser @@ -24,28 +24,22 @@ import type {EditorWithUI} from '@ckeditor/ckeditor5-core/src/editor/editorwithu */ class RteLinkBrowser { protected editor: EditorWithUI = null; - protected linkCommand: Typo3LinkCommand; - protected ranges: Iterable<Engine.Range> = null; + protected selectionStartPosition: Position = null; + protected selectionEndPosition: Position = null; /** * @param {String} editorId Id of CKEditor */ public initialize(editorId: string): void { - this.editor = Modal.currentModal.userData.ckeditor; - this.linkCommand = this.editor.commands.get('link') as Typo3LinkCommand; - - // Backup all ranges that are active when the Link Browser is requested - this.ranges = this.editor.model.document.selection.getRanges(); - window.addEventListener('beforeunload', (): void => { - this.editor.model.change((writer) => { - writer.setSelection(this.ranges); - }); - }); + this.editor = Modal.currentModal.userData.editor; + this.selectionStartPosition = Modal.currentModal.userData.selectionStartPosition; + this.selectionEndPosition = Modal.currentModal.userData.selectionEndPosition; const removeLinkElement = document.querySelector('.t3js-removeCurrentLink'); if (removeLinkElement !== null) { new RegularEvent('click', (e: Event): void => { e.preventDefault(); + this.restoreSelection(); this.editor.execute('unlink'); Modal.dismiss(); }).bindTo(removeLinkElement); @@ -65,12 +59,19 @@ class RteLinkBrowser { const linkText = ''; // @todo future feature: e.g. add page title as link-text (if applicable) const linkAttrs = this.convertAttributes(attributes, linkText); - this.editor.model.change((writer) => writer.setSelection(this.ranges)); - this.linkCommand.execute(this.sanitizeLink(link, queryParams), linkAttrs); + this.restoreSelection(); + this.editor.execute('link', this.sanitizeLink(link, queryParams), linkAttrs); Modal.dismiss(); } + private restoreSelection(): void { + this.editor.model.change((writer) => { + const ranges = [writer.createRange(this.selectionStartPosition, this.selectionEndPosition)]; + writer.setSelection(ranges); + }); + } + private convertAttributes(attributes: Record<string, string>, text?: string): Typo3LinkDict { const linkAttr: any = { attrs: {} }; for (const [attribute, value] of Object.entries(attributes)) { 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 1915fe419382..713c7a21908c 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,LinkActionsView,Widget,Utils}from"@typo3/ckeditor5-bundle.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>';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 Typo3TextView extends UI.View{constructor(e){super(e),this.set("text",void 0);const t=this.bindTemplate;this.setTemplate({tag:"span",attributes:{class:["ck","ck-linktext"],title:t.to("text")},children:[{text:t.to("text")}]})}}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((s=>{if(n.isCollapsed){const o=n.getFirstPosition();if(n.hasAttribute("linkHref")){const r=Typing.findAttributeRange(o,"linkHref",n.getAttribute("linkHref"),i);s.setAttribute("linkHref",e,r);for(const[e,i]of Object.entries(t.attrs))s.setAttribute(e,i,r);s.setSelection(s.createPositionAfter(r.end.nodeBefore))}else if(""!==e){const r=Utils.toMap(n.getAttributes());r.set("linkHref",e);for(const[e,i]of Object.entries(t.attrs))r.set(e,i);const{end:l}=i.insertContent(s.createText(e,r),o);s.setSelection(l)}s.removeSelectionAttribute("linkHref")}else{const t=i.schema.getValidRanges(n.getRanges(),"linkHref"),o=[];for(const e of n.getSelectedBlocks())i.schema.checkAttribute(e,"linkHref")&&o.push(s.createRangeOn(e));const r=o.slice();for(const e of t)this.isRangeToUpdate(e,o)&&r.push(e);for(const t of r)s.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 Typo3LinkActionsView extends LinkActionsView{_createPreviewButton(){const e=new Typo3TextView(this.locale),t=this.t;return e.bind("text").to(this,"href",(e=>e||t("This link has no URL"))),e}}const VISUAL_SELECTION_MARKER_NAME="link-ui";export class Typo3LinkUI extends Core.Plugin{init(){const e=this.editor;e.editing.view.addObserver(Engine.ClickObserver),this.actionsView=this.createActionsView(),this.balloon=e.plugins.get(UI.ContextualBalloon),this.createToolbarLinkButtons(),this.enableUserBalloonInteractions(),e.conversion.for("editingDowncast").markerToHighlight({model:"link-ui",view:{classes:["ck-fake-link-selection"]}}),e.conversion.for("editingDowncast").markerToElement({model:"link-ui",view:{name:"span",classes:["ck-fake-link-selection","ck-fake-link-selection_collapsed"]}})}createActionsView(){const e=this.editor,t=new Typo3LinkActionsView(e.locale),i=e.commands.get("link"),n=e.commands.get("unlink");return t.bind("href").to(i,"value"),t.editButtonView.bind("isEnabled").to(i),t.unlinkButtonView.bind("isEnabled").to(n),this.listenTo(t,"edit",(()=>{const e=this.getSelectedLinkElement();this.openLinkBrowser(this.editor,e)})),this.listenTo(t,"unlink",(()=>{e.execute("unlink"),this.hideUI()})),t.keystrokes.set("Esc",((e,t)=>{this.hideUI(),t()})),t}createToolbarLinkButtons(){const e=this.editor,t=e.commands.get("link"),i=e.t;e.keystrokes.set(LinkUtils.LINK_KEYSTROKE,((e,i)=>{i(),t.isEnabled&&this.showUI()})),e.ui.componentFactory.add("link",(e=>{const n=new UI.ButtonView(e);return n.isEnabled=!0,n.label=i("Link"),n.icon=linkIcon,n.keystroke=LinkUtils.LINK_KEYSTROKE,n.tooltip=!0,n.isToggleable=!0,n.bind("isEnabled").to(t,"isEnabled"),n.bind("isOn").to(t,"value",(e=>!!e)),this.listenTo(n,"execute",(()=>this.showUI())),n}))}enableUserBalloonInteractions(){const e=this.editor.editing.view.document;this.listenTo(e,"click",(()=>{this.getSelectedLinkElement()&&this.showUI()})),this.editor.keystrokes.set("Esc",((e,t)=>{this.isUIVisible()&&(this.hideUI(),t())}))}addActionsView(){this.areActionsInPanel()||this.balloon.add({view:this.actionsView,position:this.getBalloonPositionData()})}hideUI(){if(!this.isUIInPanel())return;const e=this.editor;this.stopListening(e.ui,"update"),this.stopListening(this.balloon,"change:visibleView"),e.editing.view.focus(),this.balloon.remove(this.actionsView),this.hideFakeVisualSelection()}showUI(){if(this.getSelectedLinkElement())this.addActionsView(),this.balloon.showStack("main");else{this.showFakeVisualSelection();const e=this.getSelectedLinkElement();this.openLinkBrowser(this.editor,e)}this.startUpdatingUI()}startUpdatingUI(){const e=this.editor,t=e.editing.view.document;let i=this.getSelectedLinkElement(),n=o();const s=()=>{const e=this.getSelectedLinkElement(),t=o();i&&!e||!i&&t!==n?this.hideUI():this.isUIVisible()&&this.balloon.updatePosition(this.getBalloonPositionData()),i=e,n=t};function o(){return t.selection.focus.getAncestors().reverse().find((e=>e.is("element")))}this.listenTo(e.ui,"update",s),this.listenTo(this.balloon,"change:visibleView",s)}areActionsInPanel(){return this.balloon.hasView(this.actionsView)}areActionsVisible(){return this.balloon.visibleView===this.actionsView}isUIInPanel(){return this.areActionsInPanel()}isUIVisible(){return this.areActionsVisible()}getBalloonPositionData(){const e=this.editor.editing.view,t=this.editor.model,i=e.document;let n=null;if(t.markers.has("link-ui")){const t=Array.from(this.editor.editing.mapper.markerNameToElements("link-ui")),i=e.createRange(e.createPositionBefore(t[0]),e.createPositionAfter(t[t.length-1]));n=e.domConverter.viewRangeToDom(i)}else n=()=>{const t=this.getSelectedLinkElement();return t?e.domConverter.mapViewToDom(t):e.domConverter.viewRangeToDom(i.selection.getFirstRange())};return{target:n}}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),s=this.findLinkElementAncestor(i.end);return n&&n==s&&e.createRangeIn(n).getTrimmed().isEqual(i)?n:null}}showFakeVisualSelection(){const e=this.editor.model;e.change((t=>{const i=e.document.selection.getFirstRange();if(e.markers.has("link-ui"))t.updateMarker("link-ui",{range:i});else if(i.start.isAtEnd){const n=i.start.getLastMatchingPosition((({item:t})=>!e.schema.isContent(t)),{startPosition:null,boundaries:i});t.addMarker("link-ui",{usingOperation:!1,affectsData:!1,range:t.createRange(n,i.end)})}else t.addMarker("link-ui",{usingOperation:!1,affectsData:!1,range:i})}))}hideFakeVisualSelection(){const e=this.editor.model;e.markers.has("link-ui")&&e.change((e=>{e.removeMarker("link-ui")}))}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",Typo3LinkUI.requires=[UI.ContextualBalloon];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,LinkActionsView,Widget,Utils}from"@typo3/ckeditor5-bundle.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>';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 Typo3TextView extends UI.View{constructor(e){super(e),this.set("text",void 0);const t=this.bindTemplate;this.setTemplate({tag:"span",attributes:{class:["ck","ck-linktext"],title:t.to("text")},children:[{text:t.to("text")}]})}}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 s=n.getFirstPosition();if(n.hasAttribute("linkHref")){const r=Typing.findAttributeRange(s,"linkHref",n.getAttribute("linkHref"),i);o.setAttribute("linkHref",e,r);for(const[e,i]of Object.entries(t.attrs))o.setAttribute(e,i,r);o.setSelection(o.createPositionAfter(r.end.nodeBefore))}else if(""!==e){const r=Utils.toMap(n.getAttributes());r.set("linkHref",e);for(const[e,i]of Object.entries(t.attrs))r.set(e,i);const{end:l}=i.insertContent(o.createText(e,r),s);o.setSelection(l)}o.removeSelectionAttribute("linkHref")}else{const t=i.schema.getValidRanges(n.getRanges(),"linkHref"),s=[];for(const e of n.getSelectedBlocks())i.schema.checkAttribute(e,"linkHref")&&s.push(o.createRangeOn(e));const r=s.slice();for(const e of t)this.isRangeToUpdate(e,s)&&r.push(e);for(const t of r)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 Typo3LinkActionsView extends LinkActionsView{_createPreviewButton(){const e=new Typo3TextView(this.locale),t=this.t;return e.bind("text").to(this,"href",(e=>e||t("This link has no URL"))),e}}const VISUAL_SELECTION_MARKER_NAME="link-ui";export class Typo3LinkUI extends Core.Plugin{init(){const e=this.editor;e.editing.view.addObserver(Engine.ClickObserver),this.actionsView=this.createActionsView(),this.balloon=e.plugins.get(UI.ContextualBalloon),this.createToolbarLinkButtons(),this.enableUserBalloonInteractions(),e.conversion.for("editingDowncast").markerToHighlight({model:"link-ui",view:{classes:["ck-fake-link-selection"]}}),e.conversion.for("editingDowncast").markerToElement({model:"link-ui",view:{name:"span",classes:["ck-fake-link-selection","ck-fake-link-selection_collapsed"]}})}createActionsView(){const e=this.editor,t=new Typo3LinkActionsView(e.locale),i=e.commands.get("link"),n=e.commands.get("unlink");return t.bind("href").to(i,"value"),t.editButtonView.bind("isEnabled").to(i),t.unlinkButtonView.bind("isEnabled").to(n),this.listenTo(t,"edit",(()=>{this.openLinkBrowser(this.editor)})),this.listenTo(t,"unlink",(()=>{e.execute("unlink"),this.hideUI()})),t.keystrokes.set("Esc",((e,t)=>{this.hideUI(),t()})),t}createToolbarLinkButtons(){const e=this.editor,t=e.commands.get("link"),i=e.t;e.keystrokes.set(LinkUtils.LINK_KEYSTROKE,((e,i)=>{i(),t.isEnabled&&this.showUI()})),e.ui.componentFactory.add("link",(e=>{const n=new UI.ButtonView(e);return n.isEnabled=!0,n.label=i("Link"),n.icon=linkIcon,n.keystroke=LinkUtils.LINK_KEYSTROKE,n.tooltip=!0,n.isToggleable=!0,n.bind("isEnabled").to(t,"isEnabled"),n.bind("isOn").to(t,"value",(e=>!!e)),this.listenTo(n,"execute",(()=>this.showUI())),n}))}enableUserBalloonInteractions(){const e=this.editor.editing.view.document;this.listenTo(e,"click",(()=>{this.getSelectedLinkElement()&&this.showUI()})),this.editor.keystrokes.set("Esc",((e,t)=>{this.isUIVisible()&&(this.hideUI(),t())}))}addActionsView(){this.areActionsInPanel()||this.balloon.add({view:this.actionsView,position:this.getBalloonPositionData()})}hideUI(){if(!this.isUIInPanel())return;const e=this.editor;this.stopListening(e.ui,"update"),this.stopListening(this.balloon,"change:visibleView"),e.editing.view.focus(),this.balloon.remove(this.actionsView),this.hideFakeVisualSelection()}showUI(){this.getSelectedLinkElement()?(this.addActionsView(),this.balloon.showStack("main")):(this.showFakeVisualSelection(),this.openLinkBrowser(this.editor)),this.startUpdatingUI()}startUpdatingUI(){const e=this.editor,t=e.editing.view.document;let i=this.getSelectedLinkElement(),n=s();const o=()=>{const e=this.getSelectedLinkElement(),t=s();i&&!e||!i&&t!==n?this.hideUI():this.isUIVisible()&&this.balloon.updatePosition(this.getBalloonPositionData()),i=e,n=t};function s(){return t.selection.focus.getAncestors().reverse().find((e=>e.is("element")))}this.listenTo(e.ui,"update",o),this.listenTo(this.balloon,"change:visibleView",o)}areActionsInPanel(){return this.balloon.hasView(this.actionsView)}areActionsVisible(){return this.balloon.visibleView===this.actionsView}isUIInPanel(){return this.areActionsInPanel()}isUIVisible(){return this.areActionsVisible()}getBalloonPositionData(){const e=this.editor.editing.view,t=this.editor.model,i=e.document;let n=null;if(t.markers.has("link-ui")){const t=Array.from(this.editor.editing.mapper.markerNameToElements("link-ui")),i=e.createRange(e.createPositionBefore(t[0]),e.createPositionAfter(t[t.length-1]));n=e.domConverter.viewRangeToDom(i)}else n=()=>{const t=this.getSelectedLinkElement();return t?e.domConverter.mapViewToDom(t):e.domConverter.viewRangeToDom(i.selection.getFirstRange())};return{target:n}}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}}showFakeVisualSelection(){const e=this.editor.model;e.change((t=>{const i=e.document.selection.getFirstRange();if(e.markers.has("link-ui"))t.updateMarker("link-ui",{range:i});else if(i.start.isAtEnd){const n=i.start.getLastMatchingPosition((({item:t})=>!e.schema.isContent(t)),{startPosition:null,boundaries:i});t.addMarker("link-ui",{usingOperation:!1,affectsData:!1,range:t.createRange(n,i.end)})}else t.addMarker("link-ui",{usingOperation:!1,affectsData:!1,range:i})}))}hideFakeVisualSelection(){const e=this.editor.model;e.markers.has("link-ui")&&e.change((e=>{e.removeMarker("link-ui")}))}findLinkElementAncestor(e){return e.getAncestors().find((e=>LinkUtils.isLinkElement(e)))}openLinkBrowser(e){const t=this.getSelectedLinkElement();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.editor=e,t.userData.selectionStartPosition=e.model.document.selection.getFirstPosition(),t.userData.selectionEndPosition=e.model.document.selection.getLastPosition(),t.querySelector(".t3js-modal-body")?.setAttribute("id","123")}})}}Typo3LinkUI.pluginName="Typo3LinkUI",Typo3LinkUI.requires=[UI.ContextualBalloon];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 diff --git a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/rte-link-browser.js b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/rte-link-browser.js index e5893a169e0b..28b8236c025c 100644 --- a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/rte-link-browser.js +++ b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/rte-link-browser.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import LinkBrowser from"@typo3/backend/link-browser.js";import Modal from"@typo3/backend/modal.js";import RegularEvent from"@typo3/core/event/regular-event.js";import{LINK_ALLOWED_ATTRIBUTES,addLinkPrefix}from"@typo3/rte-ckeditor/plugin/typo3-link.js";class RteLinkBrowser{constructor(){this.editor=null,this.ranges=null}initialize(e){this.editor=Modal.currentModal.userData.ckeditor,this.linkCommand=this.editor.commands.get("link"),this.ranges=this.editor.model.document.selection.getRanges(),window.addEventListener("beforeunload",(()=>{this.editor.model.change((e=>{e.setSelection(this.ranges)}))}));const t=document.querySelector(".t3js-removeCurrentLink");null!==t&&new RegularEvent("click",(e=>{e.preventDefault(),this.editor.execute("unlink"),Modal.dismiss()})).bindTo(t)}finalizeFunction(e){const t=LinkBrowser.getLinkAttributeValues(),n=t.params?t.params:"";delete t.params;const i=this.convertAttributes(t,"");this.editor.model.change((e=>e.setSelection(this.ranges))),this.linkCommand.execute(this.sanitizeLink(e,n),i),Modal.dismiss()}convertAttributes(e,t){const n={attrs:{}};for(const[t,i]of Object.entries(e))LINK_ALLOWED_ATTRIBUTES.includes(t)&&(n.attrs[addLinkPrefix(t)]=i);return"string"==typeof t&&""!==t&&(n.linkText=t),n}sanitizeLink(e,t){const n=e.match(/^([a-z0-9]+:\/\/[^:\/?#]+(?:\/?[^?#]*)?)(\??[^#]*)(#?.*)$/);if(n&&n.length>0){e=n[1]+n[2];const i=n[2].length>0?"&":"?";t.length>0&&("&"===t[0]&&(t=t.substr(1)),t.length>0&&(e+=i+t)),e+=n[3]}return e}}let rteLinkBrowser=new RteLinkBrowser;export default rteLinkBrowser;LinkBrowser.finalizeFunction=e=>{rteLinkBrowser.finalizeFunction(e)}; \ No newline at end of file +import LinkBrowser from"@typo3/backend/link-browser.js";import Modal from"@typo3/backend/modal.js";import RegularEvent from"@typo3/core/event/regular-event.js";import{LINK_ALLOWED_ATTRIBUTES,addLinkPrefix}from"@typo3/rte-ckeditor/plugin/typo3-link.js";class RteLinkBrowser{constructor(){this.editor=null,this.selectionStartPosition=null,this.selectionEndPosition=null}initialize(t){this.editor=Modal.currentModal.userData.editor,this.selectionStartPosition=Modal.currentModal.userData.selectionStartPosition,this.selectionEndPosition=Modal.currentModal.userData.selectionEndPosition;const e=document.querySelector(".t3js-removeCurrentLink");null!==e&&new RegularEvent("click",(t=>{t.preventDefault(),this.restoreSelection(),this.editor.execute("unlink"),Modal.dismiss()})).bindTo(e)}finalizeFunction(t){const e=LinkBrowser.getLinkAttributeValues(),i=e.params?e.params:"";delete e.params;const n=this.convertAttributes(e,"");this.restoreSelection(),this.editor.execute("link",this.sanitizeLink(t,i),n),Modal.dismiss()}restoreSelection(){this.editor.model.change((t=>{const e=[t.createRange(this.selectionStartPosition,this.selectionEndPosition)];t.setSelection(e)}))}convertAttributes(t,e){const i={attrs:{}};for(const[e,n]of Object.entries(t))LINK_ALLOWED_ATTRIBUTES.includes(e)&&(i.attrs[addLinkPrefix(e)]=n);return"string"==typeof e&&""!==e&&(i.linkText=e),i}sanitizeLink(t,e){const i=t.match(/^([a-z0-9]+:\/\/[^:\/?#]+(?:\/?[^?#]*)?)(\??[^#]*)(#?.*)$/);if(i&&i.length>0){t=i[1]+i[2];const n=i[2].length>0?"&":"?";e.length>0&&("&"===e[0]&&(e=e.substr(1)),e.length>0&&(t+=n+e)),t+=i[3]}return t}}let rteLinkBrowser=new RteLinkBrowser;export default rteLinkBrowser;LinkBrowser.finalizeFunction=t=>{rteLinkBrowser.finalizeFunction(t)}; \ No newline at end of file -- GitLab