mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-15 18:39:52 +00:00
54c5230b01
Opera 12 seems to work well enough, but I'm not confident enough to whitelist it just yet. Opera 15 is basically Chrome with a different interface, so it should work perfectly, but it's barely out of beta and untested right now. Bug: 36000 Change-Id: Ia80a6f53f8c128ef52d0bfde1828fdc132046afb
421 lines
14 KiB
JavaScript
421 lines
14 KiB
JavaScript
/*!
|
|
* 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, support, getTargetDeferred,
|
|
plugins = [];
|
|
|
|
/**
|
|
* 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 );
|
|
|
|
// Transfer methods
|
|
ve.init.mw.ViewPageTarget.prototype.setupSectionEditLinks = init.setupSectionEditLinks;
|
|
|
|
// Add plugins
|
|
target.addPlugins( plugins );
|
|
|
|
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 )
|
|
);
|
|
|
|
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' )
|
|
};
|
|
|
|
init = {
|
|
|
|
support: support,
|
|
|
|
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]],
|
|
// Firefox issues in versions 12 and below (bug 50780)
|
|
// Wikilink [[./]] bug in Firefox 14 and below (bug 50720)
|
|
'firefox': [['<=', 14]],
|
|
// Opera < 12 was not tested and it's userbase is almost nonexistent anyway
|
|
'opera': [['<', 12]],
|
|
// Blacklist all versions:
|
|
'blackberry': null
|
|
},
|
|
|
|
/**
|
|
* Add a plugin module or function.
|
|
*
|
|
* Plugins are run after VisualEditor is loaded, but before it is initialized. This allows
|
|
* plugins to add classes and register them with the factories and registries.
|
|
*
|
|
* The parameter to this function can be a ResourceLoader module name or a function.
|
|
*
|
|
* If it's a module name, it will be loaded together with the VisualEditor core modules when
|
|
* VE is loaded. No special care is taken to ensure that the module runs after the VE
|
|
* classes are loaded, so if this is desired, the module should depend on
|
|
* ext.visualEditor.core .
|
|
*
|
|
* If it's a function, it will be invoked once the VisualEditor core modules and any
|
|
* plugin modules registered through this function have been loaded, but before the editor
|
|
* is intialized. The function takes one parameter, which is the ve.init.mw.Target instance
|
|
* that's initializing, and can optionally return a jQuery.Promise . VisualEditor will
|
|
* only be initialized once all promises returned by plugin functions have been resolved.
|
|
*
|
|
* @example
|
|
* // Register ResourceLoader module
|
|
* ve.libs.mw.addPlugin( 'ext.gadget.foobar' );
|
|
*
|
|
* // Register a callback
|
|
* ve.libs.mw.addPlugin( function ( target ) {
|
|
* ve.dm.Foobar = .....
|
|
* } );
|
|
*
|
|
* // Register a callback that loads another script
|
|
* ve.libs.mw.addPlugin( function () {
|
|
* return $.getScript( 'http://example.com/foobar.js' );
|
|
* } );
|
|
*
|
|
* @param {string|Function} plugin Module name or callback that optionally returns a promise
|
|
*/
|
|
addPlugin: function( plugin ) {
|
|
plugins.push( plugin );
|
|
},
|
|
|
|
skinSetup: function () {
|
|
init.setupTabLayout();
|
|
init.setupSectionEditLinks();
|
|
},
|
|
|
|
setupTabLayout: 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-create
|
|
// visualeditor-ca-ve-edit
|
|
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-createsource
|
|
// visualeditor-ca-editsource
|
|
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( init.onEditTabClick );
|
|
}
|
|
},
|
|
|
|
setupSectionEditLinks: function () {
|
|
var $editsections = $( '#mw-content-text .mw-editsection' );
|
|
|
|
// match direction to the user interface
|
|
$editsections.css( 'direction', $( 'body' ).css( 'direction' ) );
|
|
// The "visibility" css construct ensures we always occupy the same space in the layout.
|
|
// This prevents the heading from changing its wrap when the user toggles editSourceLink.
|
|
$editsections.each( function () {
|
|
var $closingBracket, $expandedOnly, $hiddenBracket, $outerClosingBracket,
|
|
expandTimeout, shrinkTimeout,
|
|
$editsection = $( this ),
|
|
$heading = $editsection.closest( 'h1, h2, h3, h4, h5, h6' ),
|
|
$editLink = $editsection.find( 'a' ).eq( 0 ),
|
|
$editSourceLink = $editLink.clone(),
|
|
$links = $editLink.add( $editSourceLink ),
|
|
$divider = $( '<span>' ),
|
|
dividerText = $.trim( mw.msg( 'pipe-separator' ) ),
|
|
$brackets = $( [ this.firstChild, this.lastChild ] );
|
|
|
|
function expandSoon() {
|
|
// Cancel pending shrink, schedule expansion instead
|
|
clearTimeout( shrinkTimeout );
|
|
expandTimeout = setTimeout( expand, 100 );
|
|
}
|
|
|
|
function expand() {
|
|
clearTimeout( shrinkTimeout );
|
|
$closingBracket.css( 'visibility', 'hidden' );
|
|
$expandedOnly.css( 'visibility', 'visible' );
|
|
$heading.addClass( 'mw-editsection-expanded' );
|
|
}
|
|
|
|
function shrinkSoon() {
|
|
// Cancel pending expansion, schedule shrink instead
|
|
clearTimeout( expandTimeout );
|
|
shrinkTimeout = setTimeout( shrink, 100 );
|
|
}
|
|
|
|
function shrink() {
|
|
clearTimeout( expandTimeout );
|
|
if ( !$links.is( ':focus' ) ) {
|
|
$closingBracket.css( 'visibility', 'visible' );
|
|
$expandedOnly.css( 'visibility', 'hidden' );
|
|
$heading.removeClass( 'mw-editsection-expanded' );
|
|
}
|
|
}
|
|
|
|
// TODO: Remove this (see Id27555c6 in mediawiki/core)
|
|
if ( !$brackets.hasClass( 'mw-editsection-bracket' ) ) {
|
|
$brackets = $brackets
|
|
.wrap( $( '<span>' ).addClass( 'mw-editsection-bracket' ) )
|
|
.parent();
|
|
}
|
|
|
|
$closingBracket = $brackets.last();
|
|
$outerClosingBracket = $closingBracket.clone();
|
|
$expandedOnly = $divider.add( $editSourceLink ).add( $outerClosingBracket )
|
|
.css( 'visibility', 'hidden' );
|
|
// The hidden bracket after the devider ensures we have balanced space before and after
|
|
// divider. The space before the devider is provided by the original closing bracket.
|
|
$hiddenBracket = $closingBracket.clone().css( 'visibility', 'hidden' );
|
|
|
|
// Events
|
|
$heading.on( { 'mouseenter': expandSoon, 'mouseleave': shrinkSoon } );
|
|
$links.on( { 'focus': expand, 'blur': shrinkSoon } );
|
|
if ( isViewPage ) {
|
|
// Only init without refresh if we're on a view page. Though section edit links
|
|
// are rarely shown on non-view pages, they appear in one other case, namely
|
|
// when on a diff against the latest version of a page. In that case we mustn't
|
|
// init without refresh as that'd initialise for the wrong rev id (bug 50925)
|
|
// and would preserve the wrong DOM with a diff on top.
|
|
$editLink.click( init.onEditSectionLinkClick );
|
|
}
|
|
|
|
// Initialization
|
|
$editSourceLink
|
|
.addClass( 'mw-editsection-link-secondary' )
|
|
.text( mw.msg( 'visualeditor-ca-editsource-section' ) );
|
|
$divider
|
|
.addClass( 'mw-editsection-divider' )
|
|
.text( dividerText );
|
|
$editLink
|
|
.attr( 'href', function ( i, val ) {
|
|
return new mw.Uri( veEditUri ).extend( {
|
|
'vesection': new mw.Uri( val ).query.section
|
|
} );
|
|
} )
|
|
.addClass( 'mw-editsection-link-primary' );
|
|
$closingBracket
|
|
.after( $divider, $hiddenBracket, $editSourceLink, $outerClosingBracket );
|
|
} );
|
|
},
|
|
|
|
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;
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
getTarget().done( function ( target ) {
|
|
target.logEvent( 'Edit', { action: 'edit-link-click' } );
|
|
target.activate();
|
|
} );
|
|
},
|
|
|
|
onEditSectionLinkClick: function ( e ) {
|
|
if ( ( e.which && e.which !== 1 ) || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey ) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
getTarget().done( function ( target ) {
|
|
target.logEvent( 'Edit', { action: 'section-edit-link-click' } );
|
|
target.saveEditSection( $( e.target ).closest( 'h1, h2, h3, h4, h5, h6' ).get( 0 ) );
|
|
target.activate();
|
|
} );
|
|
}
|
|
};
|
|
|
|
support.visualEditor = support.es5 &&
|
|
support.contentEditable &&
|
|
( ( 'vewhitelist' in uri.query ) || !$.client.test( init.blacklist, null, true ) );
|
|
|
|
// Whether VisualEditor should be available for the current user, page, wiki, mediawiki skin,
|
|
// browser etc.
|
|
init.isAvailable = (
|
|
support.visualEditor &&
|
|
|
|
// Allow disabling for anonymous users separately from changing the
|
|
// default preference (bug 50000)
|
|
!( conf.disableForAnons && mw.user.isAnon() ) &&
|
|
|
|
// Disable on redirect pages until redirects are editable (bug 47328)
|
|
// Property wgIsRedirect is relatively new in core, many cached pages
|
|
// don't have it yet. We do a best-effort approach using the url query
|
|
// which will cover all working redirect (the only case where one can
|
|
// read a redirect page without ?redirect=no is in case of broken or
|
|
// double redirects).
|
|
!mw.config.get( 'wgIsRedirect', !!uri.query.redirect ) &&
|
|
|
|
// User has 'visualeditor-enable' preference enabled (for alpha opt-in)
|
|
mw.user.options.get( 'visualeditor-enable' ) &&
|
|
|
|
// User has 'visualeditor-betatempdisable' preference disabled
|
|
!mw.user.options.get( 'visualeditor-betatempdisable' ) &&
|
|
|
|
// Only in supported skins
|
|
$.inArray( mw.config.get( 'skin' ), conf.skins ) !== -1 &&
|
|
|
|
// Only in enabled namespaces
|
|
$.inArray(
|
|
new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getNamespaceId(),
|
|
conf.namespaces
|
|
) !== -1 &&
|
|
|
|
// Only for pages with a wikitext content model
|
|
mw.config.get( 'wgPageContentModel' ) === 'wikitext'
|
|
);
|
|
|
|
// Note: Though VisualEditor itself only needs this exposure for a very small reason
|
|
// (namely to access init.blacklist from the unit tests...) this has become one of the nicest
|
|
// ways to easily detect whether VisualEditor is present on this page. The VE global was once
|
|
// available always, but now that platform integration initialisation is propertly separated,
|
|
// it doesn't exist until the platform loads VisualEditor core. Though mw.libs.ve shouldn't be
|
|
// considered an API (the methods are subject to change and considered private), the presence
|
|
// of this property should be reliable.
|
|
mw.libs.ve = init;
|
|
|
|
if ( !init.isAvailable ) {
|
|
return;
|
|
}
|
|
|
|
$( function () {
|
|
if ( isViewPage ) {
|
|
if ( uri.query.veaction === 'edit' ) {
|
|
getTarget().done( function ( target ) {
|
|
target.activate();
|
|
} );
|
|
}
|
|
}
|
|
init.skinSetup();
|
|
} );
|
|
|
|
}() );
|