Merge branch 'main' into develop

This commit is contained in:
Nick the Sick 2024-06-07 16:48:53 +02:00
commit 65cef599bc
No known key found for this signature in database
GPG Key ID: F575992F156E5BCC
16 changed files with 124 additions and 43 deletions

View File

@ -19,7 +19,7 @@
"remixicon": "^2.5.0",
"shiki": "^0.10.0",
"simplify-js": "^1.2.4",
"y-prosemirror": "^1.2.5",
"y-prosemirror": "^1.2.6",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.11"
},

View File

@ -20,6 +20,7 @@ export default () => {
Link.configure({
openOnClick: false,
autolink: true,
defaultProtocol: 'https',
}),
],
content: `

View File

@ -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', () => {
cy.get('.tiptap').type('https://example.com ').find('a').should('contain', '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')
})
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', () => {
cy.get('.tiptap')
.paste({

View File

@ -63,6 +63,15 @@ context('/src/Marks/Link/Vue/', () => {
.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', () => {
cy.get('.tiptap').type('https://example.com ').find('a').should('contain', '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')
})
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', () => {
cy.get('.tiptap')
.type('{backspace}')

View File

@ -38,6 +38,7 @@ export default {
Code,
Link.configure({
openOnClick: false,
defaultProtocol: 'https',
}),
],
content: `

View File

@ -27,7 +27,7 @@ Most of the core extensions register their own keyboard shortcuts. Depending on
| Bold | `Control`&nbsp;`B` | `Cmd`&nbsp;`B` |
| Italicize | `Control`&nbsp;`I` | `Cmd`&nbsp;`I` |
| Underline | `Control`&nbsp;`U` | `Cmd`&nbsp;`U` |
| Strikethrough | `Control`&nbsp;`Shift`&nbsp;`X` | `Cmd`&nbsp;`Shift`&nbsp;`X` |
| Strikethrough | `Control`&nbsp;`Shift`&nbsp;`S` | `Cmd`&nbsp;`Shift`&nbsp;`S` |
| Highlight | `Control`&nbsp;`Shift`&nbsp;`H` | `Cmd`&nbsp;`Shift`&nbsp;`H` |
| Code | `Control`&nbsp;`E` | `Cmd`&nbsp;`E` |

View File

@ -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
Custom HTML attributes that should be added to the rendered HTML tag.

16
package-lock.json generated
View File

@ -67,7 +67,7 @@
"remixicon": "^2.5.0",
"shiki": "^0.10.0",
"simplify-js": "^1.2.4",
"y-prosemirror": "^1.2.5",
"y-prosemirror": "^1.2.6",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.11"
},
@ -20160,9 +20160,9 @@
}
},
"node_modules/y-prosemirror": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.2.5.tgz",
"integrity": "sha512-T/JATxC8P2Dbvq/dAiaiztD1a8KEwRP8oLRlT8YlaZdNlLGE1Ea0IJ8If25UlDYmk+4+uqLbqT/S+dzUmwwgbA==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.2.6.tgz",
"integrity": "sha512-rGz8kX4v/uFJrLaqZvsezY1JGN/zTDSPMO76zRbNcpE63OEiw2PBCEQi9ZlfbEwgCMoeJLUT+otNyO/Oj73TGQ==",
"dependencies": {
"lib0": "^0.2.42"
},
@ -20478,7 +20478,7 @@
"devDependencies": {
"@tiptap/core": "^2.5.0-beta.1",
"@tiptap/pm": "^2.5.0-beta.1",
"y-prosemirror": "^1.2.5"
"y-prosemirror": "^1.2.6"
},
"funding": {
"type": "github",
@ -20487,7 +20487,7 @@
"peerDependencies": {
"@tiptap/core": "^2.0.0",
"@tiptap/pm": "^2.0.0",
"y-prosemirror": "^1.2.5"
"y-prosemirror": "^1.2.6"
}
},
"packages/extension-collaboration-cursor": {
@ -20496,7 +20496,7 @@
"license": "MIT",
"devDependencies": {
"@tiptap/core": "^2.5.0-beta.1",
"y-prosemirror": "^1.2.5"
"y-prosemirror": "^1.2.6"
},
"funding": {
"type": "github",
@ -20504,7 +20504,7 @@
},
"peerDependencies": {
"@tiptap/core": "^2.0.0",
"y-prosemirror": "^1.2.5"
"y-prosemirror": "^1.2.6"
}
},
"packages/extension-color": {

View File

@ -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 { getNodeType } from '../helpers/getNodeType.js'
@ -51,37 +54,49 @@ export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, at
}
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 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) {
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
...attributes,
})
}
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,
}),
)
}
})
trimmedFrom = Math.max(pos, from)
trimmedTo = Math.min(pos + node.nodeSize, to)
lastPos = pos
lastNode = node
}
})
})
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

