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:
Cameron Hessler
2022-09-26 04:08:10 -05:00
committed by GitHub
parent 348383b96c
commit 8f5fbef742
4 changed files with 179 additions and 53 deletions

View File

@@ -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 ( return (
<div> <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} /> <EditorContent editor={editor} />
</div> </div>
) )

View File

@@ -8,25 +8,30 @@ context('/src/Examples/AutolinkValidation/React/', () => {
}) })
const validLinks = [ const validLinks = [
'https://tiptap.dev', // [rawTextInput, textThatShouldBeLinked]
'http://tiptap.dev', ['https://tiptap.dev ', 'https://tiptap.dev'],
'https://www.tiptap.dev/', ['http://tiptap.dev ', 'http://tiptap.dev'],
'http://www.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 = [ const invalidLinks = [
'tiptap.dev', 'tiptap.dev',
'www.tiptap.dev', 'www.tiptap.dev',
// If you don't type a space, don't autolink
'https://tiptap.dev',
] ]
validLinks.forEach(link => { validLinks.forEach(link => {
it(`${link} should get autolinked`, () => { it(`${link[0]} should get autolinked`, () => {
cy.get('.ProseMirror').type(link) cy.get('.ProseMirror').type(link[0])
cy.get('.ProseMirror').should('have.text', link) cy.get('.ProseMirror').should('have.text', link[0])
cy.get('.ProseMirror') cy.get('.ProseMirror')
.find('a') .find('a')
.should('have.length', 1) .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) .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')
})
}) })

View File

@@ -8,25 +8,30 @@ context('/src/Examples/AutolinkValidation/Vue/', () => {
}) })
const validLinks = [ const validLinks = [
'https://tiptap.dev', // [rawTextInput, textThatShouldBeLinked]
'http://tiptap.dev', ['https://tiptap.dev ', 'https://tiptap.dev'],
'https://www.tiptap.dev/', ['http://tiptap.dev ', 'http://tiptap.dev'],
'http://www.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 = [ const invalidLinks = [
'tiptap.dev', 'tiptap.dev',
'www.tiptap.dev', 'www.tiptap.dev',
// If you don't type a space, don't autolink
'https://tiptap.dev',
] ]
validLinks.forEach(link => { validLinks.forEach(link => {
it(`${link} should get autolinked`, () => { it(`${link[0]} should get autolinked`, () => {
cy.get('.ProseMirror').type(link) cy.get('.ProseMirror').type(link[0])
cy.get('.ProseMirror').should('have.text', link) cy.get('.ProseMirror').should('have.text', link[0])
cy.get('.ProseMirror') cy.get('.ProseMirror')
.find('a') .find('a')
.should('have.length', 1) .should('have.length', 1)
.should('have.attr', 'href', link) .should('have.attr', 'href', link[1])
}) })
}) })

View File

@@ -3,6 +3,7 @@ import {
findChildrenInRange, findChildrenInRange,
getChangedRanges, getChangedRanges,
getMarksBetween, getMarksBetween,
NodeWithPos,
} from '@tiptap/core' } from '@tiptap/core'
import { find, test } from 'linkifyjs' import { find, test } from 'linkifyjs'
import { MarkType } from 'prosemirror-model' import { MarkType } from 'prosemirror-model'
@@ -58,46 +59,59 @@ export function autolink(options: AutolinkOptions): Plugin {
}) })
// now lets see if we can add new links // now lets see if we can add new links
findChildrenInRange(newState.doc, newRange, node => node.isTextblock) const nodesInChangedRanges = 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(
textBlock.pos,
textBlock.pos + textBlock.node.nodeSize,
undefined,
' ',
)
find(text) let textBlock: NodeWithPos | undefined
.filter(link => link.isLink) let textBeforeWhitespace: string | undefined
.filter(link => {
if (options.validate) {
return options.validate(link.value)
}
return true if (nodesInChangedRanges.length > 1) {
}) // Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter)
// calculate link position textBlock = nodesInChangedRanges[0]
.map(link => ({ textBeforeWhitespace = newState.doc.textBetween(
...link, textBlock.pos,
from: textBlock.pos + link.start + 1, textBlock.pos + textBlock.node.nodeSize,
to: textBlock.pos + link.end + 1, 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,
' ',
)
}
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: lastWordAndBlockOffset + link.start + 1,
to: lastWordAndBlockOffset + link.end + 1,
}))
// add link mark
.forEach(link => {
tr.addMark(link.from, link.to, options.type.create({
href: link.href,
})) }))
// 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) { if (!tr.steps.length) {