vendor/assets/javascripts/wysihtml5x.js in wysihtml5x-rails-0.4.13 vs vendor/assets/javascripts/wysihtml5x.js in wysihtml5x-rails-0.4.14

- old
+ new

@@ -23,22 +23,22 @@ if(!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; };/** - * @license wysihtml5x v0.4.13 + * @license wysihtml5x v0.4.14 * 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.13", + version: "0.4.14", // namespaces commands: {}, dom: {}, quirks: {}, @@ -4560,10 +4560,19 @@ return isWebKit; }, supportsMutationEvents: function() { return ("MutationEvent" in window); + }, + + /** + IE (at least up to 11) does not support clipboardData on event. + It is on window but cannot return text/html + Should actually check for clipboardData on paste event, but cannot in firefox + */ + supportsModenPaste: function () { + return !("clipboardData" in window); } }; })(); ;wysihtml5.lang.array = function(arr) { return { @@ -4761,16 +4770,29 @@ /** * @example * wysihtml5.lang.object({ foo: 1 }).clone(); * // => { foo: 1 } + * + * v0.4.14 adds options for deep clone : wysihtml5.lang.object({ foo: 1 }).clone(true); */ - clone: function() { + clone: function(deep) { var newObj = {}, i; + + if (obj === null || !wysihtml5.lang.object(obj).isPlainObject()) { + return obj; + } + for (i in obj) { - newObj[i] = obj[i]; + if(obj.hasOwnProperty(i)) { + if (deep) { + newObj[i] = wysihtml5.lang.object(obj[i]).clone(deep); + } else { + newObj[i] = obj[i]; + } + } } return newObj; }, /** @@ -4778,22 +4800,36 @@ * wysihtml5.lang.object([]).isArray(); * // => true */ isArray: function() { return Object.prototype.toString.call(obj) === "[object Array]"; + }, + + /** + * @example + * wysihtml5.lang.object(function() {}).isFunction(); + * // => true + */ + isFunction: function() { + return Object.prototype.toString.call(obj) === '[object Function]'; + }, + + isPlainObject: function () { + return Object.prototype.toString.call(obj) === '[object Object]'; } }; }; ;(function() { var WHITE_SPACE_START = /^\s+/, WHITE_SPACE_END = /\s+$/, - ENTITY_REG_EXP = /[&<>"]/g, + ENTITY_REG_EXP = /[&<>\t"]/g, ENTITY_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', - '"': "&quot;" + '"': "&quot;", + '\t':"&nbsp; " }; wysihtml5.lang.string = function(str) { str = String(str); return { /** @@ -4833,12 +4869,19 @@ /** * @example * wysihtml5.lang.string("hello<br>").escapeHTML(); * // => "hello&lt;br&gt;" */ - escapeHTML: function() { - return str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; }); + escapeHTML: function(linebreaks, convertSpaces) { + var html = str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; }); + if (linebreaks) { + html = html.replace(/(?:\r\n|\r|\n)/g, '<br />'); + } + if (convertSpaces) { + html = html.replace(/ /gi, "&nbsp; "); + } + return html; } }; }; })(); ;/** @@ -5814,11 +5857,13 @@ * } * }); * // => '<p class="red">foo</p><p>bar</p>' */ -wysihtml5.dom.parse = (function() { +wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { + /* TODO: Currently escaped module pattern as otherwise folloowing default swill be shared among multiple editors. + * Refactor whole code as this method while workind is kind of awkward too */ /** * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the * node isn't closed @@ -5832,12 +5877,11 @@ }, // Rename unknown tags to this DEFAULT_NODE_NAME = "span", WHITE_SPACE_REG_EXP = /\s+/, defaultRules = { tags: {}, classes: {} }, - currentRules = {}, - uneditableClass = false; + currentRules = {}; /** * Iterates over all childs of the element, recreates them, appends them into a document fragment * which later replaces the entire body content */ @@ -5854,41 +5898,49 @@ if (config.clearInternals === true) { clearInternals = true; } - if (config.uneditableClass) { - uneditableClass = config.uneditableClass; - } - if (isString) { element = wysihtml5.dom.getAsDom(elementOrHtml, context); } else { element = elementOrHtml; } + if (currentRules.selectors) { + _applySelectorRules(element, currentRules.selectors); + } + while (element.firstChild) { firstChild = element.firstChild; - newNode = _convert(firstChild, config.cleanUp, clearInternals); + newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass); if (newNode) { fragment.appendChild(newNode); } if (firstChild !== newNode) { element.removeChild(firstChild); } } + if (config.unjoinNbsps) { + // replace joined non-breakable spaces with unjoined + var txtnodes = wysihtml5.dom.getTextNodes(fragment); + for (var n = txtnodes.length; n--;) { + txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 "); + } + } + // Clear element contents element.innerHTML = ""; // Insert new DOM tree element.appendChild(fragment); return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element; } - function _convert(oldNode, cleanUp, clearInternals) { + function _convert(oldNode, cleanUp, clearInternals, uneditableClass) { var oldNodeType = oldNode.nodeType, oldChilds = oldNode.childNodes, oldChildsLength = oldChilds.length, method = NODE_TYPE_MAPPING[oldNodeType], i = 0, @@ -5909,20 +5961,24 @@ // false defines that tag should be removed but contents should remain (unwrap) fragment = oldNode.ownerDocument.createDocumentFragment(); for (i = oldChildsLength; i--;) { if (oldChilds[i]) { - newChild = _convert(oldChilds[i], cleanUp, clearInternals); + newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass); if (newChild) { if (oldChilds[i] === newChild) { i--; } fragment.insertBefore(newChild, fragment.firstChild); } } } + if (wysihtml5.dom.getStyle("display").from(oldNode) === "block") { + fragment.appendChild(oldNode.ownerDocument.createElement("br")); + } + // TODO: try to minimize surplus spaces if (wysihtml5.lang.array([ "div", "pre", "p", "table", "td", "th", "ul", "ol", "li", @@ -5947,11 +6003,11 @@ } // Converts all childnodes for (i=0; i<oldChildsLength; i++) { if (oldChilds[i]) { - newChild = _convert(oldChilds[i], cleanUp, clearInternals); + newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass); if (newChild) { if (oldChilds[i] === newChild) { i--; } newNode.appendChild(newChild); @@ -5980,16 +6036,35 @@ newNode.normalize(); } return newNode; } + function _applySelectorRules (element, selectorRules) { + var sel, method, els; + + for (sel in selectorRules) { + if (selectorRules.hasOwnProperty(sel)) { + if (wysihtml5.lang.object(selectorRules[sel]).isFunction()) { + method = selectorRules[sel]; + } else if (typeof(selectorRules[sel]) === "string" && elementHandlingMethods[selectorRules[sel]]) { + method = elementHandlingMethods[selectorRules[sel]]; + } + els = element.querySelectorAll(sel); + for (var i = els.length; i--;) { + method(els[i]); + } + } + } + } + function _handleElement(oldNode, clearInternals) { var rule, newNode, tagRules = currentRules.tags, nodeName = oldNode.nodeName.toLowerCase(), - scopeName = oldNode.scopeName; + scopeName = oldNode.scopeName, + renameTag; /** * We already parsed that element * ignore it! (yes, this sometimes happens in IE8 when the html is invalid) */ @@ -6037,18 +6112,29 @@ } else { // Remove empty unknown elements return null; } - newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName); - _handleAttributes(oldNode, newNode, rule, clearInternals); - _handleStyles(oldNode, newNode, rule); - // tests if type condition is met or node should be removed/unwrapped + // tests if type condition is met or node should be removed/unwrapped/renamed if (rule.one_of_type && !_testTypes(oldNode, currentRules, rule.one_of_type, clearInternals)) { - return (rule.remove_action && rule.remove_action == "unwrap") ? false : null; + if (rule.remove_action) { + if (rule.remove_action === "unwrap") { + return false; + } else if (rule.remove_action === "rename") { + renameTag = rule.remove_action_rename_to || DEFAULT_NODE_NAME; + } else { + return null; + } + } else { + return null; + } } + newNode = oldNode.ownerDocument.createElement(renameTag || rule.rename_tag || nodeName); + _handleAttributes(oldNode, newNode, rule, clearInternals); + _handleStyles(oldNode, newNode, rule); + oldNode = null; if (newNode.normalize) { newNode.normalize(); } return newNode; } @@ -6132,11 +6218,11 @@ // test for attributes in general against regex match if (definition.attrs) { for (a in definition.attrs) { if (definition.attrs.hasOwnProperty(a)) { - attr = _getAttribute(oldNode, a); + attr = wysihtml5.dom.getAttribute(oldNode, a); if (typeof(attr) === "string") { if (attr.search(definition.attrs[a]) > -1) { return true; } } @@ -6145,38 +6231,92 @@ } return false; } function _handleStyles(oldNode, newNode, rule) { - var s; + var s, v; if(rule && rule.keep_styles) { for (s in rule.keep_styles) { if (rule.keep_styles.hasOwnProperty(s)) { - if (s == "float") { + v = (s === "float") ? oldNode.style.styleFloat || oldNode.style.cssFloat : oldNode.style[s]; + // value can be regex and if so should match or style skipped + if (rule.keep_styles[s] instanceof RegExp && !(rule.keep_styles[s].test(v))) { + continue; + } + if (s === "float") { // IE compability - if (oldNode.style.styleFloat) { - newNode.style.styleFloat = oldNode.style.styleFloat; - } - if (oldNode.style.cssFloat) { - newNode.style.cssFloat = oldNode.style.cssFloat; - } + newNode.style[(oldNode.style.styleFloat) ? 'styleFloat': 'cssFloat'] = v; } else if (oldNode.style[s]) { - newNode.style[s] = oldNode.style[s]; + newNode.style[s] = v; } } } } + }; + + function _getAttributesBeginningWith(beginning, attributes) { + var returnAttributes = []; + for (var attr in attributes) { + if (attributes.hasOwnProperty(attr) && attr.indexOf(beginning) === 0) { + returnAttributes.push(attr); + } + } + return returnAttributes; } + function _checkAttribute(attributeName, attributeValue, methodName, nodeName) { + var method = attributeCheckMethods[methodName], + newAttributeValue; + + if (method) { + if (attributeValue || (attributeName === "alt" && nodeName == "IMG")) { + newAttributeValue = method(attributeValue); + if (typeof(newAttributeValue) === "string") { + return newAttributeValue; + } + } + } + + return false; + } + + function _checkAttributes(oldNode, local_attributes) { + var globalAttributes = wysihtml5.lang.object(currentRules.attributes || {}).clone(), // global values for check/convert values of attributes + checkAttributes = wysihtml5.lang.object(globalAttributes).merge( wysihtml5.lang.object(local_attributes || {}).clone()).get(), + attributes = {}, + oldAttributes = wysihtml5.dom.getAttributes(oldNode), + attributeName, newValue, matchingAttributes; + + for (attributeName in checkAttributes) { + if ((/\*$/).test(attributeName)) { + + matchingAttributes = _getAttributesBeginningWith(attributeName.slice(0,-1), oldAttributes); + for (var i = 0, imax = matchingAttributes.length; i < imax; i++) { + + newValue = _checkAttribute(matchingAttributes[i], oldAttributes[matchingAttributes[i]], checkAttributes[attributeName], oldNode.nodeName); + if (newValue !== false) { + attributes[matchingAttributes[i]] = newValue; + } + } + } else { + newValue = _checkAttribute(attributeName, oldAttributes[attributeName], checkAttributes[attributeName], oldNode.nodeName); + if (newValue !== false) { + attributes[attributeName] = newValue; + } + } + } + + return attributes; + } + // TODO: refactor. Too long to read function _handleAttributes(oldNode, newNode, rule, clearInternals) { var attributes = {}, // fresh new set of attributes to set on newNode setClass = rule.set_class, // classes to set addClass = rule.add_class, // add classes based on existing attributes addStyle = rule.add_style, // add styles based on existing attributes setAttributes = rule.set_attributes, // attributes to set on the current node - checkAttributes = rule.check_attributes, // check/convert values of attributes allowedClasses = currentRules.classes, i = 0, classes = [], styles = [], newClasses = [], @@ -6184,33 +6324,18 @@ classesLength, newClassesLength, currentClass, newClass, attributeName, - newAttributeValue, - method, - oldAttribute; + method; if (setAttributes) { attributes = wysihtml5.lang.object(setAttributes).clone(); } - if (checkAttributes) { - for (attributeName in checkAttributes) { - method = attributeCheckMethods[checkAttributes[attributeName]]; - if (!method) { - continue; - } - oldAttribute = _getAttribute(oldNode, attributeName); - if (oldAttribute || (attributeName === "alt" && oldNode.nodeName == "IMG")) { - newAttributeValue = method(oldAttribute); - if (typeof(newAttributeValue) === "string") { - attributes[attributeName] = newAttributeValue; - } - } - } - } + // check/convert values of attributes + attributes = wysihtml5.lang.object(attributes).merge(_checkAttributes(oldNode, rule.check_attributes)).get(); if (setClass) { classes.push(setClass); } @@ -6218,11 +6343,11 @@ for (attributeName in addClass) { method = addClassMethods[addClass[attributeName]]; if (!method) { continue; } - newClass = method(_getAttribute(oldNode, attributeName)); + newClass = method(wysihtml5.dom.getAttribute(oldNode, attributeName)); if (typeof(newClass) === "string") { classes.push(newClass); } } } @@ -6232,20 +6357,40 @@ method = addStyleMethods[addStyle[attributeName]]; if (!method) { continue; } - newStyle = method(_getAttribute(oldNode, attributeName)); + newStyle = method(wysihtml5.dom.getAttribute(oldNode, attributeName)); if (typeof(newStyle) === "string") { styles.push(newStyle); } } } if (typeof(allowedClasses) === "string" && allowedClasses === "any" && oldNode.getAttribute("class")) { - attributes["class"] = oldNode.getAttribute("class"); + if (currentRules.classes_blacklist) { + oldClasses = oldNode.getAttribute("class"); + if (oldClasses) { + classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); + } + + classesLength = classes.length; + for (; i<classesLength; i++) { + currentClass = classes[i]; + if (!currentRules.classes_blacklist[currentClass]) { + newClasses.push(currentClass); + } + } + + if (newClasses.length) { + attributes["class"] = wysihtml5.lang.array(newClasses).unique().join(" "); + } + + } else { + attributes["class"] = oldNode.getAttribute("class"); + } } else { // make sure that wysihtml5 temp class doesn't get stripped out if (!clearInternals) { allowedClasses["_wysihtml5-temp-placeholder"] = 1; allowedClasses["_rangySelectionBoundary"] = 1; @@ -6302,53 +6447,10 @@ newNode.setAttribute("height", attributes.height); } } } - /** - * IE gives wrong results for hasAttribute/getAttribute, for example: - * var td = document.createElement("td"); - * td.getAttribute("rowspan"); // => "1" in IE - * - * Therefore we have to check the element's outerHTML for the attribute - */ - var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(); - function _getAttribute(node, attributeName) { - attributeName = attributeName.toLowerCase(); - var nodeName = node.nodeName; - if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) { - // Get 'src' attribute value via object property since this will always contain the - // full absolute url (http://...) - // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host - // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url) - return node.src; - } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) { - // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML - var outerHTML = node.outerHTML.toLowerCase(), - // TODO: This might not work for attributes without value: <input disabled> - hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1; - - return hasAttribute ? node.getAttribute(attributeName) : null; - } else{ - return node.getAttribute(attributeName); - } - } - - /** - * Check whether the given node is a proper loaded image - * FIXME: Returns undefined when unknown (Chrome, Safari) - */ - function _isLoadedImage(node) { - try { - return node.complete && !node.mozMatchesSelector(":-moz-broken"); - } catch(e) { - if (node.complete && node.readyState === "complete") { - return true; - } - } - } - var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g; function _handleText(oldNode) { var nextSibling = oldNode.nextSibling; if (nextSibling && nextSibling.nodeType === wysihtml5.TEXT_NODE) { // Concatenate text nodes @@ -6529,12 +6631,22 @@ return false; }; })() }; - return parse; -})(); + var elementHandlingMethods = { + unwrap: function (element) { + wysihtml5.dom.unwrap(element); + }, + + remove: function (element) { + element.parentNode.removeChild(element); + } + }; + + return parse(elementOrHtml_current, config_current); +}; ;/** * Checks for empty text node childs and removes them * * @param {Element} node The element in which to cleanup * @example @@ -7157,11 +7269,11 @@ wysihtml5.dom.getAttribute = function(node, attributeName) { var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(); attributeName = attributeName.toLowerCase(); var nodeName = node.nodeName; - if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) { + if (nodeName == "IMG" && attributeName == "src" && wysihtml5.dom.isLoadedImage(node) === true) { // Get 'src' attribute value via object property since this will always contain the // full absolute url (http://...) // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url) return node.src; @@ -7174,10 +7286,56 @@ return hasAttribute ? node.getAttribute(attributeName) : null; } else{ return node.getAttribute(attributeName); } }; +;/** + * Get all attributes of an element + * + * IE gives wrong results for hasAttribute/getAttribute, for example: + * var td = document.createElement("td"); + * td.getAttribute("rowspan"); // => "1" in IE + * + * Therefore we have to check the element's outerHTML for the attribute +*/ + +wysihtml5.dom.getAttributes = function(node) { + var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(), + nodeName = node.nodeName, + attributes = [], + attr; + + for (attr in node.attributes) { + if ((node.attributes.hasOwnProperty && node.attributes.hasOwnProperty(attr)) || (!node.attributes.hasOwnProperty && Object.prototype.hasOwnProperty.call(node.attributes, attr))) { + if (node.attributes[attr].specified) { + if (nodeName == "IMG" && node.attributes[attr].name.toLowerCase() == "src" && wysihtml5.dom.isLoadedImage(node) === true) { + attributes['src'] = node.src; + } else if (wysihtml5.lang.array(['rowspan', 'colspan']).contains(node.attributes[attr].name.toLowerCase()) && HAS_GET_ATTRIBUTE_BUG) { + if (node.attributes[attr].value !== 1) { + attributes[node.attributes[attr].name] = node.attributes[attr].value; + } + } else { + attributes[node.attributes[attr].name] = node.attributes[attr].value; + } + } + } + } + return attributes; +};;/** + * Check whether the given node is a proper loaded image + * FIXME: Returns undefined when unknown (Chrome, Safari) +*/ + +wysihtml5.dom.isLoadedImage = function (node) { + try { + return node.complete && !node.mozMatchesSelector(":-moz-broken"); + } catch(e) { + if (node.complete && node.readyState === "complete") { + return true; + } + } +}; ;(function(wysihtml5) { var api = wysihtml5.dom; var MapCell = function(cell) { @@ -8141,63 +8299,127 @@ while (node.lastChild) { wysihtml5.dom.insert(node.lastChild).after(node); } node.parentNode.removeChild(node); } +};;/* + * Methods for fetching pasted html before it gets inserted into content +**/ + +/* Modern event.clipboardData driven approach. + * Advantage is that it does not have to loose selection or modify dom to catch the data. + * IE does not support though. +**/ +wysihtml5.dom.getPastedHtml = function(event) { + var html; + if (event.clipboardData) { + if (wysihtml5.lang.array(event.clipboardData.types).contains('text/html')) { + html = event.clipboardData.getData('text/html'); + } else if (wysihtml5.lang.array(event.clipboardData.types).contains('text/plain')) { + html = wysihtml5.lang.string(event.clipboardData.getData('text/plain')).escapeHTML(true, true); + } + } + return html; +}; + +/* Older temprorary contenteditable as paste source catcher method for fallbacks */ +wysihtml5.dom.getPastedHtmlWithDiv = function (composer, f) { + var selBookmark = composer.selection.getBookmark(), + doc = composer.element.ownerDocument, + cleanerDiv = coc.createElement('DIV'); + + cleanerDiv.style.width = "1px"; + cleanerDiv.style.height = "1px"; + cleanerDiv.style.visibility = "hidden"; + cleanerDiv.style.overflow = "hidden"; + + cleanerDiv.setAttribute('contenteditable', 'true'); + doc.body.appendChild(cleanerDiv); + cleanerDiv.focus(); + + setTimeout(function () { + composer.selection.setBookmark(selBookmark); + f(cleanerDiv.innerHTML); + cleanerDiv.parentNode.removeChild(cleanerDiv); + }, 0); };;/** * Fix most common html formatting misbehaviors of browsers implementation when inserting * content via copy & paste contentEditable * * @author Christopher Blum */ wysihtml5.quirks.cleanPastedHTML = (function() { - // TODO: We probably need more rules here - var defaultRules = { - // When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling - "a u": wysihtml5.dom.replaceWithChildNodes + + var styleToRegex = function (styleStr) { + var trimmedStr = wysihtml5.lang.string(styleStr).trim(), + escapedStr = trimmedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + + return new RegExp("^((?!^" + escapedStr + "$).)*$", "i"); }; - function cleanPastedHTML(elementOrHtml, rules, context) { - rules = rules || defaultRules; - context = context || elementOrHtml.ownerDocument || document; + var extendRulesWithStyleExceptions = function (rules, exceptStyles) { + var newRules = wysihtml5.lang.object(rules).clone(true), + tag, style; - var element, - isString = typeof(elementOrHtml) === "string", - method, - matches, - matchesLength, - i, - j = 0, n; - if (isString) { - element = wysihtml5.dom.getAsDom(elementOrHtml, context); - } else { - element = elementOrHtml; - } + for (tag in newRules.tags) { - for (i in rules) { - matches = element.querySelectorAll(i); - method = rules[i]; - matchesLength = matches.length; - for (; j<matchesLength; j++) { - method(matches[j]); + if (newRules.tags.hasOwnProperty(tag)) { + if (newRules.tags[tag].keep_styles) { + for (style in newRules.tags[tag].keep_styles) { + if (newRules.tags[tag].keep_styles.hasOwnProperty(style)) { + if (exceptStyles[style]) { + newRules.tags[tag].keep_styles[style] = styleToRegex(exceptStyles[style]); + } + } + } + } } } - // replace joined non-breakable spaces with unjoined - var txtnodes = wysihtml5.dom.getTextNodes(element); - for (n = txtnodes.length; n--;) { - txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 "); + return newRules; + }; + + var pickRuleset = function(ruleset, html) { + var pickedSet, defaultSet; + + if (!ruleset) { + return null; } - matches = elementOrHtml = rules = null; + for (var i = 0, max = ruleset.length; i < max; i++) { + if (!ruleset[i].condition) { + defaultSet = ruleset[i].set; + } + if (ruleset[i].condition && ruleset[i].condition.test(html)) { + return ruleset[i].set; + } + } - return isString ? element.innerHTML : element; - } + return defaultSet; + }; - return cleanPastedHTML; -})(); -;/** + return function(html, options) { + var exceptStyles = { + 'color': wysihtml5.dom.getStyle("color").from(options.referenceNode), + 'fontSize': wysihtml5.dom.getStyle("font-size").from(options.referenceNode) + }, + rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles), + newHtml; + + newHtml = wysihtml5.dom.parse(html, { + "rules": rules, + "cleanUp": true, // <span> elements, empty or without attributes, should be removed/replaced with their content + "context": options.referenceNode.ownerDocument, + "uneditableClass": options.uneditableClass, + "clearInternals" : true, // don't paste temprorary selection and other markings + "unjoinNbsps" : true + }); + + return newHtml; + }; + +})();;/** * IE and Opera leave an empty paragraph in the contentEditable element after clearing it * * @param {Object} contentEditableElement The contentEditable element to observe for clearing events * @exaple * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); @@ -8823,11 +9045,11 @@ } } return false; }, - // TODO: Figure out a method from following 3 that would work universally + // TODO: Figure out a method from following 2 that would work universally executeAndRestoreRangy: function(method, restoreScrollPosition) { var win = this.doc.defaultView || this.doc.parentWindow, sel = rangy.saveSelection(win); if (!sel) { @@ -8936,14 +9158,22 @@ * @example * selection.insertHTML("<p>foobar</p>"); */ insertHTML: function(html) { var range = rangy.createRange(this.doc), - node = range.createContextualFragment(html), - lastChild = node.lastChild; + node = this.doc.createElement('DIV'), + fragment = this.doc.createDocumentFragment(), + lastChild; - this.insertNode(node); + node.innerHTML = html; + lastChild = node.lastChild; + + while (node.firstChild) { + fragment.appendChild(node.firstChild); + } + this.insertNode(fragment); + if (lastChild) { this.setAfter(lastChild); } }, @@ -12558,11 +12788,11 @@ 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"], + pasteEvents = ["drop", "paste", "beforepaste"], interactionEvents = ["drop", "paste", "mouseup", "focus", "keyup"]; // --------- destroy:composer event --------- dom.observe(container, "DOMNodeRemoved", function() { clearInterval(domNodeRemovedInterval); @@ -12631,13 +12861,13 @@ dom.observe(element, "dragenter", function() { that.parent.fire("unset_placeholder"); }); dom.observe(element, pasteEvents, function(event) { - setTimeout(function() { + //setTimeout(function() { that.parent.fire(event.type, event).fire(event.type + ":composer", event); - }, 0); + //}, 0); }); // --------- neword event --------- dom.observe(element, "keyup", function(event) { var keyCode = event.keyCode; @@ -12991,14 +13221,16 @@ autoLink: true, // Includes table editing events and cell selection tracking handleTables: true, // Tab key inserts tab into text as default behaviour. It can be disabled to regain keyboard navigation handleTabKey: true, - // Object which includes parser rules to apply when html gets inserted via copy & paste + // Object which includes parser rules to apply when html gets cleaned // See parser_rules/*.js for examples parserRules: { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} }, - // Parser method to use when the user inserts content via copy & paste + // Object which includes parser when the user inserts content via copy & paste. If null parserRules will be used instead + pasteParserRulesets: null, + // Parser method to use when the user inserts content parser: wysihtml5.dom.parse, // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option composerClassName: "wysihtml5-editor", // Class name to add to the body when the wysihtml5 editor is supported bodyClassName: "wysihtml5-supported", @@ -13139,16 +13371,42 @@ /** * Prepare html parser logic * - Observes for paste and drop */ _initParser: function() { - this.on("paste:composer", function() { - var keepScrollPosition = true, - that = this; - that.composer.selection.executeAndRestore(function() { - wysihtml5.quirks.cleanPastedHTML(that.composer.element); - that.parse(that.composer.element); - }, keepScrollPosition); + var that = this, + oldHtml, + cleanHtml; + + if (wysihtml5.browser.supportsModenPaste()) { + this.on("paste:composer", function(event) { + event.preventDefault(); + oldHtml = wysihtml5.dom.getPastedHtml(event); + if (oldHtml) { + that._cleanAndPaste(oldHtml); + } + }); + + } else { + this.on("beforepaste:composer", function(event) { + event.preventDefault(); + wysihtml5.dom.getPastedHtmlWithDiv(that.composer, function(pastedHTML) { + if (pastedHTML) { + that._cleanAndPaste(pastedHTML); + } + }); + }); + + } + }, + + _cleanAndPaste: function (oldHtml) { + var cleanHtml = wysihtml5.quirks.cleanPastedHTML(oldHtml, { + "referenceNode": this.composer.element, + "rules": this.config.pasteParserRulesets || [{"set": this.config.parserRules}], + "uneditableClass": this.config.uneditableContainerClassname }); + this.composer.selection.deleteContents(); + this.composer.selection.insertHTML(cleanHtml); } }); })(wysihtml5);