# LayersTextEditor

LayersTextEditor is a text editor for web applications written in JavaScript, with a focus on reliability, accessibility, and performance.

<details>
  <summary>
    🚀 Quick Start
  </summary>

## Installation

To install the package, run one of the following commands:

### Use npm:

```bash
npm install @layers-app/editor
```

### Use yarn:

```bash
yarn add @layers-app/editor
```

### Initializing the text editor

```
import { Editor } from '@layers-app/editor';
```

By default, LayersTextEditor works with an object and can return either an object or HTML.

Example with an object:

```js
const text = 'Hello world';

const json = {
  root: {
    children: [
      {
        children: [
          {
            detail: 0,
            format: 0,
            mode: 'normal',
            style: '',
            text: text,
            type: 'text',
            version: 1,
          },
        ],
        direction: 'ltr',
        format: '',
        indent: 0,
        type: 'paragraph',
        version: 1,
      },
    ],
    direction: 'ltr',
    format: '',
    indent: 0,
    type: 'root',
    version: 1,
  },
};

const onChange = (
  data, // json
) => <Editor initialContent={json} onChange={onChange} />;
```

You can also pass an HTML string to the editor.

Example with HTML:

```
const html = `
<h2 dir="ltr" style="text-align: left;">
   <span style="background-color: rgb(248, 231, 28); font-family: &quot;Trebuchet MS&quot;; white-space: pre-wrap;">Hello</span>
</h2>
<h2 dir="ltr">
   <br>
</h2>
<p dir="ltr">
   <br>
</p>
<p dir="ltr">
   <span style="font-size: 21px; white-space: pre-wrap;">world</span>
</p>
`

const onChange = (data) => // json

<Editor initialContent={html} onChange={onChange} />
```

The output of the data in the `onChange` function is controlled by the **outputFormat** property. **outputFormat** can be either "html" or "json". Example with **outputFormat**:

```
const html = `
<h2 dir="ltr" style="text-align: left;">
   <span style="background-color: rgb(248, 231, 28); font-family: &quot;Trebuchet MS&quot;; white-space: pre-wrap;">Hello</span>
</h2>
<h2 dir="ltr">
   <br>
</h2>
<p dir="ltr">
   <br>
</p>
<p dir="ltr">
   <span style="font-size: 21px; white-space: pre-wrap;">world</span>
</p>
`

const onChange = (data: string, text?: string) => {
  // data - html from editor
  // text - text from editor
}


<Editor initialContent={html} outputFormat="html" onChange={onChange} />
```

</details>

<details>
  <summary>
    🎨 StylesProvider
  </summary>

Use **StylesProvider** to add styling to your HTML content.

```
  <StylesProvider>
      <div
        dangerouslySetInnerHTML={{ __html: '<p>Your html here</p>' }}
      />
    </StylesProvider>
```

</details>

<details>
  <summary>
    🖼️ File upload
  </summary>

## Image upload

To start working with image uploads, use the **fetchUploadMedia** function, which takes three parameters: **file**, **success**, and **error**. After successfully uploading the image to your service, you should call the **success** function and pass two required arguments: the **URL** of the image and its **ID**.

```
  const fetchUploadMedia = async (
    file: File,
    success: (url: string, id: string) => void,
    error?: (error?: Error) => void
  ) => {
    const formData = new FormData();
    formData.append('File', file);
    formData.append('FileAccessModifier', '0');

    try {
      const response = await fetch('/api/v1/Files/Upload', {
        method: 'POST',
        body: formData,
        credentials: 'include'
      });

      if (!response.ok) {
        throw new Error('File upload failed');
      }

      const data = await response.json();
      const { Id, Url } = data;

      success(Url, Id);
    } catch (err) {
      if (error) {
        if (err instanceof Error) {
          error(err);
        } else {
          error(new Error('An unknown error occurred'));
        }
      }
    }
  };

<Editor
  ...props
  fetchUploadMedia={fetchUploadMedia}
/>
```

## Image Deletion

To have greater control over image deletion, pass an optional function **fetchDeleteMedia** to the editor, which accepts three parameters: **id**, **success**, and **error**. After successfully deleting the image from your service, the **success** function should be called.

```
  const fetchDeleteMedia = async (
    id: string,
    success: () => void,
    error?: (error?: Error) => void
  ) => {
    const body = { Ids: [id] };

    try {
      const response = await fetch('/api/v1/Documents/Delete', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(body),
        credentials: 'include'
      });

      await response.json();
      success();
    } catch (err) {
      if (error) {
        if (err instanceof Error) {
          error(err);
        } else {
          error(new Error('An unknown error occurred'));
        }
      }
    }
  };

<Editor
  ...props
   fetchUploadMedia={fetchUploadMedia}
   fetchDeleteMedia={fetchUploadMedia}
/>
```

## Additional options for working with image uploads.

