372 lines
9.0 KiB
Vue
372 lines
9.0 KiB
Vue
<template>
|
|
<view
|
|
:style="tools._style([customStyle])"
|
|
:class="classPrefix + ' ' + tClass"
|
|
disable-scroll
|
|
>
|
|
<view
|
|
:id="'id-' + classPrefix + '__bar'"
|
|
:class="classPrefix + '__sidebar ' + tClassSidebar"
|
|
@touchmove.stop.prevent="onTouchMove"
|
|
@touchcancel.stop.prevent="onTouchCancel"
|
|
@touchend.stop.prevent="onTouchEnd"
|
|
>
|
|
<view
|
|
v-for="(item, index) in _indexList"
|
|
:key="index"
|
|
:class="tools.cls(classPrefix + '__sidebar-item', [['active', dataCurrent === item]]) + ' ' + tClassSidebarItem"
|
|
:data-index="index"
|
|
@click.stop="onClick(item, index)"
|
|
>
|
|
<view
|
|
aria-role="button"
|
|
:aria-label="dataCurrent === item ? '已选中' + item : ''"
|
|
>
|
|
{{ getFirstCharacter(item) }}
|
|
</view>
|
|
|
|
<view
|
|
v-if="showTips && dataCurrent === item"
|
|
:class="classPrefix + '__sidebar-tips'"
|
|
>
|
|
{{ dataCurrent }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<slot />
|
|
</view>
|
|
</template>
|
|
<script>
|
|
import TIcon from '../icon/icon';
|
|
import TCell from '../cell/cell';
|
|
import TCellGroup from '../cell-group/cell-group';
|
|
import { uniComponent } from '../common/src/index';
|
|
import { prefix } from '../common/config';
|
|
import props from './props';
|
|
import { getRect, throttle, systemInfo } from '../common/utils';
|
|
import pageScrollMixin from '../mixins/page-scroll';
|
|
import tools from '../common/utils.wxs';
|
|
import { getFirstCharacter } from './computed.js';
|
|
import { ParentMixin, RELATION_MAP } from '../common/relation';
|
|
|
|
|
|
const name = `${prefix}-indexes`;
|
|
|
|
|
|
export default uniComponent({
|
|
name,
|
|
options: {
|
|
styleIsolation: 'shared',
|
|
},
|
|
controlledProps: [
|
|
{
|
|
key: 'current',
|
|
event: 'change',
|
|
},
|
|
],
|
|
externalClasses: [
|
|
`${prefix}-class`,
|
|
`${prefix}-class-sidebar`,
|
|
`${prefix}-class-sidebar-item`,
|
|
],
|
|
mixins: [
|
|
pageScrollMixin(),
|
|
ParentMixin(RELATION_MAP.IndexesAnchor),
|
|
],
|
|
components: {
|
|
TIcon,
|
|
TCell,
|
|
TCellGroup,
|
|
},
|
|
props: {
|
|
...props,
|
|
},
|
|
emits: [
|
|
'change',
|
|
'select',
|
|
],
|
|
data() {
|
|
return {
|
|
prefix,
|
|
classPrefix: name,
|
|
_height: 0,
|
|
_indexList: [],
|
|
scrollTop: 0,
|
|
activeAnchor: this.current,
|
|
showTips: false,
|
|
|
|
tools,
|
|
|
|
timer: null,
|
|
groupTop: [],
|
|
sidebar: null,
|
|
currentTouchAnchor: null,
|
|
|
|
dataCurrent: this.current,
|
|
};
|
|
},
|
|
watch: {
|
|
indexList: {
|
|
handler(v, pre) {
|
|
const cb = () => {
|
|
this.setIndexList(v);
|
|
this.setHeight(this._height);
|
|
};
|
|
if (!pre?.length) {
|
|
// 防止抖音小程序报错
|
|
setTimeout(() => {
|
|
cb();
|
|
}, 33);
|
|
} else {
|
|
cb();
|
|
}
|
|
},
|
|
immediate: true,
|
|
},
|
|
height(v) {
|
|
this.setHeight(v);
|
|
},
|
|
stickyOffset() {
|
|
this.setAnchorByCurrent(this.dataCurrent, 'update', true);
|
|
},
|
|
|
|
current: {
|
|
handler(val) {
|
|
this.dataCurrent = val;
|
|
},
|
|
immediate: true,
|
|
},
|
|
dataCurrent: {
|
|
handler(e) {
|
|
if (e && this.activeAnchor && e !== this.activeAnchor) {
|
|
this.setAnchorByCurrent(e, 'update');
|
|
}
|
|
},
|
|
immediate: true,
|
|
},
|
|
},
|
|
mounted() {
|
|
this.timer = null;
|
|
this.groupTop = [];
|
|
this.sidebar = null;
|
|
|
|
if (this._height === 0) {
|
|
this.setHeight();
|
|
}
|
|
|
|
if (this.indexList === null) {
|
|
this.setIndexList();
|
|
}
|
|
},
|
|
methods: {
|
|
getFirstCharacter,
|
|
setHeight(height) {
|
|
if (!height) {
|
|
const { windowHeight } = systemInfo;
|
|
height = windowHeight;
|
|
}
|
|
this._height = height;
|
|
|
|
setTimeout(() => {
|
|
this.getAllRect();
|
|
});
|
|
},
|
|
|
|
setIndexList(list) {
|
|
if (!list) {
|
|
const start = 'A'.charCodeAt(0);
|
|
const alphabet = [];
|
|
|
|
for (let i = start, end = start + 26; i < end; i += 1) {
|
|
alphabet.push(String.fromCharCode(i));
|
|
}
|
|
|
|
this._indexList = alphabet;
|
|
} else {
|
|
this._indexList = list;
|
|
}
|
|
},
|
|
|
|
getAllRect() {
|
|
this.getAnchorsRect().then(() => {
|
|
this.groupTop.forEach((item, index) => {
|
|
const next = this.groupTop[index + 1];
|
|
item.totalHeight = (next?.top || Infinity) - item.top;
|
|
});
|
|
|
|
const current = this.dataCurrent || this._indexList[0];
|
|
this.setAnchorByCurrent(current, 'init');
|
|
})
|
|
.catch((err) => {
|
|
console.log('err', err);
|
|
});
|
|
this.getSidebarRect();
|
|
},
|
|
|
|
getAnchorsRect() {
|
|
return Promise.all((this.children || [])
|
|
.map(child => getRect(child, `.${name}-anchor`)
|
|
.then((rect) => {
|
|
this.groupTop.push({
|
|
height: rect.height,
|
|
top: rect.top,
|
|
anchor: child.index,
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
console.log('err', err);
|
|
})));
|
|
},
|
|
|
|
getSidebarRect() {
|
|
getRect(this, `#id-${name}__bar`).then((rect) => {
|
|
const { top, height } = rect;
|
|
const { length } = this._indexList;
|
|
|
|
this.sidebar = {
|
|
top,
|
|
height,
|
|
itemHeight: (height - (length - 1) * 2) / length, // margin = 2px
|
|
};
|
|
})
|
|
.catch((err) => {
|
|
console.log('err', err);
|
|
});
|
|
},
|
|
|
|
toggleTips(flag) {
|
|
if (!flag) {
|
|
clearInterval(this.timer);
|
|
this.timer = setTimeout(() => {
|
|
this.showTips = false;
|
|
}, 300);
|
|
} else {
|
|
this.showTips = true;
|
|
}
|
|
},
|
|
|
|
setAnchorByCurrent(current, source, force) {
|
|
const { stickyOffset } = this;
|
|
|
|
if (this.activeAnchor !== null && this.activeAnchor === current && !force) return;
|
|
|
|
const target = this.groupTop.find(item => item.anchor === current);
|
|
|
|
if (target) {
|
|
const scrollTop = target.top - stickyOffset;
|
|
|
|
this.setAnchorOnScroll(scrollTop);
|
|
uni.pageScrollTo({
|
|
scrollTop,
|
|
duration: 0,
|
|
});
|
|
|
|
if (['click', 'touch'].includes(source)) {
|
|
this.toggleTips(true);
|
|
this.$emit('select', { index: current });
|
|
}
|
|
}
|
|
},
|
|
|
|
onClick(current) {
|
|
this.setAnchorByCurrent(current, 'click');
|
|
},
|
|
|
|
onTouchMove(e) {
|
|
this.onAnchorTouch(e);
|
|
},
|
|
|
|
onTouchCancel() {
|
|
this.toggleTips(false);
|
|
},
|
|
|
|
onTouchEnd(e) {
|
|
this.toggleTips(false);
|
|
this.onAnchorTouch(e);
|
|
},
|
|
|
|
onAnchorTouch: throttle(function (e) {
|
|
const getAnchorIndex = (clientY) => {
|
|
const offsetY = clientY - this.sidebar.top;
|
|
const max = this._indexList.length - 1;
|
|
|
|
if (offsetY <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (offsetY > this.sidebar.height) {
|
|
return max;
|
|
}
|
|
|
|
return Math.min(max, Math.floor(offsetY / this.sidebar.itemHeight));
|
|
};
|
|
const index = getAnchorIndex(e.changedTouches[0].clientY);
|
|
|
|
this.setAnchorByCurrent(this._indexList[index], 'touch');
|
|
}, 1000 / 30), // 30 frame
|
|
|
|
setAnchorOnScroll(scrollTop) {
|
|
if (!this.groupTop) {
|
|
return;
|
|
}
|
|
|
|
const { sticky, stickyOffset } = this;
|
|
|
|
scrollTop += stickyOffset;
|
|
|
|
const curIndex = this.groupTop.findIndex(group => scrollTop >= group.top - group.height && scrollTop <= group.top + group.totalHeight - group.height);
|
|
|
|
if (curIndex === -1) return;
|
|
|
|
const curGroup = this.groupTop[curIndex];
|
|
|
|
this.activeAnchor = curGroup.anchor;
|
|
setTimeout(() => {
|
|
this._trigger('change', { index: curGroup.anchor, current: curGroup.anchor });
|
|
});
|
|
|
|
if (sticky) {
|
|
const offset = curGroup.top - scrollTop;
|
|
const betwixt = offset < curGroup.height && offset > 0 && scrollTop > stickyOffset;
|
|
|
|
this.children.forEach((child, index) => {
|
|
if (index === curIndex) {
|
|
const sticky = scrollTop > stickyOffset;
|
|
const anchorStyle = `transform: translate3d(0, ${betwixt ? offset : 0}px, 0); top: ${stickyOffset}px`;
|
|
if (anchorStyle !== child.anchorStyle || sticky !== child.sticky) {
|
|
child.sticky = sticky;
|
|
child.active = true;
|
|
child.dataStyle = `height: ${curGroup.height}px`;
|
|
child.anchorStyle = anchorStyle;
|
|
}
|
|
} else if (index + 1 === curIndex) {
|
|
// 两个 anchor 同时出现时的上一个
|
|
const anchorStyle = `transform: translate3d(0, ${
|
|
betwixt ? offset - curGroup.height : 0
|
|
}px, 0); top: ${stickyOffset}px`;
|
|
if (anchorStyle !== child.anchorStyle) {
|
|
child.sticky = true;
|
|
child.active = true;
|
|
child.dataStyle = `height: ${curGroup.height}px`;
|
|
child.anchorStyle = anchorStyle;
|
|
}
|
|
} else {
|
|
child.active = false;
|
|
child.sticky = false;
|
|
child.dataStyle = '';
|
|
child.anchorStyle = '';
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
onScroll({ scrollTop }) {
|
|
this.setAnchorOnScroll(scrollTop);
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
<style scoped>
|
|
@import './indexes.css';
|
|
</style>
|