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.

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