vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

519 lines
15 KiB

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