diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index c4e79e6c..4c7f54ee 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -176,6 +176,41 @@ function Timeline (container, items, options) { this.contentPanel = new Panel(contentOptions); this.mainPanel.appendChild(this.contentPanel); + // content panel (contains the vertical lines of box items) + var backgroundOptions = util.extend(Object.create(this.options), { + top: function () { + return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : ''; + }, + bottom: function () { + return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px'); + }, + left: null, + right: null, + height: function () { + return me.contentPanel.height; + }, + width: null, + className: 'background' + }); + this.backgroundPanel = new Panel(backgroundOptions); + this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel); + + // panel with axis holding the dots of item boxes + var axisPanelOptions = util.extend(Object.create(rootOptions), { + left: 0, + top: function () { + return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : ''; + }, + bottom: function () { + return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px'); + }, + width: '100%', + height: 0, + className: 'axis' + }); + this.axisPanel = new Panel(axisPanelOptions); + this.mainPanel.appendChild(this.axisPanel); + // content panel (contains itemset(s)) var sideContentOptions = util.extend(Object.create(this.options), { top: function () { @@ -398,7 +433,7 @@ Timeline.prototype.setGroups = function(groupSet) { // remove itemset if existing if (this.itemSet) { - //this.itemSet.hide(); // TODO: not so nice having to hide here + this.itemSet.hide(); // TODO: not so nice having to hide here this.contentPanel.removeChild(this.itemSet); this.itemSet.setItems(); // disconnect from itemset this.itemSet = null; @@ -406,7 +441,7 @@ Timeline.prototype.setGroups = function(groupSet) { // create new GroupSet when needed if (!this.groupSet) { - this.groupSet = new GroupSet(this.contentPanel, this.sideContentPanel, options); + this.groupSet = new GroupSet(this.contentPanel, this.sideContentPanel, this.backgroundPanel, this.axisPanel, options); this.groupSet.on('change', this.rootPanel.repaint.bind(this.rootPanel)); this.groupSet.setRange(this.range); this.groupSet.setItems(this.itemsData); @@ -428,7 +463,7 @@ Timeline.prototype.setGroups = function(groupSet) { } // create new items - this.itemSet = new ItemSet(options); + this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, options); this.itemSet.setRange(this.range); this.itemSet.setItems(this.itemsData); this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel)); @@ -504,6 +539,8 @@ Timeline.prototype.getSelection = function getSelection() { * @param {Date | Number | String} [start] Start date of visible window * @param {Date | Number | String} [end] End date of visible window */ +// TODO: implement support for setWindow({start: ..., end: ...}) +// TODO: rename setWindow to setRange? Timeline.prototype.setWindow = function setWindow(start, end) { this.range.setRange(start, end); }; @@ -512,6 +549,7 @@ Timeline.prototype.setWindow = function setWindow(start, end) { * Get the visible window * @return {{start: Date, end: Date}} Visible range */ +// TODO: rename getWindow to getRange? Timeline.prototype.getWindow = function setWindow() { var range = this.range.getRange(); return { diff --git a/src/timeline/component/CurrentTime.js b/src/timeline/component/CurrentTime.js index 089a5e13..26606207 100644 --- a/src/timeline/component/CurrentTime.js +++ b/src/timeline/component/CurrentTime.js @@ -71,8 +71,8 @@ CurrentTime.prototype.start = function start() { me.stop(); // determine interval to refresh - var scale = me.range.conversion(parent.width).scale; - var interval = 1 / scale / 2; + var scale = me.range.conversion(me.parent.width).scale; + var interval = 1 / scale / 10; if (interval < 30) interval = 30; if (interval > 1000) interval = 1000; diff --git a/src/timeline/component/Group.js b/src/timeline/component/Group.js index ed81dca6..2c4043c5 100644 --- a/src/timeline/component/Group.js +++ b/src/timeline/component/Group.js @@ -1,16 +1,20 @@ /** * @constructor Group - * @param {Element} groupFrame - * @param {Element} labelFrame + * @param {Panel} groupPanel + * @param {Panel} labelPanel + * @param {Panel} backgroundPanel + * @param {Panel} axisPanel * @param {Number | String} groupId * @param {Object} [options] Options to set initial property values * // TODO: describe available options * @extends Component */ -function Group (groupFrame, labelFrame, groupId, options) { +function Group (groupPanel, labelPanel, backgroundPanel, axisPanel, groupId, options) { this.id = util.randomUUID(); - this.groupFrame = groupFrame; - this.labelFrame = labelFrame; + this.groupPanel = groupPanel; + this.labelPanel = labelPanel; + this.backgroundPanel = backgroundPanel; + this.axisPanel = axisPanel; this.groupId = groupId; this.itemSet = null; // ItemSet @@ -87,7 +91,7 @@ Group.prototype.setItems = function setItems(itemsData) { if (this.itemSet) { // remove current item set this.itemSet.setItems(); - this.groupFrame.removeChild(this.itemSet.getFrame()); + this.groupPanel.frame.removeChild(this.itemSet.getFrame()); this.itemSet = null; } @@ -101,9 +105,10 @@ Group.prototype.setItems = function setItems(itemsData) { return Math.max(me.props.label.height, me.itemSet.height); } }); - this.itemSet = new ItemSet(itemSetOptions); + this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, itemSetOptions); this.itemSet.on('change', this.emit.bind(this, 'change')); // propagate change event - this.groupFrame.appendChild(this.itemSet.getFrame()); + this.itemSet.parent = this; + this.groupPanel.frame.appendChild(this.itemSet.getFrame()); if (this.range) this.itemSet.setRange(this.range); @@ -121,7 +126,11 @@ Group.prototype.setItems = function setItems(itemsData) { */ Group.prototype.show = function show() { if (!this.dom.label.parentNode) { - this.labelFrame.appendChild(this.dom.label); + this.labelPanel.frame.appendChild(this.dom.label); + } + + if (this.itemSet) { + this.itemSet.show(); } var itemSetFrame = this.itemSet && this.itemSet.getFrame(); @@ -129,7 +138,7 @@ Group.prototype.show = function show() { if (itemSetFrame.parentNode) { itemSetFrame.parentNode.removeChild(itemSetFrame); } - this.groupFrame.appendChild(itemSetFrame); + this.groupPanel.frame.appendChild(itemSetFrame); } }; @@ -141,6 +150,10 @@ Group.prototype.hide = function hide() { this.dom.label.parentNode.removeChild(this.dom.label); } + if (this.itemSet) { + this.itemSet.hide(); + } + var itemSetFrame = this.itemset && this.itemSet.getFrame(); if (itemSetFrame && itemSetFrame.parentNode) { itemSetFrame.parentNode.removeChild(itemSetFrame); diff --git a/src/timeline/component/GroupSet.js b/src/timeline/component/GroupSet.js index cc439c94..f57a7e23 100644 --- a/src/timeline/component/GroupSet.js +++ b/src/timeline/component/GroupSet.js @@ -2,16 +2,22 @@ * An GroupSet holds a set of groups * @param {Panel} contentPanel Panel where the ItemSets will be created * @param {Panel} labelPanel Panel where the labels will be created + * @param {Panel} backgroundPanel Panel where the vertical lines of box + * items are created + * @param {Panel} axisPanel Panel on the axis where the dots of box + * items will be created * @param {Object} [options] See GroupSet.setOptions for the available * options. * @constructor GroupSet * @extends Panel */ -function GroupSet(contentPanel, labelPanel, options) { +function GroupSet(contentPanel, labelPanel, backgroundPanel, axisPanel, options) { this.id = util.randomUUID(); this.contentPanel = contentPanel; this.labelPanel = labelPanel; + this.backgroundPanel = backgroundPanel; + this.axisPanel = axisPanel; this.options = options || {}; this.range = null; // Range or Object {start: number, end: number} @@ -64,7 +70,7 @@ GroupSet.prototype._create = function _create () { this.frame = frame; this.labelSet = new Panel({ - className: 'labelSet', + className: 'labelset', width: '100%', height: '100%' }); @@ -272,11 +278,12 @@ GroupSet.prototype.repaint = function repaint() { // TODO: do not recreate the group with every update - group = new Group(me.frame, me.labelSet.frame, id, groupOptions); + group = new Group(me, me.labelSet, me.backgroundPanel, me.axisPanel, id, groupOptions); group.on('change', me.emit.bind(me, 'change')); // propagate change event group.setRange(me.range); group.setItems(me.itemsData); // attach items data groups[id] = group; + group.parent = me; // Note: it is important to add the binding after group.setItems // is executed, because that will start an infinite loop @@ -323,7 +330,6 @@ GroupSet.prototype.repaint = function repaint() { // reposition the labels and calculate the maximum label width // TODO: labels are not displayed correctly when orientation=='top' - // TODO: width of labelPanel is not immediately updated on a change in groups var maxWidth = 0; for (id in groups) { if (groups.hasOwnProperty(id)) { @@ -333,19 +339,19 @@ GroupSet.prototype.repaint = function repaint() { } resized = util.updateProperty(this.props.labels, 'width', maxWidth) || resized; - // recalculate the height of the groupset + // recalculate the height of the groupset, and recalculate top positions of the groups var fixedHeight = (asSize(options.height) != null); var height; if (!fixedHeight) { // height is not specified, calculate the sum of the height of all groups height = 0; - for (id in this.groups) { - if (this.groups.hasOwnProperty(id)) { - group = this.groups[id]; - height += group.height; - } - } + this.groupIds.forEach(function (id) { + var group = groups[id]; + group.top = height; + if (group.itemSet) group.itemSet.top = group.top; // TODO: this is an ugly hack + height += group.height; + }); } // update classname @@ -385,7 +391,6 @@ GroupSet.prototype.hide = function hide() { /** * Show the component in the DOM (when not already visible). - * A repaint will be executed when the component is not visible * @return {Boolean} changed */ GroupSet.prototype.show = function show() { diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index d44a2527..cdb4466a 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -2,17 +2,22 @@ * An ItemSet holds a set of items and ranges which can be displayed in a * range. The width is determined by the parent of the ItemSet, and the height * is determined by the size of the items. - * @param {Object} [options] See ItemSet.setOptions for the available - * options. + * @param {Panel} backgroundPanel Panel which can be used to display the + * vertical lines of box items. + * @param {Panel} axisPanel Panel on the axis where the dots of box-items + * can be displayed. + * @param {Object} [options] See ItemSet.setOptions for the available options. * @constructor ItemSet * @extends Panel */ // TODO: improve performance by replacing all Array.forEach with a for loop -function ItemSet(options) { +function ItemSet(backgroundPanel, axisPanel, options) { this.id = util.randomUUID(); // one options object is shared by this itemset and all its items this.options = options || {}; + this.backgroundPanel = backgroundPanel; + this.axisPanel = axisPanel; this.itemOptions = Object.create(this.options); this.dom = {}; this.hammer = null; @@ -76,7 +81,7 @@ ItemSet.prototype._create = function _create(){ // create background panel var background = document.createElement('div'); background.className = 'background'; - frame.appendChild(background); + this.backgroundPanel.frame.appendChild(background); this.dom.background = background; // create foreground panel @@ -89,7 +94,7 @@ ItemSet.prototype._create = function _create(){ var axis = document.createElement('div'); axis.className = 'axis'; this.dom.axis = axis; - frame.appendChild(axis); + this.axisPanel.frame.appendChild(axis); // attach event listeners // TODO: use event listeners from the rootpanel to improve performance @@ -131,6 +136,37 @@ ItemSet.prototype._create = function _create(){ */ ItemSet.prototype.setOptions = Component.prototype.setOptions; +/** + * Hide the component from the DOM + */ +ItemSet.prototype.hide = function hide() { + // remove the axis with dots + if (this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + } + + // remove the background with vertical lines + if (this.dom.background.parentNode) { + this.dom.background.parentNode.removeChild(this.dom.background); + } +}; + +/** + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed + */ +ItemSet.prototype.show = function show() { + // show axis with dots + if (!this.dom.axis.parentNode) { + this.axisPanel.frame.appendChild(this.dom.axis); + } + + // show background with vertical lines + if (!this.dom.background.parentNode) { + this.backgroundPanel.frame.appendChild(this.dom.background); + } +}; + /** * Set range (start and end). * @param {Range | Object} range A Range or an object containing start and end. diff --git a/src/timeline/component/Panel.js b/src/timeline/component/Panel.js index 4b7378a4..84d70e7b 100644 --- a/src/timeline/component/Panel.js +++ b/src/timeline/component/Panel.js @@ -76,7 +76,14 @@ Panel.prototype.insertBefore = function (child, beforeChild) { if (frame.parentNode) { frame.parentNode.removeChild(frame); } - this.frame.appendChild(frame); + + var beforeFrame = beforeChild.getFrame(); + if (beforeFrame) { + this.frame.insertBefore(frame, beforeFrame); + } + else { + this.frame.appendChild(frame); + } } } }; diff --git a/src/timeline/component/TimeAxis.js b/src/timeline/component/TimeAxis.js index 95f71057..feaf074c 100644 --- a/src/timeline/component/TimeAxis.js +++ b/src/timeline/component/TimeAxis.js @@ -86,7 +86,7 @@ TimeAxis.prototype.repaint = function () { frame = this.frame; // update classname - frame.className = 'axis'; // TODO: add className from options if defined + frame.className = 'timeaxis'; // TODO: add className from options if defined var parent = frame.parentNode; if (parent) { @@ -114,6 +114,7 @@ TimeAxis.prototype.repaint = function () { var beforeChild = frame.nextSibling; parent.removeChild(frame); + // TODO: top/bottom positioning should be determined by options set in the Timeline, not here if (orientation == 'top') { frame.style.top = '0'; frame.style.left = '0'; diff --git a/src/timeline/component/css/groupset.css b/src/timeline/component/css/groupset.css index cf0dafb0..2d03ae87 100644 --- a/src/timeline/component/css/groupset.css +++ b/src/timeline/component/css/groupset.css @@ -1,6 +1,6 @@ /* TODO: cleanup -.vis.timeline .groupSet { +.vis.timeline .groupset { position: relative; padding: 0; margin: 0; @@ -18,7 +18,7 @@ } */ -.vis.timeline .labelSet { +.vis.timeline .labelset { position: relative; width: 100%; @@ -28,7 +28,7 @@ box-sizing: border-box; } -.vis.timeline .labelSet .vlabel { +.vis.timeline .labelset .vlabel { position: relative; left: 0; top: 0; @@ -39,21 +39,21 @@ box-sizing: border-box; } -.vis.timeline.bottom .labelSet .vlabel, +.vis.timeline.bottom .labelset .vlabel, .vis.timeline.top .vpanel.side-content, -.vis.timeline.top .itemset .axis { +.vis.timeline.top .groupset .itemset { border-top: 1px solid #bfbfbf; border-bottom: none; } -.vis.timeline.top .labelSet .vlabel, +.vis.timeline.top .labelset .vlabel, .vis.timeline.bottom .vpanel.side-content, -.vis.timeline.bottom .itemset .axis { +.vis.timeline.bottom .groupset .itemset { border-top: none; border-bottom: 1px solid #bfbfbf; } -.vis.timeline .labelSet .vlabel .inner { +.vis.timeline .labelset .vlabel .inner { display: inline-block; padding: 5px; } diff --git a/src/timeline/component/css/item.css b/src/timeline/component/css/item.css index 08c959b4..5d176738 100644 --- a/src/timeline/component/css/item.css +++ b/src/timeline/component/css/item.css @@ -90,6 +90,8 @@ -webkit-transition: height .4s ease-in-out; transition: height .4s ease-in-out; + + color: red; } .vis.timeline .item .content { diff --git a/src/timeline/component/css/itemset.css b/src/timeline/component/css/itemset.css index 52a76f81..4d21c747 100644 --- a/src/timeline/component/css/itemset.css +++ b/src/timeline/component/css/itemset.css @@ -4,6 +4,9 @@ padding: 0; margin: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + /* FIXME: get transoition working for rootpanel and itemset -webkit-transition: height 4s ease-in-out; transition: height 4s ease-in-out; @@ -17,5 +20,5 @@ } .vis.timeline .axis { - position: absolute; + overflow: visible; } diff --git a/src/timeline/component/css/timeaxis.css b/src/timeline/component/css/timeaxis.css index dd02204a..38f8ccb9 100644 --- a/src/timeline/component/css/timeaxis.css +++ b/src/timeline/component/css/timeaxis.css @@ -1,15 +1,15 @@ -.vis.timeline .axis { +.vis.timeline .timeaxis { position: absolute; } -.vis.timeline .axis .text { +.vis.timeline .timeaxis .text { position: absolute; color: #4d4d4d; padding: 3px; white-space: nowrap; } -.vis.timeline .axis .text.measure { +.vis.timeline .timeaxis .text.measure { position: absolute; padding-left: 0; padding-right: 0; @@ -18,13 +18,13 @@ visibility: hidden; } -.vis.timeline .axis .grid.vertical { +.vis.timeline .timeaxis .grid.vertical { position: absolute; width: 0; border-right: 1px solid; } -.vis.timeline .axis .grid.horizontal { +.vis.timeline .timeaxis .grid.horizontal { position: absolute; left: 0; width: 100%; @@ -32,10 +32,10 @@ border-bottom: 1px solid; } -.vis.timeline .axis .grid.minor { +.vis.timeline .timeaxis .grid.minor { border-color: #e5e5e5; } -.vis.timeline .axis .grid.major { +.vis.timeline .timeaxis .grid.major { border-color: #bfbfbf; } diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index ff87f7e9..05aa8ffb 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -39,9 +39,8 @@ ItemBox.prototype = new Item (null, null); */ ItemBox.prototype.isVisible = function isVisible (range) { // determine visibility - // TODO: account for the width of the item. Right now we add 1/4 to the window + // TODO: account for the real width of the item. Right now we just add 1/4 to the window var interval = (range.end - range.start) / 4; - interval = 0; // TODO: remove return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); }; @@ -215,17 +214,17 @@ ItemBox.prototype.repositionY = function repositionY () { box.style.top = (this.top || 0) + 'px'; box.style.bottom = ''; - line.style.top = '0px'; + line.style.top = '0'; line.style.bottom = ''; - line.style.height = this.top + 'px'; + line.style.height = (this.parent.top + this.top + 1) + 'px'; } else { // orientation 'bottom' box.style.top = ''; box.style.bottom = (this.top || 0) + 'px'; - line.style.top = ''; - line.style.bottom = '0px'; - line.style.height = this.top + 'px'; + line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px'; + line.style.bottom = '0'; + line.style.height = ''; } dot.style.top = (-this.props.dot.height / 2) + 'px'; diff --git a/test/timeline.html b/test/timeline.html index 81fae4e2..2c842cdf 100644 --- a/test/timeline.html +++ b/test/timeline.html @@ -35,6 +35,19 @@ }); }; + +
+ +
+ +