Skip to content

Commit 94db4b3

Browse files
committed
Implement data node create and remove methods
1 parent 6fbd035 commit 94db4b3

File tree

14 files changed

+758
-117
lines changed

14 files changed

+758
-117
lines changed

packages/dom-adapters/src/BlockToolAdapter/index.ts

Lines changed: 83 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
BlockAddedEvent,
44
BlockRemovedEvent,
55
createDataKey,
6-
type DataKey,
6+
type DataKey, DataNodeAddedEvent, DataNodeRemovedEvent,
77
type EditorJSModel,
88
EventAction,
99
EventType,
@@ -103,6 +103,8 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
103103
this.#formattingAdapter = formattingAdapter;
104104
this.#toolName = toolName;
105105

106+
this.#model.addEventListener(EventType.Changed, (event: ModelEvents) => this.#handleModelUpdate(event));
107+
106108
eventBus.addEventListener(`ui:${BeforeInputUIEventName}`, (event: BeforeInputUIEvent) => {
107109
this.#processDelegatedBeforeInput(event);
108110
});
@@ -124,34 +126,64 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
124126

125127
this.#attachedInputs.set(key, input);
126128

127-
this.#model.addEventListener(EventType.Changed, (event: ModelEvents) => this.#handleModelUpdate(event, input, key));
129+
this.#model.createDataNode(
130+
this.#config.userId,
131+
this.#blockIndex,
132+
key,
133+
{
134+
$t: 't',
135+
value: '',
136+
}
137+
);
128138

129139
const builder = new IndexBuilder();
130140

