Skip to content

Fix inputs and values attachments to be data-first#127

Open
gohabereg wants to merge 15 commits intomainfrom
fix/inputs-registration
Open

Fix inputs and values attachments to be data-first#127
gohabereg wants to merge 15 commits intomainfrom
fix/inputs-registration

Conversation

@gohabereg
Copy link
Copy Markdown
Member

@gohabereg gohabereg commented Apr 17, 2026

  • Changed BlockToolAdapter API to make it data-first
  • Replaced typedi with injectify
  • Added abstract BlockToolAdapter class
  • Adapters now connected as a plugin to the core
  • Core uses only BlockToolAdapter, all the interdependencies between adapters happens inside the DOMAdapter plugin

Notes:

  • It seems we would need to move towards block id's

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

⏭️ No files to mutate for ./packages/model

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

Coverage report for ./packages/dom-adapters

St.
Category Percentage Covered / Total
🟢 Statements 96.43% 27/28
🟢 Branches 86.96% 20/23
🟢 Functions 100% 5/5
🟢 Lines 96.43% 27/28

Test suite run success

11 tests passing in 2 suites.

Report generated by 🧪jest coverage report action from 7fe438f

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

⏭️ No files to mutate for ./packages/core

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

Coverage report for ./packages/model

St.
Category Percentage Covered / Total
🟢 Statements
100% (+0.11% 🔼)
905/905
🟢 Branches
99.63% (+0.01% 🔼)
266/267
🟢 Functions
97.75% (-0.42% 🔻)
217/222
🟢 Lines
100% (+0.12% 🔼)
869/869
Show files with reduced coverage 🔻
St.
File Statements Branches Functions Lines
🟢
... / index.ts
100% 100%
93.94% (-6.06% 🔻)
100%

Test suite run success

475 tests passing in 25 suites.

Report generated by 🧪jest coverage report action from 7fe438f

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

Coverage report for ./packages/core

St.
Category Percentage Covered / Total
🟢 Statements 100% 128/128
🟢 Branches 100% 46/46
🟢 Functions 100% 27/27
🟢 Lines 100% 119/119
Show new covered files 🐣
St.
File Statements Branches Functions Lines
🟢 tokens.ts 100% 100% 100% 100%
🟢
... / SelectionManager.ts
100% 100% 100% 100%
🟢
... / BlockManager.ts
100% 100% 100% 100%
🟢 api/BlocksAPI.ts 100% 100% 100% 100%
🟢
... / BlockRenderer.ts
100% 100% 100% 100%
🟢 api/SelectionAPI.ts 100% 100% 100% 100%
🟢
... / DocumentAPI.ts
100% 100% 100% 100%

Test suite run success

73 tests passing in 7 suites.

Report generated by 🧪jest coverage report action from 7fe438f

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

Coverage report for ./packages/ot-server

St.
Category Percentage Covered / Total
🟡 Statements 70.97% 22/31
🔴 Branches 20% 1/5
🟡 Functions 75% 6/8
🟡 Lines 68.97% 20/29

Test suite run success

4 tests passing in 1 suite.

Report generated by 🧪jest coverage report action from 7fe438f

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

Coverage report for ./packages/collaboration-manager

St.
Category Percentage Covered / Total
🟢 Statements 92.51% 358/387
🟢 Branches 85.51% 118/138
🟢 Functions 98.15% 53/54
🟢 Lines 92.41% 353/382

Test suite run success

117 tests passing in 7 suites.

Report generated by 🧪jest coverage report action from 7fe438f

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

⏭️ No files to mutate for ./packages/dom-adapters

Comment thread packages/sdk/src/entities/BlockToolAdapter.ts
}

