hongluan-ui
Version:
Hongluan Component Library for Vue 3
225 lines (222 loc) • 6.71 kB
JavaScript
import { defineComponent, ref, reactive, nextTick, onMounted, onBeforeUnmount, watch, provide, computed, openBlock, createElementBlock, normalizeClass, createCommentVNode, createElementVNode, renderSlot } from 'vue';
import { isString } from '@vue/shared';
import computeScrollIntoView from 'compute-scroll-into-view';
import '../../../utils/index.mjs';
import '../../../hooks/index.mjs';
import { findNode, slide } from './utils.mjs';
import { anchorInjectionKey } from './types.mjs';
import _export_sfc from '../../../_virtual/plugin-vue_export-helper.mjs';
import { isNumber } from '../../../utils/types.mjs';
import { useNamespace } from '../../../hooks/use-namespace/index.mjs';
import { throttleByRaf } from '../../../utils/raf.mjs';
const BOUNDARY_POSITIONS = ["start", "end", "center", "nearest"];
const _sfc_main = defineComponent({
name: "Anchor",
props: {
boundary: {
type: [Number, String],
default: "start",
validator: (value) => {
return isNumber(value) || BOUNDARY_POSITIONS.includes(value);
}
},
sliderLess: {
type: Boolean,
default: false
},
scrollContainer: {
type: [String, Object]
},
changeHash: {
type: Boolean,
default: true
},
smooth: {
type: Boolean,
default: true
},
right: Boolean
},
emits: [
"select",
"change"
],
setup(props, { emit }) {
const { namespace } = useNamespace("anchor");
const anchorRef = ref(null);
const lineSliderRef = ref(null);
const links = reactive({});
const currentLink = ref("");
const isScrolling = ref(false);
const addLink = (hash, node) => {
if (!hash)
return;
links[hash] = node;
};
const removeLink = (hash) => {
delete links[hash];
};
const getContainer = (targetContainer) => {
let scrollContainer = targetContainer;
if (!scrollContainer) {
scrollContainer = props.scrollContainer;
}
if (isString(scrollContainer)) {
return findNode(document, scrollContainer);
}
return scrollContainer || window;
};
const getContainerElement = () => {
var _a;
if (isString(props.scrollContainer)) {
return findNode(document, props.scrollContainer);
}
if (props.scrollContainer && props.scrollContainer !== window) {
return props.scrollContainer;
}
return (_a = document.documentElement) != null ? _a : document.body;
};
const handleClick = (e, hash) => {
if (!props.changeHash) {
e.preventDefault();
}
handleAnchorChange(hash);
scrollIntoView(hash);
emit("select", hash, currentLink.value);
};
const scrollIntoView = (hash) => {
if (!hash)
return;
try {
const node = findNode(document, hash);
if (!node)
return;
let block = props.boundary;
let diff = 0;
if (isNumber(props.boundary)) {
block = "start";
diff = props.boundary;
}
const actions = computeScrollIntoView(node, { block });
if (!actions.length)
return;
const { el, top } = actions[0];
const targetTop = top - diff;
slide(el, targetTop, () => {
isScrolling.value = false;
});
isScrolling.value = true;
} catch (e) {
console.error(e);
}
};
const handleScroll = throttleByRaf(() => {
if (isScrolling.value)
return;
const element = getFirstInViewportEle();
if (element && element.id) {
const hash = `#${element.id}`;
handleAnchorChange(hash);
}
});
const handleAnchorChange = (hash) => {
if (!hash)
return;
if (!links[hash] && anchorRef.value) {
const node = findNode(anchorRef.value, `a[data-href='${hash}']`);
if (!node)
return;
links[hash] = node;
}
if (hash !== currentLink.value) {
currentLink.value = hash;
nextTick(() => {
emit("change", hash);
});
}
};
const getFirstInViewportEle = () => {
const boundary = isNumber(props.boundary) ? props.boundary : 0;
const container = getContainerElement();
const containerRect = container.getBoundingClientRect();
const { clientHeight } = document.documentElement;
const ele = getContainer();
for (const hash of Object.keys(links)) {
const node = findNode(document, hash);
if (node) {
const { top } = node.getBoundingClientRect();
if (ele === window) {
const offsetTop = top - boundary;
if (offsetTop >= 0 && offsetTop <= clientHeight / 2) {
return node;
}
} else {
const offsetTop = top - containerRect.top - boundary;
if (offsetTop >= 0 && offsetTop <= containerRect.height / 2) {
return node;
}
}
}
}
return void 0;
};
onMounted(() => {
handleScroll();
bindScrollEvent();
});
onBeforeUnmount(() => {
unbindScrollEvent();
});
watch(currentLink, () => {
const link = links[currentLink.value];
if (!props.sliderLess && link && lineSliderRef.value) {
lineSliderRef.value.style.top = `${link.offsetTop}px`;
}
});
const bindScrollEvent = (scrollContainer) => {
getContainer(scrollContainer).addEventListener("scroll", handleScroll);
};
const unbindScrollEvent = (scrollContainer) => {
getContainer(scrollContainer).removeEventListener("scroll", handleScroll);
};
provide(anchorInjectionKey, reactive({
name: "Anchor",
currentLink,
addLink,
removeLink,
handleClick
}));
const cls = computed(() => [
namespace.value,
{
"slider-less": props.sliderLess,
"right": props.right
}
]);
return {
cls,
anchorRef,
lineSliderRef
};
}
});
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return openBlock(), createElementBlock("div", {
ref: "anchorRef",
class: normalizeClass(_ctx.cls)
}, [
!_ctx.sliderLess ? (openBlock(), createElementBlock("div", {
key: 0,
ref: "lineSliderRef",
class: normalizeClass(`slider-bar`)
}, null, 512)) : createCommentVNode("v-if", true),
createElementVNode("div", {
class: normalizeClass(`anchor-list`)
}, [
renderSlot(_ctx.$slots, "default")
])
], 2);
}
var Anchor = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
export { Anchor as default };
//# sourceMappingURL=anchor.mjs.map