How to conditionally set a decoration based on if the dom element at node position contains a class

I’m trying to set a placeholder decoration only if the node does not contain a span with the step-history-deleted class which comes from another diffsetPlugin and it’s corresponding generated decoration set (basically showing deleted items when we track changes as spans inside of the node).

This is working technically, using view.nodeDOM and querySelector, but it’s highly unoptimized, there’s a visible flash before the placeholder is properly replaced. Is there a way to improve the speed of this? Or is there a way to access the decoration-set via the EditorView from another plugin?

/**
 * @param {Node} document
 * @returns {DecorationSet}
 */
export const getDecorations = (document, view) => {
  const decorations = [];
  document.descendants((node, position, parent) => {
    if (hasTextPlaceholder(node) && shouldDisplayPlaceholder(node, parent)) {
      try {
        const domElementAtPosition = view.nodeDOM(position);
        if (!domElementAtPosition.querySelector(`.${DELETE_CSS_CLASSNAME}`)) {
          decorations.push(
            Decoration.node(position, position + node.nodeSize, {
              class: placeholderStyles,
            })
          );
        }
      } catch (err) {
        // view and it's dom element can occasionally be undefined
        // this adds placeholders if the prior conditions are true
        decorations.push(
          Decoration.node(position, position + node.nodeSize, {
            class: placeholderStyles,
          })
        );
      }
    }
  });
  return DecorationSet.create(document, decorations);
};
/**
 * Plugin used to make placeholder attributes of text block nodes functional in the editor.
 */
export class TextPlaceholderPlugin extends Plugin {
  /**
   * Constructor
   */
  constructor() {
    super({
      state: {
        init: (config, state) => {
          this.decorations = getDecorations(state.doc);
          return this.decorations;
        },
        apply(tr, pluginState) {
          if (tr.docChanged) {
            this.decorations = getDecorations(tr.doc, this.view);
            return this.decorations;
          }
          return pluginState;
        },
      },
      view: (view) => {
        this.view = view;
        return {
          update: (view) => {
            const newDecorations = getDecorations(view.state.doc, this.view);
            if (!isEqual(this.decorations, newDecorations)) {
              this.decorations = newDecorations;
              return this.decorations;
            }
            return this.decorations;
          },
        };
      },
      props: {
        decorations: () => this.decorations,
      },
    });

    this.view = null;
    this.decorations = null;
  }
}

It might be possible to keep the DOM out of this by having your other plugin export some helper function that gives you access to its decoration set (assuming that’s stored in a plugin state field), and reading that when deciding what to decorate.

Oh nice! That makes sense. Here’s what I ended up doing

function doesNotContainDiffsetDeletedDecoration(diffsetState, from, to) {
  if (!diffsetState?.showing) {
    return true;
  }
  const diffsetDeletedDecorationAtPos = diffsetState?.decorations?.find(
    from,
    to,
    (spec) => spec?.deleted
  );

  return !diffsetDeletedDecorationAtPos?.length;
}

/**
 * @param {Node} document
 * @returns {DecorationSet}
 */
export const getDecorations = (state) => {
  const decorations = [];
  const diffsetState = diffSetPluginKey.getState(state);
  state.doc.descendants((node, position, parent) => {
    const from = position;
    const to = position + node.nodeSize;
    if (
      hasTextPlaceholder(node) &&
      shouldDisplayPlaceholder(node, parent) &&
      doesNotContainDiffsetDeletedDecoration(diffsetState, from, to)
    ) {
      decorations.push(
        Decoration.node(from, to, {
          class: placeholderStyles,
        })
      );
    }
  });
  return DecorationSet.create(state.doc, decorations);
};