From d17cd27809e1385c038605d3ae526073bf6a4686 Mon Sep 17 00:00:00 2001 From: igorpojzl <59439434+igorpojzl@users.noreply.github.com> Date: Mon, 17 Mar 2025 11:59:12 +0100 Subject: [PATCH 1/5] fix(renderFromHTML): Fix Double Render from Paste #121 --- src/ListTabulator/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ListTabulator/index.ts b/src/ListTabulator/index.ts index acb7ccea..0b215001 100644 --- a/src/ListTabulator/index.ts +++ b/src/ListTabulator/index.ts @@ -402,7 +402,7 @@ export default class ListTabulator { // get subitems. const subItems = subItemsWrapper ? getPastedItems(subItemsWrapper) : []; // get text content of the li element. - const content = child.innerHTML ?? ''; + const content = child?.firstChild?.textContent ?? ''; return { content, From dd5d41fe299fb2a84a1f5b072123d8133c4aae31 Mon Sep 17 00:00:00 2001 From: igorpojzl <59439434+igorpojzl@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:32:55 +0100 Subject: [PATCH 2/5] fix(renderFromHTML): Use Inner Html #121 Use Inner Html and determine by SubItems which innerHtml to use --- src/ListTabulator/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ListTabulator/index.ts b/src/ListTabulator/index.ts index 0b215001..21163948 100644 --- a/src/ListTabulator/index.ts +++ b/src/ListTabulator/index.ts @@ -402,7 +402,8 @@ export default class ListTabulator { // get subitems. const subItems = subItemsWrapper ? getPastedItems(subItemsWrapper) : []; // get text content of the li element. - const content = child?.firstChild?.textContent ?? ''; + const childElement = subItems.length > 0 ? child?.firstElementChild : child; + const content = childElement?.innerHTML ?? ''; return { content, From d614b18744f3649ca8a3aae030cc93e4a55b3afc Mon Sep 17 00:00:00 2001 From: igorpojzl <59439434+igorpojzl@users.noreply.github.com> Date: Mon, 17 Mar 2025 13:44:15 +0100 Subject: [PATCH 3/5] fix(renderFromHTML): Remove inner Node #121 --- src/ListTabulator/index.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ListTabulator/index.ts b/src/ListTabulator/index.ts index 21163948..0d57bc9c 100644 --- a/src/ListTabulator/index.ts +++ b/src/ListTabulator/index.ts @@ -401,9 +401,18 @@ export default class ListTabulator { const subItemsWrapper = child.querySelector(`:scope > ${tagToSearch}`); // get subitems. const subItems = subItemsWrapper ? getPastedItems(subItemsWrapper) : []; + // get text content of the li element. - const childElement = subItems.length > 0 ? child?.firstElementChild : child; - const content = childElement?.innerHTML ?? ''; + let content = child.innerHTML; + + if (subItemsWrapper) { + // Get Copy of Child and remove any nested OL or UL tags from the content + const childCopy = child.cloneNode(true) as HTMLElement; + + childCopy.querySelector(`:scope > ${tagToSearch}`)?.remove(); + + content = childCopy.innerHTML; + } return { content, From 7ff369e23513e920607611a783ae4bd751f5ad96 Mon Sep 17 00:00:00 2001 From: pjurco Date: Thu, 27 Mar 2025 16:22:06 +0100 Subject: [PATCH 4/5] feat(keydown) added delete listener --- playground/index.html | 9 +- src/ListTabulator/index.ts | 219 ++++++++++++++++++++++++++++++++++++- src/index.ts | 1 - 3 files changed, 225 insertions(+), 4 deletions(-) diff --git a/playground/index.html b/playground/index.html index 799a9f09..d2474ae4 100644 --- a/playground/index.html +++ b/playground/index.html @@ -78,7 +78,9 @@ * Wrapper of Editor */ holder: 'editorjs', - + sanitizer: { + br: false, + }, /** * Common Inline Toolbar settings * - if true (or not specified), the order from 'tool' property will be used @@ -98,6 +100,9 @@ class: List, inlineToolbar: true, shortcut: 'CMD+SHIFT+L', + sanitizer: { + br: false, + }, config: { defaultStyle: 'checklist', maxLevel: 4, @@ -107,7 +112,7 @@ /** * Example of the lacalisation dictionary */ - i18n: { + i18n: { messages: { "toolNames": { "Ordered List": "Nummerierte Liste", diff --git a/src/ListTabulator/index.ts b/src/ListTabulator/index.ts index 0d57bc9c..0c2e2b58 100644 --- a/src/ListTabulator/index.ts +++ b/src/ListTabulator/index.ts @@ -4,7 +4,7 @@ import type { ListConfig, ListData, ListDataStyle } from '../types/ListParams'; import type { ListItem } from '../types/ListParams'; import type { ItemElement, ItemChildWrapperElement } from '../types/Elements'; import { isHtmlElement } from '../utils/type-guards'; -import { getContenteditableSlice, getCaretNodeAndOffset, isCaretAtStartOfInput } from '@editorjs/caret'; +import { getContenteditableSlice, getCaretNodeAndOffset, isCaretAtStartOfInput, isCaretAtEndOfInput } from '@editorjs/caret'; import { DefaultListCssClasses } from '../ListRenderer'; import type { PasteEvent } from '../types'; import type { API, BlockAPI, PasteConfig } from '@editorjs/editorjs'; @@ -172,6 +172,9 @@ export default class ListTabulator { case 'Backspace': this.backspace(event); break; + case 'Delete': + this.delete(event); + break; case 'Tab': if (event.shiftKey) { this.shiftTab(event); @@ -575,6 +578,45 @@ export default class ListTabulator { this.mergeItemWithPrevious(currentItem); } + /** + * Handle delete + * @param event - keydown + */ + private delete(event: KeyboardEvent): void { + const currentItem = this.currentItem; + + if (currentItem === null) { + return; + } + + /** + * Caret is not at end of the item + * Then delete button should remove letter as usual + */ + if (!isCaretAtEndOfInput(currentItem)) { + return; + } + + /** + * If backspace is pressed with selection, it should be handled as usual + */ + if (window.getSelection()?.isCollapsed === false) { + return; + } + + /** + * Prevent Editor.js backspace handling + */ + event.stopPropagation(); + + /** + * Prevent default backspace behaviour + */ + event.preventDefault(); + + this.mergeItemWithCurrent(currentItem); + } + /** * Reduce indentation for current item * @param event - keydown @@ -952,6 +994,181 @@ export default class ListTabulator { item.remove(); } + /** + * Method that is used for merging current item with current one + * Content of the current item would be appended to the current item + * Current item children would not change nesting level + * @param item - current item html element + */ + private mergeItemWithCurrent(item: ItemElement): void { + console.log(item) + const nextItem = item.nextElementSibling; + const currentItemParentNode = item.parentNode; + + /** + * Check that parent node of the current element exists + */ + if (currentItemParentNode === null) { + return; + } + if (!isHtmlElement(currentItemParentNode)) { + return; + } + + let parentItem = currentItemParentNode.closest(`.${DefaultListCssClasses.item}`); + + if (parentItem === null) { + parentItem = item; + } + + const nextParentItem = parentItem?.nextElementSibling; + + if (nextParentItem === undefined) { + return; + } + + /** + * Check that current item has any next siblings to be merged with + */ + if (!nextItem && !nextParentItem) { + return; + } + + /** + * Make sure previousItem is an HTMLElement + */ + if (nextItem && !isHtmlElement(nextItem)) { + return; + } + + /** + * Make sure previousItem is an HTMLElement + */ + if (nextParentItem && !isHtmlElement(nextParentItem)) { + return; + } + + /** + * Lets compute the item which will be merged with current item text + */ + let targetItem: ItemElement | null; + + /** + * If there is a next item then we get a deepest item in its sublists + * + * Otherwise we will use the parent item + */ + if (nextItem) { + /** + * Get list of all levels children of the next item + */ + const childrenOfNextItem = getChildItems(nextItem, false); + + /** + * Target item would be deepest child of the next item or next item itself + */ + if (childrenOfNextItem.length !== 0 && childrenOfNextItem.length !== 0) { + targetItem = childrenOfNextItem[childrenOfNextItem.length - 1]; + } else { + targetItem = nextItem; + } + } else { + targetItem = nextParentItem; + } + + + /** + * Get the target item content element + */ + if (!targetItem) { + return; + } + + /** + * Set caret to the end of the target item + */ + focusItem(item, false); + + /** + * Get next item content + */ + const nextItemContent = this.renderer.getItemContent(targetItem); + + /** + * Get target item content element + */ + const targetItemContentElement = getItemContentElement(item); + + /** + * Set a new place for caret + */ + if (targetItemContentElement === null) { + return; + } + + /** + * Update target item content by merging with current item html content + */ + targetItemContentElement.insertAdjacentHTML('beforeend', nextItemContent); + /** + * Get child list of the currentItem + */ + const nextItemChildrenList = getChildItems(targetItem); + const currentItemChildrenList = getChildItems(parentItem); + + /** + * If item has no children, just remove item + * Else children of the item should be prepended to the target item child list + */ + if (nextItemChildrenList.length === 0) { + /** + * Remove current item element + */ + targetItem.remove(); + + /** + * If target item has empty child wrapper after merge, we need to remove child wrapper + * This case could be reached if the only child item of the target was merged with target + */ + removeChildWrapperIfEmpty(targetItem); + + return; + } + + /** + * Get target for child list of the currentItem + * Note that previous item and parent item could not be null at the same time + * This case is checked before + */ + const targetForChildItems = nextItem ? nextItem : parentItem!; + + const targetChildWrapper = getItemChildWrapper(targetForChildItems) ?? this.renderer.renderWrapper(false); + + /** + * Add child current item children to the target childWrapper + */ + if (nextItem) { + nextItemChildrenList.forEach((childItem) => { + targetChildWrapper.appendChild(childItem); + }); + } else { + nextItemChildrenList.forEach((childItem) => { + targetChildWrapper.append(childItem); + }); + } + + /** + * If we created new wrapper, then append childWrapper to the target item + */ + if (getItemChildWrapper(targetForChildItems) === null) { + item.appendChild(targetChildWrapper); + } + + /** + * Remove current item element + */ + targetItem.remove(); + } /** * Add indentation to current item * @param event - keydown diff --git a/src/index.ts b/src/index.ts index 96435b35..91bf436e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -265,7 +265,6 @@ export default class EditorjsList { */ public save(): ListData { this.data = this.list!.save(); - return this.data; } From f777dc8dbdf7a8c8cebfb15baf23af63a95c61d1 Mon Sep 17 00:00:00 2001 From: pjurco Date: Thu, 27 Mar 2025 16:22:37 +0100 Subject: [PATCH 5/5] feat(keydown) added shift + enter listener - breakline --- src/ListTabulator/index.ts | 87 +++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/src/ListTabulator/index.ts b/src/ListTabulator/index.ts index 0c2e2b58..6848dd36 100644 --- a/src/ListTabulator/index.ts +++ b/src/ListTabulator/index.ts @@ -167,7 +167,11 @@ export default class ListTabulator { (event) => { switch (event.key) { case 'Enter': - this.enterPressed(event); + if (event.shiftKey) { + this.enterBreakPressed(event); + } else { + this.enterPressed(event); + } break; case 'Backspace': this.backspace(event); @@ -406,7 +410,7 @@ export default class ListTabulator { const subItems = subItemsWrapper ? getPastedItems(subItemsWrapper) : []; // get text content of the li element. - let content = child.innerHTML; + let content = child.innerHTML.trim(); if (subItemsWrapper) { // Get Copy of Child and remove any nested OL or UL tags from the content @@ -527,6 +531,50 @@ export default class ListTabulator { } } + + /** + * Handles Enter break keypress + * @param event - keydown + */ + private enterBreakPressed(event: KeyboardEvent): void { + const currentItem = this.currentItem; + /** + * Prevent editor.js behaviour + */ + event.stopPropagation(); + + /** + * Prevent browser behaviour + */ + event.preventDefault(); + + /** + * Prevent duplicated event in Chinese, Japanese and Korean languages + */ + if (event.isComposing) { + return; + } + if (currentItem === null) { + return; + } + + const isEmpty = this.renderer?.getItemContent(currentItem).trim().length === 0; + + /** + * On Enter in the last empty item, get out of list + */ + if (isEmpty) { + + return; + } else { + /** + * If current item is not empty than split current item + */ + this.insertLineBreakAtCaret(currentItem); + } + } + + /** * Handle backspace * @param event - keydown @@ -1169,6 +1217,41 @@ export default class ListTabulator { */ targetItem.remove(); } + + private insertLineBreakAtCaret(currentItem: ItemElement): void { + const [currentNode, offset] = getCaretNodeAndOffset(); + + if (currentNode === null) { + return; + } + + const currentItemContent = getItemContentElement(currentItem); + + if (currentItemContent === null) { + return; + } + + const br = document.createElement('br'); + + const range = document.createRange(); + const selection = window.getSelection(); + + range.setStart(currentNode, offset); + range.setEnd(currentNode, offset); + range.insertNode(br); + + // Posun kurzor za
+ range.setStartAfter(br); + range.setEndAfter(br); + const zwsp = document.createTextNode("\u200B"); + br.after(zwsp); + range.setStartAfter(zwsp); + range.setEndAfter(zwsp); + + selection?.removeAllRanges(); + selection?.addRange(range); + } + /** * Add indentation to current item * @param event - keydown