@tiptap/react 2.0.0-beta.9 → 2.0.0-beta.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020, überdosis GbR
3
+ Copyright (c) 2021, überdosis GbR
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
  ## Introduction
8
8
  tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
9
9
 
10
- ## Offical Documentation
10
+ ## Official Documentation
11
11
  Documentation can be found on the [tiptap website](https://tiptap.dev).
12
12
 
13
13
  ## License
14
- tiptap is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap-next/blob/main/LICENSE.md).
14
+ tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/react",
3
3
  "description": "React components for tiptap",
4
- "version": "2.0.0-beta.9",
4
+ "version": "2.0.0-beta.91",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -15,23 +15,31 @@
15
15
  "main": "dist/tiptap-react.cjs.js",
16
16
  "umd": "dist/tiptap-react.umd.js",
17
17
  "module": "dist/tiptap-react.esm.js",
18
- "unpkg": "dist/tiptap-react.bundle.umd.min.js",
19
18
  "types": "dist/packages/react/src/index.d.ts",
20
19
  "files": [
21
20
  "src",
22
21
  "dist"
23
22
  ],
23
+ "devDependencies": {
24
+ "@types/react": "^17.0.34",
25
+ "@types/react-dom": "^17.0.11",
26
+ "react": "^17.0.0",
27
+ "react-dom": "^17.0.0"
28
+ },
24
29
  "peerDependencies": {
25
30
  "@tiptap/core": "^2.0.0-beta.1",
26
- "react": "^17.0.1",
27
- "react-dom": "^17.0.1"
31
+ "react": "^17.0.0",
32
+ "react-dom": "^17.0.0"
28
33
  },
29
34
  "dependencies": {
30
- "@tiptap/extension-bubble-menu": "^2.0.0-beta.3",
31
- "prosemirror-view": "^1.18.2"
35
+ "@tiptap/extension-bubble-menu": "^2.0.0-beta.48",
36
+ "@tiptap/extension-floating-menu": "^2.0.0-beta.42",
37
+ "prosemirror-view": "^1.22.0"
32
38
  },
33
- "devDependencies": {
34
- "@types/react-dom": "^17.0.3"
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/ueberdosis/tiptap",
42
+ "directory": "packages/react"
35
43
  },
36
- "gitHead": "3cf22b02c6cfba27b13f21422546f9076d5653e7"
44
+ "gitHead": "fce16e805824972834d5a8ce8d60e3ff41d63c7e"
37
45
  }
@@ -1,7 +1,9 @@
1
1
  import React, { useEffect, useRef } from 'react'
