jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/352578 )

Change subject: CX2: New link tool - Add any internal or external links
......................................................................


CX2: New link tool - Add any internal or external links

UI change: Used index layout for the internal, external selector.
Used Apply button as action for card.

A selection can be changed to an external or internal link using the
new link tool. An existing link target can be changed to a new target

TODO:
* Control + K shortcut to insert new link
* Make the selection handling more rebust. Now the selection is not
  indicated when the focus is inside the internal link selector.

Bug: T162126
Bug: T106445
Change-Id: I9590894d6d1bf478b13944511c79d21ecc887776
---
M i18n/en.json
M modules/dm/translationunits/mw.cx.dm.ExternalLinkTranslationUnit.js
M modules/dm/translationunits/mw.cx.dm.LinkTranslationUnit.js
M modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
M modules/tools/mw.cx.tools.NewLinkTool.js
M modules/tools/mw.cx.tools.TargetLinkTool.js
M modules/tools/styles/mw.cx.tools.LinkTool.less
M modules/tools/styles/mw.cx.tools.NewLinkTool.less
M modules/ui/mw.cx.ui.ToolsColumn.js
M modules/ui/translationunits/mw.cx.ui.LinkTranslationUnit.js
M modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
11 files changed, 295 insertions(+), 97 deletions(-)

Approvals:
  jenkins-bot: Verified
  Nikerabbit: Looks good to me, approved



diff --git a/i18n/en.json b/i18n/en.json
index 8854428..d5c3070 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -187,9 +187,9 @@
        "cx-your-translations-link": "{{GENDER:$1|Your translations}}",
        "cx-translationlist-empty-title": "Nothing to translate?",
        "cx-translationlist-empty-desc": "{{GENDER:|Start}} your translation 
now and continue it anytime.",
-       "cx-tools-link-internal-link": "Link to page",
+       "cx-tools-link-internal-link": "Search pages",
        "cx-tools-link-internal-link-placeholder": "Find page to link",
-       "cx-tools-link-external-link": "External",
+       "cx-tools-link-external-link": "External link",
        "cx-tools-link-external-link-placeholder": "Add external link",
        "cx-tools-link-to-another-page": "Link to another page",
        "cx-tools-link-add-as-missing": "Add as missing link",
diff --git 
a/modules/dm/translationunits/mw.cx.dm.ExternalLinkTranslationUnit.js 
b/modules/dm/translationunits/mw.cx.dm.ExternalLinkTranslationUnit.js
index 1ab985e..64dd183 100644
--- a/modules/dm/translationunits/mw.cx.dm.ExternalLinkTranslationUnit.js
+++ b/modules/dm/translationunits/mw.cx.dm.ExternalLinkTranslationUnit.js
@@ -22,16 +22,39 @@
 mw.cx.dm.ExternalLinkTranslationUnit.static.matchRdfaTypes = [ 'mw:ExtLink' ];
 
 mw.cx.dm.ExternalLinkTranslationUnit.prototype.getTargetURL = function () {
-       return this.sourceDocument.href;
+       if ( this.sourceDocument ) {
+               return this.sourceDocument.href;
+       }
+       if ( this.targetDocument ) {
+               return this.targetDocument.href;
+       }
 };
 
 /**
  * Get the id of the section
  * @return {string}
  */
