Compare commits

...

4 commits

Author SHA1 Message Date
KoshaevEugeny
530ec56bb8
fix(link-tool): open new window with url when formatted link clicked via ctrl key (#2996)
* fix(link-tool): open new window with url when formatted link clicked via ctrl key

* add test

* fix lint

* bump version and add changelog

* Apply suggestions from code review

Co-authored-by: Peter <specc.dev@gmail.com>
Co-authored-by: KoshaevEugeny <103786108+akulistus@users.noreply.github.com>

* Update test/cypress/tests/inline-tools/link.cy.ts

Co-authored-by: Peter <specc.dev@gmail.com>

---------

Co-authored-by: Peter <specc.dev@gmail.com>
2026-03-11 20:58:30 +03:00
KoshaevEugeny
b69aa1ed25
fix(inline-tool-link): use defaultValue to prevent selectionchange event (#2993)
* fix(inline-tool-link): use defaultValue to prevent selectionchange event

* fix(link-tool): handle formatted linked text clicks

* fix test errors

* Revert "fix test errors"

This reverts commit 582e137b77.

* Revert "fix(link-tool): handle formatted linked text clicks"

This reverts commit ae90e03c60.
2026-03-04 23:45:11 +03:00
KoshaevEugeny
a89f3d0eda
Fix link tool interaction with other tools (#2977)
* fix(selection): removeFakeBackground no longer removes text formatting

* fix(selection): fix jsdoc

* fix(linkTool): add test case to ensure text formatting preservation when applying link

* Add fix entry to changelog

* bump version
2026-02-18 21:09:54 +03:00
Peter Savchenko
cc624cad2e i18n and rtl examples updated 2026-02-18 15:32:29 +03:00
9 changed files with 170 additions and 24 deletions

View file

@ -1,5 +1,17 @@
# Changelog # Changelog
### 2.31.5
- `Fix` - Handle __Ctrl + click__ on links with inline styles applied (e.g., bold, italic)
### 2.31.4
- `Fix` - Prevent inline-toolbar re-renders when linked text is selected
### 2.31.3
- `Fix` - Prevent text formatting removal when applying link
### 2.31.2 ### 2.31.2
- `Fix` - Prevent link removal when applying bold to linked text - `Fix` - Prevent link removal when applying bold to linked text

View file

@ -50,7 +50,6 @@
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@latest"></script><!-- Image --> <script src="https://cdn.jsdelivr.net/npm/@editorjs/image@latest"></script><!-- Image -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script><!-- Delimiter --> <script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script><!-- Delimiter -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest"></script><!-- List --> <script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest"></script><!-- List -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest"></script><!-- Checklist -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script><!-- Quote --> <script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script><!-- Quote -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script><!-- Code --> <script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script><!-- Code -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script><!-- Embed --> <script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script><!-- Embed -->
@ -112,11 +111,6 @@
shortcut: 'CMD+SHIFT+L' shortcut: 'CMD+SHIFT+L'
}, },
checklist: {
class: Checklist,
inlineToolbar: true,
},
quote: { quote: {
class: Quote, class: Quote,
inlineToolbar: true, inlineToolbar: true,
@ -202,7 +196,7 @@
/** /**
* Translation of "Convert To" at the Block Tunes Popover * Translation of "Convert To" at the Block Tunes Popover
*/ */
"Convert to": "Конвертировать в" "Convert to": "Конвертировать в",
} }
}, },
@ -212,7 +206,8 @@
"toolNames": { "toolNames": {
"Text": "Параграф", "Text": "Параграф",
"Heading": "Заголовок", "Heading": "Заголовок",
"List": "Список", "Ordered List": "Нумерованный список",
"Unordered List": "Маркированный список",
"Warning": "Примечание", "Warning": "Примечание",
"Checklist": "Чеклист", "Checklist": "Чеклист",
"Quote": "Цитата", "Quote": "Цитата",
@ -270,7 +265,12 @@
"Wrong response format from the server": "Неполадки на сервере", "Wrong response format from the server": "Неполадки на сервере",
}, },
"header": { "header": {
"Header": "Заголовок", "Heading 1": "Заголовок 1",
"Heading 2": "Заголовок 2",
"Heading 3": "Заголовок 3",
"Heading 4": "Заголовок 4",
"Heading 5": "Заголовок 5",
"Heading 6": "Заголовок 6",
}, },
"paragraph": { "paragraph": {
"Enter something": "Введите текст" "Enter something": "Введите текст"
@ -278,6 +278,7 @@
"list": { "list": {
"Ordered": "Нумерованный", "Ordered": "Нумерованный",
"Unordered": "Маркированный", "Unordered": "Маркированный",
"Checklist": "Чеклист",
}, },
/** /**
* Translation of "Convert To" at the Inline Toolbar hint * Translation of "Convert To" at the Inline Toolbar hint
@ -298,7 +299,8 @@
* Also, there are few internal block tunes: "delete", "moveUp" and "moveDown" * Also, there are few internal block tunes: "delete", "moveUp" and "moveDown"
*/ */
"delete": { "delete": {
"Delete": "Удалить" "Delete": "Удалить",
"Click to delete": "Подтвердить удаление"
}, },
"moveUp": { "moveUp": {
"Move up": "Переместить вверх" "Move up": "Переместить вверх"

View file

@ -120,12 +120,6 @@
inlineToolbar: ['link'], inlineToolbar: ['link'],
}, },
list: {
class: List,
inlineToolbar: true,
shortcut: 'CMD+SHIFT+L'
},
checklist: { checklist: {
class: Checklist, class: Checklist,
inlineToolbar: true, inlineToolbar: true,

View file

@ -1,6 +1,6 @@
{ {
"name": "@editorjs/editorjs", "name": "@editorjs/editorjs",
"version": "2.31.2", "version": "2.31.5",
"description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output",
"main": "dist/editorjs.umd.js", "main": "dist/editorjs.umd.js",
"module": "dist/editorjs.mjs", "module": "dist/editorjs.mjs",

View file

@ -566,6 +566,16 @@ export default class Dom {
return element.tagName.toLowerCase() === 'a'; return element.tagName.toLowerCase() === 'a';
} }
/**
* Returns the closest ancestor anchor (A tag) of the given element (including itself)
*
* @param element - element to check
* @returns {HTMLAnchorElement | null}
*/
public static getClosestAnchor(element: Element): HTMLAnchorElement | null {
return element.closest("a");
}
/** /**
* Return element's offset related to the document * Return element's offset related to the document
* *
@ -623,6 +633,9 @@ export default class Dom {
/** /**
* If no node found or last node is empty, return null * If no node found or last node is empty, return null
* - The root node has no text nodes at all
* - The TreeWalker couldn't find any text nodes in the DOM tree
* - The root node itself is null or invalid
*/ */
if (!lastTextNode) { if (!lastTextNode) {
return { return {
@ -633,6 +646,14 @@ export default class Dom {
const textContent = lastTextNode.textContent; const textContent = lastTextNode.textContent;
/**
* - The text node exists but has no content (textContent is null)
* - The text node exists but has empty content (textContent.length === 0)
* This could be due to:
* - Empty text nodes (<span></span>)
* - Nodes with only whitespace
* - Nodes that were cleared but not removed
*/
if (textContent === null || textContent.length === 0) { if (textContent === null || textContent.length === 0) {
return { return {
node: null, node: null,

View file

@ -212,7 +212,7 @@ export default class LinkInlineTool implements InlineTool {
*/ */
const hrefAttr = anchorTag.getAttribute('href'); const hrefAttr = anchorTag.getAttribute('href');
this.nodes.input.value = hrefAttr !== 'null' ? hrefAttr : ''; this.nodes.input.defaultValue = hrefAttr !== 'null' ? hrefAttr : '';
this.selection.save(); this.selection.save();
} else { } else {

View file

@ -773,12 +773,13 @@ export default class UI extends Module<UINodes> {
*/ */
const element = event.target as Element; const element = event.target as Element;
const ctrlKey = event.metaKey || event.ctrlKey; const ctrlKey = event.metaKey || event.ctrlKey;
const anchor = $.getClosestAnchor(element);
if ($.isAnchor(element) && ctrlKey) {
if (anchor && ctrlKey) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
event.stopPropagation(); event.stopPropagation();
const href = element.getAttribute('href'); const href = anchor.getAttribute('href');
const validUrl = _.getValidUrl(href); const validUrl = _.getValidUrl(href);
_.openTab(validUrl); _.openTab(validUrl);

View file

@ -57,10 +57,9 @@ export default class SelectionUtils {
public isFakeBackgroundEnabled = false; public isFakeBackgroundEnabled = false;
/** /**
* Native Document's commands for fake background * Native Document's command for fake background
*/ */
private readonly commandBackground: string = 'backColor'; private readonly commandBackground: string = 'backColor';
private readonly commandRemoveFormat: string = 'removeFormat';
/** /**
* Editor styles * Editor styles
@ -416,9 +415,9 @@ export default class SelectionUtils {
if (!this.isFakeBackgroundEnabled) { if (!this.isFakeBackgroundEnabled) {
return; return;
} }
document.execCommand(this.commandBackground, false, 'transparent');
this.isFakeBackgroundEnabled = false; this.isFakeBackgroundEnabled = false;
document.execCommand(this.commandRemoveFormat);
} }
/** /**

View file

@ -121,4 +121,121 @@ describe('Inline Tool Link', () => {
.should('exist') .should('exist')
.should('contain', 'Text with link'); .should('contain', 'Text with link');
}); });
it('should preserve bold and italic when applying link', () => {
cy.createEditor({
data: {
blocks: [
{
type: 'paragraph',
data: {
text: 'Bold and italic text',
},
},
],
},
});
cy.get('[data-cy=editorjs]')
.find('.ce-paragraph')
.selectText('Bold and italic text');
cy.get('[data-cy=editorjs]')
.find('[data-item-name=bold]')
.click();
cy.get('[data-cy=editorjs]')
.find('div.ce-block')
.find('b')
.should('exist')
.should('contain', 'Bold and italic text');
cy.get('[data-cy=editorjs]')
.find('div.ce-block')
.find('b')
.selectText('Bold and italic text');
cy.get('[data-cy=editorjs]')
.find('[data-item-name=italic]')
.click();
cy.get('[data-cy=editorjs]')
.find('div.ce-block')
.find('b')
.should('exist')
.find('i')
.should('exist')
.should('contain', 'Bold and italic text');
cy.get('[data-cy=editorjs]')
.find('div.ce-block')
.find('b')
.find('i')
.selectText('Bold and italic text');
cy.get('[data-cy=editorjs]')
.find('[data-item-name=link]')
.click();
cy.get('[data-cy=editorjs]')
.find('.ce-inline-tool-input')
.type('https://editorjs.io')
.type('{enter}');
cy.get('[data-cy=editorjs]')
.find('div.ce-block')
.find('b')
.should('exist')
.find('i')
.should('exist')
.find('a')
.should('have.attr', 'href', 'https://editorjs.io')
.should('contain', 'Bold and italic text');
});
it('should open a link if it is wrapped in another formatting', () => {
cy.createEditor({
data: {
blocks: [
{
type: 'paragraph',
data: {
text: 'Link text',
},
},
],
},
});
cy.get('[data-cy=editorjs]')
.find('.ce-paragraph')
.selectText('Link text');
cy.get('[data-cy=editorjs]')
.find('[data-item-name=link]')
.click();
cy.get('[data-cy=editorjs]')
.find('.ce-inline-tool-input')
.type('https://test.io/')
.type('{enter}');
cy.get('[data-cy=editorjs]')
.find('div.ce-block')
.find('a')
.selectText('Link text');
cy.get('[data-cy=editorjs]')
.find('[data-item-name=italic]')
.click();
cy.window().then((win) => {
cy.stub(win, 'open').as('windowOpen');
});
cy.contains('[data-cy=editorjs] div.ce-block i', 'Link text')
.click({ ctrlKey: true });
cy.get('@windowOpen').should('be.calledWith', 'https://test.io/');
});
}); });