From c50eb4bc2b1ce087bfb9958600d33b8ed7611bb4 Mon Sep 17 00:00:00 2001 From: Julien Cigar Date: Mon, 4 Nov 2024 11:04:58 +0100 Subject: [PATCH 01/17] fix(core): handle selections better for `updateAttributes` (#5738) --- .changeset/swift-keys-collect.md | 5 ++ .../core/src/commands/updateAttributes.ts | 84 ++++++++++++++++--- 2 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 .changeset/swift-keys-collect.md diff --git a/.changeset/swift-keys-collect.md b/.changeset/swift-keys-collect.md new file mode 100644 index 00000000000..d9a6ebf459b --- /dev/null +++ b/.changeset/swift-keys-collect.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": patch +--- + +Improve handling of selections with `updateAttributes`. Should no longer modify parent nodes of the same type. diff --git a/packages/core/src/commands/updateAttributes.ts b/packages/core/src/commands/updateAttributes.ts index d6993b6d65f..f01fb1a750d 100644 --- a/packages/core/src/commands/updateAttributes.ts +++ b/packages/core/src/commands/updateAttributes.ts @@ -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' @@ -30,6 +33,7 @@ declare module '@tiptap/core' { } export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, attributes = {}) => ({ tr, state, dispatch }) => { + let nodeType: NodeType | null = null let markType: MarkType | null = null @@ -51,24 +55,80 @@ export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, at } if (dispatch) { - tr.selection.ranges.forEach(range => { + tr.selection.ranges.forEach((range: SelectionRange) => { + const from = range.$from.pos const to = range.$to.pos - state.doc.nodesBetween(from, to, (node, pos) => { - if (nodeType && nodeType === node.type) { - tr.setNodeMarkup(pos, undefined, { - ...node.attrs, + let lastPos: number | undefined + let lastNode: Node | undefined + let trimmedFrom: number + let trimmedTo: number + + if (tr.selection.empty) { + state.doc.nodesBetween(from, to, (node: Node, pos: number) => { + + if (nodeType && nodeType === node.type) { + trimmedFrom = Math.max(pos, from) + trimmedTo = Math.min(pos + node.nodeSize, to) + lastPos = pos + lastNode = node + } + }) + } else { + state.doc.nodesBetween(from, to, (node: Node, pos: number) => { + + if (pos < from && nodeType && nodeType === node.type) { + trimmedFrom = Math.max(pos, from) + trimmedTo = Math.min(pos + node.nodeSize, to) + lastPos = pos + lastNode = node + } + + if (pos >= from && pos <= to) { + + if (nodeType && nodeType === node.type) { + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + ...attributes, + }) + } + + if (markType && node.marks.length) { + node.marks.forEach((mark: Mark) => { + + if (markType === mark.type) { + const trimmedFrom2 = Math.max(pos, from) + const trimmedTo2 = Math.min(pos + node.nodeSize, to) + + tr.addMark( + trimmedFrom2, + trimmedTo2, + markType.create({ + ...mark.attrs, + ...attributes, + }), + ) + } + }) + } + } + }) + } + + if (lastNode) { + + if (lastPos !== undefined) { + tr.setNodeMarkup(lastPos, undefined, { + ...lastNode.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) + if (markType && lastNode.marks.length) { + lastNode.marks.forEach((mark: Mark) => { + if (markType === mark.type) { tr.addMark( trimmedFrom, trimmedTo, @@ -80,7 +140,7 @@ export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, at } }) } - }) + } }) } From 830e683ddeb6094acc7131b212aba8016c1112d3 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Mon, 4 Nov 2024 11:16:05 +0100 Subject: [PATCH 02/17] fix(bubble-menu): add `element` to `shouldShow` in BubbleMenu opts (#5790) --- .changeset/five-mice-turn.md | 5 +++++ packages/extension-bubble-menu/src/bubble-menu-plugin.ts | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changeset/five-mice-turn.md diff --git a/.changeset/five-mice-turn.md b/.changeset/five-mice-turn.md new file mode 100644 index 00000000000..a7f0aa1aa6d --- /dev/null +++ b/.changeset/five-mice-turn.md @@ -0,0 +1,5 @@ +--- +"@tiptap/extension-bubble-menu": patch +--- + +Add `element: HTMLElement` to `shouldShow` options within the BubbleMenu options. diff --git a/packages/extension-bubble-menu/src/bubble-menu-plugin.ts b/packages/extension-bubble-menu/src/bubble-menu-plugin.ts index 4b8f8d9800c..1bcfe71097c 100644 --- a/packages/extension-bubble-menu/src/bubble-menu-plugin.ts +++ b/packages/extension-bubble-menu/src/bubble-menu-plugin.ts @@ -46,6 +46,7 @@ export interface BubbleMenuPluginProps { shouldShow?: | ((props: { editor: Editor + element: HTMLElement view: EditorView state: EditorState oldState?: EditorState @@ -238,6 +239,7 @@ export class BubbleMenuView { const shouldShow = this.shouldShow?.({ editor: this.editor, + element: this.element, view, state, oldState, From e5228ea6be571bcde7b72c824c8085a4717c71b5 Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Mon, 4 Nov 2024 13:49:58 +0100 Subject: [PATCH 03/17] feat: accessibility improvements (#5758) --- packages/core/src/Editor.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts index f7cddb7238e..d3378a47b0d 100644 --- a/packages/core/src/Editor.ts +++ b/packages/core/src/Editor.ts @@ -360,6 +360,11 @@ export class Editor extends EventEmitter { this.view = new EditorView(this.options.element, { ...this.options.editorProps, + attributes: { + // add `role="textbox"` to the editor element + role: 'textbox', + ...this.options.editorProps?.attributes, + }, dispatchTransaction: this.dispatchTransaction.bind(this), state: EditorState.create({ doc, @@ -367,14 +372,6 @@ export class Editor extends EventEmitter { }), }) - // add `role="textbox"` to the editor element - this.view.dom.setAttribute('role', 'textbox') - - // add aria-label to the editor element - if (!this.view.dom.getAttribute('aria-label')) { - this.view.dom.setAttribute('aria-label', 'Rich-Text Editor') - } - // `editor.view` is not yet available at this time. // Therefore we will add all plugins and node views directly afterwards. const newState = this.state.reconfigure({ From ddd3d713e576d2ad80540d58ae0e9dc9fc0a5f76 Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Wed, 6 Nov 2024 12:48:37 +0100 Subject: [PATCH 04/17] fix(react): allow react 19 (#5807) --- .changeset/polite-buttons-wash.md | 5 +++++ package-lock.json | 4 ++-- packages/react/package.json | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/polite-buttons-wash.md diff --git a/.changeset/polite-buttons-wash.md b/.changeset/polite-buttons-wash.md new file mode 100644 index 00000000000..69aa677ea1c --- /dev/null +++ b/.changeset/polite-buttons-wash.md @@ -0,0 +1,5 @@ +--- +"@tiptap/react": patch +--- + +React 19 is now allowed as a peer dep, we did not have to make any changes for React 19 diff --git a/package-lock.json b/package-lock.json index 082a62505d7..f210c38cba6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19762,8 +19762,8 @@ "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "packages/starter-kit": { diff --git a/packages/react/package.json b/packages/react/package.json index 350739d702a..38647da7e35 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -46,8 +46,8 @@ "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "repository": { "type": "git", From 5a04885b9413a87b2f2e6fd41868e95d18e29ea6 Mon Sep 17 00:00:00 2001 From: solvsoft <41623271+solvsoft@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:00:40 +0100 Subject: [PATCH 05/17] fix(vue): pin vue-ts-types version (#5800) --- packages/vue-2/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-2/package.json b/packages/vue-2/package.json index 995e50341e1..8d47a705e15 100644 --- a/packages/vue-2/package.json +++ b/packages/vue-2/package.json @@ -31,7 +31,7 @@ "dependencies": { "@tiptap/extension-bubble-menu": "^2.9.1", "@tiptap/extension-floating-menu": "^2.9.1", - "vue-ts-types": "^1.6.0" + "vue-ts-types": "1.6.2" }, "devDependencies": { "@tiptap/core": "^2.9.1", From 4ee59c1f7d0aa8dcfea013ff4378bf1cd8d7eeb9 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 6 Nov 2024 13:01:37 +0100 Subject: [PATCH 06/17] chore: add changeset & package-lock --- .changeset/mean-moose-bow.md | 5 +++++ package-lock.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/mean-moose-bow.md diff --git a/.changeset/mean-moose-bow.md b/.changeset/mean-moose-bow.md new file mode 100644 index 00000000000..6598c1e0e30 --- /dev/null +++ b/.changeset/mean-moose-bow.md @@ -0,0 +1,5 @@ +--- +"@tiptap/vue-2": patch +--- + +Pin vue-ts-types to a working version for vue-2 diff --git a/package-lock.json b/package-lock.json index f210c38cba6..bf66efc1bb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19822,7 +19822,7 @@ "dependencies": { "@tiptap/extension-bubble-menu": "^2.9.1", "@tiptap/extension-floating-menu": "^2.9.1", - "vue-ts-types": "^1.6.0" + "vue-ts-types": "1.6.2" }, "devDependencies": { "@tiptap/core": "^2.9.1", From 444e6e5a11d6c323cd298aa5106fc168a83640e8 Mon Sep 17 00:00:00 2001 From: Armando Guarino Date: Wed, 6 Nov 2024 13:42:27 +0100 Subject: [PATCH 07/17] refactor: adjust validate and add shouldAutoLink to improve URL handling --- .changeset/witty-olives-protect.md | 6 ++ demos/src/Marks/Link/React/index.jsx | 55 +++++++++++++++++++ demos/src/Marks/Link/React/index.spec.js | 42 +++++++++++--- .../extension-link/src/helpers/autolink.ts | 3 + packages/extension-link/src/link.ts | 30 +++++++--- 5 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 .changeset/witty-olives-protect.md diff --git a/.changeset/witty-olives-protect.md b/.changeset/witty-olives-protect.md new file mode 100644 index 00000000000..cb156c4bbb1 --- /dev/null +++ b/.changeset/witty-olives-protect.md @@ -0,0 +1,6 @@ +--- +"@tiptap/extension-link": patch +"tiptap-demos": patch +--- + +Refactor validate and add shouldAutoLink function to improve URL handling diff --git a/demos/src/Marks/Link/React/index.jsx b/demos/src/Marks/Link/React/index.jsx index 452bb5b4bc2..72a153fea1f 100644 --- a/demos/src/Marks/Link/React/index.jsx +++ b/demos/src/Marks/Link/React/index.jsx @@ -20,6 +20,61 @@ export default () => { openOnClick: false, autolink: true, defaultProtocol: 'https', + protocols: ['http', 'https'], + validate: (url, ctx) => { + try { + // construct URL + const parsedUrl = url.startsWith('http') ? new URL(url) : new URL(`${ctx.defaultProtocol}://${url}`) + + // use default validation + if (!ctx.defaultValidate(parsedUrl.href)) { + return false + } + + // disallowed protocols + const disallowedProtocols = ['ftp', 'file', 'mailto'] + const protocol = parsedUrl.protocol.replace(':', '') + + if (disallowedProtocols.includes(protocol)) { + return false + } + + // only allow protocols specified in ctx.protocols + const allowedProtocols = ctx.protocols.map(p => (typeof p === 'string' ? p : p.scheme)) + + if (!allowedProtocols.includes(protocol)) { + return false + } + + // disallowed domains + const disallowedDomains = ['example-phishing.com', 'malicious-site.net'] + const domain = parsedUrl.hostname + + if (disallowedDomains.includes(domain)) { + return false + } + + // all checks have passed + return true + } catch (error) { + return false + } + }, + shouldAutoLink: url => { + try { + // construct URL + const parsedUrl = url.startsWith('http') ? new URL(url) : new URL(`https://${url}`) + + // only auto-link if the domain is not in the disallowed list + const disallowedDomains = ['example-no-autolink.com', 'another-no-autolink.com'] + const domain = parsedUrl.hostname + + return !disallowedDomains.includes(domain) + } catch (error) { + return false + } + }, + }), ], content: ` diff --git a/demos/src/Marks/Link/React/index.spec.js b/demos/src/Marks/Link/React/index.spec.js index 61fd1b281ff..bc13e1af8c7 100644 --- a/demos/src/Marks/Link/React/index.spec.js +++ b/demos/src/Marks/Link/React/index.spec.js @@ -12,27 +12,27 @@ context('/src/Marks/Link/React/', () => { it('should parse a tags correctly', () => { cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('

Example Text1

') + editor.commands.setContent('

Example Text1

') expect(editor.getHTML()).to.eq( - '

Example Text1

', + '

Example Text1

', ) }) }) it('should parse a tags with target attribute correctly', () => { cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('

Example Text2

') + editor.commands.setContent('

Example Text2

') expect(editor.getHTML()).to.eq( - '

Example Text2

', + '

Example Text2

', ) }) }) it('should parse a tags with rel attribute correctly', () => { cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('

Example Text3

') + editor.commands.setContent('

Example Text3

') expect(editor.getHTML()).to.eq( - '

Example Text3

', + '

Example Text3

', ) }) }) @@ -54,7 +54,7 @@ context('/src/Marks/Link/React/', () => { it('should allow exiting the link once set', () => { cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('

Example Text2

') + editor.commands.setContent('

Example Text2

') cy.get('.tiptap').type('{rightArrow}') cy.get('button:first').should('not.have.class', 'is-active') @@ -129,4 +129,32 @@ context('/src/Marks/Link/React/', () => { .find('a[href="http://example3.com/foobar"]') .should('contain', 'http://example3.com/foobar') }) + + it('should not allow links with disallowed protocols', () => { + const disallowedProtocols = ['ftp://example.com', 'file:///example.txt', 'mailto:test@example.com'] + + disallowedProtocols.forEach(url => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent(`

Example Text

`) + expect(editor.getHTML()).to.not.include(``) + }) + }) + }) + + it('should not allow links with disallowed domains', () => { + const disallowedDomains = ['https://example-phishing.com', 'https://malicious-site.net'] + + disallowedDomains.forEach(url => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent(`

Example Text

`) + expect(editor.getHTML()).to.not.include(``) + }) + }) + }) + + it('should not auto-link a URL from a disallowed domain', () => { + cy.get('.tiptap').type('https://example-phishing.com ') // disallowed domain + cy.get('.tiptap').should('not.have.descendants', 'a') + cy.get('.tiptap').should('contain.text', 'https://example-phishing.com') + }) }) diff --git a/packages/extension-link/src/helpers/autolink.ts b/packages/extension-link/src/helpers/autolink.ts index c15efa9a17b..7b57007a5cd 100644 --- a/packages/extension-link/src/helpers/autolink.ts +++ b/packages/extension-link/src/helpers/autolink.ts @@ -35,6 +35,7 @@ type AutolinkOptions = { type: MarkType defaultProtocol: string validate: (url: string) => boolean + shouldAutoLink: (url: string) => boolean } /** @@ -144,6 +145,8 @@ export function autolink(options: AutolinkOptions): Plugin { }) // validate link .filter(link => options.validate(link.value)) + // check whether should autolink + .filter(link => options.shouldAutoLink(link.value)) // Add link mark. .forEach(link => { if (getMarksBetween(link.from, link.to, newState.doc).some(item => item.mark.type === options.type)) { diff --git a/packages/extension-link/src/link.ts b/packages/extension-link/src/link.ts index f78dc1db0b3..353e559799e 100644 --- a/packages/extension-link/src/link.ts +++ b/packages/extension-link/src/link.ts @@ -75,9 +75,21 @@ export interface LinkOptions { /** * A validation function that modifies link verification for the auto linker. * @param url - The url to be validated. + * @param ctx - An object containing: + * - `defaultValidate`: A function that performs the default URL validation. + * - `protocols`: An array of allowed protocols for the URL (e.g., "http", "https"). + * - `defaultProtocol`: A string that represents the default protocol (e.g. 'http') * @returns - True if the url is valid, false otherwise. */ - validate: (url: string) => boolean + validate: (url: string, ctx: { defaultValidate: (url: string) => boolean, protocols: Array, defaultProtocol: string }) => boolean + + /** + * Determines whether a valid link should be automatically linked in the content. + * + * @param url - The URL that has already been validated. + * @returns - True if the link should be auto-linked; false if it should not be auto-linked. + */ + shouldAutoLink: (url: string) => boolean } declare module '@tiptap/core' { @@ -169,7 +181,8 @@ export const Link = Mark.create({ rel: 'noopener noreferrer nofollow', class: null, }, - validate: url => !!url, + validate: (url, ctx) => !!isAllowedUri(url, ctx.protocols), + shouldAutoLink: url => !!url, } }, @@ -200,7 +213,7 @@ export const Link = Mark.create({ const href = (dom as HTMLElement).getAttribute('href') // prevent XSS attacks - if (!href || !isAllowedUri(href, this.options.protocols)) { + if (!href || !this.options.validate(href, { defaultValidate: url => !!isAllowedUri(url, this.options.protocols), protocols: this.options.protocols, defaultProtocol: this.options.defaultProtocol })) { return false } return null @@ -210,7 +223,7 @@ export const Link = Mark.create({ renderHTML({ HTMLAttributes }) { // prevent XSS attacks - if (!isAllowedUri(HTMLAttributes.href, this.options.protocols)) { + if (!this.options.validate(HTMLAttributes.href, { defaultValidate: href => !!isAllowedUri(href, this.options.protocols), protocols: this.options.protocols, defaultProtocol: this.options.defaultProtocol })) { // strip out the href return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0] } @@ -250,8 +263,9 @@ export const Link = Mark.create({ const foundLinks: PasteRuleMatch[] = [] if (text) { - const { validate } = this.options - const links = find(text).filter(item => item.isLink && validate(item.value)) + console.log(text) + const { validate, protocols, defaultProtocol } = this.options + const links = find(text).filter(item => item.isLink && validate(item.href, { defaultValidate: href => !!isAllowedUri(href, protocols), protocols, defaultProtocol })) if (links.length) { links.forEach(link => (foundLinks.push({ @@ -278,13 +292,15 @@ export const Link = Mark.create({ addProseMirrorPlugins() { const plugins: Plugin[] = [] + const { validate, protocols, defaultProtocol } = this.options if (this.options.autolink) { plugins.push( autolink({ type: this.type, defaultProtocol: this.options.defaultProtocol, - validate: this.options.validate, + validate: url => validate(url, { defaultValidate: href => !!isAllowedUri(href, protocols), protocols, defaultProtocol }), + shouldAutoLink: this.options.shouldAutoLink, }), ) } From 6bdb5917a51ad59e4a7f1ecaf826cef02f30a009 Mon Sep 17 00:00:00 2001 From: Armando Guarino Date: Wed, 6 Nov 2024 14:06:48 +0100 Subject: [PATCH 08/17] refactor: remove logs and fix typo --- packages/extension-link/src/link.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/extension-link/src/link.ts b/packages/extension-link/src/link.ts index 353e559799e..738d588d00b 100644 --- a/packages/extension-link/src/link.ts +++ b/packages/extension-link/src/link.ts @@ -263,9 +263,8 @@ export const Link = Mark.create({ const foundLinks: PasteRuleMatch[] = [] if (text) { - console.log(text) const { validate, protocols, defaultProtocol } = this.options - const links = find(text).filter(item => item.isLink && validate(item.href, { defaultValidate: href => !!isAllowedUri(href, protocols), protocols, defaultProtocol })) + const links = find(text).filter(item => item.isLink && validate(item.value, { defaultValidate: href => !!isAllowedUri(href, protocols), protocols, defaultProtocol })) if (links.length) { links.forEach(link => (foundLinks.push({ From 035862b6985c8ced2b81c351393601ef6bd66d01 Mon Sep 17 00:00:00 2001 From: Armando Guarino Date: Wed, 6 Nov 2024 14:11:48 +0100 Subject: [PATCH 09/17] refactor: update tests url checking logic and comments --- demos/src/Marks/Link/React/index.spec.js | 4 ++-- packages/extension-link/src/link.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demos/src/Marks/Link/React/index.spec.js b/demos/src/Marks/Link/React/index.spec.js index bc13e1af8c7..ea5bf905771 100644 --- a/demos/src/Marks/Link/React/index.spec.js +++ b/demos/src/Marks/Link/React/index.spec.js @@ -136,7 +136,7 @@ context('/src/Marks/Link/React/', () => { disallowedProtocols.forEach(url => { cy.get('.tiptap').then(([{ editor }]) => { editor.commands.setContent(`

Example Text

`) - expect(editor.getHTML()).to.not.include(``) + expect(editor.getHTML()).to.not.include(url) }) }) }) @@ -147,7 +147,7 @@ context('/src/Marks/Link/React/', () => { disallowedDomains.forEach(url => { cy.get('.tiptap').then(([{ editor }]) => { editor.commands.setContent(`

Example Text

`) - expect(editor.getHTML()).to.not.include(``) + expect(editor.getHTML()).to.not.include(url) }) }) }) diff --git a/packages/extension-link/src/link.ts b/packages/extension-link/src/link.ts index 738d588d00b..f0500da541f 100644 --- a/packages/extension-link/src/link.ts +++ b/packages/extension-link/src/link.ts @@ -73,7 +73,7 @@ export interface LinkOptions { HTMLAttributes: Record /** - * A validation function that modifies link verification for the auto linker. + * A validation function that modifies link verification. * @param url - The url to be validated. * @param ctx - An object containing: * - `defaultValidate`: A function that performs the default URL validation. From 6b8ce3778e952b7185ea57558f2b4a96a690658e Mon Sep 17 00:00:00 2001 From: Armando Guarino Date: Wed, 6 Nov 2024 14:14:59 +0100 Subject: [PATCH 10/17] refactor: add jsdoc validate and shouldAutoLink comment --- packages/extension-link/src/link.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/extension-link/src/link.ts b/packages/extension-link/src/link.ts index f0500da541f..76368488219 100644 --- a/packages/extension-link/src/link.ts +++ b/packages/extension-link/src/link.ts @@ -74,20 +74,22 @@ export interface LinkOptions { /** * A validation function that modifies link verification. - * @param url - The url to be validated. - * @param ctx - An object containing: - * - `defaultValidate`: A function that performs the default URL validation. - * - `protocols`: An array of allowed protocols for the URL (e.g., "http", "https"). - * - `defaultProtocol`: A string that represents the default protocol (e.g. 'http') - * @returns - True if the url is valid, false otherwise. + * + * @param {string} url - The URL to be validated. + * @param {Object} ctx - An object containing: + * @param {Function} ctx.defaultValidate - A function that performs the default URL validation. + * @param {string[]} ctx.protocols - An array of allowed protocols for the URL (e.g., "http", "https"). + * @param {string} ctx.defaultProtocol - A string that represents the default protocol (e.g., 'http'). + * + * @returns {boolean} True if the URL is valid, false otherwise. */ validate: (url: string, ctx: { defaultValidate: (url: string) => boolean, protocols: Array, defaultProtocol: string }) => boolean /** * Determines whether a valid link should be automatically linked in the content. * - * @param url - The URL that has already been validated. - * @returns - True if the link should be auto-linked; false if it should not be auto-linked. + * @param {string} url - The URL that has already been validated. + * @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked. */ shouldAutoLink: (url: string) => boolean } From efac420c9f745c189a696ca424b544279dea7efb Mon Sep 17 00:00:00 2001 From: Armando Guarino Date: Wed, 6 Nov 2024 14:16:39 +0100 Subject: [PATCH 11/17] chore: improve changeset description --- .changeset/witty-olives-protect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/witty-olives-protect.md b/.changeset/witty-olives-protect.md index cb156c4bbb1..0893a2fdef4 100644 --- a/.changeset/witty-olives-protect.md +++ b/.changeset/witty-olives-protect.md @@ -3,4 +3,4 @@ "tiptap-demos": patch --- -Refactor validate and add shouldAutoLink function to improve URL handling +The link extension's `validate` option now applies to both auto-linking and XSS mitigation. While, the new `shouldAutoLink` option is used to disable auto linking on an otherwise valid url. From 585f6ef77e9e02e98ef19313fffc535ed23ab52f Mon Sep 17 00:00:00 2001 From: Armando Guarino Date: Wed, 6 Nov 2024 15:08:30 +0100 Subject: [PATCH 12/17] test: improve url parsing logic --- demos/src/Marks/Link/React/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/src/Marks/Link/React/index.jsx b/demos/src/Marks/Link/React/index.jsx index 72a153fea1f..d9a57661a30 100644 --- a/demos/src/Marks/Link/React/index.jsx +++ b/demos/src/Marks/Link/React/index.jsx @@ -24,7 +24,7 @@ export default () => { validate: (url, ctx) => { try { // construct URL - const parsedUrl = url.startsWith('http') ? new URL(url) : new URL(`${ctx.defaultProtocol}://${url}`) + const parsedUrl = url.includes(':') ? new URL(url) : new URL(`${ctx.defaultProtocol}://${url}`) // use default validation if (!ctx.defaultValidate(parsedUrl.href)) { @@ -63,7 +63,7 @@ export default () => { shouldAutoLink: url => { try { // construct URL - const parsedUrl = url.startsWith('http') ? new URL(url) : new URL(`https://${url}`) + const parsedUrl = url.includes(':') ? new URL(url) : new URL(`https://${url}`) // only auto-link if the domain is not in the disallowed list const disallowedDomains = ['example-no-autolink.com', 'another-no-autolink.com'] From 94a8d258f8cd4549de24d11e2d591ba4363250e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Le=20Ma=C3=AEtre?= Date: Thu, 7 Nov 2024 09:19:46 +0100 Subject: [PATCH 13/17] fix(vue-3): on editor destruction, transition smoothly (#5772) --- .changeset/five-flowers-eat.md | 5 ++ CONTRIBUTING.md | 2 +- .../src/Examples/Transition/Vue/Extension.js | 2 +- .../Transition/Vue/ParentComponent.vue | 36 +++++++++++++++ .../Vue/{Component.vue => VueComponent.vue} | 0 .../src/Examples/Transition/Vue/index.spec.js | 18 +++++--- demos/src/Examples/Transition/Vue/index.vue | 46 +++++++++++++++---- packages/vue-3/src/EditorContent.ts | 1 - packages/vue-3/src/useEditor.ts | 6 +++ 9 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 .changeset/five-flowers-eat.md create mode 100644 demos/src/Examples/Transition/Vue/ParentComponent.vue rename demos/src/Examples/Transition/Vue/{Component.vue => VueComponent.vue} (100%) diff --git a/.changeset/five-flowers-eat.md b/.changeset/five-flowers-eat.md new file mode 100644 index 00000000000..a44e7c58d8f --- /dev/null +++ b/.changeset/five-flowers-eat.md @@ -0,0 +1,5 @@ +--- +"@tiptap/vue-3": patch +--- + +Fix editor destruction before transition end if editor is nested diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a7ab416a06..e59b86f408a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,7 @@ Before submitting a pull request: - Check the codebase to ensure that your feature doesn't already exist. - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. -Before commiting: +Before committing: - Make sure to run the tests and linter before committing your changes. - If you are making changes to one of the packages, make sure to **always** include a [changeset](https://github.com/changesets/changesets) in your PR describing **what changed** with a **description** of the change. Those are responsible for changelog creation diff --git a/demos/src/Examples/Transition/Vue/Extension.js b/demos/src/Examples/Transition/Vue/Extension.js index 9dede7bb0d0..b83694ad721 100644 --- a/demos/src/Examples/Transition/Vue/Extension.js +++ b/demos/src/Examples/Transition/Vue/Extension.js @@ -1,7 +1,7 @@ import { mergeAttributes, Node } from '@tiptap/core' import { VueNodeViewRenderer } from '@tiptap/vue-3' -import Component from './Component.vue' +import Component from './VueComponent.vue' export default Node.create({ name: 'vueComponent', diff --git a/demos/src/Examples/Transition/Vue/ParentComponent.vue b/demos/src/Examples/Transition/Vue/ParentComponent.vue new file mode 100644 index 00000000000..7cebbc9a95b --- /dev/null +++ b/demos/src/Examples/Transition/Vue/ParentComponent.vue @@ -0,0 +1,36 @@ + + + diff --git a/demos/src/Examples/Transition/Vue/Component.vue b/demos/src/Examples/Transition/Vue/VueComponent.vue similarity index 100% rename from demos/src/Examples/Transition/Vue/Component.vue rename to demos/src/Examples/Transition/Vue/VueComponent.vue diff --git a/demos/src/Examples/Transition/Vue/index.spec.js b/demos/src/Examples/Transition/Vue/index.spec.js index 5bceb87e198..39100317dfb 100644 --- a/demos/src/Examples/Transition/Vue/index.spec.js +++ b/demos/src/Examples/Transition/Vue/index.spec.js @@ -3,26 +3,30 @@ context('/src/Examples/Transition/Vue/', () => { cy.visit('/src/Examples/Transition/Vue/') }) - it('should not have an active tiptap instance but a button', () => { + it('should have two buttons and no active tiptap instance', () => { cy.get('.tiptap').should('not.exist') - cy.get('#toggle-editor').should('exist') + cy.get('#toggle-direct-editor').should('exist') + cy.get('#toggle-nested-editor').should('exist') }) - it('clicking the button should show the editor', () => { - cy.get('#toggle-editor').click() + it('clicking the buttons should show two editors', () => { + cy.get('#toggle-direct-editor').click() + cy.get('#toggle-nested-editor').click() cy.get('.tiptap').should('exist') cy.get('.tiptap').should('be.visible') }) - it('clicking the button again should hide the editor', () => { - cy.get('#toggle-editor').click() + it('clicking the buttons again should hide the editors', () => { + cy.get('#toggle-direct-editor').click() + cy.get('#toggle-nested-editor').click() cy.get('.tiptap').should('exist') cy.get('.tiptap').should('be.visible') - cy.get('#toggle-editor').click() + cy.get('#toggle-direct-editor').click() + cy.get('#toggle-nested-editor').click() cy.get('.tiptap').should('not.exist') }) diff --git a/demos/src/Examples/Transition/Vue/index.vue b/demos/src/Examples/Transition/Vue/index.vue index 46e65dc0c5b..6bd914949bc 100644 --- a/demos/src/Examples/Transition/Vue/index.vue +++ b/demos/src/Examples/Transition/Vue/index.vue @@ -1,12 +1,18 @@