mirror of
https://github.com/codex-team/editor.js
synced 2024-06-20 22:55:15 +02:00
Merge pull request #186 from codex-team/split-pasted-data
Now pasted text splits into paragraphs
This commit is contained in:
commit
02ab4b50d2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
26
example.html
26
example.html
|
@ -56,9 +56,6 @@
|
||||||
<script src="plugins/attaches/attaches.js"></script>
|
<script src="plugins/attaches/attaches.js"></script>
|
||||||
<link rel="stylesheet" href="plugins/attaches/attaches.css">
|
<link rel="stylesheet" href="plugins/attaches/attaches.css">
|
||||||
|
|
||||||
<script src="plugins/personality/personality.js"></script>
|
|
||||||
<link rel="stylesheet" href="plugins/personality/personality.css">
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
codex.editor.start({
|
codex.editor.start({
|
||||||
holderId : "codex-editor",
|
holderId : "codex-editor",
|
||||||
|
@ -222,21 +219,6 @@
|
||||||
maxSize: 50000,
|
maxSize: 50000,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
personality: {
|
|
||||||
type : 'personality',
|
|
||||||
displayInToolbox : true,
|
|
||||||
iconClassname : 'cdx-personality-icon',
|
|
||||||
prepare : cdxEditorPersonality.prepare,
|
|
||||||
render : cdxEditorPersonality.render,
|
|
||||||
save : cdxEditorPersonality.save,
|
|
||||||
validate : cdxEditorPersonality.validate,
|
|
||||||
destroy : cdxEditorPersonality.destroy,
|
|
||||||
enableLineBreaks : true,
|
|
||||||
showInlineToolbar: true,
|
|
||||||
config: {
|
|
||||||
uploadURL: '/uploadPhoto',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data : {
|
data : {
|
||||||
items: [
|
items: [
|
||||||
|
@ -252,14 +234,6 @@
|
||||||
text : 'Пишите нам на team@ifmo.su'
|
text : 'Пишите нам на team@ifmo.su'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type : 'personality',
|
|
||||||
data : {
|
|
||||||
name : 'Красюк Светлана Ивановна',
|
|
||||||
cite : 'Заместитель директора по учебно-воспитательной работе (начальная школа)',
|
|
||||||
url : 'http://new.school332.ru/user/2'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type : 'list',
|
type : 'list',
|
||||||
data : {
|
data : {
|
||||||
|
|
|
@ -71,23 +71,8 @@ module.exports = (function (callbacks) {
|
||||||
*/
|
*/
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
var nativeInputs = editor.content.currentNode.querySelectorAll('textarea, input'),
|
|
||||||
nativeInputsAreEmpty = true,
|
|
||||||
textContentIsEmpty = !editor.content.currentNode.textContent.trim();
|
|
||||||
|
|
||||||
Array.prototype.map.call(nativeInputs, function (input) {
|
if (!editor.core.isBlockEmpty(editor.content.currentNode)) {
|
||||||
|
|
||||||
if (input.type == 'textarea' || input.type == 'text') {
|
|
||||||
|
|
||||||
nativeInputsAreEmpty = nativeInputsAreEmpty && !input.value.trim();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
var blockIsEmpty = textContentIsEmpty && nativeInputsAreEmpty;
|
|
||||||
|
|
||||||
if (!blockIsEmpty) {
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -801,7 +786,7 @@ module.exports = (function (callbacks) {
|
||||||
selectionLength,
|
selectionLength,
|
||||||
firstLevelBlocksCount;
|
firstLevelBlocksCount;
|
||||||
|
|
||||||
if (isNativeInput_(event.target)) {
|
if (editor.core.isNativeInput(event.target)) {
|
||||||
|
|
||||||
/** If input value is empty - remove block */
|
/** If input value is empty - remove block */
|
||||||
if (event.target.value.trim() == '') {
|
if (event.target.value.trim() == '') {
|
||||||
|
@ -895,104 +880,6 @@ module.exports = (function (callbacks) {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* This method prevents default behaviour.
|
|
||||||
*
|
|
||||||
* @param {Object} event
|
|
||||||
* @protected
|
|
||||||
*
|
|
||||||
* @description We get from clipboard pasted data, sanitize, make a fragment that contains of this sanitized nodes.
|
|
||||||
* Firstly, we need to memorize the caret position. We can do that by getting the range of selection.
|
|
||||||
* After all, we insert clear fragment into caret placed position. Then, we should move the caret to the last node
|
|
||||||
*/
|
|
||||||
callbacks.blockPasteCallback = function (event) {
|
|
||||||
|
|
||||||
/** If area is input or textarea then allow default behaviour */
|
|
||||||
if ( isNativeInput_(event.target) ) {
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Prevent default behaviour */
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
var editableParent = editor.content.getEditableParent(event.target),
|
|
||||||
currentNode = editor.content.currentNode;
|
|
||||||
|
|
||||||
/** Allow paste when event target placed in Editable element */
|
|
||||||
if (!editableParent) {
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/** get html pasted data - dirty data */
|
|
||||||
var htmlData = event.clipboardData.getData('text/html'),
|
|
||||||
plainData = event.clipboardData.getData('text/plain');
|
|
||||||
|
|
||||||
/** Temporary DIV that is used to work with childs as arrays item */
|
|
||||||
var div = editor.draw.node('DIV', '', {}),
|
|
||||||
cleanData,
|
|
||||||
fragment;
|
|
||||||
|
|
||||||
/** Create fragment, that we paste to range after proccesing */
|
|
||||||
fragment = document.createDocumentFragment();
|
|
||||||
|
|
||||||
if ( htmlData.trim() != '' ) {
|
|
||||||
|
|
||||||
cleanData = editor.sanitizer.clean(htmlData);
|
|
||||||
div.innerHTML = cleanData;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
div.innerText = plainData.toString();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var node, lastNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* and fill in fragment
|
|
||||||
*/
|
|
||||||
while (( node = div.firstChild) ) {
|
|
||||||
|
|
||||||
lastNode = fragment.appendChild(node);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (editor.tools[currentNode.dataset.tool].allowRenderOnPaste) {
|
|
||||||
|
|
||||||
if (editor.paste.pasted(event)) return;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* work with selection and range
|
|
||||||
*/
|
|
||||||
var selection, range;
|
|
||||||
|
|
||||||
selection = window.getSelection();
|
|
||||||
|
|
||||||
range = selection.getRangeAt(0);
|
|
||||||
range.deleteContents();
|
|
||||||
|
|
||||||
range.insertNode(fragment);
|
|
||||||
|
|
||||||
/** Preserve the selection */
|
|
||||||
if (lastNode) {
|
|
||||||
|
|
||||||
range = range.cloneRange();
|
|
||||||
range.setStartAfter(lastNode);
|
|
||||||
range.collapse(true);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* used by UI module
|
* used by UI module
|
||||||
* Clicks on block settings button
|
* Clicks on block settings button
|
||||||
|
@ -1019,21 +906,6 @@ module.exports = (function (callbacks) {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Check block
|
|
||||||
* @param target
|
|
||||||
* @private
|
|
||||||
*
|
|
||||||
* @description Checks target is it native input
|
|
||||||
*/
|
|
||||||
var isNativeInput_ = function (target) {
|
|
||||||
|
|
||||||
var nativeInputAreas = ['INPUT', 'TEXTAREA'];
|
|
||||||
|
|
||||||
return (nativeInputAreas.indexOf(target.tagName) != -1);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
return callbacks;
|
return callbacks;
|
||||||
|
|
||||||
})({});
|
})({});
|
|
@ -268,6 +268,38 @@ module.exports = (function (caret) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts node at the caret location
|
||||||
|
* @param {HTMLElement|DocumentFragment} node
|
||||||
|
*/
|
||||||
|
caret.insertNode = function (node) {
|
||||||
|
|
||||||
|
var selection, range,
|
||||||
|
lastNode = node;
|
||||||
|
|
||||||
|
if (node.nodeType == editor.core.nodeTypes.DOCUMENT_FRAGMENT) {
|
||||||
|
|
||||||
|
lastNode = node.lastChild;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
selection = window.getSelection();
|
||||||
|
|
||||||
|
range = selection.getRangeAt(0);
|
||||||
|
range.deleteContents();
|
||||||
|
|
||||||
|
range.insertNode(node);
|
||||||
|
|
||||||
|
range.setStartAfter(lastNode);
|
||||||
|
range.collapse(true);
|
||||||
|
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
return caret;
|
return caret;
|
||||||
|
|
||||||
})({});
|
})({});
|
|
@ -120,7 +120,7 @@ module.exports = (function (content) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentNode = this.getFirstLevelBlock(targetNode);
|
content.currentNode = content.getFirstLevelBlock(targetNode);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -639,10 +639,17 @@ module.exports = (function (content) {
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
* @param [String] htmlString - html content as string
|
* @param {string} htmlData - html content as string
|
||||||
|
* @param {string} plainData - plain text
|
||||||
* @return {string} - html content as string
|
* @return {string} - html content as string
|
||||||
*/
|
*/
|
||||||
content.wrapTextWithParagraphs = function (htmlString) {
|
content.wrapTextWithParagraphs = function (htmlData, plainData) {
|
||||||
|
|
||||||
|
if (!htmlData.trim()) {
|
||||||
|
|
||||||
|
return wrapPlainTextWithParagraphs(plainData);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
var wrapper = document.createElement('DIV'),
|
var wrapper = document.createElement('DIV'),
|
||||||
newWrapper = document.createElement('DIV'),
|
newWrapper = document.createElement('DIV'),
|
||||||
|
@ -656,7 +663,7 @@ module.exports = (function (content) {
|
||||||
* Make HTML Element to Wrap Text
|
* Make HTML Element to Wrap Text
|
||||||
* It allows us to work with input data as HTML content
|
* It allows us to work with input data as HTML content
|
||||||
*/
|
*/
|
||||||
wrapper.innerHTML = htmlString;
|
wrapper.innerHTML = htmlData;
|
||||||
paragraph = document.createElement('P');
|
paragraph = document.createElement('P');
|
||||||
|
|
||||||
for (i = 0; i < wrapper.childNodes.length; i++) {
|
for (i = 0; i < wrapper.childNodes.length; i++) {
|
||||||
|
@ -706,6 +713,17 @@ module.exports = (function (content) {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits strings on new line and wraps paragraphs with <p> tag
|
||||||
|
* @param plainText
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
var wrapPlainTextWithParagraphs = function (plainText) {
|
||||||
|
|
||||||
|
return '<p>' + plainText.split('\n\n').join('</p><p>') + '</p>';
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds closest Contenteditable parent from Element
|
* Finds closest Contenteditable parent from Element
|
||||||
* @param {Element} node element looking from
|
* @param {Element} node element looking from
|
||||||
|
|
|
@ -112,7 +112,8 @@ module.exports = (function (core) {
|
||||||
core.nodeTypes = {
|
core.nodeTypes = {
|
||||||
TAG : 1,
|
TAG : 1,
|
||||||
TEXT : 3,
|
TEXT : 3,
|
||||||
COMMENT : 8
|
COMMENT : 8,
|
||||||
|
DOCUMENT_FRAGMENT: 11
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -338,6 +339,49 @@ module.exports = (function (core) {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check block
|
||||||
|
* @param target
|
||||||
|
* @description Checks target is it native input
|
||||||
|
*/
|
||||||
|
core.isNativeInput = function (target) {
|
||||||
|
|
||||||
|
var nativeInputAreas = ['INPUT', 'TEXTAREA'];
|
||||||
|
|
||||||
|
return nativeInputAreas.indexOf(target.tagName) != -1;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if block is empty
|
||||||
|
* We should check block textContent, child native inputs and some exceptions like IMG and IFRAME
|
||||||
|
*
|
||||||
|
* @param block
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
core.isBlockEmpty = function (block) {
|
||||||
|
|
||||||
|
const EXCEPTION_TAGS = ['IMG', 'IFRAME'];
|
||||||
|
|
||||||
|
var nativeInputs = block.querySelectorAll('textarea, input'),
|
||||||
|
nativeInputsAreEmpty = true,
|
||||||
|
textContentIsEmpty = !block.textContent.trim();
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(nativeInputs, function (input) {
|
||||||
|
|
||||||
|
if (input.type == 'textarea' || input.type == 'text') {
|
||||||
|
|
||||||
|
nativeInputsAreEmpty = nativeInputsAreEmpty && !input.value.trim();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return textContentIsEmpty && nativeInputsAreEmpty && !EXCEPTION_TAGS.includes(block.tagName);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return core;
|
return core;
|
||||||
|
|
||||||
})({});
|
})({});
|
||||||
|
|
164
modules/paste.js
164
modules/paste.js
|
@ -107,6 +107,170 @@ module.exports = function (paste) {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method prevents default behaviour.
|
||||||
|
*
|
||||||
|
* @param {Object} event
|
||||||
|
* @protected
|
||||||
|
*
|
||||||
|
* @description We get from clipboard pasted data, sanitize, make a fragment that contains of this sanitized nodes.
|
||||||
|
* Firstly, we need to memorize the caret position. We can do that by getting the range of selection.
|
||||||
|
* After all, we insert clear fragment into caret placed position. Then, we should move the caret to the last node
|
||||||
|
*/
|
||||||
|
paste.blockPasteCallback = function (event) {
|
||||||
|
|
||||||
|
|
||||||
|
if (!needsToHandlePasteEvent(event.target)) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prevent default behaviour */
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
/** get html pasted data - dirty data */
|
||||||
|
var htmlData = event.clipboardData.getData('text/html'),
|
||||||
|
plainData = event.clipboardData.getData('text/plain');
|
||||||
|
|
||||||
|
/** Temporary DIV that is used to work with text's paragraphs as DOM-elements*/
|
||||||
|
var paragraphs = editor.draw.node('DIV', '', {}),
|
||||||
|
cleanData,
|
||||||
|
wrappedData;
|
||||||
|
|
||||||
|
/** Create fragment, that we paste to range after proccesing */
|
||||||
|
cleanData = editor.sanitizer.clean(htmlData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We wrap pasted text with <p> tags to split it logically
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
wrappedData = editor.content.wrapTextWithParagraphs(cleanData, plainData);
|
||||||
|
paragraphs.innerHTML = wrappedData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there only one paragraph, just insert in at the caret location
|
||||||
|
*/
|
||||||
|
if (paragraphs.childNodes.length == 1) {
|
||||||
|
|
||||||
|
emulateUserAgentBehaviour(paragraphs.firstChild);
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
insertPastedParagraphs(paragraphs.childNodes);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if we should handle paste event on block
|
||||||
|
* @param block
|
||||||
|
*
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
var needsToHandlePasteEvent = function (block) {
|
||||||
|
|
||||||
|
/** If area is input or textarea then allow default behaviour */
|
||||||
|
if ( editor.core.isNativeInput(block) ) {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var editableParent = editor.content.getEditableParent(block);
|
||||||
|
|
||||||
|
/** Allow paste when event target placed in Editable element */
|
||||||
|
if (!editableParent) {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts new initial plugin blocks with data in paragraphs
|
||||||
|
*
|
||||||
|
* @param {Array} paragraphs - array of paragraphs (<p></p>) whit content, that should be inserted
|
||||||
|
*/
|
||||||
|
var insertPastedParagraphs = function (paragraphs) {
|
||||||
|
|
||||||
|
var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there was no data in working node, remove it
|
||||||
|
*/
|
||||||
|
if (editor.core.isBlockEmpty(editor.content.currentNode)) {
|
||||||
|
|
||||||
|
editor.content.currentNode.remove();
|
||||||
|
editor.caret.setToPreviousBlock(editor.caret.inputIndex);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
paragraphs.forEach(function (paragraph) {
|
||||||
|
|
||||||
|
/** Don't allow empty paragraphs */
|
||||||
|
if (editor.core.isBlockEmpty(paragraph)) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.content.insertBlock({
|
||||||
|
type : NEW_BLOCK_TYPE,
|
||||||
|
block : editor.tools[NEW_BLOCK_TYPE].render({
|
||||||
|
text : paragraph.innerHTML
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.caret.inputIndex++;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.caret.setToPreviousBlock(editor.caret.getCurrentInputIndex() + 1);
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts node content at the caret position
|
||||||
|
*
|
||||||
|
* @param {Node} node - DOM node (could be DocumentFragment), that should be inserted at the caret location
|
||||||
|
*/
|
||||||
|
var emulateUserAgentBehaviour = function (node) {
|
||||||
|
|
||||||
|
var newNode;
|
||||||
|
|
||||||
|
if (node.childElementCount) {
|
||||||
|
|
||||||
|
newNode = document.createDocumentFragment();
|
||||||
|
|
||||||
|
node.childNodes.forEach(function (current) {
|
||||||
|
|
||||||
|
if (!editor.core.isDomNode(current) && current.data.trim() === '') {
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
newNode.appendChild(current.cloneNode(true));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
newNode = document.createTextNode(node.textContent);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.caret.insertNode(newNode);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return paste;
|
return paste;
|
||||||
|
|
||||||
}({});
|
}({});
|
|
@ -361,7 +361,7 @@ module.exports = (function (ui) {
|
||||||
* @example editor.callback.blockPasteViaSanitize(event), the second method.
|
* @example editor.callback.blockPasteViaSanitize(event), the second method.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
editor.listeners.add(block, 'paste', editor.callback.blockPasteCallback, false);
|
editor.listeners.add(block, 'paste', editor.paste.blockPasteCallback, false);
|
||||||
|
|
||||||
editor.listeners.add(block, 'mouseup', editor.toolbar.inline.show, false);
|
editor.listeners.add(block, 'mouseup', editor.toolbar.inline.show, false);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "codex.editor",
|
"name": "codex.editor",
|
||||||
"version": "1.6.3",
|
"version": "1.6.4",
|
||||||
"description": "Codex Editor. Native JS, based on API and Open Source",
|
"description": "Codex Editor. Native JS, based on API and Open Source",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
Loading…
Reference in a new issue