Skip to content

Commit

Permalink
Move all utils to separate files, use caret to get range
Browse files Browse the repository at this point in the history
  • Loading branch information
gohabereg committed Nov 26, 2023
1 parent 0d00ef7 commit 7dcbf60
Show file tree
Hide file tree
Showing 17 changed files with 477 additions and 453 deletions.
154 changes: 53 additions & 101 deletions packages/dom-adapters/src/InlineToolAdapter/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
import type { DataKey, EditorJSModel, InlineToolData, InlineToolName, ModelEvents } from '@editorjs/model';
import type {
DataKey,
EditorJSModel,
InlineFragment,
InlineToolData,
InlineToolName,
ModelEvents,
TextRange
} from '@editorjs/model';
import { EventType, TextFormattedEvent, TextUnformattedEvent } from '@editorjs/model';
import { getAbsoluteOffset, getRange, normalize, unwrapByToolType } from '../utils/helpers.js';
import { unwrapByToolType, createRange, normalizeNode } from '../utils/index.js';
import type { CaretAdapter } from '../caret/CaretAdapter.js';

export interface InlineTool {
name: InlineToolName;
create(data?: InlineToolData): HTMLElement;
getAction(index: TextRange, fragments: InlineFragment[], data?: InlineToolData): { action: FormattingAction; range: TextRange };
}

export enum FormattingAction {
Format = 'format',
Unformat = 'unformat',
}

/**
* InlineToolAdapter class is used to connect InlineTools with the EditorJS model and apply changes into the DOM
Expand All @@ -14,7 +33,7 @@ export class InlineToolAdapter {
*
* @todo Replace any with InlineTool type
*/
#tools: Map<InlineToolName, any> = new Map();
#tools: Map<InlineToolName, InlineTool> = new Map();

/**
* EditorJS model
Expand Down Expand Up @@ -44,6 +63,11 @@ export class InlineToolAdapter {
*/
#input: HTMLElement;

/**
* Caret adapter instance for the input
*/
#caretAdapter: CaretAdapter;

