refactor(NoticeBar): refactor with composition api

This commit is contained in:
chenjiahan 2020-08-26 16:17:05 +08:00
parent 2ec101f07b
commit eb0cc05b12

View File

@ -1,5 +1,7 @@
import { ref, reactive, nextTick, onActivated, watch } from 'vue';
import { createNamespace, isDef } from '../utils'; import { createNamespace, isDef } from '../utils';
import { doubleRaf } from '../utils/dom/raf'; import { doubleRaf } from '../utils/dom/raf';
import { useWidth } from '../composition/use-rect';
import Icon from '../icon'; import Icon from '../icon';
const [createComponent, bem] = createNamespace('notice-bar'); const [createComponent, bem] = createNamespace('notice-bar');
@ -28,158 +30,149 @@ export default createComponent({
emits: ['close', 'replay'], emits: ['close', 'replay'],
data() { setup(props, { emit, slots }) {
return { let wrapWidth = 0;
let contentWidth = 0;
const wrapRef = ref();
const contentRef = ref();
const state = reactive({
show: true, show: true,
offset: 0, offset: 0,
duration: 0, duration: 0,
wrapWidth: 0,
contentWidth: 0,
};
},
watch: {
scrollable() {
this.start();
},
text: {
handler() {
this.start();
},
immediate: true,
},
},
activated() {
this.start();
},
methods: {
onClickIcon(event) {
if (this.mode === 'closeable') {
this.show = false;
this.$emit('close', event);
}
},
onTransitionEnd() {
this.offset = this.wrapWidth;
this.duration = 0;
// wait for Vue to render offset
this.$nextTick(() => {
// use double raf to ensure animation can start
doubleRaf(() => {
this.offset = -this.contentWidth;
this.duration = (this.contentWidth + this.wrapWidth) / this.speed;
this.$emit('replay');
}); });
});
},
reset() { const renderLeftIcon = () => {
this.offset = 0;
this.duration = 0;
this.wrapWidth = 0;
this.contentWidth = 0;
},
start() {
const delay = isDef(this.delay) ? this.delay * 1000 : 0;
this.reset();
setTimeout(() => {
const { wrap, content } = this.$refs;
if (!wrap || !content || this.scrollable === false) {
return;
}
const wrapWidth = wrap.getBoundingClientRect().width;
const contentWidth = content.getBoundingClientRect().width;
if (this.scrollable || contentWidth > wrapWidth) {
doubleRaf(() => {
this.offset = -contentWidth;
this.duration = contentWidth / this.speed;
this.wrapWidth = wrapWidth;
this.contentWidth = contentWidth;
});
}
}, delay);
},
},
render() {
const slots = this.$slots;
const { mode, leftIcon, onClickIcon } = this;
const barStyle = {
color: this.color,
background: this.background,
};
const contentStyle = {
transform: this.offset ? `translateX(${this.offset}px)` : '',
transitionDuration: this.duration + 's',
};
function LeftIcon() {
if (slots['left-icon']) { if (slots['left-icon']) {
return slots['left-icon'](); return slots['left-icon']();
} }
if (props.leftIcon) {
if (leftIcon) { return <Icon class={bem('left-icon')} name={props.leftIcon} />;
return <Icon class={bem('left-icon')} name={leftIcon} />;
}
} }
};
function RightIcon() { const getRightIconName = () => {
if (props.mode === 'closeable') {
return 'cross';
}
if (props.mode === 'link') {
return 'arrow';
}
};
const onClickRightIcon = (event) => {
if (props.mode === 'closeable') {
state.show = false;
emit('close', event);
}
};
const renderRightIcon = () => {
if (slots['right-icon']) { if (slots['right-icon']) {
return slots['right-icon'](); return slots['right-icon']();
} }
let iconName; const name = getRightIconName();
if (mode === 'closeable') { if (name) {
iconName = 'cross';
} else if (mode === 'link') {
iconName = 'arrow';
}
if (iconName) {
return ( return (
<Icon <Icon
name={name}
class={bem('right-icon')} class={bem('right-icon')}
name={iconName} onClick={onClickRightIcon}
onClick={onClickIcon}
/> />
); );
} }
};
const onTransitionEnd = () => {
state.offset = wrapWidth;
state.duration = 0;
// wait for Vue to render offset
nextTick(() => {
// use double raf to ensure animation can start
doubleRaf(() => {
state.offset = -contentWidth;
state.duration = (contentWidth + wrapWidth) / props.speed;
emit('replay');
});
});
};
const renderMarquee = () => {
const ellipsis = props.scrollable === false && !props.wrapable;
const style = {
transform: state.offset ? `translateX(${state.offset}px)` : '',
transitionDuration: `${state.duration}s`,
};
return (
<div ref={wrapRef} role="marquee" class={bem('wrap')}>
<div
ref={contentRef}
style={style}
class={[bem('content'), { 'van-ellipsis': ellipsis }]}
onTransitionend={onTransitionEnd}
>
{slots.default ? slots.default() : props.text}
</div>
</div>
);
};
const reset = () => {
wrapWidth = 0;
contentWidth = 0;
state.offset = 0;
state.duration = 0;
};
const start = () => {
const { delay, speed, scrollable } = props;
const ms = isDef(delay) ? delay * 1000 : 0;
reset();
setTimeout(() => {
if (!wrapRef.value || !contentRef.value || scrollable === false) {
return;
} }
const wrapRefWidth = useWidth(wrapRef);
const contentRefWidth = useWidth(contentRef);
if (scrollable || contentRefWidth > wrapRefWidth) {
doubleRaf(() => {
wrapWidth = wrapRefWidth;
contentWidth = contentRefWidth;
state.offset = -contentWidth;
state.duration = contentWidth / speed;
});
}
}, ms);
};
onActivated(start);
watch([() => props.text, () => props.scrollable], start, {
immediate: true,
});
return () => {
const { color, wrapable, background } = props;
return ( return (
<div <div
role="alert" role="alert"
vShow={this.show} vShow={state.show}
class={bem({ wrapable: this.wrapable })} class={bem({ wrapable })}
style={barStyle} style={{ color, background }}
> >
{LeftIcon()} {renderLeftIcon()}
<div ref="wrap" class={bem('wrap')} role="marquee"> {renderMarquee()}
<div {renderRightIcon()}
ref="content"
class={[
bem('content'),
{ 'van-ellipsis': this.scrollable === false && !this.wrapable },
]}
style={contentStyle}
onTransitionend={this.onTransitionEnd}
>
{slots.default?.() || this.text}
</div>
</div>
{RightIcon()}
</div> </div>
); );
};
}, },
}); });