Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve insertnode #853

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 68 additions & 26 deletions src/lib/RangeHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export default function RangeHelper(win, d, sanitize) {
* @param {Node|string} node
* @param {Node|string} [endNode]
* @param {boolean} [returnHtml]
* @return {Node|string}
* @return {DocumentFragment|string}
* @private
*/
_prepareInput = function (node, endNode, returnHtml) {
Expand Down Expand Up @@ -191,61 +191,103 @@ export default function RangeHelper(win, d, sanitize) {
*
* Returns boolean false on fail
*
* @param {Node} node
* @param {Node} startNode
* @param {Node} endNode
* @return {false|undefined}
* @function
* @name insertNode
* @memberOf RangeHelper.prototype
*/
base.insertNode = function (node, endNode) {
var first, last,
input = _prepareInput(node, endNode),
range = base.selectedRange(),
base.insertNode = function (startNode, endNode) {
var selStartNode, selEndNode, node,
input = _prepareInput(startNode, endNode),
range = base.selectedRange(),
parent = range.commonAncestorContainer,
emptyNodes = [];
checkNodes = [],
startInlines = [],
endInlines = [];

if (!input) {
return false;
}

function removeIfEmpty(node) {
// Only remove empty node if it wasn't already empty
if (node && dom.isEmpty(node) && emptyNodes.indexOf(node) < 0) {
dom.remove(node);
}
}

if (range.startContainer !== range.endContainer) {
utils.each(parent.childNodes, function (_, node) {
if (dom.isEmpty(node)) {
emptyNodes.push(node);
function addCheckNodes(container) {
while (parent.contains(container)) {
if (!dom.isEmpty(container)) {
checkNodes.push(container);
}
});

first = input.firstChild;
last = input.lastChild;
container = container.parentNode;
}
}

range.deleteContents();

// FF allows <br /> to be selected but inserting a node
// into <br /> will cause it not to be displayed so must
// insert before the <br /> in FF.
// 3 = TextNode
if (parent && parent.nodeType !== 3 && !dom.canHaveChildren(parent)) {
range.deleteContents();
dom.insertBefore(input, parent);
} else {
if (range.startContainer !== range.endContainer) {
// Store non-empty nodes at start and end up to the parent to
// check if deleteContents() has emptied them.
// If it has, they should be removed.
addCheckNodes(range.startContainer);
addCheckNodes(range.endContainer);

// Store inlines at the start or end of the input
node = input.firstChild;
while (node && dom.isInline(node, true)) {
startInlines.unshift(node);
node = node.nextSibling;
}

node = input.lastChild;
while (node && dom.isInline(node, true)) {
endInlines.push(node);
node = node.previousSibling;
}
}

base.saveRange();
selStartNode = base.getMarker(startMarker);
selEndNode = base.getMarker(endMarker);
range.setStartAfter(selStartNode);
range.setEndBefore(selEndNode);

range.deleteContents();
range.insertNode(input);

// Move any inlines from start / end of input into start or end of
// of the selection, for example, inserting "a<div>b</div>c":
// <p>x|</p><div>y</div><p>|z</p>
// After deleteContents will become:
// <p>x</p>|<p>z</p>
// After insert becomes
// <p>x</p>a<div>b</div>c|<p>z</p>
// And after moving them below, will become:
// <p>xa</p><div>b</div><p>c|z</p>
while (endInlines.length) {
dom.insertBefore(endInlines.pop(), selEndNode);
}
while (startInlines.length) {
dom.insertBefore(startInlines.pop(), selStartNode);
}

dom.remove(selStartNode);
dom.remove(selEndNode);

// If a node was split or its contents deleted, remove any resulting
// empty tags. For example:
// <p>|test</p><div>test|</div>
// When deleteContents could become:
// <p></p>|<div></div>
// So remove the empty ones
removeIfEmpty(first && first.previousSibling);
removeIfEmpty(last && last.nextSibling);
utils.each(checkNodes, function (_, node) {
if (dom.isEmpty(node)) {
dom.remove(node);
}
});
}

base.restoreRange();
Expand Down
23 changes: 21 additions & 2 deletions src/lib/SCEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1511,9 +1511,29 @@ export default function SCEditor(original, userOptions) {
}

dom.appendChild(firstParent || container, range.cloneContents());
dom.removeWhiteSpace(container);

e.clipboardData.setData('text/html', container.innerHTML);
e.clipboardData.setData('text/plain', range.toString());

// TODO: Refactor into private shared module with plaintext plugin
// innerText adds two newlines after <p> tags so convert them to
// <div> tags
utils.each(dom.find(container, 'p'), function (_, elm) {
dom.convertElement(elm, 'div');
});
// Remove collapsed <br> tags as innerText converts them to newlines
utils.each(dom.find(container, 'br'), function (_, elm) {
if (!elm.nextSibling || !dom.isInline(elm.nextSibling, true)) {
dom.remove(elm);
}
});

// range.toString() doesn't include newlines so can't use that.
// selection.toString() seems to use the same method as innerText
// but needs to be normalised first so using container.innerText
dom.appendChild(wysiwygBody, container);
e.clipboardData.setData('text/plain', container.innerText);
dom.remove(container);

if (e.type === 'cut') {
range.deleteContents();
Expand Down Expand Up @@ -1639,7 +1659,6 @@ export default function SCEditor(original, userOptions) {

var parent = rangeHelper.getFirstBlockParent();
base.wysiwygEditorInsertHtml(paste.val, null, true);
dom.fixNesting(parent);
dom.merge(parent);
};

Expand Down
4 changes: 2 additions & 2 deletions src/lib/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -773,11 +773,11 @@ export function copyCSS(from, to) {
*/
export function isEmpty(node) {
if (node.lastChild && isEmpty(node.lastChild)) {
remove(node.lastChild);
return node.firstChild === node.lastChild;
}

return node.nodeType === 3 ? !node.nodeValue :
(canHaveChildren(node) && !node.childNodes.length);
(canHaveChildren(node) && !node.firstChild);
}

/**
Expand Down
23 changes: 21 additions & 2 deletions src/plugins/plaintext.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
(function (sceditor) {
'use strict';

var extend = sceditor.utils.extend;
var utils = sceditor.utils;
var dom = sceditor.dom;

/**
* Options:
Expand All @@ -33,7 +34,7 @@
if (opts && opts.plaintext && opts.plaintext.addButton) {
plainTextEnabled = opts.plaintext.enabled;

commands.pastetext = extend(commands.pastetext || {}, {
commands.pastetext = utils.extend(commands.pastetext || {}, {
state: function () {
return plainTextEnabled ? 1 : 0;
},
Expand All @@ -49,7 +50,25 @@
if (data.html && !data.text) {
var div = document.createElement('div');
div.innerHTML = data.html;

// TODO: Refactor into private shared module with editor
// innerText adds two newlines after <p> tags so convert
// them to <div> tags
utils.each(div.querySelectorAll('p'), function (_, elm) {
dom.convertElement(elm, 'div');
});
// Remove collapsed <br> tags as innerText converts them to
// newlines
utils.each(div.querySelectorAll('br'), function (_, elm) {
if (!elm.nextSibling ||
!dom.isInline(elm.nextSibling, true)) {
elm.parentNode.removeChild(elm);
}
});

document.body.appendChild(div);
data.text = div.innerText;
document.body.removeChild(div);
}

data.html = null;
Expand Down
24 changes: 19 additions & 5 deletions tests/unit/htmlAssert.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import * as utils from 'tests/unit/utils.js';

function nodeHtml(node) {
if (node.parentNode) {
return node.parentNode.innerHTML;
}

var div = node.ownerDocument.createElement('div');
div.appendChild(node);

return div.innerHTML;
}

var normalize = function (parentNode) {
var nextSibling,
node = parentNode.firstChild;
Expand Down Expand Up @@ -29,6 +40,9 @@ var compareNodes = function (nodeA, nodeB) {
return false;
}

nodeA.normalize();
nodeB.normalize();

if (nodeA.nodeType === 1) {
if (nodeA.attributes.length !== nodeB.attributes.length ||
nodeA.childNodes.length !== nodeB.childNodes.length) {
Expand Down Expand Up @@ -102,20 +116,20 @@ QUnit.assert.nodesEqual = function (actual, expected, message) {

this.pushResult({
result: compareNodes(actual, expected),
actual: actual,
expected: expected,
actual: nodeHtml(actual),
expected: nodeHtml(expected),
message: message || 'Expected nodes to be equal'
});
};

QUnit.assert.nodesNodeEqual = function (actual, expected, message) {
QUnit.assert.nodesNoteEqual = function (actual, expected, message) {
normalize(actual);
normalize(expected);

this.pushResult({
result: !compareNodes(actual, expected),
actual: actual,
expected: expected,
actual: nodeHtml(actual),
expected: nodeHtml(expected),
message: message || 'Expected nodes to not be equal'
});
};
Loading