/*global mw, confirm, alert */ /** * VisualEditor MediaWiki initialization ViewPageTarget class. * * @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ /** * MediaWiki Edit page target. * * @class * @constructor * @extends {ve.init.mw.Target} */ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() { var currentUri = new mw.Uri( window.location.toString() ); // Parent constructor ve.init.mw.Target.call( this, mw.config.get( 'wgRelevantPageName' ) ); // Properties this.$document = null; this.$spinner = $( '
' ); this.$toolbarSaveButton = $( '
' ); this.$toolbarEditNotices = $( '
' ); this.$toolbarEditNoticesButton = $( '
' ); this.$saveDialog = $( '
' ); this.$saveDialogSaveButton = null; this.onBeforeUnloadFallback = null; this.proxiedOnBeforeUnload = null; this.surface = null; this.active = false; this.edited = false; this.activating = false; this.deactivating = false; this.scrollTop = null; this.proxiedOnSurfaceModelTransact = ve.bind( this.onSurfaceModelTransact, this ); this.surfaceOptions = { 'toolbars': { 'top': { 'float': !this.isMobileDevice } } }; this.currentUri = currentUri; this.section = currentUri.query.vesection || null; this.namespaceName = mw.config.get( 'wgCanonicalNamespace' ); this.viewUri = new mw.Uri( mw.util.wikiGetlink( this.pageName ) ); this.veEditUri = this.viewUri.clone().extend( { 'veaction': 'edit' } ); this.isViewPage = ( mw.config.get( 'wgAction' ) === 'view' && currentUri.query.diff === undefined ); this.canBeActivated = ( $.client.test( ve.init.mw.ViewPageTarget.compatibility ) || 'vewhitelist' in currentUri.query ); this.editSummaryByteLimit = 255; // Tab layout. // * add: Adds #ca-ve-edit. // * replace: Re-creates #ca-edit for VisualEditor and adds #ca-editsource. this.tabLayout = 'add'; // Events this.addListenerMethods( this, { 'load': 'onLoad', 'save': 'onSave', 'loadError': 'onLoadError', 'saveError': 'onSaveError', 'editConflict': 'onEditConflict' } ); // Initialization if ( this.canBeActivated ) { if ( currentUri.query.venotify ) { // The following messages can be used here: // visualeditor-notification-saved // visualeditor-notification-created mw.util.jsMessage( ve.msg( 'visualeditor-notification-' + currentUri.query.venotify, this.pageName ) ); if ( window.history.replaceState ) { delete currentUri.query.venotify; window.history.replaceState( null, document.title, currentUri ); } } this.setupSkinTabs(); this.setupSectionEditLinks(); if ( this.isViewPage ) { if ( currentUri.query.veaction === 'edit' ) { this.activate(); } } } }; /* Inheritance */ ve.inheritClass( ve.init.mw.ViewPageTarget, ve.init.mw.Target ); /* Static Members */ /** * Compatibility map used with jQuery.client to black-list incompatible browsers. * * @static * @member */ ve.init.mw.ViewPageTarget.compatibility = { // Left-to-right languages ltr: { msie: false, // FIXME: Bug 42335 (temporarily added IE to blacklist for December release) // msie: [['>=', 9]], firefox: [['>=', 11]], safari: [['>=', 5]], chrome: [['>=', 19]], opera: false, netscape: false, blackberry: false }, // Right-to-left languages rtl: { msie: false, // FIXME: Bug 42335 (temporarily added IE to blacklist for December release) // msie: [['>=', 9]], firefox: [['>=', 11]], safari: [['>=', 5]], chrome: [['>=', 19]], opera: false, netscape: false, blackberry: false } }; ve.init.mw.ViewPageTarget.saveDialogTemplate = '\
\
\
\
\ \ \
\
\ \ \ \ \ \
\ \
\
\
\
\

\
'; /* Methods */ /** * Switches 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(); } }; /** * Switches to view mode. * * @method */ ve.init.mw.ViewPageTarget.prototype.deactivate = function ( override ) { if ( this.active && !this.deactivating ) { if ( override || !this.surface.getModel().getHistory().length || confirm( ve.msg( 'visualeditor-viewpage-savewarning' ) ) ) { this.deactivating = true; // User interface changes this.restoreSkinTabs(); this.restoreSiteNotice(); this.hideSpinner(); this.tearDownToolbarButtons(); this.detachToolbarButtons(); this.detachSaveDialog(); this.tearDownSurface(); this.showTableOfContents(); this.deactivating = false; } } }; /** * Handles successful DOM load event. * * @method * @param {HTMLElement} dom Parsed DOM from server */ ve.init.mw.ViewPageTarget.prototype.onLoad = function ( dom ) { this.edited = false; this.setUpSurface( dom ); this.setupToolbarEditNotices(); this.setupToolbarButtons(); this.setupSaveDialog(); this.attachToolbarButtons(); this.attachSaveDialog(); this.restoreScrollPosition(); this.restoreEditSection(); this.setupBeforeUnloadHandler(); this.$document.focus(); this.activating = false; }; /** * Handles 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; this.restoreSkinTabs(); this.hideSpinner(); this.showTableOfContents(); this.showPageContent(); this.restorePageTitle(); } }; /** * Handles successful DOM save event. * * @method * @param {HTMLElement} html Rendered HTML from server */ ve.init.mw.ViewPageTarget.prototype.onSave = function ( html ) { if ( !this.pageExists || this.pageRevId ) { // This is a page creation, refresh the page this.tearDownBeforeUnloadHandler(); window.location.href = this.viewUri.extend( { 'venotify': this.pageExists ? 'saved' : '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', this.pageName ) ); } }; /** * Handles failed DOM save 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.onSaveError = function ( response, status ) { // TODO: Don't use alert. alert( ve.msg( 'visualeditor-saveerror', status ) ); }; /** * Handles 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(); } }; /** * Handles clicks on the edit tab. * * @method * @param {jQuery.Event} e */ ve.init.mw.ViewPageTarget.prototype.onEditTabClick = function ( e ) { this.activate(); // Prevent the edit tab's normal behavior e.preventDefault(); }; /** * Handles clicks on a section edit link. * * @method * @param {jQuery.Event} e */ 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(); }; /** * Handles clicks on the view tab. * * @method * @param {jQuery.Event} e */ ve.init.mw.ViewPageTarget.prototype.onViewTabClick = function ( e ) { if ( this.active ) { this.deactivate(); // Prevent the edit tab's normal behavior e.preventDefault(); } }; /** * Handles clicks on the save button in the toolbar. * * @method * @param {jQuery.Event} e */ ve.init.mw.ViewPageTarget.prototype.onToolbarSaveButtonClick = function () { if ( this.edited ) { this.showSaveDialog(); } }; /** * Handles clicks on the edit notices button in the toolbar. * * @method * @param {jQuery.Event} e */ ve.init.mw.ViewPageTarget.prototype.onToolbarEditNoticesClick = function () { this.$toolbarEditNotices.fadeToggle( 'fast' ); }; /** * Handles 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.Transaction} tx Processed transaction */ ve.init.mw.ViewPageTarget.prototype.onSurfaceModelTransact = function () { this.edited = true; this.enableToolbarSaveButton(); this.surface.getModel().removeListener( 'transact', this.proxiedOnSurfaceModelTransact ); }; /** * Handles clicks on the save button in the save dialog. * * @method * @param {jQuery.Event} e */ ve.init.mw.ViewPageTarget.prototype.onSaveDialogSaveButtonClick = function () { this.lockSaveDialogSaveButton(); this.$saveDialogLoadingIcon.show(); this.save( ve.dm.converter.getDomFromData( this.surface.getDocumentModel().getFullData() ), this.getSaveOptions(), ve.bind( this.onSave, this ) ); }; /** * Gets 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' ) }; }; /** * Handles clicks on the close button in the save dialog. * * @method * @param {jQuery.Event} e */ ve.init.mw.ViewPageTarget.prototype.onSaveDialogCloseButtonClick = function () { this.hideSaveDialog(); }; /** * Gets 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( $( '
' ) .addClass( 've-init-mw-viewPageTarget-toolbar-editNotices-notice' ) .attr( 'rel', key ).html( this.editNotices[key] ) ); } }; /** * Switches to editing mode. * * @method * @param {HTMLElement} dom HTML DOM to edit */ ve.init.mw.ViewPageTarget.prototype.setUpSurface = function ( dom ) { var $contentText = $( '#mw-content-text' ); // Initialize surface this.surface = new ve.Surface( $( '#content' ), dom, this.surfaceOptions ); this.$document = this.surface.$.find( '.ve-ce-documentNode' ); this.surface.getModel().on( 'transact', this.proxiedOnSurfaceModelTransact ); // Transplant the toolbar this.attachToolbar(); this.transformPageTitle(); // Update UI this.hidePageContent(); this.hideSpinner(); if ( !this.currentUri.query.oldid ) { this.disableToolbarSaveButton(); } this.active = true; this.$document.attr( { 'lang': $contentText.attr( 'lang' ), 'dir': $contentText.attr( 'dir' ) } ); }; /** * Switches to viewing mode. * * @method */ ve.init.mw.ViewPageTarget.prototype.tearDownSurface = function () { // Reset tabs this.restoreSkinTabs(); // Update UI this.$document.blur(); this.$document = null; this.surface.$.empty().detach(); $( '.ve-ui-context' ).remove(); this.detachToolbar(); this.hideSpinner(); this.showPageContent(); this.restorePageTitle(); this.showTableOfContents(); // Remove handler if it's still active this.surface.getModel().removeListener( 'transact', this.proxiedOnSurfaceModelTransact ); // Destroy editor this.surface = null; this.active = false; }; /** * Modifies 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 ); } } }; /** * Modifies 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 ); } ); } }; /** * Adds content and event bindings to toolbar buttons. * * @method */ ve.init.mw.ViewPageTarget.prototype.setupToolbarButtons = function () { var editNoticeCount = this.$toolbarEditNotices .find( '.ve-init-mw-viewPageTarget-toolbar-editNotices-notice' ).length; this.$toolbarSaveButton .append( $( '' ) .text( ve.msg( 'savearticle' ) ) ) .on( { 'mousedown': function ( e ) { $(this).addClass( 've-init-mw-viewPageTarget-toolbar-saveButton-down' ); e.preventDefault(); }, 'mouseleave mouseup': function ( e ) { $(this).removeClass( 've-init-mw-viewPageTarget-toolbar-saveButton-down' ); e.preventDefault(); }, 'click': ve.bind( this.onToolbarSaveButtonClick, this ) } ); if ( editNoticeCount ) { this.$toolbarEditNoticesButton .addClass( 've-ui-icon-alert' ) .append( $( '' ) .addClass( 've-init-mw-viewPageTarget-toolbar-editNoticesButton-label' ) .text( ve.msg( 'visualeditor-editnotices-button', editNoticeCount ) ) ) .append( this.$toolbarEditNotices ) .click( ve.bind( this.onToolbarEditNoticesClick, this ) ); this.$toolbarEditNotices.fadeIn( 'fast' ); } }; /** * Removes content and event bindings from toolbar buttons. * * @method */ ve.init.mw.ViewPageTarget.prototype.tearDownToolbarButtons = function () { this.$toolbarSaveButton.empty().off( 'click' ); this.$toolbarEditNoticesButton.empty().off( 'click' ); }; /** * Adds the save button to the user interface. * * @method */ ve.init.mw.ViewPageTarget.prototype.attachToolbarButtons = function () { $( '.ve-ui-toolbar .ve-ui-actions' ) .append( this.$toolbarEditNoticesButton ) .append( this.$toolbarSaveButton ); }; /** * Removes the save button from the user interface. * * @method */ ve.init.mw.ViewPageTarget.prototype.detachToolbarButtons = function () { this.$toolbarSaveButton.detach(); this.$toolbarEditNoticesButton.detach(); }; /** * Asynchronously provides the template for the save dialog wrapped in a * plain
jQuery collection. * * @method */ ve.init.mw.ViewPageTarget.prototype.getSaveDialogHtml = function ( callback ) { var viewPage = this, $wrap = $( '
' ).html( this.constructor.saveDialogTemplate ); mw.user.getRights( function ( rights ) { // MediaWiki only allows usage of minor flag when editing an existing page // and the user has the right to use the feature. // If either is not the case, remove it from the form. if ( !viewPage.pageExists || ve.indexOf( 'minoredit', rights ) === -1 ) { $wrap .find( '.ve-init-mw-viewPageTarget-saveDialog-minorEdit-label, #ve-init-mw-viewPageTarget-saveDialog-minorEdit' ) .remove(); } callback( $wrap ); } ); }; /** * Adds content and event bindings to the save dialog. * * @method */ ve.init.mw.ViewPageTarget.prototype.setupSaveDialog = function () { var viewPage = this; viewPage.getSaveDialogHtml( function ( $wrap ) { 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( $wrap.contents() ) .find( '.ve-init-mw-viewPageTarget-saveDialog-title' ) .text( ve.msg( 'tooltip-save' ) ) .end() .find( '.ve-init-mw-viewPageTarget-saveDialog-closeButton' ) .click( ve.bind( viewPage.onSaveDialogCloseButtonClick, viewPage ) ) .end() .find( '#ve-init-mw-viewPageTarget-saveDialog-editSummary' ) .attr( { 'placeholder': ve.msg( 'visualeditor-editsummary' ) } ) .placeholder() .byteLimit( viewPage.editSummaryByteLimit ) .on( 'keydown mouseup cut paste change focus blur', function () { var $textarea = $(this), $editSummaryCount = $textarea .closest( '.ve-init-mw-viewPageTarget-saveDialog-body' ) .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() ) ); }, 0 ); } ) .end() .find( '.ve-init-mw-viewPageTarget-saveDialog-editSummaryCount' ) .text( viewPage.editSummaryByteLimit ) .end() .find( '.ve-init-mw-viewPageTarget-saveDialog-minorEdit-label' ) .text( ve.msg( 'minoredit' ) ) .end() .find( '#ve-init-mw-viewPageTarget-saveDialog-watchList' ) .prop( 'checked', mw.config.get( 'wgVisualEditor' ).isPageWatched ) .end() .find( '.ve-init-mw-viewPageTarget-saveDialog-saveButton' ) .on( { 'mousedown': function () { $(this).addClass( 've-init-mw-viewPageTarget-saveDialog-saveButton-down' ); }, 'mouseleave mouseup': function () { $(this).removeClass( 've-init-mw-viewPageTarget-saveDialog-saveButton-down' ); }, 'click': ve.bind( viewPage.onSaveDialogSaveButtonClick, viewPage ) } ) .end() .find( '.ve-init-mw-viewPageTarget-saveDialog-saveButton-label' ) .text( ve.msg( 'savearticle' ) ) .end() .find( '.ve-init-mw-viewPageTarget-saveDialog-license' ) // FIXME license text is hardcoded English .html( 'By editing this page, you agree to irrevocably release your \ contributions under the CC-BY-SA 3.0 License. If you don\'t want your \ writing to be edited mercilessly and redistrubuted at will, then \ don\'t submit it here.

You are also confirming that you \ wrote this yourself, or copied it from a public domain or similar free \ resource. See Project:Copyright for full details of the licenses \ used on this site.\ DO NOT SUBMIT COPYRIGHTED WORK WITHOUT PERMISSION!' ); viewPage.$saveDialogSaveButton = viewPage.$saveDialog .find( '.ve-init-mw-viewPageTarget-saveDialog-saveButton' ); viewPage.$saveDialogLoadingIcon = viewPage.$saveDialog .find( '.ve-init-mw-viewPageTarget-saveDialog-saving' ); } ); // 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( '#ve-init-mw-viewPageTarget-saveDialog-watchList' ) .prop( 'checked', ( action === 'watch' ) ); } ); }; /** * Adds the save dialog to the user interface. * * @method */ ve.init.mw.ViewPageTarget.prototype.attachSaveDialog = function () { // Update the minoredit and watchthis messages (which came through the message module) this.$saveDialog.find( '.ve-init-mw-viewPageTarget-saveDialog-minorEdit-label' ) .text( ve.msg( 'minoredit' ) ); this.$saveDialog.find( '.ve-init-mw-viewPageTarget-saveDialog-watchList-label' ) .text( ve.msg( 'watchthis' ) ); this.$toolbarWrapper.find( '.ve-ui-toolbar' ).append( this.$saveDialog ); }; /** * Removes the save dialog from the user interface. * * @method */ ve.init.mw.ViewPageTarget.prototype.detachSaveDialog = function () { this.$saveDialog.detach(); }; /** * Remembers the window's scroll position. * * @method */ ve.init.mw.ViewPageTarget.prototype.saveScrollPosition = function () { this.scrollTop = $( window ).scrollTop(); }; /** * Restores the window's scroll position. * * @method */ ve.init.mw.ViewPageTarget.prototype.restoreScrollPosition = function () { if ( this.scrollTop ) { $( window ).scrollTop( this.scrollTop ); this.scrollTop = null; } }; /** * Shows the loading spinner. * * @method */ ve.init.mw.ViewPageTarget.prototype.showSpinner = function () { $( '#firstHeading' ).prepend( this.$spinner ); }; /** * Hides the loading spinner. * * @method */ ve.init.mw.ViewPageTarget.prototype.hideSpinner = function () { this.$spinner.detach(); }; /** * Shows the page content. * * @method */ ve.init.mw.ViewPageTarget.prototype.showPageContent = function () { $( '#bodyContent > .ve-init-mw-viewPageTarget-content' ) .removeClass( 've-init-mw-viewPageTarget-content' ) .show() .fadeTo( 0, 1 ); }; /** * Mutes the page content. * * @method */ ve.init.mw.ViewPageTarget.prototype.mutePageContent = function () { $( '#bodyContent > :visible:not(#siteSub, #contentSub)' ) .addClass( 've-init-mw-viewPageTarget-content' ) .fadeTo( 'fast', 0.6 ); }; /** * Hides the page content. * * @method */ ve.init.mw.ViewPageTarget.prototype.hidePageContent = function () { $( '#bodyContent > :visible:not(#siteSub, #contentSub)' ) .addClass( 've-init-mw-viewPageTarget-content' ) .hide(); }; /** * Shows the table of contents in the view mode. * * @method */ ve.init.mw.ViewPageTarget.prototype.showTableOfContents = function () { var $toc = $( '#toc' ), $wrap = $toc.parent(); if ( $wrap.data( 've.hideTableOfContents' ) ) { $wrap.slideDown(function () { $toc.unwrap(); }); } }; /** * Hides the table of contents in the view mode. * * @method */ ve.init.mw.ViewPageTarget.prototype.hideTableOfContents = function () { $( '#toc' ) .wrap( '
' ) .parent() .data( 've.hideTableOfContents', true ) .slideUp(); }; /** * Shows the save dialog. * * @method */ ve.init.mw.ViewPageTarget.prototype.showSaveDialog = function () { var viewPage = this; this.$toolbarEditNotices.fadeOut( 'fast' ); this.unlockSaveDialogSaveButton(); this.$saveDialogLoadingIcon.hide(); this.$saveDialog.fadeIn( 'fast' ).find( 'textarea' ).eq( 0 ).focus(); $( document ).on( 'keydown', function ( e ) { if ( e.which === 27 ) { viewPage.onSaveDialogCloseButtonClick(); } }); }; /** * Hides the save dialog * * @method */ ve.init.mw.ViewPageTarget.prototype.hideSaveDialog = function () { this.$saveDialog.fadeOut( 'fast' ); this.$document.focus(); $( document ).off( 'keydown' ); }; /** * Resets the fields of the save dialog * * @method */ ve.init.mw.ViewPageTarget.prototype.resetSaveDialog = function () { this.$saveDialog .find( '#ve-init-mw-viewPageTarget-saveDialog-editSummary' ) .val( '' ) .end() .find( '#ve-init-mw-viewPageTarget-saveDialog-minorEdit' ) .prop( 'checked', false ); }; /** * Enables the toolbar save button. * * @method */ ve.init.mw.ViewPageTarget.prototype.enableToolbarSaveButton = function () { this.$toolbarSaveButton .removeClass( 've-init-mw-viewPageTarget-toolbar-saveButton-disabled' ); }; /** * Disables the toolbar save button. * * @method */ ve.init.mw.ViewPageTarget.prototype.disableToolbarSaveButton = function () { this.$toolbarSaveButton .addClass( 've-init-mw-viewPageTarget-toolbar-saveButton-disabled' ); }; /** * Enables the save dialog save button. * * @method */ ve.init.mw.ViewPageTarget.prototype.unlockSaveDialogSaveButton = function () { this.$saveDialogSaveButton .removeClass( 've-init-mw-viewPageTarget-saveDialog-saveButton-saving' ); }; /** * Disables the save dialog save button. * * @method */ ve.init.mw.ViewPageTarget.prototype.lockSaveDialogSaveButton = function () { this.$saveDialogSaveButton .addClass( 've-init-mw-viewPageTarget-saveDialog-saveButton-saving' ); }; /** * Shows the toolbar. * * This also transplants the toolbar to a new location. * * @method */ ve.init.mw.ViewPageTarget.prototype.attachToolbar = function () { this.$toolbarWrapper = $( '.ve-ui-toolbar-wrapper' ) .insertBefore( $( '#firstHeading' ) ) .find( '.ve-ui-toolbar' ) .slideDown( 'fast' ) .end(); }; /** * Hides the toolbar. * * @method */ ve.init.mw.ViewPageTarget.prototype.detachToolbar = function () { $( '.ve-ui-toolbar' ).slideUp( 'fast', function () { $(this).parent().remove(); } ); }; /** * Enables the toolbar save button. * * @method */ ve.init.mw.ViewPageTarget.prototype.transformPageTitle = function () { $( '#firstHeading' ).addClass( 've-init-mw-viewPageTarget-pageTitle' ); }; /** * Enables the toolbar save button. * * @method */ ve.init.mw.ViewPageTarget.prototype.mutePageTitle = function () { $( '#firstHeading, #siteSub:visible, #contentSub:visible' ).fadeTo( 'fast', 0.6 ); }; /** * Disables the toolbar save button. * * @method */ ve.init.mw.ViewPageTarget.prototype.restorePageTitle = function () { $( '#firstHeading, #siteSub:visible, #contentSub:visible' ).fadeTo( 'fast', 1 ); setTimeout( function () { $( '#firstHeading' ).removeClass( 've-init-mw-viewPageTarget-pageTitle' ); }, 1000 ); }; /** * Modifies page tabs to show that editing is taking place. * * @method */ ve.init.mw.ViewPageTarget.prototype.transformSkinTabs = function () { $( $( '#p-views' ).length ? '#p-views' : '#p-cactions' ) .find( 'li.selected' ).removeClass( 'selected' ); $( this.tabLayout === 'add' ? '#ca-ve-edit' : '#ca-edit' ) .addClass( 'selected' ); }; /** * Modifies page tabs to show that viewing is taking place. * * @method */ ve.init.mw.ViewPageTarget.prototype.restoreSkinTabs = function () { $( $( '#p-views' ).length ? '#p-views' : '#p-cactions' ) .find( 'li.selected' ).removeClass( 'selected' ); $( '#ca-view' ).addClass( 'selected' ); }; /** * Hides site notice on page if present. * * @method */ ve.init.mw.ViewPageTarget.prototype.hideSiteNotice = function () { $( '#siteNotice:visible' ) .addClass( 've-hide' ) .slideUp( 'fast' ); }; /** * Show site notice on page if present. * * @method */ ve.init.mw.ViewPageTarget.prototype.restoreSiteNotice = function () { $(' #siteNotice.ve-hide' ) .slideDown( 'fast' ); }; /** * Replaces the page content with new HTML. * * @method * @param {HTMLElement} html Rendered HTML from server */ ve.init.mw.ViewPageTarget.prototype.replacePageContent = function ( html ) { $( '#mw-content-text' ).html( html ); }; /** * Gets the numeric index of a section in the page. * * @method * @param {HTMLElement} heading Heading element of section */ ve.init.mw.ViewPageTarget.prototype.getEditSection = function ( heading ) { var $page = $( '#mw-content-text' ), section = 0; $page.find( 'h1, h2, h3, h4, h5, h6' ).not( '#toc h2' ).each( function () { section++; if ( this === heading ) { return false; } } ); return section; }; /** * Gets the numeric index of a section in the page. * * @method * @param {HTMLElement} heading Heading element of section */ ve.init.mw.ViewPageTarget.prototype.saveEditSection = function ( heading ) { this.section = this.getEditSection( heading ); }; /** * Moves the cursor in the editor to a given section. * * @method * @param {Number} section Section to move cursor to */ ve.init.mw.ViewPageTarget.prototype.restoreEditSection = function () { if ( this.section !== null ) { var offset, surfaceView = this.surface.getView(), surfaceModel = surfaceView.getModel(); this.$document.find( 'h1, h2, h3, h4, h5, h6' ).eq( this.section - 1 ).each( function () { var headingNode = $(this).data( 'node' ); if ( headingNode ) { offset = surfaceModel.getDocument().getNearestContentOffset( headingNode.getModel().getOffset() ); surfaceModel.change( null, new ve.Range( offset, offset ) ); surfaceView.showSelection( surfaceModel.getSelection() ); } } ); this.section = null; } }; /** * Adds onbeforunload handler. * * @method */ ve.init.mw.ViewPageTarget.prototype.setupBeforeUnloadHandler = function () { // Remember any already set on before unload handler this.onBeforeUnloadFallback = window.onbeforeunload; // Attach before unload handler window.onbeforeunload = this.proxiedOnBeforeUnload = ve.bind( this.onBeforeUnload, this ); // Attach page show handlers if ( window.addEventListener ) { window.addEventListener( 'pageshow', ve.bind( this.onPageShow, this ), false ); } else if ( window.attachEvent ) { window.attachEvent( 'pageshow', ve.bind( this.onPageShow, this ) ); } }; /** * Removes onbeforunload handler. * * @method */ ve.init.mw.ViewPageTarget.prototype.tearDownBeforeUnloadHandler = function () { // Restore whatever previous onbeforeload hook existed window.onbeforeunload = this.onBeforeUnloadFallback; }; /** * Responds to page show event. * * @method */ ve.init.mw.ViewPageTarget.prototype.onPageShow = function () { // Re-add onbeforeunload handler window.onbeforeunload = this.proxiedOnBeforeUnload; }; /** * Responds to before unload event. * * @method */ ve.init.mw.ViewPageTarget.prototype.onBeforeUnload = function () { var fallbackResult, message, proxiedOnBeforeUnload = this.proxiedOnBeforeUnload; // Check if someone already set on onbeforeunload hook if ( this.onBeforeUnloadFallback ) { // Get the result of their onbeforeunload hook fallbackResult = this.onBeforeUnloadFallback(); } // Check if their onbeforeunload hook returned something if ( fallbackResult !== undefined ) { // Exit here, returning their message message = fallbackResult; } else { // Override if submitting if ( this.submitting ) { return null; } // Check if there's been an edit if ( this.surface && this.surface.getModel().getHistory().length ) { // Return our message message = ve.msg( 'visualeditor-viewpage-savewarning' ); } } // Unset the onbeforeunload handler so we don't break page caching in Firefox window.onbeforeunload = null; if ( message !== undefined ) { // ...but if the user chooses not to leave the page, we need to rebind it setTimeout( function () { window.onbeforeunload = proxiedOnBeforeUnload; }, 1 ); return message; } }; /* Initialization */ ve.init.mw.targets.push( new ve.init.mw.ViewPageTarget() );