# RichTextEditor

## Overview

A lightweight, native WYSIWYG (What You See Is What You Get) document text editor built into the Xertica UI framework. It utilizes the browser's native `contentEditable` architecture and `document.execCommand` under the hood to provide essential text formatting without the overhead of massive third-party rendering engines.

The component ships in **two usage patterns**:

| Pattern               | When to use                                                                 |
| --------------------- | --------------------------------------------------------------------------- |
| `<RichTextEditor />`  | Drop-in editor with full toolbar and status bar — zero configuration needed |
| `useRichTextEditor()` | Headless hook — bring your own toolbar, layout, and styling                 |

---

## Features

- **Text Formatting:** Bold, Italic, Underline, and Unordered Lists.
- **Headings & Paragraphs:** Integrated Radix Dropdown for structured block conversion (`P`, `H1`, `H2`, `H3`), specially configured not to trap focus and lose text selection.
- **Alignment:** Left, Center, and Right text justification blocks.
- **Links:** Insert and remove hyperlinks with automatic `target="_blank"` configuration for external security.
- **Search:** Built-in "Find in Text" functionality with a dedicated search bar, supporting Next/Previous navigation and keyboard shortcuts (`Enter`, `Shift+Enter`, `Esc`).
- **Extensive Configuration:** Toggle virtually every toolbar feature on or off via boolean properties (`allowUndoRedo`, `allowHeadings`, etc.).
- **Live Metrics:** Word and character count natively supported and toggleable in the status bar (`showWordCount`, `showCharacterCount`).
- **Action Extensibility:** Supports custom `actionButton` property for injecting dynamic trailing actions into the toolbar (e.g., Save, Submit).
- **Responsive:** Scales correctly and uses built-in localized Tailwind typography (`prose` substitute).

---

## Props

| Prop                 | Type                      | Default | Required | Description                                                                          |
| -------------------- | ------------------------- | ------- | -------- | ------------------------------------------------------------------------------------ |
| `value`              | `string`                  | —       | Yes      | Initial raw HTML string content to mount the editor.                                 |
| `onChange`           | `(value: string) => void` | —       | No       | Fired whenever the editable content changes, emitting raw HTML.                      |
| `placeholder`        | `string`                  | —       | No       | Hint text rendered using CSS pseudo-elements when empty.                             |
| `className`          | `string`                  | —       | No       | Additional classes applied to the root container.                                    |
| `actionButton`       | `React.ReactNode`         | —       | No       | A custom DOM element rendered flush-right against the toolbar.                       |
| `allowSearch`        | `boolean`                 | `true`  | No       | Whether to show the search functionality in the toolbar.                             |
| `allowLinks`         | `boolean`                 | `true`  | No       | Whether to show the link insertion functionality in the toolbar.                     |
| `allowUndoRedo`      | `boolean`                 | `true`  | No       | Whether to show the undo/redo functionality in the toolbar.                          |
| `allowHeadings`      | `boolean`                 | `true`  | No       | Whether to show the headings functionality in the toolbar.                           |
| `allowFormatting`    | `boolean`                 | `true`  | No       | Whether to show text formatting (bold, italic, etc) functionality in the toolbar.    |
| `allowAlignment`     | `boolean`                 | `true`  | No       | Whether to show alignment functionality in the toolbar.                              |
| `allowLists`         | `boolean`                 | `true`  | No       | Whether to show list functionality in the toolbar.                                   |
| `showWordCount`      | `boolean`                 | `true`  | No       | Whether to show the word count in the footer.                                        |
| `showCharacterCount` | `boolean`                 | `true`  | No       | Whether to show the character count in the footer.                                   |
| `disabled`           | `boolean`                 | `false` | No       | Disables all editing and applies opacity-50. Hides the toolbar.                      |
| `readOnly`           | `boolean`                 | `false` | No       | Makes the editor non-editable while keeping full visual fidelity. Hides the toolbar. |
| `onFocus`            | `() => void`              | —       | No       | Called when the editor area receives focus.                                          |
| `onBlur`             | `() => void`              | —       | No       | Called when the editor area loses focus.                                             |
| `minHeight`          | `string`                  | —       | No       | Minimum height of the editable area (CSS value, e.g. `"200px"`).                     |
| `maxHeight`          | `string`                  | —       | No       | Maximum height of the editable area (CSS value, e.g. `"600px"`).                     |

