-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e8e06e9
commit 8737e46
Showing
19 changed files
with
379 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import "./styles/index.scss"; | ||
|
||
import { Anchor as AnchorNode } from "@arco-design/web-react"; | ||
import type { EditorKit } from "doc-editor-core"; | ||
import { EDITOR_EVENT } from "doc-editor-core"; | ||
import { cs, debounce } from "doc-editor-utils"; | ||
import type { FC } from "react"; | ||
import { useEffect, useMemo, useRef, useState } from "react"; | ||
|
||
import { getLinkElement } from "./utils/link"; | ||
import type { Anchor } from "./utils/parse"; | ||
import { parseAnchor } from "./utils/parse"; | ||
|
||
const AnchorLink = AnchorNode.Link; | ||
|
||
export const DocAnchor: FC<{ | ||
editor: EditorKit; | ||
width?: number; | ||
boundary?: number; | ||
className?: string; | ||
scrollContainer?: HTMLElement | Window; | ||
}> = props => { | ||
const ref = useRef<HTMLDivElement>(null); | ||
const [list, setList] = useState<Anchor[]>([]); | ||
|
||
const onParse = useMemo( | ||
() => | ||
debounce(() => { | ||
const res = parseAnchor(props.editor); | ||
setList(res); | ||
}, 300), | ||
[props.editor] | ||
); | ||
|
||
useEffect(() => { | ||
onParse(); | ||
props.editor.event.on(EDITOR_EVENT.CONTENT_CHANGE, onParse); | ||
return () => { | ||
props.editor.event.off(EDITOR_EVENT.CONTENT_CHANGE, onParse); | ||
}; | ||
}, [onParse, props.editor]); | ||
|
||
const onAnchorChange = (newLink: string) => { | ||
const el = ref.current; | ||
const link = el && getLinkElement(newLink); | ||
if (el && link) { | ||
const refRect = el.getBoundingClientRect(); | ||
const linkRect = link.getBoundingClientRect(); | ||
if (refRect.top > linkRect.top) { | ||
el.scrollBy({ top: linkRect.top - refRect.top, behavior: "smooth" }); | ||
} | ||
if (refRect.bottom < linkRect.bottom) { | ||
el.scrollBy({ top: linkRect.bottom - refRect.bottom, behavior: "smooth" }); | ||
} | ||
} | ||
}; | ||
|
||
return list.length ? ( | ||
<div className={cs("doc-anchor", props.className)} ref={ref}> | ||
<AnchorNode | ||
style={{ width: props.width || 200 }} | ||
affix={false} | ||
onChange={onAnchorChange} | ||
boundary={props.boundary} | ||
scrollContainer={props.scrollContainer} | ||
> | ||
{list.map(item => ( | ||
<AnchorLink | ||
title={item.title} | ||
key={item.id} | ||
href={`#${item.id}`} | ||
style={{ marginLeft: item.level * 10 }} | ||
></AnchorLink> | ||
))} | ||
</AnchorNode> | ||
</div> | ||
) : null; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
@import '../../styles/variable'; | ||
|
||
.doc-anchor { | ||
@include no-scrollbar; | ||
|
||
background-color: var(--color-bg-3); | ||
border-left: 1px solid var(--color-fill-2); | ||
box-sizing: border-box; | ||
max-height: 600px; | ||
overflow-x: visible; | ||
overflow-y: auto; | ||
padding: 5px 3px; | ||
position: fixed; | ||
right: 0; | ||
top: 130px; | ||
transition: all 0.3s; | ||
|
||
&.doc-anchor-collapse { | ||
transform: translateX(100%); | ||
} | ||
|
||
.doc-anchor-arrow { | ||
background-color: var(--color-bg-3); | ||
position: absolute; | ||
top: 10px; | ||
transform: translateX(-100%); | ||
z-index: 10000; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const getLinkElement = (hash: string): HTMLElement | null => { | ||
return document.querySelector(`a[data-href="${hash}"]`); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import type { EditorKit } from "doc-editor-core"; | ||
import type { BlockElement } from "doc-editor-delta"; | ||
|
||
import { H1, H2, H3, HEADING_KEY } from "../../heading/types"; | ||
|
||
export type Anchor = { | ||
id: string; | ||
title: string; | ||
level: number; | ||
}; | ||
|
||
export const parseAnchor = (editor: EditorKit): Anchor[] => { | ||
const list: Anchor[] = []; | ||
let minLevel = Infinity; | ||
editor.raw.children.forEach(node => { | ||
const heading = (<BlockElement>node)[HEADING_KEY]; | ||
if (heading && node.children && heading.id && heading.type) { | ||
const text = node.children.map(child => child.text || "").join(""); | ||
let level = 0; | ||
if (heading.type === H1) { | ||
level = 0; | ||
} else if (heading.type === H2) { | ||
level = 1; | ||
} else if (heading.type === H3) { | ||
level = 2; | ||
} | ||
minLevel = Math.min(minLevel, level); | ||
list.push({ id: heading.id, title: text, level: level }); | ||
} | ||
}); | ||
// 取最低的层级 | ||
list.forEach(anchor => { | ||
anchor.level = anchor.level - minLevel; | ||
}); | ||
return list; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import type { Func } from "doc-editor-utils"; | ||
import type { DependencyList, EffectCallback, MutableRefObject, SetStateAction } from "react"; | ||
import { useCallback, useEffect, useRef, useState } from "react"; | ||
|
||
/** | ||
* 当前组件挂载状态 | ||
*/ | ||
export const useIsMounted = () => { | ||
const isMounted = useRef(false); | ||
|
||
useEffect(() => { | ||
isMounted.current = true; | ||
return () => { | ||
isMounted.current = false; | ||
}; | ||
}, []); | ||
|
||
return { isMounted: () => isMounted.current, mounted: isMounted }; | ||
}; | ||
|
||
/** | ||
* 安全地使用 useState | ||
* @param value 状态 | ||
* @param mounted 组件挂载状态 useIsMounted | ||
*/ | ||
export const useMountState = <S = undefined>(value: S, mounted: MutableRefObject<boolean>) => { | ||
const [state, setStateOrigin] = useState<S>(value); | ||
|
||
const setCurrentState = useCallback((next: SetStateAction<S>) => { | ||
if (!mounted.current) return void 0; | ||
setStateOrigin(next); | ||
}, []); | ||
|
||
return [state, setCurrentState] as const; | ||
}; | ||
|
||
/** | ||
* 安全地使用 useState | ||
* @param value 状态 | ||
*/ | ||
export const useSafeState = <S = undefined>(value: S) => { | ||
const [state, setStateOrigin] = useState<S>(value); | ||
const { mounted } = useIsMounted(); | ||
|
||
const setCurrentState = useCallback((next: SetStateAction<S>) => { | ||
if (!mounted.current) return void 0; | ||
setStateOrigin(next); | ||
}, []); | ||
|
||
return [state, setCurrentState] as const; | ||
}; | ||
|
||
/** | ||
* State 与 Ref 的使用与更新 | ||
* @param value 状态 | ||
*/ | ||
export const useStateRef = <S = undefined>(value: S) => { | ||
const [state, setStateOrigin] = useState<S>(value); | ||
const { mounted } = useIsMounted(); | ||
const ref = useRef(state); | ||
|
||
const setState = useCallback((next: S) => { | ||
if (!mounted.current) return void 0; | ||
ref.current = next; | ||
setStateOrigin(next); | ||
}, []); | ||
|
||
return [state, setState, ref] as const; | ||
}; | ||
|
||
/** | ||
* 避免挂载时触发副作用 | ||
* @param effect 副作用依赖 | ||
*/ | ||
export const useUpdateEffect = (effect: EffectCallback, deps?: DependencyList) => { | ||
const isMounted = useRef(false); | ||
|
||
useEffect(() => { | ||
if (!isMounted.current) { | ||
isMounted.current = true; | ||
} else { | ||
return effect(); | ||
} | ||
}, deps); | ||
}; | ||
|
||
/** | ||
* 保证 re-render 时的同一函数引用 | ||
* @param fn Func.Any | ||
*/ | ||
export const useMemoFn = <T extends Func.Any>(fn: T) => { | ||
const fnRef = useRef(fn); | ||
const memoFn = useRef<Func.Any>(); | ||
|
||
fnRef.current = fn; | ||
if (!memoFn.current) { | ||
memoFn.current = function (this: unknown, ...args: unknown[]) { | ||
return fnRef.current.apply(this, args); | ||
}; | ||
} | ||
|
||
return memoFn.current as T; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import type { EDITOR_EVENT, EventMap } from "doc-editor-core"; | ||
import type { EDITOR_EVENT, EventMap, WithStop } from "doc-editor-core"; | ||
|
||
export type EditorSelectChangeEvent = EventMap[typeof EDITOR_EVENT.SELECTION_CHANGE]; | ||
export type ContentChangeEvent = WithStop<EventMap[typeof EDITOR_EVENT.CONTENT_CHANGE]>; | ||
export type SelectChangeEvent = WithStop<EventMap[typeof EDITOR_EVENT.SELECTION_CHANGE]>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.