Merge pull request #186 from codex-team/split-pasted-data

Now pasted text splits into paragraphs
This commit is contained in:
George Berezhnoy 2017-04-24 20:06:47 +03:00 committed by GitHub
commit 02ab4b50d2
10 changed files with 270 additions and 166 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -56,9 +56,6 @@
<script src="plugins/attaches/attaches.js"></script>
<link rel="stylesheet" href="plugins/attaches/attaches.css">
<script src="plugins/personality/personality.js"></script>
<link rel="stylesheet" href="plugins/personality/personality.css">
<script>
codex.editor.start({
holderId : "codex-editor",
@ -222,21 +219,6 @@
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 : {
items: [
@ -252,14 +234,6 @@
text : 'Пишите нам на team@ifmo.su'
}
},
{
type : 'personality',
data : {
name : 'Красюк Светлана Ивановна',
cite : 'Заместитель директора по учебно-воспитательной работе (начальная школа)',
url : 'http://new.school332.ru/user/2'
}
},
{
type : 'list',
data : {

View file

@ -71,23 +71,8 @@ module.exports = (function (callbacks) {
*/
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 (input.type == 'textarea' || input.type == 'text') {
nativeInputsAreEmpty = nativeInputsAreEmpty && !input.value.trim();
}
});
var blockIsEmpty = textContentIsEmpty && nativeInputsAreEmpty;
if (!blockIsEmpty) {
if (!editor.core.isBlockEmpty(editor.content.currentNode)) {
return;
@ -801,7 +786,7 @@ module.exports = (function (callbacks) {
selectionLength,
firstLevelBlocksCount;
if (isNativeInput_(event.target)) {
if (editor.core.isNativeInput(event.target)) {
/** If input value is empty - remove block */
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
* 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;
})({});

View file

@ -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;
})({});

View file

@ -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
*
* @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
*/
content.wrapTextWithParagraphs = function (htmlString) {
content.wrapTextWithParagraphs = function (htmlData, plainData) {
if (!htmlData.trim()) {
return wrapPlainTextWithParagraphs(plainData);
}
var wrapper = document.createElement('DIV'),
newWrapper = document.createElement('DIV'),
@ -656,7 +663,7 @@ module.exports = (function (content) {
* Make HTML Element to Wrap Text
* It allows us to work with input data as HTML content
*/
wrapper.innerHTML = htmlString;
wrapper.innerHTML = htmlData;
paragraph = document.createElement('P');
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
* @param {Element} node element looking from

View file

@ -112,7 +112,8 @@ module.exports = (function (core) {
core.nodeTypes = {
TAG : 1,
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;
})({});

View file

@ -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;
}({});

View file

@ -361,7 +361,7 @@ module.exports = (function (ui) {
* @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);

View file

@ -1,6 +1,6 @@
{
"name": "codex.editor",
"version": "1.6.3",
"version": "1.6.4",
"description": "Codex Editor. Native JS, based on API and Open Source",
"main": "index.js",
"scripts": {