Skip to content

Commit

Permalink
added support for rich text editor
Browse files Browse the repository at this point in the history
  • Loading branch information
akanshaaa19 committed Feb 10, 2025
1 parent f48b843 commit b82e13e
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 52 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@emotion/styled": "^11.13.0",
"@glific/flow-editor": "^1.26.3-18",
"@lexical/react": "^0.17.1",
"@lexical/utils": "^0.24.0",
"@mui/icons-material": "^6.1.1",
"@mui/material": "^6.1.1",
"@mui/x-date-pickers": "^7.18.0",
Expand Down
2 changes: 1 addition & 1 deletion src/common/RichEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const handleFormatterEvents = (event: KeyboardEvent) => {
return '';
};

export const handleFormatting = (text: string, formatter: string) => {
export const handleFormatting = (text: string = '', formatter: string) => {
switch (formatter) {
case 'bold':
return `*${text}*`;
Expand Down
39 changes: 36 additions & 3 deletions src/components/UI/Form/EmojiInput/Editor.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@

.Editor {
position: relative;
border: 2px solid rgba(0, 0, 0, 0.23);
border-radius: 12px;

height: 10rem;
position: relative;
padding: 1rem;
Expand Down Expand Up @@ -47,7 +46,9 @@
}

.EditorWrapper {
margin: 1rem 0;
/* margin: 1rem 0; */
border: 2px solid rgba(0, 0, 0, 0.23);
border-radius: 12px;
}

.editorScroller {
Expand Down Expand Up @@ -157,3 +158,35 @@
.MentionMenu li:hover {
background-color: #eee;
}


.FormatingOptions {
border-bottom: 1px solid #ccc;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
display: flex;
padding: 0.2rem;
}

.FormatingOptions span {
border: 0;
display: flex;
background: none;
border-radius: 10px;
padding: 8px;
cursor: pointer;
vertical-align: middle;
flex-shrink: 0;
align-items: center;
justify-content: space-between;
color: #5F5F5F;

}

.Active {
background-color: #eee !important;
}

.FormatingOptions span:hover {
background-color: #eee;
}
222 changes: 178 additions & 44 deletions src/components/UI/Form/EmojiInput/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import styles from './Editor.module.css';
import { forwardRef, useEffect } from 'react';
import { forwardRef, useEffect, useState } from 'react';
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { $getSelection, $createTextNode, $getRoot, KEY_DOWN_COMMAND, COMMAND_PRIORITY_LOW } from 'lexical';
import {
$getSelection,
$createTextNode,
$getRoot,
KEY_DOWN_COMMAND,
COMMAND_PRIORITY_LOW,
FORMAT_TEXT_COMMAND,
$createRangeSelection,
$setSelection,
$isRangeSelection,
} from 'lexical';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
Expand All @@ -14,6 +24,8 @@ import {
BeautifulMentionsMenuItemProps,
} from 'lexical-beautiful-mentions';
import { handleFormatterEvents, handleFormatting, setDefaultValue } from 'common/RichEditor';
import { FormatBold, FormatItalic, StrikethroughS } from '@mui/icons-material';
import { mergeRegister } from '@lexical/utils';

export interface EditorProps {
field: { name: string; onChange?: any; value: any; onBlur?: any };
Expand Down Expand Up @@ -46,22 +58,49 @@ export const Editor = ({ disabled = false, ...props }: EditorProps) => {
};

useEffect(() => {
return editor.registerCommand(
KEY_DOWN_COMMAND,
(event: KeyboardEvent) => {
let formatter = handleFormatterEvents(event);

editor.update(() => {
const selection = $getSelection();
if (selection?.getTextContent() && formatter) {
const text = handleFormatting(selection?.getTextContent(), formatter);
const newNode = $createTextNode(text);
selection?.insertNodes([newNode]);
}
});
return false;
},
COMMAND_PRIORITY_LOW
return mergeRegister(
editor.registerCommand(
KEY_DOWN_COMMAND,
(event: KeyboardEvent) => {
let formatter = handleFormatterEvents(event);

editor.update(() => {
const selection = $getSelection();
if (selection?.getTextContent() && formatter) {
const text = handleFormatting(selection?.getTextContent(), formatter);
const newNode = $createTextNode(text);
selection?.insertNodes([newNode]);

Check warning on line 72 in src/components/UI/Form/EmojiInput/Editor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/EmojiInput/Editor.tsx#L70-L72

Added lines #L70 - L72 were not covered by tests
}
});
return false;
},
COMMAND_PRIORITY_LOW
),
editor.registerCommand(
FORMAT_TEXT_COMMAND,
(event: any) => {
editor.update(() => {
const selection = $getSelection();
const text = handleFormatting(selection?.getTextContent(), event);

Check warning on line 84 in src/components/UI/Form/EmojiInput/Editor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/EmojiInput/Editor.tsx#L82-L84

Added lines #L82 - L84 were not covered by tests

if (!selection?.getTextContent()) {
const newNode = $createTextNode(text);
selection?.insertNodes([newNode]);
const newSelection = $createRangeSelection();
newSelection.anchor.set(newNode.getKey(), 1, 'text'); // Set the cursor between the backticks
newSelection.focus.set(newNode.getKey(), 1, 'text');
$setSelection(newSelection);

Check warning on line 92 in src/components/UI/Form/EmojiInput/Editor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/EmojiInput/Editor.tsx#L87-L92

Added lines #L87 - L92 were not covered by tests
}
if (selection?.getTextContent() && event) {
const newNode = $createTextNode(text);
selection?.insertNodes([newNode]);
editor.focus();

Check warning on line 97 in src/components/UI/Form/EmojiInput/Editor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/EmojiInput/Editor.tsx#L95-L97

Added lines #L95 - L97 were not covered by tests
}
});
return false;

Check warning on line 100 in src/components/UI/Form/EmojiInput/Editor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/EmojiInput/Editor.tsx#L100

Added line #L100 was not covered by tests
},
COMMAND_PRIORITY_LOW
)
);
}, [editor]);

Expand All @@ -81,39 +120,134 @@ export const Editor = ({ disabled = false, ...props }: EditorProps) => {
});
};

const [activeFormats, setActiveFormats] = useState<{ bold: boolean; italic: boolean; strikethrough: boolean }>({
bold: false,
italic: false,
strikethrough: false,
});

useEffect(() => {
const checkFormatting = () => {
editor.update(() => {
const selection = $getSelection();

if (!$isRangeSelection(selection)) {
setActiveFormats({ bold: false, italic: false, strikethrough: false });
return;
}

const anchorNode = selection.anchor.getNode();
const anchorOffset = selection.anchor.offset;

if (!anchorNode.getTextContent()) {
setActiveFormats({ bold: false, italic: false, strikethrough: false });
return;
}

const textContent = anchorNode.getTextContent();
const textFormat = anchorNode.getFormat(); // 🔥 Detects Lexical's internal formatting
console.log(textFormat);

// Regex patterns for formatting
const boldRegex = /\*(?:\S.*?\S|\S)\*/g; // Correctly detects *bold*
const italicRegex = /_(.*?)_/g; // Matches _italic_
const strikethroughRegex = /~(.*?)~/g; // Matches ~~strikethrough~~

const isInsideFormat = (regex: RegExp) => {
let match;
while ((match = regex.exec(textContent)) !== null) {
const start = match.index;
const end = start + match[0].length;

Check warning on line 160 in src/components/UI/Form/EmojiInput/Editor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/EmojiInput/Editor.tsx#L159-L160

Added lines #L159 - L160 were not covered by tests
if (anchorOffset > start && anchorOffset < end) {
return true;

Check warning on line 162 in src/components/UI/Form/EmojiInput/Editor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/EmojiInput/Editor.tsx#L162

Added line #L162 was not covered by tests
}
}
return false;
};
// console.log(isInsideFormat(boldRegex));

setActiveFormats({
bold: isInsideFormat(boldRegex),
italic: isInsideFormat(italicRegex),
strikethrough: isInsideFormat(strikethroughRegex),
});
});
};

// Register an update listener to track selection changes
const removeListener = editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
checkFormatting();
});
});

return () => {
removeListener();
};
}, [editor]);
// console.log(activeFormats);

