Werdna has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/95975


Change subject: Consolidate and clean up "scroll to this element" code.
......................................................................

Consolidate and clean up "scroll to this element" code.

Previously we've scrolled the top of the element to the middle of the screen
and hoped for the best. This patch refactors it into One True Scroll Into View
Method, which does the minimum amount of scrolling to bring as much of the
element as possible wholly into view, horizontally *and* vertically
(though by default it only works vertically).

Change-Id: I55a4ee7992126a4d2b3ebdae1f94cbf91c431719
---
M Resources.php
A modules/base/utils.js
M modules/discussion/forms.js
M modules/discussion/ui.js
M modules/editor/editors/ext.flow.editors.none.js
M modules/editor/editors/ext.flow.editors.visualeditor.js
M modules/editor/ext.flow.editor.js
7 files changed, 172 insertions(+), 17 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow 
refs/changes/75/95975/1

diff --git a/Resources.php b/Resources.php
index cf2a657..1d01cd4 100644
--- a/Resources.php
+++ b/Resources.php
@@ -25,6 +25,7 @@
                        'base/ext.flow.base.js',
                        'base/ui-functions.js',
                        'base/init.js',
+                       'base/utils.js',
                ),
                'dependencies' => array(
                        'mediawiki.ui',
diff --git a/modules/base/utils.js b/modules/base/utils.js
new file mode 100644
index 0000000..aaecd4f
--- /dev/null
+++ b/modules/base/utils.js
@@ -0,0 +1,139 @@
+( function( $, mw ) {
+       /**
+        * Scrolls the $parent element such that this element is in view
+        * Based on ideas from jQuery Cookbook, Lindley (2009) pp. 144
+        * @param  {jQuery}  $parent          The object to scroll. Optional, 
defaults to whole window.
+        * @param  {Object} animateDirections Which directions to consider.
+        * Object of the form {X : true|false, Y : true|false}.
+        * Optional, default is Y direction only.
+        * @param  {Object} animateOptions    Stuff to pass to animate's 
options parameter
+        * @return jQuery                     The current jQuery object, for 
chaining.
+        */
+       $.fn.scrollIntoView = function( $parent, animateDirections, 
animateOptions ) {
+               var $scrollItem,
+                       $viewportItem;
+               if ( ! $parent ) {
+                       $scrollItem = $( 'body, html' );
+                       $viewportItem = $( window );
+               } else {
+                       $scrollItem = $viewportItem = $parent;
+               }
+
+               if ( ! animateDirections ) {
+                       animateDirections = {};
+               }
+
+               if ( ! animateOptions ) {
+                       animateOptions = {};
+               }
+
+               animateDirections = $.extend(
+                               {
+                                       'X' : false,
+                                       'Y' : true
+                               },
+                               animateDirections
+                       );
+
+               var $element = $( this ),
+                       elementRect = {
+                               minX : $element.offset().left,
+                               minY : $element.offset().top,
+                               maxX : $element.offset().left + 
$element.width(),
+                               maxY : $element.offset().top + $element.height()
+                       },
+                       viewportRect = {
+                               minX : $scrollItem.scrollLeft(),
+                               minY : $scrollItem.scrollTop(),
+                               maxX : $scrollItem.scrollLeft() + 
$viewportItem.width(),
+                               maxY : $scrollItem.scrollTop() + 
$viewportItem.height()
+                       },
+                       oversizeX = $element.width() - $viewportItem.width(),
+                       oversizeY = $element.height() - $viewportItem.height();
+
+               // Internal function that is called for both X and Y positions, 
which does all the magic
+               var getNewPosition = function( elementMin, elementMax, 
viewportMin, viewportMax ) {
+                       // How many pixels too big is this element for the 
viewport?
+                       var oversize = ( elementMax - elementMin ) - ( 
viewportMax - viewportMin );
+
+                       if ( oversize < 0 ) {
+                               // Sort of a hack:
+                               // if the element isn't oversized, then give us 
a bit of leeway
+                               // at the bottom to allow for debug toolbar, etc
+                               oversize = -50;
+                       }
+
+                       if ( elementMin >= viewportMin && elementMax <= 
viewportMax ) {
+                               // Simple case: element is already within 
viewport in this direction
+                               // Do nothing
+                               return viewportMin;
+                       } else if ( elementMax > viewportMax ) {
+                               // Element's maximum edge exceeds the viewport
+                               // Move the viewport forward by the amount of 
the overflow,
+                               // but subtract out amount that the element 
exceeds the viewport
+                               // size back in.
+                               var overflow = elementMax - viewportMax;
+
+                               return viewportMin + overflow - oversize;
+                       } else if ( elementMin < viewportMin ) {
+                               // Element's minimum edge is before the start 
of the viewport
+                               // Just set the viewport to the start of the 
element
+                               return elementMin;
+                       }
+               };
+
+               var animateTarget = {};
+
+               if ( animateDirections.X ) {
+                       var XPos = getNewPosition( elementRect.minX, 
elementRect.maxX, viewportRect.minX, viewportRect.maxX );
+                       if ( XPos != viewportRect.minX ) {
+                               animateTarget.scrollLeft = XPos;
+                       }
+               }
+
+               if ( animateDirections.Y ) {
+                       var YPos = getNewPosition( elementRect.minY, 
elementRect.maxY, viewportRect.minY, viewportRect.maxY );
+                       if ( YPos != viewportRect.minY ) {
+                               animateTarget.scrollTop = YPos;
+                       }
+               }
+
+               if (
+                       animateTarget.scrollTop !== undefined ||
+                       animateTarget.scrollLeft !== undefined
+               ) {
+                       $scrollItem.animate( animateTarget, animateOptions );
+               } else if ( extraAnimation.complete ) {
+                       $scrollItem.each( function() {
+                               extraAnimation.complete.apply( this );
+                       } );
+               }
+
+               return $element;
+       };
+
+       /**
+        * Set the selection in a textarea
+        * With thanks to Mark on StackOverflow.
+        * This part is licensed under CC-BY-SA.
+        * <http://stackoverflow.com/a/841121/1552547>
+        * @param  int start Start position in characters to select
+        * @param  int end   End position in characters to select (optional)
+        * @return jQuery    The jQuery object passed in, for chaining.
+        */
+       $.fn.selectRange = function(start, end) {
+           if( !end ) end = start;
+           return this.each( function() {
+               if ( this.setSelectionRange ) {
+                   this.focus();
+                   this.setSelectionRange( start, end );
+               } else if ( this.createTextRange ) {
+                   var range = this.createTextRange();
+                   range.collapse( true );
+                   range.moveEnd( 'character', end );
+                   range.moveStart( 'character', start );
+                   range.select();
+               }
+           } );
+       };
+} )( jQuery, mediaWiki );
diff --git a/modules/discussion/forms.js b/modules/discussion/forms.js
index e4f7927..6d963d5 100644
--- a/modules/discussion/forms.js
+++ b/modules/discussion/forms.js
@@ -76,9 +76,7 @@
 
                                        $newRegion.slideDown();
 
-                                       $mainContainer.animate({
-                                               scrollTop: 
$newRegion.offset().top - $mainContainer.height() / 2
-                                       });
+                                       $newRegion.scrollIntoView();
                                } );
                }
        );
