UNPKG

@opensig/opendesign

Version:

287 lines (286 loc) 9.65 kB
"use strict"; 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;