';
/* Methods */
/**
* Switch to edit mode.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.activate = function () {
if ( !this.active && !this.activating ) {
this.activating = true;
// User interface changes
this.transformSkinTabs();
this.hideSiteNotice();
this.showSpinner();
this.hideTableOfContents();
this.mutePageContent();
this.mutePageTitle();
this.saveScrollPosition();
this.load();
}
};
/**
* Switch to view mode.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.deactivate = function ( override ) {
if ( override || ( this.active && !this.deactivating ) ) {
if (
override ||
!this.edited ||
confirm( ve.msg( 'visualeditor-viewpage-savewarning' ) )
) {
this.deactivating = true;
// User interface changes
this.restoreSkinTabs();
this.restoreSiteNotice();
this.hideSpinner();
if ( this.toolbarCancelButton ) {
// If deactivate is called before a successful load, then
// setupToolbarButtons has not been called yet and as such tearDownToolbarButtons
// would throw an error when trying call methods on the button property (bug 46456)
this.tearDownToolbarButtons();
this.detachToolbarButtons();
}
this.resetSaveDialog();
this.hideSaveDialog();
this.detachSaveDialog();
this.tearDownSurface();
this.showTableOfContents();
this.deactivating = false;
}
}
};
/**
* Handle successful DOM load event.
*
* @method
* @param {HTMLDocument} doc Parsed DOM from server
*/
ve.init.mw.ViewPageTarget.prototype.onLoad = function ( doc ) {
if ( this.activating ) {
this.edited = false;
this.doc = doc;
this.setUpSurface( doc );
this.setupToolbarEditNotices();
this.setupToolbarButtons();
this.setupSaveDialog();
this.attachToolbarButtons();
this.attachSaveDialog();
this.restoreScrollPosition();
this.restoreEditSection();
this.setupBeforeUnloadHandler();
this.$document.focus();
this.activating = false;
}
};
/**
* Handle failed DOM load event.
*
* @method
* @param {Object} data HTTP Response object
* @param {string} status Text status message
* @param {Mixed} error Thrown exception or HTTP error string
*/
ve.init.mw.ViewPageTarget.prototype.onLoadError = function ( response, status ) {
if ( confirm( ve.msg( 'visualeditor-loadwarning', status ) ) ) {
this.load();
} else {
this.activating = false;
// User interface changes
this.deactivate( true );
}
};
/**
* Handle successful DOM save event.
*
* @method
* @param {HTMLElement} html Rendered HTML from server
*/
ve.init.mw.ViewPageTarget.prototype.onSave = function ( html ) {
if ( !this.pageExists || this.restoring ) {
// This is a page creation or restoration, refresh the page
this.tearDownBeforeUnloadHandler();
window.location.href = this.viewUri.extend( {
'venotify': this.restoring ? 'restored' : 'created'
} );
} 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 ) {
var watchChecked = this.$saveDialog
.find( '#ve-init-mw-viewPageTarget-saveDialog-watchList')
.prop( 'checked' );
mw.page.watch.updateWatchLink(
$( '#ca-watch a, #ca-unwatch a' ),
watchChecked ? 'unwatch': 'watch'
);
}
this.hideSaveDialog();
this.resetSaveDialog();
this.replacePageContent( html );
this.tearDownBeforeUnloadHandler();
this.deactivate( true );
mw.util.jsMessage(
ve.msg( 'visualeditor-notification-saved',
new mw.Title( this.pageName ).toText()
)
);
}
};
/**
* Handle failed DOM save event.
*
* @method
* @param {Object} jqXHR
* @param {string} status Text status message
* @param {Mixed} error Thrown exception or HTTP error string
*/
ve.init.mw.ViewPageTarget.prototype.onSaveError = function ( jqXHR, status ) {
// TODO: Don't use alert.
alert( ve.msg( 'visualeditor-saveerror', status ) );
this.saveDialogSaveButton.setDisabled( false );
this.$saveDialogLoadingIcon.hide();
};
/**
* Handle Show changes event.
*
* @method
* @param {string} diffHtml
*/
ve.init.mw.ViewPageTarget.prototype.onShowChanges = function ( diffHtml ) {
// Invalidate the diff on next change
this.surface.getModel().on( 'transact', this.proxiedOnSurfaceModelTransact );
// Store the diff for reporting purposes
this.diffHtml = diffHtml;
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.saveDialogReviewWrongButton.setDisabled( false );
}, this ), ve.bind( function () {
this.onSaveError( null, 'Module load failed' );
}, this ) );
};
/**
* Handle failed show changes event.
*
* @method
* @param {Object} jqXHR
* @param {string} status Text status message
* @param {Mixed} error Thrown exception or HTTP error string
*/
ve.init.mw.ViewPageTarget.prototype.onShowChangesError = function ( jqXHR, status ) {
alert( ve.msg( 'visualeditor-differror', status ) );
this.$saveDialogLoadingIcon.hide();
this.saveDialogReviewWrongButton.setDisabled( false );
};
/**
* Handle edit conflict event.
*
* TODO: Don't use an operating system confirm box. Parsing may take quite some time and we should
* be showing the user some sort of progress indicator. Ideally this would be integrated into the
* save dialog.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onEditConflict = function () {
if ( confirm( ve.msg( 'visualeditor-editconflict', status ) ) ) {
// Get Wikitext from the DOM, and setup a submit call when it's done
this.serialize(
ve.dm.converter.getDomFromData( this.surface.getDocumentModel().getFullData() ),
ve.bind( function ( wikitext ) {
this.submit( wikitext, this.getSaveOptions() );
}, this )
);
} else {
// Return to editing
this.hideSaveDialog();
this.resetSaveDialog();
}
};
/**
* Handle clicks on the edit tab.
*
* @method
* @param {jQuery.Event} e Mouse click event
*/
ve.init.mw.ViewPageTarget.prototype.onEditTabClick = function ( e ) {
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 ) {
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 ( 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 () {
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 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.$document.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' );
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 setup.
*
* @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().removeListener( 'transact', this.proxiedOnSurfaceModelTransact );
};
/**
* Handle history events in the surface model.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSurfaceModelHistory = function () {
this.edited = this.surface.getModel().hasPastState();
// Disable the save button if we have no history
this.toolbarSaveButton.setDisabled( !this.edited );
};
/**
* Handle clicks on the save button in the save dialog.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogSaveButtonClick = function () {
this.saveDialogSaveButton.setDisabled( true );
this.$saveDialogLoadingIcon.show();
this.save(
ve.dm.converter.getDomFromData( this.surface.getDocumentModel().getFullData() ),
this.getSaveOptions()
);
};
/**
* Handle clicks on the review "Wrong" button in the save dialog.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogReviewWrongButtonClick = function () {
this.swapSaveDialog( 'report' );
};
/**
* 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 report a problem button in the save dialog.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogReportButtonClick = function () {
this.reportProblem( $( '#ve-init-mw-viewPageTarget-saveDialog-problem' ).val() );
this.resetSaveDialog();
this.hideSaveDialog();
mw.notify( ve.msg( 'visualeditor-notification-reported' ) );
};
/**
* 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' )
};
};
/**
* 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 () {
// Hard code "review" for now.
// Although we have more than 2 slides, slide "report" and "save" are both
// branches of "review". Later this could terminate the path based on an
// array or object, or keep a .history array even...
this.swapSaveDialog( 'review' );
};
/**
* Get a list of edit notices.
*
* @method
* @returns {string[]} HTML strings for each edit notice
*/
ve.init.mw.ViewPageTarget.prototype.setupToolbarEditNotices = function () {
var key;
this.$toolbarEditNotices.empty();
for ( key in this.editNotices ) {
this.$toolbarEditNotices.append( this.editNotices[key] );
}
};
/**
* Switch to editing mode.
*
* @method
* @param {HTMLDocument} doc HTML DOM to edit
*/
ve.init.mw.ViewPageTarget.prototype.setUpSurface = function ( doc ) {
var $contentText = $( '#mw-content-text' );
// Store the HTML for reporting purposes
this.originalHtml = doc.body.innerHTML; // TODO store entire document in the future
// Initialize surface
this.surface = new ve.Surface( this, doc, this.surfaceOptions );
this.surface.getContext().hide();
this.$document = this.surface.$.find( '.ve-ce-documentNode' );
this.surface.getModel().on( 'transact', this.proxiedOnSurfaceModelTransact );
this.surface.getModel().on( 'history', this.proxiedOnSurfaceModelHistory );
// Transplant the toolbar
this.attachToolbar();
this.transformPageTitle();
// Update UI
this.hidePageContent();
this.hideSpinner();
this.active = true;
this.$document.attr( {
'lang': $contentText.attr( 'lang' ),
'dir': $contentText.attr( 'dir' )
} );
};
/**
* Switch to viewing mode.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.tearDownSurface = function () {
// Reset tabs
this.restoreSkinTabs();
// Update UI
if ( this.$document ) {
this.$document.blur();
this.$document = null;
}
this.detachToolbar();
this.hideSpinner();
this.showPageContent();
this.restorePageTitle();
this.showTableOfContents();
// Destroy editor
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 action, pTabsId, $caSource, $caEdit, caVeEdit, caVeEditNextnode, uriClone;
$caEdit = $( '#ca-edit' );
$caSource = $( '#ca-viewsource' );
caVeEditNextnode = $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;
}
action = this.pageExists ? 'edit' : 'create';
pTabsId = $( '#p-views' ).length ? 'p-views' : 'p-cactions';
// Add independent ve-edit tab.
if ( this.tabLayout === 'add' ) {
// Create "Edit" tab.
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 with ve version, add editsource link.
} else {
// Create "Edit source" link.
// Re-create instead of convert ca-edit since we don't want to copy over accesskey etc.
mw.util.addPortletLink(
'p-cactions',
// Use original href to preserve oldid etc. (bug 38125)
$caEdit.find( 'a' ).attr( 'href' ),
ve.msg( 'visualeditor-ca-editsource' ),
'ca-editsource'
);
$caEdit.remove();
// Create "Edit" tab.
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: 'edit' or 'create'
ve.msg( action ),
'ca-edit',
ve.msg( 'tooltip-ca-edit' ),
ve.msg( 'accesskey-ca-edit' ),
caVeEditNextnode
);
}
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 ) );
}
// If there got here via veaction=edit, hide it from the URL.
if ( this.currentUri.query.veaction === 'edit' && window.history.replaceState ) {
// Remove the veaction query parameter, but don't affect the original mw.Uri instance
uriClone = this.currentUri.clone();
delete uriClone.query.veaction;
// If there are other query parameters, set the url to the current one
// (with veaction removed). Otherwise use the canonical style view url (bug 42553).
if ( ve.getObjectValues( uriClone.query ).length ) {
window.history.replaceState( null, document.title, uriClone );
} else {
window.history.replaceState( null, document.title, this.viewUri );
}
}
};
/**
* Modify page content to make section edit links activate the editor.
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.setupSectionEditLinks = function () {
var veEditUri = this.veEditUri,
$links = $( '#mw-content-text .editsection a' );
if ( this.isViewPage ) {
$links.click( ve.bind( this.onEditSectionLinkClick, this ) );
} else {
$links.each( function () {
var veSectionEditUri = new mw.Uri( veEditUri.toString() ),
sectionEditUri = new mw.Uri( $(this).attr( 'href' ) );
veSectionEditUri.extend( { 'vesection': sectionEditUri.query.section } );
$(this).attr( 'href', veSectionEditUri );
} );
}
};
/**
* 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( 'cancel' ) } );
this.toolbarSaveButton = new ve.ui.ButtonWidget( {
'label': ve.msg( 'visualeditor-toolbar-savedialog' ),
'flags': ['constructive'],
'disabled': !this.restoring
} );
this.toolbarCancelButton.on( 'click', ve.bind( this.onToolbarCancelButtonClick, this ) );
this.toolbarSaveButton.on( 'click', ve.bind( this.onToolbarSaveButtonClick, 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.$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.removeAllListeners( 'click' );
this.toolbarSaveButton.removeAllListeners( 'click' );
this.$toolbarEditNoticesTool.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 = $( '.ve-ui-toolbar .ve-ui-actions' );
$target.append( this.$toolbarFeedbackTool );
if ( !ve.isEmptyObject( this.editNotices ) ) {
$target.append( this.$toolbarEditNoticesTool );
}
$target.append( 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.$toolbarEditNoticesTool.detach();
this.$toolbarFeedbackTool.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 `