vis.js is a dynamic, browser-based visualization library

868 lines
23 KiB

12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
  1. /**
  2. * An ItemSet holds a set of items and ranges which can be displayed in a
  3. * range. The width is determined by the parent of the ItemSet, and the height
  4. * is determined by the size of the items.
  5. * @param {Component} parent
  6. * @param {Component[]} [depends] Components on which this components depends
  7. * (except for the parent)
  8. * @param {Object} [options] See ItemSet.setOptions for the available
  9. * options.
  10. * @constructor ItemSet
  11. * @extends Panel
  12. */
  13. // TODO: improve performance by replacing all Array.forEach with a for loop
  14. function ItemSet(parent, depends, options) {
  15. this.id = util.randomUUID();
  16. this.parent = parent;
  17. this.depends = depends;
  18. // event listeners
  19. this.eventListeners = {
  20. dragstart: this._onDragStart.bind(this),
  21. drag: this._onDrag.bind(this),
  22. dragend: this._onDragEnd.bind(this)
  23. };
  24. // one options object is shared by this itemset and all its items
  25. this.options = options || {};
  26. this.defaultOptions = {
  27. type: 'box',
  28. align: 'center',
  29. orientation: 'bottom',
  30. margin: {
  31. axis: 20,
  32. item: 10
  33. },
  34. padding: 5
  35. };
  36. this.dom = {};
  37. var me = this;
  38. this.itemsData = null; // DataSet
  39. this.range = null; // Range or Object {start: number, end: number}
  40. // data change listeners
  41. this.listeners = {
  42. 'add': function (event, params, senderId) {
  43. if (senderId != me.id) {
  44. me._onAdd(params.items);
  45. }
  46. },
  47. 'update': function (event, params, senderId) {
  48. if (senderId != me.id) {
  49. me._onUpdate(params.items);
  50. }
  51. },
  52. 'remove': function (event, params, senderId) {
  53. if (senderId != me.id) {
  54. me._onRemove(params.items);
  55. }
  56. }
  57. };
  58. this.items = {}; // object with an Item for every data item
  59. this.orderedItems = []; // ordered items
  60. this.visibleItems = []; // visible, ordered items
  61. this.visibleItemsStart = 0; // start index of visible items in this.orderedItems
  62. this.visibleItemsEnd = 0; // start index of visible items in this.orderedItems
  63. this.selection = []; // list with the ids of all selected nodes
  64. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  65. this.stack = new Stack(this, Object.create(this.options));
  66. this.conversion = null;
  67. this.touchParams = {}; // stores properties while dragging
  68. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  69. }
  70. ItemSet.prototype = new Panel();
  71. // available item types will be registered here
  72. ItemSet.types = {
  73. box: ItemBox,
  74. range: ItemRange,
  75. rangeoverflow: ItemRangeOverflow,
  76. point: ItemPoint
  77. };
  78. /**
  79. * Set options for the ItemSet. Existing options will be extended/overwritten.
  80. * @param {Object} [options] The following options are available:
  81. * {String | function} [className]
  82. * class name for the itemset
  83. * {String} [type]
  84. * Default type for the items. Choose from 'box'
  85. * (default), 'point', or 'range'. The default
  86. * Style can be overwritten by individual items.
  87. * {String} align
  88. * Alignment for the items, only applicable for
  89. * ItemBox. Choose 'center' (default), 'left', or
  90. * 'right'.
  91. * {String} orientation
  92. * Orientation of the item set. Choose 'top' or
  93. * 'bottom' (default).
  94. * {Number} margin.axis
  95. * Margin between the axis and the items in pixels.
  96. * Default is 20.
  97. * {Number} margin.item
  98. * Margin between items in pixels. Default is 10.
  99. * {Number} padding
  100. * Padding of the contents of an item in pixels.
  101. * Must correspond with the items css. Default is 5.
  102. * {Function} snap
  103. * Function to let items snap to nice dates when
  104. * dragging items.
  105. */
  106. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  107. /**
  108. * Set controller for this component
  109. * @param {Controller | null} controller
  110. */
  111. ItemSet.prototype.setController = function setController (controller) {
  112. var event;
  113. // unregister old event listeners
  114. if (this.controller) {
  115. for (event in this.eventListeners) {
  116. if (this.eventListeners.hasOwnProperty(event)) {
  117. this.controller.off(event, this.eventListeners[event]);
  118. }
  119. }
  120. }
  121. this.controller = controller || null;
  122. // register new event listeners
  123. if (this.controller) {
  124. for (event in this.eventListeners) {
  125. if (this.eventListeners.hasOwnProperty(event)) {
  126. this.controller.on(event, this.eventListeners[event]);
  127. }
  128. }
  129. }
  130. };
  131. /**
  132. * Set range (start and end).
  133. * @param {Range | Object} range A Range or an object containing start and end.
  134. */
  135. ItemSet.prototype.setRange = function setRange(range) {
  136. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  137. throw new TypeError('Range must be an instance of Range, ' +
  138. 'or an object containing start and end.');
  139. }
  140. this.range = range;
  141. };
  142. /**
  143. * Set selected items by their id. Replaces the current selection
  144. * Unknown id's are silently ignored.
  145. * @param {Array} [ids] An array with zero or more id's of the items to be
  146. * selected. If ids is an empty array, all items will be
  147. * unselected.
  148. */
  149. ItemSet.prototype.setSelection = function setSelection(ids) {
  150. var i, ii, id, item, selection;
  151. if (ids) {
  152. if (!Array.isArray(ids)) {
  153. throw new TypeError('Array expected');
  154. }
  155. // unselect currently selected items
  156. for (i = 0, ii = this.selection.length; i < ii; i++) {
  157. id = this.selection[i];
  158. item = this.items[id];
  159. if (item) item.unselect();
  160. }
  161. // select items
  162. this.selection = [];
  163. for (i = 0, ii = ids.length; i < ii; i++) {
  164. id = ids[i];
  165. item = this.items[id];
  166. if (item) {
  167. this.selection.push(id);
  168. item.select();
  169. }
  170. }
  171. if (this.controller) {
  172. this.requestRepaint();
  173. }
  174. }
  175. };
  176. /**
  177. * Get the selected items by their id
  178. * @return {Array} ids The ids of the selected items
  179. */
  180. ItemSet.prototype.getSelection = function getSelection() {
  181. return this.selection.concat([]);
  182. };
  183. /**
  184. * Deselect a selected item
  185. * @param {String | Number} id
  186. * @private
  187. */
  188. ItemSet.prototype._deselect = function _deselect(id) {
  189. var selection = this.selection;
  190. for (var i = 0, ii = selection.length; i < ii; i++) {
  191. if (selection[i] == id) { // non-strict comparison!
  192. selection.splice(i, 1);
  193. break;
  194. }
  195. }
  196. };
  197. /**
  198. * Repaint the component
  199. * @return {Boolean} changed
  200. */
  201. ItemSet.prototype.repaint = function repaint() {
  202. var changed = 0,
  203. update = util.updateProperty,
  204. asSize = util.option.asSize,
  205. options = this.options,
  206. orientation = this.getOption('orientation'),
  207. frame = this.frame;
  208. this._updateConversion();
  209. if (!frame) {
  210. frame = document.createElement('div');
  211. frame.className = 'itemset';
  212. frame['timeline-itemset'] = this;
  213. var className = options.className;
  214. if (className) {
  215. util.addClassName(frame, util.option.asString(className));
  216. }
  217. // create background panel
  218. var background = document.createElement('div');
  219. background.className = 'background';
  220. frame.appendChild(background);
  221. this.dom.background = background;
  222. // create foreground panel
  223. var foreground = document.createElement('div');
  224. foreground.className = 'foreground';
  225. frame.appendChild(foreground);
  226. this.dom.foreground = foreground;
  227. // create axis panel
  228. var axis = document.createElement('div');
  229. axis.className = 'itemset-axis';
  230. //frame.appendChild(axis);
  231. this.dom.axis = axis;
  232. this.frame = frame;
  233. changed += 1;
  234. }
  235. if (!this.parent) {
  236. throw new Error('Cannot repaint itemset: no parent attached');
  237. }
  238. var parentContainer = this.parent.getContainer();
  239. if (!parentContainer) {
  240. throw new Error('Cannot repaint itemset: parent has no container element');
  241. }
  242. if (!frame.parentNode) {
  243. parentContainer.appendChild(frame);
  244. changed += 1;
  245. }
  246. if (!this.dom.axis.parentNode) {
  247. parentContainer.appendChild(this.dom.axis);
  248. changed += 1;
  249. }
  250. // reposition frame
  251. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  252. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  253. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  254. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  255. // reposition axis
  256. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  257. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  258. if (orientation == 'bottom') {
  259. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  260. }
  261. else { // orientation == 'top'
  262. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  263. }
  264. // find start of visible items
  265. var start = Math.min(this.visibleItemsStart, Math.max(this.orderedItems.length - 1, 0));
  266. var item = this.orderedItems[start];
  267. while (item && item.isVisible() && start > 0) {
  268. start--;
  269. item = this.orderedItems[start];
  270. }
  271. while (item && !item.isVisible()) {
  272. if (item.displayed) item.hide();
  273. start++;
  274. item = this.orderedItems[start];
  275. }
  276. this.visibleItemsStart = start;
  277. // find end of visible items
  278. var end = Math.max(Math.min(this.visibleItemsEnd, this.orderedItems.length), this.visibleItemsStart);
  279. item = this.orderedItems[end];
  280. while (item && item.isVisible()) {
  281. end++;
  282. item = this.orderedItems[end];
  283. }
  284. item = this.orderedItems[end - 1];
  285. while (item && !item.isVisible() && end > 0) {
  286. if (item.displayed) item.hide();
  287. end--;
  288. item = this.orderedItems[end - 1];
  289. }
  290. this.visibleItemsEnd = end;
  291. console.log('visible items', start, end); // TODO: cleanup
  292. this.visibleItems = this.orderedItems.slice(start, end);
  293. // check whether zoomed (in that case we need to re-stack everything)
  294. var visibleInterval = this.range.end - this.range.start;
  295. var zoomed = this.visibleInterval != visibleInterval;
  296. this.visibleInterval = visibleInterval;
  297. // show visible items
  298. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  299. var item = this.visibleItems[i];
  300. if (!item.displayed) item.show();
  301. if (zoomed) item.top = null; // reset stacking position
  302. // reposition item horizontally
  303. item.repositionX();
  304. }
  305. // reposition visible items vertically
  306. // TODO: improve stacking, when moving the timeline to the right, update stacking in backward order
  307. this.stack.stack(this.visibleItems);
  308. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  309. this.visibleItems[i].repositionY();
  310. }
  311. return false;
  312. };
  313. /**
  314. * Get the foreground container element
  315. * @return {HTMLElement} foreground
  316. */
  317. ItemSet.prototype.getForeground = function getForeground() {
  318. return this.dom.foreground;
  319. };
  320. /**
  321. * Get the background container element
  322. * @return {HTMLElement} background
  323. */
  324. ItemSet.prototype.getBackground = function getBackground() {
  325. return this.dom.background;
  326. };
  327. /**
  328. * Get the axis container element
  329. * @return {HTMLElement} axis
  330. */
  331. ItemSet.prototype.getAxis = function getAxis() {
  332. return this.dom.axis;
  333. };
  334. /**
  335. * Reflow the component
  336. * @return {Boolean} resized
  337. */
  338. ItemSet.prototype.reflow = function reflow () {
  339. var changed = 0,
  340. options = this.options,
  341. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  342. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  343. update = util.updateProperty,
  344. asNumber = util.option.asNumber,
  345. asSize = util.option.asSize,
  346. frame = this.frame;
  347. if (frame) {
  348. this._updateConversion();
  349. /* TODO
  350. util.forEach(this.items, function (item) {
  351. changed += item.reflow();
  352. });
  353. */
  354. // TODO: stack.update should be triggered via an event, in stack itself
  355. // TODO: only update the stack when there are changed items
  356. //this.stack.update();
  357. var maxHeight = asNumber(options.maxHeight);
  358. var fixedHeight = (asSize(options.height) != null);
  359. var height;
  360. if (fixedHeight) {
  361. height = frame.offsetHeight;
  362. }
  363. else {
  364. // height is not specified, determine the height from the height and positioned items
  365. var visibleItems = this.visibleItems; // TODO: not so nice way to get the filtered items
  366. if (visibleItems.length) { // TODO: calculate max height again
  367. var min = visibleItems[0].top;
  368. var max = visibleItems[0].top + visibleItems[0].height;
  369. util.forEach(visibleItems, function (item) {
  370. min = Math.min(min, item.top);
  371. max = Math.max(max, (item.top + item.height));
  372. });
  373. height = (max - min) + marginAxis + marginItem;
  374. }
  375. else {
  376. height = marginAxis + marginItem;
  377. }
  378. }
  379. if (maxHeight != null) {
  380. height = Math.min(height, maxHeight);
  381. }
  382. height = 200; // TODO: cleanup
  383. changed += update(this, 'height', height);
  384. // calculate height from items
  385. changed += update(this, 'top', frame.offsetTop);
  386. changed += update(this, 'left', frame.offsetLeft);
  387. changed += update(this, 'width', frame.offsetWidth);
  388. }
  389. else {
  390. changed += 1;
  391. }
  392. return false;
  393. };
  394. /**
  395. * Hide this component from the DOM
  396. * @return {Boolean} changed
  397. */
  398. ItemSet.prototype.hide = function hide() {
  399. var changed = false;
  400. // remove the DOM
  401. if (this.frame && this.frame.parentNode) {
  402. this.frame.parentNode.removeChild(this.frame);
  403. changed = true;
  404. }
  405. if (this.dom.axis && this.dom.axis.parentNode) {
  406. this.dom.axis.parentNode.removeChild(this.dom.axis);
  407. changed = true;
  408. }
  409. return changed;
  410. };
  411. /**
  412. * Set items
  413. * @param {vis.DataSet | null} items
  414. */
  415. ItemSet.prototype.setItems = function setItems(items) {
  416. var me = this,
  417. ids,
  418. oldItemsData = this.itemsData;
  419. // replace the dataset
  420. if (!items) {
  421. this.itemsData = null;
  422. }
  423. else if (items instanceof DataSet || items instanceof DataView) {
  424. this.itemsData = items;
  425. }
  426. else {
  427. throw new TypeError('Data must be an instance of DataSet');
  428. }
  429. if (oldItemsData) {
  430. // unsubscribe from old dataset
  431. util.forEach(this.listeners, function (callback, event) {
  432. oldItemsData.unsubscribe(event, callback);
  433. });
  434. // remove all drawn items
  435. ids = oldItemsData.getIds();
  436. this._onRemove(ids);
  437. }
  438. if (this.itemsData) {
  439. // subscribe to new dataset
  440. var id = this.id;
  441. util.forEach(this.listeners, function (callback, event) {
  442. me.itemsData.on(event, callback, id);
  443. });
  444. // draw all new items
  445. ids = this.itemsData.getIds();
  446. this._onAdd(ids);
  447. }
  448. };
  449. /**
  450. * Get the current items items
  451. * @returns {vis.DataSet | null}
  452. */
  453. ItemSet.prototype.getItems = function getItems() {
  454. return this.itemsData;
  455. };
  456. /**
  457. * Remove an item by its id
  458. * @param {String | Number} id
  459. */
  460. ItemSet.prototype.removeItem = function removeItem (id) {
  461. var item = this.itemsData.get(id),
  462. dataset = this._myDataSet();
  463. if (item) {
  464. // confirm deletion
  465. this.options.onRemove(item, function (item) {
  466. if (item) {
  467. dataset.remove(item);
  468. }
  469. });
  470. }
  471. };
  472. /**
  473. * Handle updated items
  474. * @param {Number[]} ids
  475. * @private
  476. */
  477. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  478. var me = this,
  479. defaultOptions = {
  480. type: 'box',
  481. align: 'center',
  482. orientation: 'bottom',
  483. margin: {
  484. axis: 20,
  485. item: 10
  486. },
  487. padding: 5
  488. };
  489. ids.forEach(function (id) {
  490. var itemData = me.itemsData.get(id),
  491. item = items[id],
  492. type = itemData.type ||
  493. (itemData.start && itemData.end && 'range') ||
  494. options.type ||
  495. 'box';
  496. var constructor = ItemSet.types[type];
  497. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  498. if (item) {
  499. // update item
  500. if (!constructor || !(item instanceof constructor)) {
  501. // item type has changed, hide and delete the item
  502. item.hide();
  503. item = null;
  504. }
  505. else {
  506. item.data = itemData; // TODO: create a method item.setData ?
  507. }
  508. }
  509. if (!item) {
  510. // create item
  511. if (constructor) {
  512. item = new constructor(me, itemData, options, defaultOptions);
  513. item.id = id;
  514. }
  515. else {
  516. throw new TypeError('Unknown item type "' + type + '"');
  517. }
  518. }
  519. me.items[id] = item;
  520. });
  521. this._order();
  522. this.repaint();
  523. };
  524. /**
  525. * Handle added items
  526. * @param {Number[]} ids
  527. * @private
  528. */
  529. ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
  530. /**
  531. * Handle removed items
  532. * @param {Number[]} ids
  533. * @private
  534. */
  535. ItemSet.prototype._onRemove = function _onRemove(ids) {
  536. var me = this;
  537. ids.forEach(function (id) {
  538. var item = me.items[id];
  539. if (item) {
  540. item.hide(); // TODO: only hide when displayed
  541. delete me.items[id];
  542. delete me.visibleItems[id];
  543. }
  544. });
  545. this._order();
  546. };
  547. /**
  548. * Order the items
  549. * @private
  550. */
  551. ItemSet.prototype._order = function _order() {
  552. // reorder the items
  553. this.orderedItems = this.stack.order(this.items);
  554. }
  555. /**
  556. * Calculate the scale and offset to convert a position on screen to the
  557. * corresponding date and vice versa.
  558. * After the method _updateConversion is executed once, the methods toTime
  559. * and toScreen can be used.
  560. * @private
  561. */
  562. ItemSet.prototype._updateConversion = function _updateConversion() {
  563. var range = this.range;
  564. if (!range) {
  565. throw new Error('No range configured');
  566. }
  567. if (range.conversion) {
  568. this.conversion = range.conversion(this.width);
  569. }
  570. else {
  571. this.conversion = Range.conversion(range.start, range.end, this.width);
  572. }
  573. };
  574. /**
  575. * Convert a position on screen (pixels) to a datetime
  576. * Before this method can be used, the method _updateConversion must be
  577. * executed once.
  578. * @param {int} x Position on the screen in pixels
  579. * @return {Date} time The datetime the corresponds with given position x
  580. */
  581. ItemSet.prototype.toTime = function toTime(x) {
  582. var conversion = this.conversion;
  583. return new Date(x / conversion.scale + conversion.offset);
  584. };
  585. /**
  586. * Convert a datetime (Date object) into a position on the screen
  587. * Before this method can be used, the method _updateConversion must be
  588. * executed once.
  589. * @param {Date} time A date
  590. * @return {int} x The position on the screen in pixels which corresponds
  591. * with the given date.
  592. */
  593. ItemSet.prototype.toScreen = function toScreen(time) {
  594. var conversion = this.conversion;
  595. return (time.valueOf() - conversion.offset) * conversion.scale;
  596. };
  597. /**
  598. * Start dragging the selected events
  599. * @param {Event} event
  600. * @private
  601. */
  602. ItemSet.prototype._onDragStart = function (event) {
  603. if (!this.options.editable) {
  604. return;
  605. }
  606. var item = ItemSet.itemFromTarget(event),
  607. me = this;
  608. if (item && item.selected) {
  609. var dragLeftItem = event.target.dragLeftItem;
  610. var dragRightItem = event.target.dragRightItem;
  611. if (dragLeftItem) {
  612. this.touchParams.itemProps = [{
  613. item: dragLeftItem,
  614. start: item.data.start.valueOf()
  615. }];
  616. }
  617. else if (dragRightItem) {
  618. this.touchParams.itemProps = [{
  619. item: dragRightItem,
  620. end: item.data.end.valueOf()
  621. }];
  622. }
  623. else {
  624. this.touchParams.itemProps = this.getSelection().map(function (id) {
  625. var item = me.items[id];
  626. var props = {
  627. item: item
  628. };
  629. if ('start' in item.data) {
  630. props.start = item.data.start.valueOf()
  631. }
  632. if ('end' in item.data) {
  633. props.end = item.data.end.valueOf()
  634. }
  635. return props;
  636. });
  637. }
  638. event.stopPropagation();
  639. }
  640. };
  641. /**
  642. * Drag selected items
  643. * @param {Event} event
  644. * @private
  645. */
  646. ItemSet.prototype._onDrag = function (event) {
  647. if (this.touchParams.itemProps) {
  648. var snap = this.options.snap || null,
  649. deltaX = event.gesture.deltaX,
  650. offset = deltaX / this.conversion.scale;
  651. // move
  652. this.touchParams.itemProps.forEach(function (props) {
  653. if ('start' in props) {
  654. var start = new Date(props.start + offset);
  655. props.item.data.start = snap ? snap(start) : start;
  656. }
  657. if ('end' in props) {
  658. var end = new Date(props.end + offset);
  659. props.item.data.end = snap ? snap(end) : end;
  660. }
  661. });
  662. // TODO: implement onMoving handler
  663. // TODO: implement dragging from one group to another
  664. this.requestReflow();
  665. event.stopPropagation();
  666. }
  667. };
  668. /**
  669. * End of dragging selected items
  670. * @param {Event} event
  671. * @private
  672. */
  673. ItemSet.prototype._onDragEnd = function (event) {
  674. if (this.touchParams.itemProps) {
  675. // prepare a change set for the changed items
  676. var changes = [],
  677. me = this,
  678. dataset = this._myDataSet(),
  679. type;
  680. this.touchParams.itemProps.forEach(function (props) {
  681. var id = props.item.id,
  682. item = me.itemsData.get(id);
  683. var changed = false;
  684. if ('start' in props.item.data) {
  685. changed = (props.start != props.item.data.start.valueOf());
  686. item.start = util.convert(props.item.data.start, dataset.convert['start']);
  687. }
  688. if ('end' in props.item.data) {
  689. changed = changed || (props.end != props.item.data.end.valueOf());
  690. item.end = util.convert(props.item.data.end, dataset.convert['end']);
  691. }
  692. // only apply changes when start or end is actually changed
  693. if (changed) {
  694. me.options.onMove(item, function (item) {
  695. if (item) {
  696. // apply changes
  697. changes.push(item);
  698. }
  699. else {
  700. // restore original values
  701. if ('start' in props) props.item.data.start = props.start;
  702. if ('end' in props) props.item.data.end = props.end;
  703. me.requestReflow();
  704. }
  705. });
  706. }
  707. });
  708. this.touchParams.itemProps = null;
  709. // apply the changes to the data (if there are changes)
  710. if (changes.length) {
  711. dataset.update(changes);
  712. }
  713. event.stopPropagation();
  714. }
  715. };
  716. /**
  717. * Find an item from an event target:
  718. * searches for the attribute 'timeline-item' in the event target's element tree
  719. * @param {Event} event
  720. * @return {Item | null} item
  721. */
  722. ItemSet.itemFromTarget = function itemFromTarget (event) {
  723. var target = event.target;
  724. while (target) {
  725. if (target.hasOwnProperty('timeline-item')) {
  726. return target['timeline-item'];
  727. }
  728. target = target.parentNode;
  729. }
  730. return null;
  731. };
  732. /**
  733. * Find the ItemSet from an event target:
  734. * searches for the attribute 'timeline-itemset' in the event target's element tree
  735. * @param {Event} event
  736. * @return {ItemSet | null} item
  737. */
  738. ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
  739. var target = event.target;
  740. while (target) {
  741. if (target.hasOwnProperty('timeline-itemset')) {
  742. return target['timeline-itemset'];
  743. }
  744. target = target.parentNode;
  745. }
  746. return null;
  747. };
  748. /**
  749. * Find the DataSet to which this ItemSet is connected
  750. * @returns {null | DataSet} dataset
  751. * @private
  752. */
  753. ItemSet.prototype._myDataSet = function _myDataSet() {
  754. // find the root DataSet
  755. var dataset = this.itemsData;
  756. while (dataset instanceof DataView) {
  757. dataset = dataset.data;
  758. }
  759. return dataset;
  760. };