Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions app/components/textEditor/editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import defaultTheme from "./theme/defaultTheme";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import ToolbarPlugin from "./plugins/ToolbarPlugin";
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
import { TableCellNode, TableNode, TableRowNode } from "@lexical/table";
import { ListItemNode, ListNode } from "@lexical/list";
import { CodeHighlightNode, CodeNode } from "@lexical/code";
import { AutoLinkNode, LinkNode } from "@lexical/link";
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
import { TRANSFORMERS } from "@lexical/markdown";
import ListMaxIndentLevelPlugin from "./plugins/ListMaxIndentLevelPlugin";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import CodeHighlightPlugin from "./plugins/CodeHighlightPlugin";
import AutoLinkPlugin from "./plugins/AutoLinkPlugin";
import styles from "../../styles/Editor.module.css"
import { $generateHtmlFromNodes } from '@lexical/html';
import {$generateNodesFromDOM} from '@lexical/html';
import { useEffect, } from "react";
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$insertNodes} from 'lexical'
import PropTypes from 'prop-types'

function Placeholder() {
return <div className={styles.editor_placeholder}>Enter some rich text...</div>;
}




const editorConfig = {
// The editor theme
theme: defaultTheme,
// Handling of errors during update
onError(error) {
throw error;
},
// Any custom nodes go here
nodes: [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
CodeHighlightNode,
TableNode,
TableCellNode,
TableRowNode,
AutoLinkNode,
LinkNode
],


};


function SetInitValue(props) {
const [editor] = useLexicalComposerContext();

useEffect(() => {
editor.update(() => {
if(!props.value) return;
const parser = new DOMParser();
const doc = parser.parseFromString(props.value, "text/html");
const nodes = $generateNodesFromDOM(editor , doc);
$insertNodes(nodes);
});
}, [props.value]);

return null;
}

export default function Editor({ name, value, onChange, placeholder }) {

const on_Change = (editorState, editor) => {
editor.update(() => {
const htmlString = $generateHtmlFromNodes(editor, null);
const customFormData = {
target: {
name: name,
value: htmlString
}}
onChange(customFormData);
});
}

return (
<LexicalComposer initialConfig={editorConfig}>
<div className={styles.editor_container}>
<ToolbarPlugin />
<div className={styles.editor_inner}>
<RichTextPlugin
contentEditable={<ContentEditable className={styles.editor_input} />}
placeholder={
placeholder ? placeholder : <Placeholder />}
ErrorBoundary={LexicalErrorBoundary}
/>

<HistoryPlugin />
<AutoFocusPlugin />
<CodeHighlightPlugin />
<ListPlugin />
<LinkPlugin />
<AutoLinkPlugin />
<ListMaxIndentLevelPlugin maxDepth={7} />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
<OnChangePlugin onChange={on_Change} ignoreSelectionChange/>
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
<SetInitValue value={value} />
</div>
</div>
</LexicalComposer>
);
}

Editor.propTypes = {
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
placeholder: PropTypes.string
};
34 changes: 34 additions & 0 deletions app/components/textEditor/plugins/AutoLinkPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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} />;
}
11 changes: 11 additions & 0 deletions app/components/textEditor/plugins/CodeHighlightPlugin.js
Original file line number Diff line number Diff line change
@@ -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;
}
68 changes: 68 additions & 0 deletions app/components/textEditor/plugins/ListMaxIndentLevelPlugin.js
Original file line number Diff line number Diff line change
@@ -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;
}
Loading