JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 418
  • Score
    100M100P100Q100624F
  • License MIT

UI headless React markdown editor using only textarea

Package Exports

  • textarea-markdown-editor

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (textarea-markdown-editor) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

forthebadge

Basic Overview

Markdown textarea is a simple markdown UI headless editor using only <textarea/>. It extend textarea by adding formatting features like shortcuts, invoked commands, and other to make user experience better 🙃

Essentially this library - just provide textarea Component. You can choose any engine for markdown rendering, any layout. Can use any existing textarea Component and style it as you prefer

đŸŽ¯ Features

  • Lists wrapping
  • Auto formatting pasted links
  • Indent tabulation
  • 20 built-in customizable commands

Installation and usage

Quick Start âšĄī¸

import React, { Fragment, useRef, useState } from "react";
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor";

function App() {
    const [value, setValue] = useState("");
    const ref = useRef<TextareaMarkdownRef>(null);

    return (
        <Fragment>
            <button onClick={() => ref.current?.trigger("bold")}>Bold</button>
            <TextareaMarkdown ref={ref} value={value} onChange={(e) => setValue(e.target.value)} />
        </Fragment>
    );
}

Customize commands

You can specify or overwrite shortcuts and toggle commands

import React, { useRef, useState } from "react";
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor";

function App() {
    const [value, setValue] = useState("");
    const ref = useRef<TextareaMarkdownRef>(null);

    return (
        <TextareaMarkdown
            ref={ref}
            value={value}
            onChange={(e) => setValue(e.target.value)}
            commands={[
                {
                    name: "code",
                    shortcut: ["command+/", "ctrl+/"],
                    shortcutPreventDefault: true,
                },
                {
                    name: "indent",
                    enable: false,
                },
            ]}
        />
    );
}

â„šī¸ Mousetrap.js is used under the hood for shortcuts handling. It is great solution with simple and intuitive api. You can read more about combination in the documentation if necessary

Custom textarea Component

You can use custom textarea Component. Just wrap it with TextareaMarkdown.Wrapper

import React, { useRef, useState } from "react";
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor";
import TextareaAutosize from "react-textarea-autosize";

function App() {
    const [value, setValue] = useState("");
    const ref = useRef<TextareaMarkdownRef>(null);

    return (
        <TextareaMarkdown.Wrapper ref={ref}>
            <TextareaAutosize value={value} onChange={(e) => setValue(e.target.value)} />
        </TextareaMarkdown.Wrapper>
    );
}

â„šī¸ This solution will not create any real dom wrapper

API

TextareaMarkdownProps

TextareaMarkdown Component props

â„šī¸ extends HTMLTextAreaElement props

options?: TextareaMarkdownOptions;
commands?: CommandDefine[];

TextareaMarkdownWrapperProps

TextareaMarkdown.Wrapper Component props

options?: TextareaMarkdownOptions;
commands?: CommandDefine[];

TextareaMarkdownOptions

Option prop config

/** toggle auto wrapping with link markup when pasting the selected word */
useLinkMarkupOnSelectionPasteUrl?: boolean;

/** toggle tabulation lists prefix with content  */
useIndentListPrefixTabulation?: boolean;

/** unordered list prefix syntax  */
unorderedListSyntax?: "-" | "*";

/** bold wrapper syntax  */
boldSyntax?: "**" | "__";

/** italic wrapper syntax  */
italicSyntax?: "*" | "_";

CommandDefine

Commands array item

name: string;
/** for custom commands */
handler?: CommandHandler;
shortcut?: string | string[];
shortcutPreventDefault?: boolean;
enable?: boolean;

CommandHandler

Custom handler signature

export type CommandHandler = (context: CommandHandlerContext) => void | Promise<void> | Promise<string> | string;

export type CommandHandlerContext = {
    element: HTMLTextAreaElement;
    keyEvent?: KeyboardEvent;
    options: TextareaMarkdownOptions;
};

TextareaMarkdownRef

Ref TextareaMarkdown or TextareaMarkdown.Wrapper instance

â„šī¸ extends HTMLTextAreaElement instance

trigger: (command: string) => void;

Advanced usage đŸ§Ŧ

You can implement your own commands. For this you need to registry command by adding new item in commands array. CommandConfig contain name, handler and optional shortcut.

Handler - invoked by trigger call or by pressing shortcuts and it make side effect with textarea. Basically you can make with element whatever you want, but most likely you need to manipulate with content. For this purpose you can use Cursor service. This wrapper combines content and selection manipulation and also proved calculated information about position context and more.

import React, { Fragment, useRef, useState } from "react";
import TextareaMarkdown, { TextareaMarkdownRef, Cursor, CommandHandler } from "textarea-markdown-editor";

/** Inserts 🙃 at the end of the line and select it */
const emojiCommandHandler: CommandHandler = ({ element }) => {
    const cursor = new Cursor(element);
    const currentLine = cursor.getLine();

    // Cursor.$ - marker means cursor position, if specified two markers indicate a selection range
    cursor.spliceContent(Cursor.raw`${currentLine} ${Cursor.$}🙃${Cursor.$}`, {
        replaceCount: 1, // replace current line
    });
};

function App() {
    const [value, setValue] = useState("");
    const ref = useRef<TextareaMarkdownRef>(null);

    return (
        <Fragment>
            <button onClick={() => ref.current?.trigger("insert-emoji")}>Insert 🙃</button>
            <TextareaMarkdown
                ref={ref}
                value={value}
                onChange={(e) => setValue(e.target.value)}
                commands={[{ name: "insert-emoji", handler: emojiCommandHandler }]}
            />
        </Fragment>
    );
}

export default App;

👀 You can find more examples here

â„šī¸ Note that mutation element.value will not trigger change event on textarea element. Use cursor.setValue(...) or just return new content from handler.