Trevor Parscal has uploaded a new change for review.

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


Change subject: [WIP] Scroll into view support
......................................................................

[WIP] Scroll into view support

Scroll when needed to show highlighted (with keyboard) or selected (by any 
means) options in select widgets.

TODO: Floating toolbar and formatting drop down don't get along
TODO: Template parameters don't scroll into view when highlighted via arrow 
keys (but did at one point)

Change-Id: Ie21faa973a68f517c7cfce8bd879b5317f536365
---
M VisualEditor.php
A modules/jquery/jquery.scrollintoview.js
M modules/ve/ui/styles/ve.ui.Dialog.css
M modules/ve/ui/widgets/ve.ui.MenuWidget.js
M modules/ve/ui/widgets/ve.ui.OptionWidget.js
M modules/ve/ui/widgets/ve.ui.OutlineItemWidget.js
M modules/ve/ui/widgets/ve.ui.SearchWidget.js
M modules/ve/ve.Element.js
8 files changed, 274 insertions(+), 12 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor 
refs/changes/66/74566/1

diff --git a/VisualEditor.php b/VisualEditor.php
index 4678f5e..be9366c 100644
--- a/VisualEditor.php
+++ b/VisualEditor.php
@@ -83,6 +83,11 @@
                        'jquery/jquery.visibleText.js',
                ),
        ),
+       'jquery.scrollintoview' => $wgVisualEditorResourceTemplate + array(
+               'scripts' => array(
+                       'jquery/jquery.scrollintoview.js',
+               ),
+       ),
        'oojs' => $wgVisualEditorResourceTemplate + array(
                'scripts' => array(
                        'oojs/oo.js',
@@ -204,6 +209,7 @@
                        'jquery.client',
                        'jquery.placeholder',
                        'jquery.visibleText',
+                       'jquery.scrollintoview',
                        'mediawiki.api',
                        'mediawiki.feedback',
                        'mediawiki.jqueryMsg',
diff --git a/modules/jquery/jquery.scrollintoview.js 
b/modules/jquery/jquery.scrollintoview.js
new file mode 100644
index 0000000..6be41f6
--- /dev/null
+++ b/modules/jquery/jquery.scrollintoview.js
@@ -0,0 +1,18 @@
+/*!
+ * jQuery scrollintoview() plugin and :scrollable selector filter
+ *
+ * Version 1.8 (14 Jul 2011)
+ * Requires jQuery 1.4 or newer
+ *
+ * Copyright (c) 2011 Robert Koritnik
+ * Licensed under the terms of the MIT license
+ * http://www.opensource.org/licenses/mit-license.php
+ */
+
+(function ($) {
+       $.fn.extend({
+               
+       } );
+
+
+})(jQuery);
diff --git a/modules/ve/ui/styles/ve.ui.Dialog.css 
b/modules/ve/ui/styles/ve.ui.Dialog.css
index 30f8482..deee25a 100644
--- a/modules/ve/ui/styles/ve.ui.Dialog.css
+++ b/modules/ve/ui/styles/ve.ui.Dialog.css
@@ -157,7 +157,6 @@
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        box-sizing: border-box;
-       overflow: hidden;
 }
 
 .ve-ui-window-body .ve-ce-documentNode {
diff --git a/modules/ve/ui/widgets/ve.ui.MenuWidget.js 
b/modules/ve/ui/widgets/ve.ui.MenuWidget.js
index 163e035..1ea19d4 100644
--- a/modules/ve/ui/widgets/ve.ui.MenuWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.MenuWidget.js
@@ -28,7 +28,10 @@
        this.$previousFocus = null;
        this.isolated = !config.input;
        this.visible = false;
-       this.keydownHandler = ve.bind( this.onKeyDown, this );
+       this.onKeyDownHandler = ve.bind( this.onKeyDown, this );
+       this.onContainerScrollHandler = ve.bind( this.onContainerScroll, this );
+       this.onWindowResizeHandler = ve.bind( this.onWindowResize, this );
+       this.$scrollContainer = null;
 
        // Initialization
        this.$.hide().addClass( 've-ui-menuWidget' );
@@ -47,7 +50,8 @@
  * @param {jQuery.Event} e Key down event
  */
 ve.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
-       var handled = false,
+       var nextItem,
+               handled = false,
                highlightItem = this.getHighlightedItem();
 
        if ( !this.disabled && this.visible ) {
@@ -60,11 +64,11 @@
                                handled = true;
                                break;
                        case ve.Keys.UP:
-                               this.highlightItem( 
this.getRelativeSelectableItem( highlightItem, -1 ) );
+                               nextItem = this.getRelativeSelectableItem( 
highlightItem, -1 );
                                handled = true;
                                break;
                        case ve.Keys.DOWN:
-                               this.highlightItem( 
this.getRelativeSelectableItem( highlightItem, 1 ) );
+                               nextItem = this.getRelativeSelectableItem( 
highlightItem, 1 );
                                handled = true;
                                break;
                        case ve.Keys.ESCAPE:
@@ -75,11 +79,49 @@
                                handled = true;
                                break;
                }
+
+               if ( nextItem ) {
+                       this.highlightItem( nextItem );
+                       nextItem.scrollElementIntoView();
+               }
+
                if ( handled ) {
                        e.preventDefault();
                        e.stopPropagation();
                        return false;
                }
+       }
+};
+
+ve.ui.MenuWidget.prototype.onWindowResize = function() {
+       this.fitInScrollContainer();
+};
+
+ve.ui.MenuWidget.prototype.onContainerScroll = function() {
+       this.fitInScrollContainer();
+};
+
+// TODO: MAKE THIS STILL WORK EVEN WHEN YOU SCROLL WITH A FLOATED TOOLBAR
+// TODO: DON"T FORGET TO CHECK THE PARAMS IN THE TEMPLATE THINGY
+ve.ui.MenuWidget.prototype.fitInScrollContainer = function() {
+       var elOffset = this.$.offset(),
+               elHeight = this.$.outerHeight(),
+               elWidth = this.$.outerWidth(),
+               scOffset = this.$scrollContainer.offset() || { 'top': 0, 
'left': 0 },
+               scHeight = this.$scrollContainer.innerHeight(),
+               scWidth = this.$scrollContainer.innerWidth(),
+               xDiff = ( elWidth + elOffset.left ) - ( scWidth + scOffset.left 
),
+               yDiff = ( elHeight + elOffset.top ) - ( scHeight + scOffset.top 
);
+
+       if ( xDiff > 0 ) {
+               this.$.css( { 'overflow-x': 'auto', 'width': elWidth - xDiff } 
);
+       } else {
+               this.$.css( { 'overflow-x': '', 'width': '' } );
+       }
+       if ( yDiff > 0 ) {
+               this.$.css( { 'overflow-y': 'auto', 'height': elHeight - yDiff 
} );
+       } else {
+               this.$.css( { 'overflow-y': '', 'height': '' } );
        }
 };
 
