ScrollRadio.svelte 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. <script>
  2. import { onMount, createEventDispatcher } from 'svelte';
  3. // 定义事件派发器
  4. // Define event dispatcher
  5. const dispatch = createEventDispatcher();
  6. // 数据
  7. // Data
  8. export let data = [];
  9. // 可见行数
  10. // Visible rows
  11. export let showRow = 5;
  12. // 默认选中项索引
  13. // Default selected item index
  14. export let initIndex = 0;
  15. // data 中 label 对应的 key
  16. // The key corresponding to label in data
  17. export let labelKey = 'label';
  18. // 是否自动滚动到上次的选中项
  19. // Whether to automatically scroll to the last selected item
  20. export let autoScrollToLast = true;
  21. // 自动滚动到选中项(initIndex)时,是否使用动画
  22. // Whether to use animation when automatically scrolling to the selected item (initIndex)
  23. export let useAnimation = true;
  24. // 上次选中项索引
  25. // Last selected item index
  26. export let lastSelectedIndex = 0;
  27. // 对齐方式
  28. // Alignment
  29. export let align = 'center';
  30. // 是否触摸
  31. // Whether to touch
  32. let isTouch = false;
  33. // 对齐方式样式
  34. // Alignment style
  35. const alignClass = {
  36. center: 'text-center',
  37. left: 'text-left',
  38. right: 'text-right',
  39. };
  40. // 单条高度对象
  41. // Single height object
  42. const itemHeightObj = { '3': 4, '5': 3, '7': 2 };
  43. // 实际显示行数
  44. // Actual number of rows displayed
  45. const showRowsInner = showRow === 3 || showRow === 5 || showRow === 7 ? showRow : 5;
  46. // 单条高度
  47. // Single height
  48. const itemHeight = itemHeightObj[showRowsInner];
  49. // 定义空对象 emptyObj,当 showRowsInner 为 3 时,newData 前后各补足一条 emptyObj,当 showRowsInner 为 5 时,newData 前后各补足两条 emptyObj,当 showRowsInner 为 7 时,newData 前后各补足三条 emptyObj
  50. // Define empty object emptyObj, when showRowsInner is 3, newData is supplemented with one emptyObj at the front and back, when showRowsInner is 5, newData is supplemented with two emptyObj at the front and back, when showRowsInner is 7, newData is supplemented with three emptyObj at the front and back
  51. const emptyObj = {};
  52. emptyObj[labelKey] = '';
  53. const newData =
  54. showRowsInner === 3
  55. ? [...[emptyObj], ...data, ...[emptyObj]]
  56. : showRowsInner === 5
  57. ? [...[emptyObj, emptyObj], ...data, ...[emptyObj, emptyObj]]
  58. : [...[emptyObj, emptyObj, emptyObj], ...data, ...[emptyObj, emptyObj, emptyObj]];
  59. // 滚动元素
  60. // Scroll element
  61. let scrollElement = null;
  62. // 当前选中项索引
  63. // Current selected item index
  64. let currentIndex = 0;
  65. onMount(() => {
  66. if (scrollElement) {
  67. // 监听 scrollElement 的滚动事件,同时考虑节流,滚动结束时,计算当前最中间的元素的索引
  68. // Listen to the scroll event of scrollElement, and consider throttling at the same time. The index of the element in the middle is calculated when the scroll ends
  69. let scrollTimer = null;
  70. scrollElement.addEventListener('scroll', e => {
  71. clearTimeout(scrollTimer);
  72. scrollTimer = setTimeout(() => {
  73. const scrollTop = e.target.scrollTop;
  74. currentIndex = Math.round(scrollTop / (itemHeight * 16));
  75. dispatch('scrollEnd', { index: currentIndex, isTouch });
  76. });
  77. });
  78. }
  79. });
  80. $: {
  81. if (scrollElement) {
  82. if (autoScrollToLast) {
  83. scrollElement.scrollTop = lastSelectedIndex * itemHeight * 16;
  84. } else {
  85. scrollElement.scrollTop = initIndex * itemHeight * 16;
  86. }
  87. }
  88. }
  89. </script>
  90. <div class="overflow-auto relative picker-contents" style="height:{itemHeight * showRowsInner}rem;">
  91. <div
  92. class={`overflow-auto snap-y picker-contents ${useAnimation ? 'scroll-smooth' : 'scroll-auto'}`}
  93. style="height:{itemHeight * showRowsInner}rem;"
  94. bind:this={scrollElement}
  95. on:touchstart={() => {
  96. isTouch = true;
  97. }}
  98. >
  99. {#each newData as item}
  100. <div
  101. class={`${alignClass[align] || alignClass.center} px-2 flex flex-col justify-center snap-center`}
  102. style="height:{itemHeight}rem;"
  103. >
  104. <div class="truncate">{item[labelKey]}</div>
  105. </div>
  106. {/each}
  107. <div
  108. class="absolute inset-0 w-full pointer-events-none border-b border-t border-white dark:border-black"
  109. style="height:{itemHeight * showRowsInner}rem"
  110. >
  111. <div
  112. class="bg-gradient-to-b from-white to-white/60 dark:from-black dark:to-black/60 border-b border-black/10 dark:border-white/20"
  113. style="height:{itemHeight * ((showRowsInner - 1) / 2)}rem;"
  114. />
  115. <div style="height:{itemHeight}rem;" />
  116. <div
  117. class="bg-gradient-to-t from-white to-white/60 dark:from-black dark:to-black/60 border-t border-black/10 dark:border-white/20"
  118. style="height:{itemHeight * ((showRowsInner - 1) / 2)}rem;"
  119. />
  120. </div>
  121. </div>
  122. </div>
  123. <style>
  124. .picker-contents::-webkit-scrollbar {
  125. display: none; /* Chrome Safari */
  126. }
  127. </style>