@drcpythonmfe/lexical-playground
v0.4.20
Published
A temporary packaged fork of Lexical's official playground.
Downloads
300
Maintainers
Keywords
Readme
Use the toolbar to adjust your text formatting:
Turn into: Convert the selected text into headings, banners, a code block, or a quote block.
Rich text: Bold, italics, underline, strikethrough, or inline code formatting.
Text colors and Text highlights: Select from a range of vibrant text colors.
Badges: Insert a colorful badge to emphasize or call attention to a line or block of content. You can also add some rich text formatting to the text in the badge like you can in a banner.
Alignment: Indent and set text to left, center, or right justified.
Bulleted List: Format text into a bulleted list. All Lists: Click the caret icon next to the Bulleted List icon to format text into a Numbered List or Toggled List.
Check List: Format text into a check list.
Insert a link: Insert a hyperlink.
Create subpage: Create a series of subtopics that are part of the main Doc.
Create comment: Add comments about the Doc to the right sidebar of the Doc. Text that has comments is highlighted.
Undo: Undo your last action.
Redo: Redo your last action.
text transformation functionality to the editor
/Slash Commands
Use /Slash Commands, our custom shortcuts that quickly add rich text, attach images, move a task, change a due date, and more!
Emojis
Add emojis to give your content some flair!
const uploadImg = async (file: File, altText: string) => {
console.log("file",file)
await delay(500);
await delay(500);
let data = {
url : `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4`,
id : 126548545485465
}
return data
};
const onDataSend = async (file: File) => { // all file upload
console.log(file)
await delay(500);
let data = {
url : `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4`,
id : 126548545485465
}
return data
};
function App({
html,
setHtml,
userList
}: {
html: string;
setHtml: (newHtml: string) => void;
userList:any;
}): JSX.Element {
useSyncWithInputHtml(html);
return (
<Editor
isRichText={true}
onChange={setHtml}
onUpload={uploadImg}
onDataSend={uploadImg}
onChangeMode="html"
onDataSend={onDataSend}
dummyMentionsDatas={userList || []}
/>
);
}
dummyMentionsData
const dummyMentionsData = [
{
name: 'Aayla Secura',
email: '[email protected]',
},
{
name: 'Adi Gallia',
email: '[email protected]',
},
]
const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
const uploadImg = async (file: File, altText: string) => {
console.log("file",file)
await delay(500);
let data = {
url : `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4`,
id : 126548545485465
}
return data
};
//handleAIData
const handleAIData = async (data: string): Promise<any> => {
await delay(500); // api call
return "AI Data";
};
const toolbarConfig ={
align: true,
bgColorPicker: true,
biu: true,
codeBlock: false,
fontFamilyOptions: false,
fontSizeOptions: false,
formatBlockOptions: true,
formatTextOptions: true,
insertOptions: true,
link: true,
textColorPicker: true,
undoRedo: true,
paragraph: false, // / type data
heading1: false,
heading2: false,
heading3: false,
table: true,
numberedList: false,
bulletedList: false,
checkList: true,
embedYoutubeVideo: false,
embedVideo: false,
embedPdf: false,
embedOffice: false,
UploadDocuments: true,
alignLeft: false,
alignCenter: false,
alignRight: false,
alignJustify: false,
editorshow:true,
uppercase:true,
lowercase:true,
capitalize:true,
RTL:true,
LTR:true,
ai:true // handleAIData
video:true,
Sticky:true,
Poll:true,
Table:true,
Horizontal:true,
Collapsible:true,
}
function App({
html,
setHtml,
userList
}: {
html: string;
setHtml: (newHtml: string) => void;
userList:any;
}): JSX.Element {
useSyncWithInputHtml(html);
return (
<Editor
isRichText={true}
onChange={setHtml}
onUpload={uploadImg}
onChangeMode="html"
toolbarConfig={toolbarConfig}
handleAIData={handleAIData}
dummyMentionsDatas={userList || []}
/>
);
}
function validateParagraphs(htmlText: string): boolean {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlText.trim();
if (tempDiv.querySelectorAll('img').length > 0) {
return false;
}
if (tempDiv.querySelectorAll('table').length > 0) {
return false;
}
if (tempDiv.querySelectorAll('h1').length > 0) {
return false;
}
if (tempDiv.querySelectorAll('h2').length > 0) {
return false;
}
if (tempDiv.querySelectorAll('h3').length > 0) {
return false;
}
if (tempDiv.querySelectorAll('li').length > 0) {
return false;
}
const paragraphs = tempDiv.querySelectorAll('p');
if (paragraphs.length === 0) {
return false;
}
let nonEmptyTextCount = 0;
for (const paragraph of Array.from(paragraphs)) {
const directTextContent = Array.from(paragraph.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE)
.map(node => node.textContent?.trim())
.filter(text => text && text !== '');
if (directTextContent.length > 0) {
nonEmptyTextCount++;
}
const childTextContent = paragraph.textContent?.trim() || '';
if (childTextContent !== '') {
nonEmptyTextCount++;
}
}
return nonEmptyTextCount <= 0;
}
export default function PlaygroundApp1(): JSX.Element {
const [html, setHtml] = useState(``);
React.useEffect(()=>{
validateParagraphs(html)
},[html])
return (
<>
<EditorComposer>
<App html={html} setHtml={setHtml} userList={dummyMentionsData} />
</EditorComposer>
<button disabled={ validateParagraphs(html)}> Button </button>
<div dangerouslySetInnerHTML={{__html: html}} />
</>
);
}
Dark Mode
<body class="theme-dark"> // Add class name theme-dark
</body>
HTML as formatHTMLData
function formatHTMLData(data: string): string {
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
function cleanNestedLists(element: Element): void {
const nestedUls = element.querySelectorAll('ul > ul');
nestedUls.forEach(ul => {
const parentLi = ul.parentNode as Element;
if (parentLi.tagName === 'LI') {
parentLi.parentNode?.insertBefore(ul, parentLi.nextSibling);
}
});
}
function removeEmptyElements(element: Element): void {
element.querySelectorAll('*').forEach(el => {
if (el.innerHTML.trim() === '' && !['img', 'br', 'hr'].includes(el.tagName.toLowerCase())) {
el.parentNode?.removeChild(el);
}
});
}
function wrapTextNodesInPTags(element: Element): void {
let textContent = '';
const childNodes = Array.from(element.childNodes);
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i];
if (node.nodeType === Node.TEXT_NODE) {
textContent += node.textContent?.trim() || '';
element.removeChild(node);
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (textContent) {
const p = doc.createElement('p');
p.textContent = textContent;
element.insertBefore(p, node);
textContent = '';
}
if (node.nodeName !== 'BR') {
wrapTextNodesInPTags(node as Element);
}
}
}
if (textContent) {
const p = doc.createElement('p');
p.textContent = textContent;
element.appendChild(p);
}
}
function correctTextPrimayClass(element: Element): void {
element.querySelectorAll('a.text-primay').forEach(el => {
el.classList.remove('text-primay');
el.classList.add('text-primary');
});
}
function replaceATagWithSpan(element: Element): void {
const links = element.querySelectorAll('a.text-primary');
links.forEach(link => {
const span = doc.createElement('span');
span.setAttribute('data-lexical-text', 'true');
span.textContent = link.textContent;
const newAnchor = doc.createElement('a');
newAnchor.setAttribute('href', link.getAttribute('href'));
newAnchor.setAttribute('rel', 'noopener');
newAnchor.setAttribute('class', 'TextEditor__link TextEditor__ltr');
newAnchor.appendChild(span);
const p = doc.createElement('p');
p.appendChild(newAnchor);
link.parentNode?.replaceChild(p, link);
});
}
const wrapper = doc.createElement('div');
while (doc.body.firstChild) {
wrapper.appendChild(doc.body.firstChild);
}
cleanNestedLists(wrapper);
removeEmptyElements(wrapper);
wrapTextNodesInPTags(wrapper);
correctTextPrimayClass(wrapper);
replaceATagWithSpan(wrapper);
wrapper.innerHTML = wrapper.innerHTML.replace(/ /g, ' ').trim();
const prettyHTML = prettifyHTML(wrapper.innerHTML);
const cleanedString = prettyHTML.replace(/<br\s*\/?>/gi, "").replace(/<p class="TextEditor__paragraph"><br><\/p>\s*/g, '');
return cleanedString;
}
function prettifyHTML(html: string): string {
let indent = 0;
const tab = ' ';
let pretty = '';
html.split(/>\s*</).forEach(element => {
if (element.match(/^\/\w/)) {
indent = Math.max(0, indent - 1);
}
pretty += tab.repeat(indent) + '<' + element + '>\n';
if (element.match(/^<?\w[^>]*[^\/]$/) && !element.startsWith('input') && !element.startsWith('img') && !element.startsWith('br')) {
indent++;
}
});
return pretty.substring(1, pretty.length - 2);
}
const [html, setHtml] = useState(formatHTMLData(data3));
HTML as input/output
import { Editor, EditorComposer, useSyncWithInputHtml } from '@drcpythonmfe/lexical-playground';
import "@drcpythonmfe/lexical-playground/editor.css"
import "@drcpythonmfe/lexical-playground/theme.css"
function MyEditor({ html, setHtml }: {
html: string;
setHtml: (newHtml: string) => void;
}): JSX.Element {
useSyncWithInputHtml(html);
return (
<Editor isRichText onChange={setHtml} onUpload={uploadImg} onChangeMode="html" />
);
}
export default function PlaygroundApp(): JSX.Element {
const [html, setHtml] = useState('<b>test</b>');
return (
<EditorComposer>
<MyEditor html={html} setHtml={setHtml} />
</EditorComposer>
);
}
A JSON string as input/output
import { Editor, EditorComposer, useSyncWithInputHtml } from '@drcpythonmfe/lexical-playground';
import "@drcpythonmfe/lexical-playground/editor.css"
import "@drcpythonmfe/lexical-playground/theme.css"
function MyEditor({ json, setJson }: {
json: string;
setJson: (html: string) => void;
}): JSX.Element {
useSyncWithInputJson(json); // either a string or an object
return <Editor isRichText onChange={setJson} onChangeMode="json" />;
}
export default function PlaygroundApp(): JSX.Element {
const [json, setJson] = useState(
'{"root":{"children":[{"children":[{"detail":0,"format":1,"mode":"normal","style":"","text":"test","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}',
);
return (
<EditorComposer>
<MyEditor json={json} setJson={setJson} />
</EditorComposer>
);
}
Customizing the toolbar
Certain buttons can be ommited from the toolbar, and some can be configured if necessary:
import { Editor, EditorComposer, EditorProps } from '@drcpythonmfe/lexical-playground';
const toolbarConfig: EditorProps['toolbarConfig'] = {
textColorPicker: false,
bgColorPicker: false,
fontFamilyOptions: [
['Roboto', 'Roboto'],
['Open Sans', 'Open Sans'],
],
};
function MyEditor(): JSX.Element {
return <Editor toolbarConfig={toolbarConfig} isRichText />;
};
export default function PlaygroundApp(): JSX.Element {
return (
<EditorComposer>
<MyEditor />
</EditorComposer>
);
}
Theme overriding
It's recommended to replace built-in class names with your own:
import { Editor, EditorComposer, EditorThemeClasses } from '@drcpythonmfe/lexical-playground';
import '@drcpythonmfe/lexical-playground/editor.css';
import './myTheme.css';
const theme: EditorThemeClasses = {
characterLimit: 'MyTheme__characterLimit',
code: 'MyTheme__code',
// ...
};
function MyEditor(): JSX.Element {
return <Editor isRichText />;
}
export default function PlaygroundApp(): JSX.Element {
return (
<EditorComposer initialConfig={{ theme }}>
<MyEditor />
</EditorComposer>
);
}
Uploading an image and returning a path
By default images are converted to data URLs.
// ...
const uploadImg = async (file: File, altText: string) => {
// process the file
return urlOfImage;
}
return (
<Editor
onUpload={uploadImg}
isRichText
// ...
/>
);
Getting an access to the lexical editor's instance
function MyEditor(): JSX.Element {
const [editor] = useLexicalComposerContext();
return (
<Editor isRichText />
);
}
export default function PlaygroundApp(): JSX.Element {
return (
<EditorComposer>
<MyEditor />
</EditorComposer>
);
}
Showing exported HTML w/o loading the entire editor
The only thing that's needed to display HTML that lexical generated is to import theme.css
.
import '@drcpythonmfe/lexical-playground/theme.css'; // or import your own theme styles
export default function PlaygroundApp({ html }: { html: string }): JSX.Element {
return (
<div dangerouslySetInnerHTML={{__html: html}} />
);
}
SSR
At this point the editor does not support SSR and needs to be loaded on the client.
Next.js
import type { NextPage } from 'next'
import dynamic from 'next/dynamic'
const MyEditor = dynamic(() => import('./path-to-my-editor-that-loads-lexical-playground'), {
ssr: false,
})
const MyPage: NextPage = () => {
return (
<MyEditor />
)
}
export default MyPage
Using optional plugins
Some plugins like excalidraw
and equation
are optional, and need to be manually activated:
// ...
import {excalidrawExt} from '@drcpythonmfe/lexical-playground/ext/excalidraw';
import '@drcpythonmfe/lexical-playground/ext/excalidraw.css';
import {equationExt} from '@drcpythonmfe/lexical-playground/ext/equation';
import '@drcpythonmfe/lexical-playground/ext/equation.css';
function MyEditor(): JSX.Element {
return (
<Editor isRichText />
);
}
const extensions = [equationExt, excalidrawExt];
export default function PlaygroundApp(): JSX.Element {
return (
<EditorComposer extensions={extensions}>
<MyEditor />
</EditorComposer>
);
}