wrapper.classList.add('editorjs-paragraph');
onUpdate = (key: string, type: 'text' | 'value'): HTMLElement => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
onUpdate = (key: string, type: 'text' | 'value'): HTMLElement => {
onDataUpdated = (key: string, type: 'text' | 'value'): HTMLElement => {

this.attachInput(dataKey, input);
}

public registerKey(keyRaw: string, type: 'text' | 'value', initialData?: unknown): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
public registerKey(keyRaw: string, type: 'text' | 'value', initialData?: unknown): void {
public registerDataKey(keyRaw: string, type: 'text' | 'value', initialData?: unknown): void {

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the editor’s adapter/tool integration to be “data-first” by introducing an abstract BlockToolAdapter in the SDK, moving DOM-specific interdependencies into a dedicated DOM adapter plugin, and migrating DI from typedi to inversify across core + dom-adapters. It also adjusts model event timing (sync vs microtask) to preserve event ordering guarantees.

Changes:

  • Introduces an SDK BlockToolAdapter base class + new adapter-related events/types, and updates tool constructor typing to support adapter specialization.
  • Adds DOMAdapters as a core plugin that owns DOM adapter wiring via an Inversify container, including a shared InputsRegistry.
  • Migrates core and dom-adapters DI from typedi to inversify, and updates event dispatch ordering in the model (BlockAdded sync; BlockRemoved/data-node events microtasked).

Reviewed changes

Copilot reviewed 43 out of 45 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
yarn.lock Adds Inversify dependency graph; removes typedi.
packages/sdk/src/tools/facades/InlineToolFacade.ts Caches inline tool instance to enforce singleton behavior.
packages/sdk/src/tools/facades/BaseToolFacade.ts Broadens BlockToolConstructor typing to allow generic adapters.
packages/sdk/src/entities/index.ts Exports BlockToolAdapter as a runtime value; adds adapter plugin type export.
packages/sdk/src/entities/EventBus/events/index.ts Re-exports adapter events barrel.
packages/sdk/src/entities/EventBus/events/adapter/index.ts Adds adapter event barrel exports.
packages/sdk/src/entities/EventBus/events/adapter/ValueNodeChanged.ts Adds adapter event for value updates.
packages/sdk/src/entities/EventBus/events/adapter/KeyRemoved.ts Adds adapter event for key removal.
packages/sdk/src/entities/EventBus/events/adapter/KeyAdded.ts Adds adapter event for key addition.
packages/sdk/src/entities/EventBus/events/adapter/AdapterEventType.ts Defines adapter event type namespace.
packages/sdk/src/entities/EntityType.ts Adds PluginType.Adapter for adapter plugins.
packages/sdk/src/entities/EditorjsPlugin.ts Makes plugin constructor generic over params/instance.
packages/sdk/src/entities/EditorjsAdapterPlugin.ts Introduces adapter plugin interfaces + constructor typing.
packages/sdk/src/entities/BlockToolAdapter.ts Replaces interface with abstract base class that listens to model events and emits adapter events.
packages/sdk/src/entities/BlockTool.ts Adds adapter generic to block tool constructor/options typing.
packages/playground/src/components/index.ts Removes Input component export.
packages/playground/src/components/Input.vue Removes old adapter-attached input component.
packages/model/src/entities/EditorDocument/index.ts Adjusts block event timing; adds getDataNode.
packages/model/src/entities/BlockNode/index.ts Adds getDataNode; microtasks data-node add/remove events; changes initialization path.
packages/model/src/EditorJSModel.ts Adds getCaret and getDataNode passthroughs.
packages/dom-adapters/tsconfig.json Enables emitDecoratorMetadata for DI.
packages/dom-adapters/src/tokens.ts Adds Inversify token(s) for dom-adapters container.
packages/dom-adapters/src/index.ts Adds DOMAdapters adapter plugin with Inversify container + adapter factory.
packages/dom-adapters/src/InputsRegistry/index.ts Adds shared blockIndex/dataKey→HTMLElement registry.
packages/dom-adapters/src/FormattingAdapter/index.ts Switches to Inversify DI and tool-loaded wiring via core EventBus.
packages/dom-adapters/src/CaretAdapter/index.ts Switches to shared InputsRegistry and Inversify; removes block attachment tracking.
packages/dom-adapters/src/BlockToolAdapter/index.ts Replaces prior adapter impl with DOMBlockToolAdapter extending SDK base adapter and using registry.
packages/dom-adapters/package.json Adds inversify + reflect-metadata.
packages/core/src/tools/internal/block-tools/paragraph/index.ts Updates Paragraph tool to use DOM adapter methods and adapter key events.
packages/core/src/tools/ToolsManager.ts Migrates DI from typedi to Inversify token injection.
packages/core/src/tokens.ts Adds core Inversify tokens (EditorConfig, Adapter).
packages/core/src/index.ts Replaces typedi container with Inversify; introduces plugin container and adapter initialization.
packages/core/src/components/SelectionManager.ts Removes reliance on FormattingAdapter for apply; formats directly via model using caret segments.
packages/core/src/components/SelectionManager.spec.ts Updates mocks/tests to match new SelectionManager behavior.
packages/core/src/components/BlockRenderer.ts New component: renders blocks from model block events using adapter plugin.
packages/core/src/components/BlockRenderer.spec.ts Adds unit tests for BlockRenderer.
packages/core/src/components/BlockManager.ts Removes model event/render responsibilities (delegated to BlockRenderer); updates caret lookup.
packages/core/src/components/BlockManager.spec.ts Updates tests for BlocksManager refactor and caret lookup changes.
packages/core/src/api/index.ts Migrates API DI to Inversify property injection.
packages/core/src/api/SelectionAPI.ts Migrates SelectionAPI DI to Inversify.
packages/core/src/api/DocumentAPI/DocumentAPI.ts Migrates DocumentAPI DI to Inversify.
packages/core/src/api/BlocksAPI.ts Migrates BlocksAPI DI to Inversify token injection.
packages/core/src/api/BlocksAPI.integration.spec.ts Updates for microtasked BlockRemovedEvent.
packages/core/package.json Replaces typedi with inversify; keeps reflect-metadata.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/dom-adapters/src/BlockToolAdapter/index.ts Outdated
Comment thread packages/model/src/entities/BlockNode/index.ts Outdated
Comment thread packages/dom-adapters/src/InputsRegistry/index.ts
Comment thread packages/core/src/index.ts
Comment thread packages/sdk/src/entities/EventBus/events/adapter/index.ts Outdated
Comment thread packages/sdk/src/entities/EventBus/events/adapter/KeyRemoved.ts
Comment thread packages/dom-adapters/src/BlockToolAdapter/index.ts Outdated
Comment thread packages/model/src/entities/EditorDocument/index.ts Outdated
Comment thread packages/sdk/src/entities/EventBus/events/adapter/ValueNodeChanged.ts Outdated
Comment thread packages/dom-adapters/src/BlockToolAdapter/index.ts Outdated
@github-actions
Copy link
Copy Markdown

Coverage report for ./packages/core

Caution

Multiple errors occurred

 1 | Error: The process '/usr/local/bin/yarn' failed with exit code 1
 2 | Getting code coverage data failed.
St.
Category Percentage Covered / Total
🟢 Statements 100% 129/129
🟢 Branches 100% 46/46
🟢 Functions 100% 27/27
🟢 Lines 100% 120/120

Test suite run success

73 tests passing in 7 suites.

Report generated by 🧪jest coverage report action from ba5077d

@editor-js editor-js deleted a comment from github-actions Bot Apr 28, 2026
* - dispatches BlockAddedCoreEvent with the rendered UI element
* @param event - BlockAddedEvent
*/
async #handleBlockAddedEvent(event: BlockAddedEvent): Promise<void> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

async for no reason

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Line 124

Comment on lines +85 to +89
case event instanceof BlockAddedEvent:
return this.#handleBlockAddedEvent(event);
case event instanceof BlockRemovedEvent:
this.#handleBlockRemovedEvent(event);
break;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

make it consistent please

#config: CoreConfig;
#config: CoreConfigValidated;

#toolsManager: ToolsManager;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

no jsdoc

Comment on lines +55 to +57
#wrapper: HTMLDivElement | undefined;

#paragraph: HTMLDivElement | undefined;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

jsdoc missing

*
*/
#data: ParagraphData;
private get wrapper(): HTMLDivElement {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same

Comment thread packages/core/report.json Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

was this meant to be ignored?

Comment on lines +38 to +42
if (dataKey === undefined) {
this.#inputs.splice(blockIndex, 1);

return;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

seems like we should throw in this case, since we expose insertBlock method for registration of the block — insert of the input to the nonexistent block should be forbidden

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not sure what's this comment about. This is unregister function

Comment thread packages/model/.eslintrc.yml Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

remove this file

Comment on lines +40 to +41
Params extends EditorjsPluginParams = EditorjsPluginParams,
Instance extends EditorjsPlugin = EditorjsPlugin
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

jsdoc

* Cached instance of the inline tool
* Inline tools are singletons — the same instance is reused across all calls to create()
*/
#instance: IInlineTool | undefined;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can't see where actual InlineTool is used, maybe importing InlineTool as IInlineTool, is redundant?

this.#toolsManager
.inlineTools
.entries()
.map(([name, facade]) => [createInlineToolName(name), facade.create()])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is it ok to create a new tool instance for every caret/selection change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's not created every time. InlineToolFacade returns the same instance for each create call

#paragraph: HTMLDivElement | undefined;

/**
* Returns tool's wrapper, creates one if it doesn't exist yet
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Write here why editor needs a wrapper for each tool

}

wrapper.classList.add('editorjs-paragraph');
#onUpdate = (event: KeyAddedEvent | KeyRemovedEvent): void => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

docs missed

wrapper.contentEditable = 'true';
wrapper.style.outline = 'none';
wrapper.style.whiteSpace = 'pre-wrap';
if (event instanceof KeyRemovedEvent) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add doc here please

return;
}

const paragraph = document.createElement('div');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it will recreate a div for each model update? and caret will be dropped and set again?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No, only when a new key is being registered. For paragraph there's only one key — text


/**
* Attaches or re-attaches input to the model using key
* It handles beforeinput events and updates model data
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It handles beforeinput events and updates model data

where?

}

/**
* Attaches or re-attaches input to the model using key
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

to the model using key

Explain what is Input Registry

* @param keyRaw - key of the input to remove
* Returns the (dataKey → element) map for this block from the shared registry.
*/
public detachInput(keyRaw: string): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why detach input/value has been removed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Inputs get get removed if setInput called with input as undefined. Paragraph calls it when KeyRemovedEvent is dispatched on the adapter

Values now are not being 'attached'. Tool would just subscribe to adapter ValueChangedEvent to handle the updates. However, I'm going to return an update function from registerValueKey method so that tool can update value if needed

const registry = this.#iocContainer.get(InputsRegistry);

model.addEventListener(EventType.Changed, (event) => {
if (event instanceof BlockRemovedEvent) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why BlockRemovedEvent event is handled not in the BlockRenderer?

If you remove a Block form Registry, so it is expected to see adding a Block to Registry by BlockAddedEvent as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It is handled here to remove adapter and the block from the registry. Additions are handled in createBlockToolAdapter method.

I'm going to implement block ids later which should remove the need to update the indexes

.build();

this.dispatchEvent(new DataNodeAddedEvent(index, data, getContext<string | number>()!));
queueMicrotask(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

jsdoc explaining the reason of postponing should be added here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants