diff --git a/lib/wysihtml/rails/version.rb b/lib/wysihtml/rails/version.rb
index 4f59571..dc79025 100644
--- a/lib/wysihtml/rails/version.rb
+++ b/lib/wysihtml/rails/version.rb
@@ -1,5 +1,5 @@
module Wysihtml
module Rails
- VERSION = "0.5.1"
+ VERSION = "0.5.2"
end
end
diff --git a/vendor/assets/javascripts/wysihtml-toolbar.js b/vendor/assets/javascripts/wysihtml-toolbar.js
index e8c5f61..6906a42 100644
--- a/vendor/assets/javascripts/wysihtml-toolbar.js
+++ b/vendor/assets/javascripts/wysihtml-toolbar.js
@@ -1,5 +1,5 @@
/**
- * @license wysihtml v0.5.1
+ * @license wysihtml v0.5.2
* https://github.com/Voog/wysihtml
*
* Author: Christopher Blum (https://github.com/tiff)
@@ -10,7 +10,7 @@
*
*/
var wysihtml5 = {
- version: "0.5.1",
+ version: "0.5.2",
// namespaces
commands: {},
@@ -76,19 +76,19 @@ var wysihtml5 = {
// element.textContent polyfill.
if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(win.Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(win.Element.prototype, "textContent").get) {
- (function() {
- var innerText = Object.getOwnPropertyDescriptor(win.Element.prototype, "innerText");
- Object.defineProperty(win.Element.prototype, "textContent",
- {
- get: function() {
- return innerText.get.call(this);
- },
- set: function(s) {
- return innerText.set.call(this, s);
- }
- }
- );
- })();
+ (function() {
+ var innerText = Object.getOwnPropertyDescriptor(win.Element.prototype, "innerText");
+ Object.defineProperty(win.Element.prototype, "textContent",
+ {
+ get: function() {
+ return innerText.get.call(this);
+ },
+ set: function(s) {
+ return innerText.set.call(this, s);
+ }
+ }
+ );
+ })();
}
// isArray polyfill for ie8
@@ -133,20 +133,36 @@ var wysihtml5 = {
};
}
- // Element.matches Adds ie8 support and unifies nonstandard function names in other browsers
- win.Element && function(ElementPrototype) {
- ElementPrototype.matches = ElementPrototype.matches ||
- ElementPrototype.matchesSelector ||
- ElementPrototype.mozMatchesSelector ||
- ElementPrototype.msMatchesSelector ||
- ElementPrototype.oMatchesSelector ||
- ElementPrototype.webkitMatchesSelector ||
- function (selector) {
- var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
- while (nodes[++i] && nodes[i] != node);
- return !!nodes[i];
+ // closest and matches polyfill
+ // https://github.com/jonathantneal/closest
+ (function (ELEMENT) {
+ ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector || function matches(selector) {
+ var
+ element = this,
+ elements = (element.document || element.ownerDocument).querySelectorAll(selector),
+ index = 0;
+
+ while (elements[index] && elements[index] !== element) {
+ ++index;
+ }
+
+ return elements[index] ? true : false;
};
- }(win.Element.prototype);
+
+ ELEMENT.closest = ELEMENT.closest || function closest(selector) {
+ var element = this;
+
+ while (element) {
+ if (element.matches(selector)) {
+ break;
+ }
+
+ element = element.parentElement;
+ }
+
+ return element;
+ };
+ }(Element.prototype));
// Element.classList for ie8-9 (toggle all IE)
// source http://purl.eligrey.com/github/classList.js/blob/master/classList.js
@@ -4335,7 +4351,8 @@ wysihtml5.polyfills(window, document);
}
return api;
-}, this);;/**
+}, this);
+;/**
* Text range module for Rangy.
* Text-based manipulation and searching of ranges and selections.
*
@@ -7869,6 +7886,11 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
return nodes;
}
+ // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
+ function isBookmark(n) {
+ return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary');
+ }
+
wysihtml5.dom.domNode = function(node) {
var defaultNodeTypes = [wysihtml5.ELEMENT_NODE, wysihtml5.TEXT_NODE];
@@ -7902,6 +7924,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
}
if (
+ isBookmark(prevNode) || // is Rangy temporary boomark element (bypass)
(!wysihtml5.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check.
(options && options.ignoreBlankTexts && wysihtml5.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set
) {
@@ -7921,6 +7944,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
}
if (
+ isBookmark(nextNode) || // is Rangy temporary boomark element (bypass)
(!wysihtml5.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check.
(options && options.ignoreBlankTexts && wysihtml5.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set
) {
@@ -8082,7 +8106,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
}
}
- if (properties.nodeName && node.nodeName !== properties.nodeName) {
+ if (properties.nodeName && node.nodeName.toLowerCase() !== properties.nodeName.toLowerCase()) {
return false;
}
@@ -12532,15 +12556,40 @@ wysihtml5.quirks.ensureProperClearing = (function() {
* Select line where the caret is in
*/
selectLine: function() {
+ var r = rangy.createRange();
if (wysihtml5.browser.supportsSelectionModify()) {
this._selectLine_W3C();
- } else if (this.doc.selection) {
- this._selectLine_MSIE();
- } else {
- // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)
- this._selectLineUniversal();
+ } else if (r.nativeRange && r.nativeRange.getBoundingClientRect) {
+ // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)*/
+ this._selectLineUniversal();
}
},
+
+ includeRangyRangeHelpers: function() {
+ var s = this.getSelection(),
+ r = s.getRangeAt(0),
+ isHelperNode = function(node) {
+ return (node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'));
+ },
+ getNodeLength = function (node) {
+ if (node.nodeType === 1) {
+ return node.childNodes && node.childNodes.length || 0;
+ } else {
+ return node.data && node.data.length || 0;
+ }
+ // body...
+ },
+ anode = s.anchorNode.nodeType === 1 ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
+ fnode = s.focusNode.nodeType === 1 ? s.focusNode.childNodes[s.focusOffset] : s.focusNode;
+
+ if (fnode && s.focusOffset === getNodeLength(fnode) && fnode.nextSibling && isHelperNode(fnode.nextSibling)) {
+ r.setEndAfter(fnode.nextSibling);
+ }
+ if (anode && s.anchorOffset === 0 && anode.previousSibling && isHelperNode(anode.previousSibling)) {
+ r.setStartBefore(anode.previousSibling);
+ }
+ r.select();
+ },
/**
* See https://developer.mozilla.org/en/DOM/Selection/modify
@@ -12559,6 +12608,8 @@ wysihtml5.quirks.ensureProperClearing = (function() {
selection.focusOffset === initialBoundry[3]
) {
this._selectLineUniversal();
+ } else {
+ this.includeRangyRangeHelpers();
}
},
@@ -12610,19 +12661,45 @@ wysihtml5.quirks.ensureProperClearing = (function() {
rect,
startRange, endRange, testRange,
count = 0,
- amount, testRect, found;
+ amount, testRect, found,
+ that = this,
+ isLineBreakingElement = function(el) {
+ return el && el.nodeType === 1 && (that.win.getComputedStyle(el).display === "block" || wysihtml5.lang.array(['BR', 'HR']).contains(el.nodeName));
+ },
+ prevNode = function(node) {
+ var pnode = node;
+ if (pnode) {
+ while (pnode && ((pnode.nodeType === 1 && pnode.classList.contains('rangySelectionBoundary')) || (pnode.nodeType === 3 && (/^\s*$/).test(pnode.data)))) {
+ pnode = pnode.previousSibling;
+ }
+ }
+ return pnode;
+ };
startRange = r.cloneRange();
endRange = r.cloneRange();
if (r.collapsed) {
- r.expand('word', 1);
- rect = r.nativeRange.getBoundingClientRect();
+ // Collapsed state can not have a bounding rect. Thus need to expand it at least by 1 character first while not crossing line boundary
+ // TODO: figure out a shorter and more readable way
+ if (r.startContainer.nodeType === 3 && r.startOffset < r.startContainer.data.length) {
+ r.moveEnd('character', 1);
+ } else if (r.startContainer.nodeType === 1 && r.startContainer.childNodes[r.startOffset] && r.startContainer.childNodes[r.startOffset].nodeType === 3 && r.startContainer.childNodes[r.startOffset].data.length > 0) {
+ r.moveEnd('character', 1);
+ } else if (r.startOffset > 0 && ( r.startContainer.nodeType === 3 || (r.startContainer.nodeType === 1 && !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1]))))) {
+ r.moveStart('character', -1);
+ }
}
-
+ if (!r.collapsed) {
+ r.insertNode(this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE));
+ }
+
+ // Is probably just empty line as can not be expanded
+ rect = r.nativeRange.getBoundingClientRect();
do {
amount = r.moveStart('character', -1);
testRect = r.nativeRange.getBoundingClientRect();
+
if (!testRect || Math.floor(testRect.top) !== Math.floor(rect.top)) {
r.moveStart('character', 1);
found = true;
@@ -12638,61 +12715,24 @@ wysihtml5.quirks.ensureProperClearing = (function() {
testRect = r.nativeRange.getBoundingClientRect();
if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) {
r.moveEnd('character', -1);
+
+ // Fix a IE line end marked by linebreak element although caret is before it
+ // If causes problems should be changed to be applied only to IE
+ if (r.endContainer && r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] && r.endContainer.childNodes[r.endOffset].nodeType === 1 && r.endContainer.childNodes[r.endOffset].nodeName === "BR" && r.endContainer.childNodes[r.endOffset].previousSibling) {
+ if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) {
+ r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length);
+ } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) {
+ r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length);
+ }
+ }
+
found = true;
}
count++;
} while (amount !== 0 && !found && count < 2000);
r.select();
- },
-
- _selectLine_MSIE: function() {
- var range = this.doc.selection && this.doc.selection.createRange ? this.doc.selection.createRange() : this.doc.createRange(),
- rangeTop = range.boundingTop,
- scrollWidth = this.doc.body.scrollWidth,
- rangeBottom,
- rangeEnd,
- measureNode,
- i,
- j;
-
- window.r = range;
-
- if (!range.moveToPoint) {
- return;
- }
-
- if (rangeTop === 0) {
- // Don't know why, but when the selection ends at the end of a line
- // range.boundingTop is 0
- measureNode = this.doc.createElement("span");
- this.insertNode(measureNode);
- rangeTop = measureNode.offsetTop;
- measureNode.parentNode.removeChild(measureNode);
- }
-
- rangeTop += 1;
-
- for (i=-10; i=0; j--) {
- try {
- rangeEnd.moveToPoint(j, rangeBottom);
- break;
- } catch(e2) {}
- }
-
- range.setEndPoint("EndToEnd", rangeEnd);
- range.select();
+ this.includeRangyRangeHelpers();
},
getText: function() {
@@ -13993,18 +14033,56 @@ wysihtml5.Commands = Base.extend(
};
}
+ function getRangeNode(node, offset) {
+ if (node.nodeType === 3) {
+ return node;
+ } else {
+ return node.childNodes[offset] || node;
+ }
+ }
+
+ // Returns if node is a line break
+ function isBr(n) {
+ return n && n.nodeType === 1 && n.nodeName === "BR";
+ }
+
+ // Is block level element
+ function isBlock(n, composer) {
+ return n && n.nodeType === 1 && composer.win.getComputedStyle(n).display === "block";
+ }
+
+ // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
+ function isBookmark(n) {
+ return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary');
+ }
+
+ // Is line breaking node
+ function isLineBreaking(n, composer) {
+ return isBr(n) || isBlock(n, composer);
+ }
+
// Removes empty block level elements
- function cleanup(composer) {
+ function cleanup(composer, newBlockElements) {
+ wysihtml5.dom.removeInvisibleSpaces(composer.element);
var container = composer.element,
allElements = container.querySelectorAll(BLOCK_ELEMENTS),
- uneditables = container.querySelectorAll(composer.config.classNames.uneditableContainer),
- elements = wysihtml5.lang.array(allElements).without(uneditables);
+ noEditQuery = composer.config.classNames.uneditableContainer + ([""]).concat(BLOCK_ELEMENTS.split(',')).join(", " + composer.config.classNames.uneditableContainer + ' '),
+ uneditables = container.querySelectorAll(noEditQuery),
+ elements = wysihtml5.lang.array(allElements).without(uneditables), // Lets not touch uneditable elements and their contents
+ nbIdx;
for (var i = elements.length; i--;) {
if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "") {
+ // If cleanup removes some new block elements. remove them from newblocks array too
+ nbIdx = wysihtml5.lang.array(newBlockElements).indexOf(elements[i]);
+ if (nbIdx > -1) {
+ newBlockElements.splice(nbIdx, 1);
+ }
elements[i].parentNode.removeChild(elements[i]);
}
}
+
+ return newBlockElements;
}
function defaultNodeName(composer) {
@@ -14026,13 +14104,15 @@ wysihtml5.Commands = Base.extend(
return block;
}
+ // Clone for splitting the inner inline element out of its parent inline elements context
+ // For example if selection is in bold and italic, clone the outer nodes and wrap these around content and return
function cloneOuterInlines(node, container) {
var n = node,
innerNode,
parentNode,
el = null,
el2;
-
+
while (n && container && n !== container) {
if (n.nodeType === 1 && n.matches(INLINE_ELEMENTS)) {
parentNode = n;
@@ -14088,7 +14168,10 @@ wysihtml5.Commands = Base.extend(
// Unsets element properties by options
// If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes)
function removeOptionsFromElement(element, options, composer) {
- var style, classes;
+ var style, classes,
+ prevNode = element.previousSibling,
+ nextNode = element.nextSibling,
+ unwrapped = false;
if (options.styleProperty) {
element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = '';
@@ -14106,10 +14189,11 @@ wysihtml5.Commands = Base.extend(
element.removeAttribute('class');
}
- if (options.nodeName && element.nodeName === options.nodeName) {
+ if (options.nodeName && element.nodeName.toLowerCase() === options.nodeName.toLowerCase()) {
style = element.getAttribute('style');
if (!style || style.trim() === '') {
dom.unwrap(element);
+ unwrapped = true;
} else {
element = dom.renameElement(element, defaultNodeName(composer));
}
@@ -14119,60 +14203,79 @@ wysihtml5.Commands = Base.extend(
if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") {
element.removeAttribute('style');
}
+
+ if (unwrapped) {
+ applySurroundingLineBreaks(prevNode, nextNode, composer);
+ }
}
// Unwraps block level elements from inside content
// Useful as not all block level elements can contain other block-levels
function unwrapBlocksFromContent(element) {
- var contentBlocks = element.querySelectorAll(BLOCK_ELEMENTS) || []; // Find unnestable block elements in extracted contents
+ var blocks = element.querySelectorAll(BLOCK_ELEMENTS) || [], // Find unnestable block elements in extracted contents
+ nextEl, prevEl;
- for (var i = contentBlocks.length; i--;) {
- if (!contentBlocks[i].nextSibling || contentBlocks[i].nextSibling.nodeType !== 1 || contentBlocks[i].nextSibling.nodeName !== 'BR') {
- if ((contentBlocks[i].innerHTML || contentBlocks[i].nodeValue || '').trim() !== '') {
- contentBlocks[i].parentNode.insertBefore(contentBlocks[i].ownerDocument.createElement('BR'), contentBlocks[i].nextSibling);
+ for (var i = blocks.length; i--;) {
+ nextEl = wysihtml5.dom.domNode(blocks[i]).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
+ prevEl = wysihtml5.dom.domNode(blocks[i]).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
+
+ if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
+ if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
+ blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
}
}
- wysihtml5.dom.unwrap(contentBlocks[i]);
+ if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
+ if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
+ blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
+ }
+ }
+ wysihtml5.dom.unwrap(blocks[i]);
}
}
// Fix ranges that visually cover whole block element to actually cover the block
function fixRangeCoverage(range, composer) {
- var node;
+ var node,
+ start = range.startContainer,
+ end = range.endContainer;
- if (range.startContainer && range.startContainer.nodeType === 1 && range.startContainer === range.endContainer) {
- if (range.startContainer.firstChild === range.startContainer.lastChild && range.endOffset === 1) {
- if (range.startContainer !== composer.element) {
- range.setStartBefore(range.startContainer);
- range.setEndAfter(range.endContainer);
+ // If range has only one childNode and it is end to end the range, extend the range to contain the container element too
+ // This ensures the wrapper node is modified and optios added to it
+ if (start && start.nodeType === 1 && start === end) {
+ if (start.firstChild === start.lastChild && range.endOffset === 1) {
+ if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
+ range.setStartBefore(start);
+ range.setEndAfter(end);
}
}
return;
}
- if (range.startContainer && range.startContainer.nodeType === 1 && range.endContainer.nodeType === 3) {
- if (range.startContainer.firstChild === range.endContainer && range.endOffset === 1) {
- if (range.startContainer !== composer.element) {
- range.setEndAfter(range.startContainer);
+ // If range starts outside of node and ends inside at textrange and covers the whole node visually, extend end to cover the node end too
+ if (start && start.nodeType === 1 && end.nodeType === 3) {
+ if (start.firstChild === end && range.endOffset === end.data.length) {
+ if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
+ range.setEndAfter(start);
}
}
return;
}
-
- if (range.endContainer && range.endContainer.nodeType === 1 && range.startContainer.nodeType === 3) {
- if (range.endContainer.firstChild === range.startContainer && range.endOffset === 1) {
- if (range.endContainer !== composer.element) {
- range.setStartBefore(range.endContainer);
+
+ // If range ends outside of node and starts inside at textrange and covers the whole node visually, extend start to cover the node start too
+ if (end && end.nodeType === 1 && start.nodeType === 3) {
+ if (end.firstChild === start && range.startOffset === 0) {
+ if (end !== composer.element && end.nodeName !== 'LI' && end.nodeName !== 'TD') {
+ range.setStartBefore(end);
}
}
return;
}
-
- if (range.startContainer && range.startContainer.nodeType === 3 && range.startContainer === range.endContainer && range.startContainer.parentNode) {
- if (range.startContainer.parentNode.firstChild === range.startContainer && range.endOffset == range.endContainer.length && range.startOffset === 0) {
- node = range.startContainer.parentNode;
- if (node !== composer.element) {
+ // If range covers a whole textnode and the textnode is the only child of node, extend range to node
+ if (start && start.nodeType === 3 && start === end && start.parentNode.childNodes.length === 1) {
+ if (range.endOffset == end.data.length && range.startOffset === 0) {
+ node = start.parentNode;
+ if (node !== composer.element && node.nodeName !== 'LI' && node.nodeName !== 'TD') {
range.setStartBefore(node);
range.setEndAfter(node);
}
@@ -14180,108 +14283,285 @@ wysihtml5.Commands = Base.extend(
return;
}
}
+
+ // Scans ranges array for insertion points that are not allowed to insert block tags fixes/splits illegal ranges
+ // Some places do not allow block level elements inbetween (inside ul and outside li)
+ // TODO: might need extending for other nodes besides li (maybe dd,dl,dt)
+ function fixNotPermittedInsertionPoints(ranges) {
+ var newRanges = [],
+ lis, j, maxj, tmpRange, rangePos, closestLI;
+
+ for (var i = 0, maxi = ranges.length; i < maxi; i++) {
+
+ // Fixes range start and end positions if inside UL or OL element (outside of LI)
+ if (ranges[i].startContainer.nodeType === 1 && ranges[i].startContainer.matches('ul, ol')) {
+ ranges[i].setStart(ranges[i].startContainer.childNodes[ranges[i].startOffset], 0);
+ }
+ if (ranges[i].endContainer.nodeType === 1 && ranges[i].endContainer.matches('ul, ol')) {
+ closestLI = ranges[i].endContainer.childNodes[Math.max(ranges[i].endOffset - 1, 0)];
+ if (closestLI.childNodes) {
+ ranges[i].setEnd(closestLI, closestLI.childNodes.length);
+ }
+ }
- // Wrap the range with a block level element
- // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
- function wrapRangeWithElement(range, options, defaultName, composer) {
- var defaultOptions = (options) ? wysihtml5.lang.object(options).clone(true) : null;
- if (defaultOptions) {
- defaultOptions.nodeName = defaultOptions.nodeName || defaultName || defaultNodeName(composer);
+ // Get all LI eleemnts in selection (fully or partially covered)
+ // And make sure ranges are either inside LI or outside UL/OL
+ // Split and add new ranges as needed to cover same range content
+ // TODO: Needs improvement to accept DL, DD, DT
+ lis = ranges[i].getNodes([1], function(node) {
+ return node.nodeName === "LI";
+ });
+ if (lis.length > 0) {
+
+ for (j = 0, maxj = lis.length; j < maxj; j++) {
+ rangePos = ranges[i].compareNode(lis[j]);
+
+ // Fixes start of range that crosses LI border
+ if (rangePos === ranges[i].NODE_AFTER || rangePos === ranges[i].NODE_INSIDE) {
+ // Range starts before and ends inside the node
+
+ tmpRange = ranges[i].cloneRange();
+ closestLI = wysihtml5.dom.domNode(lis[j]).prev({nodeTypes: [1]});
+
+ if (closestLI) {
+ tmpRange.setEnd(closestLI, closestLI.childNodes.length);
+ } else if (lis[j].closest('ul, ol')) {
+ tmpRange.setEndBefore(lis[j].closest('ul, ol'));
+ } else {
+ tmpRange.setEndBefore(lis[j]);
+ }
+ newRanges.push(tmpRange);
+ ranges[i].setStart(lis[j], 0);
+ }
+
+ // Fixes end of range that crosses li border
+ if (rangePos === ranges[i].NODE_BEFORE || rangePos === ranges[i].NODE_INSIDE) {
+ // Range starts inside the node and ends after node
+
+ tmpRange = ranges[i].cloneRange();
+ tmpRange.setEnd(lis[j], lis[j].childNodes.length);
+ newRanges.push(tmpRange);
+
+ // Find next LI in list and if present set range to it, else
+ closestLI = wysihtml5.dom.domNode(lis[j]).next({nodeTypes: [1]});
+ if (closestLI) {
+ ranges[i].setStart(closestLI, 0);
+ } else if (lis[j].closest('ul, ol')) {
+ ranges[i].setStartAfter(lis[j].closest('ul, ol'));
+ } else {
+ ranges[i].setStartAfter(lis[j]);
+ }
+ }
+ }
+ newRanges.push(ranges[i]);
+ } else {
+ newRanges.push(ranges[i]);
+ }
}
- fixRangeCoverage(range, composer);
+ return newRanges;
+ }
+
+ // Return options object with nodeName set if original did not have any
+ // Node name is set to local or global default
+ function getOptionsWithNodename(options, defaultName, composer) {
+ var correctedOptions = (options) ? wysihtml5.lang.object(options).clone(true) : null;
+ if (correctedOptions) {
+ correctedOptions.nodeName = correctedOptions.nodeName || defaultName || defaultNodeName(composer);
+ }
+ return correctedOptions;
+ }
+
+ // Injects document fragment to range ensuring outer elements are split to a place where block elements are allowed to be inserted
+ // Also wraps empty clones of split parent tags around fragment to keep formatting
+ // If firstOuterBlock is given assume that instead of finding outer (useful for solving cases of some blocks are allowed into others while others are not)
+ function injectFragmentToRange(fragment, range, composer, firstOuterBlock) {
+ var rangeStartContainer = range.startContainer,
+ firstOuterBlock = firstOuterBlock || findOuterBlock(rangeStartContainer, composer.element, true),
+ outerInlines, first, last, prev, next;
+
+ if (firstOuterBlock) {
+ // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
+ first = fragment.firstChild;
+ last = fragment.lastChild;
+
+ composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
+
+ next = wysihtml5.dom.domNode(last).next({nodeTypes: [1,3], ignoreBlankTexts: true});
+ prev = wysihtml5.dom.domNode(first).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
+ if (first && !isLineBreaking(first, composer) && prev && !isLineBreaking(prev, composer)) {
+ first.parentNode.insertBefore(composer.doc.createElement('br'), first);
+ }
+
+ if (last && !isLineBreaking(last, composer) && next && !isLineBreaking(next, composer)) {
+ next.parentNode.insertBefore(composer.doc.createElement('br'), next);
+ }
+
+ } else {
+ // Ensure node does not get inserted into an inline where it is not allowed
+ outerInlines = cloneOuterInlines(rangeStartContainer, composer.element);
+ if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) {
+ if (fragment.childNodes.length === 1) {
+ while(fragment.firstChild.firstChild) {
+ outerInlines.innerNode.appendChild(fragment.firstChild.firstChild);
+ }
+ fragment.firstChild.appendChild(outerInlines.outerNode);
+ }
+ composer.selection.splitElementAtCaret(outerInlines.parent, fragment);
+ } else {
+ // Otherwise just insert
+ range.insertNode(fragment);
+ }
+ }
+ }
+
+ // Removes all block formatting from range
+ function clearRangeBlockFromating(range, closestBlockName, composer) {
var r = range.cloneRange(),
+ prevNode = getRangeNode(r.startContainer, r.startOffset).previousSibling,
+ nextNode = getRangeNode(r.endContainer, r.endOffset).nextSibling,
+ content = r.extractContents(),
+ fragment = composer.doc.createDocumentFragment(),
+ children, blocks,
+ first = true;
+
+ while(content.firstChild) {
+ // Iterate over all selection content first level childNodes
+ if (content.firstChild.nodeType === 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
+ // If node is a block element
+ // Split block formating and add new block to wrap caret
+
+ unwrapBlocksFromContent(content.firstChild);
+ children = wysihtml5.dom.unwrap(content.firstChild);
+
+ // Add line break before if needed
+ if (children.length > 0) {
+ if (
+ (fragment.lastChild && (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer))) ||
+ (!fragment.lastChild && prevNode && (prevNode.nodeType !== 1 || isLineBreaking(prevNode, composer)))
+ ){
+ fragment.appendChild(composer.doc.createElement('BR'));
+ }
+ }
+
+ for (var c = 0, cmax = children.length; c < cmax; c++) {
+ fragment.appendChild(children[c]);
+ }
+
+ // Add line break after if needed
+ if (children.length > 0) {
+ if (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer)) {
+ if (nextNode || fragment.lastChild !== content.lastChild) {
+ fragment.appendChild(composer.doc.createElement('BR'));
+ }
+ }
+ }
+
+ } else {
+ fragment.appendChild(content.firstChild);
+ }
+
+ first = false;
+ }
+ blocks = wysihtml5.lang.array(fragment.childNodes).get();
+ injectFragmentToRange(fragment, r, composer);
+ return blocks;
+ }
+
+ // When block node is inserted, look surrounding nodes and remove surplous linebreak tags (as block format breaks line itself)
+ function removeSurroundingLineBreaks(prevNode, nextNode, composer) {
+ var prevPrev = prevNode && wysihtml5.dom.domNode(prevNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
+ if (isBr(nextNode)) {
+ nextNode.parentNode.removeChild(nextNode);
+ }
+ if (isBr(prevNode) && (!prevPrev || prevPrev.nodeType !== 1 || composer.win.getComputedStyle(prevPrev).display !== "block")) {
+ prevNode.parentNode.removeChild(prevNode);
+ }
+ }
+
+ function applySurroundingLineBreaks(prevNode, nextNode, composer) {
+ var prevPrev;
+
+ if (prevNode && isBookmark(prevNode)) {
+ prevNode = prevNode.previousSibling;
+ }
+ if (nextNode && isBookmark(nextNode)) {
+ nextNode = nextNode.nextSibling;
+ }
+
+ prevPrev = prevNode && prevNode.previousSibling;
+
+ if (prevNode && (prevNode.nodeType !== 1 || (composer.win.getComputedStyle(prevNode).display !== "block" && !isBr(prevNode))) && prevNode.parentNode) {
+ prevNode.parentNode.insertBefore(composer.doc.createElement('br'), prevNode.nextSibling);
+ }
+
+ if (nextNode && (nextNode.nodeType !== 1 || composer.win.getComputedStyle(nextNode).display !== "block") && nextNode.parentNode) {
+ nextNode.parentNode.insertBefore(composer.doc.createElement('br'), nextNode);
+ }
+ }
+
+ // Wrap the range with a block level element
+ // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
+ function wrapRangeWithElement(range, options, closestBlockName, composer) {
+ var similarOptions = options ? correctOptionsForSimilarityCheck(options) : null,
+ r = range.cloneRange(),
rangeStartContainer = r.startContainer,
+ prevNode = wysihtml5.dom.domNode(getRangeNode(r.startContainer, r.startOffset)).prev({nodeTypes: [1,3], ignoreBlankTexts: true}),
+ nextNode = wysihtml5.dom.domNode(getRangeNode(r.endContainer, r.endOffset)).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
content = r.extractContents(),
fragment = composer.doc.createDocumentFragment(),
- similarOptions = defaultOptions ? correctOptionsForSimilarityCheck(defaultOptions) : null,
similarOuterBlock = similarOptions ? wysihtml5.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null,
- splitAllBlocks = !defaultOptions || (defaultName === "BLOCKQUOTE" && defaultOptions.nodeName && defaultOptions.nodeName === "BLOCKQUOTE"),
+ splitAllBlocks = !closestBlockName || !options || (options.nodeName === "BLOCKQUOTE" && closestBlockName === "BLOCKQUOTE"),
firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
wrapper, blocks, children;
- if (options && options.nodeName && options.nodeName === "BLOCKQUOTE") {
+ if (options && options.nodeName === "BLOCKQUOTE") {
+
+ // If blockquote is to be inserted no quessing just add it as outermost block on line or selection
var tmpEl = applyOptionsToElement(null, options, composer);
tmpEl.appendChild(content);
fragment.appendChild(tmpEl);
blocks = [tmpEl];
+
} else {
if (!content.firstChild) {
+ // IF selection is caret (can happen if line is empty) add format around tag
fragment.appendChild(applyOptionsToElement(null, options, composer));
} else {
while(content.firstChild) {
+ // Iterate over all selection content first level childNodes
if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
- if (options) {
- // Escape(split) block formatting at caret
- applyOptionsToElement(content.firstChild, options, composer);
- if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
- unwrapBlocksFromContent(content.firstChild);
- }
- fragment.appendChild(content.firstChild);
-
- } else {
- // Split block formating and add new block to wrap caret
+ // If node is a block element
+ // Escape(split) block formatting at caret
+ applyOptionsToElement(content.firstChild, options, composer);
+ if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
unwrapBlocksFromContent(content.firstChild);
- children = wysihtml5.dom.unwrap(content.firstChild);
- for (var c = 0, cmax = children.length; c < cmax; c++) {
- fragment.appendChild(children[c]);
- }
-
- if (fragment.childNodes.length > 0) {
- fragment.appendChild(composer.doc.createElement('BR'));
- }
}
+ fragment.appendChild(content.firstChild);
+
} else {
-
- if (options) {
- // Wrap subsequent non-block nodes inside new block element
- wrapper = applyOptionsToElement(null, defaultOptions, composer);
- while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
- if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
- unwrapBlocksFromContent(content.firstChild);
- }
- wrapper.appendChild(content.firstChild);
- }
- fragment.appendChild(wrapper);
- } else {
- // Escape(split) block formatting at selection
- if (content.firstChild.nodeType == 1) {
+ // Wrap subsequent non-block nodes inside new block element
+ wrapper = applyOptionsToElement(null, getOptionsWithNodename(options, closestBlockName, composer), composer);
+ while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
+ if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
unwrapBlocksFromContent(content.firstChild);
}
- fragment.appendChild(content.firstChild);
+ wrapper.appendChild(content.firstChild);
}
-
+ fragment.appendChild(wrapper);
}
}
}
blocks = wysihtml5.lang.array(fragment.childNodes).get();
}
- if (firstOuterBlock) {
- // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
- composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
- } else {
- // Ensure node does not get inserted into an inline where it is not allowed
- var outerInlines = cloneOuterInlines(rangeStartContainer, composer.element);
- if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) {
- if (fragment.childNodes.length === 1) {
- while(fragment.firstChild.firstChild) {
- outerInlines.innerNode.appendChild(fragment.firstChild.firstChild);
- }
- fragment.firstChild.appendChild(outerInlines.outerNode);
- }
- composer.selection.splitElementAtCaret(outerInlines.parent, fragment);
- } else {
- // Otherwise just insert
- r.insertNode(fragment);
- }
- }
-
+ injectFragmentToRange(fragment, r, composer, firstOuterBlock);
+ removeSurroundingLineBreaks(prevNode, nextNode, composer);
return blocks;
}
@@ -14293,101 +14573,154 @@ wysihtml5.Commands = Base.extend(
return (parentNode) ? parentNode.nodeName : null;
}
+
+ // Expands caret to cover the closest block that:
+ // * cannot contain other block level elements (h1-6,p, etc)
+ // * Has the same nodeName that is to be inserted
+ // * has insertingNodeName
+ // * is DIV if insertingNodeName is not present
+ //
+ // If nothing found selects the current line
+ function expandCaretToBlock(composer, insertingNodeName) {
+ var parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
+ query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (insertingNodeName ? insertingNodeName.toLowerCase() : 'div'),
+ }, null, composer.element),
+ range;
+
+ if (parent) {
+ range = composer.selection.createRange();
+ range.selectNode(parent);
+ composer.selection.setSelection(range);
+ } else if (!composer.isEmpty()) {
+ composer.selection.selectLine();
+ }
+ }
+
+ // Set selection to begin inside first created block element (beginning of it) and end inside (and after content) of last block element
+ // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway
+ function selectElements(newBlockElements, composer) {
+ var range = composer.selection.createRange(),
+ lastEl = newBlockElements[newBlockElements.length - 1],
+ lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 : lastEl.length || 0;
+
+ range.setStart(newBlockElements[0], 0);
+ range.setEnd(lastEl, lastOffset);
+ range.select();
+ }
+
+ // Get all ranges from selection (takes out uneditables and out of editor parts) and apply format to each
+ // Return created/modified block level elements
+ // Method can be either "apply" or "remove"
+ function formatSelection(method, composer, options) {
+ var ranges = composer.selection.getOwnRanges(),
+ newBlockElements = [],
+ closestBlockName;
+
+ // Some places do not allow block level elements inbetween (inside ul and outside li, inside table and outside of td/th)
+ ranges = fixNotPermittedInsertionPoints(ranges);
+
+ for (var i = ranges.length; i--;) {
+ fixRangeCoverage(ranges[i], composer);
+ closestBlockName = getParentBlockNodeName(ranges[i].startContainer, composer);
+ if (method === "remove") {
+ newBlockElements = newBlockElements.concat(clearRangeBlockFromating(ranges[i], closestBlockName, composer));
+ } else {
+ newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, closestBlockName, composer));
+ }
+ }
+ return newBlockElements;
+ }
+
+ // If properties is passed as a string, look for tag with that tagName/query
+ function parseOptions(options) {
+ if (typeof options === "string") {
+ options = {
+ nodeName: options.toUpperCase()
+ };
+ }
+ return options;
+ }
wysihtml5.commands.formatBlock = {
exec: function(composer, command, options) {
+ options = parseOptions(options);
var newBlockElements = [],
- placeholder, ranges, range, parent, bookmark, state;
-
- // If properties is passed as a string, look for tag with that tagName/query
- if (typeof options === "string") {
- options = {
- nodeName: options.toUpperCase()
- };
- }
+ ranges, range, bookmark, state, closestBlockName;
- // Remove state if toggle set and state on and selection is collapsed
+ // Find if current format state is active if options.toggle is set as true
+ // In toggle case active state elemets are formatted instead of working directly on selection
if (options && options.toggle) {
state = this.state(composer, command, options);
- if (state) {
- bookmark = rangy.saveSelection(composer.win);
- for (var j = 0, jmax = state.length; j < jmax; j++) {
- removeOptionsFromElement(state[j], options, composer);
- }
- }
}
+ if (state) {
+ // Remove format from state nodes if toggle set and state on and selection is collapsed
+ bookmark = rangy.saveSelection(composer.win);
+ for (var j = 0, jmax = state.length; j < jmax; j++) {
+ removeOptionsFromElement(state[j], options, composer);
+ }
- // Otherwise expand selection so it will cover closest block if option caretSelectsBlock is true and selection is collapsed
- if (!state) {
-
+ } else {
+ // If selection is caret expand it to cover nearest suitable block element or row if none found
if (composer.selection.isCollapsed()) {
- parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
- query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (options && options.nodeName ? options.nodeName.toLowerCase() : 'div'),
- }, null, composer.element);
- if (parent) {
- bookmark = rangy.saveSelection(composer.win);
- range = composer.selection.createRange();
- range.selectNode(parent);
- composer.selection.setSelection(range);
- } else if (!composer.isEmpty()) {
- bookmark = rangy.saveSelection(composer.win);
- composer.selection.selectLine();
- }
+ bookmark = rangy.saveSelection(composer.win);
+ expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
}
-
- // And get all selection ranges of current composer and iterate
- ranges = composer.selection.getOwnRanges();
- for (var i = ranges.length; i--;) {
- newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, getParentBlockNodeName(ranges[i].startContainer, composer), composer));
+ if (options) {
+ newBlockElements = formatSelection("apply", composer, options);
+ } else {
+ // Options == null means block formatting should be removed from selection
+ newBlockElements = formatSelection("remove", composer);
}
-
+
}
// Remove empty block elements that may be left behind
- cleanup(composer);
- // If cleanup removed some new block elements. remove them from array too
- for (var e = newBlockElements.length; e--;) {
- if (!newBlockElements[e].parentNode) {
- newBlockElements.splice(e, 1);
- }
- }
+ // Also remove them from new blocks list
+ newBlockElements = cleanup(composer, newBlockElements);
- // Restore correct selection
+ // Restore selection
if (bookmark) {
- wysihtml5.dom.removeInvisibleSpaces(composer.element);
rangy.restoreSelection(bookmark);
} else {
- wysihtml5.dom.removeInvisibleSpaces(composer.element);
- // Set selection to beging inside first created block element (beginning of it) and end inside (and after content) of last block element
- // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway
- range = composer.selection.createRange();
- range.setStart(newBlockElements[0], 0);
- var lastEl = newBlockElements[newBlockElements.length - 1],
- lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 : lastEl.length || 0;
- range.setEnd(lastEl, lastOffset);
- range.select();
+ selectElements(newBlockElements, composer);
}
},
-
- // If properties as null is passed returns status describing all block level elements
- state: function(composer, command, properties) {
+
+ // Removes all block formatting from selection
+ remove: function(composer, command, options) {
+ options = parseOptions(options);
+ var newBlockElements, bookmark;
- // If properties is passed as a string, look for tag with that tagName/query
- if (typeof properties === "string") {
- properties = {
- query: properties
- };
+ // If selection is caret expand it to cover nearest suitable block element or row if none found
+ if (composer.selection.isCollapsed()) {
+ bookmark = rangy.saveSelection(composer.win);
+ expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
}
+
+ newBlockElements = formatSelection("remove", composer);
+ newBlockElements = cleanup(composer, newBlockElements);
+
+ // Restore selection
+ if (bookmark) {
+ rangy.restoreSelection(bookmark);
+ } else {
+ selectElements(newBlockElements, composer);
+ }
+ },
+
+ // If options as null is passed returns status describing all block level elements
+ state: function(composer, command, options) {
+ options = parseOptions(options);
var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection
- return wysihtml5.dom.domNode(element).test(properties || { query: BLOCK_ELEMENTS });
+ return wysihtml5.dom.domNode(element).test(options || { query: BLOCK_ELEMENTS });
}).bind(this)),
parentNodes = composer.selection.getSelectedOwnNodes(),
parent;
// Finds matching elements that are parents of selection and adds to nodes list
for (var i = 0, maxi = parentNodes.length; i < maxi; i++) {
- parent = dom.getParentElement(parentNodes[i], properties || { query: BLOCK_ELEMENTS }, null, composer.element);
+ parent = dom.getParentElement(parentNodes[i], options || { query: BLOCK_ELEMENTS }, null, composer.element);
if (parent && nodes.indexOf(parent) === -1) {
nodes.push(parent);
}
@@ -14597,6 +14930,9 @@ wysihtml5.Commands = Base.extend(
if (options.toggle !== false && element.classList.contains(options.className)) {
element.classList.remove(options.className);
} else {
+ if (options.classRegExp) {
+ element.className = element.className.replace(options.classRegExp, '');
+ }
element.classList.add(options.className);
}
if (hasNoClass(element)) {
@@ -16247,7 +16583,7 @@ wysihtml5.views.View = Base.extend(
cleanUp: function(rules) {
var bookmark;
- if (this.selection) {
+ if (this.selection && this.selection.isInThisEditable()) {
bookmark = rangy.saveSelection(this.win);
}
this.parent.parse(this.element, undefined, rules);
@@ -16419,6 +16755,8 @@ wysihtml5.views.View = Base.extend(
]).from(this.textarea.element).to(this.element);
}
+ this._initAutoLinking();
+
dom.addClass(this.element, this.config.classNames.composer);
//
// Make the editor look like the original textarea, by syncing styles
@@ -16451,7 +16789,6 @@ wysihtml5.views.View = Base.extend(
// Make sure that the browser avoids using inline styles whenever possible
this.commands.exec("styleWithCSS", false);
- this._initAutoLinking();
this._initObjectResizing();
this._initUndoManager();
this._initLineBreaking();
@@ -16485,10 +16822,7 @@ wysihtml5.views.View = Base.extend(
supportsAutoLinking = browser.doesAutoLinkingInContentEditable();
if (supportsDisablingOfAutoLinking) {
- // I have no idea why IE edge deletes element content here when calling the command,
- var tmpHTML = this.element.innerHTML;
this.commands.exec("AutoUrlDetect", false, false);
- this.element.innerHTML = tmpHTML;
}
if (!this.config.autoLink) {
diff --git a/vendor/assets/javascripts/wysihtml.js b/vendor/assets/javascripts/wysihtml.js
index b14e8c5..b7a5ec0 100644
--- a/vendor/assets/javascripts/wysihtml.js
+++ b/vendor/assets/javascripts/wysihtml.js
@@ -1,5 +1,5 @@
/**
- * @license wysihtml v0.5.1
+ * @license wysihtml v0.5.2
* https://github.com/Voog/wysihtml
*
* Author: Christopher Blum (https://github.com/tiff)
@@ -10,7 +10,7 @@
*
*/
var wysihtml5 = {
- version: "0.5.1",
+ version: "0.5.2",
// namespaces
commands: {},
@@ -76,19 +76,19 @@ var wysihtml5 = {
// element.textContent polyfill.
if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(win.Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(win.Element.prototype, "textContent").get) {
- (function() {
- var innerText = Object.getOwnPropertyDescriptor(win.Element.prototype, "innerText");
- Object.defineProperty(win.Element.prototype, "textContent",
- {
- get: function() {
- return innerText.get.call(this);
- },
- set: function(s) {
- return innerText.set.call(this, s);
- }
- }
- );
- })();
+ (function() {
+ var innerText = Object.getOwnPropertyDescriptor(win.Element.prototype, "innerText");
+ Object.defineProperty(win.Element.prototype, "textContent",
+ {
+ get: function() {
+ return innerText.get.call(this);
+ },
+ set: function(s) {
+ return innerText.set.call(this, s);
+ }
+ }
+ );
+ })();
}
// isArray polyfill for ie8
@@ -133,20 +133,36 @@ var wysihtml5 = {
};
}
- // Element.matches Adds ie8 support and unifies nonstandard function names in other browsers
- win.Element && function(ElementPrototype) {
- ElementPrototype.matches = ElementPrototype.matches ||
- ElementPrototype.matchesSelector ||
- ElementPrototype.mozMatchesSelector ||
- ElementPrototype.msMatchesSelector ||
- ElementPrototype.oMatchesSelector ||
- ElementPrototype.webkitMatchesSelector ||
- function (selector) {
- var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
- while (nodes[++i] && nodes[i] != node);
- return !!nodes[i];
+ // closest and matches polyfill
+ // https://github.com/jonathantneal/closest
+ (function (ELEMENT) {
+ ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector || function matches(selector) {
+ var
+ element = this,
+ elements = (element.document || element.ownerDocument).querySelectorAll(selector),
+ index = 0;
+
+ while (elements[index] && elements[index] !== element) {
+ ++index;
+ }
+
+ return elements[index] ? true : false;
};
- }(win.Element.prototype);
+
+ ELEMENT.closest = ELEMENT.closest || function closest(selector) {
+ var element = this;
+
+ while (element) {
+ if (element.matches(selector)) {
+ break;
+ }
+
+ element = element.parentElement;
+ }
+
+ return element;
+ };
+ }(Element.prototype));
// Element.classList for ie8-9 (toggle all IE)
// source http://purl.eligrey.com/github/classList.js/blob/master/classList.js
@@ -4335,7 +4351,8 @@ wysihtml5.polyfills(window, document);
}
return api;
-}, this);;/**
+}, this);
+;/**
* Text range module for Rangy.
* Text-based manipulation and searching of ranges and selections.
*
@@ -7869,6 +7886,11 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
return nodes;
}
+ // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
+ function isBookmark(n) {
+ return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary');
+ }
+
wysihtml5.dom.domNode = function(node) {
var defaultNodeTypes = [wysihtml5.ELEMENT_NODE, wysihtml5.TEXT_NODE];
@@ -7902,6 +7924,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
}
if (
+ isBookmark(prevNode) || // is Rangy temporary boomark element (bypass)
(!wysihtml5.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check.
(options && options.ignoreBlankTexts && wysihtml5.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set
) {
@@ -7921,6 +7944,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
}
if (
+ isBookmark(nextNode) || // is Rangy temporary boomark element (bypass)
(!wysihtml5.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check.
(options && options.ignoreBlankTexts && wysihtml5.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set
) {
@@ -8082,7 +8106,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) {
}
}
- if (properties.nodeName && node.nodeName !== properties.nodeName) {
+ if (properties.nodeName && node.nodeName.toLowerCase() !== properties.nodeName.toLowerCase()) {
return false;
}
@@ -12532,15 +12556,40 @@ wysihtml5.quirks.ensureProperClearing = (function() {
* Select line where the caret is in
*/
selectLine: function() {
+ var r = rangy.createRange();
if (wysihtml5.browser.supportsSelectionModify()) {
this._selectLine_W3C();
- } else if (this.doc.selection) {
- this._selectLine_MSIE();
- } else {
- // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)
- this._selectLineUniversal();
+ } else if (r.nativeRange && r.nativeRange.getBoundingClientRect) {
+ // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)*/
+ this._selectLineUniversal();
}
},
+
+ includeRangyRangeHelpers: function() {
+ var s = this.getSelection(),
+ r = s.getRangeAt(0),
+ isHelperNode = function(node) {
+ return (node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'));
+ },
+ getNodeLength = function (node) {
+ if (node.nodeType === 1) {
+ return node.childNodes && node.childNodes.length || 0;
+ } else {
+ return node.data && node.data.length || 0;
+ }
+ // body...
+ },
+ anode = s.anchorNode.nodeType === 1 ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
+ fnode = s.focusNode.nodeType === 1 ? s.focusNode.childNodes[s.focusOffset] : s.focusNode;
+
+ if (fnode && s.focusOffset === getNodeLength(fnode) && fnode.nextSibling && isHelperNode(fnode.nextSibling)) {
+ r.setEndAfter(fnode.nextSibling);
+ }
+ if (anode && s.anchorOffset === 0 && anode.previousSibling && isHelperNode(anode.previousSibling)) {
+ r.setStartBefore(anode.previousSibling);
+ }
+ r.select();
+ },
/**
* See https://developer.mozilla.org/en/DOM/Selection/modify
@@ -12559,6 +12608,8 @@ wysihtml5.quirks.ensureProperClearing = (function() {
selection.focusOffset === initialBoundry[3]
) {
this._selectLineUniversal();
+ } else {
+ this.includeRangyRangeHelpers();
}
},
@@ -12610,19 +12661,45 @@ wysihtml5.quirks.ensureProperClearing = (function() {
rect,
startRange, endRange, testRange,
count = 0,
- amount, testRect, found;
+ amount, testRect, found,
+ that = this,
+ isLineBreakingElement = function(el) {
+ return el && el.nodeType === 1 && (that.win.getComputedStyle(el).display === "block" || wysihtml5.lang.array(['BR', 'HR']).contains(el.nodeName));
+ },
+ prevNode = function(node) {
+ var pnode = node;
+ if (pnode) {
+ while (pnode && ((pnode.nodeType === 1 && pnode.classList.contains('rangySelectionBoundary')) || (pnode.nodeType === 3 && (/^\s*$/).test(pnode.data)))) {
+ pnode = pnode.previousSibling;
+ }
+ }
+ return pnode;
+ };
startRange = r.cloneRange();
endRange = r.cloneRange();
if (r.collapsed) {
- r.expand('word', 1);
- rect = r.nativeRange.getBoundingClientRect();
+ // Collapsed state can not have a bounding rect. Thus need to expand it at least by 1 character first while not crossing line boundary
+ // TODO: figure out a shorter and more readable way
+ if (r.startContainer.nodeType === 3 && r.startOffset < r.startContainer.data.length) {
+ r.moveEnd('character', 1);
+ } else if (r.startContainer.nodeType === 1 && r.startContainer.childNodes[r.startOffset] && r.startContainer.childNodes[r.startOffset].nodeType === 3 && r.startContainer.childNodes[r.startOffset].data.length > 0) {
+ r.moveEnd('character', 1);
+ } else if (r.startOffset > 0 && ( r.startContainer.nodeType === 3 || (r.startContainer.nodeType === 1 && !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1]))))) {
+ r.moveStart('character', -1);
+ }
}
-
+ if (!r.collapsed) {
+ r.insertNode(this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE));
+ }
+
+ // Is probably just empty line as can not be expanded
+ rect = r.nativeRange.getBoundingClientRect();
do {
amount = r.moveStart('character', -1);
testRect = r.nativeRange.getBoundingClientRect();
+
if (!testRect || Math.floor(testRect.top) !== Math.floor(rect.top)) {
r.moveStart('character', 1);
found = true;
@@ -12638,61 +12715,24 @@ wysihtml5.quirks.ensureProperClearing = (function() {
testRect = r.nativeRange.getBoundingClientRect();
if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) {
r.moveEnd('character', -1);
+
+ // Fix a IE line end marked by linebreak element although caret is before it
+ // If causes problems should be changed to be applied only to IE
+ if (r.endContainer && r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] && r.endContainer.childNodes[r.endOffset].nodeType === 1 && r.endContainer.childNodes[r.endOffset].nodeName === "BR" && r.endContainer.childNodes[r.endOffset].previousSibling) {
+ if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) {
+ r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length);
+ } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) {
+ r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length);
+ }
+ }
+
found = true;
}
count++;
} while (amount !== 0 && !found && count < 2000);
r.select();
- },
-
- _selectLine_MSIE: function() {
- var range = this.doc.selection && this.doc.selection.createRange ? this.doc.selection.createRange() : this.doc.createRange(),
- rangeTop = range.boundingTop,
- scrollWidth = this.doc.body.scrollWidth,
- rangeBottom,
- rangeEnd,
- measureNode,
- i,
- j;
-
- window.r = range;
-
- if (!range.moveToPoint) {
- return;
- }
-
- if (rangeTop === 0) {
- // Don't know why, but when the selection ends at the end of a line
- // range.boundingTop is 0
- measureNode = this.doc.createElement("span");
- this.insertNode(measureNode);
- rangeTop = measureNode.offsetTop;
- measureNode.parentNode.removeChild(measureNode);
- }
-
- rangeTop += 1;
-
- for (i=-10; i=0; j--) {
- try {
- rangeEnd.moveToPoint(j, rangeBottom);
- break;
- } catch(e2) {}
- }
-
- range.setEndPoint("EndToEnd", rangeEnd);
- range.select();
+ this.includeRangyRangeHelpers();
},
getText: function() {
@@ -13993,18 +14033,56 @@ wysihtml5.Commands = Base.extend(
};
}
+ function getRangeNode(node, offset) {
+ if (node.nodeType === 3) {
+ return node;
+ } else {
+ return node.childNodes[offset] || node;
+ }
+ }
+
+ // Returns if node is a line break
+ function isBr(n) {
+ return n && n.nodeType === 1 && n.nodeName === "BR";
+ }
+
+ // Is block level element
+ function isBlock(n, composer) {
+ return n && n.nodeType === 1 && composer.win.getComputedStyle(n).display === "block";
+ }
+
+ // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
+ function isBookmark(n) {
+ return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary');
+ }
+
+ // Is line breaking node
+ function isLineBreaking(n, composer) {
+ return isBr(n) || isBlock(n, composer);
+ }
+
// Removes empty block level elements
- function cleanup(composer) {
+ function cleanup(composer, newBlockElements) {
+ wysihtml5.dom.removeInvisibleSpaces(composer.element);
var container = composer.element,
allElements = container.querySelectorAll(BLOCK_ELEMENTS),
- uneditables = container.querySelectorAll(composer.config.classNames.uneditableContainer),
- elements = wysihtml5.lang.array(allElements).without(uneditables);
+ noEditQuery = composer.config.classNames.uneditableContainer + ([""]).concat(BLOCK_ELEMENTS.split(',')).join(", " + composer.config.classNames.uneditableContainer + ' '),
+ uneditables = container.querySelectorAll(noEditQuery),
+ elements = wysihtml5.lang.array(allElements).without(uneditables), // Lets not touch uneditable elements and their contents
+ nbIdx;
for (var i = elements.length; i--;) {
if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "") {
+ // If cleanup removes some new block elements. remove them from newblocks array too
+ nbIdx = wysihtml5.lang.array(newBlockElements).indexOf(elements[i]);
+ if (nbIdx > -1) {
+ newBlockElements.splice(nbIdx, 1);
+ }
elements[i].parentNode.removeChild(elements[i]);
}
}
+
+ return newBlockElements;
}
function defaultNodeName(composer) {
@@ -14026,13 +14104,15 @@ wysihtml5.Commands = Base.extend(
return block;
}
+ // Clone for splitting the inner inline element out of its parent inline elements context
+ // For example if selection is in bold and italic, clone the outer nodes and wrap these around content and return
function cloneOuterInlines(node, container) {
var n = node,
innerNode,
parentNode,
el = null,
el2;
-
+
while (n && container && n !== container) {
if (n.nodeType === 1 && n.matches(INLINE_ELEMENTS)) {
parentNode = n;
@@ -14088,7 +14168,10 @@ wysihtml5.Commands = Base.extend(
// Unsets element properties by options
// If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes)
function removeOptionsFromElement(element, options, composer) {
- var style, classes;
+ var style, classes,
+ prevNode = element.previousSibling,
+ nextNode = element.nextSibling,
+ unwrapped = false;
if (options.styleProperty) {
element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = '';
@@ -14106,10 +14189,11 @@ wysihtml5.Commands = Base.extend(
element.removeAttribute('class');
}
- if (options.nodeName && element.nodeName === options.nodeName) {
+ if (options.nodeName && element.nodeName.toLowerCase() === options.nodeName.toLowerCase()) {
style = element.getAttribute('style');
if (!style || style.trim() === '') {
dom.unwrap(element);
+ unwrapped = true;
} else {
element = dom.renameElement(element, defaultNodeName(composer));
}
@@ -14119,60 +14203,79 @@ wysihtml5.Commands = Base.extend(
if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") {
element.removeAttribute('style');
}
+
+ if (unwrapped) {
+ applySurroundingLineBreaks(prevNode, nextNode, composer);
+ }
}
// Unwraps block level elements from inside content
// Useful as not all block level elements can contain other block-levels
function unwrapBlocksFromContent(element) {
- var contentBlocks = element.querySelectorAll(BLOCK_ELEMENTS) || []; // Find unnestable block elements in extracted contents
+ var blocks = element.querySelectorAll(BLOCK_ELEMENTS) || [], // Find unnestable block elements in extracted contents
+ nextEl, prevEl;
- for (var i = contentBlocks.length; i--;) {
- if (!contentBlocks[i].nextSibling || contentBlocks[i].nextSibling.nodeType !== 1 || contentBlocks[i].nextSibling.nodeName !== 'BR') {
- if ((contentBlocks[i].innerHTML || contentBlocks[i].nodeValue || '').trim() !== '') {
- contentBlocks[i].parentNode.insertBefore(contentBlocks[i].ownerDocument.createElement('BR'), contentBlocks[i].nextSibling);
+ for (var i = blocks.length; i--;) {
+ nextEl = wysihtml5.dom.domNode(blocks[i]).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
+ prevEl = wysihtml5.dom.domNode(blocks[i]).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
+
+ if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
+ if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
+ blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
}
}
- wysihtml5.dom.unwrap(contentBlocks[i]);
+ if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
+ if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
+ blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
+ }
+ }
+ wysihtml5.dom.unwrap(blocks[i]);
}
}
// Fix ranges that visually cover whole block element to actually cover the block
function fixRangeCoverage(range, composer) {
- var node;
+ var node,
+ start = range.startContainer,
+ end = range.endContainer;
- if (range.startContainer && range.startContainer.nodeType === 1 && range.startContainer === range.endContainer) {
- if (range.startContainer.firstChild === range.startContainer.lastChild && range.endOffset === 1) {
- if (range.startContainer !== composer.element) {
- range.setStartBefore(range.startContainer);
- range.setEndAfter(range.endContainer);
+ // If range has only one childNode and it is end to end the range, extend the range to contain the container element too
+ // This ensures the wrapper node is modified and optios added to it
+ if (start && start.nodeType === 1 && start === end) {
+ if (start.firstChild === start.lastChild && range.endOffset === 1) {
+ if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
+ range.setStartBefore(start);
+ range.setEndAfter(end);
}
}
return;
}
- if (range.startContainer && range.startContainer.nodeType === 1 && range.endContainer.nodeType === 3) {
- if (range.startContainer.firstChild === range.endContainer && range.endOffset === 1) {
- if (range.startContainer !== composer.element) {
- range.setEndAfter(range.startContainer);
+ // If range starts outside of node and ends inside at textrange and covers the whole node visually, extend end to cover the node end too
+ if (start && start.nodeType === 1 && end.nodeType === 3) {
+ if (start.firstChild === end && range.endOffset === end.data.length) {
+ if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
+ range.setEndAfter(start);
}
}
return;
}
-
- if (range.endContainer && range.endContainer.nodeType === 1 && range.startContainer.nodeType === 3) {
- if (range.endContainer.firstChild === range.startContainer && range.endOffset === 1) {
- if (range.endContainer !== composer.element) {
- range.setStartBefore(range.endContainer);
+
+ // If range ends outside of node and starts inside at textrange and covers the whole node visually, extend start to cover the node start too
+ if (end && end.nodeType === 1 && start.nodeType === 3) {
+ if (end.firstChild === start && range.startOffset === 0) {
+ if (end !== composer.element && end.nodeName !== 'LI' && end.nodeName !== 'TD') {
+ range.setStartBefore(end);
}
}
return;
}
-
- if (range.startContainer && range.startContainer.nodeType === 3 && range.startContainer === range.endContainer && range.startContainer.parentNode) {
- if (range.startContainer.parentNode.firstChild === range.startContainer && range.endOffset == range.endContainer.length && range.startOffset === 0) {
- node = range.startContainer.parentNode;
- if (node !== composer.element) {
+ // If range covers a whole textnode and the textnode is the only child of node, extend range to node
+ if (start && start.nodeType === 3 && start === end && start.parentNode.childNodes.length === 1) {
+ if (range.endOffset == end.data.length && range.startOffset === 0) {
+ node = start.parentNode;
+ if (node !== composer.element && node.nodeName !== 'LI' && node.nodeName !== 'TD') {
range.setStartBefore(node);
range.setEndAfter(node);
}
@@ -14180,108 +14283,285 @@ wysihtml5.Commands = Base.extend(
return;
}
}
+
+ // Scans ranges array for insertion points that are not allowed to insert block tags fixes/splits illegal ranges
+ // Some places do not allow block level elements inbetween (inside ul and outside li)
+ // TODO: might need extending for other nodes besides li (maybe dd,dl,dt)
+ function fixNotPermittedInsertionPoints(ranges) {
+ var newRanges = [],
+ lis, j, maxj, tmpRange, rangePos, closestLI;
+
+ for (var i = 0, maxi = ranges.length; i < maxi; i++) {
+
+ // Fixes range start and end positions if inside UL or OL element (outside of LI)
+ if (ranges[i].startContainer.nodeType === 1 && ranges[i].startContainer.matches('ul, ol')) {
+ ranges[i].setStart(ranges[i].startContainer.childNodes[ranges[i].startOffset], 0);
+ }
+ if (ranges[i].endContainer.nodeType === 1 && ranges[i].endContainer.matches('ul, ol')) {
+ closestLI = ranges[i].endContainer.childNodes[Math.max(ranges[i].endOffset - 1, 0)];
+ if (closestLI.childNodes) {
+ ranges[i].setEnd(closestLI, closestLI.childNodes.length);
+ }
+ }
- // Wrap the range with a block level element
- // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
- function wrapRangeWithElement(range, options, defaultName, composer) {
- var defaultOptions = (options) ? wysihtml5.lang.object(options).clone(true) : null;
- if (defaultOptions) {
- defaultOptions.nodeName = defaultOptions.nodeName || defaultName || defaultNodeName(composer);
+ // Get all LI eleemnts in selection (fully or partially covered)
+ // And make sure ranges are either inside LI or outside UL/OL
+ // Split and add new ranges as needed to cover same range content
+ // TODO: Needs improvement to accept DL, DD, DT
+ lis = ranges[i].getNodes([1], function(node) {
+ return node.nodeName === "LI";
+ });
+ if (lis.length > 0) {
+
+ for (j = 0, maxj = lis.length; j < maxj; j++) {
+ rangePos = ranges[i].compareNode(lis[j]);
+
+ // Fixes start of range that crosses LI border
+ if (rangePos === ranges[i].NODE_AFTER || rangePos === ranges[i].NODE_INSIDE) {
+ // Range starts before and ends inside the node
+
+ tmpRange = ranges[i].cloneRange();
+ closestLI = wysihtml5.dom.domNode(lis[j]).prev({nodeTypes: [1]});
+
+ if (closestLI) {
+ tmpRange.setEnd(closestLI, closestLI.childNodes.length);
+ } else if (lis[j].closest('ul, ol')) {
+ tmpRange.setEndBefore(lis[j].closest('ul, ol'));
+ } else {
+ tmpRange.setEndBefore(lis[j]);
+ }
+ newRanges.push(tmpRange);
+ ranges[i].setStart(lis[j], 0);
+ }
+
+ // Fixes end of range that crosses li border
+ if (rangePos === ranges[i].NODE_BEFORE || rangePos === ranges[i].NODE_INSIDE) {
+ // Range starts inside the node and ends after node
+
+ tmpRange = ranges[i].cloneRange();
+ tmpRange.setEnd(lis[j], lis[j].childNodes.length);
+ newRanges.push(tmpRange);
+
+ // Find next LI in list and if present set range to it, else
+ closestLI = wysihtml5.dom.domNode(lis[j]).next({nodeTypes: [1]});
+ if (closestLI) {
+ ranges[i].setStart(closestLI, 0);
+ } else if (lis[j].closest('ul, ol')) {
+ ranges[i].setStartAfter(lis[j].closest('ul, ol'));
+ } else {
+ ranges[i].setStartAfter(lis[j]);
+ }
+ }
+ }
+ newRanges.push(ranges[i]);
+ } else {
+ newRanges.push(ranges[i]);
+ }
}
- fixRangeCoverage(range, composer);
+ return newRanges;
+ }
+
+ // Return options object with nodeName set if original did not have any
+ // Node name is set to local or global default
+ function getOptionsWithNodename(options, defaultName, composer) {
+ var correctedOptions = (options) ? wysihtml5.lang.object(options).clone(true) : null;
+ if (correctedOptions) {
+ correctedOptions.nodeName = correctedOptions.nodeName || defaultName || defaultNodeName(composer);
+ }
+ return correctedOptions;
+ }
+
+ // Injects document fragment to range ensuring outer elements are split to a place where block elements are allowed to be inserted
+ // Also wraps empty clones of split parent tags around fragment to keep formatting
+ // If firstOuterBlock is given assume that instead of finding outer (useful for solving cases of some blocks are allowed into others while others are not)
+ function injectFragmentToRange(fragment, range, composer, firstOuterBlock) {
+ var rangeStartContainer = range.startContainer,
+ firstOuterBlock = firstOuterBlock || findOuterBlock(rangeStartContainer, composer.element, true),
+ outerInlines, first, last, prev, next;
+
+ if (firstOuterBlock) {
+ // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
+ first = fragment.firstChild;
+ last = fragment.lastChild;
+
+ composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
+ next = wysihtml5.dom.domNode(last).next({nodeTypes: [1,3], ignoreBlankTexts: true});
+ prev = wysihtml5.dom.domNode(first).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
+
+ if (first && !isLineBreaking(first, composer) && prev && !isLineBreaking(prev, composer)) {
+ first.parentNode.insertBefore(composer.doc.createElement('br'), first);
+ }
+
+ if (last && !isLineBreaking(last, composer) && next && !isLineBreaking(next, composer)) {
+ next.parentNode.insertBefore(composer.doc.createElement('br'), next);
+ }
+
+ } else {
+ // Ensure node does not get inserted into an inline where it is not allowed
+ outerInlines = cloneOuterInlines(rangeStartContainer, composer.element);
+ if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) {
+ if (fragment.childNodes.length === 1) {
+ while(fragment.firstChild.firstChild) {
+ outerInlines.innerNode.appendChild(fragment.firstChild.firstChild);
+ }
+ fragment.firstChild.appendChild(outerInlines.outerNode);
+ }
+ composer.selection.splitElementAtCaret(outerInlines.parent, fragment);
+ } else {
+ // Otherwise just insert
+ range.insertNode(fragment);
+ }
+ }
+ }
+
+ // Removes all block formatting from range
+ function clearRangeBlockFromating(range, closestBlockName, composer) {
var r = range.cloneRange(),
+ prevNode = getRangeNode(r.startContainer, r.startOffset).previousSibling,
+ nextNode = getRangeNode(r.endContainer, r.endOffset).nextSibling,
+ content = r.extractContents(),
+ fragment = composer.doc.createDocumentFragment(),
+ children, blocks,
+ first = true;
+
+ while(content.firstChild) {
+ // Iterate over all selection content first level childNodes
+ if (content.firstChild.nodeType === 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
+ // If node is a block element
+ // Split block formating and add new block to wrap caret
+
+ unwrapBlocksFromContent(content.firstChild);
+ children = wysihtml5.dom.unwrap(content.firstChild);
+
+ // Add line break before if needed
+ if (children.length > 0) {
+ if (
+ (fragment.lastChild && (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer))) ||
+ (!fragment.lastChild && prevNode && (prevNode.nodeType !== 1 || isLineBreaking(prevNode, composer)))
+ ){
+ fragment.appendChild(composer.doc.createElement('BR'));
+ }
+ }
+
+ for (var c = 0, cmax = children.length; c < cmax; c++) {
+ fragment.appendChild(children[c]);
+ }
+
+ // Add line break after if needed
+ if (children.length > 0) {
+ if (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer)) {
+ if (nextNode || fragment.lastChild !== content.lastChild) {
+ fragment.appendChild(composer.doc.createElement('BR'));
+ }
+ }
+ }
+
+ } else {
+ fragment.appendChild(content.firstChild);
+ }
+
+ first = false;
+ }
+ blocks = wysihtml5.lang.array(fragment.childNodes).get();
+ injectFragmentToRange(fragment, r, composer);
+ return blocks;
+ }
+
+ // When block node is inserted, look surrounding nodes and remove surplous linebreak tags (as block format breaks line itself)
+ function removeSurroundingLineBreaks(prevNode, nextNode, composer) {
+ var prevPrev = prevNode && wysihtml5.dom.domNode(prevNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
+ if (isBr(nextNode)) {
+ nextNode.parentNode.removeChild(nextNode);
+ }
+ if (isBr(prevNode) && (!prevPrev || prevPrev.nodeType !== 1 || composer.win.getComputedStyle(prevPrev).display !== "block")) {
+ prevNode.parentNode.removeChild(prevNode);
+ }
+ }
+
+ function applySurroundingLineBreaks(prevNode, nextNode, composer) {
+ var prevPrev;
+
+ if (prevNode && isBookmark(prevNode)) {
+ prevNode = prevNode.previousSibling;
+ }
+ if (nextNode && isBookmark(nextNode)) {
+ nextNode = nextNode.nextSibling;
+ }
+
+ prevPrev = prevNode && prevNode.previousSibling;
+
+ if (prevNode && (prevNode.nodeType !== 1 || (composer.win.getComputedStyle(prevNode).display !== "block" && !isBr(prevNode))) && prevNode.parentNode) {
+ prevNode.parentNode.insertBefore(composer.doc.createElement('br'), prevNode.nextSibling);
+ }
+
+ if (nextNode && (nextNode.nodeType !== 1 || composer.win.getComputedStyle(nextNode).display !== "block") && nextNode.parentNode) {
+ nextNode.parentNode.insertBefore(composer.doc.createElement('br'), nextNode);
+ }
+ }
+
+ // Wrap the range with a block level element
+ // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
+ function wrapRangeWithElement(range, options, closestBlockName, composer) {
+ var similarOptions = options ? correctOptionsForSimilarityCheck(options) : null,
+ r = range.cloneRange(),
rangeStartContainer = r.startContainer,
+ prevNode = wysihtml5.dom.domNode(getRangeNode(r.startContainer, r.startOffset)).prev({nodeTypes: [1,3], ignoreBlankTexts: true}),
+ nextNode = wysihtml5.dom.domNode(getRangeNode(r.endContainer, r.endOffset)).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
content = r.extractContents(),
fragment = composer.doc.createDocumentFragment(),
- similarOptions = defaultOptions ? correctOptionsForSimilarityCheck(defaultOptions) : null,
similarOuterBlock = similarOptions ? wysihtml5.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null,
- splitAllBlocks = !defaultOptions || (defaultName === "BLOCKQUOTE" && defaultOptions.nodeName && defaultOptions.nodeName === "BLOCKQUOTE"),
+ splitAllBlocks = !closestBlockName || !options || (options.nodeName === "BLOCKQUOTE" && closestBlockName === "BLOCKQUOTE"),
firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
wrapper, blocks, children;
- if (options && options.nodeName && options.nodeName === "BLOCKQUOTE") {
+ if (options && options.nodeName === "BLOCKQUOTE") {
+
+ // If blockquote is to be inserted no quessing just add it as outermost block on line or selection
var tmpEl = applyOptionsToElement(null, options, composer);
tmpEl.appendChild(content);
fragment.appendChild(tmpEl);
blocks = [tmpEl];
+
} else {
if (!content.firstChild) {
+ // IF selection is caret (can happen if line is empty) add format around tag
fragment.appendChild(applyOptionsToElement(null, options, composer));
} else {
while(content.firstChild) {
+ // Iterate over all selection content first level childNodes
if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
- if (options) {
- // Escape(split) block formatting at caret
- applyOptionsToElement(content.firstChild, options, composer);
- if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
- unwrapBlocksFromContent(content.firstChild);
- }
- fragment.appendChild(content.firstChild);
-
- } else {
- // Split block formating and add new block to wrap caret
+ // If node is a block element
+ // Escape(split) block formatting at caret
+ applyOptionsToElement(content.firstChild, options, composer);
+ if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
unwrapBlocksFromContent(content.firstChild);
- children = wysihtml5.dom.unwrap(content.firstChild);
- for (var c = 0, cmax = children.length; c < cmax; c++) {
- fragment.appendChild(children[c]);
- }
-
- if (fragment.childNodes.length > 0) {
- fragment.appendChild(composer.doc.createElement('BR'));
- }
}
+ fragment.appendChild(content.firstChild);
+
} else {
-
- if (options) {
- // Wrap subsequent non-block nodes inside new block element
- wrapper = applyOptionsToElement(null, defaultOptions, composer);
- while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
- if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
- unwrapBlocksFromContent(content.firstChild);
- }
- wrapper.appendChild(content.firstChild);
- }
- fragment.appendChild(wrapper);
- } else {
- // Escape(split) block formatting at selection
- if (content.firstChild.nodeType == 1) {
+ // Wrap subsequent non-block nodes inside new block element
+ wrapper = applyOptionsToElement(null, getOptionsWithNodename(options, closestBlockName, composer), composer);
+ while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
+ if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
unwrapBlocksFromContent(content.firstChild);
}
- fragment.appendChild(content.firstChild);
+ wrapper.appendChild(content.firstChild);
}
-
+ fragment.appendChild(wrapper);
}
}
}
blocks = wysihtml5.lang.array(fragment.childNodes).get();
}
- if (firstOuterBlock) {
- // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
- composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
- } else {
- // Ensure node does not get inserted into an inline where it is not allowed
- var outerInlines = cloneOuterInlines(rangeStartContainer, composer.element);
- if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) {
- if (fragment.childNodes.length === 1) {
- while(fragment.firstChild.firstChild) {
- outerInlines.innerNode.appendChild(fragment.firstChild.firstChild);
- }
- fragment.firstChild.appendChild(outerInlines.outerNode);
- }
- composer.selection.splitElementAtCaret(outerInlines.parent, fragment);
- } else {
- // Otherwise just insert
- r.insertNode(fragment);
- }
- }
-
+ injectFragmentToRange(fragment, r, composer, firstOuterBlock);
+ removeSurroundingLineBreaks(prevNode, nextNode, composer);
return blocks;
}
@@ -14293,101 +14573,154 @@ wysihtml5.Commands = Base.extend(
return (parentNode) ? parentNode.nodeName : null;
}
+
+ // Expands caret to cover the closest block that:
+ // * cannot contain other block level elements (h1-6,p, etc)
+ // * Has the same nodeName that is to be inserted
+ // * has insertingNodeName
+ // * is DIV if insertingNodeName is not present
+ //
+ // If nothing found selects the current line
+ function expandCaretToBlock(composer, insertingNodeName) {
+ var parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
+ query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (insertingNodeName ? insertingNodeName.toLowerCase() : 'div'),
+ }, null, composer.element),
+ range;
+
+ if (parent) {
+ range = composer.selection.createRange();
+ range.selectNode(parent);
+ composer.selection.setSelection(range);
+ } else if (!composer.isEmpty()) {
+ composer.selection.selectLine();
+ }
+ }
+
+ // Set selection to begin inside first created block element (beginning of it) and end inside (and after content) of last block element
+ // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway
+ function selectElements(newBlockElements, composer) {
+ var range = composer.selection.createRange(),
+ lastEl = newBlockElements[newBlockElements.length - 1],
+ lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 : lastEl.length || 0;
+
+ range.setStart(newBlockElements[0], 0);
+ range.setEnd(lastEl, lastOffset);
+ range.select();
+ }
+
+ // Get all ranges from selection (takes out uneditables and out of editor parts) and apply format to each
+ // Return created/modified block level elements
+ // Method can be either "apply" or "remove"
+ function formatSelection(method, composer, options) {
+ var ranges = composer.selection.getOwnRanges(),
+ newBlockElements = [],
+ closestBlockName;
+
+ // Some places do not allow block level elements inbetween (inside ul and outside li, inside table and outside of td/th)
+ ranges = fixNotPermittedInsertionPoints(ranges);
+
+ for (var i = ranges.length; i--;) {
+ fixRangeCoverage(ranges[i], composer);
+ closestBlockName = getParentBlockNodeName(ranges[i].startContainer, composer);
+ if (method === "remove") {
+ newBlockElements = newBlockElements.concat(clearRangeBlockFromating(ranges[i], closestBlockName, composer));
+ } else {
+ newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, closestBlockName, composer));
+ }
+ }
+ return newBlockElements;
+ }
+
+ // If properties is passed as a string, look for tag with that tagName/query
+ function parseOptions(options) {
+ if (typeof options === "string") {
+ options = {
+ nodeName: options.toUpperCase()
+ };
+ }
+ return options;
+ }
wysihtml5.commands.formatBlock = {
exec: function(composer, command, options) {
+ options = parseOptions(options);
var newBlockElements = [],
- placeholder, ranges, range, parent, bookmark, state;
-
- // If properties is passed as a string, look for tag with that tagName/query
- if (typeof options === "string") {
- options = {
- nodeName: options.toUpperCase()
- };
- }
+ ranges, range, bookmark, state, closestBlockName;
- // Remove state if toggle set and state on and selection is collapsed
+ // Find if current format state is active if options.toggle is set as true
+ // In toggle case active state elemets are formatted instead of working directly on selection
if (options && options.toggle) {
state = this.state(composer, command, options);
- if (state) {
- bookmark = rangy.saveSelection(composer.win);
- for (var j = 0, jmax = state.length; j < jmax; j++) {
- removeOptionsFromElement(state[j], options, composer);
- }
- }
}
+ if (state) {
+ // Remove format from state nodes if toggle set and state on and selection is collapsed
+ bookmark = rangy.saveSelection(composer.win);
+ for (var j = 0, jmax = state.length; j < jmax; j++) {
+ removeOptionsFromElement(state[j], options, composer);
+ }
- // Otherwise expand selection so it will cover closest block if option caretSelectsBlock is true and selection is collapsed
- if (!state) {
-
+ } else {
+ // If selection is caret expand it to cover nearest suitable block element or row if none found
if (composer.selection.isCollapsed()) {
- parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
- query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (options && options.nodeName ? options.nodeName.toLowerCase() : 'div'),
- }, null, composer.element);
- if (parent) {
- bookmark = rangy.saveSelection(composer.win);
- range = composer.selection.createRange();
- range.selectNode(parent);
- composer.selection.setSelection(range);
- } else if (!composer.isEmpty()) {
- bookmark = rangy.saveSelection(composer.win);
- composer.selection.selectLine();
- }
+ bookmark = rangy.saveSelection(composer.win);
+ expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
}
-
- // And get all selection ranges of current composer and iterate
- ranges = composer.selection.getOwnRanges();
- for (var i = ranges.length; i--;) {
- newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, getParentBlockNodeName(ranges[i].startContainer, composer), composer));
+ if (options) {
+ newBlockElements = formatSelection("apply", composer, options);
+ } else {
+ // Options == null means block formatting should be removed from selection
+ newBlockElements = formatSelection("remove", composer);
}
-
+
}
// Remove empty block elements that may be left behind
- cleanup(composer);
- // If cleanup removed some new block elements. remove them from array too
- for (var e = newBlockElements.length; e--;) {
- if (!newBlockElements[e].parentNode) {
- newBlockElements.splice(e, 1);
- }
- }
+ // Also remove them from new blocks list
+ newBlockElements = cleanup(composer, newBlockElements);
- // Restore correct selection
+ // Restore selection
if (bookmark) {
- wysihtml5.dom.removeInvisibleSpaces(composer.element);
rangy.restoreSelection(bookmark);
} else {
- wysihtml5.dom.removeInvisibleSpaces(composer.element);
- // Set selection to beging inside first created block element (beginning of it) and end inside (and after content) of last block element
- // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway
- range = composer.selection.createRange();
- range.setStart(newBlockElements[0], 0);
- var lastEl = newBlockElements[newBlockElements.length - 1],
- lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 : lastEl.length || 0;
- range.setEnd(lastEl, lastOffset);
- range.select();
+ selectElements(newBlockElements, composer);
}
},
-
- // If properties as null is passed returns status describing all block level elements
- state: function(composer, command, properties) {
+
+ // Removes all block formatting from selection
+ remove: function(composer, command, options) {
+ options = parseOptions(options);
+ var newBlockElements, bookmark;
- // If properties is passed as a string, look for tag with that tagName/query
- if (typeof properties === "string") {
- properties = {
- query: properties
- };
+ // If selection is caret expand it to cover nearest suitable block element or row if none found
+ if (composer.selection.isCollapsed()) {
+ bookmark = rangy.saveSelection(composer.win);
+ expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
}
+
+ newBlockElements = formatSelection("remove", composer);
+ newBlockElements = cleanup(composer, newBlockElements);
+
+ // Restore selection
+ if (bookmark) {
+ rangy.restoreSelection(bookmark);
+ } else {
+ selectElements(newBlockElements, composer);
+ }
+ },
+
+ // If options as null is passed returns status describing all block level elements
+ state: function(composer, command, options) {
+ options = parseOptions(options);
var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection
- return wysihtml5.dom.domNode(element).test(properties || { query: BLOCK_ELEMENTS });
+ return wysihtml5.dom.domNode(element).test(options || { query: BLOCK_ELEMENTS });
}).bind(this)),
parentNodes = composer.selection.getSelectedOwnNodes(),
parent;
// Finds matching elements that are parents of selection and adds to nodes list
for (var i = 0, maxi = parentNodes.length; i < maxi; i++) {
- parent = dom.getParentElement(parentNodes[i], properties || { query: BLOCK_ELEMENTS }, null, composer.element);
+ parent = dom.getParentElement(parentNodes[i], options || { query: BLOCK_ELEMENTS }, null, composer.element);
if (parent && nodes.indexOf(parent) === -1) {
nodes.push(parent);
}
@@ -14597,6 +14930,9 @@ wysihtml5.Commands = Base.extend(
if (options.toggle !== false && element.classList.contains(options.className)) {
element.classList.remove(options.className);
} else {
+ if (options.classRegExp) {
+ element.className = element.className.replace(options.classRegExp, '');
+ }
element.classList.add(options.className);
}
if (hasNoClass(element)) {
@@ -16247,7 +16583,7 @@ wysihtml5.views.View = Base.extend(
cleanUp: function(rules) {
var bookmark;
- if (this.selection) {
+ if (this.selection && this.selection.isInThisEditable()) {
bookmark = rangy.saveSelection(this.win);
}
this.parent.parse(this.element, undefined, rules);
@@ -16419,6 +16755,8 @@ wysihtml5.views.View = Base.extend(
]).from(this.textarea.element).to(this.element);
}
+ this._initAutoLinking();
+
dom.addClass(this.element, this.config.classNames.composer);
//
// Make the editor look like the original textarea, by syncing styles
@@ -16451,7 +16789,6 @@ wysihtml5.views.View = Base.extend(
// Make sure that the browser avoids using inline styles whenever possible
this.commands.exec("styleWithCSS", false);
- this._initAutoLinking();
this._initObjectResizing();
this._initUndoManager();
this._initLineBreaking();
@@ -16485,10 +16822,7 @@ wysihtml5.views.View = Base.extend(
supportsAutoLinking = browser.doesAutoLinkingInContentEditable();
if (supportsDisablingOfAutoLinking) {
- // I have no idea why IE edge deletes element content here when calling the command,
- var tmpHTML = this.element.innerHTML;
this.commands.exec("AutoUrlDetect", false, false);
- this.element.innerHTML = tmpHTML;
}
if (!this.config.autoLink) {