ueberdosis / tiptap

The headless rich text editor framework for web artisans.
https://tiptap.dev
MIT License
26.34k stars 2.22k forks source link

Community Extensions #819

Open hanspagel opened 3 years ago

hanspagel commented 3 years ago

Hi everyone!

I’ve seen a ton of helpful Gists with custom extensions for Tiptap. It’s amazing what you all come up with! 🥰

Unfortunately, we don’t have the capabilities to maintain all of them in the core packages, but it’s sad to see those gems hidden in some Gists. What if you - the community - could easily provide those as separate npm packages?

Advantages of packages

Proof of Concept

I built a tiny proof of concept for superscript and subscript, see here: https://github.com/hanspagel/tiptap-extension-superscript https://github.com/hanspagel/tiptap-extension-subscript

Usage:

$ yarn add tiptap-extension-superscript
import { Superscript } from 'tiptap-extension-superscript'

new Editor({
  extensions: [
    new Superscript(),
  ],
})

Examples of community Gists, code snippets, PRs and ideas

Tiptap v2

Tiptap v1

Not needed with Tiptap v2

Roadmap

I think we’d need to do a few things to make that easier for everyone:

Your feedback

What do you all think? Would you be up to contribute a community extension?

Feel free to post links to Gists of others you’d love to see published as a package.

ramsane commented 3 years ago

I would love to. I am working for a project that involves rich content editor. I am trying to bring each feature one by one. Till now, I was able to get inline math and block level math integrations without any errors. There were few minor issues. But I was able to correct them.

I am more than happy to share it. But I am not writing any unit tests. Is it okay ?

ramsane commented 3 years ago

image

like this, there are so many places where the import sizes are huge. This increased just the editor component size in total. Is there any work around for this ?

image

This is an another example for such cases.

andreasvirkus commented 3 years ago

Increasing the collection with

export class CustomLink extends Link {
  get schema() {
    return {
      attrs: {
        href: {
          default: null,
        },
        'data-link-type': {
          default: 'link',
        },
        target: {
          default: null,
        },
        rel: {
          default: null,
        },
        class: {
          default: 'oct-a',
        },
      },
      inclusive: false,
      parseDOM: [
        {
          tag: 'a[href]',
          getAttrs: (dom) => {
            return {
              href: dom.getAttribute('href'),
              target: dom.getAttribute('target'),
              rel: dom.getAttribute('rel'),
              'data-link-type': dom.getAttribute('data-link-type'),
            }
          },
        },
      ],
      toDOM: (node) => {
        return [
          'a',
          {
            ...node.attrs,
            target: '__blank',
            class: 'content-link',
            rel: 'noopener noreferrer nofollow',
          },
          0,
        ]
      },
    }
  }
andreasvirkus commented 3 years ago

Found a plugin that supports pasting images under https://github.com/ueberdosis/tiptap/issues/686#issuecomment-630083211 👇

https://gist.github.com/slava-vishnyakov/16076dff1a77ddaca93c4bccd4ec4521 and/or: https://github.com/ueberdosis/tiptap/issues/508

andreasvirkus commented 3 years ago

Leaving this here as a request: heading anchor links extension https://github.com/ueberdosis/tiptap/issues/662

andreasvirkus commented 3 years ago

Resizable image plugin: https://github.com/ueberdosis/tiptap/issues/740#issuecomment-649945319 (links to this gist)

Aymkdn commented 3 years ago

I created a gist with a TextColor extension file as well as an example with a .vue file to see how to use it: https://gist.github.com/Aymkdn/9f993c5cfe8476f718c4fd2fd7bda1f0

jelleroorda commented 3 years ago

I've ported the TrailingNode extension for TipTap 2: https://gist.github.com/jelleroorda/2a3085f45ef75b9fdd9f76a4444d6bd6

hanspagel commented 3 years ago

Oh, thanks! Great work! I’ve added it as a (more or less hidden) experiment to the documentation. I think we’ll add this as an official extension:

https://www.tiptap.dev/experiments/trailing-node

joevallender commented 3 years ago

One approach for resizable images for v2 https://github.com/ueberdosis/tiptap/issues/1283

Just doing my reading before trying to create an iframe video embed for v2...

joevallender commented 3 years ago

I got video (as) working (as I need to for now) in my project :)