2
- import { BubbleMenuPlugin, BubbleMenuPluginKey, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'
2
+ import { BubbleMenuPlugin, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'
3
3
 
4
- export type BubbleMenuProps = Omit<BubbleMenuPluginProps, 'element'> & {
4
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
5
+
6
+ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'> & {
5
7
  className?: string,
6
8
  }
7
9
 
@@ -9,21 +11,35 @@ export const BubbleMenu: React.FC<BubbleMenuProps> = props => {
9
11
  const element = useRef<HTMLDivElement>(null)
10
12
 
11
13
  useEffect(() => {
12
- const { editor, keepInBounds = true } = props
14
+ if (!element.current) {
15
+ return
16
+ }
17
+
18
+ const {
19
+ pluginKey = 'bubbleMenu',
20
+ editor,
21
+ tippyOptions = {},
22
+ shouldShow = null,
23
+ } = props
13
24
 
14
25
  editor.registerPlugin(BubbleMenuPlugin({
26
+ pluginKey,
15
27
  editor,
16
28
  element: element.current as HTMLElement,
17
- keepInBounds,
29
+ tippyOptions,
30
+ shouldShow,
18
31
  }))
19
32
 
20
33
  return () => {
21
- editor.unregisterPlugin(BubbleMenuPluginKey)
34
+ editor.unregisterPlugin(pluginKey)
22
35
  }
23
- }, [])
36
+ }, [
37
+ props.editor,
38
+ element.current,
39
+ ])
24
40
 
25
41
  return (
26
- <div ref={element} className={props.className}>
42
+ <div ref={element} className={props.className} style={{ visibility: 'hidden' }}>
27
43
  {props.children}
28
44
  </div>
29
45
  )
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { HTMLProps } from 'react'
2
2
  import ReactDOM from 'react-dom'
3
3
  import { Editor } from './Editor'
4
4
  import { ReactRenderer } from './ReactRenderer'
@@ -17,7 +17,7 @@ const Portals: React.FC<{ renderers: Map<string, ReactRenderer> }> = ({ renderer
17
17
  )
18
18
  }
19
19
 
20
- export interface EditorContentProps {
20
+ export interface EditorContentProps extends HTMLProps<HTMLDivElement> {
21
21
  editor: Editor | null,
22
22
  }
23
23
 
@@ -55,7 +55,7 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
55
55
 
56
56
  const element = this.editorContentRef.current
57
57
 
58
- element.appendChild(editor.options.element.firstChild)
58
+ element.append(...editor.options.element.childNodes)
59
59
 
60
60
  editor.setOptions({
61
61
  element,
@@ -63,8 +63,7 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
63
63
 
64
64
  editor.contentComponent = this
65
65
 
66
- // TODO: alternative to setTimeout?
67
- setTimeout(() => editor.createNodeViews(), 0)
66
+ editor.createNodeViews()
68
67
  }
69
68
  }
70
69
 
@@ -89,7 +88,7 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
89
88
 
90
89
  const newElement = document.createElement('div')
91
90
 
92
- newElement.appendChild(editor.options.element.firstChild)
91
+ newElement.append(...editor.options.element.childNodes)
93
92
 
94
93
  editor.setOptions({
95
94
  element: newElement,
@@ -97,9 +96,11 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
97
96
  }
98
97
 
99
98
  render() {
99
+ const { editor, ...rest } = this.props
100
+
100
101
  return (
101
102
  <>
102
- <div ref={this.editorContentRef} />
103
+ <div ref={this.editorContentRef} {...rest} />
103
104
  <Portals renderers={this.state.renderers} />
104
105
  </>
105
106
  )
@@ -0,0 +1,46 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import { FloatingMenuPlugin, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
3
+
4
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
5
+
6
+ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element'> & {
7
+ className?: string,
8
+ }
9
+
10
+ export const FloatingMenu: React.FC<FloatingMenuProps> = props => {
11
+ const element = useRef<HTMLDivElement>(null)
12
+
13
+ useEffect(() => {
14
+ if (!element.current) {
15
+ return
16
+ }
17
+
18
+ const {
19
+ pluginKey = 'floatingMenu',
20
+ editor,
21
+ tippyOptions = {},
22
+ shouldShow = null,
23
+ } = props
24
+
25
+ editor.registerPlugin(FloatingMenuPlugin({
26
+ pluginKey,
27
+ editor,
28
+ element: element.current as HTMLElement,
29
+ tippyOptions,
30
+ shouldShow,
31
+ }))
32
+
33
+ return () => {
34
+ editor.unregisterPlugin(pluginKey)
35
+ }
36
+ }, [
37
+ props.editor,
38
+ element.current,
39
+ ])
40
+
41
+ return (
42
+ <div ref={element} className={props.className} style={{ visibility: 'hidden' }}>
43
+ {props.children}
44
+ </div>
45
+ )
46
+ }
@@ -2,20 +2,23 @@ import React from 'react'
2
2
  import { useReactNodeView } from './useReactNodeView'
3
3
 
4
4
  export interface NodeViewContentProps {
5
- className?: string,
6
- as: React.ElementType,
5
+ [key: string]: any,
6
+ as?: React.ElementType,
7
7
  }
8
8
 
9
9
  export const NodeViewContent: React.FC<NodeViewContentProps> = props => {
10
- const { isEditable } = useReactNodeView()
11
10
  const Tag = props.as || 'div'
11
+ const { nodeViewContentRef } = useReactNodeView()
12
12
 
13
13
  return (
14
14
  <Tag
15
- className={props.className}
15
+ {...props}
16
+ ref={nodeViewContentRef}
16
17
  data-node-view-content=""
17
- contentEditable={isEditable}
18
- style={{ whiteSpace: 'pre-wrap' }}
18
+ style={{
19
+ ...props.style,
20
+ whiteSpace: 'pre-wrap',
21
+ }}
19
22
  />
20
23
  )
21
24
  }
@@ -2,22 +2,24 @@ import React from 'react'
2
2
  import { useReactNodeView } from './useReactNodeView'
3
3
 
4
4
  export interface NodeViewWrapperProps {
5
- className?: string,
6
- as: React.ElementType,
5
+ [key: string]: any,
6
+ as?: React.ElementType,
7
7
  }
8
8
 
9
- export const NodeViewWrapper: React.FC<NodeViewWrapperProps> = props => {
9
+ export const NodeViewWrapper: React.FC<NodeViewWrapperProps> = React.forwardRef((props, ref) => {
10
10
  const { onDragStart } = useReactNodeView()
11
11
  const Tag = props.as || 'div'
12
12
 
13
13
  return (
14
14
  <Tag
15
- className={props.className}
15
+ {...props}
16
+ ref={ref}
16
17
  data-node-view-wrapper=""
17
18
  onDragStart={onDragStart}
18
- style={{ whiteSpace: 'normal' }}
19
- >
20
- {props.children}
21
- </Tag>
19
+ style={{
20
+ ...props.style,
21
+ whiteSpace: 'normal',
22
+ }}
23
+ />
22
24
  )
23
- }
25
+ })
@@ -1,25 +1,33 @@
1
- import React, { useState, useEffect } from 'react'
1
+ import React from 'react'
2
2
  import {
3
3
  NodeView,
4
4
  NodeViewProps,
5
5
  NodeViewRenderer,
6
6
  NodeViewRendererProps,
7
+ NodeViewRendererOptions,
7
8
  } from '@tiptap/core'
8
9
  import { Decoration, NodeView as ProseMirrorNodeView } from 'prosemirror-view'
9
10
  import { Node as ProseMirrorNode } from 'prosemirror-model'
10
11
  import { Editor } from './Editor'
11
12
  import { ReactRenderer } from './ReactRenderer'
12
- import { ReactNodeViewContext } from './useReactNodeView'
13
-
14
- interface ReactNodeViewRendererOptions {
15
- stopEvent: ((event: Event) => boolean) | null,
16
- update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null,
13
+ import { ReactNodeViewContext, ReactNodeViewContextProps } from './useReactNodeView'
14
+
15
+ export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions {
16
+ update: ((props: {
17
+ oldNode: ProseMirrorNode,
18
+ oldDecorations: Decoration[],
19
+ newNode: ProseMirrorNode,
20
+ newDecorations: Decoration[],
21
+ updateProps: () => void,
22
+ }) => boolean) | null,
17
23
  }
18
24
 
19
- class ReactNodeView extends NodeView<React.FunctionComponent, Editor> {
25
+ class ReactNodeView extends NodeView<React.FunctionComponent, Editor, ReactNodeViewRendererOptions> {
20
26
 
21
27
  renderer!: ReactRenderer
22
28
 
29
+ contentDOMElement!: HTMLElement | null
30
+
23
31
  mount() {
24
32
  const props: NodeViewProps = {
25
33
  editor: this.editor,
@@ -29,6 +37,7 @@ class ReactNodeView extends NodeView<React.FunctionComponent, Editor> {
29
37
  extension: this.extension,
30
38
  getPos: () => this.getPos(),
31
39
  updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
40
+ deleteNode: () => this.deleteNode(),
32
41
  }
33
42
 
34
43
  if (!(this.component as any).displayName) {
@@ -36,26 +45,24 @@ class ReactNodeView extends NodeView<React.FunctionComponent, Editor> {
36
45
  return string.charAt(0).toUpperCase() + string.substring(1)
37
46
  }
38
47
 
39
- // @ts-ignore
40
- this.component.displayName = capitalizeFirstChar(this.extension.config.name)
48
+ this.component.displayName = capitalizeFirstChar(this.extension.name)
41
49
  }
42
50
 
43
51
  const ReactNodeViewProvider: React.FunctionComponent = componentProps => {
44
- const [isEditable, setIsEditable] = useState(this.editor.isEditable)
45
- const onDragStart = this.onDragStart.bind(this)
46
- const onViewUpdate = () => setIsEditable(this.editor.isEditable)
47
52
  const Component = this.component
48
-
49
- useEffect(() => {
50
- this.editor.on('viewUpdate', onViewUpdate)
51
-
52
- return () => {
53
- this.editor.off('viewUpdate', onViewUpdate)
53
+ const onDragStart = this.onDragStart.bind(this)
54
+ const nodeViewContentRef: ReactNodeViewContextProps['nodeViewContentRef'] = element => {
55
+ if (
56
+ element
57
+ && this.contentDOMElement
58
+ && element.firstChild !== this.contentDOMElement
59
+ ) {
60
+ element.appendChild(this.contentDOMElement)
54
61
  }
55
- }, [])
62
+ }
56
63
 
57
64
  return (
58
- <ReactNodeViewContext.Provider value={{ onDragStart, isEditable }}>
65
+ <ReactNodeViewContext.Provider value={{ onDragStart, nodeViewContentRef }}>
59
66
  <Component {...componentProps} />
60
67
  </ReactNodeViewContext.Provider>
61
68
  )
@@ -63,15 +70,32 @@ class ReactNodeView extends NodeView<React.FunctionComponent, Editor> {
63
70
 
64
71
  ReactNodeViewProvider.displayName = 'ReactNodeView'
65
72
 
73
+ this.contentDOMElement = this.node.isLeaf
74
+ ? null
75
+ : document.createElement(this.node.isInline ? 'span' : 'div')
76
+
77
+ if (this.contentDOMElement) {
78
+ // For some reason the whiteSpace prop is not inherited properly in Chrome and Safari
79
+ // With this fix it seems to work fine
80
+ // See: https://github.com/ueberdosis/tiptap/issues/1197
81
+ this.contentDOMElement.style.whiteSpace = 'inherit'
82
+ }
83
+
66
84
  this.renderer = new ReactRenderer(ReactNodeViewProvider, {
67
85
  editor: this.editor,
68
86
  props,
87
+ as: this.node.isInline
88
+ ? 'span'
89
+ : 'div',
69
90
  })
70
91
  }
71
92
 
72
93
  get dom() {
73
- if (!this.renderer.element.firstElementChild?.hasAttribute('data-node-view-wrapper')) {
74
- throw Error('Please use the ReactViewWrapper component for your node view.')
94
+ if (
95
+ this.renderer.element.firstElementChild
96
+ && !this.renderer.element.firstElementChild?.hasAttribute('data-node-view-wrapper')
97
+ ) {
98
+ throw Error('Please use the NodeViewWrapper component for your node view.')
75
99
  }
76
100
 
77
101
  return this.renderer.element
@@ -82,27 +106,42 @@ class ReactNodeView extends NodeView<React.FunctionComponent, Editor> {
82
106
  return null
83
107
  }
84
108
 
85
- const contentElement = this.dom.querySelector('[data-node-view-content]')
86
-
87
- return contentElement || this.dom
109
+ return this.contentDOMElement
88
110
  }
89
111
 
90
112
  update(node: ProseMirrorNode, decorations: Decoration[]) {
91
- if (typeof this.options.update === 'function') {
92
- return this.options.update(node, decorations)
113
+ const updateProps = (props?: Record<string, any>) => {
114
+ this.renderer.updateProps(props)
93
115
  }
94
116
 
95
117
  if (node.type !== this.node.type) {
96
118
  return false
97
119
  }
98
120
 
121
+ if (typeof this.options.update === 'function') {
122
+ const oldNode = this.node
123
+ const oldDecorations = this.decorations
124
+
125
+ this.node = node
126
+ this.decorations = decorations
127
+
128
+ return this.options.update({
129
+ oldNode,
130
+ oldDecorations,
131
+ newNode: node,
132
+ newDecorations: decorations,
133
+ updateProps: () => updateProps({ node, decorations }),
134
+ })
135
+ }
136
+
99
137
  if (node === this.node && this.decorations === decorations) {
100
138
  return true
101
139
  }
102
140
 
103
141
  this.node = node
104
142
  this.decorations = decorations
105
- this.renderer.updateProps({ node, decorations })
143
+
144
+ updateProps({ node, decorations })
106
145
 
107
146
  return true
108
147
  }
@@ -121,6 +160,7 @@ class ReactNodeView extends NodeView<React.FunctionComponent, Editor> {
121
160
 
122
161
  destroy() {
123
162
  this.renderer.destroy()
163
+ this.contentDOMElement = null
124
164
  }
125
165
  }
126
166
 
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
- import { AnyObject } from '@tiptap/core'
3
- import { Editor } from './Editor'
2
+ import { Editor } from '@tiptap/core'
3
+ import { Editor as ExtendedEditor } from './Editor'
4
4
 
5
5
  function isClassComponent(Component: any) {
6
6
  return !!(
@@ -10,33 +10,45 @@ function isClassComponent(Component: any) {
10
10
  )
11
11
  }
12
12
 
13
+ function isForwardRefComponent(Component: any) {
14
+ return !!(
15
+ typeof Component === 'object'
16
+ && Component.$$typeof?.toString() === 'Symbol(react.forward_ref)'
17
+ )
18
+ }
19
+
13
20
  export interface ReactRendererOptions {
14
- as?: string,
15
21
  editor: Editor,
16
- props?: AnyObject,
22
+ props?: Record<string, any>,
23
+ as?: string,
17
24
  }
18
25
 
19
- export class ReactRenderer {
26
+ type ComponentType<R> =
27
+ | React.ComponentClass
28
+ | React.FunctionComponent
29
+ | React.ForwardRefExoticComponent<{ items: any[], command: any } & React.RefAttributes<R>>
30
+
31
+ export class ReactRenderer<R = unknown> {
20
32
  id: string
21
33
 
22
- editor: Editor
34
+ editor: ExtendedEditor
23
35
 
24
36
  component: any
25
37
 
26
38
  element: Element
27
39
 
28
- props: AnyObject
40
+ props: Record<string, any>
29
41
 
30
42
  reactElement: React.ReactNode
31
43
 
32
- ref: React.Component | null = null
44
+ ref: R | null = null
33
45
 
34
- constructor(component: React.Component | React.FunctionComponent, { props = {}, editor }: ReactRendererOptions) {
46
+ constructor(component: ComponentType<R>, { editor, props = {}, as = 'div' }: ReactRendererOptions) {
35
47
  this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
36
48
  this.component = component
37
- this.editor = editor
49
+ this.editor = editor as ExtendedEditor
38
50
  this.props = props
39
- this.element = document.createElement('div')
51
+ this.element = document.createElement(as)
40
52
  this.element.classList.add('react-renderer')
41
53
  this.render()
42
54
  }
@@ -45,8 +57,8 @@ export class ReactRenderer {
45
57
  const Component = this.component
46
58
  const props = this.props
47
59
 
48
- if (isClassComponent(Component)) {
49
- props.ref = (ref: React.Component) => {
60
+ if (isClassComponent(Component) || isForwardRefComponent(Component)) {
61
+ props.ref = (ref: R) => {
50
62
  this.ref = ref
51
63
  }
52
64
  }
@@ -63,7 +75,7 @@ export class ReactRenderer {
63
75
  }
64
76
  }
65
77
 
66
- updateProps(props: AnyObject = {}): void {
78
+ updateProps(props: Record<string, any> = {}): void {
67
79
  this.props = {
68
80
  ...this.props,
69
81
  ...props,
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from '@tiptap/core'
2
2
  export * from './BubbleMenu'
3
3
  export { Editor } from './Editor'
4
+ export * from './FloatingMenu'
4
5
  export * from './useEditor'
5
6
  export * from './ReactRenderer'
6
7
  export * from './ReactNodeViewRenderer'
package/src/useEditor.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react'
1
+ import { useState, useEffect, DependencyList } from 'react'
2
2
  import { EditorOptions } from '@tiptap/core'
3
3
  import { Editor } from './Editor'
4
4
 
@@ -8,7 +8,7 @@ function useForceUpdate() {
8
8
  return () => setValue(value => value + 1)
9
9
  }
10
10
 
11
- export const useEditor = (options: Partial<EditorOptions> = {}) => {
11
+ export const useEditor = (options: Partial<EditorOptions> = {}, deps: DependencyList = []) => {
12
12
  const [editor, setEditor] = useState<Editor | null>(null)
13
13
  const forceUpdate = useForceUpdate()
14
14
 
@@ -17,12 +17,18 @@ export const useEditor = (options: Partial<EditorOptions> = {}) => {
17
17
 
18
18
  setEditor(instance)
19
19
 
20
- instance.on('transaction', forceUpdate)
20
+ instance.on('transaction', () => {
21
+ requestAnimationFrame(() => {
22
+ requestAnimationFrame(() => {
23
+ forceUpdate()
24
+ })
25
+ })
26
+ })
21
27
 
22
28
  return () => {
23
29
  instance.destroy()
24
30
  }
25
- }, [])
31
+ }, deps)
26
32
 
27
33
  return editor
28
34
  }
@@ -1,12 +1,11 @@
1
1
  import { createContext, useContext } from 'react'
2
2
 
3
3
  export interface ReactNodeViewContextProps {
4
- isEditable: boolean,
5
4
  onDragStart: (event: DragEvent) => void,
5
+ nodeViewContentRef: (element: HTMLElement | null) => void,
6
6
  }
7
7
 
8
8
  export const ReactNodeViewContext = createContext<Partial<ReactNodeViewContextProps>>({
9
- isEditable: undefined,
10
9
  onDragStart: undefined,
11
10
  })
12
11