From 16153a672f9496d83f7aba626a91e66b618775cb Mon Sep 17 00:00:00 2001 From: Oliver Pulges Date: Fri, 18 Dec 2015 12:11:36 +0200 Subject: [PATCH] Update to v0.5.4 --- lib/wysihtml/rails/version.rb | 2 +- vendor/assets/javascripts/wysihtml-toolbar.js | 573 ++++++++++-------- vendor/assets/javascripts/wysihtml.js | 573 ++++++++++-------- 3 files changed, 673 insertions(+), 475 deletions(-) diff --git a/lib/wysihtml/rails/version.rb b/lib/wysihtml/rails/version.rb index 44b5c1e..82ac8d1 100644 --- a/lib/wysihtml/rails/version.rb +++ b/lib/wysihtml/rails/version.rb @@ -1,5 +1,5 @@ module Wysihtml module Rails - VERSION = "0.5.3" + VERSION = "0.5.4" end end diff --git a/vendor/assets/javascripts/wysihtml-toolbar.js b/vendor/assets/javascripts/wysihtml-toolbar.js index e38eb74..0a349ad 100644 --- a/vendor/assets/javascripts/wysihtml-toolbar.js +++ b/vendor/assets/javascripts/wysihtml-toolbar.js @@ -1,5 +1,5 @@ /** - * @license wysihtml v0.5.3 + * @license wysihtml v0.5.4 * https://github.com/Voog/wysihtml * * Author: Christopher Blum (https://github.com/tiff) @@ -10,7 +10,7 @@ * */ var wysihtml5 = { - version: "0.5.3", + version: "0.5.4", // namespaces commands: {}, @@ -7938,11 +7938,6 @@ 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]; @@ -7951,7 +7946,12 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { is: { emptyTextNode: function(ignoreWhitespace) { var regx = ignoreWhitespace ? (/^\s*$/g) : (/^[\r\n]*$/g); - return node.nodeType === wysihtml5.TEXT_NODE && (regx).test(node.data); + return node && node.nodeType === wysihtml5.TEXT_NODE && (regx).test(node.data); + }, + + // 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) + rangyBookmark: function() { + return node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'); }, visible: function() { @@ -7990,7 +7990,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } if ( - isBookmark(prevNode) || // is Rangy temporary boomark element (bypass) + wysihtml5.dom.domNode(prevNode).is.rangyBookmark() || // 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 ) { @@ -8010,7 +8010,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } if ( - isBookmark(nextNode) || // is Rangy temporary boomark element (bypass) + wysihtml5.dom.domNode(nextNode).is.rangyBookmark() || // 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 ) { @@ -11762,7 +11762,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { } }; - blankNode.appendChild(document.createTextNode(wysihtml5.INVISIBLE_SPACE)); + blankNode.appendChild(container.ownerDocument.createTextNode(wysihtml5.INVISIBLE_SPACE)); blankNode.className = '_wysihtml5-temp-caret-fix'; blankNode.style.display = 'block'; blankNode.style.minWidth = '1px'; @@ -12198,10 +12198,17 @@ wysihtml5.quirks.ensureProperClearing = (function() { nextNode = caretNode.nextSibling; } } else { - caretNode = r[0].startContainer; + if (r[0].startOffset === 0 && r[0].startContainer.previousSibling) { + caretNode = r[0].startContainer.previousSibling; + if (caretNode.nodeType === 3) { + offset = caretNode.data.length; + } + } else { + caretNode = r[0].startContainer; + offset = r[0].startOffset; + } prevNode = caretNode.previousSibling; nextNode = caretNode.nextSibling; - offset = r[0].startOffset; } return { @@ -12647,8 +12654,8 @@ wysihtml5.quirks.ensureProperClearing = (function() { if (wysihtml5.browser.supportsSelectionModify()) { this._selectLine_W3C(); } 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(); + // For IE Edge as it ditched the old api and did not fully implement the new one (as expected) + this._selectLineUniversal(); } }, @@ -12664,7 +12671,6 @@ wysihtml5.quirks.ensureProperClearing = (function() { } 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; @@ -12773,7 +12779,16 @@ wysihtml5.quirks.ensureProperClearing = (function() { 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]))))) { + } 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); } } @@ -12783,6 +12798,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { // Is probably just empty line as can not be expanded rect = r.nativeRange.getBoundingClientRect(); + // If startnode is not line break allready move the start position of range by -1 character until clientRect top changes; do { amount = r.moveStart('character', -1); testRect = r.nativeRange.getBoundingClientRect(); @@ -12793,31 +12809,31 @@ wysihtml5.quirks.ensureProperClearing = (function() { } count++; } while (amount !== 0 && !found && count < 2000); - count = 0; found = false; rect = r.nativeRange.getBoundingClientRect(); - do { - amount = r.moveEnd('character', 1); - 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); + + if (r.endContainer !== this.contain || (this.contain.lastChild && this.contain.childNodes[r.endOffset] !== this.contain.lastChild)) { + do { + amount = r.moveEnd('character', 1); + 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; } - - found = true; - } - count++; - } while (amount !== 0 && !found && count < 2000); - + count++; + } while (amount !== 0 && !found && count < 2000); + } r.select(); this.includeRangyRangeHelpers(); }, @@ -14159,7 +14175,7 @@ wysihtml5.Commands = Base.extend( nbIdx; for (var i = elements.length; i--;) { - if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "") { + if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "" && (newBlockElements.length === 0 || elements[i] !== newBlockElements[newBlockElements.length - 1])) { // If cleanup removes some new block elements. remove them from newblocks array too nbIdx = wysihtml5.lang.array(newBlockElements).indexOf(elements[i]); if (nbIdx > -1) { @@ -14496,8 +14512,13 @@ wysihtml5.Commands = Base.extend( } composer.selection.splitElementAtCaret(outerInlines.parent, fragment); } else { - // Otherwise just insert + var fc = fragment.firstChild, + lc = fragment.lastChild; + range.insertNode(fragment); + // restore range position as it might get lost in webkit sometimes + range.setStartBefore(fc); + range.setEndAfter(lc); } } } @@ -14626,7 +14647,21 @@ wysihtml5.Commands = Base.extend( startNode = getRangeNode(r.startContainer, r.startOffset), endNode = getRangeNode(r.endContainer, r.endOffset), prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode : wysihtml5.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}), - nextNode = ((r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] === endNode) || (r.endContainer === endNode && endNode.nodeType === 3 && !isWhitespaceAfter(endNode, r.endOffset))) ? endNode : wysihtml5.dom.domNode(getRangeNode(r.endContainer, r.endOffset)).next({nodeTypes: [1,3], ignoreBlankTexts: true}), + nextNode = ( + ( + r.endContainer.nodeType === 1 && + r.endContainer.childNodes[r.endOffset] === endNode && + ( + endNode.nodeType === 1 || + !isWhitespaceAfter(endNode, r.endOffset) && + !wysihtml5.dom.domNode(endNode).is.rangyBookmark() + ) + ) || ( + r.endContainer === endNode && + endNode.nodeType === 3 && + !isWhitespaceAfter(endNode, r.endOffset) + ) + ) ? endNode : wysihtml5.dom.domNode(endNode).next({nodeTypes: [1,3], ignoreBlankTexts: true}), content = r.extractContents(), fragment = composer.doc.createDocumentFragment(), similarOuterBlock = similarOptions ? wysihtml5.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null, @@ -14635,6 +14670,11 @@ wysihtml5.Commands = Base.extend( wrapper, blocks, children, firstc, lastC; + if (wysihtml5.dom.domNode(nextNode).is.rangyBookmark()) { + endNode = nextNode; + nextNode = endNode.nextSibling; + } + trimBlankTextsAndBreaks(content); if (options && options.nodeName === "BLOCKQUOTE") { @@ -14684,6 +14724,16 @@ wysihtml5.Commands = Base.extend( } injectFragmentToRange(fragment, r, composer, firstOuterBlock); removeSurroundingLineBreaks(prevNode, nextNode, composer); + + // Fix webkit madness by inserting linebreak rangy after cursor marker to blank last block + // (if it contains rangy bookmark, so selection can be restored later correctly) + if (blocks.length > 0 && + ( + typeof blocks[blocks.length - 1].lastChild === "undefined" || wysihtml5.dom.domNode(blocks[blocks.length - 1].lastChild).is.rangyBookmark() + ) + ) { + blocks[blocks.length - 1].appendChild(composer.doc.createElement('br')); + } return blocks; } @@ -15832,7 +15882,9 @@ wysihtml5.Commands = Base.extend( for (var i = innerLists.length; i--;) { wysihtml5.dom.resolveList(innerLists[i], composer.config.useLineBreaks); } - wysihtml5.dom.resolveList(el, composer.config.useLineBreaks); + if (innerLists.length === 0) { + wysihtml5.dom.resolveList(el, composer.config.useLineBreaks); + } } }); }; @@ -15900,8 +15952,34 @@ wysihtml5.Commands = Base.extend( exec: function(composer, command, nodeName) { var doc = composer.doc, cmd = (nodeName === "OL") ? "insertOrderedList" : "insertUnorderedList", - selectedNode = composer.selection.getSelectedNode(), - list = findListEl(selectedNode, nodeName, composer); + s = composer.selection.getSelection(), + anode = s.anchorNode.nodeType === 1 && s.anchorNode.firstChild ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode, + fnode = s.focusNode.nodeType === 1 && s.focusNode.firstChild ? s.focusNode.childNodes[s.focusOffset] || s.focusNode.lastChild : s.focusNode, + selectedNode, list; + + if (s.isBackwards()) { + // swap variables + anode = [fnode, fnode = anode][0]; + } + + if (wysihtml5.dom.domNode(fnode).is.emptyTextNode(true) && fnode) { + fnode = wysihtml5.dom.domNode(fnode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + } + if (wysihtml5.dom.domNode(anode).is.emptyTextNode(true) && anode) { + anode = wysihtml5.dom.domNode(anode).next({nodeTypes: [1,3], ignoreBlankTexts: true}); + } + + if (anode && fnode) { + if (anode === fnode) { + selectedNode = anode; + } else { + selectedNode = wysihtml5.dom.domNode(anode).commonAncestor(fnode, composer.element); + } + } else { + selectedNode = composer.selection.getSelectedNode(); + } + + list = findListEl(selectedNode, nodeName, composer); if (!list.el) { if (composer.commands.support(cmd)) { @@ -17390,155 +17468,248 @@ wysihtml5.views.View = Base.extend( "73": "italic", // I "85": "underline" // U }; + + var actions = { - // Adds multiple eventlisteners to target, bound to one callback - // TODO: If needed elsewhere make it part of wysihtml5.dom or sth - var addListeners = function (target, events, callback) { - for(var i = 0, max = events.length; i < max; i++) { - target.addEventListener(events[i], callback, false); - } - }; + // Adds multiple eventlisteners to target, bound to one callback + // TODO: If needed elsewhere make it part of wysihtml5.dom or sth + addListeners: function (target, events, callback) { + for(var i = 0, max = events.length; i < max; i++) { + target.addEventListener(events[i], callback, false); + } + }, - // Removes multiple eventlisteners from target, bound to one callback - // TODO: If needed elsewhere make it part of wysihtml5.dom or sth - var removeListeners = function (target, events, callback) { - for(var i = 0, max = events.length; i < max; i++) { - target.removeEventListener(events[i], callback, false); - } - }; + // Removes multiple eventlisteners from target, bound to one callback + // TODO: If needed elsewhere make it part of wysihtml5.dom or sth + removeListeners: function (target, events, callback) { + for(var i = 0, max = events.length; i < max; i++) { + target.removeEventListener(events[i], callback, false); + } + }, - // Override for giving user ability to delete last line break in table cell - var fixLastBrDeletionInTable = function(composer, force) { - if (composer.selection.caretIsLastInSelection()) { - var sel = composer.selection.getSelection(), - aNode = sel.anchorNode; - if (aNode && aNode.nodeType === 1 && (wysihtml5.dom.getParentElement(aNode, {query: 'td, th'}, false, composer.element) || force)) { - var nextNode = aNode.childNodes[sel.anchorOffset]; - if (nextNode && nextNode.nodeType === 1 & nextNode.nodeName === "BR") { - nextNode.parentNode.removeChild(nextNode); - return true; + // Override for giving user ability to delete last line break in table cell + fixLastBrDeletionInTable: function(composer, force) { + if (composer.selection.caretIsLastInSelection()) { + var sel = composer.selection.getSelection(), + aNode = sel.anchorNode; + if (aNode && aNode.nodeType === 1 && (wysihtml5.dom.getParentElement(aNode, {query: 'td, th'}, false, composer.element) || force)) { + var nextNode = aNode.childNodes[sel.anchorOffset]; + if (nextNode && nextNode.nodeType === 1 & nextNode.nodeName === "BR") { + nextNode.parentNode.removeChild(nextNode); + return true; + } } } - } - return false; - }; + return false; + }, - // If found an uneditable before caret then notify it before deletion - var handleUneditableDeletion = function(composer) { - var before = composer.selection.getBeforeSelection(true); - if (before && (before.type === "element" || before.type === "leafnode") && before.node.nodeType === 1 && before.node.classList.contains(composer.config.classNames.uneditableContainer)) { - if (fixLastBrDeletionInTable(composer, true)) { + // If found an uneditable before caret then notify it before deletion + handleUneditableDeletion: function(composer) { + var before = composer.selection.getBeforeSelection(true); + if (before && (before.type === "element" || before.type === "leafnode") && before.node.nodeType === 1 && before.node.classList.contains(composer.config.classNames.uneditableContainer)) { + if (actions.fixLastBrDeletionInTable(composer, true)) { + return true; + } + try { + var ev = new CustomEvent("wysihtml5:uneditable:delete", {bubbles: true, cancelable: false}); + before.node.dispatchEvent(ev); + } catch (err) {} + before.node.parentNode.removeChild(before.node); return true; } - try { - var ev = new CustomEvent("wysihtml5:uneditable:delete", {bubbles: true, cancelable: false}); - before.node.dispatchEvent(ev); - } catch (err) {} - before.node.parentNode.removeChild(before.node); - return true; - } - return false; - }; + return false; + }, - // Deletion with caret in the beginning of headings and other block elvel elements needs special attention - // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit) - var fixDeleteInTheBeginningOfBlock = function(composer) { - var selection = composer.selection, - prevNode = selection.getPreviousNode(); - - if (selection.caretIsFirstInSelection() && - prevNode && - prevNode.nodeType === 1 && - (/block/).test(composer.win.getComputedStyle(prevNode).display) && - !domNode(prevNode).test({ - query: "ol, ul, table, tr, dl" - }) - ) { - if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) { - // If heading is empty remove the heading node - prevNode.parentNode.removeChild(prevNode); - return true; - } else { - if (prevNode.lastChild) { - var selNode = prevNode.lastChild, - selectedNode = selection.getSelectedNode(), - commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element); - curNode = selectedNode.nodeType === 3 ? selectedNode : wysihtml5.dom.getParentElement(selectedNode, { - query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote" - }, false, commonAncestorNode || composer.element); - - if (curNode) { - domNode(curNode).transferContentTo(prevNode, true); - selection.setAfter(selNode); + // Deletion with caret in the beginning of headings and other block elvel elements needs special attention + // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit) + fixDeleteInTheBeginningOfBlock: function(composer) { + var selection = composer.selection, + prevNode = selection.getPreviousNode(); + + if (selection.caretIsFirstInSelection() && + prevNode && + prevNode.nodeType === 1 && + (/block/).test(composer.win.getComputedStyle(prevNode).display) && + !domNode(prevNode).test({ + query: "ol, ul, table, tr, dl" + }) + ) { + if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) { + // If heading is empty remove the heading node + prevNode.parentNode.removeChild(prevNode); + return true; + } else { + if (prevNode.lastChild) { + var selNode = prevNode.lastChild, + selectedNode = selection.getSelectedNode(), + commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element), + curNode = wysihtml5.dom.getParentElement(selectedNode, { + query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote" + }, false, commonAncestorNode || composer.element); + + if (curNode) { + domNode(curNode).transferContentTo(prevNode, true); + selection.setAfter(selNode); + return true; + } + } + } + } + return false; + }, + + /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */ + /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */ + fixDeleteInTheBeginningOfLi: function(composer) { + if (wysihtml5.browser.hasLiDeletingProblem()) { + var selection = composer.selection.getSelection(), + aNode = selection.anchorNode, + listNode, prevNode, firstNode, + isInBeginnig = composer.selection.caretIsFirstInSelection(); + + // Fix caret at the beginnig of first textNode in LI + if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) { + aNode = aNode.parentNode; + isInBeginnig = true; + } + + if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") { + prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) { + prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + } + if (prevNode) { + firstNode = aNode.firstChild; + domNode(aNode).transferContentTo(prevNode, true); + if (firstNode) { + composer.selection.setBefore(firstNode); + } else if (prevNode) { + if (prevNode.nodeType === 1) { + if (prevNode.lastChild) { + composer.selection.setAfter(prevNode.lastChild); + } else { + composer.selection.selectNode(prevNode); + } + } else { + composer.selection.setAfter(prevNode); + } + } return true; } } } - } - return false; - }; + return false; + }, + + // Table management + // If present enableObjectResizing and enableInlineTableEditing command should be called with false to prevent native table handlers + initTableHandling: function() { + var hideHandlers = function() { + window.removeEventListener('load', hideHandlers); + this.doc.execCommand("enableObjectResizing", false, "false"); + this.doc.execCommand("enableInlineTableEditing", false, "false"); + }.bind(this), + iframeInitiator = (function() { + hideHandlers.call(this); + actions.removeListeners(this.sandbox.getIframe(), ["focus", "mouseup", "mouseover"], iframeInitiator); + }).bind(this); + + if( this.doc.execCommand && + wysihtml5.browser.supportsCommand(this.doc, "enableObjectResizing") && + wysihtml5.browser.supportsCommand(this.doc, "enableInlineTableEditing")) + { + if (this.sandbox.getIframe) { + actions.addListeners(this.sandbox.getIframe(), ["focus", "mouseup", "mouseover"], iframeInitiator); + } else { + window.addEventListener('load', hideHandlers); + } + } + this.tableSelection = wysihtml5.quirks.tableCellsSelection(this.element, this.parent); + }, + + // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature) + // Returns true if some corrections is applied so events know when to prevent default + doLineBreaksModeEnterWithCaret: function(composer) { + var breakNodes = "p, pre, div, blockquote", + caretInfo, parent, txtNode, + ret = false; + + caretInfo = composer.selection.getNodesNearCaret(); + if (caretInfo) { + + if (caretInfo.caretNode || caretInfo.nextNode) { + parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2); + if (parent === composer.element) { + parent = undefined; + } + } + + if (parent && caretInfo.caretNode) { + if (domNode(caretInfo.caretNode).is.lineBreak()) { + + if (composer.config.doubleLineBreakEscapesBlock) { + // Double enter (enter on blank line) exits block element in useLineBreaks mode. + ret = true; + caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode); + + // Ensure surplous line breaks are not added to preceding element + if (domNode(caretInfo.nextNode).is.lineBreak()) { + caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode); + } - /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */ - /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */ - var fixDeleteInTheBeginningOfLi = function(composer) { - if (wysihtml5.browser.hasLiDeletingProblem()) { - var selection = composer.selection.getSelection(), - aNode = selection.anchorNode, - listNode, prevNode, firstNode, - isInBeginnig = composer.selection.caretIsFirstInSelection(); - - // Fix caret at the beginnig of first textNode in LI - if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) { - aNode = aNode.parentNode; - isInBeginnig = true; - } - - if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") { - prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); - if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) { - prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); - } - if (prevNode) { - firstNode = aNode.firstChild; - domNode(aNode).transferContentTo(prevNode, true); - if (firstNode) { - composer.selection.setBefore(firstNode); - } else if (prevNode) { - if (prevNode.nodeType === 1) { - if (prevNode.lastChild) { - composer.selection.setAfter(prevNode.lastChild); + var brNode = composer.doc.createElement('br'); + if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) { + parent.parentNode.insertBefore(brNode, parent.nextSibling); } else { - composer.selection.selectNode(prevNode); + composer.selection.splitElementAtCaret(parent, brNode); } - } else { - composer.selection.setAfter(prevNode); + + // Ensure surplous blank lines are not added to preceding element + if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) { + // Replaces blank lines at the beginning of textnode + caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, ''); + } + composer.selection.setBefore(brNode); } + + } else if (caretInfo.caretNode.nodeType === 3 && wysihtml5.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) { + + // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens. + // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br + ret = true; + var br1 = composer.doc.createElement('br'), + br2 = composer.doc.createElement('br'), + f = composer.doc.createDocumentFragment(); + f.appendChild(br1); + f.appendChild(br2); + composer.selection.insertNode(f); + composer.selection.setBefore(br2); + } - return true; } } + return ret; } - return false; - } + }; var handleDeleteKeyPress = function(event, composer) { var selection = composer.selection, element = composer.element; if (selection.isCollapsed()) { - if (handleUneditableDeletion(composer)) { + if (actions.handleUneditableDeletion(composer)) { event.preventDefault(); return; } - if (fixDeleteInTheBeginningOfLi(composer)) { + if (actions.fixDeleteInTheBeginningOfLi(composer)) { event.preventDefault(); return; } - if (fixDeleteInTheBeginningOfBlock(composer)) { + if (actions.fixDeleteInTheBeginningOfBlock(composer)) { event.preventDefault(); return; } - if (fixLastBrDeletionInTable(composer)) { + if (actions.fixLastBrDeletionInTable(composer)) { event.preventDefault(); return; } @@ -17558,59 +17729,8 @@ wysihtml5.views.View = Base.extend( caretInfo, parent, txtNode; if (composer.selection.isCollapsed()) { - caretInfo = composer.selection.getNodesNearCaret(); - if (caretInfo) { - - if (caretInfo.caretNode || caretInfo.nextNode) { - parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2); - if (parent === composer.element) { - parent = undefined; - } - } - - if (parent && caretInfo.caretNode) { - if (domNode(caretInfo.caretNode).is.lineBreak()) { - - if (composer.config.doubleLineBreakEscapesBlock) { - // Double enter (enter on blank line) exits block element in useLineBreaks mode. - event.preventDefault(); - caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode); - - // Ensure surplous line breaks are not added to preceding element - if (domNode(caretInfo.nextNode).is.lineBreak()) { - caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode); - } - - var brNode = composer.doc.createElement('br'); - if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) { - parent.parentNode.insertBefore(brNode, parent.nextSibling); - } else { - composer.selection.splitElementAtCaret(parent, brNode); - } - - // Ensure surplous blank lines are not added to preceding element - if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) { - // Replaces blank lines at the beginning of textnode - caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, ''); - } - composer.selection.setBefore(brNode); - } - - } else if (caretInfo.caretNode.nodeType === 3 && wysihtml5.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) { - - // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens. - // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br - event.preventDefault(); - var br1 = composer.doc.createElement('br'), - br2 = composer.doc.createElement('br'), - f = composer.doc.createDocumentFragment(); - f.appendChild(br1); - f.appendChild(br2); - composer.selection.insertNode(f); - composer.selection.setBefore(br2); - - } - } + if (actions.doLineBreaksModeEnterWithCaret(composer)) { + event.preventDefault(); } } } @@ -17797,31 +17917,10 @@ wysihtml5.views.View = Base.extend( }).bind(this), 0); }; - // Table management - // If present enableObjectResizing and enableInlineTableEditing command should be called with false to prevent native table handlers - var initTableHandling = function () { - var hideHandlers = function () { - window.removeEventListener('load', hideHandlers); - this.doc.execCommand("enableObjectResizing", false, "false"); - this.doc.execCommand("enableInlineTableEditing", false, "false"); - }.bind(this), - iframeInitiator = (function() { - hideHandlers.call(this); - removeListeners(this.sandbox.getIframe(), ["focus", "mouseup", "mouseover"], iframeInitiator); - }).bind(this); - - if( this.doc.execCommand && - wysihtml5.browser.supportsCommand(this.doc, "enableObjectResizing") && - wysihtml5.browser.supportsCommand(this.doc, "enableInlineTableEditing")) - { - if (this.sandbox.getIframe) { - addListeners(this.sandbox.getIframe(), ["focus", "mouseup", "mouseover"], iframeInitiator); - } else { - window.addEventListener('load', hideHandlers); - } - } - this.tableSelection = wysihtml5.quirks.tableCellsSelection(this.element, this.parent); - }; + + + // Testing requires actions to be accessible from out of scope + wysihtml5.views.Composer.prototype.observeActions = actions; wysihtml5.views.Composer.prototype.observe = function() { var that = this, @@ -17847,14 +17946,14 @@ wysihtml5.views.View = Base.extend( // --------- User interactions -- if (this.config.handleTables) { // If handleTables option is true, table handling functions are bound - initTableHandling.call(this); + actions.initTableHandling.call(this); } - addListeners(focusBlurElement, ["drop", "paste", "mouseup", "focus", "keyup"], handleUserInteraction.bind(this)); + actions.addListeners(focusBlurElement, ["drop", "paste", "mouseup", "focus", "keyup"], handleUserInteraction.bind(this)); focusBlurElement.addEventListener("focus", handleFocus.bind(this), false); focusBlurElement.addEventListener("blur", handleBlur.bind(this), false); - addListeners(this.element, ["drop", "paste", "beforepaste"], handlePaste.bind(this), false); + actions.addListeners(this.element, ["drop", "paste", "beforepaste"], handlePaste.bind(this), false); this.element.addEventListener("copy", handleCopy.bind(this), false); this.element.addEventListener("mousedown", handleMouseDown.bind(this), false); this.element.addEventListener("click", handleClick.bind(this), false); diff --git a/vendor/assets/javascripts/wysihtml.js b/vendor/assets/javascripts/wysihtml.js index 0349ffe..9c8bfe5 100644 --- a/vendor/assets/javascripts/wysihtml.js +++ b/vendor/assets/javascripts/wysihtml.js @@ -1,5 +1,5 @@ /** - * @license wysihtml v0.5.3 + * @license wysihtml v0.5.4 * https://github.com/Voog/wysihtml * * Author: Christopher Blum (https://github.com/tiff) @@ -10,7 +10,7 @@ * */ var wysihtml5 = { - version: "0.5.3", + version: "0.5.4", // namespaces commands: {}, @@ -7938,11 +7938,6 @@ 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]; @@ -7951,7 +7946,12 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { is: { emptyTextNode: function(ignoreWhitespace) { var regx = ignoreWhitespace ? (/^\s*$/g) : (/^[\r\n]*$/g); - return node.nodeType === wysihtml5.TEXT_NODE && (regx).test(node.data); + return node && node.nodeType === wysihtml5.TEXT_NODE && (regx).test(node.data); + }, + + // 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) + rangyBookmark: function() { + return node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'); }, visible: function() { @@ -7990,7 +7990,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } if ( - isBookmark(prevNode) || // is Rangy temporary boomark element (bypass) + wysihtml5.dom.domNode(prevNode).is.rangyBookmark() || // 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 ) { @@ -8010,7 +8010,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } if ( - isBookmark(nextNode) || // is Rangy temporary boomark element (bypass) + wysihtml5.dom.domNode(nextNode).is.rangyBookmark() || // 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 ) { @@ -11762,7 +11762,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { } }; - blankNode.appendChild(document.createTextNode(wysihtml5.INVISIBLE_SPACE)); + blankNode.appendChild(container.ownerDocument.createTextNode(wysihtml5.INVISIBLE_SPACE)); blankNode.className = '_wysihtml5-temp-caret-fix'; blankNode.style.display = 'block'; blankNode.style.minWidth = '1px'; @@ -12198,10 +12198,17 @@ wysihtml5.quirks.ensureProperClearing = (function() { nextNode = caretNode.nextSibling; } } else { - caretNode = r[0].startContainer; + if (r[0].startOffset === 0 && r[0].startContainer.previousSibling) { + caretNode = r[0].startContainer.previousSibling; + if (caretNode.nodeType === 3) { + offset = caretNode.data.length; + } + } else { + caretNode = r[0].startContainer; + offset = r[0].startOffset; + } prevNode = caretNode.previousSibling; nextNode = caretNode.nextSibling; - offset = r[0].startOffset; } return { @@ -12647,8 +12654,8 @@ wysihtml5.quirks.ensureProperClearing = (function() { if (wysihtml5.browser.supportsSelectionModify()) { this._selectLine_W3C(); } 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(); + // For IE Edge as it ditched the old api and did not fully implement the new one (as expected) + this._selectLineUniversal(); } }, @@ -12664,7 +12671,6 @@ wysihtml5.quirks.ensureProperClearing = (function() { } 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; @@ -12773,7 +12779,16 @@ wysihtml5.quirks.ensureProperClearing = (function() { 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]))))) { + } 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); } } @@ -12783,6 +12798,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { // Is probably just empty line as can not be expanded rect = r.nativeRange.getBoundingClientRect(); + // If startnode is not line break allready move the start position of range by -1 character until clientRect top changes; do { amount = r.moveStart('character', -1); testRect = r.nativeRange.getBoundingClientRect(); @@ -12793,31 +12809,31 @@ wysihtml5.quirks.ensureProperClearing = (function() { } count++; } while (amount !== 0 && !found && count < 2000); - count = 0; found = false; rect = r.nativeRange.getBoundingClientRect(); - do { - amount = r.moveEnd('character', 1); - 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); + + if (r.endContainer !== this.contain || (this.contain.lastChild && this.contain.childNodes[r.endOffset] !== this.contain.lastChild)) { + do { + amount = r.moveEnd('character', 1); + 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; } - - found = true; - } - count++; - } while (amount !== 0 && !found && count < 2000); - + count++; + } while (amount !== 0 && !found && count < 2000); + } r.select(); this.includeRangyRangeHelpers(); }, @@ -14159,7 +14175,7 @@ wysihtml5.Commands = Base.extend( nbIdx; for (var i = elements.length; i--;) { - if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "") { + if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "" && (newBlockElements.length === 0 || elements[i] !== newBlockElements[newBlockElements.length - 1])) { // If cleanup removes some new block elements. remove them from newblocks array too nbIdx = wysihtml5.lang.array(newBlockElements).indexOf(elements[i]); if (nbIdx > -1) { @@ -14496,8 +14512,13 @@ wysihtml5.Commands = Base.extend( } composer.selection.splitElementAtCaret(outerInlines.parent, fragment); } else { - // Otherwise just insert + var fc = fragment.firstChild, + lc = fragment.lastChild; + range.insertNode(fragment); + // restore range position as it might get lost in webkit sometimes + range.setStartBefore(fc); + range.setEndAfter(lc); } } } @@ -14626,7 +14647,21 @@ wysihtml5.Commands = Base.extend( startNode = getRangeNode(r.startContainer, r.startOffset), endNode = getRangeNode(r.endContainer, r.endOffset), prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode : wysihtml5.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}), - nextNode = ((r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] === endNode) || (r.endContainer === endNode && endNode.nodeType === 3 && !isWhitespaceAfter(endNode, r.endOffset))) ? endNode : wysihtml5.dom.domNode(getRangeNode(r.endContainer, r.endOffset)).next({nodeTypes: [1,3], ignoreBlankTexts: true}), + nextNode = ( + ( + r.endContainer.nodeType === 1 && + r.endContainer.childNodes[r.endOffset] === endNode && + ( + endNode.nodeType === 1 || + !isWhitespaceAfter(endNode, r.endOffset) && + !wysihtml5.dom.domNode(endNode).is.rangyBookmark() + ) + ) || ( + r.endContainer === endNode && + endNode.nodeType === 3 && + !isWhitespaceAfter(endNode, r.endOffset) + ) + ) ? endNode : wysihtml5.dom.domNode(endNode).next({nodeTypes: [1,3], ignoreBlankTexts: true}), content = r.extractContents(), fragment = composer.doc.createDocumentFragment(), similarOuterBlock = similarOptions ? wysihtml5.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null, @@ -14635,6 +14670,11 @@ wysihtml5.Commands = Base.extend( wrapper, blocks, children, firstc, lastC; + if (wysihtml5.dom.domNode(nextNode).is.rangyBookmark()) { + endNode = nextNode; + nextNode = endNode.nextSibling; + } + trimBlankTextsAndBreaks(content); if (options && options.nodeName === "BLOCKQUOTE") { @@ -14684,6 +14724,16 @@ wysihtml5.Commands = Base.extend( } injectFragmentToRange(fragment, r, composer, firstOuterBlock); removeSurroundingLineBreaks(prevNode, nextNode, composer); + + // Fix webkit madness by inserting linebreak rangy after cursor marker to blank last block + // (if it contains rangy bookmark, so selection can be restored later correctly) + if (blocks.length > 0 && + ( + typeof blocks[blocks.length - 1].lastChild === "undefined" || wysihtml5.dom.domNode(blocks[blocks.length - 1].lastChild).is.rangyBookmark() + ) + ) { + blocks[blocks.length - 1].appendChild(composer.doc.createElement('br')); + } return blocks; } @@ -15832,7 +15882,9 @@ wysihtml5.Commands = Base.extend( for (var i = innerLists.length; i--;) { wysihtml5.dom.resolveList(innerLists[i], composer.config.useLineBreaks); } - wysihtml5.dom.resolveList(el, composer.config.useLineBreaks); + if (innerLists.length === 0) { + wysihtml5.dom.resolveList(el, composer.config.useLineBreaks); + } } }); }; @@ -15900,8 +15952,34 @@ wysihtml5.Commands = Base.extend( exec: function(composer, command, nodeName) { var doc = composer.doc, cmd = (nodeName === "OL") ? "insertOrderedList" : "insertUnorderedList", - selectedNode = composer.selection.getSelectedNode(), - list = findListEl(selectedNode, nodeName, composer); + s = composer.selection.getSelection(), + anode = s.anchorNode.nodeType === 1 && s.anchorNode.firstChild ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode, + fnode = s.focusNode.nodeType === 1 && s.focusNode.firstChild ? s.focusNode.childNodes[s.focusOffset] || s.focusNode.lastChild : s.focusNode, + selectedNode, list; + + if (s.isBackwards()) { + // swap variables + anode = [fnode, fnode = anode][0]; + } + + if (wysihtml5.dom.domNode(fnode).is.emptyTextNode(true) && fnode) { + fnode = wysihtml5.dom.domNode(fnode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + } + if (wysihtml5.dom.domNode(anode).is.emptyTextNode(true) && anode) { + anode = wysihtml5.dom.domNode(anode).next({nodeTypes: [1,3], ignoreBlankTexts: true}); + } + + if (anode && fnode) { + if (anode === fnode) { + selectedNode = anode; + } else { + selectedNode = wysihtml5.dom.domNode(anode).commonAncestor(fnode, composer.element); + } + } else { + selectedNode = composer.selection.getSelectedNode(); + } + + list = findListEl(selectedNode, nodeName, composer); if (!list.el) { if (composer.commands.support(cmd)) { @@ -17390,155 +17468,248 @@ wysihtml5.views.View = Base.extend( "73": "italic", // I "85": "underline" // U }; + + var actions = { - // Adds multiple eventlisteners to target, bound to one callback - // TODO: If needed elsewhere make it part of wysihtml5.dom or sth - var addListeners = function (target, events, callback) { - for(var i = 0, max = events.length; i < max; i++) { - target.addEventListener(events[i], callback, false); - } - }; + // Adds multiple eventlisteners to target, bound to one callback + // TODO: If needed elsewhere make it part of wysihtml5.dom or sth + addListeners: function (target, events, callback) { + for(var i = 0, max = events.length; i < max; i++) { + target.addEventListener(events[i], callback, false); + } + }, - // Removes multiple eventlisteners from target, bound to one callback - // TODO: If needed elsewhere make it part of wysihtml5.dom or sth - var removeListeners = function (target, events, callback) { - for(var i = 0, max = events.length; i < max; i++) { - target.removeEventListener(events[i], callback, false); - } - }; + // Removes multiple eventlisteners from target, bound to one callback + // TODO: If needed elsewhere make it part of wysihtml5.dom or sth + removeListeners: function (target, events, callback) { + for(var i = 0, max = events.length; i < max; i++) { + target.removeEventListener(events[i], callback, false); + } + }, - // Override for giving user ability to delete last line break in table cell - var fixLastBrDeletionInTable = function(composer, force) { - if (composer.selection.caretIsLastInSelection()) { - var sel = composer.selection.getSelection(), - aNode = sel.anchorNode; - if (aNode && aNode.nodeType === 1 && (wysihtml5.dom.getParentElement(aNode, {query: 'td, th'}, false, composer.element) || force)) { - var nextNode = aNode.childNodes[sel.anchorOffset]; - if (nextNode && nextNode.nodeType === 1 & nextNode.nodeName === "BR") { - nextNode.parentNode.removeChild(nextNode); - return true; + // Override for giving user ability to delete last line break in table cell + fixLastBrDeletionInTable: function(composer, force) { + if (composer.selection.caretIsLastInSelection()) { + var sel = composer.selection.getSelection(), + aNode = sel.anchorNode; + if (aNode && aNode.nodeType === 1 && (wysihtml5.dom.getParentElement(aNode, {query: 'td, th'}, false, composer.element) || force)) { + var nextNode = aNode.childNodes[sel.anchorOffset]; + if (nextNode && nextNode.nodeType === 1 & nextNode.nodeName === "BR") { + nextNode.parentNode.removeChild(nextNode); + return true; + } } } - } - return false; - }; + return false; + }, - // If found an uneditable before caret then notify it before deletion - var handleUneditableDeletion = function(composer) { - var before = composer.selection.getBeforeSelection(true); - if (before && (before.type === "element" || before.type === "leafnode") && before.node.nodeType === 1 && before.node.classList.contains(composer.config.classNames.uneditableContainer)) { - if (fixLastBrDeletionInTable(composer, true)) { + // If found an uneditable before caret then notify it before deletion + handleUneditableDeletion: function(composer) { + var before = composer.selection.getBeforeSelection(true); + if (before && (before.type === "element" || before.type === "leafnode") && before.node.nodeType === 1 && before.node.classList.contains(composer.config.classNames.uneditableContainer)) { + if (actions.fixLastBrDeletionInTable(composer, true)) { + return true; + } + try { + var ev = new CustomEvent("wysihtml5:uneditable:delete", {bubbles: true, cancelable: false}); + before.node.dispatchEvent(ev); + } catch (err) {} + before.node.parentNode.removeChild(before.node); return true; } - try { - var ev = new CustomEvent("wysihtml5:uneditable:delete", {bubbles: true, cancelable: false}); - before.node.dispatchEvent(ev); - } catch (err) {} - before.node.parentNode.removeChild(before.node); - return true; - } - return false; - }; + return false; + }, - // Deletion with caret in the beginning of headings and other block elvel elements needs special attention - // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit) - var fixDeleteInTheBeginningOfBlock = function(composer) { - var selection = composer.selection, - prevNode = selection.getPreviousNode(); - - if (selection.caretIsFirstInSelection() && - prevNode && - prevNode.nodeType === 1 && - (/block/).test(composer.win.getComputedStyle(prevNode).display) && - !domNode(prevNode).test({ - query: "ol, ul, table, tr, dl" - }) - ) { - if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) { - // If heading is empty remove the heading node - prevNode.parentNode.removeChild(prevNode); - return true; - } else { - if (prevNode.lastChild) { - var selNode = prevNode.lastChild, - selectedNode = selection.getSelectedNode(), - commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element); - curNode = selectedNode.nodeType === 3 ? selectedNode : wysihtml5.dom.getParentElement(selectedNode, { - query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote" - }, false, commonAncestorNode || composer.element); - - if (curNode) { - domNode(curNode).transferContentTo(prevNode, true); - selection.setAfter(selNode); + // Deletion with caret in the beginning of headings and other block elvel elements needs special attention + // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit) + fixDeleteInTheBeginningOfBlock: function(composer) { + var selection = composer.selection, + prevNode = selection.getPreviousNode(); + + if (selection.caretIsFirstInSelection() && + prevNode && + prevNode.nodeType === 1 && + (/block/).test(composer.win.getComputedStyle(prevNode).display) && + !domNode(prevNode).test({ + query: "ol, ul, table, tr, dl" + }) + ) { + if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) { + // If heading is empty remove the heading node + prevNode.parentNode.removeChild(prevNode); + return true; + } else { + if (prevNode.lastChild) { + var selNode = prevNode.lastChild, + selectedNode = selection.getSelectedNode(), + commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element), + curNode = wysihtml5.dom.getParentElement(selectedNode, { + query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote" + }, false, commonAncestorNode || composer.element); + + if (curNode) { + domNode(curNode).transferContentTo(prevNode, true); + selection.setAfter(selNode); + return true; + } + } + } + } + return false; + }, + + /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */ + /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */ + fixDeleteInTheBeginningOfLi: function(composer) { + if (wysihtml5.browser.hasLiDeletingProblem()) { + var selection = composer.selection.getSelection(), + aNode = selection.anchorNode, + listNode, prevNode, firstNode, + isInBeginnig = composer.selection.caretIsFirstInSelection(); + + // Fix caret at the beginnig of first textNode in LI + if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) { + aNode = aNode.parentNode; + isInBeginnig = true; + } + + if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") { + prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) { + prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + } + if (prevNode) { + firstNode = aNode.firstChild; + domNode(aNode).transferContentTo(prevNode, true); + if (firstNode) { + composer.selection.setBefore(firstNode); + } else if (prevNode) { + if (prevNode.nodeType === 1) { + if (prevNode.lastChild) { + composer.selection.setAfter(prevNode.lastChild); + } else { + composer.selection.selectNode(prevNode); + } + } else { + composer.selection.setAfter(prevNode); + } + } return true; } } } - } - return false; - }; + return false; + }, + + // Table management + // If present enableObjectResizing and enableInlineTableEditing command should be called with false to prevent native table handlers + initTableHandling: function() { + var hideHandlers = function() { + window.removeEventListener('load', hideHandlers); + this.doc.execCommand("enableObjectResizing", false, "false"); + this.doc.execCommand("enableInlineTableEditing", false, "false"); + }.bind(this), + iframeInitiator = (function() { + hideHandlers.call(this); + actions.removeListeners(this.sandbox.getIframe(), ["focus", "mouseup", "mouseover"], iframeInitiator); + }).bind(this); + + if( this.doc.execCommand && + wysihtml5.browser.supportsCommand(this.doc, "enableObjectResizing") && + wysihtml5.browser.supportsCommand(this.doc, "enableInlineTableEditing")) + { + if (this.sandbox.getIframe) { + actions.addListeners(this.sandbox.getIframe(), ["focus", "mouseup", "mouseover"], iframeInitiator); + } else { + window.addEventListener('load', hideHandlers); + } + } + this.tableSelection = wysihtml5.quirks.tableCellsSelection(this.element, this.parent); + }, + + // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature) + // Returns true if some corrections is applied so events know when to prevent default + doLineBreaksModeEnterWithCaret: function(composer) { + var breakNodes = "p, pre, div, blockquote", + caretInfo, parent, txtNode, + ret = false; + + caretInfo = composer.selection.getNodesNearCaret(); + if (caretInfo) { + + if (caretInfo.caretNode || caretInfo.nextNode) { + parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2); + if (parent === composer.element) { + parent = undefined; + } + } + + if (parent && caretInfo.caretNode) { + if (domNode(caretInfo.caretNode).is.lineBreak()) { + + if (composer.config.doubleLineBreakEscapesBlock) { + // Double enter (enter on blank line) exits block element in useLineBreaks mode. + ret = true; + caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode); + + // Ensure surplous line breaks are not added to preceding element + if (domNode(caretInfo.nextNode).is.lineBreak()) { + caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode); + } - /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */ - /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */ - var fixDeleteInTheBeginningOfLi = function(composer) { - if (wysihtml5.browser.hasLiDeletingProblem()) { - var selection = composer.selection.getSelection(), - aNode = selection.anchorNode, - listNode, prevNode, firstNode, - isInBeginnig = composer.selection.caretIsFirstInSelection(); - - // Fix caret at the beginnig of first textNode in LI - if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) { - aNode = aNode.parentNode; - isInBeginnig = true; - } - - if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") { - prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); - if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) { - prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); - } - if (prevNode) { - firstNode = aNode.firstChild; - domNode(aNode).transferContentTo(prevNode, true); - if (firstNode) { - composer.selection.setBefore(firstNode); - } else if (prevNode) { - if (prevNode.nodeType === 1) { - if (prevNode.lastChild) { - composer.selection.setAfter(prevNode.lastChild); + var brNode = composer.doc.createElement('br'); + if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) { + parent.parentNode.insertBefore(brNode, parent.nextSibling); } else { - composer.selection.selectNode(prevNode); + composer.selection.splitElementAtCaret(parent, brNode); } - } else { - composer.selection.setAfter(prevNode); + + // Ensure surplous blank lines are not added to preceding element + if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) { + // Replaces blank lines at the beginning of textnode + caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, ''); + } + composer.selection.setBefore(brNode); } + + } else if (caretInfo.caretNode.nodeType === 3 && wysihtml5.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) { + + // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens. + // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br + ret = true; + var br1 = composer.doc.createElement('br'), + br2 = composer.doc.createElement('br'), + f = composer.doc.createDocumentFragment(); + f.appendChild(br1); + f.appendChild(br2); + composer.selection.insertNode(f); + composer.selection.setBefore(br2); + } - return true; } } + return ret; } - return false; - } + }; var handleDeleteKeyPress = function(event, composer) { var selection = composer.selection, element = composer.element; if (selection.isCollapsed()) { - if (handleUneditableDeletion(composer)) { + if (actions.handleUneditableDeletion(composer)) { event.preventDefault(); return; } - if (fixDeleteInTheBeginningOfLi(composer)) { + if (actions.fixDeleteInTheBeginningOfLi(composer)) { event.preventDefault(); return; } - if (fixDeleteInTheBeginningOfBlock(composer)) { + if (actions.fixDeleteInTheBeginningOfBlock(composer)) { event.preventDefault(); return; } - if (fixLastBrDeletionInTable(composer)) { + if (actions.fixLastBrDeletionInTable(composer)) { event.preventDefault(); return; } @@ -17558,59 +17729,8 @@ wysihtml5.views.View = Base.extend( caretInfo, parent, txtNode; if (composer.selection.isCollapsed()) { - caretInfo = composer.selection.getNodesNearCaret(); - if (caretInfo) { - - if (caretInfo.caretNode || caretInfo.nextNode) { - parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2); - if (parent === composer.element) { - parent = undefined; - } - } - - if (parent && caretInfo.caretNode) { - if (domNode(caretInfo.caretNode).is.lineBreak()) { - - if (composer.config.doubleLineBreakEscapesBlock) { - // Double enter (enter on blank line) exits block element in useLineBreaks mode. - event.preventDefault(); - caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode); - - // Ensure surplous line breaks are not added to preceding element - if (domNode(caretInfo.nextNode).is.lineBreak()) { - caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode); - } - - var brNode = composer.doc.createElement('br'); - if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) { - parent.parentNode.insertBefore(brNode, parent.nextSibling); - } else { - composer.selection.splitElementAtCaret(parent, brNode); - } - - // Ensure surplous blank lines are not added to preceding element - if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) { - // Replaces blank lines at the beginning of textnode - caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, ''); - } - composer.selection.setBefore(brNode); - } - - } else if (caretInfo.caretNode.nodeType === 3 && wysihtml5.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) { - - // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens. - // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br - event.preventDefault(); - var br1 = composer.doc.createElement('br'), - br2 = composer.doc.createElement('br'), - f = composer.doc.createDocumentFragment(); - f.appendChild(br1); - f.appendChild(br2); - composer.selection.insertNode(f); - composer.selection.setBefore(br2); - - } - } + if (actions.doLineBreaksModeEnterWithCaret(composer)) { + event.preventDefault(); } } } @@ -17797,31 +17917,10 @@ wysihtml5.views.View = Base.extend( }).bind(this), 0); }; - // Table management - // If present enableObjectResizing and enableInlineTableEditing command should be called with false to prevent native table handlers - var initTableHandling = function () { - var hideHandlers = function () { - window.removeEventListener('load', hideHandlers); - this.doc.execCommand("enableObjectResizing", false, "false"); - this.doc.execCommand("enableInlineTableEditing", false, "false"); - }.bind(this), - iframeInitiator = (function() { - hideHandlers.call(this); - removeListeners(this.sandbox.getIframe(), ["focus", "mouseup", "mouseover"], iframeInitiator); - }).bind(this); - - if( this.doc.execCommand && - wysihtml5.browser.supportsCommand(this.doc, "enableObjectResizing") && - wysihtml5.browser.supportsCommand(this.doc, "enableInlineTableEditing")) - { - if (this.sandbox.getIframe) { - addListeners(this.sandbox.getIframe(), ["focus", "mouseup", "mouseover"], iframeInitiator); - } else { - window.addEventListener('load', hideHandlers); - } - } - this.tableSelection = wysihtml5.quirks.tableCellsSelection(this.element, this.parent); - }; + + + // Testing requires actions to be accessible from out of scope + wysihtml5.views.Composer.prototype.observeActions = actions; wysihtml5.views.Composer.prototype.observe = function() { var that = this, @@ -17847,14 +17946,14 @@ wysihtml5.views.View = Base.extend( // --------- User interactions -- if (this.config.handleTables) { // If handleTables option is true, table handling functions are bound - initTableHandling.call(this); + actions.initTableHandling.call(this); } - addListeners(focusBlurElement, ["drop", "paste", "mouseup", "focus", "keyup"], handleUserInteraction.bind(this)); + actions.addListeners(focusBlurElement, ["drop", "paste", "mouseup", "focus", "keyup"], handleUserInteraction.bind(this)); focusBlurElement.addEventListener("focus", handleFocus.bind(this), false); focusBlurElement.addEventListener("blur", handleBlur.bind(this), false); - addListeners(this.element, ["drop", "paste", "beforepaste"], handlePaste.bind(this), false); + actions.addListeners(this.element, ["drop", "paste", "beforepaste"], handlePaste.bind(this), false); this.element.addEventListener("copy", handleCopy.bind(this), false); this.element.addEventListener("mousedown", handleMouseDown.bind(this), false); this.element.addEventListener("click", handleClick.bind(this), false);