naive-ui
Version:
A Vue 3 Component Library. Fairly Complete, Theme Customizable, Uses TypeScript, Fast
282 lines • 8.07 kB
JavaScript
import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, provide, ref, toRef, watch } from 'vue';
import { throttle } from 'lodash-es';
import { unwrapElement } from 'seemly';
import { onFontsReady } from 'vooks';
import { NScrollbar } from "../../_internal/index.mjs";
import { keysOf } from "../../_utils/index.mjs";
import { anchorInjectionKey } from "./Link.mjs";
import { getOffset } from "./utils.mjs";
export const baseAnchorProps = {
type: {
type: String,
default: 'rail'
},
showRail: {
type: Boolean,
default: true
},
showBackground: {
type: Boolean,
default: true
},
bound: {
type: Number,
default: 12
},
internalScrollable: Boolean,
ignoreGap: Boolean,
offsetTarget: [String, Object, Function]
};
export const baseAnchorPropKeys = keysOf(baseAnchorProps);
export default defineComponent({
name: 'BaseAnchor',
props: Object.assign(Object.assign({}, baseAnchorProps), {
mergedClsPrefix: {
type: String,
required: true
}
}),
setup(props) {
const collectedLinkHrefs = [];
const titleEls = [];
const activeHrefRef = ref(null);
const slotRef = ref(null);
const barRef = ref(null);
const selfRef = ref(null);
const isBlockTypeRef = computed(() => {
return props.type === 'block';
});
const mergedShowRailRef = computed(() => {
return !isBlockTypeRef.value && props.showRail;
});
function disableTransitionOneTick() {
const {
value: barEl
} = barRef;
const {
value: slotEl
} = slotRef;
if (barEl) {
barEl.style.transition = 'none';
}
if (slotEl) {
slotEl.style.transition = 'none';
}
if (titleEls) {
titleEls.forEach(titleEl => {
titleEl.style.transition = 'none';
});
}
void nextTick(() => {
const {
value: nextBarEl
} = barRef;
const {
value: nextSlotEl
} = slotRef;
if (nextBarEl) {
void nextBarEl.offsetWidth;
nextBarEl.style.transition = '';
}
if (nextSlotEl) {
void nextSlotEl.offsetWidth;
nextSlotEl.style.transition = '';
}
if (titleEls) {
titleEls.forEach(titleEl => {
void titleEl.offsetWidth;
titleEl.style.transition = '';
});
}
});
}
function updateBarPosition(linkTitleEl, transition = true) {
const {
value: barEl
} = barRef;
const {
value: slotEl
} = slotRef;
const {
value: selfEl
} = selfRef;
if (!selfEl || !barEl) return;
if (!transition) {
barEl.style.transition = 'none';
if (slotEl) slotEl.style.transition = 'none';
}
const {
offsetHeight,
offsetWidth
} = linkTitleEl;
const {
top: linkTitleClientTop,
left: linkTitleClientLeft
} = linkTitleEl.getBoundingClientRect();
const {
top: anchorClientTop,
left: anchorClientLeft
} = selfEl.getBoundingClientRect();
const offsetTop = linkTitleClientTop - anchorClientTop;
const offsetLeft = linkTitleClientLeft - anchorClientLeft;
barEl.style.top = `${offsetTop}px`;
barEl.style.height = `${offsetHeight}px`;
if (slotEl) {
slotEl.style.top = `${offsetTop}px`;
slotEl.style.height = `${offsetHeight}px`;
slotEl.style.maxWidth = `${offsetWidth + offsetLeft}px`;
}
void barEl.offsetHeight;
if (slotEl) void slotEl.offsetHeight;
if (!transition) {
barEl.style.transition = '';
if (slotEl) slotEl.style.transition = '';
}
}
const handleScroll = throttle(() => {
_handleScroll(true);
}, 128);
function setActiveHref(href, transition = true) {
const idMatchResult = /^#([^#]+)$/.exec(href);
if (!idMatchResult) return;
const linkEl = document.getElementById(idMatchResult[1]);
if (!linkEl) return;
activeHrefRef.value = href;
linkEl.scrollIntoView();
if (!transition) {
disableTransitionOneTick();
}
handleScroll();
}
function _handleScroll(transition = true) {
var _a;
const links = [];
const offsetTarget = unwrapElement((_a = props.offsetTarget) !== null && _a !== void 0 ? _a : document);
collectedLinkHrefs.forEach(href => {
const idMatchResult = /#([^#]+)$/.exec(href);
if (!idMatchResult) return;
const linkEl = document.getElementById(idMatchResult[1]);
if (linkEl && offsetTarget) {
const {
top,
height
} = getOffset(linkEl, offsetTarget);
links.push({
top,
height,
href
});
}
});
links.sort((a, b) => {
// ascend top
if (a.top > b.top) {
return 1;
// descend height
} else if (a.top === b.top && a.height < b.height) {
return -1;
}
return -1;
});
const currentActiveHref = activeHrefRef.value;
const {
bound,
ignoreGap
} = props;
const activeLink = links.reduce((prevLink, link) => {
if (link.top + link.height < 0) {
if (ignoreGap) {
return link;
} else {
return prevLink;
}
}
if (link.top <= bound) {
if (prevLink === null) {
return link;
} else if (link.top === prevLink.top) {
if (link.href === currentActiveHref) {
return link;
} else {
return prevLink;
}
} else if (link.top > prevLink.top) {
return link;
} else {
return prevLink;
}
}
return prevLink;
}, null);
if (!transition) disableTransitionOneTick();
if (activeLink) {
activeHrefRef.value = activeLink.href;
} else {
activeHrefRef.value = null;
}
}
provide(anchorInjectionKey, {
activeHref: activeHrefRef,
mergedClsPrefix: toRef(props, 'mergedClsPrefix'),
updateBarPosition,
setActiveHref,
collectedLinkHrefs,
titleEls
});
onMounted(() => {
document.addEventListener('scroll', handleScroll, true);
setActiveHref(window.location.hash);
_handleScroll(false);
});
onFontsReady(() => {
setActiveHref(window.location.hash);
_handleScroll(false);
});
onBeforeUnmount(() => {
document.removeEventListener('scroll', handleScroll, true);
});
watch(activeHrefRef, value => {
if (value === null) {
const {
value: slotEl
} = slotRef;
if (slotEl && !isBlockTypeRef.value) {
slotEl.style.maxWidth = '0';
}
}
});
return {
selfRef,
barRef,
slotRef,
setActiveHref,
activeHref: activeHrefRef,
isBlockType: isBlockTypeRef,
mergedShowRail: mergedShowRailRef
};
},
render() {
var _a;
const {
mergedClsPrefix,
mergedShowRail,
isBlockType,
$slots
} = this;
const Anchor = h("div", {
class: [`${mergedClsPrefix}-anchor`, isBlockType && `${mergedClsPrefix}-anchor--block`, mergedShowRail && `${mergedClsPrefix}-anchor--show-rail`],
ref: "selfRef"
}, mergedShowRail && this.showBackground ? h("div", {
ref: "slotRef",
class: `${mergedClsPrefix}-anchor-link-background`
}) : null, mergedShowRail ? h("div", {
class: `${mergedClsPrefix}-anchor-rail`
}, h("div", {
ref: "barRef",
class: [`${mergedClsPrefix}-anchor-rail__bar`, this.activeHref !== null && `${mergedClsPrefix}-anchor-rail__bar--active`]
})) : null, (_a = $slots.default) === null || _a === void 0 ? void 0 : _a.call($slots));
return this.internalScrollable ? h(NScrollbar, null, {
default: () => Anchor
}) : Anchor;
}
});