feat:目录
This commit is contained in:
parent
4f6c3915ee
commit
91b72e671f
|
@ -31,6 +31,8 @@ import InlineImagePlugin from "./plugins/InlineImagePlugin";
|
|||
import {TablePlugin} from "@lexical/react/LexicalTablePlugin";
|
||||
import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
|
||||
import ExcalidrawPlugin from "./plugins/ExcalidrawPlugin";
|
||||
import TableOfContentsPlugin from "./plugins/TableOfContentsPlugin";
|
||||
import ContextMenuPlugin from "./plugins/ContextMenuPlugin"
|
||||
function Placeholder() {
|
||||
return <div className="editor-placeholder">Enter some rich text...</div>;
|
||||
}
|
||||
|
@ -93,7 +95,9 @@ export default function Hlexical(props) {
|
|||
{/*页分割线*/}
|
||||
|
||||
{/*目录加载*/}
|
||||
|
||||
<TableOfContentsPlugin filePath={props.filePath}/>
|
||||
{/*右键菜单*/}
|
||||
<ContextMenuPlugin />
|
||||
{/* 画图 */}
|
||||
<ExcalidrawPlugin />
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,221 @@
|
|||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {
|
||||
LexicalContextMenuPlugin,
|
||||
MenuOption,
|
||||
} from '@lexical/react/LexicalContextMenuPlugin';
|
||||
import {useCallback, useMemo} from 'react';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
COPY_COMMAND,
|
||||
CUT_COMMAND,
|
||||
PASTE_COMMAND,
|
||||
} from 'lexical';
|
||||
function ContextMenuItem({
|
||||
index,
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
option,
|
||||
}) {
|
||||
let className = 'item';
|
||||
if (isSelected) {
|
||||
className += ' selected';
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={option.key}
|
||||
tabIndex={-1}
|
||||
className={className}
|
||||
ref={option.setRefElement}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
id={'typeahead-item-' + index}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={onClick}>
|
||||
<span className="text">{option.title}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenu({
|
||||
options,
|
||||
selectedItemIndex,
|
||||
onOptionClick,
|
||||
onOptionMouseEnter,
|
||||
}) {
|
||||
return (
|
||||
<div className="typeahead-popover">
|
||||
<ul>
|
||||
{options.map((option, i) => (
|
||||
<ContextMenuItem
|
||||
index={i}
|
||||
isSelected={selectedItemIndex === i}
|
||||
onClick={() => onOptionClick(option, i)}
|
||||
onMouseEnter={() => onOptionMouseEnter(i)}
|
||||
key={option.key}
|
||||
option={option}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export class ContextMenuOption extends MenuOption {
|
||||
title;
|
||||
onSelect;
|
||||
constructor(
|
||||
title,
|
||||
options,
|
||||
) {
|
||||
super(title);
|
||||
this.title = title;
|
||||
this.onSelect = options.onSelect.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
export default function ContextMenuPlugin(){
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [
|
||||
new ContextMenuOption(`复制`, {
|
||||
onSelect: (_node) => {
|
||||
editor.dispatchCommand(COPY_COMMAND, null);
|
||||
},
|
||||
}),
|
||||
new ContextMenuOption(`剪切`, {
|
||||
onSelect: (_node) => {
|
||||
editor.dispatchCommand(CUT_COMMAND, null);
|
||||
},
|
||||
}),
|
||||
new ContextMenuOption(`粘贴`, {
|
||||
onSelect: (_node) => {
|
||||
navigator.clipboard.read().then(async (...args) => {
|
||||
const data = new DataTransfer();
|
||||
|
||||
const items = await navigator.clipboard.read();
|
||||
const item = items[0];
|
||||
|
||||
const permission = await navigator.permissions.query({
|
||||
// @ts-expect-error These types are incorrect.
|
||||
name: 'clipboard-read',
|
||||
});
|
||||
if (permission.state === 'denied') {
|
||||
alert('Not allowed to paste from clipboard.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const type of item.types) {
|
||||
const dataString = await (await item.getType(type)).text();
|
||||
data.setData(type, dataString);
|
||||
}
|
||||
|
||||
const event = new ClipboardEvent('paste', {
|
||||
clipboardData: data,
|
||||
});
|
||||
|
||||
editor.dispatchCommand(PASTE_COMMAND, event);
|
||||
});
|
||||
},
|
||||
}),
|
||||
new ContextMenuOption(`作为文本复制`, {
|
||||
onSelect: (_node) => {
|
||||
navigator.clipboard.read().then(async (...args) => {
|
||||
const permission = await navigator.permissions.query({
|
||||
// @ts-expect-error These types are incorrect.
|
||||
name: 'clipboard-read',
|
||||
});
|
||||
|
||||
if (permission.state === 'denied') {
|
||||
alert('Not allowed to paste from clipboard.');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new DataTransfer();
|
||||
const items = await navigator.clipboard.readText();
|
||||
data.setData('text/plain', items);
|
||||
|
||||
const event = new ClipboardEvent('paste', {
|
||||
clipboardData: data,
|
||||
});
|
||||
editor.dispatchCommand(PASTE_COMMAND, event);
|
||||
});
|
||||
},
|
||||
}),
|
||||
new ContextMenuOption(`删除文本块`, {
|
||||
onSelect: (_node) => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
const currentNode = selection.anchor.getNode();
|
||||
const ancestorNodeWithRootAsParent = currentNode
|
||||
.getParents()
|
||||
.at(-2);
|
||||
|
||||
ancestorNodeWithRootAsParent?.remove();
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
}, [editor]);
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(
|
||||
selectedOption,
|
||||
targetNode,
|
||||
closeMenu,
|
||||
) => {
|
||||
editor.update(() => {
|
||||
selectedOption.onSelect(targetNode);
|
||||
closeMenu();
|
||||
});
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
return (
|
||||
<LexicalContextMenuPlugin
|
||||
options={options}
|
||||
onSelectOption={onSelectOption}
|
||||
menuRenderFn={(
|
||||
anchorElementRef,
|
||||
{
|
||||
selectedIndex,
|
||||
options: _options,
|
||||
selectOptionAndCleanUp,
|
||||
setHighlightedIndex,
|
||||
},
|
||||
{setMenuRef},
|
||||
) =>
|
||||
anchorElementRef.current
|
||||
? ReactDOM.createPortal(
|
||||
<div
|
||||
className="typeahead-popover auto-embed-menu"
|
||||
style={{
|
||||
marginLeft: anchorElementRef.current.style.width,
|
||||
userSelect: 'none',
|
||||
width: 200,
|
||||
}}
|
||||
ref={setMenuRef}>
|
||||
<ContextMenu
|
||||
options={options}
|
||||
selectedItemIndex={selectedIndex}
|
||||
onOptionClick={(option, index) => {
|
||||
setHighlightedIndex(index);
|
||||
selectOptionAndCleanUp(option);
|
||||
}}
|
||||
onOptionMouseEnter={(index) => {
|
||||
setHighlightedIndex(index);
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -10,6 +10,7 @@ import {useDispatch, useSelector} from "react-redux";
|
|||
import md5 from "md5"
|
||||
import {message} from "antd";
|
||||
const {ipcRenderer} = window.require('electron')
|
||||
import "./ToobarPlugin.less"
|
||||
const SaveFilePlugin=(props)=> {
|
||||
let activeKey = useSelector(state => state.tableBarItem.activeKey);
|
||||
const dispatch = useDispatch();
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
import './index.less';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import LexicalTableOfContents from '@lexical/react/LexicalTableOfContents';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import * as React from 'react';
|
||||
import {createPortal} from "react-dom";
|
||||
import {useSelector} from "react-redux";
|
||||
|
||||
const MARGIN_ABOVE_EDITOR = 624;
|
||||
const HEADING_WIDTH = 9;
|
||||
|
||||
function indent(tagName) {
|
||||
if (tagName === 'h1') {
|
||||
return 'heading1';
|
||||
} else if (tagName === 'h2') {
|
||||
return 'heading2';
|
||||
} else if (tagName === 'h3') {
|
||||
return 'heading3';
|
||||
} else if (tagName === 'h4') {
|
||||
return 'heading4';
|
||||
} else if (tagName === 'h5') {
|
||||
return 'heading5';
|
||||
} else if (tagName === 'h6') {
|
||||
return 'heading6';
|
||||
}
|
||||
}
|
||||
|
||||
function isHeadingAtTheTopOfThePage(element) {
|
||||
const elementYPosition = element?.getClientRects()[0].y;
|
||||
return (
|
||||
elementYPosition >= MARGIN_ABOVE_EDITOR &&
|
||||
elementYPosition <= MARGIN_ABOVE_EDITOR + HEADING_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
function isHeadingAboveViewport(element) {
|
||||
const elementYPosition = element?.getClientRects()[0].y;
|
||||
return elementYPosition < MARGIN_ABOVE_EDITOR;
|
||||
}
|
||||
|
||||
function isHeadingBelowTheTopOfThePage(element) {
|
||||
const elementYPosition = element?.getClientRects()[0].y;
|
||||
return elementYPosition >= MARGIN_ABOVE_EDITOR + HEADING_WIDTH;
|
||||
}
|
||||
|
||||
function TableOfContentsList({
|
||||
tableOfContents,
|
||||
filePath
|
||||
}) {
|
||||
const [selectedKey, setSelectedKey] = useState('');
|
||||
const selectedIndex = useRef(0);
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [show,setShow]=useState(false)
|
||||
const activeFilePath=useSelector(state => state.tableBarItem.activeKey)
|
||||
function scrollToNode(key, currIndex) {
|
||||
editor.getEditorState().read(() => {
|
||||
const domElement = editor.getElementByKey(key);
|
||||
if (domElement !== null) {
|
||||
domElement.scrollIntoView();
|
||||
setSelectedKey(key);
|
||||
selectedIndex.current = currIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function scrollCallback() {
|
||||
if (
|
||||
tableOfContents.length !== 0 &&
|
||||
selectedIndex.current < tableOfContents.length - 1
|
||||
) {
|
||||
let currentHeading = editor.getElementByKey(
|
||||
tableOfContents[selectedIndex.current][0],
|
||||
);
|
||||
if (currentHeading !== null) {
|
||||
if (isHeadingBelowTheTopOfThePage(currentHeading)) {
|
||||
//On natural scroll, user is scrolling up
|
||||
while (
|
||||
currentHeading !== null &&
|
||||
isHeadingBelowTheTopOfThePage(currentHeading) &&
|
||||
selectedIndex.current > 0
|
||||
) {
|
||||
const prevHeading = editor.getElementByKey(
|
||||
tableOfContents[selectedIndex.current - 1][0],
|
||||
);
|
||||
if (
|
||||
prevHeading !== null &&
|
||||
(isHeadingAboveViewport(prevHeading) ||
|
||||
isHeadingBelowTheTopOfThePage(prevHeading))
|
||||
) {
|
||||
selectedIndex.current--;
|
||||
}
|
||||
currentHeading = prevHeading;
|
||||
}
|
||||
const prevHeadingKey = tableOfContents[selectedIndex.current][0];
|
||||
setSelectedKey(prevHeadingKey);
|
||||
} else if (isHeadingAboveViewport(currentHeading)) {
|
||||
//On natural scroll, user is scrolling down
|
||||
while (
|
||||
currentHeading !== null &&
|
||||
isHeadingAboveViewport(currentHeading) &&
|
||||
selectedIndex.current < tableOfContents.length - 1
|
||||
) {
|
||||
const nextHeading = editor.getElementByKey(
|
||||
tableOfContents[selectedIndex.current + 1][0],
|
||||
);
|
||||
if (
|
||||
nextHeading !== null &&
|
||||
(isHeadingAtTheTopOfThePage(nextHeading) ||
|
||||
isHeadingAboveViewport(nextHeading))
|
||||
) {
|
||||
selectedIndex.current++;
|
||||
}
|
||||
currentHeading = nextHeading;
|
||||
}
|
||||
const nextHeadingKey = tableOfContents[selectedIndex.current][0];
|
||||
setSelectedKey(nextHeadingKey);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedIndex.current = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let timerId;
|
||||
|
||||
function debounceFunction(func, delay) {
|
||||
clearTimeout(timerId);
|
||||
timerId = setTimeout(func, delay);
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
debounceFunction(scrollCallback, 10);
|
||||
}
|
||||
|
||||
document.addEventListener('scroll', onScroll);
|
||||
if (document.getElementById("leftTableOfContents")&&filePath===activeFilePath){
|
||||
setShow(true)
|
||||
}else {
|
||||
setShow(false)
|
||||
}
|
||||
return () => document.removeEventListener('scroll', onScroll);
|
||||
}, [tableOfContents, editor,
|
||||
useSelector(state => state.tableBarItem.leftTableOfContents),
|
||||
useSelector(state => state.tableBarItem.activeKey)
|
||||
]);
|
||||
return <>
|
||||
{show && createPortal(<div className="table-of-contents">
|
||||
<ul className="headings">
|
||||
{tableOfContents.map(([key, text, tag], index) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<div className="normal-heading-wrapper" key={key}>
|
||||
<div
|
||||
className={indent(tag)}
|
||||
onClick={() => scrollToNode(key, index)}
|
||||
role="button"
|
||||
tabIndex={0}>
|
||||
{('' + text).length > 20
|
||||
? text.substring(0, 20) + '...'
|
||||
: text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={`normal-heading-wrapper ${
|
||||
selectedKey === key ? 'selected-heading-wrapper' : ''
|
||||
}`}
|
||||
key={key}>
|
||||
<div
|
||||
onClick={() => scrollToNode(key, index)}
|
||||
role="button"
|
||||
className={indent(tag)}
|
||||
tabIndex={0}>
|
||||
<li
|
||||
className={`normal-heading ${
|
||||
selectedKey === key ? 'selected-heading' : ''
|
||||
}
|
||||
`}>
|
||||
{('' + text).length > 27
|
||||
? text.substring(0, 27) + '...'
|
||||
: text}
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>, document.getElementById("leftTableOfContents"))}
|
||||
</>
|
||||
|
||||
;
|
||||
}
|
||||
|
||||
export default function TableOfContentsPlugin(prop) {
|
||||
return (
|
||||
<LexicalTableOfContents>
|
||||
{(tableOfContents) => {
|
||||
return <TableOfContentsList tableOfContents={tableOfContents} filePath={prop.filePath}/>;
|
||||
}}
|
||||
</LexicalTableOfContents>
|
||||
);
|
||||
}
|
|
@ -6,6 +6,18 @@
|
|||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.table-of-contents .heading4 {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.table-of-contents .heading5 {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.table-of-contents .heading6 {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.selected-heading {
|
||||
color: #3578e5;
|
||||
position: relative;
|
||||
|
@ -34,8 +46,8 @@
|
|||
.table-of-contents {
|
||||
color: #65676b;
|
||||
position: fixed;
|
||||
top: 200px;
|
||||
right: -35px;
|
||||
//top: 200px;
|
||||
//right: -35px;
|
||||
padding: 10px;
|
||||
width: 250px;
|
||||
display: flex;
|
|
@ -1,197 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import type {TableOfContentsEntry} from '@lexical/react/LexicalTableOfContents';
|
||||
import type {HeadingTagType} from '@lexical/rich-text';
|
||||
import type {NodeKey} from 'lexical';
|
||||
|
||||
import './index.css';
|
||||
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import LexicalTableOfContents from '@lexical/react/LexicalTableOfContents';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
const MARGIN_ABOVE_EDITOR = 624;
|
||||
const HEADING_WIDTH = 9;
|
||||
|
||||
function indent(tagName: HeadingTagType) {
|
||||
if (tagName === 'h2') {
|
||||
return 'heading2';
|
||||
} else if (tagName === 'h3') {
|
||||
return 'heading3';
|
||||
}
|
||||
}
|
||||
|
||||
function isHeadingAtTheTopOfThePage(element: HTMLElement): boolean {
|
||||
const elementYPosition = element?.getClientRects()[0].y;
|
||||
return (
|
||||
elementYPosition >= MARGIN_ABOVE_EDITOR &&
|
||||
elementYPosition <= MARGIN_ABOVE_EDITOR + HEADING_WIDTH
|
||||
);
|
||||
}
|
||||
function isHeadingAboveViewport(element: HTMLElement): boolean {
|
||||
const elementYPosition = element?.getClientRects()[0].y;
|
||||
return elementYPosition < MARGIN_ABOVE_EDITOR;
|
||||
}
|
||||
function isHeadingBelowTheTopOfThePage(element: HTMLElement): boolean {
|
||||
const elementYPosition = element?.getClientRects()[0].y;
|
||||
return elementYPosition >= MARGIN_ABOVE_EDITOR + HEADING_WIDTH;
|
||||
}
|
||||
|
||||
function TableOfContentsList({
|
||||
tableOfContents,
|
||||
}: {
|
||||
tableOfContents: Array<TableOfContentsEntry>;
|
||||
}): JSX.Element {
|
||||
const [selectedKey, setSelectedKey] = useState('');
|
||||
const selectedIndex = useRef(0);
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
function scrollToNode(key: NodeKey, currIndex: number) {
|
||||
editor.getEditorState().read(() => {
|
||||
const domElement = editor.getElementByKey(key);
|
||||
if (domElement !== null) {
|
||||
domElement.scrollIntoView();
|
||||
setSelectedKey(key);
|
||||
selectedIndex.current = currIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function scrollCallback() {
|
||||
if (
|
||||
tableOfContents.length !== 0 &&
|
||||
selectedIndex.current < tableOfContents.length - 1
|
||||
) {
|
||||
let currentHeading = editor.getElementByKey(
|
||||
tableOfContents[selectedIndex.current][0],
|
||||
);
|
||||
if (currentHeading !== null) {
|
||||
if (isHeadingBelowTheTopOfThePage(currentHeading)) {
|
||||
//On natural scroll, user is scrolling up
|
||||
while (
|
||||
currentHeading !== null &&
|
||||
isHeadingBelowTheTopOfThePage(currentHeading) &&
|
||||
selectedIndex.current > 0
|
||||
) {
|
||||
const prevHeading = editor.getElementByKey(
|
||||
tableOfContents[selectedIndex.current - 1][0],
|
||||
);
|
||||
if (
|
||||
prevHeading !== null &&
|
||||
(isHeadingAboveViewport(prevHeading) ||
|
||||
isHeadingBelowTheTopOfThePage(prevHeading))
|
||||
) {
|
||||
selectedIndex.current--;
|
||||
}
|
||||
currentHeading = prevHeading;
|
||||
}
|
||||
const prevHeadingKey = tableOfContents[selectedIndex.current][0];
|
||||
setSelectedKey(prevHeadingKey);
|
||||
} else if (isHeadingAboveViewport(currentHeading)) {
|
||||
//On natural scroll, user is scrolling down
|
||||
while (
|
||||
currentHeading !== null &&
|
||||
isHeadingAboveViewport(currentHeading) &&
|
||||
selectedIndex.current < tableOfContents.length - 1
|
||||
) {
|
||||
const nextHeading = editor.getElementByKey(
|
||||
tableOfContents[selectedIndex.current + 1][0],
|
||||
);
|
||||
if (
|
||||
nextHeading !== null &&
|
||||
(isHeadingAtTheTopOfThePage(nextHeading) ||
|
||||
isHeadingAboveViewport(nextHeading))
|
||||
) {
|
||||
selectedIndex.current++;
|
||||
}
|
||||
currentHeading = nextHeading;
|
||||
}
|
||||
const nextHeadingKey = tableOfContents[selectedIndex.current][0];
|
||||
setSelectedKey(nextHeadingKey);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedIndex.current = 0;
|
||||
}
|
||||
}
|
||||
let timerId: ReturnType<typeof setTimeout>;
|
||||
|
||||
function debounceFunction(func: () => void, delay: number) {
|
||||
clearTimeout(timerId);
|
||||
timerId = setTimeout(func, delay);
|
||||
}
|
||||
|
||||
function onScroll(): void {
|
||||
debounceFunction(scrollCallback, 10);
|
||||
}
|
||||
|
||||
document.addEventListener('scroll', onScroll);
|
||||
return () => document.removeEventListener('scroll', onScroll);
|
||||
}, [tableOfContents, editor]);
|
||||
|
||||
return (
|
||||
<div className="table-of-contents">
|
||||
<ul className="headings">
|
||||
{tableOfContents.map(([key, text, tag], index) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<div className="normal-heading-wrapper" key={key}>
|
||||
<div
|
||||
className="first-heading"
|
||||
onClick={() => scrollToNode(key, index)}
|
||||
role="button"
|
||||
tabIndex={0}>
|
||||
{('' + text).length > 20
|
||||
? text.substring(0, 20) + '...'
|
||||
: text}
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={`normal-heading-wrapper ${
|
||||
selectedKey === key ? 'selected-heading-wrapper' : ''
|
||||
}`}
|
||||
key={key}>
|
||||
<div
|
||||
onClick={() => scrollToNode(key, index)}
|
||||
role="button"
|
||||
className={indent(tag)}
|
||||
tabIndex={0}>
|
||||
<li
|
||||
className={`normal-heading ${
|
||||
selectedKey === key ? 'selected-heading' : ''
|
||||
}
|
||||
`}>
|
||||
{('' + text).length > 27
|
||||
? text.substring(0, 27) + '...'
|
||||
: text}
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TableOfContentsPlugin() {
|
||||
return (
|
||||
<LexicalTableOfContents>
|
||||
{(tableOfContents) => {
|
||||
return <TableOfContentsList tableOfContents={tableOfContents} />;
|
||||
}}
|
||||
</LexicalTableOfContents>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
i.diagram-2 {
|
||||
background-image: url(../images/icons/diagram-2.svg);
|
||||
}
|
||||
i.horizontal-rule {
|
||||
background-image: url(../images/icons/horizontal-rule.svg);
|
||||
}
|
||||
i.image {
|
||||
background-image: url(../images/icons/file-image.svg);
|
||||
}
|
||||
|
||||
i.table {
|
||||
background-image: url(../images/icons/table.svg);
|
||||
}
|
||||
.icon.plus {
|
||||
background-image: url(../images/icons/plus.svg);
|
||||
}
|
|
@ -59,6 +59,7 @@ const supportedBlockTypes = new Set([
|
|||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"ul",
|
||||
"ol"
|
||||
]);
|
||||
|
@ -70,6 +71,7 @@ const blockTypeToBlockName = {
|
|||
h3: "三级标题",
|
||||
h4: "四级标题",
|
||||
h5: "五级标题",
|
||||
h6: "六级标题",
|
||||
ol: "有序序列",
|
||||
paragraph: "普通文本",
|
||||
quote: "引用",
|
||||
|
@ -720,14 +722,6 @@ export default function ToolbarPlugin() {
|
|||
<i className="icon horizontal-rule"/>
|
||||
<span className="text">分割线</span>
|
||||
</DropDownItem>
|
||||
{/*<DropDownItem*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* activeEditor.dispatchCommand(INSERT_PAGE_BREAK, undefined);*/}
|
||||
{/* }}*/}
|
||||
{/* className="item">*/}
|
||||
{/* <i className="icon page-break"/>*/}
|
||||
{/* <span className="text">页分割线</span>*/}
|
||||
{/*</DropDownItem>*/}
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
showModal('插入图片', (onClose) => (
|
||||
|
|
|
@ -7,7 +7,7 @@ import Hlexical from './Hlexical';
|
|||
import ItemTree from "../../components/ItemTree";
|
||||
import './index.less'
|
||||
import {useSelector, useDispatch} from "react-redux";
|
||||
import {addTableBarItem, removeTableBarItem, setActiveKey,updatedSavedFile} from "../../redux/tableBarItem_reducer"
|
||||
import {addTableBarItem, removeTableBarItem, setActiveKey,editLeftTableOfContents} from "../../redux/tableBarItem_reducer"
|
||||
|
||||
const {Sider} = Layout;
|
||||
const Note = () => {
|
||||
|
@ -31,13 +31,17 @@ const Note = () => {
|
|||
}, {
|
||||
key: '2',
|
||||
label: '标题',
|
||||
children: '开发中,尽情期待。。',
|
||||
children: <div id = "leftTableOfContents"></div>,
|
||||
forceRender:true
|
||||
}
|
||||
]
|
||||
const onChange = (newActiveKey) => {
|
||||
console.log("setActiveKey(newActiveKey)",newActiveKey)
|
||||
dispatch(setActiveKey({"activeKey":newActiveKey}));
|
||||
};
|
||||
const onChangeLeftTableOfContents = (activeKey)=>{
|
||||
dispatch(editLeftTableOfContents({"leftTableOfContents":activeKey}))
|
||||
}
|
||||
const add = () => {
|
||||
const newActiveKey = `newTab${newTabIndex.current++}`;
|
||||
dispatch(addTableBarItem(
|
||||
|
@ -90,8 +94,8 @@ const Note = () => {
|
|||
<Sider trigger={null} collapsedWidth={0} width="300px" collapsible collapsed={collapsed}
|
||||
// style={{overflow:"auto"}}
|
||||
>
|
||||
<Tabs id="itemTreeTabs" defaultActiveKey="1" items={itemTreeTab}
|
||||
// onChange={itemTreeOnChange}
|
||||
<Tabs id="itemTreeTabs" defaultActiveKey={useSelector(state => state.tableBarItem.leftTableOfContents)} items={itemTreeTab}
|
||||
onChange={onChangeLeftTableOfContents}
|
||||
style ={{background:"#fff"}}
|
||||
/>
|
||||
</Sider>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import {isEmpty} from "../utils/ObjectUtils";
|
||||
import {getFileFullNameByPath} from "../utils/PathOperate";
|
||||
|
||||
/**
|
||||
|
@ -16,7 +15,8 @@ export const tableBarItemSlice = createSlice({
|
|||
type:"tableBarItem",
|
||||
data: [],
|
||||
activeKey:"",
|
||||
expandedKeyList:[]
|
||||
expandedKeyList:[],
|
||||
leftTableOfContents:""
|
||||
},
|
||||
reducers: {
|
||||
addTableBarItem: (state, action) => {
|
||||
|
@ -82,6 +82,9 @@ export const tableBarItemSlice = createSlice({
|
|||
},
|
||||
removeExpandedKeys:(state, action)=>{
|
||||
state.expandedKeyList=state.expandedKeyList.filter(key=>key!==action.payload)
|
||||
},
|
||||
editLeftTableOfContents:(state, action)=>{
|
||||
state.leftTableOfContents=action.payload.leftTableOfContents
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -92,6 +95,7 @@ export const { addTableBarItem,
|
|||
setExpandedKeys,
|
||||
removeExpandedKeys,
|
||||
addExpandedKeys,
|
||||
updateFileName
|
||||
updateFileName,
|
||||
editLeftTableOfContents
|
||||
} = tableBarItemSlice.actions
|
||||
export default tableBarItemSlice.reducer
|
||||
|
|
Loading…
Reference in New Issue