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.

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