Open hanspagel opened 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 ?
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 ?
This is an another example for such cases.
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,
]
},
}
}
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
Leaving this here as a request: heading anchor links extension https://github.com/ueberdosis/tiptap/issues/662
Resizable image plugin: https://github.com/ueberdosis/tiptap/issues/740#issuecomment-649945319 (links to this gist)
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
I've ported the TrailingNode extension for TipTap 2: https://gist.github.com/jelleroorda/2a3085f45ef75b9fdd9f76a4444d6bd6
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:
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...
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.
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:
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 😄.
@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?
@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
You can check the gist, there’s a JavaScript and a typescript variant (two different files) in the same gist).
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.
@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
@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.
@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.
Two features I am looking for in a Rich Text Editor are
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.
How to use anchor ? (#621)
- 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 😃.
A simple extension to support mixed bi-directional text (LTR-RTL), by adding dir="auto"
to top nodes.
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.
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)
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.
So, I've created comment extension, it works for me, will open source soon it's not exactly google doc style, but it works.
@sereneinserenade Thanks for sharing! Just so you know, the link to the repository is a 404. Probably still set to private?
@hanspagel yep, that was it 😅 , now it's OSS and public ⚡
Increasing the collection with
- Custom Link from Auto Link when click Enter #783 (comment) (only enhancement on the existing Link currently is that it converts input as well instead of only paste links)
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!
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
}
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:
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 });
},
};
},
});
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
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
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
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
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
Here is some extensions https://github.com/wenerme/wode/tree/main/apps/demo/src/components/TipTapWord/extensions
Online demo here
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
@martinstoeckli that's a nice approach. Alternatively, you can just do this, where most of the things are handled by Tiptap 🙂
editor.chain().focus('start').scrollIntoView().run()
editor.chain().focus('end').scrollIntoView().run()
using https://tiptap.dev/api/commands/focus and https://tiptap.dev/api/commands/scroll-into-view
@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.
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;
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
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
Has anybody done table expansion? For example, table border color
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
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?
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:)
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
Image
node including the Upload to S3 mechanic)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:
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.