/**

Check warning on line 71 in packages/dom-adapters/src/InlineToolAdapter/index.ts

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @param "caretAdapter" declaration
* InlineToolAdapter constructor
*
Expand All @@ -52,11 +76,12 @@ export class InlineToolAdapter {
* @param dataKey - key of the data in the BlockNode inline tool is related to
* @param input - input element inline tool is related to
*/
constructor(model: EditorJSModel, blockIndex: number, dataKey: DataKey, input: HTMLElement) {
constructor(model: EditorJSModel, blockIndex: number, dataKey: DataKey, input: HTMLElement, caretAdapter: CaretAdapter) {
this.#model = model;
this.#blockIndex = blockIndex;
this.#dataKey = dataKey;
this.#input = input;
this.#caretAdapter = caretAdapter;

this.#subscribe();
}
Expand All @@ -69,7 +94,7 @@ export class InlineToolAdapter {
*
* @todo Replace any with InlineTool type
*/
public attachTool(tool: any): void {
public attachTool(tool: InlineTool): void {
this.#tools.set(tool.name, tool);
}

Expand All @@ -78,114 +103,41 @@ export class InlineToolAdapter {
*
* @param tool - tool to detach
*/
public detachTool(tool: any): void {
public detachTool(tool: InlineTool): void {
this.#tools.delete(tool.name);
}

/**
* Applies formatting to the input using current selection
*
* @param tool - name of the tool to apply
* @param [data] - inline tool data if applicable
*/
public format(tool: InlineToolName, data?: InlineToolData): void;
/**
* Applies formatting to the input using specified range
*
* @param tool - name of the tool to apply
* @param start - char start index of the range
* @param end - char end index of the range
* @param [data] - inline tool data if applicable
*/
public format(tool: InlineToolName, start: number, end: number, data?: InlineToolData): void;
/**
* General declaration
*
* @param args - arguments
*/
public format(...args: [InlineToolName, number | InlineToolData | undefined, number?, InlineToolData?]): void {
let tool: InlineToolName, start: number, end: number, data: InlineToolData | undefined;
public applyFormat(toolName: InlineToolName, data: InlineToolData): void {

Check warning on line 110 in packages/dom-adapters/src/InlineToolAdapter/index.ts

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc comment
const index = this.#caretAdapter.getIndex();

if (args.length < 3) {
tool = args[0];
data = args[1] as InlineToolData | undefined;

const selection = window.getSelection();

if (!selection || selection.rangeCount === 0) {
return;
}
if (index === null) {
console.warn('InlineToolAdapter: caret index is outside of the input');

const range = selection?.getRangeAt(0);

const comparison = range.compareBoundaryPoints(Range.START_TO_END, range);

if (comparison >= 0) {
start = getAbsoluteOffset(this.#input, range.startContainer, range.startOffset);
end = getAbsoluteOffset(this.#input, range.endContainer, range.endOffset);
} else {
start = getAbsoluteOffset(this.#input, range.endContainer, range.endOffset);
end = getAbsoluteOffset(this.#input, range.startContainer, range.startOffset);
}
} else {
tool = args[0];
start = args[1] as number;
end = args[2] as number;
data = args[3] as InlineToolData | undefined;
return;
}

const tool = this.#tools.get(toolName);

this.#model.format(this.#blockIndex, this.#dataKey, tool, start, end, data);
}

/**
* Removes formatting from the input using current selection
*
* @param tool - name of the tool to remove formatting for
*/
public unformat(tool: InlineToolName): void;
/**
* Removes formatting from the input using specified range
*
* @param tool - name of the tool to remove formatting for
* @param start - char start index of the range
* @param end - char end index of the range
*/
public unformat(tool: InlineToolName, start: number, end: number): void;
/**
* General declaration
* @param args - arguments
*/
public unformat(...args: [InlineToolName, number?, number?]): void {
let tool: InlineToolName, start: number, end: number;
if (!tool) {
console.warn(`InlineToolAdapter: tool ${toolName} is not attached`);

if (args.length === 1) {
tool = args[0];
return;
}

const selection = window.getSelection();
const fragments = this.#model.getFragments(this.#blockIndex, this.#dataKey, ...index, toolName);

if (!selection || selection.rangeCount === 0) {
return;
}
const { action, range } = tool.getAction(index, fragments, data);

const range = selection?.getRangeAt(0);
switch (action) {
case FormattingAction.Format:
this.#model.format(this.#blockIndex, this.#dataKey, toolName, ...range, data);

const comparison = range.compareBoundaryPoints(Range.START_TO_END, range);
break;
case FormattingAction.Unformat:
this.#model.unformat(this.#blockIndex, this.#dataKey, toolName, ...range);

if (comparison >= 0) {
start = getAbsoluteOffset(this.#input, range.startContainer, range.startOffset);
end = getAbsoluteOffset(this.#input, range.endContainer, range.endOffset);
} else {
start = getAbsoluteOffset(this.#input, range.endContainer, range.endOffset);
end = getAbsoluteOffset(this.#input, range.startContainer, range.startOffset);
}
} else {
tool = args[0];
start = args[1] as number;
end = args[2] as number;
break;
}

this.#model.unformat(this.#blockIndex, this.#dataKey, tool, start, end);
}

/**
Expand All @@ -206,14 +158,14 @@ export class InlineToolAdapter {
return;
}

const tool = this.#tools.get(data.tool);
const tool = this.#tools.get(data.tool)!;
const wrappedTool = tool.create(data.data);

wrappedTool.dataset.tool = data.tool;

const [start, end] = index[0];

const range = getRange(this.#input, start, end);
const range = createRange(this.#input, start, end);

/**
* Apply formatting to the input
Expand All @@ -233,7 +185,7 @@ export class InlineToolAdapter {
unwrapByToolType(range, data.tool);
}

normalize(this.#input);
normalizeNode(this.#input);
this.#input.normalize();
});
}
Expand Down
27 changes: 22 additions & 5 deletions packages/dom-adapters/src/caret/CaretAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { EditorJSModel, TextRange } from '@editorjs/model';
import { useSelectionChange, type Subscriber, type InputWithCaret } from './utils/useSelectionChange.js';
import { getAbsoluteRangeOffset } from './utils/absoluteOffset.js';

import { useSelectionChange, type Subscriber, type InputWithCaret, getAbsoluteRangeOffset } from '../utils/index.js';
/**
* Caret adapter watches input caret change and passes it to the model
*
Expand Down Expand Up @@ -87,6 +85,13 @@ export class CaretAdapter extends EventTarget {
this.#input = null;
}

/**
* Returns caret index
*/
public getIndex(): TextRange | null {
return this.#index;
}

/**
* Callback that will be called on document selection change
*
Expand All @@ -102,9 +107,21 @@ export class CaretAdapter extends EventTarget {
* @param selection - changed document selection
*/
#updateIndex(selection: Selection | null): void {
const range = selection?.getRangeAt(0);
if (selection === null) {
this.#index = null;

this.dispatchEvent(new CustomEvent('change', {
detail: {
index: this.#index,
},
}));

if (!range || !this.#input) {
return;
}

const range = selection.getRangeAt(0);

if (!this.#input) {
return;
}

Expand Down
6 changes: 1 addition & 5 deletions packages/dom-adapters/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
export * from './caret/CaretAdapter.js';
export * from './InlineToolAdapter/index.js';
export { normalize } from './utils/helpers.js';
export { unwrapByToolType } from './utils/helpers.js';
export { getRange } from './utils/helpers.js';
export { getLength } from './utils/helpers.js';
export { getAbsoluteOffset } from './utils/helpers.js';
export * from './utils/index.js';
97 changes: 97 additions & 0 deletions packages/dom-adapters/src/utils/createRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { getNodeTextLength } from './getNodeTextLength.js';
import { findChildByCharIndex } from './findChildByCharIndex.js';

/**
* Returns DOM Range by char range (relative to input)
*
* @param input - input element char indexes are related to
* @param start - start char index
* @param end - end char index
*/
export function createRange(input: HTMLElement, start: number, end: number): Range {
const length = getNodeTextLength(input);

if (start < 0 || start > length || end < 0 || end > length) {
throw new Error('InlineToolAdapter: range is out of bounds');
}

let startContainer: Node = input;
let startOffset = start;
let endContainer: Node = input;
let endOffset = end;

/**
* Find start node and offset for the range
*/
while (!(startContainer instanceof Text)) {
const [child, offset] = findChildByCharIndex(startContainer, startOffset);

startContainer = child;
startOffset = startOffset - offset;
}

/**
* Find end node and offset for the range
*/
while (!(endContainer instanceof Text)) {
const [child, offset] = findChildByCharIndex(endContainer, endOffset);

endContainer = child;
endOffset = endOffset - offset;
}

const range = new Range();

/**
* If startOffset equals to the length of the startContainer, we need to set start after the startContainer
*
* However, we also need to consider parent nodes siblings
*/
if (startOffset === getNodeTextLength(startContainer)) {
let nextSibling = startContainer.nextSibling;
let parent = startContainer.parentNode!;

while (nextSibling === null && parent !== input && parent !== null) {
nextSibling = parent.nextSibling;
parent = parent.parentNode!;
}

if (nextSibling !== null) {
range.setStartBefore(nextSibling);
} else {
range.setStartAfter(startContainer);
}
} else {
range.setStart(startContainer, startOffset);
}

/**
* If endOffset equals to 0, we need to set end before the endContainer
*/
if (endOffset === 0) {
range.setEndBefore(endContainer);
/**
* If endOffset equals to the length of the endContainer, we need to set end after the endContainer
*
* We need to consider parent nodes siblings as well
*/
} else if (endOffset === getNodeTextLength(endContainer)) {
let nextSibling = endContainer.nextSibling;
let parent = endContainer.parentNode!;

while (nextSibling === null && parent !== input) {
nextSibling = parent.nextSibling;
parent = parent.parentNode!;
}

if (nextSibling !== null) {
range.setEndBefore(nextSibling);
} else {
range.setEndAfter(endContainer);
}
} else {
range.setEnd(endContainer, endOffset);
}

return range;
}
Loading

0 comments on commit 7dcbf60

Please sign in to comment.