Skip to content

Commit

Permalink
fix(core): Can() does not work for setting marks
Browse files Browse the repository at this point in the history
Previously, setting marks did no schema validation checks for dry runs
(like the `.can()` command). The `setMark` raw command will now properly
check if the mark is possible to be set given the editor node/mark
schema.
  • Loading branch information
BTCameronHessler committed Sep 22, 2022
1 parent 95842e1 commit 67626ea
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 12 deletions.
50 changes: 48 additions & 2 deletions demos/src/Examples/Default/React/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,52 @@ const MenuBar = ({ editor }) => {
<>
<button
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleBold()
.run()
}
className={editor.isActive('bold') ? 'is-active' : ''}
>
bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleItalic()
.run()
}
className={editor.isActive('italic') ? 'is-active' : ''}
>
italic
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleStrike()
.run()
}
className={editor.isActive('strike') ? 'is-active' : ''}
>
strike
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleCode()
.run()
}
className={editor.isActive('code') ? 'is-active' : ''}
>
code
Expand Down Expand Up @@ -113,10 +141,28 @@ const MenuBar = ({ editor }) => {
<button onClick={() => editor.chain().focus().setHardBreak().run()}>
hard break
</button>
<button onClick={() => editor.chain().focus().undo().run()}>
<button
onClick={() => editor.chain().focus().undo().run()}
disabled={
!editor.can()
.chain()
.focus()
.undo()
.run()
}
>
undo
</button>
<button onClick={() => editor.chain().focus().redo().run()}>
<button
onClick={() => editor.chain().focus().redo().run()}
disabled={
!editor.can()
.chain()
.focus()
.redo()
.run()
}
>
redo
</button>
</>
Expand Down
26 changes: 26 additions & 0 deletions demos/src/Examples/Default/React/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,32 @@ context('/src/Examples/Default/React/', () => {
]

buttonMarks.forEach(m => {
it(`should disable ${m.label} when the code tag is enabled for cursor`, () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('code').click()
cy.get('button').contains(m.label).should('be.disabled')
})

it(`should enable ${m.label} when the code tag is disabled for cursor`, () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('code').click()
cy.get('button').contains('code').click()
cy.get('button').contains(m.label).should('not.be.disabled')
})

it(`should disable ${m.label} when the code tag is enabled for selection`, () => {
cy.get('.ProseMirror').type('{selectall}Hello world{selectall}')
cy.get('button').contains('code').click()
cy.get('button').contains(m.label).should('be.disabled')
})

it(`should enable ${m.label} when the code tag is disabled for selection`, () => {
cy.get('.ProseMirror').type('{selectall}Hello world{selectall}')
cy.get('button').contains('code').click()
cy.get('button').contains('code').click()
cy.get('button').contains(m.label).should('not.be.disabled')
})

it(`should apply ${m.label} when the button is pressed`, () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click()
Expand Down
18 changes: 16 additions & 2 deletions demos/src/Examples/Default/Svelte/index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,28 @@
<div>
<button
on:click={() => console.log && editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
class={editor.isActive("bold") ? "is-active" : ""}
>
bold
</button>
<button
on:click={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
class={editor.isActive("italic") ? "is-active" : ""}
>
italic
</button>
<button
on:click={() => editor.chain().focus().toggleStrike().run()}
disabled={!editor.can().chain().focus().toggleStrike().run()}
class={editor.isActive("strike") ? "is-active" : ""}
>
strike
</button>
<button
on:click={() => editor.chain().focus().toggleCode().run()}
disabled={!editor.can().chain().focus().toggleCode().run()}
class={editor.isActive("code") ? "is-active" : ""}
>
code
Expand Down Expand Up @@ -149,8 +153,18 @@
horizontal rule
</button>
<button on:click={() => editor.chain().focus().setHardBreak().run()}> hard break </button>
<button on:click={() => editor.chain().focus().undo().run()}> undo </button>
<button on:click={() => editor.chain().focus().redo().run()}> redo </button>
<button
on:click={() => editor.chain().focus().undo().run()}
disabled={!editor.can().chain().focus().undo().run()}
>
undo
</button>
<button
on:click={() => editor.chain().focus().redo().run()}
disabled={!editor.can().chain().focus().redo().run()}
>
redo
</button>
</div>
</div>
{/if}
Expand Down
12 changes: 6 additions & 6 deletions demos/src/Examples/Default/Vue/index.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
<button @click="editor.chain().focus().toggleBold().run()" :disabled="!editor.can().chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
bold
</button>
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
<button @click="editor.chain().focus().toggleItalic().run()" :disabled="!editor.can().chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
italic
</button>
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
<button @click="editor.chain().focus().toggleStrike().run()" :disabled="!editor.can().chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
strike
</button>
<button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
<button @click="editor.chain().focus().toggleCode().run()" :disabled="!editor.can().chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
code
</button>
<button @click="editor.chain().focus().unsetAllMarks().run()">
Expand Down Expand Up @@ -57,10 +57,10 @@
<button @click="editor.chain().focus().setHardBreak().run()">
hard break
</button>
<button @click="editor.chain().focus().undo().run()">
<button @click="editor.chain().focus().undo().run()" :disabled="!editor.can().chain().focus().undo().run()">
undo
</button>
<button @click="editor.chain().focus().redo().run()">
<button @click="editor.chain().focus().redo().run()" :disabled="!editor.can().chain().focus().redo().run()">
redo
</button>
</div>
Expand Down
46 changes: 44 additions & 2 deletions packages/core/src/commands/setMark.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { MarkType } from 'prosemirror-model'
import { MarkType, ResolvedPos } from 'prosemirror-model'
import { EditorState, Transaction } from 'prosemirror-state'

import { isTextSelection } from '../helpers'
import { getMarkAttributes } from '../helpers/getMarkAttributes'
import { getMarkType } from '../helpers/getMarkType'
import { RawCommands } from '../types'
Expand All @@ -15,6 +17,45 @@ declare module '@tiptap/core' {
}
}

function canSetMark(state: EditorState, tr: Transaction, newMarkType: MarkType) {
const { selection } = tr
let cursor: ResolvedPos | null = null

if (isTextSelection(selection)) {
cursor = selection.$cursor
}

if (cursor) {
const currentMarks = state.storedMarks ?? cursor.marks()

// There can be no current marks that exclude the new mark
return !!newMarkType.isInSet(currentMarks) || !currentMarks.some(mark => mark.type.excludes(newMarkType))
}

const { ranges } = selection

return ranges.some(({ $from, $to }) => {
let someNodeSupportsMark = $from.depth === 0 ? state.doc.inlineContent && state.doc.type.allowsMarkType(newMarkType) : false

state.doc.nodesBetween($from.pos, $to.pos, (node, _pos, parent) => {
// If we already found a mark that we can enable, return false to bypass the remaining search
if (someNodeSupportsMark) {
return false
}

if (node.isInline) {
const parentAllowsMarkType = !parent || parent.type.allowsMarkType(newMarkType)
const currentMarksAllowMarkType = !!newMarkType.isInSet(node.marks) || !node.marks.some(otherMark => otherMark.type.excludes(newMarkType))

someNodeSupportsMark = parentAllowsMarkType && currentMarksAllowMarkType
}
return !someNodeSupportsMark
})

return someNodeSupportsMark
})

}
export const setMark: RawCommands['setMark'] = (typeOrName, attributes = {}) => ({ tr, state, dispatch }) => {
const { selection } = tr
const { empty, ranges } = selection
Expand Down Expand Up @@ -42,6 +83,7 @@ export const setMark: RawCommands['setMark'] = (typeOrName, attributes = {}) =>
// we know that we have to merge its attributes
// otherwise we add a fresh new mark
if (someHasMark) {

node.marks.forEach(mark => {
if (type === mark.type) {
tr.addMark(trimmedFrom, trimmedTo, type.create({
Expand All @@ -58,5 +100,5 @@ export const setMark: RawCommands['setMark'] = (typeOrName, attributes = {}) =>
}
}

return true
return canSetMark(state, tr, type)
}
Loading

0 comments on commit 67626ea

Please sign in to comment.