feat: add support for checking for attributes in extendMarkRange
This commit is contained in:
@@ -1,3 +1,20 @@
|
|||||||
# extendMarkRange
|
# extendMarkRange
|
||||||
|
The `extendMarkRange` command expands the current selection to encompass the current mark. If the current selection doesn’t have the specified mark, nothing changes.
|
||||||
|
|
||||||
<ContentMissing />
|
## Parameters
|
||||||
|
`typeOrName: string | MarkType`
|
||||||
|
|
||||||
|
Name or type of the mark.
|
||||||
|
|
||||||
|
`attributes?: Record<string, any>`
|
||||||
|
|
||||||
|
Optionally, you can specify attributes that the extented mark must contain.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```js
|
||||||
|
// Expand selection to link marks
|
||||||
|
editor.commands.extendMarkRange('link')
|
||||||
|
|
||||||
|
// Expand selection to link marks with specific attributes
|
||||||
|
editor.commands.extendMarkRange('link', { href: 'https://google.com' })
|
||||||
|
```
|
||||||
|
|||||||
@@ -151,7 +151,6 @@
|
|||||||
type: draft
|
type: draft
|
||||||
- title: extendMarkRange
|
- title: extendMarkRange
|
||||||
link: /api/commands/extend-mark-range
|
link: /api/commands/extend-mark-range
|
||||||
type: draft
|
|
||||||
- title: focus
|
- title: focus
|
||||||
link: /api/commands/focus
|
link: /api/commands/focus
|
||||||
type: draft
|
type: draft
|
||||||
|
|||||||
@@ -10,20 +10,20 @@ declare module '@tiptap/core' {
|
|||||||
/**
|
/**
|
||||||
* Extends the text selection to the current mark.
|
* Extends the text selection to the current mark.
|
||||||
*/
|
*/
|
||||||
extendMarkRange: (typeOrName: string | MarkType) => Command,
|
extendMarkRange: (typeOrName: string | MarkType, attributes?: Record<string, any>) => Command,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extendMarkRange: RawCommands['extendMarkRange'] = typeOrName => ({ tr, state, dispatch }) => {
|
export const extendMarkRange: RawCommands['extendMarkRange'] = (typeOrName, attributes = {}) => ({ tr, state, dispatch }) => {
|
||||||
const type = getMarkType(typeOrName, state.schema)
|
const type = getMarkType(typeOrName, state.schema)
|
||||||
const { doc, selection } = tr
|
const { doc, selection } = tr
|
||||||
const { $from, empty } = selection
|
const { $from, from, to } = selection
|
||||||
|
|
||||||
if (empty && dispatch) {
|
if (dispatch) {
|
||||||
const range = getMarkRange($from, type)
|
const range = getMarkRange($from, type, attributes)
|
||||||
|
|
||||||
if (range) {
|
if (range && range.from <= from && range.to >= to) {
|
||||||
const newSelection = TextSelection.create(doc, range.from, range.to)
|
const newSelection = TextSelection.create(doc, range.from, range.to)
|
||||||
|
|
||||||
tr.setSelection(newSelection)
|
tr.setSelection(newSelection)
|
||||||
|
|||||||
31
packages/core/src/helpers/getDebugJSON.ts
Normal file
31
packages/core/src/helpers/getDebugJSON.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Node as ProseMirrorNode } from 'prosemirror-model'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a node tree with node positions.
|
||||||
|
*/
|
||||||
|
export default function getDebugJSON(node: ProseMirrorNode) {
|
||||||
|
const debug = (startNode: ProseMirrorNode, startOffset = 0) => {
|
||||||
|
const nodes: any[] = []
|
||||||
|
|
||||||
|
startNode.forEach((n, offset) => {
|
||||||
|
const from = startOffset + offset
|
||||||
|
const to = from + n.nodeSize
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
type: n.type.name,
|
||||||
|
attrs: { ...n.attrs },
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
marks: n.marks.map(mark => ({
|
||||||
|
type: mark.type.name,
|
||||||
|
attrs: { ...mark.attrs },
|
||||||
|
})),
|
||||||
|
content: debug(n, from + 1),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
return debug(node)
|
||||||
|
}
|
||||||
@@ -1,7 +1,30 @@
|
|||||||
import { MarkType, ResolvedPos } from 'prosemirror-model'
|
import { Mark as ProseMirrorMark, MarkType, ResolvedPos } from 'prosemirror-model'
|
||||||
|
import objectIncludes from '../utilities/objectIncludes'
|
||||||
import { Range } from '../types'
|
import { Range } from '../types'
|
||||||
|
|
||||||
export default function getMarkRange($pos: ResolvedPos, type: MarkType): Range | void {
|
function findMarkInSet(
|
||||||
|
marks: ProseMirrorMark[],
|
||||||
|
type: MarkType,
|
||||||
|
attributes: Record<string, any> = {},
|
||||||
|
): ProseMirrorMark | undefined {
|
||||||
|
return marks.find(item => {
|
||||||
|
return item.type === type && objectIncludes(item.attrs, attributes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMarkInSet(
|
||||||
|
marks: ProseMirrorMark[],
|
||||||
|
type: MarkType,
|
||||||
|
attributes: Record<string, any> = {},
|
||||||
|
): boolean {
|
||||||
|
return !!findMarkInSet(marks, type, attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function getMarkRange(
|
||||||
|
$pos: ResolvedPos,
|
||||||
|
type: MarkType,
|
||||||
|
attributes: Record<string, any> = {},
|
||||||
|
): Range | void {
|
||||||
if (!$pos || !type) {
|
if (!$pos || !type) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -12,9 +35,9 @@ export default function getMarkRange($pos: ResolvedPos, type: MarkType): Range |
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const link = start.node.marks.find(mark => mark.type === type)
|
const mark = findMarkInSet(start.node.marks, type, attributes)
|
||||||
|
|
||||||
if (!link) {
|
if (!mark) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,12 +46,17 @@ export default function getMarkRange($pos: ResolvedPos, type: MarkType): Range |
|
|||||||
let endIndex = startIndex + 1
|
let endIndex = startIndex + 1
|
||||||
let endPos = startPos + start.node.nodeSize
|
let endPos = startPos + start.node.nodeSize
|
||||||
|
|
||||||
while (startIndex > 0 && link.isInSet($pos.parent.child(startIndex - 1).marks)) {
|
findMarkInSet(start.node.marks, type, attributes)
|
||||||
|
|
||||||
|
while (startIndex > 0 && mark.isInSet($pos.parent.child(startIndex - 1).marks)) {
|
||||||
startIndex -= 1
|
startIndex -= 1
|
||||||
startPos -= $pos.parent.child(startIndex).nodeSize
|
startPos -= $pos.parent.child(startIndex).nodeSize
|
||||||
}
|
}
|
||||||
|
|
||||||
while (endIndex < $pos.parent.childCount && link.isInSet($pos.parent.child(endIndex).marks)) {
|
while (
|
||||||
|
endIndex < $pos.parent.childCount
|
||||||
|
&& isMarkInSet($pos.parent.child(endIndex).marks, type, attributes)
|
||||||
|
) {
|
||||||
endPos += $pos.parent.child(endIndex).nodeSize
|
endPos += $pos.parent.child(endIndex).nodeSize
|
||||||
endIndex += 1
|
endIndex += 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export { default as generateHTML } from './helpers/generateHTML'
|
|||||||
export { default as generateJSON } from './helpers/generateJSON'
|
export { default as generateJSON } from './helpers/generateJSON'
|
||||||
export { default as getSchema } from './helpers/getSchema'
|
export { default as getSchema } from './helpers/getSchema'
|
||||||
export { default as getHTMLFromFragment } from './helpers/getHTMLFromFragment'
|
export { default as getHTMLFromFragment } from './helpers/getHTMLFromFragment'
|
||||||
|
export { default as getDebugJSON } from './helpers/getDebugJSON'
|
||||||
export { default as getAttributes } from './helpers/getAttributes'
|
export { default as getAttributes } from './helpers/getAttributes'
|
||||||
export { default as getMarkAttributes } from './helpers/getMarkAttributes'
|
export { default as getMarkAttributes } from './helpers/getMarkAttributes'
|
||||||
export { default as getMarkRange } from './helpers/getMarkRange'
|
export { default as getMarkRange } from './helpers/getMarkRange'
|
||||||
|
|||||||
301
tests/cypress/integration/core/extendMarkRange.spec.ts
Normal file
301
tests/cypress/integration/core/extendMarkRange.spec.ts
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import { Editor, getDebugJSON } from '@tiptap/core'
|
||||||
|
import Document from '@tiptap/extension-document'
|
||||||
|
import Paragraph from '@tiptap/extension-paragraph'
|
||||||
|
import Text from '@tiptap/extension-text'
|
||||||
|
import Link from '@tiptap/extension-link'
|
||||||
|
|
||||||
|
describe('extendMarkRange', () => {
|
||||||
|
it('should extend full mark', () => {
|
||||||
|
const content = {
|
||||||
|
type: 'doc',
|
||||||
|
content: [{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
attrs: {
|
||||||
|
href: 'foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
attrs: {
|
||||||
|
href: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = new Editor({
|
||||||
|
content,
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Paragraph,
|
||||||
|
Text,
|
||||||
|
Link,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// debug
|
||||||
|
// console.log(getDebugJSON(editor.state.doc))
|
||||||
|
|
||||||
|
// set cursor in middle of first mark
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.setTextSelection({ from: 7, to: 7 })
|
||||||
|
.extendMarkRange('link')
|
||||||
|
.run()
|
||||||
|
|
||||||
|
const { from, to } = editor.state.selection
|
||||||
|
|
||||||
|
const expectedSelection = {
|
||||||
|
from: 5,
|
||||||
|
to: 13,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect({ from, to }).to.deep.eq(expectedSelection)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should extend to mark with specific attributes', () => {
|
||||||
|
const content = {
|
||||||
|
type: 'doc',
|
||||||
|
content: [{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
attrs: {
|
||||||
|
href: 'foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
attrs: {
|
||||||
|
href: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = new Editor({
|
||||||
|
content,
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Paragraph,
|
||||||
|
Text,
|
||||||
|
Link,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// debug
|
||||||
|
// console.log(getDebugJSON(editor.state.doc))
|
||||||
|
|
||||||
|
// set cursor in middle of first mark
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.setTextSelection({ from: 7, to: 7 })
|
||||||
|
.extendMarkRange('link', {
|
||||||
|
href: 'foo',
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
|
||||||
|
const { from, to } = editor.state.selection
|
||||||
|
|
||||||
|
const expectedSelection = {
|
||||||
|
from: 5,
|
||||||
|
to: 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect({ from, to }).to.deep.eq(expectedSelection)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not extend at all if selection contains no mark', () => {
|
||||||
|
const content = {
|
||||||
|
type: 'doc',
|
||||||
|
content: [{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
attrs: {
|
||||||
|
href: 'foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
attrs: {
|
||||||
|
href: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = new Editor({
|
||||||
|
content,
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Paragraph,
|
||||||
|
Text,
|
||||||
|
Link,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// debug
|
||||||
|
// console.log(getDebugJSON(editor.state.doc))
|
||||||
|
|
||||||
|
// set cursor before any mark
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.setTextSelection({ from: 2, to: 2 })
|
||||||
|
.extendMarkRange('link')
|
||||||
|
.run()
|
||||||
|
|
||||||
|
const { from, to } = editor.state.selection
|
||||||
|
|
||||||
|
const expectedSelection = {
|
||||||
|
from: 2,
|
||||||
|
to: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect({ from, to }).to.deep.eq(expectedSelection)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not extend at all if selection contains any non-matching mark', () => {
|
||||||
|
const content = {
|
||||||
|
type: 'doc',
|
||||||
|
content: [{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
attrs: {
|
||||||
|
href: 'foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
attrs: {
|
||||||
|
href: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = new Editor({
|
||||||
|
content,
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Paragraph,
|
||||||
|
Text,
|
||||||
|
Link,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// debug
|
||||||
|
// console.log(getDebugJSON(editor.state.doc))
|
||||||
|
|
||||||
|
// set cursor before across non-matching marks
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.setTextSelection({ from: 7, to: 11 })
|
||||||
|
.extendMarkRange('link', {
|
||||||
|
href: 'foo',
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
|
||||||
|
const { from, to } = editor.state.selection
|
||||||
|
|
||||||
|
const expectedSelection = {
|
||||||
|
from: 7,
|
||||||
|
to: 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect({ from, to }).to.deep.eq(expectedSelection)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user