feat: Tiptap V3 migration (#1854)

* Tiptap3 migration - WIP

* fix collaboration

* remove unused code

* fix flicker

* disable duplicate extensions

* update tiptap version

* Switch to useEditorState
- Set shouldRerenderOnTransaction to false

* fix editable state

* add tippyoptions for reference

* merge main

* tiptap 3.6.1

* fix bubble menu

* fix converter

* fix menus

* fix collaboration caret css

* fix: Set `isInitialized` to force immediate react node view rendering

* feat: Migrate tippy.js menus to Floating UI

* feat: Update collaboration connection for HocusPocus v3

* fix: Connect/disconnect websocketProvider

* cleanup

* cleanup

* feat: Improved placeholder and upload handling for images

* feat: Improved placeholder and upload handling for videos

* refactor: Image node and view clean-up

* feat: Improved placeholder and upload handling for attachments

* fix: Video view styles

* fix: Transaction handling on asset upload

* fix: Use imageDimensionsFromStream

* feat: Multiple file upload, improved placeholders, local previews

* fix: Drag & drop, paste upload

* fix: Allow media as attachment

* * add skeleton pulse animation
* add translation strings
* fix attachment view responsiveness

* fix collab connection status display

* Tiptap v3.17.0

* fix suggestion menu exit bug

* fix search shortcut

* fix history editor css

* tiptap 3.17.1

---------

Co-authored-by: Arek Nawo <areknawo@areknawo.com>
This commit is contained in:
Philip Okugbe
2026-01-24 20:41:08 +00:00
committed by GitHub
parent 98f71c95fe
commit 657fdf8cb7
74 changed files with 2532 additions and 2370 deletions
+26 -23
View File
@@ -1,6 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { sanitizeUrl } from './utils';
import { Node, mergeAttributes } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { sanitizeUrl } from "./utils";
export interface EmbedOptions {
HTMLAttributes: Record<string, any>;
@@ -14,7 +14,7 @@ export interface EmbedAttributes {
height?: number;
}
declare module '@tiptap/core' {
declare module "@tiptap/core" {
interface Commands<ReturnType> {
embeds: {
setEmbed: (attributes?: EmbedAttributes) => ReturnType;
@@ -23,9 +23,9 @@ declare module '@tiptap/core' {
}
export const Embed = Node.create<EmbedOptions>({
name: 'embed',
name: "embed",
inline: false,
group: 'block',
group: "block",
isolating: true,
atom: true,
defining: true,
@@ -40,41 +40,41 @@ export const Embed = Node.create<EmbedOptions>({
addAttributes() {
return {
src: {
default: '',
default: "",
parseHTML: (element) => {
const src = element.getAttribute('data-src');
const src = element.getAttribute("data-src");
return sanitizeUrl(src);
},
renderHTML: (attributes: EmbedAttributes) => ({
'data-src': sanitizeUrl(attributes.src),
"data-src": sanitizeUrl(attributes.src),
}),
},
provider: {
default: '',
parseHTML: (element) => element.getAttribute('data-provider'),
default: "",
parseHTML: (element) => element.getAttribute("data-provider"),
renderHTML: (attributes: EmbedAttributes) => ({
'data-provider': attributes.provider,
"data-provider": attributes.provider,
}),
},
align: {
default: 'center',
parseHTML: (element) => element.getAttribute('data-align'),
default: "center",
parseHTML: (element) => element.getAttribute("data-align"),
renderHTML: (attributes: EmbedAttributes) => ({
'data-align': attributes.align,
"data-align": attributes.align,
}),
},
width: {
default: 640,
parseHTML: (element) => element.getAttribute('data-width'),
parseHTML: (element) => element.getAttribute("data-width"),
renderHTML: (attributes: EmbedAttributes) => ({
'data-width': attributes.width,
"data-width": attributes.width,
}),
},
height: {
default: 480,
parseHTML: (element) => element.getAttribute('data-height'),
parseHTML: (element) => element.getAttribute("data-height"),
renderHTML: (attributes: EmbedAttributes) => ({
'data-height': attributes.height,
"data-height": attributes.height,
}),
},
};
@@ -91,13 +91,13 @@ export const Embed = Node.create<EmbedOptions>({
renderHTML({ HTMLAttributes }) {
const src = HTMLAttributes["data-src"];
const safeHref = sanitizeUrl(src);
return [
"div",
mergeAttributes(
{ "data-type": this.name },
this.options.HTMLAttributes,
HTMLAttributes,
HTMLAttributes
),
[
"a",
@@ -120,9 +120,9 @@ export const Embed = Node.create<EmbedOptions>({
...attrs,
src: sanitizeUrl(attrs.src),
};
return commands.insertContent({
type: 'embed',
type: "embed",
attrs: validatedAttrs,
});
},
@@ -130,6 +130,9 @@ export const Embed = Node.create<EmbedOptions>({
},
addNodeView() {
// Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191)
this.editor.isInitialized = true;
return ReactNodeViewRenderer(this.options.view);
},
});