diff --git a/VisualEditor.i18n.php b/VisualEditor.i18n.php index 6b924fa7ff..5b9827a683 100644 --- a/VisualEditor.i18n.php +++ b/VisualEditor.i18n.php @@ -185,7 +185,7 @@ $messages['en'] = array( 'visualeditor-savedialog-label-save' => 'Save page', 'visualeditor-savedialog-label-warning' => 'Warning', 'visualeditor-savedialog-title-conflict' => 'Conflict', - 'visualeditor-savedialog-title-nochanges' => 'No changes', + 'visualeditor-savedialog-title-nochanges' => 'No changes to review', 'visualeditor-savedialog-title-review' => 'Review your changes', 'visualeditor-savedialog-title-save' => 'Save your changes', 'visualeditor-savedialog-warning-dirty' => 'Your edit may have been corrupted – please review before saving.', diff --git a/VisualEditor.php b/VisualEditor.php index 6a9e71f07b..6fc9632cf4 100644 --- a/VisualEditor.php +++ b/VisualEditor.php @@ -502,6 +502,7 @@ $wgResourceModules += array( 've/ui/layouts/ve.ui.PanelLayout.js', 've/ui/layouts/ve.ui.StackPanelLayout.js', + 've-mw/ui/dialogs/ve.ui.MWSaveDialog.js', 've-mw/ui/dialogs/ve.ui.MWMetaDialog.js', 've-mw/ui/dialogs/ve.ui.MWBetaWelcomeDialog.js', 've-mw/ui/dialogs/ve.ui.MWMediaInsertDialog.js', @@ -559,6 +560,7 @@ $wgResourceModules += array( 'unicodejs.wordbreak', 'ext.visualEditor.base', 'mediawiki.Title', + 'mediawiki.action.history.diff', 'jquery.autoEllipsis', ), 'messages' => array( diff --git a/modules/ve-mw/init/styles/ve.init.mw.ViewPageTarget.css b/modules/ve-mw/init/styles/ve.init.mw.ViewPageTarget.css index d40c1c5c79..a8a86d8b16 100644 --- a/modules/ve-mw/init/styles/ve.init.mw.ViewPageTarget.css +++ b/modules/ve-mw/init/styles/ve.init.mw.ViewPageTarget.css @@ -121,226 +121,10 @@ color: #555; } -/* Save dialog styles */ - -.ve-init-mw-viewPageTarget-toolbarTracker { - position: absolute; - top: 0; - height: 0; - overflow: visible; -} - -.ve-init-mw-viewPageTarget-toolbarTracker-floating { - position: fixed; - z-index: 100; -} - -.ve-init-mw-viewPageTarget-saveDialog .ve-ui-pushButtonWidget { - float: right; - margin-left: 0.5em; - font-size: 0.8em; -} - -.ve-init-mw-viewPageTarget-saveDialog-working { - display: none; - float: right; - height: 2em; - width: 128px; - margin-right: 1em; - background-position: right center; - background-repeat: no-repeat; -} - -.ve-init-mw-viewPageTarget-saveDialog { - display: none; - top: 0.25em; - right: 0.5em; - width: 29em; - min-width: 29em; - font-family: sans-serif; - position: absolute; - border: solid 1px #ccc; - border-radius: 0.25em; - background-color: #fff; - box-shadow: 0 0.15em 0.5em 0 rgba(0, 0, 0, 0.2); - padding: 2.5em 0.75em 0.75em 0.75em; - margin: 0 0 0 0.5em; - z-index: 3; - - /* slide-diff can get quite long, handle overflow */ - /* max-height set from javascript */ - overflow-x: auto; -} - -.ve-init-mw-viewPageTarget-saveDialog-head { - position: absolute; - top: 0.4em; - left: 0.5em; - right: 0.5em; -} - -.ve-init-mw-viewPageTarget-saveDialog-title { - height: 2em; - line-height: 2em; - color: #333; - font-size: 0.9em; - float: left; - margin: 0 0.5em; -} - -.ve-init-mw-viewPageTarget-saveDialog-prevButton { - float: left; - position: relative; - top: 0.1em; - width: 1.5em; - height: 1.5em; - cursor: pointer; - opacity: 0.8; - /* @see ve.init.mw.Icons */ - background-position: left top; - background-repeat: no-repeat; - padding-right: 0.5em; - border-right: 1px solid #eee; - margin-right: 0.5em; -} - -.ve-init-mw-viewPageTarget-saveDialog-closeButton { - float: right; - position: relative; - top: 0.1em; - width: 1.5em; - height: 1.5em; - cursor: pointer; - opacity: 0.8; - /* @see ve.init.mw.Icons */ - background-position: right top; - background-repeat: no-repeat; -} - -.ve-init-mw-viewPageTarget-saveDialog-body { - border-top: 1px solid #ddd; - padding-top: 1em; -} - -.ve-init-mw-viewPageTarget-saveDialog-slide { - display: none; -} - -.ve-init-mw-viewPageTarget-saveDialog-slide-review .ve-init-mw-viewPageTarget-saveDialog-viewer { - margin-bottom: 1em; -} - -.ve-init-mw-viewPageTarget-saveDialog-slide-review .ve-init-mw-viewPageTarget-saveDialog-viewer pre { - margin: 0; -} - -.ve-init-mw-viewPageTarget-saveDialog-slide-review .ve-init-mw-viewPageTarget-saveDialog-viewer .diff { - font-size: 0.8em; -} - -.ve-init-mw-viewPageTarget-saveDialog-foot { - padding-top: 1em; -} - -.ve-init-mw-viewPageTarget-saveDialog-dirtymsg, -.ve-init-mw-viewPageTarget-saveDialog-license, -.ve-init-mw-viewPageTarget-saveDialog-report-notice { - font-size: 0.7em; - line-height: 1.25em; - padding: 0; - margin: 0; - color: #999; -} - - -.ve-init-mw-viewPageTarget-saveDialog-dirtymsg { - float: right; -} - -.ve-init-mw-viewPageTarget-saveDialog-summary, -.ve-init-mw-viewPageTarget-saveDialog-report { - background-color: #fff; - border: solid 1px #cccccc; - padding: 0.5em; - border-radius: 0.25em 0.25em 0 0; -} - -.ve-init-mw-viewPageTarget-saveDialog-report { - margin-bottom: 1em; - border-radius: 0.25em; -} - -.ve-init-mw-viewPageTarget-saveDialog-summary-focused, -.ve-init-mw-viewPageTarget-saveDialog-report-focused { - border-color: #aaa; -} - -.ve-init-mw-viewPageTarget-saveDialog-conflict { - margin-bottom: 1em; -} - -.ve-init-mw-viewPageTarget-saveDialog-messages, -.ve-init-mw-viewPageTarget-saveDialog-conflict, -.ve-init-mw-viewPageTarget-saveDialog-nochanges { - font-size: 0.8em; -} - -.ve-init-mw-viewPageTarget-saveDialog-options { - position: relative; - background-color: #f7f7f7; - margin-bottom: 1em; - border: solid 1px #cccccc; - border-top: none; - border-radius: 0 0 0.25em 0.25em; - min-height: 2.25em; -} - -.ve-init-mw-viewPageTarget-saveDialog-body label { - font-size: 0.8em; - line-height: 3em; -} - -.ve-init-mw-viewPageTarget-saveDialog input[type="checkbox"] { - margin: 0 0.5em 0 1em; - line-height: 3em; -} - -.ve-init-mw-viewPageTarget-saveDialog-body .ve-init-mw-viewPageTarget-saveDialog-editSummary-label { - line-height: 2em; -} - -.ve-init-mw-viewPageTarget-saveDialog-editSummaryCount { - position: absolute; - right: 0; - top: 0; - bottom: 0; - border-left: solid 1px #eee; - line-height: 3em; - padding: 0 1em; - color: #aaa; -} - -.ve-init-mw-viewPageTarget-saveDialog-editSummary, -.ve-init-mw-viewPageTarget-saveDialog-problem { - border: none; - background-color: transparent; - margin: 0; - padding: 0; - resize: none; - font-size: 0.8em; - font-family: sans-serif; - height: 5em; -} - -.ve-init-mw-viewPageTarget-saveDialog-editSummary:focus, -.ve-init-mw-viewPageTarget-saveDialog-problem:focus { - outline: none; -} - /* Images */ .ve-init-mw-viewPageTarget-loading, -.ve-init-mw-viewPageTarget-saveDialog-working { +.ve-ui-mwSaveDialog-working { /* @embed */ background-image: url(images/loading-ltr.gif); } 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 99a3d60549..13c1602109 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js +++ b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js @@ -34,9 +34,7 @@ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() { this.toolbarOffset = null; this.toolbarCancelButton = null; this.toolbarSaveButton = null; - this.saveDialogSlideHistory = []; - this.saveDialogSaveButton = null; - this.saveDialogReviewGoodButton = null; + this.saveDialog = null; this.toolbarEditNoticesButton = null; this.toolbarEditNotices = null; this.toolbarBetaNoticesButton = null; @@ -62,7 +60,6 @@ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() { this.actFromPopState = false; this.scrollTop = null; this.currentUri = currentUri; - this.messages = {}; this.section = currentUri.query.vesection || null; this.sectionPositionRestored = false; this.sectionTitleRestored = false; @@ -153,59 +150,6 @@ ve.init.mw.ViewPageTarget.compatibility = { } }; -// TODO: Accessibility tooltips and logical tab order for prevButton and closeButton. -ve.init.mw.ViewPageTarget.saveDialogTemplate = '\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ - \ -
\ - \ -
\ -
\ -
\ -
\ - \ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -

\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
'; - /* Methods */ /** @@ -256,10 +200,9 @@ ve.init.mw.ViewPageTarget.prototype.deactivate = function ( override ) { this.detachToolbarButtons(); } - this.resetSaveDialog(); - this.hideSaveDialog(); - this.detachSaveDialog(); - + this.saveDialog.reset(); + this.saveDialog.close(); + // Check we got as far as setting up the surface if ( this.active ) { // If we got as far as setting up the surface, tear that down this.tearDownSurface(); @@ -298,7 +241,6 @@ ve.init.mw.ViewPageTarget.prototype.onLoad = function ( doc ) { this.setupToolbarBetaNotices(); this.setupSaveDialog(); this.attachToolbarButtons(); - this.attachSaveDialog(); this.restoreScrollPosition(); this.restoreEditSection(); this.setupBeforeUnloadHandler(); @@ -394,9 +336,8 @@ ve.init.mw.ViewPageTarget.prototype.onSave = function ( html, newid ) { mw.config.set( 'wgCurRevisionId', newid ); this.revid = newid; } - - this.hideSaveDialog(); - this.resetSaveDialog(); + this.saveDialog.close(); + this.saveDialog.reset(); this.replacePageContent( html ); this.setupSectionEditLinks(); this.tearDownBeforeUnloadHandler(); @@ -422,21 +363,21 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data var api, editApi, viewPage = this; - this.saveDialogSaveButton.setDisabled( false ); - this.$saveDialogLoadingIcon.hide(); + this.saveDialog.saveButton.setDisabled( false ); + this.saveDialog.$loadingIcon.hide(); - this.clearMessage( 'api-save-error' ); + this.saveDialog.clearMessage( 'api-save-error' ); // Handle empty response if ( !data ) { - this.showMessage( + this.saveDialog.showMessage( 'api-save-error', ve.msg( 'visualeditor-saveerror', 'Empty server response' ), { wrap: 'error' } ); - this.saveDialogSaveButton.setDisabled( true ); + this.saveDialog.saveButton.setDisabled( true ); return; } @@ -444,7 +385,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data // Handle spam blacklist error (either from core or from Extension:SpamBlacklist) if ( editApi && editApi.spamblacklist ) { - this.showMessage( + this.saveDialog.showMessage( 'api-save-error', // TODO: Use mediawiki.language equivalant of Language.php::listToText once it exists ve.msg( 'spamprotectiontext' ) + ' ' + ve.msg( 'spamprotectionmatch', editApi.spamblacklist.split( '|' ).join( ', ' ) ), @@ -452,14 +393,14 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data wrap: 'error' } ); - this.saveDialogSaveButton.setDisabled( true ); + this.saveDialog.saveButton.setDisabled( true ); return; } // Handle warnings/errors from Extension:AbuseFilter // TODO: Move this to a plugin if ( editApi && editApi.info && editApi.info.indexOf( 'Hit AbuseFilter:' ) === 0 && editApi.warning ) { - this.showMessage( + this.saveDialog.showMessage( 'api-save-error', $.parseHTML( editApi.warning ), { wrap: false } @@ -473,8 +414,8 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data // Handle token errors if ( data.error && data.error.code === 'badtoken' ) { api = new mw.Api(); - viewPage.saveDialogSaveButton.setDisabled( true ); - viewPage.$saveDialogLoadingIcon.show(); + viewPage.saveDialog.saveButton.setDisabled( true ); + viewPage.saveDialog.$loadingIcon.show(); api.get( { // action=query&meta=userinfo and action=tokens&type=edit can't be combined // but action=query&meta=userinfo and action=query&prop=info can, however @@ -489,7 +430,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data 'intoken': 'edit' } ) .always( function () { - viewPage.$saveDialogLoadingIcon.hide(); + viewPage.saveDialog.$loadingIcon.hide(); } ) .done( function ( data ) { var badTokenText, userMsg, @@ -511,7 +452,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data viewPage.saveDocument(); } else { // The now current session is a different user - viewPage.saveDialogSaveButton.setDisabled( false ); + viewPage.saveDialog.saveButton.setDisabled( false ); // Trailing space is to separate from the other message. badTokenText = document.createTextNode( mw.msg( 'visualeditor-savedialog-error-badtoken' ) + ' ' ); @@ -526,7 +467,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data 'wgUserName': null } ); - viewPage.showMessage( + viewPage.saveDialog.showMessage( 'api-save-error', $( badTokenText ).add( $.parseHTML( mw.message( 'visualeditor-savedialog-identify-anon' ).parse() ) @@ -545,7 +486,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data .replace( /\$1/g, userInfo.name ) ); - viewPage.showMessage( + viewPage.saveDialog.showMessage( 'api-save-error', $( badTokenText ).add( $.parseHTML( mw.message( userMsg ).parse() ) @@ -573,9 +514,9 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data input: new ve.ui.TextInputWidget(), id: editApi.captcha.id }; - this.showMessage( + this.saveDialog.showMessage( 'api-save-error', - $( '
').append( + $( '
' ).append( // msg: simplecaptcha-edit, fancycaptcha-edit, .. $( '

' ).append( $( '' ).text( mw.msg( 'captcha-label' ) ), @@ -594,7 +535,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data } // Handle (other) unknown and/or unrecoverable errors - this.showMessage( + this.saveDialog.showMessage( 'api-save-error', document.createTextNode( ( editApi && editApi.info ) || @@ -607,7 +548,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data wrap: 'error' } ); - this.saveDialogSaveButton.setDisabled( true ); + this.saveDialog.saveButton.setDisabled( true ); }; /** @@ -619,18 +560,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status, data 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 ) ); + this.saveDialog.setDiffAndReview( diffHtml ); }; /** @@ -642,13 +572,7 @@ ve.init.mw.ViewPageTarget.prototype.onShowChanges = function ( diffHtml ) { 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( $( '

' ).text( wikitext ) );
-
-		this.$saveDialogLoadingIcon.hide();
-		this.saveDialogReviewGoodButton.setDisabled( false );
+	this.saveDialog.setDiffAndReview( $( '
' ).text( wikitext ) );
 };
 
 /**
@@ -660,7 +584,7 @@ ve.init.mw.ViewPageTarget.prototype.onSerialize = function ( wikitext ) {
  */
 ve.init.mw.ViewPageTarget.prototype.onShowChangesError = function ( jqXHR, status ) {
 	alert( ve.msg( 'visualeditor-differror', status ) );
-	this.$saveDialogLoadingIcon.hide();
+	this.saveDialog.$loadingIcon.hide();
 };
 
 /**
@@ -672,7 +596,7 @@ ve.init.mw.ViewPageTarget.prototype.onShowChangesError = function ( jqXHR, statu
  */
 ve.init.mw.ViewPageTarget.prototype.onSerializeError = function ( jqXHR, status ) {
 	alert( ve.msg( 'visualeditor-serializeerror', status ) );
-	this.$saveDialogLoadingIcon.hide();
+	this.saveDialog.$loadingIcon.hide();
 };
 
 /**
@@ -681,8 +605,8 @@ ve.init.mw.ViewPageTarget.prototype.onSerializeError = function ( jqXHR, status
  * @method
  */
 ve.init.mw.ViewPageTarget.prototype.onEditConflict = function () {
-	this.$saveDialogLoadingIcon.hide();
-	this.swapSaveDialog( 'conflict' );
+	this.saveDialog.$loadingIcon.hide();
+	this.saveDialog.swapPanel( 'conflict' );
 };
 
 /**
@@ -691,8 +615,9 @@ ve.init.mw.ViewPageTarget.prototype.onEditConflict = function () {
  * @method
  */
 ve.init.mw.ViewPageTarget.prototype.onNoChanges = function () {
-	this.$saveDialogLoadingIcon.hide();
-	this.swapSaveDialog( 'nochanges' );
+	this.saveDialog.$loadingIcon.hide();
+	this.saveDialog.swapPanel( 'nochanges' );
+	this.saveDialog.reviewGoodButton.setDisabled( false );
 };
 
 /**
@@ -778,10 +703,7 @@ ve.init.mw.ViewPageTarget.prototype.onToolbarFeedbackToolClick = function () {
  */
 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.saveDialog.$reviewViewer.empty();
 	this.surface.getModel().disconnect( this, { 'transact': 'onSurfaceModelTransact' } );
 };
 
@@ -833,8 +755,28 @@ ve.init.mw.ViewPageTarget.prototype.updateToolbarSaveButtonState = function () {
  *
  * @method
  */
-ve.init.mw.ViewPageTarget.prototype.onSaveDialogReviewButtonClick = function () {
-	this.swapSaveDialog( 'review' );
+ve.init.mw.ViewPageTarget.prototype.onSaveDialogReview = function () {
+	var doc = this.surface.getModel().getDocument();
+	this.sanityCheckVerified = true;
+	this.saveDialog.setSanityCheck( this.sanityCheckVerified );
+
+	if ( !this.saveDialog.$reviewViewer.find( 'table, pre' ).length ) {
+		this.saveDialog.reviewGoodButton.setDisabled( true );
+		this.saveDialog.$loadingIcon.show();
+		if ( this.pageExists ) {
+			// Has no callback, handled via target.onShowChanges
+			this.showChanges(
+				ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() )
+			);
+		} else {
+			this.serialize(
+				ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
+				ve.bind( this.onSerialize, this )
+			);
+		}
+	} else {
+		this.saveDialog.swapPanel( 'review' );
+	}
 };
 
 /**
@@ -842,7 +784,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveDialogReviewButtonClick = function ()
  *
  * @method
  */
-ve.init.mw.ViewPageTarget.prototype.onSaveDialogSaveButtonClick = function () {
+ve.init.mw.ViewPageTarget.prototype.onSaveDialogSave = function () {
 	this.saveDocument();
 };
 
@@ -864,17 +806,17 @@ ve.init.mw.ViewPageTarget.prototype.saveDocument = function () {
 	if (
 		+mw.user.options.get( 'forceeditsummary' ) &&
 		saveOptions.summary === '' &&
-		!this.messages.missingsummary
+		!this.saveDialog.messages.missingsummary
 	) {
-		this.showMessage(
+		this.saveDialog.showMessage(
 			'missingsummary',
 			// Wrap manually since this core message already includes a bold "Warning:" label
 			$( '

' ).append( ve.init.platform.getParsedMessage( 'missingsummary' ) ), { wrap: false } ); } else { - this.saveDialogSaveButton.setDisabled( true ); - this.$saveDialogLoadingIcon.show(); + this.saveDialog.saveButton.setDisabled( true ); + this.saveDialog.$loadingIcon.show(); this.save( ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ), saveOptions @@ -882,21 +824,12 @@ ve.init.mw.ViewPageTarget.prototype.saveDocument = function () { } }; -/** - * 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 () { +ve.init.mw.ViewPageTarget.prototype.onSaveDialogResolveConflict= function () { var doc = this.surface.getModel().getDocument(); // Get Wikitext from the DOM, and set up a submit call when it's done this.serialize( @@ -915,21 +848,21 @@ ve.init.mw.ViewPageTarget.prototype.onSaveDialogResolveConflictButtonClick = fun */ ve.init.mw.ViewPageTarget.prototype.getSaveOptions = function () { var options = { - 'summary': this.$saveDialog.find( '#ve-init-mw-viewPageTarget-saveDialog-editSummary' ).val(), + 'summary': this.saveDialog.editSummaryInput.$input.val(), 'captchaid': this.captcha && this.captcha.id, 'captchaword': this.captcha && this.captcha.input.getValue() }; if ( this.sanityCheckPromise.state() === 'rejected' ) { options.needcheck = 1; } - if ( this.$saveDialog.find( '#wpMinoredit' ).prop( 'checked' ) ) { + if ( this.saveDialog.$saveOptions.find( '#wpMinoredit' ).prop( 'checked' ) ) { options.minor = 1; } - if ( this.$saveDialog.find( '#wpWatchthis' ).prop( 'checked' ) ) { + if ( this.saveDialog.$saveOptions.find( '#wpWatchthis' ).prop( 'checked' ) ) { options.watch = 1; } - this.$saveDialog - .find( '.ve-init-mw-viewPageTarget-saveDialog-checkboxes' ) + this.saveDialog.$saveOptions + .find( '.ve-ui-mwSaveDialog-checkboxes' ) .find( 'input:not(#wpMinoredit, #wpWatchthis)' ) .each( function () { var $this = $( this ); @@ -942,33 +875,6 @@ ve.init.mw.ViewPageTarget.prototype.getSaveOptions = function () { return options; }; -/** - * 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. * @@ -1379,155 +1285,25 @@ ve.init.mw.ViewPageTarget.prototype.detachToolbarButtons = function () { ve.init.mw.ViewPageTarget.prototype.setupSaveDialog = function () { var sectionTitle = '', viewPage = this; - // Save button on "save" slide - this.saveDialogSaveButton = new ve.ui.PushButtonWidget( { - 'label': ve.msg( - // visualeditor-savedialog-label-restore, visualeditor-savedialog-label-save - 'visualeditor-savedialog-label-' + ( viewPage.restoring ? 'restore' : 'save' ) - ), - 'flags': ['constructive'] - } ); - this.saveDialogSaveButton.connect( this, { 'click': 'onSaveDialogSaveButtonClick' } ); - - // Review button on "save" slide - this.saveDialogReviewButton = new ve.ui.PushButtonWidget( { - 'label': ve.msg( - 'visualeditor-savedialog-label-review' - ) - } ); - this.saveDialogReviewButton.connect( this, { 'click': 'onSaveDialogReviewButtonClick' } ); - - this.saveDialogReviewGoodButton = new ve.ui.PushButtonWidget( { - 'label': ve.msg( 'visualeditor-savedialog-label-review-good' ), - 'flags': ['constructive'] - } ); - this.saveDialogReviewGoodButton.connect( - this, { 'click': 'onSaveDialogReviewGoodButtonClick' } - ); - - this.saveDialogResolveConflictButton = new ve.ui.PushButtonWidget( { - 'label': ve.msg( 'visualeditor-savedialog-label-resolve-conflict' ), - 'flags': ['constructive'] - } ); - this.saveDialogResolveConflictButton.connect( this, { 'click': 'onSaveDialogResolveConflictButtonClick' } ); - + viewPage.saveDialog = this.surface.getDialogs().getWindow( 'mwSave' ); if ( viewPage.section ) { sectionTitle = viewPage.$document.find( 'h1, h2, h3, h4, h5, h6' ).eq( viewPage.section - 1 ).text(); sectionTitle = '/* ' + ve.graphemeSafeSubstring( sectionTitle, 0, 244 ) + ' */ '; + viewPage.saveDialog.editSummaryInput.$input.val( sectionTitle ); viewPage.sectionTitleRestored = true; if ( viewPage.sectionPositionRestored ) { viewPage.onSectionRestored(); } } - viewPage.$saveDialog - // Must not use replaceWith because that can't be used on fragement roots, - // plus, we want to preserve the reference and class names of the wrapper. - .empty().append( this.constructor.saveDialogTemplate ) - // Attach buttons - .find( '.ve-init-mw-viewPageTarget-saveDialog-slide-save' ) - .find( '.ve-init-mw-viewPageTarget-saveDialog-actions' ) - .prepend( viewPage.saveDialogSaveButton.$, viewPage.saveDialogReviewButton.$ ) - .end() - .end() - .find( '.ve-init-mw-viewPageTarget-saveDialog-slide-review' ) - .find( '.ve-init-mw-viewPageTarget-saveDialog-actions' ) - .prepend( viewPage.saveDialogReviewGoodButton.$ ) - .end() - .end() - .find( '.ve-init-mw-viewPageTarget-saveDialog-slide-conflict' ) - .find( '.ve-init-mw-viewPageTarget-saveDialog-actions' ) - .prepend( viewPage.saveDialogResolveConflictButton.$ ) - .end() - .end() - .find( '.ve-init-mw-viewPageTarget-saveDialog-closeButton' ) - .click( ve.bind( viewPage.onSaveDialogCloseButtonClick, viewPage ) ) - .end() - .find( '.ve-init-mw-viewPageTarget-saveDialog-prevButton' ) - .click( ve.bind( viewPage.onSaveDialogPrevButtonClick, viewPage ) ) - .end() - // Attach contents - .find( '#ve-init-mw-viewPageTarget-saveDialog-editSummary-label' ) - .html( ve.init.platform.getParsedMessage( 'summary' ) ) - .end() - .find( '#ve-init-mw-viewPageTarget-saveDialog-editSummary' ) - .attr( { - 'placeholder': ve.msg( 'visualeditor-editsummary' ) - } ) - .val( sectionTitle ) - .placeholder() - .byteLimit( viewPage.editSummaryByteLimit ) - .on( { - 'focus': function () { - $( this ).parent().addClass( - 've-init-mw-viewPageTarget-saveDialog-summary-focused' - ); - }, - 'blur': function () { - $( this ).parent().removeClass( - 've-init-mw-viewPageTarget-saveDialog-summary-focused' - ); - }, - 'keyup keydown mouseup cut paste change focus blur': function () { - var $textarea = $( this ), - $editSummaryCount = $textarea - .closest( '.ve-init-mw-viewPageTarget-saveDialog-slide-save' ) - .find( '.ve-init-mw-viewPageTarget-saveDialog-editSummaryCount' ); - // TODO: This looks a bit weird, there is no unit in the UI, just numbers - // Users likely assume characters but then it seems to count down quicker - // than expected. Facing users with the word "byte" is bad? (bug 40035) - setTimeout( function () { - $editSummaryCount.text( - viewPage.editSummaryByteLimit - $.byteLength( $textarea.val() ) - ); - } ); - } - } ) - .end() - .find( '.ve-init-mw-viewPageTarget-saveDialog-editSummaryCount' ) - .text( viewPage.editSummaryByteLimit ) - .end() - .find( '.ve-init-mw-viewPageTarget-saveDialog-checkboxes' ) - .html( ve.getObjectValues( viewPage.checkboxes ).join( '\n' ) ) - .find( 'a' ) - .attr( 'target', '_blank' ) - .end() - .find( '#wpMinoredit' ) - .prop( 'checked', +mw.user.options.get( 'minordefault' ) ) - .end() - .find( '#wpWatchthis' ) - .prop( 'checked', - mw.user.options.get( 'watchdefault' ) || - ( mw.user.options.get( 'watchcreations' ) && !viewPage.pageExists ) || - mw.config.get( 'wgVisualEditor' ).isPageWatched - ) - .end() - .end() - .find( '.ve-init-mw-viewPageTarget-saveDialog-license' ) - .html( ve.init.platform.getParsedMessage( 'copyrightwarning' ) ) - .end() - .find( '.ve-init-mw-viewPageTarget-saveDialog-conflict' ) - .html( ve.init.platform.getParsedMessage( 'visualeditor-editconflict' ) ) - .end() - .find( '.ve-init-mw-viewPageTarget-saveDialog-nochanges' ) - .html( ve.init.platform.getParsedMessage( 'visualeditor-diff-nochanges' ) ) - ; - - // Get reference to loading icon - viewPage.$saveDialogLoadingIcon = viewPage.$saveDialog - .find( '.ve-init-mw-viewPageTarget-saveDialog-working' ); - - // Hook onto the 'watch' event on by mediawiki.page.watch.ajax.js - // Triggered when mw.page.watch.updateWatchLink(link, action) is called - $( '#ca-watch, #ca-unwatch' ) - .on( - 'watchpage.mw', - function ( e, action ) { - viewPage.$saveDialog - .find( '#wpWatchthis' ) - .prop( 'checked', ( action === 'watch' ) ); - } - ); + // Connect to save dialog + viewPage.saveDialog.connect( this, { + 'save': 'onSaveDialogSave', + 'review': 'onSaveDialogReview', + 'resolve': 'onSaveDialogResolveConflict' + } ); + // Setup checkboxes + viewPage.saveDialog.setupCheckboxes( ve.getObjectValues( viewPage.checkboxes ).join( '\n' ) ); }; /** @@ -1536,208 +1312,11 @@ ve.init.mw.ViewPageTarget.prototype.setupSaveDialog = function () { * @method */ ve.init.mw.ViewPageTarget.prototype.showSaveDialog = function () { - var viewPage = this; - - viewPage.surface.disable(); - viewPage.$document.css( 'opacity', 0.5 ); - - viewPage.toolbarBetaNotices.hide(); - viewPage.toolbarEditNotices.hide(); - - viewPage.swapSaveDialog( 'save' ); - - viewPage.$saveDialog.fadeIn( 'fast', function () { - // Initial size - viewPage.onResizeSaveDialog(); - } ); - - $( document ).on( 'keydown.ve-savedialog', function ( e ) { - // Escape - if ( e.which === ve.Keys.ESCAPE ) { - viewPage.onSaveDialogCloseButtonClick(); - } - } ); - - $( window ).on( 'resize.ve-savedialog', ve.bind( viewPage.onResizeSaveDialog, viewPage ) ); -}; - -/** - * Update window-size related aspects of the save dialog - * - * @method - */ -ve.init.mw.ViewPageTarget.prototype.onResizeSaveDialog = function () { - var $d = this.$saveDialog, $w = $( window ); - - // Available space for css-height is window height, - // without the space between the dialog and the window top, - // without the space above/below between css-height and outerHeight. - $d.css( 'max-height', - $w.height() - - ( $d.offset().top - $w.scrollTop() ) - - ( $d.outerHeight( true ) - $d.height() ) - - 20 // shadow - ); -}; - -/** - * Hide the save dialog - */ -ve.init.mw.ViewPageTarget.prototype.hideSaveDialog = function () { - // Reset history on close (bug 49481) - this.saveDialogSlideHistory.length = 0; - this.$saveDialog.fadeOut( 'fast' ); - if ( this.$document ) { - this.$document.focus(); - } - $( document ).off( 'keydown.ve-savedialog' ); - $( window ).off( 'resize', this.onResizeSaveDialog ); - - if ( this.surface ) { - this.surface.enable(); - this.$document.css( 'opacity', '' ); - } -}; - -/** - * Reset the fields of the save dialog. - * - * TODO: Maybe call this more cleverly only when the document changes, so that closing and - * re-opening the saveDialog doesn't remove the user input and the diff cache. - * - * @method - */ -ve.init.mw.ViewPageTarget.prototype.resetSaveDialog = function () { - this.$saveDialog - .find( '#ve-init-mw-viewPageTarget-saveDialog-editSummary' ) - .val( '' ) - .end() - .find( '#wpMinoredit' ) - .prop( 'checked', false ) - .end() - // Clear the diff - .find( '.ve-init-mw-viewPageTarget-saveDialog-viewer' ) - .empty(); -}; - -/** - * Swap state in the save dialog. - * - * @method - * @param {string} slide One of 'save', 'review', 'conflict' or 'nochanges' - * @param {Object} [options] - * @param {boolean} [options.fromHistory] Whether this swap was triggered from interaction - * with the slide history (e.g. surpresses pushing of target slide in the history again). - * @returns {jQuery} The now active slide. - * @throws {Error} Unknown saveDialog slide - */ -ve.init.mw.ViewPageTarget.prototype.swapSaveDialog = function ( slide, options ) { - var $slide, $viewer, - doc = this.surface.getModel().getDocument(); - - if ( ve.indexOf( slide, [ 'save', 'review', 'conflict', 'nochanges' ] ) === -1 ) { - throw new Error( 'Unknown saveDialog slide: ' + slide ); - } - - options = options || {}; - - if ( !options.fromHistory ) { - this.saveDialogSlideHistory.push( slide ); - } - - $slide = this.$saveDialog.find( '.ve-init-mw-viewPageTarget-saveDialog-slide-' + slide ); - - this.$saveDialog - // Hide "prev" button when (back) on the first slide - .find( '.ve-init-mw-viewPageTarget-saveDialog-prevButton' ) - .toggle( this.saveDialogSlideHistory.length >= 2 ) - .end() - // Update title to one of: - // - visualeditor-savedialog-title-save - // - visualeditor-savedialog-title-review - // - visualeditor-savedialog-title-conflict - // - visualeditor-savedialog-title-nochanges - .find( '.ve-init-mw-viewPageTarget-saveDialog-title' ) - .text( ve.msg( 'visualeditor-savedialog-title-' + slide ) ) - .end() - // Hide other slides - .find( '.ve-init-mw-viewPageTarget-saveDialog-slide' ) - .not( $slide ) - .hide(); - - // Old messages should not persist after slide changes - this.clearAllMessages(); - // Reset save button if we disabled it for e.g. unrecoverable spam error - this.saveDialogSaveButton.setDisabled( false ); - - if ( slide === 'save' ) { - if ( !this.sanityCheckVerified ) { - this.showMessage( 'dirtywarning', mw.msg( 'visualeditor-savedialog-warning-dirty' ) ); - } - } - - if ( slide === 'review' ) { - this.sanityCheckVerified = true; - $viewer = $slide.find( '.ve-init-mw-viewPageTarget-saveDialog-viewer' ); - if ( !$viewer.find( 'table, pre' ).length ) { - this.saveDialogReviewGoodButton.setDisabled( true ); - this.$saveDialogLoadingIcon.show(); - if ( this.pageExists ) { - // Has no callback, handled via target.onShowChanges - this.showChanges( - ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ) - ); - } else { - this.serialize( - ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ), - ve.bind( this.onSerialize, this ) - ); - } - } - this.$saveDialog.css( 'width', '100%' ); - } else { - this.$saveDialog.css( 'width', '' ); - } - - // Show the target slide - $slide.show(); - - mw.hook( 've.saveDialog.stateChanged' ).fire(); - - if ( slide === 'save' ) { - setTimeout( function () { - var $textarea = $slide.find( '#ve-init-mw-viewPageTarget-saveDialog-editSummary' ); - $textarea.focus(); - // If message has be pre-filled (e.g. section edit), move cursor to end - if ( $textarea.val() !== '' ) { - ve.selectEnd( $textarea[0] ); - } - } ); - } - - return $slide; -}; - -/** - * Add the save dialog to the user interface. - * - * @method - */ -ve.init.mw.ViewPageTarget.prototype.attachSaveDialog = function () { - this.surface.$globalOverlay.append( - this.$toolbarTracker.append( - this.$saveDialog - ) - ); -}; - -/** - * Remove the save dialog from the user interface. - * - * @method - */ -ve.init.mw.ViewPageTarget.prototype.detachSaveDialog = function () { - this.$saveDialog.detach(); + this.toolbarBetaNotices.hide(); + this.toolbarEditNotices.hide(); + this.saveDialog.setSanityCheck( this.sanityCheckVerified ); + this.saveDialog.swapPanel( 'save' ); + this.surface.getDialogs().open( 'mwSave' ); }; /** @@ -2117,63 +1696,6 @@ ve.init.mw.ViewPageTarget.prototype.onSectionRestored = function () { this.sectionTitleRestored = false; }; -/** - * Show a message in the save dialog. - * - * @param {string} name Message's unique name - * @param {string|jQuery|Array} message Message content (string of HTML, jQuery object or array of - * Node objects) - * @param {Object} [options] - * @param {boolean} [options.wrap="warning"] Whether to wrap the message in a paragraph and if - * so, how. One of "warning", "error" or false. - */ -ve.init.mw.ViewPageTarget.prototype.showMessage = function ( name, message, options ) { - var $message; - if ( !this.messages[name] ) { - options = options || {}; - if ( options.wrap === undefined ) { - options.wrap = 'warning'; - } - $message = $( '

' ); - if ( options.wrap !== false ) { - $message.append( $( '

').append( - // visualeditor-savedialog-label-error - // visualeditor-savedialog-label-warning - $( '' ).text( mw.msg( 'visualeditor-savedialog-label-' + options.wrap ) ), - document.createTextNode( mw.msg( 'colon-separator' ) ), - message - ) ); - } else { - $message.append( message ); - } - this.$saveDialog.find( '.ve-init-mw-viewPageTarget-saveDialog-messages' ) - .append( $message ); - - this.messages[name] = $message; - } -}; - -/** - * Remove a message from the save dialog. - * @param {string} name Message's unique name - */ -ve.init.mw.ViewPageTarget.prototype.clearMessage = function ( name ) { - if ( this.messages[name] ) { - this.messages[name].remove(); - delete this.messages[name]; - } -}; - -/** - * Remove all messages from the save dialog. - */ -ve.init.mw.ViewPageTarget.prototype.clearAllMessages = function () { - this.$saveDialog - .find( '.ve-init-mw-viewPageTarget-saveDialog-messages' ) - .empty(); - this.messages = {}; -}; - /** * Add onbeforunload handler. * diff --git a/modules/ve-mw/ui/dialogs/ve.ui.MWSaveDialog.js b/modules/ve-mw/ui/dialogs/ve.ui.MWSaveDialog.js new file mode 100644 index 0000000000..07bb726b00 --- /dev/null +++ b/modules/ve-mw/ui/dialogs/ve.ui.MWSaveDialog.js @@ -0,0 +1,377 @@ +/*! + * VisualEditor UserInterface MWSaveDialog class. + * + * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/*global mw */ + +/** + * Dialog for saving MediaWiki articles. + * + * @class + * @extends ve.ui.MWDialog + * + * @constructor + * @param {ve.ui.Surface} surface + * @param {Object} [config] Config options + */ +ve.ui.MWSaveDialog = function VeUiMWSaveDialog( surface, config ) { + // Configuration initialization + config = ve.extendObject( { 'small': true }, config ); + + // Parent constructor + ve.ui.MWDialog.call( this, surface, config ); + + // Properties + this.sanityCheckVerified = false; + this.editSummaryByteLimit = 255; + this.restoring = false; + this.messages = {}; +}; + +/* Inheritance */ + +ve.inheritClass( ve.ui.MWSaveDialog, ve.ui.MWDialog ); + +/* Static Properties */ + +ve.ui.MWSaveDialog.static.name = 'mwSave'; + +ve.ui.MWSaveDialog.static.titleMessage = 'visualeditor-savedialog-title-save'; + +/* Methods */ + +/** + * @inheritdoc + */ +ve.ui.MWSaveDialog.prototype.initialize = function () { + var saveDialog = this; + // Parent method + ve.ui.MWDialog.prototype.initialize.call( this ); + + // Properties + this.savePanel = new ve.ui.PanelLayout( { '$$': this.frame.$$, 'scrollable': true } ); + + // Save panel + this.$editSummaryLabel = this.frame.$$( '

' ).addClass( 've-ui-mwSaveDialog-summaryLabel' ) + .html( ve.init.platform.getParsedMessage( 'summary' ) ); + this.editSummaryInput = new ve.ui.TextInputWidget( + { '$$': this.frame.$$, 'multiline': true, 'placeholder': ve.msg( 'visualeditor-editsummary' ) } + ); + this.editSummaryInput.$.addClass( 've-ui-mwSaveDialog-summary' ); + this.editSummaryInput.$input + .placeholder() + .byteLimit( this.editSummaryByteLimit ) + .prop( 'tabIndex', 0 ); + this.editSummaryInput.on( 'change', ve.bind( function () { + var $textarea = this.editSummaryInput.$input, + $editSummaryCount = this.savePanel.$.find( '.ve-ui-mwSaveDialog-editSummary-count' ); + // TODO: This looks a bit weird, there is no unit in the UI, just numbers + // Users likely assume characters but then it seems to count down quicker + // than expected. Facing users with the word "byte" is bad? (bug 40035) + setTimeout( function () { + $editSummaryCount.text( + saveDialog.editSummaryByteLimit - $.byteLength( $textarea.val() ) + ); + } ); + }, this ) ); + + this.$saveOptions = this.frame.$$( '
' ).addClass( 've-ui-mwSaveDialog-options' ).append( + this.frame.$$( '
' ).addClass( 've-ui-mwSaveDialog-checkboxes' ), + new ve.ui.InputLabelWidget( { '$$': this.frame.$$, 'label': 'text' } ).$ + .addClass( 've-ui-mwSaveDialog-editSummary-count' ).text( this.editSummaryByteLimit ) + ); + this.$saveMessages = this.frame.$$( '
' ); + this.$saveActions = this.frame.$$( '
' ).append( + this.frame.$$( '
' ).addClass( 've-ui-mwSaveDialog-dirtymsg' ) + ); + this.$saveFoot = this.frame.$$( '
' ).addClass( 've-ui-mwSaveDialog-foot' ).append( + this.frame.$$( '

' ).addClass( 've-ui-mwSaveDialog-license' ) + .html( ve.init.platform.getParsedMessage( 'copyrightwarning' ) ) + ); + this.savePanel.$.append( + this.$editSummaryLabel, + this.editSummaryInput.$, + this.$saveOptions, + this.$saveMessages, + this.$saveActions, + this.$saveFoot + ); + + // Review panel + this.reviewPanel = new ve.ui.PanelLayout( { '$$': this.frame.$$, 'scrollable': true } ); + this.$reviewViewer = this.frame.$$( '

' ).addClass( 've-ui-mwSaveDialog-viewer' ); + this.$reviewActions = this.frame.$$( '
' ).addClass( 've-ui-mwSaveDialog-actions' ); + this.reviewPanel.$.append( this.$reviewViewer, this.$reviewActions ); + + // Conflict panel + this.conflictPanel = new ve.ui.PanelLayout( { '$$': this.frame.$$, 'scrollable': true } ); + this.$conflict = this.frame.$$( '
' ).addClass( 've-ui-mwSaveDialog-conflict' ) + .html( ve.init.platform.getParsedMessage( 'visualeditor-editconflict' ) ); + this.conflictPanel.$.append( this.$conflict ); + + // No changes panel + this.nochangesPanel = new ve.ui.PanelLayout( { '$$': this.frame.$$, 'scrollable': true } ); + this.$noChanges = this.frame.$$( '
' ).addClass( 've-ui-mwSaveDialog-nochanges' ) + .html( ve.init.platform.getParsedMessage( 'visualeditor-diff-nochanges' ) ); + this.nochangesPanel.$.append( this.$noChanges ); + + // Panel stack + this.panel = new ve.ui.StackPanelLayout( { '$$': this.frame.$$, 'scrollable': true } ); + this.panel.$.addClass( 've-ui-mwSaveDialog-panel' ); + this.panel.addItems( [this.savePanel, this.reviewPanel, this.conflictPanel, this.nochangesPanel], 0 ); + + /* Buttons */ + + // Save button for "save" panel + this.saveButton = new ve.ui.PushButtonWidget( { + 'label': ve.msg( + // visualeditor-savedialog-label-restore, visualeditor-savedialog-label-save + 'visualeditor-savedialog-label-' + ( this.restoring ? 'restore' : 'save' ) + ), + 'flags': ['constructive'] + } ); + this.saveButton.connect( this, { 'click': 'onSaveButtonClick' } ); + + // Review button for "save" panel + this.reviewButton = new ve.ui.PushButtonWidget( { + 'label': ve.msg( 'visualeditor-savedialog-label-review' ) + } ); + this.reviewButton.connect( this, { 'click': 'onReviewButtonClick' } ); + + // Review good button on "review" panel + this.reviewGoodButton = new ve.ui.PushButtonWidget( { + 'label': ve.msg( 'visualeditor-savedialog-label-review-good' ), + 'flags': ['constructive'] + } ); + this.reviewGoodButton.connect( this, { 'click': 'onReviewGoodButtonClick' } ); + // Resolve conflict + this.resolveConflictButton = new ve.ui.PushButtonWidget( { + 'label': ve.msg( 'visualeditor-savedialog-label-resolve-conflict' ), + 'flags': ['constructive'] + } ); + this.resolveConflictButton.connect( this, { 'click': 'onResolveConflictButtonClick' } ); + + this.$loadingIcon = this.frame.$$( '
' ).addClass( 've-ui-mwSaveDialog-working' ); + + // Initialization + this.$body.append( this.panel.$ ); + this.$foot.append( + this.reviewButton.$, + this.saveButton.$, + this.reviewGoodButton.$, + this.resolveConflictButton.$, + this.$loadingIcon + ); +}; + +ve.ui.MWSaveDialog.prototype.onSaveButtonClick = function () { + this.emit( 'save' ); +}; + +ve.ui.MWSaveDialog.prototype.onReviewButtonClick = function () { + this.emit( 'review' ); +}; + +ve.ui.MWSaveDialog.prototype.onReviewGoodButtonClick = function () { + this.swapPanel( 'save' ); +}; + +ve.ui.MWSaveDialog.prototype.onResolveConflictButtonClick = function () { + this.emit( 'resolve' ); +}; + +/** + * Swap state in the save dialog. + * + * @param {string} panel One of 'save', 'review', 'conflict' or 'nochanges' + * @returns {jQuery} The now active panel + * @throws {Error} Unknown saveDialog panel + */ +ve.ui.MWSaveDialog.prototype.swapPanel = function ( panel ) { + var dialog = this, + panelObj = dialog[panel + 'Panel']; + + if ( ve.indexOf( panel, [ 'save', 'review', 'conflict', 'nochanges' ] ) === -1 ) { + throw new Error( 'Unknown saveDialog panel: ' + panel ); + } + + // Update the window title + this.setTitle( ve.msg( 'visualeditor-savedialog-title-' + panel ) ); + + // Old messages should not persist after panel changes + this.clearAllMessages(); + + // Reset save button if we disabled it for e.g. unrecoverable spam error + this.saveButton.setDisabled( false ); + + switch( panel ) { + case 'save': + if ( !this.sanityCheckVerified ) { + this.showMessage( 'dirtywarning', mw.msg( 'visualeditor-savedialog-warning-dirty' ) ); + } else { + this.saveButton.$.show(); + this.reviewButton.$.show(); + this.reviewGoodButton.$.hide(); + this.resolveConflictButton.$.hide(); + setTimeout( function () { + // fix input reference + var $textarea = dialog.editSummaryInput.$input; + $textarea.focus(); + // If message has be pre-filled (e.g. section edit), move cursor to end + if ( $textarea.val() !== '' ) { + ve.selectEnd( $textarea[0] ); + } + } ); + } + break; + case 'conflict': + this.saveButton.$.hide(); + this.reviewButton.$.hide(); + this.reviewGoodButton.$.hide(); + this.resolveConflictButton.$.show(); + break; + case 'review': + // Make room for the diff by transitioning to a non-small window + this.$frame.removeClass( 've-ui-window-frame-small' ); + /* falls through */ + case 'nochanges': + this.saveButton.$.hide(); + this.reviewButton.$.hide(); + this.reviewGoodButton.$.show(); + this.resolveConflictButton.$.hide(); + break; + } + + if ( panel !== 'review' ) { + // Restore original "small" size + this.$frame.addClass( 've-ui-window-frame-small' ); + } + + // Show the target panel + this.panel.showItem( panelObj ); + + mw.hook( 've.saveDialog.stateChanged' ).fire(); + + return dialog; +}; + +/** + * Show a message in the save dialog. + * + * @param {string} name Message's unique name + * @param {string|jQuery|Array} message Message content (string of HTML, jQuery object or array of + * Node objects) + * @param {Object} [options] + * @param {boolean} [options.wrap="warning"] Whether to wrap the message in a paragraph and if + * so, how. One of "warning", "error" or false. + */ +ve.ui.MWSaveDialog.prototype.showMessage = function ( name, message, options ) { + var $message; + if ( !this.messages[name] ) { + options = options || {}; + if ( options.wrap === undefined ) { + options.wrap = 'warning'; + } + $message = $( '
' ); + if ( options.wrap !== false ) { + $message.append( $( '

').append( + // visualeditor-savedialog-label-error + // visualeditor-savedialog-label-warning + $( '' ).text( mw.msg( 'visualeditor-savedialog-label-' + options.wrap ) ), + document.createTextNode( mw.msg( 'colon-separator' ) ), + message + ) ); + } else { + $message.append( message ); + } + this.$saveMessages.append( $message ); + + this.messages[name] = $message; + } +}; + +/** + * Remove a message from the save dialog. + * @param {string} name Message's unique name + */ +ve.ui.MWSaveDialog.prototype.clearMessage = function ( name ) { + if ( this.messages[name] ) { + this.messages[name].remove(); + delete this.messages[name]; + } +}; + +/** + * Remove all messages from the save dialog. + */ +ve.ui.MWSaveDialog.prototype.clearAllMessages = function () { + this.$saveMessages.empty(); + this.messages = {}; +}; + +/** + * Reset the fields of the save dialog. + * + * @method + */ +ve.ui.MWSaveDialog.prototype.reset = function () { + // Reset summary input + this.editSummaryInput.$input.val( '' ); + // Uncheck minoredit + this.$saveOptions.find( '.ve-ui-mwSaveDialog-checkboxes' ) + .find( '#wpMinoredit' ).prop( 'checked', false ); + // Clear the diff + this.$reviewViewer.empty(); +}; + +/** + * Initialize MediaWiki page specific checkboxes + * + * @param {string} checkboxes Multiline HTML + */ +ve.ui.MWSaveDialog.prototype.setupCheckboxes = function ( checkboxes ) { + this.$saveOptions.find( '.ve-ui-mwSaveDialog-checkboxes' ) + .html( checkboxes ) + .find( 'a' ) + .attr( 'target', '_blank' ) + .end() + .find( '#wpMinoredit' ) + .prop( 'checked', mw.user.options.get( 'minordefault' ) ) + .prop( 'tabIndex', 0 ) + .end() + .find( '#wpWatchthis' ) + .prop( 'checked', + mw.user.options.get( 'watchdefault' ) || + ( mw.user.options.get( 'watchcreations' ) && !this.pageExists ) || + mw.config.get( 'wgVisualEditor' ).isPageWatched + ).prop( 'tabIndex', 0 ); + // TODO: Need to set all checkboxes provided by api tabindex to 0 for proper accessibility +}; + +/** + * Set review content and show review panel + * + * @param {string} content Diff HTML or wikitext + */ +ve.ui.MWSaveDialog.prototype.setDiffAndReview = function ( content ) { + this.$reviewViewer.empty().append( content ); + this.reviewGoodButton.setDisabled( false ); + this.$loadingIcon.hide(); + this.swapPanel( 'review' ); +}; + +/** + * Set sanity check flag + * + * @param {boolean} verified Status of sanity check + */ +ve.ui.MWSaveDialog.prototype.setSanityCheck = function ( verified ) { + this.sanityCheckVerified = !!verified; +}; + +/* Registration */ + +ve.ui.dialogFactory.register( ve.ui.MWSaveDialog ); diff --git a/modules/ve-mw/ui/styles/ve.ui.MWDialog.css b/modules/ve-mw/ui/styles/ve.ui.MWDialog.css index 7157ca66d0..1222dd6fe3 100644 --- a/modules/ve-mw/ui/styles/ve.ui.MWDialog.css +++ b/modules/ve-mw/ui/styles/ve.ui.MWDialog.css @@ -88,3 +88,95 @@ left: 0; right: 0; } + +/* ve.ui.MWSaveDialog */ + +.ve-ui-mwSaveDialog-panel { + margin: 1.25em; +} + +.ve-ui-mwSaveDialog-summaryLabel { + padding: 0.25em 0; +} + +.ve-ui-mwSaveDialog-summary { + width: 100%; + background-color: #fff; + border: solid 1px #ccc; + padding: 0.5em; + border-radius: 0.25em 0.25em 0 0; +} + +.ve-ui-mwSaveDialog-summary textarea { + margin: 0; + padding: 0; + resize: none; + height: 80px; + border: none; + box-shadow: none; + background-color: transparent; +} + +.ve-ui-mwSaveDialog-summary textarea:focus, +.ve-ui-mwSaveDialog-summary textarea:active { + box-shadow: none; +} + +.ve-ui-mwSaveDialog-foot { + margin: 0.5em; +} + +.ve-ui-mwSaveDialog-options { + position: relative; + background-color: #f7f7f7; + border: solid 1px #ccc; + border-top: none; + border-radius: 0 0 0.25em 0.25em; + min-height: 3em; +} + +.ve-ui-mwSaveDialog-checkboxes { + margin-right: 3.25em; /* Hack to prevent overlap on edit summary count */ + line-height: 3em; + padding: 0 0.75em; +} + +.ve-ui-mwSaveDialog-checkboxes label { + padding-right: 0.75em; + vertical-align: middle; +} + +.ve-ui-mwSaveDialog-checkboxes input { + vertical-align: middle; +} + +.ve-ui-mwSaveDialog-editSummary-count { + position: absolute; + right: 0; + top: 0; + bottom: 0; + border-left: solid 1px #eee; + line-height: 3em; + padding: 0 1em; + color: #aaa; +} + +.ve-ui-mwSaveDialog-working { + display: none; + float: right; + height: 2.5em; + width: 128px; + margin-right: 1em; + background-position: right center; + background-repeat: no-repeat; +} + +.ve-ui-mwSaveDialog-license, +.ve-ui-mwSaveDialog-dirtymsg, +.ve-ui-mwSaveDialog-report-notice { + font-size: 0.85em; + line-height: 1.25em; + padding: 0; + margin: 0; + color: #999; +} diff --git a/modules/ve/ui/styles/ve.ui.Dialog.css b/modules/ve/ui/styles/ve.ui.Dialog.css index 9c463cd46b..d2101b30b4 100644 --- a/modules/ve/ui/styles/ve.ui.Dialog.css +++ b/modules/ve/ui/styles/ve.ui.Dialog.css @@ -17,7 +17,6 @@ background-color: rgba(255,255,255,0.5); -webkit-animation: ve-ui-fade-in 250ms ease-in-out 0 1 normal; -moz-animation: ve-ui-fade-in 250ms ease-in-out 0 1 normal; - -ms-animation: ve-ui-fade-in 250ms ease-in-out 0 1 normal; -o-animation: ve-ui-fade-in 250ms ease-in-out 0 1 normal; animation: ve-ui-fade-in 250ms ease-in-out 0 1 normal; } @@ -25,7 +24,6 @@ .ve-ui-dialog-closing { -webkit-animation: ve-ui-fade-in 250ms ease-in-out 0 1 reverse; -moz-animation: ve-ui-fade-in 250ms ease-in-out 0 1 reverse; - -ms-animation: ve-ui-fade-in 250ms ease-in-out 0 1 reverse; -o-animation: ve-ui-fade-in 250ms ease-in-out 0 1 reverse; animation: ve-ui-fade-in 250ms ease-in-out 0 1 reverse; } @@ -45,22 +43,24 @@ border-radius: 0.5em; box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3); overflow: hidden; + -webkit-transition: all 250ms ease-in-out; + -moz-transition: all 250ms ease-in-out; + -o-transition: all 250ms ease-in-out; + transition: all 250ms ease-in-out; -webkit-animation: ve-ui-zoom-in 250ms ease-in-out 0 1 normal; -moz-animation: ve-ui-zoom-in 250ms ease-in-out 0 1 normal; - -ms-animation: ve-ui-zoom-in 250ms ease-in-out 0 1 normal; -o-animation: ve-ui-zoom-in 250ms ease-in-out 0 1 normal; animation: ve-ui-zoom-in 250ms ease-in-out 0 1 normal; } -.ve-ui-dialog .ve-ui-window-frame.ve-ui-window-frame-small { - max-width: 600px; - max-height: 300px; +.ve-ui-dialog .ve-ui-window-frame-small { + width: 600px; + max-height: 375px; } .ve-ui-dialog-closing .ve-ui-window-frame { -webkit-animation: ve-ui-zoom-in 250ms ease-in-out 0 1 reverse; -moz-animation: ve-ui-zoom-in 250ms ease-in-out 0 1 reverse; - -ms-animation: ve-ui-zoom-in 250ms ease-in-out 0 1 reverse; -o-animation: ve-ui-zoom-in 250ms ease-in-out 0 1 reverse; animation: ve-ui-zoom-in 250ms ease-in-out 0 1 reverse; } @@ -185,3 +185,7 @@ right: 0; box-shadow: 0 0 0.25em rgba(0,0,0,0.25); } + +.ve-ui-window-body .ve-ce-documentNode { + padding: 1.875em; /* 1.5/0.8 */ +} diff --git a/modules/ve/ui/ve.ui.Window.js b/modules/ve/ui/ve.ui.Window.js index 4bccb6b10b..df3f8461a3 100644 --- a/modules/ve/ui/ve.ui.Window.js +++ b/modules/ve/ui/ve.ui.Window.js @@ -115,7 +115,7 @@ ve.ui.Window.prototype.initialize = function () { // Properties this.$title = this.$$( '

' ); if ( this.getTitle() ) { - this.$title.text( this.getTitle() ); + this.setTitle(); } this.$icon = this.$$( '
' ) .addClass( 've-ui-icon-' + this.constructor.static.icon ); @@ -214,8 +214,10 @@ ve.ui.Window.prototype.getTitle = function () { }; /** + * Set the size of window frame. * - * @method + * @param {number} [width=auto] Custom width + * @param {number} [height=auto] Custom height */ ve.ui.Window.prototype.setSize = function ( width, height ) { if ( !this.frame.$content ) { @@ -229,8 +231,19 @@ ve.ui.Window.prototype.setSize = function ( width, height ) { }; /** + * Set the title of the window. * - * @method + * @param {string} [customTitle] Custom title, override the static.titleMessage + */ +ve.ui.Window.prototype.setTitle = function ( customTitle ) { + this.$title.text( customTitle || this.getTitle() ); +}; + +/** + * Set the height of window to fit with contents. + * + * @param {number} [min=0] Min height + * @param {number} [max] Max height (defaults to content's outer height) */ ve.ui.Window.prototype.fitHeightToContents = function ( min, max ) { var height = this.frame.$content.outerHeight(); @@ -241,7 +254,10 @@ ve.ui.Window.prototype.fitHeightToContents = function ( min, max ) { }; /** + * Set the width of window to fit with contents. * + * @param {number} [min=0] Min height + * @param {number} [max] Max height (defaults to content's outer width) */ ve.ui.Window.prototype.fitWidthToContents = function ( min, max ) { var width = this.frame.$content.outerWidth(); @@ -252,8 +268,10 @@ ve.ui.Window.prototype.fitWidthToContents = function ( min, max ) { }; /** + * Set the position of window to fit with contents.. * - * @method + * @param {string} left Left offset + * @param {string} top Top offset */ ve.ui.Window.prototype.setPosition = function ( left, top ) { this.$.css( { 'left': left, 'top': top } ); diff --git a/modules/ve/ui/ve.ui.WindowSet.js b/modules/ve/ui/ve.ui.WindowSet.js index 9bf2ba5c09..a09e5a4288 100644 --- a/modules/ve/ui/ve.ui.WindowSet.js +++ b/modules/ve/ui/ve.ui.WindowSet.js @@ -105,16 +105,13 @@ ve.ui.WindowSet.prototype.getCurrent = function () { }; /** - * Opens a given window. + * Return a given window. * - * Any already open dialog will be closed. - * - * @method * @param {string} name Symbolic name of window * @param {Object} [config] Configuration options to be sent to the window class constructor - * @chainable + * @return {ve.ui.Window} Window with specified name */ -ve.ui.WindowSet.prototype.open = function ( name, config ) { +ve.ui.WindowSet.prototype.getWindow = function ( name, config ) { var win; if ( !this.factory.lookup( name ) ) { @@ -133,8 +130,19 @@ ve.ui.WindowSet.prototype.open = function ( name, config ) { this.$.append( win.$ ); win.getFrame().load(); } + return this.windows[name]; +}; - this.windows[name].open(); - +/** + * Opens a given window. + * + * Any already open dialog will be closed. + * + * @param {string} name Symbolic name of window + * @param {Object} [config] Config options to be sent to the window class constructor + * @chainable + */ +ve.ui.WindowSet.prototype.open = function ( name, config ) { + this.getWindow( name, config ).open(); return this; }; diff --git a/modules/ve/ui/widgets/ve.ui.ButtonWidget.js b/modules/ve/ui/widgets/ve.ui.ButtonWidget.js index 751943d7ae..192a511b2b 100644 --- a/modules/ve/ui/widgets/ve.ui.ButtonWidget.js +++ b/modules/ve/ui/widgets/ve.ui.ButtonWidget.js @@ -51,11 +51,11 @@ ve.ui.ButtonWidget = function VeUiButtonWidget( config ) { .append( this.$label ) .attr( { 'role': 'button', - 'tabIndex': config.tabIndex || 0, 'title': config.title, 'href': config.href, 'target': config.target - } ); + } ) + .prop( 'tabIndex', config.tabIndex || 0 ); this.$ .addClass( 've-ui-buttonWidget' ) .append( this.$button );