Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(playground): playground uses editor, input rendering #90

Merged
merged 7 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EditorJSModel } from '@editorjs/model';
import { EditorJSModel, EventType } from '@editorjs/model';
import type { ContainerInstance } from 'typedi';
import { Container } from 'typedi';
import { composeDataFromVersion2 } from './utils/composeDataFromVersion2.js';
Expand Down Expand Up @@ -62,8 +62,10 @@ export default class Core {
* Inline toolbar is responsible for handling selection changes
* When model selection changes, it determines, whenever to show toolbar element,
* Which calls apply format method of the adapter
*
* null when inline toolbar is not initialized
*/
#inlineToolbar: InlineToolbar;
#inlineToolbar: InlineToolbar | null = null;

/**
* @param config - Editor configuration
Expand Down Expand Up @@ -92,19 +94,26 @@ export default class Core {
this.#formattingAdapter = new FormattingAdapter(this.#model, this.#caretAdapter);
this.#iocContainer.set(FormattingAdapter, this.#formattingAdapter);

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

this.#prepareUI();
if (config.onModelUpdate !== undefined) {
this.#model.addEventListener(EventType.Changed, () => {
config.onModelUpdate?.(this.#model);
});
}

this.#iocContainer.get(BlocksManager);
this.#prepareUI();

/**
* @todo avait when isReady API is implemented
*/
void this.#toolsManager.prepareTools();
this.#toolsManager.prepareTools()
.then(() => {
this.#inlineToolbar = new InlineToolbar(this.#model, this.#formattingAdapter, this.#toolsManager.inlineTools, this.#config.holder);
this.#iocContainer.set(InlineToolbar, this.#inlineToolbar);

this.#model.initializeDocument({ blocks });
this.#model.initializeDocument({ blocks });
})
.catch((error) => {
console.error('Editor.js initialization failed', error);
});
}

/**
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/utils/composeDataFromVersion2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import type { OutputData } from '@editorjs/editorjs';
import type { InlineFragment } from '@editorjs/model';
import { createInlineToolData, createInlineToolName, TextNode, ValueNode, type BlockNodeSerialized } from '@editorjs/model';

/**
* Removes HTML tags from the input string
* @param input - any string with HTML tags like '<b>bold</b> <a href="https://editorjs.io">link</a>'
*/
function stripTags(input: string): string {
return input.replace(/<\/?[^>]+(>|$)/g, '');
}

