jenkins-bot has submitted this change and it was merged. Change subject: Specialized inspector for ISBN magic links ......................................................................
Specialized inspector for ISBN magic links Implement a special node type, context item, and inspector for ISBN/PMID/RFC magic links. Add buttons to the link inspectors to convert back and forth between "simple" links, and magic links. Depends on I5d000d8b63dafdfe0a2753069d3f0ac5b03b8829 in Parsoid for clean round-tripping of localized ISBN magic links. Bug: T63558 Change-Id: Id5b7a2ae3c80b0e5eed598f0bd024d3e94f7e9aa --- M VisualEditor.hooks.php M extension.json A modules/ve-mw/ce/nodes/ve.ce.MWMagicLinkNode.js M modules/ve-mw/dm/annotations/ve.dm.MWInternalLinkAnnotation.js A modules/ve-mw/dm/nodes/ve.dm.MWMagicLinkNode.js M modules/ve-mw/i18n/en.json M modules/ve-mw/i18n/qqq.json M modules/ve-mw/tests/ui/actions/ve.ui.MWLinkAction.test.js M modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.test.js M modules/ve-mw/ui/actions/ve.ui.MWLinkAction.js A modules/ve-mw/ui/contextitems/ve.ui.MWMagicLinkNodeContextItem.js M modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js M modules/ve-mw/ui/inspectors/ve.ui.MWLinkAnnotationInspector.js A modules/ve-mw/ui/inspectors/ve.ui.MWMagicLinkNodeInspector.js M modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js 15 files changed, 926 insertions(+), 95 deletions(-) Approvals: Esanders: Looks good to me, approved jenkins-bot: Verified diff --git a/VisualEditor.hooks.php b/VisualEditor.hooks.php index 767b4d8..84b278f 100644 --- a/VisualEditor.hooks.php +++ b/VisualEditor.hooks.php @@ -434,6 +434,7 @@ 'enableTocWidget' => $veConfig->get( 'VisualEditorEnableTocWidget' ), 'svgMaxSize' => $coreConfig->get( 'SVGMaxSize' ), 'namespacesWithSubpages' => $coreConfig->get( 'NamespacesWithSubpages' ), + 'specialBooksources' => urldecode( SpecialPage::getTitleFor( 'Booksources' )->getPrefixedURL() ), 'restbaseUrl' => $coreConfig->get( 'VisualEditorRestbaseURL' ), ); diff --git a/extension.json b/extension.json index ad1d4f4..80960ca 100644 --- a/extension.json +++ b/extension.json @@ -1190,9 +1190,11 @@ }, "ext.visualEditor.mwlink": { "scripts": [ + "modules/ve-mw/dm/nodes/ve.dm.MWMagicLinkNode.js", "modules/ve-mw/dm/nodes/ve.dm.MWNumberedExternalLinkNode.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.MWMagicLinkNode.js", "modules/ve-mw/ce/nodes/ve.ce.MWNumberedExternalLinkNode.js", "modules/ve-mw/ce/annotations/ve.ce.MWExternalLinkAnnotation.js", "modules/ve-mw/ce/annotations/ve.ce.MWInternalLinkAnnotation.js", @@ -1200,8 +1202,10 @@ "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.MWMagicLinkNodeInspector.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.MWMagicLinkNodeContextItem.js", "modules/ve-mw/ui/contextitems/ve.ui.MWNumberedExternalLinkNodeContextItem.js" ], "styles": [ @@ -1213,11 +1217,21 @@ ], "messages": [ "visualeditor-annotationbutton-linknode-tooltip", + "visualeditor-annotationbutton-magiclinknode-tooltip-isbn", + "visualeditor-annotationbutton-magiclinknode-tooltip-pmid", + "visualeditor-annotationbutton-magiclinknode-tooltip-rfc", "visualeditor-linkinspector-button-link-external", "visualeditor-linkinspector-button-link-internal", + "visualeditor-linkinspector-convert-link-isbn", + "visualeditor-linkinspector-convert-link-pmid", + "visualeditor-linkinspector-convert-link-rfc", "visualeditor-linkinspector-illegal-title", "visualeditor-linknodeinspector-add-label", - "visualeditor-linknodeinspector-title" + "visualeditor-linknodeinspector-title", + "visualeditor-magiclinknodeinspector-convert-link", + "visualeditor-magiclinknodeinspector-title-isbn", + "visualeditor-magiclinknodeinspector-title-pmid", + "visualeditor-magiclinknodeinspector-title-rfc" ], "targets": [ "desktop", diff --git a/modules/ve-mw/ce/nodes/ve.ce.MWMagicLinkNode.js b/modules/ve-mw/ce/nodes/ve.ce.MWMagicLinkNode.js new file mode 100644 index 0000000..6ce9750 --- /dev/null +++ b/modules/ve-mw/ce/nodes/ve.ce.MWMagicLinkNode.js @@ -0,0 +1,81 @@ +/*! + * VisualEditor ContentEditable MWMagicLinkNode class. + * + * @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * ContentEditable MediaWiki magic link node. + * + * @class + * @extends ve.ce.LeafNode + * @mixins ve.ce.FocusableNode + * @constructor + * @param {ve.dm.MWMagicLinkNode} model Model to observe + * @param {Object} [config] Configuration options + */ +ve.ce.MWMagicLinkNode = function VeCeMWMagicLinkNode( 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-mwMagicLinkNode' ) + // Need CE=false to prevent selection issues + .prop( 'contentEditable', 'false' ); + + // Add link + this.$link = $( '<a>' ) + .appendTo( this.$element ); + + // Events + this.model.connect( this, { update: 'onUpdate' } ); + + // Initialization + this.onUpdate(); +}; + +/* Inheritance */ + +OO.inheritClass( ve.ce.MWMagicLinkNode, ve.ce.LeafNode ); + +OO.mixinClass( ve.ce.MWMagicLinkNode, ve.ce.FocusableNode ); + +/* Static Properties */ + +ve.ce.MWMagicLinkNode.static.name = 'link/mwMagic'; + +ve.ce.MWMagicLinkNode.static.tagName = 'span'; + +ve.ce.MWMagicLinkNode.static.primaryCommandName = 'link'; + +/* Static Methods */ + +/** + * @inheritdoc + */ +ve.ce.MWMagicLinkNode.static.getDescription = function ( model ) { + return model.getAttribute( 'content' ); +}; + +/* Methods */ + +/** + * Handle model update events. + * + * @method + */ +ve.ce.MWMagicLinkNode.prototype.onUpdate = function () { + this.$link + .attr( 'href', this.model.getHref() ) + .attr( 'rel', this.model.getRel() ) + .text( this.model.getAttribute( 'content' ) ); +}; + +/* Registration */ + +ve.ce.nodeFactory.register( ve.ce.MWMagicLinkNode ); diff --git a/modules/ve-mw/dm/annotations/ve.dm.MWInternalLinkAnnotation.js b/modules/ve-mw/dm/annotations/ve.dm.MWInternalLinkAnnotation.js index dbe22b0..13843ed 100644 --- a/modules/ve-mw/dm/annotations/ve.dm.MWInternalLinkAnnotation.js +++ b/modules/ve-mw/dm/annotations/ve.dm.MWInternalLinkAnnotation.js @@ -137,7 +137,8 @@ href = dataElement.attributes.hrefPrefix + href; } } else { - href = encodeURIComponent( title ); + // Don't escape slashes in the title; they represent subpages. + href = title.split( '/' ).map( encodeURIComponent ).join( '/' ); } return href; }; diff --git a/modules/ve-mw/dm/nodes/ve.dm.MWMagicLinkNode.js b/modules/ve-mw/dm/nodes/ve.dm.MWMagicLinkNode.js new file mode 100644 index 0000000..d09b9b9 --- /dev/null +++ b/modules/ve-mw/dm/nodes/ve.dm.MWMagicLinkNode.js @@ -0,0 +1,415 @@ +/*! + * VisualEditor DataModel MWMagicLinkNode class. + * + * @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * DataModel MediaWiki magic link node. + * + * @class + * @extends ve.dm.LeafNode + * @mixins ve.dm.FocusableNode + * + * @constructor + * @param {Object} [element] Reference to element in linear model + */ +ve.dm.MWMagicLinkNode = function VeDmMWMagicLinkNode() { + // Parent constructor + ve.dm.LeafNode.apply( this, arguments ); + + // Mixin constructors + ve.dm.FocusableNode.call( this ); +}; + +/* Inheritance */ + +OO.inheritClass( ve.dm.MWMagicLinkNode, ve.dm.LeafNode ); + +OO.mixinClass( ve.dm.MWMagicLinkNode, ve.dm.FocusableNode ); + +/* Static Properties */ + +ve.dm.MWMagicLinkNode.static.name = 'link/mwMagic'; + +ve.dm.MWMagicLinkNode.static.isContent = true; + +ve.dm.MWMagicLinkNode.static.matchTagNames = [ 'a' ]; + +ve.dm.MWMagicLinkNode.static.matchRdfaTypes = [ 'mw:WikiLink', 'mw:ExtLink' ]; + +ve.dm.MWMagicLinkNode.static.blacklistedAnnotationTypes = [ 'link' ]; + +/** + * Determine whether the given `element` is a magic link. + * + * @return {boolean} True if the element is a magic link + */ +ve.dm.MWMagicLinkNode.static.matchFunction = function ( element ) { + var i, + 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; + } + + // Check that text content matches one of the magic link types and that + // the href matches that expected for the magic link type. + return ve.dm.MWMagicLinkNode.static.validateHref( element.textContent, href ); +}; + +/** + * Test that a proposed content string is valid for a magic link. + * If `optType` is given, additionally verify that the content string is + * valid for the particular type of magic link (RFC/ISBN/PMID). + * + * @param {string} content + * The content string to test. + * @param {string} [optType] + * The desired type of magic link, one of "RFC", "ISBN", or "PMID". + * If not supplied, returns true if the content is valid for any one + * of these. + * @return {boolean} + * True if the content string is valid for a magic link of the appropriate + * type (or any type). + */ +ve.dm.MWMagicLinkNode.static.validateContent = function ( content, optType ) { + var type = ve.dm.MWMagicLinkType.static.fromContent( content ); + if ( type === null || ( optType !== undefined && type.type !== optType ) ) { + // Not a valid magic link, or a magic link of the wrong type. + return false; + } + return true; +}; + +/** + * Test that a proposed content string and href is valid for a magic link. + * + * @param {string} content + * The content string to test. + * @param {string} href + * The URL target of the magic link. + * @return {boolean} + * True if the content string and href are valid for a magic link. + */ +ve.dm.MWMagicLinkNode.static.validateHref = function ( content, href ) { + var type = ve.dm.MWMagicLinkType.static.fromContent( content ); + return type && type.matchHref( href ); +}; + +/** + * Return a link annotation appropriate for converting a magic link + * with the given content into a simple link, or `null` if the given + * content is not a valid magic link. + * + * @return {ve.dm.MWExternalLinkAnnotation|ve.dm.MWInternalLinkAnnotation|null} + */ +ve.dm.MWMagicLinkNode.static.annotationFromContent = function ( content ) { + var type = ve.dm.MWMagicLinkType.static.fromContent( content ); + return type !== null ? type.getAnnotation() : null; +}; + +/** + * @inheritdoc + */ +ve.dm.MWMagicLinkNode.static.toDataElement = function ( domElements ) { + var textContent = domElements[ 0 ].textContent, + htmlContent = domElements[ 0 ].innerHTML; + return { + type: this.name, + attributes: { + content: textContent, + // These next two attributes allow lossless round-tripping + // if the original wikitext contained html entities like + // + origText: textContent, + origHtml: htmlContent + } + }; +}; + +/** + * @inheritdoc + */ +ve.dm.MWMagicLinkNode.static.toDomElements = function ( dataElement, doc ) { + var content = dataElement.attributes.content, + type = ve.dm.MWMagicLinkType.static.fromContent( content ), + href = type.getHref(), + domElement = doc.createElement( 'a' ); + domElement.setAttribute( 'href', href ); + domElement.setAttribute( 'rel', type.rel ); + if ( content === dataElement.attributes.origText ) { + // Preserve <span typeof="mw:Entity"> elements from the original. + domElement.innerHTML = dataElement.attributes.origHtml; + } else { + domElement.textContent = content; + } + return [ domElement ]; +}; + +/* Methods */ + +/** + * Return the link target appropriate for this magic link node. + * + * @return {string} Link href + */ +ve.dm.MWMagicLinkNode.prototype.getHref = function () { + var content = this.element.attributes.content, + type = ve.dm.MWMagicLinkType.static.fromContent( content ); + return type.getHref(); +}; + +/** + * Return the rel attribute appropriate for this magic link node. + * + * @return {string} Either "mw:ExtLink" or "mw:WikiLink" + */ +ve.dm.MWMagicLinkNode.prototype.getRel = function () { + var content = this.element.attributes.content, + type = ve.dm.MWMagicLinkType.static.fromContent( content ); + return type.rel; +}; + +/** + * Return the type of this magic link node: one of "RFC", "PMID", or "ISBN". + * + * @return {string} Magic link type + */ +ve.dm.MWMagicLinkNode.prototype.getMagicType = function () { + var content = this.element.attributes.content, + type = ve.dm.MWMagicLinkType.static.fromContent( content ); + return type.type; +}; + +/** + * Return the numeric code associated with this magic link node. + * + * @return {string} + */ +ve.dm.MWMagicLinkNode.prototype.getCode = function () { + var content = this.element.attributes.content, + type = ve.dm.MWMagicLinkType.static.fromContent( content ); + return type.num; +}; + +/** + * Encapsulation of a particular magic link type. + * + * @class + * @private + * + * @constructor + * @param {string} type + * The type of magic link; one of `'ISBN'`, `'PMID'`, or `'RFC'`. + * @param {string} rel + * The value of the link's "rel" attribute. + * Either `'mw:ExtLink'` or `'mw:WikiLink'`. + * @param {string} content + * The content of the magic link. + */ +ve.dm.MWMagicLinkType = function VeDmMWMagicLinkType( type, rel, content ) { + this.type = type; + this.rel = rel; + this.content = content; + // Make the code available as a property; this is also used for + // validity checking. + this.code = this.getCode(); +}; + +OO.initClass( ve.dm.MWMagicLinkType ); + +/** + * @inheritdoc ve.dm.MWMagicLinkNode#annotationFromContent + */ +ve.dm.MWMagicLinkType.prototype.getAnnotation = function () { + return new ve.dm.MWExternalLinkAnnotation( { + type: 'link/mwExternal', + attributes: { href: this.getHref() } + } ); +}; + +/** + * @inheritdoc ve.dm.MWMagicLinkNode#getCode + * @protected + */ +ve.dm.MWMagicLinkType.prototype.getCode = function () { + var m = /^([A-Z]+)[\t \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+(\d+)$/.exec( this.content ); + if ( !m || m[ 1 ] !== this.type ) { + return null; + } + return m[ 2 ]; +}; + +/** + * @method getHref + * @inheritdoc ve.dm.MWMagicLinkNode#getHref + */ + +/** + * Return true if the given href is appropriate for this magic link. + * + * @param {string} href + * @return {boolean} + */ +ve.dm.MWMagicLinkType.prototype.matchHref = function ( href ) { + return href.replace( /^https?:/i, '' ) === this.getHref(); +}; + +/** + * Return the subclass of {@link ve.dm.MWMagicLinkType} + * appropriate for the given content, or `null` if the content + * is not appropriate for a magic link. + * + * @param {string} content + * @return {ve.dm.MWMagicLinkType|null} + */ +ve.dm.MWMagicLinkType.static.fromContent = function ( content ) { + var m = /^(ISBN|PMID|RFC)/.exec( content ), + typeStr = m && m[ 1 ], + type = null; + if ( typeStr === 'ISBN' ) { + type = new ve.dm.MWMagicLinkIsbnType( content ); + } else if ( typeStr === 'PMID' ) { + type = new ve.dm.MWMagicLinkPmidType( content ); + } else if ( typeStr === 'RFC' ) { + type = new ve.dm.MWMagicLinkRfcType( content ); + } + // validate parsed number + return type && type.code !== null ? type : null; +}; + +/** + * An ISBN magic link. + * + * @class + * @extends ve.dm.MWMagicLinkType + * @private + * + * @constructor + * @param {string} content + */ +ve.dm.MWMagicLinkIsbnType = function VeDmMWMagicLinkIsbnType( content ) { + // Parent constructor + ve.dm.MWMagicLinkIsbnType.super.call( this, 'ISBN', 'mw:WikiLink', content ); +}; + +OO.inheritClass( ve.dm.MWMagicLinkIsbnType, ve.dm.MWMagicLinkType ); + +/** + * @inheritdoc + */ +ve.dm.MWMagicLinkIsbnType.prototype.getAnnotation = function () { + var conf = mw.config.get( 'wgVisualEditorConfig' ), + title = mw.Title.newFromText( conf.specialBooksources + '/' + this.code ); + return ve.dm.MWInternalLinkAnnotation.static.newFromTitle( title ); +}; + +/** + * @inheritdoc + */ +ve.dm.MWMagicLinkIsbnType.prototype.getCode = function () { + var spaceOrDash, isbnCode, + content = this.content; + + if ( !/^ISBN[^-0-9][\s\S]+[0-9Xx]$/.test( content ) ) { + return null; + } + + // Remove unicode whitespace and dashes + spaceOrDash = /[-\t \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+/g; + isbnCode = content.replace( spaceOrDash, '' ).replace( /^ISBN/, '' ); + + // Verify format of ISBN + if ( !/^(97[89])?\d{9}[0-9Xx]$/.test( isbnCode ) ) { + return null; + } + return isbnCode.toUpperCase(); +}; + +/** + * @inheritdoc + */ +ve.dm.MWMagicLinkIsbnType.prototype.getHref = function () { + var conf = mw.config.get( 'wgVisualEditorConfig' ); + return './' + conf.specialBooksources + '/' + this.code; +}; + +/** + * @inheritdoc + */ +ve.dm.MWMagicLinkIsbnType.prototype.matchHref = function ( href ) { + var conf, m, normalized; + + conf = mw.config.get( 'wgVisualEditorConfig' ); + m = /^(?:[.]+\/)*([^\/]+)\/(\d+[Xx]?)$/.exec( href ); + if ( !m ) { + return false; + } + // conf.specialBooksources has localized name for Special:Booksources + normalized = ve.safeDecodeURIComponent( m[ 1 ] ).replace( ' ', '_' ); + if ( normalized !== 'Special:BookSources' && normalized !== conf.specialBooksources ) { + return false; + } + if ( m[ 2 ] !== this.code ) { + return false; + } + return true; +}; + +/** + * A PMID magic link. + * + * @class + * @extends ve.dm.MWMagicLinkType + * @private + * + * @constructor + * @param {string} content + */ +ve.dm.MWMagicLinkPmidType = function VeDmMWMagicLinkPmidType( content ) { + // Parent constructor + ve.dm.MWMagicLinkPmidType.super.call( this, 'PMID', 'mw:ExtLink', content ); +}; + +OO.inheritClass( ve.dm.MWMagicLinkPmidType, ve.dm.MWMagicLinkType ); + +ve.dm.MWMagicLinkPmidType.prototype.getHref = function () { + return '//www.ncbi.nlm.nih.gov/pubmed/' + this.code + '?dopt=Abstract'; +}; + +/** + * An RFC magic link. + * + * @class + * @extends ve.dm.MWMagicLinkType + * @private + * + * @constructor + * @param {string} content + */ +ve.dm.MWMagicLinkRfcType = function VeDmMWMagicLinkRfcType( content ) { + // Parent constructor + ve.dm.MWMagicLinkRfcType.super.call( this, 'RFC', 'mw:ExtLink', content ); +}; + +OO.inheritClass( ve.dm.MWMagicLinkRfcType, ve.dm.MWMagicLinkType ); + +ve.dm.MWMagicLinkRfcType.prototype.getHref = function () { + return '//tools.ietf.org/html/rfc' + this.code; +}; + +/* Registration */ + +ve.dm.modelRegistry.register( ve.dm.MWMagicLinkNode ); diff --git a/modules/ve-mw/i18n/en.json b/modules/ve-mw/i18n/en.json index 0e74169..43de2bb 100644 --- a/modules/ve-mw/i18n/en.json +++ b/modules/ve-mw/i18n/en.json @@ -49,6 +49,9 @@ "tooltip-ca-ve-edit": "Edit this page", "visualeditor-advancedsettings-tool": "Advanced settings", "visualeditor-annotationbutton-linknode-tooltip": "Simple link", + "visualeditor-annotationbutton-magiclinknode-tooltip-isbn": "ISBN link", + "visualeditor-annotationbutton-magiclinknode-tooltip-pmid": "PMID link", + "visualeditor-annotationbutton-magiclinknode-tooltip-rfc": "RFC link", "visualeditor-backbutton-tooltip": "Go back", "visualeditor-beta-appendix": "beta", "visualeditor-beta-label": "beta", @@ -245,6 +248,9 @@ "visualeditor-languages-tool": "Languages", "visualeditor-linkinspector-button-link-external": "External link", "visualeditor-linkinspector-button-link-internal": "Search pages", + "visualeditor-linkinspector-convert-link-isbn": "Convert to ISBN link", + "visualeditor-linkinspector-convert-link-pmid": "Convert to PMID link", + "visualeditor-linkinspector-convert-link-rfc": "Convert to RFC link", "visualeditor-linkinspector-illegal-title": "Invalid page title", "visualeditor-linknodeinspector-add-label": "Add label", "visualeditor-linknodeinspector-title": "Simple link", @@ -253,6 +259,10 @@ "visualeditor-loadwarning": "Error loading data from server: $1. Would you like to retry?", "visualeditor-loadwarning-token": "Error loading edit token from server: $1. Would you like to retry?", "visualeditor-mainnamespacepagelink": "Project:Main namespace", + "visualeditor-magiclinknodeinspector-convert-link": "Convert to simple link", + "visualeditor-magiclinknodeinspector-title-isbn": "ISBN link", + "visualeditor-magiclinknodeinspector-title-pmid": "PMID link", + "visualeditor-magiclinknodeinspector-title-rfc": "RFC link", "visualeditor-media-input-placeholder": "Search for media", "visualeditor-meta-tool": "Options", "visualeditor-mweditmodesource-title": "Switch to source editing?", diff --git a/modules/ve-mw/i18n/qqq.json b/modules/ve-mw/i18n/qqq.json index c4ab098..abd9d5f 100644 --- a/modules/ve-mw/i18n/qqq.json +++ b/modules/ve-mw/i18n/qqq.json @@ -58,6 +58,9 @@ "tooltip-ca-ve-edit": "Tooltip of the dedicated VisualEditor \"Edit\" tab.\n{{Identical|Edit this page}}", "visualeditor-advancedsettings-tool": "Tool for opening the advanced settings section of the meta dialog.\n{{Identical|Advanced settings}}", "visualeditor-annotationbutton-linknode-tooltip": "Tooltip text for link button for auto-numbered, labelless, external links.\n\nSee also:\n* {{msg-mw|Visualeditor-linknodeinspector-title}}\n{{Related|Visualeditor-annotationbutton}}", + "visualeditor-annotationbutton-magiclinknode-tooltip-isbn": "Tooltip text for link button for ISBN magic links.\n\nSee also:\n* {{msg-mw|Visualeditor-magiclinknodeinspector-title-isbn}}\n{{Related|Visualeditor-annotationbutton}}", + "visualeditor-annotationbutton-magiclinknode-tooltip-pmid": "Tooltip text for link button for PMID magic links.\n\nSee also:\n* {{msg-mw|Visualeditor-magiclinknodeinspector-title-pmid}}\n{{Related|Visualeditor-annotationbutton}}", + "visualeditor-annotationbutton-magiclinknode-tooltip-rfc": "Tooltip text for link button for RFC magic links.\n\nSee also:\n* {{msg-mw|Visualeditor-magiclinknodeinspector-title-rfc}}\n{{Related|Visualeditor-annotationbutton}}", "visualeditor-backbutton-tooltip": "Tooltip text for back button taking user to reading mode and closing the editor.\n{{Identical|Go back}}", "visualeditor-beta-appendix": "Used in {{msg-mw|Guidedtour-tour-firsteditve-edit-page-description}}.\n{{Identical|Beta}}", "visualeditor-beta-label": "Text of tool in the toolbar that highlights that VisualEditor is still in beta.\n{{Identical|Beta}}", @@ -254,6 +257,9 @@ "visualeditor-languages-tool": "Tool for opening the languages links section of the meta dialog.\n{{Identical|Language}}", "visualeditor-linkinspector-button-link-external": "Button label for entering an external link.\n{{Identical|External link}}", "visualeditor-linkinspector-button-link-internal": "Button label for entering an internal link.", + "visualeditor-linkinspector-convert-link-isbn": "Button label for converting a simple link to a ISBN magic link.", + "visualeditor-linkinspector-convert-link-pmid": "Button label for converting a simple link to a PMID magic link.", + "visualeditor-linkinspector-convert-link-rfc": "Button label for converting a simple link to a RFC magic link.", "visualeditor-linkinspector-illegal-title": "Warning that the entered text is not a valid page title.", "visualeditor-linknodeinspector-add-label": "Label of button that converts an auto-numbered, external, labelless link into a labeled external link", "visualeditor-linknodeinspector-title": "Title of inspector for editing auto-numbered, external, labelless links.\n\nSee also:\n* {{msg-mw|Visualeditor-annotationbutton-linknode-tooltip}}", @@ -262,6 +268,10 @@ "visualeditor-loadwarning": "Text (JavaScript confirm()) shown when the editor fails to load properly.\n\nParameters:\n* $1 - the error message from the server, in English. e.g. \"parsoidserver-http-err\"", "visualeditor-loadwarning-token": "Text (JavaScript confirm()) shown when the editor fails to load properly.\n\nParameters:\n* $1 - the error message from the server.", "visualeditor-mainnamespacepagelink": "Name of a page describing the main namespace (NS0) in this project.\n{{doc-important|Do not translate \"Project\"; it is automatically converted to the wiki's project namespace.}}", + "visualeditor-magiclinknodeinspector-convert-link": "Label of button that converts a magic link into a normal labeled link", + "visualeditor-magiclinknodeinspector-title-isbn": "Title of inspector for editing ISBN magic links.\n\nSee also:\n* {{msg-mw|Visualeditor-annotationbutton-magiclinknode-tooltip-isbn}}", + "visualeditor-magiclinknodeinspector-title-pmid": "Title of inspector for editing PMID magic links.\n\nSee also:\n* {{msg-mw|Visualeditor-annotationbutton-magiclinknode-tooltip-pmid}}", + "visualeditor-magiclinknodeinspector-title-rfc": "Title of inspector for editing RFC magic links.\n\nSee also:\n* {{msg-mw|Visualeditor-annotationbutton-magiclinknode-tooltip-rfc}}", "visualeditor-media-input-placeholder": "Place holder text for media search input", "visualeditor-meta-tool": "Text of tool in the toolbar the lets users set categories, language links and other page settings.\n{{Identical|Options}}", "visualeditor-mweditmodesource-title": "Title of dialog to confirm switching to source mode.", diff --git a/modules/ve-mw/tests/ui/actions/ve.ui.MWLinkAction.test.js b/modules/ve-mw/tests/ui/actions/ve.ui.MWLinkAction.test.js index ba60cc3..f85a8ea 100644 --- a/modules/ve-mw/tests/ui/actions/ve.ui.MWLinkAction.test.js +++ b/modules/ve-mw/tests/ui/actions/ve.ui.MWLinkAction.test.js @@ -114,7 +114,8 @@ expectedRange: new ve.Range( 21, 21 ), expectedData: function ( data, makeAnnotation ) { var i, - a = makeAnnotation( './Special:BookSources/9780596517748' ); + conf = mw.config.get( 'wgVisualEditorConfig' ), + a = makeAnnotation( './' + conf.specialBooksources + '/9780596517748' ); for ( i = 1; i < 20; i++ ) { data[ i ] = [ data[ i ], [ a ] ]; } diff --git a/modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.test.js b/modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.test.js index 73844b4..73dee72 100644 --- a/modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.test.js +++ b/modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.test.js @@ -166,22 +166,19 @@ pasteString: 'RFC 1234', pasteType: 'text/plain', parsoidResponse: '<body data-parsoid=\'{"dsr":[0,8,0,0]}\' lang="en" class="mw-content-ltr sitedir-ltr ltr mw-body mw-body-content mediawiki" dir="ltr"><p data-parsoid=\'{"dsr":[0,8,0,0]}\'><a href="//tools.ietf.org/html/rfc1234" rel="mw:ExtLink" data-parsoid=\'{"stx":"magiclink","dsr":[0,8,0,0]}\'>RFC 1234</a></p></body>', - annotations: [ { - type: 'link/mwExternal', - attributes: { - href: '//tools.ietf.org/html/rfc1234', - rel: 'mw:ExtLink' - } - } ], + annotations: [], expectedData: [ - [ 'R', [ 0 ] ], - [ 'F', [ 0 ] ], - [ 'C', [ 0 ] ], - [ ' ', [ 0 ] ], - [ '1', [ 0 ] ], - [ '2', [ 0 ] ], - [ '3', [ 0 ] ], - [ '4', [ 0 ] ], + { + type: 'link/mwMagic', + attributes: { + content: 'RFC 1234', + origText: 'RFC 1234', + origHtml: 'RFC 1234' + } + }, + { + type: '/link/mwMagic' + }, { type: 'internalList' }, { type: '/internalList' } ] @@ -191,23 +188,19 @@ pasteString: 'PMID 1234', pasteType: 'text/plain', parsoidResponse: '<body data-parsoid=\'{"dsr":[0,9,0,0]}\' lang="en" class="mw-content-ltr sitedir-ltr ltr mw-body mw-body-content mediawiki" dir="ltr"><p data-parsoid=\'{"dsr":[0,9,0,0]}\'><a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink" data-parsoid=\'{"stx":"magiclink","dsr":[0,9,0,0]}\'>PMID 1234</a></p></body>', - annotations: [ { - type: 'link/mwExternal', - attributes: { - href: '//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract', - rel: 'mw:ExtLink' - } - } ], + annotations: [], expectedData: [ - [ 'P', [ 0 ] ], - [ 'M', [ 0 ] ], - [ 'I', [ 0 ] ], - [ 'D', [ 0 ] ], - [ ' ', [ 0 ] ], - [ '1', [ 0 ] ], - [ '2', [ 0 ] ], - [ '3', [ 0 ] ], - [ '4', [ 0 ] ], + { + type: 'link/mwMagic', + attributes: { + content: 'PMID 1234', + origText: 'PMID 1234', + origHtml: 'PMID 1234' + } + }, + { + type: '/link/mwMagic' + }, { type: 'internalList' }, { type: '/internalList' } ] @@ -217,29 +210,19 @@ pasteString: 'ISBN 123456789X', pasteType: 'text/plain', parsoidResponse: '<body data-parsoid=\'{"dsr":[0,15,0,0]}\' lang="en" class="mw-content-ltr sitedir-ltr ltr mw-body mw-body-content mediawiki" dir="ltr"><p data-parsoid=\'{"dsr":[0,15,0,0]}\'><a href="./Special:BookSources/123456789X" rel="mw:ExtLink" data-parsoid=\'{"stx":"magiclink","dsr":[0,15,0,0]}\'>ISBN 123456789X</a></p></body>', - annotations: [ { - type: 'link/mwExternal', - attributes: { - href: './Special:BookSources/123456789X', - rel: 'mw:ExtLink' - } - } ], + annotations: [], expectedData: [ - [ 'I', [ 0 ] ], - [ 'S', [ 0 ] ], - [ 'B', [ 0 ] ], - [ 'N', [ 0 ] ], - [ ' ', [ 0 ] ], - [ '1', [ 0 ] ], - [ '2', [ 0 ] ], - [ '3', [ 0 ] ], - [ '4', [ 0 ] ], - [ '5', [ 0 ] ], - [ '6', [ 0 ] ], - [ '7', [ 0 ] ], - [ '8', [ 0 ] ], - [ '9', [ 0 ] ], - [ 'X', [ 0 ] ], + { + type: 'link/mwMagic', + attributes: { + content: 'ISBN 123456789X', + origText: 'ISBN 123456789X', + origHtml: 'ISBN 123456789X' + } + }, + { + type: '/link/mwMagic' + }, { type: 'internalList' }, { type: '/internalList' } ] diff --git a/modules/ve-mw/ui/actions/ve.ui.MWLinkAction.js b/modules/ve-mw/ui/actions/ve.ui.MWLinkAction.js index a9f44de..9595a01 100644 --- a/modules/ve-mw/ui/actions/ve.ui.MWLinkAction.js +++ b/modules/ve-mw/ui/actions/ve.ui.MWLinkAction.js @@ -62,33 +62,27 @@ * @return {ve.dm.MWExternalLinkAnnotation} The annotation to use. */ ve.ui.MWLinkAction.prototype.getLinkAnnotation = function ( linktext ) { - var title, targetData, m, + var title, targetData, href = linktext; - // The link has been validated in #autolinkMagicLink and/or - // #autolinkUrl, so we can use a quick and dirty regexp here to pull - // apart the magic link. - m = /^(RFC|PMID|ISBN)\s+(\S.*)$/.exec( linktext ); - if ( m && m[ 1 ] === 'RFC' ) { - href = '//tools.ietf.org/html/rfc' + m[ 2 ]; - } else if ( m && m[ 1 ] === 'PMID' ) { - href = '//www.ncbi.nlm.nih.gov/pubmed/' + m[ 2 ] + '?dopt=Abstract'; - } else if ( m && m[ 1 ] === 'ISBN' ) { - title = mw.Title.newFromText( 'Special:BookSources/' + m[ 2 ].replace( /[^0-9Xx]/g, '' ) ); - } else { - targetData = ve.dm.MWInternalLinkAnnotation.static.getTargetDataFromHref( - href, - this.surface.getModel().getDocument().getHtmlDocument() - ); - if ( targetData.isInternal ) { - title = mw.Title.newFromText( targetData.title ); - } + + // Is this a "magic link"? + if ( ve.dm.MWMagicLinkNode.static.validateContent( linktext ) ) { + return ve.dm.MWMagicLinkNode.static.annotationFromContent( linktext ); } - return title ? - ve.dm.MWInternalLinkAnnotation.static.newFromTitle( title ) : - new ve.dm.MWExternalLinkAnnotation( { - type: 'link/mwExternal', - attributes: { href: href } - } ); + // Is this an internal link? + targetData = ve.dm.MWInternalLinkAnnotation.static.getTargetDataFromHref( + href, + this.surface.getModel().getDocument().getHtmlDocument() + ); + if ( targetData.isInternal ) { + title = mw.Title.newFromText( targetData.title ); + return ve.dm.MWInternalLinkAnnotation.static.newFromTitle( title ); + } + // It's an external link. + return new ve.dm.MWExternalLinkAnnotation( { + type: 'link/mwExternal', + attributes: { href: href } + } ); }; /** @@ -102,13 +96,7 @@ */ ve.ui.MWLinkAction.prototype.autolinkMagicLink = function () { return this.autolink( function ( linktext ) { - if ( /^(RFC|PMID) [0-9]+$/.test( linktext ) ) { - return true; // Valid RFC/PMID - } - if ( /^ISBN (97[89][- ]?)?([0-9][- ]?){9}[0-9Xx]$/.test( linktext ) ) { - return true; // Valid ISBN - } - return false; + return ve.dm.MWMagicLinkNode.static.validateContent( linktext ); } ); }; @@ -120,10 +108,13 @@ */ ve.ui.MWLinkAction.prototype.open = function () { var fragment = this.surface.getModel().getFragment(), + selectedNode = fragment.getSelectedNode(), windowName = 'link'; - if ( fragment.getSelectedNode() instanceof ve.dm.MWNumberedExternalLinkNode ) { + if ( selectedNode instanceof ve.dm.MWNumberedExternalLinkNode ) { windowName = 'linkNode'; + } else if ( selectedNode instanceof ve.dm.MWMagicLinkNode ) { + windowName = 'linkMagicNode'; } this.surface.execute( 'window', 'open', windowName ); return true; diff --git a/modules/ve-mw/ui/contextitems/ve.ui.MWMagicLinkNodeContextItem.js b/modules/ve-mw/ui/contextitems/ve.ui.MWMagicLinkNodeContextItem.js new file mode 100644 index 0000000..0ecacde --- /dev/null +++ b/modules/ve-mw/ui/contextitems/ve.ui.MWMagicLinkNodeContextItem.js @@ -0,0 +1,55 @@ +/*! + * VisualEditor MWMagicLinkNodeContextItem class. + * + * @copyright 2011-2015 VisualEditor Team and others; see http://ve.mit-license.org + */ + +/** + * Context item for a MWMagicLinkNode. + * + * @class + * @extends ve.ui.LinkContextItem + * + * @constructor + * @param {ve.ui.Context} context Context item is in + * @param {ve.dm.MWMagicLinkNode} model Model item is related to + * @param {Object} config Configuration options + */ +ve.ui.MWMagicLinkNodeContextItem = function VeUiMWMagicLinkNodeContextItem() { + // Parent constructor + ve.ui.MWMagicLinkNodeContextItem.super.apply( this, arguments ); + + // Initialization + this.$element.addClass( 've-ui-mwMagicLinkNodeContextItem' ); +}; + +/* Inheritance */ + +OO.inheritClass( ve.ui.MWMagicLinkNodeContextItem, ve.ui.LinkContextItem ); + +/* Static Properties */ + +ve.ui.MWMagicLinkNodeContextItem.static.name = 'link/mwMagic'; + +ve.ui.MWMagicLinkNodeContextItem.static.label = null; // see #setup() + +ve.ui.MWMagicLinkNodeContextItem.static.modelClasses = [ ve.dm.MWMagicLinkNode ]; + +/* Methods */ + +ve.ui.MWMagicLinkNodeContextItem.prototype.setup = function () { + // Set up label + var msg = 'visualeditor-magiclinknodeinspector-title-' + + this.model.getMagicType().toLowerCase(); + this.setLabel( OO.ui.deferMsg( msg ) ); + // Invoke superclass method. + return ve.ui.MWMagicLinkNodeContextItem.super.prototype.setup.call( this ); +}; + +ve.ui.MWMagicLinkNodeContextItem.prototype.getDescription = function () { + return this.model.getAttribute( 'content' ); +}; + +/* Registration */ + +ve.ui.contextItemFactory.register( ve.ui.MWMagicLinkNodeContextItem ); diff --git a/modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js b/modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js index 2b227ee..79b5c6d 100644 --- a/modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js +++ b/modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js @@ -59,7 +59,7 @@ // Detect autolink opportunities for magic words. // (The link should be the only contents of paste to match this heuristic) - if ( /^\s*(RFC|ISBN|PMID)[-\s0-9]+[Xx]?\s*$/.test( text ) ) { + if ( ve.dm.MWMagicLinkNode.static.validateContent( text.trim() ) ) { return true; } diff --git a/modules/ve-mw/ui/inspectors/ve.ui.MWLinkAnnotationInspector.js b/modules/ve-mw/ui/inspectors/ve.ui.MWLinkAnnotationInspector.js index 5bb2f99..372effa 100644 --- a/modules/ve-mw/ui/inspectors/ve.ui.MWLinkAnnotationInspector.js +++ b/modules/ve-mw/ui/inspectors/ve.ui.MWLinkAnnotationInspector.js @@ -32,6 +32,14 @@ ve.dm.MWInternalLinkAnnotation ]; +ve.ui.MWLinkAnnotationInspector.static.actions = ve.ui.MWLinkAnnotationInspector.static.actions.concat( [ + { + action: 'convert', + label: null, // see #updateActions + modes: [ 'edit', 'insert' ] + } +] ); + /* Methods */ /** @@ -132,6 +140,38 @@ /** * @inheritdoc */ +ve.ui.MWLinkAnnotationInspector.prototype.updateActions = function () { + var content, annotation, href, type, + msg = null; + + ve.ui.MWLinkAnnotationInspector.super.prototype.updateActions.call( this ); + + // show/hide convert action + content = this.fragment ? this.fragment.getText() : ''; + annotation = this.annotationInput.getAnnotation(); + href = annotation && annotation.getHref(); + if ( href && ve.dm.MWMagicLinkNode.static.validateHref( content, href ) ) { + type = ve.dm.MWMagicLinkType.static.fromContent( content ).type; + msg = 'visualeditor-linkinspector-convert-link-' + type.toLowerCase(); + } + + // Once we toggle the visibility of the ActionWidget, we can't filter + // it with `get` any more. So we have to use `forEach`: + this.actions.forEach( null, function ( action ) { + if ( action.getAction() === 'convert' ) { + if ( msg ) { + action.setLabel( OO.ui.deferMsg( msg ) ); + action.toggle( true ); + } else { + action.toggle( false ); + } + } + } ); +}; + +/** + * @inheritdoc + */ ve.ui.MWLinkAnnotationInspector.prototype.createAnnotationInput = function () { return this.isExternal() ? this.externalAnnotationInput : this.internalAnnotationInput; }; @@ -162,9 +202,44 @@ /** * @inheritdoc */ +ve.ui.MWLinkAnnotationInspector.prototype.getActionProcess = function ( action ) { + if ( action === 'convert' ) { + return new OO.ui.Process( function () { + this.close( { action: 'done', convert: true } ); + }, this ); + } + return ve.ui.MWLinkAnnotationInspector.super.prototype.getActionProcess.call( this, action ); +}; + +/** + * @inheritdoc + */ ve.ui.MWLinkAnnotationInspector.prototype.getTeardownProcess = function ( data ) { + var fragment; return ve.ui.MWLinkAnnotationInspector.super.prototype.getTeardownProcess.call( this, data ) + .first( function () { + // Save the original fragment for later. + fragment = this.getFragment(); + }, this ) .next( function () { + var selection = fragment && fragment.getSelection(); + + // Handle conversion to magic link. + if ( data.convert && selection instanceof ve.dm.LinearSelection ) { + fragment.insertContent( [ + { + type: 'link/mwMagic', + attributes: { + content: fragment.getText() + } + }, + { + type: '/link/mwMagic' + } + ], true ); + } + + // Clear dialog state. this.allowProtocolInInternal = false; // Make sure both inputs are cleared this.internalAnnotationInput.setAnnotation( null ); diff --git a/modules/ve-mw/ui/inspectors/ve.ui.MWMagicLinkNodeInspector.js b/modules/ve-mw/ui/inspectors/ve.ui.MWMagicLinkNodeInspector.js new file mode 100644 index 0000000..053ee05 --- /dev/null +++ b/modules/ve-mw/ui/inspectors/ve.ui.MWMagicLinkNodeInspector.js @@ -0,0 +1,182 @@ +/*! + * VisualEditor UserInterface MWMagicLinkNodeInspector class. + * + * @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * Inspector for editing MediaWiki magic links (RFC/ISBN/PMID). + * + * @class + * @extends ve.ui.NodeInspector + * + * @constructor + * @param {Object} [config] Configuration options + */ +ve.ui.MWMagicLinkNodeInspector = function VeUiMWMagicLinkNodeInspector( config ) { + // Parent constructor + ve.ui.NodeInspector.call( this, config ); +}; + +/* Inheritance */ + +OO.inheritClass( ve.ui.MWMagicLinkNodeInspector, ve.ui.NodeInspector ); + +/* Static properties */ + +ve.ui.MWMagicLinkNodeInspector.static.name = 'linkMagicNode'; + +ve.ui.MWMagicLinkNodeInspector.static.icon = 'link'; + +ve.ui.MWMagicLinkNodeInspector.static.title = null; // see #getSetupProcess + +ve.ui.MWMagicLinkNodeInspector.static.modelClasses = [ ve.dm.MWMagicLinkNode ]; + +ve.ui.MWMagicLinkNodeInspector.static.actions = ve.ui.MWMagicLinkNodeInspector.super.static.actions.concat( [ + { + action: 'convert', + label: OO.ui.deferMsg( 'visualeditor-magiclinknodeinspector-convert-link' ), + modes: [ 'edit' ] + } +] ); + +/* Methods */ + +/** + * @inheritdoc + */ +ve.ui.MWMagicLinkNodeInspector.prototype.initialize = function () { + // Parent method + ve.ui.MWMagicLinkNodeInspector.super.prototype.initialize.call( this ); + + // Properties + this.targetInput = new OO.ui.TextInputWidget( { + validate: this.validate.bind( this ) + } ); + this.targetInput.on( 'change', this.onChange.bind( this ) ); + + // Initialization + this.form.$element.append( this.targetInput.$element ); +}; + +/** + * Return true if the given string is a valid magic link of the + * appropriate type. + * + * @private + */ +ve.ui.MWMagicLinkNodeInspector.prototype.validate = function ( str ) { + var node = this.getFragment().getSelectedNode(); + return node.constructor.static.validateContent( str, node.getMagicType() ); +}; + +ve.ui.MWMagicLinkNodeInspector.prototype.onChange = function ( value ) { + // Disable the unsafe action buttons if the input isn't valid + var isValid = this.validate( value ); + this.actions.forEach( null, function ( action ) { + if ( !action.hasFlag( 'safe' ) ) { + action.setDisabled( !isValid ); + } + } ); +}; + +/** + * @inheritdoc + */ +ve.ui.MWMagicLinkNodeInspector.prototype.getActionProcess = function ( action ) { + if ( ( action === 'done' || action === 'convert' ) && + !this.validate( this.targetInput.getValue() ) ) { + // Don't close dialog: input isn't valid. + return new OO.ui.Process( 0 ); + } + if ( action === 'convert' ) { + return new OO.ui.Process( function () { + this.close( { action: action } ); + }, this ); + } + return ve.ui.MWMagicLinkNodeInspector.super.prototype.getActionProcess.call( this, action ); +}; + +/** + * @inheritdoc + */ +ve.ui.MWMagicLinkNodeInspector.prototype.getSetupProcess = function ( data ) { + // Set the title based on the node type + var fragment = data.fragment, + node = fragment instanceof ve.dm.SurfaceFragment ? + fragment.getSelectedNode() : null, + type = node instanceof ve.dm.MWMagicLinkNode ? + node.getMagicType() : null, + msg = type ? + 'visualeditor-magiclinknodeinspector-title-' + type.toLowerCase() : + null; + + data = $.extend( { + title: msg ? OO.ui.deferMsg( msg ) : null + }, data ); + return ve.ui.MWMagicLinkNodeInspector.super.prototype.getSetupProcess.call( this, data ) + .next( function () { + // Initialization + this.targetInput.setValue( + this.selectedNode ? this.selectedNode.getAttribute( 'content' ) : '' + ); + }, this ); +}; + +/** + * @inheritdoc + */ +ve.ui.MWMagicLinkNodeInspector.prototype.getReadyProcess = function ( data ) { + return ve.ui.MWMagicLinkNodeInspector.super.prototype.getReadyProcess.call( this, data ) + .next( function () { + this.targetInput.focus().select(); + }, this ); +}; + +/** + * @inheritdoc + */ +ve.ui.MWMagicLinkNodeInspector.prototype.getTeardownProcess = function ( data ) { + data = data || {}; + return ve.ui.MWMagicLinkNodeInspector.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(), + done = data.action === 'done', + convert = data.action === 'convert', + remove = data.action === 'remove' || ( done && !value ); + + if ( remove ) { + surfaceModel.change( + ve.dm.Transaction.newFromRemoval( doc, nodeRange ) + ); + } else if ( convert ) { + annotation = ve.dm.MWMagicLinkNode.static.annotationFromContent( + value + ); + if ( annotation ) { + 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 if ( done && this.validate( value ) ) { + surfaceModel.change( + ve.dm.Transaction.newFromAttributeChanges( + doc, nodeRange.start, { content: value } + ) + ); + } + }, this ); +}; + +/* Registration */ + +ve.ui.windowFactory.register( ve.ui.MWMagicLinkNodeInspector ); diff --git a/modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js b/modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js index e41f1d3..c988d92 100644 --- a/modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js +++ b/modules/ve-mw/ui/tools/ve.ui.MWLinkInspectorTool.js @@ -24,15 +24,27 @@ OO.inheritClass( ve.ui.MWLinkInspectorTool, ve.ui.LinkInspectorTool ); -// FIXME should eventually vary title based on link type -// Use message visualeditor-annotationbutton-linknode-tooltip - ve.ui.MWLinkInspectorTool.static.modelClasses = ve.ui.MWLinkInspectorTool.super.static.modelClasses.concat( [ - ve.dm.MWNumberedExternalLinkNode + ve.dm.MWNumberedExternalLinkNode, + ve.dm.MWMagicLinkNode ] ); -ve.ui.MWLinkInspectorTool.static.associatedWindows = [ 'link', 'linkNode' ]; +ve.ui.MWLinkInspectorTool.static.associatedWindows = [ 'link', 'linkNode', 'linkMagicNode' ]; + +ve.ui.MWLinkInspectorTool.prototype.onUpdateState = function ( fragment ) { + // Vary title based on link type. + var node = fragment instanceof ve.dm.SurfaceFragment ? + fragment.getSelectedNode() : null, + type = node instanceof ve.dm.MWMagicLinkNode ? + 'magiclinknode-tooltip-' + node.getMagicType().toLowerCase() : + node instanceof ve.dm.MWNumberedExternalLinkNode ? + 'linknode-tooltip' : null, + title = type ? + OO.ui.deferMsg( 'visualeditor-annotationbutton-' + type ) : + ve.ui.MWLinkInspectorTool.static.title; + this.setTitle( title ); +}; 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: merged Gerrit-Change-Id: Id5b7a2ae3c80b0e5eed598f0bd024d3e94f7e9aa Gerrit-PatchSet: 11 Gerrit-Project: mediawiki/extensions/VisualEditor Gerrit-Branch: master Gerrit-Owner: Cscott <canan...@wikimedia.org> Gerrit-Reviewer: Catrope <roan.katt...@gmail.com> Gerrit-Reviewer: Cscott <canan...@wikimedia.org> Gerrit-Reviewer: Esanders <esand...@wikimedia.org> Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org> Gerrit-Reviewer: Krinkle <krinklem...@gmail.com> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits