From b21fe5fbc1c5b35c8d301f8fb190b61f0899c5d5 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Thu, 4 Jul 2013 00:14:52 +0200 Subject: [PATCH] Split off setup from the rest of mw.ViewPageTarget Initialisation initialisation? It's time to rename ve.init to ve.platform and ve.init.Platform to ve.platform.Environment, but that'll come later. * Moved support detection and skin set up to separate class-less file. * Swapped usage of ve.msg for mw.msg. * Callback of edit tab now does an mw.loader call to fetch the actual VisualEditor libraries. Though mw.loader won't load the same thing twice, we would bind a callback each time. To avoid instantiating ViewPageTarget more than once we use a Deferred. Bug: 50542 Bug: 50608 Bug: 50612 Change-Id: Ic8b0004ab5288fa91bb29d496485b93ffd8d977e --- VisualEditor.hooks.php | 3 +- VisualEditor.php | 34 ++- .../targets/ve.init.mw.ViewPageTarget.init.js | 216 ++++++++++++++++++ .../init/targets/ve.init.mw.ViewPageTarget.js | 147 +----------- .../targets/ve.init.mw.ViewPageTarget.test.js | 13 +- 5 files changed, 253 insertions(+), 160 deletions(-) create mode 100644 modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.init.js diff --git a/VisualEditor.hooks.php b/VisualEditor.hooks.php index 59149f29bb..aefa361c95 100644 --- a/VisualEditor.hooks.php +++ b/VisualEditor.hooks.php @@ -85,7 +85,7 @@ class VisualEditorHooks { if ( $wgVisualEditorEnableEventLogging ) { $output->addModules( array( 'schema.Edit' ) ); } - $output->addModules( array( 'ext.visualEditor.viewPageTarget' ) ); + $output->addModules( array( 'ext.visualEditor.viewPageTarget.init' ) ); } else { if ( $wgVisualEditorEnableEventLogging ) { $output->addModules( array( 'schema.Edit', 'ext.visualEditor.splitTest' ) ); @@ -226,6 +226,7 @@ class VisualEditorHooks { 'ext.visualEditor.standalone', 'ext.visualEditor.core', 'ext.visualEditor.experimental', + 'ext.visualEditor.viewPageTarget.init', 'ext.visualEditor.viewPageTarget', ), 'localBasePath' => dirname( __FILE__ ) . '/modules', diff --git a/VisualEditor.php b/VisualEditor.php index 738a7f4261..87244ca057 100644 --- a/VisualEditor.php +++ b/VisualEditor.php @@ -152,6 +152,27 @@ $wgResourceModules += array( 've-mw/init/styles/ve.init.mw.ViewPageTarget.Icons-vector.css', ), ), + 'ext.visualEditor.viewPageTarget.init' => $wgVisualEditorResourceTemplate + array( + 'scripts' => array( + 've-mw/init/targets/ve.init.mw.ViewPageTarget.init.js', + ), + 'dependencies' => array( + 'jquery.client', + 'mediawiki.Uri', + 'mediawiki.util', + ), + 'messages' => array( + 'accesskey-ca-ve-edit', + 'tooltip-ca-createsource', + 'tooltip-ca-editsource', + 'tooltip-ca-ve-edit', + 'visualeditor-ca-createsource', + 'visualeditor-ca-editsource', + 'visualeditor-ca-ve-create', + 'visualeditor-ca-ve-edit', + ), + 'position' => 'top', + ), 'ext.visualEditor.viewPageTarget' => $wgVisualEditorResourceTemplate + array( 'scripts' => array( 've-mw/init/targets/ve.init.mw.ViewPageTarget.js', @@ -181,12 +202,12 @@ $wgResourceModules += array( 'jquery.client', 'jquery.placeholder', 'jquery.visibleText', + 'mediawiki.feedback', 'mediawiki.jqueryMsg', + 'mediawiki.notify', 'mediawiki.Title', 'mediawiki.Uri', 'mediawiki.user', - 'mediawiki.notify', - 'mediawiki.feedback', 'user.options', 'user.tokens', ), @@ -196,17 +217,8 @@ $wgResourceModules += array( 'editing', // Messages needed by VE in init phase only (rest go below) - 'accesskey-ca-editsource', - 'accesskey-ca-ve-edit', - 'tooltip-ca-createsource', - 'tooltip-ca-editsource', - 'tooltip-ca-ve-edit', 'pipe-separator', - 'visualeditor-ca-createsource', - 'visualeditor-ca-editsource', 'visualeditor-ca-editsource-section', - 'visualeditor-ca-ve-create', - 'visualeditor-ca-ve-edit', 'visualeditor-loadwarning', 'visualeditor-loadwarning-token', 'visualeditor-notification-created', diff --git a/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.init.js b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.init.js new file mode 100644 index 0000000000..c2c2819e52 --- /dev/null +++ b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.init.js @@ -0,0 +1,216 @@ +/*! + * VisualEditor MediaWiki ViewPageTarget init. + * + * This file must remain as widely compatible as the base compatibility + * for MediaWiki itself (see mediawiki/core:/resources/startup.js). + * Avoid use of: ES5, SVG, HTML5 DOM, ContentEditable etc. + * + * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/*global mw */ + +/** + * Platform preparation for the MediaWiki view page. This loads (when user needs it) the + * actual MediaWiki integration and VisualEditor library. + * + * @class ve.init.mw.ViewPageTarget.init + * @singleton + */ +( function () { + var conf, uri, pageExists, viewUri, veEditUri, isViewPage, + init, isBlacklisted, getTargetDeferred; + + /** + * Use deferreds to avoid loading and instantiating Target multiple times. + * @return {jQuery.Promise} + */ + function getTarget() { + var loadTargetDeferred; + if ( !getTargetDeferred ) { + getTargetDeferred = $.Deferred(); + loadTargetDeferred = $.Deferred() + .done( function () { + var target = new ve.init.mw.ViewPageTarget(); + ve.init.mw.targets.push( target ); + getTargetDeferred.resolve( target ); + } ) + .fail( getTargetDeferred.reject ); + + mw.loader.using( 'ext.visualEditor.viewPageTarget', loadTargetDeferred.resolve, loadTargetDeferred.reject ); + } + return getTargetDeferred.promise(); + } + + conf = mw.config.get( 'wgVisualEditorConfig' ); + uri = new mw.Uri(); + pageExists = !!mw.config.get( 'wgArticleId' ); + viewUri = new mw.Uri( mw.util.wikiGetlink( mw.config.get( 'wgRelevantPageName' ) ) ); + veEditUri = viewUri.clone().extend( { 'veaction': 'edit' } ); + isViewPage = ( + mw.config.get( 'wgAction' ) === 'view' && + !( 'diff' in uri.query ) + ); + + + init = { + + support: { + es5: ( + // It would be much easier to do a quick inline function that asserts "use strict" + // works, but since IE9 doesn't support strict mode (and we don't use strict mode) we + // have to instead list all the ES5 features we do use. + Array.isArray && + Array.prototype.filter && + Array.prototype.indexOf && + Array.prototype.map && + Date.prototype.toJSON && + Function.prototype.bind && + Object.create && + Object.keys && + String.prototype.trim && + window.JSON && + JSON.parse && + JSON.stringify + ), + contentEditable: 'contentEditable' in document.createElement( 'div' ) + }, + + blacklist: { + // IE <= 8 has various incompatibilities in layout and feature support + // IE9 and IE10 generally work but fail in ajax handling when making POST + // requests to the VisualEditor/Parsoid API which is causing silent failures + // when trying to save a page (bug 49187) + 'msie': [['<=', 10]], + // Android 2.x and below "support" CE but don't trigger keyboard input + 'android': [['<', 3]], + // Bug 50534 - apparently Firefox is broken in versions 10 and below + 'firefox': [['<=', 10]], + // Blacklist all versions: + 'opera': null, + 'blackberry': null + }, + + skinSetup: function () { + var caVeEdit, caVeEditSource, + action = pageExists ? 'edit' : 'create', + pTabsId = $( '#p-views' ).length ? 'p-views' : 'p-cactions', + $caSource = $( '#ca-viewsource' ), + $caEdit = $( '#ca-edit' ), + $caEditLink = $caEdit.find( 'a' ), + reverseTabOrder = $( 'body' ).hasClass( 'rtl' ) && pTabsId === 'p-views', + caVeEditNextnode = reverseTabOrder ? $caEdit.get( 0 ) : $caEdit.next().get( 0 ); + + if ( !$caEdit.length || $caSource.length ) { + // If there is no edit tab or a view-source tab, + // the user doesn't have permission to edit. + return; + } + + // Add independent "VisualEditor" tab (#ca-ve-edit). + if ( conf.tabLayout === 'add' ) { + + caVeEdit = mw.util.addPortletLink( + pTabsId, + // Use url instead of '#'. + // So that 1) one can always open it in a new tab, even when + // onEditTabClick is bound. + // 2) when onEditTabClick is not bound (!isViewPage) it will + // just work. + veEditUri, + // visualeditor-ca-ve-edit + // visualeditor-ca-ve-create + mw.msg( 'visualeditor-ca-ve-' + action ), + 'ca-ve-edit', + mw.msg( 'tooltip-ca-ve-edit' ), + mw.msg( 'accesskey-ca-ve-edit' ), + caVeEditNextnode + ); + + // Replace "Edit" tab with a veEditUri version, add "Edit source" tab. + } else { + // Create "Edit source" link. + // Re-create instead of convert ca-edit since we don't want to copy over accesskey etc. + caVeEditSource = mw.util.addPortletLink( + pTabsId, + // Use original href to preserve oldid etc. (bug 38125) + $caEditLink.attr( 'href' ), + // visualeditor-ca-editsource + // visualeditor-ca-createsource + mw.msg( 'visualeditor-ca-' + action + 'source' ), + 'ca-editsource', + // tooltip-ca-editsource + // tooltip-ca-createsource + mw.msg( 'tooltip-ca-' + action + 'source' ), + mw.msg( 'accesskey-ca-editsource' ), + caVeEditNextnode + ); + // Copy over classes (e.g. 'selected') + $( caVeEditSource ).addClass( $caEdit.attr( 'class' ) ); + + // Create "Edit" tab. + $caEdit.remove(); + caVeEdit = mw.util.addPortletLink( + pTabsId, + // Use url instead of '#'. + // So that 1) one can always open it in a new tab, even when + // onEditTabClick is bound. + // 2) when onEditTabClick is not bound (!isViewPage) it will + // just work. + veEditUri, + $caEditLink.text(), + $caEdit.attr( 'id' ), + $caEditLink.attr( 'title' ), + mw.msg( 'accesskey-ca-ve-edit' ), + reverseTabOrder ? caVeEditSource.nextSibling : caVeEditSource + ); + } + + if ( isViewPage ) { + // Allow instant switching to edit mode, without refresh + $( caVeEdit ).click( function ( e ) { + // Default mouse button is normalised by jQuery to key code 1. + // Only do our handling if no keys are pressed, mouse button is 1 + // (e.g. not middle click or right click) and no modifier keys + // (e.g. cmd-click to open in new tab). + if ( ( e.which && e.which !== 1 ) || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey ) { + return; + } + // Prevent the edit tab's normal behavior + e.preventDefault(); + + getTarget().done( function ( target ) { + target.logEvent( 'Edit', { action: 'edit-link-click' } ); + target.activate(); + } ); + } ); + } + } + }; + + // Expose + mw.libs.ve = init; + if ( window.console ) { + window.console.log( 'Support ES5: ' + String(!!init.support.es5) + '; UA: ' + navigator.userAgent ); + } + + isBlacklisted = !( 'vewhitelist' in uri.query ) && $.client.test( init.blacklist, null, true ); + + if ( !init.support.es5 || !init.support.contentEditable || isBlacklisted ) { + mw.log( 'Browser does not support VisualEditor' ); + return; + } + + $( function () { + if ( isViewPage ) { + if ( uri.query.veaction === 'edit' ) { + getTarget().done( function ( target ) { + target.activate(); + } ); + } + } + init.skinSetup(); + } ); + +}() ); diff --git a/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js index a198ed8c82..7548800e5c 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js +++ b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js @@ -17,26 +17,7 @@ */ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() { var browserWhitelisted, - browserBlacklisted, - currentUri = new mw.Uri(), - supportsES5subset = ( - // It would be much easier to do a quick inline function that asserts "use strict" - // works, but since IE9 doesn't support strict mode (and we don't use strict mode) we - // have to instead list all the ES5 features we use. - Array.isArray && - Array.prototype.filter && - Array.prototype.indexOf && - Array.prototype.map && - Date.prototype.toJSON && - Function.prototype.bind && - Object.create && - Object.keys && - String.prototype.trim && - window.JSON && - JSON.parse && - JSON.stringify - ), - supportsContentEditable = 'contentEditable' in document.createElement( 'div' ); + currentUri = new mw.Uri(); // Parent constructor ve.init.mw.Target.call( @@ -110,10 +91,6 @@ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() { 'vewhitelist' in currentUri.query || $.client.test( ve.init.mw.ViewPageTarget.compatibility.whitelist, null, true ) ); - browserBlacklisted = ( - !( 'vewhitelist' in currentUri.query ) && - $.client.test( ve.init.mw.ViewPageTarget.compatibility.blacklist, null, true ) - ); if ( mw.config.get( 'wgVisualEditorConfig' ).enableEventLogging ) { this.setUpEventLogging(); @@ -135,11 +112,6 @@ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() { 'serializeError': 'onSerializeError' } ); - if ( !supportsES5subset || !supportsContentEditable || browserBlacklisted ) { - // Don't initialise in browsers that are broken - return; - } - if ( !browserWhitelisted ) { // Show warning in unknown browsers that pass the support test // Continue at own risk. @@ -164,11 +136,6 @@ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() { this.setupSkinTabs(); this.setupSectionEditLinks(); - if ( this.isViewPage ) { - if ( currentUri.query.veaction === 'edit' ) { - this.activate(); - } - } window.addEventListener( 'popstate', ve.bind( this.onWindowPopState, this ) ) ; }; @@ -194,20 +161,6 @@ ve.init.mw.ViewPageTarget.compatibility = { 'iceweasel': [['>=', 10]], 'safari': [['>=', 5]], 'chrome': [['>=', 19]] - }, - 'blacklist': { - // IE <= 8 has various incompatibilities in layout and feature support - // IE9 and IE10 generally work but fail in ajax handling when making POST - // requests to the VisualEditor/Parsoid API which is causing silent failures - // when trying to save a page (bug 49187) - 'msie': [['<=', 10]], - // Android 2.x and below "support" CE but don't trigger keyboard input - 'android': [['<', 3]], - // Bug 50534 - apparently Firefox is broken in versions 10 and below - 'firefox': [['<=', 10]], - // Blacklist all versions: - 'opera': null, - 'blackberry': null } }; @@ -619,26 +572,6 @@ ve.init.mw.ViewPageTarget.prototype.onNoChanges = function () { this.swapSaveDialog( 'nochanges' ); }; -/** - * Handle clicks on the edit tab. - * - * @method - * @param {jQuery.Event} e Mouse click event - */ -ve.init.mw.ViewPageTarget.prototype.onEditTabClick = function ( e ) { - // Default mouse button is normalised by jQuery to key code 1. - // Only do our handling if no keys are pressed, mouse button is 1 - // (e.g. not middle click or right click) and no modifier keys - // (e.g. cmd-click to open in new tab). - if ( ( e.which && e.which !== 1 ) || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey ) { - return; - } - this.logEvent( 'Edit', { action: 'edit-link-click' } ); - this.activate(); - // Prevent the edit tab's normal behavior - e.preventDefault(); -}; - /** * Handle clicks on a section edit link. * @@ -1150,84 +1083,12 @@ ve.init.mw.ViewPageTarget.prototype.tearDownSurface = function () { /** * Modify tabs in the skin to support in-place editing. + * Edit tab is bound outside the module in mw.ViewPageTarget.init. * * @method */ ve.init.mw.ViewPageTarget.prototype.setupSkinTabs = function () { - var caVeEdit, caVeEditSource, - action = this.pageExists ? 'edit' : 'create', - pTabsId = $( '#p-views' ).length ? 'p-views' : 'p-cactions', - $caSource = $( '#ca-viewsource' ), - $caEdit = $( '#ca-edit' ), - $caEditLink = $caEdit.find( 'a' ), - reverseTabOrder = $( 'body' ).hasClass( 'rtl' ) && pTabsId === 'p-views', - caVeEditNextnode = reverseTabOrder ? $caEdit.get( 0 ) : $caEdit.next().get( 0 ); - - if ( !$caEdit.length || $caSource.length ) { - // If there is no edit tab or a view-source tab, - // the user doesn't have permission to edit. - return; - } - - // Add independent "VisualEditor" tab (#ca-ve-edit). - if ( this.tabLayout === 'add' ) { - - caVeEdit = mw.util.addPortletLink( - pTabsId, - // Use url instead of '#'. - // So that 1) one can always open it in a new tab, even when - // onEditTabClick is bound. - // 2) when onEditTabClick is not bound (!isViewPage) it will - // just work. - this.veEditUri, - // Message: 'visualeditor-ca-ve-edit' or 'visualeditor-ca-ve-create' - ve.msg( 'visualeditor-ca-ve-' + action ), - 'ca-ve-edit', - ve.msg( 'tooltip-ca-ve-edit' ), - ve.msg( 'accesskey-ca-ve-edit' ), - caVeEditNextnode - ); - - // Replace "Edit" tab with a veEditUri version, add "Edit source" tab. - } else { - // Create "Edit source" link. - // Re-create instead of convert ca-edit since we don't want to copy over accesskey etc. - caVeEditSource = mw.util.addPortletLink( - pTabsId, - // Use original href to preserve oldid etc. (bug 38125) - $caEditLink.attr( 'href' ), - // Message: 'visualeditor-ca-editsource' or 'visualeditor-ca-createsource' - ve.msg( 'visualeditor-ca-' + action + 'source' ), - 'ca-editsource', - // Message: 'tooltip-ca-editsource' or 'tooltip-ca-createsource' - ve.msg( 'tooltip-ca-' + action + 'source' ), - ve.msg( 'accesskey-ca-editsource' ), - caVeEditNextnode - ); - // Copy over classes (e.g. 'selected') - $( caVeEditSource ).addClass( $caEdit.attr( 'class' ) ); - - // Create "Edit" tab. - $caEdit.remove(); - caVeEdit = mw.util.addPortletLink( - pTabsId, - // Use url instead of '#'. - // So that 1) one can always open it in a new tab, even when - // onEditTabClick is bound. - // 2) when onEditTabClick is not bound (!isViewPage) it will - // just work. - this.veEditUri, - $caEditLink.text(), - $caEdit.attr( 'id' ), - $caEditLink.attr( 'title' ), - ve.msg( 'accesskey-ca-ve-edit' ), - reverseTabOrder ? caVeEditSource.nextSibling : caVeEditSource - ); - } - if ( this.isViewPage ) { - // Allow instant switching to edit mode, without refresh - $( caVeEdit ).click( ve.bind( this.onEditTabClick, this ) ); // Allow instant switching back to view mode, without refresh $( '#ca-view a, #ca-nstab-visualeditor a' ) .click( ve.bind( this.onViewTabClick, this ) ); @@ -2317,7 +2178,3 @@ ve.init.mw.ViewPageTarget.prototype.onBeforeUnload = function () { return message; } }; - -/* Initialization */ - -ve.init.mw.targets.push( new ve.init.mw.ViewPageTarget() ); diff --git a/modules/ve-mw/test/init/targets/ve.init.mw.ViewPageTarget.test.js b/modules/ve-mw/test/init/targets/ve.init.mw.ViewPageTarget.test.js index 19a7fc114e..45378f0961 100644 --- a/modules/ve-mw/test/init/targets/ve.init.mw.ViewPageTarget.test.js +++ b/modules/ve-mw/test/init/targets/ve.init.mw.ViewPageTarget.test.js @@ -5,10 +5,11 @@ * @license The MIT License (MIT); see LICENSE.txt */ +/*global mw */ QUnit.module( 've.init.mw.ViewPageTarget' ); QUnit.test( 'compatibility', function ( assert ) { - var i, profile, list, matches, + var i, profile, list, matches, compatibility, cases = [ { 'msg': 'Unidentified browser', @@ -122,12 +123,18 @@ QUnit.test( 'compatibility', function ( assert ) { } ]; + compatibility = { + 'whitelist': ve.init.mw.ViewPageTarget.compatibility.whitelist, + // TODO: Fix this mess when we split ve.init from ve.platform + 'blacklist': mw.libs.ve.blacklist + }; + QUnit.expect( cases.length ); for ( i = 0; i < cases.length; i++ ) { profile = $.client.profile( { 'userAgent': cases[i].userAgent, 'platform': '' } ); matches = []; - for ( list in ve.init.mw.ViewPageTarget.compatibility ) { - if ( $.client.test( ve.init.mw.ViewPageTarget.compatibility[list], profile, true ) ) { + for ( list in compatibility ) { + if ( $.client.test( compatibility[list], profile, true ) ) { matches.push( list ); } }