UNPKG

11.7 kBJavaScriptView Raw
1var Item = require('./Item');
2
3/**
4 * @constructor RangeItem
5 * @extends Item
6 * @param {Object} data Object containing parameters start, end
7 * content, className.
8 * @param {{toScreen: function, toTime: function}} conversion
9 * Conversion functions from time to screen and vice versa
10 * @param {Object} [options] Configuration options
11 * // TODO: describe options
12 */
13function RangeItem (data, conversion, options) {
14 this.props = {
15 content: {
16 width: 0
17 }
18 };
19 this.overflow = false; // if contents can overflow (css styling), this flag is set to true
20 this.options = options;
21 // validate data
22 if (data) {
23 if (data.start == undefined) {
24 throw new Error('Property "start" missing in item ' + data.id);
25 }
26 if (data.end == undefined) {
27 throw new Error('Property "end" missing in item ' + data.id);
28 }
29 }
30
31 Item.call(this, data, conversion, options);
32}
33
34RangeItem.prototype = new Item (null, null, null);
35
36RangeItem.prototype.baseClassName = 'vis-item vis-range';
37
38/**
39 * Check whether this item is visible inside given range
40 *
41 * @param {vis.Range} range with a timestamp for start and end
42 * @returns {boolean} True if visible
43 */
44RangeItem.prototype.isVisible = function(range) {
45 // determine visibility
46 return (this.data.start < range.end) && (this.data.end > range.start);
47};
48
49RangeItem.prototype._createDomElement = function() {
50 if (!this.dom) {
51 // create DOM
52 this.dom = {};
53
54 // background box
55 this.dom.box = document.createElement('div');
56 // className is updated in redraw()
57
58 // frame box (to prevent the item contents from overflowing)
59 this.dom.frame = document.createElement('div');
60 this.dom.frame.className = 'vis-item-overflow';
61 this.dom.box.appendChild(this.dom.frame);
62
63 // visible frame box (showing the frame that is always visible)
64 this.dom.visibleFrame = document.createElement('div');
65 this.dom.visibleFrame.className = 'vis-item-visible-frame';
66 this.dom.box.appendChild(this.dom.visibleFrame);
67
68 // contents box
69 this.dom.content = document.createElement('div');
70 this.dom.content.className = 'vis-item-content';
71 this.dom.frame.appendChild(this.dom.content);
72
73 // attach this item as attribute
74 this.dom.box['timeline-item'] = this;
75
76 this.dirty = true;
77 }
78
79}
80
81RangeItem.prototype._appendDomElement = function() {
82 if (!this.parent) {
83 throw new Error('Cannot redraw item: no parent attached');
84 }
85 if (!this.dom.box.parentNode) {
86 var foreground = this.parent.dom.foreground;
87 if (!foreground) {
88 throw new Error('Cannot redraw item: parent has no foreground container element');
89 }
90 foreground.appendChild(this.dom.box);
91 }
92 this.displayed = true;
93}
94
95RangeItem.prototype._updateDirtyDomComponents = function() {
96 // update dirty DOM. An item is marked dirty when:
97 // - the item is not yet rendered
98 // - the item's data is changed
99 // - the item is selected/deselected
100 if (this.dirty) {
101 this._updateContents(this.dom.content);
102 this._updateDataAttributes(this.dom.box);
103 this._updateStyle(this.dom.box);
104
105 var editable = (this.editable.updateTime || this.editable.updateGroup);
106
107 // update class
108 var className = (this.data.className ? (' ' + this.data.className) : '') +
109 (this.selected ? ' vis-selected' : '') +
110 (editable ? ' vis-editable' : ' vis-readonly');
111 this.dom.box.className = this.baseClassName + className;
112
113 // turn off max-width to be able to calculate the real width
114 // this causes an extra browser repaint/reflow, but so be it
115 this.dom.content.style.maxWidth = 'none';
116 }
117}
118
119RangeItem.prototype._getDomComponentsSizes = function() {
120 // determine from css whether this box has overflow
121 this.overflow = window.getComputedStyle(this.dom.frame).overflow !== 'hidden';
122 return {
123 content: {
124 width: this.dom.content.offsetWidth,
125 },
126 box: {
127 height: this.dom.box.offsetHeight
128 }
129 }
130}
131
132RangeItem.prototype._updateDomComponentsSizes = function(sizes) {
133 this.props.content.width = sizes.content.width;
134 this.height = sizes.box.height;
135 this.dom.content.style.maxWidth = '';
136 this.dirty = false;
137}
138
139RangeItem.prototype._repaintDomAdditionals = function() {
140 this._repaintOnItemUpdateTimeTooltip(this.dom.box);
141 this._repaintDeleteButton(this.dom.box);
142 this._repaintDragCenter();
143 this._repaintDragLeft();
144 this._repaintDragRight();
145}
146
147/**
148 * Repaint the item
149 * @param {boolean} [returnQueue=false] return the queue
150 * @return {boolean} the redraw queue if returnQueue=true
151 */
152RangeItem.prototype.redraw = function(returnQueue) {
153 var sizes;
154 var queue = [
155 // create item DOM
156 this._createDomElement.bind(this),
157
158 // append DOM to parent DOM
159 this._appendDomElement.bind(this),
160
161 // update dirty DOM
162 this._updateDirtyDomComponents.bind(this),
163
164 (function() {
165 if (this.dirty) {
166 sizes = this._getDomComponentsSizes.bind(this)();
167 }
168 }).bind(this),
169
170 (function() {
171 if (this.dirty) {
172 this._updateDomComponentsSizes.bind(this)(sizes);
173 }
174 }).bind(this),
175
176 // repaint DOM additionals
177 this._repaintDomAdditionals.bind(this)
178 ];
179
180 if (returnQueue) {
181 return queue;
182 } else {
183 var result;
184 queue.forEach(function (fn) {
185 result = fn();
186 });
187 return result;
188 }
189};
190
191/**
192 * Show the item in the DOM (when not already visible). The items DOM will
193 * be created when needed.
194 * @param {boolean} [returnQueue=false] whether to return a queue of functions to execute instead of just executing them
195 * @return {boolean} the redraw queue if returnQueue=true
196 */
197RangeItem.prototype.show = function(returnQueue) {
198 if (!this.displayed) {
199 return this.redraw(returnQueue);
200 }
201};
202
203/**
204 * Hide the item from the DOM (when visible)
205 */
206RangeItem.prototype.hide = function() {
207 if (this.displayed) {
208 var box = this.dom.box;
209
210 if (box.parentNode) {
211 box.parentNode.removeChild(box);
212 }
213
214 this.displayed = false;
215 }
216};
217
218/**
219 * Reposition the item horizontally
220 * @param {boolean} [limitSize=true] If true (default), the width of the range
221 * item will be limited, as the browser cannot
222 * display very wide divs. This means though
223 * that the applied left and width may
224 * not correspond to the ranges start and end
225 * @Override
226 */
227RangeItem.prototype.repositionX = function(limitSize) {
228 var parentWidth = this.parent.width;
229 var start = this.conversion.toScreen(this.data.start);
230 var end = this.conversion.toScreen(this.data.end);
231 var align = this.data.align === undefined ? this.options.align : this.data.align;
232 var contentStartPosition;
233 var contentWidth;
234
235 // limit the width of the range, as browsers cannot draw very wide divs
236 // unless limitSize: false is explicitly set in item data
237 if (this.data.limitSize !== false && (limitSize === undefined || limitSize === true)) {
238 if (start < -parentWidth) {
239 start = -parentWidth;
240 }
241 if (end > 2 * parentWidth) {
242 end = 2 * parentWidth;
243 }
244 }
245
246 // add 0.5 to compensate floating-point values rounding
247 var boxWidth = Math.max(end - start + 0.5, 1);
248
249 if (this.overflow) {
250 if (this.options.rtl) {
251 this.right = start;
252 } else {
253 this.left = start;
254 }
255 this.width = boxWidth + this.props.content.width;
256 contentWidth = this.props.content.width;
257
258 // Note: The calculation of width is an optimistic calculation, giving
259 // a width which will not change when moving the Timeline
260 // So no re-stacking needed, which is nicer for the eye;
261 }
262 else {
263 if (this.options.rtl) {
264 this.right = start;
265 } else {
266 this.left = start;
267 }
268 this.width = boxWidth;
269 contentWidth = Math.min(end - start, this.props.content.width);
270 }
271
272 if (this.options.rtl) {
273 this.dom.box.style.right = this.right + 'px';
274 } else {
275 this.dom.box.style.left = this.left + 'px';
276 }
277 this.dom.box.style.width = boxWidth + 'px';
278
279 switch (align) {
280 case 'left':
281 if (this.options.rtl) {
282 this.dom.content.style.right = '0';
283 } else {
284 this.dom.content.style.left = '0';
285 }
286 break;
287
288 case 'right':
289 if (this.options.rtl) {
290 this.dom.content.style.right = Math.max((boxWidth - contentWidth), 0) + 'px';
291 } else {
292 this.dom.content.style.left = Math.max((boxWidth - contentWidth), 0) + 'px';
293 }
294 break;
295
296 case 'center':
297 if (this.options.rtl) {
298 this.dom.content.style.right = Math.max((boxWidth - contentWidth) / 2, 0) + 'px';
299 } else {
300 this.dom.content.style.left = Math.max((boxWidth - contentWidth) / 2, 0) + 'px';
301 }
302
303 break;
304
305 default: // 'auto'
306 // when range exceeds left of the window, position the contents at the left of the visible area
307 if (this.overflow) {
308 if (end > 0) {
309 contentStartPosition = Math.max(-start, 0);
310 }
311 else {
312 contentStartPosition = -contentWidth; // ensure it's not visible anymore
313 }
314 }
315 else {
316 if (start < 0) {
317 contentStartPosition = -start;
318 }
319 else {
320 contentStartPosition = 0;
321 }
322 }
323 if (this.options.rtl) {
324 this.dom.content.style.right = contentStartPosition + 'px';
325 } else {
326 this.dom.content.style.left = contentStartPosition + 'px';
327 this.dom.content.style.width = 'calc(100% - ' + contentStartPosition + 'px)';
328 }
329 }
330};
331
332/**
333 * Reposition the item vertically
334 * @Override
335 */
336RangeItem.prototype.repositionY = function() {
337 var orientation = this.options.orientation.item;
338 var box = this.dom.box;
339
340 if (orientation == 'top') {
341 box.style.top = this.top + 'px';
342 }
343 else {
344 box.style.top = (this.parent.height - this.top - this.height) + 'px';
345 }
346};
347
348/**
349 * Repaint a drag area on the left side of the range when the range is selected
350 * @protected
351 */
352RangeItem.prototype._repaintDragLeft = function () {
353 if ((this.selected || this.options.itemsAlwaysDraggable.range) && this.options.editable.updateTime && !this.dom.dragLeft) {
354 // create and show drag area
355 var dragLeft = document.createElement('div');
356 dragLeft.className = 'vis-drag-left';
357 dragLeft.dragLeftItem = this;
358
359 this.dom.box.appendChild(dragLeft);
360 this.dom.dragLeft = dragLeft;
361 }
362 else if (!this.selected && !this.options.itemsAlwaysDraggable.range && this.dom.dragLeft) {
363 // delete drag area
364 if (this.dom.dragLeft.parentNode) {
365 this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
366 }
367 this.dom.dragLeft = null;
368 }
369};
370
371/**
372 * Repaint a drag area on the right side of the range when the range is selected
373 * @protected
374 */
375RangeItem.prototype._repaintDragRight = function () {
376 if ((this.selected || this.options.itemsAlwaysDraggable.range) && this.options.editable.updateTime && !this.dom.dragRight) {
377 // create and show drag area
378 var dragRight = document.createElement('div');
379 dragRight.className = 'vis-drag-right';
380 dragRight.dragRightItem = this;
381
382 this.dom.box.appendChild(dragRight);
383 this.dom.dragRight = dragRight;
384 }
385 else if (!this.selected && !this.options.itemsAlwaysDraggable.range && this.dom.dragRight) {
386 // delete drag area
387 if (this.dom.dragRight.parentNode) {
388 this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
389 }
390 this.dom.dragRight = null;
391 }
392};
393
394module.exports = RangeItem;