Javascript-Tiptap [email protected]: A rich-text editor for Vue.js

icon
Latest Release: [email protected]
  • add async suggestions (#757, fix #755)
  • update dependencies
Source code(tar.gz)
Source code(zip)

tiptap

A renderless and extendable rich-text editor for Vue.js

Why I built tiptap

I was looking for a text editor for Vue.js and found some solutions that didn't really satisfy me. The editor should be easy to extend and not based on old dependencies such as jQuery. For React there is already a great editor called Slate.js, which impresses with its modularity. I came across Prosemirror and decided to build on it. Prosemirror is a toolkit for building rich-text editors that is already in use at many well-known companies such as Atlassian or New York Times.

What means renderless?

Will renderless components you'll have (almost) full control over markup and styling. I don't want to tell you what a menu should look like or where it should be rendered in the DOM. That's all up to you. There is also a good article about renderless components by Adam Wathan.

How is the data stored under the hood?

You can save your data as a raw HTML string or can get a JSON-serializeable representation of your document. And of course you can pass these two types back to the editor.

Examples

To check out some live examples, visit tiptap.scrumpy.io.

Installation

npm install tiptap

or

yarn add tiptap

Basic Setup

<template>
  <editor>
    <!-- Add HTML to the scoped slot called `content` -->
    <div slot="content" slot-scope="props">
      <p>Hi, I'm just a boring paragraph</p>
    </div>
  </editor>
</template>

<script>
// Import the editor
import { Editor } from 'tiptap'

export default {
  components: {
    Editor,
  },
}
</script>

Editor Properties

Property Type Default Description
editable Boolean true When set to false the editor is read-only.
doc Object null The editor state object used by Prosemirror. You can also pass HTML to the content slot. When used both, the content slot will be ignored.
extensions Array [] A list of extensions used, by the editor. This can be Nodes, Marks or Plugins.
@init Object undefined This will return an Object with the current state and view of Prosemirror on init.
@update Object undefined This will return an Object with the current state of Prosemirror, a getJSON() and getHTML() function on every change.

Scoped Slots

Name Description
editor Here the content will be rendered.
menubar Here a menu bar will be rendered.
menububble Here a menu bubble will be rendered.

Slot Properties

The menubar and menububble slot will receive some properties.

Property Type Description
nodes Object A list of available nodes with active state and command.
marks Object A list of available marks with active state and command.
focused Boolean Whether the editor is focused.
focus Function A function to focus the editor.

Extensions

By default the editor will only support paragraphs. Other nodes and marks are available as extensions. There is a package called tiptap-extensions with the most basic nodes, marks and plugins.

Available Extensions

<template>
  <editor :extensions="extensions">
    <div slot="content" slot-scope="props">
      <h1>Yay Headlines!</h1>
      <p>All these <strong>cool tags</strong> are working now.</p>
    </div>
  </editor>
</template>

<script>
import { Editor } from 'tiptap'
import {
  // Nodes
  BlockquoteNode,
  BulletListNode,
  CodeBlockNode,
  CodeBlockHighlightNode,
  HardBreakNode,
  HeadingNode,
  ImageNode,
  ListItemNode,
  OrderedListNode,
  TodoItemNode,
  TodoListNode,

  // Marks
  BoldMark,
  CodeMark,
  ItalicMark,
  LinkMark,
  StrikeMark,

  // General Extensions
  HistoryExtension,
  PlaceholderExtension,
} from 'tiptap-extensions'

export default {
  components: {
    Editor,
  },
  data() {
    return {
      extensions: [
        new BlockquoteNode(),
        new BulletListNode(),
        new CodeBlockNode(),
        new HardBreakNode(),
        new HeadingNode({ maxLevel: 3 }),
        new ImageNode(),
        new ListItemNode(),
        new OrderedListNode(),
        new TodoItemNode(),
        new TodoListNode(),
        new BoldMark(),
        new CodeMark(),
        new ItalicMark(),
        new LinkMark(),
        new StrikeMark(),
        new HistoryExtension(),
        new PlaceholderExtension(),
      ],
    }
  },
}
</script>

Create Custom Extensions

The most powerful feature of tiptap is that you can create your own extensions. There are 3 types of extensions.

Type Description
Extension The most basic type. It's useful to register some Prosemirror plugins or some input rules.
Node Add a custom node. Nodes are basically block elements like a headline or a paragraph.
Mark Add a custom mark. Marks are used to add extra styling or other information to inline content like a strong tag or links.

Extension Class

Method Type Default Description
get name() String null Define a name for your extension.
get defaultOptions() Object {} Define some default options. The options are available as this.$options.
get plugins() Array [] Define a list of Prosemirror plugins.
keys({ schema }) Object null Define some keybindings.
inputRules({ schema }) Array [] Define a list of input rules.

Node|Mark Class

Method Type Default Description
get name() String null Define a name for your node or mark.
get defaultOptions() Object {} Define some default options. The options are available as this.$options.
get schema() Object null Define a schema.
get view() Object null Define a node view as a vue component.
keys({ type, schema }) Object null Define some keybindings.
command({ type, schema, attrs }) Object null Define a command. This is used for menus to convert to this node or mark.
inputRules({ type, schema }) Array [] Define a list of input rules.
get plugins() Array [] Define a list of Prosemirror plugins.

Create a Node

Let's take a look at a real example. This is basically how the default blockquote node from tiptap-extensions looks like.

import { Node } from 'tiptap'
import { wrappingInputRule, setBlockType, wrapIn } from 'tiptap-commands'

export default class BlockquoteNode extends Node {

  // choose a unique name
  get name() {
    return 'blockquote'
  }

  // the prosemirror schema object
  // take a look at https://prosemirror.net/docs/guide/#schema for a detailed explanation
  get schema() {
    return {
      content: 'block+',
      group: 'block',
      defining: true,
      draggable: false,
      // define how the editor will detect your node from pasted HTML
      // every blockquote tag will be converted to this blockquote node
      parseDOM: [
        { tag: 'blockquote' },
      ],
      // this is how this node will be rendered
      // in this case a blockquote tag with a class called `awesome-blockquote` will be rendered
      // the '0' stands for its text content inside
      toDOM: () => ['blockquote', { class: 'awesome-blockquote' }, 0],
    }
  }

  // this command will be called from menus to add a blockquote
  // `type` is the prosemirror schema object for this blockquote
  // `schema` is a collection of all registered nodes and marks
  command({ type, schema }) {
    return wrapIn(type)
  }

  // here you can register some shortcuts
  // in this case you can create a blockquote with `ctrl` + `>`
  keys({ type }) {
    return {
      'Ctrl->': wrapIn(type),
    }
  }

  // a blockquote will be created when you are on a new line and type `>` followed by a space
  inputRules({ type }) {
    return [
      wrappingInputRule(/^\s*>\s$/, type),
    ]
  }

}

Create a Node as a Vue Component

The real power of the nodes comes in combination with Vue components. Lets build an iframe node, where you can change its url (this can also be found in our examples).

import { Node } from 'tiptap'

export default class IframeNode extends Node {

  get name() {
    return 'iframe'
  }

  get schema() {
    return {
      // here you have to specify all values that can be stored in this node
      attrs: {
        src: {
          default: null,
        },
      },
      group: 'block',
      selectable: false,
      // parseDOM and toDOM is still required to make copy and paste work
      parseDOM: [{
        tag: 'iframe',
        getAttrs: dom => ({
          src: dom.getAttribute('src'),
        }),
      }],
      toDOM: node => ['iframe', {
        src: node.attrs.src,
        frameborder: 0,
        allowfullscreen: 'true',
      }],
    }
  }

  // return a vue component
  // this can be an object or an imported component
  get view() {
    return {
      // there are some props available
      // `node` is a Prosemirror Node Object
      // `updateAttrs` is a function to update attributes defined in `schema`
      // `editable` is the global editor prop whether the content can be edited
      props: ['node', 'updateAttrs', 'editable'],
      data() {
        return {
          // save the iframe src in a new variable because `this.node.attrs` is immutable
          url: this.node.attrs.src,
        }
      },
      methods: {
        onChange(event) {
          this.url = event.target.value

          // update the iframe url
          this.updateAttrs({
            src: this.url,
          })
        },
      },
      template: `
        <div class="iframe">
          <iframe class="iframe__embed" :src="url"></iframe>
          <input class="iframe__input" type="text" :value="url" @input="onChange" v-if="editable" />
        </div>
      `,
    }
  }

}

Building a Menu

This is a basic example of building a custom menu. A more advanced menu can be found at the examples page.

<template>
  <editor :extensions="extensions">
    <div slot="menubar" slot-scope="{ nodes, marks }">
      <div v-if="nodes && marks">
        <button :class="{ 'is-active': nodes.heading.active({ level: 1 }) }" @click="nodes.heading.command({ level: 1 })">
          H1
        </button>
        <button :class="{ 'is-active': marks.bold.active() }" @click="marks.bold.command()">
          Bold
        </button>
      </div>
    </div>
    <div slot="content" slot-scope="props">
      <p>This text can be made bold.</p>
    </div>
  </editor>
</template>

<script>
import { Editor } from 'tiptap'
import { HeadingNode, BoldMark } from 'tiptap-extensions'

export default {
  components: {
    Editor,
  },
  data() {
    return {
      extensions: [
        new HeadingNode({ maxLevel: 3 }),
        new BoldMark(),
      ],
    }
  },
}
</script>

Development Setup

Currently only Yarn is supported for development because of a feature called workspaces we are using here.

# install deps
yarn install

# serve examples at localhost:3000
yarn start

# build dist files for packages
yarn build:packages

# build dist files for examples
yarn build:examples

Contributing

Please see CONTRIBUTING for details.

Credits

License

The MIT License (MIT). Please see License File for more information.

Comments

  • UniqueID extension adds extra empty paragraphs when used with HocusPocus
    UniqueID extension adds extra empty paragraphs when used with HocusPocus

    Jan 18, 2022

    What’s the bug you are facing?

    If the UniqueId extension (2.0.0-beta.11) is enabled with collaborative editing (via HocusPocus), TipTap adds an empty paragraph (sometimes in different locations) on every page reload.

    How can we reproduce the bug on our side?

    TipTap editor setup:

    export const Tiptap = () => {
      const websocketProvider = useMemo(() => new HocuspocusProvider({
        url: 'ws://localhost:1234',
        name: 'unique-id-test-01',
      }), []);
    
      const editor = useEditor({
        extensions: [
          StarterKit.configure({
            history: false
          }),
          UniqueID.configure({
            types: ['paragraph'],
            // seems to happen with any filterTransaction,
            // whether returning true or false
            filterTransaction: isChangeOrigin,
          }),
          Collaboration.configure({
            document: websocketProvider.document,
          }),
        ],
      }, [websocketProvider]);
    
      return (
          <div style={{ border: '1px solid blue'}}>
            <EditorContent editor={editor} />
          </div>
      );
    };
    

    HocusPocus setup:

    const server = Server.configure({
      port: 1234,
      extensions: [
        new RocksDB()
      ]
    });
    server.listen();
    

    Steps to reproduce:

    • open the app
    • refresh the browser
    • see that it added an empty paragraph

    Can you provide a CodeSandbox?

    No response

    What did you expect to happen?

    No empty paragraphs should be added.

    Anything to add? (optional)

    No response

    Did you update your dependencies?

    • [X] Yes, I’ve updated my dependencies to use the latest version of all packages.

    Are you sponsoring us?

    • [X] Yes, I’m a sponsor. ?
    bug 
    Reply
  • When a table is the last node in the document and its last cell is empty, Ctrl+A fails to select anything but the first node of the document.
    When a table is the last node in the document and its last cell is empty, Ctrl+A fails to select anything but the first node of the document.

    Jan 18, 2022

    What’s the bug you are facing?

    When:

    • a table is the last node in the document
    • that table's last cell is empty

    Ctrl+A will select the first node of the document. image

    If the last cell of that table has any content, Ctrl+A will select everything as expected. image

    How can we reproduce the bug on our side?

    • open the document
    • add any paragraphs or other nodes
    • add a table as the last node
    • hit Ctrl+A to ensure "select all" works
    • leave the table blank OR only leave table's last (= bottom right) cell empty
    • hit Ctrl+A again to ensure "select all" will only select the first node in the document

    Can you provide a CodeSandbox?

    https://codesandbox.io/s/suspicious-swartz-xfx53?file=/src/App.js

    What did you expect to happen?

    I expect Ctrl+A to select all the nodes in the document, but it doesn't depending on table node's contents.

    Anything to add? (optional)

    First, I wanted to ensure where this behavior is coming from. So, I've tried to reproduce this on the clean demo of prosemirror-tables extension - but it works fine.

    Though on this tiptap demo it works exactly as in my project, i.e. "select all" is broken for the described scenario.

    Did you update your dependencies?

    • [X] Yes, I’ve updated my dependencies to use the latest version of all packages.

    Are you sponsoring us?

    • [ ] Yes, I’m a sponsor. ?
    bug 
    Reply
  • onUpdate callback does not update after re-render
    onUpdate callback does not update after re-render

    Jan 18, 2022

    What’s the bug you are facing?

    I have a wrapper around tiptap, whenever the text changes I trigger a request to my back-end, this works fine the first time tiptap is mounted but the parent component (where tiptap is mounted) can change its internal variables, and therefore the closure should capture a new context, the problem is that it doesn't after the parent component changes the state, the closure/lambda passed on the onUpdate function remains the same and therefore tiptap tries to update the wrong component.

    Here is some of the code, my high level component on the parent, notice the id param, which is the param that changes at some point:

    <Tiptap
      onFocus={({ editor }) => editor.commands.blur()}
      initialContent={project.notes ? JSON.parse(project.notes) : null}
      placeholder="You can add a default checklist in the settings."
      className="md:max-w-2xl lg:max-w-none"
      onChange={async (e) => {
        console.warn("URL PARAM ID", id) // ALWAYS REMAINS THE SAME, THEREFORE CANNOT UPDATE THE PROJECT CORRECTLY
    
        await updateProjectMutation({
          id,
          notes: JSON.stringify(e),
        })
        refetch()
      }}
      ref={tiptapRef}
    />
    

    My internal TIptap implementation, notice the onUpdate function that I'm passing to the useEditor hook:

    import Link from "@tiptap/extension-link"
    import Placeholder from "@tiptap/extension-placeholder"
    import TaskItem from "@tiptap/extension-task-item"
    import TaskList from "@tiptap/extension-task-list"
    import { BubbleMenu, EditorContent, Extension, useEditor } from "@tiptap/react"
    import StarterKit from "@tiptap/starter-kit"
    import React, { forwardRef, useImperativeHandle, useState } from "react"
    import { useBoolean } from "../hooks/useBoolean"
    import { Button } from "./Button"
    
    interface IProps {
      editable?: boolean
      onClick?: (this: unknown, view: any, pos: number, event: MouseEvent) => boolean
      initialContent?: any
      // content?: any
      onChange?: (content: any) => void
      autofocus?: boolean | null | "end" | "start"
      onFocus?: (params: { editor: any }) => void
      placeholder?: string
      className?: string
    }
    
    export const Tiptap = forwardRef<any, IProps>(
      (
        {
          editable = true,
          onClick,
          initialContent,
          onChange,
          autofocus,
          onFocus,
          placeholder,
          className,
          // content,
        },
        ref
      ) => {
        const [isAddingLink, addLinkOn, addLinkOff] = useBoolean()
        const [link, setLink] = useState("")
        const editor = useEditor({
          autofocus,
          onFocus: onFocus ? onFocus : () => {},
          editorProps: {
            attributes: {
              class: "prose focus:outline-none dark:prose-dark dark:text-gray-300 text-base",
            },
            editable: () => editable,
            handleClick: onClick,
          },
          content: initialContent,
          onUpdate: ({ editor }) => {
            onChange?.(editor.getJSON())
          },
          extensions: [
            StarterKit,
            Placeholder.configure({
              showOnlyWhenEditable: false,
              placeholder,
            }),
            TaskList.configure({
              HTMLAttributes: {
                class: "pl-0",
              },
            }),
            TaskItem.configure({
              HTMLAttributes: {
                class: "before:hidden pl-0 flex items-center dumb-prose-remove",
              },
            }),
            Extension.create({
              // Do not insert line break when pressing CMD+Enter
              // Most of the time handled by upper components
              addKeyboardShortcuts() {
                return {
                  "Cmd-Enter"() {
                    return true
                  },
                  "Ctrl-Enter"() {
                    return true
                  },
                }
              },
            }),
            Link,
          ],
        })
    
        useImperativeHandle(ref, () => ({
          getEditorInstance() {
            return editor
          },
        }))
    
    
        return (
            <EditorContent editor={editor} className={className} />
        )
      }
    )
    

    In any case, it seems the useEditor hook saves only the first passed onUpdate function and does not update it in sub-sequent renders

    How can we reproduce the bug on our side?

    Attached the code above, but if necessary I can try to reproduce the issue in a code sandbox

    Can you provide a CodeSandbox?

    No response

    What did you expect to happen?

    The passed callback onUpdate should be updated when a new value is passed to it, instead of constantly re-using the first memoized value

    Anything to add? (optional)

    I tried to update tiptap to the latest version but then I faced this other crash: https://github.com/ueberdosis/tiptap/issues/577 so I reverted to my old/current versions

    "@tiptap/extension-bubble-menu": "2.0.0-beta.51",
        "@tiptap/extension-link": "2.0.0-beta.33",
        "@tiptap/extension-placeholder": "2.0.0-beta.45",
        "@tiptap/extension-task-item": "2.0.0-beta.30",
        "@tiptap/extension-task-list": "2.0.0-beta.24",
        "@tiptap/react": "2.0.0-beta.98",
        "@tiptap/starter-kit": "2.0.0-beta.154",
    

    Did you update your dependencies?

    • [X] Yes, I’ve updated my dependencies to use the latest version of all packages.

    Are you sponsoring us?

    • [ ] Yes, I’m a sponsor. ?
    bug 
    Reply
  • fix: use toggleHeader from prosemirror-tables
    fix: use toggleHeader from prosemirror-tables

    Jan 20, 2022

    This PR will update the tables extension to use the toggleHeader command instead of the deprecated toggleHeaderColumn and toggleHeaderRow commands from prosemirror-tables. This is a breaking change because the commands will now only change the first column or row, respectively, instead of the row(s) or column(s) in the current selection. See the attached videos for examples.

    The exception to the changes is toggleHeaderCell. I'm not sure what we'd like to be done with it and am looking for some feedback. Currently, it toggles any cell(s) in the selection to be "header cells". This is the deprecated behavior in prosemirror-tables. If we change it to use toggleHeader, then it will only toggle cells that are within the first row or column and, even then, it will only remove a header cell, it doesn't put one back. This is confusing which is why I haven't included that change here.

    Old Behavior

    https://user-images.githubusercontent.com/2583256/150428805-21660e4f-9a11-48f9-91ac-c34ba3a5bd65.mp4

    New Behavior

    https://user-images.githubusercontent.com/2583256/150428804-6b54d70c-169e-47de-aa81-bea25737c4d9.mp4

    Reply
  • BulletList delete Mention
    BulletList delete Mention

    Jan 21, 2022

    What’s the bug you are facing?

    If a paragraph node starts by a mention, and then I type * , - or +, a bullet list is created, and the previous mention is deleted.

    How can we reproduce the bug on our side?

    On the codesandbox, add a new line, use @ to mention a user, type * , - or +.

    Can you provide a CodeSandbox?

    https://codesandbox.io/s/tiptap-issue-template-forked-ox987?file=/src/components/Tiptap.vue

    What did you expect to happen?

    How can I prevent this behavior ?

    Anything to add? (optional)

    ezgif-7-0d4604a5da

    Did you update your dependencies?

    • [X] Yes, I’ve updated my dependencies to use the latest version of all packages.

    Are you sponsoring us?

    • [ ] Yes, I’m a sponsor. ?
    bug 
    Reply
  • Lazy load lowlight.js
    Lazy load lowlight.js

    Jan 22, 2022

    What problem are you facing?

    Hello, I have a codeblock in my implementation. The problem I facing is low lighthouse score due to high volume of highlight.js. is there any way to lazy load highlight JS only in case codeblock is actually used? How can I avoid loading 1.9mb for nothing? Would be great to lazy load everything until it is used, now library loads a lot of bytes that user potentially might not need. To be fair, user don't even need the library to be loaded as critical resource.

    What’s the solution you would like to see?

    Lazy load for plugins

    What alternatives did you consider?

    Quill, but it's looks inactive now

    Anything to add? (optional)

    No response

    Are you sponsoring us?

    • [X] Yes, I’m a sponsor. ?
    feature request 
    Reply
  • Make sure editor is available on first render
    Make sure editor is available on first render

    Dec 15, 2021

    This is a fix for #2182 - it makes sure that the editor is available on the first render of useEditor in the react package.

    The reason for this fix is that the first render never has an editor and that causes the page's content to jump and flash. First render has no editor, so nothing is rendered. Then the second render has the editor and the WYSIWYG editor is rendered causing all the content on the page to shift down.

    The effect will also re-create the editor if anything in the deps list changes.

    cc @philippkuehn

    Reply
  • Markdown input and output
    Markdown input and output

    Oct 19, 2018

    In my use case, I can only load and save Markdown. It is similar to the Prosemirror Markdown example, but instead of the Prosemirror editor I want to use TipTap.

    What’s the best way to integrate the Prosemirror Markdown package into TipTap?

    feature request 
    Reply
  • Bug after update to 1.19.0 : RangeError: Adding different instances of a keyed plugin (plugin$1)
    Bug after update to 1.19.0 : RangeError: Adding different instances of a keyed plugin (plugin$1)

    May 9, 2019

    I've updated tiptap from v1.8.0 to v1.19.0 and now I have this bug in the console :

    [Vue warn]: Error in data(): "RangeError: Adding different instances of a keyed plugin (plugin$1)"

    The editor does not work anymore.

    bug 
    Reply
  • Vue 3 Support
    Vue 3 Support

    Jan 11, 2021

    I understand Vue 3 support is on the roadmap and you are waiting for Gridsome to support it in order to move the documentation to Vue 3: https://github.com/ueberdosis/tiptap/issues/735#issuecomment-719729347

    I'm using TipTap in a Vue 3 (Ionic) app and so intend to at least port EditorContent but probably also VueRenderer to Vue 3. Has any work been done on this? I seems wasteful to replicate something that may have already been done. Happy to contribute my changes if not.

    One thing that needs to be considered is if TipTap 2 will support both Vue 2 and 3. It seems that most libraries are adding Vue 3 support by having a 'next' branch (this could be tiptap/packages/vue3 rather than a branch) although there is at least one attempt at creating a tool to enable you to have a single codebase that supports both 2 and 3 (https://github.com/antfu/vue-demi).

    Reply
  • Error: Looks like multiple versions of prosemirror-model were loaded
    Error: Looks like multiple versions of prosemirror-model were loaded

    Jan 8, 2020

    Describe the bug When using the editor (e.g. using ENTER key) it's not working, and we got error in the console:

    Can not convert <> to a Fragment (looks like multiple versions of prosemirror-model were loaded)
    

    Due to [email protected] and [email protected] loaded simultaneously.

    tiptap-extensions package requires strictly version 1.8.2 and other packages [email protected]^1.0.0, [email protected]^1.1.0, [email protected]^1.8.1 which resolves to 1.9.0

    Steps to Reproduce / Codesandbox Example Steps to reproduce the behaviour:

    1. install
    "tiptap": "^1.27.1",
    "tiptap-extensions": "^1.29.1"
    

    with Yarn. This will install [email protected] and [email protected].

    1. When using the editor (e.g. using ENTER key) it's not working, and we got error in the console

    Expected behaviour Editor should work, there should be only one prosemirror-model package installed.

    Quick and dirty workaround for users (it's not proper fix!) - use resolutions entry in package.json:

        "resolutions": {
            "prosemirror-model": "1.9.1"
        }
    

    Then remove node_modules, yarn.lock and install packages again.

    bug 
    Reply
  • tiptap v2
    tiptap v2

    Dec 16, 2019

    First of all I would like to say thank you for the last year. You made tiptap to what it is today – probably the best rich text editor for Vue.js ?

    Tiptap hits 200K downloads per month now. Incredible!

    However, there is always something to do and a new major version is a good opportunity to tackle some bigger challenges. I would like to say that I have officially started the development of tiptap 2! ?

    In this thread I want to show the first new features and keep you up to date for new ideas. Please write your wishes here so that we can discuss them! ✌️

    ? Some updates about the project structure

    Move packages to its own organisation

    tiptap -> @tiptap
    

    Split tiptap-extensions into multiple packages

    Splitting the extensions into separate packages has the advantage to better optimize its bundle size. Especially if there are some larger dev-dependencies like hightlight.js for syntax highlighting. It's also easier to release alternative packages like @tiptap/highlight-code-extension and @tiptap/prism-code-extension.

    @tiptap/bold-extension
    @tiptap/italic-extension
    @tiptap/code-extension
    …
    

    Move Vue.js components out of the core

    The Vue components will no longer be part of the core. Maybe I'll also try to release a React package – but I'm not quite sure yet if I want to do this to myself ?

    @tiptap/core
    @tiptap/vue
    

    Move core extensions out of the core

    So they won't be core extensions anymore. Makes it easier to define custom documents like this.

    @tiptap/document-extension
    @tiptap/paragraph-extension
    @tiptap/text-extension
    

    Create extension collections

    For a better quick start for people who just want to use the full functionality of tiptap, I can imagine putting together collections.

    import extensions from '@tiptap/starter—kit'
    
    new Editor({ extensions })
    

    TypeScript #54

    I see the advantages of TypeScript, so I will try to rewrite tiptap completely in TypeScript.

    Combine landingpage and docs

    The documentation is really awful at the moment. That will change. In my mind there is a guide with integrated demos.

    E2E tests

    At the moment there are just some function tests which is not enough. To provide better stability, I would like to write some E2E tests.

    ? New Features

    Chained Commands

    Chain a list of core commands.

    editor
      .focus('end')
      .insertText('at the end')
      .newLine()
      .insertText('new line')
      .toggleBlockNode('heading', { level: 2 })
    

    Or register your own commands.

    editor.registerCommand('log', (next, editor, text) => {
      console.log(text)
      next()
    })
    
    editor.log('hey!')
    // 'hey!'
    

    Commands can be asynchronous too.

    editor.registerCommand('insertUserName', await (next, editor, id) => {
      let response = await fetch(`/api/${id}`);
      let data = await response.json()
      editor.insertText(data.user.name)
      next()
    })
    
    editor.insertUserName(123)
    

    Global node attributes

    Not sure about final syntax for this but basically I want to provide an option to add attributes to multiple nodes or marks at once.

    So instead of repeating attributes (like text-align #180)…

    class Paragraph extends Node {
      schema = {
        attrs: {
          textAlign: {
            default: 'left',
          }
        }
      }
    }
    
    class Heading extends Node {
      schema = {
        attrs: {
          textAlign: {
            default: 'left',
          }
        }
      }
    }
    

    … you can register global attributes.

    editor.registerAttribute(['paragraph', 'heading'], {
      textAlign: {
        default: 'left',
      }
    })
    

    More ideas?

    Comment here and let’s discuss your ideas! I will also go through the existing issues soon and tag with v2 if possible.

    ☠️ Things I'm not gonna do in v2:

    ~~Markdown support~~

    After playing around with ProseMirror and CodeMirror, I absolutely don't see any point in integrating Markdown support. Just use CodeMirror (or something else) and export/import from/to tiptap.

    Reply