```
import { Editor, Dropzone } from "@sinups/editor-dsd";

const Content = () => (
      <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
           {/*
      The components Dropzone.Accept, Dropzone.Reject, and Dropzone.Idle are visible only when the user performs specific actions:

Dropzone.Accept is visible only when the user drags a file that can be accepted into the drop zone.
Dropzone.Reject is visible only when the user drags a file that cannot be accepted into the drop zone.
Dropzone.Idle is visible when the user is not dragging any file into the drop zone.
          */}
            <Dropzone.Accept>
              <IconUpload
                style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-blue-6)' }}
                stroke={1.5}
              />
            </Dropzone.Accept>
            <Dropzone.Reject>
              <IconX
                style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-red-6)' }}
                stroke={1.5}
              />
            </Dropzone.Reject>
            <Dropzone.Idle>
              <IconPhoto
                style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-dimmed)' }}
                stroke={1.5}
              />
            </Dropzone.Idle>

            <div>
              <Text size="xl" inline>
                 Drag images here or click to select files
              </Text>
              <Text size="sm" c="dimmed" inline mt={7}>
               Attach as many files as you want, each file must not exceed{' '}                {maxFileSize} МБ.
              </Text>
            </div>
          </Group>
  );

<Editor
  ...props
   fetchUploadMedia={fetchUploadMedia}
   contentModalUploadImage={Content}
   maxFileSize={5}
   maxImageSizeError={() => {}}
/>
```

## File upload

For uploading a file or audio, you might need the third parameter "data".

```
  const fetchUploadMedia = async (
    file: File,
    success: (url: string, id: string, data?: {
      contentType: string;
      fileSize: string;
      originalFileName: string;
    }) => void,
    error?: (error?: Error) => void
  ) => {
    const formData = new FormData();
    formData.append('File', file);
    formData.append('FileAccessModifier', '0');

    try {
      const response = await fetch('/api/v1/Files/Upload', {
        method: 'POST',
        body: formData,
        credentials: 'include'
      });

      if (!response.ok) {
        throw new Error('File upload failed');
      }

      const data = await response.json();
      const { Id, Url } = data;

      success(Url, Id, data);
    } catch (err) {
      if (error) {
        if (err instanceof Error) {
          error(err);
        } else {
          error(new Error('An unknown error occurred'));
        }
      }
    }
  };
```
</details>


<details>
  <summary>
    🤖 AI
  </summary>

## Connect AI


```
  const fetchPromptResult = async (
    prompt: string,
    success: (data: string) => void,
    error?: () => void,
  ) => {
    try {
      const response = await fetch(
        'https://domain/api/v1/openai/call-any-prompt',
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',

            Authorization: 'token',
          },
          body: JSON.stringify({ prompt }),
        },
      );

      if (!response.ok) {
        const errText = await response.text();
        console.error('server error', errText);
        throw new Error('API failed');
      }

      const data = await response.json();

      success(data.content);
    } catch (err) {
      if (error) {
        error();
        console.error('Error');
      }
    }
  };

<Editor
  ...props
  fetchPromptResult={fetchPromptResult}
/>
```
</details>


<details>
  <summary>
    👥 Collaboration
  </summary>

```jsx
<Editor
  {...props}
  ws={{
    url: 'https://wss.dudoc.io/', // WebSocket URL
    id: '322323', // Unique document ID
    user: userProfile, // Current user
    getActiveUsers: (users) => {
      // Returns active users editing the document
      setActiveUsers(users);
    },
  }}
/>
```

</details>

<details>
  <summary>
    📝 Additional options
  </summary>
  
## Reset editor content

```

import {  CLEAR_EDITOR_COMMAND } from './EditorLexical';

<>
   <button
      onClick={() => {
        if (editorRef.current) {
          editorRef.current.update(() => {
            editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
          });
        }
      }}
    >
      Reset
    </button>
    <Editor
    ...props
     editorRef={editorRef}
    />
<>
```

</details>
<details>
  <summary>
    ⚙️ Properties
  </summary>

```
onChange: (value: string | object) => undefined - A function that triggers every time the editor content changes and returns an HTML string or an object depending on the outputFormat property.
debounce?: number - Defines how often the onChange function is called, in milliseconds.
onBlur: (value: string | object) => undefined - A function that triggers when the editor loses focus and returns an HTML string or an object depending on the outputFormat property.
outputFormat?: 'html' | 'json' - The outputFormat property defines whether we want to output an HTML string or a JSON object. The default is JSON.
initialContent: string | object - The initial content for the editor.
maxHeight?: number - Sets the height of the editor. The default is 100%.
mode?: 'simple' | 'default' | 'full' | 'editor' - The editor mode. Depending on the chosen mode, functionality may be restricted or extended. The default is default.
fetchUploadMedia?: (file: File, success: (url: string, id: string, error?: (error?: Error) => void) => void) - Function to upload an image to your service.
fetchDeleteMedia?: (id: string, success: () => void, error?: (error?: Error) => void) - Helper function to delete an image.
maxFileSize?: number - The maximum image size in megabytes.
contentModalUploadImage?: React.FunctionComponent - A React component to replace content in DropZone.
maxImageSizeError?: () => void - A function that is called if the image exceeds the maxFileSize.
disable?: boolean - Toggles the editor into read-only mode.
ws?: { url: string, id: string, user: { color: string, name: string }, getActiveUsers: (users) => void } - WebSocket settings: URL, document ID, current user details, and function to return active users editing the document.
editorRef?: { current: EditorType | null } - Reference to the editor.
```

</details>
