diff --git a/src/views/composer.observe.js b/src/views/composer.observe.js index 3d280f3..e836494 100644 --- a/src/views/composer.observe.js +++ b/src/views/composer.observe.js @@ -18,155 +18,184 @@ "73": "italic", // I "85": "underline" // U }; - - // 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); - } - }; - - // 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); - } - }; - - // 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; - } + + var actions = { + + // 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); } - } - 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)) { - return true; + // 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); } - 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; - }; - - // 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); + }, + + // 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; - }; - - /* 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; + return false; + }, + + // 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; } - - 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}); + 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) + 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; + } + } } - 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); + } + 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.selectNode(prevNode); + composer.selection.setAfter(prevNode); } - } else { - composer.selection.setAfter(prevNode); } + return true; } - return true; } } + 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); } - 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; } @@ -425,31 +454,10 @@ }).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, @@ -475,14 +483,14 @@ // --------- 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/test/index.html b/test/index.html index 9f46dfa..dd50645 100644 --- a/test/index.html +++ b/test/index.html @@ -173,7 +173,8 @@ ' diff --git a/test/views/composer_test.js b/test/views/composer_test.js new file mode 100644 index 0000000..89e1a02 --- /dev/null +++ b/test/views/composer_test.js @@ -0,0 +1,63 @@ +if (wysihtml5.browser.supported()) { + + module("wysihtml5.view.composer", { + setup: function() { + this.editableArea = document.createElement("div"); + this.editableArea.id = "wysihtml5-test-editable"; + this.editableArea.innerHTML = ""; + this.rules = { + parserRules: { tags: { + "strong": true, + "p": true, + "ul": true, + "li": true, + "h1": true, + "h2": true + } } + } + + document.body.appendChild(this.editableArea); + }, + + teardown: function() { + var leftover; + if (this.editableArea && this.editableArea.parentNode) { + this.editableArea.parentNode.removeChild(this.editableArea); + } + }, + + setCaretTo: function(editor, el, offset) { + var r1 = editor.composer.selection.createRange(); + + r1.setEnd(el, offset); + r1.setStart(el, offset); + editor.composer.selection.setSelection(r1); + }, + }); + +// Editor initiation tests + asyncTest("fixDeleteInTheBeginningOfBlock", function() { + expect(2); + var that = this; + var editor = new wysihtml5.Editor(this.editableArea, this.rules); + editor.on("load", function() { + var e = that.editableArea, + composer = editor.composer; + + e.innerHTML = "

test

line1
line2

"; + that.setCaretTo(editor, e.childNodes[1].childNodes[0], 0); + composer.observeActions.fixDeleteInTheBeginningOfBlock(composer); + equal(e.innerHTML, "

testline1
line2

", "Merges previous and current paragraph node correctly"); + + e.innerHTML = "

test


line1
line2

"; + that.setCaretTo(editor, e.childNodes[1], 0); + composer.observeActions.fixDeleteInTheBeginningOfBlock(composer); + equal(e.innerHTML, "

test
line1
line2

", "Merges previous and current paragraph node correctly"); + + + start(); + }); + }); + + +}