How to prevent node deletion?

Hey, I am trying to lock title, subtitle and contents elements down so that they cannot be removed from the document. The structure of the document looks like this

<doc>
   <title>...</title>
   <subtitle>...</subtitle>
   <contents>...</contents>
</doc>

So I tried to lock the elements like this:

class Title/Subtitle/Contents extends Textblock {
   get locked() { return true }

}

This way, if I selected text crossing from title to subtitle or from subtitle into contents and his backspace/delete, it would not merge the elements. Unfortunately, I could still delete the each of the three element itself by selecting it and then hitting backspace/delete.

So I changed it to this:

class Title/Subtitle/Contents extends Textblock {
   get locked() { return true }
   get selectable() { return false }

}

This worked in the sense that I could no longer select the elements, so there was no (easy) way to delete them. Unfortunately if I selected text all the way from title title node to the contents node, it will still delete the subtitle node (the middle node). Any suggestions? (Adding “locked” to the Doc node is something I already tried).

Should they be able edit the title and subtitle but not delete them? I did try something like

Title.register("command", {
   name: "deleteTitle",
   label: "don't touch",
   run: {
       let (from,to) = pm.selection
       return !Pos.samePath(from.path, to.path) || pm.doc.path(from.path).size == 0
   },
  keys: ["Backspace(1)", "Mod-Backspace(1)"]
})

I ranked it 1 to catch the backspace first. It did inhibit deletion across nodes although an empty node could still be deleted even though it can be empty. Not sure about that.

@peteb: Yes, they should be able to edit the contents of all parts, but not be able to delete them. I think the locked attribute was supposed to solve that. It just seems that something is missing or that I’m not fully understanding how to use it. I moved it to the doc node now and removed it from those elements that need to contain more that just inline text, because otherwise I wasn’t able to change paragraph to headline to code, etc. . Still not quite working as I expected.

This is one of the use cases for locked nodes, which have not been implemented yet. Until they are, there isn’t really a good solution for this.

1 Like

gotcha. Ok, will wait.

is there a solution for this already ? I am trying to find a way to make it impossible to delete a node using the keyboard (still want to be able to delete it by programmatically dispatching a transaction though).

I think filterTransaction would be the best way to do this.

1 Like

Hi @marijn,

If I implement the deletion of the node via filterTransaction, what properties of the transaction/state parameters should let me know if the action is “deletion” of a node. I am inspecting the properties of the transaction object inside filterTransaction and so far the info I could use is .docChanged and if the steps array contains any ReplaceStep which affects the nodes that are supposed to be non-deletable. Am I on the right path?

You’ll want to look at each step, find the deleted regions in its step map (with forEach), and check the document before the step to see whether it contains the type of node you want to prevent deletion of in such a range.

I had a similar problem and found another way to do this. Basically, I have a set of custom title nodes which I want to be reorderable (draggable) by the user, but always remain in the document. Simply checking for steps that delete content didn’t work, because drag’n’drop deletes the content in one step and adds it back in another. Instead, I wrote a plugin that prevents deletion / addition of such nodes as follows:

filterTransaction: (transaction, state) => {
  // Avoid endless recursion when simulating the effects of the transaction
  if (transaction.getMeta("filteringRequiredNodeDeletion") === true) return true
  transaction.setMeta("filteringRequiredNodeDeletion", true)

  // Simulate the transaction
  const newState = state.apply(transaction)

  // Check that the same required nodes are still present in any order
  return isEqual(
    this.requiredNodes(state.doc.content).sort(),
    this.requiredNodes(newState.doc.content).sort()
  )
}

I used the isEqual function from lodash to compare array equality, but you can do it with vanilla JS as well. Also, I had to write a function requiredNodes which extracts the set of required nodes from a document:

requiredNodes(node) {
  const requiredNodes = []
  node.descendants(child => {
    if (child.type.name === 'myRequiredNodeType') {
      requiredNodes.push(child.attrs.someUniqueAttr)
    }
  })
  return requiredNodes
}