@@ -93,7 +135,6 @@
        return this.visible;
 };
 
-
 /**
  * Bind keydown listener
  *
@@ -101,10 +142,10 @@
  */
 ve.ui.MenuWidget.prototype.bindKeydownListener = function () {
        if ( this.$input ) {
-               this.$input.on( 'keydown', this.keydownHandler );
+               this.$input.on( 'keydown', this.onKeyDownHandler );
        } else {
                // Capture menu navigation keys
-               window.addEventListener( 'keydown', this.keydownHandler, true );
+               window.addEventListener( 'keydown', this.onKeyDownHandler, true 
);
        }
 };
 
@@ -117,7 +158,7 @@
        if ( this.$input ) {
                this.$input.off( 'keydown' );
        } else {
-               window.removeEventListener( 'keydown', this.keydownHandler, 
true );
+               window.removeEventListener( 'keydown', this.onKeyDownHandler, 
true );
        }
 };
 
@@ -202,6 +243,17 @@
                        }
                        this.newItems = [];
                }
+
+               if ( !this.$scrollContainer ) {
+                       this.$scrollContainer = this.$$( 
this.getClosestScrollableElementContainer() )
+                               .on( 'scroll.ve-ui-menuWidget', 
this.onContainerScrollHandler );
+               }
+               if ( !this.$resizeWindow ) {
+                       this.$resizeWindow = this.$$( this.getElementWindow() )
+                               .on( 'resize.ve-ui-menuWidget', 
this.onWindowResizeHandler );
+               }
+
+               this.fitInScrollContainer();
        }
 
        return this;
@@ -223,5 +275,14 @@
                this.$previousFocus = null;
        }
 
+       if ( this.$scrollContainer ) {
+               this.$scrollContainer.off( '.ve-ui-menuWidget' );
+               this.$scrollContainer = null;
+       }
+       if ( this.$resizeWindow ) {
+               this.$resizeWindow.off( '.ve-ui-menuWidget' );
+               this.$resizeWindow = null;
+       }
+
        return this;
 };
diff --git a/modules/ve/ui/widgets/ve.ui.OptionWidget.js 
b/modules/ve/ui/widgets/ve.ui.OptionWidget.js
index d1d83c5..974d7ad 100644
--- a/modules/ve/ui/widgets/ve.ui.OptionWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.OptionWidget.js
@@ -68,6 +68,8 @@
 
 ve.ui.OptionWidget.static.highlightable = true;
 
