Text-Editor

Overview

The TextEditor component is a rich text editor built using the Lexical Composer library. It provides a user-friendly interface for editing and formatting text, supporting features such as mentions, links, tables, images and more.

Installation

To use the TextEditor component in your project, you can install it via npm or yarn:

npm install @locoworks/reusejs-react-text-editor
# or
yarn add @locoworks/reusejs-react-text-editor

External Dependencies

The TextEditor component relies on external dependencies - tailwind-merge, lexical and @lexical/react.

npm install lexical @lexical/react tailwind-merge
# or
yarn add lexical @lexical/react tailwind-merge

Importing

Import the TextEditor component into your React application as follows:

import { TextEditor } from "@locoworks/reusejs-react-text-editor";

Types/ Exported Components

Props

The TextEditor component accepts the following props:

  • mentionsData: An optional array of objects representing mentions data. It indicates enabling or disabling the mentions.Its value is "false" by default which means disabled mentions.
  • useMentionLookupService: An optional function to handle mention lookup service and return results for search.
  • convertFilesToImageUrl: A function to convert files to image URLs and return Array of imageUrls.
  • onChangeCallback: An optional callback for editor reference changes and payload.
  • wrapperClass: An optional class for styling the editor wrapper.
  • editable: A boolean indicating the edit state of the editor.
  • setEditable: An optional function to set the editable state of the editor. It is a React state setter function that accepts a boolean value.
  • htmlData: An optional string to set the intialState of the editor. You can pass the html data directly to the editor as its initial state.
  • placeholderText: An optional string for the placeholder inside the editor. Its value is "Start Typing..." by default.
  • hideToolbar: A boolean value either true or false, by default value is false.

Exported properties

The onChange function within the TextEditor component serves as a callback, providing access to the editorRef and a payload object. This payload encapsulates various properties derived from the editor's state, including HTML content, JSON representation, text content, and, if mentions are enabled, a reference to the mentions array. For example: You can access the HTML content specifically using payload["html"] in the callback function.

Usage/Examples

TextEditor demo

Start Typing...
Here
import React, { useState } from "react";
import { LexicalEditor } from "lexical";
import { TextEditor } from "@locoworks/reusejs-react-text-editor";
import "@locoworks/reusejs-react-text-editor/css";

const Example = () => {
	const [editable, setEditable] = useState<boolean>(false);
	const [data, setData] = useState<string | TrustedHTML>("Here");

	function convertFilesToImageUrl(files: FileList | null) {
		if (!files || files.length === 0) {
			return null;
		}
		const imageUrls = [];

		for (let i = 0; i < files.length; i++) {
			const file = files[i];

			const imageUrl = URL.createObjectURL(file);
			imageUrls.push(imageUrl);
		}
		return imageUrls.length > 0 ? imageUrls : null;
	}

	function onChange(_editorRef: LexicalEditor | null, payload: any) {
		setData(payload["html"]);
	}

	return (
		<div className="flex flex-col items-center justify-center py-10 mt-10 bg-gray-100 border rounded gap-x-3">
			<TextEditor
				editable={editable}
				setEditable={setEditable}
				convertFilesToImageUrl={convertFilesToImageUrl}
				onChangeCallback={onChange}
				hideToolbar={false}
				showToolbarText={false}
			/>
			{!editable && (
				<div
					className="w-full min-h-[50px] cursor-text bg-white"
					dangerouslySetInnerHTML={{ __html: data }}
					onClick={() => {
						setEditable(true);
					}}
				/>
			)}
		</div>
	);
};

export default Example;

PrepopulatedTextEditor

Start Typing...

Here hello

import React, { useEffect, useState } from "react";
import { LexicalEditor } from "lexical";
import { TextEditor } from "@locoworks/reusejs-react-text-editor";
import "@locoworks/reusejs-react-text-editor/css";

