diff --git a/app/assets/javascripts/active_admin/editor/config.js.erb b/app/assets/javascripts/active_admin/editor/config.js.erb index 49a0014..21056e5 100644 --- a/app/assets/javascripts/active_admin/editor/config.js.erb +++ b/app/assets/javascripts/active_admin/editor/config.js.erb @@ -10,4 +10,5 @@ config.spinner = '<%= asset_path 'active_admin/editor/loader.gif' %>' config.uploads_enabled = <%= ActiveAdmin::Editor.configuration.s3_configured? %> config.parserRules = <%= ActiveAdmin::Editor.configuration.parser_rules.to_json %> + config.cleanUp = <%= ActiveAdmin::Editor.configuration.clean_up.to_json %> })(window) diff --git a/app/assets/javascripts/active_admin/editor/editor.js b/app/assets/javascripts/active_admin/editor/editor.js index 11365b6..2282de4 100644 --- a/app/assets/javascripts/active_admin/editor/editor.js +++ b/app/assets/javascripts/active_admin/editor/editor.js @@ -77,7 +77,8 @@ this._editor = new wysihtml5.Editor(this.$textarea.attr('id'), { toolbar: this.$toolbar.attr('id'), stylesheets: config.stylesheets, - parserRules: config.parserRules + parserRules: config.parserRules, + cleanUp: config.cleanUp }) } diff --git a/lib/active_admin/editor/config.rb b/lib/active_admin/editor/config.rb index 1478296..a0b8cd1 100644 --- a/lib/active_admin/editor/config.rb +++ b/lib/active_admin/editor/config.rb @@ -22,12 +22,11 @@ class Configuration # The s3 bucket to store uploads. attr_accessor :s3_bucket - # Base directory to store the uploaded files in the bucket. Defaults to - # 'uploads'. - attr_accessor :storage_dir - # wysiwyg stylesheets that get included in the backend and the frontend. - attr_accessor :stylesheets + attr_writer :stylesheets + + # wysihtml5 cleanUp options + attr_writer :clean_up def storage_dir @storage_dir ||= 'uploads' @@ -40,6 +39,10 @@ def storage_dir=(dir) def stylesheets @stylesheets ||= [ 'active_admin/editor/wysiwyg.css' ] end + + def clean_up + @clean_up.nil? ? true : !!@clean_up + end def s3_configured? aws_access_key_id.present? && diff --git a/lib/generators/active_admin/editor/templates/active_admin_editor.rb b/lib/generators/active_admin/editor/templates/active_admin_editor.rb index 3815a08..8bf0d16 100644 --- a/lib/generators/active_admin/editor/templates/active_admin_editor.rb +++ b/lib/generators/active_admin/editor/templates/active_admin_editor.rb @@ -3,4 +3,5 @@ # config.aws_access_key_id = '' # config.aws_access_secret = '' # config.storage_dir = 'uploads' + # config.clean_up = false end diff --git a/spec/lib/config_spec.rb b/spec/lib/config_spec.rb index 7d28f1e..be728da 100644 --- a/spec/lib/config_spec.rb +++ b/spec/lib/config_spec.rb @@ -15,6 +15,7 @@ its(:aws_access_secret) { should be_nil } its(:s3_bucket) { should be_nil } its(:storage_dir) { should eq 'uploads' } + its(:clean_up) { should be_true } end describe '.s3_configured?' do diff --git a/vendor/assets/javascripts/wysihtml5.js b/vendor/assets/javascripts/wysihtml5.js index 939dca9..1a66197 100755 --- a/vendor/assets/javascripts/wysihtml5.js +++ b/vendor/assets/javascripts/wysihtml5.js @@ -1,5 +1,5 @@ /** - * @license wysihtml5 v0.3.0 + * @license wysihtml5 v0.4.0pre * https://github.com/xing/wysihtml5 * * Author: Christopher Blum (https://github.com/tiff) @@ -9,7 +9,7 @@ * */ var wysihtml5 = { - version: "0.3.0", + version: "0.4.0pre", // namespaces commands: {}, @@ -3395,7 +3395,11 @@ wysihtml5.browser = (function() { isOpera = userAgent.indexOf("Opera/") !== -1; function iosVersion(userAgent) { - return ((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1]; + return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1]; + } + + function androidVersion(userAgent) { + return +(userAgent.match(/android (\d+)/) || [, 0])[1]; } return { @@ -3419,8 +3423,7 @@ wysihtml5.browser = (function() { // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+ hasQuerySelectorSupport = document.querySelector && document.querySelectorAll, // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05) - isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1; - + isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1; return hasContentEditableSupport && hasEditingApiSupport && hasQuerySelectorSupport @@ -3432,8 +3435,11 @@ wysihtml5.browser = (function() { }, isIos: function() { - var userAgent = this.USER_AGENT.toLowerCase(); - return userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1; + return (/ipad|iphone|ipod/i).test(this.USER_AGENT); + }, + + isAndroid: function() { + return this.USER_AGENT.indexOf("Android") !== -1; }, /** @@ -3463,7 +3469,7 @@ wysihtml5.browser = (function() { * Firefox sometimes shows a huge caret in the beginning after focusing */ displaysCaretInEmptyContentEditableCorrectly: function() { - return !isGecko; + return isIE; }, /** @@ -3474,6 +3480,13 @@ wysihtml5.browser = (function() { 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
when pressing enter in a contentEditable element @@ -3499,29 +3512,7 @@ wysihtml5.browser = (function() { supportsEventsInIframeCorrectly: function() { return !isOpera; }, - - /** - * Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled - * with event.preventDefault - * Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs - * to be cancelled - */ - firesOnDropOnlyWhenOnDragOverIsCancelled: function() { - return isWebKit || isGecko; - }, - /** - * Whether the browser supports the event.dataTransfer property in a proper way - */ - supportsDataTransfer: function() { - try { - // Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does) - return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData; - } catch(e) { - return false; - } - }, - /** * Everything below IE9 doesn't know how to treat HTML5 tags * @@ -3557,8 +3548,8 @@ wysihtml5.browser = (function() { // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets // converted into a list (,
  1. ...
) // IE and Opera act a bit different here as they convert the entire content of the current block element into a list - "insertUnorderedList": isIE || isOpera || isWebKit, - "insertOrderedList": isIE || isOpera || isWebKit + "insertUnorderedList": isIE || isWebKit, + "insertOrderedList": isIE || isWebKit }; // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands @@ -3630,14 +3621,6 @@ wysihtml5.browser = (function() { return isGecko || isIE || isOpera; }, - /** - * When the caret is in an empty list () which is the first child in an contentEditable container - * pressing backspace doesn't remove the entire list as done in other browsers - */ - clearsListsInContentEditableCorrectly: function() { - return isGecko || isIE || isWebKit; - }, - /** * All browsers except Safari and Chrome automatically scroll the range/caret position into view */ @@ -3678,14 +3661,6 @@ wysihtml5.browser = (function() { return "getSelection" in window && "modify" in window.getSelection(); }, - /** - * Whether the browser supports the classList object for fast className manipulation - * See https://developer.mozilla.org/en/DOM/element.classList - */ - supportsClassList: function() { - return "classList" in testElement; - }, - /** * Opera needs a white space after a
in order to position the caret correctly */ @@ -3733,6 +3708,35 @@ wysihtml5.browser = (function() { hasUndoInContextMenu: function() { return isGecko || isChrome || isOpera; + }, + + /** + * Opera sometimes doesn't insert the node at the right position when range.insertNode(someNode) + * is used (regardless if rangy or native) + * This especially happens when the caret is positioned right after a
because then + * insertNode() will insert the node right before the
+ */ + hasInsertNodeIssue: function() { + return isOpera; + }, + + /** + * IE 8+9 don't fire the focus event of the when the iframe gets focused (even though the caret gets set into the ) + */ + hasIframeFocusIssue: function() { + return isIE; + }, + + /** + * Chrome + Safari create invalid nested markup after paste + * + *

+ * foo + *

bar

+ *

+ */ + createsNestedInvalidMarkupAfterPaste: function() { + return isWebKit; } }; })();wysihtml5.lang.array = function(arr) { @@ -3794,28 +3798,14 @@ wysihtml5.browser = (function() { }; };wysihtml5.lang.Dispatcher = Base.extend( /** @scope wysihtml5.lang.Dialog.prototype */ { - observe: function(eventName, handler) { + on: function(eventName, handler) { this.events = this.events || {}; this.events[eventName] = this.events[eventName] || []; this.events[eventName].push(handler); return this; }, - on: function() { - return this.observe.apply(this, wysihtml5.lang.array(arguments).get()); - }, - - fire: function(eventName, payload) { - this.events = this.events || {}; - var handlers = this.events[eventName] || [], - i = 0; - for (; i"]/g, + ENTITY_MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': """ + }; wysihtml5.lang.string = function(str) { str = String(str); return { @@ -3913,7 +3930,16 @@ wysihtml5.browser = (function() { by: function(replace) { return str.split(search).join(replace); } - } + }; + }, + + /** + * @example + * wysihtml5.lang.string("hello
").escapeHTML(); + * // => "hello<br>" + */ + escapeHTML: function() { + return str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; }); } }; }; @@ -4004,11 +4030,12 @@ wysihtml5.browser = (function() { */ function _wrapMatchesInNode(textNode) { var parentNode = textNode.parentNode, + nodeValue = wysihtml5.lang.string(textNode.data).escapeHTML(), tempElement = _getTempElement(parentNode.ownerDocument); // We need to insert an empty/temporary to fix IE quirks // Elsewise IE would strip white space in the beginning - tempElement.innerHTML = "" + _convertUrlsToLinks(textNode.data); + tempElement.innerHTML = "" + _convertUrlsToLinks(nodeValue); tempElement.removeChild(tempElement.firstChild); while (tempElement.firstChild) { @@ -4058,12 +4085,12 @@ wysihtml5.browser = (function() { // Reveal url reg exp to the outside wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP; })(wysihtml5);(function(wysihtml5) { - var supportsClassList = wysihtml5.browser.supportsClassList(), - api = wysihtml5.dom; + var api = wysihtml5.dom; api.addClass = function(element, className) { - if (supportsClassList) { - return element.classList.add(className); + var classList = element.classList; + if (classList) { + return classList.add(className); } if (api.hasClass(element, className)) { return; @@ -4072,16 +4099,18 @@ wysihtml5.browser = (function() { }; api.removeClass = function(element, className) { - if (supportsClassList) { - return element.classList.remove(className); + var classList = element.classList; + if (classList) { + return classList.remove(className); } element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " "); }; api.hasClass = function(element, className) { - if (supportsClassList) { - return element.classList.contains(className); + var classList = element.classList; + if (classList) { + return classList.contains(className); } var elementClassName = element.className; @@ -4198,6 +4227,10 @@ wysihtml5.dom.convertToList = (function() { currentListItem.appendChild(childNode); } + if (childNodes.length === 0) { + _createListItem(doc, list); + } + element.parentNode.replaceChild(list, element); return list; } @@ -4262,6 +4295,8 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then * its computed css width will be 198px + * + * See https://bugzilla.mozilla.org/show_bug.cgi?id=520992 */ var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"]; @@ -4516,7 +4551,7 @@ wysihtml5.dom.getStyle = (function() { // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio if (currentStyle) { try { - return currentStyle[camelizedProperty]; + return currentStyle[camelizedProperty]; } catch(e) { //ie will occasionally fail for unknown reasons. swallowing exception } @@ -4623,19 +4658,24 @@ wysihtml5.dom.insert = function(elementToInsert) { return { into: function(doc) { - var head = doc.head || doc.getElementsByTagName("head")[0], - styleElement = doc.createElement("style"); - + var styleElement = doc.createElement("style"); styleElement.type = "text/css"; - + if (styleElement.styleSheet) { styleElement.styleSheet.cssText = rules; } else { styleElement.appendChild(doc.createTextNode(rules)); } - - if (head) { - head.appendChild(styleElement); + + var link = doc.querySelector("head link"); + if (link) { + link.parentNode.insertBefore(styleElement, link); + return; + } else { + var head = doc.querySelector("head"); + if (head) { + head.appendChild(styleElement); + } } } }; @@ -4781,9 +4821,9 @@ wysihtml5.dom.parse = (function() { } while (element.firstChild) { - firstChild = element.firstChild; - element.removeChild(firstChild); + firstChild = element.firstChild; newNode = _convert(firstChild, cleanUp); + element.removeChild(firstChild); if (newNode) { fragment.appendChild(newNode); } @@ -4802,9 +4842,11 @@ wysihtml5.dom.parse = (function() { var oldNodeType = oldNode.nodeType, oldChilds = oldNode.childNodes, oldChildsLength = oldChilds.length, - newNode, method = NODE_TYPE_MAPPING[oldNodeType], - i = 0; + i = 0, + fragment, + newNode, + newChild; newNode = method && method(oldNode); @@ -4821,10 +4863,13 @@ wysihtml5.dom.parse = (function() { // Cleanup senseless elements if (cleanUp && - newNode.childNodes.length <= 1 && newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME && - !newNode.attributes.length) { - return newNode.firstChild; + (!newNode.childNodes.length || !newNode.attributes.length)) { + fragment = newNode.ownerDocument.createDocumentFragment(); + while (newNode.firstChild) { + fragment.appendChild(newNode.firstChild); + } + return fragment; } return newNode; @@ -4833,7 +4878,6 @@ wysihtml5.dom.parse = (function() { function _handleElement(oldNode) { var rule, newNode, - endTag, tagRules = currentRules.tags, nodeName = oldNode.nodeName.toLowerCase(), scopeName = oldNode.scopeName; @@ -5043,8 +5087,17 @@ wysihtml5.dom.parse = (function() { } } + var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g; function _handleText(oldNode) { - return oldNode.ownerDocument.createTextNode(oldNode.data); + var nextSibling = oldNode.nextSibling; + if (nextSibling && nextSibling.nodeType === wysihtml5.TEXT_NODE) { + // Concatenate text nodes + nextSibling.data = oldNode.data + nextSibling.data; + } else { + // \uFEFF = wysihtml5.INVISIBLE_SPACE (used as a hack in certain rich text editing situations) + var data = oldNode.data.replace(INVISIBLE_SPACE_REG_EXP, ""); + return oldNode.ownerDocument.createTextNode(data); + } } @@ -5061,6 +5114,30 @@ wysihtml5.dom.parse = (function() { }); }; })(), + + src: (function() { + var REG_EXP = /^(\/|https?:\/\/)/i; + return function(attributeValue) { + if (!attributeValue || !attributeValue.match(REG_EXP)) { + return null; + } + return attributeValue.replace(REG_EXP, function(match) { + return match.toLowerCase(); + }); + }; + })(), + + href: (function() { + var REG_EXP = /^(\/|https?:\/\/|mailto:)/i; + return function(attributeValue) { + if (!attributeValue || !attributeValue.match(REG_EXP)) { + return null; + } + return attributeValue.replace(REG_EXP, function(match) { + return match.toLowerCase(); + }); + }; + })(), alt: (function() { var REG_EXP = /[^ a-z0-9_\-]/gi; @@ -5136,7 +5213,8 @@ wysihtml5.dom.parse = (function() { }; return parse; -})();/** +})(); +/** * Checks for empty text node childs and removes them * * @param {Element} node The element in which to cleanup @@ -5255,8 +5333,8 @@ wysihtml5.dom.replaceWithChildNodes = function(node) { element.appendChild(lineBreak); } - function resolveList(list) { - if (list.nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") { + function resolveList(list, useLineBreaks) { + if (!list.nodeName.match(/^(MENU|UL|OL)$/)) { return; } @@ -5267,26 +5345,46 @@ wysihtml5.dom.replaceWithChildNodes = function(node) { lastChild, isLastChild, shouldAppendLineBreak, + paragraph, listItem; - if (previousSibling && !_isBlockElement(previousSibling)) { - _appendLineBreak(fragment); - } - - while (listItem = list.firstChild) { - lastChild = listItem.lastChild; - while (firstChild = listItem.firstChild) { - isLastChild = firstChild === lastChild; - // This needs to be done before appending it to the fragment, as it otherwise will loose style information - shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild); - fragment.appendChild(firstChild); - if (shouldAppendLineBreak) { - _appendLineBreak(fragment); + if (useLineBreaks) { + // Insert line break if list is after a non-block element + if (previousSibling && !_isBlockElement(previousSibling)) { + _appendLineBreak(fragment); + } + + while (listItem = (list.firstElementChild || list.firstChild)) { + lastChild = listItem.lastChild; + while (firstChild = listItem.firstChild) { + isLastChild = firstChild === lastChild; + // This needs to be done before appending it to the fragment, as it otherwise will lose style information + shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild); + fragment.appendChild(firstChild); + if (shouldAppendLineBreak) { + _appendLineBreak(fragment); + } } + + listItem.parentNode.removeChild(listItem); + } + } else { + while (listItem = (list.firstElementChild || list.firstChild)) { + if (listItem.querySelector && listItem.querySelector("div, p, ul, ol, menu, blockquote, h1, h2, h3, h4, h5, h6")) { + while (firstChild = listItem.firstChild) { + fragment.appendChild(firstChild); + } + } else { + paragraph = doc.createElement("p"); + while (firstChild = listItem.firstChild) { + paragraph.appendChild(firstChild); + } + fragment.appendChild(paragraph); + } + listItem.parentNode.removeChild(listItem); } - - listItem.parentNode.removeChild(listItem); } + list.parentNode.replaceChild(fragment, list); } @@ -5554,7 +5652,7 @@ wysihtml5.dom.replaceWithChildNodes = function(node) { element.setAttribute(mapping[i] || i, attributes[i]); } } - } + }; }; })();wysihtml5.dom.setStyles = function(styles) { return { @@ -5595,21 +5693,23 @@ wysihtml5.dom.replaceWithChildNodes = function(node) { if (view.hasPlaceholderSet()) { view.clear(); } + view.placeholderSet = false; dom.removeClass(view.element, CLASS_NAME); }, set = function() { if (view.isEmpty()) { + view.placeholderSet = true; view.setValue(placeholderText); dom.addClass(view.element, CLASS_NAME); } }; editor - .observe("set_placeholder", set) - .observe("unset_placeholder", unset) - .observe("focus:composer", unset) - .observe("paste:composer", unset) - .observe("blur:composer", set); + .on("set_placeholder", set) + .on("unset_placeholder", unset) + .on("focus:composer", unset) + .on("paste:composer", unset) + .on("blur:composer", set); set(); }; @@ -5695,79 +5795,22 @@ wysihtml5.quirks.cleanPastedHTML = (function() { * @exaple * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); */ -(function(wysihtml5) { - var dom = wysihtml5.dom; - - wysihtml5.quirks.ensureProperClearing = (function() { - var clearIfNecessary = function(event) { - var element = this; - setTimeout(function() { - var innerHTML = element.innerHTML.toLowerCase(); - if (innerHTML == "

 

" || - innerHTML == "

 

 

") { - element.innerHTML = ""; - } - }, 0); - }; - - return function(composer) { - dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); - }; - })(); - - - - /** - * In Opera when the caret is in the first and only item of a list (
  • |
) and the list is the first child of the contentEditable element, it's impossible to delete the list by hitting backspace - * - * @param {Object} contentEditableElement The contentEditable element to observe for clearing events - * @exaple - * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); - */ - wysihtml5.quirks.ensureProperClearingOfLists = (function() { - var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"]; - - var clearIfNecessary = function(element, contentEditableElement) { - if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) { - return; - } - - var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI }); - if (!list) { - return; - } - - var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild; - if (!listIsFirstChildOfContentEditable) { - return; - } - - var hasOnlyOneListItem = list.childNodes.length <= 1; - if (!hasOnlyOneListItem) { - return; - } - - var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true; - if (!onlyListItemIsEmpty) { - return; - } - - list.parentNode.removeChild(list); - }; - - return function(composer) { - dom.observe(composer.element, "keydown", function(event) { - if (event.keyCode !== wysihtml5.BACKSPACE_KEY) { - return; - } - - var element = composer.selection.getSelectedNode(); - clearIfNecessary(element, composer.element); - }); - }; - })(); +wysihtml5.quirks.ensureProperClearing = (function() { + var clearIfNecessary = function() { + var element = this; + setTimeout(function() { + var innerHTML = element.innerHTML.toLowerCase(); + if (innerHTML == "

 

" || + innerHTML == "

 

 

") { + element.innerHTML = ""; + } + }, 0); + }; -})(wysihtml5); + return function(composer) { + wysihtml5.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); + }; +})(); // See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 // // In Firefox this: @@ -5797,83 +5840,6 @@ wysihtml5.quirks.cleanPastedHTML = (function() { } return innerHTML; }; -})(wysihtml5);/** - * Some browsers don't insert line breaks when hitting return in a contentEditable element - * - Opera & IE insert new

on return - * - Chrome & Safari insert new

on return - * - Firefox inserts
on return (yippie!) - * - * @param {Element} element - * - * @example - * wysihtml5.quirks.insertLineBreakOnReturn(element); - */ -(function(wysihtml5) { - var dom = wysihtml5.dom, - USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"], - LIST_TAGS = ["UL", "OL", "MENU"]; - - wysihtml5.quirks.insertLineBreakOnReturn = function(composer) { - function unwrap(selectedNode) { - var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2); - if (!parentElement) { - return; - } - - var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE); - dom.insert(invisibleSpace).before(parentElement); - dom.replaceWithChildNodes(parentElement); - composer.selection.selectNode(invisibleSpace); - } - - function keyDown(event) { - var keyCode = event.keyCode; - if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) { - return; - } - - var element = event.target, - selectedNode = composer.selection.getSelectedNode(), - blockElement = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4); - if (blockElement) { - // Some browsers create

elements after leaving a list - // check after keydown of backspace and return whether a

got inserted and unwrap it - if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) { - setTimeout(function() { - var selectedNode = composer.selection.getSelectedNode(), - list, - div; - if (!selectedNode) { - return; - } - - list = dom.getParentElement(selectedNode, { - nodeName: LIST_TAGS - }, 2); - - if (list) { - return; - } - - unwrap(selectedNode); - }, 0); - } else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) { - setTimeout(function() { - unwrap(composer.selection.getSelectedNode()); - }, 0); - } - return; - } - - if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) { - composer.commands.exec("insertLineBreak"); - event.preventDefault(); - } - } - - // keypress doesn't fire when you hit backspace - dom.observe(composer.element.ownerDocument, "keydown", keyDown); - }; })(wysihtml5);/** * Force rerendering of a given element * Needed to fix display misbehaviors of IE @@ -5985,7 +5951,7 @@ wysihtml5.quirks.cleanPastedHTML = (function() { * @example * selection.selectNode(document.getElementById("my-image")); */ - selectNode: function(node) { + selectNode: function(node, avoidInvisibleSpace) { var range = rangy.createRange(this.doc), isElement = node.nodeType === wysihtml5.ELEMENT_NODE, canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"), @@ -5994,7 +5960,7 @@ wysihtml5.quirks.cleanPastedHTML = (function() { displayStyle = dom.getStyle("display").from(node), isBlockElement = (displayStyle === "block" || displayStyle === "list-item"); - if (isEmpty && isElement && canHaveHTML) { + if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) { // Make sure that caret is visible in node by inserting a zero width no breaking space try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} } @@ -6048,8 +6014,12 @@ wysihtml5.quirks.cleanPastedHTML = (function() { oldScrollTop = restoreScrollPosition && body.scrollTop, oldScrollLeft = restoreScrollPosition && body.scrollLeft, className = "_wysihtml5-temp-placeholder", - placeholderHTML = '' + wysihtml5.INVISIBLE_SPACE + '', + placeholderHtml = '' + wysihtml5.INVISIBLE_SPACE + '', range = this.getRange(this.doc), + caretPlaceholder, + newCaretPlaceholder, + nextSibling, + node, newRange; // Nothing selected, execute and say goodbye @@ -6058,21 +6028,34 @@ wysihtml5.quirks.cleanPastedHTML = (function() { return; } - var node = range.createContextualFragment(placeholderHTML); - range.insertNode(node); + if (wysihtml5.browser.hasInsertNodeIssue()) { + this.doc.execCommand("insertHTML", false, placeholderHtml); + } else { + node = range.createContextualFragment(placeholderHtml); + range.insertNode(node); + } // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder try { method(range.startContainer, range.endContainer); - } catch(e3) { - setTimeout(function() { throw e3; }, 0); + } catch(e) { + setTimeout(function() { throw e; }, 0); } caretPlaceholder = this.doc.querySelector("." + className); if (caretPlaceholder) { newRange = rangy.createRange(this.doc); - newRange.selectNode(caretPlaceholder); - newRange.deleteContents(); + nextSibling = caretPlaceholder.nextSibling; + // Opera is so fucked up when you wanna set focus before a
+ if (wysihtml5.browser.hasInsertNodeIssue() && nextSibling && nextSibling.nodeName === "BR") { + newCaretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + dom.insert(newCaretPlaceholder).after(caretPlaceholder); + newRange.setStartBefore(newCaretPlaceholder); + newRange.setEndBefore(newCaretPlaceholder); + } else { + newRange.selectNode(caretPlaceholder); + newRange.deleteContents(); + } this.setSelection(newRange); } else { // fallback for when all hell breaks loose @@ -6087,7 +6070,7 @@ wysihtml5.quirks.cleanPastedHTML = (function() { // Remove it again, just to make sure that the placeholder is definitely out of the dom tree try { caretPlaceholder.parentNode.removeChild(caretPlaceholder); - } catch(e4) {} + } catch(e2) {} }, /** @@ -6132,7 +6115,13 @@ wysihtml5.quirks.cleanPastedHTML = (function() { try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {} try { this.setSelection(newRange); } catch(e3) {} }, - + + set: function(node, offset) { + var newRange = rangy.createRange(this.doc); + newRange.setStart(node, offset || 0); + this.setSelection(newRange); + }, + /** * Insert html at the caret position and move the cursor after the inserted html * @@ -6195,6 +6184,7 @@ wysihtml5.quirks.cleanPastedHTML = (function() { */ scrollIntoView: function() { var doc = this.doc, + tolerance = 5, // px hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight, tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() { var element = doc.createElement("span"); @@ -6208,7 +6198,7 @@ wysihtml5.quirks.cleanPastedHTML = (function() { this.insertNode(tempElement); offsetTop = _getCumulativeOffsetTop(tempElement); tempElement.parentNode.removeChild(tempElement); - if (offsetTop > doc.body.scrollTop) { + if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) { doc.body.scrollTop = offsetTop; } } @@ -6238,7 +6228,6 @@ wysihtml5.quirks.cleanPastedHTML = (function() { _selectLine_MSIE: function() { var range = this.doc.selection.createRange(), rangeTop = range.boundingTop, - rangeHeight = range.boundingHeight, scrollWidth = this.doc.body.scrollWidth, rangeBottom, rangeEnd, @@ -6826,53 +6815,22 @@ wysihtml5.Commands = Base.extend( return false; } } - }, - - /** - * Get the current command's value - * - * @param {String} command The command string which to check (eg. "formatBlock") - * @return {String} The command value - * @example - * var currentBlockElement = commands.value("formatBlock"); - */ - value: function(command) { - var obj = wysihtml5.commands[command], - method = obj && obj.value; - if (method) { - return method.call(obj, this.composer, command); - } else { - try { - // try/catch for buggy firefox - return this.doc.queryCommandValue(command); - } catch(e) { - return null; - } - } } }); -(function(wysihtml5) { - var undef; - - wysihtml5.commands.bold = { - exec: function(composer, command) { - return wysihtml5.commands.formatInline.exec(composer, command, "b"); - }, - - state: function(composer, command, color) { - // element.ownerDocument.queryCommandState("bold") results: - // firefox: only - // chrome: , ,

,

, ... - // ie: , - // opera: , - return wysihtml5.commands.formatInline.state(composer, command, "b"); - }, +wysihtml5.commands.bold = { + exec: function(composer, command) { + return wysihtml5.commands.formatInline.exec(composer, command, "b"); + }, - value: function() { - return undef; - } - }; -})(wysihtml5); + state: function(composer, command) { + // element.ownerDocument.queryCommandState("bold") results: + // firefox: only + // chrome: , ,

,

, ... + // ie: , + // opera: , + return wysihtml5.commands.formatInline.state(composer, command, "b"); + } +}; (function(wysihtml5) { var undef, @@ -6935,7 +6893,7 @@ wysihtml5.Commands = Base.extend( dom.setTextContent(anchor, attributes.text || anchor.href); whiteSpace = doc.createTextNode(" "); composer.selection.setAfter(anchor); - composer.selection.insertNode(whiteSpace); + dom.insert(whiteSpace).after(anchor); elementToSetCaretAfter = whiteSpace; } } @@ -6972,10 +6930,6 @@ wysihtml5.Commands = Base.extend( state: function(composer, command) { return wysihtml5.commands.formatInline.state(composer, command, "A"); - }, - - value: function() { - return undef; } }; })(wysihtml5);/** @@ -6984,8 +6938,7 @@ wysihtml5.Commands = Base.extend( * Instead we set a css class */ (function(wysihtml5) { - var undef, - REG_EXP = /wysiwyg-font-size-[a-z\-]+/g; + var REG_EXP = /wysiwyg-font-size-[0-9a-z\-]+/g; wysihtml5.commands.fontSize = { exec: function(composer, command, size) { @@ -6994,10 +6947,6 @@ wysihtml5.Commands = Base.extend( state: function(composer, command, size) { return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP); - }, - - value: function() { - return undef; } }; })(wysihtml5); @@ -7007,8 +6956,7 @@ wysihtml5.Commands = Base.extend( * Instead we set a css class */ (function(wysihtml5) { - var undef, - REG_EXP = /wysiwyg-color-[a-z]+/g; + var REG_EXP = /wysiwyg-color-[0-9a-z]+/g; wysihtml5.commands.foreColor = { exec: function(composer, command, color) { @@ -7017,20 +6965,14 @@ wysihtml5.Commands = Base.extend( state: function(composer, command, color) { return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); - }, - - value: function() { - return undef; } }; })(wysihtml5);(function(wysihtml5) { - var undef, - dom = wysihtml5.dom, - DEFAULT_NODE_NAME = "DIV", + var dom = wysihtml5.dom, // Following elements are grouped // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4 // instead of creating a H4 within a H1 which would result in semantically invalid html - BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", DEFAULT_NODE_NAME]; + BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", "DIV"]; /** * Remove similiar classes (based on classRegExp) @@ -7167,7 +7109,7 @@ wysihtml5.Commands = Base.extend( composer.selection.surround(element); _removeLineBreakBeforeAndAfter(element); _removeLastChildIfLineBreak(element); - composer.selection.selectNode(element); + composer.selection.selectNode(element, wysihtml5.browser.displaysCaretInEmptyContentEditableCorrectly()); } function _hasClasses(element) { @@ -7176,26 +7118,28 @@ wysihtml5.Commands = Base.extend( wysihtml5.commands.formatBlock = { exec: function(composer, command, nodeName, className, classRegExp) { - var doc = composer.doc, - blockElement = this.state(composer, command, nodeName, className, classRegExp), + var doc = composer.doc, + blockElement = this.state(composer, command, nodeName, className, classRegExp), + useLineBreaks = composer.config.useLineBreaks, + defaultNodeName = useLineBreaks ? "DIV" : "P", selectedNode; nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; - + if (blockElement) { composer.selection.executeAndRestoreSimple(function() { if (classRegExp) { _removeClass(blockElement, classRegExp); } var hasClasses = _hasClasses(blockElement); - if (!hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) { + if (!hasClasses && (useLineBreaks || nodeName === "P")) { // Insert a line break afterwards and beforewards when there are siblings // that are not of type line break or block element _addLineBreakBeforeAndAfter(blockElement); dom.replaceWithChildNodes(blockElement); - } else if (hasClasses) { - // Make sure that styling is kept by renaming the element to
and copying over the class name - dom.renameElement(blockElement, DEFAULT_NODE_NAME); + } else { + // Make sure that styling is kept by renaming the element to a
or

and copying over the class name + dom.renameElement(blockElement, nodeName === "P" ? "DIV" : defaultNodeName); } }); return; @@ -7209,7 +7153,7 @@ wysihtml5.Commands = Base.extend( }); if (blockElement) { - composer.selection.executeAndRestoreSimple(function() { + composer.selection.executeAndRestore(function() { // Rename current block element to new block element and add class if (nodeName) { blockElement = dom.renameElement(blockElement, nodeName); @@ -7223,11 +7167,11 @@ wysihtml5.Commands = Base.extend( } if (composer.commands.support(command)) { - _execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className); + _execCommand(doc, command, nodeName || defaultNodeName, className); return; } - blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME); + blockElement = doc.createElement(nodeName || defaultNodeName); if (className) { blockElement.className = className; } @@ -7242,10 +7186,6 @@ wysihtml5.Commands = Base.extend( className: className, classRegExp: classRegExp }); - }, - - value: function() { - return undef; } }; })(wysihtml5);/** @@ -7282,8 +7222,7 @@ wysihtml5.Commands = Base.extend( * ab|c de|fgh */ (function(wysihtml5) { - var undef, - // Treat as and vice versa + var // Treat as and vice versa ALIAS_MAPPING = { "strong": "b", "em": "i", @@ -7337,33 +7276,22 @@ wysihtml5.Commands = Base.extend( } return _getApplier(tagName, className, classRegExp).isAppliedToRange(range); - }, - - value: function() { - return undef; } }; -})(wysihtml5);(function(wysihtml5) { - var undef; - - wysihtml5.commands.insertHTML = { - exec: function(composer, command, html) { - if (composer.commands.support(command)) { - composer.doc.execCommand(command, false, html); - } else { - composer.selection.insertHTML(html); - } - }, - - state: function() { - return false; - }, - - value: function() { - return undef; +})(wysihtml5);wysihtml5.commands.insertHTML = { + exec: function(composer, command, html) { + if (composer.commands.support(command)) { + composer.doc.execCommand(command, false, html); + } else { + composer.selection.insertHTML(html); } - }; -})(wysihtml5);(function(wysihtml5) { + }, + + state: function() { + return false; + } +}; +(function(wysihtml5) { var NODE_NAME = "IMG"; wysihtml5.commands.insertImage = { @@ -7383,7 +7311,6 @@ wysihtml5.Commands = Base.extend( var doc = composer.doc, image = this.state(composer), textNode, - i, parent; if (image) { @@ -7405,9 +7332,9 @@ wysihtml5.Commands = Base.extend( } image = doc.createElement(NODE_NAME); - - for (i in value) { - image[i] = value[i]; + + for (var i in value) { + image.setAttribute(i === "className" ? "class" : i, value[i]); } composer.selection.insertNode(image); @@ -7459,16 +7386,10 @@ wysihtml5.Commands = Base.extend( } return imagesInSelection[0]; - }, - - value: function(composer) { - var image = this.state(composer); - return image && image.src; } }; })(wysihtml5);(function(wysihtml5) { - var undef, - LINE_BREAK = "
" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : ""); + var LINE_BREAK = "
" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : ""); wysihtml5.commands.insertLineBreak = { exec: function(composer, command) { @@ -7484,151 +7405,122 @@ wysihtml5.Commands = Base.extend( state: function() { return false; - }, - - value: function() { - return undef; } }; -})(wysihtml5);(function(wysihtml5) { - var undef; - - wysihtml5.commands.insertOrderedList = { - exec: function(composer, command) { - var doc = composer.doc, - selectedNode = composer.selection.getSelectedNode(), - list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), - otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), - tempClassName = "_wysihtml5-temp-" + new Date().getTime(), - isEmpty, - tempElement; - - if (composer.commands.support(command)) { - doc.execCommand(command, false, null); - return; - } - - if (list) { - // Unwrap list - //

  1. foo
  2. bar
- // becomes: - // foo
bar
- composer.selection.executeAndRestoreSimple(function() { - wysihtml5.dom.resolveList(list); - }); - } else if (otherList) { - // Turn an unordered list into an ordered list - //
  • foo
  • bar
- // becomes: - //
  1. foo
  2. bar
- composer.selection.executeAndRestoreSimple(function() { - wysihtml5.dom.renameElement(otherList, "ol"); - }); - } else { - // Create list - composer.commands.exec("formatBlock", "div", tempClassName); - tempElement = doc.querySelector("." + tempClassName); - isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; - composer.selection.executeAndRestoreSimple(function() { - list = wysihtml5.dom.convertToList(tempElement, "ol"); - }); - if (isEmpty) { - composer.selection.selectNode(list.querySelector("li")); - } - } - }, +})(wysihtml5);wysihtml5.commands.insertOrderedList = { + exec: function(composer, command) { + var doc = composer.doc, + selectedNode = composer.selection.getSelectedNode(), + list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), + otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), + tempClassName = "_wysihtml5-temp-" + new Date().getTime(), + isEmpty, + tempElement; - state: function(composer) { - var selectedNode = composer.selection.getSelectedNode(); - return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }); - }, - - value: function() { - return undef; + if (!list && !otherList && composer.commands.support(command)) { + doc.execCommand(command, false, null); + return; } - }; -})(wysihtml5);(function(wysihtml5) { - var undef; - - wysihtml5.commands.insertUnorderedList = { - exec: function(composer, command) { - var doc = composer.doc, - selectedNode = composer.selection.getSelectedNode(), - list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), - otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), - tempClassName = "_wysihtml5-temp-" + new Date().getTime(), - isEmpty, - tempElement; - - if (composer.commands.support(command)) { - doc.execCommand(command, false, null); - return; - } - - if (list) { - // Unwrap list - //
  • foo
  • bar
- // becomes: - // foo
bar
- composer.selection.executeAndRestoreSimple(function() { - wysihtml5.dom.resolveList(list); - }); - } else if (otherList) { - // Turn an ordered list into an unordered list - //
  1. foo
  2. bar
- // becomes: - //
  • foo
  • bar
- composer.selection.executeAndRestoreSimple(function() { - wysihtml5.dom.renameElement(otherList, "ul"); - }); - } else { - // Create list - composer.commands.exec("formatBlock", "div", tempClassName); - tempElement = doc.querySelector("." + tempClassName); - isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; - composer.selection.executeAndRestoreSimple(function() { - list = wysihtml5.dom.convertToList(tempElement, "ul"); - }); - if (isEmpty) { - composer.selection.selectNode(list.querySelector("li")); - } + + if (list) { + // Unwrap list + //
  1. foo
  2. bar
+ // becomes: + // foo
bar
+ composer.selection.executeAndRestore(function() { + wysihtml5.dom.resolveList(list, composer.config.useLineBreaks); + }); + } else if (otherList) { + // Turn an unordered list into an ordered list + //
  • foo
  • bar
+ // becomes: + //
  1. foo
  2. bar
+ composer.selection.executeAndRestore(function() { + wysihtml5.dom.renameElement(otherList, "ol"); + }); + } else { + // Create list + composer.commands.exec("formatBlock", "div", tempClassName); + tempElement = doc.querySelector("." + tempClassName); + isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE || tempElement.innerHTML === "
"; + composer.selection.executeAndRestore(function() { + list = wysihtml5.dom.convertToList(tempElement, "ol"); + }); + if (isEmpty) { + composer.selection.selectNode(list.querySelector("li"), true); } - }, + } + }, + + state: function(composer) { + var selectedNode = composer.selection.getSelectedNode(); + return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }); + } +};wysihtml5.commands.insertUnorderedList = { + exec: function(composer, command) { + var doc = composer.doc, + selectedNode = composer.selection.getSelectedNode(), + list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), + otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), + tempClassName = "_wysihtml5-temp-" + new Date().getTime(), + isEmpty, + tempElement; - state: function(composer) { - var selectedNode = composer.selection.getSelectedNode(); - return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }); - }, - - value: function() { - return undef; + if (!list && !otherList && composer.commands.support(command)) { + doc.execCommand(command, false, null); + return; } - }; -})(wysihtml5);(function(wysihtml5) { - var undef; + + if (list) { + // Unwrap list + //
  • foo
  • bar
+ // becomes: + // foo
bar
+ composer.selection.executeAndRestore(function() { + wysihtml5.dom.resolveList(list, composer.config.useLineBreaks); + }); + } else if (otherList) { + // Turn an ordered list into an unordered list + //
  1. foo
  2. bar
+ // becomes: + //
  • foo
  • bar
+ composer.selection.executeAndRestore(function() { + wysihtml5.dom.renameElement(otherList, "ul"); + }); + } else { + // Create list + composer.commands.exec("formatBlock", "div", tempClassName); + tempElement = doc.querySelector("." + tempClassName); + isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE || tempElement.innerHTML === "
"; + composer.selection.executeAndRestore(function() { + list = wysihtml5.dom.convertToList(tempElement, "ul"); + }); + if (isEmpty) { + composer.selection.selectNode(list.querySelector("li"), true); + } + } + }, - wysihtml5.commands.italic = { - exec: function(composer, command) { - return wysihtml5.commands.formatInline.exec(composer, command, "i"); - }, - - state: function(composer, command, color) { - // element.ownerDocument.queryCommandState("italic") results: - // firefox: only - // chrome: , ,
, ... - // ie: , - // opera: only - return wysihtml5.commands.formatInline.state(composer, command, "i"); - }, + state: function(composer) { + var selectedNode = composer.selection.getSelectedNode(); + return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }); + } +};wysihtml5.commands.italic = { + exec: function(composer, command) { + return wysihtml5.commands.formatInline.exec(composer, command, "i"); + }, - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef, - CLASS_NAME = "wysiwyg-text-align-center", - REG_EXP = /wysiwyg-text-align-[a-z]+/g; + state: function(composer, command) { + // element.ownerDocument.queryCommandState("italic") results: + // firefox: only + // chrome: , ,
, ... + // ie: , + // opera: only + return wysihtml5.commands.formatInline.state(composer, command, "i"); + } +};(function(wysihtml5) { + var CLASS_NAME = "wysiwyg-text-align-center", + REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; wysihtml5.commands.justifyCenter = { exec: function(composer, command) { @@ -7637,16 +7529,11 @@ wysihtml5.Commands = Base.extend( state: function(composer, command) { return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); - }, - - value: function() { - return undef; } }; })(wysihtml5);(function(wysihtml5) { - var undef, - CLASS_NAME = "wysiwyg-text-align-left", - REG_EXP = /wysiwyg-text-align-[a-z]+/g; + var CLASS_NAME = "wysiwyg-text-align-left", + REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; wysihtml5.commands.justifyLeft = { exec: function(composer, command) { @@ -7655,16 +7542,11 @@ wysihtml5.Commands = Base.extend( state: function(composer, command) { return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); - }, - - value: function() { - return undef; } }; })(wysihtml5);(function(wysihtml5) { - var undef, - CLASS_NAME = "wysiwyg-text-align-right", - REG_EXP = /wysiwyg-text-align-[a-z]+/g; + var CLASS_NAME = "wysiwyg-text-align-right", + REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; wysihtml5.commands.justifyRight = { exec: function(composer, command) { @@ -7673,28 +7555,47 @@ wysihtml5.Commands = Base.extend( state: function(composer, command) { return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); - }, - - value: function() { - return undef; } }; })(wysihtml5);(function(wysihtml5) { - var undef; - wysihtml5.commands.underline = { + var CLASS_NAME = "wysiwyg-text-align-justify", + REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; + + wysihtml5.commands.justifyFull = { exec: function(composer, command) { - return wysihtml5.commands.formatInline.exec(composer, command, "u"); + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); }, state: function(composer, command) { - return wysihtml5.commands.formatInline.state(composer, command, "u"); - }, - - value: function() { - return undef; + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); } }; -})(wysihtml5);/** +})(wysihtml5); +wysihtml5.commands.redo = { + exec: function(composer) { + return composer.undoManager.redo(); + }, + + state: function(composer) { + return false; + } +};wysihtml5.commands.underline = { + exec: function(composer, command) { + return wysihtml5.commands.formatInline.exec(composer, command, "u"); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, "u"); + } +};wysihtml5.commands.undo = { + exec: function(composer) { + return composer.undoManager.undo(); + }, + + state: function(composer) { + return false; + } +};/** * Undo Manager for wysihtml5 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface */ @@ -7703,7 +7604,9 @@ wysihtml5.Commands = Base.extend( Y_KEY = 89, BACKSPACE_KEY = 8, DELETE_KEY = 46, - MAX_HISTORY_ENTRIES = 40, + MAX_HISTORY_ENTRIES = 25, + DATA_ATTR_NODE = "data-wysihtml5-selection-node", + DATA_ATTR_OFFSET = "data-wysihtml5-selection-offset", UNDO_HTML = '' + wysihtml5.INVISIBLE_SPACE + '', REDO_HTML = '' + wysihtml5.INVISIBLE_SPACE + '', dom = wysihtml5.dom; @@ -7721,13 +7624,14 @@ wysihtml5.Commands = Base.extend( this.editor = editor; this.composer = editor.composer; this.element = this.composer.element; - this.history = [this.composer.getValue()]; - this.position = 1; - // Undo manager currently only supported in browsers who have the insertHTML command (not IE) - if (this.composer.commands.support("insertHTML")) { - this._observe(); - } + this.position = 0; + this.historyStr = []; + this.historyDom = []; + + this.transact(); + + this._observe(); }, _observe: function() { @@ -7814,56 +7718,134 @@ wysihtml5.Commands = Base.extend( } this.editor - .observe("newword:composer", function() { + .on("newword:composer", function() { that.transact(); }) - .observe("beforecommand:composer", function() { + .on("beforecommand:composer", function() { that.transact(); }); }, transact: function() { - var previousHtml = this.history[this.position - 1], - currentHtml = this.composer.getValue(); + var previousHtml = this.historyStr[this.position - 1], + currentHtml = this.composer.getValue(); - if (currentHtml == previousHtml) { + if (currentHtml === previousHtml) { return; } - var length = this.history.length = this.position; + var length = this.historyStr.length = this.historyDom.length = this.position; if (length > MAX_HISTORY_ENTRIES) { - this.history.shift(); + this.historyStr.shift(); + this.historyDom.shift(); this.position--; } this.position++; - this.history.push(currentHtml); + + var range = this.composer.selection.getRange(), + node = range.startContainer || this.element, + offset = range.startOffset || 0, + element, + position; + + if (node.nodeType === wysihtml5.ELEMENT_NODE) { + element = node; + } else { + element = node.parentNode; + position = this.getChildNodeIndex(element, node); + } + + element.setAttribute(DATA_ATTR_OFFSET, offset); + if (typeof(position) !== "undefined") { + element.setAttribute(DATA_ATTR_NODE, position); + } + + var clone = this.element.cloneNode(!!currentHtml); + this.historyDom.push(clone); + this.historyStr.push(currentHtml); + + element.removeAttribute(DATA_ATTR_OFFSET); + element.removeAttribute(DATA_ATTR_NODE); }, undo: function() { this.transact(); - if (this.position <= 1) { + if (!this.undoPossible()) { return; } - this.set(this.history[--this.position - 1]); + this.set(this.historyDom[--this.position - 1]); this.editor.fire("undo:composer"); }, redo: function() { - if (this.position >= this.history.length) { + if (!this.redoPossible()) { return; } - this.set(this.history[++this.position - 1]); + this.set(this.historyDom[++this.position - 1]); this.editor.fire("redo:composer"); }, - set: function(html) { - this.composer.setValue(html); - this.editor.focus(true); + undoPossible: function() { + return this.position > 1; + }, + + redoPossible: function() { + return this.position < this.historyStr.length; + }, + + set: function(historyEntry) { + this.element.innerHTML = ""; + + var i = 0, + childNodes = historyEntry.childNodes, + length = historyEntry.childNodes.length; + + for (; i" || + innerHTML === "

" || + innerHTML === "


" || + this.hasPlaceholderSet(); }, _initSandbox: function() { @@ -8034,17 +8018,18 @@ wysihtml5.views.View = Base.extend( stylesheets: this.config.stylesheets }); this.iframe = this.sandbox.getIframe(); - - // Create hidden field which tells the server after submit, that the user used an wysiwyg editor - var hiddenField = document.createElement("input"); - hiddenField.type = "hidden"; - hiddenField.name = "_wysihtml5_mode"; - hiddenField.value = 1; - - // Store reference to current wysihtml5 instance on the textarea element + var textareaElement = this.textarea.element; dom.insert(this.iframe).after(textareaElement); - dom.insert(hiddenField).after(textareaElement); + + // Create hidden field which tells the server after submit, that the user used an wysiwyg editor + if (textareaElement.form) { + var hiddenField = document.createElement("input"); + hiddenField.type = "hidden"; + hiddenField.name = "_wysihtml5_mode"; + hiddenField.value = 1; + dom.insert(hiddenField).after(textareaElement); + } }, _create: function() { @@ -8054,33 +8039,38 @@ wysihtml5.views.View = Base.extend( this.element = this.doc.body; this.textarea = this.parent.textarea; this.element.innerHTML = this.textarea.getValue(true); - this.enable(); // Make sure our selection handler is ready this.selection = new wysihtml5.Selection(this.parent); // Make sure commands dispatcher is ready this.commands = new wysihtml5.Commands(this.parent); - + dom.copyAttributes([ "className", "spellcheck", "title", "lang", "dir", "accessKey" ]).from(this.textarea.element).to(this.element); dom.addClass(this.element, this.config.composerClassName); - - // Make the editor look like the original textarea, by syncing styles + // + // // Make the editor look like the original textarea, by syncing styles if (this.config.style) { this.style(); } - + this.observe(); - + var name = this.config.name; if (name) { dom.addClass(this.element, name); dom.addClass(this.iframe, name); } - + + this.enable(); + + if (this.textarea.element.disabled) { + this.disable(); + } + // Simulate html5 placeholder attribute on contentEditable element var placeholderText = typeof(this.config.placeholder) === "string" ? this.config.placeholder @@ -8091,35 +8081,31 @@ wysihtml5.views.View = Base.extend( // Make sure that the browser avoids using inline styles whenever possible this.commands.exec("styleWithCSS", false); - + this._initAutoLinking(); this._initObjectResizing(); this._initUndoManager(); - + this._initLineBreaking(); + // Simulate html5 autofocus on contentEditable element - if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) { - setTimeout(function() { that.focus(); }, 100); + // This doesn't work on IOS (5.1.1) + if ((this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) { + setTimeout(function() { that.focus(true); }, 100); } - - wysihtml5.quirks.insertLineBreakOnReturn(this); - + // IE sometimes leaves a single paragraph, which can't be removed by the user if (!browser.clearsContentEditableCorrectly()) { wysihtml5.quirks.ensureProperClearing(this); } - - if (!browser.clearsListsInContentEditableCorrectly()) { - wysihtml5.quirks.ensureProperClearingOfLists(this); - } - + // Set up a sync that makes sure that textarea and editor have the same content if (this.initSync && this.config.sync) { this.initSync(); } - + // Okay hide the textarea, we are ready to go this.textarea.hide(); - + // Fire global (before-)load event this.parent.fire("beforeload").fire("load"); }, @@ -8139,10 +8125,16 @@ wysihtml5.views.View = Base.extend( // Only do the auto linking by ourselves when the browser doesn't support auto linking // OR when he supports auto linking but we were able to turn it off (IE9+) if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) { - this.parent.observe("newword:composer", function() { - that.selection.executeAndRestore(function(startContainer, endContainer) { - dom.autoLink(endContainer.parentNode); - }); + this.parent.on("newword:composer", function() { + if (dom.getTextContent(that.element).match(dom.autoLink.URL_REG_EXP)) { + that.selection.executeAndRestore(function(startContainer, endContainer) { + dom.autoLink(endContainer.parentNode); + }); + } + }); + + dom.observe(this.element, "blur", function() { + dom.autoLink(that.element); }); } @@ -8193,42 +8185,130 @@ wysihtml5.views.View = Base.extend( }, _initObjectResizing: function() { - var properties = ["width", "height"], - propertiesLength = properties.length, - element = this.element; - - this.commands.exec("enableObjectResizing", this.config.allowObjectResizing); + this.commands.exec("enableObjectResizing", true); - if (this.config.allowObjectResizing) { - // IE sets inline styles after resizing objects - // The following lines make sure that the width/height css properties - // are copied over to the width/height attributes - if (browser.supportsEvent("resizeend")) { - dom.observe(element, "resizeend", function(event) { - var target = event.target || event.srcElement, - style = target.style, - i = 0, - property; - for(; i or tags after paste + // Inserting an invisible white space in front of it fixes the issue + if (browser.createsNestedInvalidMarkupAfterPaste()) { + dom.observe(this.element, "paste", function(event) { + var invisibleSpace = that.doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + that.selection.insertNode(invisibleSpace); + }); + } + + + dom.observe(this.doc, "keydown", function(event) { + var keyCode = event.keyCode; + + if (event.shiftKey) { + return; + } + + if (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY) { + return; + } + + var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { nodeName: USE_NATIVE_LINE_BREAK_INSIDE_TAGS }, 4); + if (blockElement) { + setTimeout(function() { + // Unwrap paragraph after leaving a list or a H1-6 + var selectedNode = that.selection.getSelectedNode(), + list; + + if (blockElement.nodeName === "LI") { + if (!selectedNode) { + return; + } + + list = dom.getParentElement(selectedNode, { nodeName: LIST_TAGS }, 2); + + if (!list) { + adjust(selectedNode); + } + } + + if (keyCode === wysihtml5.ENTER_KEY && blockElement.nodeName.match(/^H[1-6]$/)) { + adjust(selectedNode); + } + }, 0); + return; + } + + if (that.config.useLineBreaks && keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) { + that.commands.exec("insertLineBreak"); + event.preventDefault(); + } + }); } }); })(wysihtml5);(function(wysihtml5) { @@ -8271,23 +8351,16 @@ wysihtml5.views.View = Base.extend( "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius", "width", "height" ], - /** - * Styles to sync while the window gets resized - */ - RESIZE_STYLE = [ - "width", "height", - "top", "left", "right", "bottom" - ], ADDITIONAL_CSS_RULES = [ - "html { height: 100%; }", - "body { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }", - "._wysihtml5-temp { display: none; }", + "html { height: 100%; }", + "body { height: 100%; padding: 1px 0 0 0; margin: -1px 0 0 0; }", + "body > p:first-child { margin-top: 0; }", + "._wysihtml5-temp { display: none; }", wysihtml5.browser.isGecko ? "body.placeholder { color: graytext !important; }" : "body.placeholder { color: #a9a9a9 !important; }", - "body[disabled] { background-color: #eee !important; color: #999 !important; cursor: default !important; }", // Ensure that user see's broken images and can delete them - "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }" + "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }" ]; /** @@ -8341,9 +8414,14 @@ wysihtml5.views.View = Base.extend( originalActiveElement = doc.querySelector(":focus"), textareaElement = this.textarea.element, hasPlaceholder = textareaElement.hasAttribute("placeholder"), - originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"); - this.focusStylesHost = this.focusStylesHost || HOST_TEMPLATE.cloneNode(false); - this.blurStylesHost = this.blurStylesHost || HOST_TEMPLATE.cloneNode(false); + originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"), + originalDisplayValue = textareaElement.style.display, + originalDisabled = textareaElement.disabled, + displayValueForCopying; + + this.focusStylesHost = HOST_TEMPLATE.cloneNode(false); + this.blurStylesHost = HOST_TEMPLATE.cloneNode(false); + this.disabledStylesHost = HOST_TEMPLATE.cloneNode(false); // Remove placeholder before copying (as the placeholder has an affect on the computed style) if (hasPlaceholder) { @@ -8353,72 +8431,84 @@ wysihtml5.views.View = Base.extend( if (textareaElement === originalActiveElement) { textareaElement.blur(); } - + + // enable for copying styles + textareaElement.disabled = false; + + // set textarea to display="none" to get cascaded styles via getComputedStyle + textareaElement.style.display = displayValueForCopying = "none"; + + if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") || + (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) { + textareaElement.style.display = displayValueForCopying = originalDisplayValue; + } + // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) --------- dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost); - + // --------- editor styles --------- dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost); - + // --------- apply standard rules --------- dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument); - + + // --------- :disabled styles --------- + textareaElement.disabled = true; + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost); + dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost); + textareaElement.disabled = originalDisabled; + // --------- :focus styles --------- + textareaElement.style.display = originalDisplayValue; focusWithoutScrolling(textareaElement); + textareaElement.style.display = displayValueForCopying; + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost); dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost); - + + // reset textarea + textareaElement.style.display = originalDisplayValue; + + dom.copyStyles(["display"]).from(textareaElement).to(this.iframe); + // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus // this is needed for when the change_view event is fired where the iframe is hidden and then // the blur event fires and re-displays it var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]); - + // --------- restore focus --------- if (originalActiveElement) { originalActiveElement.focus(); } else { textareaElement.blur(); } - + // --------- restore placeholder --------- if (hasPlaceholder) { textareaElement.setAttribute("placeholder", originalPlaceholder); } - - // When copying styles, we only get the computed style which is never returned in percent unit - // Therefore we've to recalculate style onresize - if (!wysihtml5.browser.hasCurrentStyleProperty()) { - var winObserver = dom.observe(win, "resize", function() { - // Remove event listener if composer doesn't exist anymore - if (!dom.contains(document.documentElement, that.iframe)) { - winObserver.stop(); - return; - } - var originalTextareaDisplayStyle = dom.getStyle("display").from(textareaElement), - originalComposerDisplayStyle = dom.getStyle("display").from(that.iframe); - textareaElement.style.display = ""; - that.iframe.style.display = "none"; - dom.copyStyles(RESIZE_STYLE) - .from(textareaElement) - .to(that.iframe) - .andTo(that.focusStylesHost) - .andTo(that.blurStylesHost); - that.iframe.style.display = originalComposerDisplayStyle; - textareaElement.style.display = originalTextareaDisplayStyle; - }); - } - + // --------- Sync focus/blur styles --------- - this.parent.observe("focus:composer", function() { + this.parent.on("focus:composer", function() { dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe); dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element); }); - - this.parent.observe("blur:composer", function() { + + this.parent.on("blur:composer", function() { dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe); dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); }); - + + this.parent.observe("disable:composer", function() { + dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.iframe); + dom.copyStyles(TEXT_FORMATTING) .from(that.disabledStylesHost).to(that.element); + }); + + this.parent.observe("enable:composer", function() { + dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe); + dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); + }); + return this; }; })(wysihtml5);/** @@ -8447,8 +8537,7 @@ wysihtml5.views.View = Base.extend( iframe = this.sandbox.getIframe(), element = this.element, focusBlurElement = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(), - // Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same - pasteEvents = browser.supportsEvent("drop") ? ["drop", "paste"] : ["dragdrop", "paste"]; + pasteEvents = ["drop", "paste"]; // --------- destroy:composer event --------- dom.observe(iframe, "DOMNodeRemoved", function() { @@ -8464,7 +8553,6 @@ wysihtml5.views.View = Base.extend( } }, 250); - // --------- Focus & blur logic --------- dom.observe(focusBlurElement, "focus", function() { that.parent.fire("focus").fire("focus:composer"); @@ -8480,56 +8568,16 @@ wysihtml5.views.View = Base.extend( } that.parent.fire("blur").fire("blur:composer"); }); - - if (wysihtml5.browser.isIos()) { - // When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus - // but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible) - // We prevent that by focusing a temporary input element which immediately loses focus - dom.observe(element, "blur", function() { - var input = element.ownerDocument.createElement("input"), - originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop, - originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; - try { - that.selection.insertNode(input); - } catch(e) { - element.appendChild(input); - } - input.focus(); - input.parentNode.removeChild(input); - - window.scrollTo(originalScrollLeft, originalScrollTop); - }); - } // --------- Drag & Drop logic --------- dom.observe(element, "dragenter", function() { that.parent.fire("unset_placeholder"); }); - if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) { - dom.observe(element, ["dragover", "dragenter"], function(event) { - event.preventDefault(); - }); - } - - dom.observe(element, pasteEvents, function(event) { - var dataTransfer = event.dataTransfer, - data; - - if (dataTransfer && browser.supportsDataTransfer()) { - data = dataTransfer.getData("text/html") || dataTransfer.getData("text/plain"); - } - if (data) { - element.focus(); - that.commands.exec("insertHTML", data); + dom.observe(element, pasteEvents, function() { + setTimeout(function() { that.parent.fire("paste").fire("paste:composer"); - event.stopPropagation(); - event.preventDefault(); - } else { - setTimeout(function() { - that.parent.fire("paste").fire("paste:composer"); - }, 0); - } + }, 0); }); // --------- neword event --------- @@ -8540,7 +8588,7 @@ wysihtml5.views.View = Base.extend( } }); - this.parent.observe("paste:composer", function() { + this.parent.on("paste:composer", function() { setTimeout(function() { that.parent.fire("newword:composer"); }, 0); }); @@ -8555,6 +8603,34 @@ wysihtml5.views.View = Base.extend( }); } + if (browser.hasHistoryIssue() && browser.supportsSelectionModify()) { + dom.observe(element, "keydown", function(event) { + if (!event.metaKey && !event.ctrlKey) { + return; + } + + var keyCode = event.keyCode, + win = element.ownerDocument.defaultView, + selection = win.getSelection(); + + 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(); + } + }); + } + // --------- Shortcut logic --------- dom.observe(element, "keydown", function(event) { var keyCode = event.keyCode, @@ -8583,7 +8659,24 @@ wysihtml5.views.View = Base.extend( event.preventDefault(); } }); + + // --------- IE 8+9 focus the editor when the iframe is clicked (without actually firing the 'focus' event on the ) --------- + if (browser.hasIframeFocusIssue()) { + dom.observe(this.iframe, "focus", function() { + setTimeout(function() { + if (that.doc.querySelector(":focus") !== that.element) { + that.focus(); + } + }, 0); + }); + dom.observe(this.element, "blur", function() { + setTimeout(function() { + that.selection.getSelection().removeAllRanges(); + }, 0); + }); + } + // --------- Show url in tooltip when hovering links or images --------- var titlePrefixes = { IMG: "Image: ", @@ -8687,7 +8780,7 @@ wysihtml5.views.View = Base.extend( }); } - this.editor.observe("change_view", function(view) { + this.editor.on("change_view", function(view) { if (view === "composer" && !interval) { that.fromTextareaToComposer(true); startInterval(); @@ -8697,7 +8790,7 @@ wysihtml5.views.View = Base.extend( } }); - this.editor.observe("destroy:composer", stopInterval); + this.editor.on("destroy:composer", stopInterval); } }); })(wysihtml5); @@ -8755,7 +8848,7 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( */ events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"]; - parent.observe("beforeload", function() { + parent.on("beforeload", function() { wysihtml5.dom.observe(element, events, function(event) { var eventName = eventMapping[event.type] || event.type; parent.fire(eventName).fire(eventName + ":textarea"); @@ -8829,7 +8922,7 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( event.stopPropagation(); }; - dom.observe(that.link, "click", function(event) { + dom.observe(that.link, "click", function() { if (dom.hasClass(that.link, CLASS_NAME_OPENED)) { setTimeout(function() { that.hide(); }, 0); } @@ -8841,6 +8934,7 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( callbackWrapper(event); } if (keyCode === wysihtml5.ESCAPE_KEY) { + that.fire("cancel"); that.hide(); } }); @@ -8930,6 +9024,10 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( * Show the dialog element */ show: function(elementToChange) { + if (dom.hasClass(this.link, CLASS_NAME_OPENED)) { + return; + } + var that = this, firstField = this.container.querySelector(SELECTOR_FORM_ELEMENTS); this.elementToChange = elementToChange; @@ -9014,7 +9112,11 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( link.style.display = "none"; return; } - + var lang = parent.editor.textarea.element.getAttribute("lang"); + if (lang) { + inputAttributes.lang = lang; + } + var wrapper = document.createElement("div"); wysihtml5.lang.object(wrapperStyles).merge({ @@ -9026,7 +9128,7 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( dom.insert(wrapper).into(link); dom.setStyles(inputStyles).on(input); - dom.setAttributes(inputAttributes).on(input) + dom.setAttributes(inputAttributes).on(input); dom.setStyles(wrapperStyles).on(wrapper); dom.setStyles(linkStyles).on(link); @@ -9126,13 +9228,13 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( if (dialogElement) { dialog = new wysihtml5.toolbar.Dialog(link, dialogElement); - dialog.observe("show", function() { + dialog.on("show", function() { caretBookmark = that.composer.selection.getBookmark(); that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); }); - dialog.observe("save", function(attributes) { + dialog.on("save", function(attributes) { if (caretBookmark) { that.composer.selection.setBookmark(caretBookmark); } @@ -9141,7 +9243,7 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); }); - dialog.observe("cancel", function() { + dialog.on("cancel", function() { that.editor.focus(false); that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); }); @@ -9180,14 +9282,12 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( execAction: function(action) { var editor = this.editor; - switch(action) { - case "change_view": - if (editor.currentView === editor.textarea) { - editor.fire("change_view", "composer"); - } else { - editor.fire("change_view", "textarea"); - } - break; + if (action === "change_view") { + if (editor.currentView === editor.textarea) { + editor.fire("change_view", "composer"); + } else { + editor.fire("change_view", "textarea"); + } } }, @@ -9202,14 +9302,18 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( for (; i for line breaks, set this to false to use

+ useLineBreaks: true, // Array (or single string) of stylesheet urls to be loaded in the editor's iframe stylesheets: [], // Placeholder text to use, defaults to the placeholder attribute on the textarea element placeholderText: undef, - // Whether the composer should allow the user to manually resize images, tables etc. - allowObjectResizing: true, // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5) - supportTouchDevices: true + supportTouchDevices: true, + // Whether senseless elements (empty or without attributes) should be removed/replaced with their content + cleanUp: true }; wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend( @@ -9426,7 +9533,7 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( this._initParser(); } - this.observe("beforeload", function() { + this.on("beforeload", function() { this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer); if (this.config.toolbar) { this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar); @@ -9452,9 +9559,12 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( }, setValue: function(html, parse) { + this.fire("unset_placeholder"); + if (!html) { return this.clear(); } + this.currentView.setValue(html, parse); return this; }, @@ -9489,7 +9599,7 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( }, parse: function(htmlOrElement) { - var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true); + var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), this.config.cleanUp); if (typeof(htmlOrElement) === "object") { wysihtml5.quirks.redraw(htmlOrElement); } @@ -9501,7 +9611,7 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( * - Observes for paste and drop */ _initParser: function() { - this.observe("paste:composer", function() { + this.on("paste:composer", function() { var keepScrollPosition = true, that = this; that.composer.selection.executeAndRestore(function() { @@ -9509,13 +9619,6 @@ wysihtml5.views.Textarea = wysihtml5.views.View.extend( that.parse(that.composer.element); }, keepScrollPosition); }); - - this.observe("paste:textarea", function() { - var value = this.textarea.getValue(), - newValue; - newValue = this.parse(value); - this.textarea.setValue(newValue); - }); } }); })(wysihtml5);