diff --git a/modules/discussion/ui.js b/modules/discussion/ui.js
index 8d8205e..9caed77 100644
--- a/modules/discussion/ui.js
+++ b/modules/discussion/ui.js
@@ -49,7 +49,6 @@
                                e.preventDefault();
 
                                var $formContainer,
-                                       $viewport = $( 'main, html' ),
                                        username = '',
                                        defaultContent = '';
 
@@ -82,14 +81,15 @@
 
                                $textarea = $formContainer.find( 'textarea' );
                                $textarea
-                                       .focus()
                                        .removeClass( 'flow-reply-box-closed' );
                                mw.flow.editor.load( $textarea, defaultContent, 
'wikitext' );
 
                                // Scroll to the form
-                               $viewport.animate( {
-                                       'scrollTop' : 
$formContainer.offset().top - $viewport.height() / 2
-                               }, 500 );
+                               $formContainer.scrollIntoView( null, null, {
+                                       'complete' : function() {
+                                               mw.flow.editor.focus( $textarea 
);
+                                       }
+                               } );
                        } );
 
                $( '<a />' )
@@ -218,13 +218,12 @@
                                e.stopPropagation();
 
                                $hideElement.slideDown( function() {
-                                       var $viewport = $( 'html,body' ),
-                                               $replyContainer = $( 
'#flow-topic-reply-' + $( self ).data( 'topic-id' ) );
+                                               var $replyContainer = $( 
'#flow-topic-reply-' + $( self ).data( 'topic-id' ) ),
+                                                       $viewport = 
$replyContainer.offsetParent();
 
-                                       $viewport.animate( {
-                                               'scrollTop': 
$replyContainer.offset().top - $viewport.height()/2,
+                                       $replyContainer.scrollIntoView( null, 
null, {
                                                'complete': 
$replyContainer.find( '.flow-topic-reply-content' ).click()
-                                       }, 500 );
+                                       } );
                                } );
                        }
                );
