Source: infiniteGrid.js

  1. /**
  2. * Copyright (c) 2015 NAVER Corp.
  3. * egjs projects are licensed under the MIT license
  4. */
  5. // jscs:disable validateLineBreaks, maximumLineLength
  6. eg.module("infiniteGrid", ["jQuery", eg, window, document], function($, ns, global, doc) {
  7. "use strict";
  8. /**
  9. * A module used to arrange card elements including content infinitely on a grid layout. With this module, you can implement a grid-pattern user interface composed of different card elements whose sizes vary. It guarantees performance by maintaining the number of DOMs the module is handling under any circumstance
  10. * @group egjs
  11. * @ko 콘텐츠가 있는 카드 엘리먼트를 그리드 레이아웃에 무한으로 배치하는 모듈. 다양한 크기의 카드 엘리먼트를 격자 모양으로 배치하는 UI를 만들 수 있다. 카드 엘리먼트의 개수가 계속 늘어나도 모듈이 처리하는 DOM의 개수를 일정하게 유지해 최적의 성능을 보장한다
  12. * @class
  13. * @name eg.InfiniteGrid
  14. * @extends eg.Component
  15. *
  16. * @param {HTMLElement|String|jQuery} element A base element for a module <ko>모듈을 적용할 기준 엘리먼트</ko>
  17. * @param {Object} [options] The option object of the eg.InfiniteGrid module <ko>eg.InfiniteGrid 모듈의 옵션 객체</ko>
  18. * @param {String} [options.itemSelector] A selector to select card elements that make up the layout (@deprecated since 1.3.0)<ko>레이아웃을 구성하는 카드 엘리먼트를 선택할 선택자(selector) (@deprecated since 1.3.0)</ko>
  19. * @param {Number} [options.count=30] The number of DOMs handled by module. If the count value is greater than zero, the number of DOMs is maintained. If the count value is zero or less than zero, the number of DOMs will increase as card elements are added. <ko>모듈이 유지할 실제 DOM의 개수. count 값이 0보다 크면 DOM 개수를 일정하게 유지한다. count 값이 0 이하면 카드 엘리먼트가 추가될수록 DOM 개수가 계속 증가한다.</ko>
  20. * @param {String} [options.defaultGroupKey=null] The default group key configured in a card element contained in the markup upon initialization of a module object <ko>모듈 객체를 초기화할 때 마크업에 있는 카드 엘리먼트에 설정할 그룹 키 </ko>
  21. * @param {Boolean} [options.isEqualSize=false] Indicates whether sizes of all card elements are equal to one another. If sizes of card elements to be arranged are all equal and this option is set to "true", the performance of layout arrangement can be improved. <ko>카드 엘리먼트의 크기가 동일한지 여부. 배치될 카드 엘리먼트의 크기가 모두 동일할 때 이 옵션을 'true'로 설정하면 레이아웃 배치 성능을 높일 수 있다</ko>
  22. * @param {Number} [options.threshold=300] The threshold size of an event area where card elements are added to a layout.<br>- append event: If the current vertical position of the scroll bar is greater than "the bottom property value of the card element at the top of the layout" plus "the value of the threshold option", the append event will occur.<br>- prepend event: If the current vertical position of the scroll bar is less than "the bottom property value of the card element at the top of the layout" minus "the value of the threshold option", the prepend event will occur. <ko>− 레이아웃에 카드 엘리먼트를 추가하는 이벤트가 발생하는 기준 영역의 크기.<br>- append 이벤트: 현재 스크롤의 y 좌표 값이 '레이아웃의 맨 아래에 있는 카드 엘리먼트의 top 속성의 값 + threshold 옵션의 값'보다 크면 append 이벤트가 발생한다.<br>- prepend 이벤트: 현재 스크롤의 y 좌표 값이 '레이아웃의 맨 위에 있는 카드 엘리먼트의 bottom 속성의 값 - threshold 옵션의 값'보다 작으면 prepend 이벤트가 발생한다</ko>
  23. *
  24. * @codepen {"id":"zvrbap", "ko":"InfiniteGrid 데모", "en":"InfiniteGrid example", "collectionId":"DPYEww", "height": 403}
  25. * @support {"ie": "8+", "ch" : "latest", "ff" : "latest", "sf" : "latest", "edge" : "latest", "ios" : "7+", "an" : "2.1+ (except 3.x)"}
  26. *
  27. * @example
  28. <!-- HTML -->
  29. <ul id="grid">
  30. <li class="card">
  31. <div>test1</div>
  32. </li>
  33. <li class="card">
  34. <div>test2</div>
  35. </li>
  36. <li class="card">
  37. <div>test3</div>
  38. </li>
  39. <li class="card">
  40. <div>test4</div>
  41. </li>
  42. <li class="card">
  43. <div>test5</div>
  44. </li>
  45. <li class="card">
  46. <div>test6</div>
  47. </li>
  48. </ul>
  49. <script>
  50. var some = new eg.InfiniteGrid("#grid").on("layoutComplete", function(e) {
  51. // ...
  52. });
  53. </script>
  54. */
  55. var EVENTS = {
  56. "layoutComplete": "layoutComplete",
  57. "append": "append",
  58. "prepend": "prepend"
  59. };
  60. var RETRY = 3;
  61. ns.InfiniteGrid = ns.Class.extend(ns.Component, {
  62. _events: function() {
  63. return EVENTS;
  64. },
  65. construct: function(el, options, _prefix) {
  66. var ua = global.navigator.userAgent;
  67. this.options = $.extend({
  68. isEqualSize: false,
  69. defaultGroupKey: null,
  70. count: 30,
  71. threshold: 300
  72. }, options);
  73. // if el is jQuery instance, el should change to HTMLElement.
  74. this.$el = el instanceof $ ? el : $(el);
  75. this.el = this.$el.get(0);
  76. this.el.style.position = "relative";
  77. this._prefix = _prefix || "";
  78. this._isIos = /iPhone|iPad/.test(ua);
  79. this._isIE = /MSIE|Trident|Windows Phone|Edge/.test(ua);
  80. this._appendCols = this._prependCols = [];
  81. this.$view = $(global);
  82. this._reset();
  83. this._refreshViewport();
  84. if (this.el.children.length > 0) {
  85. this.layout(true, this._itemize($.makeArray(this.el.children), this.options.defaultGroupKey));
  86. }
  87. this._onScroll = $.proxy(this._onScroll, this);
  88. this._onResize = $.proxy(this._onResize, this);
  89. this.$view.on("scroll", this._onScroll)
  90. .on("resize", this._onResize);
  91. },
  92. _getScrollTop: function() {
  93. return doc.body.scrollTop || doc.documentElement.scrollTop;
  94. },
  95. _onScroll: function() {
  96. if (this.isProcessing()) {
  97. return;
  98. }
  99. var scrollTop = this._getScrollTop();
  100. var prevScrollTop = this._prevScrollTop;
  101. if (this._isIos && scrollTop === 0 || prevScrollTop === scrollTop) {
  102. return;
  103. }
  104. var ele;
  105. var rect;
  106. if (prevScrollTop < scrollTop) {
  107. if ($.isEmptyObject(this._bottomElement)) {
  108. this._bottomElement = this.getBottomElement();
  109. if (this._bottomElement == null) {
  110. return;
  111. }
  112. }
  113. ele = this._bottomElement;
  114. rect = ele.getBoundingClientRect();
  115. if (rect.top <= this._clientHeight + this.options.threshold) {
  116. /**
  117. * This event is fired when a card element must be added at the bottom of a grid layout because there is no card to be displayed on screen when a user scrolls near bottom.
  118. * @ko 카드 엘리먼트가 그리드 레이아웃의 아래에 추가돼야 할 때 발생하는 이벤트. 사용자가 아래로 스크롤해서 화면에 표시될 카드가 없을 때 발생한다
  119. * @name eg.InfiniteGrid#append
  120. * @event
  121. *
  122. * @param {Object} param The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
  123. * @param {Number} param.scrollTop Current vertical position of the scroll bar<ko>현재 스크롤의 y 좌표 값</ko>
  124. */
  125. this.trigger(this._prefix + EVENTS.append, {
  126. scrollTop: scrollTop
  127. });
  128. }
  129. } else {
  130. if ($.isEmptyObject(this._topElement)) {
  131. this._topElement = this.getTopElement();
  132. if (this._topElement == null) {
  133. return;
  134. }
  135. }
  136. ele = this._topElement;
  137. rect = ele.getBoundingClientRect();
  138. if (rect.bottom >= -this.options.threshold) {
  139. /**
  140. * This event is fired when a card element must be added at the top of a grid layout because there is no card to be displayed on screen when a user scrolls near top. This event is available only if the isRecycling() method returns true.
  141. * @ko 카드가 그리드 레이아웃의 위에 추가돼야 할 때 발생하는 이벤트. 사용자가 위로 스크롤해서 화면에 표시될 카드가 없을 때 발생한다. 이 이벤트는 isRecycling() 메서드의 반환값이 'true'일 때만 발생한다
  142. * @name eg.InfiniteGrid#prepend
  143. * @event
  144. *
  145. * @param {Object} param The object of data to be sent to an event<ko>이벤트에 전달되는 데이터 객체</ko>
  146. * @param {Number} param.scrollTop Current vertical position of the scroll bar<ko>현재 스크롤의 y 좌표 값</ko>
  147. */
  148. var croppedDistance = this.fit();
  149. if (croppedDistance > 0) {
  150. scrollTop -= croppedDistance;
  151. this.$view.scrollTop(scrollTop);
  152. }
  153. this.trigger(this._prefix + EVENTS.prepend, {
  154. scrollTop: scrollTop
  155. });
  156. }
  157. }
  158. this._prevScrollTop = scrollTop;
  159. },
  160. _onResize: function() {
  161. if (this._resizeTimeout) {
  162. clearTimeout(this._resizeTimeout);
  163. }
  164. var self = this;
  165. this._resizeTimeout = setTimeout(function() {
  166. self._refreshViewport();
  167. (self.$el.innerWidth() !== self._containerWidth) && self.layout(true);
  168. self._resizeTimeout = null;
  169. self._prevScrollTop = -1;
  170. }, 100);
  171. },
  172. _refreshViewport: function() {
  173. var el = this.$view.get(0);
  174. if (el) {
  175. this._clientHeight = $.isWindow(el) ? el.innerHeight || document.documentElement.clientHeight : el.clientHeight;
  176. }
  177. },
  178. /**
  179. * Returns the current state of a module such as location information. You can use the setStatus() method to restore the information returned through a call to this method.
  180. * @ko 카드의 위치 정보 등 모듈의 현재 상태 정보를 반환한다. 이 메서드가 반환한 정보를 저장해 두었다가 setStatus() 메서드로 복원할 수 있다
  181. * @method eg.InfiniteGrid#getStatus
  182. * @return {Object} State object of the eg.InfiniteGrid module<ko>eg.InfiniteGrid 모듈의 상태 객체</ko>
  183. */
  184. getStatus: function() {
  185. var data = {};
  186. var p;
  187. for (p in this) {
  188. if (this.hasOwnProperty(p) && /^_/.test(p) &&
  189. typeof this[p] !== "function" && !(this[p] instanceof Element)) {
  190. data[p] = this[p];
  191. }
  192. }
  193. return {
  194. prop: data,
  195. options: $.extend({}, this.options),
  196. items: $.map(this.items, function(v) {
  197. var clone = $.extend({}, v);
  198. delete clone.el;
  199. return clone;
  200. }),
  201. html: this.el.innerHTML,
  202. cssText: this.el.style.cssText
  203. };
  204. },
  205. /**
  206. * Sets the state of the eg.InfiniteGrid module with the information returned through a call to the getStatue() method.
  207. * @ko getStatue() 메서드가 저장한 정보로 eg.InfiniteGrid 모듈의 상태를 설정한다.
  208. * @method eg.InfiniteGrid#setStatus
  209. * @param {Object} status State object of the eg.InfiniteGrid module <ko>eg.InfiniteGrid 모듈의 상태 객체</ko>
  210. * @return {eg.InfiniteGrid} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
  211. */
  212. setStatus: function(status) {
  213. if (!status || !status.cssText || !status.html ||
  214. !status.prop || !status.items) {
  215. return this;
  216. }
  217. this.el.style.cssText = status.cssText;
  218. this.el.innerHTML = status.html;
  219. $.extend(this, status.prop);
  220. this._topElement = this._bottomElement = null;
  221. this.items = $.map(this.el.children, function(v, i) {
  222. status.items[i].el = v;
  223. return status.items[i];
  224. });
  225. return this;
  226. },
  227. /**
  228. * Checks whether a card element is being added.
  229. * @ko 카드 엘리먼트 추가가 진행 중인지 확인한다
  230. * @method eg.InfiniteGrid#isProcessing
  231. * @return {Boolean} Indicates whether a card element is being added <ko>카드 엘리먼트 추가 진행 중 여부</ko>
  232. */
  233. isProcessing: function() {
  234. return this._isProcessing;
  235. },
  236. /**
  237. * Checks whether the total number of added card elements is greater than the value of the count option. Note that the value of the count option is always greater than zero. If it returns true, the number of DOMs won't increase even though card elements are added; instead of adding a new DOM, existing DOMs are recycled to maintain the number of DOMs.
  238. * @ko 추가된 카드 엘리먼트의 전체 개수가 count 옵션의 값보다 큰지 확인한다. 단, count 옵션의 값은 0보다 크다. 'true'가 반환되면 카드 엘리먼트가 더 추가돼도 DOM의 개수를 증가하지 않고 기존 DOM을 재활용(recycle)해 DOM의 개수를 일정하게 유지한다
  239. * @method eg.InfiniteGrid#isRecycling
  240. * @return {Boolean} Indicates whether the total number of added card elements is greater than the value of the count option. <ko>추가된 카드 엘리먼트의 전체 개수가 count 옵션의 값보다 큰지 여부</ko>
  241. */
  242. isRecycling: function() {
  243. return (this.options.count > 0) && this._isRecycling;
  244. },
  245. /**
  246. * Returns the list of group keys which belongs to card elements currently being maintained. You can use the append() or prepend() method to configure group keys so that multiple card elements can be managed at once. If you do not use these methods to configure group keys, it returns undefined as a group key.
  247. * @ko 현재 유지하고 있는 카드 엘리먼트의 그룹 키 목록을 반환한다. 여러 개의 카드 엘리먼트를 묶어서 관리할 수 있도록 append() 메서드나 prepend() 메서드에서 그룹 키를 지정할 수 있다. append() 메서드나 prepend() 메서드에서 그룹 키를 지정하지 않았다면 'undefined'가 그룹 키로 반환된다
  248. * @method eg.InfiniteGrid#getGroupKeys
  249. * @return {Array} List of group keys <ko>그룹 키의 목록</ko>
  250. */
  251. getGroupKeys: function() {
  252. return $.map(this.items, function(v) {
  253. return v.groupKey;
  254. });
  255. },
  256. /**
  257. * Rearranges a layout.
  258. * @ko 레이아웃을 다시 배치한다.
  259. * @method eg.InfiniteGrid#layout
  260. * @param {Boolean} [isRelayout=true] Indicates whether a card element is being relayouted <ko>카드 엘리먼트 재배치 여부</ko>
  261. * @return {eg.InfiniteGrid} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
  262. *
  263. * [private parameter]
  264. * _addItems: added items
  265. * _options: {
  266. * isAppend: Checks whether the append() method is used to add a card element.
  267. * removedCount: The number of deleted card elements to maintain the number of DOMs.
  268. *}
  269. */
  270. layout: function(isRelayout, _addItems, _options) {
  271. var options = $.extend({
  272. isAppend: true,
  273. removedCount: 0
  274. }, _options);
  275. isRelayout = typeof isRelayout === "undefined" || isRelayout;
  276. // for except case.
  277. if (!_addItems && !options.isAppend) {
  278. options.isAppend = true;
  279. }
  280. this._waitResource(isRelayout, options.isAppend ? _addItems : _addItems.reverse(), options);
  281. return this;
  282. },
  283. _layoutComplete: function(isRelayout, addItems, options) {
  284. var isInit = !this.items.length;
  285. // insert items (when appending)
  286. if (addItems && options.isAppend) {
  287. this.items = this.items.concat(addItems);
  288. }
  289. if (isInit) {
  290. $.each(addItems, function(i, v) {
  291. v.el.style.position = "absolute";
  292. });
  293. }
  294. if (isInit || isRelayout) {
  295. this._resetCols(this._measureColumns());
  296. } else {
  297. if (!addItems) {
  298. this._appendCols = this._prependCols.concat();
  299. }
  300. }
  301. this._layoutItems(isRelayout, addItems, options);
  302. this._postLayout(isRelayout, addItems, options);
  303. },
  304. _layoutItems: function(isRelayout, addItems, options) {
  305. var self = this;
  306. var items = addItems || this.items;
  307. $.each(items, function(i, v) {
  308. v.position = self._getItemLayoutPosition(isRelayout, v, options.isAppend);
  309. });
  310. if (addItems && !options.isAppend) {
  311. // insert items (when prepending)
  312. this.items = addItems.sort(function(p, c) {
  313. return p.position.y - c.position.y;
  314. }).concat(this.items);
  315. var y = this._getTopPositonY();
  316. if (y !== 0) {
  317. items = this.items;
  318. $.each(items, function(i, v) {
  319. v.position.y -= y;
  320. });
  321. this._syncCols(false); // for prepending
  322. this._syncCols(true); // for appending
  323. }
  324. }
  325. // for performance
  326. $.each(items, function(i, v) {
  327. if (v.el) {
  328. var style = v.el.style;
  329. style.left = v.position.x + "px";
  330. style.top = v.position.y + "px";
  331. }
  332. });
  333. },
  334. /**
  335. * Adds a card element at the bottom of a grid layout. This method is available only if the isProcessing() method returns false.
  336. * @ko 카드 엘리먼트를 그리드 레이아웃의 아래에 추가한다. isProcessing() 메서드의 반환값이 'false'일 때만 이 메서드를 사용할 수 있다
  337. * 이 메소드는 isProcessing()의 반환값이 false일 경우에만 사용 가능하다.
  338. * @method eg.InfiniteGrid#append
  339. * @param {Array|String|jQuery} elements Array of the card elements to be added <ko>추가할 카드 엘리먼트의 배열</ko>
  340. * @param {Number|String} [groupKey] The group key to be configured in a card element. It is set to "undefined" by default.<ko>추가할 카드 엘리먼트에 설정할 그룹 키. 생략하면 값이 'undefined'로 설정된다</ko>
  341. * @return {Number} The number of added card elements <ko>추가된 카드 엘리먼트의 개수</ko>
  342. */
  343. append: function($elements, groupKey) {
  344. if (this._isProcessing || $elements.length === 0) {
  345. return;
  346. }
  347. // convert jQuery instance
  348. $elements = $($elements);
  349. this._insert($elements, groupKey, true);
  350. return $elements.length;
  351. },
  352. /**
  353. * Adds a card element at the top of a grid layout. This method is available only if the isProcessing() method returns false and the isRecycling() method returns true.
  354. * @ko 카드 엘리먼트를 그리드 레이아웃의 위에 추가한다. isProcessing() 메서드의 반환값이 'false'이고, isRecycling() 메서드의 반환값이 'true'일 때만 이 메서드를 사용할 수 있다
  355. * @method eg.InfiniteGrid#prepend
  356. * @param {Array|String|jQuery} elements Array of the card elements to be added <ko>추가할 카드 엘리먼트 배열</ko>
  357. * @param {Number|String} [groupKey] The group key to be configured in a card element. It is set to "undefined" by default.<ko>추가할 카드 엘리먼트에 설정할 그룹 키. 생략하면 값이 'undefined'로 설정된다</ko>
  358. * @return {Number} The number of added card elements <ko>추가된 카드 엘리먼트의 개수</ko>
  359. */
  360. prepend: function($elements, groupKey) {
  361. if (this._isProcessing || $elements.length === 0) {
  362. return;
  363. }
  364. // convert jQuery instance
  365. $elements = $($elements);
  366. this._insert($elements, groupKey, false);
  367. return $elements.length;
  368. },
  369. /**
  370. * Clears added card elements and data.
  371. * @ko 추가된 카드 엘리먼트와 데이터를 모두 지운다.
  372. * @method eg.InfiniteGrid#clear
  373. * @return {eg.InfiniteGrid} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
  374. */
  375. clear: function() {
  376. this.el.innerHTML = "";
  377. this.el.style.height = "";
  378. this._reset();
  379. return this;
  380. },
  381. /**
  382. * Returns a card element at the top of a layout.
  383. * @ko 레이아웃의 맨 위에 있는 카드 엘리먼트를 반환한다.
  384. * @method eg.InfiniteGrid#getTopElement
  385. *
  386. * @return {HTMLElement} Card element at the top of a layout. (if the position of card elements are same, it returns the first left element) <ko>레이아웃의 맨 위에 있는 카드 엘리먼트 (카드의 위치가 같은 경우, 왼쪽 엘리먼트가 반환된다)</ko>
  387. */
  388. getTopElement: function() {
  389. var item = this._getTopItem();
  390. return item && item.el;
  391. },
  392. _getTopItem: function() {
  393. var item = null;
  394. var min = Infinity;
  395. $.each(this._getColItems(false), function(i, v) {
  396. if (v && v.position.y < min) {
  397. min = v.position.y;
  398. item = v;
  399. }
  400. });
  401. return item;
  402. },
  403. _getTopPositonY: function() {
  404. var item = this._getTopItem();
  405. return item ? item.position.y : 0;
  406. },
  407. /**
  408. * Returns a card element at the bottom of a layout.
  409. * @ko 레이아웃의 맨 아래에 있는 카드 엘리먼트를 반환한다.
  410. * @method eg.InfiniteGrid#getBottomElement
  411. *
  412. * @return {HTMLElement} Card element at the bottom of a layout (if the position of card elements are same, it returns the first right element)<ko>레이아웃의 맨 아래에 있는 카드 엘리먼트 (카드의 위치가 같은 경우, 오른쪽 엘리먼트가 반환된다)</ko>
  413. */
  414. getBottomElement: function() {
  415. var item = null;
  416. var max = -Infinity;
  417. var pos;
  418. $.each(this._getColItems(true), function(i, v) {
  419. pos = v ? v.position.y + v.size.height : 0;
  420. if (pos >= max) {
  421. max = pos;
  422. item = v;
  423. }
  424. });
  425. return item && item.el;
  426. },
  427. _postLayout: function(isRelayout, addItems, options) {
  428. if (!this._isProcessing) {
  429. return;
  430. }
  431. addItems = addItems || [];
  432. this.el.style.height = this._getContainerSize().height + "px";
  433. this._doubleCheckCount = RETRY;
  434. // refresh element
  435. this._topElement = this.getTopElement();
  436. this._bottomElement = this.getBottomElement();
  437. var distance = 0;
  438. if (!options.isAppend) {
  439. distance = addItems.length >= this.items.length ?
  440. 0 : this.items[addItems.length].position.y;
  441. if (distance > 0) {
  442. this._prevScrollTop = this._getScrollTop() + distance;
  443. this.$view.scrollTop(this._prevScrollTop);
  444. }
  445. }
  446. // reset flags
  447. this._isProcessing = false;
  448. /**
  449. * This event is fired when layout is successfully arranged through a call to the append(), prepend(), or layout() method.
  450. * @ko 레이아웃 배치가 완료됐을 때 발생하는 이벤트. append() 메서드나 prepend() 메서드, layout() 메서드 호출 후 카드의 배치가 완료됐을 때 발생한다
  451. * @name eg.InfiniteGrid#layoutComplete
  452. * @event
  453. *
  454. * @param {Object} param The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
  455. * @param {Array} param.target Rearranged card elements<ko>재배치된 카드 엘리먼트들</ko>
  456. * @param {Boolean} param.isAppend Checks whether the append() method is used to add a card element. It returns true even though the layoutComplete event is fired after the layout() method is called. <ko>카드 엘리먼트가 append() 메서드로 추가됐는지 확인한다. layout() 메서드가 호출된 후 layoutComplete 이벤트가 발생해도 'true'를 반환한다.</ko>
  457. * @param {Number} param.distance Distance the card element at the top of a grid layout has moved after the layoutComplete event is fired. In other words, it is the same as an increased height with a new card element added using the prepend() method <ko>그리드 레이아웃의 맨 위에 있던 카드 엘리먼트가 layoutComplete 이벤트 발생 후 이동한 거리. 즉, prepend() 메서드로 카드 엘리먼트가 추가돼 늘어난 높이다.</ko>
  458. * @param {Number} param.croppedCount The number of deleted card elements to maintain the number of DOMs<ko>일정한 DOM 개수를 유지하기 위해, 삭제한 카드 엘리먼트들의 개수</ko>
  459. */
  460. this.trigger(this._prefix + EVENTS.layoutComplete, {
  461. target: addItems.concat(),
  462. isAppend: options.isAppend,
  463. distance: distance,
  464. croppedCount: options.removedCount
  465. });
  466. // doublecheck!!! (workaround)
  467. if (!options.isAppend) {
  468. if (this._getScrollTop() === 0) {
  469. var self = this;
  470. clearInterval(this._doubleCheckTimer);
  471. this._doubleCheckTimer = setInterval(function() {
  472. if (self._getScrollTop() === 0) {
  473. self.trigger(self._prefix + EVENTS.prepend, {
  474. scrollTop: 0
  475. });
  476. (--self._doubleCheckCount <= 0) && clearInterval(self._doubleCheckTimer);
  477. }
  478. }, 500);
  479. }
  480. }
  481. },
  482. // $elements => $([HTMLElement, HTMLElement, ...])
  483. _insert: function($elements, groupKey, isAppend) {
  484. this._isProcessing = true;
  485. if (!this.isRecycling()) {
  486. this._isRecycling = (this.items.length + $elements.length) >= this.options.count;
  487. }
  488. if ($elements.length === 0) {
  489. return;
  490. }
  491. var elements = $elements.toArray();
  492. var $cloneElements = $(elements);
  493. var dummy = -this._clientHeight + "px";
  494. $.each(elements, function(i, v) {
  495. v.style.position = "absolute";
  496. v.style.top = dummy;
  497. });
  498. var removedCount = this._adjustRange(isAppend, $cloneElements);
  499. // prepare HTML
  500. this.$el[isAppend ? "append" : "prepend"]($cloneElements);
  501. this.layout(false, this._itemize($cloneElements, groupKey), {
  502. isAppend: isAppend,
  503. removedCount: removedCount
  504. });
  505. },
  506. _waitResource: function(isRelayout, addItems, options) {
  507. this._isProcessing = true;
  508. var needCheck = this._checkImageLoaded();
  509. var self = this;
  510. var callback = function() {
  511. self._layoutComplete(isRelayout, addItems, options);
  512. };
  513. if (needCheck.length > 0) {
  514. this._waitImageLoaded(needCheck, callback);
  515. } else {
  516. // convert to async
  517. setTimeout(function() {
  518. callback && callback();
  519. }, 0);
  520. }
  521. },
  522. _adjustRange: function (isTop, $elements) {
  523. var removedCount = 0;
  524. if (!this.isRecycling()) {
  525. return removedCount;
  526. }
  527. // trim $elements
  528. if (this.options.count <= $elements.length) {
  529. removedCount += isTop ? $elements.splice(0, $elements.length - this.options.count).length
  530. : $elements.splice(this.options.count).length;
  531. }
  532. var diff = this.items.length + $elements.length - this.options.count;
  533. var targets;
  534. var idx;
  535. if (diff <= 0 || (idx = this._getDelimiterIndex(isTop, diff)) < 0) {
  536. return removedCount;
  537. }
  538. if (isTop) {
  539. targets = this.items.splice(0, idx);
  540. this._syncCols(false); // for prepending
  541. } else {
  542. targets = idx === this.items.length ? this.items.splice(0) : this.items.splice(idx, this.items.length - idx);
  543. this._syncCols(true); // for appending;
  544. }
  545. // @todo improve performance
  546. $.each(targets, function(i, v) {
  547. idx = $elements.index(v.el);
  548. if (idx !== -1) {
  549. $elements.splice(idx, 1);
  550. } else {
  551. v.el.parentNode.removeChild(v.el);
  552. }
  553. });
  554. removedCount += targets.length;
  555. return removedCount;
  556. },
  557. _getDelimiterIndex: function(isTop, removeCount) {
  558. var len = this.items.length;
  559. if (len === removeCount) {
  560. return len;
  561. }
  562. var i;
  563. var idx = 0;
  564. var baseIdx = isTop ? removeCount - 1 : len - removeCount;
  565. var targetIdx = baseIdx + (isTop ? 1 : -1);
  566. var groupKey = this.items[baseIdx].groupKey;
  567. if (groupKey != null && groupKey === this.items[targetIdx].groupKey) {
  568. if (isTop) {
  569. for (i = baseIdx; i > 0; i--) {
  570. if (groupKey !== this.items[i].groupKey) {
  571. break;
  572. }
  573. }
  574. idx = i === 0 ? -1 : i + 1;
  575. } else {
  576. for (i = baseIdx; i < len; i++) {
  577. if (groupKey !== this.items[i].groupKey) {
  578. break;
  579. }
  580. }
  581. idx = i === len ? -1 : i;
  582. }
  583. } else {
  584. idx = isTop ? targetIdx : baseIdx;
  585. }
  586. return idx;
  587. },
  588. // fit size
  589. _fit: function(applyDom) {
  590. // for caching
  591. if (this.options.count <= 0) {
  592. this._fit = function() {
  593. return 0;
  594. };
  595. return 0;
  596. }
  597. var y = this._getTopPositonY();
  598. if (y !== 0) {
  599. // need to fit
  600. $.each(this.items, function(i, v) {
  601. v.position.y -= y;
  602. applyDom && (v.el.style.top = v.position.y + "px");
  603. });
  604. this._syncCols(false); // for prepending
  605. this._syncCols(true); // for appending
  606. applyDom && (this.el.style.height = this._getContainerSize().height + "px");
  607. }
  608. return y;
  609. },
  610. /**
  611. * Removes extra space caused by adding card elements.
  612. * @ko 카드 엘리먼트를 추가한 다음 생긴 빈 공간을 제거한다
  613. * @method eg.InfiniteGrid#fit
  614. * @deprecated since version 1.3.0
  615. * @return {Number} Actual length of space removed (unit: px) <ko>빈 공간이 제거된 실제 길이(단위: px)</ko>
  616. */
  617. fit: function() {
  618. return this._fit(true);
  619. },
  620. _reset: function() {
  621. this._isProcessing = false;
  622. this._topElement = null;
  623. this._bottomElement = null;
  624. this._isRecycling = false;
  625. this._prevScrollTop = 0;
  626. this._equalItemSize = 0;
  627. this._resizeTimeout = null;
  628. this._doubleCheckTimer = null;
  629. this._doubleCheckCount = RETRY;
  630. this._resetCols(this._appendCols.length || 0);
  631. this.items = [];
  632. },
  633. _checkImageLoaded: function() {
  634. return this.$el.find("img").filter(function(k, v) {
  635. if (v.nodeType && ($.inArray(v.nodeType, [1,9,11]) !== -1)) {
  636. return !v.complete;
  637. }
  638. }).toArray();
  639. },
  640. _waitImageLoaded: function(needCheck, callback) {
  641. var checkCount = needCheck.length;
  642. var onCheck = function(e) {
  643. checkCount--;
  644. $(e.target).off("load error");
  645. checkCount <= 0 && callback && callback();
  646. };
  647. var $el;
  648. var self = this;
  649. $.each(needCheck, function(i, v) {
  650. $el = $(v);
  651. // workaround for IE
  652. if (self._isIE) {
  653. var url = v.getAttribute("src");
  654. v.setAttribute("src", "");
  655. v.setAttribute("src", url);
  656. }
  657. $el.on("load error", onCheck);
  658. });
  659. },
  660. _measureColumns: function() {
  661. this.el.style.width = null;
  662. this._containerWidth = this.$el.innerWidth();
  663. this._columnWidth = this._getColumnWidth() || this._containerWidth;
  664. var cols = this._containerWidth / this._columnWidth;
  665. var excess = this._columnWidth - this._containerWidth % this._columnWidth;
  666. // if overshoot is less than a pixel, round up, otherwise floor it
  667. cols = Math.max(Math[ excess && excess <= 1 ? "round" : "floor" ](cols), 1);
  668. return cols || 0;
  669. },
  670. _resetCols: function(count) {
  671. count = typeof count === "undefined" ? 0 : count;
  672. var arr = [];
  673. while (count--) {
  674. arr.push(0);
  675. }
  676. this._appendCols = arr.concat();
  677. this._prependCols = arr.concat();
  678. },
  679. _getContainerSize: function() {
  680. return {
  681. height: Math.max.apply(Math, this._appendCols),
  682. width: this._containerWidth
  683. };
  684. },
  685. _getColumnWidth: function() {
  686. var el = this.items[0] && this.items[0].el;
  687. var width = 0;
  688. if (el) {
  689. var $el = $(el);
  690. width = $el.innerWidth();
  691. if (this.options.isEqualSize) {
  692. this._equalItemSize = {
  693. width: width,
  694. height: $el.innerHeight()
  695. };
  696. }
  697. }
  698. return width;
  699. },
  700. _syncCols: function(isBottom) {
  701. if (!this.items.length) {
  702. return;
  703. }
  704. var items = this._getColItems(isBottom);
  705. var col = isBottom ? this._appendCols : this._prependCols;
  706. var len = col.length;
  707. var i;
  708. for (i = 0; i < len; i++) {
  709. col[i] = items[i].position.y + (isBottom ? items[i].size.height : 0);
  710. }
  711. },
  712. _getColIdx: function(item) {
  713. return parseInt(item.position.x / parseInt(this._columnWidth, 10), 10);
  714. },
  715. _getColItems: function(isBottom) {
  716. var len = this._appendCols.length;
  717. var colItems = new Array(len);
  718. var item;
  719. var idx;
  720. var count = 0;
  721. var i = isBottom ? this.items.length - 1 : 0;
  722. while (item = this.items[i]) {
  723. idx = this._getColIdx(item);
  724. if (!colItems[idx]) {
  725. colItems[idx] = item;
  726. if (++count === len) {
  727. return colItems;
  728. }
  729. }
  730. i += isBottom ? -1 : 1;
  731. }
  732. return colItems;
  733. },
  734. _itemize: function(elements, groupKey) {
  735. return $.map(elements, function(v) {
  736. return {
  737. el: v,
  738. position: {
  739. x: 0,
  740. y: 0
  741. },
  742. groupKey: typeof groupKey === "undefined" ? null : groupKey
  743. };
  744. });
  745. },
  746. _getItemLayoutPosition: function(isRelayout, item, isAppend) {
  747. if (!item || !item.el) {
  748. return;
  749. }
  750. var $el = $(item.el);
  751. if (isRelayout || !item.size) {
  752. item.size = this._getItemSize($el);
  753. }
  754. var cols = isAppend ? this._appendCols : this._prependCols;
  755. var y = Math[isAppend ? "min" : "max"].apply(Math, cols);
  756. var shortColIndex;
  757. if (isAppend) {
  758. shortColIndex = $.inArray(y, cols);
  759. } else {
  760. var i = cols.length;
  761. while (i-- >= 0) {
  762. if (cols[i] === y) {
  763. shortColIndex = i;
  764. break;
  765. }
  766. }
  767. }
  768. cols[shortColIndex] = y + (isAppend ? item.size.height : -item.size.height);
  769. return {
  770. x: this._columnWidth * shortColIndex,
  771. y: isAppend ? y : y - item.size.height
  772. };
  773. },
  774. _getItemSize: function($el) {
  775. return this._equalItemSize || {
  776. width: $el.innerWidth(),
  777. height: $el.innerHeight()
  778. };
  779. },
  780. /**
  781. * Removes a card element on a grid layout.
  782. * @ko 그리드 레이아웃의 카드 엘리먼트를 삭제한다.
  783. * @method eg.InfiniteGrid#remove
  784. * @param {HTMLElement} Card element to be removed <ko>삭제될 카드 엘리먼트</ko>
  785. * @return {Object} Removed card element <ko>삭제된 카드 엘리먼트 정보</ko>
  786. */
  787. remove: function(element) {
  788. var item = null;
  789. var idx = -1;
  790. for (var i = 0, len = this.items.length; i < len; i++) {
  791. if (this.items[i].el === element) {
  792. idx = i;
  793. break;
  794. }
  795. }
  796. if (~idx) {
  797. // remove item information
  798. item = $.extend({}, this.items[idx]);
  799. this.items.splice(idx, 1);
  800. // remove item element
  801. item.el.parentNode.removeChild(item.el);
  802. }
  803. return item;
  804. },
  805. /**
  806. * Destroys elements, properties, and events used on a grid layout.
  807. * @ko 그리드 레이아웃에 사용한 엘리먼트와 속성, 이벤트를 해제한다
  808. * @method eg.InfiniteGrid#destroy
  809. */
  810. destroy: function() {
  811. this.off();
  812. this.$view.off("resize", this._onResize)
  813. .off("scroll", this._onScroll);
  814. this._reset();
  815. }
  816. });
  817. });
  818. /**
  819. * A jQuery plugin available in the eg.InfiniteGrid module.
  820. * @ko eg.InfiniteGrid 모듈의 jQuery 플러그인
  821. * @method jQuery.infiniteGrid
  822. * @example
  823. <ul id="grid">
  824. <li class="item">
  825. <div>test1</div>
  826. </li>
  827. <li class="item">
  828. <div>test3</div>
  829. </li>
  830. </ul>
  831. <script>
  832. // create
  833. $("#grid").infiniteGrid();
  834. // method
  835. $("#grid").infiniteGrid("option","count","60"); //Set option
  836. $("#grid").infiniteGrid("instance"); // Return infiniteGrid instance
  837. $("#grid").infiniteGrid("getBottomElement"); // Get bottom element
  838. </script>
  839. * @see eg.InfiniteGrid
  840. */
  841. /**
  842. * A jQuery custom event of the eg.InfiniteGrid module. This event is fired when a layout is successfully arranged.
  843. *
  844. * @ko eg.InfiniteGrid 모듈의 jQuery 커스텀 이벤트. 레이아웃 배치가 완료됐을 때 발생한다
  845. * @name jQuery#infiniteGrid:layoutComplete
  846. * @event
  847. * @example
  848. <ul id="grid">
  849. <li class="item">
  850. <div>test1</div>
  851. </li>
  852. <li class="item">
  853. <div>test3</div>
  854. </li>
  855. </ul>
  856. <script>
  857. // create
  858. $("#grid").infiniteGrid();
  859. // event
  860. $("#grid").on("infiniteGrid:layoutComplete",callback);
  861. $("#grid").off("infiniteGrid:layoutComplete",callback);
  862. $("#grid").trigger("infiniteGrid:layoutComplete",callback);
  863. </script>
  864. * @see eg.InfiniteGrid#event:layoutComplete
  865. */
  866. /**
  867. * A jQuery custom event of the eg.InfiniteGrid module. This event is fired when a card element must be added at the bottom of a grid layout
  868. *
  869. * @ko eg.InfiniteGrid 모듈의 jQuery 커스텀 이벤트. 그리드 레이아웃 아래에 카드 엘리먼트가 추가돼야 할 때 발생한다.
  870. * @name jQuery#infiniteGrid:append
  871. * @event
  872. * @example
  873. <ul id="grid">
  874. <li class="item">
  875. <div>test1</div>
  876. </li>
  877. <li class="item">
  878. <div>test3</div>
  879. </li>
  880. </ul>
  881. <script>
  882. // create
  883. $("#grid").infiniteGrid();
  884. // event
  885. $("#grid").on("infiniteGrid:append",callback);
  886. $("#grid").off("infiniteGrid:append",callback);
  887. $("#grid").trigger("infiniteGrid:append",callback);
  888. </script>
  889. * @see eg.InfiniteGrid#event:append
  890. */
  891. /**
  892. * A jQuery custom event of the eg.InfiniteGrid module. This event is fired when a card element must be added at the top of a grid layout
  893. *
  894. * @ko eg.InfiniteGrid 모듈의 jQuery 커스텀 이벤트. 그리드 레이아웃 위에 카드 엘리먼트가 추가돼야 할 때 발생한다
  895. * @name jQuery#infiniteGrid:prepend
  896. * @event
  897. * @example
  898. <ul id="grid">
  899. <li class="item">
  900. <div>test1</div>
  901. </li>
  902. <li class="item">
  903. <div>test3</div>
  904. </li>
  905. </ul>
  906. <script>
  907. // create
  908. $("#grid").infiniteGrid();
  909. // event
  910. $("#grid").on("infiniteGrid:prepend",callback);
  911. $("#grid").off("infiniteGrid:prepend",callback);
  912. $("#grid").trigger("infiniteGrid:prepend",callback);
  913. </script>
  914. * @see eg.InfiniteGrid#event:prepend
  915. */
comments powered by Disqus