return (
<div className={styles.EditorWrapper}>
<div className={disabled ? styles?.disabled : styles.Editor} data-testid="resizer">
<PlainTextPlugin
placeholder={<Placeholder />}
contentEditable={
<div className={styles.editorScroller}>
<div className={styles.editor}>
<ContentEditable
data-testid={`editor-${field.name}`}
disabled={disabled}
className={styles.EditorInput}
/>
<>
<div className={styles.EditorWrapper}>
<div className={styles.FormatingOptions}>
<span
className={activeFormats.bold ? styles.Active : ''}
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');

Check warning on line 197 in src/components/UI/Form/EmojiInput/Editor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/EmojiInput/Editor.tsx#L197

Added line #L197 was not covered by tests
}}
>
<FormatBold fontSize="small" color="inherit" />
</span>
<span
className={activeFormats.italic ? styles.Active : ''}
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');

Check warning on line 205 in src/components/UI/Form/EmojiInput/Editor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/EmojiInput/Editor.tsx#L205

Added line #L205 was not covered by tests
}}
>
<FormatItalic fontSize="small" color="inherit" />
</span>
<span
className={activeFormats.strikethrough ? styles.Active : ''}
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');

Check warning on line 213 in src/components/UI/Form/EmojiInput/Editor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/EmojiInput/Editor.tsx#L213