My use case is embedding YouTube, Vimeo or Loom video (the URLs of which have already been created/sanitised outside of tiptap). I pulled some example code out of my project and put it in my example

I'd hacked the helper class for the parent div

return ['div', {class: 'video-wrapper'}, ['iframe', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]]

Which would be better retrieved from the .configure({HTMLAttributes ... set when instantiating the editor and extensions - but you get the idea. Then set some 'classic' responsive iframe CSS yourself

I guess to make these draggable we'd need to add a handle since all clicks on the iframe belong to the iframe. How does tiptap handle dragging, is it explicitly the node itself, or can we set a handle element somehow? I'm happy with deleting/re-adding in my project tbh.

jelleroorda commented 3 years ago

Oh, thanks! Great work! I’ve added it as a (more or less hidden) experiment to the documentation. I think we’ll add this as an official extension:

https://www.tiptap.dev/experiments/trailing-node

I've updated the gist, by adding a TypeScript variant for trailing node as well.

@hanspagel I have also ported your Subscript extension and your Superscript extension for v2 with TypeScript. Thanks again for those 😄.

andon94 commented 3 years ago

@jelleroorda Could you give me a couple of tips on how to implement this Subscript/Superscript extensions to an existing Vue project, where changing the file extensions to .ts is not an option?

joevallender commented 3 years ago

@andon94 it should be almost exactly the same, it doesn't require TS. See a quick (untested) example here https://gist.github.com/joevallender/47e957298d7fbf4c41f5a1ba462d1d59

jelleroorda commented 3 years ago

You can check the gist, there’s a JavaScript and a typescript variant (two different files) in the same gist).

alancwoo commented 3 years ago

Related to #1304, can someone please help guide the conversation for something as basic as:

For a Vue app, with JS, how can one create a button that simply toggles a class (and repeat accordingly for any desired custom classes).

For example:

[ Uppercase ] [ Large ]

The quick brown fox

Where selecting brown and clicking Uppercase would result in:

The quick <span class="uppercase">brown</span> box

I've tried looking at this example but see it is for TipTap v1. I'm at a bit of a loss because the sup gist is for creating a new mark, the font-family extension is fairly complex and written as a typescript extension.

I think there is just some incredibly basic understanding I am lacking, if someone could just provide some guidance, I would happily try to help contribute to the documentation once I can wrap my head around it.

Perhaps it's one of those things that is so basic/obvious that it can be overlooked by people with more familiarity, but I can imagine it's a very common use-case, to be able to select text and toggle custom classes.

sereneinserenade commented 3 years ago

@alancwoo, I've created spanClass extension for that. Let me know if you have a better name for it.

import { Extension } from "@tiptap/core";
import "@tiptap/extension-text-style";
export const SpanClass = Extension.create({
  name: "spanClass",
  defaultOptions: {
    types: ["textStyle"]
  },
  addGlobalAttributes() {
    return [
      {
        types: this.options.types,
        attributes: {
          spanClass: {
            default: "none",
            renderHTML: (attributes) => {
              if (!attributes.spanClass) {
                return {};
              }
              return {
                class: attributes.spanClass
              };
            },
            parseHTML: (element) => ({
              spanClass: element.classList.value
            })
          }
        }
      }
    ];
  },
  addCommands() {
    return {
      setSpanClass: (spanClass) => ({ chain }) => {
        return chain().setMark("textStyle", { spanClass }).run();
      },
      unsetSpanClass: () => ({ chain }) => {
        return chain()
          .setMark("textStyle", { spanClass: "" })
          .removeEmptyTextStyle()
          .run();
      }
    };
  }
});

and this is how it can be used to add a class to text after wrapping it with a span

this.editor.chain().focus().setSpanClass("uppercase").run();

you can write anything you want instead of uppercase. Also multiple classes are allowed just like we write them in HTML. so something like this should totally work.

this.editor.chain().focus().setSpanClass("uppercase italic bold").run();

this will add the given class to selected text after converting it to a span. Here's a codesandbox of how I am using it https://codesandbox.io/s/wonderful-gauss-j8prn?file=/src/components/Tiptap.vue

alancwoo commented 3 years ago

@sereneinserenade thank you, you are such a life saver 👏🏽 This is exactly what I needed. I made some slight adjustments to try to simplify things and to make it possible to make multiple buttons of different classes:

Vue (can set multiple buttons with different classes, with their own active class (this 'active' check seems messy to me, but it works - please let me know if there is a cleaner way about this)):

<button title="Uppercase"
  @click="editor.chain().focus().toggleSpanClass('uppercase').run()"
  :class="{
  'is-active':
    editor.isActive('textStyle') &&
    editor.getAttributes('textStyle').spanClass.includes('uppercase'),
  }"
