import { For, JSXElement, children, createEffect, onCleanup, onMount, untrack } from "solid-js";
import { createStore } from "solid-js/store";
import { useClassList } from "../utils/useProps"
import { scrollTop } from "../utils/utils";
import { AnchorLink, AnchorLinkProps } from './AnchorLink';
type AnchorProps = {
children?: any,
classList?: any,
class?: string,
container?: string | HTMLElement,
scrollContainer?: string | HTMLElement,
scrollOffset?: number,
offsetTop?: number,
bounds?: number,
showInk?: boolean,
mode?: 'hash'|'history'
}
type AnchorStore = {
inkTop: number,
inkHeight: number,
currentId: string,
currentLink: string,
animating: boolean,
links: AnchorLinkProps[],
upperFirstTitle?: boolean
}
export function Anchor (props: AnchorProps) {
const classList = () => useClassList(props, 'cm-anchor');
const links = children(() => props.children)
const evaluatedLinks = () => links.toArray() as unknown as AnchorLinkProps[]
const [store, setStore] = createStore({
inkTop: 0,
inkHeight: 0,
currentId: '',
currentLink: '',
animating: false,
links: [],
upperFirstTitle: true
} as AnchorStore);
createEffect(() => {
setStore('links', evaluatedLinks());
})
let scrollContainer: any = null;
let scrollElement: any = null;
let wrapperTop = 0;
let bounds: number = props.bounds || 5;
let titlesOffsetArr: any = [];
let mode = props.mode ?? 'hash';
let showInk = props.showInk ?? false;
const handleHashChange = () => {
let sharpLinkMatch: any;
if (mode === 'hash') {
const url = window.location.href;
sharpLinkMatch = /#([^#]+)$/.exec(url);
} else {
let path = window.location.href;
const search = path.includes('?') ? path.split('?')[1] : '';
const params = new URLSearchParams(search);
const has = params.has('_to');
if (has) {
if (params.get('_to')) {
sharpLinkMatch = [];
sharpLinkMatch[0] = params.get('_to');
sharpLinkMatch[1] = params.get('_to')?.replace('#', '');
}
}
}
if (!sharpLinkMatch) {
setTimeout(() => {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
getCurrentScrollAtTitleId(scrollTop);
}, 10)
return;
};
setStore('currentLink', sharpLinkMatch[0]);
setStore('currentId', sharpLinkMatch[1]);
}
const removeListener = () => {
scrollContainer && scrollContainer.removeEventListener('scroll', handleScroll);
window.removeEventListener('hashchange', handleHashChange);
}
const handleScroll = (e: any) => {
// this.upperFirstTitle = !!this.titlesOffsetArr[0] && e.target.scrollTop < this.titlesOffsetArr[0].offset;
if (store.animating) return;
// this.updateTitleOffset();
const scrollTop = (document.documentElement.scrollTop || document.body.scrollTop || e.target.scrollTop);
getCurrentScrollAtTitleId(scrollTop);
}
const handleScrollTo = () => {
const anchor = document.getElementById(store.currentId);
const currentLinkElementA: any = document.querySelector(`a[data-href="${store.currentLink}"]`);
let offset = props.scrollOffset || 0;
if (currentLinkElementA) {
offset = parseFloat(currentLinkElementA.getAttribute('data-scroll-offset'));
}
if (!anchor) return;
const offsetTop = anchor.offsetTop - wrapperTop - offset;
setStore('animating', true);
scrollTop(scrollContainer, scrollElement.scrollTop, offsetTop, 600, () => {
setStore('animating', false);
});
}
createEffect(() => {
store.currentLink;
const currentLinkElementA: any = document.querySelector(`a[data-href="${store.currentLink}"]`)?.parentElement;
if (!currentLinkElementA) return;
const elementATop = currentLinkElementA.offsetTop;
const elementAHeight = currentLinkElementA.getBoundingClientRect().height;
const offset = elementAHeight / 4;
const top = (elementATop < 0 ? (props.offsetTop || 0) : elementATop);
untrack(() => {
setStore('inkTop', top + offset / 2);
setStore('inkHeight', elementAHeight * 3 / 4);
});
})
const getContainer = () => {
scrollContainer = props.container ? (typeof props.container === 'string' ? document.querySelector(props.container) : props.container) : window;
scrollElement = props.container ? scrollContainer : (document.documentElement || document.body);
}
const getCurrentScrollAtTitleId = (scrollTop: number) => {
let i = -1;
let len = titlesOffsetArr.length;
let titleItem = {
link: '#',
offset: 0
};
scrollTop += bounds;
while (++i < len) {
let currentEle = titlesOffsetArr[i];
let nextEle = titlesOffsetArr[i + 1];
if (scrollTop >= currentEle.offset && scrollTop < ((nextEle && nextEle.offset) || Infinity)) {
titleItem = titlesOffsetArr[i];
break;
}
}
setStore('currentLink', titleItem.link)
}
const containerIsWindow = () => scrollContainer === window;
const init = () => {
handleHashChange();
setTimeout(() => {
removeListener();
getContainer();
wrapperTop = containerIsWindow() ? 0 : scrollElement.offsetTop;
handleScrollTo();
// if (titlesOffsetArr[0]) {
// setStore('upperFirstTitle', scrollElement.scrollTop < titlesOffsetArr[0].offset);
// }
scrollContainer.addEventListener('scroll', handleScroll);
window.addEventListener('hashchange', handleHashChange);
}, 0);
}
createEffect(() => {
const links = store.links.map(item => {
return item.href;
});
untrack(() => {
const idArr = links.map(link => {
return link.split('#')[1];
});
if (!scrollElement) {
getContainer();
}
const arr: any = [];
idArr.forEach(id => {
const titleEle = document.getElementById(id);
if (titleEle) arr.push({
link: `#${id}`,
offset: titleEle.offsetTop - scrollElement.offsetTop
});
});
titlesOffsetArr = arr;
});
});
const gotoAnchor = (href: any, e: any) => {
e.stopPropagation && e.stopPropagation();
e.preventDefault && e.preventDefault();
setStore('currentLink', href);
setStore('currentId', href.replace('#', ''));
handleScrollTo();
if (mode === 'hash') {
window.location.hash = href;
} else {
let path = window.location.href;
const search = path.includes('?') ? path.split('?')[1] : '';
const index = location.hash.indexOf('?');
const hash = index > -1 ? location.hash.substring(0, index) : location.hash;
const params = new URLSearchParams(search);
params.set('_to', href);
window.history.replaceState({}, '', `${location.pathname}${hash}?${params.toString()}`)
}
}
onMount(() => {
init();
let timer = setInterval(() => {
const links = store.links.map(item => {
return item.href;
});
const idArr = links.map(link => {
return link.split('#')[1];
});
idArr.forEach((id, index) => {
const titleEle = document.getElementById(id);
if (titleEle) {
const offset = titleEle.offsetTop - scrollElement.offsetTop;
if (titlesOffsetArr[index] && titlesOffsetArr[index].offset !== offset) {
titlesOffsetArr[index].offset = offset;
}
}
});
}, 500);
onCleanup(() => {
clearInterval(timer);
})
});
onCleanup(() => {
removeListener();
});
const renderLinks = (arr: AnchorLinkProps[]): JSXElement => {
if (arr && arr.length) {
return