/**
* Extracts inline fragments from the HTML string
* @param html - any html string like '<b>bold</b> <a href="https://editorjs.io">link</a>'
Expand Down Expand Up @@ -97,7 +105,7 @@ export function composeDataFromVersion2(data: OutputData): {
if (typeof value === 'string') {
const fragments = extractFragments(value);
const textNode = new TextNode({
value,
value: stripTags(value),
fragments,
});

Expand Down
6 changes: 4 additions & 2 deletions packages/dom-adapters/src/BlockToolAdapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,13 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
this.#caretAdapter.attachInput(input, builder.build());

try {
const value = this.#model.getText(this.#blockIndex, key);
const fragments = this.#model.getFragments(this.#blockIndex, key);

input.textContent = value;

fragments.forEach(fragment => {
console.log('fragment', fragment);
// this.#formattingAdapter.formatElementContent(input, fragment);
this.#formattingAdapter.formatElementContent(input, fragment);
});
} catch (_) {
// do nothing — TextNode is not created yet as there is no initial data in the model
Expand Down
26 changes: 18 additions & 8 deletions packages/dom-adapters/src/FormattingAdapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,27 @@ export class FormattingAdapter {

const [start, end] = index;

/**
* Create range with positions specified in index
*/
const range = document.createRange();
try {
/**
* Create range with positions specified in index
*/
const range = document.createRange();

range.setStart(input, start);
range.setEnd(input, end);
const inputTextNode = input.firstChild;

const inlineElement = tool.createWrapper(toolData);
if (inputTextNode === null) {
throw new Error('FormattingAdapter: input element should contain text node');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

pass checked text node to avoid this error catching


range.setStart(inputTextNode, start);
range.setEnd(inputTextNode, end);

surround(range, inlineElement);
const inlineElement = tool.createWrapper(toolData);

surround(range, inlineElement);
} catch (e) {
console.error('Error while formatting element content', e);
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/model/src/EditorJSModel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('EditorJSModel', () => {
'updateValue',
'removeBlock',
'moveBlock',
'getText',
'insertText',
'removeText',
'format',
Expand All @@ -25,6 +26,7 @@ describe('EditorJSModel', () => {
'createCaret',
'updateCaret',
'removeCaret',
'devModeGetDocument',
];
const ownProperties = Object.getOwnPropertyNames(EditorJSModel.prototype);

Expand Down
20 changes: 20 additions & 0 deletions packages/model/src/EditorJSModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,17 @@
return this.#document.updateTuneData(...parameters);
}

/**
* Returns a text from the specified block and data key
*
* @param parameters - getText method parameters
* @param parameters.blockIndex - index of the block
* @param parameters.dataKey - key of the data
*/
public getText(...parameters: Parameters<EditorDocument['getText']>): ReturnType<EditorDocument['getText']> {

Check warning on line 244 in packages/model/src/EditorJSModel.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
return this.#document.getText(...parameters);

Check warning on line 245 in packages/model/src/EditorJSModel.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

/**
* Inserts text to the specified block
*
Expand Down Expand Up @@ -303,6 +314,15 @@
return this.#document.getFragments(...parameters);
}

/**
* Exposing document for dev-tools
*
* USE ONLY FOR DEV PURPOSES
*/
public devModeGetDocument(): EditorDocument {
return this.#document;
}

/**
* Listens to BlockNode events and bubbles re-emits them from the EditorJSModel instance
*
Expand Down
27 changes: 26 additions & 1 deletion packages/model/src/entities/BlockNode/BlockNode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { EditorDocument } from '../EditorDocument';
import type { ValueNodeConstructorParameters } from '../ValueNode';
import type { InlineFragment, InlineToolData, InlineToolName } from '../inline-fragments';
import { TextNode } from '../inline-fragments/index.js';
import type { BlockNodeData, BlockNodeDataSerialized } from './types';
import type { BlockNodeData, BlockNodeDataSerialized, DataKey } from './types';
import { BlockChildType } from './types/index.js';
import { NODE_TYPE_HIDDEN_PROP } from './consts.js';
import { TextAddedEvent, TuneModifiedEvent, ValueModifiedEvent } from '../../EventBus/events/index.js';
Expand Down Expand Up @@ -574,6 +574,31 @@ describe('BlockNode', () => {
});
});

describe('.getText()', () => {
it('should call .serialized getter of the TextNode', () => {
const spy = jest.spyOn(TextNode.prototype, 'serialized', 'get');
const node = createBlockNodeWithData({
text: {
[NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text,
value: '',
fragments: [],
},
});

node.getText(createDataKey('text'));

expect(spy)
.toHaveBeenCalled();
});

it('should throw an error if data key is invalid', () => {
const node = createBlockNodeWithData({});

expect(() => node.getText('invalid-key' as DataKey))
.toThrow();
});
});

describe('.insertText()', () => {
const dataKey = createDataKey('text');
const text = 'Some text';
Expand Down
7 changes: 7 additions & 0 deletions packages/model/src/entities/BlockNode/__mocks__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export class BlockNode extends EventBus {
return;
}

/**
* Mock method
*/
public getText(): string {
return 'mocked text';
}

/**
* Mock method
*/
Expand Down
13 changes: 13 additions & 0 deletions packages/model/src/entities/BlockNode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,19 @@ export class BlockNode extends EventBus {
node.update(value);
}

/**
* Returns a text value of the specified text node
Copy link
Contributor

Choose a reason for hiding this comment

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

Node is not specified for this function

*
* @param dataKey - key of the data
*/
public getText(dataKey: DataKey): string {
this.#validateKey(dataKey, TextNode);

const node = get(this.#data, dataKey as string) as TextNode;

return node.serialized.value;
}

/**
* Inserts text to the specified text node by index, by default appends text to the end of the current value
*
Expand Down
36 changes: 36 additions & 0 deletions packages/model/src/entities/EditorDocument/EditorDocument.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,42 @@ describe('EditorDocument', () => {
});
});

describe('.getText()', () => {
let document: EditorDocument;
const dataKey = 'text' as DataKey;
const text = 'Some text';
let block: BlockNode;

beforeEach(() => {
const blockData = {
name: 'text' as BlockToolName,
data: {
[dataKey]: text,
},
};

document = new EditorDocument();

document.initialize([ blockData ]);

block = document.getBlock(0);
});

it('should call .getText() method of the BlockNode if index and data key are correct', () => {
const spy = jest.spyOn(block, 'getText');

document.getText(0, dataKey);

expect(spy)
.toHaveBeenCalledWith(dataKey);
});

it('should throw an error if index is out of bounds', () => {
expect(() => document.getText(document.length + 1, dataKey))
.toThrow('Index out of bounds');
});
});

describe('.insertText()', () => {
let document: EditorDocument;
const dataKey = 'text' as DataKey;
Expand Down
12 changes: 12 additions & 0 deletions packages/model/src/entities/EditorDocument/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,18 @@ export class EditorDocument extends EventBus {
this.#children[blockIndex].updateTuneData(tuneName, data);
}

/**
* Returns text of the specified block and data key
*
* @param blockIndex - index of the block
* @param dataKey - key of the data containing the text
*/
public getText(blockIndex: number, dataKey: DataKey): string {
this.#checkIndexOutOfBounds(blockIndex, this.length - 1);

return this.#children[blockIndex].getText(dataKey);
}

/**
* Inserts text to the specified block
*
Expand Down
Loading
Loading