| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416 |
- <script>
- import { onMount, createEventDispatcher, getContext } from 'svelte';
- import { fly } from 'svelte/transition';
- import { throttle } from '../utils';
- import zh_CN from '../../lang/zh_CN';
- import Mask from '../mask/Mask.svelte';
- import Icon from '../icon/Icon.svelte';
- // 当前语言
- // current language
- const currentLang = getContext('STDF_lang') || zh_CN;
- const bottomSheetLang = currentLang.bottomSheet;
- /**
- * 是否显示
- * show or not
- * @type {Boolean}
- * @default false
- */
- export let visible = false;
- /**
- * 标题
- * title
- * @type {string}
- * @default Current language bottomSheet.title
- */
- export let title = bottomSheetLang.title;
- /**
- * 标题对齐方式
- * title align
- * @type {'left'|'center'|'right'}
- * @default 'left'
- */
- export let titleAlign = 'left';
- /**
- * 是否显示返回图标
- * show back icon or not
- * @type {Boolean}
- * @default false
- */
- export let showBackIcon = false;
- /**
- * 关闭区域内容
- * close content
- * @type {'downIcon'|'closeIcon'|''|string}
- * @default 'downIcon'
- */
- export let closeContent = 'downIcon';
- /**
- * 是否在顶部显示分割线
- * show divider or not at the top
- * @type {Boolean}
- * @default true
- */
- export let showDivider = true;
- /**
- * 过渡动画出现时间
- * transition animation appear time
- * @type {number}
- * @default 450
- */
- export let duration = 450;
- /**
- * 过渡动画退出时间
- * transition animation exit time
- * @type {number}
- * @default 240
- */
- export let outDuration = 240;
- /**
- * 遮罩层参数
- * mask params
- * @type {object}
- * @default {}
- */
- export let mask = {};
- /**
- * 点击遮罩层是否关闭
- * click mask to close or not
- * @type {Boolean}
- * @default true
- */
- export let maskClosable = false;
- /**
- * z-index
- * @type {number}
- * @default 600
- */
- export let zIndex = 600;
- /**
- * 固定高度列表
- * stay height list
- * @type {Array<number>}
- * @default [10, 50, 90]
- */
- export let stayHeightList = [10, 50, 90];
- /**
- * 初始固定高度索引
- * initial stay height index
- * @type {number}
- * @default 1
- */
- export let stayHeightIndex = 1;
- /**
- * 滑动结束时位置低于此高度自动关闭
- * close when position lower than this height
- * @type {number}
- * @range 1 - 100
- */
- export let closeHeight = 0;
- /**
- * 圆角风格
- * radius style
- * @type {'none'|'base'|'full'}
- * @default 'full'
- */
- export let radius = 'full';
- // 创建事件派发器
- // create event dispatcher
- const dispatch = createEventDispatcher();
- // 固定高度
- // stay height
- let stayHeight = stayHeightList[stayHeightIndex] || stayHeightList[stayHeightList.length - 1];
- // 此时是否正在滑动
- // is sliding or not now
- let isTouch = false;
- // 滑动开始Y坐标,px
- // start Y coordinate, px
- let startY = 0;
- // 滑动中Y方向当前位置,px
- // current Y coordinate, px
- let currentY = 0;
- // 滑动开始距离顶部高度,%
- // start distance from top, %
- let startTop = 100 - stayHeight;
- // 滑动距离,%
- // move distance, %
- let moveDistance = 0;
- // 顶部滚动区域高度,%
- // top scroll area height, %
- let scrollTopHeight = 5;
- // 顶部滚动区域元素
- // top scroll area element
- let scrollTopDom = null;
- // 当前距离顶部高度,%
- // current distance from top, %
- $: currentTop = startTop + moveDistance;
- // 如果 stayHeightList 不是数组,或者元素不是正数,或者元素不是 0-100 之间的数,或者元素不是整数,给出警告。
- // If stayHeightList is not an array, or the element is not a positive number, or the element is not a number between 0 and 100, or the element is not an integer, give a warning.
- if (!Array.isArray(stayHeightList) || stayHeightList.some(item => typeof item !== 'number' || item < 0 || item > 100 || item % 1 !== 0)) {
- console.error(
- '[STDF BottomSheet error]stayHeightList 必须是一个 0-100 之间的正整数数组。(stayHeightList must be an array of positive integers between 0 and 100)',
- );
- }
- // 如果 stayHeightList 元素不是递增的,给出警告。
- // If the elements of stayHeightList are not increasing, give a warning.
- if (stayHeightList.some((item, index) => index > 0 && item <= stayHeightList[index - 1])) {
- console.error(
- '[STDF BottomSheet error]stayHeightList 数组元素必须是升序排列。(stayHeightList array elements must be in ascending order)',
- );
- }
- // 如果 stayHeightIndex 超出 stayHeightList 长度给出警告。
- // If stayHeightIndex exceeds the length of stayHeightList, give a warning.
- if (stayHeightIndex > stayHeightList.length - 1) {
- console.warn(
- '[STDF BottomSheet warn]stayHeightIndex 超出 stayHeightList 长度,将使用 stayHeightList 最后一个值。(stayHeightIndex exceeds the length of stayHeightList, the last value of stayHeightList will be used.)',
- );
- }
- // 如果 closeHeight 大于 stayHeightList 最小值给出警告。
- // If closeHeight is greater than the minimum value of stayHeightList, give a warning.
- if (closeHeight > stayHeightList[0]) {
- console.warn(
- '[STDF BottomSheet warn]closeHeight 大于 stayHeightList 最小值,closeHeight 将失效。(closeHeight is greater than the minimum value of stayHeightList, closeHeight will be invalid.)',
- );
- }
- // 标题对齐方式
- // title align
- const titleAlignClass = {
- left: ' text-left',
- center: ' text-center',
- right: ' text-right',
- };
- // 窗口圆角风格
- // window radius style
- const windowRadiusClass = {
- none: ' rounded-none',
- base: ' rounded-t-lg',
- full: ' rounded-t-2xl',
- };
- // 图标圆角风格
- // icon radius style
- const iconRadiusClass = {
- none: ' rounded-none',
- base: ' rounded',
- full: ' rounded-full',
- };
- // 滑动开始
- // start sliding
- const touchstartFun = e => {
- moveDistance = 0;
- startTop = currentTop;
- startY = e.clientY;
- isTouch = true;
- };
- // 滑动中
- // sliding
- const touchmoveFun = e => {
- if (!isTouch) return;
- scrollTopDom.setPointerCapture(e.pointerId);
- currentY = e.clientY;
- //移动百分比,moveDistance为正时,向下移动
- //Move percentage, moveDistance is positive when moving down
- moveDistance = ((currentY - startY) / window.innerHeight) * 100;
- //处理向上滑动时超过最大高度情况
- //Handle the case when the maximum height is exceeded when sliding up
- if (moveDistance + startTop < 100 - stayHeightList[stayHeightList.length - 1]) {
- moveDistance = 100 - stayHeightList[stayHeightList.length - 1] - startTop;
- }
- };
- // 滑动结束
- // end sliding
- const touchendFun = () => {
- isTouch = false;
- //计算出每个高度对应的top值
- //Calculate the top value corresponding to each height
- let toTopList = stayHeightList.map(item => 100 - item);
- let min = 100;
- let currentIndex = 0;
- toTopList.forEach((item, index) => {
- if (Math.abs(item - currentTop) < min) {
- min = Math.abs(item - currentTop);
- currentIndex = index;
- }
- });
- currentTop = toTopList[currentIndex];
- //派发事件,传递当前高度
- //Dispatch events and pass the current
- dispatch('heightChange', stayHeightList[currentIndex]);
- //判断滑动结束时如果位置低于 closeHeight 自动关闭,并派发事件
- //If the position is lower than closeHeight at the end of the slide, it will be automatically closed and the event will be dispatched.
- if (((window.innerHeight - currentY) / window.innerHeight) * 100 < closeHeight && closeHeight > 0) {
- visible = false;
- dispatch('close');
- }
- };
- //点击遮罩层
- //click mask
- const clickMask = () => {
- //点击遮罩时派发clickMask事件
- //Dispatch clickMask event when clicking mask
- dispatch('clickMask');
- if (maskClosable) {
- visible = false;
- }
- };
- //点击关闭图标
- //click close icon
- const closeFunc = () => {
- visible = false;
- //点击关闭时派发close事件
- //Dispatch close event when clicking close
- dispatch('close');
- };
- //点击返回图标
- //click back icon
- const backFunc = () => {
- //点击返回时派发back事件
- //Dispatch back event when clicking back
- dispatch('back');
- };
- // 滚动时禁止 body 滚动
- // Disable body scrolling when scrolling
- $: {
- if (visible) {
- //当 visible 为 true 时,禁止 body 滚动
- //When visible is true, body scrolling is disabled
- const top = document.documentElement.scrollTop || document.body.scrollTop;
- document.body.style.cssText += `
- position: fixed;
- width: 100vw;
- left: 0;
- top: ${-top}px;
- touch-action:none;
- `;
- } else {
- const top = document.body.style.top;
- document.body.style.cssText += `
- position: static;
- touch-action:auto;
- `;
- window.scrollTo(0, Math.abs(parseFloat(top)));
- }
- }
- onMount(() => {
- if (visible) {
- // 滚动内容高度
- // Scroll content height
- scrollTopHeight = scrollTopDom.clientHeight;
- }
- });
- </script>
- {#if visible}
- <Mask visible {duration} {outDuration} {...mask} on:clickMask={clickMask} />
- {/if}
- <div class={`fixed w-screen h-screen inset-0 flex flex-col justify-end px-0 pointer-events-none`} style={`z-index:${zIndex};`}>
- {#if visible}
- <div
- class={`fixed w-screen bg-white dark:bg-gray-950${windowRadiusClass[radius] || windowRadiusClass['full']} pointer-events-auto${
- isTouch ? '' : ' transition-all duration-300'
- }`}
- style={`height:${stayHeightList[stayHeightList.length - 1]}%;top:${currentTop}%;`}
- in:fly={{ y: (stayHeightList[stayHeightList.length - 1] / 100) * window.innerHeight, opacity: 1, duration }}
- out:fly={{ y: (stayHeightList[stayHeightList.length - 1] / 100) * window.innerHeight, opacity: 1, duration: outDuration }}
- >
- <div
- on:pointerdown={touchstartFun}
- on:pointermove={throttle(touchmoveFun)}
- on:pointerup={touchendFun}
- bind:this={scrollTopDom}
- class="py-1 touch-none cursor-move select-none"
- >
- <div class={`w-8 h-1 bg-black/20 dark:bg-white/30 mx-auto${radius === 'none' ? ' rounded-none' : ' rounded-full'}`} />
- <div class="px-3 py-1 flex justify-between items-center gap-2">
- {#if showBackIcon}
- <!-- svelte-ignore a11y-click-events-have-key-events -->
- <!-- svelte-ignore a11y-no-static-element-interactions -->
- <div
- class={`flex-none bg-black/5 dark:bg-white/10 w-6 h-6 text-center${iconRadiusClass[radius] || iconRadiusClass['full']}`}
- on:click={backFunc}
- >
- <Icon name="ri-arrow-left-s-line" alpha={0.4} size={16} top={-2} />
- </div>
- {/if}
- <div class={`font-bold text-lg h-7 truncate grow${titleAlignClass[titleAlign] || titleAlignClass['left']}`}>
- {title}
- </div>
- {#if closeContent === ''}
- <!-- null -->
- {:else if closeContent === 'closeIcon'}
- <!-- svelte-ignore a11y-click-events-have-key-events -->
- <!-- svelte-ignore a11y-no-static-element-interactions -->
- <div
- class={`flex-none bg-black/5 dark:bg-white/10 w-6 h-6 text-center${iconRadiusClass[radius] || iconRadiusClass['full']}`}
- on:click={closeFunc}
- >
- <Icon name="ri-close-line" alpha={0.4} size={14} top={-2} />
- </div>
- {:else if closeContent === 'downIcon'}
- <!-- svelte-ignore a11y-click-events-have-key-events -->
- <!-- svelte-ignore a11y-no-static-element-interactions -->
- <div
- class={`flex-none bg-black/5 dark:bg-white/10 w-6 h-6 text-center${iconRadiusClass[radius] || iconRadiusClass['full']}`}
- on:click={closeFunc}
- >
- <Icon name="ri-arrow-down-s-line" alpha={0.4} size={16} top={-1} />
- </div>
- {:else}
- <!-- svelte-ignore a11y-click-events-have-key-events -->
- <!-- svelte-ignore a11y-no-static-element-interactions -->
- <div class="text-primary dark:text-dark font-bold cursor-pointer" on:click={closeFunc}>{closeContent}</div>
- {/if}
- </div>
- </div>
- {#if showDivider}
- <div class="w-full h-px bg-black/5 dark:bg-white/10" />
- {/if}
- <div
- class="overflow-auto"
- style="height:{window.innerHeight * ((100 - currentTop) / 100) - scrollTopHeight}px;overscroll-behavior-y: contain;"
- >
- <slot />
- </div>
- </div>
- {/if}
- </div>
|