>
  Uppercase
</button>

// repeat as desired for different classes

SpanClass.js (reduced to one method)

  addCommands () {
    return {
      toggleSpanClass: (spanClass) => ({ editor, chain }) => {
        console.log(editor)

        if (!editor.isActive('textStyle')) {
          return chain().setMark('textStyle', { spanClass }).run()
        } else {
          let textStyleClasses = editor.getAttributes('textStyle').spanClass.split(' ')

          if ((textStyleClasses).includes(spanClass)) {
            textStyleClasses = textStyleClasses.filter(className => className !== spanClass)
          } else {
            textStyleClasses.push(spanClass)
          }

          if (textStyleClasses.length) {
            return chain().setMark('textStyle', { spanClass: textStyleClasses.join(' ') }).run()
          } else {
            return chain().setMark("textStyle", { spanClass: "" })
              .removeEmptyTextStyle()
              .run()
          }
        }
      },
    }
  }

Thank you again, this is enormously helpful and completely unblocked me.

alancwoo commented 3 years ago

@sereneinserenade I'm sorry but in the end, it looks like while the data is saved to the database, but the span and its classes are stripped upon re-rendering/loading the editor.

If you look at this fork of your sandbox, I've added an existing uppercase span which is removed on load: https://codesandbox.io/s/tiptap-spanclass-extension-forked-8vh2v?file=/src/components/Tiptap.vue

I imagine it has to do with https://github.com/ueberdosis/tiptap/issues/495 but am confused how to fix this.

davesag commented 3 years ago