131141
builder.addBlockIndex(this.#blockIndex).addDataKey(key);
132142

133143
this.#caretAdapter.attachInput(input, builder.build());
134144

135-
try {
136-
const value = this.#model.getText(this.#blockIndex, key);
137-
const fragments = this.#model.getFragments(this.#blockIndex, key);
145+
const value = this.#model.getText(this.#blockIndex, key);
146+
const fragments = this.#model.getFragments(this.#blockIndex, key);
138147

139-
input.textContent = value;
148+
input.textContent = value;
149+
150+
fragments.forEach(fragment => {
151+
this.#formattingAdapter.formatElementContent(input, fragment);
152+
});
153+
}
154+
155+
/**
156+
* Removes the input from the DOM by key
157+
*
158+
* @param keyRaw - key of the input to remove
159+
*/
160+
public detachInput(keyRaw: string): void {
161+
const key = createDataKey(keyRaw);
162+
const input = this.#attachedInputs.get(key);
140163

141-
fragments.forEach(fragment => {
142-
this.#formattingAdapter.formatElementContent(input, fragment);
143-
});
144-
} catch (_) {
145-
// do nothing — TextNode is not created yet as there is no initial data in the model
164+
if (!input) {
165+
return;
146166
}
167+
168+
input.remove();
169+
this.#caretAdapter.detachInput(
170+
new IndexBuilder()
171+
.addBlockIndex(this.#blockIndex)
172+
.addDataKey(key)
173+
.build()
174+
);
175+
176+
this.#attachedInputs.delete(key);
177+
178+
this.#model.removeDataNode(this.#config.userId, this.#blockIndex, key);
147179
}
148180

149181
/**
150182
* Check current selection and find it across all attached inputs
151183
*
152184
* @returns tuple of data key and input element or null if no focused input is found
153185
*/
154-
#findFocusedInput(): [DataKey, HTMLElement] | null {
186+
#findFocusedInput(): [ DataKey, HTMLElement ] | null {
155187
const currentInput = Array.from(this.#attachedInputs.entries()).find(([_, input]) => {
156188
/**
157189
* Case 1: Input is a native input — check if it has selection
@@ -421,7 +453,7 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
421453
this.#config.userId,
422454
{
423455
name: this.#toolName,
424-
data : {
456+
data: {
425457
[key]: {
426458
$t: 't',
427459
value: newValueAfter,
@@ -454,32 +486,10 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
454486
* @param key - data key input is attached to
455487
*/
456488
#handleModelUpdateForNativeInput(event: ModelEvents, input: HTMLInputElement | HTMLTextAreaElement, key: DataKey): void {
457-
if (!(event instanceof TextAddedEvent) && !(event instanceof TextRemovedEvent)) {
458-
return;
459-
}
460-
461-
const { textRange, dataKey, blockIndex } = event.detail.index;
462-
463-
if (textRange === undefined) {
464-
return;
465-
}
466-
467-
/**
468-
* Event is not related to the attached block
469-
*/
470-
if (blockIndex !== this.#blockIndex) {
471-
return;
472-
}
473-
474-
/**
475-
* Event is not related to the attached data key
476-
*/
477-
if (dataKey !== key) {
478-
return;
479-
}
489+
const { textRange } = event.detail.index;
480490

481491
const currentElement = input;
482-
const [start, end] = textRange;
492+
const [start, end] = textRange!;
483493

484494
const action = event.detail.action;
485495

@@ -519,31 +529,10 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
519529
* @param key - data key input is attached to
520530
*/
521531
#handleModelUpdateForContentEditableElement(event: ModelEvents, input: HTMLElement, key: DataKey): void {
522-
if (!(event instanceof TextAddedEvent) && !(event instanceof TextRemovedEvent)) {
523-
return;
524-
}
525-
526-
const { textRange, dataKey, blockIndex } = event.detail.index;
527-
528-
if (blockIndex !== this.#blockIndex) {
529-
return;
530-
}
531-
532-
/**
533-
* Event is not related to the attached data key
534-
*/
535-
if (dataKey !== key) {
536-
return;
537-
}
538-
539-
if (textRange === undefined) {
540-
return;
541-
}
542-
532+
const { textRange } = event.detail.index;
543533
const action = event.detail.action;
544534

545-
const start = textRange[0];
546-
const end = textRange[1];
535+
const [start, end] = textRange!;
547536

548537
const [startNode, startOffset] = getBoundaryPointByAbsoluteOffset(input, start);
549538
const [endNode, endOffset] = getBoundaryPointByAbsoluteOffset(input, end);
@@ -589,19 +578,50 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
589578
* @param input - attached input element
590579
* @param key - data key input is attached to
591580
*/
592-
#handleModelUpdate(event: ModelEvents, input: HTMLElement, key: DataKey): void {
581+
#handleModelUpdate(event: ModelEvents): void {
593582
if (event instanceof BlockAddedEvent || event instanceof BlockRemovedEvent) {
594583
if (event.detail.index.blockIndex! <= this.#blockIndex) {
595584
this.#blockIndex += event.detail.action === EventAction.Added ? 1 : -1;
596585
}
586+
587+
return;
588+
}
589+
590+
const { textRange, dataKey, blockIndex } = event.detail.index;
591+
592+
if (blockIndex !== this.#blockIndex) {
593+
return;
594+
}
595+
596+
597+
if (event instanceof DataNodeRemovedEvent) {
598+
this.detachInput(dataKey as string);
599+
600+
return;
601+
}
602+
603+
if (event instanceof DataNodeAddedEvent) {
604+
/**
605+
* @todo Decide how to handle this case as only BlockTool knows how to render an input
606+
*/
607+
}
608+
609+
if (!(event instanceof TextAddedEvent) && !(event instanceof TextRemovedEvent)) {
610+
return;
611+
}
612+
613+
const input = this.#attachedInputs.get(dataKey!);
614+
615+
if (!input || textRange === undefined) {
616+
return;
597617
}
598618

599619
const isInputNative = isNativeInput(input);
600620

601621
if (isInputNative === true) {
602-
this.#handleModelUpdateForNativeInput(event, input as HTMLInputElement | HTMLTextAreaElement, key);
622+
this.#handleModelUpdateForNativeInput(event, input as HTMLInputElement | HTMLTextAreaElement, dataKey!);
603623
} else {
604-
this.#handleModelUpdateForContentEditableElement(event, input, key);
624+
this.#handleModelUpdateForContentEditableElement(event, input, dataKey!);
605625
}
606626
};
607627
}

packages/dom-adapters/src/CaretAdapter/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ export class CaretAdapter extends EventTarget {
9797
this.#inputs.set(index.serialize(), input);
9898
}
9999