---

## Examples

### Basic Implementation

```tsx
import { useState } from 'react';
import { RichTextEditor } from 'xertica-ui';
import { Button } from 'xertica-ui/button';

export function EditorExample() {
  const [content, setContent] = useState('<p>Start typing your document...</p>');

  return (
    <div className="h-[400px]">
      <RichTextEditor
        value={content}
        onChange={setContent}
        placeholder="Escreva algo brilhante..."
        actionButton={
          <Button size="sm" onClick={() => console.log(content)}>
            Save
          </Button>
        }
      />
    </div>
  );
}
```

### Minimal Toolbar

```tsx
<RichTextEditor
  value={content}
  onChange={setContent}
  allowHeadings={false}
  allowAlignment={false}
  allowLists={false}
  allowLinks={false}
  allowSearch={false}
  showWordCount={false}
  showCharacterCount={false}
/>
```

---

## Headless Hook — `useRichTextEditor()`

Use the headless hook when you need full control over the editor's UI — custom toolbar layout, custom styling, or embedding the editor logic inside a larger component.

### Import

```tsx
import { useRichTextEditor } from 'xertica-ui/ui';
```

### Hook Props

| Prop       | Type                      | Required | Description                                |
| ---------- | ------------------------- | -------- | ------------------------------------------ |
| `value`    | `string`                  | Yes      | Current HTML value of the editor           |
| `onChange` | `(value: string) => void` | No       | Called whenever the editor content changes |

### Hook Return Value

| Property                  | Type                                         | Description                                              |
| ------------------------- | -------------------------------------------- | -------------------------------------------------------- |
| `editorRef`               | `RefObject<HTMLDivElement>`                  | Attach to the `contenteditable` div                      |
| `searchInputRef`          | `RefObject<HTMLInputElement>`                | Attach to the search `<input>`                           |
| `linkInputRef`            | `RefObject<HTMLInputElement>`                | Attach to the link URL `<input>`                         |
| `activeFormats`           | `ActiveFormats`                              | Map of currently active formatting commands              |
| `isSearchOpen`            | `boolean`                                    | Whether the inline search bar is open                    |
| `setIsSearchOpen`         | `(open: boolean) => void`                    | Toggle the search bar                                    |
| `searchQuery`             | `string`                                     | Current search query string                              |
| `setSearchQuery`          | `(query: string) => void`                    | Update the search query                                  |
| `linkUrl`                 | `string`                                     | Current link URL being edited                            |
| `setLinkUrl`              | `(url: string) => void`                      | Update the link URL                                      |
| `isLinkOpen`              | `boolean`                                    | Whether the link popover is open                         |
| `wordCount`               | `number`                                     | Word count of the editor content                         |
| `characterCount`          | `number`                                     | Character count of the editor content                    |
| `updateActiveFormats`     | `() => void`                                 | Re-read the current selection and update `activeFormats` |
| `execCommand`             | `(command: string, value?: string) => void`  | Execute a `document.execCommand` and sync state          |
| `handleInput`             | `() => void`                                 | Call on every `input` event of the editor                |
| `performSearch`           | `(text: string, backward?: boolean) => void` | Search forward or backward                               |
| `handleCreateLink`        | `() => void`                                 | Create or update a hyperlink at the current selection    |
| `handleUnlink`            | `() => void`                                 | Remove the hyperlink at the current cursor position      |
| `onLinkPopoverOpenChange` | `(open: boolean) => void`                    | Handle link popover open/close                           |

### `ActiveFormats` Type

```typescript
interface ActiveFormats {
  bold: boolean;
  italic: boolean;
  underline: boolean;
  justifyLeft: boolean;
  justifyCenter: boolean;
  justifyRight: boolean;
  insertUnorderedList: boolean;
  insertOrderedList: boolean;
  link: boolean;
  h1: boolean;
  h2: boolean;
  h3: boolean;
  p: boolean;
}
```

### Headless Hook Example

