> ## Documentation Index
> Fetch the complete documentation index at: https://cometchat-22654f5b-docs-rn-guide-message-privately.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Text Formatter

> Custom Text Formatter — CometChat integration guide.

## Overview

You can create your custom text formatter for CometChat using the `CometChatTextFormatter`. `CometChatTextFormatter` is an abstract utility class that serves as a foundational structure for enabling text formatting in the message composer and text message bubbles. It can be extended to create custom formatter classes, tailored to suit specific application needs, making it a valuable tool for text customization and enhancement in chat interfaces.

## Features

* **Text Formatting:**: Enables automatic formatting of text within messages based on specified styles and settings, enhancing the visual presentation of chat content.
* **Customizable Styles:**: Tailor text styles, including colors, fonts, and background colors, to match the desired appearance for formatted text.
* **Dynamic Text Replacement:**: Utilizes regular expression patterns to identify and replace specific text patterns with formatted content, offering flexibility in text manipulation.
* **Input Field Integration:**: Seamlessly integrates with the text input field, allowing for real-time monitoring and processing of user input for formatting purposes.
* **Callback Functions:**: Implement callback functions for key up and key down events, providing hooks for custom actions or formatting logic based on user interactions.

## Usage

here are the steps to create your custom text formatter for CometChat using the `CometChatTextFormatter`:

To integrate the `CometChatTextFormatter` class into your application:

1. Firstly, you need to import `CometChatTextFormatter` from the CometChat UI Kit react library.

```javascript theme={null}
import { CometChatTextFormatter } from "@cometchat/chat-uikit-react";
```

2. Now, extend the `CometChatTextFormatter` class to create your custom text formatter class. In this case, let's create a `HashTagTextFormatter`.

```javascript theme={null}
class HashTagTextFormatter extends CometChatTextFormatter {
  ...
}
```

3. set up the `trackCharacter`, `regexPatterns` and `regexToReplaceFormatting`. For a hashtag formatter, we're setting the track character to `#`.

```javascript theme={null}
this.setTrackingCharacter("#");
this.setRegexPatterns([/\B#(\w+)\b/g]);
this.setRegexToReplaceFormatting([
  /<span style="color: #30b3ff;">#(\w+)<\/span>/g,
]);
```

4. Set callback functions for key up and key down events.

```javascript theme={null}
this.setKeyUpCallBack(this.onKeyUp.bind(this));
this.setKeyDownCallBack(this.onKeyDown.bind(this));
```

5. Implement the `getFormattedText`, `getOriginalText` and custom logic to format text methods.

```javascript theme={null}
getFormattedText(inputText:string) { ... }
getOriginalText(inputText:string) { ... }
customLogicToFormatText(inputText: string) { ... }
```

## Example

Below is an example demonstrating how to use a custom formatter class in components such as [CometChatConversations](/ui-kit/react/v4/conversations), [CometChatMessageList](/ui-kit/react/v4/message-list), [CometChatMessageComposer](/ui-kit/react/v4/message-composer).

<Frame>
  <img src="https://mintcdn.com/cometchat-22654f5b-docs-rn-guide-message-privately/fxK75AS6FzDL1Oqo/images/b4954228-custom_hashtag_formatter_web_screens-046d5772c55ca5dd4e4761a74cd27a7c.png?fit=max&auto=format&n=fxK75AS6FzDL1Oqo&q=85&s=843acc778e103c8800f39acf2b1a8a68" width="3600" height="2400" data-path="images/b4954228-custom_hashtag_formatter_web_screens-046d5772c55ca5dd4e4761a74cd27a7c.png" />
</Frame>