-mw.cx.dm.TranslationUnit.prototype.getSectionId = function () {
+mw.cx.dm.ExternalLinkTranslationUnit.prototype.getSectionId = function () {
        // Make sure that there is an id for the unit even if id attribute is 
not present.
-       return this.sourceDocument.id || this.sourceDocument.dataset.linkid || 
OO.ui.generateElementId();
+       var id;
+       // Make sure that there is an id for the unit even if id attribute is 
not present.
+       if ( this.sourceDocument ) {
+               id = this.sourceDocument.id || 
this.sourceDocument.dataset.linkid;
+       }
+       // Source document does not exist. See if there is target document
+       if ( !id && this.targetDocument ) {
+               id = this.targetDocument.id || 
this.targetDocument.dataset.linkid;
+       }
+
+       return id || OO.ui.generateElementId();
+};
+
+/**
+ * @param {string} url
+ * @return {boolean}
+ */
+mw.cx.dm.ExternalLinkTranslationUnit.static.isSafeUrl = function ( url ) {
+       return OO.ui.isSafeUrl( url );
 };
 
 mw.cx.dm.modelRegistry.register( mw.cx.dm.ExternalLinkTranslationUnit );
diff --git a/modules/dm/translationunits/mw.cx.dm.LinkTranslationUnit.js 
b/modules/dm/translationunits/mw.cx.dm.LinkTranslationUnit.js
index cb0f19c..e9ead85 100644
--- a/modules/dm/translationunits/mw.cx.dm.LinkTranslationUnit.js
+++ b/modules/dm/translationunits/mw.cx.dm.LinkTranslationUnit.js
@@ -185,4 +185,38 @@
        this.redlink = true;
 };
 
+mw.cx.dm.LinkTranslationUnit.prototype.changeLinkTarget = function ( newTarget 
) {
+       var href, title;
+       title = mw.cx.dm.LinkTranslationUnit.static.getValidTitle( newTarget );
+       if ( !title ) {
+               mw.log.error( '[CX] Invalid title given' );
+               return;
+       }
+       // Convert title to a relative URL to avoid insecure values like
+       // javascript:.. appearing in it.
+       if ( title.indexOf( './' ) < 0 ) {
+               href = './' + title;
+       }
+       this.targetTitle = title;
+       this.targetDocument.setAttribute( 'href', href );
+       this.targetDocument.setAttribute( 'title', this.targetTitle );
+};
+
+/**
+ * Get a valid normalized title from the given text
+ * If the text is not suitable for the title, return null;
+ * Validation is done by mw.Title
+ *
+ * @param {string} text Text for the title.
+ * @return {string|null}
+ */
+mw.cx.dm.LinkTranslationUnit.static.getValidTitle = function ( text ) {
+       var title = text.trim();
+
+       title = mw.Title.newFromText( title );
+       title = title && title.toText();
+
+       return title;
+};
+
 mw.cx.dm.modelRegistry.register( mw.cx.dm.LinkTranslationUnit );
diff --git a/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js 
b/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
index 0f93831..a11f886 100644
--- a/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
+++ b/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
@@ -147,11 +147,22 @@
  * @param {boolean} targetExists Whether the title exist in target language
  */
 mw.cx.dm.SectionTranslationUnit.prototype.addLink = function ( selection, 
title, targetExists ) {
-       var range, html, newLink;
+       var newLink, href;
+
+       title = mw.cx.dm.LinkTranslationUnit.static.getValidTitle( title );
+       if ( !title ) {
+               mw.log.error( '[CX] Invalid title given' );
+               return;
+       }
+       // Convert title to a relative URL to avoid insecure values like
+       // javascript:.. appearing in it.
+       if ( title.indexOf( './' ) < 0 ) {
+               href = './' + title;
+       }
 
        newLink = document.createElement( 'a' );
        newLink.appendChild( document.createTextNode( title ) );
-       newLink.setAttribute( 'href', title );
+       newLink.setAttribute( 'href', href );
        newLink.setAttribute( 'title', title );
        newLink.setAttribute( 'rel', 'mw:WikiLink' );
        // Set a sufficiently good random id
@@ -162,21 +173,53 @@
                // translation view. It has no effect on generated wiki text.
                newLink.className = 'new';
        }
