Merge branch 'main' of github.com:ueberdosis/tiptap
This commit is contained in:
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -23,8 +23,8 @@ body:
|
||||
value: |
|
||||
Chances that we can fix your issue are way higher, if you can provide a CodeSandbox to reproduce the issue.
|
||||
|
||||
* Vue: https://codesandbox.io/s/tiptap-issue-template-b83rr?file=/src/components/Tiptap.vue
|
||||
* React: https://codesandbox.io/s/tiptap-react-08yxr
|
||||
* Vue: https://codesandbox.io/s/tiptap-vue-3-issue-template-tnlpv?file=/src/App.vue
|
||||
* React: https://codesandbox.io/s/tiptap-react-issue-template-b4hmi?file=/src/App.js
|
||||
- type: textarea
|
||||
id: codesandbox
|
||||
attributes:
|
||||
|
||||
@@ -5,7 +5,6 @@ prosemirror-commands
|
||||
prosemirror-dropcursor
|
||||
prosemirror-gapcursor
|
||||
prosemirror-history
|
||||
prosemirror-inputrules
|
||||
prosemirror-keymap
|
||||
prosemirror-model
|
||||
prosemirror-schema-list
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"ts": "tsc --project tsconfig.base.json --noEmit && tsc --project tsconfig.react.json --noEmit && tsc --project tsconfig.vue-2.json --noEmit && tsc --project tsconfig.vue-3.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3": "^7.0.4",
|
||||
"d3": "^7.1.1",
|
||||
"fast-glob": "^3.2.7",
|
||||
"remixicon": "^2.5.0",
|
||||
"shiki": "^0.9.11",
|
||||
@@ -16,22 +16,22 @@
|
||||
"y-indexeddb": "^9.0.6",
|
||||
"y-webrtc": "^10.2.0",
|
||||
"y-websocket": "^1.3.16",
|
||||
"yjs": "^13.5.12"
|
||||
"yjs": "^13.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^8.3.1",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.6",
|
||||
"@vitejs/plugin-vue": "^1.9.2",
|
||||
"autoprefixer": "^10.3.6",
|
||||
"@vitejs/plugin-vue": "^1.9.3",
|
||||
"autoprefixer": "^10.3.7",
|
||||
"iframe-resizer": "^4.3.2",
|
||||
"postcss": "^8.3.8",
|
||||
"postcss": "^8.3.9",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"sass": "^1.42.1",
|
||||
"tailwindcss": "^2.2.16",
|
||||
"typescript": "^4.4.3",
|
||||
"uuid": "^8.3.2",
|
||||
"vite": "^2.6.2",
|
||||
"vite": "^2.6.5",
|
||||
"vite-plugin-checker": "^0.3.4",
|
||||
"vue": "^3.0.5",
|
||||
"vue-router": "^4.0.11"
|
||||
|
||||
134
demos/src/Examples/CollaborativeEditing/React/MenuBar.jsx
Normal file
134
demos/src/Examples/CollaborativeEditing/React/MenuBar.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import MenuItem from './MenuItem'
|
||||
import './MenuBar.scss'
|
||||
|
||||
export default ({ editor }) => {
|
||||
const items = [
|
||||
{
|
||||
icon: 'bold',
|
||||
title: 'Bold',
|
||||
action: () => editor.chain().focus().toggleBold().run(),
|
||||
isActive: () => editor.isActive('bold'),
|
||||
},
|
||||
{
|
||||
icon: 'italic',
|
||||
title: 'Italic',
|
||||
action: () => editor.chain().focus().toggleItalic().run(),
|
||||
isActive: () => editor.isActive('italic'),
|
||||
},
|
||||
{
|
||||
icon: 'strikethrough',
|
||||
title: 'Strike',
|
||||
action: () => editor.chain().focus().toggleStrike().run(),
|
||||
isActive: () => editor.isActive('strike'),
|
||||
},
|
||||
{
|
||||
icon: 'code-view',
|
||||
title: 'Code',
|
||||
action: () => editor.chain().focus().toggleCode().run(),
|
||||
isActive: () => editor.isActive('code'),
|
||||
},
|
||||
{
|
||||
icon: 'mark-pen-line',
|
||||
title: 'Highlight',
|
||||
action: () => editor.chain().focus().toggleHighlight().run(),
|
||||
isActive: () => editor.isActive('highlight'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'h-1',
|
||||
title: 'Heading 1',
|
||||
action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
isActive: () => editor.isActive('heading', { level: 1 }),
|
||||
},
|
||||
{
|
||||
icon: 'h-2',
|
||||
title: 'Heading 2',
|
||||
action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
isActive: () => editor.isActive('heading', { level: 2 }),
|
||||
},
|
||||
{
|
||||
icon: 'paragraph',
|
||||
title: 'Paragraph',
|
||||
action: () => editor.chain().focus().setParagraph().run(),
|
||||
isActive: () => editor.isActive('paragraph'),
|
||||
},
|
||||
{
|
||||
icon: 'list-unordered',
|
||||
title: 'Bullet List',
|
||||
action: () => editor.chain().focus().toggleBulletList().run(),
|
||||
isActive: () => editor.isActive('bulletList'),
|
||||
},
|
||||
{
|
||||
icon: 'list-ordered',
|
||||
title: 'Ordered List',
|
||||
action: () => editor.chain().focus().toggleOrderedList().run(),
|
||||
isActive: () => editor.isActive('orderedList'),
|
||||
},
|
||||
{
|
||||
icon: 'list-check-2',
|
||||
title: 'Task List',
|
||||
action: () => editor.chain().focus().toggleTaskList().run(),
|
||||
isActive: () => editor.isActive('taskList'),
|
||||
},
|
||||
{
|
||||
icon: 'code-box-line',
|
||||
title: 'Code Block',
|
||||
action: () => editor.chain().focus().toggleCodeBlock().run(),
|
||||
isActive: () => editor.isActive('codeBlock'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'double-quotes-l',
|
||||
title: 'Blockquote',
|
||||
action: () => editor.chain().focus().toggleBlockquote().run(),
|
||||
isActive: () => editor.isActive('blockquote'),
|
||||
},
|
||||
{
|
||||
icon: 'separator',
|
||||
title: 'Horizontal Rule',
|
||||
action: () => editor.chain().focus().setHorizontalRule().run(),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'text-wrap',
|
||||
title: 'Hard Break',
|
||||
action: () => editor.chain().focus().setHardBreak().run(),
|
||||
},
|
||||
{
|
||||
icon: 'format-clear',
|
||||
title: 'Clear Format',
|
||||
action: () => editor.chain().focus().clearNodes().unsetAllMarks()
|
||||
.run(),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'arrow-go-back-line',
|
||||
title: 'Undo',
|
||||
action: () => editor.chain().focus().undo().run(),
|
||||
},
|
||||
{
|
||||
icon: 'arrow-go-forward-line',
|
||||
title: 'Redo',
|
||||
action: () => editor.chain().focus().redo().run(),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="editor__header">
|
||||
{items.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
{item.type === 'divider' ? <div className="divider" /> : <MenuItem {...item} />}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.divider {
|
||||
background-color: rgba(#000, 0.1);
|
||||
height: 1.25rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.75rem;
|
||||
width: 2px;
|
||||
}
|
||||
17
demos/src/Examples/CollaborativeEditing/React/MenuItem.jsx
Normal file
17
demos/src/Examples/CollaborativeEditing/React/MenuItem.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react'
|
||||
import './MenuItem.scss'
|
||||
import remixiconUrl from 'remixicon/fonts/remixicon.symbol.svg'
|
||||
|
||||
export default ({
|
||||
icon, title, action, isActive = null,
|
||||
}) => (
|
||||
<button
|
||||
className={`menu-item${isActive && isActive() ? ' is-active' : ''}`}
|
||||
onClick={action}
|
||||
title={title}
|
||||
>
|
||||
<svg className="remix">
|
||||
<use xlinkHref={`${remixiconUrl}#ri-${icon}`} />
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
22
demos/src/Examples/CollaborativeEditing/React/MenuItem.scss
Normal file
22
demos/src/Examples/CollaborativeEditing/React/MenuItem.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.menu-item {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0.4rem;
|
||||
color: #0d0d0d;
|
||||
height: 1.75rem;
|
||||
margin-right: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
width: 1.75rem;
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
background-color: #0d0d0d;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
15
demos/src/Examples/CollaborativeEditing/React/index.html
Normal file
15
demos/src/Examples/CollaborativeEditing/React/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module">
|
||||
import setup from '../../../../setup/react.ts'
|
||||
import source from '@source'
|
||||
setup('Examples/CollaborativeEditing', source)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
140
demos/src/Examples/CollaborativeEditing/React/index.jsx
Normal file
140
demos/src/Examples/CollaborativeEditing/React/index.jsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, {
|
||||
useState, useCallback, useEffect,
|
||||
} from 'react'
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import { IndexeddbPersistence } from 'y-indexeddb'
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import TaskList from '@tiptap/extension-task-list'
|
||||
import TaskItem from '@tiptap/extension-task-item'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import CharacterCount from '@tiptap/extension-character-count'
|
||||
import Collaboration from '@tiptap/extension-collaboration'
|
||||
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
||||
import MenuBar from './MenuBar'
|
||||
import './styles.scss'
|
||||
|
||||
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D']
|
||||
const rooms = ['rooms.7', 'rooms.8', 'rooms.9']
|
||||
const names = [
|
||||
'Lea Thompson',
|
||||
'Cyndi Lauper',
|
||||
'Tom Cruise',
|
||||
'Madonna',
|
||||
'Jerry Hall',
|
||||
'Joan Collins',
|
||||
'Winona Ryder',
|
||||
'Christina Applegate',
|
||||
'Alyssa Milano',
|
||||
'Molly Ringwald',
|
||||
'Ally Sheedy',
|
||||
'Debbie Harry',
|
||||
'Olivia Newton-John',
|
||||
'Elton John',
|
||||
'Michael J. Fox',
|
||||
'Axl Rose',
|
||||
'Emilio Estevez',
|
||||
'Ralph Macchio',
|
||||
'Rob Lowe',
|
||||
'Jennifer Grey',
|
||||
'Mickey Rourke',
|
||||
'John Cusack',
|
||||
'Matthew Broderick',
|
||||
'Justine Bateman',
|
||||
'Lisa Bonet',
|
||||
]
|
||||
|
||||
const getRandomElement = list => list[Math.floor(Math.random() * list.length)]
|
||||
|
||||
const getRandomRoom = () => getRandomElement(rooms)
|
||||
const getRandomColor = () => getRandomElement(colors)
|
||||
const getRandomName = () => getRandomElement(names)
|
||||
|
||||
const room = getRandomRoom()
|
||||
|
||||
const ydoc = new Y.Doc()
|
||||
const websocketProvider = new WebsocketProvider('wss://websocket.tiptap.dev', room, ydoc)
|
||||
|
||||
const getInitialUser = () => {
|
||||
return JSON.parse(localStorage.getItem('currentUser')) || {
|
||||
name: getRandomName(),
|
||||
color: getRandomColor(),
|
||||
}
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [status, setStatus] = useState('connecting')
|
||||
const [users, setUsers] = useState([])
|
||||
const [currentUser, setCurrentUser] = useState(getInitialUser)
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
history: false,
|
||||
}),
|
||||
Highlight,
|
||||
TaskList,
|
||||
TaskItem,
|
||||
CharacterCount.configure({
|
||||
limit: 10000,
|
||||
}),
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCursor.configure({
|
||||
provider: websocketProvider,
|
||||
onUpdate: updatedUsers => {
|
||||
setUsers(updatedUsers)
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Store shared data persistently in browser to make offline editing possible
|
||||
const indexeddbProvider = new IndexeddbPersistence(room, ydoc)
|
||||
|
||||
indexeddbProvider.on('synced', () => {
|
||||
console.log('Loaded content from database …')
|
||||
})
|
||||
|
||||
// Update status changes
|
||||
websocketProvider.on('status', event => {
|
||||
setStatus(event.status)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Save current user to localStorage and emit to editor
|
||||
useEffect(() => {
|
||||
if (editor && currentUser) {
|
||||
localStorage.setItem('currentUser', JSON.stringify(currentUser))
|
||||
editor.chain().focus().user(currentUser).run()
|
||||
}
|
||||
}, [editor, currentUser])
|
||||
|
||||
const setName = useCallback(() => {
|
||||
const name = (window.prompt('Name') || '').trim().substring(0, 32)
|
||||
|
||||
if (name) {
|
||||
return setCurrentUser({ ...currentUser, name })
|
||||
}
|
||||
}, [currentUser])
|
||||
|
||||
return (
|
||||
<div className="editor">
|
||||
{editor && <MenuBar editor={editor} />}
|
||||
<EditorContent className="editor__content" editor={editor} />
|
||||
<div className="editor__footer">
|
||||
<div className={`editor__status editor__status--${status}`}>
|
||||
{status === 'connected'
|
||||
? `${users.length} user${users.length === 1 ? '' : 's'} online in ${room}`
|
||||
: 'offline'}
|
||||
</div>
|
||||
<div className="editor__name">
|
||||
<button onClick={setName}>{currentUser.name}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
context('/src/Examples/CollaborativeEditing/React/', () => {
|
||||
before(() => {
|
||||
cy.visit('/src/Examples/CollaborativeEditing/React/')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
196
demos/src/Examples/CollaborativeEditing/React/styles.scss
Normal file
196
demos/src/Examples/CollaborativeEditing/React/styles.scss
Normal file
@@ -0,0 +1,196 @@
|
||||
/* Basic editor styles */
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: rgba(#616161, 0.1);
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #0d0d0d;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-family: "JetBrainsMono", monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: #faf594;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid rgba(#0d0d0d, 0.1);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid rgba(#0d0d0d, 0.1);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor {
|
||||
background-color: #fff;
|
||||
border: 3px solid #0d0d0d;
|
||||
border-radius: 0.75rem;
|
||||
color: #0d0d0d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 26rem;
|
||||
|
||||
&__header {
|
||||
align-items: center;
|
||||
border-bottom: 3px solid #0d0d0d;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem 1rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
align-items: center;
|
||||
border-top: 3px solid #0d0d0d;
|
||||
color: #0d0d0d;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 12px;
|
||||
flex-wrap: wrap;
|
||||
font-weight: 600;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Some information about the status */
|
||||
&__status {
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
|
||||
&::before {
|
||||
background: rgba(#0d0d0d, 0.5);
|
||||
border-radius: 50%;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
height: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
&--connecting::before {
|
||||
background: #616161;
|
||||
}
|
||||
|
||||
&--connected::before {
|
||||
background: #b9f18d;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0.4rem;
|
||||
color: #0d0d0d;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: #0d0d0d;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Give a remote user a caret */
|
||||
.collaboration-cursor__caret {
|
||||
border-left: 1px solid #0d0d0d;
|
||||
border-right: 1px solid #0d0d0d;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
/* Render the username above the caret */
|
||||
.collaboration-cursor__label {
|
||||
border-radius: 3px 3px 3px 0;
|
||||
color: #0d0d0d;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
left: -1px;
|
||||
line-height: normal;
|
||||
padding: 0.1rem 0.3rem;
|
||||
position: absolute;
|
||||
top: -1.4em;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react'
|
||||
import tippy from 'tippy.js'
|
||||
import { useEditor, EditorContent, ReactRenderer } from '@tiptap/react'
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import CharacterCount from '@tiptap/extension-character-count'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
import { MentionList } from './MentionList'
|
||||
import suggestion from './suggestion'
|
||||
import './styles.scss'
|
||||
|
||||
export default () => {
|
||||
@@ -24,56 +23,7 @@ export default () => {
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestion: {
|
||||
items: query => {
|
||||
return [
|
||||
'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
|
||||
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
|
||||
},
|
||||
render: () => {
|
||||
let reactRenderer
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
reactRenderer = new ReactRenderer(MentionList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: reactRenderer.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
reactRenderer.updateProps(props)
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return reactRenderer.ref?.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
reactRenderer.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
suggestion,
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
|
||||
54
demos/src/Examples/Community/React/suggestion.js
Normal file
54
demos/src/Examples/Community/React/suggestion.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ReactRenderer } from '@tiptap/react'
|
||||
import tippy from 'tippy.js'
|
||||
import { MentionList } from './MentionList'
|
||||
|
||||
export default {
|
||||
items: query => {
|
||||
return [
|
||||
'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
|
||||
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
|
||||
},
|
||||
render: () => {
|
||||
let reactRenderer
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
reactRenderer = new ReactRenderer(MentionList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: reactRenderer.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
reactRenderer.updateProps(props)
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return reactRenderer.ref?.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
reactRenderer.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -41,14 +41,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tippy from 'tippy.js'
|
||||
import { Editor, EditorContent, VueRenderer } from '@tiptap/vue-3'
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import CharacterCount from '@tiptap/extension-character-count'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
import MentionList from './MentionList.vue'
|
||||
import suggestion from './suggestion'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -75,59 +74,7 @@ export default {
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestion: {
|
||||
items: query => {
|
||||
return [
|
||||
'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
|
||||
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
|
||||
},
|
||||
render: () => {
|
||||
let component
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
component = new VueRenderer(MentionList, {
|
||||
// using vue 2:
|
||||
// parent: this,
|
||||
// propsData: props,
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
component.updateProps(props)
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
component.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
suggestion,
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
|
||||
57
demos/src/Examples/Community/Vue/suggestion.js
Normal file
57
demos/src/Examples/Community/Vue/suggestion.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { VueRenderer } from '@tiptap/vue-3'
|
||||
import tippy from 'tippy.js'
|
||||
import MentionList from './MentionList.vue'
|
||||
|
||||
export default {
|
||||
items: query => {
|
||||
return [
|
||||
'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
|
||||
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
|
||||
},
|
||||
render: () => {
|
||||
let component
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
component = new VueRenderer(MentionList, {
|
||||
// using vue 2:
|
||||
// parent: this,
|
||||
// propsData: props,
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
component.updateProps(props)
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
component.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,134 +1,133 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { InputRule } from 'prosemirror-inputrules'
|
||||
import { Extension, textInputRule } from '@tiptap/core'
|
||||
|
||||
export const SmilieReplacer = Extension.create({
|
||||
name: 'smilieReplacer',
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
new InputRule(/-___- $/, '😑 '),
|
||||
new InputRule(/:'-\) $/, '😂 '),
|
||||
new InputRule(/':-\) $/, '😅 '),
|
||||
new InputRule(/':-D $/, '😅 '),
|
||||
new InputRule(/>:-\) $/, '😆 '),
|
||||
new InputRule(/-__- $/, '😑 '),
|
||||
new InputRule(/':-\( $/, '😓 '),
|
||||
new InputRule(/:'-\( $/, '😢 '),
|
||||
new InputRule(/>:-\( $/, '😠 '),
|
||||
new InputRule(/O:-\) $/, '😇 '),
|
||||
new InputRule(/0:-3 $/, '😇 '),
|
||||
new InputRule(/0:-\) $/, '😇 '),
|
||||
new InputRule(/0;\^\) $/, '😇 '),
|
||||
new InputRule(/O;-\) $/, '😇 '),
|
||||
new InputRule(/0;-\) $/, '😇 '),
|
||||
new InputRule(/O:-3 $/, '😇 '),
|
||||
new InputRule(/:'\) $/, '😂 '),
|
||||
new InputRule(/:-D $/, '😃 '),
|
||||
new InputRule(/':\) $/, '😅 '),
|
||||
new InputRule(/'=\) $/, '😅 '),
|
||||
new InputRule(/':D $/, '😅 '),
|
||||
new InputRule(/'=D $/, '😅 '),
|
||||
new InputRule(/>:\) $/, '😆 '),
|
||||
new InputRule(/>;\) $/, '😆 '),
|
||||
new InputRule(/>=\) $/, '😆 '),
|
||||
new InputRule(/;-\) $/, '😉 '),
|
||||
new InputRule(/\*-\) $/, '😉 '),
|
||||
new InputRule(/;-\] $/, '😉 '),
|
||||
new InputRule(/;\^\) $/, '😉 '),
|
||||
new InputRule(/B-\) $/, '😎 '),
|
||||
new InputRule(/8-\) $/, '😎 '),
|
||||
new InputRule(/B-D $/, '😎 '),
|
||||
new InputRule(/8-D $/, '😎 '),
|
||||
new InputRule(/:-\* $/, '😘 '),
|
||||
new InputRule(/:\^\* $/, '😘 '),
|
||||
new InputRule(/:-\) $/, '🙂 '),
|
||||
new InputRule(/-_- $/, '😑 '),
|
||||
new InputRule(/:-X $/, '😶 '),
|
||||
new InputRule(/:-# $/, '😶 '),
|
||||
new InputRule(/:-x $/, '😶 '),
|
||||
new InputRule(/>.< $/, '😣 '),
|
||||
new InputRule(/:-O $/, '😮 '),
|
||||
new InputRule(/:-o $/, '😮 '),
|
||||
new InputRule(/O_O $/, '😮 '),
|
||||
new InputRule(/>:O $/, '😮 '),
|
||||
new InputRule(/:-P $/, '😛 '),
|
||||
new InputRule(/:-p $/, '😛 '),
|
||||
new InputRule(/:-Þ $/, '😛 '),
|
||||
new InputRule(/:-þ $/, '😛 '),
|
||||
new InputRule(/:-b $/, '😛 '),
|
||||
new InputRule(/>:P $/, '😜 '),
|
||||
new InputRule(/X-P $/, '😜 '),
|
||||
new InputRule(/x-p $/, '😜 '),
|
||||
new InputRule(/':\( $/, '😓 '),
|
||||
new InputRule(/'=\( $/, '😓 '),
|
||||
new InputRule(/>:\\ $/, '😕 '),
|
||||
new InputRule(/>:\/ $/, '😕 '),
|
||||
new InputRule(/:-\/ $/, '😕 '),
|
||||
new InputRule(/:-. $/, '😕 '),
|
||||
new InputRule(/>:\[ $/, '😞 '),
|
||||
new InputRule(/:-\( $/, '😞 '),
|
||||
new InputRule(/:-\[ $/, '😞 '),
|
||||
new InputRule(/:'\( $/, '😢 '),
|
||||
new InputRule(/;-\( $/, '😢 '),
|
||||
new InputRule(/#-\) $/, '😵 '),
|
||||
new InputRule(/%-\) $/, '😵 '),
|
||||
new InputRule(/X-\) $/, '😵 '),
|
||||
new InputRule(/>:\( $/, '😠 '),
|
||||
new InputRule(/0:3 $/, '😇 '),
|
||||
new InputRule(/0:\) $/, '😇 '),
|
||||
new InputRule(/O:\) $/, '😇 '),
|
||||
new InputRule(/O=\) $/, '😇 '),
|
||||
new InputRule(/O:3 $/, '😇 '),
|
||||
new InputRule(/<\/3 $/, '💔 '),
|
||||
new InputRule(/:D $/, '😃 '),
|
||||
new InputRule(/=D $/, '😃 '),
|
||||
new InputRule(/;\) $/, '😉 '),
|
||||
new InputRule(/\*\) $/, '😉 '),
|
||||
new InputRule(/;\] $/, '😉 '),
|
||||
new InputRule(/;D $/, '😉 '),
|
||||
new InputRule(/B\) $/, '😎 '),
|
||||
new InputRule(/8\) $/, '😎 '),
|
||||
new InputRule(/:\* $/, '😘 '),
|
||||
new InputRule(/=\* $/, '😘 '),
|
||||
new InputRule(/:\) $/, '🙂 '),
|
||||
new InputRule(/=\] $/, '🙂 '),
|
||||
new InputRule(/=\) $/, '🙂 '),
|
||||
new InputRule(/:\] $/, '🙂 '),
|
||||
new InputRule(/:X $/, '😶 '),
|
||||
new InputRule(/:# $/, '😶 '),
|
||||
new InputRule(/=X $/, '😶 '),
|
||||
new InputRule(/=x $/, '😶 '),
|
||||
new InputRule(/:x $/, '😶 '),
|
||||
new InputRule(/=# $/, '😶 '),
|
||||
new InputRule(/:O $/, '😮 '),
|
||||
new InputRule(/:o $/, '😮 '),
|
||||
new InputRule(/:P $/, '😛 '),
|
||||
new InputRule(/=P $/, '😛 '),
|
||||
new InputRule(/:p $/, '😛 '),
|
||||
new InputRule(/=p $/, '😛 '),
|
||||
new InputRule(/:Þ $/, '😛 '),
|
||||
new InputRule(/:þ $/, '😛 '),
|
||||
new InputRule(/:b $/, '😛 '),
|
||||
new InputRule(/d: $/, '😛 '),
|
||||
new InputRule(/:\/ $/, '😕 '),
|
||||
new InputRule(/:\\ $/, '😕 '),
|
||||
new InputRule(/=\/ $/, '😕 '),
|
||||
new InputRule(/=\\ $/, '😕 '),
|
||||
new InputRule(/:L $/, '😕 '),
|
||||
new InputRule(/=L $/, '😕 '),
|
||||
new InputRule(/:\( $/, '😞 '),
|
||||
new InputRule(/:\[ $/, '😞 '),
|
||||
new InputRule(/=\( $/, '😞 '),
|
||||
new InputRule(/;\( $/, '😢 '),
|
||||
new InputRule(/D: $/, '😨 '),
|
||||
new InputRule(/:\$ $/, '😳 '),
|
||||
new InputRule(/=\$ $/, '😳 '),
|
||||
new InputRule(/#\) $/, '😵 '),
|
||||
new InputRule(/%\) $/, '😵 '),
|
||||
new InputRule(/X\) $/, '😵 '),
|
||||
new InputRule(/:@ $/, '😠 '),
|
||||
new InputRule(/<3 $/, '❤️ '),
|
||||
new InputRule(/\/shrug $/, '¯\\_(ツ)_/¯'),
|
||||
textInputRule({ find: /-___- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /:'-\) $/, replace: '😂 ' }),
|
||||
textInputRule({ find: /':-\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /':-D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: />:-\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: /-__- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /':-\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: /:'-\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: />:-\( $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /O:-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:-3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0;\^\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O;-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0;-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:-3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /:'\) $/, replace: '😂 ' }),
|
||||
textInputRule({ find: /:-D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /':\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /'=\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /':D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /'=D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: />:\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: />;\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: />=\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: /;-\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /\*-\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;-\] $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;\^\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /B-\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8-\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /B-D $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8-D $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /:-\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:\^\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:-\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /-_- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /:-X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:-# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:-x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: />.< $/, replace: '😣 ' }),
|
||||
textInputRule({ find: /:-O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:-o $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /O_O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: />:O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:-P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-Þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-b $/, replace: '😛 ' }),
|
||||
textInputRule({ find: />:P $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /X-P $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /x-p $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /':\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: /'=\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: />:\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: />:\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:-\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:-. $/, replace: '😕 ' }),
|
||||
textInputRule({ find: />:\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:-\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:-\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:'\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /;-\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /#-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /%-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /X-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: />:\( $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /0:3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O=\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /<\/3 $/, replace: '💔 ' }),
|
||||
textInputRule({ find: /:D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /=D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /;\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /\*\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;\] $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;D $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /B\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /:\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /=\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /=\] $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /=\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /:\] $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /:X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:o $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /=P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /=p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:Þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:b $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /d: $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:L $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=L $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /=\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /;\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /D: $/, replace: '😨 ' }),
|
||||
textInputRule({ find: /:\$ $/, replace: '😳 ' }),
|
||||
textInputRule({ find: /=\$ $/, replace: '😳 ' }),
|
||||
textInputRule({ find: /#\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /%\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /X\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /:@ $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /<3 $/, replace: '❤️ ' }),
|
||||
textInputRule({ find: /\/shrug $/, replace: '¯\\_(ツ)_/¯' }),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tippy from 'tippy.js'
|
||||
import { Editor, EditorContent, VueRenderer } from '@tiptap/vue-3'
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Commands from './commands'
|
||||
import CommandsList from './CommandsList.vue'
|
||||
import suggestion from './suggestion'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -27,102 +26,7 @@ export default {
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Commands.configure({
|
||||
suggestion: {
|
||||
items: query => {
|
||||
return [
|
||||
{
|
||||
title: 'H1',
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode('heading', { level: 1 })
|
||||
.run()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'H2',
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode('heading', { level: 2 })
|
||||
.run()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'bold',
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setMark('bold')
|
||||
.run()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'italic',
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setMark('italic')
|
||||
.run()
|
||||
},
|
||||
},
|
||||
].filter(item => item.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10)
|
||||
},
|
||||
render: () => {
|
||||
let component
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
component = new VueRenderer(CommandsList, {
|
||||
// using vue 2:
|
||||
// parent: this,
|
||||
// propsData: props,
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
component.updateProps(props)
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
component.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
suggestion,
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
|
||||
100
demos/src/Experiments/Commands/Vue/suggestion.js
Normal file
100
demos/src/Experiments/Commands/Vue/suggestion.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import tippy from 'tippy.js'
|
||||
import { VueRenderer } from '@tiptap/vue-3'
|
||||
import CommandsList from './CommandsList.vue'
|
||||
|
||||
export default {
|
||||
items: query => {
|
||||
return [
|
||||
{
|
||||
title: 'H1',
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode('heading', { level: 1 })
|
||||
.run()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'H2',
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode('heading', { level: 2 })
|
||||
.run()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'bold',
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setMark('bold')
|
||||
.run()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'italic',
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setMark('italic')
|
||||
.run()
|
||||
},
|
||||
},
|
||||
].filter(item => item.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10)
|
||||
},
|
||||
render: () => {
|
||||
let component
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
component = new VueRenderer(CommandsList, {
|
||||
// using vue 2:
|
||||
// parent: this,
|
||||
// propsData: props,
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
component.updateProps(props)
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
component.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import Bold from '@tiptap/extension-bold'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -35,14 +34,13 @@ export default {
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Bold,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<p>
|
||||
Wow, this editor has support for links to the whole <strong><a href="https://en.wikipedia.org/wiki/World_Wide_Web">world wide web</a></strong>. We tested a lot of URLs and I think you can add *every URL* you want. Isn’t that cool? Let’s try <a href="https://statamic.com/">another one!</a> Yep, seems to work.
|
||||
Wow, this editor has support for links to the whole <a href="https://en.wikipedia.org/wiki/World_Wide_Web">world wide web</a>. We tested a lot of URLs and I think you can add *every URL* you want. Isn’t that cool? Let’s try <a href="https://statamic.com/">another one!</a> Yep, seems to work.
|
||||
</p>
|
||||
<p>
|
||||
By default every link will get a \`rel="noopener noreferrer nofollow"\` attribute. It’s configurable though.
|
||||
|
||||
@@ -126,6 +126,17 @@ context('/src/Nodes/CodeBlock/Vue/', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should make a code block from backtick markdown shortcuts followed by enter', () => {
|
||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||
editor.commands.clearContent()
|
||||
|
||||
cy.get('.ProseMirror')
|
||||
.type('```{enter}Code')
|
||||
.find('pre>code')
|
||||
.should('contain', 'Code')
|
||||
})
|
||||
})
|
||||
|
||||
it('reverts the markdown shortcut when pressing backspace', () => {
|
||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||
editor.commands.clearContent()
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tippy from 'tippy.js'
|
||||
import { Editor, EditorContent, VueRenderer } from '@tiptap/vue-3'
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
import MentionList from './MentionList.vue'
|
||||
import suggestion from './suggestion'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -34,59 +33,7 @@ export default {
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestion: {
|
||||
items: query => {
|
||||
return [
|
||||
'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
|
||||
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10)
|
||||
},
|
||||
render: () => {
|
||||
let component
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
component = new VueRenderer(MentionList, {
|
||||
// using vue 2:
|
||||
// parent: this,
|
||||
// propsData: props,
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
component.updateProps(props)
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
component.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
suggestion,
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
|
||||
57
demos/src/Nodes/Mention/Vue/suggestion.js
Normal file
57
demos/src/Nodes/Mention/Vue/suggestion.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { VueRenderer } from '@tiptap/vue-3'
|
||||
import tippy from 'tippy.js'
|
||||
import MentionList from './MentionList.vue'
|
||||
|
||||
export default {
|
||||
items: query => {
|
||||
return [
|
||||
'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
|
||||
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
|
||||
},
|
||||
render: () => {
|
||||
let component
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
component = new VueRenderer(MentionList, {
|
||||
// using vue 2:
|
||||
// parent: this,
|
||||
// propsData: props,
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
component.updateProps(props)
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
component.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -387,15 +387,18 @@ import Strike from '@tiptap/extension-strike'
|
||||
import { markInputRule } from '@tiptap/core'
|
||||
|
||||
// Default:
|
||||
// const inputRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))$/gm
|
||||
// const inputRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))$/
|
||||
|
||||
// New:
|
||||
const inputRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))$/gm
|
||||
const inputRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))$/
|
||||
|
||||
const CustomStrike = Strike.extend({
|
||||
addInputRules() {
|
||||
return [
|
||||
markInputRule(inputRegex, this.type),
|
||||
markInputRule({
|
||||
find: inputRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
@@ -414,15 +417,18 @@ import Strike from '@tiptap/extension-strike'
|
||||
import { markPasteRule } from '@tiptap/core'
|
||||
|
||||
// Default:
|
||||
// const pasteRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))/gm
|
||||
// const pasteRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))/g
|
||||
|
||||
// New:
|
||||
const pasteRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))/gm
|
||||
const pasteRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))/g
|
||||
|
||||
const CustomStrike = Strike.extend({
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule(pasteRegex, this.type),
|
||||
markPasteRule({
|
||||
find: pasteRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -102,7 +102,7 @@ const CustomBold = Bold.extend({
|
||||
new Editor({
|
||||
extensions: [
|
||||
// …
|
||||
CustomBold(),
|
||||
CustomBold,
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -47,7 +47,7 @@ yarn add @tiptap/react @tiptap/starter-kit
|
||||
If you followed step 1 and 2, you can now start your project with `npm run start` or `yarn start`, and open [http://localhost:3000](http://localhost:3000) in your favorite browser. This might be different, if you’re working with an existing project.
|
||||
|
||||
## 3. Create a new component
|
||||
To actually start using tiptap, you’ll need to add a new component to your app. Let’s call it `Tiptap` and put the following example code in `src/Tiptap.jsx`.
|
||||
To actually start using tiptap, you’ll need to create a new component in your app. Let’s call it `Tiptap` and put the following example code in `src/Tiptap.jsx`.
|
||||
|
||||
This is the fastest way to get tiptap up and running with React. It will give you a very basic version of tiptap, without any buttons. No worries, you will be able to add more functionality soon.
|
||||
|
||||
|
||||
16
package.json
16
package.json
@@ -24,20 +24,20 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atomico/rollup-plugin-sizes": "^1.1.4",
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/core": "^7.15.8",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@cypress/webpack-preprocessor": "^5.9.1",
|
||||
"@lerna/batch-packages": "^3.16.0",
|
||||
"@lerna/filter-packages": "^4.0.0",
|
||||
"@lerna/project": "^4.0.0",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^20.0.0",
|
||||
"@rollup/plugin-commonjs": "^21.0.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.32.0",
|
||||
"@typescript-eslint/parser": "^4.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"cypress": "^8.5.0",
|
||||
"eslint": "^7.32.0",
|
||||
@@ -45,15 +45,15 @@
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-html": "^6.2.0",
|
||||
"eslint-plugin-import": "^2.24.0",
|
||||
"eslint-plugin-vue": "^7.18.0",
|
||||
"eslint-plugin-vue": "^7.19.1",
|
||||
"lerna": "^4.0.0",
|
||||
"minimist": "^1.2.5",
|
||||
"rollup": "^2.57.0",
|
||||
"rollup": "^2.58.0",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"rollup-plugin-sourcemaps": "^0.6.3",
|
||||
"rollup-plugin-typescript2": "^0.30.0",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "^4.4.3",
|
||||
"webpack": "^5.55.1"
|
||||
"webpack": "^5.58.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,50 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.122](https://github.com/ueberdosis/tiptap/compare/@tiptap/core@2.0.0-beta.121...@tiptap/core@2.0.0-beta.122) (2021-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow to re-apply current mark for input rules and paste rules, fix [#2003](https://github.com/ueberdosis/tiptap/issues/2003) ([3958bf1](https://github.com/ueberdosis/tiptap/commit/3958bf1c226621142e9216c8eda91a70b71a211a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.121](https://github.com/ueberdosis/tiptap/compare/@tiptap/core@2.0.0-beta.120...@tiptap/core@2.0.0-beta.121) (2021-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not apply paste rules for ProseMirror HTML ([9d281e9](https://github.com/ueberdosis/tiptap/commit/9d281e9e998a5cfa9ca1002ad428caf1cee59682))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.120](https://github.com/ueberdosis/tiptap/compare/@tiptap/core@2.0.0-beta.119...@tiptap/core@2.0.0-beta.120) (2021-10-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix mappes positions for paste rules, fix [#2002](https://github.com/ueberdosis/tiptap/issues/2002) ([770c4e1](https://github.com/ueberdosis/tiptap/commit/770c4e18e19504f4c44feb85b7d202fcc5201b12))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.119](https://github.com/ueberdosis/tiptap/compare/@tiptap/core@2.0.0-beta.118...@tiptap/core@2.0.0-beta.119) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.118](https://github.com/ueberdosis/tiptap/compare/@tiptap/core@2.0.0-beta.117...@tiptap/core@2.0.0-beta.118) (2021-10-03)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/core",
|
||||
"description": "headless rich text editor",
|
||||
"version": "2.0.0-beta.118",
|
||||
"version": "2.0.0-beta.122",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
@@ -25,21 +25,19 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/prosemirror-commands": "^1.0.4",
|
||||
"@types/prosemirror-inputrules": "^1.0.4",
|
||||
"@types/prosemirror-keymap": "^1.0.4",
|
||||
"@types/prosemirror-model": "^1.13.2",
|
||||
"@types/prosemirror-schema-list": "^1.0.3",
|
||||
"@types/prosemirror-state": "^1.2.7",
|
||||
"@types/prosemirror-transform": "^1.1.4",
|
||||
"@types/prosemirror-view": "^1.19.1",
|
||||
"prosemirror-commands": "^1.1.10",
|
||||
"prosemirror-inputrules": "^1.1.3",
|
||||
"prosemirror-commands": "^1.1.11",
|
||||
"prosemirror-keymap": "^1.1.3",
|
||||
"prosemirror-model": "^1.14.3",
|
||||
"prosemirror-schema-list": "^1.1.6",
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-transform": "^1.3.3",
|
||||
"prosemirror-view": "^1.20.1"
|
||||
"prosemirror-view": "^1.20.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EditorState, Transaction } from 'prosemirror-state'
|
||||
import { Transaction } from 'prosemirror-state'
|
||||
import { Editor } from './Editor'
|
||||
import createChainableState from './helpers/createChainableState'
|
||||
import {
|
||||
SingleCommands,
|
||||
ChainedCommands,
|
||||
@@ -106,7 +107,10 @@ export default class CommandManager {
|
||||
tr,
|
||||
editor,
|
||||
view,
|
||||
state: this.chainableState(tr, state),
|
||||
state: createChainableState({
|
||||
state,
|
||||
transaction: tr,
|
||||
}),
|
||||
dispatch: shouldDispatch
|
||||
? () => undefined
|
||||
: undefined,
|
||||
@@ -124,36 +128,4 @@ export default class CommandManager {
|
||||
return props
|
||||
}
|
||||
|
||||
public chainableState(tr: Transaction, state: EditorState): EditorState {
|
||||
let { selection } = tr
|
||||
let { doc } = tr
|
||||
let { storedMarks } = tr
|
||||
|
||||
return {
|
||||
...state,
|
||||
schema: state.schema,
|
||||
plugins: state.plugins,
|
||||
apply: state.apply.bind(state),
|
||||
applyTransaction: state.applyTransaction.bind(state),
|
||||
reconfigure: state.reconfigure.bind(state),
|
||||
toJSON: state.toJSON.bind(state),
|
||||
get storedMarks() {
|
||||
return storedMarks
|
||||
},
|
||||
get selection() {
|
||||
return selection
|
||||
},
|
||||
get doc() {
|
||||
return doc
|
||||
},
|
||||
get tr() {
|
||||
selection = tr.selection
|
||||
doc = tr.doc
|
||||
storedMarks = tr.storedMarks
|
||||
|
||||
return tr
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import getText from './helpers/getText'
|
||||
import isNodeEmpty from './helpers/isNodeEmpty'
|
||||
import getTextSeralizersFromSchema from './helpers/getTextSeralizersFromSchema'
|
||||
import createStyleTag from './utilities/createStyleTag'
|
||||
import isFunction from './utilities/isFunction'
|
||||
import CommandManager from './CommandManager'
|
||||
import ExtensionManager from './ExtensionManager'
|
||||
import EventEmitter from './EventEmitter'
|
||||
@@ -184,7 +185,7 @@ export class Editor extends EventEmitter<EditorEvents> {
|
||||
* @param handlePlugins Control how to merge the plugin into the existing plugins.
|
||||
*/
|
||||
public registerPlugin(plugin: Plugin, handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[]): void {
|
||||
const plugins = typeof handlePlugins === 'function'
|
||||
const plugins = isFunction(handlePlugins)
|
||||
? handlePlugins(plugin, this.state.plugins)
|
||||
: [...this.state.plugins, plugin]
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Plugin, Transaction } from 'prosemirror-state'
|
||||
import { InputRule } from 'prosemirror-inputrules'
|
||||
import { InputRule } from './InputRule'
|
||||
import { PasteRule } from './PasteRule'
|
||||
import { Editor } from './Editor'
|
||||
import { Node } from './Node'
|
||||
import { Mark } from './Mark'
|
||||
@@ -81,7 +82,7 @@ declare module '@tiptap/core' {
|
||||
options: Options,
|
||||
editor: Editor,
|
||||
parent: ParentConfig<ExtensionConfig<Options>>['addPasteRules'],
|
||||
}) => Plugin[],
|
||||
}) => PasteRule[],
|
||||
|
||||
/**
|
||||
* ProseMirror plugins
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import { Schema, Node as ProsemirrorNode } from 'prosemirror-model'
|
||||
import { inputRules as inputRulesPlugin } from 'prosemirror-inputrules'
|
||||
import { inputRulesPlugin } from './InputRule'
|
||||
import { pasteRulesPlugin } from './PasteRule'
|
||||
import { EditorView, Decoration } from 'prosemirror-view'
|
||||
import { Plugin } from 'prosemirror-state'
|
||||
import { Editor } from './Editor'
|
||||
@@ -210,7 +211,12 @@ export default class ExtensionManager {
|
||||
// so it feels more natural to run plugins at the end of an array first.
|
||||
// That’s why we have to reverse the `extensions` array and sort again
|
||||
// based on the `priority` option.
|
||||
return ExtensionManager.sort([...this.extensions].reverse())
|
||||
const extensions = ExtensionManager.sort([...this.extensions].reverse())
|
||||
|
||||
const inputRules: any[] = []
|
||||
const pasteRules: any[] = []
|
||||
|
||||
const allPlugins = extensions
|
||||
.map(extension => {
|
||||
const context = {
|
||||
name: extension.name,
|
||||
@@ -248,12 +254,7 @@ export default class ExtensionManager {
|
||||
)
|
||||
|
||||
if (this.editor.options.enableInputRules && addInputRules) {
|
||||
const inputRules = addInputRules()
|
||||
const inputRulePlugins = inputRules.length
|
||||
? [inputRulesPlugin({ rules: inputRules })]
|
||||
: []
|
||||
|
||||
plugins.push(...inputRulePlugins)
|
||||
inputRules.push(...addInputRules())
|
||||
}
|
||||
|
||||
const addPasteRules = getExtensionField<AnyConfig['addPasteRules']>(
|
||||
@@ -263,9 +264,7 @@ export default class ExtensionManager {
|
||||
)
|
||||
|
||||
if (this.editor.options.enablePasteRules && addPasteRules) {
|
||||
const pasteRulePlugins = addPasteRules()
|
||||
|
||||
plugins.push(...pasteRulePlugins)
|
||||
pasteRules.push(...addPasteRules())
|
||||
}
|
||||
|
||||
const addProseMirrorPlugins = getExtensionField<AnyConfig['addProseMirrorPlugins']>(
|
||||
@@ -283,6 +282,12 @@ export default class ExtensionManager {
|
||||
return plugins
|
||||
})
|
||||
.flat()
|
||||
|
||||
return [
|
||||
inputRulesPlugin(inputRules),
|
||||
pasteRulesPlugin(pasteRules),
|
||||
...allPlugins,
|
||||
]
|
||||
}
|
||||
|
||||
get attributes() {
|
||||
|
||||
245
packages/core/src/InputRule.ts
Normal file
245
packages/core/src/InputRule.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { EditorState, Plugin, TextSelection } from 'prosemirror-state'
|
||||
import createChainableState from './helpers/createChainableState'
|
||||
import isRegExp from './utilities/isRegExp'
|
||||
import { Range, ExtendedRegExpMatchArray } from './types'
|
||||
|
||||
export type InputRuleMatch = {
|
||||
index: number,
|
||||
text: string,
|
||||
replaceWith?: string,
|
||||
match?: RegExpMatchArray,
|
||||
data?: Record<string, any>,
|
||||
}
|
||||
|
||||
export type InputRuleFinder =
|
||||
| RegExp
|
||||
| ((text: string) => InputRuleMatch | null)
|
||||
|
||||
export class InputRule {
|
||||
find: InputRuleFinder
|
||||
|
||||
handler: (props: {
|
||||
state: EditorState,
|
||||
range: Range,
|
||||
match: ExtendedRegExpMatchArray,
|
||||
}) => void
|
||||
|
||||
constructor(config: {
|
||||
find: InputRuleFinder,
|
||||
handler: (props: {
|
||||
state: EditorState,
|
||||
range: Range,
|
||||
match: ExtendedRegExpMatchArray,
|
||||
}) => void,
|
||||
}) {
|
||||
this.find = config.find
|
||||
this.handler = config.handler
|
||||
}
|
||||
}
|
||||
|
||||
const inputRuleMatcherHandler = (text: string, find: InputRuleFinder): ExtendedRegExpMatchArray | null => {
|
||||
if (isRegExp(find)) {
|
||||
return find.exec(text)
|
||||
}
|
||||
|
||||
const inputRuleMatch = find(text)
|
||||
|
||||
if (!inputRuleMatch) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result: ExtendedRegExpMatchArray = []
|
||||
|
||||
result.push(inputRuleMatch.text)
|
||||
result.index = inputRuleMatch.index
|
||||
result.input = text
|
||||
result.data = inputRuleMatch.data
|
||||
|
||||
if (inputRuleMatch.replaceWith) {
|
||||
if (!inputRuleMatch.text.includes(inputRuleMatch.replaceWith)) {
|
||||
console.warn('[tiptap warn]: "inputRuleMatch.replaceWith" must be part of "inputRuleMatch.text".')
|
||||
}
|
||||
|
||||
result.push(inputRuleMatch.replaceWith)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function run(config: {
|
||||
view: EditorView,
|
||||
from: number,
|
||||
to: number,
|
||||
text: string,
|
||||
rules: InputRule[],
|
||||
plugin: Plugin,
|
||||
}): any {
|
||||
const {
|
||||
view,
|
||||
from,
|
||||
to,
|
||||
text,
|
||||
rules,
|
||||
plugin,
|
||||
} = config
|
||||
|
||||
if (view.composing) {
|
||||
return false
|
||||
}
|
||||
|
||||
const $from = view.state.doc.resolve(from)
|
||||
|
||||
if (
|
||||
// check for code node
|
||||
$from.parent.type.spec.code
|
||||
// check for code mark
|
||||
|| !!($from.nodeBefore || $from.nodeAfter)?.marks.find(mark => mark.type.spec.code)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
let matched = false
|
||||
const maxMatch = 500
|
||||
const textBefore = $from.parent.textBetween(
|
||||
Math.max(0, $from.parentOffset - maxMatch),
|
||||
$from.parentOffset,
|
||||
undefined,
|
||||
'\ufffc',
|
||||
) + text
|
||||
|
||||
rules.forEach(rule => {
|
||||
if (matched) {
|
||||
return
|
||||
}
|
||||
|
||||
const match = inputRuleMatcherHandler(textBefore, rule.find)
|
||||
|
||||
if (!match) {
|
||||
return
|
||||
}
|
||||
|
||||
const tr = view.state.tr
|
||||
const state = createChainableState({
|
||||
state: view.state,
|
||||
transaction: tr,
|
||||
})
|
||||
const range = {
|
||||
from: from - (match[0].length - text.length),
|
||||
to,
|
||||
}
|
||||
|
||||
rule.handler({
|
||||
state,
|
||||
range,
|
||||
match,
|
||||
})
|
||||
|
||||
// stop if there are no changes
|
||||
if (!tr.steps.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// store transform as meta data
|
||||
// so we can undo input rules within the `undoInputRules` command
|
||||
tr.setMeta(plugin, {
|
||||
transform: tr,
|
||||
from,
|
||||
to,
|
||||
text,
|
||||
})
|
||||
|
||||
view.dispatch(tr)
|
||||
matched = true
|
||||
})
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an input rules plugin. When enabled, it will cause text
|
||||
* input that matches any of the given rules to trigger the rule’s
|
||||
* action.
|
||||
*/
|
||||
export function inputRulesPlugin(rules: InputRule[]): Plugin {
|
||||
const plugin = new Plugin({
|
||||
state: {
|
||||
init() {
|
||||
return null
|
||||
},
|
||||
apply(tr, prev) {
|
||||
const stored = tr.getMeta(this)
|
||||
|
||||
if (stored) {
|
||||
return stored
|
||||
}
|
||||
|
||||
return tr.selectionSet || tr.docChanged
|
||||
? null
|
||||
: prev
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
handleTextInput(view, from, to, text) {
|
||||
return run({
|
||||
view,
|
||||
from,
|
||||
to,
|
||||
text,
|
||||
rules,
|
||||
plugin,
|
||||
})
|
||||
},
|
||||
|
||||
handleDOMEvents: {
|
||||
compositionend: view => {
|
||||
setTimeout(() => {
|
||||
const { $cursor } = view.state.selection as TextSelection
|
||||
|
||||
if ($cursor) {
|
||||
run({
|
||||
view,
|
||||
from: $cursor.pos,
|
||||
to: $cursor.pos,
|
||||
text: '',
|
||||
rules,
|
||||
plugin,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
|
||||
// add support for input rules to trigger on enter
|
||||
// this is useful for example for code blocks
|
||||
handleKeyDown(view, event) {
|
||||
if (event.key !== 'Enter') {
|
||||
return false
|
||||
}
|
||||
|
||||
const { $cursor } = view.state.selection as TextSelection
|
||||
|
||||
if ($cursor) {
|
||||
return run({
|
||||
view,
|
||||
from: $cursor.pos,
|
||||
to: $cursor.pos,
|
||||
text: '\n',
|
||||
rules,
|
||||
plugin,
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
isInputRules: true,
|
||||
}) as Plugin
|
||||
|
||||
return plugin
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
MarkType,
|
||||
} from 'prosemirror-model'
|
||||
import { Plugin, Transaction } from 'prosemirror-state'
|
||||
import { InputRule } from 'prosemirror-inputrules'
|
||||
import { InputRule } from './InputRule'
|
||||
import { PasteRule } from './PasteRule'
|
||||
import mergeDeep from './utilities/mergeDeep'
|
||||
import {
|
||||
Extensions,
|
||||
@@ -91,7 +92,7 @@ declare module '@tiptap/core' {
|
||||
editor: Editor,
|
||||
type: MarkType,
|
||||
parent: ParentConfig<MarkConfig<Options>>['addPasteRules'],
|
||||
}) => Plugin[],
|
||||
}) => PasteRule[],
|
||||
|
||||
/**
|
||||
* ProseMirror plugins
|
||||
@@ -281,6 +282,15 @@ declare module '@tiptap/core' {
|
||||
parent: ParentConfig<MarkConfig<Options>>['spanning'],
|
||||
}) => MarkSpec['spanning']),
|
||||
|
||||
/**
|
||||
* Code
|
||||
*/
|
||||
code?: boolean | ((this: {
|
||||
name: string,
|
||||
options: Options,
|
||||
parent: ParentConfig<MarkConfig<Options>>['code'],
|
||||
}) => boolean),
|
||||
|
||||
/**
|
||||
* Parse HTML
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
NodeType,
|
||||
} from 'prosemirror-model'
|
||||
import { Plugin, Transaction } from 'prosemirror-state'
|
||||
import { InputRule } from 'prosemirror-inputrules'
|
||||
import { InputRule } from './InputRule'
|
||||
import { PasteRule } from './PasteRule'
|
||||
import mergeDeep from './utilities/mergeDeep'
|
||||
import {
|
||||
Extensions,
|
||||
@@ -91,7 +92,7 @@ declare module '@tiptap/core' {
|
||||
editor: Editor,
|
||||
type: NodeType,
|
||||
parent: ParentConfig<NodeConfig<Options>>['addPasteRules'],
|
||||
}) => Plugin[],
|
||||
}) => PasteRule[],
|
||||
|
||||
/**
|
||||
* ProseMirror plugins
|
||||
|
||||
188
packages/core/src/PasteRule.ts
Normal file
188
packages/core/src/PasteRule.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { EditorState, Plugin } from 'prosemirror-state'
|
||||
import createChainableState from './helpers/createChainableState'
|
||||
import isRegExp from './utilities/isRegExp'
|
||||
import { Range, ExtendedRegExpMatchArray } from './types'
|
||||
|
||||
export type PasteRuleMatch = {
|
||||
index: number,
|
||||
text: string,
|
||||
replaceWith?: string,
|
||||
match?: RegExpMatchArray,
|
||||
data?: Record<string, any>,
|
||||
}
|
||||
|
||||
export type PasteRuleFinder =
|
||||
| RegExp
|
||||
| ((text: string) => PasteRuleMatch[] | null | undefined)
|
||||
|
||||
export class PasteRule {
|
||||
find: PasteRuleFinder
|
||||
|
||||
handler: (props: {
|
||||
state: EditorState,
|
||||
range: Range,
|
||||
match: ExtendedRegExpMatchArray,
|
||||
}) => void
|
||||
|
||||
constructor(config: {
|
||||
find: PasteRuleFinder,
|
||||
handler: (props: {
|
||||
state: EditorState,
|
||||
range: Range,
|
||||
match: ExtendedRegExpMatchArray,
|
||||
}) => void,
|
||||
}) {
|
||||
this.find = config.find
|
||||
this.handler = config.handler
|
||||
}
|
||||
}
|
||||
|
||||
const pasteRuleMatcherHandler = (text: string, find: PasteRuleFinder): ExtendedRegExpMatchArray[] => {
|
||||
if (isRegExp(find)) {
|
||||
return [...text.matchAll(find)]
|
||||
}
|
||||
|
||||
const matches = find(text)
|
||||
|
||||
if (!matches) {
|
||||
return []
|
||||
}
|
||||
|
||||
return matches.map(pasteRuleMatch => {
|
||||
const result: ExtendedRegExpMatchArray = []
|
||||
|
||||
result.push(pasteRuleMatch.text)
|
||||
result.index = pasteRuleMatch.index
|
||||
result.input = text
|
||||
result.data = pasteRuleMatch.data
|
||||
|
||||
if (pasteRuleMatch.replaceWith) {
|
||||
if (!pasteRuleMatch.text.includes(pasteRuleMatch.replaceWith)) {
|
||||
console.warn('[tiptap warn]: "pasteRuleMatch.replaceWith" must be part of "pasteRuleMatch.text".')
|
||||
}
|
||||
|
||||
result.push(pasteRuleMatch.replaceWith)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
function run(config: {
|
||||
state: EditorState,
|
||||
from: number,
|
||||
to: number,
|
||||
rules: PasteRule[],
|
||||
plugin: Plugin,
|
||||
}): any {
|
||||
const {
|
||||
state,
|
||||
from,
|
||||
to,
|
||||
rules,
|
||||
} = config
|
||||
|
||||
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||
if (!node.isTextblock || node.type.spec.code) {
|
||||
return
|
||||
}
|
||||
|
||||
const resolvedFrom = Math.max(from, pos)
|
||||
const resolvedTo = Math.min(to, pos + node.content.size)
|
||||
const textToMatch = node.textBetween(
|
||||
resolvedFrom - pos,
|
||||
resolvedTo - pos,
|
||||
undefined,
|
||||
'\ufffc',
|
||||
)
|
||||
|
||||
rules.forEach(rule => {
|
||||
const matches = pasteRuleMatcherHandler(textToMatch, rule.find)
|
||||
|
||||
matches.forEach(match => {
|
||||
if (match.index === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const start = resolvedFrom + match.index + 1
|
||||
const end = start + match[0].length
|
||||
const range = {
|
||||
from: state.tr.mapping.map(start),
|
||||
to: state.tr.mapping.map(end),
|
||||
}
|
||||
|
||||
rule.handler({
|
||||
state,
|
||||
range,
|
||||
match,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an paste rules plugin. When enabled, it will cause pasted
|
||||
* text that matches any of the given rules to trigger the rule’s
|
||||
* action.
|
||||
*/
|
||||
export function pasteRulesPlugin(rules: PasteRule[]): Plugin {
|
||||
let isProseMirrorHTML = false
|
||||
|
||||
const plugin = new Plugin({
|
||||
props: {
|
||||
handlePaste: (view, event) => {
|
||||
const html = event.clipboardData?.getData('text/html')
|
||||
|
||||
isProseMirrorHTML = !!html?.includes('data-pm-slice')
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
appendTransaction: (transactions, oldState, state) => {
|
||||
const transaction = transactions[0]
|
||||
|
||||
// stop if there is not a paste event
|
||||
if (!transaction.getMeta('paste') || isProseMirrorHTML) {
|
||||
return
|
||||
}
|
||||
|
||||
// stop if there is no changed range
|
||||
const { doc, before } = transaction
|
||||
const from = before.content.findDiffStart(doc.content)
|
||||
const to = before.content.findDiffEnd(doc.content)
|
||||
|
||||
if (!from || !to || from === to.b) {
|
||||
return
|
||||
}
|
||||
|
||||
// build a chainable state
|
||||
// so we can use a single transaction for all paste rules
|
||||
const tr = state.tr
|
||||
const chainableState = createChainableState({
|
||||
state,
|
||||
transaction: tr,
|
||||
})
|
||||
|
||||
run({
|
||||
state: chainableState,
|
||||
from: Math.max(from - 1, 0),
|
||||
to: to.b,
|
||||
rules,
|
||||
plugin,
|
||||
})
|
||||
|
||||
// stop if there are no changes
|
||||
if (!tr.steps.length) {
|
||||
return
|
||||
}
|
||||
|
||||
return tr
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
isPasteRules: true,
|
||||
})
|
||||
|
||||
return plugin
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { undoInputRule as originalUndoInputRule } from 'prosemirror-inputrules'
|
||||
import { RawCommands } from '../types'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
@@ -13,5 +12,34 @@ declare module '@tiptap/core' {
|
||||
}
|
||||
|
||||
export const undoInputRule: RawCommands['undoInputRule'] = () => ({ state, dispatch }) => {
|
||||
return originalUndoInputRule(state, dispatch)
|
||||
const plugins = state.plugins
|
||||
|
||||
for (let i = 0; i < plugins.length; i += 1) {
|
||||
const plugin = plugins[i]
|
||||
let undoable
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
if (plugin.spec.isInputRules && (undoable = plugin.getState(state))) {
|
||||
if (dispatch) {
|
||||
const tr = state.tr
|
||||
const toUndo = undoable.transform
|
||||
|
||||
for (let j = toUndo.steps.length - 1; j >= 0; j -= 1) {
|
||||
tr.step(toUndo.steps[j].invert(toUndo.docs[j]))
|
||||
}
|
||||
|
||||
if (undoable.text) {
|
||||
const marks = tr.doc.resolve(undoable.from).marks()
|
||||
tr.replaceWith(undoable.from, undoable.to, state.schema.text(undoable.text, marks))
|
||||
} else {
|
||||
tr.delete(undoable.from, undoable.to)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
37
packages/core/src/helpers/createChainableState.ts
Normal file
37
packages/core/src/helpers/createChainableState.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { EditorState, Transaction } from 'prosemirror-state'
|
||||
|
||||
export default function createChainableState(config: {
|
||||
transaction: Transaction,
|
||||
state: EditorState,
|
||||
}): EditorState {
|
||||
const { state, transaction } = config
|
||||
let { selection } = transaction
|
||||
let { doc } = transaction
|
||||
let { storedMarks } = transaction
|
||||
|
||||
return {
|
||||
...state,
|
||||
schema: state.schema,
|
||||
plugins: state.plugins,
|
||||
apply: state.apply.bind(state),
|
||||
applyTransaction: state.applyTransaction.bind(state),
|
||||
reconfigure: state.reconfigure.bind(state),
|
||||
toJSON: state.toJSON.bind(state),
|
||||
get storedMarks() {
|
||||
return storedMarks
|
||||
},
|
||||
get selection() {
|
||||
return selection
|
||||
},
|
||||
get doc() {
|
||||
return doc
|
||||
},
|
||||
get tr() {
|
||||
selection = transaction.selection
|
||||
doc = transaction.doc
|
||||
storedMarks = transaction.storedMarks
|
||||
|
||||
return transaction
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -108,10 +108,11 @@ export default function getSchemaByResolvedExtensions(extensions: Extensions): S
|
||||
|
||||
const schema: MarkSpec = cleanUpSchemaItem({
|
||||
...extraMarkFields,
|
||||
inclusive: callOrReturn(getExtensionField<NodeConfig['inclusive']>(extension, 'inclusive', context)),
|
||||
excludes: callOrReturn(getExtensionField<NodeConfig['excludes']>(extension, 'excludes', context)),
|
||||
group: callOrReturn(getExtensionField<NodeConfig['group']>(extension, 'group', context)),
|
||||
spanning: callOrReturn(getExtensionField<NodeConfig['spanning']>(extension, 'spanning', context)),
|
||||
inclusive: callOrReturn(getExtensionField<MarkConfig['inclusive']>(extension, 'inclusive', context)),
|
||||
excludes: callOrReturn(getExtensionField<MarkConfig['excludes']>(extension, 'excludes', context)),
|
||||
group: callOrReturn(getExtensionField<MarkConfig['group']>(extension, 'group', context)),
|
||||
spanning: callOrReturn(getExtensionField<MarkConfig['spanning']>(extension, 'spanning', context)),
|
||||
code: callOrReturn(getExtensionField<MarkConfig['code']>(extension, 'code', context)),
|
||||
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
|
||||
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
|
||||
})),
|
||||
|
||||
@@ -7,11 +7,17 @@ export * from './Node'
|
||||
export * from './Mark'
|
||||
export * from './NodeView'
|
||||
export * from './Tracker'
|
||||
export * from './InputRule'
|
||||
export * from './PasteRule'
|
||||
export * from './types'
|
||||
|
||||
export { default as nodeInputRule } from './inputRules/nodeInputRule'
|
||||
export { default as markInputRule } from './inputRules/markInputRule'
|
||||
export { default as textblockTypeInputRule } from './inputRules/textblockTypeInputRule'
|
||||
export { default as textInputRule } from './inputRules/textInputRule'
|
||||
export { default as wrappingInputRule } from './inputRules/wrappingInputRule'
|
||||
export { default as markPasteRule } from './pasteRules/markPasteRule'
|
||||
export { default as textPasteRule } from './pasteRules/textPasteRule'
|
||||
|
||||
export { default as callOrReturn } from './utilities/callOrReturn'
|
||||
export { default as mergeAttributes } from './utilities/mergeAttributes'
|
||||
|
||||
@@ -1,50 +1,69 @@
|
||||
import { InputRule } from 'prosemirror-inputrules'
|
||||
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||
import { MarkType } from 'prosemirror-model'
|
||||
import getMarksBetween from '../helpers/getMarksBetween'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
import { ExtendedRegExpMatchArray } from '../types'
|
||||
|
||||
export default function (regexp: RegExp, markType: MarkType, getAttributes?: Function): InputRule {
|
||||
return new InputRule(regexp, (state, match, start, end) => {
|
||||
const attributes = getAttributes instanceof Function
|
||||
? getAttributes(match)
|
||||
: getAttributes
|
||||
const { tr } = state
|
||||
const captureGroup = match[match.length - 1]
|
||||
const fullMatch = match[0]
|
||||
let markEnd = end
|
||||
/**
|
||||
* Build an input rule that adds a mark when the
|
||||
* matched text is typed into it.
|
||||
*/
|
||||
export default function markInputRule(config: {
|
||||
find: InputRuleFinder,
|
||||
type: MarkType,
|
||||
getAttributes?:
|
||||
| Record<string, any>
|
||||
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||
| false
|
||||
| null
|
||||
,
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
const attributes = callOrReturn(config.getAttributes, undefined, match)
|
||||
|
||||
if (captureGroup) {
|
||||
const startSpaces = fullMatch.search(/\S/)
|
||||
const textStart = start + fullMatch.indexOf(captureGroup)
|
||||
const textEnd = textStart + captureGroup.length
|
||||
|
||||
const excludedMarks = getMarksBetween(start, end, state)
|
||||
.filter(item => {
|
||||
// TODO: PR to add excluded to MarkType
|
||||
// @ts-ignore
|
||||
const { excluded } = item.mark.type
|
||||
return excluded.find((type: MarkType) => type.name === markType.name)
|
||||
})
|
||||
.filter(item => item.to > textStart)
|
||||
|
||||
if (excludedMarks.length) {
|
||||
return null
|
||||
if (attributes === false || attributes === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (textEnd < end) {
|
||||
tr.delete(textEnd, end)
|
||||
const { tr } = state
|
||||
const captureGroup = match[match.length - 1]
|
||||
const fullMatch = match[0]
|
||||
let markEnd = range.to
|
||||
|
||||
if (captureGroup) {
|
||||
const startSpaces = fullMatch.search(/\S/)
|
||||
const textStart = range.from + fullMatch.indexOf(captureGroup)
|
||||
const textEnd = textStart + captureGroup.length
|
||||
|
||||
const excludedMarks = getMarksBetween(range.from, range.to, state)
|
||||
.filter(item => {
|
||||
// @ts-ignore
|
||||
const excluded = item.mark.type.excluded as MarkType[]
|
||||
|
||||
return excluded.find(type => type === config.type && type !== item.mark.type)
|
||||
})
|
||||
.filter(item => item.to > textStart)
|
||||
|
||||
if (excludedMarks.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (textEnd < range.to) {
|
||||
tr.delete(textEnd, range.to)
|
||||
}
|
||||
|
||||
if (textStart > range.from) {
|
||||
tr.delete(range.from + startSpaces, textStart)
|
||||
}
|
||||
|
||||
markEnd = range.from + startSpaces + captureGroup.length
|
||||
|
||||
tr.addMark(range.from + startSpaces, markEnd, config.type.create(attributes || {}))
|
||||
|
||||
tr.removeStoredMark(config.type)
|
||||
}
|
||||
|
||||
if (textStart > start) {
|
||||
tr.delete(start + startSpaces, textStart)
|
||||
}
|
||||
|
||||
markEnd = start + startSpaces + captureGroup.length
|
||||
|
||||
tr.addMark(start + startSpaces, markEnd, markType.create(attributes))
|
||||
|
||||
tr.removeStoredMark(markType)
|
||||
}
|
||||
|
||||
return tr
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,32 +1,49 @@
|
||||
import { InputRule } from 'prosemirror-inputrules'
|
||||
import { NodeType } from 'prosemirror-model'
|
||||
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||
import { ExtendedRegExpMatchArray } from '../types'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
|
||||
export default function (regexp: RegExp, type: NodeType, getAttributes?: (match: any) => any): InputRule {
|
||||
return new InputRule(regexp, (state, match, start, end) => {
|
||||
const attributes = getAttributes instanceof Function
|
||||
? getAttributes(match)
|
||||
: getAttributes
|
||||
const { tr } = state
|
||||
/**
|
||||
* Build an input rule that adds a node when the
|
||||
* matched text is typed into it.
|
||||
*/
|
||||
export default function nodeInputRule(config: {
|
||||
find: InputRuleFinder,
|
||||
type: NodeType,
|
||||
getAttributes?:
|
||||
| Record<string, any>
|
||||
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||
| false
|
||||
| null
|
||||
,
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
|
||||
const { tr } = state
|
||||
const start = range.from
|
||||
let end = range.to
|
||||
|
||||
if (match[1]) {
|
||||
const offset = match[0].lastIndexOf(match[1])
|
||||
let matchStart = start + offset
|
||||
if (matchStart > end) {
|
||||
matchStart = end
|
||||
} else {
|
||||
end = matchStart + match[1].length
|
||||
if (match[1]) {
|
||||
const offset = match[0].lastIndexOf(match[1])
|
||||
let matchStart = start + offset
|
||||
|
||||
if (matchStart > end) {
|
||||
matchStart = end
|
||||
} else {
|
||||
end = matchStart + match[1].length
|
||||
}
|
||||
|
||||
// insert last typed character
|
||||
const lastChar = match[0][match[0].length - 1]
|
||||
tr.insertText(lastChar, start + match[0].length - 1)
|
||||
|
||||
// insert node from input rule
|
||||
tr.replaceWith(matchStart, end, config.type.create(attributes))
|
||||
} else if (match[0]) {
|
||||
tr.replaceWith(start, end, config.type.create(attributes))
|
||||
}
|
||||
|
||||
// insert last typed character
|
||||
const lastChar = match[0][match[0].length - 1]
|
||||
tr.insertText(lastChar, start + match[0].length - 1)
|
||||
|
||||
// insert node from input rule
|
||||
tr.replaceWith(matchStart, end, type.create(attributes))
|
||||
} else if (match[0]) {
|
||||
tr.replaceWith(start, end, type.create(attributes))
|
||||
}
|
||||
|
||||
return tr
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
35
packages/core/src/inputRules/textInputRule.ts
Normal file
35
packages/core/src/inputRules/textInputRule.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||
|
||||
/**
|
||||
* Build an input rule that replaces text when the
|
||||
* matched text is typed into it.
|
||||
*/
|
||||
export default function textInputRule(config: {
|
||||
find: InputRuleFinder,
|
||||
replace: string,
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
let insert = config.replace
|
||||
let start = range.from
|
||||
const end = range.to
|
||||
|
||||
if (match[1]) {
|
||||
const offset = match[0].lastIndexOf(match[1])
|
||||
|
||||
insert += match[0].slice(offset + match[1].length)
|
||||
start += offset
|
||||
|
||||
const cutOff = start - end
|
||||
|
||||
if (cutOff > 0) {
|
||||
insert = match[0].slice(offset - cutOff, offset) + insert
|
||||
start = end
|
||||
}
|
||||
}
|
||||
|
||||
state.tr.insertText(insert, start, end)
|
||||
},
|
||||
})
|
||||
}
|
||||
37
packages/core/src/inputRules/textblockTypeInputRule.ts
Normal file
37
packages/core/src/inputRules/textblockTypeInputRule.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||
import { NodeType } from 'prosemirror-model'
|
||||
import { ExtendedRegExpMatchArray } from '../types'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
|
||||
/**
|
||||
* Build an input rule that changes the type of a textblock when the
|
||||
* matched text is typed into it. When using a regular expresion you’ll
|
||||
* probably want the regexp to start with `^`, so that the pattern can
|
||||
* only occur at the start of a textblock.
|
||||
*/
|
||||
export default function textblockTypeInputRule(config: {
|
||||
find: InputRuleFinder,
|
||||
type: NodeType,
|
||||
getAttributes?:
|
||||
| Record<string, any>
|
||||
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||
| false
|
||||
| null
|
||||
,
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
const $start = state.doc.resolve(range.from)
|
||||
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
|
||||
|
||||
if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), config.type)) {
|
||||
return null
|
||||
}
|
||||
|
||||
state.tr
|
||||
.delete(range.from, range.to)
|
||||
.setBlockType(range.from, range.from, config.type, attributes)
|
||||
},
|
||||
})
|
||||
}
|
||||
59
packages/core/src/inputRules/wrappingInputRule.ts
Normal file
59
packages/core/src/inputRules/wrappingInputRule.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||
import { NodeType, Node as ProseMirrorNode } from 'prosemirror-model'
|
||||
import { findWrapping, canJoin } from 'prosemirror-transform'
|
||||
import { ExtendedRegExpMatchArray } from '../types'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
|
||||
/**
|
||||
* Build an input rule for automatically wrapping a textblock when a
|
||||
* given string is typed. When using a regular expresion you’ll
|
||||
* probably want the regexp to start with `^`, so that the pattern can
|
||||
* only occur at the start of a textblock.
|
||||
*
|
||||
* `type` is the type of node to wrap in.
|
||||
*
|
||||
* By default, if there’s a node with the same type above the newly
|
||||
* wrapped node, the rule will try to join those
|
||||
* two nodes. You can pass a join predicate, which takes a regular
|
||||
* expression match and the node before the wrapped node, and can
|
||||
* return a boolean to indicate whether a join should happen.
|
||||
*/
|
||||
export default function wrappingInputRule(config: {
|
||||
find: InputRuleFinder,
|
||||
type: NodeType,
|
||||
getAttributes?:
|
||||
| Record<string, any>
|
||||
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||
| false
|
||||
| null
|
||||
,
|
||||
joinPredicate?: (match: ExtendedRegExpMatchArray, node: ProseMirrorNode) => boolean,
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
|
||||
const tr = state.tr.delete(range.from, range.to)
|
||||
const $start = tr.doc.resolve(range.from)
|
||||
const blockRange = $start.blockRange()
|
||||
const wrapping = blockRange && findWrapping(blockRange, config.type, attributes)
|
||||
|
||||
if (!wrapping) {
|
||||
return null
|
||||
}
|
||||
|
||||
tr.wrap(blockRange, wrapping)
|
||||
|
||||
const before = tr.doc.resolve(range.from - 1).nodeBefore
|
||||
|
||||
if (
|
||||
before
|
||||
&& before.type === config.type
|
||||
&& canJoin(tr.doc, range.from - 1)
|
||||
&& (!config.joinPredicate || config.joinPredicate(match, before))
|
||||
) {
|
||||
tr.join(range.from - 1)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,76 +1,69 @@
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
import { Slice, Fragment, MarkType } from 'prosemirror-model'
|
||||
import { PasteRule, PasteRuleFinder } from '../PasteRule'
|
||||
import { MarkType } from 'prosemirror-model'
|
||||
import getMarksBetween from '../helpers/getMarksBetween'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
import { ExtendedRegExpMatchArray } from '../types'
|
||||
|
||||
export default function (
|
||||
regexp: RegExp,
|
||||
/**
|
||||
* Build an paste rule that adds a mark when the
|
||||
* matched text is pasted into it.
|
||||
*/
|
||||
export default function markPasteRule(config: {
|
||||
find: PasteRuleFinder,
|
||||
type: MarkType,
|
||||
getAttributes?:
|
||||
| Record<string, any>
|
||||
| ((match: RegExpExecArray) => Record<string, any>)
|
||||
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||
| false
|
||||
| null
|
||||
,
|
||||
): Plugin {
|
||||
const handler = (fragment: Fragment, parent?: any) => {
|
||||
const nodes: any[] = []
|
||||
}) {
|
||||
return new PasteRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
const attributes = callOrReturn(config.getAttributes, undefined, match)
|
||||
|
||||
fragment.forEach(child => {
|
||||
if (child.isText && child.text) {
|
||||
const { text } = child
|
||||
let pos = 0
|
||||
let match
|
||||
|
||||
// eslint-disable-next-line
|
||||
while ((match = regexp.exec(text)) !== null) {
|
||||
const outerMatch = Math.max(match.length - 2, 0)
|
||||
const innerMatch = Math.max(match.length - 1, 0)
|
||||
|
||||
if (parent?.type.allowsMarkType(type)) {
|
||||
const start = match.index
|
||||
const matchStart = start + match[0].indexOf(match[outerMatch])
|
||||
const matchEnd = matchStart + match[outerMatch].length
|
||||
const textStart = matchStart + match[outerMatch].lastIndexOf(match[innerMatch])
|
||||
const textEnd = textStart + match[innerMatch].length
|
||||
const attrs = getAttributes instanceof Function
|
||||
? getAttributes(match)
|
||||
: getAttributes
|
||||
|
||||
if (!attrs && attrs !== undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
// adding text before markdown to nodes
|
||||
if (matchStart > 0) {
|
||||
nodes.push(child.cut(pos, matchStart))
|
||||
}
|
||||
|
||||
// adding the markdown part to nodes
|
||||
nodes.push(child
|
||||
.cut(textStart, textEnd)
|
||||
.mark(type.create(attrs).addToSet(child.marks)))
|
||||
|
||||
pos = matchEnd
|
||||
}
|
||||
}
|
||||
|
||||
// adding rest of text to nodes
|
||||
if (pos < text.length) {
|
||||
nodes.push(child.cut(pos))
|
||||
}
|
||||
} else {
|
||||
nodes.push(child.copy(handler(child.content, child)))
|
||||
if (attributes === false || attributes === null) {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
return Fragment.fromArray(nodes)
|
||||
}
|
||||
const { tr } = state
|
||||
const captureGroup = match[match.length - 1]
|
||||
const fullMatch = match[0]
|
||||
let markEnd = range.to
|
||||
|
||||
return new Plugin({
|
||||
key: new PluginKey('markPasteRule'),
|
||||
props: {
|
||||
transformPasted: slice => {
|
||||
return new Slice(handler(slice.content), slice.openStart, slice.openEnd)
|
||||
},
|
||||
if (captureGroup) {
|
||||
const startSpaces = fullMatch.search(/\S/)
|
||||
const textStart = range.from + fullMatch.indexOf(captureGroup)
|
||||
const textEnd = textStart + captureGroup.length
|
||||
|
||||
const excludedMarks = getMarksBetween(range.from, range.to, state)
|
||||
.filter(item => {
|
||||
// @ts-ignore
|
||||
const excluded = item.mark.type.excluded as MarkType[]
|
||||
|
||||
return excluded.find(type => type === config.type && type !== item.mark.type)
|
||||
})
|
||||
.filter(item => item.to > textStart)
|
||||
|
||||
if (excludedMarks.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (textEnd < range.to) {
|
||||
tr.delete(textEnd, range.to)
|
||||
}
|
||||
|
||||
if (textStart > range.from) {
|
||||
tr.delete(range.from + startSpaces, textStart)
|
||||
}
|
||||
|
||||
markEnd = range.from + startSpaces + captureGroup.length
|
||||
|
||||
tr.addMark(range.from + startSpaces, markEnd, config.type.create(attributes || {}))
|
||||
|
||||
tr.removeStoredMark(config.type)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
35
packages/core/src/pasteRules/textPasteRule.ts
Normal file
35
packages/core/src/pasteRules/textPasteRule.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { PasteRule, PasteRuleFinder } from '../PasteRule'
|
||||
|
||||
/**
|
||||
* Build an paste rule that replaces text when the
|
||||
* matched text is pasted into it.
|
||||
*/
|
||||
export default function textPasteRule(config: {
|
||||
find: PasteRuleFinder,
|
||||
replace: string,
|
||||
}) {
|
||||
return new PasteRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
let insert = config.replace
|
||||
let start = range.from
|
||||
const end = range.to
|
||||
|
||||
if (match[1]) {
|
||||
const offset = match[0].lastIndexOf(match[1])
|
||||
|
||||
insert += match[0].slice(offset + match[1].length)
|
||||
start += offset
|
||||
|
||||
const cutOff = start - end
|
||||
|
||||
if (cutOff > 0) {
|
||||
insert = match[0].slice(offset - cutOff, offset) + insert
|
||||
start = end
|
||||
}
|
||||
}
|
||||
|
||||
state.tr.insertText(insert, start, end)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -229,3 +229,7 @@ export type TextSerializer = (props: {
|
||||
parent: ProseMirrorNode,
|
||||
index: number,
|
||||
}) => string
|
||||
|
||||
export type ExtendedRegExpMatchArray = RegExpMatchArray & {
|
||||
data?: Record<string, any>,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MaybeReturnType } from '../types'
|
||||
import isFunction from './isFunction'
|
||||
|
||||
/**
|
||||
* Optionally calls `value` as a function.
|
||||
@@ -8,7 +9,7 @@ import { MaybeReturnType } from '../types'
|
||||
* @param props Optional props to pass to function.
|
||||
*/
|
||||
export default function callOrReturn<T>(value: T, context: any = undefined, ...props: any[]): MaybeReturnType<T> {
|
||||
if (typeof value === 'function') {
|
||||
if (isFunction(value)) {
|
||||
if (context) {
|
||||
return value.bind(context)(...props)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default function isClass(item: any): boolean {
|
||||
if (item.constructor?.toString().substring(0, 5) !== 'class') {
|
||||
export default function isClass(value: any): boolean {
|
||||
if (value.constructor?.toString().substring(0, 5) !== 'class') {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function isEmptyObject(object = {}): boolean {
|
||||
return Object.keys(object).length === 0 && object.constructor === Object
|
||||
export default function isEmptyObject(value = {}): boolean {
|
||||
return Object.keys(value).length === 0 && value.constructor === Object
|
||||
}
|
||||
|
||||
3
packages/core/src/utilities/isFunction.ts
Normal file
3
packages/core/src/utilities/isFunction.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function isObject(value: any): value is Function {
|
||||
return typeof value === 'function'
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import isClass from './isClass'
|
||||
|
||||
export default function isObject(item: any): boolean {
|
||||
export default function isObject(value: any): boolean {
|
||||
return (
|
||||
item
|
||||
&& typeof item === 'object'
|
||||
&& !Array.isArray(item)
|
||||
&& !isClass(item)
|
||||
value
|
||||
&& typeof value === 'object'
|
||||
&& !Array.isArray(value)
|
||||
&& !isClass(value)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// see: https://github.com/mesqueeb/is-what/blob/88d6e4ca92fb2baab6003c54e02eedf4e729e5ab/src/index.ts
|
||||
|
||||
function getType(payload: any): string {
|
||||
return Object.prototype.toString.call(payload).slice(8, -1)
|
||||
function getType(value: any): string {
|
||||
return Object.prototype.toString.call(value).slice(8, -1)
|
||||
}
|
||||
|
||||
export default function isPlainObject(payload: any): payload is Record<string, any> {
|
||||
if (getType(payload) !== 'Object') return false
|
||||
return payload.constructor === Object && Object.getPrototypeOf(payload) === Object.prototype
|
||||
export default function isPlainObject(value: any): value is Record<string, any> {
|
||||
if (getType(value) !== 'Object') return false
|
||||
return value.constructor === Object && Object.getPrototypeOf(value) === Object.prototype
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function isRegExp(value: any): boolean {
|
||||
export default function isRegExp(value: any): value is RegExp {
|
||||
return Object.prototype.toString.call(value) === '[object RegExp]'
|
||||
}
|
||||
|
||||
3
packages/core/src/utilities/isString.ts
Normal file
3
packages/core/src/utilities/isString.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function isString(value: any): value is string {
|
||||
return typeof value === 'string'
|
||||
}
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.17](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-blockquote@2.0.0-beta.16...@tiptap/extension-blockquote@2.0.0-beta.17) (2021-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix regex modifier for input rules and paste rules, fix [#2003](https://github.com/ueberdosis/tiptap/issues/2003) ([65eddf0](https://github.com/ueberdosis/tiptap/commit/65eddf0e789c620a53d9bec9509b515211fb9b3f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.16](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-blockquote@2.0.0-beta.15...@tiptap/extension-blockquote@2.0.0-beta.16) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.15](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-blockquote@2.0.0-beta.14...@tiptap/extension-blockquote@2.0.0-beta.15) (2021-07-26)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-blockquote
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-blockquote",
|
||||
"description": "blockquote extension for tiptap",
|
||||
"version": "2.0.0-beta.15",
|
||||
"version": "2.0.0-beta.17",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
@@ -23,9 +23,6 @@
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.0.0-beta.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prosemirror-inputrules": "^1.1.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ueberdosis/tiptap",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
import { wrappingInputRule } from 'prosemirror-inputrules'
|
||||
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core'
|
||||
|
||||
export interface BlockquoteOptions {
|
||||
HTMLAttributes: Record<string, any>,
|
||||
@@ -24,7 +23,7 @@ declare module '@tiptap/core' {
|
||||
}
|
||||
}
|
||||
|
||||
export const inputRegex = /^\s*>\s$/gm
|
||||
export const inputRegex = /^\s*>\s$/
|
||||
|
||||
export const Blockquote = Node.create<BlockquoteOptions>({
|
||||
|
||||
@@ -72,7 +71,10 @@ export const Blockquote = Node.create<BlockquoteOptions>({
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
wrappingInputRule(inputRegex, this.type),
|
||||
wrappingInputRule({
|
||||
find: inputRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.17](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-bold@2.0.0-beta.16...@tiptap/extension-bold@2.0.0-beta.17) (2021-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix regex modifier for input rules and paste rules, fix [#2003](https://github.com/ueberdosis/tiptap/issues/2003) ([65eddf0](https://github.com/ueberdosis/tiptap/commit/65eddf0e789c620a53d9bec9509b515211fb9b3f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.16](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-bold@2.0.0-beta.15...@tiptap/extension-bold@2.0.0-beta.16) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.15](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-bold@2.0.0-beta.14...@tiptap/extension-bold@2.0.0-beta.15) (2021-07-26)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-bold
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-bold",
|
||||
"description": "bold extension for tiptap",
|
||||
"version": "2.0.0-beta.15",
|
||||
"version": "2.0.0-beta.17",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
|
||||
@@ -28,10 +28,10 @@ declare module '@tiptap/core' {
|
||||
}
|
||||
}
|
||||
|
||||
export const starInputRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))$/gm
|
||||
export const starPasteRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))/gm
|
||||
export const underscoreInputRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))$/gm
|
||||
export const underscorePasteRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))/gm
|
||||
export const starInputRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))$/
|
||||
export const starPasteRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))/g
|
||||
export const underscoreInputRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))$/
|
||||
export const underscorePasteRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))/g
|
||||
|
||||
export const Bold = Mark.create<BoldOptions>({
|
||||
name: 'bold',
|
||||
@@ -82,15 +82,27 @@ export const Bold = Mark.create<BoldOptions>({
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
markInputRule(starInputRegex, this.type),
|
||||
markInputRule(underscoreInputRegex, this.type),
|
||||
markInputRule({
|
||||
find: starInputRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
markInputRule({
|
||||
find: underscoreInputRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule(starPasteRegex, this.type),
|
||||
markPasteRule(underscorePasteRegex, this.type),
|
||||
markPasteRule({
|
||||
find: starPasteRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
markPasteRule({
|
||||
find: underscorePasteRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.40](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-bubble-menu@2.0.0-beta.39...@tiptap/extension-bubble-menu@2.0.0-beta.40) (2021-10-08)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-bubble-menu
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.39](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-bubble-menu@2.0.0-beta.38...@tiptap/extension-bubble-menu@2.0.0-beta.39) (2021-10-02)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-bubble-menu
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-bubble-menu",
|
||||
"description": "bubble-menu extension for tiptap",
|
||||
"version": "2.0.0-beta.39",
|
||||
"version": "2.0.0-beta.40",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
@@ -25,8 +25,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-view": "^1.20.1",
|
||||
"tippy.js": "^6.3.1"
|
||||
"prosemirror-view": "^1.20.2",
|
||||
"tippy.js": "^6.3.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.16](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-bullet-list@2.0.0-beta.15...@tiptap/extension-bullet-list@2.0.0-beta.16) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.15](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-bullet-list@2.0.0-beta.14...@tiptap/extension-bullet-list@2.0.0-beta.15) (2021-07-26)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-bullet-list
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-bullet-list",
|
||||
"description": "bullet list extension for tiptap",
|
||||
"version": "2.0.0-beta.15",
|
||||
"version": "2.0.0-beta.16",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
@@ -23,9 +23,6 @@
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.0.0-beta.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prosemirror-inputrules": "^1.1.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ueberdosis/tiptap",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
import { wrappingInputRule } from 'prosemirror-inputrules'
|
||||
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core'
|
||||
|
||||
export interface BulletListOptions {
|
||||
HTMLAttributes: Record<string, any>,
|
||||
@@ -55,7 +54,10 @@ export const BulletList = Node.create<BulletListOptions>({
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
wrappingInputRule(inputRegex, this.type),
|
||||
wrappingInputRule({
|
||||
find: inputRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.45](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code-block-lowlight@2.0.0-beta.44...@tiptap/extension-code-block-lowlight@2.0.0-beta.45) (2021-10-13)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-code-block-lowlight
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.44](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code-block-lowlight@2.0.0-beta.43...@tiptap/extension-code-block-lowlight@2.0.0-beta.44) (2021-10-12)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-code-block-lowlight
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.43](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code-block-lowlight@2.0.0-beta.42...@tiptap/extension-code-block-lowlight@2.0.0-beta.43) (2021-10-12)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-code-block-lowlight
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.42](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code-block-lowlight@2.0.0-beta.41...@tiptap/extension-code-block-lowlight@2.0.0-beta.42) (2021-10-08)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-code-block-lowlight
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.41](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code-block-lowlight@2.0.0-beta.40...@tiptap/extension-code-block-lowlight@2.0.0-beta.41) (2021-10-02)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-code-block-lowlight
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-code-block-lowlight",
|
||||
"description": "code block extension for tiptap",
|
||||
"version": "2.0.0-beta.41",
|
||||
"version": "2.0.0-beta.45",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
@@ -24,12 +24,12 @@
|
||||
"@tiptap/core": "^2.0.0-beta.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/extension-code-block": "^2.0.0-beta.18",
|
||||
"@tiptap/extension-code-block": "^2.0.0-beta.22",
|
||||
"@types/lowlight": "^0.0.3",
|
||||
"lowlight": "^1.20.0",
|
||||
"prosemirror-model": "^1.14.3",
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-view": "^1.20.1"
|
||||
"prosemirror-view": "^1.20.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -3,6 +3,50 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.22](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code-block@2.0.0-beta.21...@tiptap/extension-code-block@2.0.0-beta.22) (2021-10-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* strip carriage return chars from text pasted as code and add missing paste meta information ([b1c56e6](https://github.com/ueberdosis/tiptap/commit/b1c56e6f163af1a432621894e19f39243a38c4e4))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.21](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code-block@2.0.0-beta.20...@tiptap/extension-code-block@2.0.0-beta.21) (2021-10-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix bug on paste when using code blocks ([d60fe35](https://github.com/ueberdosis/tiptap/commit/d60fe350d148a0b39a6cdd6b439e3d9325506872))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.20](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code-block@2.0.0-beta.19...@tiptap/extension-code-block@2.0.0-beta.20) (2021-10-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add support for pasted content from VS Code, fix [#2022](https://github.com/ueberdosis/tiptap/issues/2022) ([0e0b0b6](https://github.com/ueberdosis/tiptap/commit/0e0b0b6a8c1d80ef9084d1913a51dd9bff8a69d8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.19](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code-block@2.0.0-beta.18...@tiptap/extension-code-block@2.0.0-beta.19) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.18](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code-block@2.0.0-beta.17...@tiptap/extension-code-block@2.0.0-beta.18) (2021-09-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-code-block",
|
||||
"description": "code block extension for tiptap",
|
||||
"version": "2.0.0-beta.18",
|
||||
"version": "2.0.0-beta.22",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@tiptap/core": "^2.0.0-beta.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prosemirror-inputrules": "^1.1.3"
|
||||
"prosemirror-state": "^1.3.4"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node } from '@tiptap/core'
|
||||
import { textblockTypeInputRule } from 'prosemirror-inputrules'
|
||||
import { Node, textblockTypeInputRule } from '@tiptap/core'
|
||||
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
|
||||
|
||||
export interface CodeBlockOptions {
|
||||
languageClassPrefix: string,
|
||||
@@ -21,8 +21,8 @@ declare module '@tiptap/core' {
|
||||
}
|
||||
}
|
||||
|
||||
export const backtickInputRegex = /^```(?<language>[a-z]*)? $/
|
||||
export const tildeInputRegex = /^~~~(?<language>[a-z]*)? $/
|
||||
export const backtickInputRegex = /^```(?<language>[a-z]*)?[\s\n]$/
|
||||
export const tildeInputRegex = /^~~~(?<language>[a-z]*)?[\s\n]$/
|
||||
|
||||
export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
name: 'codeBlock',
|
||||
@@ -121,8 +121,71 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
textblockTypeInputRule(backtickInputRegex, this.type, ({ groups }: any) => groups),
|
||||
textblockTypeInputRule(tildeInputRegex, this.type, ({ groups }: any) => groups),
|
||||
textblockTypeInputRule({
|
||||
find: backtickInputRegex,
|
||||
type: this.type,
|
||||
getAttributes: ({ groups }) => groups,
|
||||
}),
|
||||
textblockTypeInputRule({
|
||||
find: tildeInputRegex,
|
||||
type: this.type,
|
||||
getAttributes: ({ groups }) => groups,
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
// this plugin creates a code block for pasted content from VS Code
|
||||
// we can also detect the copied code language
|
||||
new Plugin({
|
||||
key: new PluginKey('codeBlockVSCodeHandler'),
|
||||
props: {
|
||||
handlePaste: (view, event) => {
|
||||
if (!event.clipboardData) {
|
||||
return false
|
||||
}
|
||||
|
||||
// don’t create a new code block within code blocks
|
||||
if (this.editor.isActive(this.type.name)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const text = event.clipboardData.getData('text/plain')
|
||||
const vscode = event.clipboardData.getData('vscode-editor-data')
|
||||
const vscodeData = vscode
|
||||
? JSON.parse(vscode)
|
||||
: undefined
|
||||
const language = vscodeData?.mode
|
||||
|
||||
if (!text || !language) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { tr } = view.state
|
||||
|
||||
// create an empty code block
|
||||
tr.replaceSelectionWith(this.type.create({ language }))
|
||||
|
||||
// put cursor inside the newly created code block
|
||||
tr.setSelection(TextSelection.near(tr.doc.resolve(Math.max(0, tr.selection.from - 2))))
|
||||
|
||||
// add text to code block
|
||||
// strip carriage return chars from text pasted as code
|
||||
// see: https://github.com/ProseMirror/prosemirror-view/commit/a50a6bcceb4ce52ac8fcc6162488d8875613aacd
|
||||
tr.insertText(text.replace(/\r\n?/g, '\n'))
|
||||
|
||||
// store meta information
|
||||
// this is useful for other plugins that depends on the paste event
|
||||
// like the paste rule plugin
|
||||
tr.setMeta('paste', true)
|
||||
|
||||
view.dispatch(tr)
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.18](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code@2.0.0-beta.17...@tiptap/extension-code@2.0.0-beta.18) (2021-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix regex modifier for input rules and paste rules, fix [#2003](https://github.com/ueberdosis/tiptap/issues/2003) ([65eddf0](https://github.com/ueberdosis/tiptap/commit/65eddf0e789c620a53d9bec9509b515211fb9b3f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.17](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code@2.0.0-beta.16...@tiptap/extension-code@2.0.0-beta.17) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.16](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-code@2.0.0-beta.15...@tiptap/extension-code@2.0.0-beta.16) (2021-08-09)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-code",
|
||||
"description": "code extension for tiptap",
|
||||
"version": "2.0.0-beta.16",
|
||||
"version": "2.0.0-beta.18",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
|
||||
@@ -28,8 +28,8 @@ declare module '@tiptap/core' {
|
||||
}
|
||||
}
|
||||
|
||||
export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/gm
|
||||
export const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/gm
|
||||
export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/
|
||||
export const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/g
|
||||
|
||||
export const Code = Mark.create<CodeOptions>({
|
||||
name: 'code',
|
||||
@@ -40,6 +40,8 @@ export const Code = Mark.create<CodeOptions>({
|
||||
|
||||
excludes: '_',
|
||||
|
||||
code: true,
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{ tag: 'code' },
|
||||
@@ -72,13 +74,19 @@ export const Code = Mark.create<CodeOptions>({
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
markInputRule(inputRegex, this.type),
|
||||
markInputRule({
|
||||
find: inputRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule(pasteRegex, this.type),
|
||||
markPasteRule({
|
||||
find: pasteRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.23](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-collaboration-cursor@2.0.0-beta.22...@tiptap/extension-collaboration-cursor@2.0.0-beta.23) (2021-10-08)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-collaboration-cursor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.22](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-collaboration-cursor@2.0.0-beta.21...@tiptap/extension-collaboration-cursor@2.0.0-beta.22) (2021-10-04)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-collaboration-cursor",
|
||||
"description": "collaboration cursor extension for tiptap",
|
||||
"version": "2.0.0-beta.22",
|
||||
"version": "2.0.0-beta.23",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.23](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-collaboration@2.0.0-beta.22...@tiptap/extension-collaboration@2.0.0-beta.23) (2021-10-08)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-collaboration
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.22](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-collaboration@2.0.0-beta.21...@tiptap/extension-collaboration@2.0.0-beta.22) (2021-10-04)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-collaboration",
|
||||
"description": "collaboration extension for tiptap",
|
||||
"version": "2.0.0-beta.22",
|
||||
"version": "2.0.0-beta.23",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.34](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-floating-menu@2.0.0-beta.33...@tiptap/extension-floating-menu@2.0.0-beta.34) (2021-10-08)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-floating-menu
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.33](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-floating-menu@2.0.0-beta.32...@tiptap/extension-floating-menu@2.0.0-beta.33) (2021-10-02)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-floating-menu
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-floating-menu",
|
||||
"description": "floating-menu extension for tiptap",
|
||||
"version": "2.0.0-beta.33",
|
||||
"version": "2.0.0-beta.34",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
@@ -25,8 +25,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-view": "^1.20.1",
|
||||
"tippy.js": "^6.3.1"
|
||||
"prosemirror-view": "^1.20.2",
|
||||
"tippy.js": "^6.3.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.27](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-focus@2.0.0-beta.26...@tiptap/extension-focus@2.0.0-beta.27) (2021-10-08)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-focus
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.26](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-focus@2.0.0-beta.25...@tiptap/extension-focus@2.0.0-beta.26) (2021-09-15)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-focus
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-focus",
|
||||
"description": "focus extension for tiptap",
|
||||
"version": "2.0.0-beta.26",
|
||||
"version": "2.0.0-beta.27",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-view": "^1.20.1"
|
||||
"prosemirror-view": "^1.20.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.25](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-gapcursor@2.0.0-beta.24...@tiptap/extension-gapcursor@2.0.0-beta.25) (2021-10-08)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-gapcursor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.24](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-gapcursor@2.0.0-beta.23...@tiptap/extension-gapcursor@2.0.0-beta.24) (2021-10-02)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-gapcursor
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-gapcursor",
|
||||
"description": "gapcursor extension for tiptap",
|
||||
"version": "2.0.0-beta.24",
|
||||
"version": "2.0.0-beta.25",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.22](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-hard-break@2.0.0-beta.21...@tiptap/extension-hard-break@2.0.0-beta.22) (2021-10-08)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-hard-break
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.21](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-hard-break@2.0.0-beta.20...@tiptap/extension-hard-break@2.0.0-beta.21) (2021-10-02)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-hard-break
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-hard-break",
|
||||
"description": "hard break extension for tiptap",
|
||||
"version": "2.0.0-beta.21",
|
||||
"version": "2.0.0-beta.22",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.16](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-heading@2.0.0-beta.15...@tiptap/extension-heading@2.0.0-beta.16) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.15](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-heading@2.0.0-beta.14...@tiptap/extension-heading@2.0.0-beta.15) (2021-07-26)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-heading
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-heading",
|
||||
"description": "heading extension for tiptap",
|
||||
"version": "2.0.0-beta.15",
|
||||
"version": "2.0.0-beta.16",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
@@ -23,9 +23,6 @@
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.0.0-beta.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prosemirror-inputrules": "^1.1.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ueberdosis/tiptap",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
import { textblockTypeInputRule } from 'prosemirror-inputrules'
|
||||
import { Node, mergeAttributes, textblockTypeInputRule } from '@tiptap/core'
|
||||
|
||||
type Level = 1 | 2 | 3 | 4 | 5 | 6
|
||||
|
||||
@@ -93,7 +92,13 @@ export const Heading = Node.create<HeadingOptions>({
|
||||
|
||||
addInputRules() {
|
||||
return this.options.levels.map(level => {
|
||||
return textblockTypeInputRule(new RegExp(`^(#{1,${level}})\\s$`), this.type, { level })
|
||||
return textblockTypeInputRule({
|
||||
find: new RegExp(`^(#{1,${level}})\\s$`),
|
||||
type: this.type,
|
||||
getAttributes: {
|
||||
level,
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.23](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-highlight@2.0.0-beta.22...@tiptap/extension-highlight@2.0.0-beta.23) (2021-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix regex modifier for input rules and paste rules, fix [#2003](https://github.com/ueberdosis/tiptap/issues/2003) ([65eddf0](https://github.com/ueberdosis/tiptap/commit/65eddf0e789c620a53d9bec9509b515211fb9b3f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.22](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-highlight@2.0.0-beta.21...@tiptap/extension-highlight@2.0.0-beta.22) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.21](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-highlight@2.0.0-beta.20...@tiptap/extension-highlight@2.0.0-beta.21) (2021-10-02)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-highlight
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-highlight",
|
||||
"description": "highlight extension for tiptap",
|
||||
"version": "2.0.0-beta.21",
|
||||
"version": "2.0.0-beta.23",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
|
||||
@@ -29,8 +29,8 @@ declare module '@tiptap/core' {
|
||||
}
|
||||
}
|
||||
|
||||
export const inputRegex = /(?:^|\s)((?:==)((?:[^~]+))(?:==))$/gm
|
||||
export const pasteRegex = /(?:^|\s)((?:==)((?:[^~]+))(?:==))/gm
|
||||
export const inputRegex = /(?:^|\s)((?:==)((?:[^~]+))(?:==))$/
|
||||
export const pasteRegex = /(?:^|\s)((?:==)((?:[^~]+))(?:==))/g
|
||||
|
||||
export const Highlight = Mark.create<HighlightOptions>({
|
||||
name: 'highlight',
|
||||
@@ -97,13 +97,19 @@ export const Highlight = Mark.create<HighlightOptions>({
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
markInputRule(inputRegex, this.type),
|
||||
markInputRule({
|
||||
find: inputRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule(pasteRegex, this.type),
|
||||
markPasteRule({
|
||||
find: pasteRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.22](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-horizontal-rule@2.0.0-beta.21...@tiptap/extension-horizontal-rule@2.0.0-beta.22) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.21](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-horizontal-rule@2.0.0-beta.20...@tiptap/extension-horizontal-rule@2.0.0-beta.21) (2021-10-02)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-horizontal-rule
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-horizontal-rule",
|
||||
"description": "horizontal rule extension for tiptap",
|
||||
"version": "2.0.0-beta.21",
|
||||
"version": "2.0.0-beta.22",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
|
||||
@@ -92,7 +92,10 @@ export const HorizontalRule = Node.create<HorizontalRuleOptions>({
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule(/^(?:---|—-|___\s|\*\*\*\s)$/, this.type),
|
||||
nodeInputRule({
|
||||
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.17](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-image@2.0.0-beta.16...@tiptap/extension-image@2.0.0-beta.17) (2021-10-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix [#2016](https://github.com/ueberdosis/tiptap/issues/2016) Image input rule ([#2020](https://github.com/ueberdosis/tiptap/issues/2020)) ([503a3f2](https://github.com/ueberdosis/tiptap/commit/503a3f2cf508271cc303fb918568dc8c9b900055)), closes [#1574](https://github.com/ueberdosis/tiptap/issues/1574)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.16](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-image@2.0.0-beta.15...@tiptap/extension-image@2.0.0-beta.16) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.15](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-image@2.0.0-beta.14...@tiptap/extension-image@2.0.0-beta.15) (2021-07-26)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-image
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@tiptap/extension-image",
|
||||
"description": "image extension for tiptap",
|
||||
"version": "2.0.0-beta.15",
|
||||
"version": "2.0.0-beta.17",
|
||||
"homepage": "https://tiptap.dev",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
|
||||
@@ -20,7 +20,7 @@ declare module '@tiptap/core' {
|
||||
}
|
||||
}
|
||||
|
||||
export const inputRegex = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
|
||||
export const inputRegex = /(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))/
|
||||
|
||||
export const Image = Node.create<ImageOptions>({
|
||||
name: 'image',
|
||||
@@ -79,10 +79,14 @@ export const Image = Node.create<ImageOptions>({
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule(inputRegex, this.type, match => {
|
||||
const [, alt, src, title] = match
|
||||
nodeInputRule({
|
||||
find: inputRegex,
|
||||
type: this.type,
|
||||
getAttributes: match => {
|
||||
const [,, alt, src, title] = match
|
||||
|
||||
return { src, alt, title }
|
||||
return { src, alt, title }
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.17](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-italic@2.0.0-beta.16...@tiptap/extension-italic@2.0.0-beta.17) (2021-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix regex modifier for input rules and paste rules, fix [#2003](https://github.com/ueberdosis/tiptap/issues/2003) ([65eddf0](https://github.com/ueberdosis/tiptap/commit/65eddf0e789c620a53d9bec9509b515211fb9b3f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.16](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-italic@2.0.0-beta.15...@tiptap/extension-italic@2.0.0-beta.16) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.15](https://github.com/ueberdosis/tiptap/compare/@tiptap/extension-italic@2.0.0-beta.14...@tiptap/extension-italic@2.0.0-beta.15) (2021-07-26)
|
||||
|
||||
**Note:** Version bump only for package @tiptap/extension-italic
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user