From 51a8977b3a0f05494028c3bafb3ae7cc4ff79f4e Mon Sep 17 00:00:00 2001 From: Ian Oberst Date: Sat, 30 Sep 2017 12:16:02 -0700 Subject: [PATCH] Vertical focus (#3504) * - Added support for vertical scrolling while the timeline is focusing on an element, both for animated and non-animated calls of focus * - Adjusted item offset calculations to use the item parent - Turned on animation for the focus in the example - Updated function documentation * - Fixing lint issues * - Added documentation for the new 'frameCallback' parameter of 'setRange' - Fixed the documentation on 'setRange' for the 'callback' parameter - Fixed code not meeting style guidelines * - Updated the example for "setSelection" to be more clear about what the example buttons do. Focus the language to be more consistent with that fact that the demo uses "setSelection" --- .../timeline/interaction/setSelection.html | 108 ++++++++++++++++++ lib/timeline/Range.js | 14 ++- lib/timeline/Timeline.js | 101 +++++++++++++++- 3 files changed, 218 insertions(+), 5 deletions(-) diff --git a/examples/timeline/interaction/setSelection.html b/examples/timeline/interaction/setSelection.html index 36278797..45901bf1 100644 --- a/examples/timeline/interaction/setSelection.html +++ b/examples/timeline/interaction/setSelection.html @@ -26,6 +26,21 @@

+
+

If the height of the timeline is limited some items may be vertically offscreen. This demo uses Timeline.setSelection(ids, {focus: true}) and demonstrates that focusing on an item will +cause the timeline to scroll vertically to the item that is being focused on. You can use the buttons below select a random item either above or below the currently selected item. +

+ + +
+ +

If focusing on multiple items only the first item will be scrolled to. Try entering several ids and hitting select.

+

+Select item(s):
+

+ +
+ \ No newline at end of file diff --git a/lib/timeline/Range.js b/lib/timeline/Range.js index 96662559..6de8c64a 100644 --- a/lib/timeline/Range.js +++ b/lib/timeline/Range.js @@ -187,11 +187,15 @@ Range.prototype.stopRolling = function() { * function is 'easeInOutQuad'. * {boolean} [byUser=false] * {Event} event Mouse event - * {Function} a callback funtion to be executed at the end of this function - * + * @param {Function} callback a callback function to be executed at the end of this function + * @param {Function} frameCallback a callback function executed each frame of the range animation. + * The callback will be passed three parameters: + * {number} easeCoefficient an easing coefficent + * {boolean} willDraw If true the caller will redraw after the callback completes + * {boolean} done If true then animation is ending after the current frame */ -Range.prototype.setRange = function(start, end, options, callback) { +Range.prototype.setRange = function(start, end, options, callback, frameCallback) { if (!options) { options = {}; } @@ -238,7 +242,9 @@ Range.prototype.setRange = function(start, end, options, callback) { event: options.event }; - if (changed) { + if (frameCallback) { frameCallback(ease, changed, done); } + + if (changed) { me.body.emitter.emit('rangechange', params); } diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index d98d3529..375a6248 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -392,13 +392,68 @@ Timeline.prototype.focus = function(id, options) { } }); + if (start !== null && end !== null) { + var me = this; + // Use the first item for the vertical focus + var item = this.itemSet.items[ids[0]]; + var startPos = this._getScrollTop() * -1; + var initialVerticalScroll = null; + + // Setup a handler for each frame of the vertical scroll + var verticalAnimationFrame = function(ease, willDraw, done) { + var verticalScroll = getItemVerticalScroll(me, item); + + if(!initialVerticalScroll) { + initialVerticalScroll = verticalScroll; + } + + if(initialVerticalScroll.itemTop == verticalScroll.itemTop && !initialVerticalScroll.shouldScroll) { + return; // We don't need to scroll, so do nothing + } + else if(initialVerticalScroll.itemTop != verticalScroll.itemTop && verticalScroll.shouldScroll) { + // The redraw shifted elements, so reset the animation to correct + initialVerticalScroll = verticalScroll; + startPos = me._getScrollTop() * -1; + } + + var from = startPos; + var to = initialVerticalScroll.scrollOffset; + var scrollTop = done ? to : (from + (to - from) * ease); + + me._setScrollTop(-scrollTop); + + if(!willDraw) { + me._redraw(); + } + }; + + // Perform one last check at the end to make sure the final vertical + // position is correct + var finalVerticalCallback = function() { + var finalVerticalScroll = getItemVerticalScroll(me, item); + + if(finalVerticalScroll.shouldScroll && finalVerticalScroll.itemTop != initialVerticalScroll.itemTop) { + me._setScrollTop(-finalVerticalScroll.scrollOffset); + me._redraw(); + } + }; + // calculate the new middle and interval for the window var middle = (start + end) / 2; var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1); var animation = (options && options.animation !== undefined) ? options.animation : true; - this.range.setRange(middle - interval / 2, middle + interval / 2, { animation: animation }); + + if(!animation) { + // We aren't animating so set a default so that the final callback forces the vertical location + initialVerticalScroll = {shouldScroll: false, scrollOffset: -1, itemTop: -1}; + } + + this.range.setRange(middle - interval / 2, middle + interval / 2, { animation: animation }, finalVerticalCallback, verticalAnimationFrame); + + // Let the redraw settle and finalize the position + setTimeout(finalVerticalCallback, 100); } }; @@ -448,6 +503,50 @@ function getEnd(item) { return util.convert(end, 'Date').valueOf(); } +/** + * @param {vis.Timeline} timeline + * @param {vis.Item} item + * @return {{shouldScroll: bool, scrollOffset: number, itemTop: number}} + */ +function getItemVerticalScroll(timeline, item) { + var leftHeight = timeline.props.leftContainer.height; + var contentHeight = timeline.props.left.height; + + var group = item.parent; + var offset = group.top; + var shouldScroll = true; + var orientation = timeline.timeAxis.options.orientation.axis; + + var itemTop = function () { + if (orientation == "bottom") { + return group.height - item.top - item.height; + } + else { + return item.top; + } + }; + + var currentScrollHeight = timeline._getScrollTop() * -1; + var targetOffset = offset + itemTop(); + var height = item.height; + + if (targetOffset < currentScrollHeight) { + if (offset + leftHeight <= offset + itemTop() + height) { + offset += itemTop() - timeline.itemSet.options.margin.item.vertical; + } + } + else if (targetOffset + height > currentScrollHeight + leftHeight) { + offset += itemTop() + height - leftHeight + timeline.itemSet.options.margin.item.vertical; + } + else { + shouldScroll = false; + } + + offset = Math.min(offset, contentHeight - leftHeight); + + return { shouldScroll: shouldScroll, scrollOffset: offset, itemTop: targetOffset }; +} + /** * Determine the range of the items, taking into account their actual width * and a margin of 10 pixels on both sides.