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.

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