Skip to content

Commit

Permalink
feat: impl aside scroll
Browse files Browse the repository at this point in the history
  • Loading branch information
sanyuan0704 committed Sep 9, 2022
1 parent 799bfb8 commit 1815713
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 109 deletions.
6 changes: 4 additions & 2 deletions src/client/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Layout } from 'island:theme';
import React from 'react';
import { routes } from 'virtual:routes';
import { matchRoutes } from 'react-router-dom';
import { matchRoutes, Route } from 'react-router-dom';

export async function waitForApp(path: string) {
const matched = matchRoutes(routes, path)!;
return matched;
// @ts-ignore
const mod = await import(matched[0].route.componentPath);
return mod;
}

export function App() {
Expand Down
11 changes: 7 additions & 4 deletions src/client/app/client-entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { hydrateRoot, createRoot } from 'react-dom/client';
import React, { createElement } from 'react';
import { BrowserRouter } from 'react-router-dom';
import './sideEffects';
import { DataContext } from './hooks';

async function renderInBrowser() {
const containerEl = document.getElementById('root');
Expand All @@ -14,11 +15,13 @@ async function renderInBrowser() {
// So there is no need to worry that the complete hydration will be executed in production
const { waitForApp, App } = await import('./app');

await waitForApp('/');
const mod = await waitForApp('/');
createRoot(containerEl).render(
<BrowserRouter>
<App />
</BrowserRouter>
<DataContext.Provider value={mod}>
<BrowserRouter>
<App />
</BrowserRouter>
</DataContext.Provider>
);
} else {
await import('./island-inject');
Expand Down
7 changes: 7 additions & 0 deletions src/client/app/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import data from 'island:page-data';
import { createContext, useContext } from 'react';

export const DataContext = createContext({});

export const usePageData = () => {
return data;
};

export const useDataContext = () => {
return useContext(DataContext);
};
2 changes: 1 addition & 1 deletion src/client/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { Content } from './Content';
export { usePageData } from './hooks';
export { usePageData, useDataContext } from './hooks';
25 changes: 12 additions & 13 deletions src/client/app/sideEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const DEFAULT_NAV_HEIGHT = 72;
if (inBrowser) {
function scrollTo(el: HTMLElement, hash: string, smooth = false) {
let target: HTMLElement | null = null;

try {
target = el.classList.contains('header-anchor')
? el
Expand Down Expand Up @@ -45,19 +44,19 @@ if (inBrowser) {
// only intercept inbound links
if (hash && target !== `_blank` && origin === currentUrl.origin) {
e.preventDefault();
if (
pathname === currentUrl.pathname &&
search === currentUrl.search
) {
// scroll between hash anchors in the same page
if (hash && hash !== currentUrl.hash) {
history.pushState(null, '', hash);
// still emit the event so we can listen to it in themes
window.dispatchEvent(new Event('hashchange'));
// use smooth scroll when clicking on header anchor links
scrollTo(link, hash, link.classList.contains('header-anchor'));
}
// if (
// pathname === currentUrl.pathname &&
// search === currentUrl.search
// ) {
// scroll between hash anchors in the same page
if (hash && hash !== currentUrl.hash) {
history.pushState(null, '', hash);
// still emit the event so we can listen to it in themes
window.dispatchEvent(new Event('hashchange'));
// use smooth scroll when clicking on header anchor links
scrollTo(link, hash, link.classList.contains('header-anchor'));
}
// }
}
}
},
Expand Down
135 changes: 47 additions & 88 deletions src/client/theme/components/Aside/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import React, { useEffect } from 'react';
import styles from './index.module.scss';
import { throttle } from 'lodash-es';
import { useLocation } from 'react-router-dom';
interface Header {
text: string;
link: string;
children: Header[];
hidden: boolean;
}
import { useDataContext } from 'island:client';

function isBottom() {
return (
Expand All @@ -17,102 +11,67 @@ function isBottom() {
}

export function Aside() {
// For outline text highlight
const [activeIndex, setActiveIndex] = React.useState(0);
const markerRef = React.useRef<HTMLDivElement>(null);
const headers = [
{
text: 'Introduction',
link: '/introduction',
children: [
{
text: 'Getting Started',
link: '/introduction/getting-started',
children: [],
hidden: false
},
{
text: 'Why Island',
link: '/introduction/why-island',
children: [
{
text: 'Why 123',
link: '/introduction/why-island',
children: [],
hidden: false
}
],
hidden: false
},
{
text: 'Features',
link: '/introduction/features',
children: [],
hidden: false
}
],
hidden: false
}
];
const data = useDataContext();
const headers = data.toc;
const SCROLL_INTO_HEIGHT = 150;
useEffect(() => {
const links = document.querySelectorAll('.island-doc .header-anchor');
const normalizeHref = (href: string) => {
return href.toLocaleLowerCase().replace(/ +/g, '-').replace(/\./g, '');
};

const onScroll = () => {
if (isBottom()) {
setActiveIndex(links.length - 1);
} else {
// Compute current index
for (let i = 0; i < links.length; i++) {
if (links[i].getBoundingClientRect().top < SCROLL_INTO_HEIGHT) {
if (
i < links.length - 1 &&
links[i + 1].getBoundingClientRect().top < SCROLL_INTO_HEIGHT
) {
continue;
} else {
setActiveIndex(i);
useEffect(() => {
const onScroll = throttle(
function listen() {
const links = document.querySelectorAll<HTMLAnchorElement>(
'.island-doc .header-anchor'
);
if (isBottom()) {
setActiveIndex(links.length - 1);
markerRef.current!.style.top = `${33 + (headers.length - 1) * 28}px`;
} else {
// Compute current index
for (let i = 0; i < links.length; i++) {
const topDistance = links[i].getBoundingClientRect().top;
if (topDistance > 0 && topDistance < SCROLL_INTO_HEIGHT) {
const id = links[i].getAttribute('href');
const index = headers.findIndex(
(item: any) => normalizeHref(item.text) === id?.slice(1)
);
if (index > -1 && index !== activeIndex) {
setActiveIndex(index);
markerRef.current!.style.top = `${33 + index * 28}px`;
} else {
setActiveIndex(0);
markerRef.current!.style.top = '33px';
}
break;
}
break;
}
}
}
};
window.addEventListener('scroll', throttle(onScroll, 200));
},
100,
{ trailing: true }
);
window.addEventListener('scroll', onScroll);

return () => {
window.removeEventListener('scroll', onScroll);
};
}, []);

useEffect(() => {
if (markerRef.current) {
markerRef.current.style.top = `${33 + activeIndex * 28}px`;
}
}, [activeIndex]);

const renderHeader = (header: Header) => {
let children = null;
if (header.children.length) {
children = (
<ul>
{header.children.map((child) => (
<li key={child.link}>
<a
href={child.link}
className={`${styles.outlineLink} ${styles.nested}`}
>
{child.text}
</a>
</li>
))}
</ul>
);
}
const renderHeader = (header: any, index: number) => {
const isNested = header.depth > 2;
return (
<li>
<a href={header.link} className={styles.outlineLink}>
<li key={header.text}>
<a
href={`#${normalizeHref(header.text)}`}
className={`${styles.outlineLink} ${
index == activeIndex ? styles.active : ''
} ${isNested ? styles.nested : ''}`}
>
{header.text}
{children}
</a>
</li>
);
Expand Down
1 change: 1 addition & 0 deletions src/client/type.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ declare module 'island:client' {

export const Content: ComponentType<any>;
export const usePageData: <T = DefaultTheme.Config>() => PageData<T>;
export const useDataContext: () => any;
}

declare module 'virtual:routes' {
Expand Down
2 changes: 1 addition & 1 deletion src/node/plugin-routes/RouteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ ${this.#routeData
export const routes = [
${this.#routeData
.map((route, index) => {
return `{ path: '${route.routePath}', element: React.createElement(Route${index}) },`;
return `{ path: '${route.routePath}', element: React.createElement(Route${index}), componentPath: '${route.absolutePath}' },`;
})
.join('\n')}
];
Expand Down

0 comments on commit 1815713

Please sign in to comment.