jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
338 lines (264 loc) • 7.74 kB
text/typescript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2020 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import * as consts from '../../core/constants';
import { Dom } from '../../core/dom';
import { $$, scrollIntoView } from '../../core/helpers';
import { HTMLTagNames, IJodit, Nullable } from '../../types';
import { Plugin } from '../../core/plugin';
import { INVISIBLE_SPACE } from '../../core/constants';
/**
* Insert default paragraph
*
* @param editor
* @param [fake]
* @param [wrapperTag]
* @param [style]
* @return {HTMLElement}
*/
export const insertParagraph = (
editor: IJodit,
fake: Nullable<Text>,
wrapperTag: HTMLTagNames,
style?: CSSStyleDeclaration
): HTMLElement => {
const p = editor.createInside.element(wrapperTag),
helper_node = editor.createInside.element('br');
p.appendChild(helper_node);
if (style && style.cssText) {
p.setAttribute('style', style.cssText);
}
editor.s.insertNode(p, false, false);
editor.s.setCursorBefore(helper_node);
const range = editor.s.createRange();
range.setStartBefore(wrapperTag.toLowerCase() !== 'br' ? helper_node : p);
range.collapse(true);
editor.s.selectRange(range);
Dom.safeRemove(fake);
scrollIntoView(p, editor.editor, editor.ed);
editor.events?.fire('synchro'); // fire change
return p;
};
/**
* One of most important core plugins. It is responsible for all the browsers to have the same effect when the Enter
* button is pressed. By default, it should insert the <p>
*/
export class enter extends Plugin {
private brMode = false;
private defaultTag: 'p' | 'br' | 'div' = consts.PARAGRAPH;
/** @override */
afterInit(editor: IJodit): void {
// use 'enter' option if no set
this.defaultTag = editor.o.enter.toLowerCase() as 'p' | 'div' | 'br';
this.brMode = this.defaultTag === consts.BR.toLowerCase();
if (!editor.o.enterBlock) {
editor.o.enterBlock = this.brMode
? consts.PARAGRAPH
: (this.defaultTag as 'p' | 'div');
}
editor.e
.off('.enter')
.on('keydown.enter', (event: KeyboardEvent): false | void => {
if (event.key === consts.KEY_ENTER) {
/**
* Fired on processing `Enter` key. If return some value, plugin `enter` will do nothing.
* if return false - prevent default Enter behavior
*
* @event beforeEnter
*/
const beforeEnter = editor.e.fire('beforeEnter', event);
if (beforeEnter !== undefined) {
return beforeEnter;
}
if (!editor.s.isCollapsed()) {
editor.execCommand('Delete');
}
editor.s.focus();
this.onEnter(event);
return false;
}
});
}
private onEnter(event: KeyboardEvent): false | void {
const editor = this.j,
sel = editor.selection,
defaultTag = this.defaultTag;
let current = sel.current(false) as Node;
if (!current || current === editor.editor) {
current = editor.createInside.text(INVISIBLE_SPACE);
sel.insertNode(current);
sel.select(current);
}
let currentBox = this.getBlockWrapper(current);
const isLi = Dom.isTag(currentBox, 'li');
// if use <br> defaultTag for break line or when was entered SHIFt key or in <td> or <th> or <blockquote>
if (!isLi && !this.checkBR(current, event.shiftKey)) {
return false;
}
// wrap no wrapped element
if (!currentBox && !this.hasPreviousBlock(current)) {
currentBox = this.wrapText(current);
}
if (!currentBox || currentBox === current) {
insertParagraph(editor, null, isLi ? 'li' : defaultTag);
return false;
}
if (!this.checkUnsplittableBox(currentBox)) {
return false;
}
if (isLi && Dom.isEmpty(currentBox)) {
this.enterInsideEmptyLIelement(currentBox);
return false;
}
const canSplit =
currentBox.tagName.toLowerCase() === this.defaultTag || isLi;
const cursorOnTheRight = sel.cursorOnTheRight(currentBox);
const cursorOnTheLeft = sel.cursorOnTheLeft(currentBox);
if (
(!canSplit || Dom.isEmpty(currentBox)) &&
(cursorOnTheRight || cursorOnTheLeft)
) {
let fake: Nullable<Text> = null;
if (cursorOnTheRight) {
fake = sel.setCursorAfter(currentBox);
} else {
fake = sel.setCursorBefore(currentBox);
}
insertParagraph(editor, fake, this.defaultTag);
if (cursorOnTheLeft && !cursorOnTheRight) {
sel.setCursorIn(currentBox, true);
}
return;
}
sel.splitSelection(currentBox);
}
private getBlockWrapper(
current: Node | null,
tagReg = consts.IS_BLOCK
): Nullable<HTMLElement> {
let node = current;
const root = this.j.editor;
do {
if (!node || node === root) {
break;
}
if (tagReg.test(node.nodeName)) {
if (Dom.isTag(node, 'li')) {
return node;
}
return (
this.getBlockWrapper(node.parentNode, /^li$/i) ||
(node as HTMLElement)
);
}
node = node.parentNode;
} while (node && node !== root);
return null;
}
private checkBR(current: Node, shiftKeyPressed: boolean): boolean {
const isMultyLineBlock = Dom.closest(
current,
['pre', 'blockquote'],
this.j.editor
);
// if use <br> defaultTag for break line or when was entered SHIFt key or in <td> or <th> or <blockquote>
if (
this.brMode ||
(shiftKeyPressed && !isMultyLineBlock) ||
(!shiftKeyPressed && isMultyLineBlock)
) {
const br = this.j.createInside.element('br');
this.j.s.insertNode(br, true);
scrollIntoView(br, this.j.editor, this.j.ed);
return false;
}
return true;
}
private wrapText(current: Node) {
let needWrap: Node = current;
Dom.up(
needWrap,
node => {
if (node && node.hasChildNodes() && node !== this.j.editor) {
needWrap = node;
}
},
this.j.editor
);
const currentBox = Dom.wrapInline(needWrap, this.j.o.enter, this.j);
if (Dom.isEmpty(currentBox)) {
const helper_node = this.j.createInside.element('br');
currentBox.appendChild(helper_node);
this.j.s.setCursorBefore(helper_node);
}
return currentBox;
}
private hasPreviousBlock(current: Node): boolean {
const editor = this.j;
return Boolean(
Dom.prev(
current,
(elm: Node | null) =>
Dom.isBlock(elm, editor.ew) || Dom.isImage(elm, editor.ew),
editor.editor
)
);
}
private checkUnsplittableBox(currentBox: HTMLElement): boolean {
const editor = this.j,
sel = editor.selection;
if (!Dom.canSplitBlock(currentBox, editor.ew)) {
const br = editor.createInside.element('br');
sel.insertNode(br, false);
sel.setCursorAfter(br);
return false;
}
return true;
}
private enterInsideEmptyLIelement(currentBox: HTMLElement): void {
let fakeTextNode: Nullable<Text> = null;
const ul = Dom.closest(currentBox, ['ol', 'ul'], this.j.editor);
if (!ul) {
return;
}
// If there is no LI element before
if (
!Dom.prev(
currentBox,
(elm: Node | null) => Dom.isTag(elm, 'li'),
ul
)
) {
fakeTextNode = this.j.s.setCursorBefore(ul);
// If there is no LI element after
} else if (
!Dom.next(
currentBox,
(elm: Node | null) => Dom.isTag(elm, 'li'),
ul
)
) {
fakeTextNode = this.j.s.setCursorAfter(ul);
} else {
const leftRange = this.j.s.createRange();
leftRange.setStartBefore(ul);
leftRange.setEndAfter(currentBox);
const fragment = leftRange.extractContents();
if (ul.parentNode) {
ul.parentNode.insertBefore(fragment, ul);
}
fakeTextNode = this.j.s.setCursorBefore(ul);
}
Dom.safeRemove(currentBox);
insertParagraph(this.j, fakeTextNode, this.defaultTag);
if (!$$('li', ul).length) {
Dom.safeRemove(ul);
}
}
/** @override */
beforeDestruct(editor: IJodit): void {
editor.e.off('keydown.enter');
}
}