import { defineComponent, onMounted, reactive, ref, watch, PropType } from 'vue';
export default defineComponent({
name: 'DSticky',
props: {
zIndex: {
type: Number,
},
container: {
type: Object as PropType<Element>
},
view: {
type: Object,
default: () => { return { top: 0, bottom: 0 }; },
},
scrollTarget: {
type: Object as PropType<Element>
},
},
emits: ['statusChange'],
setup(props, ctx) {
const { slots } = ctx;
let container: Element;
let scrollTarget: Element | Window;
let scrollTimer: any;
let scrollPreStart: number | null;
const THROTTLE_DELAY = 16;
const THROTTLE_TRIGGER = 100;
let parentNode: Element;
let containerLeft = 0;
const state = reactive({
status: 'normal'
});
watch(
() => props.zIndex,
() => {
init();
}
);
watch(
() => props.container,
() => {
init();
}
);
watch(
() => props.scrollTarget,
() => {
init();
}
);
watch(
() => state.status,
() => {
ctx.emit('statusChange', state.status);
},
{ immediate: true }
);
const init = () => {
parentNode = stickyRef.value.parentElement;
if (!props.container) {
container = parentNode;
} else {
container = props.container;
}
stickyRef.value.style.zIndex = props.zIndex;
scrollTarget = props.scrollTarget || window;
scrollTarget.addEventListener('scroll', throttle);
initScrollStatus(scrollTarget);
};
const initScrollStatus = (target: any) => {
const scrollTargets = target === window ?
[document.documentElement, document.body] : [target];
let flag = false;
scrollTargets.forEach((scrollTarget) => {
if (scrollTarget.scrollTop && scrollTarget.scrollTop > 0) {
flag = true;
}
});
if (flag) {
setTimeout(scrollHandler);
}
};
const statusProcess = (status: any) => {
const wrapper = stickyRef.value || document.createElement('div');
switch (status) {
case 'normal':
wrapper.style.top = 'auto';
wrapper.style.left = 'auto';
wrapper.style.position = 'static';
break;
case 'follow':
const scrollTargetElement: any = scrollTarget;
const viewOffset = scrollTarget && scrollTarget !== window ?
scrollTargetElement.getBoundingClientRect().top : 0;
wrapper.style.top = +viewOffset + ((props.view && props.view.top) || 0) + 'px';
wrapper.style.left = wrapper.getBoundingClientRect().left + 'px';
wrapper.style.position = 'fixed';
break;
case 'stay':
wrapper.style.top = calculateRelativePosition(wrapper, parentNode, 'top') + 'px';
wrapper.style.left = 'auto';
wrapper.style.position = 'relative';
break;
case 'remain':
if (wrapper.style.position !== 'fixed' && wrapper.style.position !== 'absolute') {
wrapper.style.top = calculateRelativePosition(wrapper, parentNode, 'top') + 'px';
wrapper.style.left = 'auto';
wrapper.style.position = 'absolute';
}
wrapper.style.top =
calculateRemainPosition(wrapper, parentNode, container) + 'px';
wrapper.style.left = calculateRelativePosition(wrapper, parentNode, 'left') + 'px';
wrapper.style.position = 'relative';
break;
default:
break;
}
};
const throttle = () => {
const fn = scrollAndResizeHock;
const time = Date.now();
if (scrollTimer) {
clearTimeout(scrollTimer);
}
if (!scrollPreStart) {
scrollPreStart = time;
}
if (time - scrollPreStart > THROTTLE_TRIGGER) {
fn();
scrollPreStart = null;
scrollTimer = null;
} else {
scrollTimer = setTimeout(() => {
fn();
scrollPreStart = null;
scrollTimer = null;
}, THROTTLE_DELAY);
}
};
const scrollAndResizeHock = () => {
if (container.getBoundingClientRect().left - (containerLeft || 0) !== 0) {
state.status = 'stay';
containerLeft = container.getBoundingClientRect().left;
} else {
scrollHandler();
}
};
const scrollHandler = () => {
const scrollTargetElement: any = scrollTarget;
const wrapper = stickyRef.value || document.createElement('div');
const viewOffsetTop = scrollTarget && scrollTarget !== window ?
scrollTargetElement.getBoundingClientRect().top : 0;
const computedStyle = window.getComputedStyle(container);
if (parentNode.getBoundingClientRect().top - viewOffsetTop > ((props.view && props.view.top) || 0)) {
state.status = 'normal';
statusProcess(state.status);
} else if (
container.getBoundingClientRect().top +
parseInt(computedStyle.paddingTop, 10) +
parseInt(computedStyle.borderTopWidth, 10) -
viewOffsetTop >=
((props.view && props.view.top) || 0)
) {
state.status = 'normal';
statusProcess(state.status);
} else if (
container.getBoundingClientRect().bottom -
parseInt(computedStyle.paddingBottom, 10) -
parseInt(computedStyle.borderBottomWidth, 10) <
viewOffsetTop +
((props.view && props.view.top) || 0) +
wrapper.getBoundingClientRect().height +
((props.view && props.view.bottom) || 0)
) {
state.status = 'remain';
statusProcess(state.status);
} else if (
container.getBoundingClientRect().top + parseInt(computedStyle.paddingTop, 10) - viewOffsetTop <
((props.view && props.view.top) || 0)
) {
state.status = 'follow';
statusProcess(state.status);
}
};
const calculateRelativePosition = (element: any, relativeElement: any, direction: 'left' | 'top') => {
const key = {
left: ['left', 'Left'],
top: ['top', 'Top'],
};
if (window && window.getComputedStyle) {
const computedStyle = window.getComputedStyle(relativeElement);
return (
element.getBoundingClientRect()[key[direction][0]] -
relativeElement.getBoundingClientRect()[key[direction][0]] -
parseInt(computedStyle[direction === 'left' ? 'paddingLeft' : 'paddingTop'], 10) -
parseInt(computedStyle[direction === 'left' ? 'borderLeftWidth' : 'borderTopWidth'], 10)
);
}
};
const calculateRemainPosition = (element: any, relativeElement: any, container: any) => {
if (window && window.getComputedStyle) {
const computedStyle = window.getComputedStyle(container);
const result =
container.getBoundingClientRect().height -
element.getBoundingClientRect().height +
container.getBoundingClientRect().top -
relativeElement.getBoundingClientRect().top -
parseInt(computedStyle['paddingTop'], 10) -
parseInt(computedStyle['borderTopWidth'], 10) -
parseInt(computedStyle['paddingBottom'], 10) -
parseInt(computedStyle['borderBottomWidth'], 10);
return result < 0 ? 0 : result;
}
};
onMounted(() => {
init();
});
const stickyRef = ref();
return () => {
return (
<div ref={stickyRef}>
{slots.default ? slots.default() : ''}
</div>
);
};
}
});