diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 82e69e92..33b0571d 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -355,6 +355,7 @@ Timeline.prototype.setGroups = function(groups) { // create new GroupSet this.groupSet = new GroupSet(this.labelPanel, options); + this.groupSet.on('change', this.rootPanel.repaint.bind(this.rootPanel, 'changes')); this.groupSet.setRange(this.range); this.groupSet.setItems(this.itemsData); this.groupSet.setGroups(this.groupsData); @@ -373,6 +374,7 @@ Timeline.prototype.setGroups = function(groups) { this.itemSet = new ItemSet(options); this.itemSet.setRange(this.range); this.itemSet.setItems(this.itemsData); + this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel)); this.contentPanel.appendChild(this.itemSet); } }; diff --git a/src/timeline/component/Component.js b/src/timeline/component/Component.js index 412b76cd..b8737264 100644 --- a/src/timeline/component/Component.js +++ b/src/timeline/component/Component.js @@ -14,6 +14,9 @@ function Component () { this.height = 0; } +// Turn the Component into an event emitter +Emitter(Component.prototype); + /** * Set parameters for the frame. Parameters will be merged in current parameter * set. @@ -72,9 +75,11 @@ Component.prototype.getFrame = function getFrame() { /** * Repaint the component + * @return {boolean} Returns true if the component is resized */ Component.prototype.repaint = function repaint() { // should be implemented by the component + return false; }; /** @@ -104,3 +109,18 @@ Component.prototype.show = function show() { return false; } }; + +/** + * Test whether the component is resized since the last time _isResized() was + * called. + * @return {Boolean} Returns true if the component is resized + * @private + */ +Component.prototype._isResized = function _isResized() { + var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height); + + this._previousWidth = this.width; + this._previousHeight = this.height; + + return resized; +}; diff --git a/src/timeline/component/CurrentTime.js b/src/timeline/component/CurrentTime.js index a39e3330..d141afeb 100644 --- a/src/timeline/component/CurrentTime.js +++ b/src/timeline/component/CurrentTime.js @@ -32,7 +32,7 @@ CurrentTime.prototype.getContainer = function () { /** * Repaint the component - * @return {Boolean} changed + * @return {boolean} Returns true if the component is resized */ CurrentTime.prototype.repaint = function () { var bar = this.frame, diff --git a/src/timeline/component/CustomTime.js b/src/timeline/component/CustomTime.js index 09a55575..f75fec73 100644 --- a/src/timeline/component/CustomTime.js +++ b/src/timeline/component/CustomTime.js @@ -20,8 +20,6 @@ function CustomTime (options) { CustomTime.prototype = new Component(); -Emitter(CustomTime.prototype); - CustomTime.prototype.setOptions = Component.prototype.setOptions; /** @@ -35,7 +33,7 @@ CustomTime.prototype.getContainer = function () { /** * Repaint the component - * @return {Boolean} changed + * @return {boolean} Returns true if the component is resized */ CustomTime.prototype.repaint = function () { var bar = this.frame, diff --git a/src/timeline/component/Group.js b/src/timeline/component/Group.js index c166d4ce..78a3d2ec 100644 --- a/src/timeline/component/Group.js +++ b/src/timeline/component/Group.js @@ -63,6 +63,7 @@ Group.prototype.setItems = function setItems(items) { var itemSetOptions = Object.create(this.options); this.itemSet = new ItemSet(itemSetOptions); + this.itemSet.on('change', this.emit.bind(this, 'change')); // propagate change event if (this.range) this.itemSet.setRange(this.range); this.contentPanel.appendChild(this.itemSet); @@ -113,10 +114,10 @@ Group.prototype.getSelection = function getSelection() { /** * Repaint the group - * @return {Boolean} changed + * @return {boolean} Returns true if the component is resized */ Group.prototype.repaint = function repaint() { - this.itemSet.repaint(); + var resized = this.itemSet.repaint(); this.top = this.itemSet ? this.itemSet.top : 0; this.height = this.itemSet ? this.itemSet.height : 0; @@ -133,4 +134,6 @@ Group.prototype.repaint = function repaint() { this.props.label.width = 0; this.props.label.height = 0; } + + return resized; }; diff --git a/src/timeline/component/GroupSet.js b/src/timeline/component/GroupSet.js index d3a90d1e..60594c38 100644 --- a/src/timeline/component/GroupSet.js +++ b/src/timeline/component/GroupSet.js @@ -79,6 +79,7 @@ GroupSet.prototype.setItems = function setItems(items) { for (var id in this.groups) { if (this.groups.hasOwnProperty(id)) { var group = this.groups[id]; + // TODO: every group will emit a change event, causing a lot of unnecessary repaints. improve this. group.setItems(items); } } @@ -147,6 +148,8 @@ GroupSet.prototype.setGroups = function setGroups(groups) { ids = this.groupsData.getIds(); this._onAdd(ids); } + + this.emit('change'); }; /** @@ -244,6 +247,8 @@ GroupSet.prototype.repaint = function repaint() { groups = this.groups, groupsData = this.groupsData; + this.queue = {}; // clear old queue, we have a copy here + // show/hide added/changed/removed groups var ids = Object.keys(queue); if (ids.length) { @@ -262,15 +267,18 @@ GroupSet.prototype.repaint = function repaint() { }); group = new Group(me, me.labelPanel, 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; + + // Note: it is important to add the binding after group.setItems + // is executed, because that will start an infinite loop + // as this call will already triger a } // TODO: update group data group.data = groupsData.get(id); - - delete queue[id]; break; case 'remove': @@ -279,8 +287,6 @@ GroupSet.prototype.repaint = function repaint() { delete groups[id]; } - // update lists - delete queue[id]; break; default: @@ -306,7 +312,7 @@ GroupSet.prototype.repaint = function repaint() { // repaint all groups in order this.groupIds.forEach(function (id) { - group = groups[id].repaint(); + groups[id].repaint(); }); // reposition the labels and calculate the maximum label width @@ -479,6 +485,8 @@ GroupSet.prototype._toQueue = function _toQueue(ids, action) { ids.forEach(function (id) { queue[id] = action; }); + + this.emit('change'); }; /** diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index bf37050a..74881565 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -45,6 +45,7 @@ function ItemSet(options) { this.selection = []; // list with the ids of all selected nodes this.queue = {}; // queue with id/actions: 'add', 'update', 'delete' this.stack = new Stack(Object.create(this.options)); + this.stackDirty = true; // if true, all items will be restacked on next repaint this.conversion = null; this.touchParams = {}; // stores properties while dragging @@ -164,11 +165,9 @@ ItemSet.prototype._deselect = function _deselect(id) { /** * Repaint the component - * @param {boolean} [force=false] If true, all items will be re-stacked. - * If false (default), only items having a - * top===null will be re-stacked + * @return {boolean} Returns true if the component is resized */ -ItemSet.prototype.repaint = function repaint(force) { +ItemSet.prototype.repaint = function repaint() { var asSize = util.option.asSize, asString = util.option.asString, options = this.options, @@ -225,7 +224,6 @@ ItemSet.prototype.repaint = function repaint(force) { var visibleInterval = this.range.end - this.range.start; var zoomed = (this.visibleInterval != visibleInterval); this.visibleInterval = visibleInterval; - force = force || zoomed; /* TODO: implement+fix smarter way to update visible items // find the first visible item @@ -293,7 +291,9 @@ ItemSet.prototype.repaint = function repaint(force) { // reposition visible items vertically //this.stack.order(this.visibleItems); // TODO: solve ordering issue + var force = this.stackDirty || zoomed; // force re-stacking of all items if true this.stack.stack(this.visibleItems, force); + this.stackDirty = false; for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { this.visibleItems[i].repositionY(); } @@ -340,7 +340,7 @@ ItemSet.prototype.repaint = function repaint(force) { this.dom.axis.style.top = (orientation == 'top') ? '0' : ''; this.dom.axis.style.bottom = (orientation == 'top') ? '' : '0'; - return false; + return this._isResized(); }; /** @@ -505,7 +505,9 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) { }); this._order(); - this.repaint(); + + this.stackDirty = true; // force re-stacking of all items next repaint + this.emit('change'); }; /** @@ -534,9 +536,9 @@ ItemSet.prototype._onRemove = function _onRemove(ids) { }); if (count) { - var force = true; // force restacking of all items this._order(); - this.repaint(force); + this.stackDirty = true; // force re-stacking of all items next repaint + this.emit('change'); } }; @@ -682,8 +684,8 @@ ItemSet.prototype._onDrag = function (event) { // TODO: implement dragging from one group to another - var force = true; // force restacking of all items - this.repaint(force); // TODO: must repaint the rootPanel instead + this.stackDirty = true; // force re-stacking of all items next repaint + this.emit('change'); event.stopPropagation(); } @@ -726,8 +728,9 @@ ItemSet.prototype._onDragEnd = function (event) { // restore original values if ('start' in props) props.item.data.start = props.start; if ('end' in props) props.item.data.end = props.end; - var force = true; // force restacking of all items - me.repaint(force); + + this.stackDirty = true; // force re-stacking of all items next repaint + this.emit('change'); } }); } diff --git a/src/timeline/component/Panel.js b/src/timeline/component/Panel.js index 12f2cf27..d7c02bb2 100644 --- a/src/timeline/component/Panel.js +++ b/src/timeline/component/Panel.js @@ -75,6 +75,7 @@ Panel.prototype.removeChild = function (child) { /** * Repaint the component + * @return {boolean} Returns true if the component was resized since previous repaint */ Panel.prototype.repaint = function () { var asString = util.option.asString, @@ -97,20 +98,25 @@ Panel.prototype.repaint = function () { frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : ''); // repaint the child components - this._repaintChilds(); + var childsResized = this._repaintChilds(); // update frame size this._updateSize(); + + return this._isResized() || childsResized; }; /** * Repaint all childs of the panel + * @return {boolean} Returns true if the component is resized * @private */ Panel.prototype._repaintChilds = function () { + var resized = false; for (var i = 0, ii = this.childs.length; i < ii; i++) { - this.childs[i].repaint(); + resized = this.childs[i].repaint() || resized; } + return resized; }; /** diff --git a/src/timeline/component/RootPanel.js b/src/timeline/component/RootPanel.js index 41ae7bfc..d8f61901 100644 --- a/src/timeline/component/RootPanel.js +++ b/src/timeline/component/RootPanel.js @@ -18,9 +18,6 @@ function RootPanel(container, options) { RootPanel.prototype = new Panel(); -// turn RootPanel into an event emitter -Emitter(RootPanel.prototype); - /** * Set options. Will extend the current options. * @param {Object} [options] Available parameters: @@ -48,7 +45,7 @@ RootPanel.prototype.setOptions = function (options) { }; /** - * Repaint the component + * Repaint the root panel */ RootPanel.prototype.repaint = function () { // create frame @@ -87,11 +84,18 @@ RootPanel.prototype.repaint = function () { this.frame.className = className; // repaint the child components - this._repaintChilds(); + var childsResized = this._repaintChilds(); // update frame size this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, ''); this._updateSize(); + + // if the root panel or any of its childs is resized, repaint again, + // as other components may need to be resized accordingly + var resized = this._isResized() || childsResized; + if (resized) { + setTimeout(this.repaint.bind(this), 0); + } }; /** diff --git a/src/timeline/component/TimeAxis.js b/src/timeline/component/TimeAxis.js index d4cf9045..17df2f37 100644 --- a/src/timeline/component/TimeAxis.js +++ b/src/timeline/component/TimeAxis.js @@ -59,6 +59,7 @@ TimeAxis.prototype.setRange = function (range) { /** * Repaint the component + * @return {boolean} Returns true if the component is resized */ TimeAxis.prototype.repaint = function () { var asSize = util.option.asSize, @@ -137,6 +138,8 @@ TimeAxis.prototype.repaint = function () { parent.appendChild(frame) } } + + return this._isResized(); }; /**