Reorganize files

This commit is contained in:
1ilit
2024-04-01 19:44:50 +03:00
parent d4bc5a9669
commit 9df9527950
28 changed files with 280 additions and 253 deletions

View File

@@ -12,14 +12,16 @@ import {
tableThemes,
defaultBlue,
State,
} from "../data/constants";
import useLayout from "../hooks/useLayout";
import useSettings from "../hooks/useSettings";
import useUndoRedo from "../hooks/useUndoRedo";
import useSelect from "../hooks/useSelect";
import useAreas from "../hooks/useAreas";
import useSaveState from "../hooks/useSaveState";
import useTransform from "../hooks/useTransform";
} from "../../data/constants";
import {
useLayout,
useSettings,
useUndoRedo,
useSelect,
useAreas,
useSaveState,
useTransform,
} from "../../hooks";
export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
const [hovered, setHovered] = useState(false);

View File

@@ -1,17 +1,17 @@
import { useRef, useState, useEffect } from "react";
import { Action, Cardinality, Constraint, ObjectType } from "../data/constants";
import { Action, Cardinality, Constraint, ObjectType } from "../../data/constants";
import { Toast } from "@douyinfe/semi-ui";
import Table from "./Table";
import Area from "./Area";
import Relationship from "./Relationship";
import Note from "./Note";
import useSettings from "../hooks/useSettings";
import useTransform from "../hooks/useTransform";
import useTables from "../hooks/useTables";
import useUndoRedo from "../hooks/useUndoRedo";
import useSelect from "../hooks/useSelect";
import useAreas from "../hooks/useAreas";
import useNotes from "../hooks/useNotes";
import useSettings from "../../hooks/useSettings";
import useTransform from "../../hooks/useTransform";
import useTables from "../../hooks/useTables";
import useUndoRedo from "../../hooks/useUndoRedo";
import useSelect from "../../hooks/useSelect";
import useAreas from "../../hooks/useAreas";
import useNotes from "../../hooks/useNotes";
export default function Canvas() {
const { tables, updateTable, relationships, addRelationship } = useTables();

View File

@@ -1,16 +1,16 @@
import { useState } from "react";
import { Action, ObjectType, noteThemes, Tab, State } from "../data/constants";
import { Action, ObjectType, noteThemes, Tab, State } from "../../data/constants";
import { Input, Button, Popover, Toast } from "@douyinfe/semi-ui";
import {
IconEdit,
IconDeleteStroked,
IconCheckboxTick,
} from "@douyinfe/semi-icons";
import useLayout from "../hooks/useLayout";
import useUndoRedo from "../hooks/useUndoRedo";
import useSelect from "../hooks/useSelect";
import useNotes from "../hooks/useNotes";
import useSaveState from "../hooks/useSaveState";
import useLayout from "../../hooks/useLayout";
import useUndoRedo from "../../hooks/useUndoRedo";
import useSelect from "../../hooks/useSelect";
import useNotes from "../../hooks/useNotes";
import useSaveState from "../../hooks/useSaveState";
export default function Note({ data, onMouseDown }) {
const w = 180;

View File

@@ -1,7 +1,7 @@
import { useRef } from "react";
import { Cardinality } from "../data/constants";
import useSettings from "../hooks/useSettings";
import { calcPath } from "../utils/calcPath";
import { Cardinality } from "../../data/constants";
import useSettings from "../../hooks/useSettings";
import { calcPath } from "../../utils/calcPath";
export default function Relationship({ data }) {
const { settings } = useSettings();

View File

@@ -6,7 +6,7 @@ import {
Tab,
Action,
ObjectType,
} from "../data/constants";
} from "../../data/constants";
import {
IconEdit,
IconMore,
@@ -31,13 +31,13 @@ import {
SideSheet,
Toast,
} from "@douyinfe/semi-ui";
import { getSize, hasCheck, hasPrecision, isSized } from "../utils/toSQL";
import useLayout from "../hooks/useLayout";
import useSettings from "../hooks/useSettings";
import useUndoRedo from "../hooks/useUndoRedo";
import useTables from "../hooks/useTables";
import useSelect from "../hooks/useSelect";
import useTypes from "../hooks/useTypes";
import { getSize, hasCheck, hasPrecision, isSized } from "../../utils/toSQL";
import useLayout from "../../hooks/useLayout";
import useSettings from "../../hooks/useSettings";
import useUndoRedo from "../../hooks/useUndoRedo";
import useTables from "../../hooks/useTables";
import useSelect from "../../hooks/useSelect";
import useTypes from "../../hooks/useTypes";
export default function Table(props) {
const [isHovered, setIsHovered] = useState(false);

View File

@@ -12,7 +12,7 @@ import {
IconEdit,
} from "@douyinfe/semi-icons";
import { Link, useNavigate } from "react-router-dom";
import icon from "../assets/icon_dark_64.png";
import icon from "../../assets/icon_dark_64.png";
import {
Button,
Divider,
@@ -31,9 +31,9 @@ import {
Select,
Checkbox,
} from "@douyinfe/semi-ui";
import timeLine from "../assets/process.png";
import timeLineDark from "../assets/process_dark.png";
import todo from "../assets/calendar.png";
import timeLine from "../../assets/process.png";
import timeLineDark from "../../assets/process_dark.png";
import todo from "../../assets/calendar.png";
import { toPng, toJpeg, toSvg } from "html-to-image";
import { saveAs } from "file-saver";
import {
@@ -42,32 +42,32 @@ import {
jsonToSQLite,
jsonToMariaDB,
jsonToSQLServer,
} from "../utils/toSQL";
import { IconAddTable, IconAddArea, IconAddNote } from "./CustomIcons";
import { ObjectType, Action, Tab, State, Cardinality } from "../data/constants";
} from "../../utils/toSQL";
import { IconAddTable, IconAddArea, IconAddNote } from "../CustomIcons";
import { ObjectType, Action, Tab, State, Cardinality } from "../../data/constants";
import jsPDF from "jspdf";
import { useHotkeys } from "react-hotkeys-hook";
import { Validator } from "jsonschema";
import { areaSchema, noteSchema, tableSchema } from "../data/schemas";
import { areaSchema, noteSchema, tableSchema } from "../../data/schemas";
import Editor from "@monaco-editor/react";
import { db } from "../data/db";
import { db } from "../../data/db";
import { useLiveQuery } from "dexie-react-hooks";
import { Parser } from "node-sql-parser";
import Todo from "./Todo";
import { Thumbnail } from "./Thumbnail";
import useLayout from "../hooks/useLayout";
import useSettings from "../hooks/useSettings";
import useTransform from "../hooks/useTransform";
import useTables from "../hooks/useTables";
import useUndoRedo from "../hooks/useUndoRedo";
import useSelect from "../hooks/useSelect";
import { enterFullscreen, exitFullscreen } from "../utils/fullscreen";
import { ddbDiagramIsValid, jsonDiagramIsValid } from "../utils/validateSchema";
import { dataURItoBlob } from "../utils/utils";
import useAreas from "../hooks/useAreas";
import useNotes from "../hooks/useNotes";
import useTypes from "../hooks/useTypes";
import useSaveState from "../hooks/useSaveState";
import useLayout from "../../hooks/useLayout";
import useSettings from "../../hooks/useSettings";
import useTransform from "../../hooks/useTransform";
import useTables from "../../hooks/useTables";
import useUndoRedo from "../../hooks/useUndoRedo";
import useSelect from "../../hooks/useSelect";
import { enterFullscreen, exitFullscreen } from "../../utils/fullscreen";
import { ddbDiagramIsValid, jsonDiagramIsValid } from "../../utils/validateSchema";
import { dataURItoBlob } from "../../utils/utils";
import useAreas from "../../hooks/useAreas";
import useNotes from "../../hooks/useNotes";
import useTypes from "../../hooks/useTypes";
import useSaveState from "../../hooks/useSaveState";
export default function ControlPanel({
diagramId,

View File

@@ -1,4 +1,4 @@
import { calcPath } from "../utils/calcPath";
import { calcPath } from "../../utils/calcPath";
export function Thumbnail({ diagram, i, zoom }) {
const translateX = 32 * zoom;

View File

@@ -19,9 +19,9 @@ import {
IconDeleteStroked,
IconCaretdown,
} from "@douyinfe/semi-icons";
import { State } from "../data/constants";
import useTasks from "../hooks/useTasks";
import useSaveState from "../hooks/useSaveState";
import { State } from "../../data/constants";
import useTasks from "../../hooks/useTasks";
import useSaveState from "../../hooks/useSaveState";
const Priority = {
NONE: 0,

View File

@@ -20,10 +20,10 @@ import {
Action,
ObjectType,
State,
} from "../data/constants";
import useUndoRedo from "../hooks/useUndoRedo";
import useAreas from "../hooks/useAreas";
import useSaveState from "../hooks/useSaveState";
} from "../../data/constants";
import useUndoRedo from "../../hooks/useUndoRedo";
import useAreas from "../../hooks/useAreas";
import useSaveState from "../../hooks/useSaveState";
import Empty from "./Empty";
export default function AreasOverview() {

View File

@@ -1,10 +1,10 @@
import { useState, useEffect } from "react";
import { Collapse, Badge } from "@douyinfe/semi-ui";
import { arrayIsEqual } from "../utils/utils";
import { getIssues } from "../utils/issues";
import useSettings from "../hooks/useSettings";
import useTables from "../hooks/useTables";
import useTypes from "../hooks/useTypes";
import { arrayIsEqual } from "../../utils/utils";
import { getIssues } from "../../utils/issues";
import useSettings from "../../hooks/useSettings";
import useTables from "../../hooks/useTables";
import useTypes from "../../hooks/useTypes";
export default function Issues() {
const { settings } = useSettings();

View File

@@ -16,9 +16,9 @@ import {
IconSearch,
IconCheckboxTick,
} from "@douyinfe/semi-icons";
import { noteThemes, Action, ObjectType } from "../data/constants";
import useUndoRedo from "../hooks/useUndoRedo";
import useNotes from "../hooks/useNotes";
import { noteThemes, Action, ObjectType } from "../../data/constants";
import useUndoRedo from "../../hooks/useUndoRedo";
import useNotes from "../../hooks/useNotes";
import Empty from "./Empty";
export default function NotesOverview() {

View File

@@ -15,9 +15,9 @@ import {
IconMore,
IconSearch,
} from "@douyinfe/semi-icons";
import { Cardinality, Constraint, Action, ObjectType } from "../data/constants";
import useTables from "../hooks/useTables";
import useUndoRedo from "../hooks/useUndoRedo";
import { Cardinality, Constraint, Action, ObjectType } from "../../data/constants";
import useTables from "../../hooks/useTables";
import useUndoRedo from "../../hooks/useUndoRedo";
import Empty from "./Empty";
export default function RelationshipsOverview() {

View File

@@ -1,13 +1,13 @@
import { Tabs } from "@douyinfe/semi-ui";
import { Tab } from "../data/constants";
import { Tab } from "../../data/constants";
import TablesOverview from "./TablesOverview";
import RelationshipsOverview from "./RelationshipsOverview";
import AreasOverview from "./AreasOverview";
import NotesOverview from "./NotesOverview";
import TypesOverview from "./TypesOverview";
import Issues from "./Issues";
import useLayout from "../hooks/useLayout";
import useSelect from "../hooks/useSelect";
import useLayout from "../../hooks/useLayout";
import useSelect from "../../hooks/useSelect";
export default function SidePanel({ width, resize, setResize }) {
const { layout } = useLayout();

View File

@@ -5,7 +5,7 @@ import {
defaultBlue,
sqlDataTypes,
tableThemes,
} from "../data/constants";
} from "../../data/constants";
import {
Collapse,
Row,
@@ -30,11 +30,11 @@ import {
IconPlus,
IconSearch,
} from "@douyinfe/semi-icons";
import { getSize, hasCheck, hasPrecision, isSized } from "../utils/toSQL";
import useTables from "../hooks/useTables";
import useUndoRedo from "../hooks/useUndoRedo";
import useSelect from "../hooks/useSelect";
import useTypes from "../hooks/useTypes";
import { getSize, hasCheck, hasPrecision, isSized } from "../../utils/toSQL";
import useTables from "../../hooks/useTables";
import useUndoRedo from "../../hooks/useUndoRedo";
import useSelect from "../../hooks/useSelect";
import useTypes from "../../hooks/useTypes";
import NoElements from "./Empty";
export default function TablesOverview() {

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { Action, ObjectType, sqlDataTypes } from "../data/constants";
import { Action, ObjectType, sqlDataTypes } from "../../data/constants";
import {
Collapse,
Row,
@@ -22,9 +22,9 @@ import {
IconInfoCircle,
IconMore,
} from "@douyinfe/semi-icons";
import { isSized, hasPrecision, getSize } from "../utils/toSQL";
import useUndoRedo from "../hooks/useUndoRedo";
import useTypes from "../hooks/useTypes";
import { isSized, hasPrecision, getSize } from "../../utils/toSQL";
import useUndoRedo from "../../hooks/useUndoRedo";
import useTypes from "../../hooks/useTypes";
import NoElements from "./Empty";
export default function TypesOverview() {

View File

@@ -3,7 +3,7 @@ import useTransform from "../hooks/useTransform";
import useLayout from "../hooks/useLayout";
import { exitFullscreen } from "../utils/fullscreen";
export default function Controls() {
export default function FloatingControls() {
const { transform, setTransform } = useTransform();
const { setLayout } = useLayout();

View File

@@ -0,0 +1,36 @@
import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin";
const URL_MATCHER =
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
const EMAIL_MATCHER =
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
const MATCHERS = [
(text) => {
const match = URL_MATCHER.exec(text);
return (
match && {
index: match.index,
length: match[0].length,
text: match[0],
url: match[0],
}
);
},
(text) => {
const match = EMAIL_MATCHER.exec(text);
return (
match && {
index: match.index,
length: match[0].length,
text: match[0],
url: `mailto:${match[0]}`,
}
);
},
];
export default function PlaygroundAutoLinkPlugin() {
return <AutoLinkPlugin matchers={MATCHERS} />;
}

View File

@@ -0,0 +1,11 @@
import { registerCodeHighlighting } from "@lexical/code";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useEffect } from "react";
export default function CodeHighlightPlugin() {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return registerCodeHighlighting(editor);
}, [editor]);
return null;
}

View File

@@ -0,0 +1,68 @@
import { $getListDepth, $isListItemNode, $isListNode } from "@lexical/list";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import {
$getSelection,
$isElementNode,
$isRangeSelection,
INDENT_CONTENT_COMMAND,
COMMAND_PRIORITY_HIGH,
} from "lexical";
import { useEffect } from "react";
function getElementNodesInSelection(selection) {
const nodesInSelection = selection.getNodes();
if (nodesInSelection.length === 0) {
return new Set([
selection.anchor.getNode().getParentOrThrow(),
selection.focus.getNode().getParentOrThrow(),
]);
}
return new Set(
nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow()))
);
}
function isIndentPermitted(maxDepth) {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
const elementNodesInSelection = getElementNodesInSelection(selection);
let totalDepth = 0;
for (const elementNode of elementNodesInSelection) {
if ($isListNode(elementNode)) {
totalDepth = Math.max($getListDepth(elementNode) + 1, totalDepth);
} else if ($isListItemNode(elementNode)) {
const parent = elementNode.getParent();
if (!$isListNode(parent)) {
throw new Error(
"ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent."
);
}
totalDepth = Math.max($getListDepth(parent) + 1, totalDepth);
}
}
return totalDepth <= maxDepth;
}
export default function ListMaxIndentLevelPlugin({ maxDepth }) {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return editor.registerCommand(
INDENT_CONTENT_COMMAND,
() => !isIndentPermitted(maxDepth ?? 7),
COMMAND_PRIORITY_HIGH
);
}, [editor, maxDepth]);
return null;
}

View File

@@ -8,11 +8,11 @@ import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPl
import { ClearEditorPlugin } from "@lexical/react/LexicalClearEditorPlugin";
import { TRANSFORMERS } from "@lexical/markdown";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import ToolbarPlugin from "../plugins/ToolbarPlugin";
import ListMaxIndentLevelPlugin from "../plugins/ListMaxIndentLevelPlugin";
import CodeHighlightPlugin from "../plugins/CodeHighlightPlugin";
import AutoLinkPlugin from "../plugins/AutoLinkPlugin";
import "../styles/richeditor.css";
import ToolbarPlugin from "./ToolbarPlugin";
import ListMaxIndentLevelPlugin from "./ListMaxIndentLevelPlugin";
import CodeHighlightPlugin from "./CodeHighlightPlugin";
import AutoLinkPlugin from "./AutoLinkPlugin";
import "./styles/index.css";
function Placeholder({ text }) {
return <div className="editor-placeholder">{text || ""}</div>;

View File

@@ -0,0 +1,630 @@
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
CAN_REDO_COMMAND,
CAN_UNDO_COMMAND,
REDO_COMMAND,
UNDO_COMMAND,
SELECTION_CHANGE_COMMAND,
FORMAT_TEXT_COMMAND,
FORMAT_ELEMENT_COMMAND,
$getSelection,
$isRangeSelection,
$createParagraphNode,
$getNodeByKey,
} from "lexical";
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
import {
$isParentElementRTL,
$wrapNodes,
$isAtNodeEnd,
} from "@lexical/selection";
import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils";
import {
INSERT_ORDERED_LIST_COMMAND,
INSERT_UNORDERED_LIST_COMMAND,
REMOVE_LIST_COMMAND,
$isListNode,
ListNode,
} from "@lexical/list";
import { createPortal } from "react-dom";
import {
$createHeadingNode,
$createQuoteNode,
$isHeadingNode,
} from "@lexical/rich-text";
import {
$createCodeNode,
$isCodeNode,
getDefaultCodeLanguage,
getCodeLanguages,
} from "@lexical/code";
import { Dropdown } from "@douyinfe/semi-ui";
import "./styles/index.css";
const LowPriority = 1;
const blockTypeToIcon = {
code: "bi-code-slash",
h1: "bi-type-h1",
h2: "bi-type-h2",
ol: "bi-list-ol",
paragraph: "bi-text-paragraph",
quote: "bi-chat-square-quote",
ul: "bi-list-ul",
};
const blockTypeToBlockName = {
paragraph: "Paragraph",
h1: "Large Heading",
h2: "Small Heading",
ul: "Bulleted List",
ol: "Numbered List",
code: "Code Block",
quote: "Quote",
};
function Divider() {
return <div className="divider" />;
}
function positionEditorElement(editor, rect) {
if (rect === null) {
editor.style.opacity = "0";
editor.style.top = "-1000px";
editor.style.left = "-1000px";
} else {
editor.style.opacity = "1";
editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`;
editor.style.left = `${
rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2
}px`;
}
}
function FloatingLinkEditor({ editor }) {
const editorRef = useRef(null);
const inputRef = useRef(null);
const mouseDownRef = useRef(false);
const [linkUrl, setLinkUrl] = useState("");
const [isEditMode, setEditMode] = useState(false);
const [lastSelection, setLastSelection] = useState(null);
const updateLinkEditor = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const node = getSelectedNode(selection);
const parent = node.getParent();
if ($isLinkNode(parent)) {
setLinkUrl(parent.getURL());
} else if ($isLinkNode(node)) {
setLinkUrl(node.getURL());
} else {
setLinkUrl("");
}
}
const editorElem = editorRef.current;
const nativeSelection = window.getSelection();
const activeElement = document.activeElement;
if (editorElem === null) {
return;
}
const rootElement = editor.getRootElement();
if (
selection !== null &&
!nativeSelection.isCollapsed &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
const domRange = nativeSelection.getRangeAt(0);
let rect;
if (nativeSelection.anchorNode === rootElement) {
let inner = rootElement;
while (inner.firstElementChild != null) {
inner = inner.firstElementChild;
}
rect = inner.getBoundingClientRect();
} else {
rect = domRange.getBoundingClientRect();
}
if (!mouseDownRef.current) {
positionEditorElement(editorElem, rect);
}
setLastSelection(selection);
} else if (!activeElement || activeElement.className !== "link-input") {
positionEditorElement(editorElem, null);
setLastSelection(null);
setEditMode(false);
setLinkUrl("");
}
return true;
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateLinkEditor();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateLinkEditor();
return true;
},
LowPriority
)
);
}, [editor, updateLinkEditor]);
useEffect(() => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
}, [editor, updateLinkEditor]);
useEffect(() => {
if (isEditMode && inputRef.current) {
inputRef.current.focus();
}
}, [isEditMode]);
return (
<div ref={editorRef} className="link-editor">
{isEditMode ? (
<input
ref={inputRef}
className="link-input"
value={linkUrl}
onChange={(event) => {
setLinkUrl(event.target.value);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
if (lastSelection !== null) {
if (linkUrl !== "") {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
}
setEditMode(false);
}
} else if (event.key === "Escape") {
event.preventDefault();
setEditMode(false);
}
}}
/>
) : (
<>
<div className="link-input">
<a href={linkUrl} target="_blank" rel="noopener noreferrer">
{linkUrl}
</a>
</div>
</>
)}
</div>
);
}
function Select({ onChange, className, options, value }) {
return (
<select className={className} onChange={onChange} value={value}>
<option hidden={true} value="" />
{options.map((option) => (
<option className="option" key={option} value={option}>
{option}
</option>
))}
</select>
);
}
function getSelectedNode(selection) {
const anchor = selection.anchor;
const focus = selection.focus;
const anchorNode = selection.anchor.getNode();
const focusNode = selection.focus.getNode();
if (anchorNode === focusNode) {
return anchorNode;
}
const isBackward = selection.isBackward();
if (isBackward) {
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
} else {
return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
}
}
function BlockOptionsDropdownList({ editor, blockType }) {
const formatParagraph = () => {
if (blockType !== "paragraph") {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createParagraphNode());
}
});
}
};
const formatLargeHeading = () => {
if (blockType !== "h1") {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createHeadingNode("h1"));
}
});
}
};
const formatSmallHeading = () => {
if (blockType !== "h2") {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createHeadingNode("h2"));
}
});
}
};
const formatBulletList = () => {
if (blockType !== "ul") {
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND);
}
};
const formatNumberedList = () => {
if (blockType !== "ol") {
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND);
}
};
const formatQuote = () => {
if (blockType !== "quote") {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createQuoteNode());
}
});
}
};
const formatCode = () => {
if (blockType !== "code") {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createCodeNode());
}
});
}
};
return (
<Dropdown
trigger="click"
clickToHide
render={
<Dropdown.Menu>
<Dropdown.Item
onClick={formatParagraph}
icon={<i className={`bi ${blockTypeToIcon.paragraph}`} />}
>
Paragraph
</Dropdown.Item>
<Dropdown.Item
onClick={formatLargeHeading}
icon={<i className={`bi ${blockTypeToIcon.h1}`} />}
>
Large Heading
</Dropdown.Item>
<Dropdown.Item
onClick={formatSmallHeading}
icon={<i className={`bi ${blockTypeToIcon.h2}`} />}
>
Small Heading
</Dropdown.Item>
<Dropdown.Item
onClick={formatBulletList}
icon={<i className={`bi ${blockTypeToIcon.ul}`} />}
>
Bullet List
</Dropdown.Item>
<Dropdown.Item
onClick={formatNumberedList}
icon={<i className={`bi ${blockTypeToIcon.ol}`} />}
>
Numbered List
</Dropdown.Item>
<Dropdown.Item
onClick={formatQuote}
icon={<i className={`bi ${blockTypeToIcon.quote}`} />}
>
Quote
</Dropdown.Item>
<Dropdown.Item
onClick={formatCode}
icon={<i className={`bi ${blockTypeToIcon.code}`} />}
>
Code Block
</Dropdown.Item>
</Dropdown.Menu>
}
>
<button
className="flex mx-2 justify-center items-center"
aria-label="Formatting Options"
>
<i className={`bi ${blockTypeToIcon[blockType]} me-3`} />
<span className="me-3 text-sm">{blockTypeToBlockName[blockType]}</span>
<i className="bi bi-chevron-down" />
</button>
</Dropdown>
);
}
export default function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const toolbarRef = useRef(null);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const [blockType, setBlockType] = useState("paragraph");
const [selectedElementKey, setSelectedElementKey] = useState(null);
const [codeLanguage, setCodeLanguage] = useState("");
const [, setIsRTL] = useState(false);
const [isLink, setIsLink] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const [isCode, setIsCode] = useState(false);
const updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode();
const element =
anchorNode.getKey() === "root"
? anchorNode
: anchorNode.getTopLevelElementOrThrow();
const elementKey = element.getKey();
const elementDOM = editor.getElementByKey(elementKey);
if (elementDOM !== null) {
setSelectedElementKey(elementKey);
if ($isListNode(element)) {
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
const type = parentList ? parentList.getTag() : element.getTag();
setBlockType(type);
} else {
const type = $isHeadingNode(element)
? element.getTag()
: element.getType();
setBlockType(type);
if ($isCodeNode(element)) {
setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage());
}
}
}
setIsBold(selection.hasFormat("bold"));
setIsItalic(selection.hasFormat("italic"));
setIsUnderline(selection.hasFormat("underline"));
setIsStrikethrough(selection.hasFormat("strikethrough"));
setIsCode(selection.hasFormat("code"));
setIsRTL($isParentElementRTL(selection));
const node = getSelectedNode(selection);
const parent = node.getParent();
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true);
} else {
setIsLink(false);
}
}
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateToolbar();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateToolbar();
return false;
},
LowPriority
),
editor.registerCommand(
CAN_UNDO_COMMAND,
(payload) => {
setCanUndo(payload);
return false;
},
LowPriority
),
editor.registerCommand(
CAN_REDO_COMMAND,
(payload) => {
setCanRedo(payload);
return false;
},
LowPriority
)
);
}, [editor, updateToolbar]);
const codeLanguges = useMemo(() => getCodeLanguages(), []);
const onCodeLanguageSelect = useCallback(
(e) => {
editor.update(() => {
if (selectedElementKey !== null) {
const node = $getNodeByKey(selectedElementKey);
if ($isCodeNode(node)) {
node.setLanguage(e.target.value);
}
}
});
},
[editor, selectedElementKey]
);
const insertLink = useCallback(() => {
if (!isLink) {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://");
} else {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}
}, [editor, isLink]);
return (
<div className="toolbar" ref={toolbarRef}>
<button
disabled={!canUndo}
onClick={() => editor.dispatchCommand(UNDO_COMMAND)}
className="toolbar-item spaced"
aria-label="Undo"
>
<i
className={`bi bi-arrow-counterclockwise ${
canUndo ? "" : "opacity-30"
}`}
/>
</button>
<button
disabled={!canRedo}
onClick={() => editor.dispatchCommand(REDO_COMMAND)}
className="toolbar-item"
aria-label="Redo"
>
<i className={`bi bi-arrow-clockwise ${canRedo ? "" : "opacity-30"}`} />
</button>
<Divider />
<BlockOptionsDropdownList editor={editor} blockType={blockType} />
<Divider />
{blockType === "code" ? (
<div className="flex items-center">
<Select
className="toolbar-item code-language"
onChange={onCodeLanguageSelect}
options={codeLanguges}
value={codeLanguage}
/>
<i className="bi bi-chevron-down" />
</div>
) : (
<>
<button
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")}
className={"toolbar-item spaced " + (isBold ? "active" : "")}
aria-label="Format Bold"
>
<i className="bi bi-type-bold" />
</button>
<button
onClick={() =>
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic")
}
className={"toolbar-item spaced " + (isItalic ? "active" : "")}
aria-label="Format Italics"
>
<i className="bi bi-type-italic" />
</button>
<button
onClick={() =>
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline")
}
className={"toolbar-item spaced " + (isUnderline ? "active" : "")}
aria-label="Format Underline"
>
<i className="bi bi-type-underline" />
</button>
<button
onClick={() =>
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough")
}
className={
"toolbar-item spaced " + (isStrikethrough ? "active" : "")
}
aria-label="Format Strikethrough"
>
<i className="bi bi-type-strikethrough" />
</button>
<button
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code")}
className={"toolbar-item spaced " + (isCode ? "active" : "")}
aria-label="Insert Code"
>
<i className="bi bi-code-slash" />
</button>
<button
onClick={insertLink}
className={"toolbar-item spaced " + (isLink ? "active" : "")}
aria-label="Insert Link"
>
<i className="bi bi-link" />
</button>
{isLink &&
createPortal(
<FloatingLinkEditor editor={editor} />,
document.body
)}
<Divider />
<button
onClick={() =>
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "left")
}
className="toolbar-item spaced"
aria-label="Left Align"
>
<i className="bi bi-text-left" />
</button>
<button
onClick={() =>
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "center")
}
className="toolbar-item spaced"
aria-label="Center Align"
>
<i className="bi bi-text-center" />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "right");
}}
className="toolbar-item spaced"
aria-label="Right Align"
>
<i className="bi bi-text-right" />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "justify");
}}
className="toolbar-item"
aria-label="Justify Align"
>
<i className="bi bi-justify" />
</button>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,391 @@
.ltr {
text-align: left;
}
.rtl {
text-align: right;
}
.editor-container {
margin: 16px auto 16px auto;
border-radius: 6px;
color: var(--semi-color-text-1);
background-color: rgba(var(--semi-grey-1), 1);
position: relative;
line-height: 20px;
font-weight: 400;
text-align: left;
}
.editor-inner {
background-color: rgba(var(--semi-grey-1), 1);
position: relative;
border-radius: 6px;
}
.editor-input {
min-height: 160px;
resize: none;
font-size: 15px;
position: relative;
tab-size: 1;
outline: 0;
padding: 15px 10px;
}
.editor-placeholder {
color: #999;
position: absolute;
text-overflow: ellipsis;
top: 15px;
left: 10px;
font-size: 15px;
user-select: none;
display: inline-block;
pointer-events: none;
}
.editor-text-bold {
font-weight: bold;
}
.editor-text-italic {
font-style: italic;
}
.editor-text-underline {
text-decoration: underline;
}
.editor-text-strikethrough {
text-decoration: line-through;
}
.editor-text-underlineStrikethrough {
text-decoration: underline line-through;
}
.editor-text-code {
background-color: rgba(var(--semi-grey-2), 1);
padding: 1px 0.25rem;
font-family: Menlo, Consolas, Monaco, monospace;
font-size: 94%;
}
.editor-link {
color: rgb(33, 111, 219);
text-decoration: none;
}
.editor-code {
background-color: rgba(var(--semi-grey-0), 1);
font-family: Menlo, Consolas, Monaco, monospace;
display: block;
padding: 8px 8px 8px 52px;
line-height: 1.53;
font-size: 13px;
margin: 0;
margin-top: 8px;
margin-bottom: 8px;
tab-size: 2;
overflow-x: auto;
position: relative;
}
.editor-code:before {
content: attr(data-gutter);
position: absolute;
background-color: rgba(var(--semi-grey-0), 1);
left: 0;
top: 0;
border-right: 1px solid rgba(var(--semi-grey-3), 1);
padding: 8px;
color: #777;
white-space: pre-wrap;
text-align: right;
min-width: 25px;
}
.editor-code:after {
content: attr(data-highlight-language);
top: 0;
right: 3px;
padding: 3px;
font-size: 10px;
text-transform: uppercase;
position: absolute;
color: rgba(var(--semi-text-1), 1);
}
.editor-tokenComment {
color: slategray;
}
.editor-tokenPunctuation {
color: #999;
}
.editor-tokenProperty {
color: #905;
}
.editor-tokenSelector {
color: #690;
}
.editor-tokenOperator {
color: #9a6e3a;
}
.editor-tokenAttr {
color: #07a;
}
.editor-tokenVariable {
color: #e90;
}
.editor-tokenFunction {
color: #dd4a68;
}
.editor-paragraph {
margin: 0;
margin-bottom: 8px;
position: relative;
}
.editor-paragraph:last-child {
margin-bottom: 0;
}
.editor-heading-h1 {
font-size: 24px;
margin: 0;
margin-bottom: 12px;
padding: 0;
}
.editor-heading-h2 {
font-size: 16px;
margin: 0;
margin-top: 10px;
padding: 0;
}
.editor-quote {
margin: 0;
margin-left: 20px;
font-size: 15px;
color: rgb(101, 103, 107);
border-left-color: rgb(206, 208, 212);
border-left-width: 4px;
border-left-style: solid;
padding-left: 16px;
}
.editor-list-ol {
padding: 0;
margin: 0;
margin-left: 16px;
list-style-type: decimal;
}
.editor-list-ul {
padding: 0;
margin: 0;
margin-left: 16px;
list-style-type: circle;
}
.editor-listitem {
margin: 8px 32px 8px 32px;
}
.editor-nested-listitem {
list-style-type: none;
}
pre::-webkit-scrollbar {
background: transparent;
width: 10px;
}
pre::-webkit-scrollbar-thumb {
background: #999;
}
.toolbar {
display: flex;
flex-wrap: wrap;
margin-bottom: 1px;
background-color: rgba(var(--semi-grey-1), 1);
padding: 4px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom: rgba(var(--semi-grey-2), 1) solid 2px;
vertical-align: middle;
}
.toolbar button.toolbar-item {
border: 0;
display: flex;
border-radius: 6px;
padding: 8px;
cursor: pointer;
vertical-align: middle;
fill: currentColor;
}
.toolbar button.toolbar-item:disabled {
cursor: not-allowed;
}
.toolbar button.toolbar-item.spaced {
margin-right: 2px;
}
.toolbar button.toolbar-item i.format {
background-size: contain;
display: inline-block;
height: 18px;
width: 18px;
margin-top: 2px;
vertical-align: -0.25em;
display: flex;
opacity: 1;
fill: currentColor;
}
.toolbar button.toolbar-item:disabled i.format {
opacity: 0.4;
}
.toolbar button.toolbar-item.active {
background-color: rgba(var(--semi-grey-2), 1);
}
.toolbar button.toolbar-item.active i {
opacity: 1;
}
.toolbar .toolbar-item:hover:not([disabled]) {
background-color: rgba(var(--semi-grey-2), 1);
}
.toolbar .divider {
width: 2px;
background-color: rgba(var(--semi-grey-2), 1);
margin: 4px 4px;
}
.toolbar select.toolbar-item {
border-radius: 6px;
padding: 8px;
vertical-align: middle;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
font-size: 14px;
background-color: rgba(var(--semi-grey-1), 1);
text-overflow: ellipsis;
outline: none;
}
.toolbar select.code-language {
text-transform: capitalize;
width: 130px;
}
.toolbar .toolbar-item .text {
line-height: 20px;
width: 200px;
vertical-align: middle;
font-size: 14px;
color: rgba(var(--semi-text-1), 1);
text-overflow: ellipsis;
width: 70px;
overflow: hidden;
height: 20px;
text-align: left;
}
.toolbar .toolbar-item .icon {
width: 20px;
height: 20px;
user-select: none;
margin-right: 8px;
line-height: 16px;
background-size: contain;
}
.link-editor {
position: absolute;
z-index: 100;
top: -10000px;
left: -10000px;
margin-top: -6px;
max-width: 300px;
width: 100%;
opacity: 0;
background-color: rgba(var(--semi-grey-1), 1);
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
border-radius: 6px;
transition: opacity 0.5s;
}
.link-editor .link-input {
display: block;
width: calc(100% - 24px);
box-sizing: border-box;
margin: 8px 12px;
padding: 8px 12px;
border-radius: 6px;
background-color: rgba(var(--semi-grey-2), 1);
font-size: 15px;
color: var(--semi-color-text-1);
border: 0;
outline: 0;
position: relative;
font-family: inherit;
}
.link-editor .link-input a {
color: rgb(33, 111, 219);
text-decoration: none;
display: block;
white-space: nowrap;
overflow: hidden;
margin-right: 30px;
text-overflow: ellipsis;
}
.link-editor .link-input a:hover {
text-decoration: underline;
}
.link-editor .button {
width: 20px;
height: 20px;
display: inline-block;
padding: 6px;
border-radius: 8px;
cursor: pointer;
margin: 0 2px;
}
.link-editor .button.hovered {
width: 20px;
height: 20px;
display: inline-block;
background-color: #eee;
}
.link-editor .button i,
.actions i {
background-size: contain;
display: inline-block;
height: 20px;
width: 20px;
vertical-align: -0.25em;
}

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from "react";
import ControlPanel from "../components/ControlPanel";
import Canvas from "../components/Canvas";
import SidePanel from "../components/SidePanel";
import ControlPanel from "./EditorHeader/ControlPanel";
import Canvas from "./EditorCanvas/Canvas";
import SidePanel from "./EditorSidePanel/SidePanel";
import { State } from "../data/constants";
import { db } from "../data/db";
import useLayout from "../hooks/useLayout";
@@ -9,7 +9,7 @@ import useSettings from "../hooks/useSettings";
import useTransform from "../hooks/useTransform";
import useTables from "../hooks/useTables";
import useUndoRedo from "../hooks/useUndoRedo";
import Controls from "../components/Controls";
import FloatingControls from "./FloatingControls";
import useAreas from "../hooks/useAreas";
import useNotes from "../hooks/useNotes";
import useTypes from "../hooks/useTypes";
@@ -302,7 +302,7 @@ export default function WorkSpace() {
<Canvas saveState={saveState} setSaveState={setSaveState} />
{!(layout.sidebar || layout.toolbar || layout.header) && (
<div className="fixed right-5 bottom-4">
<Controls />
<FloatingControls />
</div>
)}
</div>