```tsx
import { useState } from 'react';
import { useRichTextEditor } from 'xertica-ui/ui';

function MyMinimalEditor({ value, onChange }) {
  const { editorRef, activeFormats, execCommand, handleInput, wordCount } = useRichTextEditor({
    value,
    onChange,
  });

  return (
    <div className="border rounded-lg overflow-hidden">
      {/* Custom minimal toolbar */}
      <div className="flex gap-1 p-2 border-b bg-muted/30">
        <button
          onClick={() => execCommand('bold')}
          className={activeFormats.bold ? 'font-bold text-primary' : 'text-muted-foreground'}
        >
          B
        </button>
        <button
          onClick={() => execCommand('italic')}
          className={activeFormats.italic ? 'italic text-primary' : 'text-muted-foreground'}
        >
          I
        </button>
        <button
          onClick={() => execCommand('underline')}
          className={activeFormats.underline ? 'underline text-primary' : 'text-muted-foreground'}
        >
          U
        </button>
      </div>

      {/* Editor area */}
      <div
        ref={editorRef}
        contentEditable
        onInput={handleInput}
        className="p-4 min-h-[200px] focus:outline-none"
        data-placeholder="Start typing..."
      />

      {/* Custom footer */}
      <div className="px-4 py-1 border-t text-xs text-muted-foreground">{wordCount} words</div>
    </div>
  );
}
```

---

## Component Architecture Notes

- **Data Binding:** The editor takes `value` _only for its initial render mount_, then operates autonomously internally. It uses continuous `innerHTML` tracking out to the `onChange` event. Avoid forcefully re-rendering the component from the top-down while typing, as it can cause cursor jump displacement.
- **Radix UI Interaction:** The `DropdownMenu` inside the toolbar is customized with `modal={false}` and intercepts `onPointerDown` events on its MenuItems. This is a critical technical choice that prevents modern React Portals from stealing DOM focus away from the `contentEditable` area when selecting Headings.
- **CSS Encapsulation:** Core semantic elements (`h1`, `h2`, `h3`, `p`, `ul`, `b`, `i`) are uniquely targeted via the internal `.prose-editor` stylesheet, freeing the global scope from collisions.
- **Headless Hook:** All state, refs, and DOM-manipulation logic live in `useRichTextEditor`. The `RichTextEditor` component is a thin presentation layer on top of this hook.

---

## Accessibility

The `contentEditable` area ships with the following ARIA attributes (v2.1.9+):

| Attribute        | Value                              | Description                                                    |
| ---------------- | ---------------------------------- | -------------------------------------------------------------- |
| `role`           | `"textbox"`                        | Identifies the editable area as a text input to screen readers |
| `aria-multiline` | `"true"`                           | Signals that the textbox accepts multiple lines                |
| `aria-label`     | `placeholder \| "Editor de texto"` | Accessible name — uses `placeholder` when provided             |
| `aria-readonly`  | `readOnly` prop                    | Signals non-editable state to AT without hiding the toolbar    |
| `aria-disabled`  | `disabled` prop                    | Signals disabled state to AT                                   |

> **Note**: The footer "Auto-save ativo" label was removed in v2.1.9. Word and character counters are now reactive `useState` values — they update on every keystroke via `handleInput` and re-sync when `value` changes externally.

---

## AI Rules

```
- ALWAYS use `useRichTextEditor()` when building a custom editor UI — never re-implement `execCommand` logic manually.
- The `editorRef` from the hook MUST be attached to the `contenteditable` div.
- Call `handleInput` on every `onInput` event of the editor div to keep `onChange` and `activeFormats` in sync.
- `execCommand` already calls `handleInput` internally — do not call both for the same action.
- `wordCount` and `characterCount` are reactive state values (useState) — they update after `handleInput` is called and re-sync when `value` changes externally. Do NOT try to read them from editorRef.current.innerText directly.
- The `activeFormats` object reflects the formatting state at the current cursor position. Use it to highlight active toolbar buttons.
- The contentEditable div already has ARIA attributes (role, aria-multiline, aria-label, aria-readonly, aria-disabled) — do NOT add them manually when using <RichTextEditor />.
```
