timeline-view.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. /**
  6. * @api
  7. */
  8. define([
  9. 'ko',
  10. 'Magento_Ui/js/lib/view/utils/async',
  11. 'underscore',
  12. 'Magento_Ui/js/lib/view/utils/raf',
  13. 'uiRegistry',
  14. 'uiClass'
  15. ], function (ko, $, _, raf, registry, Class) {
  16. 'use strict';
  17. var hasClassList = (function () {
  18. var list = document.createElement('_').classList;
  19. return !!list && !list.toggle('_test', false);
  20. })();
  21. /**
  22. * Polyfill of the 'classList.toggle' method.
  23. *
  24. * @param {HTMLElement} elem
  25. */
  26. function toggleClass(elem) {
  27. var classList = elem.classList,
  28. args = Array.prototype.slice.call(arguments, 1),
  29. $elem;
  30. if (hasClassList) {
  31. classList.toggle.apply(classList, args);
  32. } else {
  33. $elem = $(elem);
  34. $elem.toggleClass.apply($elem, args);
  35. }
  36. }
  37. return Class.extend({
  38. defaults: {
  39. selectors: {
  40. content: '.timeline-content',
  41. timeUnit: '.timeline-unit',
  42. item: '.timeline-item:not([data-role=no-data-msg])',
  43. event: '.timeline-event'
  44. }
  45. },
  46. /**
  47. * Initializes TimelineView component.
  48. *
  49. * @returns {TimelineView} Chainable.
  50. */
  51. initialize: function () {
  52. _.bindAll(
  53. this,
  54. 'refresh',
  55. 'initContent',
  56. 'initItem',
  57. 'initTimeUnit',
  58. 'getItemBindings',
  59. 'updateItemsPosition',
  60. 'onScaleChange',
  61. 'onEventElementRender',
  62. 'onWindowResize',
  63. 'onContentScroll',
  64. 'onDataReloaded',
  65. 'onToStartClick',
  66. 'onToEndClick'
  67. );
  68. this._super()
  69. .initModel()
  70. .waitContent();
  71. return this;
  72. },
  73. /**
  74. * Applies listeners for the model properties changes.
  75. *
  76. * @returns {TimelineView} Chainable.
  77. */
  78. initModel: function () {
  79. var model = registry.get(this.model);
  80. model.on('scale', this.onScaleChange);
  81. model.source.on('reloaded', this.onDataReloaded);
  82. this.model = model;
  83. return this;
  84. },
  85. /**
  86. * Applies DOM watcher for the
  87. * content element rendering.
  88. *
  89. * @returns {TimelineView} Chainable.
  90. */
  91. waitContent: function () {
  92. $.async({
  93. selector: this.selectors.content,
  94. component: this.model
  95. }, this.initContent);
  96. return this;
  97. },
  98. /**
  99. * Initializes timelines' content element.
  100. *
  101. * @param {HTMLElement} content
  102. * @returns {TimelineView} Chainable.
  103. */
  104. initContent: function (content) {
  105. this.$content = content;
  106. $(content).on('scroll', this.onContentScroll);
  107. $(window).on('resize', this.onWindowResize);
  108. $.async(this.selectors.item, content, this.initItem);
  109. $.async(this.selectors.event, content, this.onEventElementRender);
  110. $.async(this.selectors.timeUnit, content, this.initTimeUnit);
  111. this.refresh();
  112. return this;
  113. },
  114. /**
  115. * Initializes timeline item element,
  116. * e.g. establishes event listeners and applies data bindings.
  117. *
  118. * @param {HTMLElement} elem
  119. * @returns {TimelineView} Chainable.
  120. */
  121. initItem: function (elem) {
  122. $(elem)
  123. .bindings(this.getItemBindings)
  124. .on('click', '._toend', this.onToEndClick)
  125. .on('click', '._tostart', this.onToStartClick);
  126. return this;
  127. },
  128. /**
  129. * Initializes timeline unit element.
  130. *
  131. * @param {HTMLElement} elem
  132. * @returns {TimelineView} Chainable.
  133. */
  134. initTimeUnit: function (elem) {
  135. $(elem).bindings(this.getTimeUnitBindings());
  136. return this;
  137. },
  138. /**
  139. * Updates items positions in a
  140. * loop if state of a view has changed.
  141. */
  142. refresh: function () {
  143. raf(this.refresh);
  144. if (this._update) {
  145. this._update = false;
  146. this.updateItemsPosition();
  147. }
  148. },
  149. /**
  150. * Returns object width additional bindings
  151. * for a timeline unit element.
  152. *
  153. * @returns {Object}
  154. */
  155. getTimeUnitBindings: function () {
  156. return {
  157. style: {
  158. width: ko.computed(function () {
  159. return this.getTimeUnitWidth() + '%';
  160. }.bind(this))
  161. }
  162. };
  163. },
  164. /**
  165. * Returns object with additional
  166. * bindings for a timeline item element.
  167. *
  168. * @param {Object} ctx
  169. * @returns {Object}
  170. */
  171. getItemBindings: function (ctx) {
  172. return {
  173. style: {
  174. width: ko.computed(function () {
  175. return this.getItemWidth(ctx.$row()) + '%';
  176. }.bind(this)),
  177. 'margin-left': ko.computed(function () {
  178. return this.getItemMargin(ctx.$row()) + '%';
  179. }.bind(this))
  180. }
  181. };
  182. },
  183. /**
  184. * Calculates width in percents of a timeline unit element.
  185. *
  186. * @returns {Number}
  187. */
  188. getTimeUnitWidth: function () {
  189. return 100 / this.model.scale;
  190. },
  191. /**
  192. * Calculates width of a record in percents.
  193. *
  194. * @param {Object} record
  195. * @returns {String}
  196. */
  197. getItemWidth: function (record) {
  198. var days = 0;
  199. if (record) {
  200. days = this.model.getDaysLength(record);
  201. }
  202. return this.getTimeUnitWidth() * days;
  203. },
  204. /**
  205. * Calculates left margin value for provided record.
  206. *
  207. * @param {Object} record
  208. * @returns {String}
  209. */
  210. getItemMargin: function (record) {
  211. var offset = 0;
  212. if (record) {
  213. offset = this.model.getStartDelta(record);
  214. }
  215. return this.getTimeUnitWidth() * offset;
  216. },
  217. /**
  218. * Returns collection of currently available
  219. * timeline item elements.
  220. *
  221. * @returns {Array<HTMLElement>}
  222. */
  223. getItems: function () {
  224. var items = this.$content.querySelectorAll(this.selectors.item);
  225. return _.toArray(items);
  226. },
  227. /**
  228. * Updates positions of timeline elements.
  229. *
  230. * @returns {TimelineView} Chainable.
  231. */
  232. updateItemsPosition: function () {
  233. this.getItems()
  234. .forEach(this.updatePositionFor, this);
  235. return this;
  236. },
  237. /**
  238. * Updates position of provided timeline element.
  239. *
  240. * @param {HTMLElement} $elem
  241. * @returns {TimelineView} Chainable.
  242. */
  243. updatePositionFor: function ($elem) {
  244. var $event = $elem.querySelector(this.selectors.event),
  245. leftEdge = this.getLeftEdgeFor($elem),
  246. rightEdge = this.getRightEdgeFor($elem);
  247. if ($event) {
  248. $event.style.left = Math.max(-leftEdge, 0) + 'px';
  249. $event.style.right = Math.max(rightEdge, 0) + 'px';
  250. }
  251. toggleClass($elem, '_scroll-start', leftEdge < 0);
  252. toggleClass($elem, '_scroll-end', rightEdge > 0);
  253. return this;
  254. },
  255. /**
  256. * Scrolls content area to the start of provided element.
  257. *
  258. * @param {HTMLElement} elem
  259. * @returns {TimelineView}
  260. */
  261. toStartOf: function (elem) {
  262. var leftEdge = this.getLeftEdgeFor(elem);
  263. this.$content.scrollLeft += leftEdge;
  264. return this;
  265. },
  266. /**
  267. * Scrolls content area to the end of provided element.
  268. *
  269. * @param {HTMLElement} elem
  270. * @returns {TimelineView}
  271. */
  272. toEndOf: function (elem) {
  273. var rightEdge = this.getRightEdgeFor(elem);
  274. this.$content.scrollLeft += rightEdge + 1;
  275. return this;
  276. },
  277. /**
  278. * Calculates location of the left edge of an element
  279. * relative to the contents' left edge.
  280. *
  281. * @param {HTMLElement} elem
  282. * @returns {Number}
  283. */
  284. getLeftEdgeFor: function (elem) {
  285. var leftOffset = elem.getBoundingClientRect().left;
  286. return leftOffset - this.$content.getBoundingClientRect().left;
  287. },
  288. /**
  289. * Calculates location of the right edge of an element
  290. * relative to the contents' right edge.
  291. *
  292. * @param {HTMLElement} elem
  293. * @returns {Number}
  294. */
  295. getRightEdgeFor: function (elem) {
  296. var elemWidth = elem.offsetWidth,
  297. leftEdge = this.getLeftEdgeFor(elem);
  298. return leftEdge + elemWidth - this.$content.offsetWidth;
  299. },
  300. /**
  301. * 'To Start' button 'click' event handler.
  302. *
  303. * @param {jQueryEvent} event
  304. */
  305. onToStartClick: function (event) {
  306. var elem = event.originalEvent.currentTarget;
  307. event.stopPropagation();
  308. this.toStartOf(elem);
  309. },
  310. /**
  311. * 'To End' button 'click' event handler.
  312. *
  313. * @param {jQueryEvent} event
  314. */
  315. onToEndClick: function (event) {
  316. var elem = event.originalEvent.currentTarget;
  317. event.stopPropagation();
  318. this.toEndOf(elem);
  319. },
  320. /**
  321. * Handler of the scale value 'change' event.
  322. */
  323. onScaleChange: function () {
  324. this._update = true;
  325. },
  326. /**
  327. * Callback function which is invoked
  328. * when event element was rendered.
  329. */
  330. onEventElementRender: function () {
  331. this._update = true;
  332. },
  333. /**
  334. * Window 'resize' event handler.
  335. */
  336. onWindowResize: function () {
  337. this._update = true;
  338. },
  339. /**
  340. * Content container 'scroll' event handler.
  341. */
  342. onContentScroll: function () {
  343. this._update = true;
  344. },
  345. /**
  346. * Data 'reload' event handler.
  347. */
  348. onDataReloaded: function () {
  349. this._update = true;
  350. }
  351. });
  352. });