How to create non-editable text segments in the document


#1

I will provide a high level overview of what I am trying to accomplish first (as I could be trying to solve it incorrectly).

Goal

I would like to make a markdown editor that has unifies input and preview, such that user can type in plain markdown format, but such that formatting annotations are only visible when caret is with-in the formatting range. Here are couple of examples to show what I mean.

Note: caret position will be indicated via “🖊” character.

  1. Caret is with in the bold text range (** are visible):

    writing some **text🖊**

  2. Caret is outside of the bold text range (** fades out):

    writing some text over🖊

  3. Caret is with in the heading range (### is visible)

    ### Title🖊

  4. Caret is outside of the heading range ((### fades out)

    Title

    text🖊

Implementation

I am not sure this is the right implementation strategy but what I end up doing is building upon prosemirror-markdown schema but replacing marks with inline nodes:

new Schema({
  nodes: schema.spec.nodes.append({
    code_inline: {
      name: "code_inline",
      inline: true,
      code: true,
      group: "inline",
      content: "text*",
      defining: true,
      selectable: true,
      parseDOM: [{ tag: "code" }],
      toDOM(node) {
        return ["code", 0]
      }
    }
    // ...
  }),
  marks: schema.spec.marks
})

For which I have added custom NodeView implementations. For example NodeView for code_inline node produces following this.dom element:

<span contenteditable="false">
  <span class="prefix">`</span>
  <code class="content" contenteditable="true">hello</code>
  <span class="suffix">`</span>
</span>

And it sets this.contentDOM to inner <code> element. With an intent of fading in / out formatting span elements when caret is in / out of the range. But I run into certain issues with this approach (described in the next section).

Issues

  1. Delete (via backspace) has inconsistent behavior that varies between. In the document like “Hello `world` !” I get one of the following behaviors:
    • Deletes the whole inline_code even when the caret is after wo. I believe it occurs if caret entered range from the end.
    • Does not delete anything. I believe it occurs if cursor was placed via mouse click.
    • If caret enter text from the left and initiated delete after wr 3rd backspace produces following DOM:
      <div class="ProseMirror" contenteditable="true">
        <p>hello 
          <code>rd</code>`!
          <span contenteditable="false" class="code">
            <span class="prefix">`</span>
            <code contenteditable="true" class="content"></code>
            <span class="suffix">`</span>
          </span>
          <br/>
        </p>
      </div>
      
      I guess it attempts to split content <code> element.
  2. Some navigational bindings do not quite as expected, for instance Ctrl-e / Ctrl-a jumps to the next / previous code_inline node instead of end / beginning of the line.

Questions

  • Is my approach correct or am I using wrong tools for the job ?
  • Is there better way to achieve my goal ? I noticed that use of ::before / ::after pseudo-selectors for rendering prefix / suffix causes less issues, but it limits things I can do and I’d rather avoid that.
  • What is the good way to track when caret is entering a this.contentDOM ? I kind of wish NodeView interface had methods for notifying when caret enters / leaves a range but maybe there is a way to implement this ? I would prefer to avoid doing it with custom keymap as users may define their own navigation keybindings.
  • Is in possible to insert inline_code followed by something like an empty node ? The problem I’m running into is that if last thing I insert is a code segment it is impossible to place caret past it.

Thanks


#2

I recommend against creating editable ‘islands’, i.e. uneditable elements with editable elements within them. Browsers kind of support that, but it has all kinds of problems. For example selections aren’t allowed cross such uneditable boundaries—you can’t have a selection that starts inside the island and continues outside of it—and focus handling in ProseMirror breaks since the focus will be on the inner element rather than on the top-level editable element.

Same answer as always: inspect transactions. You can add decorations to a node to send “messages” to its node view—the update method will get them as an argument. So you’d have a plugin that maintains a set of ‘active’ markup nodes and for each transaction updates that and generates the appropriate decorations.

Definitely not at the document level. There’s kludges in view/src/viewdesc.js to insert BR nodes after certain types of content (empty textblocks, other BRs), but I’m not sure what factor in your node view is causing the problem and how to detect that.


#3

Thanks for responding @marijn. Does this also apply to decorations via pseudo elements like ::before / ::after ? Only other thing I could think of to achieve desired results is by replacing marked fragments whenever user caret enters / leaves it. Is that something you would recommend doing instead of using custom NodeView types ?

I am afraid I don’t quite understand your suggestion. Are you suggesting I could use apply method and by inspecting a transaction tell if caret entered / left marked area ? I don’t really understand how one would send a message to the node view though ? Is there code like this I could take a look at ? Also from what I’ve seen I don’t really get update calls on caret movements I get them only on edits.

Hmm… I don’t think it’s issues specific to my use case, for example if you go to http://prosemirror.net/ and on the the document end position create inline code (Toggle code font in menu) there is no way to go past that other than adding a line break and in order to add normal text after the inline code you have to do following key presses:

  1. Enter
  2. Space
  3. ArrowLeft
  4. Backspace
  5. ArrowRight

I wish there was a way to achive this by just ArrowRight although it could be that empty textblock is not a right tool for this.


#4

I think I found answer to this question and an example. I believe what you meant is I could use tr.setMeta to store a message on the transaction and then in a NodeViews update method use tr.getMeta to check if the message is there and read it. But unless I’ve missed something not every transaction will be passed to update & I am not entirely sure how to make sure one’s that intended to will end up.


#5

@marijn can you please verify that readyou suggesting correctly ? Which is to insert Widget Decorators at the beginning and end of the marked ranges, so that **, *, #, ## …, are decorators. Use apply method to inspect transactions and figure out if the cursor when caret is entering / exiting marked range and if it does insert appropriate into transaction via setMeta method. Then in update method of NodeView check for that info via getMeta method to handle entry / exit. And of course I’d have to maintain set of decorators in the plugin state and update them on each transaction.

What is your opinion regarding the fact that I have replaced all markers with node’s instead. Would you advice against that as well ? I did notice that Enter splits my NodeView in two but does not split enclosing paragraph. I imagine that is solvable but I’m starting to wonder if going with my custom NodeView will a can of worms.

Thanks


#6

No, it applies to editable elements wrapped in non-editable one, as I described.

That’s entirely by design. The way to ‘exit’ inline code is to toggle off the mark.

Nope, not at all. The idea is to have a plugin that either computes the needed decorations directly in its decorations prop, or, if you have a lot of them (probably not) keeps a decoration set as state and updates it in its state apply method.


#7

Very neat, I wasn’t aware of this pattern before, thanks for spelling it out explicitly!