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.

454 lines
13 KiB

10 years ago
10 years ago
10 years ago
10 years ago
  1. var util = require('../../util');
  2. var stack = require('../Stack');
  3. var RangeItem = require('./item/RangeItem');
  4. var DateUtil = require('../DateUtil');
  5. /**
  6. * @constructor Group
  7. * @param {Number | String} groupId
  8. * @param {Object} data
  9. * @param {ItemSet} itemSet
  10. */
  11. function Group (groupId, data, itemSet) {
  12. this.groupId = groupId;
  13. this.itemSet = itemSet;
  14. this.dom = {};
  15. this.props = {
  16. label: {
  17. width: 0,
  18. height: 0
  19. }
  20. };
  21. this.className = null;
  22. this.items = {}; // items filtered by groupId of this group
  23. this.visibleItems = []; // items currently visible in window
  24. this.orderedItems = { // items sorted by start and by end
  25. byStart: [],
  26. byEnd: []
  27. };
  28. this._create();
  29. this.setData(data);
  30. }
  31. /**
  32. * Create DOM elements for the group
  33. * @private
  34. */
  35. Group.prototype._create = function() {
  36. var label = document.createElement('div');
  37. label.className = 'vlabel';
  38. this.dom.label = label;
  39. var inner = document.createElement('div');
  40. inner.className = 'inner';
  41. label.appendChild(inner);
  42. this.dom.inner = inner;
  43. var foreground = document.createElement('div');
  44. foreground.className = 'group';
  45. foreground['timeline-group'] = this;
  46. this.dom.foreground = foreground;
  47. this.dom.background = document.createElement('div');
  48. this.dom.background.className = 'group';
  49. this.dom.axis = document.createElement('div');
  50. this.dom.axis.className = 'group';
  51. // create a hidden marker to detect when the Timelines container is attached
  52. // to the DOM, or the style of a parent of the Timeline is changed from
  53. // display:none is changed to visible.
  54. this.dom.marker = document.createElement('div');
  55. this.dom.marker.style.visibility = 'hidden';
  56. this.dom.marker.innerHTML = '?';
  57. this.dom.background.appendChild(this.dom.marker);
  58. };
  59. /**
  60. * Set the group data for this group
  61. * @param {Object} data Group data, can contain properties content and className
  62. */
  63. Group.prototype.setData = function(data) {
  64. // update contents
  65. var content = data && data.content;
  66. if (content instanceof Element) {
  67. this.dom.inner.appendChild(content);
  68. }
  69. else if (content !== undefined && content !== null) {
  70. this.dom.inner.innerHTML = content;
  71. }
  72. else {
  73. this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null
  74. }
  75. // update title
  76. this.dom.label.title = data && data.title || '';
  77. if (!this.dom.inner.firstChild) {
  78. util.addClassName(this.dom.inner, 'hidden');
  79. }
  80. else {
  81. util.removeClassName(this.dom.inner, 'hidden');
  82. }
  83. // update className
  84. var className = data && data.className || null;
  85. if (className != this.className) {
  86. if (this.className) {
  87. util.removeClassName(this.dom.label, this.className);
  88. util.removeClassName(this.dom.foreground, this.className);
  89. util.removeClassName(this.dom.background, this.className);
  90. util.removeClassName(this.dom.axis, this.className);
  91. }
  92. util.addClassName(this.dom.label, className);
  93. util.addClassName(this.dom.foreground, className);
  94. util.addClassName(this.dom.background, className);
  95. util.addClassName(this.dom.axis, className);
  96. this.className = className;
  97. }
  98. // update style
  99. if (this.style) {
  100. util.removeCssText(this.dom.label, this.style);
  101. this.style = null;
  102. }
  103. if (data && data.style) {
  104. util.addCssText(this.dom.label, data.style);
  105. this.style = data.style;
  106. }
  107. };
  108. /**
  109. * Get the width of the group label
  110. * @return {number} width
  111. */
  112. Group.prototype.getLabelWidth = function() {
  113. return this.props.label.width;
  114. };
  115. /**
  116. * Repaint this group
  117. * @param {{start: number, end: number}} range
  118. * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
  119. * @param {boolean} [restack=false] Force restacking of all items
  120. * @return {boolean} Returns true if the group is resized
  121. */
  122. Group.prototype.redraw = function(range, margin, restack) {
  123. var resized = false;
  124. this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
  125. // force recalculation of the height of the items when the marker height changed
  126. // (due to the Timeline being attached to the DOM or changed from display:none to visible)
  127. var markerHeight = this.dom.marker.clientHeight;
  128. if (markerHeight != this.lastMarkerHeight) {
  129. this.lastMarkerHeight = markerHeight;
  130. util.forEach(this.items, function (item) {
  131. item.dirty = true;
  132. if (item.displayed) item.redraw();
  133. });
  134. restack = true;
  135. }
  136. // reposition visible items vertically
  137. if (this.itemSet.options.stack) { // TODO: ugly way to access options...
  138. stack.stack(this.visibleItems, margin, restack);
  139. }
  140. else { // no stacking
  141. stack.nostack(this.visibleItems, margin);
  142. }
  143. // recalculate the height of the group
  144. var height;
  145. var visibleItems = this.visibleItems;
  146. if (visibleItems.length) {
  147. var min = visibleItems[0].top;
  148. var max = visibleItems[0].top + visibleItems[0].height;
  149. util.forEach(visibleItems, function (item) {
  150. min = Math.min(min, item.top);
  151. max = Math.max(max, (item.top + item.height));
  152. });
  153. if (min > margin.axis) {
  154. // there is an empty gap between the lowest item and the axis
  155. var offset = min - margin.axis;
  156. max -= offset;
  157. util.forEach(visibleItems, function (item) {
  158. item.top -= offset;
  159. });
  160. }
  161. height = max + margin.item.vertical / 2;
  162. }
  163. else {
  164. height = margin.axis + margin.item.vertical;
  165. }
  166. height = Math.max(height, this.props.label.height);
  167. // calculate actual size and position
  168. var foreground = this.dom.foreground;
  169. this.top = foreground.offsetTop;
  170. this.left = foreground.offsetLeft;
  171. this.width = foreground.offsetWidth;
  172. resized = util.updateProperty(this, 'height', height) || resized;
  173. // recalculate size of label
  174. resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
  175. resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
  176. // apply new height
  177. this.dom.background.style.height = height + 'px';
  178. this.dom.foreground.style.height = height + 'px';
  179. this.dom.label.style.height = height + 'px';
  180. // update vertical position of items after they are re-stacked and the height of the group is calculated
  181. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  182. var item = this.visibleItems[i];
  183. item.repositionY();
  184. }
  185. return resized;
  186. };
  187. /**
  188. * Show this group: attach to the DOM
  189. */
  190. Group.prototype.show = function() {
  191. if (!this.dom.label.parentNode) {
  192. this.itemSet.dom.labelSet.appendChild(this.dom.label);
  193. }
  194. if (!this.dom.foreground.parentNode) {
  195. this.itemSet.dom.foreground.appendChild(this.dom.foreground);
  196. }
  197. if (!this.dom.background.parentNode) {
  198. this.itemSet.dom.background.appendChild(this.dom.background);
  199. }
  200. if (!this.dom.axis.parentNode) {
  201. this.itemSet.dom.axis.appendChild(this.dom.axis);
  202. }
  203. };
  204. /**
  205. * Hide this group: remove from the DOM
  206. */
  207. Group.prototype.hide = function() {
  208. var label = this.dom.label;
  209. if (label.parentNode) {
  210. label.parentNode.removeChild(label);
  211. }
  212. var foreground = this.dom.foreground;
  213. if (foreground.parentNode) {
  214. foreground.parentNode.removeChild(foreground);
  215. }
  216. var background = this.dom.background;
  217. if (background.parentNode) {
  218. background.parentNode.removeChild(background);
  219. }
  220. var axis = this.dom.axis;
  221. if (axis.parentNode) {
  222. axis.parentNode.removeChild(axis);
  223. }
  224. };
  225. /**
  226. * Add an item to the group
  227. * @param {Item} item
  228. */
  229. Group.prototype.add = function(item) {
  230. this.items[item.id] = item;
  231. item.setParent(this);
  232. if (this.visibleItems.indexOf(item) == -1) {
  233. var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
  234. this._checkIfVisible(item, this.visibleItems, range);
  235. }
  236. };
  237. /**
  238. * Remove an item from the group
  239. * @param {Item} item
  240. */
  241. Group.prototype.remove = function(item) {
  242. delete this.items[item.id];
  243. item.setParent(this.itemSet);
  244. // remove from visible items
  245. var index = this.visibleItems.indexOf(item);
  246. if (index != -1) this.visibleItems.splice(index, 1);
  247. // TODO: also remove from ordered items?
  248. };
  249. /**
  250. * Remove an item from the corresponding DataSet
  251. * @param {Item} item
  252. */
  253. Group.prototype.removeFromDataSet = function(item) {
  254. this.itemSet.removeItem(item.id);
  255. };
  256. /**
  257. * Reorder the items
  258. */
  259. Group.prototype.order = function() {
  260. var array = util.toArray(this.items);
  261. this.orderedItems.byStart = array;
  262. this.orderedItems.byEnd = this._constructByEndArray(array);
  263. stack.orderByStart(this.orderedItems.byStart);
  264. stack.orderByEnd(this.orderedItems.byEnd);
  265. };
  266. /**
  267. * Create an array containing all items being a range (having an end date)
  268. * @param {Item[]} array
  269. * @returns {RangeItem[]}
  270. * @private
  271. */
  272. Group.prototype._constructByEndArray = function(array) {
  273. var endArray = [];
  274. for (var i = 0; i < array.length; i++) {
  275. if (array[i] instanceof RangeItem) {
  276. endArray.push(array[i]);
  277. }
  278. }
  279. return endArray;
  280. };
  281. /**
  282. * Update the visible items
  283. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
  284. * @param {Item[]} visibleItems The previously visible items.
  285. * @param {{start: number, end: number}} range Visible range
  286. * @return {Item[]} visibleItems The new visible items.
  287. * @private
  288. */
  289. Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) {
  290. var initialPosByStart,
  291. newVisibleItems = [],
  292. i;
  293. // first check if the items that were in view previously are still in view.
  294. // this handles the case for the RangeItem that is both before and after the current one.
  295. if (visibleItems.length > 0) {
  296. for (i = 0; i < visibleItems.length; i++) {
  297. this._checkIfVisible(visibleItems[i], newVisibleItems, range);
  298. }
  299. }
  300. // If there were no visible items previously, use binarySearch to find a visible PointItem or RangeItem (based on startTime)
  301. if (newVisibleItems.length == 0) {
  302. initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start');
  303. }
  304. else {
  305. initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
  306. }
  307. // use visible search to find a visible RangeItem (only based on endTime)
  308. var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end');
  309. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  310. if (initialPosByStart != -1) {
  311. for (i = initialPosByStart; i >= 0; i--) {
  312. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  313. }
  314. for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
  315. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  316. }
  317. }
  318. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  319. if (initialPosByEnd != -1) {
  320. for (i = initialPosByEnd; i >= 0; i--) {
  321. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  322. }
  323. for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
  324. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  325. }
  326. }
  327. return newVisibleItems;
  328. };
  329. /**
  330. * this function checks if an item is invisible. If it is NOT we make it visible
  331. * and add it to the global visible items. If it is, return true.
  332. *
  333. * @param {Item} item
  334. * @param {Item[]} visibleItems
  335. * @param {{start:number, end:number}} range
  336. * @returns {boolean}
  337. * @private
  338. */
  339. Group.prototype._checkIfInvisible = function(item, visibleItems, range) {
  340. //if (DateUtil.isHidden(item.data.start,this.itemSet.body.hiddenDates).hidden == false) {
  341. if (item.isVisible(range)) {
  342. if (!item.displayed) item.show();
  343. item.repositionX();
  344. if (visibleItems.indexOf(item) == -1) {
  345. visibleItems.push(item);
  346. }
  347. return false;
  348. }
  349. else {
  350. if (item.displayed) item.hide();
  351. return true;
  352. }
  353. //}
  354. //else {
  355. // if (item.isVisible(range)) {
  356. // return false;
  357. // }
  358. // else {
  359. // if (item.displayed) item.hide();
  360. // return true;
  361. // }
  362. //}
  363. };
  364. /**
  365. * this function is very similar to the _checkIfInvisible() but it does not
  366. * return booleans, hides the item if it should not be seen and always adds to
  367. * the visibleItems.
  368. * this one is for brute forcing and hiding.
  369. *
  370. * @param {Item} item
  371. * @param {Array} visibleItems
  372. * @param {{start:number, end:number}} range
  373. * @private
  374. */
  375. Group.prototype._checkIfVisible = function(item, visibleItems, range) {
  376. //if (DateUtil.isHidden(item.data.start,this.itemSet.body.hiddenDates).hidden == false) {
  377. if (item.isVisible(range)) {
  378. if (!item.displayed) item.show();
  379. // reposition item horizontally
  380. item.repositionX();
  381. visibleItems.push(item);
  382. }
  383. else {
  384. if (item.displayed) item.hide();
  385. }
  386. //}
  387. //else {
  388. // if (!item.isVisible(range)) {
  389. // if (item.displayed) item.hide();
  390. // }
  391. //}
  392. };
  393. module.exports = Group;