@drcpythonmfe/lexical-playground

0.4.20 • Public • Published

image info

Use the toolbar to adjust your text formatting:

Turn into: Convert the selected text into headings, banners, a code block, or a quote block.

image info

Rich text: Bold, italics, underline, strikethrough, or inline code formatting.

Text colors and Text highlights: Select from a range of vibrant text colors.

image info

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. image info

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.

image info image info image info image info image info

text transformation functionality to the editor

image info

image info

/Slash Commands

Use /Slash Commands, our custom shortcuts that quickly add rich text, attach images, move a task, change a due date, and more!

image info

image info

Emojis

Add emojis to give your content some flair! image info

image info image info

image info

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: 'aayla.secura@example.com',
  },
  {
    name: 'Adi Gallia',
    email: 'adi.gallia@example.com',
  },
]

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(/&nbsp;/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>
  );
}

Package Sidebar

Install

npm i @drcpythonmfe/lexical-playground

Weekly Downloads

38

Version

0.4.20

License

MIT

Unpacked Size

5.46 MB

Total Files

280

Last publish

Collaborators

  • drcpythonmfe