feat(extension-link) Change autolink to only apply on space (#3232)
Co-authored-by: Cameron Hessler <cameron.hessler@buildertrend.com>
This commit is contained in:
@@ -23,8 +23,48 @@ export default () => {
|
||||
},
|
||||
})
|
||||
|
||||
const setLink = React.useCallback(() => {
|
||||
const previousUrl = editor.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
// cancelled
|
||||
if (url === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink()
|
||||
.run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url })
|
||||
.run()
|
||||
}, [editor])
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={setLink}
|
||||
className={editor.isActive('link') ? 'is-active' : ''}
|
||||
data-testid="setLink"
|
||||
>
|
||||
setLink
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().unsetLink().run()}
|
||||
disabled={!editor.isActive('link')}
|
||||
data-testid="unsetLink"
|
||||
>
|
||||
unsetLink
|
||||
</button>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -8,25 +8,30 @@ context('/src/Examples/AutolinkValidation/React/', () => {
|
||||
})
|
||||
|
||||
const validLinks = [
|
||||
'https://tiptap.dev',
|
||||
'http://tiptap.dev',
|
||||
'https://www.tiptap.dev/',
|
||||
'http://www.tiptap.dev/',
|
||||
// [rawTextInput, textThatShouldBeLinked]
|
||||
['https://tiptap.dev ', 'https://tiptap.dev'],
|
||||
['http://tiptap.dev ', 'http://tiptap.dev'],
|
||||
['https://www.tiptap.dev/ ', 'https://www.tiptap.dev/'],
|
||||
['http://www.tiptap.dev/ ', 'http://www.tiptap.dev/'],
|
||||
['[http://www.example.com/] ', 'http://www.example.com/'],
|
||||
['(http://www.example.com/) ', 'http://www.example.com/'],
|
||||
]
|
||||
|
||||
const invalidLinks = [
|
||||
'tiptap.dev',
|
||||
'www.tiptap.dev',
|
||||
// If you don't type a space, don't autolink
|
||||
'https://tiptap.dev',
|
||||
]
|
||||
|
||||
validLinks.forEach(link => {
|
||||
it(`${link} should get autolinked`, () => {
|
||||
cy.get('.ProseMirror').type(link)
|
||||
cy.get('.ProseMirror').should('have.text', link)
|
||||
it(`${link[0]} should get autolinked`, () => {
|
||||
cy.get('.ProseMirror').type(link[0])
|
||||
cy.get('.ProseMirror').should('have.text', link[0])
|
||||
cy.get('.ProseMirror')
|
||||
.find('a')
|
||||
.should('have.length', 1)
|
||||
.should('have.attr', 'href', link)
|
||||
.should('have.attr', 'href', link[1])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -39,4 +44,66 @@ context('/src/Examples/AutolinkValidation/React/', () => {
|
||||
.should('have.length', 0)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not relink unset links after entering second link', () => {
|
||||
cy.get('.ProseMirror').type('https://tiptap.dev {home}')
|
||||
cy.get('.ProseMirror').should('have.text', 'https://tiptap.dev ')
|
||||
cy.get('[data-testid=unsetLink]').click()
|
||||
cy.get('.ProseMirror')
|
||||
.find('a')
|
||||
.should('have.length', 0)
|
||||
cy.get('.ProseMirror').type('{end}http://www.example.com/ ')
|
||||
cy.get('.ProseMirror')
|
||||
.find('a')
|
||||
.should('have.length', 1)
|
||||
.should('have.attr', 'href', 'http://www.example.com/')
|
||||
})
|
||||
|
||||
it('should not relink unset links after hitting next paragraph', () => {
|
||||
cy.get('.ProseMirror').type('https://tiptap.dev {home}')
|
||||
cy.get('.ProseMirror').should('have.text', 'https://tiptap.dev ')
|
||||
cy.get('[data-testid=unsetLink]').click()
|
||||
cy.get('.ProseMirror')
|
||||
.find('a')
|
||||
.should('have.length', 0)
|
||||
cy.get('.ProseMirror').type('{end}typing other text should prevent the link from relinking when hitting enter{enter}')
|
||||
cy.get('.ProseMirror')
|
||||
.find('a')
|
||||
.should('have.length', 0)
|
||||
})
|
||||
|
||||
it('should not relink unset links after modifying', () => {
|
||||
cy.get('.ProseMirror').type('https://tiptap.dev {home}')
|
||||
cy.get('.ProseMirror').should('have.text', 'https://tiptap.dev ')
|
||||
cy.get('[data-testid=unsetLink]').click()
|
||||
cy.get('.ProseMirror')
|
||||
.find('a')
|
||||
.should('have.length', 0)
|
||||
cy.get('.ProseMirror')
|
||||
.type('{home}')
|
||||
.type('{rightArrow}'.repeat('https://'.length))
|
||||
.type('blah')
|
||||
cy.get('.ProseMirror').should('have.text', 'https://blahtiptap.dev ')
|
||||
cy.get('.ProseMirror')
|
||||
.find('a')
|
||||
.should('have.length', 0)
|
||||
})
|
||||
|
||||
it('should autolink after hitting enter (new paragraph)', () => {
|
||||
cy.get('.ProseMirror').type('https://tiptap.dev{enter}')
|
||||
cy.get('.ProseMirror').should('have.text', 'https://tiptap.dev')
|
||||
cy.get('.ProseMirror')
|
||||
.find('a')
|
||||
.should('have.length', 1)
|
||||
.should('have.attr', 'href', 'https://tiptap.dev')
|
||||
})
|
||||
|
||||
it('should autolink after hitting shift-enter (hardbreak)', () => {
|
||||
cy.get('.ProseMirror').type('https://tiptap.dev{shift+enter}')
|
||||
cy.get('.ProseMirror').should('have.text', 'https://tiptap.dev')
|
||||
cy.get('.ProseMirror')
|
||||
.find('a')
|
||||
.should('have.length', 1)
|
||||
.should('have.attr', 'href', 'https://tiptap.dev')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,25 +8,30 @@ context('/src/Examples/AutolinkValidation/Vue/', () => {
|
||||
})
|
||||
|
||||
const validLinks = [
|
||||
'https://tiptap.dev',
|
||||
'http://tiptap.dev',
|
||||
'https://www.tiptap.dev/',
|
||||
'http://www.tiptap.dev/',
|
||||
// [rawTextInput, textThatShouldBeLinked]
|
||||
['https://tiptap.dev ', 'https://tiptap.dev'],
|
||||
['http://tiptap.dev ', 'http://tiptap.dev'],
|
||||
['https://www.tiptap.dev/ ', 'https://www.tiptap.dev/'],
|
||||
['http://www.tiptap.dev/ ', 'http://www.tiptap.dev/'],
|
||||
['[http://www.example.com/] ', 'http://www.example.com/'],
|
||||
['(http://www.example.com/) ', 'http://www.example.com/'],
|
||||
]
|
||||
|
||||
const invalidLinks = [
|
||||
'tiptap.dev',
|
||||
'www.tiptap.dev',
|
||||
// If you don't type a space, don't autolink
|
||||
'https://tiptap.dev',
|
||||
]
|
||||
|
||||
validLinks.forEach(link => {
|
||||
it(`${link} should get autolinked`, () => {
|
||||
cy.get('.ProseMirror').type(link)
|
||||
cy.get('.ProseMirror').should('have.text', link)
|
||||
it(`${link[0]} should get autolinked`, () => {
|
||||
cy.get('.ProseMirror').type(link[0])
|
||||
cy.get('.ProseMirror').should('have.text', link[0])
|
||||
cy.get('.ProseMirror')
|
||||
.find('a')
|
||||
.should('have.length', 1)
|
||||
.should('have.attr', 'href', link)
|
||||
.should('have.attr', 'href', link[1])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
findChildrenInRange,
|
||||
getChangedRanges,
|
||||
getMarksBetween,
|
||||
NodeWithPos,
|
||||
} from '@tiptap/core'
|
||||
import { find, test } from 'linkifyjs'
|
||||
import { MarkType } from 'prosemirror-model'
|
||||
@@ -58,46 +59,59 @@ export function autolink(options: AutolinkOptions): Plugin {
|
||||
})
|
||||
|
||||
// now let’s see if we can add new links
|
||||
findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
|
||||
.forEach(textBlock => {
|
||||
// we need to define a placeholder for leaf nodes
|
||||
// so that the link position can be calculated correctly
|
||||
const text = newState.doc.textBetween(
|
||||
const nodesInChangedRanges = findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
|
||||
|
||||
let textBlock: NodeWithPos | undefined
|
||||
let textBeforeWhitespace: string | undefined
|
||||
|
||||
if (nodesInChangedRanges.length > 1) {
|
||||
// Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter)
|
||||
textBlock = nodesInChangedRanges[0]
|
||||
textBeforeWhitespace = newState.doc.textBetween(
|
||||
textBlock.pos,
|
||||
textBlock.pos + textBlock.node.nodeSize,
|
||||
undefined,
|
||||
' ',
|
||||
)
|
||||
} else if (
|
||||
// We want to make sure to include the block seperator argument to treat hard breaks like spaces
|
||||
newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ').endsWith(' ')
|
||||
) {
|
||||
textBlock = nodesInChangedRanges[0]
|
||||
textBeforeWhitespace = newState.doc.textBetween(
|
||||
textBlock.pos,
|
||||
newRange.to,
|
||||
undefined,
|
||||
' ',
|
||||
)
|
||||
}
|
||||
|
||||
find(text)
|
||||
if (textBlock && textBeforeWhitespace) {
|
||||
const wordsBeforeWhitespace = textBeforeWhitespace.split(' ').filter(s => s !== '')
|
||||
const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]
|
||||
const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)
|
||||
|
||||
find(lastWordBeforeSpace)
|
||||
.filter(link => link.isLink)
|
||||
.filter(link => {
|
||||
if (options.validate) {
|
||||
return options.validate(link.value)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
// calculate link position
|
||||
.map(link => ({
|
||||
...link,
|
||||
from: textBlock.pos + link.start + 1,
|
||||
to: textBlock.pos + link.end + 1,
|
||||
from: lastWordAndBlockOffset + link.start + 1,
|
||||
to: lastWordAndBlockOffset + link.end + 1,
|
||||
}))
|
||||
// check if link is within the changed range
|
||||
.filter(link => {
|
||||
const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to
|
||||
const toIsInRange = newRange.to >= link.from && newRange.to <= link.to
|
||||
|
||||
return fromIsInRange || toIsInRange
|
||||
})
|
||||
// add link mark
|
||||
.forEach(link => {
|
||||
tr.addMark(link.from, link.to, options.type.create({
|
||||
href: link.href,
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (!tr.steps.length) {
|
||||
|
||||
Reference in New Issue
Block a user