<Tabs>
  <Tab title="HashTagTextFormatter.ts">
    ```typescript theme={null}
    import { CometChatTextFormatter } from "@cometchat/chat-uikit-react";

    class HashTagTextFormatter extends CometChatTextFormatter {
      constructor() {
        super();
        // Set the tracking character to #
        this.setTrackingCharacter("#");

        // Define regex patterns to find specific text patterns
        this.setRegexPatterns([
          /\B#(\w+)\b/g, // Matches hashtags starting with #
        ]);

        // Define regex patterns to replace formatter text with original text
        this.setRegexToReplaceFormatting([
          /#(\w+)/g, // Replace hashtags without formatting
        ]);

        // Set callback functions for key up and key down events
        this.setKeyUpCallBack(this.onKeyUp.bind(this));
        this.setKeyDownCallBack(this.onKeyDown.bind(this));

        // Set the re-render callback function
        this.setReRender(() => {
          // Trigger re-rendering of the message composer component
          // This could involve updating the UI with the formatted text
          console.log("Re-rendering message composer to update text content.");
        });

        // Initialize composer tracking
        this.initializeComposerTracking();
      }

      initializeComposerTracking() {
        // Get the reference to the input field (composer)
        const composerInput = document.getElementById("yourComposerInputId");

        // Set the reference to the input field
        this.setInputElementReference(composerInput);
      }

      getCaretPosition(): number {
        if (!this.inputElementReference) return 0;

        const selection = window.getSelection();
        if (!selection || selection.rangeCount === 0) return 0;

        const range = selection.getRangeAt(0);
        const clonedRange = range.cloneRange();
        clonedRange.selectNodeContents(this.inputElementReference);
        clonedRange.setEnd(range.endContainer, range.endOffset);

        return clonedRange.toString().length;
      }

      setCaretPosition(position: number) {
        if (!this.inputElementReference) return;

        const range = document.createRange();
        const selection = window.getSelection();
        if (!selection) return;

        range.setStart(
          this.inputElementReference.childNodes[0] || this.inputElementReference,
          position
        );
        range.collapse(true);

        selection.removeAllRanges();
        selection.addRange(range);
      }

      onKeyUp(event: KeyboardEvent) {
        if (event.key === this.trackCharacter) {
          this.startTracking = true;
        }
        // Custom logic to format hashtags as users type in the composer
        if (this.startTracking && (event.key === " " || event.key === "Enter")) {
          const caretPosition = this.getCaretPosition();
          this.formatText();
          this.setCaretPosition(caretPosition);
        }
        // Check if the last character typed was not a space or enter
        // and if the caret position is at the end, then we need to set start tracking to false
        if (
          this.startTracking &&
          event.key !== " " &&
          event.key !== "Enter" &&
          this.getCaretPosition() === this.inputElementReference?.innerText?.length
        ) {
          this.startTracking = false;
        }
      }

      formatText() {
        const inputValue =
          this.inputElementReference?.innerText ||
          this.inputElementReference?.textContent ||
          "";
        const formattedText = this.getFormattedText(inputValue);
        // Update the composer with formatted text
        if (this.inputElementReference) {
          this.inputElementReference.innerHTML = formattedText || "";
          // Trigger re-render of the composer
          this.reRender();
        }
      }

      onKeyDown(event: KeyboardEvent) {
        // Custom onKeyDown logic can be added here
      }

      getFormattedText(inputText: string) {
        if (!inputText) {
          // Edit at cursor position and return void
          return;
        }
        // Perform custom logic to format text (e.g., formatting hashtags)
        return this.customLogicToFormatText(inputText);
      }

      customLogicToFormatText(inputText: string) {
        // Replace hashtags with HTML span elements to change color to green
        return inputText.replace(
          /\B#(\w+)\b/g,
          '<span style="color: #5dff05;">#$1</span>'
        );
      }

      getOriginalText(inputText: string) {
        // Sample implementation
        if (!inputText) {
          return "";
        }
        // Remove formatting and return original text
        for (let i = 0; i < this.regexToReplaceFormatting.length; i++) {
          let regexPattern = this.regexToReplaceFormatting[i];

          if (inputText) {
            inputText = inputText.replace(regexPattern, "#$1");
          }
        }

        return inputText;
      }
    }

    export default HashTagTextFormatter;
    ```
  </Tab>

  <Tab title="MessageListDemo.tsx">
    ```typescript theme={null}
    import { HashTagTextFormatter } from "./HashTagTextFormatter";

    export default function MessageListDemo() {

        const [chatUser, setChatUser] = React.useState<CometChat.User | undefined>();

        React.useEffect(() => {
            CometChat.getUser("uid").then((user) => {
                setChatUser(user);
            })
        }, [])
      return (
        <div className='App'>
          <div>
            <CometChatMessageList
            user={chatUser}
            textFormatters={[new HashTagTextFormatter()]}
            />
          </div>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

## Methods

| Methods                              | Setter                                                                           | Description                                                                                                                                                                                                                            |
| ------------------------------------ | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| protected `trackCharacter`           | `setTrackingCharacter(trackCharacter: string)`                                   | The character to track in the text input field. This can be used to start capturing texts to format. Example: This would be `#` in case of HashTags. And a HashTag formatter would start capturing texts as soon as it encounters `#`. |
| protected `currentCaretPosition`     | `setCaretPositionAndRange(currentCaretPosition: Selection, currentRange: Range)` | Message Composer sets the current selection in this field                                                                                                                                                                              |
| protected `currentRange`             | `setCaretPositionAndRange(currentCaretPosition: Selection, currentRange: Range)` | Message Composer sets the text range that the user has selected or the cursor position in this field                                                                                                                                   |
| protected `inputElementReference`    | `setInputElementReference(inputElementReference: HTMLElement)`                   | Message Composer sets the Reference to the text input field(DOM element) in this field. This can be used to `read` the `textContent`.                                                                                                  |
| protected `regexPatterns`            | `setRegexPatterns(regexPatterns: Array<RegExp>)`                                 | Array of regex patterns to find specific text pattern in the user input text in order to replace those with formatted texts                                                                                                            |
| protected `regexToReplaceFormatting` | `setRegexToReplaceFormatting(regexToReplaceFormatting: Array<RegExp>)`           | Array of regex patterns to replace the formatter text with original text                                                                                                                                                               |
| protected `keyUpCallBack`            | `setKeyUpCallBack(keyUpCallBack: Function)`                                      | The callback function to be called for key up events                                                                                                                                                                                   |
| protected `keyDownCallBack`          | `setKeyDownCallBack(keyDownCallBack: Function)`                                  | The callback function to be called for key down events                                                                                                                                                                                 |
| protected `reRender`                 | `setReRender(reRender: Function)`                                                | Callback function to be called in order to trigger a re-render of the message composer component. This is useful for force rerendering the composer in order to update text content on the UI.                                         |
| protected `loggedInUser`             | `setLoggedInUser(loggedInUser: CometChat.User)`                                  | Composer and text bubbles will set the logged in user object in this field.                                                                                                                                                            |
| protected `id`                       | `setId(id: string)`                                                              | Use this to set a unique id for the formatter instance.                                                                                                                                                                                |

