UNPKG

15.4 kBJavaScriptView Raw
1import Hammer from '../../../module/hammer';
2import util from 'vis-util';
3import moment from '../../../module/moment';
4
5import '../css/item.css';
6
7/**
8 * @constructor Item
9 * @param {Object} data Object containing (optional) parameters type,
10 * start, end, content, group, className.
11 * @param {{toScreen: function, toTime: function}} conversion
12 * Conversion functions from time to screen and vice versa
13 * @param {Object} options Configuration options
14 * // TODO: describe available options
15 */
16function Item (data, conversion, options) {
17 this.id = null;
18 this.parent = null;
19 this.data = data;
20 this.dom = null;
21 this.conversion = conversion || {};
22 this.options = options || {};
23 this.selected = false;
24 this.displayed = false;
25 this.groupShowing = true;
26 this.dirty = true;
27
28 this.top = null;
29 this.right = null;
30 this.left = null;
31 this.width = null;
32 this.height = null;
33
34 this.editable = null;
35 this._updateEditStatus();
36}
37
38Item.prototype.stack = true;
39
40/**
41 * Select current item
42 */
43Item.prototype.select = function() {
44 this.selected = true;
45 this.dirty = true;
46 if (this.displayed) this.redraw();
47};
48
49/**
50 * Unselect current item
51 */
52Item.prototype.unselect = function() {
53 this.selected = false;
54 this.dirty = true;
55 if (this.displayed) this.redraw();
56};
57
58/**
59 * Set data for the item. Existing data will be updated. The id should not
60 * be changed. When the item is displayed, it will be redrawn immediately.
61 * @param {Object} data
62 */
63Item.prototype.setData = function(data) {
64 var groupChanged = data.group != undefined && this.data.group != data.group;
65 if (groupChanged && this.parent != null) {
66 this.parent.itemSet._moveToGroup(this, data.group);
67 }
68
69 if (this.parent) {
70 this.parent.stackDirty = true;
71 }
72
73 var subGroupChanged = data.subgroup != undefined && this.data.subgroup != data.subgroup;
74 if (subGroupChanged && this.parent != null) {
75 this.parent.changeSubgroup(this, this.data.subgroup, data.subgroup);
76 }
77
78 this.data = data;
79 this._updateEditStatus();
80 this.dirty = true;
81 if (this.displayed) this.redraw();
82};
83
84/**
85 * Set a parent for the item
86 * @param {Group} parent
87 */
88Item.prototype.setParent = function(parent) {
89 if (this.displayed) {
90 this.hide();
91 this.parent = parent;
92 if (this.parent) {
93 this.show();
94 }
95 }
96 else {
97 this.parent = parent;
98 }
99};
100
101/**
102 * Check whether this item is visible inside given range
103 * @param {vis.Range} range with a timestamp for start and end
104 * @returns {boolean} True if visible
105 */
106Item.prototype.isVisible = function(range) { // eslint-disable-line no-unused-vars
107 return false;
108};
109
110/**
111 * Show the Item in the DOM (when not already visible)
112 * @return {Boolean} changed
113 */
114Item.prototype.show = function() {
115 return false;
116};
117
118/**
119 * Hide the Item from the DOM (when visible)
120 * @return {Boolean} changed
121 */
122Item.prototype.hide = function() {
123 return false;
124};
125
126/**
127 * Repaint the item
128 */
129Item.prototype.redraw = function() {
130 // should be implemented by the item
131};
132
133/**
134 * Reposition the Item horizontally
135 */
136Item.prototype.repositionX = function() {
137 // should be implemented by the item
138};
139
140/**
141 * Reposition the Item vertically
142 */
143Item.prototype.repositionY = function() {
144 // should be implemented by the item
145};
146
147/**
148 * Repaint a drag area on the center of the item when the item is selected
149 * @protected
150 */
151Item.prototype._repaintDragCenter = function () {
152 if (this.selected && this.options.editable.updateTime && !this.dom.dragCenter) {
153 var me = this;
154 // create and show drag area
155 var dragCenter = document.createElement('div');
156 dragCenter.className = 'vis-drag-center';
157 dragCenter.dragCenterItem = this;
158 var hammer = new Hammer(dragCenter);
159
160 hammer.on('tap', function (event) {
161 me.parent.itemSet.body.emitter.emit('click', {
162 event: event,
163 item: me.id
164 });
165 });
166 hammer.on('doubletap', function (event) {
167 event.stopPropagation();
168 me.parent.itemSet._onUpdateItem(me);
169 me.parent.itemSet.body.emitter.emit('doubleClick', {
170 event: event,
171 item: me.id
172 });
173 });
174
175 if (this.dom.box) {
176 if (this.dom.dragLeft) {
177 this.dom.box.insertBefore(dragCenter, this.dom.dragLeft);
178 }
179 else {
180 this.dom.box.appendChild(dragCenter);
181 }
182 }
183 else if (this.dom.point) {
184 this.dom.point.appendChild(dragCenter);
185 }
186
187 this.dom.dragCenter = dragCenter;
188 }
189 else if (!this.selected && this.dom.dragCenter) {
190 // delete drag area
191 if (this.dom.dragCenter.parentNode) {
192 this.dom.dragCenter.parentNode.removeChild(this.dom.dragCenter);
193 }
194 this.dom.dragCenter = null;
195 }
196};
197
198/**
199 * Repaint a delete button on the top right of the item when the item is selected
200 * @param {HTMLElement} anchor
201 * @protected
202 */
203Item.prototype._repaintDeleteButton = function (anchor) {
204 var editable = ((this.options.editable.overrideItems || this.editable == null) && this.options.editable.remove) ||
205 (!this.options.editable.overrideItems && this.editable != null && this.editable.remove);
206
207 if (this.selected && editable && !this.dom.deleteButton) {
208 // create and show button
209 var me = this;
210
211 var deleteButton = document.createElement('div');
212
213 if (this.options.rtl) {
214 deleteButton.className = 'vis-delete-rtl';
215 } else {
216 deleteButton.className = 'vis-delete';
217 }
218 deleteButton.title = 'Delete this item';
219
220 // TODO: be able to destroy the delete button
221 new Hammer(deleteButton).on('tap', function (event) {
222 event.stopPropagation();
223 me.parent.removeFromDataSet(me);
224 });
225
226 anchor.appendChild(deleteButton);
227 this.dom.deleteButton = deleteButton;
228 }
229 else if (!this.selected && this.dom.deleteButton) {
230 // remove button
231 if (this.dom.deleteButton.parentNode) {
232 this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
233 }
234 this.dom.deleteButton = null;
235 }
236};
237
238/**
239 * Repaint a onChange tooltip on the top right of the item when the item is selected
240 * @param {HTMLElement} anchor
241 * @protected
242 */
243Item.prototype._repaintOnItemUpdateTimeTooltip = function (anchor) {
244 if (!this.options.tooltipOnItemUpdateTime) return;
245
246 var editable = (this.options.editable.updateTime ||
247 this.data.editable === true) &&
248 this.data.editable !== false;
249
250 if (this.selected && editable && !this.dom.onItemUpdateTimeTooltip) {
251 var onItemUpdateTimeTooltip = document.createElement('div');
252
253 onItemUpdateTimeTooltip.className = 'vis-onUpdateTime-tooltip';
254 anchor.appendChild(onItemUpdateTimeTooltip);
255 this.dom.onItemUpdateTimeTooltip = onItemUpdateTimeTooltip;
256
257 } else if (!this.selected && this.dom.onItemUpdateTimeTooltip) {
258 // remove button
259 if (this.dom.onItemUpdateTimeTooltip.parentNode) {
260 this.dom.onItemUpdateTimeTooltip.parentNode.removeChild(this.dom.onItemUpdateTimeTooltip);
261 }
262 this.dom.onItemUpdateTimeTooltip = null;
263 }
264
265 // position onChange tooltip
266 if (this.dom.onItemUpdateTimeTooltip) {
267
268 // only show when editing
269 this.dom.onItemUpdateTimeTooltip.style.visibility = this.parent.itemSet.touchParams.itemIsDragging ? 'visible' : 'hidden';
270
271 // position relative to item's content
272 if (this.options.rtl) {
273 this.dom.onItemUpdateTimeTooltip.style.right = this.dom.content.style.right;
274 } else {
275 this.dom.onItemUpdateTimeTooltip.style.left = this.dom.content.style.left;
276 }
277
278 // position above or below the item depending on the item's position in the window
279 var tooltipOffset = 50; // TODO: should be tooltip height (depends on template)
280 var scrollTop = this.parent.itemSet.body.domProps.scrollTop;
281
282 // TODO: this.top for orientation:true is actually the items distance from the bottom...
283 // (should be this.bottom)
284 var itemDistanceFromTop
285 if (this.options.orientation.item == 'top') {
286 itemDistanceFromTop = this.top;
287 } else {
288 itemDistanceFromTop = (this.parent.height - this.top - this.height)
289 }
290 var isCloseToTop = itemDistanceFromTop + this.parent.top - tooltipOffset < -scrollTop;
291
292 if (isCloseToTop) {
293 this.dom.onItemUpdateTimeTooltip.style.bottom = "";
294 this.dom.onItemUpdateTimeTooltip.style.top = this.height + 2 + "px";
295 } else {
296 this.dom.onItemUpdateTimeTooltip.style.top = "";
297 this.dom.onItemUpdateTimeTooltip.style.bottom = this.height + 2 + "px";
298 }
299
300 // handle tooltip content
301 var content;
302 var templateFunction;
303
304 if (this.options.tooltipOnItemUpdateTime && this.options.tooltipOnItemUpdateTime.template) {
305 templateFunction = this.options.tooltipOnItemUpdateTime.template.bind(this);
306 content = templateFunction(this.data);
307 } else {
308 content = 'start: ' + moment(this.data.start).format('MM/DD/YYYY hh:mm');
309 if (this.data.end) {
310 content += '<br> end: ' + moment(this.data.end).format('MM/DD/YYYY hh:mm');
311 }
312 }
313 this.dom.onItemUpdateTimeTooltip.innerHTML = content;
314 }
315};
316
317
318/**
319 * Set HTML contents for the item
320 * @param {Element} element HTML element to fill with the contents
321 * @private
322 */
323Item.prototype._updateContents = function (element) {
324 var content;
325 var changed;
326 var templateFunction;
327 var itemVisibleFrameContent;
328 var visibleFrameTemplateFunction;
329 var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset
330
331 var frameElement = this.dom.box || this.dom.point;
332 var itemVisibleFrameContentElement = frameElement.getElementsByClassName('vis-item-visible-frame')[0]
333
334 if (this.options.visibleFrameTemplate) {
335 visibleFrameTemplateFunction = this.options.visibleFrameTemplate.bind(this);
336 itemVisibleFrameContent = visibleFrameTemplateFunction(itemData, frameElement);
337 } else {
338 itemVisibleFrameContent = '';
339 }
340
341 if (itemVisibleFrameContentElement) {
342 if ((itemVisibleFrameContent instanceof Object) && !(itemVisibleFrameContent instanceof Element)) {
343 visibleFrameTemplateFunction(itemData, itemVisibleFrameContentElement)
344 } else {
345 changed = this._contentToString(this.itemVisibleFrameContent) !== this._contentToString(itemVisibleFrameContent);
346 if (changed) {
347 // only replace the content when changed
348 if (itemVisibleFrameContent instanceof Element) {
349 itemVisibleFrameContentElement.innerHTML = '';
350 itemVisibleFrameContentElement.appendChild(itemVisibleFrameContent);
351 }
352 else if (itemVisibleFrameContent != undefined) {
353 itemVisibleFrameContentElement.innerHTML = itemVisibleFrameContent;
354 }
355 else {
356 if (!(this.data.type == 'background' && this.data.content === undefined)) {
357 throw new Error('Property "content" missing in item ' + this.id);
358 }
359 }
360
361 this.itemVisibleFrameContent = itemVisibleFrameContent;
362 }
363 }
364 }
365
366 if (this.options.template) {
367 templateFunction = this.options.template.bind(this);
368 content = templateFunction(itemData, element, this.data);
369 } else {
370 content = this.data.content;
371 }
372
373 if ((content instanceof Object) && !(content instanceof Element)) {
374 templateFunction(itemData, element)
375 } else {
376 changed = this._contentToString(this.content) !== this._contentToString(content);
377 if (changed) {
378 // only replace the content when changed
379 if (content instanceof Element) {
380 element.innerHTML = '';
381 element.appendChild(content);
382 }
383 else if (content != undefined) {
384 element.innerHTML = content;
385 }
386 else {
387 if (!(this.data.type == 'background' && this.data.content === undefined)) {
388 throw new Error('Property "content" missing in item ' + this.id);
389 }
390 }
391 this.content = content;
392 }
393 }
394};
395
396/**
397 * Process dataAttributes timeline option and set as data- attributes on dom.content
398 * @param {Element} element HTML element to which the attributes will be attached
399 * @private
400 */
401 Item.prototype._updateDataAttributes = function(element) {
402 if (this.options.dataAttributes && this.options.dataAttributes.length > 0) {
403 var attributes = [];
404
405 if (Array.isArray(this.options.dataAttributes)) {
406 attributes = this.options.dataAttributes;
407 }
408 else if (this.options.dataAttributes == 'all') {
409 attributes = Object.keys(this.data);
410 }
411 else {
412 return;
413 }
414
415 for (var i = 0; i < attributes.length; i++) {
416 var name = attributes[i];
417 var value = this.data[name];
418
419 if (value != null) {
420 element.setAttribute('data-' + name, value);
421 }
422 else {
423 element.removeAttribute('data-' + name);
424 }
425 }
426 }
427};
428
429/**
430 * Update custom styles of the element
431 * @param {Element} element
432 * @private
433 */
434Item.prototype._updateStyle = function(element) {
435 // remove old styles
436 if (this.style) {
437 util.removeCssText(element, this.style);
438 this.style = null;
439 }
440
441 // append new styles
442 if (this.data.style) {
443 util.addCssText(element, this.data.style);
444 this.style = this.data.style;
445 }
446};
447
448
449/**
450 * Stringify the items contents
451 * @param {string | Element | undefined} content
452 * @returns {string | undefined}
453 * @private
454 */
455Item.prototype._contentToString = function (content) {
456 if (typeof content === 'string') return content;
457 if (content && 'outerHTML' in content) return content.outerHTML;
458 return content;
459};
460
461/**
462 * Update the editability of this item.
463 */
464Item.prototype._updateEditStatus = function() {
465 if (this.options) {
466 if(typeof this.options.editable === 'boolean') {
467 this.editable = {
468 updateTime: this.options.editable,
469 updateGroup: this.options.editable,
470 remove: this.options.editable
471 };
472 } else if(typeof this.options.editable === 'object') {
473 this.editable = {};
474 util.selectiveExtend(['updateTime', 'updateGroup', 'remove'], this.editable, this.options.editable);
475 }
476 }
477 // Item data overrides, except if options.editable.overrideItems is set.
478 if (!this.options || !(this.options.editable) || (this.options.editable.overrideItems !== true)) {
479 if (this.data) {
480 if (typeof this.data.editable === 'boolean') {
481 this.editable = {
482 updateTime: this.data.editable,
483 updateGroup: this.data.editable,
484 remove: this.data.editable
485 }
486 } else if (typeof this.data.editable === 'object') {
487 // TODO: in vis.js 5.0, we should change this to not reset options from the timeline configuration.
488 // Basically just remove the next line...
489 this.editable = {};
490 util.selectiveExtend(['updateTime', 'updateGroup', 'remove'], this.editable, this.data.editable);
491 }
492 }
493 }
494};
495
496/**
497 * Return the width of the item left from its start date
498 * @return {number}
499 */
500Item.prototype.getWidthLeft = function () {
501 return 0;
502};
503
504/**
505 * Return the width of the item right from the max of its start and end date
506 * @return {number}
507 */
508Item.prototype.getWidthRight = function () {
509 return 0;
510};
511
512/**
513 * Return the title of the item
514 * @return {string | undefined}
515 */
516Item.prototype.getTitle = function () {
517 return this.data.title;
518};
519
520export default Item;