RangeItem.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. var Item = require('./Item');
  2. /**
  3. * @constructor RangeItem
  4. * @extends Item
  5. * @param {Object} data Object containing parameters start, end
  6. * content, className.
  7. * @param {{toScreen: function, toTime: function}} conversion
  8. * Conversion functions from time to screen and vice versa
  9. * @param {Object} [options] Configuration options
  10. * // TODO: describe options
  11. */
  12. function RangeItem (data, conversion, options) {
  13. this.props = {
  14. content: {
  15. width: 0
  16. }
  17. };
  18. this.overflow = false; // if contents can overflow (css styling), this flag is set to true
  19. this.options = options;
  20. // validate data
  21. if (data) {
  22. if (data.start == undefined) {
  23. throw new Error('Property "start" missing in item ' + data.id);
  24. }
  25. if (data.end == undefined) {
  26. throw new Error('Property "end" missing in item ' + data.id);
  27. }
  28. }
  29. Item.call(this, data, conversion, options);
  30. }
  31. RangeItem.prototype = new Item (null, null, null);
  32. RangeItem.prototype.baseClassName = 'vis-item vis-range';
  33. /**
  34. * Check whether this item is visible inside given range
  35. *
  36. * @param {vis.Range} range with a timestamp for start and end
  37. * @returns {boolean} True if visible
  38. */
  39. RangeItem.prototype.isVisible = function(range) {
  40. // determine visibility
  41. return (this.data.start < range.end) && (this.data.end > range.start);
  42. };
  43. RangeItem.prototype._createDomElement = function() {
  44. if (!this.dom) {
  45. // create DOM
  46. this.dom = {};
  47. // background box
  48. this.dom.box = document.createElement('div');
  49. // className is updated in redraw()
  50. // frame box (to prevent the item contents from overflowing)
  51. this.dom.frame = document.createElement('div');
  52. this.dom.frame.className = 'vis-item-overflow';
  53. this.dom.box.appendChild(this.dom.frame);
  54. // visible frame box (showing the frame that is always visible)
  55. this.dom.visibleFrame = document.createElement('div');
  56. this.dom.visibleFrame.className = 'vis-item-visible-frame';
  57. this.dom.box.appendChild(this.dom.visibleFrame);
  58. // contents box
  59. this.dom.content = document.createElement('div');
  60. this.dom.content.className = 'vis-item-content';
  61. this.dom.frame.appendChild(this.dom.content);
  62. // attach this item as attribute
  63. this.dom.box['timeline-item'] = this;
  64. this.dirty = true;
  65. }
  66. }
  67. RangeItem.prototype._appendDomElement = function() {
  68. if (!this.parent) {
  69. throw new Error('Cannot redraw item: no parent attached');
  70. }
  71. if (!this.dom.box.parentNode) {
  72. var foreground = this.parent.dom.foreground;
  73. if (!foreground) {
  74. throw new Error('Cannot redraw item: parent has no foreground container element');
  75. }
  76. foreground.appendChild(this.dom.box);
  77. }
  78. this.displayed = true;
  79. }
  80. RangeItem.prototype._updateDirtyDomComponents = function() {
  81. // update dirty DOM. An item is marked dirty when:
  82. // - the item is not yet rendered
  83. // - the item's data is changed
  84. // - the item is selected/deselected
  85. if (this.dirty) {
  86. this._updateContents(this.dom.content);
  87. this._updateDataAttributes(this.dom.box);
  88. this._updateStyle(this.dom.box);
  89. var editable = (this.editable.updateTime || this.editable.updateGroup);
  90. // update class
  91. var className = (this.data.className ? (' ' + this.data.className) : '') +
  92. (this.selected ? ' vis-selected' : '') +
  93. (editable ? ' vis-editable' : ' vis-readonly');
  94. this.dom.box.className = this.baseClassName + className;
  95. // turn off max-width to be able to calculate the real width
  96. // this causes an extra browser repaint/reflow, but so be it
  97. this.dom.content.style.maxWidth = 'none';
  98. }
  99. }
  100. RangeItem.prototype._getDomComponentsSizes = function() {
  101. // determine from css whether this box has overflow
  102. this.overflow = window.getComputedStyle(this.dom.frame).overflow !== 'hidden';
  103. return {
  104. content: {
  105. width: this.dom.content.offsetWidth,
  106. },
  107. box: {
  108. height: this.dom.box.offsetHeight
  109. }
  110. }
  111. }
  112. RangeItem.prototype._updateDomComponentsSizes = function(sizes) {
  113. this.props.content.width = sizes.content.width;
  114. this.height = sizes.box.height;
  115. this.dom.content.style.maxWidth = '';
  116. this.dirty = false;
  117. }
  118. RangeItem.prototype._repaintDomAdditionals = function() {
  119. this._repaintOnItemUpdateTimeTooltip(this.dom.box);
  120. this._repaintDeleteButton(this.dom.box);
  121. this._repaintDragCenter();
  122. this._repaintDragLeft();
  123. this._repaintDragRight();
  124. }
  125. /**
  126. * Repaint the item
  127. * @param {boolean} [returnQueue=false] return the queue
  128. * @return {boolean} the redraw queue if returnQueue=true
  129. */
  130. RangeItem.prototype.redraw = function(returnQueue) {
  131. var sizes;
  132. var queue = [
  133. // create item DOM
  134. this._createDomElement.bind(this),
  135. // append DOM to parent DOM
  136. this._appendDomElement.bind(this),
  137. // update dirty DOM
  138. this._updateDirtyDomComponents.bind(this),
  139. (function() {
  140. if (this.dirty) {
  141. sizes = this._getDomComponentsSizes.bind(this)();
  142. }
  143. }).bind(this),
  144. (function() {
  145. if (this.dirty) {
  146. this._updateDomComponentsSizes.bind(this)(sizes);
  147. }
  148. }).bind(this),
  149. // repaint DOM additionals
  150. this._repaintDomAdditionals.bind(this)
  151. ];
  152. if (returnQueue) {
  153. return queue;
  154. } else {
  155. var result;
  156. queue.forEach(function (fn) {
  157. result = fn();
  158. });
  159. return result;
  160. }
  161. };
  162. /**
  163. * Show the item in the DOM (when not already visible). The items DOM will
  164. * be created when needed.
  165. */
  166. RangeItem.prototype.show = function() {
  167. if (!this.displayed) {
  168. this.redraw();
  169. }
  170. };
  171. /**
  172. * Hide the item from the DOM (when visible)
  173. */
  174. RangeItem.prototype.hide = function() {
  175. if (this.displayed) {
  176. var box = this.dom.box;
  177. if (box.parentNode) {
  178. box.parentNode.removeChild(box);
  179. }
  180. this.displayed = false;
  181. }
  182. };
  183. /**
  184. * Reposition the item horizontally
  185. * @param {boolean} [limitSize=true] If true (default), the width of the range
  186. * item will be limited, as the browser cannot
  187. * display very wide divs. This means though
  188. * that the applied left and width may
  189. * not correspond to the ranges start and end
  190. * @Override
  191. */
  192. RangeItem.prototype.repositionX = function(limitSize) {
  193. var parentWidth = this.parent.width;
  194. var start = this.conversion.toScreen(this.data.start);
  195. var end = this.conversion.toScreen(this.data.end);
  196. var align = this.data.align === undefined ? this.options.align : this.data.align;
  197. var contentStartPosition;
  198. var contentWidth;
  199. // limit the width of the range, as browsers cannot draw very wide divs
  200. // unless limitSize: false is explicitly set in item data
  201. if (this.data.limitSize !== false && (limitSize === undefined || limitSize === true)) {
  202. if (start < -parentWidth) {
  203. start = -parentWidth;
  204. }
  205. if (end > 2 * parentWidth) {
  206. end = 2 * parentWidth;
  207. }
  208. }
  209. // add 0.5 to compensate floating-point values rounding
  210. var boxWidth = Math.max(end - start + 0.5, 1);
  211. if (this.overflow) {
  212. if (this.options.rtl) {
  213. this.right = start;
  214. } else {
  215. this.left = start;
  216. }
  217. this.width = boxWidth + this.props.content.width;
  218. contentWidth = this.props.content.width;
  219. // Note: The calculation of width is an optimistic calculation, giving
  220. // a width which will not change when moving the Timeline
  221. // So no re-stacking needed, which is nicer for the eye;
  222. }
  223. else {
  224. if (this.options.rtl) {
  225. this.right = start;
  226. } else {
  227. this.left = start;
  228. }
  229. this.width = boxWidth;
  230. contentWidth = Math.min(end - start, this.props.content.width);
  231. }
  232. if (this.options.rtl) {
  233. this.dom.box.style.right = this.right + 'px';
  234. } else {
  235. this.dom.box.style.left = this.left + 'px';
  236. }
  237. this.dom.box.style.width = boxWidth + 'px';
  238. switch (align) {
  239. case 'left':
  240. if (this.options.rtl) {
  241. this.dom.content.style.right = '0';
  242. } else {
  243. this.dom.content.style.left = '0';
  244. }
  245. break;
  246. case 'right':
  247. if (this.options.rtl) {
  248. this.dom.content.style.right = Math.max((boxWidth - contentWidth), 0) + 'px';
  249. } else {
  250. this.dom.content.style.left = Math.max((boxWidth - contentWidth), 0) + 'px';
  251. }
  252. break;
  253. case 'center':
  254. if (this.options.rtl) {
  255. this.dom.content.style.right = Math.max((boxWidth - contentWidth) / 2, 0) + 'px';
  256. } else {
  257. this.dom.content.style.left = Math.max((boxWidth - contentWidth) / 2, 0) + 'px';
  258. }
  259. break;
  260. default: // 'auto'
  261. // when range exceeds left of the window, position the contents at the left of the visible area
  262. if (this.overflow) {
  263. if (end > 0) {
  264. contentStartPosition = Math.max(-start, 0);
  265. }
  266. else {
  267. contentStartPosition = -contentWidth; // ensure it's not visible anymore
  268. }
  269. }
  270. else {
  271. if (start < 0) {
  272. contentStartPosition = -start;
  273. }
  274. else {
  275. contentStartPosition = 0;
  276. }
  277. }
  278. if (this.options.rtl) {
  279. this.dom.content.style.right = contentStartPosition + 'px';
  280. } else {
  281. this.dom.content.style.left = contentStartPosition + 'px';
  282. this.dom.content.style.width = 'calc(100% - ' + contentStartPosition + 'px)';
  283. }
  284. }
  285. };
  286. /**
  287. * Reposition the item vertically
  288. * @Override
  289. */
  290. RangeItem.prototype.repositionY = function() {
  291. var orientation = this.options.orientation.item;
  292. var box = this.dom.box;
  293. if (orientation == 'top') {
  294. box.style.top = this.top + 'px';
  295. }
  296. else {
  297. box.style.top = (this.parent.height - this.top - this.height) + 'px';
  298. }
  299. };
  300. /**
  301. * Repaint a drag area on the left side of the range when the range is selected
  302. * @protected
  303. */
  304. RangeItem.prototype._repaintDragLeft = function () {
  305. if ((this.selected || this.options.itemsAlwaysDraggable.range) && this.options.editable.updateTime && !this.dom.dragLeft) {
  306. // create and show drag area
  307. var dragLeft = document.createElement('div');
  308. dragLeft.className = 'vis-drag-left';
  309. dragLeft.dragLeftItem = this;
  310. this.dom.box.appendChild(dragLeft);
  311. this.dom.dragLeft = dragLeft;
  312. }
  313. else if (!this.selected && !this.options.itemsAlwaysDraggable.range && this.dom.dragLeft) {
  314. // delete drag area
  315. if (this.dom.dragLeft.parentNode) {
  316. this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
  317. }
  318. this.dom.dragLeft = null;
  319. }
  320. };
  321. /**
  322. * Repaint a drag area on the right side of the range when the range is selected
  323. * @protected
  324. */
  325. RangeItem.prototype._repaintDragRight = function () {
  326. if ((this.selected || this.options.itemsAlwaysDraggable.range) && this.options.editable.updateTime && !this.dom.dragRight) {
  327. // create and show drag area
  328. var dragRight = document.createElement('div');
  329. dragRight.className = 'vis-drag-right';
  330. dragRight.dragRightItem = this;
  331. this.dom.box.appendChild(dragRight);
  332. this.dom.dragRight = dragRight;
  333. }
  334. else if (!this.selected && !this.options.itemsAlwaysDraggable.range && this.dom.dragRight) {
  335. // delete drag area
  336. if (this.dom.dragRight.parentNode) {
  337. this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
  338. }
  339. this.dom.dragRight = null;
  340. }
  341. };
  342. module.exports = RangeItem;