Browse Source

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"
jittering-top
Ian Oberst 7 years ago
committed by Yotam Berkowitz
parent
commit
51a8977b3a
3 changed files with 218 additions and 5 deletions
  1. +108
    -0
      examples/timeline/interaction/setSelection.html
  2. +10
    -4
      lib/timeline/Range.js
  3. +100
    -1
      lib/timeline/Timeline.js

+ 108
- 0
examples/timeline/interaction/setSelection.html View File

@ -26,6 +26,21 @@
</p> </p>
<div id="visualization"></div> <div id="visualization"></div>
<br/>
<p>If the height of the timeline is limited some items may be vertically offscreen. This demo uses <code>Timeline.setSelection(ids, {focus: true})</code> 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.
</p>
<button id="prevFocus">Select Item Above</button>
<button id="nextFocus">Select Item Below</button>
<br/>
<p>If focusing on multiple items only the first item will be scrolled to. Try entering several ids and hitting <em>select</em>.</p>
<p>
Select item(s): <input type="text" id="selectionVertical" value="g1_5, g2_3"><input type="button" id="selectVertical" value="Select"><br>
</p>
<div id="vertical-visualization"></div>
<script> <script>
// create a dataset with items // create a dataset with items
// we specify the type of the fields `start` and `end` here to be strings // we specify the type of the fields `start` and `end` here to be strings
@ -61,6 +76,99 @@
}); });
timeline.setSelection(ids, {focus: focus.checked}); timeline.setSelection(ids, {focus: focus.checked});
}; };
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive
};
// Vertical scroll example
var groups = [];
var items = [];
var groupItems = {};
for (var g = 0; g < 10; g++) {
groups.push({
id: g,
content: "Group " + g
});
groupItems[g] = [];
for (var i = 0; i < 30; i++) {
items.push({
id: "g" + g + "_" + i,
content: "g" + g + "_" + i,
group: g,
start: "2014-" + (g + 1) + "-" + getRandomInt(1, 20)
});
groupItems[g].push(items[items.length - 1]);
}
}
var container2 = document.getElementById('vertical-visualization');
var options = {
editable: false,
stack: true,
height: 300,
verticalScroll: true,
groupOrder: 'id'
};
var timeline2 = new vis.Timeline(container2, items, groups, options);
var groupIndex = 0;
var itemIndex = 0;
var moveToItem = function(direction) {
itemIndex += direction;
groupIndex += direction;
if (groupIndex < 0) {
groupIndex = groups.length - 1;
} else if (groupIndex >= groups.length) {
groupIndex = 0;
}
var items = groupItems[groupIndex];
if (itemIndex < 0) {
itemIndex = items.length - 1;
} else if (itemIndex >= items.length) {
itemIndex = 0;
}
var id = items[itemIndex].id;
timeline2.setSelection(id, {focus: true});
}
var nextFocus = document.getElementById('nextFocus');
var prevFocus = document.getElementById('prevFocus');
var selectionVertical = document.getElementById('selectionVertical');
var selectVertical = document.getElementById('selectVertical');
selectVertical.onclick = function () {
var ids = selectionVertical.value.split(',').map(function (value) {
return value.trim();
});
timeline2.setSelection(ids, {focus: focus.checked});
};
nextFocus.onclick = function() {
moveToItem(1);
};
prevFocus.onclick = function() {
moveToItem(-1);
};
// Set the initial focus
setTimeout(function() {
moveToItem(0);
}, 500);
</script> </script>
</body> </body>
</html> </html>

+ 10
- 4
lib/timeline/Range.js View File

@ -187,11 +187,15 @@ Range.prototype.stopRolling = function() {
* function is 'easeInOutQuad'. * function is 'easeInOutQuad'.
* {boolean} [byUser=false] * {boolean} [byUser=false]
* {Event} event Mouse event * {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) { if (!options) {
options = {}; options = {};
} }
@ -238,7 +242,9 @@ Range.prototype.setRange = function(start, end, options, callback) {
event: options.event event: options.event
}; };
if (changed) {
if (frameCallback) { frameCallback(ease, changed, done); }
if (changed) {
me.body.emitter.emit('rangechange', params); me.body.emitter.emit('rangechange', params);
} }

+ 100
- 1
lib/timeline/Timeline.js View File

@ -392,13 +392,68 @@ Timeline.prototype.focus = function(id, options) {
} }
}); });
if (start !== null && end !== null) { 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 // calculate the new middle and interval for the window
var middle = (start + end) / 2; var middle = (start + end) / 2;
var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1); var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1);
var animation = (options && options.animation !== undefined) ? options.animation : true; 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(); 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 * Determine the range of the items, taking into account their actual width
* and a margin of 10 pixels on both sides. * and a margin of 10 pixels on both sides.

Loading…
Cancel
Save