-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move all utils to separate files, use caret to get range
- Loading branch information
Showing
17 changed files
with
477 additions
and
453 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
@@ -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 | ||
|
@@ -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 GitHub Actions / lint
|
||
* InlineToolAdapter constructor | ||
* | ||
|
@@ -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(); | ||
} | ||
|
@@ -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); | ||
} | ||
|
||
|
@@ -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 { | ||
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); | ||
} | ||
|
||
/** | ||
|
@@ -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 | ||
|
@@ -233,7 +185,7 @@ export class InlineToolAdapter { | |
unwrapByToolType(range, data.tool); | ||
} | ||
|
||
normalize(this.#input); | ||
normalizeNode(this.#input); | ||
this.#input.normalize(); | ||
}); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.