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.

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