From b889a5057d027e8c94ac98f10b44c908dc1009d6 Mon Sep 17 00:00:00 2001 From: yotamberk Date: Fri, 21 Oct 2016 22:21:09 +0300 Subject: [PATCH] Add a vertical scroll option for timeline [solves #273, #1060, #466] (#2196) * Add initial scroller without options * Add initial scroll without an option * Add verticalScroll option * Fix scrollbar positions * Add docs * fix example * remove jquery dependency * Fix example * Fix review comments --- docs/timeline/index.html | 8 ++ examples/timeline/other/verticalScroll.html | 92 ++++++++++++++ lib/timeline/Core.js | 126 +++++++++++++------- lib/timeline/Timeline.js | 2 +- lib/timeline/component/ItemSet.js | 17 ++- lib/timeline/component/css/panel.css | 23 +++- lib/timeline/optionsTimeline.js | 1 + lib/util.js | 26 ++++ 8 files changed, 249 insertions(+), 46 deletions(-) create mode 100644 examples/timeline/other/verticalScroll.html diff --git a/docs/timeline/index.html b/docs/timeline/index.html index c45440ed..7cc75507 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -1024,6 +1024,14 @@ function (option, path) { + + verticalScroll + Boolean + false + Show a vertical scroll on the side of the group list. + + + width String or Number diff --git a/examples/timeline/other/verticalScroll.html b/examples/timeline/other/verticalScroll.html new file mode 100644 index 00000000..07a68ad4 --- /dev/null +++ b/examples/timeline/other/verticalScroll.html @@ -0,0 +1,92 @@ + + + Timeline | Vertical Scroll Option + + + + + + + + + +

Timeline vertical scroll option

+ +

With +verticalScroll: true, +zoomKey: 'ctrlKey' +

+
+ +

With +horizontalScroll: true, +verticalScroll: true, +zoomKey: 'ctrlKey' +

+
+ + + + diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index 76ed050b..43f336e2 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -82,7 +82,6 @@ Core.prototype._create = function (container) { this.dom.centerContainer.appendChild(this.dom.center); this.dom.leftContainer.appendChild(this.dom.left); this.dom.rightContainer.appendChild(this.dom.right); - this.dom.centerContainer.appendChild(this.dom.shadowTop); this.dom.centerContainer.appendChild(this.dom.shadowBottom); this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); @@ -90,9 +89,26 @@ Core.prototype._create = function (container) { this.dom.rightContainer.appendChild(this.dom.shadowTopRight); this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; + this.on('rangechange', function () { if (this.initialDrawDone === true) { - this._redraw(); // this allows overriding the _redraw method + this._redraw(); } }.bind(this)); this.on('touch', this._onTouch.bind(this)); @@ -154,15 +170,15 @@ Core.prototype._create = function (container) { }.bind(this)); function onMouseWheel(event) { - if (me.isActive()) { - me.emit('mousewheel', event); + if (this.isActive()) { + this.emit('mousewheel', event); } // prevent scrolling when zoomKey defined or activated - if (!me.options.zoomKey || event[me.options.zoomKey]) return + if (!this.options.zoomKey || event[this.options.zoomKey]) return // prevent scrolling vertically when horizontalScroll is true - if (me.options.horizontalScroll) return + if (this.options.horizontalScroll) return var delta = 0; if (event.wheelDelta) { /* IE/Opera. */ @@ -173,12 +189,12 @@ Core.prototype._create = function (container) { delta = -event.detail / 3; } - var current = me.props.scrollTop; + var current = this.props.scrollTop; var adjusted = current + delta * 120; - if (me.isActive()) { - me._setScrollTop(adjusted); - me._redraw(); - me.emit('scroll', event); + if (this.isActive()) { + this._setScrollTop(adjusted); + this._redraw(); + this.emit('scroll', event); } // Prevent default actions caused by mouse wheel @@ -188,30 +204,27 @@ Core.prototype._create = function (container) { if (this.dom.root.addEventListener) { // IE9, Chrome, Safari, Opera - this.dom.root.addEventListener("mousewheel", onMouseWheel, false); + this.dom.root.addEventListener("mousewheel", onMouseWheel.bind(this), false); // Firefox - this.dom.root.addEventListener("DOMMouseScroll", onMouseWheel, false); + this.dom.root.addEventListener("DOMMouseScroll", onMouseWheel.bind(this), false); } else { // IE 6/7/8 - this.dom.root.attachEvent("onmousewheel", onMouseWheel); + this.dom.root.attachEvent("onmousewheel", onMouseWheel.bind(this)); } - // size properties of each of the panels - this.props = { - root: {}, - background: {}, - centerContainer: {}, - leftContainer: {}, - rightContainer: {}, - center: {}, - left: {}, - right: {}, - top: {}, - bottom: {}, - border: {}, - scrollTop: 0, - scrollTopMin: 0 - }; + function onMouseScrollSide(event) { + var current = this.scrollTop; + var adjusted = -current; + if (me.isActive()) { + me._setScrollTop(adjusted); + + me._redraw(); + me.emit('scroll', event); + } + } + + this.dom.left.parentNode.addEventListener('scroll', onMouseScrollSide); + this.dom.right.parentNode.addEventListener('scroll', onMouseScrollSide); this.customTimes = []; @@ -257,17 +270,23 @@ Core.prototype.setOptions = function (options) { var fields = [ 'width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates', - 'locale', 'locales', 'moment', 'rtl', 'zoomKey', 'horizontalScroll' + 'locale', 'locales', 'moment', 'rtl', 'zoomKey', 'horizontalScroll', 'verticalScroll' ]; util.selectiveExtend(fields, this.options, options); if (this.options.rtl) { - var contentContainer = this.dom.leftContainer; - this.dom.leftContainer = this.dom.rightContainer; - this.dom.rightContainer = contentContainer; this.dom.container.style.direction = "rtl"; - this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical-rtl'; } + this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical-rtl'; + } + + if (this.options.verticalScroll) { + if (this.options.rtl) { + this.dom.rightContainer.className = 'vis-panel vis-right vis-vertical-scroll'; + } else { + this.dom.leftContainer.className = 'vis-panel vis-left vis-vertical-scroll'; + } + } this.options.orientation = {item:undefined,axis:undefined}; if ('orientation' in options) { @@ -740,9 +759,25 @@ Core.prototype._redraw = function() { // calculate the widths of the panels props.root.width = dom.root.offsetWidth; props.background.width = props.root.width - borderRootWidth; - props.left.width = dom.leftContainer.clientWidth || -props.border.left; + + if (!this.initialDrawDone) { + props.scrollbarWidth = util.getScrollBarWidth(); + } + + if (this.options.verticalScroll) { + if (this.options.rtl) { + props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.right.width = dom.rightContainer.clientWidth + props.scrollbarWidth || -props.border.right; + } else { + props.left.width = dom.leftContainer.clientWidth + props.scrollbarWidth || -props.border.left; + props.right.width = dom.rightContainer.clientWidth || -props.border.right; + } + } else { + props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.right.width = dom.rightContainer.clientWidth || -props.border.right; + } + props.leftContainer.width = props.left.width; - props.right.width = dom.rightContainer.clientWidth || -props.border.right; props.rightContainer.width = props.right.width; var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; props.center.width = centerWidth; @@ -796,10 +831,8 @@ Core.prototype._redraw = function() { dom.center.style.left = '0'; dom.center.style.top = offset + 'px'; dom.left.style.left = '0'; - dom.left.style.top = offset + 'px'; dom.right.style.left = '0'; - dom.right.style.top = offset + 'px'; - + // show shadows when vertical scrolling is available var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; @@ -810,6 +843,18 @@ Core.prototype._redraw = function() { dom.shadowTopRight.style.visibility = visibilityTop; dom.shadowBottomRight.style.visibility = visibilityBottom; + if (this.options.verticalScroll) { + this.dom.shadowTopRight.style.visibility = "hidden"; + this.dom.shadowBottomRight.style.visibility = "hidden"; + this.dom.shadowTopLeft.style.visibility = "hidden"; + this.dom.shadowBottomLeft.style.visibility = "hidden"; + document.getElementsByClassName('vis-left')[0].scrollTop = -offset; + document.getElementsByClassName('vis-right')[0].scrollTop = -offset; + } else { + dom.left.style.top = offset + 'px'; + dom.right.style.top = offset + 'px'; + } + // enable/disable vertical panning var contentsOverflow = this.props.center.height > this.props.centerContainer.height; this.hammer.get('pan').set({ @@ -832,6 +877,7 @@ Core.prototype._redraw = function() { } else { this.redrawCount = 0; } + this.initialDrawDone = true; //Emit public 'changed' event for UI updates, see issue #1592 diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index a8e55e92..a397498e 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -52,7 +52,6 @@ function Timeline (container, items, groups, options) { axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' item: 'bottom' // not relevant }, - rtl: false, moment: moment, width: null, @@ -61,6 +60,7 @@ function Timeline (container, items, groups, options) { minHeight: null }; this.options = util.deepExtend({}, this.defaultOptions); + this.options.rtl = options.rtl; // Create the DOM, props, and emitter this._create(container); diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 77912821..9eb46393 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -27,7 +27,6 @@ var BACKGROUND = '__background__'; // reserved group id for background items wit function ItemSet(body, options) { this.body = body; this.defaultOptions = { - rtl: false, type: null, // 'box', 'point', 'range', 'background' orientation: { item: 'bottom' // item orientation: 'top' or 'bottom' @@ -96,7 +95,8 @@ function ItemSet(body, options) { // options is shared by this ItemSet and all its items this.options = util.extend({}, this.defaultOptions); - + this.options.rtl = options.rtl; + // options for getting items from the DataSet with the correct type this.itemOptions = { type: {start: 'Date', end: 'Date'} @@ -230,7 +230,12 @@ ItemSet.prototype._create = function(){ // add item on doubletap this.hammer.on('doubletap', this._onAddItem.bind(this)); - this.groupHammer = new Hammer(this.body.dom.leftContainer); + + if (this.options.rtl) { + this.groupHammer = new Hammer(this.body.dom.rightContainer); + } else { + this.groupHammer = new Hammer(this.body.dom.leftContainer); + } this.groupHammer.on('panstart', this._onGroupDragStart.bind(this)); this.groupHammer.on('panmove', this._onGroupDrag.bind(this)); @@ -451,7 +456,11 @@ ItemSet.prototype.show = function() { // show labelset containing labels if (!this.dom.labelSet.parentNode) { - this.body.dom.left.appendChild(this.dom.labelSet); + if (this.options.rtl) { + this.body.dom.right.appendChild(this.dom.labelSet); + } else { + this.body.dom.left.appendChild(this.dom.labelSet); + } } }; diff --git a/lib/timeline/component/css/panel.css b/lib/timeline/component/css/panel.css index 4c5088a9..a02ee5bc 100644 --- a/lib/timeline/component/css/panel.css +++ b/lib/timeline/component/css/panel.css @@ -1,4 +1,3 @@ - .vis-panel { position: absolute; @@ -24,6 +23,28 @@ overflow: hidden; } +.vis-left.vis-panel.vis-vertical-scroll, .vis-right.vis-panel.vis-vertical-scroll { + height: 100%; + overflow-x: hidden; + overflow-y: scroll; +} + +.vis-left.vis-panel.vis-vertical-scroll { + direction: rtl; +} + +.vis-left.vis-panel.vis-vertical-scroll .vis-content { + direction: ltr; +} + +.vis-right.vis-panel.vis-vertical-scroll { + direction: ltr; +} + +.vis-right.vis-panel.vis-vertical-scroll .vis-content { + direction: rtl; +} + .vis-panel.vis-center, .vis-panel.vis-top, .vis-panel.vis-bottom { diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index bfcac84b..4c976d85 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -26,6 +26,7 @@ let allOptions = { //globals : align: {string}, rtl: {boolean, 'undefined': 'undefined'}, + verticalScroll: {boolean, 'undefined': 'undefined'}, horizontalScroll: {boolean, 'undefined': 'undefined'}, autoResize: {boolean}, clickToUse: {boolean}, diff --git a/lib/util.js b/lib/util.js index c1dc1c21..f9506a6a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1452,3 +1452,29 @@ exports.easingFunctions = { return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t } }; + +exports.getScrollBarWidth = function () { + var inner = document.createElement('p'); + inner.style.width = "100%"; + inner.style.height = "200px"; + + var outer = document.createElement('div'); + outer.style.position = "absolute"; + outer.style.top = "0px"; + outer.style.left = "0px"; + outer.style.visibility = "hidden"; + outer.style.width = "200px"; + outer.style.height = "150px"; + outer.style.overflow = "hidden"; + outer.appendChild (inner); + + document.body.appendChild (outer); + var w1 = inner.offsetWidth; + outer.style.overflow = 'scroll'; + var w2 = inner.offsetWidth; + if (w1 == w2) w2 = outer.clientWidth; + + document.body.removeChild (outer); + + return (w1 - w2); +}; \ No newline at end of file