Cscott has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/233669

Change subject: WIP: Specialized inspector for ISBN magic links
......................................................................

WIP: Specialized inspector for ISBN magic links

TODO: ve.ui.MWIsbnLinkNodeInspector is unfinished.

TODO: i18n for href

TODO: refactor so other types of magic links can reuse most of this code.
Right now we have 4 new classes for each magic link type.  Probably don't
want to add 12 new files just for ISBN/RFC/PMID links.  Probably rename
*IsbnInternalLink* to *MagicLink*, and then parameterize by magic link
type (RFC/PMID/ISBN).

Bug: 63558
Change-Id: Id5b7a2ae3c80b0e5eed598f0bd024d3e94f7e9aa
---
M extension.json
A modules/ve-mw/ce/nodes/ve.ce.MWIsbnInternalLinkNode.js
A modules/ve-mw/dm/nodes/ve.dm.MWIsbnInternalLinkNode.js
M modules/ve-mw/ui/actions/ve.ui.MWLinkAction.js
A modules/ve-mw/ui/contextitems/ve.ui.MWIsbnInternalLinkNodeContextItem.js
A modules/ve-mw/ui/inspectors/ve.ui.MWIsbnLinkNodeInspector.js
M modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js
7 files changed, 423 insertions(+), 3 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor 
refs/changes/69/233669/1