<Warning>
  The `textContent` or `innerHtml` of the reference element should not be directly modified in a formatter. The formatters should instead call the `reRender` callback to force the composer to call the `getFormattedText` function of the formatter instead.
</Warning>

<Note>
  Upon calling `reRender`, the composer will invoke the `getFormattedText` function for all text formatters, following the same order in which they were received as props.
</Note>

## Methods to be overriden for your custom formatter implementations:

<Tabs>
  <Tab title="getFormattedText">
    ```javascript theme={null}
      /**
       * If the input text is provided, it returns the formatted text. Otherwise, it edits the text using the current cursor position.
       * @param {string|null} inputText - The text to format.
       * @return {string|void} - The original or formatted input text, or void if editing was done based on cursor position.
       */
      getFormattedText(inputText: string | null, params: any): string | void {
        if (!inputText) {
          //edit at cursor position and return void
          return;
        }
        return this.customLogicToFromatText(inputText);
      }
    ```
  </Tab>

  <Tab title="onKeyUp">
    ```javascript theme={null}
    /**
     * Handles 'keydown' events.
     * @param {KeyboardEvent} event - The keyboard event.
     */
    onKeyUp(event: KeyboardEvent) {
      if (event.key == this.trackCharacter) {
        this.startTracking = true;
      }
      //sample implementation
      if (this.startTracking && event.key == " ") {
        this.debouncedFormatTextOnKeyUp();
      }
    }
    ```
  </Tab>

  <Tab title="onKeyDown">
    ```javascript theme={null}
    /**
     * Handles 'keydown' events.
     * @param {KeyboardEvent} event - The keyboard event.
     */
    onKeyDown(event: KeyboardEvent) {}
    ```
  </Tab>

  <Tab title="registerEventListeners">
    ```javascript theme={null}
    /**
     * Composer and Text Bubbles will call this function when rendering the HTML content.
     * This will be called for each HTML Element present in the formatted string.
     * Each formatter will check if the HTML Element belongs to the formatter before registering event listeners
     * Formatters can identify the respective HTML Element using unique css classes
     * @param {HTMLElement} element - The element on which the events need to be registered
     * @param {DOMTokenList} domTokenList - The classes to be added
     * @return {HTMLElement} - The element with the registered event listeners
     */
    registerEventListeners(element: HTMLElement, domTokenList: DOMTokenList) {
      //sample implementation
      let classList: string[] = Array.from(domTokenList);
      for (let i = 0; i < classList.length; i++) {
        //mentionsCssClassMapping is a list of unique css classes Mentions Formatter checks for
        if (classList[i] in this.mentionsCssClassMapping) {
          element.addEventListener("click", (event: Event) => {
            clearTimeout(this.timeoutID);
            CometChatUIEvents.ccMouseEvent.next({
              body: {
                CometChatUserGroupMembersObject:
                  this.mentionsCssClassMapping[classList[i]],
                message: this.messageObject ?? null,
                id: this.getId(),
              },
              event,
              source: MouseEventSource.mentions,
            });
          });

          element.addEventListener("mouseover", (event: Event) => {
            this.timeoutID = setTimeout(() => {
              this.mouseOverEventDispatched = true;
              CometChatUIEvents.ccMouseEvent.next({
                body: {
                  CometChatUserGroupMembersObject:
                    this.mentionsCssClassMapping[classList[i]],
                  message: this.messageObject ?? null,
                  id: this.getId(),
                },
                event,
                source: MouseEventSource.mentions,
              });
            }, CometChatUtilityConstants.MentionsFormatter.MENTIONS_HOVER_TIMEOUT);
          });

          element.addEventListener("mouseout", (event: Event) => {
            clearTimeout(this.timeoutID);
            if (this.mouseOverEventDispatched) {
              CometChatUIEvents.ccMouseEvent.next({
                body: {
                  CometChatUserGroupMembersObject:
                    this.mentionsCssClassMapping[classList[i]],
                  message: this.messageObject ?? null,
                  id: this.getId(),
                },
                event,
                source: MouseEventSource.mentions,
              });
              this.mouseOverEventDispatched = false;
            }
          });
        }
      }
      return element;
    }
    ```
  </Tab>

  <Tab title="getOriginalText">
    ```javascript theme={null}
      /**
       * Returns the original unformatted text from the input text.
       * @param {string|null|undefined} inputText - The input text to get original text from.
       * @return {string} - The original text.
       */
      getOriginalText(inputText: string | null | undefined): string {
        //sample implmentation
        if (!inputText) {
          return "";
        }
        for (let i = 0; i < this.regexToReplaceFormatting.length; i++) {
          let regexPattern = this.regexToReplaceFormatting[i];

          if (inputText) {
            inputText = inputText.replace(regexPattern, "$1");
          }
        }

        return inputText;

    }
    ```
  </Tab>
</Tabs>