100+
/**
101+
* Removes input from the caret adapter
102+
* @param index - index of the input to remove
103+
*/
104+
public detachInput(index: Index): void {
105+
this.#inputs.delete(index.serialize());
106+
}
107+
100108
/**
101109
* Updates current user's caret index
102110
*

packages/model/src/EditorJSModel.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ describe('EditorJSModel', () => {
2222
'updateValue',
2323
'removeBlock',
2424
'moveBlock',
25+
'createDataNode',
26+
'removeDataNode',
2527
'getText',
2628
'insertText',
2729
'removeText',

packages/model/src/EditorJSModel.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,35 @@ export class EditorJSModel extends EventBus {
257257
this.#document.modifyData(index, data);
258258
}
259259

260+
/**
261+
* Creates a data node (ValueNode or TextNode) with the specified key in the BlockNode at the specified index.
262+
*
263+
* @param _userId - user identifier which is being set to the context
264+
* @param parameters - updateValue method parameters
265+
* @param parameters.blockIndex - The index of the BlockNode to update
266+
* @param parameters.dataKey - The key of the data node to create
267+
* @param parameters.value - The initial value of the data node
268+
* @throws Error if the index is out of bounds
269+
*/
270+
@WithContext
271+
public createDataNode(_userId: string | number, ...parameters: Parameters<EditorDocument['createDataNode']>): ReturnType<EditorDocument['createDataNode']> {
272+
return this.#document.createDataNode(...parameters);
273+
}
274+
275+
/**
276+
* Removes a data node (ValueNode or TextNode) with the specified key in the BlockNode at the specified index.
277+
*
278+
* @param _userId - user identifier which is being set to the context
279+
* @param parameters - updateValue method parameters
280+
* @param parameters.blockIndex - The index of the BlockNode to update
281+
* @param parameters.dataKey - The key of the data node to remove
282+
* @throws Error if the index is out of bounds
283+
*/
284+
@WithContext
285+
public removeDataNode(_userId: string | number, ...parameters: Parameters<EditorDocument['removeDataNode']>): ReturnType<EditorDocument['removeDataNode']> {
286+
return this.#document.removeDataNode(...parameters);
287+
}
288+
260289
/**
261290
* Updates the ValueNode data associated with the BlockNode at the specified index.
262291
*
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { BlockNodeDataSerializedValue } from '../../entities/BlockNode/types/index.js';
2+
import type { Index } from '../../entities/Index/index.js';
3+
import { EventAction } from '../types/EventAction.js';
4+
import { BaseDocumentEvent } from './BaseEvent.js';
5+
6+
7+
/**
8+
* DataNodeAdded Custom Event
9+
*/
10+
export class DataNodeAddedEvent extends BaseDocumentEvent<EventAction.Added, BlockNodeDataSerializedValue> {
11+
/**
12+
* DataNodeAdded class constructor
13+
*
14+
* @param index - index of the added BlockNode in the document
15+
* @param data - data serialized value
16+
* @param userId - user identifier
17+
*/
18+
constructor(index: Index, data: BlockNodeDataSerializedValue, userId: string | number) {
19+
// Stryker disable next-line ObjectLiteral
20+
super(index, EventAction.Added, data, userId);
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { BlockNodeDataSerializedValue } from '../../entities/BlockNode/types/index.js';
2+
import type { Index } from '../../entities/Index/index.js';
3+
import { EventAction } from '../types/EventAction.js';
4+
import { BaseDocumentEvent } from './BaseEvent.js';
5+
6+
7+
/**
8+
* DataNodeRemoved Custom Event
9+
*/
10+
export class DataNodeRemovedEvent extends BaseDocumentEvent<EventAction.Removed, BlockNodeDataSerializedValue> {
11+
/**
12+
* DataNodeRemoved class constructor
13+
*
14+
* @param index - index of the added BlockNode in the document
15+
* @param data - data serialized value
16+
* @param userId - user identifier
17+
*/
18+
constructor(index: Index, data: BlockNodeDataSerializedValue, userId: string | number) {
19+
// Stryker disable next-line ObjectLiteral
20+
super(index, EventAction.Removed, data, userId);
21+
}
22+
}

packages/model/src/EventBus/events/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export * from './ValueModifiedEvent.js';
1010
export * from './CaretManagerCaretUpdatedEvent.js';
1111
export * from './CaretManagerCaretAddedEvent.js';
1212
export * from './CaretManagerCaretRemovedEvent.js';
13+
export * from './DataNodeAddedEvent.js';
14+
export * from './DataNodeRemovedEvent.js';

0 commit comments

Comments
 (0)