+ve.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
+
 /* Methods */
 
 /**
@@ -122,6 +124,9 @@
                this.selected = !!state;
                if ( this.selected ) {
                        this.$.addClass( 've-ui-optionWidget-selected' );
+                       if ( this.constructor.static.scrollIntoViewOnSelect ) {
+                               this.scrollElementIntoView();
+                       }
                } else {
                        this.$.removeClass( 've-ui-optionWidget-selected' );
                }
diff --git a/modules/ve/ui/widgets/ve.ui.OutlineItemWidget.js 
b/modules/ve/ui/widgets/ve.ui.OutlineItemWidget.js
index fd98b0d..9bdc8a3 100644
--- a/modules/ve/ui/widgets/ve.ui.OutlineItemWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.OutlineItemWidget.js
@@ -41,6 +41,8 @@
 
 ve.ui.OutlineItemWidget.static.highlightable = false;
 
+ve.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true;
+
 ve.ui.OutlineItemWidget.static.levelClass = 've-ui-outlineItemWidget-level-';
 
 ve.ui.OutlineItemWidget.static.levels = 3;
diff --git a/modules/ve/ui/widgets/ve.ui.SearchWidget.js 
b/modules/ve/ui/widgets/ve.ui.SearchWidget.js
index e5a030d..349df90 100644
--- a/modules/ve/ui/widgets/ve.ui.SearchWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.SearchWidget.js
@@ -82,7 +82,7 @@
  * @param {jQuery.Event} e Key down event
  */
 ve.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
-       var highlightedItem,
+       var highlightedItem, nextItem,
                dir = e.which === ve.Keys.DOWN ? 1 : ( e.which === ve.Keys.UP ? 
-1 : 0 );
 
        if ( dir ) {
@@ -90,7 +90,9 @@
                if ( !highlightedItem ) {
                        highlightedItem = this.results.getSelectedItem();
                }
-               this.results.highlightItem( 
this.results.getRelativeSelectableItem( highlightedItem, dir ) );
+               nextItem = this.results.getRelativeSelectableItem( 
highlightedItem, dir );
+               this.results.highlightItem( nextItem );
+               nextItem.scrollElementIntoView();
        }
 };
 
diff --git a/modules/ve/ve.Element.js b/modules/ve/ve.Element.js
index d9238d4..cd0c303 100644
--- a/modules/ve/ve.Element.js
+++ b/modules/ve/ve.Element.js
@@ -107,7 +107,6 @@
        return doc.parentWindow || doc.defaultView;
 };
 
