feat: add SeamlessScroll component

This commit is contained in:
xiaoxian521 2021-04-15 16:16:55 +08:00
parent db237d2f51
commit e09ed0fb47
7 changed files with 683 additions and 22 deletions

View File

@ -1,15 +1,545 @@
<template>
<div></div>
<div ref="wrap">
<div :style="leftSwitch" v-if="navigation" :class="leftSwitchClass" @click="leftSwitchClick">
<slot name="left-switch"></slot>
</div>
<div :style="rightSwitch" v-if="navigation" :class="rightSwitchClass" @click="rightSwitchClick">
<slot name="right-switch"></slot>
</div>
<div
ref="realBox"
:style="pos"
@mouseenter="enter"
@mouseleave="leave"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
>
<div ref="slotList" :style="float">
<slot></slot>
</div>
<div v-html="copyHtml" :style="float"></div>
</div>
</div>
</template>
<script lang='ts'>
export default {
name: "SeamlessScroll",
setup() {
return {};
}
};
</script>
import {
defineComponent,
computed,
ref,
unref,
watchEffect,
nextTick,
} from "vue";
import { tryOnMounted, tryOnUnmounted, templateRef } from "@vueuse/core";
import * as utilsMethods from "./utils";
const { animationFrame, copyObj } = utilsMethods;
animationFrame();
<style scoped>
</style>
// moveanimationFrame
let reqFrame = null;
let startPos = null;
// single
let singleWaitTime = null;
//touchStartposY
let startPosY = null;
//touchStartposX
let startPosX = null;
// mouseenter mouseleave scrollMove()
let isHover = false;
let ease = "ease-in";
export default defineComponent({
name: "SeamlessScroll",
props: {
data: {
type: Array,
default: () => {
return [];
},
},
classOption: {
type: Object,
default: () => {
return {};
},
},
},
emits: ["ScrollEnd"],
setup(props, { emit }) {
let xPos = ref(0);
let yPos = ref(0);
let delay = ref(0);
let copyHtml = ref("");
let height = ref(0);
//
let width = ref(0);
//
let realBoxWidth = ref(0);
let realBoxHeight = ref(0);
const wrap = templateRef<HTMLElement | null>("wrap", null);
const slotList = templateRef<HTMLElement | null>("slotList", null);
const realBox = templateRef<HTMLElement | null>("realBox", null);
let { data, classOption } = props;
let leftSwitchState = computed(() => {
return unref(xPos) < 0;
});
let rightSwitchState = computed(() => {
return Math.abs(unref(xPos)) < unref(realBoxWidth) - unref(width);
});
let defaultOption = computed(() => {
return {
//
step: 1,
//
limitMoveNum: 5,
//hover
hoverStop: true,
// bottom top () left right
direction: "top",
//touch
openTouch: true,
//hoverStop
singleHeight: 0,
//hoverStop
singleWidth: 0,
//
waitTime: 1000,
switchOffset: 30,
autoPlay: true,
navigation: false,
switchSingleStep: 134,
switchDelay: 400,
switchDisabledClass: "disabled",
// singleWidth/singleHeight rem
isSingleRemUnit: false,
};
});
let options = computed(() => {
// @ts-ignore
return copyObj({}, unref(defaultOption), classOption);
});
let leftSwitchClass = computed(() => {
return unref(leftSwitchState) ? "" : unref(options).switchDisabledClass;
});
let rightSwitchClass = computed(() => {
return unref(rightSwitchState) ? "" : unref(options).switchDisabledClass;
});
let leftSwitch = computed(() => {
return {
position: "absolute",
margin: `${unref(height) / 2}px 0 0 -${unref(options).switchOffset}px`,
transform: "translate(-100%,-50%)",
};
});
let rightSwitch = computed(() => {
return {
position: "absolute",
margin: `${unref(height) / 2}px 0 0 ${
unref(width) + unref(options).switchOffset
}px`,
transform: "translateY(-50%)",
};
});
let isHorizontal = computed(() => {
return (
unref(options).direction !== "bottom" &&
unref(options).direction !== "top"
);
});
let float = computed(() => {
return unref(isHorizontal)
? { float: "left", overflow: "hidden" }
: { overflow: "hidden" };
});
let pos = computed(() => {
return {
transform: `translate(${unref(xPos)}px,${unref(yPos)}px)`,
transition: `all ${ease} ${unref(delay)}ms`,
overflow: "hidden",
};
});
let navigation = computed(() => {
return unref(options).navigation;
});
let autoPlay = computed(() => {
if (unref(navigation)) return false;
return unref(options).autoPlay;
});
let scrollSwitch = computed(() => {
return data.length >= unref(options).limitMoveNum;
});
let hoverStopSwitch = computed(() => {
return unref(options).hoverStop && unref(autoPlay) && unref(scrollSwitch);
});
let canTouchScroll = computed(() => {
return unref(options).openTouch;
});
let baseFontSize = computed(() => {
return unref(options).isSingleRemUnit
? parseInt(
window.getComputedStyle(document.documentElement, null).fontSize
)
: 1;
});
let realSingleStopWidth = computed(() => {
return unref(options).singleWidth * unref(baseFontSize);
});
let realSingleStopHeight = computed(() => {
return unref(options).singleHeight * unref(baseFontSize);
});
let step = computed(() => {
let singleStep;
let step = unref(options).step;
if (unref(isHorizontal)) {
singleStep = unref(realSingleStopWidth);
} else {
singleStep = unref(realSingleStopHeight);
}
if (singleStep > 0 && singleStep % step > 0) {
throw "如果设置了单步滚动step需是单步大小的约数否则无法保证单步滚动结束的位置是否准确";
}
return step;
});
function reset() {
scrollCancle();
scrollInitMove();
}
function leftSwitchClick() {
if (!unref(leftSwitchState)) return;
//
if (Math.abs(unref(xPos)) < unref(options).switchSingleStep) {
xPos.value = 0;
return;
}
xPos.value += unref(options).switchSingleStep;
}
function rightSwitchClick() {
if (!unref(rightSwitchState)) return;
//
if (
unref(realBoxWidth) - unref(width) + unref(xPos) <
unref(options).switchSingleStep
) {
xPos.value = unref(width) - unref(realBoxWidth);
return;
}
xPos.value -= unref(options).switchSingleStep;
}
function scrollCancle() {
cancelAnimationFrame(reqFrame || "");
}
function touchStart(e) {
if (!unref(canTouchScroll)) return;
let timer;
//touchestouchtouch
const touch = e.targetTouches[0];
const { waitTime, singleHeight, singleWidth } = unref(options);
//touch
startPos = {
x: touch.pageX,
y: touch.pageY,
};
//touchStartposY
startPosY = unref(yPos);
//touchStartposX
startPosX = unref(xPos);
if (!!singleHeight && !!singleWidth) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
scrollCancle();
}, waitTime + 20);
} else {
scrollCancle();
}
}
function touchMove(e) {
//touchmove
if (
!unref(canTouchScroll) ||
e.targetTouches.length > 1 ||
(e.scale && e.scale !== 1)
)
return;
const touch = e.targetTouches[0];
const { direction } = unref(options);
let endPos = {
x: touch.pageX - startPos.x,
y: touch.pageY - startPos.y,
};
//
e.preventDefault();
//dir10
const dir = Math.abs(endPos.x) < Math.abs(endPos.y) ? 1 : 0;
if (
(dir === 1 && direction === "bottom") ||
(dir === 1 && direction === "top")
) {
// &&
yPos.value = startPosY + endPos.y;
} else if (
(dir === 0 && direction === "left") ||
(dir === 0 && direction === "right")
) {
// &&
xPos.value = startPosX + endPos.x;
}
}
function touchEnd() {
if (!unref(canTouchScroll)) return;
let timer;
const direction = unref(options).direction;
delay.value = 50;
if (direction === "top") {
if (unref(yPos) > 0) yPos.value = 0;
} else if (direction === "bottom") {
let h = (unref(realBoxHeight) / 2) * -1;
if (unref(yPos) < h) yPos.value = h;
} else if (direction === "left") {
if (unref(xPos) > 0) xPos.value = 0;
} else if (direction === "right") {
let w = unref(realBoxWidth) * -1;
if (unref(xPos) < w) xPos.value = w;
}
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
delay.value = 0;
scrollMove();
}, unref(delay));
}
function enter() {
if (unref(hoverStopSwitch)) scrollStopMove();
}
function leave() {
if (unref(hoverStopSwitch)) scrollStartMove();
}
function scrollMove() {
// scrollMove()
if (isHover) return;
//move touchMove
scrollCancle();
reqFrame = requestAnimationFrame(function () {
//
const h = unref(realBoxHeight) / 2;
//
const w = unref(realBoxWidth) / 2;
let { direction, waitTime } = unref(options);
if (direction === "top") {
//
if (Math.abs(unref(yPos)) >= h) {
emit("ScrollEnd");
yPos.value = 0;
}
yPos.value -= step.value;
} else if (direction === "bottom") {
//
if (unref(yPos) >= 0) {
emit("ScrollEnd");
yPos.value = h * -1;
}
yPos.value += step.value;
} else if (direction === "left") {
//
if (Math.abs(unref(xPos)) >= w) {
emit("ScrollEnd");
xPos.value = 0;
}
xPos.value -= step.value;
} else if (direction === "right") {
//
if (unref(xPos) >= 0) {
emit("ScrollEnd");
xPos.value = w * -1;
}
xPos.value += step.value;
}
if (singleWaitTime) clearTimeout(singleWaitTime);
if (!!unref(realSingleStopHeight)) {
//
if (
Math.abs(unref(yPos)) % unref(realSingleStopHeight) <
unref(step)
) {
// waitTime
singleWaitTime = setTimeout(() => {
scrollMove();
}, waitTime);
} else {
scrollMove();
}
} else if (!!unref(realSingleStopWidth)) {
if (
Math.abs(unref(xPos)) % unref(realSingleStopWidth) <
unref(step)
) {
// waitTime
singleWaitTime = setTimeout(() => {
scrollMove();
}, waitTime);
} else {
scrollMove();
}
} else {
scrollMove();
}
});
}
function scrollInitMove() {
nextTick(() => {
const { switchDelay } = unref(options);
//copy
copyHtml.value = "";
if (unref(isHorizontal)) {
height.value = unref(wrap).offsetHeight;
width.value = unref(wrap).offsetWidth;
let slotListWidth = unref(slotList).offsetWidth;
// warp width
if (unref(autoPlay)) {
// offsetWidth
slotListWidth = slotListWidth * 2 + 1;
}
unref(realBox).style.width = slotListWidth + "px";
realBoxWidth.value = slotListWidth;
}
if (unref(autoPlay)) {
ease = "ease-in";
delay.value = 0;
} else {
ease = "linear";
delay.value = switchDelay;
return;
}
//
if (unref(scrollSwitch)) {
let timer;
if (timer) clearTimeout(timer);
copyHtml.value = unref(slotList).innerHTML;
setTimeout(() => {
realBoxHeight.value = unref(realBox).offsetHeight;
scrollMove();
}, 0);
} else {
scrollCancle();
yPos.value = xPos.value = 0;
}
});
}
function scrollStartMove() {
//scrollMove
isHover = false;
scrollMove();
}
function scrollStopMove() {
//scrollMove
isHover = true;
// hover,
if (singleWaitTime) clearTimeout(singleWaitTime);
scrollCancle();
}
watchEffect(() => {
const watchData = data;
if (!watchData) return;
nextTick(() => {
reset();
});
const watchAutoPlay = unref(autoPlay);
if (watchAutoPlay) {
reset();
} else {
scrollStopMove();
}
});
tryOnMounted(() => {
scrollInitMove();
});
tryOnUnmounted(() => {
scrollCancle();
clearTimeout(singleWaitTime);
});
return {
xPos,
yPos,
delay,
copyHtml,
height,
width,
realBoxWidth,
leftSwitchState,
rightSwitchState,
defaultOption,
options,
leftSwitchClass,
rightSwitchClass,
leftSwitch,
rightSwitch,
isHorizontal,
float,
pos,
navigation,
autoPlay,
scrollSwitch,
hoverStopSwitch,
canTouchScroll,
baseFontSize,
realSingleStopWidth,
realSingleStopHeight,
step,
reset,
leftSwitchClick,
rightSwitchClick,
scrollCancle,
touchStart,
touchMove,
touchEnd,
enter,
leave,
scrollMove,
scrollInitMove,
scrollStartMove,
scrollStopMove,
};
},
});
</script>