const PrepopulatedTextEditor = () => {
	const payload: string = `<table>
	 	<tr>
	 	  <th>Company</th>
	 	  <th>Contact</th>
	 	  <th>Country</th>
	 	</tr>
	 	<tr>
	 	  <td>Alfreds Futterkiste</td>
	 	  <td>Maria Anders</td>
	 	  <td>Germany</td>
	 	</tr>
	 	<tr>
	 	  <td>Centro comercial Moctezuma</td>
	 	  <td>Francisco Chang</td>
	 	  <td>Mexico</td>
	 	</tr>
	 	<tr>
	 	  <td>Ernst Handel</td>
	 	  <td>Roland Mendel</td>
	 	  <td>Austria</td>
	 	</tr>
	 	<tr>
	 	  <td>Island Trading</td>
	 	  <td>Helen Bennett</td>
	 	  <td>UK</td>
	 	</tr>
	 	<tr>
	 	  <td>Laughing Bacchus Winecellars</td>
	 	  <td>Yoshi Tannamuri</td>
	 	  <td>Canada</td>
	 	</tr>
	 	<tr>
	 	  <td>Magazzini Alimentari Riuniti</td>
	 	  <td>Giovanni Rovelli</td>
	 	  <td>Italy</td>
	 	</tr>
	   </table>
	  `;
	const [htmlData, setHtmlData] = useState(`<p>Here hello</p>`);

	setTimeout(() => {
		setHtmlData(payload);
	}, 5000);

	useEffect(() => {
		setData(htmlData);
	}, [htmlData]);

	const [editable, setEditable] = useState<boolean>(false);
	const [data, setData] = useState<string | TrustedHTML>(htmlData);

	function convertFilesToImageUrl(files: FileList | null) {
		if (!files || files.length === 0) {
			return null;
		}
		const imageUrls = [];

		for (let i = 0; i < files.length; i++) {
			const file = files[i];

			const imageUrl = URL.createObjectURL(file);
			imageUrls.push(imageUrl);
		}
		return imageUrls.length > 0 ? imageUrls : null;
	}

	function onChange(_editorRef: LexicalEditor | null, payload: any) {
		setData(payload["html"]);
	}

	return (
		<div className="flex flex-col items-center justify-center py-10 mt-10 bg-gray-100 border rounded gap-x-3">
			<TextEditor
				htmlData={htmlData}
				editable={editable}
				setEditable={setEditable}
				convertFilesToImageUrl={convertFilesToImageUrl}
				onChangeCallback={onChange}
			/>
			{!editable && (
				<div
					className="w-full min-h-[50px] cursor-text bg-white"
					dangerouslySetInnerHTML={{ __html: data }}
					onClick={() => {
						setEditable(true);
					}}
				/>
			)}
		</div>
	);
};

export default PrepopulatedTextEditor;

Enabled Mentions

Start Typing...
Here
import React, { useState } from "react";
import { LexicalEditor } from "lexical";
import { TextEditor } from "@locoworks/reusejs-react-text-editor";
import "@locoworks/reusejs-react-text-editor/css";

const EnabledMentions = () => {
	const [editable, setEditable] = useState<boolean>(false);
	const [data, setData] = useState<string | TrustedHTML>("Here");

	const dummyMentionsData = [
		"Aayla Secura",
		"Adi Gallia",
		"Niima the Hutt",
		"Nines",
		"Norra Wexley",
		"Nute Gunray",
		"Val Beckett",
		"Vanden Willard",
		"Vice Admiral Amilyn Holdo",
		"Vober Dand",
		"WAC-47",
		"Wedge Antilles",
		"Wes Janson",
		"Wicket W. Warrick",
		"Wilhuff Tarkin",
		"Wollivan",
		"Wuher",
		"Wullf Yularen",
		"Xamuel Lennox",
		"Yaddle",
		"Yarael Poof",
		"Yoda",
		"Zam Wesell",
		"Zev Senesca",
		"Ziro the Hutt",
		"Zuckuss",
	].map((name) => ({
		mentionName: `user_${name}`,
		label: name,
	}));

	function convertFilesToImageUrl(files: FileList | null) {
		if (!files || files.length === 0) {
			return null;
		}
		const imageUrls = [];

		for (let i = 0; i < files.length; i++) {
			const file = files[i];

			const imageUrl = URL.createObjectURL(file);
			imageUrls.push(imageUrl);
		}
		return imageUrls.length > 0 ? imageUrls : null;
	}

	function onChange(_editorRef: LexicalEditor | null, payload: any) {
		setData(payload["html"]);
	}

	return (
		<div className="flex flex-col items-center justify-center py-10 mt-10 bg-gray-100 border rounded gap-x-3">
			<TextEditor
				editable={editable}
				setEditable={setEditable}
				mentionsData={dummyMentionsData}
				convertFilesToImageUrl={convertFilesToImageUrl}
				onChangeCallback={onChange}
			/>
			{!editable && (
				<div
					className="w-full min-h-[50px] cursor-text bg-white"
					dangerouslySetInnerHTML={{ __html: data }}
					onClick={() => {
						setEditable(true);
					}}
				/>
			)}
		</div>
	);
};

export default EnabledMentions;

Enabled Mentions with Custom lookup function

