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.

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