vendor/assets/javascripts/wysihtml5x.js in wysihtml5x-rails-0.4.16 vs vendor/assets/javascripts/wysihtml5x.js in wysihtml5x-rails-0.4.17

- old
+ new

@@ -1,10 +1,86 @@ // TODO: in future try to replace most inline compability checks with polyfills for code readability -// element.textContent polyfill. -// Unsupporting browsers: IE8 +// IE8 SUPPORT BLOCK +// You can compile wuthout all this if IE8 is not needed +// addEventListener, removeEventListener +// TODO: make usage of wysihtml5.dom.observe obsolete +(function() { + if (!Event.prototype.preventDefault) { + Event.prototype.preventDefault=function() { + this.returnValue=false; + }; + } + if (!Event.prototype.stopPropagation) { + Event.prototype.stopPropagation=function() { + this.cancelBubble=true; + }; + } + if (!Element.prototype.addEventListener) { + var eventListeners=[]; + + var addEventListener=function(type,listener /*, useCapture (will be ignored) */) { + var self=this; + var wrapper=function(e) { + e.target=e.srcElement; + e.currentTarget=self; + if (listener.handleEvent) { + listener.handleEvent(e); + } else { + listener.call(self,e); + } + }; + if (type=="DOMContentLoaded") { + var wrapper2=function(e) { + if (document.readyState=="complete") { + wrapper(e); + } + }; + document.attachEvent("onreadystatechange",wrapper2); + eventListeners.push({object:this,type:type,listener:listener,wrapper:wrapper2}); + + if (document.readyState=="complete") { + var e=new Event(); + e.srcElement=window; + wrapper2(e); + } + } else { + this.attachEvent("on"+type,wrapper); + eventListeners.push({object:this,type:type,listener:listener,wrapper:wrapper}); + } + }; + var removeEventListener=function(type,listener /*, useCapture (will be ignored) */) { + var counter=0; + while (counter<eventListeners.length) { + var eventListener=eventListeners[counter]; + if (eventListener.object==this && eventListener.type==type && eventListener.listener==listener) { + if (type=="DOMContentLoaded") { + this.detachEvent("onreadystatechange",eventListener.wrapper); + } else { + this.detachEvent("on"+type,eventListener.wrapper); + } + eventListeners.splice(counter, 1); + break; + } + ++counter; + } + }; + Element.prototype.addEventListener=addEventListener; + Element.prototype.removeEventListener=removeEventListener; + if (HTMLDocument) { + HTMLDocument.prototype.addEventListener=addEventListener; + HTMLDocument.prototype.removeEventListener=removeEventListener; + } + if (Window) { + Window.prototype.addEventListener=addEventListener; + Window.prototype.removeEventListener=removeEventListener; + } + } +})(); + +// element.textContent polyfill. if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(Element.prototype, "textContent").get) { (function() { var innerText = Object.getOwnPropertyDescriptor(Element.prototype, "innerText"); Object.defineProperty(Element.prototype, "textContent", { @@ -22,23 +98,50 @@ // isArray polyfill for ie8 if(!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; +} + +// Function.prototype.bind() +// TODO: clean the code from variable 'that' as it can be confusing +if (!Function.prototype.bind) { + Function.prototype.bind = function(oThis) { + if (typeof this !== 'function') { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function() {}, + fBound = function() { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; };/** - * @license wysihtml5x v0.4.16 + * @license wysihtml5x v0.4.17 * https://github.com/Edicy/wysihtml5 * * Author: Christopher Blum (https://github.com/tiff) * Secondary author of extended features: Oliver Pulges (https://github.com/pulges) * * Copyright (C) 2012 XING AG * Licensed under the MIT license (MIT) * */ var wysihtml5 = { - version: "0.4.16", + version: "0.4.17", // namespaces commands: {}, dom: {}, quirks: {}, @@ -46,46 +149,44 @@ lang: {}, selection: {}, views: {}, INVISIBLE_SPACE: "\uFEFF", + INVISIBLE_SPACE_REG_EXP: /\uFEFF/g, EMPTY_FUNCTION: function() {}, ELEMENT_NODE: 1, TEXT_NODE: 3, BACKSPACE_KEY: 8, ENTER_KEY: 13, ESCAPE_KEY: 27, SPACE_KEY: 32, + TAB_KEY: 9, DELETE_KEY: 46 }; ;/** * Rangy, a cross-browser JavaScript range and selection library - * http://code.google.com/p/rangy/ + * https://github.com/timdown/rangy * * Copyright 2014, Tim Down * Licensed under the MIT license. - * Version: 1.3alpha.20140804 - * Build date: 4 August 2014 + * Version: 1.3.0-alpha.20140921 + * Build date: 21 September 2014 */ -(function(factory, global) { +(function(factory, root) { if (typeof define == "function" && define.amd) { // AMD. Register as an anonymous module. define(factory); -/* - TODO: look into this properly. - - } else if (typeof exports == "object") { - // Node/CommonJS style for Browserify - module.exports = factory; -*/ + } else if (typeof module != "undefined" && typeof exports == "object") { + // Node/CommonJS style + module.exports = factory(); } else { - // No AMD or CommonJS support so we place Rangy in a global variable - global.rangy = factory(); + // No AMD or CommonJS support so we place Rangy in (probably) the global variable + root.rangy = factory(); } })(function() { var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; @@ -148,55 +249,57 @@ return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; } var modules = {}; + var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED); + + var util = { + isHostMethod: isHostMethod, + isHostObject: isHostObject, + isHostProperty: isHostProperty, + areHostMethods: areHostMethods, + areHostObjects: areHostObjects, + areHostProperties: areHostProperties, + isTextRange: isTextRange, + getBody: getBody + }; + var api = { - version: "1.3alpha.20140804", + version: "1.3.0-alpha.20140921", initialized: false, + isBrowser: isBrowser, supported: true, - - util: { - isHostMethod: isHostMethod, - isHostObject: isHostObject, - isHostProperty: isHostProperty, - areHostMethods: areHostMethods, - areHostObjects: areHostObjects, - areHostProperties: areHostProperties, - isTextRange: isTextRange, - getBody: getBody - }, - + util: util, features: {}, - modules: modules, config: { alertOnFail: true, alertOnWarn: false, preferTextRange: false, autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize } }; function consoleLog(msg) { - if (isHostObject(window, "console") && isHostMethod(window.console, "log")) { - window.console.log(msg); + if (typeof console != UNDEFINED && isHostMethod(console, "log")) { + console.log(msg); } } function alertOrLog(msg, shouldAlert) { - if (shouldAlert) { - window.alert(msg); + if (isBrowser && shouldAlert) { + alert(msg); } else { consoleLog(msg); } } function fail(reason) { api.initialized = true; api.supported = false; - alertOrLog("Rangy is not supported on this page in your browser. Reason: " + reason, api.config.alertOnFail); + alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail); } api.fail = fail; function warn(msg) { @@ -204,87 +307,106 @@ } api.warn = warn; // Add utility extend() method + var extend; if ({}.hasOwnProperty) { - api.util.extend = function(obj, props, deep) { + util.extend = extend = function(obj, props, deep) { var o, p; for (var i in props) { if (props.hasOwnProperty(i)) { o = obj[i]; p = props[i]; if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") { - api.util.extend(o, p, true); + extend(o, p, true); } obj[i] = p; } } // Special case for toString, which does not show up in for...in loops in IE <= 8 if (props.hasOwnProperty("toString")) { obj.toString = props.toString; } return obj; }; + + util.createOptions = function(optionsParam, defaults) { + var options = {}; + extend(options, defaults); + if (optionsParam) { + extend(options, optionsParam); + } + return options; + }; } else { fail("hasOwnProperty not supported"); } + + // Test whether we're in a browser and bail out if not + if (!isBrowser) { + fail("Rangy can only run in a browser"); + } // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not (function() { - var el = document.createElement("div"); - el.appendChild(document.createElement("span")); - var slice = [].slice; var toArray; - try { - if (slice.call(el.childNodes, 0)[0].nodeType == 1) { - toArray = function(arrayLike) { - return slice.call(arrayLike, 0); - }; - } - } catch (e) {} + if (isBrowser) { + var el = document.createElement("div"); + el.appendChild(document.createElement("span")); + var slice = [].slice; + try { + if (slice.call(el.childNodes, 0)[0].nodeType == 1) { + toArray = function(arrayLike) { + return slice.call(arrayLike, 0); + }; + } + } catch (e) {} + } + if (!toArray) { toArray = function(arrayLike) { var arr = []; for (var i = 0, len = arrayLike.length; i < len; ++i) { arr[i] = arrayLike[i]; } return arr; }; } - api.util.toArray = toArray; + util.toArray = toArray; })(); - // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or // normalization of event properties var addListener; - if (isHostMethod(document, "addEventListener")) { - addListener = function(obj, eventType, listener) { - obj.addEventListener(eventType, listener, false); - }; - } else if (isHostMethod(document, "attachEvent")) { - addListener = function(obj, eventType, listener) { - obj.attachEvent("on" + eventType, listener); - }; - } else { - fail("Document does not have required addEventListener or attachEvent method"); + if (isBrowser) { + if (isHostMethod(document, "addEventListener")) { + addListener = function(obj, eventType, listener) { + obj.addEventListener(eventType, listener, false); + }; + } else if (isHostMethod(document, "attachEvent")) { + addListener = function(obj, eventType, listener) { + obj.attachEvent("on" + eventType, listener); + }; + } else { + fail("Document does not have required addEventListener or attachEvent method"); + } + + util.addListener = addListener; } - api.util.addListener = addListener; - var initListeners = []; function getErrorDesc(ex) { return ex.message || ex.description || String(ex); } // Initialization function init() { - if (api.initialized) { + if (!isBrowser || api.initialized) { return; } var testRange; var implementsDomRange = false, implementsTextRange = false; @@ -366,11 +488,13 @@ for (var i = 0, len = shimListeners.length; i < len; ++i) { shimListeners[i](win); } } - api.shim = api.createMissingNativeApi = shim; + if (isBrowser) { + api.shim = api.createMissingNativeApi = shim; + } function Module(name, dependencies, initializer) { this.name = name; this.dependencies = dependencies; this.initialized = false; @@ -418,24 +542,28 @@ createError: function(msg) { return new Error("Error in Rangy " + this.name + " module: " + msg); } }; - function createModule(isCore, name, dependencies, initFunc) { + function createModule(name, dependencies, initFunc) { var newModule = new Module(name, dependencies, function(module) { if (!module.initialized) { module.initialized = true; try { initFunc(api, module); module.supported = true; } catch (ex) { var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex); consoleLog(errorMessage); + if (ex.stack) { + consoleLog(ex.stack); + } } } }); modules[name] = newModule; + return newModule; } api.createModule = function(name) { // Allow 2 or 3 arguments (second argument is an optional array of dependencies) var initFunc, dependencies; @@ -445,20 +573,20 @@ } else { initFunc = arguments[2]; dependencies = arguments[1]; } - var module = createModule(false, name, dependencies, initFunc); + var module = createModule(name, dependencies, initFunc); // Initialize the module immediately if the core is already initialized - if (api.initialized) { + if (api.initialized && api.supported) { module.init(); } }; api.createCoreModule = function(name, dependencies, initFunc) { - createModule(true, name, dependencies, initFunc); + createModule(name, dependencies, initFunc); }; /*----------------------------------------------------------------------------------------------------------------*/ // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately @@ -470,42 +598,10 @@ function SelectionPrototype() {} api.selectionPrototype = new SelectionPrototype(); /*----------------------------------------------------------------------------------------------------------------*/ - // Wait for document to load before running tests - - var docReady = false; - - var loadHandler = function(e) { - if (!docReady) { - docReady = true; - if (!api.initialized && api.config.autoInitialize) { - init(); - } - } - }; - - // Test whether we have window and document objects that we will need - if (typeof window == UNDEFINED) { - fail("No window found"); - return; - } - if (typeof document == UNDEFINED) { - fail("No document found"); - return; - } - - if (isHostMethod(document, "addEventListener")) { - document.addEventListener("DOMContentLoaded", loadHandler, false); - } - - // Add a fallback in case the DOMContentLoaded event isn't supported - addListener(window, "load", loadHandler); - - /*----------------------------------------------------------------------------------------------------------------*/ - // DOM utility methods used by Rangy api.createCoreModule("DomUtil", [], function(api, module) { var UNDEF = "undefined"; var util = api.util; @@ -2383,11 +2479,11 @@ }; } /*--------------------------------------------------------------------------------------------------------*/ - // Test for IE 9 deleteContents() and extractContents() bug and correct it. See issue 107. + // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. var el = document.createElement("div"); el.innerHTML = "123"; var textNode = el.firstChild; var body = getBody(document); @@ -2741,11 +2837,11 @@ // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which // implementation to use by default. if (!api.features.implementsDomRange || api.config.preferTextRange) { // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work - var globalObj = (function() { return this; })(); + var globalObj = (function(f) { return f("return this;")(); })(Function); if (typeof globalObj.Range == "undefined") { globalObj.Range = WrappedTextRange; } api.createNativeRange = function(doc) { @@ -3277,11 +3373,15 @@ previousRangeCount = 0; } // Clone the native range so that changing the selected range does not affect the selection. // This is contrary to the spec but is the only way to achieve consistency between browsers. See // issue 80. - this.nativeSelection.addRange(getNativeRange(range).cloneRange()); + var clonedNativeRange = getNativeRange(range).cloneRange(); + try { + this.nativeSelection.addRange(clonedNativeRange); + } catch (ex) { + } // Check whether adding the range was successful this.rangeCount = this.nativeSelection.rangeCount; if (this.rangeCount == previousRangeCount + 1) { @@ -3790,37 +3890,61 @@ }); /*----------------------------------------------------------------------------------------------------------------*/ + // Wait for document to load before initializing + var docReady = false; + + var loadHandler = function(e) { + if (!docReady) { + docReady = true; + if (!api.initialized && api.config.autoInitialize) { + init(); + } + } + }; + + if (isBrowser) { + // Test whether the document has already been loaded and initialize immediately if so + if (document.readyState == "complete") { + loadHandler(); + } else { + if (isHostMethod(document, "addEventListener")) { + document.addEventListener("DOMContentLoaded", loadHandler, false); + } + + // Add a fallback in case the DOMContentLoaded event isn't supported + addListener(window, "load", loadHandler); + } + } + return api; }, this);;/** * Selection save and restore module for Rangy. * Saves and restores user selections using marker invisible elements in the DOM. * * Part of Rangy, a cross-browser JavaScript range and selection library - * http://code.google.com/p/rangy/ + * https://github.com/timdown/rangy * * Depends on Rangy core. * * Copyright 2014, Tim Down * Licensed under the MIT license. - * Version: 1.3alpha.20140804 - * Build date: 4 August 2014 + * Version: 1.3.0-alpha.20140921 + * Build date: 21 September 2014 */ -(function(factory, global) { +(function(factory, root) { if (typeof define == "function" && define.amd) { // AMD. Register as an anonymous module with a dependency on Rangy. - define(["rangy"], factory); - /* - } else if (typeof exports == "object") { - // Node/CommonJS style for Browserify - module.exports = factory; - */ + define(["./rangy-core"], factory); + } else if (typeof module != "undefined" && typeof exports == "object") { + // Node/CommonJS style + module.exports = factory( require("rangy") ); } else { - // No AMD or CommonJS support so we use the rangy global variable - factory(global.rangy); + // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) + factory(root.rangy); } })(function(rangy) { rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) { var dom = api.dom; @@ -4301,17 +4425,10 @@ hasCurrentStyleProperty: function() { return "currentStyle" in testElement; }, /** - * Firefox on OSX navigates through history when hitting CMD + Arrow right/left - */ - hasHistoryIssue: function() { - return isGecko && navigator.platform.substr(0, 3) === "Mac"; - }, - - /** * Whether the browser inserts a <br> when pressing enter in a contentEditable element */ insertsLineBreaksOnReturn: function() { return isGecko; }, @@ -5909,11 +6026,14 @@ }, // Rename unknown tags to this DEFAULT_NODE_NAME = "span", WHITE_SPACE_REG_EXP = /\s+/, defaultRules = { tags: {}, classes: {} }, - currentRules = {}; + currentRules = {}, + blockElements = ["ADDRESS" ,"BLOCKQUOTE" ,"CENTER" ,"DIR" ,"DIV" ,"DL" ,"FIELDSET" , + "FORM", "H1" ,"H2" ,"H3" ,"H4" ,"H5" ,"H6" ,"ISINDEX" ,"MENU", + "NOFRAMES", "NOSCRIPT" ,"OL" ,"P" ,"PRE","TABLE", "UL"]; /** * Iterates over all childs of the element, recreates them, appends them into a document fragment * which later replaces the entire body content */ @@ -5976,11 +6096,12 @@ oldChildsLength = oldChilds.length, method = NODE_TYPE_MAPPING[oldNodeType], i = 0, fragment, newNode, - newChild; + newChild, + nodeDisplay; // Passes directly elemets with uneditable class if (uneditableClass && oldNodeType === 1 && wysihtml5.dom.hasClass(oldNode, uneditableClass)) { return oldNode; } @@ -6003,11 +6124,17 @@ fragment.insertBefore(newChild, fragment.firstChild); } } } - if (wysihtml5.dom.getStyle("display").from(oldNode) === "block") { + nodeDisplay = wysihtml5.dom.getStyle("display").from(oldNode); + + if (nodeDisplay === '') { + // Handle display style when element not in dom + nodeDisplay = wysihtml5.lang.array(blockElements).contains(oldNode.tagName) ? "block" : ""; + } + if (wysihtml5.lang.array(["block", "flex", "table"]).contains(nodeDisplay)) { fragment.appendChild(oldNode.ownerDocument.createElement("br")); } // TODO: try to minimize surplus spaces if (wysihtml5.lang.array([ @@ -6479,19 +6606,18 @@ newNode.setAttribute("height", attributes.height); } } } - var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g; function _handleText(oldNode) { var nextSibling = oldNode.nextSibling; if (nextSibling && nextSibling.nodeType === wysihtml5.TEXT_NODE) { // Concatenate text nodes - nextSibling.data = oldNode.data.replace(INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(INVISIBLE_SPACE_REG_EXP, ""); + nextSibling.data = oldNode.data.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, ""); } else { // \uFEFF = wysihtml5.INVISIBLE_SPACE (used as a hack in certain rich text editing situations) - var data = oldNode.data.replace(INVISIBLE_SPACE_REG_EXP, ""); + var data = oldNode.data.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, ""); return oldNode.ownerDocument.createTextNode(data); } } function _handleComment(oldNode) { @@ -8830,23 +8956,100 @@ range.setStartBefore(node); range.setEndBefore(node); return this.setSelection(range); }, + // Constructs a self removing whitespace (ain absolute positioned span) for placing selection caret when normal methods fail. + // Webkit has an issue with placing caret into places where there are no textnodes near by. + creteTemporaryCaretSpaceAfter: function (node) { + var caretPlaceholder = this.doc.createElement('span'), + caretPlaceholderText = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE), + placeholderRemover = (function(event) { + // Self-destructs the caret and keeps the text inserted into it by user + var lastChild; + + this.contain.removeEventListener('mouseup', placeholderRemover); + this.contain.removeEventListener('keydown', keyDownHandler); + this.contain.removeEventListener('touchstart', placeholderRemover); + this.contain.removeEventListener('focus', placeholderRemover); + this.contain.removeEventListener('blur', placeholderRemover); + this.contain.removeEventListener('paste', delayedPlaceholderRemover); + this.contain.removeEventListener('drop', delayedPlaceholderRemover); + this.contain.removeEventListener('beforepaste', delayedPlaceholderRemover); + + // If user inserted sth it is in the placeholder and sgould be unwrapped and stripped of invisible whitespace hack + // Otherwise the wrapper can just be removed + if (caretPlaceholder && caretPlaceholder.parentNode) { + caretPlaceholder.innerHTML = caretPlaceholder.innerHTML.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, ""); + if ((/[^\s]+/).test(caretPlaceholder.innerHTML)) { + lastChild = caretPlaceholder.lastChild; + wysihtml5.dom.unwrap(caretPlaceholder); + this.setAfter(lastChild); + } else { + caretPlaceholder.parentNode.removeChild(caretPlaceholder); + } + + } + }).bind(this), + delayedPlaceholderRemover = function (event) { + if (caretPlaceholder && caretPlaceholder.parentNode) { + setTimeout(placeholderRemover, 0); + } + }, + keyDownHandler = function(event) { + if (event.which !== 8 && event.which !== 91 && event.which !== 17 && (event.which !== 86 || (!event.ctrlKey && !event.metaKey))) { + placeholderRemover(); + } + }; + + caretPlaceholder.style.position = 'absolute'; + caretPlaceholder.style.display = 'block'; + caretPlaceholder.style.minWidth = '1px'; + caretPlaceholder.style.zIndex = '99999'; + caretPlaceholder.appendChild(caretPlaceholderText); + + node.parentNode.insertBefore(caretPlaceholder, node.nextSibling); + this.setBefore(caretPlaceholderText); + + // Remove the caret fix on any of the following events (some are delayed as content change happens after event) + this.contain.addEventListener('mouseup', placeholderRemover); + this.contain.addEventListener('keydown', keyDownHandler); + this.contain.addEventListener('touchstart', placeholderRemover); + this.contain.addEventListener('focus', placeholderRemover); + this.contain.addEventListener('blur', placeholderRemover); + this.contain.addEventListener('paste', delayedPlaceholderRemover); + this.contain.addEventListener('drop', delayedPlaceholderRemover); + this.contain.addEventListener('beforepaste', delayedPlaceholderRemover); + + return caretPlaceholder; + }, + /** * Set the caret after the given node * * @param {Object} node The element or text node where to position the caret in front of * @example * selection.setBefore(myElement); */ setAfter: function(node) { - var range = rangy.createRange(this.doc); + var range = rangy.createRange(this.doc), + originalScrollTop = this.doc.documentElement.scrollTop || this.doc.body.scrollTop || this.doc.defaultView.pageYOffset, + originalScrollLeft = this.doc.documentElement.scrollLeft || this.doc.body.scrollLeft || this.doc.defaultView.pageXOffset, + sel; range.setStartAfter(node); range.setEndAfter(node); - return this.setSelection(range); + this.composer.element.focus(); + this.doc.defaultView.scrollTo(originalScrollLeft, originalScrollTop); + sel = this.setSelection(range); + + // Webkit fails to add selection if there are no textnodes in that region + // (like an uneditable container at the end of content). + if (!sel) { + this.creteTemporaryCaretSpaceAfter(node); + } + return sel; }, /** * Ability to select/mark nodes * @@ -8952,22 +9155,35 @@ } return false; }, - // deletes selection contents making sure uneditables/unselectables are not partially deleted + // Deletes selection contents making sure uneditables/unselectables are not partially deleted + // Triggers wysihtml5:uneditable:delete custom event on all deleted uneditables if customevents suppoorted deleteContents: function() { var range = this.getRange(), - startParent, endParent; + startParent, endParent, uneditables, ev; if (this.unselectableClass) { if ((startParent = wysihtml5.dom.getParentElement(range.startContainer, { className: this.unselectableClass }, false, this.contain))) { range.setStartBefore(startParent); } if ((endParent = wysihtml5.dom.getParentElement(range.endContainer, { className: this.unselectableClass }, false, this.contain))) { range.setEndAfter(endParent); } + + // If customevents present notify uneditable elements of being deleted + uneditables = range.getNodes([1], (function (node) { + return wysihtml5.dom.hasClass(node, this.unselectableClass); + }).bind(this)); + for (var i = uneditables.length; i--;) { + try { + ev = new CustomEvent("wysihtml5:uneditable:delete"); + uneditables[i].dispatchEvent(ev); + } catch (err) {} + } + } range.deleteContents(); this.setSelection(range); }, @@ -9383,10 +9599,28 @@ selection = win.getSelection(); selection.modify("move", "left", "lineboundary"); selection.modify("extend", "right", "lineboundary"); }, + // collapses selection to current line beginning or end + toLineBoundary: function (location, collapse) { + collapse = (typeof collapse === 'undefined') ? false : collapse; + if (wysihtml5.browser.supportsSelectionModify()) { + var win = this.doc.defaultView, + selection = win.getSelection(); + + selection.modify("extend", location, "lineboundary"); + if (collapse) { + if (location === "left") { + selection.collapseToStart(); + } else if (location === "right") { + selection.collapseToEnd(); + } + } + } + }, + _selectLine_MSIE: function() { var range = this.doc.selection.createRange(), rangeTop = range.boundingTop, scrollWidth = this.doc.body.scrollWidth, rangeBottom, @@ -9548,14 +9782,18 @@ getSelection: function() { return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow); }, + // Sets selection in document to a given range + // Set selection method detects if it fails to set any selection in document and returns null on fail + // (especially needed in webkit where some ranges just can not create selection for no reason) setSelection: function(range) { var win = this.doc.defaultView || this.doc.parentWindow, selection = rangy.getSelection(win); - return selection.setSingleRange(range); + selection.setSingleRange(range); + return (selection && selection.anchorNode && selection.focusNode) ? selection : null; }, createRange: function() { return rangy.createRange(this.doc); }, @@ -10302,10 +10540,17 @@ var obj = wysihtml5.commands[command], args = wysihtml5.lang.array(arguments).get(), method = obj && obj.exec, result = null; + // If composer ahs placeholder unset it before command + // Do not apply on commands that are behavioral + if (this.composer.hasPlaceholderSet() && !wysihtml5.lang.array(['styleWithCSS', 'enableObjectResizing', 'enableInlineTableEditing']).contains(command)) { + this.composer.element.innerHTML = ""; + this.composer.selection.selectNode(this.composer.element); + } + this.editor.fire("beforecommand:composer"); if (method) { args.unshift(this.composer); result = method.apply(obj, args); @@ -11410,11 +11655,11 @@ }), isEmpty, list; // This space causes new lists to never break on enter var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g; - tempElement.innerHTML = tempElement.innerHTML.replace(INVISIBLE_SPACE_REG_EXP, ""); + tempElement.innerHTML = tempElement.innerHTML.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, ""); if (tempElement) { isEmpty = wysihtml5.lang.array(["", "<br>", wysihtml5.INVISIBLE_SPACE]).contains(tempElement.innerHTML); list = wysihtml5.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.uneditableContainerClassname); if (isEmpty) { @@ -12775,10 +13020,26 @@ "66": "bold", // B "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); + } + }; + var deleteAroundEditable = function(selection, uneditable, element) { // merge node with previous node from uneditable var prevNode = selection.getPreviousNode(uneditable, true), curNode = selection.getSelectedNode(); @@ -12809,11 +13070,14 @@ selection.setBefore(curNode); } } }; - var handleDeleteKeyPress = function(event, selection, element, composer) { + var handleDeleteKeyPress = function(event, composer) { + var selection = composer.selection, + element = composer.element; + if (selection.isCollapsed()) { if (selection.caretIsInTheBeginnig('LI')) { event.preventDefault(); composer.commands.exec('outdentList'); } else if (selection.caretIsInTheBeginnig()) { @@ -12868,251 +13132,261 @@ // Is &emsp; close enough to tab. Could not find enough counter arguments for now. composer.commands.exec("insertHTML", "&emsp;"); }; - wysihtml5.views.Composer.prototype.observe = function() { - var that = this, - state = this.getValue(false, false), - container = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(), - element = this.element, - focusBlurElement = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? element : this.sandbox.getWindow(), - pasteEvents = ["drop", "paste", "beforepaste"], - interactionEvents = ["drop", "paste", "mouseup", "focus", "keyup"]; + var handleDomNodeRemoved = function(event) { + if (this.domNodeRemovedInterval) { + clearInterval(domNodeRemovedInterval); + } + this.parent.fire("destroy:composer"); + }; - // --------- destroy:composer event --------- - dom.observe(container, "DOMNodeRemoved", function() { - clearInterval(domNodeRemovedInterval); - that.parent.fire("destroy:composer"); - }); + // Listens to "drop", "paste", "mouseup", "focus", "keyup" events and fires + var handleUserInteraction = function (event) { + this.parent.fire("beforeinteraction").fire("beforeinteraction:composer"); + setTimeout((function() { + this.parent.fire("interaction").fire("interaction:composer"); + }).bind(this), 0); + }; - // DOMNodeRemoved event is not supported in IE 8 - if (!browser.supportsMutationEvents()) { - var domNodeRemovedInterval = setInterval(function() { - if (!dom.contains(document.documentElement, container)) { - clearInterval(domNodeRemovedInterval); - that.parent.fire("destroy:composer"); - } - }, 250); - } + var handleFocus = function(event) { + this.parent.fire("focus", event).fire("focus:composer", event); - // --------- User interaction tracking -- + // Delay storing of state until all focus handler are fired + // especially the one which resets the placeholder + setTimeout((function() { + this.focusState = this.getValue(false, false); + }).bind(this), 0); + }; - dom.observe(focusBlurElement, interactionEvents, function() { - setTimeout(function() { - that.parent.fire("interaction").fire("interaction:composer"); - }, 0); - }); - - - if (this.config.handleTables) { - if(!this.tableClickHandle && this.doc.execCommand && wysihtml5.browser.supportsCommand(this.doc, "enableObjectResizing") && wysihtml5.browser.supportsCommand(this.doc, "enableInlineTableEditing")) { - if (this.sandbox.getIframe) { - this.tableClickHandle = dom.observe(container , ["focus", "mouseup", "mouseover"], function() { - that.doc.execCommand("enableObjectResizing", false, "false"); - that.doc.execCommand("enableInlineTableEditing", false, "false"); - that.tableClickHandle.stop(); - }); - } else { - setTimeout(function() { - that.doc.execCommand("enableObjectResizing", false, "false"); - that.doc.execCommand("enableInlineTableEditing", false, "false"); - }, 0); - } + var handleBlur = function(event) { + if (this.focusState !== this.getValue(false, false)) { + //create change event if supported (all except IE8) + var changeevent = event; + if(typeof Object.create == 'function') { + changeevent = Object.create(event, { type: { value: 'change' } }); } - this.tableSelection = wysihtml5.quirks.tableCellsSelection(element, that.parent); + this.parent.fire("change", changeevent).fire("change:composer", changeevent); } + this.parent.fire("blur", event).fire("blur:composer", event); + }; - // --------- Focus & blur logic --------- - dom.observe(focusBlurElement, "focus", function(event) { - that.parent.fire("focus", event).fire("focus:composer", event); + var handlePaste = function(event) { + this.parent.fire(event.type, event).fire(event.type + ":composer", event); + if (event.type === "paste") { + setTimeout((function() { + this.parent.fire("newword:composer"); + }).bind(this), 0); + } + }; - // Delay storing of state until all focus handler are fired - // especially the one which resets the placeholder - setTimeout(function() { state = that.getValue(false, false); }, 0); - }); - - dom.observe(focusBlurElement, "blur", function(event) { - if (state !== that.getValue(false, false)) { - //create change event if supported (all except IE8) - var changeevent = event; - if(typeof Object.create == 'function') { - changeevent = Object.create(event, { type: { value: 'change' } }); - } - that.parent.fire("change", changeevent).fire("change:composer", changeevent); - } - that.parent.fire("blur", event).fire("blur:composer", event); - }); - - // --------- Drag & Drop logic --------- - dom.observe(element, "dragenter", function() { - that.parent.fire("unset_placeholder"); - }); - - dom.observe(element, pasteEvents, function(event) { - that.parent.fire(event.type, event).fire(event.type + ":composer", event); - }); - - + var handleCopy = function(event) { if (this.config.copyedFromMarking) { - // If supported the copied source is based directly on selection + // If supported the copied source can be based directly on selection // Very useful for webkit based browsers where copy will otherwise contain a lot of code and styles based on whatever and not actually in selection. - dom.observe(element, "copy", function(event) { - if (event.clipboardData) { - event.clipboardData.setData("text/html", that.config.copyedFromMarking + that.selection.getHtml()); - event.clipboardData.setData("text/plain", that.selection.getPlainText()); - event.preventDefault(); - } - that.parent.fire(event.type, event).fire(event.type + ":composer", event); - }); + if (event.clipboardData) { + event.clipboardData.setData("text/html", this.config.copyedFromMarking + this.selection.getHtml()); + event.clipboardData.setData("text/plain", this.selection.getPlainText()); + event.preventDefault(); + } + this.parent.fire(event.type, event).fire(event.type + ":composer", event); } + }; - // --------- neword event --------- - dom.observe(element, "keyup", function(event) { - var keyCode = event.keyCode; - if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) { - that.parent.fire("newword:composer"); - } - }); + var handleKeyUp = function(event) { + var keyCode = event.keyCode; + if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) { + this.parent.fire("newword:composer"); + } + }; - this.parent.on("paste:composer", function() { - setTimeout(function() { that.parent.fire("newword:composer"); }, 0); - }); - - // --------- Make sure that images are selected when clicking on them --------- + var handleMouseDown = function(event) { if (!browser.canSelectImagesInContentEditable()) { - dom.observe(element, "mousedown", function(event) { - var target = event.target; - var allImages = element.querySelectorAll('img'), - notMyImages = element.querySelectorAll('.' + that.config.uneditableContainerClassname + ' img'), - myImages = wysihtml5.lang.array(allImages).without(notMyImages); + // Make sure that images are selected when clicking on them + var target = event.target, + allImages = this.element.querySelectorAll('img'), + notMyImages = this.element.querySelectorAll('.' + this.config.uneditableContainerClassname + ' img'), + myImages = wysihtml5.lang.array(allImages).without(notMyImages); - if (target.nodeName === "IMG" && wysihtml5.lang.array(myImages).contains(target)) { - that.selection.selectNode(target); - } - }); + if (target.nodeName === "IMG" && wysihtml5.lang.array(myImages).contains(target)) { + this.selection.selectNode(target); + } } + }; - // If uneditables configured makes click on uneditable moves caret after clicked element (so it can be deleted like text) - // If uneditable needs text selection itself event.stopPropagation can be used to prevent this behaviour + // TODO: mouseover is not actually a foolproof and obvious place for this, must be changed as it modifies dom on random basis + // Shows url in tooltip when hovering links or images + var handleMouseOver = function(event) { + var titlePrefixes = { + IMG: "Image: ", + A: "Link: " + }, + target = event.target, + nodeName = target.nodeName, + title; + + if (nodeName !== "A" && nodeName !== "IMG") { + return; + } + if(!target.hasAttribute("title")){ + title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src")); + target.setAttribute("title", title); + } + }; + + var handleClick = function(event) { if (this.config.uneditableContainerClassname) { - dom.observe(element, "click", function(event) { - var uneditable = wysihtml5.dom.getParentElement(event.target, { className: that.config.uneditableContainerClassname }, false, that.element); - if (uneditable) { - that.selection.setAfter(uneditable); - } - }); + // If uneditables is configured, makes clicking on uneditable move caret after clicked element (so it can be deleted like text) + // If uneditable needs text selection itself event.stopPropagation can be used to prevent this behaviour + var uneditable = wysihtml5.dom.getParentElement(event.target, { className: this.config.uneditableContainerClassname }, false, this.element); + if (uneditable) { + this.selection.setAfter(uneditable); + } } + }; + var handleDrop = function(event) { if (!browser.canSelectImagesInContentEditable()) { - dom.observe(element, "drop", function(event) { - // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case - setTimeout(function() { - that.selection.getSelection().removeAllRanges(); - }, 0); - }); + // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case + setTimeout((function() { + this.selection.getSelection().removeAllRanges(); + }).bind(this), 0); } + }; - if (browser.hasHistoryIssue() && browser.supportsSelectionModify()) { - dom.observe(element, "keydown", function(event) { - if (!event.metaKey && !event.ctrlKey) { - return; - } + var handleKeyDown = function(event) { + var keyCode = event.keyCode, + command = shortcuts[keyCode], + target, parent; - var keyCode = event.keyCode, - win = element.ownerDocument.defaultView, - selection = win.getSelection(); + // Shortcut logic + if ((event.ctrlKey || event.metaKey) && !event.altKey && command) { + this.commands.exec(command); + event.preventDefault(); + } - if (keyCode === 37 || keyCode === 39) { - if (keyCode === 37) { - selection.modify("extend", "left", "lineboundary"); - if (!event.shiftKey) { - selection.collapseToStart(); - } - } - if (keyCode === 39) { - selection.modify("extend", "right", "lineboundary"); - if (!event.shiftKey) { - selection.collapseToEnd(); - } - } - event.preventDefault(); - } - }); + if (keyCode === wysihtml5.BACKSPACE_KEY) { + // Delete key override for special cases + handleDeleteKeyPress(event, this); } - // --------- Shortcut logic --------- - dom.observe(element, "keydown", function(event) { - var keyCode = event.keyCode, - command = shortcuts[keyCode]; - if ((event.ctrlKey || event.metaKey) && !event.altKey && command) { - that.commands.exec(command); + // Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor + if (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY) { + target = this.selection.getSelectedNode(true); + if (target && target.nodeName === "IMG") { event.preventDefault(); - } - if (keyCode === 8) { - // delete key - handleDeleteKeyPress(event, that.selection, element, that); - } else if (that.config.handleTabKey && keyCode === 9) { - event.preventDefault(); - handleTabKeyDown(that, element); - } - }); - - // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor --------- - dom.observe(element, "keydown", function(event) { - var target = that.selection.getSelectedNode(true), - keyCode = event.keyCode, - parent; - if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete parent = target.parentNode; - // delete the <img> - parent.removeChild(target); - // and it's parent <a> too if it hasn't got any other child nodes + parent.removeChild(target);// delete the <img> + // And it's parent <a> too if it hasn't got any other child nodes if (parent.nodeName === "A" && !parent.firstChild) { parent.parentNode.removeChild(parent); } - - setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0); - event.preventDefault(); - } - }); - - // --------- IE 8+9 focus the editor when the iframe is clicked (without actually firing the 'focus' event on the <body>) --------- - if (!this.config.contentEditableMode && browser.hasIframeFocusIssue()) { - dom.observe(container, "focus", function() { setTimeout(function() { - if (that.doc.querySelector(":focus") !== that.element) { - that.focus(); - } + wysihtml5.quirks.redraw(element); }, 0); - }); + } + } - dom.observe(this.element, "blur", function() { - setTimeout(function() { - that.selection.getSelection().removeAllRanges(); - }, 0); - }); + if (this.config.handleTabKey && keyCode === wysihtml5.TAB_KEY) { + // TAB key handling + event.preventDefault(); + handleTabKeyDown(this, element); } - // --------- Show url in tooltip when hovering links or images --------- - var titlePrefixes = { - IMG: "Image: ", - A: "Link: " - }; + }; - dom.observe(element, "mouseover", function(event) { - var target = event.target, - nodeName = target.nodeName, - title; - if (nodeName !== "A" && nodeName !== "IMG") { - return; + var handleIframeFocus = function(event) { + setTimeout((function() { + if (this.doc.querySelector(":focus") !== this.element) { + this.focus(); } - var hasTitle = target.hasAttribute("title"); - if(!hasTitle){ - title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src")); - target.setAttribute("title", title); + }).bind(this), 0); + }; + + var handleIframeBlur = function(event) { + setTimeout((function() { + this.selection.getSelection().removeAllRanges(); + }).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 () { + this.doc.execCommand("enableObjectResizing", false, "false"); + this.doc.execCommand("enableInlineTableEditing", false, "false"); + }, + 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 { + setTimeout((function() { + hideHandlers.call(this); + }).bind(this), 0); } - }); + } + this.tableSelection = wysihtml5.quirks.tableCellsSelection(this.element, this.parent); + }; + + wysihtml5.views.Composer.prototype.observe = function() { + var that = this, + container = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(), + element = this.element, + focusBlurElement = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? this.element : this.sandbox.getWindow(); + + this.focusState = this.getValue(false, false); + + // --------- destroy:composer event --------- + container.addEventListener(["DOMNodeRemoved"], handleDomNodeRemoved.bind(this), false); + + // DOMNodeRemoved event is not supported in IE 8 + // TODO: try to figure out a polyfill style fix, so it could be transferred to polyfills and removed if ie8 is not needed + if (!browser.supportsMutationEvents()) { + this.domNodeRemovedInterval = setInterval(function() { + if (!dom.contains(document.documentElement, container)) { + handleDomNodeRemoved.call(this); + } + }, 250); + } + + // --------- User interactions -- + if (this.config.handleTables) { + // If handleTables option is true, table handling functions are bound + initTableHandling.call(this); + } + + 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); + this.element.addEventListener("copy", handleCopy.bind(this), false); + this.element.addEventListener("mousedown", handleMouseDown.bind(this), false); + this.element.addEventListener("mouseover", handleMouseOver.bind(this), false); + this.element.addEventListener("click", handleClick.bind(this), false); + this.element.addEventListener("drop", handleDrop.bind(this), false); + this.element.addEventListener("keyup", handleKeyUp.bind(this), false); + this.element.addEventListener("keydown", handleKeyDown.bind(this), false); + + this.element.addEventListener("dragenter", (function() { + this.parent.fire("unset_placeholder"); + }).bind(this), false); + + // --------- IE 8+9 focus the editor when the iframe is clicked (without actually firing the 'focus' event on the <body>) --------- + if (!this.config.contentEditableMode && browser.hasIframeFocusIssue()) { + container.addEventListener("focus", handleIframeFocus.bind(this), false); + container.addEventListener("blur", handleIframeBlur.bind(this), false); + } + }; })(wysihtml5); ;/** * Class that takes care that the value of the composer and the textarea is always in sync */