tile_view.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. /**
  2. * DevExtreme (ui/tile_view.js)
  3. * Version: 19.1.16
  4. * Build date: Tue Oct 18 2022
  5. *
  6. * Copyright (c) 2012 - 2022 Developer Express Inc. ALL RIGHTS RESERVED
  7. * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
  8. */
  9. "use strict";
  10. var $ = require("../core/renderer");
  11. var devices = require("../core/devices");
  12. var registerComponent = require("../core/component_registrator");
  13. var inflector = require("../core/utils/inflector");
  14. var iteratorUtils = require("../core/utils/iterator");
  15. var isDefined = require("../core/utils/type").isDefined;
  16. var extend = require("../core/utils/extend").extend;
  17. var windowUtils = require("../core/utils/window");
  18. var getPublicElement = require("../core/utils/dom").getPublicElement;
  19. var deferRender = require("../core/utils/common").deferRender;
  20. var ScrollView = require("./scroll_view");
  21. var CollectionWidget = require("./collection/ui.collection_widget.edit");
  22. var TILEVIEW_CLASS = "dx-tileview";
  23. var TILEVIEW_CONTAINER_CLASS = "dx-tileview-wrapper";
  24. var TILEVIEW_ITEM_CLASS = "dx-tile";
  25. var TILEVIEW_ITEM_SELECTOR = "." + TILEVIEW_ITEM_CLASS;
  26. var TILEVIEW_ITEM_DATA_KEY = "dxTileData";
  27. var CONFIGS = {
  28. horizontal: {
  29. itemMainRatio: "widthRatio",
  30. itemCrossRatio: "heightRatio",
  31. baseItemMainDimension: "baseItemWidth",
  32. baseItemCrossDimension: "baseItemHeight",
  33. mainDimension: "width",
  34. crossDimension: "height",
  35. mainPosition: "left",
  36. crossPosition: "top"
  37. },
  38. vertical: {
  39. itemMainRatio: "heightRatio",
  40. itemCrossRatio: "widthRatio",
  41. baseItemMainDimension: "baseItemHeight",
  42. baseItemCrossDimension: "baseItemWidth",
  43. mainDimension: "height",
  44. crossDimension: "width",
  45. mainPosition: "top",
  46. crossPosition: "left"
  47. }
  48. };
  49. var TileView = CollectionWidget.inherit({
  50. _activeStateUnit: TILEVIEW_ITEM_SELECTOR,
  51. _getDefaultOptions: function() {
  52. return extend(this.callBase(), {
  53. items: null,
  54. direction: "horizontal",
  55. hoverStateEnabled: true,
  56. showScrollbar: false,
  57. height: 500,
  58. baseItemWidth: 100,
  59. baseItemHeight: 100,
  60. itemMargin: 20,
  61. activeStateEnabled: true,
  62. indicateLoading: true
  63. })
  64. },
  65. _defaultOptionsRules: function() {
  66. return this.callBase().concat([{
  67. device: function() {
  68. return "desktop" === devices.real().deviceType && !devices.isSimulator()
  69. },
  70. options: {
  71. focusStateEnabled: true
  72. }
  73. }])
  74. },
  75. _itemClass: function() {
  76. return TILEVIEW_ITEM_CLASS
  77. },
  78. _itemDataKey: function() {
  79. return TILEVIEW_ITEM_DATA_KEY
  80. },
  81. _itemContainer: function() {
  82. return this._$container
  83. },
  84. _init: function() {
  85. this.callBase();
  86. this.$element().addClass(TILEVIEW_CLASS);
  87. this._initScrollView()
  88. },
  89. _dataSourceLoadingChangedHandler: function(isLoading) {
  90. var scrollView = this._scrollView;
  91. if (!scrollView || !scrollView.startLoading) {
  92. return
  93. }
  94. if (isLoading && this.option("indicateLoading")) {
  95. scrollView.startLoading()
  96. } else {
  97. scrollView.finishLoading()
  98. }
  99. },
  100. _hideLoadingIfLoadIndicationOff: function() {
  101. if (!this.option("indicateLoading")) {
  102. this._dataSourceLoadingChangedHandler(false)
  103. }
  104. },
  105. _initScrollView: function() {
  106. this._scrollView = this._createComponent(this.$element(), ScrollView, {
  107. direction: this.option("direction"),
  108. scrollByContent: true,
  109. useKeyboard: false,
  110. showScrollbar: this.option("showScrollbar")
  111. });
  112. this._$container = $(this._scrollView.content());
  113. this._$container.addClass(TILEVIEW_CONTAINER_CLASS);
  114. this._scrollView.option("onUpdated", this._renderGeometry.bind(this))
  115. },
  116. _initMarkup: function() {
  117. this.callBase();
  118. deferRender(function() {
  119. this._cellsPerDimension = 1;
  120. this._renderGeometry();
  121. this._updateScrollView();
  122. this._fireContentReadyAction()
  123. }.bind(this))
  124. },
  125. _updateScrollView: function() {
  126. this._scrollView.option("direction", this.option("direction"));
  127. this._scrollView.update();
  128. this._indicateLoadingIfAlreadyStarted()
  129. },
  130. _indicateLoadingIfAlreadyStarted: function() {
  131. if (this._isDataSourceLoading()) {
  132. this._dataSourceLoadingChangedHandler(true)
  133. }
  134. },
  135. _renderGeometry: function() {
  136. this._config = CONFIGS[this.option("direction")];
  137. var items = this.option("items") || [];
  138. var config = this._config;
  139. var itemMargin = this.option("itemMargin");
  140. var maxItemCrossRatio = Math.max.apply(Math, iteratorUtils.map(items || [], function(item) {
  141. return Math.round(item[config.itemCrossRatio] || 1)
  142. }));
  143. var crossDimensionValue = windowUtils.hasWindow() ? this.$element()[config.crossDimension]() : parseInt(this.$element().get(0).style[config.crossDimension]);
  144. this._cellsPerDimension = Math.floor(crossDimensionValue / (this.option(config.baseItemCrossDimension) + itemMargin));
  145. this._cellsPerDimension = Math.max(this._cellsPerDimension, maxItemCrossRatio);
  146. this._cells = [];
  147. this._cells.push(new Array(this._cellsPerDimension));
  148. this._arrangeItems(items);
  149. this._renderContentSize(config, itemMargin)
  150. },
  151. _renderContentSize: function(config, itemMargin) {
  152. if (windowUtils.hasWindow()) {
  153. var actualContentSize = this._cells.length * this.option(config.baseItemMainDimension) + (this._cells.length + 1) * itemMargin;
  154. var containerSize = this._$container[config.mainDimension]();
  155. if (actualContentSize > containerSize) {
  156. this._$container[config.mainDimension](actualContentSize)
  157. }
  158. }
  159. },
  160. _arrangeItems: function(items) {
  161. var config = this._config;
  162. var itemMainRatio = config.itemMainRatio;
  163. var itemCrossRatio = config.itemCrossRatio;
  164. var mainPosition = config.mainPosition;
  165. this._itemsPositions = [];
  166. iteratorUtils.each(items, function(index, item) {
  167. var currentItem = {};
  168. currentItem[itemMainRatio] = item[itemMainRatio] || 1;
  169. currentItem[itemCrossRatio] = item[itemCrossRatio] || 1;
  170. currentItem.index = index;
  171. currentItem[itemMainRatio] = currentItem[itemMainRatio] <= 0 ? 0 : Math.round(currentItem[config.itemMainRatio]);
  172. currentItem[itemCrossRatio] = currentItem[itemCrossRatio] <= 0 ? 0 : Math.round(currentItem[config.itemCrossRatio]);
  173. var itemPosition = this._getItemPosition(currentItem);
  174. if (itemPosition[mainPosition] === -1) {
  175. itemPosition[mainPosition] = this._cells.push(new Array(this._cellsPerDimension)) - 1
  176. }
  177. this._occupyCells(currentItem, itemPosition);
  178. this._arrangeItem(currentItem, itemPosition);
  179. this._itemsPositions.push(itemPosition)
  180. }.bind(this))
  181. },
  182. _getItemPosition: function(item) {
  183. var config = this._config;
  184. var mainPosition = config.mainPosition;
  185. var crossPosition = config.crossPosition;
  186. var position = {};
  187. position[mainPosition] = -1;
  188. position[crossPosition] = 0;
  189. for (var main = 0; main < this._cells.length; main++) {
  190. for (var cross = 0; cross < this._cellsPerDimension; cross++) {
  191. if (this._itemFit(main, cross, item)) {
  192. position[mainPosition] = main;
  193. position[crossPosition] = cross;
  194. break
  195. }
  196. }
  197. if (position[mainPosition] > -1) {
  198. break
  199. }
  200. }
  201. return position
  202. },
  203. _itemFit: function(mainPosition, crossPosition, item) {
  204. var result = true;
  205. var config = this._config;
  206. var itemRatioMain = item[config.itemMainRatio];
  207. var itemRatioCross = item[config.itemCrossRatio];
  208. if (crossPosition + itemRatioCross > this._cellsPerDimension) {
  209. return false
  210. }
  211. for (var main = mainPosition; main < mainPosition + itemRatioMain; main++) {
  212. for (var cross = crossPosition; cross < crossPosition + itemRatioCross; cross++) {
  213. if (this._cells.length - 1 < main) {
  214. this._cells.push(new Array(this._cellsPerDimension))
  215. } else {
  216. if (void 0 !== this._cells[main][cross]) {
  217. result = false;
  218. break
  219. }
  220. }
  221. }
  222. }
  223. return result
  224. },
  225. _occupyCells: function(item, itemPosition) {
  226. var config = this._config;
  227. var itemPositionMain = itemPosition[config.mainPosition];
  228. var itemPositionCross = itemPosition[config.crossPosition];
  229. var itemRatioMain = item[config.itemMainRatio];
  230. var itemRatioCross = item[config.itemCrossRatio];
  231. for (var main = itemPositionMain; main < itemPositionMain + itemRatioMain; main++) {
  232. for (var cross = itemPositionCross; cross < itemPositionCross + itemRatioCross; cross++) {
  233. this._cells[main][cross] = item.index
  234. }
  235. }
  236. },
  237. _arrangeItem: function(item, itemPosition) {
  238. var config = this._config;
  239. var itemPositionMain = itemPosition[config.mainPosition];
  240. var itemPositionCross = itemPosition[config.crossPosition];
  241. var itemRatioMain = item[config.itemMainRatio];
  242. var itemRatioCross = item[config.itemCrossRatio];
  243. var baseItemCross = this.option(config.baseItemCrossDimension);
  244. var baseItemMain = this.option(config.baseItemMainDimension);
  245. var itemMargin = this.option("itemMargin");
  246. var cssProps = {
  247. display: itemRatioMain <= 0 || itemRatioCross <= 0 ? "none" : ""
  248. };
  249. var mainDimension = itemRatioMain * baseItemMain + (itemRatioMain - 1) * itemMargin;
  250. var crossDimension = itemRatioCross * baseItemCross + (itemRatioCross - 1) * itemMargin;
  251. cssProps[config.mainDimension] = mainDimension < 0 ? 0 : mainDimension;
  252. cssProps[config.crossDimension] = crossDimension < 0 ? 0 : crossDimension;
  253. cssProps[config.mainPosition] = itemPositionMain * baseItemMain + (itemPositionMain + 1) * itemMargin;
  254. cssProps[config.crossPosition] = itemPositionCross * baseItemCross + (itemPositionCross + 1) * itemMargin;
  255. if (this.option("rtlEnabled")) {
  256. var offsetCorrection = this._$container.width();
  257. var baseItemWidth = this.option("baseItemWidth");
  258. var itemPositionX = itemPosition.left;
  259. var offsetPosition = itemPositionX * baseItemWidth;
  260. var itemBaseOffset = baseItemWidth + itemMargin;
  261. var itemWidth = itemBaseOffset * item.widthRatio;
  262. var subItemMargins = itemPositionX * itemMargin;
  263. cssProps.left = offsetCorrection - (offsetPosition + itemWidth + subItemMargins)
  264. }
  265. this._itemElements().eq(item.index).css(cssProps)
  266. },
  267. _moveFocus: function(location) {
  268. var FOCUS_UP = "up";
  269. var FOCUS_DOWN = "down";
  270. var FOCUS_LEFT = this.option("rtlEnabled") ? "right" : "left";
  271. var FOCUS_RIGHT = this.option("rtlEnabled") ? "left" : "right";
  272. var FOCUS_PAGE_UP = "pageup";
  273. var FOCUS_PAGE_DOWN = "pagedown";
  274. var horizontalDirection = "horizontal" === this.option("direction");
  275. var cells = this._cells;
  276. var index = $(this.option("focusedElement")).index();
  277. var targetCol = this._itemsPositions[index].left;
  278. var targetRow = this._itemsPositions[index].top;
  279. var colCount = (horizontalDirection ? cells : cells[0]).length;
  280. var rowCount = (horizontalDirection ? cells[0] : cells).length;
  281. var getCell = function(col, row) {
  282. if (horizontalDirection) {
  283. return cells[col][row]
  284. }
  285. return cells[row][col]
  286. };
  287. switch (location) {
  288. case FOCUS_PAGE_UP:
  289. case FOCUS_UP:
  290. while (targetRow > 0 && index === getCell(targetCol, targetRow)) {
  291. targetRow--
  292. }
  293. if (targetRow < 0) {
  294. targetRow = 0
  295. }
  296. break;
  297. case FOCUS_PAGE_DOWN:
  298. case FOCUS_DOWN:
  299. while (targetRow < rowCount && index === getCell(targetCol, targetRow)) {
  300. targetRow++
  301. }
  302. if (targetRow === rowCount) {
  303. targetRow = rowCount - 1
  304. }
  305. break;
  306. case FOCUS_RIGHT:
  307. while (targetCol < colCount && index === getCell(targetCol, targetRow)) {
  308. targetCol++
  309. }
  310. if (targetCol === colCount) {
  311. targetCol = colCount - 1
  312. }
  313. break;
  314. case FOCUS_LEFT:
  315. while (targetCol >= 0 && index === getCell(targetCol, targetRow)) {
  316. targetCol--
  317. }
  318. if (targetCol < 0) {
  319. targetCol = 0
  320. }
  321. break;
  322. default:
  323. this.callBase.apply(this, arguments);
  324. return
  325. }
  326. var newTargetIndex = getCell(targetCol, targetRow);
  327. if (!isDefined(newTargetIndex)) {
  328. return
  329. }
  330. var $newTarget = this._itemElements().eq(newTargetIndex);
  331. this.option("focusedElement", getPublicElement($newTarget));
  332. this._scrollToItem($newTarget)
  333. },
  334. _scrollToItem: function($itemElement) {
  335. if (!$itemElement.length) {
  336. return
  337. }
  338. var config = this._config;
  339. var outerMainProp = "outer" + inflector.captionize(config.mainDimension);
  340. var itemMargin = this.option("itemMargin");
  341. var itemPosition = $itemElement.position()[config.mainPosition];
  342. var itemDimension = $itemElement[outerMainProp]();
  343. var itemTail = itemPosition + itemDimension;
  344. var scrollPosition = this.scrollPosition();
  345. var clientWidth = this.$element()[outerMainProp]();
  346. if (scrollPosition <= itemPosition && itemTail <= scrollPosition + clientWidth) {
  347. return
  348. }
  349. if (scrollPosition > itemPosition) {
  350. this._scrollView.scrollTo(itemPosition - itemMargin)
  351. } else {
  352. this._scrollView.scrollTo(itemPosition + itemDimension - clientWidth + itemMargin)
  353. }
  354. },
  355. _optionChanged: function(args) {
  356. switch (args.name) {
  357. case "items":
  358. this.callBase(args);
  359. this._renderGeometry();
  360. this._updateScrollView();
  361. break;
  362. case "showScrollbar":
  363. this._initScrollView();
  364. break;
  365. case "disabled":
  366. this._scrollView.option("disabled", args.value);
  367. this.callBase(args);
  368. break;
  369. case "baseItemWidth":
  370. case "baseItemHeight":
  371. case "itemMargin":
  372. this._renderGeometry();
  373. break;
  374. case "width":
  375. case "height":
  376. this.callBase(args);
  377. this._renderGeometry();
  378. this._updateScrollView();
  379. break;
  380. case "direction":
  381. this._renderGeometry();
  382. this._updateScrollView();
  383. break;
  384. case "indicateLoading":
  385. this._hideLoadingIfLoadIndicationOff();
  386. break;
  387. default:
  388. this.callBase(args)
  389. }
  390. },
  391. scrollPosition: function() {
  392. return this._scrollView.scrollOffset()[this._config.mainPosition]
  393. }
  394. });
  395. registerComponent("dxTileView", TileView);
  396. module.exports = TileView;
  397. module.exports.default = module.exports;