Skip to content

Commit

Permalink
Merge branch 'main' into inline-tool-adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
e11sy committed Aug 28, 2024
2 parents 6998adc + dada291 commit e6c4f7c
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 55 deletions.
25 changes: 12 additions & 13 deletions packages/core/src/utils/composeDataFromVersion2.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { OutputData } from '@editorjs/editorjs';
import { BlockChildType, type BlockNodeDataSerializedValue, type BlockNodeSerialized, type TextNodeSerialized } from '@editorjs/model';
import { TextNode, ValueNode, type BlockNodeSerialized } from '@editorjs/model';

/**
* Converst OutputData from version 2 to version 3
Expand All @@ -19,20 +19,19 @@ export function composeDataFromVersion2(data: OutputData): {
Object
.entries(block.data as Record<string, unknown>)
.map(([key, value]) => {
const valueObject: BlockNodeDataSerializedValue = {
value,
};

if (typeof value === 'string') {
(valueObject as TextNodeSerialized).$t = BlockChildType.Text;
}
const textNode = new TextNode({ value });

return [
key, {
value,
$t: typeof value === 'string' ? '$t' : '$v',
},
];
return [
key, textNode.serialized,
];
} else {
const valueNode = new ValueNode({ value });

return [
key, valueNode.serialized,
];
}
})
),
};
Expand Down
105 changes: 87 additions & 18 deletions packages/dom-adapters/src/BlockToolAdapter/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { type DataKey, type EditorJSModel, IndexBuilder, type ModelEvents } from '@editorjs/model';
import { isNativeInput } from '@editorjs/dom';
import {
type EditorJSModel,
type DataKey,
createDataKey,
EventAction,
EventType,
IndexBuilder,
type ModelEvents,
TextAddedEvent,
TextRemovedEvent
} from '@editorjs/model';
import { InputType } from './types/InputType.js';
import type { CaretAdapter } from '../CaretAdapter/index.js';
import {
findNextHardLineBoundary,
findNextWordBoundary, findPreviousHardLineBoundary,
findPreviousWordBoundary,
getAbsoluteRangeOffset,
getBoundaryPointByAbsoluteOffset,
isNonTextInput
} from '../utils/index.js';
import type { CaretAdapter } from '../CaretAdapter/index.js';

import { isNativeInput } from '@editorjs/dom';
import { InputType } from './types/InputType.js';

/**
* BlockToolAdapter is using inside Block tools to connect browser DOM elements to the model
Expand Down Expand Up @@ -99,8 +104,14 @@ export class BlockToolAdapter {
let end = input.selectionEnd;

/**
* @todo Handle all possible deletion events
* If selection is not collapsed, just remove selected text
*/
if (start !== end) {
this.#model.removeText(this.#blockIndex, key, start, end);

return;
}

switch (inputType) {
case InputType.DeleteContentForward: {
/**
Expand All @@ -109,13 +120,55 @@ export class BlockToolAdapter {
end = end !== input.value.length ? end + 1 : end;
break;
}
default: {
case InputType.DeleteContentBackward: {
/**
* If start is already 0, then there is nothing to delete
*/
start = start !== 0 ? start - 1 : start;

break;
}

case InputType.DeleteWordBackward: {
start = findPreviousWordBoundary(input.value, start);

break;
}

case InputType.DeleteWordForward: {
end = findNextWordBoundary(input.value, start);

break;
}

case InputType.DeleteHardLineBackward: {
start = findPreviousHardLineBoundary(input.value, start);

break;
}
case InputType.DeleteHardLineForward: {
end = findNextHardLineBoundary(input.value, start);

break;
}

case InputType.DeleteSoftLineBackward:
case InputType.DeleteSoftLineForward:
case InputType.DeleteEntireSoftLine:
/**
* @todo Think of how to find soft line boundaries
*/

case InputType.DeleteByDrag:
case InputType.DeleteByCut:
case InputType.DeleteContent:

default:
/**
* do nothing, use start and end from user selection
*/
}

