jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
551 lines (476 loc) • 13.2 kB
text/typescript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Licensed under GNU General Public License version 2 or later or a commercial license or MIT;
* For GPL see LICENSE-GPL.txt in the project root for license information.
* For MIT see LICENSE-MIT.txt in the project root for license information.
* For commercial licenses see https://xdsoft.net/jodit/commercial/
* Copyright (c) 2013-2019 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Config } from '../Config';
import {
INSERT_AS_HTML,
INSERT_AS_TEXT,
INSERT_CLEAR_HTML,
INSERT_ONLY_TEXT,
IS_IE,
TEXT_HTML,
TEXT_PLAIN
} from '../constants';
import { Confirm, Dialog } from '../modules/dialog/';
import {
applyStyles,
browser,
cleanFromWord,
htmlspecialchars,
isHTML,
isHTMLFromWord,
trim,
type,
setTimeout,
stripTags
} from '../modules/helpers/';
import { IControlType } from '../types/toolbar';
import { Dom } from '../modules/Dom';
import { IJodit } from '../types';
import { nl2br } from '../modules/helpers/html/nl2br';
declare module '../Config' {
interface Config {
/**
* Ask before paste HTML in WYSIWYG mode
*/
askBeforePasteHTML: boolean;
askBeforePasteFromWord: boolean;
/**
* Inserts HTML line breaks before all newlines in a string
*/
nl2brInPlainText: boolean;
/**
* Default insert method
*/
defaultActionOnPaste: string;
}
}
Config.prototype.askBeforePasteHTML = true;
Config.prototype.askBeforePasteFromWord = true;
Config.prototype.nl2brInPlainText = true;
Config.prototype.defaultActionOnPaste = INSERT_AS_HTML;
Config.prototype.controls.cut = {
command: 'cut',
isDisable: (editor: IJodit) => {
const sel = editor.selection.sel;
return !sel || sel.isCollapsed;
},
tooltip: 'Cut selection'
} as IControlType;
/**
* Ask before paste HTML source
*/
export function paste(editor: IJodit) {
let buffer: string = '';
const clearOrKeep = (
msg: string,
title: string,
callback: (yes: boolean | number) => void,
clearButton: string = 'Clean',
clear2Button: string = 'Insert only Text'
): Dialog | void => {
if (
editor.events &&
editor.events.fire(
'beforeOpenPasteDialog',
msg,
title,
callback,
clearButton,
clear2Button
) === false
) {
return;
}
const dialog: Dialog = Confirm(
`<div style="word-break: normal; white-space: normal">${msg}</div>`,
title,
callback
);
dialog.container.setAttribute('data-editor_id', editor.id);
const keep = dialog.create.fromHTML(
'<a href="javascript:void(0)" style="float:left;" class="jodit_button">' +
'<span>' +
editor.i18n('Keep') +
'</span>' +
'</a>'
) as HTMLAnchorElement;
const clear = dialog.create.fromHTML(
'<a href="javascript:void(0)" style="float:left;" class="jodit_button">' +
'<span>' +
editor.i18n(clearButton) +
'</span>' +
'</a>'
) as HTMLAnchorElement;
const clear2 = dialog.create.fromHTML(
'<a href="javascript:void(0)" style="float:left;" class="jodit_button">' +
'<span>' +
editor.i18n(clear2Button) +
'</span>' +
'</a>'
) as HTMLAnchorElement;
const cancel = dialog.create.fromHTML(
'<a href="javascript:void(0)" style="float:right;" class="jodit_button">' +
'<span>' +
editor.i18n('Cancel') +
'</span>' +
'</a>'
) as HTMLAnchorElement;
editor.events.on(keep, 'click', () => {
dialog.close();
callback && callback(true);
});
editor.events.on(clear, 'click', () => {
dialog.close();
callback && callback(false);
});
editor.events.on(clear2, 'click', () => {
dialog.close();
callback && callback(0);
});
editor.events.on(cancel, 'click', () => {
dialog.close();
});
dialog.setFooter([keep, clear, clear2Button ? clear2 : '', cancel]);
editor.events &&
editor.events.fire(
'afterOpenPasteDialog',
dialog,
msg,
title,
callback,
clearButton,
clear2Button
);
return dialog;
};
const insertByType = (html: string | Node, subtype: string) => {
if (typeof html === 'string') {
switch (subtype) {
case INSERT_CLEAR_HTML:
html = cleanFromWord(html);
break;
case INSERT_ONLY_TEXT:
html = stripTags(html);
break;
case INSERT_AS_TEXT:
html = htmlspecialchars(html);
break;
default:
}
}
editor.selection.insertHTML(html);
};
const insertHTML = (
html: string,
event: DragEvent | ClipboardEvent
): void | false => {
if (isHTML(html) && buffer !== trimFragment(html)) {
editor.events.stopPropagation('beforePaste');
html = trimFragment(html);
clearOrKeep(
editor.i18n('Your code is similar to HTML. Keep as HTML?'),
editor.i18n('Paste as HTML'),
(agree: boolean | number) => {
let insertType: string = INSERT_AS_HTML;
if (agree === false) {
insertType = INSERT_AS_TEXT;
}
if (agree === 0) {
insertType = INSERT_ONLY_TEXT;
}
if (event.type === 'drop') {
editor.selection.insertCursorAtPoint(
(event as DragEvent).clientX,
(event as DragEvent).clientY
);
}
insertByType(html, insertType);
editor.setEditorValue();
},
'Insert as Text'
);
return false;
}
};
const trimFragment = (html: string): string => {
const start: number = html.search(/<!--StartFragment-->/i);
if (start !== -1) {
html = html.substr(start + 20);
}
const end: number = html.search(/<!--EndFragment-->/i);
if (end !== -1) {
html = html.substr(0, end);
}
return html;
};
const getDataTransfer = (
event: ClipboardEvent | DragEvent
): DataTransfer | null => {
if ((event as ClipboardEvent).clipboardData) {
return (event as ClipboardEvent).clipboardData;
}
return (event as DragEvent).dataTransfer || new DataTransfer();
};
editor.events
.on(
'copy cut',
(event: ClipboardEvent): false | void => {
const selectedText: string = editor.selection.getHTML();
const clipboardData =
getDataTransfer(event) ||
getDataTransfer(editor.editorWindow as any) ||
getDataTransfer((event as any).originalEvent);
if (clipboardData) {
clipboardData.setData(TEXT_PLAIN, stripTags(selectedText));
clipboardData.setData(TEXT_HTML, selectedText);
}
buffer = selectedText;
if (event.type === 'cut') {
editor.selection.remove();
editor.selection.focus();
}
event.preventDefault();
editor.events.fire('afterCopy', selectedText);
}
)
.on(
'paste',
(event: ClipboardEvent | DragEvent): false | void => {
/**
* Triggered before pasting something into the Jodit Editor
*
* @event beforePaste
* @param {ClipboardEvent} event
* @return Returning false in the handler assigned toWYSIWYG the event will cancel the current action.
* @example
* ```javascript
* var editor = new Jodit("#redactor");
* editor.events.on('beforePaste', function (event) {
* return false; // deny paste
* });
* ```
*/
if (editor.events.fire('beforePaste', event) === false) {
event.preventDefault();
return false;
}
const dt = getDataTransfer(event);
if (event && dt) {
const types: ReadonlyArray<string> | string = dt.types;
let
types_str: string = '',
clipboard_html: any = '';
if (
Array.isArray(types) ||
type(types) === 'domstringlist'
) {
for (let i = 0; i < types.length; i += 1) {
types_str += types[i] + ';';
}
} else {
types_str = types.toString() + ';';
}
if (/text\/html/i.test(types_str)) {
clipboard_html = dt.getData('text/html');
} else if (
/text\/rtf/i.test(types_str) && browser('safari')
) {
clipboard_html = dt.getData('text/rtf');
} else if (
/text\/plain/i.test(types_str) && !browser('mozilla')
) {
clipboard_html = dt.getData(TEXT_PLAIN);
} else if (/text/i.test(types_str) && IS_IE) {
clipboard_html = dt.getData(TEXT_PLAIN);
}
if (
clipboard_html instanceof
(editor.editorWindow as any).Node ||
trim(clipboard_html) !== ''
) {
/**
* Triggered after the content is pasted from the clipboard into the Jodit.
* If a string is returned the new string will be used as the pasted content.
*
* @event beforePaste
* @param {ClipboardEvent} event
* @return Return {string|undefined}
* @example
* ```javascript
* var editor = new Jodit("#redactor");
* editor.events.on('beforePaste', function (event) {
* return false; // deny paste
* });
* ```
*/
clipboard_html = trimFragment(clipboard_html);
if (buffer !== clipboard_html) {
clipboard_html = editor.events.fire(
'processPaste', event, clipboard_html, types_str
);
}
if (
typeof clipboard_html === 'string' ||
Dom.isNode(clipboard_html, editor.editorWindow)
) {
if (event.type === 'drop') {
editor.selection.insertCursorAtPoint(
(event as DragEvent).clientX,
(event as DragEvent).clientY
);
}
insertByType(
clipboard_html,
editor.options.defaultActionOnPaste
);
}
event.preventDefault();
event.stopPropagation();
}
}
/**
* Triggered after pasting something into the Jodit
*
* @event afterPaste
* @param {ClipboardEvent} event
* @return Return {string|undefined}
* @example
* ```javascript
* var editor = new Jodit("#redactor");
* editor.events.on('afterPaste', function (event) {
* return false; // deny paste
* });
* ```
*/
if (editor.events.fire('afterPaste', event) === false) {
return false;
}
}
);
if (editor.options.askBeforePasteHTML) {
editor.events.on(
'beforePaste',
(event: ClipboardEvent | DragEvent): false | void => {
const dt = getDataTransfer(event);
if (event && dt && dt.getData(TEXT_PLAIN)) {
const html: string = dt.getData(TEXT_PLAIN);
return insertHTML(html, event);
}
}
);
}
if (editor.options.askBeforePasteFromWord) {
editor.events.on(
'beforePaste',
(event: ClipboardEvent): false | void => {
const dt = getDataTransfer(event);
if (event && dt && dt.getData && dt.getData(TEXT_HTML)) {
const processHTMLData = (html: string): void | false => {
if (isHTML(html) && buffer !== trimFragment(html)) {
if (isHTMLFromWord(html)) {
clearOrKeep(
editor.i18n(
'The pasted content is coming from a Microsoft Word/Excel document. ' +
'Do you want to keep the format or clean it up?'
),
editor.i18n('Word Paste Detected'),
(agree: boolean | number) => {
if (agree === true) {
html = applyStyles(html);
if (
editor.options.beautifyHTML &&
(editor.ownerWindow as any)
.html_beautify
) {
html = (editor.ownerWindow as any).html_beautify(
html
);
}
}
if (agree === false) {
html = cleanFromWord(html);
}
if (agree === 0) {
html = stripTags(
cleanFromWord(html)
);
}
editor.selection.insertHTML(html);
editor.setEditorValue();
}
);
} else {
insertHTML(html, event);
}
return false;
}
};
if (
dt.types &&
Array.from(dt.types).indexOf('text/html') !== -1
) {
const html: string = dt.getData(TEXT_HTML);
return processHTMLData(html);
} else if (event.type !== 'drop') {
const div = editor.create.div('', {
tabindex: -1,
contenteditable: true,
style: {
left: -9999,
top: 0,
width: 0,
height: '100%',
lineHeight: '140%',
overflow: 'hidden',
position: 'fixed',
zIndex: 2147483647,
wordBreak: 'break-all'
}
});
editor.container.appendChild(div);
const selData = editor.selection.save();
div.focus();
let tick: number = 0;
const removeFakeFocus = () => {
Dom.safeRemove(div);
editor.selection && editor.selection.restore(selData);
};
const waitData = () => {
tick += 1;
// If data has been processes by browser, process it
if (div.childNodes && div.childNodes.length > 0) {
const pastedData: string = div.innerHTML;
removeFakeFocus();
if (processHTMLData(pastedData) !== false) {
editor.selection.insertHTML(pastedData);
}
} else {
if (tick < 5) {
setTimeout(waitData, 20);
} else {
removeFakeFocus();
}
}
};
waitData();
}
}
}
);
}
if (editor.options.nl2brInPlainText) {
editor.events.on(
'processPaste',
(event: ClipboardEvent, text: string, type: string): string | void => {
if (type === TEXT_PLAIN + ';' && !isHTML(text)) {
return nl2br(text);
}
})
}
}