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
9 changes: 7 additions & 2 deletions playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -98,6 +100,9 @@
class: List,
inlineToolbar: true,
shortcut: 'CMD+SHIFT+L',
sanitizer: {
br: false,
},
config: {
defaultStyle: 'checklist',
maxLevel: 4,
Expand All @@ -107,7 +112,7 @@
/**
* Example of the lacalisation dictionary
*/
i18n: {
i18n: {
messages: {
"toolNames": {
"Ordered List": "Nummerierte Liste",
Expand Down
316 changes: 313 additions & 3 deletions src/ListTabulator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -167,11 +167,18 @@ export default class ListTabulator<Renderer extends ListRenderer> {
(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);
break;
case 'Delete':
this.delete(event);
break;
case 'Tab':
if (event.shiftKey) {
this.shiftTab(event);
Expand Down Expand Up @@ -401,8 +408,18 @@ export default class ListTabulator<Renderer extends ListRenderer> {
const subItemsWrapper = child.querySelector(`:scope > ${tagToSearch}`);
// get subitems.
const subItems = subItemsWrapper ? getPastedItems(subItemsWrapper) : [];

// get text content of the li element.
const 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
const childCopy = child.cloneNode(true) as HTMLElement;

childCopy.querySelector(`:scope > ${tagToSearch}`)?.remove();

content = childCopy.innerHTML;
}

return {
content,
Expand Down Expand Up @@ -514,6 +531,50 @@ export default class ListTabulator<Renderer extends ListRenderer> {
}
}


/**
* 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
Expand Down Expand Up @@ -565,6 +626,45 @@ export default class ListTabulator<Renderer extends ListRenderer> {
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
Expand Down Expand Up @@ -942,6 +1042,216 @@ export default class ListTabulator<Renderer extends ListRenderer> {
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<ItemElement>(`.${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();
}

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 <br>
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
Expand Down
Loading