diff --git a/src/timeline/TimeStep.js b/src/timeline/TimeStep.js index fc56b518..b22a1f37 100644 --- a/src/timeline/TimeStep.js +++ b/src/timeline/TimeStep.js @@ -281,35 +281,38 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) { }; /** - * Snap a date to a rounded value. The snap intervals are dependent on the - * current scale and step. - * @param {Date} date the date to be snapped + * Snap a date to a rounded value. + * The snap intervals are dependent on the current scale and step. + * @param {Date} date the date to be snapped. + * @return {Date} snappedDate */ TimeStep.prototype.snap = function(date) { + var clone = new Date(date.valueOf()); + if (this.scale == TimeStep.SCALE.YEAR) { - var year = date.getFullYear() + Math.round(date.getMonth() / 12); - date.setFullYear(Math.round(year / this.step) * this.step); - date.setMonth(0); - date.setDate(0); - date.setHours(0); - date.setMinutes(0); - date.setSeconds(0); - date.setMilliseconds(0); + var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); + clone.setFullYear(Math.round(year / this.step) * this.step); + clone.setMonth(0); + clone.setDate(0); + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); } else if (this.scale == TimeStep.SCALE.MONTH) { - if (date.getDate() > 15) { - date.setDate(1); - date.setMonth(date.getMonth() + 1); + if (clone.getDate() > 15) { + clone.setDate(1); + clone.setMonth(clone.getMonth() + 1); // important: first set Date to 1, after that change the month. } else { - date.setDate(1); + clone.setDate(1); } - date.setHours(0); - date.setMinutes(0); - date.setSeconds(0); - date.setMilliseconds(0); + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); } else if (this.scale == TimeStep.SCALE.DAY || this.scale == TimeStep.SCALE.WEEKDAY) { @@ -317,56 +320,58 @@ TimeStep.prototype.snap = function(date) { switch (this.step) { case 5: case 2: - date.setHours(Math.round(date.getHours() / 24) * 24); break; + clone.setHours(Math.round(clone.getHours() / 24) * 24); break; default: - date.setHours(Math.round(date.getHours() / 12) * 12); break; + clone.setHours(Math.round(clone.getHours() / 12) * 12); break; } - date.setMinutes(0); - date.setSeconds(0); - date.setMilliseconds(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); } else if (this.scale == TimeStep.SCALE.HOUR) { switch (this.step) { case 4: - date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; + clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break; default: - date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; + clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break; } - date.setSeconds(0); - date.setMilliseconds(0); + clone.setSeconds(0); + clone.setMilliseconds(0); } else if (this.scale == TimeStep.SCALE.MINUTE) { //noinspection FallthroughInSwitchStatementJS switch (this.step) { case 15: case 10: - date.setMinutes(Math.round(date.getMinutes() / 5) * 5); - date.setSeconds(0); + clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); + clone.setSeconds(0); break; case 5: - date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; + clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break; default: - date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; + clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break; } - date.setMilliseconds(0); + clone.setMilliseconds(0); } else if (this.scale == TimeStep.SCALE.SECOND) { //noinspection FallthroughInSwitchStatementJS switch (this.step) { case 15: case 10: - date.setSeconds(Math.round(date.getSeconds() / 5) * 5); - date.setMilliseconds(0); + clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); + clone.setMilliseconds(0); break; case 5: - date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break; default: - date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break; } } else if (this.scale == TimeStep.SCALE.MILLISECOND) { var step = this.step > 5 ? this.step / 2 : 1; - date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); + clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step); } + + return clone; }; /** diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 8cc98c9f..6fff7245 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -13,6 +13,7 @@ function Timeline (container, items, options) { autoResize: true, editable: true, selectable: true, + snap: null, // will be specified after timeaxis is created min: null, max: null, @@ -24,7 +25,7 @@ function Timeline (container, items, options) { showMinorLabels: true, showMajorLabels: true, showCurrentTime: false, - showCustomTime: false, + showCustomTime: false }; // controller @@ -119,6 +120,7 @@ function Timeline (container, items, options) { this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions); this.timeaxis.setRange(this.range); this.controller.add(this.timeaxis); + this.options.snap = this.timeaxis.snap.bind(this.timeaxis); // current time bar this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions); @@ -211,26 +213,26 @@ Timeline.prototype.setItems = function(items) { var initialLoad = (this.itemsData == null); // convert to type DataSet when needed - var newItemSet; + var newDataSet; if (!items) { - newItemSet = null; + newDataSet = null; } else if (items instanceof DataSet) { - newItemSet = items; + newDataSet = items; } if (!(items instanceof DataSet)) { - newItemSet = new DataSet({ + newDataSet = new DataSet({ convert: { start: 'Date', end: 'Date' } }); - newItemSet.add(items); + newDataSet.add(items); } // set items - this.itemsData = newItemSet; - this.content.setItems(newItemSet); + this.itemsData = newDataSet; + this.content.setItems(newDataSet); if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) { // apply the data range as range diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 381f098a..aa5c1cb3 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -106,6 +106,9 @@ ItemSet.types = { * {Number} padding * Padding of the contents of an item in pixels. * Must correspond with the items css. Default is 5. + * {Function} snap + * Function to let items snap to nice dates when + * dragging items. */ ItemSet.prototype.setOptions = Component.prototype.setOptions; @@ -688,9 +691,9 @@ ItemSet.prototype._onDragStart = function (event) { me = this; if (item && item.selected) { - var dragLeftItem = event.target.dragLeftItem; var dragRightItem = event.target.dragRightItem; + if (dragLeftItem) { this.touchParams.itemProps = [{ item: dragLeftItem, @@ -710,8 +713,12 @@ ItemSet.prototype._onDragStart = function (event) { item: item }; - if ('start' in item.data) { props.start = item.data.start.valueOf() } - if ('end' in item.data) { props.end = item.data.end.valueOf() } + if ('start' in item.data) { + props.start = item.data.start.valueOf() + } + if ('end' in item.data) { + props.end = item.data.end.valueOf() + } return props; }); @@ -728,17 +735,22 @@ ItemSet.prototype._onDragStart = function (event) { */ ItemSet.prototype._onDrag = function (event) { if (this.touchParams.itemProps) { - var deltaX = event.gesture.deltaX, + var snap = this.options.snap || null, + deltaX = event.gesture.deltaX, offset = deltaX / this.conversion.scale; // move this.touchParams.itemProps.forEach(function (props) { - if ('start' in props) { props.item.data.start = new Date(props.start + offset); } - if ('end' in props) { props.item.data.end = new Date(props.end + offset); } + if ('start' in props) { + var start = new Date(props.start + offset); + props.item.data.start = snap ? snap(start) : start; + } + if ('end' in props) { + var end = new Date(props.end + offset); + props.item.data.end = snap ? snap(end) : end; + } }); - // TODO: implement snapping to nice dates - // TODO: implement dragging from one group to another this.requestReflow(); @@ -754,24 +766,35 @@ ItemSet.prototype._onDrag = function (event) { */ ItemSet.prototype._onDragEnd = function (event) { if (this.touchParams.itemProps) { - // prepare a changeset for the changed items - var changes = this.touchParams.itemProps.map(function (props) { + // prepare a change set for the changed items + var changes = []; + this.touchParams.itemProps.forEach(function (props) { var change = { id: props.item.id }; - if ('start' in props.item.data) { change.start = props.item.data.start; } - if ('end' in props.item.data) { change.end = props.item.data.end; } - - // TODO: only fire changes when start or end is actually changed + var changed = false; + if ('start' in props.item.data) { + changed = (props.start != props.item.data.start.valueOf()); + change.start = props.item.data.start; + } + if ('end' in props.item.data) { + changed = changed || (props.end != props.item.data.end.valueOf()); + change.end = props.item.data.end; + } - return change; + // only add changes when start or end is actually changed + if (changed) { + changes.push(change); + } }); this.touchParams.itemProps = null; - // apply the changes to the data - var dataset = this._myDataSet(); - dataset.update(changes); + // apply the changes to the data (if there are changes) + if (changes.length) { + var dataset = this._myDataSet(); + dataset.update(changes); + } event.stopPropagation(); } diff --git a/src/timeline/component/TimeAxis.js b/src/timeline/component/TimeAxis.js index dadb37d2..cfee216d 100644 --- a/src/timeline/component/TimeAxis.js +++ b/src/timeline/component/TimeAxis.js @@ -520,3 +520,13 @@ TimeAxis.prototype._updateConversion = function() { this.conversion = Range.conversion(range.start, range.end, this.width); } }; + +/** + * Snap a date to a rounded value. + * The snap intervals are dependent on the current scale and step. + * @param {Date} date the date to be snapped. + * @return {Date} snappedDate + */ +TimeAxis.prototype.snap = function snap (date) { + return this.step.snap(date); +};