Start Typing...
Here
import React, { useEffect, useState } from "react";
import { LexicalEditor } from "lexical";
import { TextEditor } from "@locoworks/reusejs-react-text-editor";
import "@locoworks/reusejs-react-text-editor/css";

const EnabledMentionsWithCustomLookupService = () => {
	const [editable, setEditable] = useState<boolean>(false);
	const [data, setData] = useState<string | TrustedHTML>("Here");

	const dummyMentionsData = [
		"Aayla Secura",
		"Adi Gallia",
		"Niima the Hutt",
		"Nines",
		"Norra Wexley",
		"Nute Gunray",
		"Val Beckett",
		"Vanden Willard",
		"Vice Admiral Amilyn Holdo",
		"Vober Dand",
		"WAC-47",
		"Wedge Antilles",
		"Wes Janson",
		"Wicket W. Warrick",
		"Wilhuff Tarkin",
		"Wollivan",
		"Wuher",
		"Wullf Yularen",
		"Xamuel Lennox",
		"Yaddle",
		"Yarael Poof",
		"Yoda",
		"Zam Wesell",
		"Zev Senesca",
		"Ziro the Hutt",
		"Zuckuss",
	].map((name) => ({
		mentionName: `user_${name}`,
		label: name,
	}));

	function useMentionLookupService(
		mentionString: string | null,
		mentionsData: Array<{
			mentionName: string;
			label: string;
		}>,
	) {
		const [results, setResults] = useState<
			Array<{ mentionName: string; label: string }>
		>([]);

		useEffect(() => {
			if (mentionString === null) {
				setResults([]);
			} else {
				setResults(
					mentionsData.filter((mention) =>
						mention.label.toLowerCase().includes(mentionString.toLowerCase()),
					),
				);
			}
		}, [mentionString]);

		return results;
	}

	function convertFilesToImageUrl(files: FileList | null) {
		if (!files || files.length === 0) {
			return null;
		}
		const imageUrls = [];

		for (let i = 0; i < files.length; i++) {
			const file = files[i];

			const imageUrl = URL.createObjectURL(file);
			imageUrls.push(imageUrl);
		}
		return imageUrls.length > 0 ? imageUrls : null;
	}

	function onChange(_editorRef: LexicalEditor | null, payload: any) {
		setData(payload["html"]);
	}

	return (
		<div className="flex flex-col items-center justify-center py-10 mt-10 bg-gray-100 border rounded gap-x-3">
			<TextEditor
				editable={editable}
				setEditable={setEditable}
				mentionsData={dummyMentionsData}
				useMentionLookupService={useMentionLookupService}
				convertFilesToImageUrl={convertFilesToImageUrl}
				onChangeCallback={onChange}
			/>
			{!editable && (
				<div
					className="w-full min-h-[50px] cursor-text bg-white"
					dangerouslySetInnerHTML={{ __html: data }}
					onClick={() => {
						setEditable(true);
					}}
				/>
			)}
		</div>
	);
};

export default EnabledMentionsWithCustomLookupService;

Styling

The styling of the editorWrapper can be customized using Tailwind CSS .

Note: To use the styling of the editor component , you must import the StyleSheet in your application.

import "@locoworks/reusejs-react-text-editor/css";

Best Practices

  • Ensure that necessary dependencies are installed.
  • Handle editor reference changes and payload using the onChangeCallback prop.

Props Table

PropTypeDescription
mentionsData Array<{ mentionName: string; label: string; }> | falseAn optional array of objects representing mentions data. Each object should have mentionName and label properties. Its false by default.
useMentionLookupService(mentionString: string | null, mentionsData) => Array<{ mentionName: string; label: string; }>An optional function to handle mention lookup and return results.
convertFilesToImageUrl(files: FileList | null) => Array<string> | nullA function to convert files to image URLs and return image URLs.
onChangeCallback(editorRef: LexicalEditor | null, payload: any) => voidAn optional callback for editor reference changes and payload.
wrapperClassstringAn optional class for styling the editor wrapper.
editablebooleanA boolean indicating the edit state of the editor.
setEditableReact.Dispatch<React.SetStateAction<boolean>>An optional function to set the editable state of the editor.
htmlDatastringAn optional string to set the initial state of the editor.
placeholderTextstringAn optional string for the placeholder inside the editor. Its value is "Start Typing..." by default
hideToolbarbooleanIt accepts either true or false by default it is false.

Notes

The TextEditor component uses the Lexical library for rich text editing. Check the Lexical Composer documentation for advanced configuration options.

To enable the mentions plugin , you have to pass the mentionsData which is false by default and thus disabled.