jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
979 lines (850 loc) • 21.8 kB
JavaScript
/*!
* 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
*/
typeof window.chai !== 'undefined' &&
(function () {
chai.config.includeStack = true;
chai.config.showDiff = true;
})();
typeof window.mocha !== 'undefined' && mocha.timeout(15000);
const oldI18n = Jodit.prototype.i18n,
oldAjaxSender = Jodit.modules.Ajax.prototype.send,
naturalPromise = window.Promise;
function mockPromise() {
window.Promise = SynchronousPromise;
}
function unmockPromise() {
window.Promise = naturalPromise;
}
const defaultPermissions = {
permissions: {
allowFiles: true,
allowFileMove: true,
allowFileUpload: true,
allowFileUploadRemote: true,
allowFileRemove: true,
allowFileRename: true,
allowFolders: true,
allowFolderMove: true,
allowFolderCreate: true,
allowFolderRemove: true,
allowFolderRename: true,
allowImageResize: true,
allowImageCrop: true
}
};
function mockAjax() {
if (typeof window.chai !== 'undefined') {
Jodit.modules.Ajax.prototype.send = function () {
const ajax = this;
const request = this.prepareRequest();
let action = request.data.action;
if (!action && request.data.get) {
action = request.data.get('action');
}
if (
action === undefined &&
request.url &&
request.url.match(/action=/)
) {
const actioExec = /action=([\w]+)/.exec(request.url);
action = actioExec[1];
}
return new Promise(function (resolve) {
switch (action) {
case 'fileUpload': {
const file = ajax.options.data.get('files[0]');
resolve({
success: true,
time: '2018-03-31 23:38:54',
data: {
baseurl: 'https://xdsoft.net/jodit/files/',
messages: [],
files: [file.name],
isImages: [/\.(png|jpg|gif)$/.test(file.name)],
code: 220
}
});
break;
}
case 'files':
resolve({
success: true,
time: '2018-03-15 12:49:49',
data: {
sources: {
default: {
baseurl:
'https://xdsoft.net/jodit/files/',
path: '',
files: [
{
file:
'1966051_524428741092238_1051008806888563137_o.jpg',
thumb:
'_thumbs/1966051_524428741092238_1051008806888563137_o.jpg',
changed: '03/15/2018 12:40 PM',
size: '126.59kB',
isImage: true
},
{
file: 'images.jpg',
thumb: '_thumbs/images.jpg',
changed: '01/15/2018 12:40 PM',
size: '6.84kB',
isImage: true
},
{
file: 'ibanez-s520-443140.jpg',
thumb:
'_thumbs/ibanez-s520-443140.jpg',
changed: '04/15/2018 12:40 PM',
size: '18.73kB',
isImage: true
},
{
file: 'test.txt',
thumb: '_thumbs/test.txt.png',
changed: '05/15/2018 12:40 PM',
size: '18.72kB',
isImage: false
}
]
}
},
code: 220
}
});
break;
case 'folders':
resolve({
success: true,
time: '2018-03-15 12:49:49',
data: {
sources: {
default: {
baseurl:
'https://xdsoft.net/jodit/files/',
path: '',
folders: ['.', 'ceicom', 'test']
}
},
code: 220
}
});
break;
case 'permissions':
resolve({
success: true,
time: '2018-03-15 12:49:49',
data: defaultPermissions,
code: 220
});
break;
case 'fileUploadRemote':
resolve({
success: true,
time: '2018-03-15 12:45:03',
data: {
newfilename: 'artio.jpg',
baseurl: 'https://xdsoft.net/jodit/files/',
code: 220
}
});
break;
case 'getLocalFileByUrl':
switch (ajax.options.data.url) {
case location.protocol +
'//' +
location.host +
'/artio.jpg':
case location.protocol +
'//' +
location.host +
'/tests/artio.jpg':
case location.protocol +
'//' +
location.host +
'/test/tests/artio.jpg':
case location.protocol +
'//' +
location.host +
'/jodit/test/tests/artio.jpg':
case 'https://xdsoft.net/jodit/files/th.jpg':
resolve({
success: true,
time: '2018-03-15 12:55:00',
data: {
path: '',
name: 'th.jpg',
source: 'default',
code: 220
}
});
break;
default:
resolve({
success: false,
time: '2018-03-15 12:08:54',
data: {
messages: [
'File does not exist or is above the root of the connector'
],
code: 424
}
});
break;
}
break;
default:
break;
}
});
};
}
}
function unmockAjax() {
Jodit.modules.Ajax.prototype.send = oldAjaxSender;
}
if (typeof window.chai !== 'undefined') {
mockPromise();
mockAjax();
window.FormData = function () {
this.data = {};
this.append = function (key, value) {
this.data[key] = value;
};
this.get = function (key) {
return this.data[key];
};
};
}
const i18nkeys = [];
const excludeI18nKeys = ['adddate'];
Jodit.prototype.i18n = function (key) {
!excludeI18nKeys.includes(key) &&
i18nkeys.includes(key) &&
!key.includes('<svg') &&
i18nkeys.push(key);
return oldI18n.apply(this, arguments);
};
Jodit.defaultOptions.events.afterInit = function (editor) {
editor && editor.container.setAttribute('data-test-case', window.mochaTestName);
}
Jodit.defaultOptions.filebrowser.saveStateInStorage = false;
Jodit.defaultOptions.observer.timeout = 0;
Jodit.modules.View.defaultOptions.defaultTimeout = 0;
if (Jodit.defaultOptions.cleanHTML) {
Jodit.defaultOptions.cleanHTML.timeout = 0;
Jodit.defaultOptions.cleanHTML.fillEmptyParagraph = false;
}
Jodit.defaultOptions.sourceEditor = 'area';
Jodit.defaultOptions.language = 'en';
Jodit.defaultOptions.iframeCSSLinks.push('/app.css');
Jodit.defaultOptions.iframeStyle +=
'* {\
-webkit-box-sizing: border-box;\
-moz-box-sizing: border-box;\
box-sizing: border-box;\
}\
td,th {\
padding: 2px 5px;\
vertical-align: top;\
}';
if (String.prototype.repeat === undefined) {
String.prototype.repeat = function (count) {
const result = [];
for (let i = 0; i < count; i++) {
result.push(this);
}
return result.join('');
};
}
const expect = typeof chai !== 'undefined' ? chai.expect : function () {},
stuff = [];
const stringify = Jodit.ns.Helpers.stringify;
const box = document.createElement('div');
document.body.appendChild(box);
function getBox() {
return box;
}
function removeStuff() {
Object.keys(Jodit.instances).forEach(function (key) {
Jodit.instances[key].destruct();
});
stuff.forEach(function (elm) {
elm && elm.parentNode && elm.parentNode.removeChild(elm);
});
stuff.length = 0;
Array.from(
document.querySelectorAll('.jodit.jodit-dialog__box.active')
).forEach(function (dialog) {
simulateEvent('close_dialog', 0, dialog);
});
Jodit.modules.Ajax.log.length = 0;
getBox().removeAttribute('style');
mockPromise();
}
if (typeof afterEach === 'function') {
afterEach(removeStuff);
}
if (typeof beforeEach === 'function') {
beforeEach(function () {
window.mochaTestName = this.test.ctx.currentTest.fullTitle();
});
}
/**
* Create and insert into DOM test textarea
*
* @param [id]
* @param [noput]
* @returns {HTMLTextAreaElement}
*/
function appendTestArea(id, noput) {
const textarea = document.createElement('textarea');
textarea.setAttribute('id', id || 'editor_' + new Date().getTime());
box.appendChild(textarea);
!noput && stuff.push(textarea);
return textarea;
}
/**
* Jodit Factory
*
* @param {object} options
* @param {HTMLElement|undefined} element
* @return {Jodit}
*/
function getJodit(options, element) {
const editor = new Jodit(element || appendTestArea(), options);
window.scrollTo(
0,
Jodit.modules.Helpers.offset(editor.container, editor, editor.od).top
);
editor.container &&
editor.container.setAttribute('data-test-case', window.mochaTestName);
return editor;
}
/**
* Create empty DIV block and but it inside Box
*
* @param [id]
* @param [noput]
* @returns {HTMLDivElement}
*/
function appendTestDiv(id, noput) {
const textarea = document.createElement('div');
textarea.setAttribute('id', id || 'editor_' + new Date().getTime());
box.appendChild(textarea);
!noput && stuff.push(textarea);
return textarea;
}
function trim(value) {
return value.replace(/^[\s\r\t\n]+/g, '').replace(/[\s\r\t\n]+$/g, '');
}
function toFixedWithoutRounding(value, precision) {
const factorError = Math.pow(10, 14);
const factorTruncate = Math.pow(10, 14 - precision);
const factorDecimal = Math.pow(10, precision);
return (
Math.floor(Math.floor(value * factorError + 1) / factorTruncate) /
factorDecimal
);
}
function sortStyles(matches) {
let styles = matches
.replace(/"/g, "'")
.replace(/"/g, "'")
.split(';');
styles = styles.map(trim).filter(function (elm) {
return elm.length;
});
let border = null;
styles = styles
.map(function (elm) {
const keyValue = elm.split(':').map(trim);
if (keyValue[0] === 'border-image') {
return null;
}
if (/rgb\(/.test(keyValue[1])) {
keyValue[1] = keyValue[1].replace(/rgb\([^)]+\)/, function (
match
) {
return Jodit.modules.Helpers.normalizeColor(match);
});
}
if (keyValue[0].match(/^border$/)) {
keyValue[1] = keyValue[1].split(/[\s]+/);
}
if (keyValue[0].match(/^border-(style|width|color)/)) {
if (border === null) {
border = keyValue;
keyValue[0] = 'border';
keyValue[1] = [keyValue[1]];
} else {
border[1].push(keyValue[1]);
return null;
}
}
if (/font-family/i.test(keyValue[0])) {
keyValue[1] = keyValue[1]
.split(',')
.map(Jodit.modules.Helpers.trim)
.join(',');
}
if (/%$/.test(keyValue[1])) {
const fl = parseFloat(keyValue[1]),
nt = parseInt(keyValue[1], 10);
if (fl - nt > 0) {
keyValue[1] = toFixedWithoutRounding(fl, 2) + '%';
}
}
return keyValue;
})
.filter(function (a) {
return a !== null;
})
.map(function (a) {
return a
.map(function (item) {
return typeof item === 'string'
? item
: item.sort().join(' ');
})
.join(':');
})
.sort(function (a, b) {
if (a < b) {
return -1;
}
return a > b ? 1 : 0;
});
return styles.join(';');
}
function sortAttributes(html) {
const tag = /<([^>]+)>/g;
const reg = /([a-z_-]+)[\s]*=[\s]*"([^"]*)"/i,
tags = [];
let matches, tagMatch;
while ((tagMatch = tag.exec(html))) {
const attrs = [];
let newTag = tagMatch[0];
do {
matches = reg.exec(newTag);
if (!matches) {
break;
}
if (matches[1].toLowerCase() === 'style') {
matches[2] = sortStyles(matches[2]);
}
if (matches[1].toLowerCase() !== 'unselectable') {
attrs.push({
name: matches[1].toLowerCase(),
value: matches[2]
});
newTag = newTag.replace(
matches[0],
'attribute:' + attrs.length
);
} else {
newTag = newTag.replace(' ' + matches[0], '');
}
} while (matches);
attrs.sort(function (a, b) {
if (a.name < b.name) {
return -1;
}
return a.name > b.name ? 1 : 0;
});
attrs.forEach(function (elm, i) {
newTag = newTag.replace(
'attribute:' + (i + 1),
elm.name + '="' + elm.value + '"'
);
});
tags.push({
name: tagMatch[0],
value: newTag
});
}
tags.forEach(function (elm) {
html = html.replace(elm.name, elm.value);
});
return html.replace(/ /g, ' ');
}
/**
* Fill the box some elements for space
* @param count
*/
function fillBoxBr(count) {
for (let i = 0; i < 100; i += 1) {
const br = document.createElement('br');
getBox().appendChild(br);
stuff.push(br);
}
}
const codeKey = {
8: 'Backspace',
13: 'Enter',
38: 'ArrowUp',
40: 'ArrowDown',
55: '7',
56: '8',
73: 'i',
70: 'f',
72: 'h',
86: 'v',
89: 'y',
114: 'F3'
};
const keyCode = Object.keys(codeKey).reduce((res, code) => {
res[codeKey[code]] = code;
return res;
}, {});
/**
*
* @param {string} type
* @param {string|number|HTMLElement} keyCodeOrElement
* @param {HTMLElement} [element]
* @param {Function} applyOpt
*/
function simulateEvent(type, keyCodeOrElement, elementOrApplyOpt, applyOpt) {
if (Array.isArray(type)) {
return type.forEach(function (event) {
simulateEvent(event, keyCodeOrElement, elementOrApplyOpt, applyOpt);
});
}
let element = elementOrApplyOpt;
if (typeof keyCodeOrElement === 'object') {
element = keyCodeOrElement;
keyCodeOrElement = null;
if (typeof elementOrApplyOpt === 'function') {
applyOpt = elementOrApplyOpt;
}
}
if (Array.isArray(element)) {
return element.forEach(function (elm) {
simulateEvent(type, keyCodeOrElement, elm, applyOpt);
});
}
const evt = (element.ownerDocument || document).createEvent('HTMLEvents');
evt.initEvent(type, true, true);
if (keyCodeOrElement) {
if (typeof keyCodeOrElement === 'number') {
evt.keyCode = keyCodeOrElement;
evt.which = keyCodeOrElement;
evt.key = codeKey[keyCodeOrElement];
} else if (typeof keyCodeOrElement !== 'object') {
evt.key = keyCodeOrElement;
evt.which = keyCodeOrElement[keyCodeOrElement];
}
}
if (applyOpt) {
applyOpt(evt);
} else if (element.getBoundingClientRect) {
const pos = Jodit.modules.Helpers.position(element);
evt.clientX = pos.left + 5;
evt.clientY = pos.top + 5;
}
if (type.match(/^mouse/)) {
['pageX', 'pageY', 'clientX', 'clientY'].forEach(function (key) {
if (evt[key] === undefined) {
evt[key] = 0;
}
});
}
if (type.match(/^touch/) && !evt.changedTouches) {
const changedTouches = {};
['pageX', 'pageY', 'clientX', 'clientY'].forEach(function (key) {
changedTouches[key] = evt[key];
});
evt.changedTouches = changedTouches;
}
element.dispatchEvent(evt);
}
/**
* @param {Jodit} editor
* @return {HTMLElement|null}
*/
function getOpenedPopup(editor) {
const popups = editor.ownerDocument.querySelectorAll(
'[role="popup"][data-editor_id="' + editor.id + '"]:last-child'
);
return popups.length ? popups[popups.length - 1] : null;
}
/**
* @param {Jodit} editor
* @return {HTMLElement|null}
*/
function getOpenedDialog(editor) {
const dlgs = editor.ownerDocument.querySelectorAll('.jodit-dialog__box');
return dlgs.length ? dlgs[dlgs.length - 1] : null;
}
/**
* Find button inside element
*
* @param {string} buttonName
* @param {Jodit|Element} joditOrElement
* @param {string} [role]
* @param {boolean} [last]
* @returns {HTMLElement|null}
*/
function getButton(buttonName, joditOrElement, role, last) {
const elm = joditOrElement.container || joditOrElement;
return (
elm.querySelector(
'.jodit-toolbar-button.jodit-toolbar-button_' +
buttonName +
(last ? ':last-child' : '') +
' [role="' +
(role || 'button') +
'"]'
) ||
elm.querySelector(
'.jodit-ui-button.jodit-ui-button_' +
buttonName +
(last ? ':last-child' : '') +
'[role="' +
(role || 'button') +
'"]'
)
);
}
/**
* Click and trigger some button event
*
* @param {string} buttonName
* @param {Jodit|HTMLElement} joditOrElement
* @param {string} role
* @param {boolean} last
*/
function clickButton(buttonName, joditOrElement, role, last) {
simulateEvent(
'click',
0,
getButton(buttonName, joditOrElement, role, last)
);
}
function clickTrigger(buttonName, joditOrElement) {
clickButton(buttonName, joditOrElement, 'trigger');
}
/**
* Select table cells
* @param {Jodit} editor
* @param {number[]} indexes
*/
function selectCells(editor, indexes) {
const cells = editor.editor.querySelectorAll('td,th');
indexes.forEach(index => {
editor.getInstance('Table', editor.options).addSelection(cells[index]);
});
}
/**
* Set listener and remove it after first call
*
* @param {string} event
* @param {HTMLElement} element
* @param {Function} callback
*/
function one(event, element, callback) {
const on = function () {
element.removeEventListener(event, on);
callback.apply(element, arguments);
};
element.addEventListener(event, on);
}
/**
* Set one handler for load image
*
* @param {HTMLImageElement} image
* @param {Function} callback
*/
function onLoadImage(image, callback) {
if (!image.complete) {
one('load', image, callback);
} else {
callback.apply(image);
}
}
/**
*
* @param element
* @param pastedText
*/
function simulatePaste(element, pastedText) {
simulateEvent('paste', 0, element, function (data) {
data.clipboardData = {
types: ['text/html'],
getData: function () {
return pastedText;
}
};
});
}
function setCursor(elm, inEnd) {
const range = document.createRange();
range.selectNodeContents(elm);
range.collapse(!inEnd);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
}
function createPoint(x, y, color, fixed = false) {
const div = document.createElement('div');
div.setAttribute(
'style',
'position: ' +
(fixed ? 'fixed' : 'absolute') +
'; z-index: 1000000000;width: 5px; height: 5px; background: ' +
(color || 'red') +
';'
);
div.style.left = parseInt(x, 10) + 'px';
div.style.top = parseInt(y, 10) + 'px';
document.body.appendChild(div);
stuff.push(div);
}
function offset(el) {
const rect = el.getBoundingClientRect(),
scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
scrollTop = window.pageYOffset || document.documentElement.scrollTop;
return {
top: rect.top + scrollTop,
left: rect.left + scrollLeft,
width: rect.width,
height: rect.height
};
}
/**
* innerHTML property for SVGElement
* Copyright(c) 2010, Jeff Schiller
*
* Licensed under the Apache License, Version 2
*
* Works in a SVG document in Chrome 6+, Safari 5+, Firefox 4+ and IE9+.
* Works in a HTML5 document in Chrome 7+, Firefox 4+ and IE9+.
* Does not work in Opera since it doesn't support the SVGElement interface yet.
*
* I haven't decided on the best name for this property - thus the duplication.
*/
(function () {
const serializeXML = function (node, output) {
const nodeType = node.nodeType;
if (nodeType === 3) {
output.push(
node.textContent
.replace(/&/, '&')
.replace(/</, '<')
.replace('>', '>')
);
} else if (nodeType === 1) {
output.push('<', node.tagName);
if (node.hasAttributes()) {
const attrMap = node.attributes;
for (let i = 0, len = attrMap.length; i < len; ++i) {
const attrNode = attrMap.item(i);
output.push(' ', attrNode.name, "='", attrNode.value, "'");
}
}
if (node.hasChildNodes()) {
output.push('>');
const childNodes = node.childNodes;
for (let i = 0, len = childNodes.length; i < len; i += 1) {
serializeXML(childNodes.item(i), output);
}
output.push('</', node.tagName, '>');
} else {
output.push('/>');
}
} else if (nodeType === 8) {
output.push('<!--', node.nodeValue, '-->');
} else {
throw new Error(
'Error serializing XML. Unhandled node of type: ' + nodeType
);
}
};
// The innerHTML DOM property for SVGElement.
Object.defineProperty(SVGElement.prototype, 'innerHTML', {
get: function () {
const output = [];
let childNode = this.firstChild;
while (childNode) {
serializeXML(childNode, output);
childNode = childNode.nextSibling;
}
return output.join('');
},
set: function (markupText) {
// Wipe out the current contents of the element.
while (this.firstChild) {
this.removeChild(this.firstChild);
}
try {
// Parse the markup into valid nodes.
const dXML = new DOMParser();
dXML.async = false;
// Wrap the markup into a SVG node to ensure parsing works.
const sXML =
"<svg xmlns='http://www.w3.org/2000/svg'>" +
markupText +
'</svg>';
const svgDocElement = dXML.parseFromString(sXML, 'text/xml')
.documentElement;
// Now take each node, import it and append to this element.
let childNode = svgDocElement.firstChild;
while (childNode) {
this.appendChild(
this.ownerDocument.importNode(childNode, true)
);
childNode = childNode.nextSibling;
}
} catch (e) {
throw new Error('Error parsing XML string' + e.toString());
}
}
});
// The innerSVG DOM property for SVGElement.
Object.defineProperty(SVGElement.prototype, 'innerSVG', {
get: function () {
return this.innerHTML;
},
set: function (markupText) {
this.innerHTML = markupText;
}
});
})();
// Files
function FileImage() {
return {
name: 'logo.gif',
dataURI:
'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
type: 'image/gif'
};
}
// Files
function FileXLS() {
return {
name: 'file.xls',
type: 'application/xls'
};
}
if (typeof window.chai !== 'undefined') {
window.FileReader = function () {
const self = this;
self.result = null;
/**
*
* @param {FileImage} file
*/
self.readAsDataURL = function (file) {
self.result = file.dataURI;
self.onloadend && self.onloadend();
};
};
}
Object.defineProperty(navigator, 'userAgent', {
value:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 YaBrowser/18.9.0.3363 Yowser/2.5 Safari/537.36'
});