/*! * VisualEditor MediaWiki Initialization ViewPageTarget class. * * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ /*global mw, confirm, alert */ /** * Initialization MediaWiki view page target. * * @class * @extends ve.init.mw.Target * * @constructor */ 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' ); // Parent constructor ve.init.mw.Target.call( this, $( '#content' ), mw.config.get( 'wgRelevantPageName' ), currentUri.query.oldid ); // Properties this.$document = null; this.$spinner = $( '
' ); this.$toolbarTracker = $( '' ); this.toolbarCancelButton = null; this.toolbarSaveButton = null; this.saveDialogSlideHistory = []; this.saveDialogSaveButton = null; this.saveDialogReviewGoodButton = null; this.$toolbarEditNotices = $( '' ).append(
$( '' ).text( mw.msg( 'captcha-label' ) ),
document.createTextNode( mw.msg( 'colon-separator' ) ),
$( $.parseHTML( mw.message( 'fancycaptcha-edit' ).parse() ) )
.filter( 'a' ).attr( 'target', '_blank ' ).end()
),
$( '' ).attr( 'src', data.edit.captcha.url ),
this.captcha.input.$
),
{
wrap: false
}
);
return;
}
// TODO: Don't use alert.
alert( ve.msg( 'visualeditor-saveerror', status ) );
};
/**
* Handle Show changes event.
*
* @method
* @param {string} diffHtml
*/
ve.init.mw.ViewPageTarget.prototype.onShowChanges = function ( diffHtml ) {
// Invalidate the viewer diff on next change
this.surface.getModel().connect( this, { 'transact': 'onSurfaceModelTransact' } );
mw.loader.using( 'mediawiki.action.history.diff', ve.bind( function () {
this.$saveDialog
.find( '.ve-init-mw-viewPageTarget-saveDialog-viewer' )
.empty().append( diffHtml );
this.$saveDialogLoadingIcon.hide();
this.saveDialogReviewGoodButton.setDisabled( false );
}, this ), ve.bind( function () {
this.onSaveError( null, 'Module load failed' );
}, this ) );
};
/**
* Handle Serialize event.
*
* @method
* @param {string} wikitext
*/
ve.init.mw.ViewPageTarget.prototype.onSerialize = function ( wikitext ) {
// Invalidate the viewer wikitext on next change
this.surface.getModel().connect( this, { 'transact': 'onSurfaceModelTransact' } );
this.$saveDialog
.find( '.ve-init-mw-viewPageTarget-saveDialog-viewer' )
.empty().append( $( ' ').append( message ) );
} else {
$warning.append( message );
}
this.$saveDialog.find( '.ve-init-mw-viewPageTarget-saveDialog-warnings' )
.append( $warning );
this.warnings[name] = $warning;
}
};
/**
* Remove an inline warning.
* @param {string} name Warning's unique name
*/
ve.init.mw.ViewPageTarget.prototype.clearWarning = function ( name ) {
if ( this.warnings[name] ) {
this.warnings[name].remove();
delete this.warnings[name];
}
};
/**
* Remove all inline warnings.
*/
ve.init.mw.ViewPageTarget.prototype.clearAllWarnings = function () {
this.$saveDialog
.find( '.ve-init-mw-viewPageTarget-saveDialog-warnings' )
.empty();
this.warnings = {};
};
/**
* Add onbeforunload handler.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.setupBeforeUnloadHandler = function () {
// Remember any already set on before unload handler
this.onBeforeUnloadFallback = window.onbeforeunload;
// Attach before unload handler
window.onbeforeunload = this.onBeforeUnloadHandler = ve.bind( this.onBeforeUnload, this );
// Attach page show handlers
if ( window.addEventListener ) {
window.addEventListener( 'pageshow', ve.bind( this.onPageShow, this ), false );
} else if ( window.attachEvent ) {
window.attachEvent( 'pageshow', ve.bind( this.onPageShow, this ) );
}
};
/**
* Remove onbeforunload handler.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.tearDownBeforeUnloadHandler = function () {
// Restore whatever previous onbeforeload hook existed
window.onbeforeunload = this.onBeforeUnloadFallback;
};
/**
* Handle page show event.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onPageShow = function () {
// Re-add onbeforeunload handler
window.onbeforeunload = this.onBeforeUnloadHandler;
};
/**
* Handle before unload event.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onBeforeUnload = function () {
var fallbackResult,
message,
onBeforeUnloadHandler = this.onBeforeUnloadHandler;
// Check if someone already set on onbeforeunload hook
if ( this.onBeforeUnloadFallback ) {
// Get the result of their onbeforeunload hook
fallbackResult = this.onBeforeUnloadFallback();
}
// Check if their onbeforeunload hook returned something
if ( fallbackResult !== undefined ) {
// Exit here, returning their message
message = fallbackResult;
} else {
// Override if submitting
if ( this.submitting ) {
return null;
}
// Check if there's been an edit
if ( this.surface && this.edited ) {
// Return our message
message = ve.msg( 'visualeditor-viewpage-savewarning' );
}
}
// Unset the onbeforeunload handler so we don't break page caching in Firefox
window.onbeforeunload = null;
if ( message !== undefined ) {
// ...but if the user chooses not to leave the page, we need to rebind it
setTimeout( function () {
window.onbeforeunload = onBeforeUnloadHandler;
} );
return message;
}
};
/* Initialization */
ve.init.mw.targets.push( new ve.init.mw.ViewPageTarget() );
' ).text( wikitext ) );
this.$saveDialogLoadingIcon.hide();
this.saveDialogReviewGoodButton.setDisabled( false );
};
/**
* Handle failed show changes event.
*
* @method
* @param {Object} jqXHR
* @param {string} status Text status message
*/
ve.init.mw.ViewPageTarget.prototype.onShowChangesError = function ( jqXHR, status ) {
alert( ve.msg( 'visualeditor-differror', status ) );
this.$saveDialogLoadingIcon.hide();
};
/**
* Called if a call to target.serialize() failed.
*
* @method
* @param {jqXHR|null} jqXHR
* @param {string} status Text status message
*/
ve.init.mw.ViewPageTarget.prototype.onSerializeError = function ( jqXHR, status ) {
alert( ve.msg( 'visualeditor-serializeerror', status ) );
this.$saveDialogLoadingIcon.hide();
};
/**
* Handle edit conflict event.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onEditConflict = function () {
this.$saveDialogLoadingIcon.hide();
this.swapSaveDialog( 'conflict' );
};
/**
* Handle failed show changes event.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onNoChanges = function () {
this.$saveDialogLoadingIcon.hide();
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.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onEditSectionLinkClick = function ( e ) {
if ( ( e.which && e.which !== 1 ) || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey ) {
return;
}
this.logEvent( 'Edit', { action: 'section-edit-link-click' } );
this.saveEditSection( $( e.target ).closest( 'h1, h2, h3, h4, h5, h6' ).get( 0 ) );
this.activate();
// Prevent the edit tab's normal behavior
e.preventDefault();
};
/**
* Handle clicks on the view tab.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onViewTabClick = function ( e ) {
if ( ( e.which && e.which !== 1 ) || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey ) {
return;
}
if ( this.active ) {
this.deactivate();
// Prevent the edit tab's normal behavior
e.preventDefault();
} else if ( this.activating ) {
this.deactivate( true );
this.activating = false;
e.preventDefault();
}
};
/**
* Handle clicks on the save button in the toolbar.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onToolbarSaveButtonClick = function () {
this.logEvent( 'Edit', { action: 'page-save-attempt' } );
if ( this.edited || this.restoring ) {
this.showSaveDialog();
}
};
/**
* Handle clicks on the save button in the toolbar.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onToolbarCancelButtonClick = function () {
this.deactivate();
};
/**
* Handle clicks on the MwMeta button in the toolbar.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onToolbarMwMetaButtonClick = function () {
this.surface.getDialogs().open( 'mwMeta' );
};
/**
* Handle clicks on the edit notices tool in the toolbar.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onToolbarEditNoticesToolClick = function () {
this.$toolbarEditNotices.fadeToggle( 'fast' );
this.$toolbarBetaNotice.fadeOut( 'fast' );
this.$document[0].focus();
};
/**
* Handle clicks on the beta notices tool in the toolbar.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onToolbarBetaNoticeToolClick = function () {
this.$toolbarBetaNotice.fadeToggle( 'fast' );
this.$toolbarEditNotices.fadeOut( 'fast' );
this.$document[0].focus();
};
/**
* Handle clicks on the feedback tool in the toolbar.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onToolbarFeedbackToolClick = function () {
this.$toolbarEditNotices.fadeOut( 'fast' );
if ( !this.feedback ) {
// This can't be constructed until the editor has loaded as it uses special messages
this.feedback = new mw.Feedback( {
'title': new mw.Title( ve.msg( 'visualeditor-feedback-link' ) ),
'bugsLink': new mw.Uri( 'https://bugzilla.wikimedia.org/enter_bug.cgi?product=VisualEditor&component=General' ),
'bugsListLink': new mw.Uri( 'https://bugzilla.wikimedia.org/buglist.cgi?query_format=advanced&resolution=---&resolution=LATER&resolution=DUPLICATE&product=VisualEditor&list_id=166234' )
} );
}
this.feedback.launch();
};
/**
* Handle the first transaction in the surface model.
*
* This handler is removed the first time it's used, but added each time the surface is set up.
*
* @method
* @param {ve.dm.Transaction} tx Processed transaction
*/
ve.init.mw.ViewPageTarget.prototype.onSurfaceModelTransact = function () {
// Clear the diff
this.$saveDialog
.find( '.ve-init-mw-viewPageTarget-saveDialog-slide-review .ve-init-mw-viewPageTarget-saveDialog-viewer' )
.empty();
this.surface.getModel().disconnect( this, { 'transact': 'onSurfaceModelTransact' } );
};
/**
* Re-evaluate whether the toolbar save button should be disabled or not.
*/
ve.init.mw.ViewPageTarget.prototype.updateToolbarSaveButtonState = function () {
this.edited = this.surface.getModel().hasPastState();
// Disable the save button if we have no history or if the sanity check is not finished
this.toolbarSaveButton.setDisabled( ( !this.edited && !this.restoring ) || !this.sanityCheckFinished );
this.toolbarSaveButton.$.toggleClass( 've-init-mw-viewPageTarget-waiting', !this.sanityCheckFinished );
};
/**
* Handle clicks on the review button in the save dialog.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogReviewButtonClick = function () {
this.swapSaveDialog( 'review' );
};
/**
* Handle clicks on the save button in the save dialog.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogSaveButtonClick = function () {
var doc = this.surface.getModel().getDocument(),
saveOptions = this.getSaveOptions();
// Once we've retrieved the save options,
// reset save start and any old captcha data
this.saveStart = +new Date();
if ( this.captcha ) {
this.clearWarning( 'captcha' );
delete this.captcha;
}
if (
+mw.user.options.get( 'forceeditsummary' ) &&
saveOptions.summary === '' &&
!this.warnings.missingsummary
) {
this.showWarning( 'missingsummary', ve.init.platform.getParsedMessage( 'missingsummary' ) );
} else {
this.saveDialogSaveButton.setDisabled( true );
this.$saveDialogLoadingIcon.show();
this.save(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
saveOptions
);
}
};
/**
* Handle clicks on the review "Good" button in the save dialog.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogReviewGoodButtonClick = function () {
this.swapSaveDialog( 'save' );
};
/**
* Handle clicks on the resolve conflict button in the conflict dialog.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogResolveConflictButtonClick = function () {
var doc = this.surface.getModel().getDocument();
// Get Wikitext from the DOM, and set up a submit call when it's done
this.serialize(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
ve.bind( function ( wikitext ) {
this.submit( wikitext, this.getSaveOptions() );
}, this )
);
};
/**
* Get save options from the save dialog form.
*
* @method
* @returns {Object} Save options, including summary, minor and watch properties
*/
ve.init.mw.ViewPageTarget.prototype.getSaveOptions = function () {
return {
'summary': $( '#ve-init-mw-viewPageTarget-saveDialog-editSummary' ).val(),
'minor': $( '#ve-init-mw-viewPageTarget-saveDialog-minorEdit' ).prop( 'checked' ),
'watch': $( '#ve-init-mw-viewPageTarget-saveDialog-watchList' ).prop( 'checked' ),
'needcheck': this.sanityCheckPromise.state() === 'rejected',
'captchaid': this.captcha && this.captcha.id,
'captchaword': this.captcha && this.captcha.input.getValue()
};
};
/**
* Handle clicks on the close button in the save dialog.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogCloseButtonClick = function () {
this.hideSaveDialog();
};
/**
* Handle clicks on the previous view button in the save dialog.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogPrevButtonClick = function () {
var history = this.saveDialogSlideHistory;
if ( history.length < 2 ) {
throw new Error( 'PrevButton was triggered without a history' );
}
// Pop off current slide
history.pop();
// Navigate to last slide
this.swapSaveDialog( history[ history.length -1 ], { fromHistory: true } );
};
/**
* Set up the list of edit notices.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.setupToolbarEditNotices = function () {
var key;
this.$toolbarEditNotices.empty();
for ( key in this.editNotices ) {
this.$toolbarEditNotices.append( this.editNotices[key] );
}
};
/**
* Set up the beta notices panel.
*
* @method
* @returns {string[]} HTML strings for each edit notice
*/
ve.init.mw.ViewPageTarget.prototype.setupToolbarBetaNotice = function () {
this.$toolbarBetaNotice.empty();
this.$toolbarBetaNotice
.append( $( '' ).text( ve.msg( 'visualeditor-beta-warning' ) ) );
};
/**
* Switch to editing mode.
*
* @method
* @param {HTMLDocument} doc HTML DOM to edit
*/
ve.init.mw.ViewPageTarget.prototype.setUpSurface = function ( doc ) {
// Initialize surface
this.surface = new ve.ui.Surface( doc, this.surfaceOptions );
this.surface.connect( this, { 'toolbarPosition': 'onSurfaceToolbarPosition' } );
this.surface.getContext().hide();
this.$document = this.surface.$.find( '.ve-ce-documentNode' );
this.surface.getModel().connect( this, { 'transact': 'onSurfaceModelTransact' } );
this.surface.getModel().connect( this, { 'history': 'updateToolbarSaveButtonState' } );
this.$.append( this.surface.$ );
this.setUpToolbar();
this.transformPageTitle();
this.changeDocumentTitle();
// Update UI
this.hidePageContent();
this.hideSpinner();
this.active = true;
this.$document.attr( {
'lang': mw.config.get( 'wgVisualEditor' ).pageLanguageCode,
'dir': mw.config.get( 'wgVisualEditor' ).pageLanguageDir
} );
// Add appropriately mw-content-ltr or mw-content-rtl class
this.surface.$.addClass( 'mw-content-' + mw.config.get( 'wgVisualEditor' ).pageLanguageDir );
this.surface.initialize();
};
/**
* Fire off the sanity check. Must be called before the surface is activated.
*
* To access the result, check whether #sanityCheckPromise has been resolved or rejected
* (it's asynchronous, so it may still be pending when you check).
*/
ve.init.mw.ViewPageTarget.prototype.startSanityCheck = function () {
// We have to get the converted DOM now, before we unlock the surface and let the user edit,
// but we can defer the actual comparison
var viewPage = this,
doc = viewPage.surface.getModel().getDocument(),
newDom = ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
oldDom = viewPage.doc,
d = $.Deferred();
// Reset
viewPage.sanityCheckFinished = false;
viewPage.sanityCheckVerified = false;
setTimeout( function () {
// We can't compare oldDom.body and newDom.body directly, because the attributes on the
// were ignored in the conversion. So compare each child separately.
var i,
len = oldDom.body.childNodes.length;
if ( len !== newDom.body.childNodes.length ) {
// Different number of children, so they're definitely different
d.reject();
return;
}
for ( i = 0; i < len; i++ ) {
if ( !oldDom.body.childNodes[i].isEqualNode( newDom.body.childNodes[i] ) ) {
d.reject();
return;
}
}
d.resolve();
} );
viewPage.sanityCheckPromise = d.promise()
.done( function () {
// If we detect no roundtrip errors,
// don't emphasize "review changes" to the user.
viewPage.sanityCheckVerified = true;
})
.always( function () {
viewPage.sanityCheckFinished = true;
viewPage.updateToolbarSaveButtonState();
} );
};
/**
* The toolbar has updated its position.
* @param {jQuery} $bar
*/
ve.init.mw.ViewPageTarget.prototype.onSurfaceToolbarPosition = function ( $bar ) {
var css, offset, startProp, startOffset,
dir = mw.config.get( 'wgVisualEditor' ).pageLanguageDir,
type = $bar.css( 'position' );
// HACK: If the toolbar is floating, also apply a floating class to the toolbar tracker
if ( $bar.parent().hasClass( 've-ui-toolbar-floating' ) ) {
this.$toolbarTracker.addClass( 've-init-mw-viewPageTarget-toolbarTracker-floating' );
} else {
this.$toolbarTracker.removeClass( 've-init-mw-viewPageTarget-toolbarTracker-floating' );
}
// It's important that the toolbar tracker has 0 height. Else it will block events on the
// toolbar (e.g. clicking "Save page") as it would overlap that space. The save dialog
// will remain visible for the same reason elsewhere: As long as we don't have overflow:hidden,
// the save dialog will stick out of the tracker in the right place without the tracker itself
// blocking the toolbar.
if ( type === 'relative' ) {
offset = $bar.offset();
css = {
'position': 'absolute',
'top': offset.top
};
if ( dir === 'ltr' ) {
startProp = 'left';
startOffset = offset.left;
} else {
startProp = 'right';
startOffset = $( window ).width() - ( offset.left + $bar.outerWidth() );
}
css[ startProp ] = startOffset;
} else if ( type === 'absolute' || type === 'fixed' ) {
css = {
'position': type,
'top': $bar.css( 'top' ),
'left': $bar.css( 'left' )
};
} else {
return;
}
this.$toolbarTracker.css( css );
};
/**
* Set up the logging of analytic edit events using EventLogging.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.setUpEventLogging = function () {
mw.loader.using( 'schema.Edit', function () {
mw.eventLog.setDefaults( 'Edit', {
version: 0,
editor: 'visualeditor',
pageId: mw.config.get( 'wgArticleId' ),
pageNs: mw.config.get( 'wgNamespaceNumber' ),
pageName: mw.config.get( 'wgPageName' ),
pageViewSessionId: mw.user.generateRandomSessionId(),
revId: function () {
return mw.config.get( 'wgCurRevisionId' );
},
userId: +mw.config.get( 'wgUserId' )
} );
} );
};
/**
* Thin wrapper around EventLogging's 'logEvent' method which ensures the
* relevant schema module has been loaded.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.logEvent = function ( schemaName, event ) {
mw.loader.using( 'schema.' + schemaName, function () {
mw.eventLog.logEvent( schemaName, event );
} );
};
/**
* Switch to viewing mode.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.tearDownSurface = function () {
// Update UI
if ( this.$document ) {
this.$document.blur();
this.$document = null;
}
this.tearDownToolbar();
this.hideSpinner();
this.showPageContent();
this.restorePageTitle();
this.restoreDocumentTitle();
this.showTableOfContents();
// Destroy surface
if ( this.surface ) {
this.surface.destroy();
this.surface = null;
}
this.active = false;
};
/**
* Modify tabs in the skin to support in-place editing.
*
* @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 ) );
}
};
/**
* Modify page content to make section edit links activate the editor.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.setupSectionEditLinks = function () {
var veEditUri = this.veEditUri,
$editsections = $( '#mw-content-text .mw-editsection' ),
handler = ve.bind( this.onEditSectionLinkClick, this );
// 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 = $( '' ),
dividerText = $.trim( ve.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( $( '' ).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 } );
$editLink.click( handler );
// 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 );
} );
};
/**
* Add content and event bindings to toolbar buttons.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.setupToolbarButtons = function () {
var editNoticeCount = ve.getObjectKeys( this.editNotices ).length;
this.toolbarCancelButton = new ve.ui.ButtonWidget( { 'label': ve.msg( 'visualeditor-toolbar-cancel' ) } );
this.toolbarSaveButton = new ve.ui.ButtonWidget( {
'label': ve.msg( 'visualeditor-toolbar-savedialog' ),
'flags': ['constructive'],
'disabled': !this.restoring
} );
this.updateToolbarSaveButtonState();
this.toolbarCancelButton.connect( this, { 'click': 'onToolbarCancelButtonClick' } );
this.toolbarSaveButton.connect( this, { 'click': 'onToolbarSaveButtonClick' } );
this.$toolbarMwMetaButton
.addClass( 've-ui-icon-settings' )
.append(
$( '' )
.addClass( 've-init-mw-viewPageTarget-tool-label' )
.text( ve.msg( 'visualeditor-meta-tool' ) )
)
.click( ve.bind( this.onToolbarMwMetaButtonClick, this ) );
if ( editNoticeCount ) {
this.$toolbarEditNoticesTool
.addClass( 've-ui-icon-alert' )
.append(
$( '' )
.addClass( 've-init-mw-viewPageTarget-tool-label' )
.text( ve.msg( 'visualeditor-editnotices-tool', editNoticeCount ) )
)
.append( this.$toolbarEditNotices )
.click( ve.bind( this.onToolbarEditNoticesToolClick, this ) );
this.$toolbarEditNotices.fadeIn( 'fast' );
}
this.$toolbarBetaNoticeTool
.append(
$( '' )
.addClass( 've-init-mw-viewPageTarget-tool-label ve-init-mw-viewPageTarget-tool-beta-label' )
.text( ve.msg( 'visualeditor-beta-label' ) )
)
.append( this.$toolbarBetaNotice )
.click( ve.bind( this.onToolbarBetaNoticeToolClick, this ) );
this.$toolbarFeedbackTool
.addClass( 've-ui-icon-comment' )
.append(
$( '' )
.addClass( 've-init-mw-viewPageTarget-tool-label' )
.text( ve.msg( 'visualeditor-feedback-tool' ) )
)
.click( ve.bind( this.onToolbarFeedbackToolClick, this ) );
};
/**
* Remove content and event bindings from toolbar buttons.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.tearDownToolbarButtons = function () {
this.toolbarCancelButton.disconnect( this );
this.toolbarSaveButton.disconnect( this );
this.$toolbarMwMetaButton.empty().off( 'click' );
this.$toolbarEditNoticesTool.empty().off( 'click' );
this.$toolbarBetaNoticeTool.empty().off( 'click' );
this.$toolbarFeedbackTool.empty().off( 'click' );
};
/**
* Add the save button to the user interface.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.attachToolbarButtons = function () {
var $target = this.toolbar.$actions;
$target.append( this.$toolbarBetaNoticeTool );
this.$toolbarBetaNotice.append( this.$toolbarFeedbackTool );
if ( !ve.isEmptyObject( this.editNotices ) ) {
$target.append( this.$toolbarEditNoticesTool );
}
$target.append(
this.$toolbarMwMetaButton,
this.toolbarCancelButton.$,
this.toolbarSaveButton.$
);
};
/**
* Remove the save button from the user interface.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.detachToolbarButtons = function () {
this.toolbarCancelButton.$.detach();
this.toolbarSaveButton.$.detach();
this.$toolbarMwMetaButton.detach();
this.$toolbarEditNoticesTool.detach();
this.$toolbarFeedbackTool.detach();
this.$toolbarBetaNoticeTool.detach();
};
/**
* Get a template for the save dialog.
*
* The result of this function depends on an API call, so the result it provided asynchronously.
* The template will be wrapped in a plain `