+       mw.cx.dm.SectionTranslationUnit.static.pasteAtSelection( selection, 
newLink );
+       this.buildSubTranslationUnits( this.sourceDocument, this.targetDocument 
);
+};
+
+/**
+ * Add an external link to the section.
+ * @param {Selection} selection The selection object
+ * @param {string} url Target URL
+ */
+mw.cx.dm.SectionTranslationUnit.prototype.addExternalLink = function ( 
selection, url ) {
+       var newLink;
+
+       // Validate the link
+       if ( !mw.cx.dm.ExternalLinkTranslationUnit.static.isSafeUrl( url ) ) {
+               mw.log.error( '[CX] Invalid or unsafe url given' );
+               return;
+       }
+       newLink = document.createElement( 'a' );
+       newLink.appendChild( document.createTextNode( url ) );
+       newLink.setAttribute( 'href', url );
+       newLink.setAttribute( 'rel', 'mw:ExtLink' );
+       // Set a sufficiently good random id
+       newLink.setAttribute( 'id', 'cx' + new Date().valueOf() );
+       mw.cx.dm.SectionTranslationUnit.static.pasteAtSelection( selection, 
newLink );
+       this.buildSubTranslationUnits( this.sourceDocument, this.targetDocument 
);
+};
+
+/**
+ * Paste an element at given selection
+ * @param {Selection} selection The selection object
+ * @param {Element} elementToPaste [description]
+ */
+mw.cx.dm.SectionTranslationUnit.static.pasteAtSelection = function ( 
selection, elementToPaste ) {
+       var range, html;
        // TODO: We can probably move the below block to a utility library
        // after we see more usecases similar to this and consolidate.
        if ( window.getSelection ) {
                if ( selection.getRangeAt && selection.rangeCount ) {
                        range = selection.getRangeAt( 0 );
                        range.deleteContents();
-                       range.insertNode( newLink );
+                       range.insertNode( elementToPaste );
                }
        } else if ( document.selection && document.selection.createRange ) {
                range = selection.createRange();
-               html = newLink.outerHTML;
+               html = elementToPaste.outerHTML;
                range.pasteHTML( html );
        }
-
-       this.buildSubTranslationUnits( this.sourceDocument, this.targetDocument 
);
 };
 
 /* Register */
diff --git a/modules/tools/mw.cx.tools.NewLinkTool.js 
b/modules/tools/mw.cx.tools.NewLinkTool.js
index a8dd29c..73534a2 100644
--- a/modules/tools/mw.cx.tools.NewLinkTool.js
+++ b/modules/tools/mw.cx.tools.NewLinkTool.js
@@ -14,12 +14,9 @@
        config.language = config.targetLanguage;
        // Parent constructor
        mw.cx.tools.NewLinkTool.super.call( this, model, config );
-
-       this.sourceTitle = null;
-       this.targetTitle = null;
-       this.pageInfo = null;
-       this.makeRedLinkButton = null;
-       this.removeLinkButton = null;
+       this.linkTabs = null;
+       this.internalLink = null;
+       this.externalLink = null;
 };
 
 /* Inheritance */
@@ -27,9 +24,35 @@
 
 mw.cx.tools.NewLinkTool.static.name = 'newlink';
 
