Changing doc.attrs?

Hello all,

Sorry to revive this topic but I just wanted to share some updated code, as using some of the code above I ran into some issues.

The fromJSON method in the examples above is expecting just a single json argument, but the actual signature is fromJSON(schema, json), which is pretty important if you want this to work!

I’ve also added the check @kapouer mentions above, I haven’t had time to test/verify that it’s still relevant but I think there’s no harm in including it.

I’ve also modified the map method to return this, since mapping should have no effect on doc attribute updates (there are no positions to map). Returning null (as in examples above) would mean any time this step gets mapped the changes are lost, if I’m understanding things correctly.

Here’s the code I’m using:

const { Step, StepResult } = require('prosemirror-transform')

const STEP_TYPE = 'setDocAttr'

// adapted from https://discuss.prosemirror.net/t/changing-doc-attrs/784
class SetDocAttrStep extends Step {
  constructor (key, value) {
    super()
    this.key = key
    this.value = value
  }

  get stepType () { return STEP_TYPE }

  apply (doc) {
    this.prevValue = doc.attrs[this.key]
    // avoid clobbering doc.type.defaultAttrs
    if (doc.attrs === doc.type.defaultAttrs) doc.attrs = Object.assign({}, doc.attrs)
    doc.attrs[this.key] = this.value
    return StepResult.ok(doc)
  }

  invert () {
    return new SetDocAttrStep(this.key, this.prevValue)
  }

  // position never changes so map should always return same step
  map () { return this }

  toJSON () {
    return {
      stepType: this.stepType,
      key: this.key,
      value: this.value
    }
  }

  static fromJSON (schema, json) {
    return new SetDocAttrStep(json.key, json.value)
  }

  static register () {
    try {
      Step.jsonID(STEP_TYPE, SetDocAttrStep)
    } catch (err) {
      if (err.message !== `Duplicate use of step JSON ID ${STEP_TYPE}`) throw err
    }
    return true
  }
}

module.exports = {
  SetDocAttrStep
}

I’ve added a static register method to make it easy to import and control when it gets called. That said you may want to modify it to just call Step.jsonID(STEP_TYPE, SetDocAttrStep) on require. I try to avoid executing any code on require or creating modules that end up acting as singletons unless absolutely necessary, but needs must!

So with the code above you’d use it more or less like this:

const { SetDocAttrStep } = require('./set-doc-attr-step')

SetDocAttrStep.register()

...

const tr = editorState.tr.step(new SetDocAttrStep('attributeKey', 'attributeValue'))

editorView.dispatch(tr)

Some caveats about registering the step type:

  • must be registered before any processing of steps using prosemirror-transform
  • must make absolutely sure there is only one version of prosemirror-transform in dep tree, as step types registered via Step.jsonID() are kept in an object in the file scope of prosemirror-transform/master/src/step.js, so relies on module caching to work

I considered publishing this as a module, but considering the caveats mentioned in the code above and the frequency with which this feature’s been needed as far as threads in this forum indicate, I think there’s a strong case to be made that this should be a core feature. Perhaps in a 2.x release as mentioned above if this is breaking enough to warrant so. I’d feel better about using this if there was a canonical step type for doc attribute updates to avoid having to maintain a non-standard step type for historical reasons down the line.

2 Likes