mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-11 11:45:15 +08:00
Merge branch 'main' into develop
This commit is contained in:
commit
65cef599bc
@ -19,7 +19,7 @@
|
|||||||
"remixicon": "^2.5.0",
|
"remixicon": "^2.5.0",
|
||||||
"shiki": "^0.10.0",
|
"shiki": "^0.10.0",
|
||||||
"simplify-js": "^1.2.4",
|
"simplify-js": "^1.2.4",
|
||||||
"y-prosemirror": "^1.2.5",
|
"y-prosemirror": "^1.2.6",
|
||||||
"y-webrtc": "^10.3.0",
|
"y-webrtc": "^10.3.0",
|
||||||
"yjs": "^13.6.11"
|
"yjs": "^13.6.11"
|
||||||
},
|
},
|
||||||
|
@ -20,6 +20,7 @@ export default () => {
|
|||||||
Link.configure({
|
Link.configure({
|
||||||
openOnClick: false,
|
openOnClick: false,
|
||||||
autolink: true,
|
autolink: true,
|
||||||
|
defaultProtocol: 'https',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
|
@ -52,6 +52,15 @@ context('/src/Marks/Link/React/', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should allow exiting the link once set', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
editor.commands.setContent('<p><a href="#" target="_self">Example Text2</a></p>')
|
||||||
|
cy.get('.tiptap').type('{rightArrow}')
|
||||||
|
|
||||||
|
cy.get('button:first').should('not.have.class', 'is-active')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('detects autolinking', () => {
|
it('detects autolinking', () => {
|
||||||
cy.get('.tiptap').type('https://example.com ').find('a').should('contain', 'https://example.com')
|
cy.get('.tiptap').type('https://example.com ').find('a').should('contain', 'https://example.com')
|
||||||
.should('have.attr', 'href', 'https://example.com')
|
.should('have.attr', 'href', 'https://example.com')
|
||||||
@ -62,6 +71,16 @@ context('/src/Marks/Link/React/', () => {
|
|||||||
.should('have.attr', 'href', 'https://tiptap4u.com')
|
.should('have.attr', 'href', 'https://tiptap4u.com')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses the default protocol', () => {
|
||||||
|
cy.get('.tiptap').type('example.com ').find('a').should('contain', 'example.com')
|
||||||
|
.should('have.attr', 'href', 'https://example.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses a non-default protocol if present', () => {
|
||||||
|
cy.get('.tiptap').type('http://example.com ').find('a').should('contain', 'http://example.com')
|
||||||
|
.should('have.attr', 'href', 'http://example.com')
|
||||||
|
})
|
||||||
|
|
||||||
it('detects a pasted URL within a text', () => {
|
it('detects a pasted URL within a text', () => {
|
||||||
cy.get('.tiptap')
|
cy.get('.tiptap')
|
||||||
.paste({
|
.paste({
|
||||||
|
@ -63,6 +63,15 @@ context('/src/Marks/Link/Vue/', () => {
|
|||||||
.should('have.attr', 'href', 'https://example2.com')
|
.should('have.attr', 'href', 'https://example2.com')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should allow exiting the link once set', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
editor.commands.setContent('<p><a href="#" target="_self">Example Text2</a></p>')
|
||||||
|
cy.get('.tiptap').type('{rightArrow}')
|
||||||
|
|
||||||
|
cy.get('button:first').should('not.have.class', 'is-active')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('detects autolinking', () => {
|
it('detects autolinking', () => {
|
||||||
cy.get('.tiptap').type('https://example.com ').find('a').should('contain', 'https://example.com')
|
cy.get('.tiptap').type('https://example.com ').find('a').should('contain', 'https://example.com')
|
||||||
.should('have.attr', 'href', 'https://example.com')
|
.should('have.attr', 'href', 'https://example.com')
|
||||||
@ -73,6 +82,16 @@ context('/src/Marks/Link/Vue/', () => {
|
|||||||
.should('have.attr', 'href', 'https://tiptap4u.com')
|
.should('have.attr', 'href', 'https://tiptap4u.com')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses the default protocol', () => {
|
||||||
|
cy.get('.tiptap').type('example.com ').find('a').should('contain', 'example.com')
|
||||||
|
.should('have.attr', 'href', 'https://example.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses a non-default protocol if present', () => {
|
||||||
|
cy.get('.tiptap').type('http://example.com ').find('a').should('contain', 'http://example.com')
|
||||||
|
.should('have.attr', 'href', 'http://example.com')
|
||||||
|
})
|
||||||
|
|
||||||
it('detects a pasted URL with query params', () => {
|
it('detects a pasted URL with query params', () => {
|
||||||
cy.get('.tiptap')
|
cy.get('.tiptap')
|
||||||
.type('{backspace}')
|
.type('{backspace}')
|
||||||
|
@ -38,6 +38,7 @@ export default {
|
|||||||
Code,
|
Code,
|
||||||
Link.configure({
|
Link.configure({
|
||||||
openOnClick: false,
|
openOnClick: false,
|
||||||
|
defaultProtocol: 'https',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
|
@ -27,7 +27,7 @@ Most of the core extensions register their own keyboard shortcuts. Depending on
|
|||||||
| Bold | `Control` `B` | `Cmd` `B` |
|
| Bold | `Control` `B` | `Cmd` `B` |
|
||||||
| Italicize | `Control` `I` | `Cmd` `I` |
|
| Italicize | `Control` `I` | `Cmd` `I` |
|
||||||
| Underline | `Control` `U` | `Cmd` `U` |
|
| Underline | `Control` `U` | `Cmd` `U` |
|
||||||
| Strikethrough | `Control` `Shift` `X` | `Cmd` `Shift` `X` |
|
| Strikethrough | `Control` `Shift` `S` | `Cmd` `Shift` `S` |
|
||||||
| Highlight | `Control` `Shift` `H` | `Cmd` `Shift` `H` |
|
| Highlight | `Control` `Shift` `H` | `Cmd` `Shift` `H` |
|
||||||
| Code | `Control` `E` | `Cmd` `E` |
|
| Code | `Control` `E` | `Cmd` `E` |
|
||||||
|
|
||||||
|
@ -76,6 +76,20 @@ Link.configure({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### default protocol
|
||||||
|
The default protocol used by `linkOnPaste` and `autolink` when no protocol is defined.
|
||||||
|
|
||||||
|
By default, the href generated for example.com is http://example.com and this option allows that protocol to be customized.
|
||||||
|
|
||||||
|
Default: `http`
|
||||||
|
|
||||||
|
```js
|
||||||
|
Link.configure({
|
||||||
|
defaultProtocol: 'https',
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
### HTMLAttributes
|
### HTMLAttributes
|
||||||
Custom HTML attributes that should be added to the rendered HTML tag.
|
Custom HTML attributes that should be added to the rendered HTML tag.
|
||||||
|
|
||||||
|
16
package-lock.json
generated
16
package-lock.json
generated
@ -67,7 +67,7 @@
|
|||||||
"remixicon": "^2.5.0",
|
"remixicon": "^2.5.0",
|
||||||
"shiki": "^0.10.0",
|
"shiki": "^0.10.0",
|
||||||
"simplify-js": "^1.2.4",
|
"simplify-js": "^1.2.4",
|
||||||
"y-prosemirror": "^1.2.5",
|
"y-prosemirror": "^1.2.6",
|
||||||
"y-webrtc": "^10.3.0",
|
"y-webrtc": "^10.3.0",
|
||||||
"yjs": "^13.6.11"
|
"yjs": "^13.6.11"
|
||||||
},
|
},
|
||||||
@ -20160,9 +20160,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/y-prosemirror": {
|
"node_modules/y-prosemirror": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.2.6.tgz",
|
||||||
"integrity": "sha512-T/JATxC8P2Dbvq/dAiaiztD1a8KEwRP8oLRlT8YlaZdNlLGE1Ea0IJ8If25UlDYmk+4+uqLbqT/S+dzUmwwgbA==",
|
"integrity": "sha512-rGz8kX4v/uFJrLaqZvsezY1JGN/zTDSPMO76zRbNcpE63OEiw2PBCEQi9ZlfbEwgCMoeJLUT+otNyO/Oj73TGQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.42"
|
"lib0": "^0.2.42"
|
||||||
},
|
},
|
||||||
@ -20478,7 +20478,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tiptap/core": "^2.5.0-beta.1",
|
"@tiptap/core": "^2.5.0-beta.1",
|
||||||
"@tiptap/pm": "^2.5.0-beta.1",
|
"@tiptap/pm": "^2.5.0-beta.1",
|
||||||
"y-prosemirror": "^1.2.5"
|
"y-prosemirror": "^1.2.6"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -20487,7 +20487,7 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0",
|
"@tiptap/core": "^2.0.0",
|
||||||
"@tiptap/pm": "^2.0.0",
|
"@tiptap/pm": "^2.0.0",
|
||||||
"y-prosemirror": "^1.2.5"
|
"y-prosemirror": "^1.2.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/extension-collaboration-cursor": {
|
"packages/extension-collaboration-cursor": {
|
||||||
@ -20496,7 +20496,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tiptap/core": "^2.5.0-beta.1",
|
"@tiptap/core": "^2.5.0-beta.1",
|
||||||
"y-prosemirror": "^1.2.5"
|
"y-prosemirror": "^1.2.6"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -20504,7 +20504,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0",
|
"@tiptap/core": "^2.0.0",
|
||||||
"y-prosemirror": "^1.2.5"
|
"y-prosemirror": "^1.2.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/extension-color": {
|
"packages/extension-color": {
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { MarkType, NodeType } from '@tiptap/pm/model'
|
import {
|
||||||
|
Mark, MarkType, Node, NodeType,
|
||||||
|
} from '@tiptap/pm/model'
|
||||||
|
import { SelectionRange } from '@tiptap/pm/state'
|
||||||
|
|
||||||
import { getMarkType } from '../helpers/getMarkType.js'
|
import { getMarkType } from '../helpers/getMarkType.js'
|
||||||
import { getNodeType } from '../helpers/getNodeType.js'
|
import { getNodeType } from '../helpers/getNodeType.js'
|
||||||
@ -51,37 +54,49 @@ export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, at
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
tr.selection.ranges.forEach(range => {
|
let lastPos: number | undefined
|
||||||
|
let lastNode: Node | undefined
|
||||||
|
let trimmedFrom: number
|
||||||
|
let trimmedTo: number
|
||||||
|
|
||||||
|
tr.selection.ranges.forEach((range: SelectionRange) => {
|
||||||
const from = range.$from.pos
|
const from = range.$from.pos
|
||||||
const to = range.$to.pos
|
const to = range.$to.pos
|
||||||
|
|
||||||
state.doc.nodesBetween(from, to, (node, pos) => {
|
state.doc.nodesBetween(from, to, (node: Node, pos: number) => {
|
||||||
if (nodeType && nodeType === node.type) {
|
if (nodeType && nodeType === node.type) {
|
||||||
tr.setNodeMarkup(pos, undefined, {
|
trimmedFrom = Math.max(pos, from)
|
||||||
...node.attrs,
|
trimmedTo = Math.min(pos + node.nodeSize, to)
|
||||||
...attributes,
|
lastPos = pos
|
||||||
})
|
lastNode = node
|
||||||
}
|
|
||||||
|
|
||||||
if (markType && node.marks.length) {
|
|
||||||
node.marks.forEach(mark => {
|
|
||||||
if (markType === mark.type) {
|
|
||||||
const trimmedFrom = Math.max(pos, from)
|
|
||||||
const trimmedTo = Math.min(pos + node.nodeSize, to)
|
|
||||||
|
|
||||||
tr.addMark(
|
|
||||||
trimmedFrom,
|
|
||||||
trimmedTo,
|
|
||||||
markType.create({
|
|
||||||
...mark.attrs,
|
|
||||||
...attributes,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (lastNode) {
|
||||||
|
|
||||||
|
if (lastPos !== undefined) {
|
||||||
|
tr.setNodeMarkup(lastPos, undefined, {
|
||||||
|
...lastNode.attrs,
|
||||||
|
...attributes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markType && lastNode.marks.length) {
|
||||||
|
lastNode.marks.forEach((mark: Mark) => {
|
||||||
|
if (markType === mark.type) {
|
||||||
|
tr.addMark(
|
||||||
|
trimmedFrom,
|
||||||
|
trimmedTo,
|
||||||
|
markType.create({
|
||||||
|
...mark.attrs,
|
||||||
|
...attributes,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -189,7 +189,7 @@ export class BubbleMenuView {
|
|||||||
|
|
||||||
update(view: EditorView, oldState?: EditorState) {
|
update(view: EditorView, oldState?: EditorState) {
|
||||||
const { state } = view
|
const { state } = view
|
||||||
const hasValidSelection = state.selection.$from.pos !== state.selection.$to.pos
|
const hasValidSelection = state.selection.from !== state.selection.to
|
||||||
|
|
||||||
if (this.updateDelay > 0 && hasValidSelection) {
|
if (this.updateDelay > 0 && hasValidSelection) {
|
||||||
this.handleDebouncedUpdate(view, oldState)
|
this.handleDebouncedUpdate(view, oldState)
|
||||||
|
@ -30,11 +30,11 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tiptap/core": "^2.5.0-beta.1",
|
"@tiptap/core": "^2.5.0-beta.1",
|
||||||
"y-prosemirror": "^1.2.5"
|
"y-prosemirror": "^1.2.6"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0",
|
"@tiptap/core": "^2.0.0",
|
||||||
"y-prosemirror": "^1.2.5"
|
"y-prosemirror": "^1.2.6"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -31,12 +31,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tiptap/core": "^2.5.0-beta.1",
|
"@tiptap/core": "^2.5.0-beta.1",
|
||||||
"@tiptap/pm": "^2.5.0-beta.1",
|
"@tiptap/pm": "^2.5.0-beta.1",
|
||||||
"y-prosemirror": "^1.2.5"
|
"y-prosemirror": "^1.2.6"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0",
|
"@tiptap/core": "^2.0.0",
|
||||||
"@tiptap/pm": "^2.0.0",
|
"@tiptap/pm": "^2.0.0",
|
||||||
"y-prosemirror": "^1.2.5"
|
"y-prosemirror": "^1.2.6"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -33,6 +33,7 @@ function isValidLinkStructure(tokens: Array<ReturnType<MultiToken['toObject']>>)
|
|||||||
|
|
||||||
type AutolinkOptions = {
|
type AutolinkOptions = {
|
||||||
type: MarkType
|
type: MarkType
|
||||||
|
defaultProtocol: string
|
||||||
validate: (url: string) => boolean
|
validate: (url: string) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +116,7 @@ export function autolink(options: AutolinkOptions): Plugin {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t => t.toObject())
|
const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t => t.toObject(options.defaultProtocol))
|
||||||
|
|
||||||
if (!isValidLinkStructure(linksBeforeSpace)) {
|
if (!isValidLinkStructure(linksBeforeSpace)) {
|
||||||
return false
|
return false
|
||||||
|
@ -5,6 +5,7 @@ import { find } from 'linkifyjs'
|
|||||||
|
|
||||||
type PasteHandlerOptions = {
|
type PasteHandlerOptions = {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
|
defaultProtocol: string
|
||||||
type: MarkType
|
type: MarkType
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ export function pasteHandler(options: PasteHandlerOptions): Plugin {
|
|||||||
textContent += node.textContent
|
textContent += node.textContent
|
||||||
})
|
})
|
||||||
|
|
||||||
const link = find(textContent).find(item => item.isLink && item.value === textContent)
|
const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find(item => item.isLink && item.value === textContent)
|
||||||
|
|
||||||
if (!textContent || !link) {
|
if (!textContent || !link) {
|
||||||
return false
|
return false
|
||||||
|
@ -42,6 +42,11 @@ export interface LinkOptions {
|
|||||||
*/
|
*/
|
||||||
protocols: Array<LinkProtocolOptions | string>
|
protocols: Array<LinkProtocolOptions | string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default protocol to use when no protocol is specified.
|
||||||
|
* @default 'http'
|
||||||
|
*/
|
||||||
|
defaultProtocol: string
|
||||||
/**
|
/**
|
||||||
* If enabled, links will be opened on click.
|
* If enabled, links will be opened on click.
|
||||||
* @default true
|
* @default true
|
||||||
@ -115,6 +120,8 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
|
|
||||||
keepOnSplit: false,
|
keepOnSplit: false,
|
||||||
|
|
||||||
|
exitable: true,
|
||||||
|
|
||||||
onCreate() {
|
onCreate() {
|
||||||
this.options.protocols.forEach(protocol => {
|
this.options.protocols.forEach(protocol => {
|
||||||
if (typeof protocol === 'string') {
|
if (typeof protocol === 'string') {
|
||||||
@ -139,6 +146,7 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
linkOnPaste: true,
|
linkOnPaste: true,
|
||||||
autolink: true,
|
autolink: true,
|
||||||
protocols: [],
|
protocols: [],
|
||||||
|
defaultProtocol: 'http',
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
rel: 'noopener noreferrer nofollow',
|
rel: 'noopener noreferrer nofollow',
|
||||||
@ -255,6 +263,7 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
plugins.push(
|
plugins.push(
|
||||||
autolink({
|
autolink({
|
||||||
type: this.type,
|
type: this.type,
|
||||||
|
defaultProtocol: this.options.defaultProtocol,
|
||||||
validate: this.options.validate,
|
validate: this.options.validate,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -272,6 +281,7 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
plugins.push(
|
plugins.push(
|
||||||
pasteHandler({
|
pasteHandler({
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
|
defaultProtocol: this.options.defaultProtocol,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
@ -302,7 +302,7 @@ export const Table = Node.create<TableOptions>({
|
|||||||
const node = createTable(editor.schema, rows, cols, withHeaderRow)
|
const node = createTable(editor.schema, rows, cols, withHeaderRow)
|
||||||
|
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
const offset = tr.selection.anchor + 1
|
const offset = tr.selection.from + 1
|
||||||
|
|
||||||
tr.replaceSelectionWith(node)
|
tr.replaceSelectionWith(node)
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
|
Loading…
Reference in New Issue
Block a user