Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 87 additions & 69 deletions packages/dom-adapters/src/BlockToolAdapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
BlockAddedEvent,
BlockRemovedEvent,
createDataKey,
type DataKey,
type DataKey, DataNodeAddedEvent, DataNodeRemovedEvent,
type EditorJSModel,
EventAction,
EventType,
Expand Down Expand Up @@ -71,8 +71,6 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {

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

Expand Down Expand Up @@ -103,6 +101,8 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
this.#formattingAdapter = formattingAdapter;
this.#toolName = toolName;

this.#model.addEventListener(EventType.Changed, (event: ModelEvents) => this.#handleModelUpdate(event));

eventBus.addEventListener(`ui:${BeforeInputUIEventName}`, (event: BeforeInputUIEvent) => {
this.#processDelegatedBeforeInput(event);
});
Expand All @@ -124,34 +124,67 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {

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

this.#model.addEventListener(EventType.Changed, (event: ModelEvents) => this.#handleModelUpdate(event, input, key));
this.#model.createDataNode(
this.#config.userId,
this.#blockIndex,
key,
{
$t: 't',
value: '',
}
);

const builder = new IndexBuilder();

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

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

try {
const value = this.#model.getText(this.#blockIndex, key);
const fragments = this.#model.getFragments(this.#blockIndex, key);
const value = this.#model.getText(this.#blockIndex, key);
const fragments = this.#model.getFragments(this.#blockIndex, key);

input.textContent = value;
input.textContent = value;

fragments.forEach(fragment => {
this.#formattingAdapter.formatElementContent(input, fragment);
});
}

/**
* Removes the input from the DOM by key
*
* @param keyRaw - key of the input to remove
*/
public detachInput(keyRaw: string): void {
const key = createDataKey(keyRaw);
const input = this.#attachedInputs.get(key);

fragments.forEach(fragment => {
this.#formattingAdapter.formatElementContent(input, fragment);
});
} catch (_) {
// do nothing — TextNode is not created yet as there is no initial data in the model
if (!input) {
return;
}

/**
* @todo Let BlockTool handle DOM update
*/
input.remove();
this.#caretAdapter.detachInput(
new IndexBuilder()
.addBlockIndex(this.#blockIndex)
.addDataKey(key)
.build()
);

this.#attachedInputs.delete(key);

this.#model.removeDataNode(this.#config.userId, this.#blockIndex, key);
}

/**
* Check current selection and find it across all attached inputs
*
* @returns tuple of data key and input element or null if no focused input is found
*/
#findFocusedInput(): [DataKey, HTMLElement] | null {
#findFocusedInput(): [ DataKey, HTMLElement ] | null {
const currentInput = Array.from(this.#attachedInputs.entries()).find(([_, input]) => {
/**
* Case 1: Input is a native input — check if it has selection
Expand Down Expand Up @@ -421,7 +454,7 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
this.#config.userId,
{
name: this.#toolName,
data : {
data: {
[key]: {
$t: 't',
value: newValueAfter,
Expand Down Expand Up @@ -451,35 +484,12 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
*
* @param event - model update event
* @param input - input element
* @param key - data key input is attached to
*/
#handleModelUpdateForNativeInput(event: ModelEvents, input: HTMLInputElement | HTMLTextAreaElement, key: DataKey): void {
if (!(event instanceof TextAddedEvent) && !(event instanceof TextRemovedEvent)) {
return;
}

const { textRange, dataKey, blockIndex } = event.detail.index;

if (textRange === undefined) {
return;
}

/**
* Event is not related to the attached block
*/
if (blockIndex !== this.#blockIndex) {
return;
}

/**
* Event is not related to the attached data key
*/
if (dataKey !== key) {
return;
}
#handleModelUpdateForNativeInput(event: ModelEvents, input: HTMLInputElement | HTMLTextAreaElement): void {
const { textRange } = event.detail.index;

const currentElement = input;
const [start, end] = textRange;
const [start, end] = textRange!;

const action = event.detail.action;

Expand Down Expand Up @@ -519,31 +529,10 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
* @param key - data key input is attached to
*/
#handleModelUpdateForContentEditableElement(event: ModelEvents, input: HTMLElement, key: DataKey): void {
if (!(event instanceof TextAddedEvent) && !(event instanceof TextRemovedEvent)) {
return;
}

const { textRange, dataKey, blockIndex } = event.detail.index;

if (blockIndex !== this.#blockIndex) {
return;
}

/**
* Event is not related to the attached data key
*/
if (dataKey !== key) {
return;
}

if (textRange === undefined) {
return;
}

const { textRange } = event.detail.index;
const action = event.detail.action;

const start = textRange[0];
const end = textRange[1];
const [start, end] = textRange!;

const [startNode, startOffset] = getBoundaryPointByAbsoluteOffset(input, start);
const [endNode, endOffset] = getBoundaryPointByAbsoluteOffset(input, end);
Expand Down Expand Up @@ -586,22 +575,51 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
* Handles model update events and updates DOM
*
* @param event - model update event
* @param input - attached input element
* @param key - data key input is attached to
*/
#handleModelUpdate(event: ModelEvents, input: HTMLElement, key: DataKey): void {
#handleModelUpdate(event: ModelEvents): void {
if (event instanceof BlockAddedEvent || event instanceof BlockRemovedEvent) {
if (event.detail.index.blockIndex! <= this.#blockIndex) {
this.#blockIndex += event.detail.action === EventAction.Added ? 1 : -1;
}

return;
}

const { textRange, dataKey, blockIndex } = event.detail.index;

if (blockIndex !== this.#blockIndex) {
return;
}


if (event instanceof DataNodeRemovedEvent) {
this.detachInput(dataKey as string);

return;
}

if (event instanceof DataNodeAddedEvent) {
/**
* @todo Decide how to handle this case as only BlockTool knows how to render an input
*/
}

if (!(event instanceof TextAddedEvent) && !(event instanceof TextRemovedEvent)) {
return;
}

const input = this.#attachedInputs.get(dataKey!);

if (!input || textRange === undefined) {
return;
}

const isInputNative = isNativeInput(input);

if (isInputNative === true) {
this.#handleModelUpdateForNativeInput(event, input as HTMLInputElement | HTMLTextAreaElement, key);
this.#handleModelUpdateForNativeInput(event, input as HTMLInputElement | HTMLTextAreaElement);
} else {
this.#handleModelUpdateForContentEditableElement(event, input, key);
this.#handleModelUpdateForContentEditableElement(event, input, dataKey!);
}
};
}
9 changes: 9 additions & 0 deletions packages/dom-adapters/src/CaretAdapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ export class CaretAdapter extends EventTarget {
this.#inputs.set(index.serialize(), input);
}