@@ -308,15 +307,13 @@
                        .prependTo( $container.find( 
'.flow-post-content-allowed' ) );
 
                var highlightPost = function( $elem ) {
-                       var $viewport = $( 'main, html' );
+                       var $viewport = $elem.offsetParent();
 
                        $container.find( '.flow-post-highlighted' 
).removeClass( 'flow-post-highlighted' );
                        $elem
                                .closest( '.flow-post-container' )
                                .addClass( 'flow-post-highlighted' );
-                       $viewport.animate( {
-                               'scrollTop' : $elem.offset().top - 
$viewport.height()/2
-                       }, 500 );
+                       $elem.scrollIntoView();
                };
 
                if ( window.location.hash ) {
diff --git a/modules/editor/editors/ext.flow.editors.none.js 
b/modules/editor/editors/ext.flow.editors.none.js
index b9b30e6..4188418 100644
--- a/modules/editor/editors/ext.flow.editors.none.js
+++ b/modules/editor/editors/ext.flow.editors.none.js
@@ -65,4 +65,10 @@
                        $( this ).animate( { height: scrollHeight + padding }, 
50 );
                }
        };
+
+       mw.flow.editors.none.prototype.focus = function() {
+               return this.$node
+                       .focus()
+                       .selectRange( this.$node.val().length );
+       };
 } ( jQuery, mediaWiki ) );
diff --git a/modules/editor/editors/ext.flow.editors.visualeditor.js 
b/modules/editor/editors/ext.flow.editors.visualeditor.js
index 1d19cad..4af4056 100644
--- a/modules/editor/editors/ext.flow.editors.visualeditor.js
+++ b/modules/editor/editors/ext.flow.editors.visualeditor.js
@@ -131,4 +131,8 @@
        mw.flow.editors.visualeditor.isSupported = function() {
                return mw.user.options.get( 'visualeditor-enable' ) ? true : 
false;
        };
+
+       mw.flow.editors.visualeditor.focus = function() {
+               this.target.surface.$.find( '.ve-ce-documentNode' ).focus();
+       };
 } ( jQuery, mediaWiki ) );
diff --git a/modules/editor/ext.flow.editor.js 
b/modules/editor/ext.flow.editor.js
index 8f27ef3..9c72fc6 100644
--- a/modules/editor/ext.flow.editor.js
+++ b/modules/editor/ext.flow.editor.js
@@ -161,6 +161,16 @@
                 */
                getEditor: function ( $node ) {
                        return mw.flow.editor.editors[$node.data( 'flow-editor' 
)];
+               },
+
+               focus: function( $node ) {
+                       var editor = mw.flow.editor.getEditor( $node );
+
+                       if ( editor.focus ) {
+                               editor.focus();
+                       } else {
+                               $node.focus();
+                       }
                }
        };
        $( mw.flow.editor.init );

-- 
To view, visit https://gerrit.wikimedia.org/r/95975
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I55a4ee7992126a4d2b3ebdae1f94cbf91c431719
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Werdna <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to