-
 /**
  * Get the offset between two frames.
  *
@@ -166,6 +165,156 @@
        return { 'top': from.top - to.top, 'left': from.left - to.left };
 };
 
+
+ve.Element.getBorders = function ( el ) {
+       var doc = el.ownerDocument,
+               win = doc.parentWindow || doc.defaultView,
+               sty = win && win.getComputedStyle ?
+                       win.getComputedStyle( el, null ) : el.currentStyle,
+               loc = win && win.getComputedStyle ? true : false,
+               top = parseFloat( loc ? sty.borderTopWidth : $.css( el, 
'borderTopWidth' ) ) || 0,
+               left = parseFloat( loc ? sty.borderLeftWidth : $.css( el, 
'borderLeftWidth' ) ) || 0,
+               bottom = parseFloat( loc ? sty.borderBottomWidth : $.css( el, 
'borderBottomWidth' ) ) || 0,
+               right = parseFloat( loc ? sty.borderRightWidth : $.css( el, 
'borderRightWidth' ) ) || 0;
+
+       return {
+               'top': Math.round( top ),
+               'left': Math.round( left ),
+               'bottom': Math.round( bottom ),
+               'right': Math.round( right )
+       };
+};
+
+ve.Element.getDimensions = function ( el ) {
+       var $el, $win,
+               doc = el.ownerDocument || el.document,
+               win = doc.parentWindow || doc.defaultView;
+
+       if ( win === el || el.nodeName.toLowerCase() === 'html' ) {
+               $win = $( win );
+               return {
+                       'borders': { 'top': 0, 'left': 0, 'bottom': 0, 'right': 
0 },
+                       'scroll': {
+                               'top': $win.scrollTop(),
+                               'left': $win.scrollLeft()
+                       },
+                       'scrollbar': { 'right': 0, 'bottom': 0 },
+                       'rect': {
+                               'top': 0,
+                               'left': 0,
+                               'bottom': $win.innerHeight(),
+                               'right': $win.innerWidth()
+                       }
+               };
+       } else {
+               $el = $( el );
+               return {
+                       'borders': this.getBorders( el ),
+                       'scroll': {
+                               'top': $el.scrollTop(),
+                               'left': $el.scrollLeft()
+                       },
+                       'scrollbar': {
+                               'right': $el.innerWidth() - el.clientWidth,
+                               'bottom': $el.innerHeight() - el.clientHeight
+                       },
+                       'rect': el.getBoundingClientRect()
+               };
+       }
+};
+
+/**
+ * Get closest scrollable container.
+ *
+ * Traverses up until either a scrollable element or the root is reached, in 
which case the window
+ * will be returned.
+ *
+ * @static
+ * @param {HTMLElement} el Element
+ * @return {HTMLElement|Window} Closest scrollable container
+ */
+ve.Element.getClosestScrollableContainer = function ( el, dimension ) {
+       var i, val,
+               props = [ 'overflow' ],
+               $parent = $( el ).parent();
+
+       if ( dimension === 'x' || dimension === 'y' ) {
+               props.push( 'overflow-' + dimension );
+       }
+
+       while ( $parent ) {
+               if ( $parent[0].nodeName.toLowerCase() === 'html' ) {
+                       break;
+               }
+               i = props.length;
+               while ( i-- ) {
+                       val = $parent.css( props[i] );
+                       if ( val === 'auto' || val === 'scroll' ) {
+                               return $parent[0];
+                       }
+               }
+               $parent = $parent.parent();
+       }
+       return this.getWindow( el );
+};
+
+/**
+ * Scroll element into view
+ *
+ * @static
+ * @param {Object} [config] Configuration config
+ * @param {string} [config.duration] jQuery animation duration value
+ * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' 
or 'y', omit
+ *   to scroll in both directions
+ * @param {Function} [config.complete] Function to call when scrolling 
completes
+ * @chainable
+ */
+ve.Element.scrollIntoView = function ( el, config ) {
+       // Configuration initialization
+       config = config || {};
+
+       var anim = {},
+               callback = typeof config.complete === 'function' && 
config.complete,
+               sc = this.getClosestScrollableContainer( el, config.direction ),
+               $sc = $( sc ),
+               eld = this.getDimensions( el ),
+               scd = this.getDimensions( sc ),
+               rel = {
+                       'top': eld.rect.top - ( scd.rect.top + scd.borders.top 
),
+                       'bottom': scd.rect.bottom - scd.borders.bottom - 
scd.scrollbar.bottom - eld.rect.bottom,
+                       'left': eld.rect.left - ( scd.rect.left + 
scd.borders.left ),
+                       'right': scd.rect.right - scd.borders.right - 
scd.scrollbar.right - eld.rect.right
+               };
+
+       if ( !config.direction || config.direction === 'y' ) {
+               if ( rel.top < 0 ) {
+                       anim.scrollTop = scd.scroll.top + rel.top;
+               } else if ( rel.top > 0 && rel.bottom < 0 ) {
+                       anim.scrollTop = scd.scroll.top + Math.min( rel.top, 
-rel.bottom );
+               }
+       }
+       if ( !config.direction || config.direction === 'x' ) {
+               if ( rel.left < 0 ) {
+                       anim.scrollLeft = scd.scroll.left + rel.left;
+               } else if ( rel.left > 0 && rel.right < 0 ) {
+                       anim.scrollLeft = scd.scroll.left + Math.min( rel.left, 
-rel.right );
+               }
+       }
+       if ( !ve.isEmptyObject( anim ) ) {
+               $sc.stop( true ).animate( anim, config.duration || 'fast' );
+               if ( callback ) {
+                       $sc.queue( function ( next ) {
+                               callback();
+                               next();
+                       } );
+               }
+       } else {
+               if ( callback ) {
+                       callback();
+               }
+       }
+};
+
 /* Methods */
 
 /**
@@ -199,3 +348,23 @@
 ve.Element.prototype.getElementWindow = function () {
        return ve.Element.getWindow( this.$ );
 };
+
+/**
+ * Get closest scrollable container.
+ *
+ * @method
+ * @see #static-method-getClosestScrollableContainer
+ */
+ve.Element.prototype.getClosestScrollableElementContainer = function () {
+       return ve.Element.getClosestScrollableContainer( this.$[0] );
+};
+
+/**
+ * Scroll element into view
+ *
+ * @method
+ * @see #static-method-scrollIntoView
+ */
+ve.Element.prototype.scrollElementIntoView = function ( config ) {
+       return ve.Element.scrollIntoView( this.$[0], config );
+};

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ie21faa973a68f517c7cfce8bd879b5317f536365
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Trevor Parscal <[email protected]>

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

Reply via email to