/**
* Removes input from the caret adapter
*
* @param index - index of the input to remove
*/
public detachInput(index: Index): void {
this.#inputs.delete(index.serialize());
}

/**
* Updates current user's caret index
*
Expand Down
2 changes: 2 additions & 0 deletions packages/model/src/EditorJSModel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ describe('EditorJSModel', () => {
'updateValue',
'removeBlock',
'moveBlock',
'createDataNode',
'removeDataNode',
'getText',
'insertText',
'removeText',
Expand Down
29 changes: 29 additions & 0 deletions packages/model/src/EditorJSModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,35 @@ export class EditorJSModel extends EventBus {
this.#document.modifyData(index, data);
}

/**
* Creates a data node (ValueNode or TextNode) with the specified key in the BlockNode at the specified index.
*
* @param _userId - user identifier which is being set to the context
* @param parameters - updateValue method parameters
* @param parameters.blockIndex - The index of the BlockNode to update
* @param parameters.dataKey - The key of the data node to create
* @param parameters.value - The initial value of the data node
* @throws Error if the index is out of bounds
*/
@WithContext
public createDataNode(_userId: string | number, ...parameters: Parameters<EditorDocument['createDataNode']>): ReturnType<EditorDocument['createDataNode']> {
return this.#document.createDataNode(...parameters);
}

/**
* Removes a data node (ValueNode or TextNode) with the specified key in the BlockNode at the specified index.
*
* @param _userId - user identifier which is being set to the context
* @param parameters - updateValue method parameters
* @param parameters.blockIndex - The index of the BlockNode to update
* @param parameters.dataKey - The key of the data node to remove
* @throws Error if the index is out of bounds
*/
@WithContext
public removeDataNode(_userId: string | number, ...parameters: Parameters<EditorDocument['removeDataNode']>): ReturnType<EditorDocument['removeDataNode']> {
return this.#document.removeDataNode(...parameters);
}

/**
* Updates the ValueNode data associated with the BlockNode at the specified index.
*
Expand Down
22 changes: 22 additions & 0 deletions packages/model/src/EventBus/events/DataNodeAddedEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { BlockNodeDataSerializedValue } from '../../entities/BlockNode/types/index.js';
import type { Index } from '../../entities/Index/index.js';
import { EventAction } from '../types/EventAction.js';
import { BaseDocumentEvent } from './BaseEvent.js';


/**
* DataNodeAdded Custom Event
*/
export class DataNodeAddedEvent extends BaseDocumentEvent<EventAction.Added, BlockNodeDataSerializedValue> {
/**
* DataNodeAdded class constructor
*
* @param index - index of the added BlockNode in the document
* @param data - data serialized value
* @param userId - user identifier
*/
constructor(index: Index, data: BlockNodeDataSerializedValue, userId: string | number) {
// Stryker disable next-line ObjectLiteral
super(index, EventAction.Added, data, userId);
}
}
22 changes: 22 additions & 0 deletions packages/model/src/EventBus/events/DataNodeRemovedEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { BlockNodeDataSerializedValue } from '../../entities/BlockNode/types/index.js';
import type { Index } from '../../entities/Index/index.js';
import { EventAction } from '../types/EventAction.js';
import { BaseDocumentEvent } from './BaseEvent.js';


/**
* DataNodeRemoved Custom Event
*/
export class DataNodeRemovedEvent extends BaseDocumentEvent<EventAction.Removed, BlockNodeDataSerializedValue> {
/**
* DataNodeRemoved class constructor
*
* @param index - index of the added BlockNode in the document
* @param data - data serialized value
* @param userId - user identifier
*/
constructor(index: Index, data: BlockNodeDataSerializedValue, userId: string | number) {

Check warning on line 18 in packages/model/src/EventBus/events/DataNodeRemovedEvent.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
// Stryker disable next-line ObjectLiteral
super(index, EventAction.Removed, data, userId);

Check warning on line 20 in packages/model/src/EventBus/events/DataNodeRemovedEvent.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}
}
2 changes: 2 additions & 0 deletions packages/model/src/EventBus/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export * from './ValueModifiedEvent.js';
export * from './CaretManagerCaretUpdatedEvent.js';
export * from './CaretManagerCaretAddedEvent.js';
export * from './CaretManagerCaretRemovedEvent.js';
export * from './DataNodeAddedEvent.js';
export * from './DataNodeRemovedEvent.js';
Loading