+/**
+ * Text selection handler
+ * @param {Selection} selectionObj Selection object
+ */
+mw.cx.tools.NewLinkTool.prototype.onSelect = function ( selectionObj ) {
+       var selection;
+
+       // TODO: Sanitize content
+       selection = selectionObj.toString().trim();
+       if ( selection && selection.length < 1000 ) {
+               this.selectionObj = selectionObj;
+       } else {
+               return;
+       }
+       // Save the selection with a name so that it can be restored after the 
tool
+       // modify the selected content.
+       mw.cx.selection.save( 'translation', this.selectionObj );
+       // Check if selection changed.
+       if ( this.selection !== selection ) {
+               this.selection = selection;
+               this.pageInfo = null;
+               this.refresh();
+       }
+};
+
 mw.cx.tools.NewLinkTool.prototype.getContent = function () {
-       var card, compactTrigger, tabBar, internalLink, internalLinkPanel, 
externalLink, externalLinkPanel,
-               internalLinkTab, externalLinkTab, applyButton;
+       var card, compactTrigger, internalLinkTab, externalLinkTab;
+
+       this.collapse();
 
        compactTrigger = new OO.ui.PanelLayout( {
                classes: [ 'cx-tools-newlink-compact-trigger' ],
@@ -38,61 +61,50 @@
                padded: false,
                text: mw.msg( 'cx-tools-link-to-another-page' )
        } );
-       internalLinkTab = new OO.ui.ButtonWidget( {
-               classes: [ 'cx-tools-newlink-internal-trigger' ],
-               framed: false,
-               label: mw.msg( 'cx-tools-link-internal-link' )
-       } );
-       externalLinkTab = new OO.ui.ButtonWidget( {
-               classes: [ 'cx-tools-newlink-external-trigger' ],
-               framed: false,
-               label: mw.msg( 'cx-tools-link-external-link' )
-       } );
-       applyButton = new OO.ui.ButtonWidget( {
-               classes: [ 'cx-tools-newlink-apply' ],
-               flags: [ 'primary', 'progressive' ],
-               icon: 'check',
-               label: mw.msg( 'cx-tools-link-apply' )
-       } );
-       tabBar = new OO.ui.HorizontalLayout( {
-               classes: [ 'cx-tools-newlink-tabs' ],
-               items: [ internalLinkTab, externalLinkTab, applyButton ]
-       } );
-       internalLink = new mw.cx.ui.PageSelectorWidget( {
-               classes: [ 'cx-tools-newlink-internallink-selector' ],
+
+       this.internalLink = new mw.cx.ui.PageSelectorWidget( {
+               value: this.selection,
+               classes: [ 'cx-tools-newlink-internallink' ],
                language: this.language,
+               placeholder: mw.msg( 'cx-tools-link-internal-link-placeholder' 
),
                siteMapper: this.model.config.siteMapper
        } );
-       externalLink = new OO.ui.TextInputWidget( {
-               classes: [ 'cx-tools-newlink-internallink' ],
-               icon: 'linkExternal'
+       internalLinkTab = new OO.ui.CardLayout( 'internal', {
+               label: mw.msg( 'cx-tools-link-internal-link' ),
+               expanded: false,
+               scrollable: false
        } );
-       internalLinkPanel = new OO.ui.PanelLayout( {
-               classes: [ 'cx-tools-newlink-internallink-panel', 'active' ],
-               content: [ internalLink ]
+       internalLinkTab.$element.append( this.internalLink.$element );
+
+       this.externalLink = new OO.ui.TextInputWidget( {
+               classes: [ 'cx-tools-newlink-externallink' ],
+               icon: 'linkExternal',
+               placeholder: 'https://...'
        } );
-       externalLinkPanel = new OO.ui.PanelLayout( {
-               classes: [ 'cx-tools-newlink-externallink-panel' ],
-               content: [ externalLink ]
+       externalLinkTab = new OO.ui.CardLayout( 'external', {
+               label: mw.msg( 'cx-tools-link-external-link' ),
+               expanded: false,
+               scrollable: false
        } );
+       externalLinkTab.$element.append( this.externalLink.$element );
+
+       this.linkTabs = new OO.ui.IndexLayout( {
+               expanded: false,
+               scrollable: false,
+               classes: [ 'cx-tools-newlink-tabs' ]
+       } );
+       this.linkTabs.addCards( [ internalLinkTab, externalLinkTab ] );
        card = new OO.ui.StackLayout( {
                continuous: true,
                expanded: false,
                scrollable: false,
-               items: [ compactTrigger, tabBar, internalLinkPanel, 
externalLinkPanel ]
+               items: [ compactTrigger, this.linkTabs ]
        } );
 
-       compactTrigger.$element.on( 'click', function () {
-               card.$element.addClass( 'expanded' );
-       } );
-       internalLinkTab.on( 'click', function () {
-               internalLinkPanel.$element.addClass( 'active' );
-               externalLinkPanel.$element.removeClass( 'active' );
-       } );
-       externalLinkTab.on( 'click', function () {
-               externalLinkPanel.$element.addClass( 'active' );
-               internalLinkPanel.$element.removeClass( 'active' );
-       } );
+       compactTrigger.$element.on( 'click', this.expand.bind( this ) );
+
+       this.internalLink.getLookupMenu().connect( this, { choose: 'onApply' } 
);
+       this.externalLink.connect( this, { enter: 'onApply' } );
        return card.$element;
 };
 
@@ -106,9 +118,40 @@
        return null;
 };
 
-mw.cx.tools.NewLinkTool.prototype.removeLink = function () {
-       this.emit( 'remove' );
-       this.destroy();
+mw.cx.tools.NewLinkTool.prototype.expand = function () {
+       this.getCard().$element.addClass( 'expanded' );
+       this.linkTabs.getCard( 'internal' ).focus();
+       // Hide the source link and target link cards
+       this.card.$element.siblings( '.cx-card-sourcelink' ).addClass( 
'collapse' );
+       this.card.$element.siblings( '.cx-card-targetlink' ).addClass( 
'collapse' );
+};
+
+mw.cx.tools.NewLinkTool.prototype.collapse = function () {
+       if ( this.card ) {
+               this.card.$element.removeClass( 'expanded' );
+               this.card.$element.siblings( '.cx-card-sourcelink' 
).removeClass( 'collapse' );
+               this.card.$element.siblings( '.cx-card-targetlink' 
).removeClass( 'collapse' );
+       }
+};
+
+/**
+ * Check if the current input mode is for external links
+ *
+ * @return {boolean} Input mode is for external links
+ */
+mw.cx.tools.NewLinkTool.prototype.isExternal = function () {
+       return this.linkTabs.getCurrentCardName() === 'external';
+};
+
+mw.cx.tools.NewLinkTool.prototype.onApply = function () {
+       this.collapse();
+       if ( this.isExternal() ) {
+               this.emit( 'addExternalLink', this.selectionObj, 
this.externalLink.getValue() );
+       } else if ( this.selection ) {
+               this.emit( 'addlink', this.selectionObj, 
this.internalLink.getValue(), true );
+       } else {
+               this.emit( 'changeLinkTarget', this.internalLink.getValue() );
+       }
 };
 
 /* Register */
diff --git a/modules/tools/mw.cx.tools.TargetLinkTool.js 
b/modules/tools/mw.cx.tools.TargetLinkTool.js
index 7446699..68bb897 100644
--- a/modules/tools/mw.cx.tools.TargetLinkTool.js
+++ b/modules/tools/mw.cx.tools.TargetLinkTool.js
@@ -44,6 +44,8 @@
        } else {
                return;
        }
+       // Save the selection with a name so that it can be restored after the 
tool
+       // modify the selected content.
        mw.cx.selection.save( 'translation', this.selectionObj );
        // Check if selection changed.
        if ( this.selection !== selection ) {
diff --git a/modules/tools/styles/mw.cx.tools.LinkTool.less 
b/modules/tools/styles/mw.cx.tools.LinkTool.less
index d54cd5f..3728767 100644
--- a/modules/tools/styles/mw.cx.tools.LinkTool.less
+++ b/modules/tools/styles/mw.cx.tools.LinkTool.less
@@ -34,6 +34,9 @@
                        padding: 5px 0;
                        vertical-align: top;
                        color: @gray-dark;
+                       overflow: hidden;
+                       white-space: nowrap;
+                       text-overflow: ellipsis;
                }
        }
        .cx-widget-translationtool-actions {
@@ -43,6 +46,10 @@
                        margin-right: 0;
                }
        }
+
+       &.collapse {
+               display: none;
+       }
 }
 
 // Stack the source and target link cards
diff --git a/modules/tools/styles/mw.cx.tools.NewLinkTool.less 
b/modules/tools/styles/mw.cx.tools.NewLinkTool.less
index 7fd40e4..aeadd5f 100644
--- a/modules/tools/styles/mw.cx.tools.NewLinkTool.less
+++ b/modules/tools/styles/mw.cx.tools.NewLinkTool.less
@@ -7,6 +7,14 @@
                display: none;
        }
 
+       .cx-widget-translationtool-container {
+               padding: 0;
+       }
+
+       .cx-widget-translationtool-actions {
+               display: none;
+       }
+
        .cx-tools-newlink-compact-trigger {
                padding-left: 30px;
                background-repeat: no-repeat;
@@ -16,31 +24,32 @@
                cursor: pointer;
        }
 
-       .cx-tools-newlink-tabs,
-       .cx-tools-newlink-internallink-panel,
-       .cx-tools-newlink-externallink-panel {
+       .cx-tools-newlink-tabs {
+               display: none;
+               .oo-ui-menuLayout-content .oo-ui-indexLayout-stackLayout {
+                       overflow: visible;
+               }
+       }
+}
+
+.cx-card-newlink.expanded {
+       height: 100px;
+       padding: 2px 0;
+
+       // Hide the compact trigger
+       .cx-tools-newlink-compact-trigger {
                display: none;
        }
+       // Show the tabs
+       .cx-tools-newlink-tabs {
+               display: block;
+       }
 
-       .expanded {
-               .cx-tools-newlink-compact-trigger {
-                       display: none;
-               }
-               .cx-tools-newlink-tabs {
-                       display: block;
-               }
-               .cx-tools-newlink-internal-trigger,
-               .cx-tools-newlink-external-trigger,
-               .cx-tools-newlink-apply {
-                       margin-right: 0;
-                       width: 33%;
-               }
+       .cx-tools-link-external-link-add-button {
+               float: right;
+       }
 
-               .cx-tools-newlink-internallink-panel.active {
-                       display: block;
-               }
-               .cx-tools-newlink-externallink-panel.active {
-                       display: block;
-               }
+       .oo-ui-indexLayout-stackLayout > .oo-ui-panelLayout {
+               padding: 1.5em 0;
        }
 }
diff --git a/modules/ui/mw.cx.ui.ToolsColumn.js 
b/modules/ui/mw.cx.ui.ToolsColumn.js
index b91c7aa..79609a3 100644
--- a/modules/ui/mw.cx.ui.ToolsColumn.js
+++ b/modules/ui/mw.cx.ui.ToolsColumn.js
@@ -21,6 +21,7 @@
                continuous: true,
                classes: [ 'cx-column-tools-container' ],
                expanded: false,
+               scrollable: false,
                padded: true
        } );
        // Configuration initialization
diff --git a/modules/ui/translationunits/mw.cx.ui.LinkTranslationUnit.js 
b/modules/ui/translationunits/mw.cx.ui.LinkTranslationUnit.js
index 282a9ca..ab3916b 100644
--- a/modules/ui/translationunits/mw.cx.ui.LinkTranslationUnit.js
+++ b/modules/ui/translationunits/mw.cx.ui.LinkTranslationUnit.js
@@ -31,7 +31,12 @@
                        makeRedLink: 'makeRedLink'
                }
        },