View File

@ -1,7 +1,7 @@
/**
* @desc AnimationFrame简单兼容hack
*/
const animationFrame = () => {
export const animationFrame = () => {
window.cancelAnimationFrame = (function () {
return window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
@ -31,7 +31,7 @@ const animationFrame = () => {
* @param {arr1,arr2}
* @return {Boolean}
*/
const arrayEqual = (arr1: Array<any>, arr2: Array<any>) => {
export const arrayEqual = (arr1: Array<any>, arr2: Array<any>) => {
if (arr1 === arr2) return true
if (arr1.length !== arr2.length) return false
for (let i = 0; i < arr1.length; ++i) {
@ -43,7 +43,7 @@ const arrayEqual = (arr1: Array<any>, arr2: Array<any>) => {
/**
* @desc
*/
function copyObj() {
export function copyObj() {
if (!Array.isArray) {
// @ts-expect-error
Array.isArray = function (arg) {
@ -104,11 +104,5 @@ function copyObj() {
return target
}
export default {
animationFrame,
arrayEqual,
copyObj
}

View File

@ -18,5 +18,6 @@
"cropping": "图片裁剪",
"countTo": "数字动画",
"selector": "选择器组件",
"flowChart": "流程图"
"flowChart": "流程图",
"seamless": "无缝滚动"
}

View File

@ -18,5 +18,6 @@
"cropping": "Picture Cropping",
"countTo": "Digital Animation",
"selector": "Selector Components",
"flowChart": "flow Chart"
"flowChart": "flow Chart",
"seamless": "Seamless Scroll"
}

View File

@ -104,6 +104,15 @@ const routes: Array<RouteRecordRaw> = [
savedPosition: true
}
},
{
path: '/components/seamlessScroll',
component: () => import(/* webpackChunkName: "components" */ '../views/components/seamless-scroll/index.vue'),
meta: {
title: 'seamless',
showLink: false,
savedPosition: true
}
}
// {
// path: '/components/flowChart',
// component: () => import(/* webpackChunkName: "components" */ '../views/components/flow-chart/index.vue'),

View File

@ -1,7 +1,7 @@
<template>
<div style="margin: 10px">
<div class="cropper-container">
<Cropper ref="refCropper" :width="'45vw'" :src="img" />
<Cropper ref="refCropper" :width="'40vw'" :src="img" />
<img :src="cropperImg" class="croppered" v-if="cropperImg" />
</div>
<el-button type="primary" @click="onCropper">裁剪</el-button>

View File

@ -0,0 +1,126 @@
<template>
<el-space wrap>
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>无缝滚动示例</span>
<el-button class="button" type="text" @click="changeDirection('top')">向上滚动</el-button>
<el-button class="button" type="text" @click="changeDirection('bottom')">向下滚动</el-button>
<el-button class="button" type="text" @click="changeDirection('left')">向左滚动</el-button>
<el-button class="button" type="text" @click="changeDirection('right')">向右滚动</el-button>
</div>
</template>
<SeamlessScroll ref="scroll" :data="listData" :class-option="classOption" class="warp">
<ul class="item">
<li v-for="(item, index) in listData" :key="index">
<span class="title" v-text="item.title"></span>
<span class="date" v-text="item.date"></span>
</li>
</ul>
</SeamlessScroll>
</el-card>
</el-space>
</template>
<script lang='ts'>
import { ref, unref } from "vue";
import { templateRef } from "@vueuse/core";
import SeamlessScroll from "/@/components/SeamlessScroll";
export default {
components: {
SeamlessScroll,
},
setup() {
const scroll = templateRef<HTMLElement | null>("scroll", null);
let listData = ref([
{
title: "无缝滚动第一行无缝滚动第一行",
date: "2021-5-1",
},
{
title: "无缝滚动第二行无缝滚动第二行",
date: "2021-5-1",
},
{
title: "无缝滚动第三行无缝滚动第三行",
date: "2021-5-1",
},
{
title: "无缝滚动第四行无缝滚动第四行",
date: "2021-5-1",
},
{
title: "无缝滚动第五行无缝滚动第五行",
date: "2021-5-1",
},
{
title: "无缝滚动第六行无缝滚动第六行",
date: "2021-5-1",
},
{
title: "无缝滚动第七行无缝滚动第七行",
date: "2021-5-1",
},
{
title: "无缝滚动第八行无缝滚动第八行",
date: "2021-5-1",
},
{
title: "无缝滚动第九行无缝滚动第九行",
date: "2021-5-1",
},
]);
let classOption = ref({
direction: "top",
});
function changeDirection(val) {
scroll.value.scrollInitMove();
classOption.value.direction = val;
}
return {
listData,
classOption,
changeDirection,
};
},
};
</script>
<style lang="scss" scoped>
.box-card {
margin: 10px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
margin-right: 20px;
}
}
.warp {
height: 270px;
width: 360px;
margin: 0 auto;
overflow: hidden;
ul {
list-style: none;
padding: 0;
margin: 0 auto;
li,
a {
display: block;
height: 30px;
line-height: 30px;
display: flex;
justify-content: space-between;
font-size: 15px;
}
}
}
</style>