Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
48 changes: 41 additions & 7 deletions src/components/modules/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,30 +418,31 @@ export default class UI extends Module<UINodes> {
/**
* Used to not emit the same block multiple times to the 'block-hovered' event on every mousemove
*/
let blockHoveredEmitted;
let blockHoveredEmitted: Element;

this.readOnlyMutableListeners.on(this.nodes.redactor, 'mousemove', _.throttle((event: MouseEvent | TouchEvent) => {
const hoveredBlock = (event.target as Element).closest('.ce-block');

/**
* Do not trigger 'block-hovered' for cross-block selection
*/
if (this.Editor.BlockSelection.anyBlockSelected) {
return;
}

const hoveredElement = event.target as Element;
const hoveredBlock = this.findByNodeBlockBelongsToCurrentInstance(hoveredElement);

if (!hoveredBlock) {
return;
}

if (blockHoveredEmitted === hoveredBlock) {
if (blockHoveredEmitted === hoveredBlock.holder) {
return;
}

blockHoveredEmitted = hoveredBlock;
blockHoveredEmitted = hoveredBlock.holder;

this.eventsDispatcher.emit(BlockHovered, {
block: this.Editor.BlockManager.getBlockByChildNode(hoveredBlock),
block: hoveredBlock,
});
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20), {
Expand Down Expand Up @@ -734,7 +735,12 @@ export default class UI extends Module<UINodes> {
* Select clicked Block as Current
*/
try {
this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);
const blockNode = this.findByNodeBlockBelongsToCurrentInstance(clickedNode);
if (!blockNode) {
return;
}

this.Editor.BlockManager.setCurrentBlockByChildNode(blockNode.holder);
} catch (e) {
/**
* If clicked outside first-level Blocks and it is not RectSelection, set Caret to the last empty Block
Expand Down Expand Up @@ -930,4 +936,32 @@ export default class UI extends Module<UINodes> {
this.readOnlyMutableListeners.on(this.nodes.wrapper, 'focusin', handleInputOrFocusChange);
this.readOnlyMutableListeners.on(this.nodes.wrapper, 'focusout', handleInputOrFocusChange);
}

/**
* Find a Block belonging to the current editor instance by element
*
* It is necessary for the case of nested editors.
* This allows each editor to send and process events only for blocks belong the current editor.
* And also allows for top level editors to identify in which block
* of their instance the event occurred in a nested editor block.
*/
private findByNodeBlockBelongsToCurrentInstance(node: Element): Block|null {
Copy link
Member

Choose a reason for hiding this comment

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

we have the getBlock() method of BlockManager which seems to be used for the case "find block by element".

Maybe we can:

  1. Improve that method to handle case with nested editor instance
  2. Reuse it here
  3. (I'm not sure) Move this method to /utils/blocks

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is not the right solution to use this method. Because for now getBlock() method is used to get a block only by its holder element, without searching by children elements. So, searching for the parent editor block by the nested editor element will greatly change the logic of this method. And as I can tell from a quick look at the code, it will break some modules.

A better solution may be to combine this method with methods getBlockByChildNode() and/or setCurrentBlockByChildNode(). However, this also cannot be done clearly, because it is also will break some modules. For example, as far as I understand, in this case the paste module will start pasting data into all editors in the chain, rather than a instance in focus: paste.ts. Also, selection processing methods wait to receive blocks of the editor in which the selection occurs, and not of a nested one.

So, to combine this method with others in the current implementation, it will be necessary to add a flag option that would prohibit searching in nested editors, because many methods don't need it. Therefore, in my commit I did not combine them for now, and made a separate method that is used only in places where it is necessary.

let blockNode;

while (blockNode = node.closest(`.${Block.CSS.wrapper}`)) {
const blockInstance = this.Editor.BlockManager.getBlockByChildNode(blockNode);
if (blockInstance) {
return blockInstance;
}

const editorWrapper = blockNode.closest(`.${this.CSS.editorWrapper}`);
if (!editorWrapper) {
return null;
}

node = editorWrapper;
}

return null;
}
}
44 changes: 44 additions & 0 deletions test/cypress/tests/modules/Ui.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createEditorWithTextBlocks } from '../../support/utils/createEditorWithTextBlocks';
import type EditorJS from '../../../../types/index';
import NestedEditor, { NESTED_EDITOR_ID } from '../../support/utils/nestedEditorInstance';

describe('Ui module', function () {
describe('documentKeydown', function () {
Expand Down Expand Up @@ -139,5 +140,48 @@ describe('Ui module', function () {
expect(currentBlockIndex).to.eq(1);
});
});

it('click on block of nested editor should also set the block of parent editor as current', function () {
cy.createEditor({
tools: {
nestedEditor: {
class: NestedEditor,
},
},
data: {
blocks: [
{
type: 'paragraph',
data: {
text: 'First block of parent editor',
},
},
{
type: 'paragraph',
data: {
text: 'Second block of parent editor',
},
},
{
type: 'nestedEditor',
data: {
text: 'First block of nested editor',
},
},
],
},
}).as('editorInstance');

cy.get(`[data-cy=${NESTED_EDITOR_ID}]`)
.find('.ce-paragraph')
.click();

cy.get<EditorJS>('@editorInstance')
.then(async (editor) => {
const currentBlockIndex = editor.blocks.getCurrentBlockIndex();

expect(currentBlockIndex).to.eq(2); // 3 block in numbering from 0
});
});
});
});
Loading