-       newlink: [ 'click', 'focus' ]
+       newlink: {
+               triggers: [ 'click', 'focus' ],
+               events: {
+                       changeLinkTarget: 'changeLinkTarget'
+               }
+       }
 };
 
 mw.cx.ui.LinkTranslationUnit.prototype.adapt = function () {
@@ -92,8 +97,6 @@
 };
 
 mw.cx.ui.LinkTranslationUnit.prototype.removeLink = function () {
-       // Save the selection
-       mw.cx.selection.save( 'translation' );
        this.model.removeLink();
 
        if ( this.model.getTargetDocument() === null ) {
@@ -105,14 +108,13 @@
        // Destroy all tools
        this.tools.forEach( function ( tool ) { tool.destroy(); } );
        this.parentTranslationUnit.focus();
-       // Restore the cursor
+       // Restore the cursor. This was saved with this label when selection 
was made.
+       // See mw.cx.tools.TargetLinkTool.prototype.onSelect method
        mw.cx.selection.restore( 'translation' );
        this.emit( 'change' );
 };
 
 mw.cx.ui.LinkTranslationUnit.prototype.makeRedLink = function () {
-       // Save the selection
-       mw.cx.selection.save( 'translation' );
        this.model.makeRedLink();
        this.$translationSection.removeClass( 'cx-target-link-unadapted' 
).addClass( 'new' );
        this.parentTranslationUnit.focus();
@@ -121,5 +123,22 @@
        this.emit( 'change' );
 };
 
+mw.cx.ui.LinkTranslationUnit.prototype.changeLinkTarget = function ( newTarget 
) {
+       if ( !newTarget || !newTarget.trim() ) {
+               mw.log.error( '[CX] Attempting to change the link target to 
blank ' + this );
+               return;
+       }
+       this.model.changeLinkTarget( newTarget );
+       if ( this.model.getTargetTitle() === newTarget ) {
+               mw.log( '[CX] Target link target changed successfully. ' + this 
);
+       } else {
+               mw.log.error( '[CX] Target link target change failed. ' + this 
);
+       }
+       this.parentTranslationUnit.focus();
+       // Restore the cursor
+       mw.cx.selection.restore( 'translation' );
+       this.emit( 'change' );
+};
+
 /* Register */
 mw.cx.ui.translationUnitFactory.register( mw.cx.ui.LinkTranslationUnit );
diff --git a/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js 
b/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
index 841c038..0499ba9 100644
--- a/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
+++ b/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
@@ -38,7 +38,8 @@
        newlink: {
                triggers: [ 'select' ],
                events: {
-                       addlink: 'addLink'
+                       addlink: 'addLink',
+                       addExternalLink: 'addExternalLink'
                }
        }
 };
@@ -165,10 +166,26 @@
                return;
        }
        // Restore the selection
