@opensig/opendesign
Version:
287 lines (286 loc) • 9.65 kB
JavaScript
;
const vue = require("vue");
const types = require("./types.js");
const provide = require("./provide.js");
const is = require("../_utils/is.js");
const dom = require("../_utils/dom.js");
const helper = require("../_utils/helper.js");
const useResizeObserver = require("../hooks/use-resize-observer.js");
const _hoisted_1 = {
key: 0,
class: "o-anchor-line"
};
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
__name: "OAnchor",
props: types.anchorProps,
emits: ["click", "change"],
setup(__props, { emit: __emit }) {
const props = __props;
const emits = __emit;
const ANCHOR_REGX = /#([\S ]+)$/;
const anchorRef = vue.ref();
const anchorItemsRef = vue.ref();
const isScrolling = vue.ref(false);
const links = vue.ref(/* @__PURE__ */ new Set());
const activeLink = vue.ref("");
const indicatorStyle = vue.ref({});
const scrollContainer = vue.ref();
const getContainer = (container = window) => {
if (is.isString(container)) {
const dom2 = document.querySelector(container);
return dom2 ? dom2 : window;
}
return container;
};
const updateIndicatorPosition = () => {
var _a, _b;
const el = (_a = anchorRef.value) == null ? void 0 : _a.querySelector(".o-anchor-item-link.is-active");
if (!el) {
indicatorStyle.value = {};
return;
}
const { offsetTop, offsetHeight } = el;
const depth = el.getAttribute("data-depth");
indicatorStyle.value.top = `${offsetTop}px`;
indicatorStyle.value.height = `${offsetHeight}px`;
indicatorStyle.value.opacity = depth === "0" ? 0 : 1;
if (props.layout !== "h") {
return;
}
const { isOverflowLeft, isOverflowRight, overflowLeft, overflowRight } = dom.checkElementOverflowHorizontal({ element: el });
if (!isOverflowLeft && !isOverflowRight) {
return;
}
const xOverflownWidth = Number.parseInt(dom.getCssVariable("--anchor-x-overflown-width", el));
const itemGap = Number.parseInt(dom.getCssVariable("--anchor-item-gap", el));
const adjustX = xOverflownWidth + itemGap;
const toScrollLeftBy = isOverflowLeft ? -overflowLeft - adjustX : overflowRight + adjustX;
(_b = anchorItemsRef.value) == null ? void 0 : _b.scrollBy({
left: toScrollLeftBy,
behavior: "smooth"
});
};
const xOverflown = vue.ref({ left: false, right: false });
const itemsScrollLeft = vue.ref(0);
const handleScroll = helper.throttleRAF(() => {
if (props.layout !== "h" || !anchorItemsRef.value) {
return;
}
const { scrollLeft, scrollWidth, clientWidth } = anchorItemsRef.value;
itemsScrollLeft.value = scrollLeft;
xOverflown.value.left = scrollLeft > 0;
xOverflown.value.right = scrollLeft + clientWidth < scrollWidth;
});
vue.onMounted(handleScroll);
const setActiveLink = async (link) => {
if (activeLink.value === link) {
return;
}
activeLink.value = link;
emits("change", activeLink.value);
await vue.nextTick();
updateIndicatorPosition();
};
const getAnchorTarget = (link) => {
const anchorMatches = ANCHOR_REGX.exec(link);
if (!anchorMatches) {
return;
}
const target = document.getElementById(anchorMatches[1]);
return target;
};
const getOffsetTop = (el, container) => {
const { top } = el.getBoundingClientRect();
if (is.isWindow(container)) {
return top - document.documentElement.clientTop;
}
return top - container.getBoundingClientRect().top;
};
const scrollIntoView = async (link) => {
if (!is.isCurrentPageLink(link)) {
return;
}
setActiveLink(link);
const target = getAnchorTarget(link);
if (!target) {
return;
}
isScrolling.value = true;
const { scrollTop } = dom.getScroll(scrollContainer.value);
const offsetTop = getOffsetTop(target, scrollContainer.value);
const y = scrollTop + offsetTop - props.targetOffset;
await dom.scrollTo(y, {
container: scrollContainer.value
});
isScrolling.value = false;
};
const activeNearest = () => {
const distances = [];
const { targetOffset, bounds } = props;
let active = "";
links.value.forEach((link) => {
const target = getAnchorTarget(link);
if (target) {
const top = getOffsetTop(target, scrollContainer.value);
if (top < targetOffset + bounds) {
distances.push({
link,
top
});
}
}
});
if (distances.length) {
const max = distances.reduce((prev, cur) => prev.top > cur.top ? prev : cur);
active = max.link;
}
setActiveLink(active);
};
const onScroll = () => {
if (isScrolling.value) {
return;
}
activeNearest();
};
const bindEvent = () => {
if (is.isUndefined(scrollContainer.value)) {
return;
}
scrollContainer.value.addEventListener("scroll", onScroll, { passive: true });
};
const unbindEvent = () => {
if (is.isUndefined(scrollContainer.value)) {
return;
}
scrollContainer.value.removeEventListener("scroll", onScroll);
};
const addLink = (link) => {
if (!ANCHOR_REGX.test(link) || links.value.has(link)) {
return;
}
links.value.add(link);
};
const removeLink = (link) => {
links.value.add(link);
};
const onItemClick = (options) => {
const { event, link } = options;
emits("click", event, link);
};
vue.provide(provide.anchorInjectKey, {
addLink,
removeLink,
onItemClick,
activeLink,
scrollIntoView,
layout: vue.toRef(props, "layout"),
getChangeHash: () => props.changeHash
});
const onAnchorResize = helper.throttleRAF(() => {
updateIndicatorPosition();
handleScroll();
});
vue.onMounted(() => {
const ro = useResizeObserver.useResizeObserver();
scrollContainer.value = getContainer(props.container);
const hash = decodeURIComponent(window.location.hash);
if (hash) {
scrollIntoView(hash);
} else {
activeNearest();
}
vue.nextTick(() => {
bindEvent();
if (anchorRef.value) {
ro.observe(anchorRef.value, onAnchorResize);
}
});
});
vue.onUnmounted(() => {
unbindEvent();
const ro = useResizeObserver.useResizeObserver();
if (anchorRef.value) {
ro.unobserve(anchorRef.value, onAnchorResize);
}
});
const isAnchorStickying = vue.ref(false);
const anchorParent = vue.shallowRef();
const detectSticking = helper.throttleRAF(() => {
if (!anchorRef.value) {
return;
}
const elRect = anchorRef.value.getBoundingClientRect();
const containerTop = anchorParent.value ? anchorParent.value.getBoundingClientRect().top : 0;
const stickyOffset = parseFloat(window.getComputedStyle(anchorRef.value).top) || 0;
isAnchorStickying.value = elRect.top <= containerTop + stickyOffset && elRect.bottom > containerTop + stickyOffset;
});
vue.onMounted(() => {
if (props.layout !== "h") {
return;
}
anchorParent.value = dom.getScrollParents(anchorRef.value)[0];
if (anchorParent.value) {
anchorParent.value.addEventListener("scroll", detectSticking, { passive: true });
} else {
window.addEventListener("scroll", detectSticking, { passive: true });
}
});
vue.onUnmounted(() => {
if (anchorParent.value) {
anchorParent.value.removeEventListener("scroll", detectSticking);
} else {
window.removeEventListener("scroll", detectSticking);
}
});
return (_ctx, _cache) => {
return vue.openBlock(), vue.createElementBlock(
"div",
{
ref_key: "anchorRef",
ref: anchorRef,
class: vue.normalizeClass(["o-anchor", `o-anchor-${props.layout}`, `o-anchor-${props.size}`, isAnchorStickying.value && `o-anchor-stickying`])
},
[
props.layout === "v" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [
vue.createElementVNode(
"div",
{
class: "o-anchor-indicator",
style: vue.normalizeStyle(indicatorStyle.value)
},
null,
4
/* STYLE */
)
])) : vue.createCommentVNode("v-if", true),
vue.createElementVNode(
"div",
{
ref_key: "anchorItemsRef",
ref: anchorItemsRef,
class: vue.normalizeClass({
"o-anchor-items": true,
"left-overflown": xOverflown.value.left,
"right-overflown": xOverflown.value.right
}),
style: vue.normalizeStyle({
"--o-anchor-ellipsis-left": itemsScrollLeft.value,
"--o-anchor-ellipsis-right": -itemsScrollLeft.value
}),
onScroll: _cache[0] || (_cache[0] = //@ts-ignore
(...args) => vue.unref(handleScroll) && vue.unref(handleScroll)(...args))
},
[
vue.renderSlot(_ctx.$slots, "default")
],
38
/* CLASS, STYLE, NEED_HYDRATION */
)
],
2
/* CLASS */
);
};
}
});
module.exports = _sfc_main;