Two features I am looking for in a Rich Text Editor are

  1. Tracked Changes (not necessarily real-time collaboration, but being able to see - Google Docs / MS Word / Apple Pages style - the changes others have made. and a way to 'accept' one, or all changes, and
  2. Commenting (again ala Google Docs / MS Word / Apple Pages)

Commercial RTEs support this kind of thing and it would be amazing to have examples of this in TipTap. Maybe I've just not found the right extensions tho.

wiwat-p commented 3 years ago

How to use anchor ? (#621)

aaronguernsey commented 3 years ago
  1. Commenting (again ala Google Docs / MS Word / Apple Pages)

If you create or find the right commenting solution @davesag, it would be awesome to see it here 😃.

cadars commented 2 years ago

A simple extension to support mixed bi-directional text (LTR-RTL), by adding dir="auto" to top nodes.

tabdon commented 2 years ago

Two features I am looking for in a Rich Text Editor are

1. Tracked Changes (not necessarily real-time collaboration, but being able to see - Google Docs / MS Word / Apple Pages style - the changes others have made. and a way to 'accept' one, or all changes, and

2. Commenting (again ala Google Docs / MS Word / Apple Pages)

Commercial RTEs support this kind of thing and it would be amazing to have examples of this in TipTap. Maybe I've just not found the right extensions tho.

I really want this functionality, too. I just created an Upwork task to try and find someone to build it. I will open source any code that we create! I'll update here.

cadars commented 2 years ago

I needed a KBD extension, the shortcut is simply <kbd>…</kbd> like you would write on Github. https://gist.github.com/cadars/78a6c96eac8faf3b11feda3d6ad033e3

(it doesn’t use mergeAttributes because I didn’t need them, but they could be added easily)

pdmwxy commented 2 years ago

Two features I am looking for in a Rich Text Editor are

1. Tracked Changes (not necessarily real-time collaboration, but being able to see - Google Docs / MS Word / Apple Pages style - the changes others have made. and a way to 'accept' one, or all changes, and

2. Commenting (again ala Google Docs / MS Word / Apple Pages)

Commercial RTEs support this kind of thing and it would be amazing to have examples of this in TipTap. Maybe I've just not found the right extensions tho.

I really want this functionality, too. I just created an Upwork task to try and find someone to build it. I will open source any code that we create! I'll update here.

hello Has this function been realized? Can we open source? I also need this function.

sereneinserenade commented 2 years ago

So, I've created comment extension, it works for me, will open source soon it's not exactly google doc style, but it works.

https://user-images.githubusercontent.com/45892659/139601783-e7e22560-a8f5-408b-84f7-7b5e269f5685.mov

sereneinserenade commented 2 years ago

Repo : https://github.com/sereneinserenade/tiptap-comment-extension

Try it out : https://tiptap-comment-extension.vercel.app/

hanspagel commented 2 years ago

@sereneinserenade Thanks for sharing! Just so you know, the link to the repository is a 404. Probably still set to private?

sereneinserenade commented 2 years ago

@hanspagel yep, that was it 😅 , now it's OSS and public ⚡

calebebteixeira commented 2 years ago

Increasing the collection with

export class CustomLink extends Link {
  get schema() {
    return {
      attrs: {
        href: {
          default: null,
        },
        'data-link-type': {
          default: 'link',
        },
        target: {
          default: null,
        },
        rel: {
          default: null,
        },
        class: {
          default: 'oct-a',
        },
      },
      inclusive: false,
      parseDOM: [
        {
          tag: 'a[href]',
          getAttrs: (dom) => {
            return {
              href: dom.getAttribute('href'),
              target: dom.getAttribute('target'),
              rel: dom.getAttribute('rel'),
              'data-link-type': dom.getAttribute('data-link-type'),
            }
          },
        },
      ],
      toDOM: (node) => {
        return [
          'a',
          {
            ...node.attrs,
            target: '__blank',
            class: 'content-link',
            rel: 'noopener noreferrer nofollow',
          },
          0,
        ]
      },
    }
  }

You are a lovely man! thanks dude!

janzheng commented 2 years ago

So I did a thing that converts a-z to greek text and back, for scientific symbols. Had to wrangle a ton of ProseMirror, but this takes care of selection / transformations as well as copy / paste. Took a long time to figure out, hopefully it's useful to someone else.

import {
  Mark,
  markInputRule,
  markPasteRule,
  mergeAttributes,
  textInputRule
} from '@tiptap/core'

import { Plugin, PluginKey } from 'prosemirror-state'

export const Greek = Mark.create({
  name: 'greek',

  addOptions() {
    return {
      HTMLAttributes: {
        class: 'greek'
      },
    }
  },

  parseHTML() {
    return [
      {
        tag: 'o',
      },
    ]
  },

  renderHTML({ HTMLAttributes }) {
    return ['o', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
  },

  addCommands() {
    let _mark = this
    return {
      useGreek: () => ({state, dispatch, commands}) => {
        let { empty, ranges } = state.selection
        console.log('state:', state, state.selection, state.selection.content())
        const content = state.selection.content()

        if (!empty) {

          // ripped out from toggleMark
          let has = false, tr = state.tr, markType = _mark.type

          console.log('====> type:', markType, _mark)
          for (let i = 0; !has && i < ranges.length; i++) {
            let { $from, $to } = ranges[i]
            has = state.doc.rangeHasMark($from.pos, $to.pos, markType)
          }
          for (let i = 0; i < ranges.length; i++) {
            let { $from, $to } = ranges[i]
            if (has) {
              symbolSlice(content, true, true, true)
              // console.log('un-symbolized: ', normaltext, content, 'selection:', jsonID)
              state.selection.replace(tr, content)
              // tr.insertText(`${trailspace}${normaltext}${trailspace}`, $from.pos, $to.pos)
              tr.removeMark($from.pos, $to.pos, markType)
              // tr.replaceSelection(normalSlice)
            } else {
              // turn selected text into symbols
              const symboltext = symbolSlice(content, true, false, true)
              if (!state.selection.empty) // selection
                state.selection.replace(tr, content)
              tr.addMark($from.pos, $to.pos, markType.create())
              if (state.selection.empty) // necessary for typing in symbols
                tr.insertText(`${symboltext}${trailspace}`, $from.pos, $to.pos)
            }
            // console.log('tr.stateSel:', tr.selection)
            tr.scrollIntoView()
          }

          // toggleMark(cmd.type)(state, dispatch)
          // tr.insertText(`${text}${trailspace}`, state.selection.from, state.selection.to)
          dispatch(tr)
          // return true
          return commands.toggleMark('greek')

        } else {
          // console.log('command...', state, state.tr, 'content:?:?:', )
          // return dispatch(state.tr.insertText("wtf"))
          // return toggleMark(cmd.type)(state, dispatch)
          return commands.toggleMark('greek')
        }
      },
    }
  },

  addKeyboardShortcuts() {
    return {
      'Mod-Shift-g': () => this.editor.commands.toggleGreek(),
    }
  },

  addProseMirrorPlugins() {
    const plugins = []

    plugins.push(
      new Plugin({
        key: new PluginKey('applyGreekText'),
        state: {
          init: (_, state) => {
          },
          // apply: (tr, value, oldState, newState) => {
          apply: (tr) => {
            if (tr.docChanged) {
              // console.log('symbol_node apply:', tr.doc, tr.doc.lastChild, tr.doc.lastChild['textContent'])
              // applies symbol transformation directly on the tr node by ref
              // the reason is b/c we don't want to undo conversion from alpha to greek as a separate transaction
              // const symboltext = symbolSlice(tr.doc.lastChild, false, false, true)
              const symboltext = symbolSlice(tr.doc, false, false, true)
            }
          },
        },
      }),
    )

    return plugins
  }

})

export default Greek

export const symbolSwap = (strdata, isReverse, alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$^*\\") => {
  // swaps all occurrences of a letter in the alpha map to symbols
  // these mimic the symbols font used in Word
  if (strdata === null) {
    return ''
  }

  const symbols = "αβχδεφγηιϕκλμνοπθρστυϖωξψζΑΒΧΔΕΦΓΗΙϑΚΛΜΝΟΠΘΡΣΤΥςΩΞΨΖ!≅#∃⊥∗∴"
  // const alphakeys = Object.keys(alpha)
  // console.log('alpha:', alpha, strdata)

  let output = ""
  if (isReverse) {
    strdata.split('').forEach(str => {
      // console.log('trying...', alpha.indexOf(str))
      if (symbols.indexOf(str) >= 0) { // convert greek to a-z
        // console.log('rev...', str, alpha.charAt(symbols.indexOf(str)))
        output += alpha.charAt(symbols.indexOf(str))
      } else {
        output += str
      }
    })

  } else {
    strdata.split('').forEach(str => {
      // console.log('trying...', alpha.indexOf(str))
      if (alpha.indexOf(str) >= 0) {
        output += symbols.charAt(alpha.indexOf(str))
      } else {
        output += str
      }
    })
  }

  // console.log('!!! output:', output)
  return output
}

export const symbolSlice = (slice, _isSymbol = false, isReverse = false, objReplace = false) => {
  // isSymbol is set to false, and used to look for 'greek' objects when pasting symbols from Word
  // set it to true to convert any kinds of slices, e.g. from a command
  let text = ''

  const dig = (content) => {
    if (Array.isArray(content)) {
      content.map(node => {
        let isSymbol = _isSymbol
        // if(!isReverse)
        text = ''
        if (node['marks'] && node['marks'].length > 0) {
          node['marks'].map(mark => {
            // console.log('mark:', mark)
            if (mark['type']['name'] === "greek")
              isSymbol = true
          })
        }
        // console.log('node :::', isSymbol, node['marks'], node['text'], node)
        if (isSymbol && node && node['text']) {
          // console.log('symbol node:', node, node['text'], isReverse)
          node['text'].split('').map((str) => {
            // console.log('swapping:', str)
            const symbol = symbolSwap(str, isReverse)
            text += symbol
          })
          if (objReplace) {
            node['text'] = text
            // console.log('new text:', text)
          }
          // return node['text'] = text
          return
        }
        else if (node['content']) {
          // console.log('>> digging deeper')
          return dig(node['content'])
        }
        return
      })
    }
    else if (content['content']) {
      // console.log('* digging deeper', content['content'])
      return dig(content['content'])
    }
  }

  // console.log('>--------')
  // console.log(':::: symbol convert:', slice, slice['textContent'])
  if (slice['content']) {
    dig(slice['content'])
  }

  // console.log('returning slice:', slice['content'], text)
  // console.log('<--------')
  return text
}
sereneinserenade commented 2 years ago

For those who wanted a google docs like commenting solution, I've implemented in sereneinserenade/tiptap-comment-extension#1.

Try it out: https://tiptap-comment-extension.vercel.app/

here's a demo:

https://user-images.githubusercontent.com/45892659/160260884-34b77b9f-3fb3-4ddf-af1d-066659b12774.mp4

cadars commented 2 years ago

I needed to support <dl> definition lists on paste somehow, so I shamelessly adapted Gitlab’s solution that converts them to plain <ul> lists with classes for styling, maybe this can be useful to someone:

// description-list.js

import { mergeAttributes, Node, wrappingInputRule } from '@tiptap/core';

export default Node.create({
  name: 'descriptionList',
  group: 'block list',
  content: 'descriptionItem+',

  parseHTML() {
    return [{ tag: 'dl' }];
  },

  renderHTML({ HTMLAttributes }) {
    return ['ul', mergeAttributes(HTMLAttributes, { class: 'dl-content' }), 0];
  },

  addInputRules() {
    const inputRegex = /^\s*(<dl>)$/;

    return [wrappingInputRule({ find: inputRegex, type: this.type })];
  },
});
// description-item.js

import { mergeAttributes, Node } from '@tiptap/core';

export default Node.create({
  name: 'descriptionItem',
  content: 'block+',
  defining: true,

  addAttributes() {
    return {
      isTerm: {
        default: true,
        parseHTML: (element) => element.tagName.toLowerCase() === 'dt',
      },
    };
  },

  parseHTML() {
    return [{ tag: 'dt' }, { tag: 'dd' }];
  },

  renderHTML({ HTMLAttributes: { isTerm, ...HTMLAttributes } }) {
    return [
      'li',
      mergeAttributes(HTMLAttributes, { class: isTerm ? 'dl-term' : 'dl-description' }),
      0,
    ];
  },

  addKeyboardShortcuts() {
    return {
      Enter: () => {
        return this.editor.commands.splitListItem('descriptionItem');
      },
      Tab: () => {
        const { isTerm } = this.editor.getAttributes('descriptionItem');
        if (isTerm)
          return this.editor.commands.updateAttributes('descriptionItem', {
            isTerm: !isTerm,
          });

        return false;
      },
      'Shift-Tab': () => {
        const { isTerm } = this.editor.getAttributes('descriptionItem');
        if (isTerm) return this.editor.commands.liftListItem('descriptionItem');

        return this.editor.commands.updateAttributes('descriptionItem', { isTerm: true });
      },
    };
  },
});
RalphDeving commented 2 years ago

Hello , contributing with another Image Resize extension:

Repo with example: https://github.com/RalphDeving/tiptap-img-resize Explanation: https://ralphdeving.github.io/blog/post/tiptap-image-resize-vue

deathg0d commented 2 years ago

Hi guys, has anyone tried creating something like this with tiptap? https://stackoverflow.com/questions/53374806/how-to-build-a-smart-compose-like-gmail-possible-in-a-textarea

gregveres commented 2 years ago

Here is a gist to font-size for tiptap 2. It is a direct copy of the official font-family extension. https://gist.github.com/gregveres/64ec1d8a733feb735b7dd4c46331abae

gregveres commented 2 years ago

Here is a gist to set background-color on text for tiptap 2. It is a direct copy of the official color extension. https://gist.github.com/gregveres/973e8d545ab40dc375b47ebc63f92846

gregveres commented 2 years ago

Here is a gist for a line-height extension for tiptap 2. It is a copy of the TextAlign extension as suggested by @hanspagel on this comment for someone else's line-extension pr

https://gist.github.com/gregveres/8757756d56becc2c053c46540cb6b314

wenerme commented 2 years ago

Here is some extensions https://github.com/wenerme/wode/tree/main/apps/demo/src/components/TipTapWord/extensions

Online demo here

https://wode.vercel.app/tiptap

martinstoeckli commented 2 years ago

Wrote a minimal extension for schrolling to the top [Ctrl-Home] or to the bottom [Ctrl-End] of the document. https://github.com/martinstoeckli/SilentNotes/blob/main/src/ProseMirrorBundle/src/scroll-to-extension.ts

sereneinserenade commented 2 years ago

@martinstoeckli that's a nice approach. Alternatively, you can just do this, where most of the things are handled by Tiptap 🙂

using https://tiptap.dev/api/commands/focus and https://tiptap.dev/api/commands/scroll-into-view

martinstoeckli commented 2 years ago

@sereneinserenade Thanks for the tip, I'm aware of this method, but I have the requirement that it must work on a disabled editor, when the editor cannot get the focus. For some reason after calling setContent() with large documents, the page is not always on the top.

puopg commented 2 years ago

Super tiny, simple file paste handler extension:

import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "prosemirror-state";

const extensionName = "pasteFileHandler";

export type PasteFileHandlerOptions = {
  onFilePasted: (file: File) => boolean;
};

const handleFilePaste = (event: ClipboardEvent, onPasteEvent?: (file: File) => void): void => {
  const { items } = event.clipboardData || event.originalEvent.clipboardData;

  const keys = Object.keys(items);

  keys.some((key) => {
    const item = items[key];

    if (item.kind === "file") {
      const file = item.getAsFile();

      if (onPasteEvent) {
        onPasteEvent(file);
      }

      return true;
    }

    return false;
  });
};

const PasteFileHandler = Extension.create<PasteFileHandlerOptions>({
  name: extensionName,

  addOptions() {
    return {
      onFilePasted: () => false,
    };
  },

  addProseMirrorPlugins() {
    return [
      new Plugin({
        key: new PluginKey(extensionName),
        props: {
          handlePaste: (view, event) => {
            return handleFilePaste(event, this.options.onFilePasted);
          },
        },
      }),
    ];
  },
});

export default PasteFileHandler;
fantasticit commented 2 years ago
image

I've made an open source project called think, which relies on tiptap to develop a lot of extensions, maybe it can help you.

https://github.com/fantasticit/think/tree/main/packages/client/src/tiptap/core/extensions

tabdon commented 2 years ago

Wow! That’s nice. Thanks

On Tue, Aug 23, 2022 at 4:26 PM fantasticit @.***> wrote:

[image: image] https://user-images.githubusercontent.com/26452939/186122578-10af264b-601a-4c8b-bae3-ea0fb8a25922.png

I've made an open source project called think, which relies on tiptap to develop a lot of extensions, maybe it can help you.

https://github.com/fantasticit/think/tree/main/packages/client/src/tiptap/core/extensions

— Reply to this email directly, view it on GitHub https://github.com/ueberdosis/tiptap/issues/819#issuecomment-1223803505, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAGNFSLRRRT5WNNSMY2MCALV2SKLDANCNFSM4QRTKZ5Q . You are receiving this because you commented.Message ID: @.***>

-- Thanks, Tobias

lzxlzxlzxl commented 2 years ago

Has anybody done table expansion? For example, table border color

chenyuncai commented 1 year ago

Track Changes like Microsoft Office Word. I have implemented this feature, but the code is just in my project. I will publish one day. mark it

tslater commented 1 year ago
image

I've made an open source project called think, which relies on tiptap to develop a lot of extensions, maybe it can help you.

https://github.com/fantasticit/think/tree/main/packages/client/src/tiptap/core/extensions

Very cool. What does the app (think) do?

hansmei commented 1 year ago

Track Changes like Microsoft Office Word. I have implemented this feature, but the code is just in my project. I will publish one day. mark it

@chenyuncai I'm looking for this functionality these days. Please do share:)