-       mw.cx.selection.save( 'translation' );
-       this.model.addLink( selection, title, targetExists );
-       // Set the cursor at the end of inserted link.
        mw.cx.selection.restore( 'translation' );
+       this.model.addLink( selection, title, targetExists );
+       mw.cx.selection.setCursorAfter( 'translation' );
+       this.emit( 'change' );
+       this.buildSubTranslationUnits( this.model );
+};
+
+/**
+ * Add a new link to the section.
+ * @param {Selection} selection The selection object
+ * @param {string} url External link URL.
+ */
+mw.cx.ui.SectionTranslationUnit.prototype.addExternalLink = function ( 
selection, url ) {
+       if ( !url || !url.trim() || !OO.ui.isSafeUrl( url ) ) {
+               mw.log.error( '[CX] Attempting to create external link with 
blank or invalid URL: ' + this );
+               return;
+       }
+       // Restore the selection
+       mw.cx.selection.restore( 'translation' );
+       this.model.addExternalLink( selection, url );
        mw.cx.selection.setCursorAfter( 'translation' );
        this.emit( 'change' );
        this.buildSubTranslationUnits( this.model );

-- 
To view, visit https://gerrit.wikimedia.org/r/352578
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I9590894d6d1bf478b13944511c79d21ecc887776
Gerrit-PatchSet: 13
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Santhosh <santhosh.thottin...@gmail.com>
Gerrit-Reviewer: Nikerabbit <niklas.laxst...@gmail.com>
Gerrit-Reviewer: Santhosh <santhosh.thottin...@gmail.com>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to