Skip to content

Commit bffd733

Browse files
authored
Implement data node create and remove methods (#116)
* Implement data node create and remove methods * Fix lint * Add todo
1 parent 6fbd035 commit bffd733

File tree

14 files changed

+764
-123
lines changed

14 files changed

+764
-123
lines changed

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

Lines changed: 87 additions & 69 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,
@@ -71,8 +71,6 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
7171

7272
/**
7373
* Inputs that bound to the model
74-
*
75-
* @todo handle inputs deletion — remove inputs from the map when they are removed from the DOM
7674
*/
7775
#attachedInputs = new Map<DataKey, HTMLElement>();
7876

@@ -103,6 +101,8 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
103101
this.#formattingAdapter = formattingAdapter;
104102
this.#toolName = toolName;
105103

104+
this.#model.addEventListener(EventType.Changed, (event: ModelEvents) => this.#handleModelUpdate(event));
105+
106106
eventBus.addEventListener(`ui:${BeforeInputUIEventName}`, (event: BeforeInputUIEvent) => {
107107
this.#processDelegatedBeforeInput(event);
108108
});
@@ -124,34 +124,67 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
124124

125125
this.#attachedInputs.set(key, input);
126126

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

129137
const builder = new IndexBuilder();
130138

131139
builder.addBlockIndex(this.#blockIndex).addDataKey(key);
132140

133141
this.#caretAdapter.attachInput(input, builder.build());
134142

135-
try {
136-
const value = this.#model.getText(this.#blockIndex, key);
137-
const fragments = this.#model.getFragments(this.#blockIndex, key);
143+
const value = this.#model.getText(this.#blockIndex, key);
144+
const fragments = this.#model.getFragments(this.#blockIndex, key);
138145

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

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
162+
if (!input) {
163+
return;
146164
}
165+
166+
/**
167+
* @todo Let BlockTool handle DOM update
168+
*/
169+
input.remove();
170+
this.#caretAdapter.detachInput(
171+
new IndexBuilder()
172+
.addBlockIndex(this.#blockIndex)
173+
.addDataKey(key)
174+
.build()
175+
);
176+
177+
this.#attachedInputs.delete(key);
178+
179+
this.#model.removeDataNode(this.#config.userId, this.#blockIndex, key);
147180
}
148181

149182
/**
150183
* Check current selection and find it across all attached inputs
151184
*
152185
* @returns tuple of data key and input element or null if no focused input is found
153186
*/
154-
#findFocusedInput(): [DataKey, HTMLElement] | null {
187+
#findFocusedInput(): [ DataKey, HTMLElement ] | null {
155188
const currentInput = Array.from(this.#attachedInputs.entries()).find(([_, input]) => {
156189
/**
157190
* Case 1: Input is a native input — check if it has selection
@@ -421,7 +454,7 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
421454
this.#config.userId,
422455
{
423456
name: this.#toolName,
424-
data : {
457+
data: {
425458
[key]: {
426459
$t: 't',
427460
value: newValueAfter,
@@ -451,35 +484,12 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
451484
*
452485
* @param event - model update event
453486
* @param input - input element
454-
* @param key - data key input is attached to
455487
*/
456-
#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-
}
488+
#handleModelUpdateForNativeInput(event: ModelEvents, input: HTMLInputElement | HTMLTextAreaElement): void {
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);
@@ -586,22 +575,51 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
586575
* Handles model update events and updates DOM
587576
*
588577
* @param event - model update event
589-
* @param input - attached input element
590-
* @param key - data key input is attached to
591578
*/
592-
#handleModelUpdate(event: ModelEvents, input: HTMLElement, key: DataKey): void {
579+
#handleModelUpdate(event: ModelEvents): void {
593580
if (event instanceof BlockAddedEvent || event instanceof BlockRemovedEvent) {
594581
if (event.detail.index.blockIndex! <= this.#blockIndex) {
595582
this.#blockIndex += event.detail.action === EventAction.Added ? 1 : -1;
596583
}
584+
585+
return;
586+
}
587+
588+
const { textRange, dataKey, blockIndex } = event.detail.index;
589+
590+
if (blockIndex !== this.#blockIndex) {
591+
return;
592+
}
593+
594+
595+
if (event instanceof DataNodeRemovedEvent) {
596+
this.detachInput(dataKey as string);
597+
598+
return;
599+
}
600+
601+
if (event instanceof DataNodeAddedEvent) {
602+
/**
603+
* @todo Decide how to handle this case as only BlockTool knows how to render an input
604+
*/
605+
}
606+
607+
if (!(event instanceof TextAddedEvent) && !(event instanceof TextRemovedEvent)) {
608+
return;
609+
}
610+
611+
const input = this.#attachedInputs.get(dataKey!);
612+
613+
if (!input || textRange === undefined) {
614+
return;
597615
}
598616

599617
const isInputNative = isNativeInput(input);
600618

601619
if (isInputNative === true) {
602-
this.#handleModelUpdateForNativeInput(event, input as HTMLInputElement | HTMLTextAreaElement, key);
620+
this.#handleModelUpdateForNativeInput(event, input as HTMLInputElement | HTMLTextAreaElement);
603621
} else {
604-
this.#handleModelUpdateForContentEditableElement(event, input, key);
622+
this.#handleModelUpdateForContentEditableElement(event, input, dataKey!);
605623
}
606624
};
607625
}

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

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

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

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)