IndexBar.svelte 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <script>
  2. import { onMount, createEventDispatcher } from 'svelte';
  3. import { fly } from 'svelte/transition';
  4. import { debounce } from '../utils';
  5. // 定义事件派发器
  6. // Define event dispatcher
  7. const dispatch = createEventDispatcher();
  8. //数据
  9. //Data
  10. export let data = [];
  11. //当前页码
  12. //Current page
  13. export let current = 0;
  14. //索引内容区域距离文档顶部的距离
  15. //The distance from the top of the document to the index content area
  16. export let top = 0;
  17. //索引内容区域高度
  18. //Index content area height
  19. export let height = 100;
  20. //圆角风格
  21. //Rounded style
  22. export let radius = 'base';
  23. //是否滚动对齐
  24. //Whether to scroll align
  25. export let scrollAlign = true;
  26. // 索引组标题注入 CSS
  27. // Index group title injection CSS
  28. export let titleInjClass = '';
  29. // 索引项文本注入 CSS
  30. // Index item text injection CSS
  31. export let textInjClass = '';
  32. // 用于绑定bar的高度
  33. // Used to bind the height of the bar
  34. let barHeight = 0;
  35. // 主体内容高度累计,用于监听滑动
  36. // The cumulative height of the main content is used to listen to the slide
  37. let longSumList = [];
  38. // 当前滑动的索引
  39. // Current sliding index
  40. let currentTouch = -1;
  41. // 主体内容元素
  42. // Main body content element
  43. let bodyDom = null;
  44. // Bar外层距离顶部的距离
  45. // Bar outside the distance from the top
  46. let barToTop = 0;
  47. // bar元素
  48. // bar element
  49. let barDom = null;
  50. let isDown = false; //是否按下 is down
  51. // 每一个的高度
  52. // Height of each
  53. $: itemHeight = barHeight / data.length;
  54. // 圆角风格样式
  55. // Rounded style style
  56. const radiusObj = {
  57. none: 'rounded-none',
  58. base: 'rounded',
  59. full: 'rounded-full',
  60. };
  61. onMount(() => {
  62. bodyDom.scrollTop = 0;
  63. for (let t = 0; t < data.length + 1; t++) {
  64. const long = data.slice(0, t).reduce((sum, current) => {
  65. return sum + current.height;
  66. }, 0);
  67. longSumList.push(long);
  68. }
  69. //如果索引内容区域高度不够,中英文报错
  70. //If the index content area height is not enough, Chinese and English errors
  71. if (height < barHeight) {
  72. console.error('[STDF IndexBar error]注意:索引内容区域高度不够。(Note: The height of the index content area is not enough.)');
  73. }
  74. barToTop = top + (height - barHeight) / 2;
  75. });
  76. //bar区域滑动开始
  77. //Bar area sliding start
  78. const touchBoxStart = e => {
  79. isDown = true;
  80. const clientY = e.clientY;
  81. currentTouch = Math.floor((clientY - barToTop) / itemHeight);
  82. current = Math.floor((clientY - barToTop) / itemHeight);
  83. bodyDom.scrollTop = data.slice(0, current).reduce((sum, current) => {
  84. return sum + current.height;
  85. }, 0);
  86. };
  87. // bar区域滑动中
  88. // bar area sliding in the middle
  89. const touchBoxMove = e => {
  90. if (!isDown) {
  91. return;
  92. }
  93. barDom.setPointerCapture(e.pointerId);
  94. const clientY = e.clientY;
  95. currentTouch = Math.floor((clientY - barToTop) / itemHeight);
  96. current = Math.floor((clientY - barToTop) / itemHeight);
  97. if (clientY < barToTop) {
  98. currentTouch = 0;
  99. current = 0;
  100. }
  101. if (clientY > barHeight + barToTop) {
  102. currentTouch = data.length - 1;
  103. current = data.length - 1;
  104. }
  105. bodyDom.scrollTop = data.slice(0, current).reduce((sum, current) => {
  106. return sum + current.height;
  107. }, 0);
  108. };
  109. // bar区域滑动结束
  110. // bar area sliding end
  111. const touchBoxEnd = () => {
  112. currentTouch = -1;
  113. isDown = false;
  114. };
  115. //监听主体内容滚动
  116. //Listen to the main content scroll
  117. const scrollBody = () => {
  118. const scrollTop = bodyDom.scrollTop + top;
  119. for (let w = 0; w < longSumList.length; w++) {
  120. if (scrollTop - top >= longSumList[w] && scrollTop - top < longSumList[w + 1]) {
  121. current = w;
  122. }
  123. }
  124. };
  125. // 点击主体内容区域
  126. // Click on the main body content area
  127. const chickChildFun = (index, group, childIndex, child) => {
  128. // 派发点击事件,向外传递出四个参数。 1. index:点击项的父级组索引值;2. group:点击项的父级组内容;3. childIndex:点击项索引值;4. child:点击项内容。
  129. // Dispatch click events to pass four parameters out. 1. index: the parent group index value of the clicked item; 2. group: the parent group content of the clicked item; 3. childIndex: the index value of the clicked item; 4. child: the content of the clicked item.
  130. dispatch('clickchild', { index, group, childIndex, child });
  131. };
  132. </script>
  133. <div bind:this={bodyDom} class={`overflow-y-auto ${scrollAlign && 'snap-y'}`} on:scroll={scrollBody} style="height:{height}px;">
  134. {#each data as group, index}
  135. <div class="px-4 pt-8 snap-start" bind:clientHeight={group.height}>
  136. <div class={`text-primary dark:text-dark text-sm ${titleInjClass}`}>{group.title}</div>
  137. {#each group.child as child, childIndex}
  138. <!-- svelte-ignore a11y-click-events-have-key-events -->
  139. <!-- svelte-ignore a11y-no-static-element-interactions -->
  140. <div class={`py-2 ${textInjClass}`} on:click={() => chickChildFun(index, group, childIndex, child)}>
  141. {child.text}
  142. </div>
  143. <div class="h-px bg-black/5 dark:bg-white/5" />
  144. {/each}
  145. </div>
  146. {/each}
  147. </div>
  148. <div
  149. on:pointerdown={touchBoxStart}
  150. on:pointermove={debounce(touchBoxMove, 5)}
  151. on:pointerup={debounce(touchBoxEnd, 15)}
  152. bind:clientHeight={barHeight}
  153. bind:this={barDom}
  154. class={`fixed right-5 bg-black/5 dark:bg-white/5 w-7 p-1 flex flex-col justify-around touch-none cursor-move select-none ${
  155. radiusObj[radius] || radiusObj.base
  156. }`}
  157. style="top:{top + (height - barHeight) / 2}px;min-height:{height / 4}px;"
  158. >
  159. {#each data as group, i}
  160. <div class={`flex-1 relative flex flex-col justify-center`}>
  161. <div
  162. class={`w-5 h-5 leading-5 text-center text-xs transition-all ${
  163. current === i ? 'bg-primary dark:bg-dark text-white dark:text-black' : 'text-gray-600 dark:text-gray-400'
  164. } ${radiusObj[radius] || radiusObj.base}`}
  165. >
  166. {group.index}
  167. </div>
  168. {#if currentTouch === i}
  169. <div
  170. class={`absolute text-center -left-24 top-1/2 -translate-y-2/4 text-3xl border border-primary dark:border-dark w-14 h-14 leading-[3.5rem] text-primary dark:text-dark ${
  171. radiusObj[radius] || radiusObj.base
  172. }`}
  173. transition:fly={{ x: 38, duration: 300 }}
  174. >
  175. {group.index}
  176. </div>
  177. {/if}
  178. </div>
  179. {/each}
  180. </div>