Skip to content

Commit

Permalink
feat(core): inputs content rendering (#86)
Browse files Browse the repository at this point in the history
* passing formattingadapter to blocktooladapter

* lint

* Update BlockManager.ts

* Update BlockManager.ts

* Update App.vue
  • Loading branch information
neSpecc authored Aug 30, 2024
1 parent 1c8dcf2 commit fd02b47
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 33 deletions.
16 changes: 12 additions & 4 deletions packages/core/src/BlockManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BlockAddedEvent, BlockRemovedEvent, EditorJSModel, EventType, ModelEven
import 'reflect-metadata';
import { Service } from 'typedi';
import { EditorUI } from './ui/Editor/index.js';
import { BlockToolAdapter, CaretAdapter } from '@editorjs/dom-adapters';
import { BlockToolAdapter, CaretAdapter, FormattingAdapter } from '@editorjs/dom-adapters';
import ToolsManager from './tools/ToolsManager.js';
import { BlockAPI } from '@editorjs/editorjs';

Expand Down Expand Up @@ -34,24 +34,32 @@ export class BlocksManager {
*/
#toolsManager: ToolsManager;

/**
* Will be passed to BlockToolAdapter for rendering inputs` formatted text
*/
#formattingAdapter: FormattingAdapter;

/**
* BlocksManager constructor
* All parameters are injected thorugh the IoC container
* @param model - Editor's Document Model instance
* @param editorUI - Editor's UI class instance
* @param caretAdapter - Caret Adapter instance
* @param toolsManager - Tools manager instance
* @param formattingAdapter - will be passed to BlockToolAdapter for rendering inputs` formatted text
*/
constructor(
model: EditorJSModel,
editorUI: EditorUI,
caretAdapter: CaretAdapter,
toolsManager: ToolsManager
toolsManager: ToolsManager,
formattingAdapter: FormattingAdapter
) {
this.#model = model;
this.#editorUI = editorUI;
this.#caretAdapter = caretAdapter;
this.#toolsManager = toolsManager;
this.#formattingAdapter = formattingAdapter;

this.#model.addEventListener(EventType.Changed, event => this.#handleModelUpdate(event));
}
Expand Down Expand Up @@ -87,11 +95,11 @@ export class BlocksManager {
throw new Error('[BlockManager] Block index should be defined. Probably something wrong with the Editor Model. Please, report this issue');
}

const blockToolAdapter = new BlockToolAdapter(this.#model, this.#caretAdapter, index.blockIndex);
const blockToolAdapter = new BlockToolAdapter(this.#model, this.#caretAdapter, index.blockIndex, this.#formattingAdapter);

const tool = this.#toolsManager.blockTools.get(event.detail.data.name);

if (!tool) {
if (tool === undefined) {
throw new Error(`[BlockManager] Block Tool ${event.detail.data.name} not found`);
}

Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ContainerInstance } from 'typedi';
import { Container } from 'typedi';
import { composeDataFromVersion2 } from './utils/composeDataFromVersion2.js';
import ToolsManager from './tools/ToolsManager.js';
import { CaretAdapter, InlineToolsAdapter } from '@editorjs/dom-adapters';
import { CaretAdapter, FormattingAdapter } from '@editorjs/dom-adapters';
import { InlineToolbar } from './ui/InlineToolbar/index.js';
import type { CoreConfigValidated } from './entities/Config.js';
import type { CoreConfig } from '@editorjs/sdk';
Expand Down Expand Up @@ -53,7 +53,7 @@ export default class Core {
* Applies format, got from inline toolbar to the model
* When model changed with formatting event, it renders related fragment
*/
#inlineToolsAdapter: InlineToolsAdapter;
#formattingAdapter: FormattingAdapter;

/**
* @todo inline toolbar should subscripe on selection change event called by EventBus
Expand Down Expand Up @@ -86,10 +86,10 @@ export default class Core {
this.#caretAdapter = new CaretAdapter(this.#config.holder, this.#model);
this.#iocContainer.set(CaretAdapter, this.#caretAdapter);

this.#inlineToolsAdapter = new InlineToolsAdapter(this.#model, this.#caretAdapter);
this.#iocContainer.set(InlineToolsAdapter, this.#inlineToolsAdapter);
this.#formattingAdapter = new FormattingAdapter(this.#model, this.#caretAdapter);
this.#iocContainer.set(FormattingAdapter, this.#formattingAdapter);

this.#inlineToolbar = new InlineToolbar(this.#model, this.#inlineToolsAdapter, this.#toolsManager.inlineTools, this.#config.holder);
this.#inlineToolbar = new InlineToolbar(this.#model, this.#formattingAdapter, this.#toolsManager.inlineTools, this.#config.holder);
this.#iocContainer.set(InlineToolbar, this.#inlineToolbar);

this.#iocContainer.get(BlocksManager);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class Paragraph implements BlockTool<ParagraphData, ParagraphConfig> {

wrapper.contentEditable = 'true';
wrapper.style.outline = 'none';
wrapper.style.whiteSpace = 'pre-wrap';

this.#adapter.attachInput('text', wrapper);

Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/ui/InlineToolbar/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { InlineToolsAdapter } from '@editorjs/dom-adapters';
import type { FormattingAdapter } from '@editorjs/dom-adapters';
import type { InlineToolName } from '@editorjs/model';
import { type EditorJSModel, type TextRange, createInlineToolData, Index } from '@editorjs/model';
import { EventType } from '@editorjs/model';
Expand All @@ -21,7 +21,7 @@ export class InlineToolbar {
* Inline tool adapter instance
* Used for inline tools attaching and format apply
*/
#inlineToolAdapter: InlineToolsAdapter;
#formattingAdapter: FormattingAdapter;

/**
* Current selection range
Expand All @@ -42,13 +42,13 @@ export class InlineToolbar {

/**
* @param model - editor model instance
* @param inlineToolAdapter - inline tool adapter instance
* @param formattingAdapter - needed for applying format to the model
* @param tools - tools, that should be attached to adapter
* @param holder - editor holder element
*/
constructor(model: EditorJSModel, inlineToolAdapter: InlineToolsAdapter, tools: ToolsCollection<InlineToolFacade>, holder: HTMLElement) {
constructor(model: EditorJSModel, formattingAdapter: FormattingAdapter, tools: ToolsCollection<InlineToolFacade>, holder: HTMLElement) {
this.#model = model;
this.#inlineToolAdapter = inlineToolAdapter;
this.#formattingAdapter = formattingAdapter;
this.#holder = holder;
this.#tools = tools;

Expand Down Expand Up @@ -93,7 +93,7 @@ export class InlineToolbar {
*/
#attachTools(): void {
Array.from(this.#tools.entries()).forEach(([toolName, tool]) => {
this.#inlineToolAdapter.attachTool(toolName as InlineToolName, tool.create());
this.#formattingAdapter.attachTool(toolName as InlineToolName, tool.create());
});
}

Expand Down Expand Up @@ -154,6 +154,6 @@ export class InlineToolbar {
/**
* @todo pass to applyFormat inline tool data formed in toolbar
*/
this.#inlineToolAdapter.applyFormat(toolName, createInlineToolData({}));
this.#formattingAdapter.applyFormat(toolName, createInlineToolData({}));
};
}
83 changes: 81 additions & 2 deletions packages/core/src/utils/composeDataFromVersion2.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
import type { OutputData } from '@editorjs/editorjs';
import { TextNode, ValueNode, type BlockNodeSerialized } from '@editorjs/model';
import type { InlineFragment } from '@editorjs/model';
import { createInlineToolData, createInlineToolName, TextNode, ValueNode, type BlockNodeSerialized } from '@editorjs/model';

/**
* Extracts inline fragments from the HTML string
* @param html - any html string like '<b>bold</b> <a href="https://editorjs.io">link</a>'
*
* NOW ONLY <b>, <strong> AND <a> TAGS ARE SUPPORTED
* @todo support all inline tools
*/
function extractFragments(html: string): InlineFragment[] {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const fragments: InlineFragment[] = [];
let index = 0;

/**
* Traverses children of the parent node
* @param parent - node to traverse children from
* @param startIndex - start index of the text
*/
function traverseChildren(parent: HTMLElement, startIndex: number): number {
parent.childNodes.forEach((child) => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
startIndex = traverse(child, startIndex);
});

return startIndex;
}

/**
* Traverses the node and its children
* @param node - node to traverse children from
* @param startIndex - start index of the text
*/
function traverse(node: ChildNode, startIndex: number): number {
if (node.nodeType === Node.TEXT_NODE) {
index += node.textContent?.length ?? 0;

return index;
}

if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement;
const tagName = element.tagName.toLowerCase();
const currentStartIndex = startIndex;

if (tagName === 'b' || tagName === 'strong') {
index = traverseChildren(element, startIndex);
fragments.push({
tool: createInlineToolName('bold'),
range: [currentStartIndex, index],
});
} else if (tagName === 'a') {
const href = element.getAttribute('href') ?? '';

index = traverseChildren(element, startIndex);
fragments.push({
tool: createInlineToolName('link'),
data: createInlineToolData({
href,
}),
range: [currentStartIndex, index],
});
} else {
index = traverseChildren(element, startIndex);
}
}

return index;
}

traverseChildren(doc.body, 0);

return fragments;
}

/**
* Converst OutputData from version 2 to version 3
Expand All @@ -20,7 +95,11 @@ export function composeDataFromVersion2(data: OutputData): {
.entries(block.data as Record<string, unknown>)
.map(([key, value]) => {
if (typeof value === 'string') {
const textNode = new TextNode({ value });
const fragments = extractFragments(value);
const textNode = new TextNode({
value,
fragments,
});

return [
key, textNode.serialized,
Expand Down
19 changes: 16 additions & 3 deletions packages/dom-adapters/src/BlockToolAdapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '../utils/index.js';
import { InputType } from './types/InputType.js';
import type { BlockToolAdapter as BlockToolAdapterInterface } from '@editorjs/sdk';
import type { FormattingAdapter } from '../InlineToolsAdapter/index.js';

/**
* BlockToolAdapter is using inside Block tools to connect browser DOM elements to the model
Expand All @@ -40,22 +41,27 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {

/**
* Caret adapter instance
*
* @private
*/
#caretAdapter: CaretAdapter;

/**
* Formatting adapter instance
*/
#formattingAdapter: FormattingAdapter;

/**
* BlockToolAdapter constructor
*
* @param model - EditorJSModel instance
* @param caretAdapter - CaretAdapter instance
* @param blockIndex - index of the block that this adapter is connected to
* @param formattingAdapter - needed to render formatted text
*/
constructor(model: EditorJSModel, caretAdapter: CaretAdapter, blockIndex: number) {
constructor(model: EditorJSModel, caretAdapter: CaretAdapter, blockIndex: number, formattingAdapter: FormattingAdapter) {
this.#model = model;
this.#blockIndex = blockIndex;
this.#caretAdapter = caretAdapter;
this.#formattingAdapter = formattingAdapter;
}

/**
Expand All @@ -81,6 +87,13 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
builder.addBlockIndex(this.#blockIndex).addDataKey(key);

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

const fragments = this.#model.getFragments(this.#blockIndex, key);

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

/**
Expand Down
6 changes: 3 additions & 3 deletions packages/dom-adapters/src/InlineToolsAdapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { InlineTool } from '@editorjs/sdk';
* Class handles on format model events and renders inline tools
* Applies format to the model
*/
export class InlineToolsAdapter {
export class FormattingAdapter {
/**
* Editor model instance
*/
Expand Down Expand Up @@ -108,7 +108,7 @@ export class InlineToolsAdapter {
const index = this.#caretAdapter.userCaretIndex;

if (index === null) {
throw new Error('InlineToolsAdapter: caret index is outside of the input');
throw new Error('FormattingAdapter: caret index is outside of the input');
}

const textRange = index.textRange;
Expand All @@ -129,7 +129,7 @@ export class InlineToolsAdapter {
const tool = this.#tools.get(toolName);

if (tool === undefined) {
throw new Error(`InlineToolsAdapter: tool ${toolName} is not attached`);
throw new Error(`FormattingAdapter: tool ${toolName} is not attached`);
}

const fragments = this.#model.getFragments(blockIndex, dataKey, ...textRange, toolName);
Expand Down
21 changes: 12 additions & 9 deletions packages/playground/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import CaretIndex from '@/components/CaretIndex.vue';
import { BlockToolAdapter, CaretAdapter } from '@editorjs/dom-adapters';
import { BlockToolAdapter, CaretAdapter, FormattingAdapter } from '@editorjs/dom-adapters';
import { EditorDocument, EditorJSModel, EventType } from '@editorjs/model';
import Core from '@editorjs/core';
import { ref, onMounted } from 'vue';
Expand Down Expand Up @@ -45,8 +45,9 @@ const caretAdapter = new CaretAdapter(window.document.body, model);
/**
* Block Tool Adapter instance will be passed to a Tool constructor by Editor.js core
*/
const blockToolAdapter = new BlockToolAdapter(model, caretAdapter, 0);
const anotherBlockToolAdapter = new BlockToolAdapter(model, caretAdapter, 1);
const formattingAdapter = new FormattingAdapter(model, caretAdapter);
const blockToolAdapter = new BlockToolAdapter(model, caretAdapter, 0, formattingAdapter);
const anotherBlockToolAdapter = new BlockToolAdapter(model, caretAdapter, 1, formattingAdapter);
const serialized = ref(model.serialized);
Expand All @@ -63,7 +64,7 @@ onMounted(() => {
blocks: [ {
type: 'paragraph',
data: {
text: 'Hello, World!',
text: 'Hello, <b>World</b>!',
},
} ],
},
Expand All @@ -85,6 +86,12 @@ onMounted(() => {
</div>
</div>
<div :class="$style.body">
<div>
<div
id="editorjs"
:class="$style.editor"
/>
</div>
<div :class="$style.playground">
<CaretIndex :model="model" />
<Input
Expand All @@ -111,10 +118,6 @@ onMounted(() => {
<Node
:node="editorDocument"
/>
<div
id="editorjs"
:class="$style.editor"
/>
</div>
</div>
</template>
Expand All @@ -128,7 +131,7 @@ onMounted(() => {
.body {
padding: 16px;
display: grid;
grid-template-columns: 50% 50%;
grid-template-columns: repeat(3, 1fr);
grid-gap: 16px;
}
Expand Down

2 comments on commit fd02b47

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for ./packages/model

St.
Category Percentage Covered / Total
🟢 Statements 100% 753/753
🟢 Branches 99.5% 201/202
🟢 Functions 98.37% 181/184
🟢 Lines 100% 725/725

Test suite run success

389 tests passing in 24 suites.

Report generated by 🧪jest coverage report action from fd02b47

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for ./packages/collaboration-manager

St.
Category Percentage Covered / Total
🟢 Statements 86.11% 62/72
🟡 Branches 62.5% 15/24
🟢 Functions 100% 10/10
🟢 Lines 86.11% 62/72

Test suite run success

6 tests passing in 1 suite.

Report generated by 🧪jest coverage report action from fd02b47

Please sign in to comment.