Skip to content

Commit 7a696c6

Browse files
falsaadehhharbournickchittolinag
authored
HAR-10208 - Trackable id (#788)
* feat: adding node block unique ids * feat: add trackable ids block commands * fix: same id for nodes of same type * fix: same id for nodes of same type * fix: same id for nodes of same type * fix: same id for nodes of same type * Feature: Block node IDs utilities, add hasInitialized to run plugin when first loaded (#793) * chore: update content block * fix: create utility fns for block node, add tests, add hasInitialized for first pass * fix: consider code review comments * test: adding block-node commands and helpers tests * fix: change plugin name * chore: fix helpers blockNode capitalization namespace issue --------- Co-authored-by: Nick Bernal <117235294+harbournick@users.noreply.github.com> Co-authored-by: Gabriel Chittolina <gabriel@harbourshare.com> Co-authored-by: Nick Bernal <nick@harbourshare.com>
1 parent d4ad898 commit 7a696c6

13 files changed

Lines changed: 812 additions & 2 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { Extension } from '@core/Extension.js';
2+
import { helpers } from '@core/index.js';
3+
import { Plugin, PluginKey } from 'prosemirror-state';
4+
import { ReplaceStep } from 'prosemirror-transform';
5+
import { v4 as uuidv4 } from 'uuid';
6+
7+
const { findChildren } = helpers;
8+
const SD_BLOCK_ID_ATTRIBUTE_NAME = 'sdBlockId';
9+
export const BlockNodePluginKey = new PluginKey('blockNodePlugin');
10+
export const BlockNode = Extension.create({
11+
name: 'blockNode',
12+
13+
addCommands() {
14+
return {
15+
replaceBlockNodeById:
16+
(id, contentNode) =>
17+
({ dispatch, tr }) => {
18+
const blockNode = this.editor.helpers.blockNode.getBlockNodeById(id);
19+
if (!blockNode || blockNode.length > 1) {
20+
return false;
21+
}
22+
23+
if (dispatch) {
24+
let { pos, node } = blockNode[0];
25+
let newPosFrom = tr.mapping.map(pos);
26+
let newPosTo = tr.mapping.map(pos + node.nodeSize);
27+
28+
let currentNode = tr.doc.nodeAt(newPosFrom);
29+
if (node.eq(currentNode)) {
30+
tr.replaceWith(newPosFrom, newPosTo, contentNode);
31+
}
32+
}
33+
34+
return true;
35+
},
36+
37+
deleteBlockNodeById:
38+
(id) =>
39+
({ dispatch, tr }) => {
40+
const blockNode = this.editor.helpers.blockNode.getBlockNodeById(id);
41+
if (!blockNode || blockNode.length > 1) {
42+
return false;
43+
}
44+
45+
if (dispatch) {
46+
let { pos, node } = blockNode[0];
47+
let newPosFrom = tr.mapping.map(pos);
48+
let newPosTo = tr.mapping.map(pos + node.nodeSize);
49+
50+
let currentNode = tr.doc.nodeAt(newPosFrom);
51+
if (node.eq(currentNode)) {
52+
tr.delete(newPosFrom, newPosTo);
53+
}
54+
}
55+
56+
return true;
57+
},
58+
59+
updateBlockNodeAttributes:
60+
(id, attrs = {}) =>
61+
({ dispatch, tr }) => {
62+
const blockNode = this.editor.helpers.blockNode.getBlockNodeById(id);
63+
if (!blockNode || blockNode.length > 1) {
64+
return false;
65+
}
66+
if (dispatch) {
67+
let { pos, node } = blockNode[0];
68+
let newPos = tr.mapping.map(pos);
69+
let currentNode = tr.doc.nodeAt(newPos);
70+
if (node.eq(currentNode)) {
71+
tr.setNodeMarkup(newPos, undefined, {
72+
...node.attrs,
73+
...attrs,
74+
});
75+
}
76+
77+
return true;
78+
}
79+
},
80+
};
81+
},
82+
83+
addHelpers() {
84+
return {
85+
getBlockNodes: () => {
86+
return findChildren(this.editor.state.doc, (node) => nodeAllowsSdBlockIdAttr(node));
87+
},
88+
89+
getBlockNodeById: (id) => {
90+
return findChildren(this.editor.state.doc, (node) => node.attrs.sdBlockId === id);
91+
},
92+
93+
getBlockNodesByType: (type) => {
94+
return findChildren(this.editor.state.doc, (node) => node.type.name === type);
95+
},
96+
97+
getBlockNodesInRange: (from, to) => {
98+
let blockNodes = [];
99+
100+
this.editor.state.doc.nodesBetween(from, to, (node, pos) => {
101+
if (nodeAllowsSdBlockIdAttr(node)) {
102+
blockNodes.push({
103+
node,
104+
pos,
105+
});
106+
}
107+
});
108+
109+
return blockNodes;
110+
},
111+
};
112+
},
113+
addPmPlugins() {
114+
let hasInitialized = false;
115+
116+
return [
117+
new Plugin({
118+
key: BlockNodePluginKey,
119+
appendTransaction: (transactions, _oldState, newState) => {
120+
if (hasInitialized && !transactions.some((tr) => tr.docChanged)) return null;
121+
122+
// Check for new block nodes and if none found, we don't need to do anything
123+
if (hasInitialized && !checkForNewBlockNodesInTrs(transactions)) return null;
124+
125+
let tr = null;
126+
let changed = false;
127+
newState.doc.descendants((node, pos) => {
128+
// Only allow block nodes with a valid sdBlockId attribute
129+
if (!nodeAllowsSdBlockIdAttr(node) || !nodeNeedsSdBlockId(node)) return null;
130+
131+
tr = tr ?? newState.tr;
132+
tr.setNodeMarkup(
133+
pos,
134+
undefined,
135+
{
136+
...node.attrs,
137+
sdBlockId: uuidv4(),
138+
},
139+
node.marks,
140+
);
141+
changed = true;
142+
});
143+
144+
if (changed && !hasInitialized) hasInitialized = true;
145+
return changed ? tr : null;
146+
},
147+
}),
148+
];
149+
},
150+
});
151+
152+
/**
153+
* Check if a node allows sdBlockId attribute
154+
* @param {import("prosemirror-model").Node} node - The node to check
155+
* @returns {boolean} - True if the node type supports sdBlockId attribute
156+
*/
157+
export const nodeAllowsSdBlockIdAttr = (node) => {
158+
return !!(node?.isBlock && node?.type?.spec?.attrs?.[SD_BLOCK_ID_ATTRIBUTE_NAME]);
159+
};
160+
161+
/**
162+
* Check if a node needs an sdBlockId (doesn't have one or has null/empty value)
163+
* @param {import("prosemirror-model").Node} node - The node to check
164+
* @returns {boolean} - True if the node needs an sdBlockId assigned
165+
*/
166+
export const nodeNeedsSdBlockId = (node) => {
167+
const currentId = node?.attrs?.[SD_BLOCK_ID_ATTRIBUTE_NAME];
168+
return !currentId;
169+
};
170+
171+
/**
172+
* Check for new block nodes in ProseMirror transactions.
173+
* Iterate through the list of transactions, and in each tr check if there are any new block nodes.
174+
* @param {Array<Transaction>} transactions - The ProseMirror transactions to check.
175+
* @returns {boolean} - True if new block nodes are found, false otherwise.
176+
*/
177+
export const checkForNewBlockNodesInTrs = (transactions) => {
178+
return transactions.some((tr) => {
179+
return tr.steps.some((step) => {
180+
const hasValidSdBlockNodes = step.slice?.content?.content?.some((node) => nodeAllowsSdBlockIdAttr(node));
181+
return step instanceof ReplaceStep && hasValidSdBlockNodes;
182+
});
183+
});
184+
};

0 commit comments

Comments
 (0)