UNPKG

49 kBJavaScriptView Raw
1import Emitter from 'emitter-component';
2import Hammer from '../module/hammer';
3import hammerUtil from '../hammerUtil';
4import util from 'vis-util';
5import TimeAxis from './component/TimeAxis';
6import Activator from '../shared/Activator';
7import DateUtil from './DateUtil';
8import CustomTime from './component/CustomTime';
9
10import './component/css/animation.css';
11import './component/css/currenttime.css';
12import './component/css/panel.css';
13import './component/css/pathStyles.css';
14import './component/css/timeline.css';
15import '../shared/bootstrap.css';
16
17/**
18 * Create a timeline visualization
19 * @constructor Core
20 */
21function Core () {}
22
23// turn Core into an event emitter
24Emitter(Core.prototype);
25
26/**
27 * Create the main DOM for the Core: a root panel containing left, right,
28 * top, bottom, content, and background panel.
29 * @param {Element} container The container element where the Core will
30 * be attached.
31 * @protected
32 */
33Core.prototype._create = function (container) {
34 this.dom = {};
35
36 this.dom.container = container;
37 this.dom.container.style.position = 'relative';
38
39 this.dom.root = document.createElement('div');
40 this.dom.background = document.createElement('div');
41 this.dom.backgroundVertical = document.createElement('div');
42 this.dom.backgroundHorizontal = document.createElement('div');
43 this.dom.centerContainer = document.createElement('div');
44 this.dom.leftContainer = document.createElement('div');
45 this.dom.rightContainer = document.createElement('div');
46 this.dom.center = document.createElement('div');
47 this.dom.left = document.createElement('div');
48 this.dom.right = document.createElement('div');
49 this.dom.top = document.createElement('div');
50 this.dom.bottom = document.createElement('div');
51 this.dom.shadowTop = document.createElement('div');
52 this.dom.shadowBottom = document.createElement('div');
53 this.dom.shadowTopLeft = document.createElement('div');
54 this.dom.shadowBottomLeft = document.createElement('div');
55 this.dom.shadowTopRight = document.createElement('div');
56 this.dom.shadowBottomRight = document.createElement('div');
57 this.dom.rollingModeBtn = document.createElement('div');
58 this.dom.loadingScreen = document.createElement('div');
59
60 this.dom.root.className = 'vis-timeline';
61 this.dom.background.className = 'vis-panel vis-background';
62 this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical';
63 this.dom.backgroundHorizontal.className = 'vis-panel vis-background vis-horizontal';
64 this.dom.centerContainer.className = 'vis-panel vis-center';
65 this.dom.leftContainer.className = 'vis-panel vis-left';
66 this.dom.rightContainer.className = 'vis-panel vis-right';
67 this.dom.top.className = 'vis-panel vis-top';
68 this.dom.bottom.className = 'vis-panel vis-bottom';
69 this.dom.left.className = 'vis-content';
70 this.dom.center.className = 'vis-content';
71 this.dom.right.className = 'vis-content';
72 this.dom.shadowTop.className = 'vis-shadow vis-top';
73 this.dom.shadowBottom.className = 'vis-shadow vis-bottom';
74 this.dom.shadowTopLeft.className = 'vis-shadow vis-top';
75 this.dom.shadowBottomLeft.className = 'vis-shadow vis-bottom';
76 this.dom.shadowTopRight.className = 'vis-shadow vis-top';
77 this.dom.shadowBottomRight.className = 'vis-shadow vis-bottom';
78 this.dom.rollingModeBtn.className = 'vis-rolling-mode-btn';
79 this.dom.loadingScreen.className = 'vis-loading-screen';
80
81 this.dom.root.appendChild(this.dom.background);
82 this.dom.root.appendChild(this.dom.backgroundVertical);
83 this.dom.root.appendChild(this.dom.backgroundHorizontal);
84 this.dom.root.appendChild(this.dom.centerContainer);
85 this.dom.root.appendChild(this.dom.leftContainer);
86 this.dom.root.appendChild(this.dom.rightContainer);
87 this.dom.root.appendChild(this.dom.top);
88 this.dom.root.appendChild(this.dom.bottom);
89 this.dom.root.appendChild(this.dom.rollingModeBtn);
90
91 this.dom.centerContainer.appendChild(this.dom.center);
92 this.dom.leftContainer.appendChild(this.dom.left);
93 this.dom.rightContainer.appendChild(this.dom.right);
94 this.dom.centerContainer.appendChild(this.dom.shadowTop);
95 this.dom.centerContainer.appendChild(this.dom.shadowBottom);
96 this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
97 this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
98 this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
99 this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
100
101 // size properties of each of the panels
102 this.props = {
103 root: {},
104 background: {},
105 centerContainer: {},
106 leftContainer: {},
107 rightContainer: {},
108 center: {},
109 left: {},
110 right: {},
111 top: {},
112 bottom: {},
113 border: {},
114 scrollTop: 0,
115 scrollTopMin: 0
116 };
117
118 this.on('rangechange', function () {
119 if (this.initialDrawDone === true) {
120 this._redraw();
121 }
122 }.bind(this));
123 this.on('rangechanged', function () {
124 if (!this.initialRangeChangeDone) {
125 this.initialRangeChangeDone = true;
126 }
127 }.bind(this));
128 this.on('touch', this._onTouch.bind(this));
129 this.on('panmove', this._onDrag.bind(this));
130
131 var me = this;
132 this._origRedraw = this._redraw.bind(this);
133 this._redraw = util.throttle(this._origRedraw);
134
135 this.on('_change', function (properties) {
136 if (me.itemSet && me.itemSet.initialItemSetDrawn && properties && properties.queue == true) {
137 me._redraw()
138 } else {
139 me._origRedraw();
140 }
141 });
142
143 // create event listeners for all interesting events, these events will be
144 // emitted via emitter
145 this.hammer = new Hammer(this.dom.root);
146 var pinchRecognizer = this.hammer.get('pinch').set({enable: true});
147 pinchRecognizer && hammerUtil.disablePreventDefaultVertically(pinchRecognizer);
148 this.hammer.get('pan').set({threshold:5, direction: Hammer.DIRECTION_HORIZONTAL});
149 this.listeners = {};
150
151 var events = [
152 'tap', 'doubletap', 'press',
153 'pinch',
154 'pan', 'panstart', 'panmove', 'panend'
155 // TODO: cleanup
156 //'touch', 'pinch',
157 //'tap', 'doubletap', 'hold',
158 //'dragstart', 'drag', 'dragend',
159 //'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
160 ];
161 events.forEach(function (type) {
162 var listener = function (event) {
163 if (me.isActive()) {
164 me.emit(type, event);
165 }
166 };
167 me.hammer.on(type, listener);
168 me.listeners[type] = listener;
169 });
170
171 // emulate a touch event (emitted before the start of a pan, pinch, tap, or press)
172 hammerUtil.onTouch(this.hammer, function (event) {
173 me.emit('touch', event);
174 }.bind(this));
175
176 // emulate a release event (emitted after a pan, pinch, tap, or press)
177 hammerUtil.onRelease(this.hammer, function (event) {
178 me.emit('release', event);
179 }.bind(this));
180
181 /**
182 *
183 * @param {WheelEvent} event
184 */
185 function onMouseWheel(event) {
186
187 // Reasonable default wheel deltas
188 const LINE_HEIGHT = 40;
189 const PAGE_HEIGHT = 800;
190
191 if (this.isActive()) {
192 this.emit('mousewheel', event);
193 }
194
195 // deltaX and deltaY normalization from jquery.mousewheel.js
196 var deltaX = 0;
197 var deltaY = 0;
198
199 // Old school scrollwheel delta
200 if ( 'detail' in event ) { deltaY = event.detail * -1; }
201 if ( 'wheelDelta' in event ) { deltaY = event.wheelDelta; }
202 if ( 'wheelDeltaY' in event ) { deltaY = event.wheelDeltaY; }
203 if ( 'wheelDeltaX' in event ) { deltaX = event.wheelDeltaX * -1; }
204
205 // Firefox < 17 horizontal scrolling related to DOMMouseScroll event
206 if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
207 deltaX = deltaY * -1;
208 deltaY = 0;
209 }
210
211 // New school wheel delta (wheel event)
212 if ( 'deltaY' in event ) {
213 deltaY = event.deltaY * -1;
214 }
215 if ( 'deltaX' in event ) {
216 deltaX = event.deltaX;
217 }
218
219 // Normalize deltas
220 if (event.deltaMode) {
221 if (event.deltaMode === 1) { // delta in LINE units
222 deltaX *= LINE_HEIGHT;
223 deltaY *= LINE_HEIGHT;
224 } else { // delta in PAGE units
225 deltaX *= LINE_HEIGHT;
226 deltaY *= PAGE_HEIGHT;
227 }
228 }
229
230 // Prevent scrolling when zooming (no zoom key, or pressing zoom key)
231 if (!this.options.zoomKey || event[this.options.zoomKey]) return;
232
233 // Don't preventDefault if you can't scroll
234 if (!this.options.verticalScroll && !this.options.horizontalScroll) return;
235
236 // Prevent default actions caused by mouse wheel
237 // (else the page and timeline both scroll)
238 event.preventDefault();
239
240 if (this.options.verticalScroll && Math.abs(deltaY) >= Math.abs(deltaX)) {
241 var current = this.props.scrollTop;
242 var adjusted = current + deltaY;
243
244 if (this.isActive()) {
245 this._setScrollTop(adjusted);
246 this._redraw();
247 this.emit('scroll', event);
248 }
249 } else if (this.options.horizontalScroll) {
250 var delta = Math.abs(deltaX) >= Math.abs(deltaY) ? deltaX : deltaY;
251
252 // calculate a single scroll jump relative to the range scale
253 var diff = (delta / 120) * (this.range.end - this.range.start) / 20;
254 // calculate new start and end
255 var newStart = this.range.start + diff;
256 var newEnd = this.range.end + diff;
257
258 var options = {
259 animation: false,
260 byUser: true,
261 event: event
262 };
263 this.range.setRange(newStart, newEnd, options);
264 }
265 }
266
267 // Add modern wheel event listener
268 if (this.dom.centerContainer.addEventListener) {
269 const wheel = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel"
270 document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel"
271 "DOMMouseScroll"; // Older Firefox versions like "DOMMouseScroll"
272 this.dom.centerContainer.addEventListener(wheel, onMouseWheel.bind(this), false);
273 } else {
274 // IE 6/7/8
275 this.dom.centerContainer.attachEvent("onmousewheel", onMouseWheel.bind(this));
276 }
277
278 /**
279 *
280 * @param {scroll} event
281 */
282 function onMouseScrollSide(event) {
283 if (!me.options.verticalScroll) return;
284 event.preventDefault();
285 if (me.isActive()) {
286 var adjusted = -event.target.scrollTop;
287 me._setScrollTop(adjusted);
288 me._redraw();
289 me.emit('scrollSide', event);
290 }
291 }
292
293 this.dom.left.parentNode.addEventListener('scroll', onMouseScrollSide.bind(this));
294 this.dom.right.parentNode.addEventListener('scroll', onMouseScrollSide.bind(this));
295
296 var itemAddedToTimeline = false;
297
298 /**
299 *
300 * @param {dragover} event
301 * @returns {boolean}
302 */
303 function handleDragOver(event) {
304 if (event.preventDefault) {
305 event.preventDefault(); // Necessary. Allows us to drop.
306 }
307
308 // make sure your target is a vis element
309 if (!event.target.className.indexOf("vis") > -1) return;
310
311 // make sure only one item is added every time you're over the timeline
312 if (itemAddedToTimeline) return;
313
314 event.dataTransfer.dropEffect = 'move';
315 itemAddedToTimeline = true;
316 return false;
317 }
318
319 /**
320 *
321 * @param {drop} event
322 * @returns {boolean}
323 */
324 function handleDrop(event) {
325 // prevent redirect to blank page - Firefox
326 if(event.preventDefault) { event.preventDefault(); }
327 if(event.stopPropagation) { event.stopPropagation(); }
328 // return when dropping non-vis items
329 try {
330 var itemData = JSON.parse(event.dataTransfer.getData("text"))
331 if (!itemData || !itemData.content) return
332 } catch (err) {
333 return false;
334 }
335
336 itemAddedToTimeline = false;
337 event.center = {
338 x: event.clientX,
339 y: event.clientY
340 };
341
342 if (itemData.target !== 'item') {
343 me.itemSet._onAddItem(event);
344 } else {
345 me.itemSet._onDropObjectOnItem(event);
346 }
347 me.emit('drop', me.getEventProperties(event))
348 return false;
349 }
350
351 this.dom.center.addEventListener('dragover', handleDragOver.bind(this), false);
352 this.dom.center.addEventListener('drop', handleDrop.bind(this), false);
353
354 this.customTimes = [];
355
356 // store state information needed for touch events
357 this.touch = {};
358
359 this.redrawCount = 0;
360 this.initialDrawDone = false;
361 this.initialRangeChangeDone = false;
362
363 // attach the root panel to the provided container
364 if (!container) throw new Error('No container provided');
365 container.appendChild(this.dom.root);
366 container.appendChild(this.dom.loadingScreen);
367};
368
369/**
370 * Set options. Options will be passed to all components loaded in the Timeline.
371 * @param {Object} [options]
372 * {String} orientation
373 * Vertical orientation for the Timeline,
374 * can be 'bottom' (default) or 'top'.
375 * {string | number} width
376 * Width for the timeline, a number in pixels or
377 * a css string like '1000px' or '75%'. '100%' by default.
378 * {string | number} height
379 * Fixed height for the Timeline, a number in pixels or
380 * a css string like '400px' or '75%'. If undefined,
381 * The Timeline will automatically size such that
382 * its contents fit.
383 * {string | number} minHeight
384 * Minimum height for the Timeline, a number in pixels or
385 * a css string like '400px' or '75%'.
386 * {string | number} maxHeight
387 * Maximum height for the Timeline, a number in pixels or
388 * a css string like '400px' or '75%'.
389 * {number | Date | string} start
390 * Start date for the visible window
391 * {number | Date | string} end
392 * End date for the visible window
393 */
394Core.prototype.setOptions = function (options) {
395 if (options) {
396 // copy the known options
397 var fields = [
398 'width', 'height', 'minHeight', 'maxHeight', 'autoResize',
399 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates',
400 'locale', 'locales', 'moment', 'rtl', 'zoomKey', 'horizontalScroll', 'verticalScroll'
401 ];
402 util.selectiveExtend(fields, this.options, options);
403
404 this.dom.rollingModeBtn.style.visibility = 'hidden';
405
406 if (this.options.rtl) {
407 this.dom.container.style.direction = "rtl";
408 this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical-rtl';
409 }
410
411 if (this.options.verticalScroll) {
412 if (this.options.rtl) {
413 this.dom.rightContainer.className = 'vis-panel vis-right vis-vertical-scroll';
414 } else {
415 this.dom.leftContainer.className = 'vis-panel vis-left vis-vertical-scroll';
416 }
417 }
418
419 if (typeof this.options.orientation !== 'object') {
420 this.options.orientation = {item:undefined,axis:undefined};
421 }
422 if ('orientation' in options) {
423 if (typeof options.orientation === 'string') {
424 this.options.orientation = {
425 item: options.orientation,
426 axis: options.orientation
427 };
428 }
429 else if (typeof options.orientation === 'object') {
430 if ('item' in options.orientation) {
431 this.options.orientation.item = options.orientation.item;
432 }
433 if ('axis' in options.orientation) {
434 this.options.orientation.axis = options.orientation.axis;
435 }
436 }
437 }
438
439 if (this.options.orientation.axis === 'both') {
440 if (!this.timeAxis2) {
441 var timeAxis2 = this.timeAxis2 = new TimeAxis(this.body);
442 timeAxis2.setOptions = function (options) {
443 var _options = options ? util.extend({}, options) : {};
444 _options.orientation = 'top'; // override the orientation option, always top
445 TimeAxis.prototype.setOptions.call(timeAxis2, _options);
446 };
447 this.components.push(timeAxis2);
448 }
449 }
450 else {
451 if (this.timeAxis2) {
452 var index = this.components.indexOf(this.timeAxis2);
453 if (index !== -1) {
454 this.components.splice(index, 1);
455 }
456 this.timeAxis2.destroy();
457 this.timeAxis2 = null;
458 }
459 }
460
461 // if the graph2d's drawPoints is a function delegate the callback to the onRender property
462 if (typeof options.drawPoints == 'function') {
463 options.drawPoints = {
464 onRender: options.drawPoints
465 };
466 }
467
468 if ('hiddenDates' in this.options) {
469 DateUtil.convertHiddenOptions(this.options.moment, this.body, this.options.hiddenDates);
470 }
471
472 if ('clickToUse' in options) {
473 if (options.clickToUse) {
474 if (!this.activator) {
475 this.activator = new Activator(this.dom.root);
476 }
477 }
478 else {
479 if (this.activator) {
480 this.activator.destroy();
481 delete this.activator;
482 }
483 }
484 }
485
486 if ('showCustomTime' in options) {
487 throw new Error('Option `showCustomTime` is deprecated. Create a custom time bar via timeline.addCustomTime(time [, id])');
488 }
489
490 // enable/disable autoResize
491 this._initAutoResize();
492 }
493
494 // propagate options to all components
495 this.components.forEach(component => component.setOptions(options));
496
497 // enable/disable configure
498 if ('configure' in options) {
499 if (!this.configurator) {
500 this.configurator = this._createConfigurator();
501 }
502
503 this.configurator.setOptions(options.configure);
504
505 // collect the settings of all components, and pass them to the configuration system
506 var appliedOptions = util.deepExtend({}, this.options);
507 this.components.forEach(function (component) {
508 util.deepExtend(appliedOptions, component.options);
509 });
510 this.configurator.setModuleOptions({global: appliedOptions});
511 }
512
513 this._redraw();
514};
515
516/**
517 * Returns true when the Timeline is active.
518 * @returns {boolean}
519 */
520Core.prototype.isActive = function () {
521 return !this.activator || this.activator.active;
522};
523
524/**
525 * Destroy the Core, clean up all DOM elements and event listeners.
526 */
527Core.prototype.destroy = function () {
528 // unbind datasets
529 this.setItems(null);
530 this.setGroups(null);
531
532 // remove all event listeners
533 this.off();
534
535 // stop checking for changed size
536 this._stopAutoResize();
537
538 // remove from DOM
539 if (this.dom.root.parentNode) {
540 this.dom.root.parentNode.removeChild(this.dom.root);
541 }
542 this.dom = null;
543
544 // remove Activator
545 if (this.activator) {
546 this.activator.destroy();
547 delete this.activator;
548 }
549
550 // cleanup hammer touch events
551 for (var event in this.listeners) {
552 if (this.listeners.hasOwnProperty(event)) {
553 delete this.listeners[event];
554 }
555 }
556 this.listeners = null;
557 this.hammer && this.hammer.destroy();
558 this.hammer = null;
559
560 // give all components the opportunity to cleanup
561 this.components.forEach(component => component.destroy());
562
563 this.body = null;
564};
565
566
567/**
568 * Set a custom time bar
569 * @param {Date} time
570 * @param {number} [id=undefined] Optional id of the custom time bar to be adjusted.
571 */
572Core.prototype.setCustomTime = function (time, id) {
573 var customTimes = this.customTimes.filter(function (component) {
574 return id === component.options.id;
575 });
576
577 if (customTimes.length === 0) {
578 throw new Error('No custom time bar found with id ' + JSON.stringify(id))
579 }
580
581 if (customTimes.length > 0) {
582 customTimes[0].setCustomTime(time);
583 }
584};
585
586/**
587 * Retrieve the current custom time.
588 * @param {number} [id=undefined] Id of the custom time bar.
589 * @return {Date | undefined} customTime
590 */
591Core.prototype.getCustomTime = function(id) {
592 var customTimes = this.customTimes.filter(function (component) {
593 return component.options.id === id;
594 });
595
596 if (customTimes.length === 0) {
597 throw new Error('No custom time bar found with id ' + JSON.stringify(id))
598 }
599 return customTimes[0].getCustomTime();
600};
601
602/**
603 * Set a custom title for the custom time bar.
604 * @param {string} [title] Custom title
605 * @param {number} [id=undefined] Id of the custom time bar.
606 * @returns {*}
607 */
608Core.prototype.setCustomTimeTitle = function(title, id) {
609 var customTimes = this.customTimes.filter(function (component) {
610 return component.options.id === id;
611 });
612
613 if (customTimes.length === 0) {
614 throw new Error('No custom time bar found with id ' + JSON.stringify(id))
615 }
616 if (customTimes.length > 0) {
617 return customTimes[0].setCustomTitle(title);
618 }
619};
620
621/**
622 * Retrieve meta information from an event.
623 * Should be overridden by classes extending Core
624 * @param {Event} event
625 * @return {Object} An object with related information.
626 */
627Core.prototype.getEventProperties = function (event) {
628 return { event: event };
629};
630
631/**
632 * Add custom vertical bar
633 * @param {Date | string | number} [time] A Date, unix timestamp, or
634 * ISO date string. Time point where
635 * the new bar should be placed.
636 * If not provided, `new Date()` will
637 * be used.
638 * @param {number | string} [id=undefined] Id of the new bar. Optional
639 * @param {object} [options={}] Control options for the new bar. Supported
640 * optoins are:
641 * editable {true, false} determines whether the
642 * bar can be dragged by the user. Default is true.
643 * @return {number | string} Returns the id of the new bar
644 */
645Core.prototype.addCustomTime = function (time, id, options) {
646 var timestamp = time !== undefined
647 ? util.convert(time, 'Date').valueOf()
648 : new Date();
649
650 var exists = this.customTimes.some(function (customTime) {
651 return customTime.options.id === id;
652 });
653 if (exists) {
654 throw new Error('A custom time with id ' + JSON.stringify(id) + ' already exists');
655 }
656
657 var customTime = new CustomTime(this.body, util.extend({}, this.options, options, {
658 time : timestamp,
659 id : id
660 }));
661
662 this.customTimes.push(customTime);
663 this.components.push(customTime);
664 this._redraw();
665
666 return id;
667};
668
669/**
670 * Remove previously added custom bar
671 * @param {int} id ID of the custom bar to be removed
672 * [at]returns {boolean} True if the bar exists and is removed, false otherwise
673 */
674Core.prototype.removeCustomTime = function (id) {
675 var customTimes = this.customTimes.filter(function (bar) {
676 return (bar.options.id === id);
677 });
678
679 if (customTimes.length === 0) {
680 throw new Error('No custom time bar found with id ' + JSON.stringify(id))
681 }
682
683 customTimes.forEach(function (customTime) {
684 this.customTimes.splice(this.customTimes.indexOf(customTime), 1);
685 this.components.splice(this.components.indexOf(customTime), 1);
686 customTime.destroy();
687 }.bind(this))
688};
689
690
691/**
692 * Get the id's of the currently visible items.
693 * @returns {Array} The ids of the visible items
694 */
695Core.prototype.getVisibleItems = function() {
696 return this.itemSet && this.itemSet.getVisibleItems() || [];
697};
698
699/**
700 * Get the id's of the currently visible groups.
701 * @returns {Array} The ids of the visible groups
702 */
703Core.prototype.getVisibleGroups = function() {
704 return this.itemSet && this.itemSet.getVisibleGroups() || [];
705};
706
707/**
708 * Set Core window such that it fits all items
709 * @param {Object} [options] Available options:
710 * `animation: boolean | {duration: number, easingFunction: string}`
711 * If true (default), the range is animated
712 * smoothly to the new window. An object can be
713 * provided to specify duration and easing function.
714 * Default duration is 500 ms, and default easing
715 * function is 'easeInOutQuad'.
716 * @param {function} [callback] a callback funtion to be executed at the end of this function
717 */
718Core.prototype.fit = function(options, callback) {
719 var range = this.getDataRange();
720
721 // skip range set if there is no min and max date
722 if (range.min === null && range.max === null) {
723 return;
724 }
725
726 // apply a margin of 1% left and right of the data
727 var interval = range.max - range.min;
728 var min = new Date(range.min.valueOf() - interval * 0.01);
729 var max = new Date(range.max.valueOf() + interval * 0.01);
730 var animation = (options && options.animation !== undefined) ? options.animation : true;
731 this.range.setRange(min, max, { animation: animation }, callback);
732};
733
734/**
735 * Calculate the data range of the items start and end dates
736 * [at]returns {{min: [Date], max: [Date]}}
737 * @protected
738 */
739Core.prototype.getDataRange = function() {
740 // must be implemented by Timeline and Graph2d
741 throw new Error('Cannot invoke abstract method getDataRange');
742};
743
744/**
745 * Set the visible window. Both parameters are optional, you can change only
746 * start or only end. Syntax:
747 *
748 * TimeLine.setWindow(start, end)
749 * TimeLine.setWindow(start, end, options)
750 * TimeLine.setWindow(range)
751 *
752 * Where start and end can be a Date, number, or string, and range is an
753 * object with properties start and end.
754 *
755 * @param {Date | number | string | Object} [start] Start date of visible window
756 * @param {Date | number | string} [end] End date of visible window
757 * @param {Object} [options] Available options:
758 * `animation: boolean | {duration: number, easingFunction: string}`
759 * If true (default), the range is animated
760 * smoothly to the new window. An object can be
761 * provided to specify duration and easing function.
762 * Default duration is 500 ms, and default easing
763 * function is 'easeInOutQuad'.
764 * @param {function} [callback] a callback funtion to be executed at the end of this function
765 */
766Core.prototype.setWindow = function(start, end, options, callback) {
767 if (typeof arguments[2] == "function") {
768 callback = arguments[2];
769 options = {};
770 }
771 var animation;
772 var range;
773 if (arguments.length == 1) {
774 range = arguments[0];
775 animation = (range.animation !== undefined) ? range.animation : true;
776 this.range.setRange(range.start, range.end, { animation: animation });
777 }
778 else if (arguments.length == 2 && typeof arguments[1] == "function") {
779 range = arguments[0];
780 callback = arguments[1];
781 animation = (range.animation !== undefined) ? range.animation : true;
782 this.range.setRange(range.start, range.end, { animation: animation }, callback);
783 }
784 else {
785 animation = (options && options.animation !== undefined) ? options.animation : true;
786 this.range.setRange(start, end, { animation: animation }, callback);
787 }
788};
789
790/**
791 * Move the window such that given time is centered on screen.
792 * @param {Date | number | string} time
793 * @param {Object} [options] Available options:
794 * `animation: boolean | {duration: number, easingFunction: string}`
795 * If true (default), the range is animated
796 * smoothly to the new window. An object can be
797 * provided to specify duration and easing function.
798 * Default duration is 500 ms, and default easing
799 * function is 'easeInOutQuad'.
800 * @param {function} [callback] a callback funtion to be executed at the end of this function
801 */
802Core.prototype.moveTo = function(time, options, callback) {
803 if (typeof arguments[1] == "function") {
804 callback = arguments[1];
805 options = {};
806 }
807 var interval = this.range.end - this.range.start;
808 var t = util.convert(time, 'Date').valueOf();
809
810 var start = t - interval / 2;
811 var end = t + interval / 2;
812 var animation = (options && options.animation !== undefined) ? options.animation : true;
813
814 this.range.setRange(start, end, { animation: animation }, callback);
815};
816
817/**
818 * Get the visible window
819 * @return {{start: Date, end: Date}} Visible range
820 */
821Core.prototype.getWindow = function() {
822 var range = this.range.getRange();
823 return {
824 start: new Date(range.start),
825 end: new Date(range.end)
826 };
827};
828
829/**
830 * Zoom in the window such that given time is centered on screen.
831 * @param {number} percentage - must be between [0..1]
832 * @param {Object} [options] Available options:
833 * `animation: boolean | {duration: number, easingFunction: string}`
834 * If true (default), the range is animated
835 * smoothly to the new window. An object can be
836 * provided to specify duration and easing function.
837 * Default duration is 500 ms, and default easing
838 * function is 'easeInOutQuad'.
839 * @param {function} [callback] a callback funtion to be executed at the end of this function
840 */
841Core.prototype.zoomIn = function(percentage, options, callback) {
842 if (!percentage || percentage < 0 || percentage > 1) return;
843 if (typeof arguments[1] == "function") {
844 callback = arguments[1];
845 options = {};
846 }
847 var range = this.getWindow();
848 var start = range.start.valueOf();
849 var end = range.end.valueOf();
850 var interval = end - start;
851 var newInterval = interval / (1 + percentage);
852 var distance = (interval - newInterval) / 2;
853 var newStart = start + distance;
854 var newEnd = end - distance;
855
856 this.setWindow(newStart, newEnd, options, callback);
857};
858
859/**
860 * Zoom out the window such that given time is centered on screen.
861 * @param {number} percentage - must be between [0..1]
862 * @param {Object} [options] Available options:
863 * `animation: boolean | {duration: number, easingFunction: string}`
864 * If true (default), the range is animated
865 * smoothly to the new window. An object can be
866 * provided to specify duration and easing function.
867 * Default duration is 500 ms, and default easing
868 * function is 'easeInOutQuad'.
869 * @param {function} [callback] a callback funtion to be executed at the end of this function
870 */
871Core.prototype.zoomOut = function(percentage, options, callback) {
872 if (!percentage || percentage < 0 || percentage > 1) return
873 if (typeof arguments[1] == "function") {
874 callback = arguments[1];
875 options = {};
876 }
877 var range = this.getWindow();
878 var start = range.start.valueOf();
879 var end = range.end.valueOf();
880 var interval = end - start;
881 var newStart = start - interval * percentage / 2;
882 var newEnd = end + interval * percentage / 2;
883
884 this.setWindow(newStart, newEnd, options, callback);
885};
886
887/**
888 * Force a redraw. Can be overridden by implementations of Core
889 *
890 * Note: this function will be overridden on construction with a trottled version
891 */
892Core.prototype.redraw = function() {
893 this._redraw();
894};
895
896/**
897 * Redraw for internal use. Redraws all components. See also the public
898 * method redraw.
899 * @protected
900 */
901Core.prototype._redraw = function() {
902 this.redrawCount++;
903 var resized = false;
904 var options = this.options;
905 var props = this.props;
906 var dom = this.dom;
907
908 if (!dom || !dom.container || dom.root.offsetWidth == 0) return; // when destroyed, or invisible
909
910 DateUtil.updateHiddenDates(this.options.moment, this.body, this.options.hiddenDates);
911
912 // update class names
913 if (options.orientation == 'top') {
914 util.addClassName(dom.root, 'vis-top');
915 util.removeClassName(dom.root, 'vis-bottom');
916 }
917 else {
918 util.removeClassName(dom.root, 'vis-top');
919 util.addClassName(dom.root, 'vis-bottom');
920 }
921
922 // update root width and height options
923 dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
924 dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
925 dom.root.style.width = util.option.asSize(options.width, '');
926
927 // calculate border widths
928 props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
929 props.border.right = props.border.left;
930 props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
931 props.border.bottom = props.border.top;
932 props.borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
933 props.borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
934
935 // workaround for a bug in IE: the clientWidth of an element with
936 // a height:0px and overflow:hidden is not calculated and always has value 0
937 if (dom.centerContainer.clientHeight === 0) {
938 props.border.left = props.border.top;
939 props.border.right = props.border.left;
940 }
941 if (dom.root.clientHeight === 0) {
942 props.borderRootWidth = props.borderRootHeight;
943 }
944
945 // calculate the heights. If any of the side panels is empty, we set the height to
946 // minus the border width, such that the border will be invisible
947 props.center.height = dom.center.offsetHeight;
948 props.left.height = dom.left.offsetHeight;
949 props.right.height = dom.right.offsetHeight;
950 props.top.height = dom.top.clientHeight || -props.border.top;
951 props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
952
953 // TODO: compensate borders when any of the panels is empty.
954
955 // apply auto height
956 // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
957 var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
958 var autoHeight = props.top.height + contentHeight + props.bottom.height +
959 props.borderRootHeight + props.border.top + props.border.bottom;
960 dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
961
962 // calculate heights of the content panels
963 props.root.height = dom.root.offsetHeight;
964 props.background.height = props.root.height - props.borderRootHeight;
965 var containerHeight = props.root.height - props.top.height - props.bottom.height -
966 props.borderRootHeight;
967 props.centerContainer.height = containerHeight;
968 props.leftContainer.height = containerHeight;
969 props.rightContainer.height = props.leftContainer.height;
970
971 // calculate the widths of the panels
972 props.root.width = dom.root.offsetWidth;
973 props.background.width = props.root.width - props.borderRootWidth;
974
975 if (!this.initialDrawDone) {
976 props.scrollbarWidth = util.getScrollBarWidth();
977 }
978
979 if (options.verticalScroll) {
980 if (options.rtl) {
981 props.left.width = dom.leftContainer.clientWidth || -props.border.left;
982 props.right.width = dom.rightContainer.clientWidth + props.scrollbarWidth || -props.border.right;
983 } else {
984 props.left.width = dom.leftContainer.clientWidth + props.scrollbarWidth || -props.border.left;
985 props.right.width = dom.rightContainer.clientWidth || -props.border.right;
986 }
987 } else {
988 props.left.width = dom.leftContainer.clientWidth || -props.border.left;
989 props.right.width = dom.rightContainer.clientWidth || -props.border.right;
990 }
991
992 this._setDOM();
993
994 // update the scrollTop, feasible range for the offset can be changed
995 // when the height of the Core or of the contents of the center changed
996 var offset = this._updateScrollTop();
997
998 // reposition the scrollable contents
999 if (options.orientation.item != 'top') {
1000 offset += Math.max(props.centerContainer.height - props.center.height -
1001 props.border.top - props.border.bottom, 0);
1002 }
1003 dom.center.style.top = offset + 'px';
1004
1005 // show shadows when vertical scrolling is available
1006 var visibilityTop = props.scrollTop == 0 ? 'hidden' : '';
1007 var visibilityBottom = props.scrollTop == props.scrollTopMin ? 'hidden' : '';
1008 dom.shadowTop.style.visibility = visibilityTop;
1009 dom.shadowBottom.style.visibility = visibilityBottom;
1010 dom.shadowTopLeft.style.visibility = visibilityTop;
1011 dom.shadowBottomLeft.style.visibility = visibilityBottom;
1012 dom.shadowTopRight.style.visibility = visibilityTop;
1013 dom.shadowBottomRight.style.visibility = visibilityBottom;
1014
1015 if (options.verticalScroll) {
1016 dom.rightContainer.className = 'vis-panel vis-right vis-vertical-scroll';
1017 dom.leftContainer.className = 'vis-panel vis-left vis-vertical-scroll';
1018
1019 dom.shadowTopRight.style.visibility = "hidden";
1020 dom.shadowBottomRight.style.visibility = "hidden";
1021 dom.shadowTopLeft.style.visibility = "hidden";
1022 dom.shadowBottomLeft.style.visibility = "hidden";
1023
1024 dom.left.style.top = '0px';
1025 dom.right.style.top = '0px';
1026 }
1027
1028 if (!options.verticalScroll || props.center.height < props.centerContainer.height) {
1029 dom.left.style.top = offset + 'px';
1030 dom.right.style.top = offset + 'px';
1031 dom.rightContainer.className = dom.rightContainer.className.replace(new RegExp('(?:^|\\s)'+ 'vis-vertical-scroll' + '(?:\\s|$)'), ' ');
1032 dom.leftContainer.className = dom.leftContainer.className.replace(new RegExp('(?:^|\\s)'+ 'vis-vertical-scroll' + '(?:\\s|$)'), ' ');
1033 props.left.width = dom.leftContainer.clientWidth || -props.border.left;
1034 props.right.width = dom.rightContainer.clientWidth || -props.border.right;
1035 this._setDOM();
1036 }
1037
1038 // enable/disable vertical panning
1039 var contentsOverflow = props.center.height > props.centerContainer.height;
1040 this.hammer.get('pan').set({
1041 direction: contentsOverflow ? Hammer.DIRECTION_ALL : Hammer.DIRECTION_HORIZONTAL
1042 });
1043
1044 // redraw all components
1045 this.components.forEach(function (component) {
1046 resized = component.redraw() || resized;
1047 });
1048 var MAX_REDRAW = 5;
1049 if (resized) {
1050 if (this.redrawCount < MAX_REDRAW) {
1051 this.body.emitter.emit('_change');
1052 return;
1053 }
1054 else {
1055 console.log('WARNING: infinite loop in redraw?');
1056 }
1057 } else {
1058 this.redrawCount = 0;
1059 }
1060
1061 //Emit public 'changed' event for UI updates, see issue #1592
1062 this.body.emitter.emit("changed");
1063};
1064
1065Core.prototype._setDOM = function () {
1066 var props = this.props;
1067 var dom = this.dom;
1068
1069 props.leftContainer.width = props.left.width;
1070 props.rightContainer.width = props.right.width;
1071 var centerWidth = props.root.width - props.left.width - props.right.width - props.borderRootWidth;
1072 props.center.width = centerWidth;
1073 props.centerContainer.width = centerWidth;
1074 props.top.width = centerWidth;
1075 props.bottom.width = centerWidth;
1076
1077 // resize the panels
1078 dom.background.style.height = props.background.height + 'px';
1079 dom.backgroundVertical.style.height = props.background.height + 'px';
1080 dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
1081 dom.centerContainer.style.height = props.centerContainer.height + 'px';
1082 dom.leftContainer.style.height = props.leftContainer.height + 'px';
1083 dom.rightContainer.style.height = props.rightContainer.height + 'px';
1084
1085 dom.background.style.width = props.background.width + 'px';
1086 dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
1087 dom.backgroundHorizontal.style.width = props.background.width + 'px';
1088 dom.centerContainer.style.width = props.center.width + 'px';
1089 dom.top.style.width = props.top.width + 'px';
1090 dom.bottom.style.width = props.bottom.width + 'px';
1091
1092 // reposition the panels
1093 dom.background.style.left = '0';
1094 dom.background.style.top = '0';
1095 dom.backgroundVertical.style.left = (props.left.width + props.border.left) + 'px';
1096 dom.backgroundVertical.style.top = '0';
1097 dom.backgroundHorizontal.style.left = '0';
1098 dom.backgroundHorizontal.style.top = props.top.height + 'px';
1099 dom.centerContainer.style.left = props.left.width + 'px';
1100 dom.centerContainer.style.top = props.top.height + 'px';
1101 dom.leftContainer.style.left = '0';
1102 dom.leftContainer.style.top = props.top.height + 'px';
1103 dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
1104 dom.rightContainer.style.top = props.top.height + 'px';
1105 dom.top.style.left = props.left.width + 'px';
1106 dom.top.style.top = '0';
1107 dom.bottom.style.left = props.left.width + 'px';
1108 dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
1109 dom.center.style.left = '0';
1110 dom.left.style.left = '0';
1111 dom.right.style.left = '0';
1112};
1113
1114// TODO: deprecated since version 1.1.0, remove some day
1115Core.prototype.repaint = function () {
1116 throw new Error('Function repaint is deprecated. Use redraw instead.');
1117};
1118
1119/**
1120 * Set a current time. This can be used for example to ensure that a client's
1121 * time is synchronized with a shared server time.
1122 * Only applicable when option `showCurrentTime` is true.
1123 * @param {Date | string | number} time A Date, unix timestamp, or
1124 * ISO date string.
1125 */
1126Core.prototype.setCurrentTime = function(time) {
1127 if (!this.currentTime) {
1128 throw new Error('Option showCurrentTime must be true');
1129 }
1130
1131 this.currentTime.setCurrentTime(time);
1132};
1133
1134/**
1135 * Get the current time.
1136 * Only applicable when option `showCurrentTime` is true.
1137 * @return {Date} Returns the current time.
1138 */
1139Core.prototype.getCurrentTime = function() {
1140 if (!this.currentTime) {
1141 throw new Error('Option showCurrentTime must be true');
1142 }
1143
1144 return this.currentTime.getCurrentTime();
1145};
1146
1147/**
1148 * Convert a position on screen (pixels) to a datetime
1149 * @param {int} x Position on the screen in pixels
1150 * @return {Date} time The datetime the corresponds with given position x
1151 * @protected
1152 */
1153// TODO: move this function to Range
1154Core.prototype._toTime = function(x) {
1155 return DateUtil.toTime(this, x, this.props.center.width);
1156};
1157
1158/**
1159 * Convert a position on the global screen (pixels) to a datetime
1160 * @param {int} x Position on the screen in pixels
1161 * @return {Date} time The datetime the corresponds with given position x
1162 * @protected
1163 */
1164// TODO: move this function to Range
1165Core.prototype._toGlobalTime = function(x) {
1166 return DateUtil.toTime(this, x, this.props.root.width);
1167 //var conversion = this.range.conversion(this.props.root.width);
1168 //return new Date(x / conversion.scale + conversion.offset);
1169};
1170
1171/**
1172 * Convert a datetime (Date object) into a position on the screen
1173 * @param {Date} time A date
1174 * @return {int} x The position on the screen in pixels which corresponds
1175 * with the given date.
1176 * @protected
1177 */
1178// TODO: move this function to Range
1179Core.prototype._toScreen = function(time) {
1180 return DateUtil.toScreen(this, time, this.props.center.width);
1181};
1182
1183
1184
1185/**
1186 * Convert a datetime (Date object) into a position on the root
1187 * This is used to get the pixel density estimate for the screen, not the center panel
1188 * @param {Date} time A date
1189 * @return {int} x The position on root in pixels which corresponds
1190 * with the given date.
1191 * @protected
1192 */
1193// TODO: move this function to Range
1194Core.prototype._toGlobalScreen = function(time) {
1195 return DateUtil.toScreen(this, time, this.props.root.width);
1196 //var conversion = this.range.conversion(this.props.root.width);
1197 //return (time.valueOf() - conversion.offset) * conversion.scale;
1198};
1199
1200
1201/**
1202 * Initialize watching when option autoResize is true
1203 * @private
1204 */
1205Core.prototype._initAutoResize = function () {
1206 if (this.options.autoResize == true) {
1207 this._startAutoResize();
1208 }
1209 else {
1210 this._stopAutoResize();
1211 }
1212};
1213
1214/**
1215 * Watch for changes in the size of the container. On resize, the Panel will
1216 * automatically redraw itself.
1217 * @private
1218 */
1219Core.prototype._startAutoResize = function () {
1220 var me = this;
1221
1222 this._stopAutoResize();
1223
1224 this._onResize = function() {
1225 if (me.options.autoResize != true) {
1226 // stop watching when the option autoResize is changed to false
1227 me._stopAutoResize();
1228 return;
1229 }
1230
1231 if (me.dom.root) {
1232 // check whether the frame is resized
1233 // Note: we compare offsetWidth here, not clientWidth. For some reason,
1234 // IE does not restore the clientWidth from 0 to the actual width after
1235 // changing the timeline's container display style from none to visible
1236 if ((me.dom.root.offsetWidth != me.props.lastWidth) ||
1237 (me.dom.root.offsetHeight != me.props.lastHeight)) {
1238 me.props.lastWidth = me.dom.root.offsetWidth;
1239 me.props.lastHeight = me.dom.root.offsetHeight;
1240 me.props.scrollbarWidth = util.getScrollBarWidth();
1241
1242 me.body.emitter.emit('_change');
1243 }
1244 }
1245 };
1246
1247 // add event listener to window resize
1248 util.addEventListener(window, 'resize', this._onResize);
1249
1250 //Prevent initial unnecessary redraw
1251 if (me.dom.root) {
1252 me.props.lastWidth = me.dom.root.offsetWidth;
1253 me.props.lastHeight = me.dom.root.offsetHeight;
1254 }
1255
1256 this.watchTimer = setInterval(this._onResize, 1000);
1257};
1258
1259/**
1260 * Stop watching for a resize of the frame.
1261 * @private
1262 */
1263Core.prototype._stopAutoResize = function () {
1264 if (this.watchTimer) {
1265 clearInterval(this.watchTimer);
1266 this.watchTimer = undefined;
1267 }
1268
1269 // remove event listener on window.resize
1270 if (this._onResize) {
1271 util.removeEventListener(window, 'resize', this._onResize);
1272 this._onResize = null;
1273 }
1274};
1275
1276/**
1277 * Start moving the timeline vertically
1278 * @param {Event} event
1279 * @private
1280 */
1281Core.prototype._onTouch = function (event) { // eslint-disable-line no-unused-vars
1282 this.touch.allowDragging = true;
1283 this.touch.initialScrollTop = this.props.scrollTop;
1284};
1285
1286/**
1287 * Start moving the timeline vertically
1288 * @param {Event} event
1289 * @private
1290 */
1291Core.prototype._onPinch = function (event) { // eslint-disable-line no-unused-vars
1292 this.touch.allowDragging = false;
1293};
1294
1295/**
1296 * Move the timeline vertically
1297 * @param {Event} event
1298 * @private
1299 */
1300Core.prototype._onDrag = function (event) {
1301 if (!event) return
1302 // refuse to drag when we where pinching to prevent the timeline make a jump
1303 // when releasing the fingers in opposite order from the touch screen
1304 if (!this.touch.allowDragging) return;
1305
1306 var delta = event.deltaY;
1307
1308 var oldScrollTop = this._getScrollTop();
1309 var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
1310
1311 if (this.options.verticalScroll) {
1312 this.dom.left.parentNode.scrollTop = -this.props.scrollTop;
1313 this.dom.right.parentNode.scrollTop = -this.props.scrollTop;
1314 }
1315
1316 if (newScrollTop != oldScrollTop) {
1317 this.emit("verticalDrag");
1318 }
1319};
1320
1321/**
1322 * Apply a scrollTop
1323 * @param {number} scrollTop
1324 * @returns {number} scrollTop Returns the applied scrollTop
1325 * @private
1326 */
1327Core.prototype._setScrollTop = function (scrollTop) {
1328 this.props.scrollTop = scrollTop;
1329 this._updateScrollTop();
1330 return this.props.scrollTop;
1331};
1332
1333/**
1334 * Update the current scrollTop when the height of the containers has been changed
1335 * @returns {number} scrollTop Returns the applied scrollTop
1336 * @private
1337 */
1338Core.prototype._updateScrollTop = function () {
1339 // recalculate the scrollTopMin
1340 var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
1341 if (scrollTopMin != this.props.scrollTopMin) {
1342 // in case of bottom orientation, change the scrollTop such that the contents
1343 // do not move relative to the time axis at the bottom
1344 if (this.options.orientation.item != 'top') {
1345 this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
1346 }
1347 this.props.scrollTopMin = scrollTopMin;
1348 }
1349
1350 // limit the scrollTop to the feasible scroll range
1351 if (this.props.scrollTop > 0) this.props.scrollTop = 0;
1352 if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
1353
1354 if (this.options.verticalScroll) {
1355 this.dom.left.parentNode.scrollTop = -this.props.scrollTop;
1356 this.dom.right.parentNode.scrollTop = -this.props.scrollTop;
1357 }
1358
1359 return this.props.scrollTop;
1360};
1361
1362/**
1363 * Get the current scrollTop
1364 * @returns {number} scrollTop
1365 * @private
1366 */
1367Core.prototype._getScrollTop = function () {
1368 return this.props.scrollTop;
1369};
1370
1371/**
1372 * Load a configurator
1373 * [at]returns {Object}
1374 * @private
1375 */
1376Core.prototype._createConfigurator = function () {
1377 throw new Error('Cannot invoke abstract method _createConfigurator');
1378};
1379
1380export default Core;