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