I noticed huge performance depreciation when started to use below plugin. I’m pretty new in React and maybe you have some suggestions what could I change to “fix” it? Its an extension that adds ability to add annotations to the editor.
import {
generateHTML,
getNodeType,
mergeAttributes,
Node,
NodeWithPos,
} from '@tiptap/core';
import { DOMOutputSpec, NodeType, Node as PMNode } from '@tiptap/pm/model';
import { Editor } from '@tiptap/react';
import { Plugin, PluginKey, Transaction } from '@tiptap/pm/state';
import { getAllNodesOfType } from '@/components/editor/utils/utils';
declare module '@tiptap/core' {
interface Commands<ReturnType> {
annotation: {
setAnnotation: (
content?: PMNode,
type?: 'annotation' | 'remark'
) => ReturnType;
deleteAnnotation: () => ReturnType;
};
}
}
const ALPHABET = 'abcdefghijklmnopqrstuvwxyz';
export const getAnnotationNumber = (
editor: Editor,
node: PMNode,
nodes?: PMNode[]
) => {
if (!nodes) {
let annotations = getAllNodesOfType(editor, 'annotation');
nodes = [...annotations.map((el) => el.node)];
}
nodes = nodes.filter((x) => x.attrs['type'] === node.attrs['type']);
let number = nodes.indexOf(node);
if (node.attrs['type'] === 'annotation') return number + 1;
return ALPHABET[number];
};
const updateAnnotations = (state: {
editor: Editor;
storage: AnnotationStorage;
onUpdate: any;
}) => {
const { editor, onUpdate } = state;
let data: AnnotationData = [];
// Find all annotation nodes and its positions within the doc
const nodesWithPos = getAllNodesOfType(editor, 'annotation');
// Convert annotation nodes to annotation data
nodesWithPos.forEach((nodeWithPos, index) => {
const element = editor.view.domAtPos(nodeWithPos.pos + 1)
.node as HTMLElement;
data.push({
label: nodeWithPos.node.attrs['label'],
id: nodeWithPos.node.attrs['id'],
dom: element,
type: nodeWithPos.node.attrs['type'],
});
});
if (onUpdate) {
const isCreate = 0 === state.storage.content.length;
onUpdate(data, isCreate);
}
state.storage.content = data;
// editor.state.tr.setMeta('annotation', data);
// editor.view.dispatch(editor.state.tr);
};
export type AnnotationData = Array<AnnotationDataItem>;
export type AnnotationDataItem = {
dom: HTMLElement;
id: string;
label: string | number;
type: string;
};
export type AnnotationStorage = {
content: AnnotationData;
elements: Array<HTMLElement>;
};
export interface AnnotationOptions {
HTMLAttributes: Record<string, any>;
onUpdate?: (data: AnnotationData, isCreate?: boolean) => void;
}
export const Annotation = Node.create<AnnotationOptions, AnnotationStorage>({
name: 'annotation',
group: 'inline',
content: 'block*',
inline: true,
selectable: true,
atom: true,
addStorage: () => ({
content: [],
elements: [],
}),
addAttributes() {
return {
id: {
default: null,
renderHTML: (attributes) => ({
id: attributes['id'],
}),
parseHTML: (element: HTMLElement) => element.getAttribute('id'),
},
label: {
default: null,
},
type: {
default: 'annotation',
},
};
},
parseHTML() {
return [
{
tag: 'span[data-type="annotation"]',
},
];
},
renderHTML({ HTMLAttributes, node }) {
let attrs = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
'data-type': 'annotation',
class: 'inline-block',
contentEditable: false,
});
let content: DOMOutputSpec[];
content = [
// Label (number of annotation)
[
'span',
{ 'data-type': 'annotation-label' },
`[${getAnnotationNumber(this.editor as Editor, node)}]`,
],
// Content (any `block`)
['div', { 'data-type': 'annotation-content', class: 'hidden' }, 0],
];
return ['div', attrs, ...content];
},
addCommands() {
return {
setAnnotation:
(content, type) =>
({ tr, editor, chain }) => {
return chain()
.command(({ tr }) => {
tr.replaceSelectionWith(
this.type.create({ type }, content ? content : undefined)
);
return true;
})
.setMeta('refresh-annotation', true)
.run();
},
deleteAnnotation:
() =>
({ tr, editor, chain }) => {
return (
chain()
// .command(({ tr }) => {
// tr.replaceSelectionWith(this.type.create());
// return true;
// })
.deleteNode('annotation')
.setMeta('refresh-annotation', true)
.run()
);
},
};
},
onUpdate() {
updateAnnotations({
editor: this.editor as Editor,
storage: this.storage,
onUpdate:
this.options.onUpdate === null || this.options.onUpdate === void 0
? void 0
: this.options.onUpdate.bind(this),
});
},
addProseMirrorPlugins() {
let editor = this.editor;
let plugin = new Plugin({
key: new PluginKey('annotation'),
appendTransaction(transactions, oldState, newState) {
let { tr } = newState;
if (transactions.some((tr) => tr.docChanged)) {
let annotations = getAllNodesOfType(
editor as Editor,
'annotation',
tr
);
let nodes = annotations.map((el) => el.node);
annotations.forEach((item) => {
tr.setNodeMarkup(item.pos, void 0, {
...item.node.attrs,
label: getAnnotationNumber(editor as Editor, item.node, nodes),
});
});
}
return tr;
},
});
return [plugin];
},
});
UPDATE It seems that onUpdate
method is the reason. It essentailly calls setAnnotations
in the top-level component (there were useEditor
is called as well).