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.

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