diff --git a/VisualEditor.php b/VisualEditor.php index 5ed04d65a7..c315d0bc9a 100644 --- a/VisualEditor.php +++ b/VisualEditor.php @@ -126,12 +126,12 @@ $wgResourceModules += array( // ve 'jquery/jquery.json.js', 've2/ve.js', + 've2/ve.EventEmitter.js', ) ), 'ext.visualEditor.core' => $wgVisualEditorResourceTemplate + array( 'scripts' => array( // ve - 've2/ve.EventEmitter.js', 've2/ve.Factory.js', 've2/ve.Position.js', 've2/ve.Range.js', diff --git a/modules/ve2/ce/styles/ve.ce.Node.css b/modules/ve2/ce/styles/ve.ce.Node.css index d3d96ace14..27e8520f86 100644 --- a/modules/ve2/ce/styles/ve.ce.Node.css +++ b/modules/ve2/ce/styles/ve.ce.Node.css @@ -25,3 +25,8 @@ .ve-ce-branchNode p:empty:before { content: url('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='); } + +li.ve-ce-branchNode p.ve-ce-branchNode:first-child { + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/modules/ve2/init/styles/ve.init.ViewPageTarget.css b/modules/ve2/init/styles/ve.init.ViewPageTarget.css index c6d7397a3b..a07b752052 100644 --- a/modules/ve2/init/styles/ve.init.ViewPageTarget.css +++ b/modules/ve2/init/styles/ve.init.ViewPageTarget.css @@ -1,9 +1,9 @@ +.ve-surface { + margin-top: 0.8em; +} + .es-toolbar-wrapper { margin: -1em -1em 1em -1em; - transition: margin 250ms, height 250ms; - -moz-transition: margin 250ms, height 250ms; - -webkit-transition: margin 250ms, height 250ms; - -o-transition: margin 250ms, height 250ms; } .es-toolbar { @@ -31,7 +31,7 @@ float: right; } -.ve-init-viewPageTarget-button { +.ve-init-viewPageTarget-toolbar-saveButton { display: inline-block; border: solid 1px transparent; @@ -47,40 +47,31 @@ border: solid 1px transparent; } -.ve-init-viewPageTarget-button:before { +.ve-init-viewPageTarget-toolbar-saveButton:before { content: " "; position: absolute; display: block; height: 22px; } -.ve-init-viewPageTarget-button:hover { +.ve-init-viewPageTarget-toolbar-saveButton:hover { border-color: #eeeeee; } -.ve-init-viewPageTarget-button:active, -.ve-init-viewPageTarget-button-down { +.ve-init-viewPageTarget-toolbar-saveButton:active, +.ve-init-viewPageTarget-toolbar-saveButton-down { border-color: #dddddd; -webkit-box-shadow: inset 0px 1px 4px 0px rgba(0, 0, 0, 0.07); -moz-box-shadow: inset 0px 1px 4px 0px rgba(0, 0, 0, 0.07); box-shadow: inset 0px 1px 4px 0px rgba(0, 0, 0, 0.07); } -.ve-init-viewPageTarget-button-disabled { +.ve-init-viewPageTarget-toolbar-saveButton-disabled { opacity: 0.5; -moz-opacity: 0.5; } -/* inspector styles */ -.ve-init-viewPageTarget-saveButton { - border: 1px solid transparent; - border-radius: 0.125em; - -webkit-border-radius: 0.125em; - -moz-border-radius: 0.125em; - -o-border-radius: 0.125em; -} - -.ve-init-viewPageTarget-saveButton-icon { +.ve-init-viewPageTarget-toolbar-saveButton-icon { display: inline-block; vertical-align: middle; width: 32px; @@ -91,7 +82,7 @@ background-repeat: no-repeat; } -.ve-init-viewPageTarget-saveButton-label { +.ve-init-viewPageTarget-toolbar-saveButton-label { display: inline-block; vertical-align: middle; height: 32px; @@ -110,36 +101,39 @@ .ve-init-viewPageTarget-saveDialog-saveButton { position: absolute; - border: 1px solid rgb(196,229,154); + cursor: pointer; + border: 1px solid #c3e59a; margin-top: 10px; right: 10px; font-size: 1em; padding: 0.5em 1em; border-radius: 0.25em; -moz-border-radius: 0.25em; - background-image: url(../../ui/styles/images/close.png); - background-position: center right; - background-repeat: no-repeat; /* Fancy CSS background */ - background-image: linear-gradient(bottom, rgb(195,229,154) 0%, rgb(240,251,225) 100%); - background-image: -o-linear-gradient(bottom, rgb(195,229,154) 0%, rgb(240,251,225) 100%); - background-image: -moz-linear-gradient(bottom, rgb(195,229,154) 0%, rgb(240,251,225) 100%); - background-image: -webkit-linear-gradient(bottom, rgb(195,229,154) 0%, rgb(240,251,225) 100%); - background-image: -ms-linear-gradient(bottom, rgb(195,229,154) 0%, rgb(240,251,225) 100%); + background-image: linear-gradient(bottom, #c3e59a 0%, #f0fbe1 100%); + background-image: -o-linear-gradient(bottom, #c3e59a 0%, #f0fbe1 100%); + background-image: -moz-linear-gradient(bottom, #c3e59a 0%, #f0fbe1 100%); + background-image: -webkit-linear-gradient(bottom, #c3e59a 0%, #f0fbe1 100%); + background-image: -ms-linear-gradient(bottom, #c3e59a 0%, #f0fbe1 100%); background-image: -webkit-gradient( linear, left bottom, left top, - color-stop(0, rgb(195,229,154)), - color-stop(1, rgb(240,251,225)) + color-stop(0, #c3e59a), + color-stop(1, #f0fbe1) ); } +.ve-init-viewPageTarget-saveDialog-saveButton:hover { + border-color: #a7cd76; +} + .ve-init-viewPageTarget-saveDialog-saveButton-icon { display: inline-block; vertical-align: middle; height: 2em; width: 28px; + margin-right: -4px; background: transparent; background-image: url(../../ui/styles/images/accept-clear.png); background-position: right top; @@ -171,7 +165,7 @@ } .ve-init-viewPageTarget-saveDialog input[type='text'] { - width: 96%; + width: 98%; font-size: 12px; padding: 4px; margin: 10px 0; diff --git a/modules/ve2/init/targets/ve.init.ViewPageTarget.js b/modules/ve2/init/targets/ve.init.ViewPageTarget.js index 71d5f4f7a8..db4d9e56f7 100644 --- a/modules/ve2/init/targets/ve.init.ViewPageTarget.js +++ b/modules/ve2/init/targets/ve.init.ViewPageTarget.js @@ -11,34 +11,35 @@ ve.init.ViewPageTarget = function() { ve.init.Target.call( this, mw.config.get( 'wgPageName' ) ); // Properties - this.$content = $( '#content' ); - this.$page = $( '#mw-content-text' ); - this.$view = $( '#bodyContent' ); - this.$toc = $( '#toc' ); - this.$heading = $( '#firstHeading' ); this.$surface = $( '
' ); - this.$toolbar = null; + this.$spinner = $( '
' ); + this.$toolbarSaveButton = $( '
' ); + this.$saveDialog = $( '
' ); this.surface = null; this.active = false; this.edited = false; this.activating = false; this.deactivating = false; + this.scrollTop = null; + this.section = null; this.proxiedOnSurfaceModelTransact = ve.proxy( this.onSurfaceModelTransact, this ); - this.surfaceOptions = { - 'toolbars': { - 'top': { - // If mobile device, float false - 'float': !this.isMobileDevice, - // Toolbar modes - 'modes': ['wikitext'] - } - } - }; + this.surfaceOptions = { 'toolbars': { 'top': { 'float': !this.isMobileDevice } } }; + + // Events + this.addListenerMethods( this, { + 'load': 'onLoad', + 'save': 'onSave', + 'loadError': 'onLoadError', + 'saveError': 'onSaveError' + } ); // Initialization if ( mw.config.get('wgCanonicalNamespace') === 'VisualEditor' ) { // Clicking the edit tab is the only way any other code gets run, and this sets that up - this.setupTabs(); + this.setupSkinTabs(); + this.setupSectionEditLinks(); + this.setupToolbarSaveButton(); + this.setupSaveDialog(); } }; @@ -46,7 +47,6 @@ ve.init.ViewPageTarget = function() { /*jshint multistr: true*/ ve.init.ViewPageTarget.saveDialogTemplate = '\ -
\
\
\
\ @@ -72,103 +72,181 @@ ve.init.ViewPageTarget.saveDialogTemplate = '\
\
\

\ -
\ -
'; + '; /* Methods */ /** - * ... + * Switches to edit mode. * * @method */ -ve.init.ViewPageTarget.prototype.onEditTabClick = function( e, section ) { - // Ignore multiple clicks while editor is active +ve.init.ViewPageTarget.prototype.activate = function() { if ( !this.active && !this.activating ) { this.activating = true; - // UI updates - this.setSelectedTab( 'ca-edit' ); + // User interface changes + this.transformSkinTabs(); this.showSpinner(); - this.$toc.addClass( 've-init-viewPageTarget-pageToc' ).slideUp( 'fast' ); - // Remember scroll position - var scrollTop = $( window ).scrollTop(); - // Asynchronous initialization - load ve modules at the same time as the content - this.load( ve.proxy( function( error, dom ) { - this.onLoad( error, dom ); - if ( section !== undefined ) { - // HACK: All of this code is fragile, be careful and suspicious - var $heading = this.$surface - .find( '.ve-ce-documentNode' ) - .find( 'h1, h2, h3, h4, h5, h6' ) - .eq( section ); - surfaceView = this.surface.getView(), - surfaceModel = surfaceView.getModel(), - doc = surfaceModel.getDocument(); - if ( $heading.length ) { - var offset = doc.getNearestContentOffset( - $heading.data( 'node' ).getModel().getOffset() - ); - surfaceModel.setSelection( new ve.Range( offset, offset ) ); - surfaceView.showSelection( surfaceModel.getSelection() ); - // Restore scroll position - $( window ).scrollTop( scrollTop ); - } - } - }, this ) ); + this.hideTableOfContents(); + this.mutePageContent(); + this.mutePageTitle(); + this.saveScrollPosition(); + this.load(); } - // Prevent the edit tab's normal behavior - e.preventDefault(); - return false; }; /** - * ... + * Switches to view mode. * * @method */ -ve.init.ViewPageTarget.prototype.onEditSectionLinkClick = function( e ) { - var heading = $( e.target ).closest( 'h1, h2, h3, h4, h5, h6' )[0], - tocHeading = this.$page.find( '#toc h2' )[0]; - section = 0; - this.$page.find( 'h1, h2, h3, h4, h5, h6' ).each( function() { - if ( this === heading ) { - return false; - } - if ( this !== tocHeading ) { - section++; - } - } ); - return this.onEditTabClick( e, section ); -}; - -/** - * ... - * - * @method - */ -ve.init.ViewPageTarget.prototype.onViewTabClick = function( e ) { - // Don't do anything special unless we are in editing mode +ve.init.ViewPageTarget.prototype.deactivate = function() { if ( this.active && !this.deactivating ) { - this.deactivating = true; if ( !this.surface.getModel().getHistory().length || confirm( 'Are you sure you want to go back to view mode without saving first?' ) ) { + this.deactivating = true; + // User interface changes + this.restoreSkinTabs(); + this.hideSpinner(); + this.detachToolbarSaveButton(); + this.detachSaveDialog(); this.tearDownSurface(); + this.showTableOfContents(); this.deactivating = false; } - // Prevent the edit tab's normal behavior - e.preventDefault(); - return false; } }; /** - * ... + * Handles successful DOM load event. * * @method + * @param {HTMLElement} dom Parsed DOM from server */ -ve.init.ViewPageTarget.prototype.onSaveDialogSaveButtonClick = function( e ) { +ve.init.ViewPageTarget.prototype.onLoad = function( dom ) { + this.edited = false; + this.setUpSurface( dom ); + this.attachToolbarSaveButton(); + this.attachSaveDialog(); + this.restoreScrollPosition(); + this.restoreEditSection(); + this.$surface.find( '.ve-ce-documentNode' ).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.ViewPageTarget.prototype.onLoadError = function( response, status, error ) { + // TODO: Something... +}; + +/** + * Handles successful DOM save event. + * + * @method + * @param {HTMLElement} html Rendered HTML from server + */ +ve.init.ViewPageTarget.prototype.onSave = function( html ) { + this.hideSaveDialog(); + this.replacePageContent( html ); + this.deactivate(); +}; + +/** + * 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.ViewPageTarget.prototype.onSaveError = function( response, status, error ) { + // TODO: Something... +}; + +/** + * Handles clicks on the edit tab. + * + * @method + * @param {Event} e DOM event + */ +ve.init.ViewPageTarget.prototype.onEditTabClick = function( event ) { + console.log( this ); + this.activate(); + // Prevent the edit tab's normal behavior + event.preventDefault(); + return false; +}; + +/** + * Handles clicks on a section edit link. + * + * @method + * @param {Event} event DOM event + */ +ve.init.ViewPageTarget.prototype.onEditSectionLinkClick = function( event ) { + this.saveEditingSection( $( event.target ).closest( 'h1, h2, h3, h4, h5, h6' )[0] ); + this.activate(); + // Prevent the edit tab's normal behavior + event.preventDefault(); + return false; +}; + +/** + * Handles clicks on the view tab. + * + * @method + * @param {Event} event DOM event + */ +ve.init.ViewPageTarget.prototype.onViewTabClick = function( event ) { + console.log( this ); + this.deactivate(); + // Prevent the edit tab's normal behavior + event.preventDefault(); + return false; +}; + +/** + * Handles clicks on the save button in the toolbar. + * + * @method + * @param {Event} event DOM event + */ +ve.init.ViewPageTarget.prototype.onToolbarSaveButtonClick = function( event ) { + if ( this.edited ) { + this.showSaveDialog(); + } +}; + +/** + * 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.ViewPageTarget.prototype.onSurfaceModelTransact = function( tx ) { + this.edited = true; + this.enableToolbarSaveButton(); + this.surface.getModel().removeListener( 'transact', this.proxiedOnSurfaceModelTransact ); +}; + +/** + * Handles clicks on the save button in the save dialog. + * + * @method + * @param {Event} event DOM event + */ +ve.init.ViewPageTarget.prototype.onSaveDialogSaveButtonClick = function( event ) { this.showSpinner(); // Save this.save( @@ -183,183 +261,65 @@ ve.init.ViewPageTarget.prototype.onSaveDialogSaveButtonClick = function( e ) { }; /** - * ... + * Handles clicks on the close button in the save dialog. * * @method + * @param {Event} event DOM event */ -ve.init.ViewPageTarget.prototype.onSurfaceModelTransact = function() { - if ( !this.edited ) { - this.edited = true; - this.$toolbar.find( '.ve-init-viewPageTarget-saveButton ' ) - .removeClass( 've-init-viewPageTarget-button-disabled' ); - this.surface.getModel().removeListener( 'transact', this.proxiedOnSurfaceModelTransact ); - } +ve.init.ViewPageTarget.prototype.onSaveDialogCloseButtonClick = function( event ) { + this.hideSaveDialog(); }; /** - * ... - * - * @method - */ -ve.init.ViewPageTarget.prototype.onSaveButtonClick = function( e ) { - if ( this.edited ) { - this.$dialog.fadeIn( 'fast' ); - this.$dialog.find( 'input:first' ).focus(); - } -}; - -/** - * ... - * - * @method - */ -ve.init.ViewPageTarget.prototype.onSaveDialogCloseButtonClick = function( e ) { - this.$dialog.fadeOut( 'fast' ); - this.$surface.find( '.ve-ce-documentNode' ).focus(); -}; - -/** - * ... - * - * @method - */ -ve.init.ViewPageTarget.prototype.onLoad = function( error, dom ) { - this.activating = false; - if ( error ) { - // TODO: Error handling in the UI - } else { - this.edited = false; - this.setUpSurface( dom ); - this.$surface.find( '.ve-ce-documentNode' ).focus(); - } -}; - -/** - * ... - * - * @method - */ -ve.init.ViewPageTarget.prototype.onSave = function( error, content ) { - if ( error ) { - // TODO: Handle error in UI - } else { - // Hide the save dialog - this.$dialog.fadeOut(); - // Refresh page with changed content - this.$content.find( '#mw-content-text' ).html( content ); - // Restore the page to how it used to be - this.tearDownSurface(); - } -}; - -/** - * ... + * Switches to editing mode. * * @method + * @param {HTMLElement} dom HTML DOM to edit */ ve.init.ViewPageTarget.prototype.setUpSurface = function( dom ) { // Initialize surface - this.$surface.appendTo( this.$content ); + this.attachSurface(); this.surface = new ve.Surface( this.$surface, dom, this.surfaceOptions ); this.surface.getModel().on( 'transact', this.proxiedOnSurfaceModelTransact ); // Transplant the toolbar - this.$toolbar = this.$surface.find( '.es-toolbar-wrapper' ); - this.$toolbar.find( '.es-toolbar' ).slideDown( 'fast' ); - this.$heading - .before( this.$toolbar ) - .addClass( 've-init-viewPageTarget-pageTitle' ) - .fadeTo( 'fast', 0.5 ); + this.attachToolbar(); + this.transformPageTitle(); // Update UI - this.$view.hide(); - this.$spinner.remove(); - this.$dialog = $( ve.init.ViewPageTarget.saveDialogTemplate ); - // Add save and close buttons - this.$toolbar - .find( '.es-modes' ) - .append( - $( '
' ) - .addClass( - 've-init-viewPageTarget-button ' + - 've-init-viewPageTarget-button-disabled ' + - 've-init-viewPageTarget-saveButton' - ) - .append( - $( '' ) - .text( mw.msg( 'savearticle' ) ) - ) - .append( $( '' ) ) - .mousedown( function( e ) { - e.preventDefault(); - return false; - } ) - .click( ve.proxy( this.onSaveButtonClick, this ) ) - ); - // Set up save dialog - this.$dialog - .find( '.ve-init-viewPageTarget-saveDialog-title' ) - .text( mw.msg( 'tooltip-save' ) ) - .end() - .find( '.ve-init-viewPageTarget-saveDialog-closeButton' ) - .click( ve.proxy( this.onSaveDialogCloseButtonClick, this ) ) - .end() - .find( '.ve-init-viewPageTarget-saveDialog-editSummary-label' ) - .text( mw.msg( 'summary' ) ) - .end() - .find( '.ve-init-viewPageTarget-saveDialog-minorEdit-label' ) - .text( mw.msg( 'minoredit' ) ) - .end() - .find( '.ve-init-viewPageTarget-saveDialog-watchList' ) - .prop( 'checked', ve.config.isPageWatched ) - .end() - .find( '.ve-init-viewPageTarget-saveDialog-watchList-label' ) - .text( mw.msg( 'watchthis' ) ) - .end() - .find( '.ve-init-viewPageTarget-saveDialog-saveButton' ) - .click( ve.proxy( this.onSaveDialogSaveButtonClick, this ) ) - .end() - .find( '.ve-init-viewPageTarget-saveDialog-saveButton-label' ) - .text( mw.msg( 'savearticle' ) ) - .end() - .find( '.ve-init-viewPageTarget-saveDialog-license' ) - .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 editied 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!" - ) - .end() - .insertAfter( this.$toolbar.find( '.ve-init-viewPageTarget-saveButton' ) ); + this.hidePageContent(); + this.hideSpinner(); + this.disableToolbarSaveButton(); this.active = true; }; -ve.init.ViewPageTarget.prototype.tearDownSurface = function( content ) { +/** + * Switches to viewing mode. + * + * @method + */ +ve.init.ViewPageTarget.prototype.tearDownSurface = function() { // Reset tabs - this.setSelectedTab( 'ca-view' ); + this.restoreSkinTabs(); // Update UI + this.$surface.find( '.ve-ce-documentNode' ).blur(); this.$surface.empty().detach(); - this.$toolbar.find( '.es-toolbar' ).slideUp( 'fast', function() { - $(this).parent().remove(); - } ); - this.$spinner.remove(); - $( '.es-contextView' ).remove(); - this.$view.show().fadeTo( 'fast', 1 ); - this.$heading.fadeTo( 'fast', 1 ); - setTimeout( ve.proxy( function() { - $(this).removeClass( 've-init-viewPageTarget-pageTitle' ); - }, this.$heading ), 1000 ); - this.$toc.slideDown( 'fast', function() { - $(this).removeClass( 've-init-viewPageTarget-pageToc' ); - } ); + 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; }; -ve.init.ViewPageTarget.prototype.setupTabs = function(){ +/** + * Modifies tabs in the skin to support in-place editing. + * + * @method + */ +ve.init.ViewPageTarget.prototype.setupSkinTabs = function() { // Only sysop users will have an edit tab in this namespace, so we might need to add one if ( $( '#ca-edit' ).length === 0 ) { // Add edit tab @@ -395,32 +355,401 @@ ve.init.ViewPageTarget.prototype.setupTabs = function(){ ); } $( '#ca-edit a' ).click( ve.proxy( this.onEditTabClick, this ) ); - $( '#mw-content-text .editsection a' ).click( ve.proxy( this.onEditSectionLinkClick, this ) ); $( '#ca-view a' ).click( ve.proxy( this.onViewTabClick, this ) ); }; /** - * Shows a loading spinner. + * Modifies page content to make section edit links activate the editor. + * + * @method + */ +ve.init.ViewPageTarget.prototype.setupSectionEditLinks = function() { + $( '#mw-content-text .editsection a' ).click( ve.proxy( this.onEditSectionLinkClick, this ) ); +}; + +/** + * Adds content and event bindings to the save button. + * + * @method + */ +ve.init.ViewPageTarget.prototype.setupToolbarSaveButton = function() { + this.$toolbarSaveButton + .append( + $( '' ) + .text( mw.msg( 'savearticle' ) ) + ) + .append( $( '' ) ) + .mousedown( function( e ) { + $(this).addClass( 've-init-viewPageTarget-toolbar-saveButton-down' ); + e.preventDefault(); + return false; + } ) + .mouseup( function( e ) { + $(this).removeClass( 've-init-viewPageTarget-toolbar-saveButton-down' ); + e.preventDefault(); + return false; + } ) + .click( ve.proxy( this.onToolbarSaveButtonClick, this ) ); +}; + +/** + * Adds the save button to the user interface. + * + * @method + */ +ve.init.ViewPageTarget.prototype.attachToolbarSaveButton = function() { + $( '.es-toolbar .es-modes' ).append( this.$toolbarSaveButton ); + this.disableToolbarSaveButton(); +}; + +/** + * Removes the save button from the user interface. + * + * @method + */ +ve.init.ViewPageTarget.prototype.detachToolbarSaveButton = function() { + this.$toolbarSaveButton.detach(); +}; + +/** + * Adds content and event bindings to the save dialog. + * + * @method + */ +ve.init.ViewPageTarget.prototype.setupSaveDialog = function() { + this.$saveDialog + .html( ve.init.ViewPageTarget.saveDialogTemplate ) + .find( '.ve-init-viewPageTarget-saveDialog-title' ) + .text( mw.msg( 'tooltip-save' ) ) + .end() + .find( '.ve-init-viewPageTarget-saveDialog-closeButton' ) + .click( ve.proxy( this.onSaveDialogCloseButtonClick, this ) ) + .end() + .find( '.ve-init-viewPageTarget-saveDialog-editSummary-label' ) + .text( mw.msg( 'summary' ) ) + .end() + .find( '.ve-init-viewPageTarget-saveDialog-minorEdit-label' ) + .text( mw.msg( 'minoredit' ) ) + .end() + .find( '.ve-init-viewPageTarget-saveDialog-watchList' ) + .prop( 'checked', mw.config.get( 'wgVisualEditor' ).isPageWatched ) + .end() + .find( '.ve-init-viewPageTarget-saveDialog-watchList-label' ) + .text( mw.msg( 'watchthis' ) ) + .end() + .find( '.ve-init-viewPageTarget-saveDialog-saveButton' ) + .click( ve.proxy( this.onSaveDialogSaveButtonClick, this ) ) + .end() + .find( '.ve-init-viewPageTarget-saveDialog-saveButton-label' ) + .text( mw.msg( 'savearticle' ) ) + .end() + .find( '.ve-init-viewPageTarget-saveDialog-license' ) + .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 editied 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!" + ); +}; + +/** + * Adds the save dialog to the user interface. + * + * @method + */ +ve.init.ViewPageTarget.prototype.attachSaveDialog = function() { + this.$saveDialog.insertAfter( this.$toolbarSaveButton ); +}; + +/** + * Removes the save dialog from the user interface. + * + * @method + */ +ve.init.ViewPageTarget.prototype.detachSaveDialog = function() { + this.$saveDialog.detach(); +}; + +/** + * Remembers the window's scroll position. + * + * @method + */ +ve.init.ViewPageTarget.prototype.saveScrollPosition = function() { + this.scrollTop = $( window ).scrollTop(); +}; + +/** + * Restores the window's scroll position. + * + * @method + */ +ve.init.ViewPageTarget.prototype.restoreScrollPosition = function() { + if ( this.scrollTop ) { + $( window ).scrollTop( this.scrollTop ); + this.scrollTop = null; + } +}; + +/** + * Shows the loading spinner. * * @method */ ve.init.ViewPageTarget.prototype.showSpinner = function() { - this.$spinner = $( '
' ) - .addClass( 've-init-viewPageTarget-loadingSpinner' ) - .prependTo( this.$heading ); + this.$spinner.prependTo( $( '#firstHeading' ) ); }; /** - * Resets all tabs in the UI and selects a specific one. - * - * If no ID is given, or no ID matches the given ID, all tabs will be unselected. + * Hides the loading spinner. * * @method - * @param {String} id HTML ID of tab to select */ -ve.init.ViewPageTarget.prototype.setSelectedTab = function( id ) { +ve.init.ViewPageTarget.prototype.hideSpinner = function() { + this.$spinner.detach(); +}; + +/** + * Shows the page content. + * + * @method + */ +ve.init.ViewPageTarget.prototype.showPageContent = function() { + $( '#bodyContent' ).children().not( '#siteSub' ).show().fadeTo( 0, 1 ); +}; + +/** + * Mutes the page content. + * + * @method + */ +ve.init.ViewPageTarget.prototype.mutePageContent = function() { + $( '#bodyContent' ).children().not( '#siteSub' ).fadeTo( 'fast', 0.25 ); +}; + +/** + * Hides the page content. + * + * @method + */ +ve.init.ViewPageTarget.prototype.hidePageContent = function() { + $( '#bodyContent' ).children().not( '#siteSub' ).hide(); +}; + +/** + * Shows the table of contents in the view mode. + * + * @method + */ +ve.init.ViewPageTarget.prototype.showTableOfContents = function() { + $( '#toc' ).slideDown( 'fast', function() { + $(this).removeClass( 've-init-viewPageTarget-pageToc' ); + } ); +}; + +/** + * Hides the table of contents in the view mode. + * + * @method + */ +ve.init.ViewPageTarget.prototype.hideTableOfContents = function() { + $( '#toc' ).addClass( 've-init-viewPageTarget-pageToc' ).slideUp( 'fast' ); +}; + +/** + * Shows the save dialog. + * + * @method + */ +ve.init.ViewPageTarget.prototype.showSaveDialog = function() { + this.$saveDialog.fadeIn( 'fast' ).find( 'input:first' ).focus(); +}; + +/** + * Hides the save dialog + * + * @method + */ +ve.init.ViewPageTarget.prototype.hideSaveDialog = function() { + this.$saveDialog.fadeOut( 'fast' ); + this.$surface.find( '.ve-ce-documentNode' ).focus(); +}; + +/** + * Enables the toolbar save button. + * + * @method + */ +ve.init.ViewPageTarget.prototype.enableToolbarSaveButton = function() { + this.$toolbarSaveButton.removeClass( 've-init-viewPageTarget-toolbar-saveButton-disabled' ); +}; + +/** + * Disables the toolbar save button. + * + * @method + */ +ve.init.ViewPageTarget.prototype.disableToolbarSaveButton = function() { + this.$toolbarSaveButton.addClass( 've-init-viewPageTarget-toolbar-saveButton-disabled' ); +}; + +/** + * Shows the toolbar. + * + * This also transplants the toolbar to a new location. + * + * @method + */ +ve.init.ViewPageTarget.prototype.attachToolbar = function() { + $( '.es-toolbar-wrapper' ) + .insertBefore( $( '#firstHeading' ) ) + .find( '.es-toolbar' ) + .slideDown( 'fast' ); +}; + +/** + * Hides the toolbar. + * + * @method + */ +ve.init.ViewPageTarget.prototype.detachToolbar = function() { + $( '.es-toolbar' ).slideUp( 'fast', function() { + $(this).parent().remove(); + } ); +}; + +/** + * Enables the toolbar save button. + * + * @method + */ +ve.init.ViewPageTarget.prototype.transformPageTitle = function() { + $( '#firstHeading' ).addClass( 've-init-viewPageTarget-pageTitle' ); +}; + +/** + * Enables the toolbar save button. + * + * @method + */ +ve.init.ViewPageTarget.prototype.mutePageTitle = function() { + $( '#firstHeading' ).fadeTo( 'fast', 0.25 ); + $( '#siteSub' ).fadeTo( 'fast', 0.25 ); +}; + +/** + * Disables the toolbar save button. + * + * @method + */ +ve.init.ViewPageTarget.prototype.restorePageTitle = function() { + $( '#firstHeading' ).fadeTo( 'fast', 1 ); + $( '#siteSub' ).fadeTo( 'fast', 1 ); + setTimeout( function() { + $( '#firstHeading' ).removeClass( 've-init-viewPageTarget-pageTitle' ); + }, 1000 ); +}; + +/** + * Modifies page tabs to show that editing is taking place. + * + * @method + */ +ve.init.ViewPageTarget.prototype.transformSkinTabs = function() { $( '#p-views' ).find( 'li.selected' ).removeClass( 'selected' ); - $( '#' + id ).addClass( 'selected' ); + $( '#ca-edit' ).addClass( 'selected' ); +}; + +/** + * Modifies page tabs to show that viewing is taking place. + * + * @method + */ +ve.init.ViewPageTarget.prototype.restoreSkinTabs = function() { + $( '#p-views' ).find( 'li.selected' ).removeClass( 'selected' ); + $( '#ca-view' ).addClass( 'selected' ); +}; + +/** + * Replaces the page content with new HTML. + * + * @method + * @param {HTMLElement} html Rendered HTML from server + */ +ve.init.ViewPageTarget.prototype.replacePageContent = function( html ) { + $( '#mw-content-text' ).html( html ); +}; + +/** + * Attaches the surface to the page. + * + * @method + */ +ve.init.ViewPageTarget.prototype.attachSurface = function() { + $( '#content' ).append( this.$surface ); +}; + +/** + * Attaches the surface to the page. + * + * @method + */ +ve.init.ViewPageTarget.prototype.detachSurface = function() { + this.$surface.detach(); + $( '.es-contextView' ).remove(); +}; + +/** + * Gets the numeric index of a section in the page. + * + * @method + * @param {HTMLElement} heading Heading element of section + */ +ve.init.ViewPageTarget.prototype.saveEditSection = function( heading ) { + var $page = $( '#mw-content-text' ); + tocHeading = $page.find( '#toc h2' )[0]; + section = 0; + $page.find( 'h1, h2, h3, h4, h5, h6' ).each( function() { + if ( this === heading ) { + return false; + } + if ( this !== tocHeading ) { + section++; + } + } ); + this.section = section; +}; + +/** + * Moves the cursor in the editor to a given section. + * + * @method + * @param {Number} section Section to move cursor to + */ +ve.init.ViewPageTarget.prototype.restoreEditSection = function() { + if ( this.section ) { + var surfaceView = this.surface.getView(), + surfaceModel = surfaceView.getModel(); + this.$surface + .find( '.ve-ce-documentNode' ) + .find( 'h1, h2, h3, h4, h5, h6' ) + .eq( this.section ) + .each( function() { + var headingNode = $(this).data( 'node' ); + if ( headingNode ) { + var offset = surfaceModel.getDocument().getNearestContentOffset( + headingNode.getModel().getOffset() + ); + surfaceModel.setSelection( new ve.Range( offset, offset ) ); + surfaceView.showSelection( surfaceModel.getSelection() ); + } + } ); + this.section = null; + } }; /* Inheritance */ @@ -429,7 +758,4 @@ ve.extendClass( ve.init.ViewPageTarget, ve.init.Target ); /* Initialization */ -// TODO: Clean this stuff up - -ve.config = mw.config.get( 'wgVisualEditor' ); -ve.init.current = new ve.init.ViewPageTarget(); +ve.init.viewPageTarget = new ve.init.ViewPageTarget(); diff --git a/modules/ve2/init/ve.init.Target.js b/modules/ve2/init/ve.init.Target.js index 095c16e0eb..7a4260a2a5 100644 --- a/modules/ve2/init/ve.init.Target.js +++ b/modules/ve2/init/ve.init.Target.js @@ -6,19 +6,129 @@ * @param {String} title Page title of target */ ve.init.Target = function( title ) { + // Inheritance + ve.EventEmitter.call( this ); + // Properties this.title = title; this.editToken = mw.user.tokens.get( 'editToken' ); this.apiUrl = mw.util.wikiScript( 'api' ); this.modules = ['ext.visualEditor.core']; - this.isDomLoading = false; - this.isDomSaving = false; + this.loading = false; + this.saving = false; + this.dom = null; this.isMobileDevice = ( 'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch ) ); }; +/* Static Methods */ + +/** + * Handle response to a successful load request. + * + * This method is called within the context of a target instance. If successful the DOM from the + * server will be parsed, stored in {this.dom} and then {ve.init.Target.onReady} will be called once + * the modules are ready. + * + * @static + * @method + * @param {Object} response XHR Response object + * @param {String} status Text status message + * @emits loadError (message) + */ +ve.init.Target.onLoad = function( response, status ) { + var data = response['ve-parsoid']; + if ( !data ) { + this.loading = false; + this.emit( 'loadError', 'Invalid response from server' ); + } else if ( typeof data.parsed !== 'string' ) { + this.loading = false; + this.emit( 'loadError', 'Invalid HTML content in response from server' ); + } else { + this.dom = $( '
' ).html( data.parsed )[0]; + // Everything worked, the page was loaded, continue as soon as the module is ready + mw.loader.using( this.modules, ve.proxy( ve.init.Target.onReady, this ) ); + } +}; + +/** + * Handle both DOM and modules being loaded and ready. + * + * This method is called within the context of a target instance. After the load event is emitted + * this.dom is cleared, allowing it to be garbage collected. + * + * @static + * @method + * @emits load (dom) + */ +ve.init.Target.onReady = function() { + this.loading = false; + this.emit( 'load', this.dom ); + // Release DOM data + this.dom = null; +}; + +/** + * Handle response to a successful load request. + * + * This method is called within the context of a target instance. + * + * @static + * @method + * @param {Object} response XHR Response object + * @param {String} status Text status message + * @param {Mixed} error Thrown exception or HTTP error string + * @emits load (dom) + */ +ve.init.Target.onLoadError = function( response, text, exception ) { + this.loading = false; + this.emit( 'loadError', response, text, exception ); +}; + +/** + * Handle response to a successful save request. + * + * This method is called within the context of a target instance. + * + * @static + * @method + * @param {Object} response XHR Response object + * @param {String} status Text status message + * @emits save (html) + */ +ve.init.Target.onSave = function( response, status ) { + this.saving = false; + var data = response['ve-parsoid']; + if ( !response ) { + this.emit( 'saveError', 'Invalid response from server' ); + } else if ( data.result !== 'success' ) { + this.emit( 'saveError', 'Unsuccessful request: ' + data.result ); + } else if ( typeof data.content !== 'string' ) { + this.emit( 'saveError', 'Invalid HTML content in response from server' ); + } else { + this.emit( 'save', data.content ); + } +}; + +/** + * Handle response to a successful save request. + * + * This method is called within the context of a target instance. + * + * @static + * @method + * @param {Object} data HTTP Response object + * @param {String} status Text status message + * @param {Mixed} error Thrown exception or HTTP error string + * @emits save (html) + */ +ve.init.Target.onSaveError = function( response, status, error ) { + this.saving = false; + this.emit( 'saveError', response, status, error ); +}; + /* Methods */ /** @@ -39,13 +149,13 @@ ve.init.Target = function( title ) { */ ve.init.Target.prototype.load = function( callback ) { // Prevent duplicate requests - if ( this.isDomLoading ) { + if ( this.loading ) { return false; } // Start loading the module immediately mw.loader.load( this.modules ); // Load DOM - this.isDomLoading = true; + this.loading = true; $.ajax( { 'url': this.apiUrl, 'data': { @@ -59,21 +169,8 @@ ve.init.Target.prototype.load = function( callback ) { 'cache': 'false', // Wait up to 9 seconds 'timeout': 9000, - 'error': callback, - 'success': ve.proxy( function( data ) { - this.isDomLoading = false; - var response = data['ve-parsoid']; - if ( !response ) { - callback( 'Invalid response from server' ); - } else if ( typeof response.parsed !== 'string' ) { - callback( 'Invalid HTML content in response from server' ); - } else { - // Everything worked, the page was loaded, continue as soon as the module is ready - mw.loader.using( this.modules, function() { - callback( null, $( '
' ).html( data['ve-parsoid'].parsed )[0] ); - } ); - } - }, this ) + 'error': ve.proxy( ve.init.Target.onLoadError, this ), + 'success': ve.proxy( ve.init.Target.onLoad, this ) } ); return true; }; @@ -103,11 +200,11 @@ ve.init.Target.prototype.load = function( callback ) { */ ve.init.Target.prototype.save = function( dom, options, callback ) { // Prevent duplicate requests - if ( this.isDomSaving ) { + if ( this.saving ) { return false; } // Save DOM - this.isDomSaving = true; + this.saving = true; $.ajax( { 'url': this.apiUrl, 'data': { @@ -123,21 +220,12 @@ ve.init.Target.prototype.save = function( dom, options, callback ) { }, 'dataType': 'json', 'type': 'POST', - 'error': callback, - 'success': ve.proxy( function( data ) { - this.isDomSaving = false; - var response = data['ve-parsoid']; - if ( !response ) { - callback( 'Invalid response from server' ); - } else if ( response.result !== 'success' ) { - callback( 'Unsuccessful request: ' + response.result ); - } else if ( typeof response.content !== 'string' ) { - callback( 'Invalid HTML content in response from server' ); - } else { - // Everything worked, the page was saved, continue immediately - callback( null, response.content ); - } - }, this ) + 'error': ve.proxy( ve.init.Target.onSaveError, this ), + 'success': ve.proxy( ve.init.Target.onSave, this ) } ); return true; }; + +/* Inheritance */ + +ve.extendClass( ve.init.Target, ve.EventEmitter );