Highlighting a selectable, non-editable node

I’m working on a simple footnote number node that shows a bracketed, non-editable value i.e. [1] or [37]. I created a schema based on this footnote example:

const noteFlagSpec = {
  group: "inline",
  content: "text*",
  inline: true,
  draggable: true,
  selectable: true,
  attrs: {
    id: {default: null}, 
    value: {default: "[1]"}
  },
  atom: true,
  toDOM: node => { 
    let ele = document.createElement("footnote")
    ele.textContent = node.attrs.value
    return ele
  },
  parseDOM: [{tag: "footnote"}]
}

I also created a node view because my understanding is this is necessary to make it non-editable, even when clicking and keyboarding into the node.

export class NoteFlagView {
  constructor(node, view, getPos) {
    this.dom = document.createElement("footnote")
  }
  
  selectNode() {
    this.dom.classList.add("ProseMirror-selectednode")
  }

  deselectNode() {
    this.dom.classList.remove("ProseMirror-selectednode")
  }

  ignoreMutation() { return true }
}

This code does almost exactly what I want, except for one case: when creating a selection the node does not appear highlighted like the rest of the inline text.

Is this because I overrode selectNode? Did I overdo it with the overrides?

I ended up scrapping my view and doing this with the node spec and a plugin.

const noteFlagSpec = {
  group: "inline",
  content: "inline*",
  inline: true,
  draggable: true,
  selectable: true,
  attrs: {
    id: {default: null}, 
    type: {default: "number"}, 
    value: {default: 1},
    inSelection: {default: false}
  },
  atom: true,
  toDOM: node => { 
    let ele = document.createElement("footnote")
    ele.textContent = '[' + node.attrs.value +']'
    if (node.attrs.inSelection) {
      ele.classList.add("node-in-selection")
    } else {
      ele.classList.remove("node-in-selection")
    }
    return ele
  },
  parseDOM: [{tag: "footnote"}]
}

export const noteFlagPlugin = new Plugin({
  appendTransaction: function(transactions, oldState, newState) {
    var {ranges} = newState.selection
    var noteFlagVal = 1
    var tr = newState.tr
    
    newState.doc.descendants((node, pos) => {
      if (node.type.name === 'footnote') {    
        // If this node is inside a text selection mark it selected
        let inSel = false
        for (var i = 0; i < ranges.length; i++) {
          if (ranges[i].$from.pos <= pos && ranges[i].$to.pos > pos) {
            tr.setNodeMarkup(pos, node.type, {inSelection: true, value: noteFlagVal})
            inSel = true
          }
        }
        if (!inSel) {
          tr.setNodeMarkup(pos, node.type, {inSelection: false, value: noteFlagVal})
        }

        noteFlagVal += 1
      } 
    });

    // setNodeMarkup drops the selection so re-select whatever was selected
    // to begin with
    tr.setSelection(newState.selection)

    return tr
  }
})

I override the browser selection color because it’s apparently not possible to get this information using Javascript. CSS:

::selection {
  background: #99def7; /* WebKit/Blink Browsers */
}
::-moz-selection {
  background: #99def7; /* Gecko Browsers */
}
.node-in-selection { 
  background: #99def7;
}
1 Like

Thank you for sharing!

I have a similar problem, but I want to try a combination of your two approaches.