/*! * VisualEditor MediaWiki Initialization ViewPageTarget class. * * @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ /*global confirm, alert */ /** * Initialization MediaWiki view page target. * * @class * @extends ve.init.mw.Target * * @constructor */ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() { var prefName, prefValue, // A workaround, as default URI does not get updated after pushState (bug 72334) currentUri = new mw.Uri( location.href ), conf = mw.config.get( 'wgVisualEditorConfig' ); // Parent constructor ve.init.mw.Target.call( this, $( '#content' ), mw.config.get( 'wgRelevantPageName' ), currentUri.query.oldid ); // Properties this.toolbarSaveButton = null; this.saveDialog = null; this.onBeforeUnloadFallback = null; this.onBeforeUnloadHandler = null; this.active = false; this.activating = false; this.deactivating = false; this.edited = false; // If this is true then #transformPage / #restorePage will not call pushState // This is to avoid adding a new history entry for the url we just got from onpopstate // (which would mess up with the expected order of Back/Forwards browsing) this.actFromPopState = false; this.popState = { tag: 'visualeditor' }; this.scrollTop = null; this.currentUri = currentUri; this.section = currentUri.query.vesection; this.initialEditSummary = currentUri.query.summary; this.namespaceName = mw.config.get( 'wgCanonicalNamespace' ); this.viewUri = new mw.Uri( mw.util.getUrl( this.pageName ) ); this.veEditUri = this.viewUri.clone().extend( { veaction: 'edit' } ); this.isViewPage = ( mw.config.get( 'wgAction' ) === 'view' && currentUri.query.diff === undefined ); this.originalDocumentTitle = document.title; this.tabLayout = mw.config.get( 'wgVisualEditorConfig' ).tabLayout; /** * @property {jQuery.Promise|null} */ this.sanityCheckPromise = null; // Add modules specific to desktop (modules shared with mobile go in MWTarget) this.modules.push( 'ext.visualEditor.mwformatting', 'ext.visualEditor.mwgallery', 'ext.visualEditor.mwimage', 'ext.visualEditor.mwmeta' ); // Load preference modules for ( prefName in conf.preferenceModules ) { prefValue = mw.config.get( 'wgUserName' ) === null ? conf.defaultUserOptions[prefName] : mw.user.options.get( prefName, conf.defaultUserOptions[prefName] ); if ( prefValue && prefValue !== '0' ) { this.modules.push( conf.preferenceModules[prefName] ); } } // Events this.connect( this, { save: 'onSave', saveErrorEmpty: 'onSaveErrorEmpty', saveErrorSpamBlacklist: 'onSaveErrorSpamBlacklist', saveErrorAbuseFilter: 'onSaveErrorAbuseFilter', saveErrorNewUser: 'onSaveErrorNewUser', saveErrorCaptcha: 'onSaveErrorCaptcha', saveErrorUnknown: 'onSaveErrorUnknown', loadError: 'onLoadError', surfaceReady: 'onSurfaceReady', editConflict: 'onEditConflict', showChanges: 'onShowChanges', showChangesError: 'onShowChangesError', noChanges: 'onNoChanges', serializeError: 'onSerializeError', sanityCheckComplete: 'updateToolbarSaveButtonState' } ); if ( currentUri.query.venotify ) { // The following messages can be used here: // postedit-confirmation-saved // postedit-confirmation-created // postedit-confirmation-restored mw.hook( 'postEdit' ).fire( { message: ve.msg( 'postedit-confirmation-' + currentUri.query.venotify, mw.user ) } ); delete currentUri.query.venotify; } if ( history.replaceState ) { // This is to stop the back button breaking when it's supposed to take us back out // of VE. It used to only be called when venotify is used. FIXME: there should be // a much better solution than this. history.replaceState( this.popState, document.title, currentUri ); } this.setupSkinTabs(); window.addEventListener( 'popstate', this.onWindowPopState.bind( this ) ) ; }; /* Inheritance */ OO.inheritClass( ve.init.mw.ViewPageTarget, ve.init.mw.Target ); /* Static Properties */ /** * Compatibility map used with jQuery.client to black-list incompatible browsers. * * @static * @property */ ve.init.mw.ViewPageTarget.compatibility = { // The key is the browser name returned by jQuery.client // The value is either null (match all versions) or a list of tuples // containing an inequality (<,>,<=,>=) and a version number whitelist: { firefox: [['>=', 15]], iceweasel: [['>=', 10]], safari: [['>=', 6]], chrome: [['>=', 19]], opera: [['>=', 15]] } }; /* Events */ /** * @event saveWorkflowBegin * Fired when user clicks the button to open the save dialog. */ /** * @event saveWorkflowEnd * Fired when user exits the save workflow */ /** * @event saveReview * Fired when user initiates review changes in save workflow */ /** * @event saveInitiated * Fired when user initiates saving of the document */ /* Methods */ /** * Verify that a PopStateEvent correlates to a state we created. * * @param {Mixed} popState From PopStateEvent#state * @return {boolean} */ ve.init.mw.ViewPageTarget.prototype.verifyPopState = function ( popState ) { return popState && popState.tag === 'visualeditor'; }; /** * @inheritdoc */ ve.init.mw.ViewPageTarget.prototype.setupToolbar = function () { var $firstHeading; // Parent method ve.init.mw.Target.prototype.setupToolbar.call( this ); // Keep it hidden so that we can slide it down smoothly (avoids sudden // offset flash when original content is hidden, and replaced in-place with a // similar-looking surface). // FIXME: This is not ideal, the parent class creates it and appends // to target (visibly), only for us to hide it again 0ms later. // Though we can't hide it by default because it needs visible dimensions // to compute stuff during setup. this.toolbar.$bar.hide(); this.toolbar.enableFloatable(); this.toolbar.$element .addClass( 've-init-mw-viewPageTarget-toolbar' ); // Move the toolbar to before #firstHeading if it exists $firstHeading = $( '#firstHeading' ); if ( $firstHeading.length ) { this.toolbar.$element.insertBefore( $firstHeading ); } this.toolbar.$bar.slideDown( 'fast', function () { // Check the surface wasn't torn down while the toolbar was animating if ( this.surface ) { this.toolbar.initialize(); this.surface.getView().emit( 'position' ); this.surface.getContext().updateDimensions(); } }.bind( this ) ); }; /** * Set up notices for things like unknown browsers. * Needs to be done on each activation because localNoticeMessages is cleared in clearState * * @method */ ve.init.mw.ViewPageTarget.prototype.setupLocalNoticeMessages = function () { if ( mw.config.get( 'wgTranslatePageTranslation' ) === 'source' ) { // Warn users if they're on a source of the Page Translation feature this.localNoticeMessages.push( 'visualeditor-pagetranslationwarning' ); } if ( !( 'vewhitelist' in this.currentUri.query || $.client.test( ve.init.mw.ViewPageTarget.compatibility.whitelist, null, true ) ) ) { // Show warning in unknown browsers that pass the support test // Continue at own risk. this.localNoticeMessages.push( 'visualeditor-browserwarning' ); } }; /** * Switch to edit mode. * * @method * @return {jQuery.Promise} */ ve.init.mw.ViewPageTarget.prototype.activate = function () { if ( !this.active && !this.activating ) { this.activating = true; this.activatingDeferred = $.Deferred(); this.originalEditondbclick = mw.user.options.get( 'editondblclick' ); mw.user.options.set( 'editondblclick', 0 ); // User interface changes this.transformPage(); this.hideReadOnlyContent(); this.mutePageContent(); this.mutePageTitle(); this.setupLocalNoticeMessages(); this.saveScrollPosition(); this.load( [ 'site', 'user' ] ); } return this.activatingDeferred.promise(); }; /** * Determines whether we want to switch to view mode or not (displaying a dialog if necessary) * Then, if we do, actually switches to view mode. * * @param {boolean} [override] Do not display a dialog * @param {string} [trackMechanism] Abort mechanism; used for event tracking if present */ ve.init.mw.ViewPageTarget.prototype.deactivate = function ( override, trackMechanism ) { var target = this; if ( override || ( this.active && !this.deactivating ) ) { if ( override || !this.edited ) { this.cancel( trackMechanism ); } else { this.surface.dialogs.openWindow( 'cancelconfirm' ).then( function ( opened ) { opened.then( function ( closing ) { closing.then( function ( data ) { if ( data.action === 'discard' ) { target.cancel( trackMechanism ); } } ); } ); } ); } } }; /** * Switch to view mode * * @param {string} [trackMechanism] Abort mechanism; used for event tracking if present */ ve.init.mw.ViewPageTarget.prototype.cancel = function ( trackMechanism ) { var abortType, promises = []; // Event tracking if ( trackMechanism ) { if ( this.activating ) { abortType = 'preinit'; } else if ( !this.edited ) { abortType = 'nochange'; } else if ( this.saving ) { abortType = 'abandonMidsave'; } else { // switchwith and switchwithout do not go through this code path, // they go through switchToWikitextEditor() instead abortType = 'abandon'; } ve.track( 'mwedit.abort', { type: abortType, mechanism: trackMechanism } ); } this.deactivating = true; // User interface changes if ( this.elementsThatHadOurAccessKey ) { this.elementsThatHadOurAccessKey.attr( 'accesskey', ve.msg( 'accesskey-save' ) ); } this.restorePage(); this.showReadOnlyContent(); mw.user.options.set( 'editondblclick', this.originalEditondbclick ); this.originalEditondbclick = undefined; if ( this.toolbarSaveButton ) { // If deactivate is called before a successful load, then the save button has not yet been // fully set up so disconnecting it would throw an error when trying call methods on the // button property (bug 46456) this.toolbarSaveButton.disconnect( this ); this.toolbarSaveButton.$element.detach(); this.toolbar.$actions.empty(); } // Check we got as far as setting up the surface if ( this.active ) { this.tearDownBeforeUnloadHandler(); // If we got as far as setting up the surface, tear that down promises.push( this.tearDownSurface() ); } $.when.apply( $, promises ).then( function () { // Show/restore components that are otherwise handled by tearDownSurface this.showPageContent(); this.restorePageTitle(); // If there is a load in progress, abort it if ( this.loading ) { this.loading.abort(); } this.clearState(); this.docToSave = null; this.initialEditSummary = new mw.Uri().query.summary; this.deactivating = false; this.activatingDeferred.reject(); mw.hook( 've.deactivationComplete' ).fire( this.edited ); }.bind( this ) ); }; /** * Handle failed DOM load event. * * @method * @param {jqXHR|null} jqXHR jQuery XHR object * @param {string} status Text status message * @param {Mixed|null} error Thrown exception or HTTP error string */ ve.init.mw.ViewPageTarget.prototype.onLoadError = function ( jqXHR, status ) { // Don't show an error if the load was manually aborted // The response.status check here is to catch aborts triggered by navigation away from the page if ( status !== 'abort' && ( !jqXHR || ( jqXHR.status !== 0 && jqXHR.status !== 504 ) ) && confirm( ve.msg( 'visualeditor-loadwarning', status ) ) ) { this.load(); } else if ( jqXHR && jqXHR.status === 504 && confirm( ve.msg( 'visualeditor-timeout' ) ) ) { if ( 'veaction' in this.currentUri.query ) { delete this.currentUri.query.veaction; } this.currentUri.query.action = 'edit'; location.href = this.currentUri.toString(); } else { // Something weird happened? Deactivate // TODO: how does this handle load errors triggered from // calling this.loading.abort()? this.activating = false; // Not passing trackMechanism because we don't know what happened // and this is not a user action this.deactivate( true ); } }; /** * Once surface is ready ready, init UI * * @method */ ve.init.mw.ViewPageTarget.prototype.onSurfaceReady = function () { if ( !this.activating ) { // Activation was aborted before we got here. Do nothing // TODO are there things we need to clean up? return; } this.activating = false; this.surface.getModel().connect( this, { documentUpdate: function () { mw.loader.using( 'mediawiki.notification' ).then( function () { this.wikitextWarning = ve.init.mw.ViewPageTarget.static.checkForWikitextWarning( this.surface, this.wikitextWarning ); }.bind( this ) ); }, history: 'updateToolbarSaveButtonState' } ); this.surface.setPasteRules( this.constructor.static.pasteRules ); // TODO: mwTocWidget should probably live in a ve.ui.MWSurface subclass if ( mw.config.get( 'wgVisualEditorConfig' ).enableTocWidget ) { this.surface.mwTocWidget = new ve.ui.MWTocWidget( this.surface ); } // Update UI this.transformPageTitle(); this.changeDocumentTitle(); this.hidePageContent(); this.surface.getView().focus(); this.setupToolbarSaveButton(); this.attachToolbarSaveButton(); this.restoreScrollPosition(); this.restoreEditSection(); this.setupBeforeUnloadHandler(); this.maybeShowDialogs(); this.activatingDeferred.resolve(); mw.hook( 've.activationComplete' ).fire(); }; /** * Handle successful DOM save event. * * @method * @param {string} html Rendered page HTML from server * @param {string} categoriesHtml Rendered categories HTML from server * @param {number} newid New revision id, undefined if unchanged * @param {boolean} isRedirect Whether this page is a redirect or not * @param {string} displayTitle What HTML to show as the page title * @param {Object} lastModified Object containing user-formatted date and time strings, or undefined if we made no change. */ ve.init.mw.ViewPageTarget.prototype.onSave = function ( html, categoriesHtml, newid, isRedirect, displayTitle, lastModified, contentSub ) { var newUrlParams, watchChecked; this.saveDeferred.resolve(); if ( !this.pageExists || this.restoring ) { // This is a page creation or restoration, refresh the page this.tearDownBeforeUnloadHandler(); newUrlParams = newid === undefined ? {} : { venotify: this.restoring ? 'restored' : 'created' }; if ( isRedirect ) { newUrlParams.redirect = 'no'; } location.href = this.viewUri.extend( newUrlParams ); } else { // Update watch link to match 'watch checkbox' in save dialog. // User logged in if module loaded. // Just checking for mw.page.watch is not enough because in Firefox // there is Object.prototype.watch... if ( mw.page.watch && mw.page.watch.updateWatchLink ) { watchChecked = this.saveDialog.$saveOptions .find( '.ve-ui-mwSaveDialog-checkboxes' ) .find( '#wpWatchthis' ) .prop( 'checked' ); mw.page.watch.updateWatchLink( $( '#ca-watch a, #ca-unwatch a' ), watchChecked ? 'unwatch' : 'watch' ); } // If we were explicitly editing an older version, make sure we won't // load the same old version again, now that we've saved the next edit // will be against the latest version. // TODO: What about oldid in the url? this.restoring = false; if ( newid !== undefined ) { mw.config.set( { wgCurRevisionId: newid, wgRevisionId: newid } ); this.revid = newid; } this.saveDialog.reset(); this.replacePageContent( html, categoriesHtml, displayTitle, lastModified, contentSub ); this.setupSectionEditLinks(); // Tear down the target now that we're done saving // Not passing trackMechanism because this isn't an abort action this.deactivate( true ); if ( newid !== undefined ) { mw.hook( 'postEdit' ).fire( { message: ve.msg( 'postedit-confirmation-saved', mw.user ) } ); } } }; /** * Update save dialog message on general error * * @method */ ve.init.mw.ViewPageTarget.prototype.onSaveErrorEmpty = function () { this.showSaveError( ve.msg( 'visualeditor-saveerror', 'Empty server response' ), false /* prevents reapply */ ); }; /** * Update save dialog message on spam blacklist error * * @method * @param {Object} editApi */ ve.init.mw.ViewPageTarget.prototype.onSaveErrorSpamBlacklist = function ( editApi ) { this.showSaveError( ve.msg( 'spamprotectiontext' ) + ' ' + ve.msg( 'spamprotectionmatch', mw.language.listToText( editApi.spamblacklist.split( '|' ) ) ), false // prevents reapply ); }; /** * Update save dialog message on spam blacklist error * * @method * @param {Object} editApi */ ve.init.mw.ViewPageTarget.prototype.onSaveErrorAbuseFilter = function ( editApi ) { this.showSaveError( $( $.parseHTML( editApi.warning ) ) ); // Don't disable the save button. If the action is not disallowed the user may save the // edit by pressing Save again. The AbuseFilter API currently has no way to distinguish // between filter triggers that are and aren't disallowing the action. }; /** * Update save dialog when token fetch indicates another user is logged in * * @method * @param {boolean|undefined} isAnon Is newly logged in user anonymous. If * undefined, user is logged in */ ve.init.mw.ViewPageTarget.prototype.onSaveErrorNewUser = function ( isAnon ) { var badToken, userMsg; badToken = document.createTextNode( mw.msg( 'visualeditor-savedialog-error-badtoken' ) + ' ' ); // mediawiki.jqueryMsg has a bug with [[User:$1|$1]] (bug 51388) if ( isAnon ) { userMsg = 'visualeditor-savedialog-identify-anon'; } else { userMsg = 'visualeditor-savedialog-identify-user---' + mw.config.get( 'wgUserName' ); } this.showSaveError( $( badToken ).add( $.parseHTML( mw.message( userMsg ).parse() ) ) ); }; /** * Update save dialog on captcha error * * @method * @param {Object} editApi */ ve.init.mw.ViewPageTarget.prototype.onSaveErrorCaptcha = function ( editApi ) { var $captchaDiv = $( '
' );
this.captcha = {
input: new OO.ui.TextInputWidget(),
id: editApi.captcha.id
};
$captchaDiv.append( $captchaParagraph );
$captchaParagraph.append(
$( '' ).text( mw.msg( 'captcha-label' ) ),
document.createTextNode( mw.msg( 'colon-separator' ) )
);
if ( editApi.captcha.url ) { // FancyCaptcha
$captchaParagraph.append(
$( $.parseHTML( mw.message( 'fancycaptcha-edit' ).parse() ) )
.filter( 'a' ).attr( 'target', '_blank' ).end()
);
$captchaDiv.append(
$( '' ).attr( 'src', editApi.captcha.url )
);
} else if ( editApi.captcha.type === 'simple' || editApi.captcha.type === 'math' ) {
// SimpleCaptcha and MathCaptcha
$captchaParagraph.append(
mw.message( 'captcha-edit' ).parse(),
' ' ).append( ve.init.platform.getParsedMessage( 'missingsummary' ) ),
{ wrap: false }
);
this.saveDialog.popPending();
} else {
this.save( this.docToSave, saveOptions );
this.saveDeferred = saveDeferred;
}
};
/**
* Open the dialog to switch to edit source mode with the current wikitext, or just do it straight
* away if the document is unmodified. If we open the dialog, the document opacity will be set to
* half, which can be reset with the resetDocumentOpacity function.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.editSource = function () {
if ( !this.surface.getModel().hasBeenModified() ) {
this.switchToWikitextEditor( true );
return;
}
this.surface.getView().getDocument().getDocumentNode().$element.css( 'opacity', 0.5 );
this.surface.getDialogs().openWindow( 'wikitextswitchconfirm', { target: this } );
};
/**
* Handle clicks on the resolve conflict button in the conflict dialog.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogResolveConflict = function () {
// Get Wikitext from the DOM, and set up a submit call when it's done
this.serialize(
this.docToSave,
this.submitWithSaveFields.bind( this, { wpSave: 1 } )
);
};
/**
* Get save form fields from the save dialog form.
* @returns {Object} Form data for submission to the MediaWiki action=edit UI
*/
ve.init.mw.ViewPageTarget.prototype.getSaveFields = function () {
var fields = {};
this.$checkboxes
.each( function () {
var $this = $( this );
// We can't just use $this.val() because .val() always returns the value attribute of
// a checkbox even when it's unchecked
if ( $this.prop ( 'name' ) && ( $this.prop( 'type' ) !== 'checkbox' || $this.prop( 'checked' ) ) ) {
fields[$this.prop( 'name' )] = $this.val();
}
} );
ve.extendObject( fields, {
wpSummary: this.saveDialog ? this.saveDialog.editSummaryInput.getValue() : this.initialEditSummary,
wpCaptchaId: this.captcha && this.captcha.id,
wpCaptchaWord: this.captcha && this.captcha.input.getValue()
} );
return fields;
};
/**
* Invoke #submit with the data from #getSaveFields
* @param {Object} fields Fields to add in addition to those from #getSaveFields
* @param {string} wikitext Wikitext to submit
* @returns {boolean} Whether submission was started
*/
ve.init.mw.ViewPageTarget.prototype.submitWithSaveFields = function ( fields, wikitext ) {
return this.submit( wikitext, $.extend( this.getSaveFields(), fields ) );
};
/**
* Get edit API options from the save dialog form.
* @returns {Object} Save options for submission to the MediaWiki API
*/
ve.init.mw.ViewPageTarget.prototype.getSaveOptions = function () {
var key, options = this.getSaveFields(),
fieldMap = {
wpSummary: 'summary',
wpMinoredit: 'minor',
wpWatchthis: 'watch',
wpCaptchaId: 'captchaid',
wpCaptchaWord: 'captchaword'
};
for ( key in fieldMap ) {
if ( options[key] !== undefined ) {
options[fieldMap[key]] = options[key];
delete options[key];
}
}
if ( this.sanityCheckPromise.state() === 'rejected' ) {
options.needcheck = 1;
}
return options;
};
/**
* 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 a copy of the data now, before we unlock the surface and let the user edit,
// but we can defer the actual conversion and comparison
var viewPage = this,
doc = viewPage.surface.getModel().getDocument(),
data = new ve.dm.FlatLinearData( doc.getStore().clone(), ve.copy( doc.getFullData() ) ),
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
//
',
document.createTextNode( editApi.captcha.question )
);
} else if ( editApi.captcha.type === 'question' ) {
// QuestyCaptcha
$captchaParagraph.append(
mw.message( 'questycaptcha-edit' ).parse(),
'
',
editApi.captcha.question
);
}
$captchaDiv.append( this.captcha.input.$element );
// ProcessDialog's error system isn't great for this yet.
this.saveDialog.clearMessage( 'api-save-error' );
this.saveDialog.showMessage( 'api-save-error', $captchaDiv );
this.saveDialog.popPending();
};
/**
* Update save dialog message on unknown error
*
* @method
* @param {Object} editApi
* @param {Object|null} data API response data
*/
ve.init.mw.ViewPageTarget.prototype.onSaveErrorUnknown = function ( editApi, data ) {
this.showSaveError(
$( document.createTextNode(
( editApi && editApi.info ) ||
( data.error && data.error.info ) ||
( editApi && editApi.code ) ||
( data.error && data.error.code ) ||
'Unknown error'
) ),
false // prevents reapply
);
};
/**
* Update save dialog api-save-error message
*
* @method
* @param {string|jQuery|Node[]} msg Message content (string of HTML, jQuery object or array of
* Node objects)
* @param {boolean} [allowReapply=true] Whether or not to allow the user to reapply.
* Reset when swapping panels. Assumed to be true unless explicitly set to false.
*/
ve.init.mw.ViewPageTarget.prototype.showSaveError = function ( msg, allowReapply ) {
this.saveDeferred.reject( [ new OO.ui.Error( msg, { recoverable: allowReapply } ) ] );
};
/**
* 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().getDocument().once( 'transact',
this.saveDialog.clearDiff.bind( this.saveDialog )
);
this.saveDialog.setDiffAndReview( diffHtml );
};
/**
* 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.saveDialog.popPending();
};
/**
* 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 ) );
// It's possible to get here while the save dialog has never been opened (if the user uses
// the switch to source mode option)
if ( this.saveDialog ) {
this.saveDialog.popPending();
}
};
/**
* Handle edit conflict event.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onEditConflict = function () {
this.saveDialog.popPending();
this.saveDialog.swapPanel( 'conflict' );
};
/**
* Handle failed show changes event.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onNoChanges = function () {
this.saveDialog.popPending();
this.saveDialog.swapPanel( 'nochanges' );
this.saveDialog.getActions().setAbilities( { approve: true } );
};
/**
* 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( false, 'navigate-read' );
// Prevent the edit tab's normal behavior
e.preventDefault();
} else if ( this.activating ) {
this.deactivate( true, 'navigate-read' );
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 () {
if ( this.edited || this.restoring ) {
this.showSaveDialog();
}
};
/**
* Handle clicks on the MwMeta button in the toolbar.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onToolbarMetaButtonClick = function () {
this.surface.getDialogs().openWindow( 'meta' );
};
/**
* Check if the user is entering wikitext, and show a notification if they are.
*
* This check is fairly simplistic: it checks whether the content branch node the selection is in
* looks like wikitext, so it can trigger if the user types in a paragraph that has pre-existing
* wikitext-like content.
*
* This method is bound to the 'documentUpdate' event on the surface model, and unbinds itself when
* the wikitext notification is displayed.
*
* @param {ve.ui.Surface} surface
* @param {Object} [wikitextWarning] MediaWiki notification object
*/
ve.init.mw.ViewPageTarget.static.checkForWikitextWarning = function ( surface, wikitextWarning ) {
var text, node, doc = surface.getView().getDocument(),
selection = surface.getModel().getSelection(),
textMatches;
if ( !( selection instanceof ve.dm.LinearSelection ) ) {
return;
}
node = doc.getBranchNodeFromOffset( selection.getRange().start );
if ( !( node instanceof ve.ce.ContentBranchNode ) ) {
return;
}
text = ve.ce.getDomText( node.$element[0] );
textMatches = text.match( /\[\[|\{\{|''|