itemFoundation.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  2. import isElement from '../utils/isElement';
  3. import { slice, find, findIndex } from 'lodash-es';
  4. import { append, prepend } from '../utils/dom';
  5. export interface Item {
  6. [x: string]: any;
  7. transform?: (value: any, text: string) => string;
  8. value: any;
  9. text?: string;
  10. disabled?: boolean;
  11. }
  12. export interface ScrollItemAdapter<P = Record<string, any>, S = Record<string, any>, I = Item> extends DefaultAdapter<P, S> {
  13. setPrependCount: (prependCount: number) => void;
  14. setAppendCount: (appendCount: number) => void;
  15. setSelectedNode: (el: HTMLElement) => void;
  16. isDisabledIndex: (i: number) => boolean;
  17. notifySelectItem: (data: I) => void;
  18. scrollToCenter: (selectedNode: Element, scrollWrapper?: Element, duration?: number) => void;
  19. }
  20. export default class ItemFoundation<P = Record<string, any>, S = Record<string, any>, I = Item> extends BaseFoundation<ScrollItemAdapter<P, S, I>, P, S> {
  21. _cachedSelectedNode: HTMLElement = null;
  22. selectIndex(index: number, listWrapper: HTMLElement) {
  23. const { type, list } = this.getProps();
  24. if (index > -1 && Array.isArray(list) && list.length && isElement(listWrapper)) {
  25. const indexInData = index % list.length;
  26. const item = list[indexInData];
  27. const node = listWrapper.children[index] as HTMLElement;
  28. this._adapter.setSelectedNode(node);
  29. this._adapter.notifySelectItem({
  30. ...item,
  31. value: item.value,
  32. type,
  33. index: indexInData,
  34. });
  35. }
  36. }
  37. selectNode(node: HTMLElement, listWrapper: HTMLElement) {
  38. const { type, list: data } = this.getProps();
  39. if (isElement(node) && isElement(listWrapper)) {
  40. const indexInList = findIndex(listWrapper.children, ele => ele === node);
  41. const indexInData = indexInList % data.length;
  42. const cachedIndexInList = findIndex(listWrapper.children, ele => ele === this._cachedSelectedNode);
  43. const cachedIndexData = cachedIndexInList % data.length;
  44. const item = data[indexInData];
  45. this._adapter.setSelectedNode(node);
  46. this._adapter.scrollToCenter(node);
  47. // Avoid triggerring notifySelectItem twice,
  48. // because that scroll event will be trigger
  49. // when you click to select an item.
  50. if (this._cachedSelectedNode !== node) {
  51. this._cachedSelectedNode = node;
  52. if (cachedIndexData !== indexInData) {
  53. this._adapter.notifySelectItem({
  54. ...item,
  55. value: item.value,
  56. type,
  57. index: indexInData,
  58. });
  59. }
  60. }
  61. }
  62. }
  63. /**
  64. *
  65. * @param {HTMLElement} listWrapper
  66. * @param {HTMLElement} scrollWrapper
  67. * @param {number} ratio
  68. * @returns {boolean}
  69. */
  70. shouldAppend(listWrapper: HTMLElement, scrollWrapper: HTMLElement, ratio = 2) {
  71. const tag = 'li';
  72. if (isElement(listWrapper) && isElement(scrollWrapper)) {
  73. const itemNodes = listWrapper.querySelectorAll(tag);
  74. const lastNode = itemNodes[itemNodes.length - 1];
  75. const { list } = this.getProps();
  76. if (lastNode) {
  77. const scrollRect = scrollWrapper.getBoundingClientRect();
  78. const lastRect = lastNode.getBoundingClientRect();
  79. const listHeight = lastRect.height * list.length;
  80. let baseTop = lastRect.top;
  81. let count = 0;
  82. while (baseTop <= scrollRect.top + scrollRect.height * ratio) {
  83. count += 1;
  84. baseTop += listHeight;
  85. }
  86. return count;
  87. }
  88. }
  89. return false;
  90. }
  91. /**
  92. *
  93. * @param {HTMLElement} listWrapper
  94. * @param {HTMLElement} scrollWrapper
  95. * @param {number} ratio
  96. *
  97. * @returns {boolean}
  98. */
  99. shouldPrepend(listWrapper: HTMLElement, scrollWrapper: HTMLElement, ratio = 2) {
  100. const tag = 'li';
  101. if (isElement(listWrapper) && isElement(scrollWrapper)) {
  102. const itemNodes = listWrapper.querySelectorAll(tag);
  103. const firstNode = itemNodes[0];
  104. const { list } = this.getProps();
  105. if (firstNode) {
  106. const scrollRect = scrollWrapper.getBoundingClientRect();
  107. const firstRect = firstNode.getBoundingClientRect();
  108. const listHeight = firstRect.height * list.length;
  109. let baseTop = firstRect.top;
  110. let count = 0;
  111. while (baseTop + firstRect.height >= scrollRect.top - scrollRect.height * ratio) {
  112. count += 1;
  113. baseTop -= listHeight;
  114. }
  115. return count;
  116. }
  117. }
  118. return 0;
  119. }
  120. /**
  121. *
  122. * @param {HTMLElement} listWrapper
  123. * @param {HTMLElement} wrapper
  124. * @param {Function} [callback]
  125. */
  126. initWheelList(listWrapper: HTMLElement, wrapper: HTMLElement, callback: () => void) {
  127. const { list } = this.getProps();
  128. if (isElement(wrapper) && isElement(listWrapper) && list && list.length) {
  129. const allNodes = listWrapper.children;
  130. const baseNodes = slice(allNodes, 0, list.length);
  131. const prependCount = this.shouldPrepend(listWrapper, wrapper);
  132. const appendCount = this.shouldAppend(listWrapper, wrapper);
  133. // this._adapter.setPrependCount(prependCount);
  134. // this._adapter.setAppendCount(appendCount);
  135. this._adapter.setState(
  136. {
  137. prependCount,
  138. appendCount,
  139. } as any,
  140. callback
  141. );
  142. }
  143. }
  144. /**
  145. *
  146. * @param {HTMLElement} listWrapper
  147. * @param {HTMLElement} wrapper
  148. * @param {HTMLElement} [nearestNode]
  149. */
  150. adjustInfiniteList(listWrapper: HTMLElement, wrapper: HTMLElement, nearestNode: HTMLElement) {
  151. const { list } = this.getProps();
  152. const nodeTag = 'li';
  153. if (isElement(wrapper) && isElement(listWrapper) && list && list.length) {
  154. const allNodes = listWrapper.querySelectorAll(nodeTag);
  155. const total = allNodes.length;
  156. const ratio = 1;
  157. const prependCount = this.shouldPrepend(listWrapper, wrapper, ratio);
  158. const appendCount = this.shouldAppend(listWrapper, wrapper, ratio);
  159. // while (this.shouldPrepend(listWrapper, wrapper, nearestNode)) {
  160. if (prependCount) {
  161. // move last nodes to first position
  162. for (let i = 0; i < prependCount; i++) {
  163. const nodes = slice(allNodes, total - list.length * (i + 1), total - list.length * i);
  164. prepend(listWrapper, ...nodes);
  165. }
  166. }
  167. // while (this.shouldAppend(listWrapper, wrapper, nearestNode)) {
  168. if (appendCount) {
  169. for (let i = 0; i < appendCount; i++) {
  170. const nodes = slice(allNodes, i * list.length, (i + 1) * list.length);
  171. append(listWrapper, ...nodes);
  172. }
  173. }
  174. }
  175. }
  176. /**
  177. *
  178. * @param {HTMLElement} listWrapper
  179. * @param {HTMLElement} selector
  180. *
  181. */
  182. getNearestNodeInfo(listWrapper: HTMLElement, selector: HTMLElement) {
  183. if (isElement(listWrapper) && isElement(selector)) {
  184. const selectorRect = selector.getBoundingClientRect();
  185. const selectorTop = selectorRect.top;
  186. const itemNodes = listWrapper.querySelectorAll('li');
  187. let nearestNode: HTMLElement = null;
  188. let nearestIndex = -1;
  189. let nearestDistance = Infinity;
  190. Array.from(itemNodes).map((node, index) => {
  191. const rect = node.getBoundingClientRect();
  192. const rectTop = rect.top;
  193. const absDistance = Math.abs(rectTop - selectorTop);
  194. if (absDistance < nearestDistance && !this._adapter.isDisabledIndex(index)) {
  195. nearestDistance = absDistance;
  196. nearestNode = node;
  197. nearestIndex = index;
  198. }
  199. });
  200. return { nearestNode, nearestIndex };
  201. }
  202. return undefined;
  203. }
  204. /**
  205. *
  206. * @param {HTMLElement} listWrapper
  207. *
  208. * @param {HTMLElement|null}
  209. */
  210. getTargetNode(e: any, listWrapper: HTMLElement) {
  211. if (e && isElement(listWrapper)) {
  212. const targetTagName = 'li';
  213. const currentTarget = e.target;
  214. const itemNodes = listWrapper.querySelectorAll(targetTagName);
  215. const list = this.getProp('list');
  216. const length = Array.isArray(list) ? list.length : 0;
  217. let targetIndex = -1;
  218. let indexInList = -1;
  219. let infoInList = null;
  220. const targetNode = find(itemNodes, (node, index) => {
  221. if (node === currentTarget || node.contains(currentTarget)) {
  222. targetIndex = index;
  223. if (length > 0) {
  224. indexInList = index % length;
  225. }
  226. return true;
  227. }
  228. return undefined;
  229. });
  230. if (indexInList > -1) {
  231. infoInList = list[indexInList];
  232. }
  233. return {
  234. targetNode,
  235. targetIndex,
  236. indexInList,
  237. infoInList,
  238. };
  239. }
  240. return null;
  241. }
  242. }