Added line #L213 was not covered by tests
}}
>
<StrikethroughS fontSize="small" color="inherit" />
</span>
</div>
<div className={disabled ? styles?.disabled : styles.Editor} data-testid="resizer">
<PlainTextPlugin
placeholder={<Placeholder />}
contentEditable={
<div className={styles.editorScroller}>
<div className={styles.editor}>
<ContentEditable
data-testid={`editor-${field.name}`}
disabled={disabled}
className={styles.EditorInput}
/>
</div>
</div>
</div>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<BeautifulMentionsPlugin
menuComponent={CustomMenu}
menuItemComponent={CustomMenuItem}
triggers={['@']}
items={suggestions}
/>
<OnChangePlugin onChange={handleChange} />
{picker}
}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<BeautifulMentionsPlugin
menuComponent={CustomMenu}
menuItemComponent={CustomMenuItem}
triggers={['@']}
items={suggestions}
/>
<OnChangePlugin onChange={handleChange} />
{picker}
</div>
</div>
{form && form.errors[field.name] && form.touched[field.name] ? (
<FormHelperText className={styles.DangerText}>{form.errors[field.name]}</FormHelperText>
) : null}
{props.helperText && <FormHelperText className={styles.HelperText}>{props.helperText}</FormHelperText>}
</div>
</>
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/UI/Form/WhatsAppEditor/WhatsAppEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const WhatsAppEditor = ({ setEditorState, sendMessage, readOnly = false }
editor.update(() => {
const selection = $getSelection();
if (selection?.getTextContent() && formatter) {
const text = handleFormatting(selection?.getTextContent(), formatter);
const text = handleFormatting(formatter, selection?.getTextContent());

Check warning on line 59 in src/components/UI/Form/WhatsAppEditor/WhatsAppEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/UI/Form/WhatsAppEditor/WhatsAppEditor.tsx#L59

Added line #L59 was not covered by tests
const newNode = $createTextNode(text);
selection?.insertNodes([newNode]);
}
Expand Down
Loading

0 comments on commit b82e13e

Please sign in to comment.