diff --git a/extension.json b/extension.json
index ad1d4f4..95a344a 100644
--- a/extension.json
+++ b/extension.json
@@ -1191,18 +1191,22 @@
                "ext.visualEditor.mwlink": {
                        "scripts": [
                                
"modules/ve-mw/dm/nodes/ve.dm.MWNumberedExternalLinkNode.js",
+                               
"modules/ve-mw/dm/nodes/ve.dm.MWIsbnInternalLinkNode.js",
                                
"modules/ve-mw/dm/annotations/ve.dm.MWExternalLinkAnnotation.js",
                                
"modules/ve-mw/dm/annotations/ve.dm.MWInternalLinkAnnotation.js",
                                
"modules/ve-mw/ce/nodes/ve.ce.MWNumberedExternalLinkNode.js",
+                               
"modules/ve-mw/ce/nodes/ve.ce.MWIsbnInternalLinkNode.js",
                                
"modules/ve-mw/ce/annotations/ve.ce.MWExternalLinkAnnotation.js",
                                
"modules/ve-mw/ce/annotations/ve.ce.MWInternalLinkAnnotation.js",
                                
"modules/ve-mw/ui/widgets/ve.ui.MWInternalLinkAnnotationWidget.js",
                                
"modules/ve-mw/ui/widgets/ve.ui.MWExternalLinkAnnotationWidget.js",
                                
"modules/ve-mw/ui/inspectors/ve.ui.MWLinkAnnotationInspector.js",
                                
"modules/ve-mw/ui/inspectors/ve.ui.MWLinkNodeInspector.js",
+                               
"modules/ve-mw/ui/inspectors/ve.ui.MWIsbnLinkNodeInspector.js",
                                
"modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js",
                                
"modules/ve-mw/ui/contextitems/ve.ui.MWInternalLinkContextItem.js",
-                               
"modules/ve-mw/ui/contextitems/ve.ui.MWNumberedExternalLinkNodeContextItem.js"
+                               
"modules/ve-mw/ui/contextitems/ve.ui.MWNumberedExternalLinkNodeContextItem.js",
+                               
"modules/ve-mw/ui/contextitems/ve.ui.MWIsbnInternalLinkNodeContextItem.js"
                        ],
                        "styles": [
                                
"modules/ve-mw/ui/styles/contextitems/ve.ui.MWInternalLinkContextItem.css",
diff --git a/modules/ve-mw/ce/nodes/ve.ce.MWIsbnInternalLinkNode.js 
b/modules/ve-mw/ce/nodes/ve.ce.MWIsbnInternalLinkNode.js
new file mode 100644
index 0000000..420c4e3
--- /dev/null
+++ b/modules/ve-mw/ce/nodes/ve.ce.MWIsbnInternalLinkNode.js
@@ -0,0 +1,80 @@
+/*!
+ * VisualEditor ContentEditable MWIsbnInternalLinkNode class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+/**
+ * ContentEditable MediaWiki numbered external link node.
+ *
+ * @class
+ * @extends ve.ce.LeafNode
+ * @mixins ve.ce.FocusableNode
+ * @constructor
+ * @param {ve.dm.MWIsbnInternalLinkNode} model Model to observe
+ * @param {Object} [config] Configuration options
+ */
+ve.ce.MWIsbnInternalLinkNode = function VeCeMWIsbnInternalLinkNode( model, 
config ) {
+       // Parent constructor
+       ve.ce.LeafNode.call( this, model, config );
+
+       // Mixin constructors
+       ve.ce.FocusableNode.call( this );
+
+       // DOM changes
+       this.$element
+               .addClass( 've-ce-mwIsbnInternalLinkNode' )
+               // Need CE=false to prevent selection issues
+               .prop( 'contentEditable', 'false' );
+
+       // Add link
+       this.$link = $( '<a>' )
+               .attr( 'rel', 'mw:WikiLink' )
+               .appendTo( this.$element );
+
+       // Events
+       this.model.connect( this, { update: 'onUpdate' } );
+
+       // Initialization
+       this.onUpdate();
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ce.MWIsbnInternalLinkNode, ve.ce.LeafNode );
+
+OO.mixinClass( ve.ce.MWIsbnInternalLinkNode, ve.ce.FocusableNode );
+
+/* Static Properties */
+
+ve.ce.MWIsbnInternalLinkNode.static.name = 'link/mwIsbnInternal';
+
+ve.ce.MWIsbnInternalLinkNode.static.tagName = 'span';
+
+ve.ce.MWIsbnInternalLinkNode.static.primaryCommandName = 'link';
+
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ce.MWIsbnInternalLinkNode.static.getDescription = function ( model ) {
+       return model.getAttribute( 'content' );
+};
+
+/* Methods */
+
+/**
+ * Handle model update events.
+ *
+ * @method
+ */
+ve.ce.MWIsbnInternalLinkNode.prototype.onUpdate = function () {
+       this.$link.attr( 'href', this.model.getHref() );
+       this.$link.text( this.model.getAttribute( 'content' ) );
+};
+
+/* Registration */
+
+ve.ce.nodeFactory.register( ve.ce.MWIsbnInternalLinkNode );
diff --git a/modules/ve-mw/dm/nodes/ve.dm.MWIsbnInternalLinkNode.js 
b/modules/ve-mw/dm/nodes/ve.dm.MWIsbnInternalLinkNode.js
new file mode 100644
index 0000000..f5bdaba
--- /dev/null
+++ b/modules/ve-mw/dm/nodes/ve.dm.MWIsbnInternalLinkNode.js
@@ -0,0 +1,140 @@
+/*!
+ * VisualEditor DataModel MWIsbnInternalLinkNode class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+/**
+ * DataModel MediaWiki numbered external link node.
+ *
+ * @class
+ * @extends ve.dm.LeafNode
+ * @mixins ve.dm.FocusableNode
+ *
+ * @constructor
+ * @param {Object} [element] Reference to element in linear model
+ */
+ve.dm.MWIsbnInternalLinkNode = function VeDmMWIsbnInternalLinkNode() {
+       // Parent constructor
+       ve.dm.LeafNode.apply( this, arguments );
+
+       // Mixin constructors
+       ve.dm.FocusableNode.call( this );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.dm.MWIsbnInternalLinkNode, ve.dm.LeafNode );
+
+OO.mixinClass( ve.dm.MWIsbnInternalLinkNode, ve.dm.FocusableNode );
+
+/* Static Properties */
+
+ve.dm.MWIsbnInternalLinkNode.static.name = 'link/mwIsbnInternal';
+
+ve.dm.MWIsbnInternalLinkNode.static.isContent = true;
+
+ve.dm.MWIsbnInternalLinkNode.static.matchTagNames = [ 'a' ];
+
+ve.dm.MWIsbnInternalLinkNode.static.matchRdfaTypes = [ 'mw:WikiLink' ];
+
+ve.dm.MWIsbnInternalLinkNode.static.blacklistedAnnotationTypes = [ 'link' ];
+
+ve.dm.MWIsbnInternalLinkNode.static.getIsbnCode = function ( isbnText ) {
+       if ( !/^ISBN[^-0-9][\s\S]+[0-9Xx]$/.test( isbnText ) ) {
+               return null;
+       }
+       // Remove unicode whitespace and dashes
+       space_dash = /[-\t \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+/g;
+       isbncode = isbnText.replace( space_dash, '' ).replace( /^ISBN/, '' );
+       if ( !/^(97[89])?\d{9}[0-9Xx]$/.test( isbncode ) ) {
+               return null;
+       }
+       return isbncode;
+};
+
+ve.dm.MWIsbnInternalLinkNode.static.getHref = function ( isbncode ) {
+       // XXX localize
+       return './Special:BookSources/' + isbncode;
+};
+
+ve.dm.MWIsbnInternalLinkNode.static.matchFunction = function ( element ) {
+       var i, content, isbncode,
+               children = element.childNodes,
+               href = element.getAttribute( 'href' );
+       // All children must be text nodes, or a <span> representing an entity.
+       for ( i = 0; i < children.length; i++ ) {
+               if (children[ i ].nodeType === Node.TEXT_NODE) {
+                       continue;
+               }
+               // <span typeof='mw:Entity'>...</span> (for &nbsp;)
+               if (children[ i ].nodeType === Node.ELEMENT_NODE &&
+                       children[ i ].tagName === 'span' &&
+                       children[ i ].getAttribute( 'typeof' ) === 'mw:Entity') 
{
+                       continue;
+               }
+               return false;
+       }
+       content = element.textContent;
+       isbncode = ve.dm.MWIsbnInternalLinkNode.static.getIsbnCode(
+               element.textContent
+       );
+       if ( isbncode === null ) {
+               return false;
+       }
+       // Now the href must also match.
+       // XXX LOCALIZE Special:BookSources
+       if ( !/^([.]+\/)*Special:BookSources\/\d+[Xx]?$/.test( href ) ) {
+               return false;
+       }
+       if ( href.slice( -(isbncode.length + 1) ) !== ( '/' + isbncode ) ) {
+               return false;
+       }
+       return true;
+};
+
+ve.dm.MWIsbnInternalLinkNode.static.toDataElement = function ( domElements ) {
+       return {
+               type: this.name,
+               attributes: {
+                       content: domElements[ 0 ].textContent
+               }
+       };
+};
+
+ve.dm.MWIsbnInternalLinkNode.static.toDomElements = function ( dataElement, 
doc ) {
+       var content = dataElement.attributes.content,
+               isbncode = ve.dm.MWIsbnInternalLinkNode.static.getIsbnCode( 
content ),
+               href = ve.dm.MWIsbnInternalLinkNode.static.getHref( isbncode ),
+               domElement = doc.createElement( 'a' );
+       domElement.setAttribute( 'href', href );
+       domElement.setAttribute( 'rel', 'mw:WikiLink' );
+       domElement.textContent = content;
+       return [ domElement ];
+};
+
+/* Methods */
+
+/**
+ * Convenience wrapper for .getHref() on the current element.
+ *
+ * @return {string} Link href
+ */
+ve.dm.MWIsbnInternalLinkNode.prototype.getHref = function () {
+       return ve.dm.MWIsbnInternalLinkNode.static.getHref(
+               this.getIsbnCode()
+       );
+};
+
+/**
+ */
+ve.dm.MWIsbnInternalLinkNode.prototype.getIsbnCode = function () {
+       return ve.dm.MWIsbnInternalLinkNode.static.getIsbnCode(
+               this.element.attributes.content
+       );
+};
+
+/* Registration */
+
+ve.dm.modelRegistry.register( ve.dm.MWIsbnInternalLinkNode );
diff --git a/modules/ve-mw/ui/actions/ve.ui.MWLinkAction.js 
b/modules/ve-mw/ui/actions/ve.ui.MWLinkAction.js
index a9f44de..bbf22e3 100644
--- a/modules/ve-mw/ui/actions/ve.ui.MWLinkAction.js
+++ b/modules/ve-mw/ui/actions/ve.ui.MWLinkAction.js
@@ -124,6 +124,8 @@
 
        if ( fragment.getSelectedNode() instanceof 
ve.dm.MWNumberedExternalLinkNode ) {
                windowName = 'linkNode';
+       } else if ( fragment.getSelectedNode() instanceof 
ve.dm.MWIsbnInternalLinkNode ) {
+               windowName = 'linkIsbnNode';
        }
        this.surface.execute( 'window', 'open', windowName );
        return true;
diff --git 
a/modules/ve-mw/ui/contextitems/ve.ui.MWIsbnInternalLinkNodeContextItem.js 
b/modules/ve-mw/ui/contextitems/ve.ui.MWIsbnInternalLinkNodeContextItem.js
new file mode 100644
index 0000000..a9ddb7f
--- /dev/null
+++ b/modules/ve-mw/ui/contextitems/ve.ui.MWIsbnInternalLinkNodeContextItem.js
@@ -0,0 +1,44 @@
+/*!
+ * VisualEditor MWIsbnInternalLinkNodeContextItem class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Context item for a MWIsbnInternalLinkNode.
+ *
+ * @class
+ * @extends ve.ui.LinkContextItem
+ *
+ * @constructor
+ * @param {ve.ui.Context} context Context item is in
+ * @param {ve.dm.Model} model Model item is related to
+ * @param {Object} config Configuration options
+ */
+ve.ui.MWIsbnInternalLinkNodeContextItem = function 
VeUiMWIsbnInternalLinkNodeContextItem() {
+       // Parent constructor
+       ve.ui.MWIsbnInternalLinkNodeContextItem.super.apply( this, arguments );
+
+       // Initialization
+       this.$element.addClass( 've-ui-mwIsbnInternalLinkNodeContextItem' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.MWIsbnInternalLinkNodeContextItem, 
ve.ui.LinkContextItem );
+
+/* Static Properties */
+
+ve.ui.MWIsbnInternalLinkNodeContextItem.static.name = 'link/mwIsbnInternal';
+
+ve.ui.MWIsbnInternalLinkNodeContextItem.static.modelClasses = [ 
ve.dm.MWIsbnInternalLinkNode ];
+
+/* Methods */
+
+ve.ui.MWIsbnInternalLinkNodeContextItem.prototype.getDescription = function () 
{
+       return this.model.getAttribute( 'content' );
+};
+
+/* Registration */
+
+ve.ui.contextItemFactory.register( ve.ui.MWIsbnInternalLinkNodeContextItem );
diff --git a/modules/ve-mw/ui/inspectors/ve.ui.MWIsbnLinkNodeInspector.js 
b/modules/ve-mw/ui/inspectors/ve.ui.MWIsbnLinkNodeInspector.js
new file mode 100644
index 0000000..3d952e7
--- /dev/null
+++ b/modules/ve-mw/ui/inspectors/ve.ui.MWIsbnLinkNodeInspector.js
@@ -0,0 +1,149 @@
+/*!
+ * VisualEditor UserInterface MWIsbnLinkNodeInspector class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+/**
+ * Inspector for editing unlabeled MediaWiki external links.
+ *
+ * @class
+ * @extends ve.ui.NodeInspector
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+ve.ui.MWIsbnLinkNodeInspector = function VeUiMWIsbnLinkNodeInspector( config ) 
{
+       // Parent constructor
+       ve.ui.NodeInspector.call( this, config );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.MWIsbnLinkNodeInspector, ve.ui.NodeInspector );
+
+/* Static properties */
+
+ve.ui.MWIsbnLinkNodeInspector.static.name = 'linkIsbnNode';
+
+ve.ui.MWIsbnLinkNodeInspector.static.icon = 'link';
+
+ve.ui.MWIsbnLinkNodeInspector.static.title = OO.ui.deferMsg( 
'visualeditor-linknodeinspector-title' );
+
+ve.ui.MWIsbnLinkNodeInspector.static.modelClasses = [ 
ve.dm.MWIsbnInternalLinkNode ];
+
+ve.ui.MWIsbnLinkNodeInspector.static.actions = 
ve.ui.MWIsbnLinkNodeInspector.super.static.actions.concat( [
+       {
+               action: 'convert',
+               label: OO.ui.deferMsg( 
'visualeditor-linknodeinspector-add-label' ),
+               modes: [ 'edit' ]
+       }
+] );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ui.MWIsbnLinkNodeInspector.prototype.initialize = function () {
+       // Parent method
+       ve.ui.MWIsbnLinkNodeInspector.super.prototype.initialize.call( this );
+
+       // Properties
+       this.targetInput = new OO.ui.TextInputWidget( {
+               validate: ve.init.platform.getExternalLinkUrlProtocolsRegExp()
+       } );
+
+       // Initialization
+       this.form.$element.append( this.targetInput.$element );
+};
+
+/**
+ * @inheritdoc
+ */
+ve.ui.MWIsbnLinkNodeInspector.prototype.getActionProcess = function ( action ) 
{
+       if ( action === 'convert' ) {
+               return new OO.ui.Process( function () {
+                       this.close( { action: action } );
+               }, this );
+       }
+       return 
ve.ui.MWIsbnLinkNodeInspector.super.prototype.getActionProcess.call( this, 
action );
+};
+
+/**
+ * @inheritdoc
+ */
+ve.ui.MWIsbnLinkNodeInspector.prototype.getSetupProcess = function ( data ) {
+       return 
ve.ui.MWIsbnLinkNodeInspector.super.prototype.getSetupProcess.call( this, data )
+               .next( function () {
+                       // Initialization
+                       this.targetInput.setValue(
+                               this.selectedNode ? 
this.selectedNode.getAttribute( 'href' ) : ''
+                       );
+               }, this );
+};
+
+/**
+ * @inheritdoc
+ */
+ve.ui.MWIsbnLinkNodeInspector.prototype.getReadyProcess = function ( data ) {
+       return 
ve.ui.MWIsbnLinkNodeInspector.super.prototype.getReadyProcess.call( this, data )
+               .next( function () {
+                       this.targetInput.focus().select();
+               }, this );
+};
+
+/**
+ * @inheritdoc
+ */
+ve.ui.MWIsbnLinkNodeInspector.prototype.getTeardownProcess = function ( data ) 
{
+       data = data || {};
+       return 
ve.ui.MWIsbnLinkNodeInspector.super.prototype.getTeardownProcess.call( this, 
data )
+               .first( function () {
+                       var content, annotation, annotations,
+                               surfaceModel = this.getFragment().getSurface(),
+                               doc = surfaceModel.getDocument(),
+                               nodeRange = this.selectedNode.getOuterRange(),
+                               value = this.targetInput.getValue(),
+                               convert = data.action === 'convert',
+                               remove = data.action === 'remove' || !value;
+
+                       // Default to http:// if the external link doesn't 
already begin with a supported
+                       // protocol - this prevents the link from being 
converted into literal text upon
+                       // save and also fixes a common mistake users may make
+                       if ( 
!ve.init.platform.getExternalLinkUrlProtocolsRegExp().test( value ) ) {
+                               value = 'http://' + value;
+                       }
+
+                       if ( remove ) {
+                               surfaceModel.change(
+                                       ve.dm.Transaction.newFromRemoval( doc, 
nodeRange )
+                               );
+                       } else if ( convert ) {
+                               annotation = new 
ve.dm.MWExternalLinkAnnotation( {
+                                       type: 'link/mwExternal',
+                                       attributes: {
+                                               href: value
+                                       }
+                               } );
+                               annotations = 
doc.data.getAnnotationsFromOffset( nodeRange.start ).clone();
+                               annotations.push( annotation );
+                               content = value.split( '' );
+                               ve.dm.Document.static.addAnnotationsToData( 
content, annotations );
+                               surfaceModel.change(
+                                       ve.dm.Transaction.newFromReplacement( 
doc, nodeRange, content )
+                               );
+                       } else {
+                               surfaceModel.change(
+                                       
ve.dm.Transaction.newFromAttributeChanges(
+                                               doc, nodeRange.start, { href: 
value }
+                                       )
+                               );
+                       }
+               }, this );
+};
+
+/* Registration */
+
+ve.ui.windowFactory.register( ve.ui.MWIsbnLinkNodeInspector );
diff --git a/modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js 
b/modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js
index e41f1d3..bccc7b9 100644
--- a/modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js
+++ b/modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js
@@ -29,10 +29,11 @@
 
 ve.ui.MWLinkInspectorTool.static.modelClasses =
        ve.ui.MWLinkInspectorTool.super.static.modelClasses.concat( [
-               ve.dm.MWNumberedExternalLinkNode
+               ve.dm.MWNumberedExternalLinkNode,
+               ve.dm.MWIsbnInternalLinkNode
        ] );
 
-ve.ui.MWLinkInspectorTool.static.associatedWindows = [ 'link', 'linkNode' ];
+ve.ui.MWLinkInspectorTool.static.associatedWindows = [ 'link', 'linkNode', 
'linkIsbnNode' ];
 
 ve.ui.toolFactory.register( ve.ui.MWLinkInspectorTool );
 

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Id5b7a2ae3c80b0e5eed598f0bd024d3e94f7e9aa
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Cscott <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to