View File

@ -189,7 +189,7 @@ export class BubbleMenuView {
update(view: EditorView, oldState?: EditorState) {
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) {
this.handleDebouncedUpdate(view, oldState)

View File

@ -30,11 +30,11 @@
],
"devDependencies": {
"@tiptap/core": "^2.5.0-beta.1",
"y-prosemirror": "^1.2.5"
"y-prosemirror": "^1.2.6"
},
"peerDependencies": {
"@tiptap/core": "^2.0.0",
"y-prosemirror": "^1.2.5"
"y-prosemirror": "^1.2.6"
},
"repository": {
"type": "git",

View File

@ -31,12 +31,12 @@
"devDependencies": {
"@tiptap/core": "^2.5.0-beta.1",
"@tiptap/pm": "^2.5.0-beta.1",
"y-prosemirror": "^1.2.5"
"y-prosemirror": "^1.2.6"
},
"peerDependencies": {
"@tiptap/core": "^2.0.0",
"@tiptap/pm": "^2.0.0",
"y-prosemirror": "^1.2.5"
"y-prosemirror": "^1.2.6"
},
"repository": {
"type": "git",

View File

@ -33,6 +33,7 @@ function isValidLinkStructure(tokens: Array<ReturnType<MultiToken['toObject']>>)
type AutolinkOptions = {
type: MarkType
defaultProtocol: string
validate: (url: string) => boolean
}
@ -115,7 +116,7 @@ export function autolink(options: AutolinkOptions): Plugin {
return false
}
const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t => t.toObject())
const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t => t.toObject(options.defaultProtocol))
if (!isValidLinkStructure(linksBeforeSpace)) {
return false

View File

@ -5,6 +5,7 @@ import { find } from 'linkifyjs'
type PasteHandlerOptions = {
editor: Editor
defaultProtocol: string
type: MarkType
}
@ -27,7 +28,7 @@ export function pasteHandler(options: PasteHandlerOptions): Plugin {
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) {
return false

View File

@ -42,6 +42,11 @@ export interface LinkOptions {
*/
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.
* @default true
@ -115,6 +120,8 @@ export const Link = Mark.create<LinkOptions>({
keepOnSplit: false,
exitable: true,
onCreate() {
this.options.protocols.forEach(protocol => {
if (typeof protocol === 'string') {
@ -139,6 +146,7 @@ export const Link = Mark.create<LinkOptions>({
linkOnPaste: true,
autolink: true,
protocols: [],
defaultProtocol: 'http',
HTMLAttributes: {
target: '_blank',
rel: 'noopener noreferrer nofollow',
@ -255,6 +263,7 @@ export const Link = Mark.create<LinkOptions>({
plugins.push(
autolink({
type: this.type,
defaultProtocol: this.options.defaultProtocol,
validate: this.options.validate,
}),
)
@ -272,6 +281,7 @@ export const Link = Mark.create<LinkOptions>({
plugins.push(
pasteHandler({
editor: this.editor,
defaultProtocol: this.options.defaultProtocol,
type: this.type,
}),
)

View File

@ -302,7 +302,7 @@ export const Table = Node.create<TableOptions>({
const node = createTable(editor.schema, rows, cols, withHeaderRow)
if (dispatch) {
const offset = tr.selection.anchor + 1
const offset = tr.selection.from + 1
tr.replaceSelectionWith(node)
.scrollIntoView()