this.#model.removeText(this.#blockIndex, key, start, end);
};

Expand Down Expand Up @@ -171,17 +224,25 @@ export class BlockToolAdapter {

switch (inputType) {
case InputType.InsertReplacementText:
case InputType.InsertFromDrop:
case InputType.InsertFromPaste: {
this.#model.removeText(this.#blockIndex, key, start, end);
if (start !== end) {
this.#model.removeText(this.#blockIndex, key, start, end);
}

let data: string;

/**
* DataTransfer object is guaranteed to be not null for these types of event for contenteditable elements
*
* However, it is not guaranteed for INPUT and TEXTAREA elements, so @todo handle this case
* For native inputs data for those events comes from event.data property
* while for contenteditable elements it's stored in event.dataTransfer
*
* @see https://www.w3.org/TR/input-events-2/#overview
*/
const data = event.dataTransfer!.getData('text/plain');
if (isInputNative) {
data = event.data ?? '';
} else {
data = event.dataTransfer!.getData('text/plain');
}

this.#model.insertText(this.#blockIndex, key, data, start);

Expand Down Expand Up @@ -215,6 +276,7 @@ export class BlockToolAdapter {
case InputType.DeleteHardLineForward:
case InputType.DeleteSoftLineBackward:
case InputType.DeleteSoftLineForward:
case InputType.DeleteEntireSoftLine:
case InputType.DeleteWordBackward:
case InputType.DeleteWordForward: {
if (isInputNative === true) {
Expand All @@ -225,6 +287,13 @@ export class BlockToolAdapter {
break;
}

case InputType.InsertLineBreak:
/**
* @todo Think if we need to keep that or not
*/
if (isInputNative === true) {
this.#model.insertText(this.#blockIndex, key, '\n', start);
}
default:
}
};
Expand Down Expand Up @@ -266,32 +335,32 @@ export class BlockToolAdapter {

const action = event.detail.action;

const builder = new IndexBuilder();
const caretIndexBuilder = new IndexBuilder();

builder.from(event.detail.index);
caretIndexBuilder.from(event.detail.index);

switch (action) {
case EventAction.Added: {
const text = event.detail.data as string;
const prevValue = currentElement.value;

currentElement.value = prevValue.slice(0, start) + text + prevValue.slice(end - 1);
currentElement.value = prevValue.slice(0, start) + text + prevValue.slice(start);

builder.addTextRange([start + text.length, start + text.length]);
caretIndexBuilder.addTextRange([start + text.length, start + text.length]);

break;
}
case EventAction.Removed: {
currentElement.value = currentElement.value.slice(0, start) +
currentElement.value.slice(end);

builder.addTextRange([start, start]);
caretIndexBuilder.addTextRange([start, start]);

break;
}
}

this.#caretAdapter.updateIndex(builder.build());
this.#caretAdapter.updateIndex(caretIndexBuilder.build());
};

/**
Expand Down
39 changes: 39 additions & 0 deletions packages/dom-adapters/src/utils/findHardLineBoundary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Finds nearest next carriage return symbol from passed position
*
* @param text - string to search in
* @param position - search starting position
*/
export function findNextHardLineBoundary(text: string, position: number): number {
const nextLineBoundary = /\n/g;

nextLineBoundary.lastIndex = position;

const match = nextLineBoundary.exec(text);

return match ? match.index : text.length;
}

/**
* Finds nearest previous caret symbol before passed position
*
* @param text - sting to search in
* @param position - search finish position
*/
export function findPreviousHardLineBoundary(text: string, position: number): number {
const previousLineBoundary = /\n/g;

let match = previousLineBoundary.exec(text);

while (match) {
const newMatch = previousLineBoundary.exec(text);

if (!newMatch || newMatch.index >= position) {
break;
}

match = newMatch;
}

return match && match.index < position ? match.index : 0;
}
62 changes: 62 additions & 0 deletions packages/dom-adapters/src/utils/findWordBoundary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const APOSTROPHE_AND_CURLY_QUOTES = "['\u2018\u2019]";
const PUNCTUATION = '.,!?:;"\\(\\){}\\[\\]<>@*~\\/\\-#$&|^%+=';
const WHITESPACE = '\\s';

const WHITESPACE_AND_PUNCTUATION = `[${WHITESPACE}${PUNCTUATION}]`;

/**
* Finds the nearest next word boundary from the passed position
*
* @param text - string to search in
* @param position - search starting position
*/
export function findNextWordBoundary(text: string, position: number): number {
const nextWordBoundary = new RegExp(
/**
* Match whitespace or punctuation
* or an apostrophe or curly quotes followed by a whitespace character or punctuation
*/
`(${WHITESPACE_AND_PUNCTUATION}|${APOSTROPHE_AND_CURLY_QUOTES}(?=${WHITESPACE_AND_PUNCTUATION}))`,
'g'
);

/**
* Start searching from the next character to allow word deletion with one non-word character before the word
*/
nextWordBoundary.lastIndex = position + 1;

const match = nextWordBoundary.exec(text);

return match ? match.index : text.length;
}

/**
* Finds the nearest previous word boundary before the passed position
*
* @param text - string to search in
* @param position - search finish position
*/
export function findPreviousWordBoundary(text: string, position: number): number {
const previousWordBoundary = new RegExp(
/**
* Match whitespace or punctuation,
* or an apostrophe or curly quotes preceded by whitespace or punctuation
*/
`(${WHITESPACE_AND_PUNCTUATION}|(?<=${WHITESPACE_AND_PUNCTUATION})${APOSTROPHE_AND_CURLY_QUOTES})`,
'g'
);

let match = previousWordBoundary.exec(text);

while (match) {
const newMatch = previousWordBoundary.exec(text);

if (!newMatch || newMatch.index >= position) {
break;
}

match = newMatch;
}

return match && match.index < position ? match.index : 0;
}
2 changes: 2 additions & 0 deletions packages/dom-adapters/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ export * from './getAbsoluteRangeOffset.js';
export * from './getRelativeIndex.js';
export * from './isNonTextInput.js';
export * from './useSelectionChange.js';
export * from './findWordBoundary.js';
export * from './findHardLineBoundary.js';
export * from './doRangesIntersect.js';
export * from './mergeTextRanges.js';
9 changes: 0 additions & 9 deletions packages/model/src/CaretManagement/Caret/Caret.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,6 @@ describe('Caret', () => {
}));
});

it('should not update index if it is the same', () => {
const caret = new Caret(new Index());
const index = new Index();

caret.update(index);

expect(caret.index).not.toBe(index);
});

it('should serialize to JSON', () => {
const index = new Index();
const caret = new Caret(index);
Expand Down
4 changes: 0 additions & 4 deletions packages/model/src/CaretManagement/Caret/Caret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@ export class Caret extends EventBus {
* @param index - new caret index
*/
public update(index: Index): void {
if (this.#index?.serialize() === index.serialize()) {
return;
}

this.#index = index;

this.dispatchEvent(new CaretUpdatedEvent(this));
Expand Down
4 changes: 2 additions & 2 deletions packages/model/src/entities/BlockNode/BlockNode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ describe('BlockNode', () => {
expect(() => {
blockNode.updateValue(dataKey, value);
})
.toThrowError(`BlockNode: data with key ${dataKey} does not exist`);
.toThrowError(`BlockNode: data with key "${dataKey}" does not exist`);
});

it('should throw an error if the ValueNode with the passed dataKey is not a ValueNode', () => {
Expand All @@ -570,7 +570,7 @@ describe('BlockNode', () => {
expect(() => {
blockNode.updateValue(dataKey, value);
})
.toThrowError(`BlockNode: data with key ${dataKey} is not a ValueNode`);
.toThrowError(`BlockNode: data with key "${dataKey}" is not a ValueNode`);
});
});

Expand Down
Loading

0 comments on commit e6c4f7c

Please sign in to comment.