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 )
+ 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