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.

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