Login To DeskApp
Photographs replaced with artwork generated in the page, so this is one HTML file with no external images and no build step.
") { 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 (
- |
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 * * @param {Element} element The element object which needs to be rerendered * @example * wysihtml5.quirks.redraw(document.body); */ (function(wysihtml5) { var CLASS_NAME = "wysihtml5-quirks-redraw"; wysihtml5.quirks.redraw = function(element) { wysihtml5.dom.addClass(element, CLASS_NAME); wysihtml5.dom.removeClass(element, CLASS_NAME); // Following hack is needed for firefox to make sure that image resize handles are properly removed try { var doc = element.ownerDocument; doc.execCommand("italic", false, null); doc.execCommand("italic", false, null); } catch(e) {} }; })(wysihtml5);/** * Selection API * * @example * var selection = new wysihtml5.Selection(editor); */ (function(wysihtml5) { var dom = wysihtml5.dom; function _getCumulativeOffsetTop(element) { var top = 0; if (element.parentNode) { do { top += element.offsetTop || 0; element = element.offsetParent; } while (element); } return top; } wysihtml5.Selection = Base.extend( /** @scope wysihtml5.Selection.prototype */ { constructor: function(editor) { // Make sure that our external range library is initialized window.rangy.init(); this.editor = editor; this.composer = editor.composer; this.doc = this.composer.doc; }, /** * Get the current selection as a bookmark to be able to later restore it * * @return {Object} An object that represents the current selection */ getBookmark: function() { var range = this.getRange(); return range && range.cloneRange(); }, /** * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark * * @param {Object} bookmark An object that represents the current selection */ setBookmark: function(bookmark) { if (!bookmark) { return; } this.setSelection(bookmark); }, /** * Set the caret in front of the given node * * @param {Object} node The element or text node where to position the caret in front of * @example * selection.setBefore(myElement); */ setBefore: function(node) { var range = rangy.createRange(this.doc); range.setStartBefore(node); range.setEndBefore(node); return this.setSelection(range); }, /** * Set the caret after the given node * * @param {Object} node The element or text node where to position the caret in front of * @example * selection.setBefore(myElement); */ setAfter: function(node) { var range = rangy.createRange(this.doc); range.setStartAfter(node); range.setEndAfter(node); return this.setSelection(range); }, /** * Ability to select/mark nodes * * @param {Element} node The node/element to select * @example * selection.selectNode(document.getElementById("my-image")); */ selectNode: function(node) { var range = rangy.createRange(this.doc), isElement = node.nodeType === wysihtml5.ELEMENT_NODE, canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"), content = isElement ? node.innerHTML : node.data, isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE), displayStyle = dom.getStyle("display").from(node), isBlockElement = (displayStyle === "block" || displayStyle === "list-item"); if (isEmpty && isElement && canHaveHTML) { // Make sure that caret is visible in node by inserting a zero width no breaking space try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} } if (canHaveHTML) { range.selectNodeContents(node); } else { range.selectNode(node); } if (canHaveHTML && isEmpty && isElement) { range.collapse(isBlockElement); } else if (canHaveHTML && isEmpty) { range.setStartAfter(node); range.setEndAfter(node); } this.setSelection(range); }, /** * Get the node which contains the selection * * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange" * @return {Object} The node that contains the caret * @example * var nodeThatContainsCaret = selection.getSelectedNode(); */ getSelectedNode: function(controlRange) { var selection, range; if (controlRange && this.doc.selection && this.doc.selection.type === "Control") { range = this.doc.selection.createRange(); if (range && range.length) { return range.item(0); } } selection = this.getSelection(this.doc); if (selection.focusNode === selection.anchorNode) { return selection.focusNode; } else { range = this.getRange(this.doc); return range ? range.commonAncestorContainer : this.doc.body; } }, executeAndRestore: function(method, restoreScrollPosition) { var body = this.doc.body, oldScrollTop = restoreScrollPosition && body.scrollTop, oldScrollLeft = restoreScrollPosition && body.scrollLeft, className = "_wysihtml5-temp-placeholder", placeholderHTML = '' + wysihtml5.INVISIBLE_SPACE + '', range = this.getRange(this.doc), newRange; // Nothing selected, execute and say goodbye if (!range) { method(body, body); return; } var 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); } caretPlaceholder = this.doc.querySelector("." + className); if (caretPlaceholder) { newRange = rangy.createRange(this.doc); newRange.selectNode(caretPlaceholder); newRange.deleteContents(); this.setSelection(newRange); } else { // fallback for when all hell breaks loose body.focus(); } if (restoreScrollPosition) { body.scrollTop = oldScrollTop; body.scrollLeft = oldScrollLeft; } // Remove it again, just to make sure that the placeholder is definitely out of the dom tree try { caretPlaceholder.parentNode.removeChild(caretPlaceholder); } catch(e4) {} }, /** * Different approach of preserving the selection (doesn't modify the dom) * Takes all text nodes in the selection and saves the selection position in the first and last one */ executeAndRestoreSimple: function(method) { var range = this.getRange(), body = this.doc.body, newRange, firstNode, lastNode, textNodes, rangeBackup; // Nothing selected, execute and say goodbye if (!range) { method(body, body); return; } textNodes = range.getNodes([3]); firstNode = textNodes[0] || range.startContainer; lastNode = textNodes[textNodes.length - 1] || range.endContainer; rangeBackup = { collapsed: range.collapsed, startContainer: firstNode, startOffset: firstNode === range.startContainer ? range.startOffset : 0, endContainer: lastNode, endOffset: lastNode === range.endContainer ? range.endOffset : lastNode.length }; try { method(range.startContainer, range.endContainer); } catch(e) { setTimeout(function() { throw e; }, 0); } newRange = rangy.createRange(this.doc); try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {} try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {} try { this.setSelection(newRange); } catch(e3) {} }, /** * Insert html at the caret position and move the cursor after the inserted html * * @param {String} html HTML string to insert * @example * selection.insertHTML("
foobar
"); */ insertHTML: function(html) { var range = rangy.createRange(this.doc), node = range.createContextualFragment(html), lastChild = node.lastChild; this.insertNode(node); if (lastChild) { this.setAfter(lastChild); } }, /** * Insert a node at the caret position and move the cursor behind it * * @param {Object} node HTML string to insert * @example * selection.insertNode(document.createTextNode("foobar")); */ insertNode: function(node) { var range = this.getRange(); if (range) { range.insertNode(node); } }, /** * Wraps current selection with the given node * * @param {Object} node The node to surround the selected elements with */ surround: function(node) { var range = this.getRange(); if (!range) { return; } try { // This only works when the range boundaries are not overlapping other elements range.surroundContents(node); this.selectNode(node); } catch(e) { // fallback node.appendChild(range.extractContents()); range.insertNode(node); } }, /** * Scroll the current caret position into the view * FIXME: This is a bit hacky, there might be a smarter way of doing this * * @example * selection.scrollIntoView(); */ scrollIntoView: function() { var doc = this.doc, hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight, tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() { var element = doc.createElement("span"); // The element needs content in order to be able to calculate it's position properly element.innerHTML = wysihtml5.INVISIBLE_SPACE; return element; })(), offsetTop; if (hasScrollBars) { this.insertNode(tempElement); offsetTop = _getCumulativeOffsetTop(tempElement); tempElement.parentNode.removeChild(tempElement); if (offsetTop > doc.body.scrollTop) { doc.body.scrollTop = offsetTop; } } }, /** * Select line where the caret is in */ selectLine: function() { if (wysihtml5.browser.supportsSelectionModify()) { this._selectLine_W3C(); } else if (this.doc.selection) { this._selectLine_MSIE(); } }, /** * See # */ _selectLine_W3C: function() { var win = this.doc.defaultView, selection = win.getSelection(); selection.modify("extend", "left", "lineboundary"); selection.modify("extend", "right", "lineboundary"); }, _selectLine_MSIE: function() { var range = this.doc.selection.createRange(), rangeTop = range.boundingTop, rangeHeight = range.boundingHeight, scrollWidth = this.doc.body.scrollWidth, rangeBottom, rangeEnd, measureNode, i, j; if (!range.moveToPoint) { return; } if (rangeTop === 0) { // Don't know why, but when the selection ends at the end of a line // range.boundingTop is 0 measureNode = this.doc.createElement("span"); this.insertNode(measureNode); rangeTop = measureNode.offsetTop; measureNode.parentNode.removeChild(measureNode); } rangeTop += 1; for (i=-10; i=0; j--) { try { rangeEnd.moveToPoint(j, rangeBottom); break; } catch(e2) {} } range.setEndPoint("EndToEnd", rangeEnd); range.select(); }, getText: function() { var selection = this.getSelection(); return selection ? selection.toString() : ""; }, getNodes: function(nodeType, filter) { var range = this.getRange(); if (range) { return range.getNodes([nodeType], filter); } else { return []; } }, getRange: function() { var selection = this.getSelection(); return selection && selection.rangeCount && selection.getRangeAt(0); }, getSelection: function() { return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow); }, setSelection: function(range) { var win = this.doc.defaultView || this.doc.parentWindow, selection = rangy.getSelection(win); return selection.setSingleRange(range); } }); })(wysihtml5); /** * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license. * # * * changed in order to be able ... * - to use custom tags * - to detect and replace similar css classes via reg exp */ (function(wysihtml5, rangy) { var defaultTagName = "span"; var REG_EXP_WHITE_SPACE = /\s+/g; function hasClass(el, cssClass, regExp) { if (!el.className) { return false; } var matchingClassNames = el.className.match(regExp) || []; return matchingClassNames[matchingClassNames.length - 1] === cssClass; } function addClass(el, cssClass, regExp) { if (el.className) { removeClass(el, regExp); el.className += " " + cssClass; } else { el.className = cssClass; } } function removeClass(el, regExp) { if (el.className) { el.className = el.className.replace(regExp, ""); } } function hasSameClasses(el1, el2) { return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " "); } function replaceWithOwnChildren(el) { var parent = el.parentNode; while (el.firstChild) { parent.insertBefore(el.firstChild, el); } parent.removeChild(el); } function elementsHaveSameNonClassAttributes(el1, el2) { if (el1.attributes.length != el2.attributes.length) { return false; } for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { attr1 = el1.attributes[i]; name = attr1.name; if (name != "class") { attr2 = el2.attributes.getNamedItem(name); if (attr1.specified != attr2.specified) { return false; } if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) { return false; } } } return true; } function isSplitPoint(node, offset) { if (rangy.dom.isCharacterDataNode(node)) { if (offset == 0) { return !!node.previousSibling; } else if (offset == node.length) { return !!node.nextSibling; } else { return true; } } return offset > 0 && offset < node.childNodes.length; } function splitNodeAt(node, descendantNode, descendantOffset) { var newNode; if (rangy.dom.isCharacterDataNode(descendantNode)) { if (descendantOffset == 0) { descendantOffset = rangy.dom.getNodeIndex(descendantNode); descendantNode = descendantNode.parentNode; } else if (descendantOffset == descendantNode.length) { descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1; descendantNode = descendantNode.parentNode; } else { newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset); } } if (!newNode) { newNode = descendantNode.cloneNode(false); if (newNode.id) { newNode.removeAttribute("id"); } var child; while ((child = descendantNode.childNodes[descendantOffset])) { newNode.appendChild(child); } rangy.dom.insertAfter(newNode, descendantNode); } return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode)); } function Merge(firstNode) { this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE); this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; this.textNodes = [this.firstTextNode]; } Merge.prototype = { doMerge: function() { var textBits = [], textNode, parent, text; for (var i = 0, len = this.textNodes.length; i < len; ++i) { textNode = this.textNodes[i]; parent = textNode.parentNode; textBits[i] = textNode.data; if (i) { parent.removeChild(textNode); if (!parent.hasChildNodes()) { parent.parentNode.removeChild(parent); } } } this.firstTextNode.data = text = textBits.join(""); return text; }, getLength: function() { var i = this.textNodes.length, len = 0; while (i--) { len += this.textNodes[i].length; } return len; }, toString: function() { var textBits = []; for (var i = 0, len = this.textNodes.length; i < len; ++i) { textBits[i] = "'" + this.textNodes[i].data + "'"; } return "[Merge(" + textBits.join(",") + ")]"; } }; function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) { this.tagNames = tagNames || [defaultTagName]; this.cssClass = cssClass || ""; this.similarClassRegExp = similarClassRegExp; this.normalize = normalize; this.applyToAnyTagName = false; } HTMLApplier.prototype = { getAncestorWithClass: function(node) { var cssClassMatch; while (node) { cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true; if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) { return node; } node = node.parentNode; } return false; }, // Normalizes nodes after applying a CSS class to a Range. postApply: function(textNodes, range) { var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; var merges = [], currentMerge; var rangeStartNode = firstNode, rangeEndNode = lastNode; var rangeStartOffset = 0, rangeEndOffset = lastNode.length; var textNode, precedingTextNode; for (var i = 0, len = textNodes.length; i < len; ++i) { textNode = textNodes[i]; precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false); if (precedingTextNode) { if (!currentMerge) { currentMerge = new Merge(precedingTextNode); merges.push(currentMerge); } currentMerge.textNodes.push(textNode); if (textNode === firstNode) { rangeStartNode = currentMerge.firstTextNode; rangeStartOffset = rangeStartNode.length; } if (textNode === lastNode) { rangeEndNode = currentMerge.firstTextNode; rangeEndOffset = currentMerge.getLength(); } } else { currentMerge = null; } } // Test whether the first node after the range needs merging var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true); if (nextTextNode) { if (!currentMerge) { currentMerge = new Merge(lastNode); merges.push(currentMerge); } currentMerge.textNodes.push(nextTextNode); } // Do the merges if (merges.length) { for (i = 0, len = merges.length; i < len; ++i) { merges[i].doMerge(); } // Set the range boundaries range.setStart(rangeStartNode, rangeStartOffset); range.setEnd(rangeEndNode, rangeEndOffset); } }, getAdjacentMergeableTextNode: function(node, forward) { var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE); var el = isTextNode ? node.parentNode : node; var adjacentNode; var propName = forward ? "nextSibling" : "previousSibling"; if (isTextNode) { // Can merge if the node's previous/next sibling is a text node adjacentNode = node[propName]; if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) { return adjacentNode; } } else { // Compare element with its sibling adjacentNode = el[propName]; if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) { return adjacentNode[forward ? "firstChild" : "lastChild"]; } } return null; }, areElementsMergeable: function(el1, el2) { return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase()) && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase()) && hasSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2); }, createContainer: function(doc) { var el = doc.createElement(this.tagNames[0]); if (this.cssClass) { el.className = this.cssClass; } return el; }, applyToTextNode: function(textNode) { var parent = textNode.parentNode; if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) { if (this.cssClass) { addClass(parent, this.cssClass, this.similarClassRegExp); } } else { var el = this.createContainer(rangy.dom.getDocument(textNode)); textNode.parentNode.insertBefore(el, textNode); el.appendChild(textNode); } }, isRemovable: function(el) { return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass; }, undoToTextNode: function(textNode, range, ancestorWithClass) { if (!range.containsNode(ancestorWithClass)) { // Split out the portion of the ancestor from which we can remove the CSS class var ancestorRange = range.cloneRange(); ancestorRange.selectNode(ancestorWithClass); if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) { splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset); range.setEndAfter(ancestorWithClass); } if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) { ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset); } } if (this.similarClassRegExp) { removeClass(ancestorWithClass, this.similarClassRegExp); } if (this.isRemovable(ancestorWithClass)) { replaceWithOwnChildren(ancestorWithClass); } }, applyToRange: function(range) { var textNodes = range.getNodes([wysihtml5.TEXT_NODE]); if (!textNodes.length) { try { var node = this.createContainer(range.endContainer.ownerDocument); range.surroundContents(node); this.selectNode(range, node); return; } catch(e) {} } range.splitBoundaries(); textNodes = range.getNodes([wysihtml5.TEXT_NODE]); if (textNodes.length) { var textNode; for (var i = 0, len = textNodes.length; i < len; ++i) { textNode = textNodes[i]; if (!this.getAncestorWithClass(textNode)) { this.applyToTextNode(textNode); } } range.setStart(textNodes[0], 0); textNode = textNodes[textNodes.length - 1]; range.setEnd(textNode, textNode.length); if (this.normalize) { this.postApply(textNodes, range); } } }, undoToRange: function(range) { var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass; if (textNodes.length) { range.splitBoundaries(); textNodes = range.getNodes([wysihtml5.TEXT_NODE]); } else { var doc = range.endContainer.ownerDocument, node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); range.insertNode(node); range.selectNode(node); textNodes = [node]; } for (var i = 0, len = textNodes.length; i < len; ++i) { textNode = textNodes[i]; ancestorWithClass = this.getAncestorWithClass(textNode); if (ancestorWithClass) { this.undoToTextNode(textNode, range, ancestorWithClass); } } if (len == 1) { this.selectNode(range, textNodes[0]); } else { range.setStart(textNodes[0], 0); textNode = textNodes[textNodes.length - 1]; range.setEnd(textNode, textNode.length); if (this.normalize) { this.postApply(textNodes, range); } } }, selectNode: function(range, node) { var isElement = node.nodeType === wysihtml5.ELEMENT_NODE, canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true, content = isElement ? node.innerHTML : node.data, isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE); if (isEmpty && isElement && canHaveHTML) { // Make sure that caret is visible in node by inserting a zero width no breaking space try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} } range.selectNodeContents(node); if (isEmpty && isElement) { range.collapse(false); } else if (isEmpty) { range.setStartAfter(node); range.setEndAfter(node); } }, getTextSelectedByRange: function(textNode, range) { var textRange = range.cloneRange(); textRange.selectNodeContents(textNode); var intersectionRange = textRange.intersection(range); var text = intersectionRange ? intersectionRange.toString() : ""; textRange.detach(); return text; }, isAppliedToRange: function(range) { var ancestors = [], ancestor, textNodes = range.getNodes([wysihtml5.TEXT_NODE]); if (!textNodes.length) { ancestor = this.getAncestorWithClass(range.startContainer); return ancestor ? [ancestor] : false; } for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) { selectedText = this.getTextSelectedByRange(textNodes[i], range); ancestor = this.getAncestorWithClass(textNodes[i]); if (selectedText != "" && !ancestor) { return false; } else { ancestors.push(ancestor); } } return ancestors; }, toggleRange: function(range) { if (this.isAppliedToRange(range)) { this.undoToRange(range); } else { this.applyToRange(range); } } }; wysihtml5.selection.HTMLApplier = HTMLApplier; })(wysihtml5, rangy);/** * Rich Text Query/Formatting Commands * * @example * var commands = new wysihtml5.Commands(editor); */ wysihtml5.Commands = Base.extend( /** @scope wysihtml5.Commands.prototype */ { constructor: function(editor) { this.editor = editor; this.composer = editor.composer; this.doc = this.composer.doc; }, /** * Check whether the browser supports the given command * * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") * @example * commands.supports("createLink"); */ support: function(command) { return wysihtml5.browser.supportsCommand(this.doc, command); }, /** * Check whether the browser supports the given command * * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList") * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...) * @example * commands.exec("insertImage", "#"); */ exec: function(command, value) { var obj = wysihtml5.commands[command], args = wysihtml5.lang.array(arguments).get(), method = obj && obj.exec, result = null; this.editor.fire("beforecommand:composer"); if (method) { args.unshift(this.composer); result = method.apply(obj, args); } else { try { // try/catch for buggy firefox result = this.doc.execCommand(command, false, value); } catch(e) {} } this.editor.fire("aftercommand:composer"); return result; }, /** * Check whether the current command is active * If the caret is within a bold text, then calling this with command "bold" should return true * * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src) * @return {Boolean} Whether the command is active * @example * var isCurrentSelectionBold = commands.state("bold"); */ state: function(command, commandValue) { var obj = wysihtml5.commands[command], args = wysihtml5.lang.array(arguments).get(), method = obj && obj.state; if (method) { args.unshift(this.composer); return method.apply(obj, args); } else { try { // try/catch for buggy firefox return this.doc.queryCommandState(command); } catch(e) { 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");
},
value: function() {
return undef;
}
};
})(wysihtml5);
(function(wysihtml5) {
var undef,
NODE_NAME = "A",
dom = wysihtml5.dom;
function _removeFormat(composer, anchors) {
var length = anchors.length,
i = 0,
anchor,
codeElement,
textContent;
for (; i contains url-like text content, rename it to to prevent re-autolinking
// else replace with its childNodes
if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {
// element is used to prevent later auto-linking of the content
codeElement = dom.renameElement(anchor, "code");
} else {
dom.replaceWithChildNodes(anchor);
}
}
}
function _format(composer, attributes) {
var doc = composer.doc,
tempClass = "_wysihtml5-temp-" + (+new Date()),
tempClassRegExp = /non-matching-class/g,
i = 0,
length,
anchors,
anchor,
hasElementChild,
isEmpty,
elementToSetCaretAfter,
textContent,
whiteSpace,
j;
wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp);
anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);
length = anchors.length;
for (; i element
* The element is needed to avoid auto linking
*
* @example
* // either ...
* wysihtml5.commands.createLink.exec(composer, "createLink", "#");
* // ... or ...
* wysihtml5.commands.createLink.exec(composer, "createLink", { href: "#", target: "_blank" });
*/
exec: function(composer, command, value) {
var anchors = this.state(composer, command);
if (anchors) {
// Selection contains links
composer.selection.executeAndRestore(function() {
_removeFormat(composer, anchors);
});
} else {
// Create links
value = typeof(value) === "object" ? value : { href: value };
_format(composer, value);
}
},
state: function(composer, command) {
return wysihtml5.commands.formatInline.state(composer, command, "A");
},
value: function() {
return undef;
}
};
})(wysihtml5);/**
* document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags
* which we don't want
* Instead we set a css class
*/
(function(wysihtml5) {
var undef,
REG_EXP = /wysiwyg-font-size-[a-z\-]+/g;
wysihtml5.commands.fontSize = {
exec: function(composer, command, size) {
return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
},
state: function(composer, command, size) {
return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
},
value: function() {
return undef;
}
};
})(wysihtml5);
/**
* document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
* which we don't want
* Instead we set a css class
*/
(function(wysihtml5) {
var undef,
REG_EXP = /wysiwyg-color-[a-z]+/g;
wysihtml5.commands.foreColor = {
exec: function(composer, command, color) {
return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
},
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",
// 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];
/**
* Remove similiar classes (based on classRegExp)
* and add the desired class name
*/
function _addClass(element, className, classRegExp) {
if (element.className) {
_removeClass(element, classRegExp);
element.className += " " + className;
} else {
element.className = className;
}
}
function _removeClass(element, classRegExp) {
element.className = element.className.replace(classRegExp, "");
}
/**
* Check whether given node is a text node and whether it's empty
*/
function _isBlankTextNode(node) {
return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim();
}
/**
* Returns previous sibling node that is not a blank text node
*/
function _getPreviousSiblingThatIsNotBlank(node) {
var previousSibling = node.previousSibling;
while (previousSibling && _isBlankTextNode(previousSibling)) {
previousSibling = previousSibling.previousSibling;
}
return previousSibling;
}
/**
* Returns next sibling node that is not a blank text node
*/
function _getNextSiblingThatIsNotBlank(node) {
var nextSibling = node.nextSibling;
while (nextSibling && _isBlankTextNode(nextSibling)) {
nextSibling = nextSibling.nextSibling;
}
return nextSibling;
}
/**
* Adds line breaks before and after the given node if the previous and next siblings
* aren't already causing a visual line break (block element or
)
*/
function _addLineBreakBeforeAndAfter(node) {
var doc = node.ownerDocument,
nextSibling = _getNextSiblingThatIsNotBlank(node),
previousSibling = _getPreviousSiblingThatIsNotBlank(node);
if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
node.parentNode.insertBefore(doc.createElement("br"), nextSibling);
}
if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
node.parentNode.insertBefore(doc.createElement("br"), node);
}
}
/**
* Removes line breaks before and after the given node
*/
function _removeLineBreakBeforeAndAfter(node) {
var nextSibling = _getNextSiblingThatIsNotBlank(node),
previousSibling = _getPreviousSiblingThatIsNotBlank(node);
if (nextSibling && _isLineBreak(nextSibling)) {
nextSibling.parentNode.removeChild(nextSibling);
}
if (previousSibling && _isLineBreak(previousSibling)) {
previousSibling.parentNode.removeChild(previousSibling);
}
}
function _removeLastChildIfLineBreak(node) {
var lastChild = node.lastChild;
if (lastChild && _isLineBreak(lastChild)) {
lastChild.parentNode.removeChild(lastChild);
}
}
function _isLineBreak(node) {
return node.nodeName === "BR";
}
/**
* Checks whether the elment causes a visual line break
* (
or block elements)
*/
function _isLineBreakOrBlockElement(element) {
if (_isLineBreak(element)) {
return true;
}
if (dom.getStyle("display").from(element) === "block") {
return true;
}
return false;
}
/**
* Execute native query command
* and if necessary modify the inserted node's className
*/
function _execCommand(doc, command, nodeName, className) {
if (className) {
var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {
var target = event.target,
displayStyle;
if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
return;
}
displayStyle = dom.getStyle("display").from(target);
if (displayStyle.substr(0, 6) !== "inline") {
// Make sure that only block elements receive the given class
target.className += " " + className;
}
});
}
doc.execCommand(command, false, nodeName);
if (eventListener) {
eventListener.stop();
}
}
function _selectLineAndWrap(composer, element) {
composer.selection.selectLine();
composer.selection.surround(element);
_removeLineBreakBeforeAndAfter(element);
_removeLastChildIfLineBreak(element);
composer.selection.selectNode(element);
}
function _hasClasses(element) {
return !!wysihtml5.lang.string(element.className).trim();
}
wysihtml5.commands.formatBlock = {
exec: function(composer, command, nodeName, className, classRegExp) {
var doc = composer.doc,
blockElement = this.state(composer, command, nodeName, className, classRegExp),
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)) {
// 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);
}
});
return;
}
// Find similiar block element and rename it ( => )
if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {
selectedNode = composer.selection.getSelectedNode();
blockElement = dom.getParentElement(selectedNode, {
nodeName: BLOCK_ELEMENTS_GROUP
});
if (blockElement) {
composer.selection.executeAndRestoreSimple(function() {
// Rename current block element to new block element and add class
if (nodeName) {
blockElement = dom.renameElement(blockElement, nodeName);
}
if (className) {
_addClass(blockElement, className, classRegExp);
}
});
return;
}
}
if (composer.commands.support(command)) {
_execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className);
return;
}
blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME);
if (className) {
blockElement.className = className;
}
_selectLineAndWrap(composer, blockElement);
},
state: function(composer, command, nodeName, className, classRegExp) {
nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
var selectedNode = composer.selection.getSelectedNode();
return dom.getParentElement(selectedNode, {
nodeName: nodeName,
className: className,
classRegExp: classRegExp
});
},
value: function() {
return undef;
}
};
})(wysihtml5);/**
* formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
*
* #1 caret in unformatted text:
* abcdefg|
* output:
* abcdefg|
*
* #2 unformatted text selected:
* abc|deg|h
* output:
* abc|deg|h
*
* #3 unformatted text selected across boundaries:
* ab|c defg|h
* output:
* ab|c defg|h
*
* #4 formatted text entirely selected
* |abc|
* output:
* |abc|
*
* #5 formatted text partially selected
* ab|c|
* output:
* ab|c|
*
* #6 formatted text selected across boundaries
* ab|c de|fgh
* output:
* ab|c de|fgh
*/
(function(wysihtml5) {
var undef,
// Treat as and vice versa
ALIAS_MAPPING = {
"strong": "b",
"em": "i",
"b": "strong",
"i": "em"
},
htmlApplier = {};
function _getTagNames(tagName) {
var alias = ALIAS_MAPPING[tagName];
return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
}
function _getApplier(tagName, className, classRegExp) {
var identifier = tagName + ":" + className;
if (!htmlApplier[identifier]) {
htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true);
}
return htmlApplier[identifier];
}
wysihtml5.commands.formatInline = {
exec: function(composer, command, tagName, className, classRegExp) {
var range = composer.selection.getRange();
if (!range) {
return false;
}
_getApplier(tagName, className, classRegExp).toggleRange(range);
composer.selection.setSelection(range);
},
state: function(composer, command, tagName, className, classRegExp) {
var doc = composer.doc,
aliasTagName = ALIAS_MAPPING[tagName] || tagName,
range;
// Check whether the document contains a node with the desired tagName
if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&
!wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
return false;
}
// Check whether the document contains a node with the desired className
if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
return false;
}
range = composer.selection.getRange();
if (!range) {
return false;
}
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);(function(wysihtml5) {
var NODE_NAME = "IMG";
wysihtml5.commands.insertImage = {
/**
* Inserts an
* If selection is already an image link, it removes it
*
* @example
* // either ...
* wysihtml5.commands.insertImage.exec(composer, "insertImage", "#");
* // ... or ...
* wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "#", title: "foo" });
*/
exec: function(composer, command, value) {
value = typeof(value) === "object" ? value : { src: value };
var doc = composer.doc,
image = this.state(composer),
textNode,
i,
parent;
if (image) {
// Image already selected, set the caret before it and delete it
composer.selection.setBefore(image);
parent = image.parentNode;
parent.removeChild(image);
// and it's parent too if it hasn't got any other relevant child nodes
wysihtml5.dom.removeEmptyTextNodes(parent);
if (parent.nodeName === "A" && !parent.firstChild) {
composer.selection.setAfter(parent);
parent.parentNode.removeChild(parent);
}
// firefox and ie sometimes don't remove the image handles, even though the image got removed
wysihtml5.quirks.redraw(composer.element);
return;
}
image = doc.createElement(NODE_NAME);
for (i in value) {
image[i] = value[i];
}
composer.selection.insertNode(image);
if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) {
textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
composer.selection.insertNode(textNode);
composer.selection.setAfter(textNode);
} else {
composer.selection.setAfter(image);
}
},
state: function(composer) {
var doc = composer.doc,
selectedNode,
text,
imagesInSelection;
if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {
return false;
}
selectedNode = composer.selection.getSelectedNode();
if (!selectedNode) {
return false;
}
if (selectedNode.nodeName === NODE_NAME) {
// This works perfectly in IE
return selectedNode;
}
if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
return false;
}
text = composer.selection.getText();
text = wysihtml5.lang.string(text).trim();
if (text) {
return false;
}
imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) {
return node.nodeName === "IMG";
});
if (imagesInSelection.length !== 1) {
return false;
}
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() ? " " : "");
wysihtml5.commands.insertLineBreak = {
exec: function(composer, command) {
if (composer.commands.support(command)) {
composer.doc.execCommand(command, false, null);
if (!wysihtml5.browser.autoScrollsToCaret()) {
composer.selection.scrollIntoView();
}
} else {
composer.commands.exec("insertHTML", LINE_BREAK);
}
},
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
// - foo
- 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:
// - foo
- 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"));
}
}
},
state: function(composer) {
var selectedNode = composer.selection.getSelectedNode();
return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" });
},
value: function() {
return undef;
}
};
})(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
// - foo
- 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"));
}
}
},
state: function(composer) {
var selectedNode = composer.selection.getSelectedNode();
return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" });
},
value: function() {
return undef;
}
};
})(wysihtml5);(function(wysihtml5) {
var undef;
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");
},
value: function() {
return undef;
}
};
})(wysihtml5);(function(wysihtml5) {
var undef,
CLASS_NAME = "wysiwyg-text-align-center",
REG_EXP = /wysiwyg-text-align-[a-z]+/g;
wysihtml5.commands.justifyCenter = {
exec: function(composer, command) {
return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
},
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;
wysihtml5.commands.justifyLeft = {
exec: function(composer, command) {
return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
},
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;
wysihtml5.commands.justifyRight = {
exec: function(composer, command) {
return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
},
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 = {
exec: function(composer, command) {
return wysihtml5.commands.formatInline.exec(composer, command, "u");
},
state: function(composer, command) {
return wysihtml5.commands.formatInline.state(composer, command, "u");
},
value: function() {
return undef;
}
};
})(wysihtml5);/**
* Undo Manager for wysihtml5
* slightly inspired by #
*/
(function(wysihtml5) {
var Z_KEY = 90,
Y_KEY = 89,
BACKSPACE_KEY = 8,
DELETE_KEY = 46,
MAX_HISTORY_ENTRIES = 40,
UNDO_HTML = '' + wysihtml5.INVISIBLE_SPACE + '',
REDO_HTML = '' + wysihtml5.INVISIBLE_SPACE + '',
dom = wysihtml5.dom;
function cleanTempElements(doc) {
var tempElement;
while (tempElement = doc.querySelector("._wysihtml5-temp")) {
tempElement.parentNode.removeChild(tempElement);
}
}
wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(
/** @scope wysihtml5.UndoManager.prototype */ {
constructor: function(editor) {
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();
}
},
_observe: function() {
var that = this,
doc = this.composer.sandbox.getDocument(),
lastKey;
// Catch CTRL+Z and CTRL+Y
dom.observe(this.element, "keydown", function(event) {
if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
return;
}
var keyCode = event.keyCode,
isUndo = keyCode === Z_KEY && !event.shiftKey,
isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
if (isUndo) {
that.undo();
event.preventDefault();
} else if (isRedo) {
that.redo();
event.preventDefault();
}
});
// Catch delete and backspace
dom.observe(this.element, "keydown", function(event) {
var keyCode = event.keyCode;
if (keyCode === lastKey) {
return;
}
lastKey = keyCode;
if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
that.transact();
}
});
// Now this is very hacky:
// These days browsers don't offer a undo/redo event which we could hook into
// to be notified when the user hits undo/redo in the contextmenu.
// Therefore we simply insert two elements as soon as the contextmenu gets opened.
// The last element being inserted will be immediately be removed again by a exexCommand("undo")
// => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu
// => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu
if (wysihtml5.browser.hasUndoInContextMenu()) {
var interval, observed, cleanUp = function() {
cleanTempElements(doc);
clearInterval(interval);
};
dom.observe(this.element, "contextmenu", function() {
cleanUp();
that.composer.selection.executeAndRestoreSimple(function() {
if (that.element.lastChild) {
that.composer.selection.setAfter(that.element.lastChild);
}
// enable undo button in context menu
doc.execCommand("insertHTML", false, UNDO_HTML);
// enable redo button in context menu
doc.execCommand("insertHTML", false, REDO_HTML);
doc.execCommand("undo", false, null);
});
interval = setInterval(function() {
if (doc.getElementById("_wysihtml5-redo")) {
cleanUp();
that.redo();
} else if (!doc.getElementById("_wysihtml5-undo")) {
cleanUp();
that.undo();
}
}, 400);
if (!observed) {
observed = true;
dom.observe(document, "mousedown", cleanUp);
dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp);
}
});
}
this.editor
.observe("newword:composer", function() {
that.transact();
})
.observe("beforecommand:composer", function() {
that.transact();
});
},
transact: function() {
var previousHtml = this.history[this.position - 1],
currentHtml = this.composer.getValue();
if (currentHtml == previousHtml) {
return;
}
var length = this.history.length = this.position;
if (length > MAX_HISTORY_ENTRIES) {
this.history.shift();
this.position--;
}
this.position++;
this.history.push(currentHtml);
},
undo: function() {
this.transact();
if (this.position <= 1) {
return;
}
this.set(this.history[--this.position - 1]);
this.editor.fire("undo:composer");
},
redo: function() {
if (this.position >= this.history.length) {
return;
}
this.set(this.history[++this.position - 1]);
this.editor.fire("redo:composer");
},
set: function(html) {
this.composer.setValue(html);
this.editor.focus(true);
}
});
})(wysihtml5);
/**
* TODO: the following methods still need unit test coverage
*/
wysihtml5.views.View = Base.extend(
/** @scope wysihtml5.views.View.prototype */ {
constructor: function(parent, textareaElement, config) {
this.parent = parent;
this.element = textareaElement;
this.config = config;
this._observeViewChange();
},
_observeViewChange: function() {
var that = this;
this.parent.observe("beforeload", function() {
that.parent.observe("change_view", function(view) {
if (view === that.name) {
that.parent.currentView = that;
that.show();
// Using tiny delay here to make sure that the placeholder is set before focusing
setTimeout(function() { that.focus(); }, 0);
} else {
that.hide();
}
});
});
},
focus: function() {
if (this.element.ownerDocument.querySelector(":focus") === this.element) {
return;
}
try { this.element.focus(); } catch(e) {}
},
hide: function() {
this.element.style.display = "none";
},
show: function() {
this.element.style.display = "";
},
disable: function() {
this.element.setAttribute("disabled", "disabled");
},
enable: function() {
this.element.removeAttribute("disabled");
}
});(function(wysihtml5) {
var dom = wysihtml5.dom,
browser = wysihtml5.browser;
wysihtml5.views.Composer = wysihtml5.views.View.extend(
/** @scope wysihtml5.views.Composer.prototype */ {
name: "composer",
// Needed for firefox in order to display a proper caret in an empty contentEditable
CARET_HACK: "
",
constructor: function(parent, textareaElement, config) {
this.base(parent, textareaElement, config);
this.textarea = this.parent.textarea;
this._initSandbox();
},
clear: function() {
this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK;
},
getValue: function(parse) {
var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element);
if (parse) {
value = this.parent.parse(value);
}
// Replace all "zero width no breaking space" chars
// which are used as hacks to enable some functionalities
// Also remove all CARET hacks that somehow got left
value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by("");
return value;
},
setValue: function(html, parse) {
if (parse) {
html = this.parent.parse(html);
}
this.element.innerHTML = html;
},
show: function() {
this.iframe.style.display = this._displayStyle || "";
// Firefox needs this, otherwise contentEditable becomes uneditable
this.disable();
this.enable();
},
hide: function() {
this._displayStyle = dom.getStyle("display").from(this.iframe);
if (this._displayStyle === "none") {
this._displayStyle = null;
}
this.iframe.style.display = "none";
},
disable: function() {
this.element.removeAttribute("contentEditable");
this.base();
},
enable: function() {
this.element.setAttribute("contentEditable", "true");
this.base();
},
focus: function(setToEnd) {
// IE 8 fires the focus event after .focus()
// This is needed by our simulate_placeholder.js to work
// therefore we clear it ourselves this time
if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
this.clear();
}
this.base();
var lastChild = this.element.lastChild;
if (setToEnd && lastChild) {
if (lastChild.nodeName === "BR") {
this.selection.setBefore(this.element.lastChild);
} else {
this.selection.setAfter(this.element.lastChild);
}
}
},
getTextContent: function() {
return dom.getTextContent(this.element);
},
hasPlaceholderSet: function() {
return this.getTextContent() == this.textarea.element.getAttribute("placeholder");
},
isEmpty: function() {
var innerHTML = this.element.innerHTML,
elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea";
return innerHTML === "" ||
innerHTML === this.CARET_HACK ||
this.hasPlaceholderSet() ||
(this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue));
},
_initSandbox: function() {
var that = this;
this.sandbox = new dom.Sandbox(function() {
that._create();
}, {
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: function() {
var that = this;
this.doc = this.sandbox.getDocument();
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
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);
}
// Simulate html5 placeholder attribute on contentEditable element
var placeholderText = typeof(this.config.placeholder) === "string"
? this.config.placeholder
: this.textarea.element.getAttribute("placeholder");
if (placeholderText) {
dom.simulatePlaceholder(this.parent, this, placeholderText);
}
// Make sure that the browser avoids using inline styles whenever possible
this.commands.exec("styleWithCSS", false);
this._initAutoLinking();
this._initObjectResizing();
this._initUndoManager();
// Simulate html5 autofocus on contentEditable element
if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) {
setTimeout(function() { that.focus(); }, 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");
},
_initAutoLinking: function() {
var that = this,
supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
supportsAutoLinking = browser.doesAutoLinkingInContentEditable();
if (supportsDisablingOfAutoLinking) {
this.commands.exec("autoUrlDetect", false);
}
if (!this.config.autoLink) {
return;
}
// 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);
});
});
}
// Assuming we have the following:
// #
// If a user now changes the url in the innerHTML we want to make sure that
// it's synchronized with the href attribute (as long as the innerHTML is still a url)
var // Use a live NodeList to check whether there are any links in the document
links = this.sandbox.getDocument().getElementsByTagName("a"),
// The autoLink helper method reveals a reg exp to detect correct urls
urlRegExp = dom.autoLink.URL_REG_EXP,
getTextContent = function(element) {
var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim();
if (textContent.substr(0, 4) === "www.") {
textContent = "http://" + textContent;
}
return textContent;
};
dom.observe(this.element, "keydown", function(event) {
if (!links.length) {
return;
}
var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),
textContent;
if (!link) {
return;
}
textContent = getTextContent(link);
// keydown is fired before the actual content is changed
// therefore we set a timeout to change the href
setTimeout(function() {
var newTextContent = getTextContent(link);
if (newTextContent === textContent) {
return;
}
// Only set href when new href looks like a valid url
if (newTextContent.match(urlRegExp)) {
link.setAttribute("href", newTextContent);
}
}, 0);
});
},
_initObjectResizing: function() {
var properties = ["width", "height"],
propertiesLength = properties.length,
element = this.element;
this.commands.exec("enableObjectResizing", this.config.allowObjectResizing);
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 backspace, 46 => delete
parent = target.parentNode;
// delete the
parent.removeChild(target);
// and it's parent too if it hasn't got any other child nodes
if (parent.nodeName === "A" && !parent.firstChild) {
parent.parentNode.removeChild(parent);
}
setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);
event.preventDefault();
}
});
// --------- Show url in tooltip when hovering links or images ---------
var titlePrefixes = {
IMG: "Image: ",
A: "Link: "
};
dom.observe(element, "mouseover", function(event) {
var target = event.target,
nodeName = target.nodeName,
title;
if (nodeName !== "A" && nodeName !== "IMG") {
return;
}
var hasTitle = target.hasAttribute("title");
if(!hasTitle){
title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
target.setAttribute("title", title);
}
});
};
})(wysihtml5);/**
* Class that takes care that the value of the composer and the textarea is always in sync
*/
(function(wysihtml5) {
var INTERVAL = 400;
wysihtml5.views.Synchronizer = Base.extend(
/** @scope wysihtml5.views.Synchronizer.prototype */ {
constructor: function(editor, textarea, composer) {
this.editor = editor;
this.textarea = textarea;
this.composer = composer;
this._observe();
},
/**
* Sync html from composer to textarea
* Takes care of placeholders
* @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
*/
fromComposerToTextarea: function(shouldParseHtml) {
this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml);
},
/**
* Sync value of textarea to composer
* Takes care of placeholders
* @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
*/
fromTextareaToComposer: function(shouldParseHtml) {
var textareaValue = this.textarea.getValue();
if (textareaValue) {
this.composer.setValue(textareaValue, shouldParseHtml);
} else {
this.composer.clear();
this.editor.fire("set_placeholder");
}
},
/**
* Invoke syncing based on view state
* @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
*/
sync: function(shouldParseHtml) {
if (this.editor.currentView.name === "textarea") {
this.fromTextareaToComposer(shouldParseHtml);
} else {
this.fromComposerToTextarea(shouldParseHtml);
}
},
/**
* Initializes interval-based syncing
* also makes sure that on-submit the composer's content is synced with the textarea
* immediately when the form gets submitted
*/
_observe: function() {
var interval,
that = this,
form = this.textarea.element.form,
startInterval = function() {
interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
},
stopInterval = function() {
clearInterval(interval);
interval = null;
};
startInterval();
if (form) {
// If the textarea is in a form make sure that after onreset and onsubmit the composer
// has the correct state
wysihtml5.dom.observe(form, "submit", function() {
that.sync(true);
});
wysihtml5.dom.observe(form, "reset", function() {
setTimeout(function() { that.fromTextareaToComposer(); }, 0);
});
}
this.editor.observe("change_view", function(view) {
if (view === "composer" && !interval) {
that.fromTextareaToComposer(true);
startInterval();
} else if (view === "textarea") {
that.fromComposerToTextarea(true);
stopInterval();
}
});
this.editor.observe("destroy:composer", stopInterval);
}
});
})(wysihtml5);
wysihtml5.views.Textarea = wysihtml5.views.View.extend(
/** @scope wysihtml5.views.Textarea.prototype */ {
name: "textarea",
constructor: function(parent, textareaElement, config) {
this.base(parent, textareaElement, config);
this._observe();
},
clear: function() {
this.element.value = "";
},
getValue: function(parse) {
var value = this.isEmpty() ? "" : this.element.value;
if (parse) {
value = this.parent.parse(value);
}
return value;
},
setValue: function(html, parse) {
if (parse) {
html = this.parent.parse(html);
}
this.element.value = html;
},
hasPlaceholderSet: function() {
var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),
placeholderText = this.element.getAttribute("placeholder") || null,
value = this.element.value,
isEmpty = !value;
return (supportsPlaceholder && isEmpty) || (value === placeholderText);
},
isEmpty: function() {
return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
},
_observe: function() {
var element = this.element,
parent = this.parent,
eventMapping = {
focusin: "focus",
focusout: "blur"
},
/**
* Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
* This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
*/
events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
parent.observe("beforeload", function() {
wysihtml5.dom.observe(element, events, function(event) {
var eventName = eventMapping[event.type] || event.type;
parent.fire(eventName).fire(eventName + ":textarea");
});
wysihtml5.dom.observe(element, ["paste", "drop"], function() {
setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
});
});
}
});/**
* Toolbar Dialog
*
* @param {Element} link The toolbar link which causes the dialog to show up
* @param {Element} container The dialog container
*
* @example
*
* insert an image
*
*
*
*
* URL:
*
*
* Alternative text:
*
*
*
*
Based on dropways/deskapp · MIT License · Copyright (c) 2018 DeskApp
Photographs replaced with artwork generated in the page, so this is one HTML file with no external images and no build step.
element is used to prevent later auto-linking of the content
codeElement = dom.renameElement(anchor, "code");
} else {
dom.replaceWithChildNodes(anchor);
}
}
}
function _format(composer, attributes) {
var doc = composer.doc,
tempClass = "_wysihtml5-temp-" + (+new Date()),
tempClassRegExp = /non-matching-class/g,
i = 0,
length,
anchors,
anchor,
hasElementChild,
isEmpty,
elementToSetCaretAfter,
textContent,
whiteSpace,
j;
wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp);
anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);
length = anchors.length;
for (; i element
* The element is needed to avoid auto linking
*
* @example
* // either ...
* wysihtml5.commands.createLink.exec(composer, "createLink", "#");
* // ... or ...
* wysihtml5.commands.createLink.exec(composer, "createLink", { href: "#", target: "_blank" });
*/
exec: function(composer, command, value) {
var anchors = this.state(composer, command);
if (anchors) {
// Selection contains links
composer.selection.executeAndRestore(function() {
_removeFormat(composer, anchors);
});
} else {
// Create links
value = typeof(value) === "object" ? value : { href: value };
_format(composer, value);
}
},
state: function(composer, command) {
return wysihtml5.commands.formatInline.state(composer, command, "A");
},
value: function() {
return undef;
}
};
})(wysihtml5);/**
* document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags
* which we don't want
* Instead we set a css class
*/
(function(wysihtml5) {
var undef,
REG_EXP = /wysiwyg-font-size-[a-z\-]+/g;
wysihtml5.commands.fontSize = {
exec: function(composer, command, size) {
return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
},
state: function(composer, command, size) {
return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
},
value: function() {
return undef;
}
};
})(wysihtml5);
/**
* document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
* which we don't want
* Instead we set a css class
*/
(function(wysihtml5) {
var undef,
REG_EXP = /wysiwyg-color-[a-z]+/g;
wysihtml5.commands.foreColor = {
exec: function(composer, command, color) {
return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
},
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",
// 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];
/**
* Remove similiar classes (based on classRegExp)
* and add the desired class name
*/
function _addClass(element, className, classRegExp) {
if (element.className) {
_removeClass(element, classRegExp);
element.className += " " + className;
} else {
element.className = className;
}
}
function _removeClass(element, classRegExp) {
element.className = element.className.replace(classRegExp, "");
}
/**
* Check whether given node is a text node and whether it's empty
*/
function _isBlankTextNode(node) {
return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim();
}
/**
* Returns previous sibling node that is not a blank text node
*/
function _getPreviousSiblingThatIsNotBlank(node) {
var previousSibling = node.previousSibling;
while (previousSibling && _isBlankTextNode(previousSibling)) {
previousSibling = previousSibling.previousSibling;
}
return previousSibling;
}
/**
* Returns next sibling node that is not a blank text node
*/
function _getNextSiblingThatIsNotBlank(node) {
var nextSibling = node.nextSibling;
while (nextSibling && _isBlankTextNode(nextSibling)) {
nextSibling = nextSibling.nextSibling;
}
return nextSibling;
}
/**
* Adds line breaks before and after the given node if the previous and next siblings
* aren't already causing a visual line break (block element or
)
*/
function _addLineBreakBeforeAndAfter(node) {
var doc = node.ownerDocument,
nextSibling = _getNextSiblingThatIsNotBlank(node),
previousSibling = _getPreviousSiblingThatIsNotBlank(node);
if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
node.parentNode.insertBefore(doc.createElement("br"), nextSibling);
}
if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
node.parentNode.insertBefore(doc.createElement("br"), node);
}
}
/**
* Removes line breaks before and after the given node
*/
function _removeLineBreakBeforeAndAfter(node) {
var nextSibling = _getNextSiblingThatIsNotBlank(node),
previousSibling = _getPreviousSiblingThatIsNotBlank(node);
if (nextSibling && _isLineBreak(nextSibling)) {
nextSibling.parentNode.removeChild(nextSibling);
}
if (previousSibling && _isLineBreak(previousSibling)) {
previousSibling.parentNode.removeChild(previousSibling);
}
}
function _removeLastChildIfLineBreak(node) {
var lastChild = node.lastChild;
if (lastChild && _isLineBreak(lastChild)) {
lastChild.parentNode.removeChild(lastChild);
}
}
function _isLineBreak(node) {
return node.nodeName === "BR";
}
/**
* Checks whether the elment causes a visual line break
* (
or block elements)
*/
function _isLineBreakOrBlockElement(element) {
if (_isLineBreak(element)) {
return true;
}
if (dom.getStyle("display").from(element) === "block") {
return true;
}
return false;
}
/**
* Execute native query command
* and if necessary modify the inserted node's className
*/
function _execCommand(doc, command, nodeName, className) {
if (className) {
var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {
var target = event.target,
displayStyle;
if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
return;
}
displayStyle = dom.getStyle("display").from(target);
if (displayStyle.substr(0, 6) !== "inline") {
// Make sure that only block elements receive the given class
target.className += " " + className;
}
});
}
doc.execCommand(command, false, nodeName);
if (eventListener) {
eventListener.stop();
}
}
function _selectLineAndWrap(composer, element) {
composer.selection.selectLine();
composer.selection.surround(element);
_removeLineBreakBeforeAndAfter(element);
_removeLastChildIfLineBreak(element);
composer.selection.selectNode(element);
}
function _hasClasses(element) {
return !!wysihtml5.lang.string(element.className).trim();
}
wysihtml5.commands.formatBlock = {
exec: function(composer, command, nodeName, className, classRegExp) {
var doc = composer.doc,
blockElement = this.state(composer, command, nodeName, className, classRegExp),
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)) {
// 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);
}
});
return;
}
// Find similiar block element and rename it ( => )
if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {
selectedNode = composer.selection.getSelectedNode();
blockElement = dom.getParentElement(selectedNode, {
nodeName: BLOCK_ELEMENTS_GROUP
});
if (blockElement) {
composer.selection.executeAndRestoreSimple(function() {
// Rename current block element to new block element and add class
if (nodeName) {
blockElement = dom.renameElement(blockElement, nodeName);
}
if (className) {
_addClass(blockElement, className, classRegExp);
}
});
return;
}
}
if (composer.commands.support(command)) {
_execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className);
return;
}
blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME);
if (className) {
blockElement.className = className;
}
_selectLineAndWrap(composer, blockElement);
},
state: function(composer, command, nodeName, className, classRegExp) {
nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
var selectedNode = composer.selection.getSelectedNode();
return dom.getParentElement(selectedNode, {
nodeName: nodeName,
className: className,
classRegExp: classRegExp
});
},
value: function() {
return undef;
}
};
})(wysihtml5);/**
* formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
*
* #1 caret in unformatted text:
* abcdefg|
* output:
* abcdefg|
*
* #2 unformatted text selected:
* abc|deg|h
* output:
* abc|deg|h
*
* #3 unformatted text selected across boundaries:
* ab|c defg|h
* output:
* ab|c defg|h
*
* #4 formatted text entirely selected
* |abc|
* output:
* |abc|
*
* #5 formatted text partially selected
* ab|c|
* output:
* ab|c|
*
* #6 formatted text selected across boundaries
* ab|c de|fgh
* output:
* ab|c de|fgh
*/
(function(wysihtml5) {
var undef,
// Treat as and vice versa
ALIAS_MAPPING = {
"strong": "b",
"em": "i",
"b": "strong",
"i": "em"
},
htmlApplier = {};
function _getTagNames(tagName) {
var alias = ALIAS_MAPPING[tagName];
return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
}
function _getApplier(tagName, className, classRegExp) {
var identifier = tagName + ":" + className;
if (!htmlApplier[identifier]) {
htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true);
}
return htmlApplier[identifier];
}
wysihtml5.commands.formatInline = {
exec: function(composer, command, tagName, className, classRegExp) {
var range = composer.selection.getRange();
if (!range) {
return false;
}
_getApplier(tagName, className, classRegExp).toggleRange(range);
composer.selection.setSelection(range);
},
state: function(composer, command, tagName, className, classRegExp) {
var doc = composer.doc,
aliasTagName = ALIAS_MAPPING[tagName] || tagName,
range;
// Check whether the document contains a node with the desired tagName
if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&
!wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
return false;
}
// Check whether the document contains a node with the desired className
if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
return false;
}
range = composer.selection.getRange();
if (!range) {
return false;
}
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);(function(wysihtml5) {
var NODE_NAME = "IMG";
wysihtml5.commands.insertImage = {
/**
* Inserts an
* If selection is already an image link, it removes it
*
* @example
* // either ...
* wysihtml5.commands.insertImage.exec(composer, "insertImage", "#");
* // ... or ...
* wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "#", title: "foo" });
*/
exec: function(composer, command, value) {
value = typeof(value) === "object" ? value : { src: value };
var doc = composer.doc,
image = this.state(composer),
textNode,
i,
parent;
if (image) {
// Image already selected, set the caret before it and delete it
composer.selection.setBefore(image);
parent = image.parentNode;
parent.removeChild(image);
// and it's parent too if it hasn't got any other relevant child nodes
wysihtml5.dom.removeEmptyTextNodes(parent);
if (parent.nodeName === "A" && !parent.firstChild) {
composer.selection.setAfter(parent);
parent.parentNode.removeChild(parent);
}
// firefox and ie sometimes don't remove the image handles, even though the image got removed
wysihtml5.quirks.redraw(composer.element);
return;
}
image = doc.createElement(NODE_NAME);
for (i in value) {
image[i] = value[i];
}
composer.selection.insertNode(image);
if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) {
textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
composer.selection.insertNode(textNode);
composer.selection.setAfter(textNode);
} else {
composer.selection.setAfter(image);
}
},
state: function(composer) {
var doc = composer.doc,
selectedNode,
text,
imagesInSelection;
if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {
return false;
}
selectedNode = composer.selection.getSelectedNode();
if (!selectedNode) {
return false;
}
if (selectedNode.nodeName === NODE_NAME) {
// This works perfectly in IE
return selectedNode;
}
if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
return false;
}
text = composer.selection.getText();
text = wysihtml5.lang.string(text).trim();
if (text) {
return false;
}
imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) {
return node.nodeName === "IMG";
});
if (imagesInSelection.length !== 1) {
return false;
}
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() ? " " : "");
wysihtml5.commands.insertLineBreak = {
exec: function(composer, command) {
if (composer.commands.support(command)) {
composer.doc.execCommand(command, false, null);
if (!wysihtml5.browser.autoScrollsToCaret()) {
composer.selection.scrollIntoView();
}
} else {
composer.commands.exec("insertHTML", LINE_BREAK);
}
},
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
// - foo
- 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:
// - foo
- 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"));
}
}
},
state: function(composer) {
var selectedNode = composer.selection.getSelectedNode();
return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" });
},
value: function() {
return undef;
}
};
})(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
// - foo
- 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"));
}
}
},
state: function(composer) {
var selectedNode = composer.selection.getSelectedNode();
return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" });
},
value: function() {
return undef;
}
};
})(wysihtml5);(function(wysihtml5) {
var undef;
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");
},
value: function() {
return undef;
}
};
})(wysihtml5);(function(wysihtml5) {
var undef,
CLASS_NAME = "wysiwyg-text-align-center",
REG_EXP = /wysiwyg-text-align-[a-z]+/g;
wysihtml5.commands.justifyCenter = {
exec: function(composer, command) {
return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
},
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;
wysihtml5.commands.justifyLeft = {
exec: function(composer, command) {
return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
},
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;
wysihtml5.commands.justifyRight = {
exec: function(composer, command) {
return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
},
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 = {
exec: function(composer, command) {
return wysihtml5.commands.formatInline.exec(composer, command, "u");
},
state: function(composer, command) {
return wysihtml5.commands.formatInline.state(composer, command, "u");
},
value: function() {
return undef;
}
};
})(wysihtml5);/**
* Undo Manager for wysihtml5
* slightly inspired by #
*/
(function(wysihtml5) {
var Z_KEY = 90,
Y_KEY = 89,
BACKSPACE_KEY = 8,
DELETE_KEY = 46,
MAX_HISTORY_ENTRIES = 40,
UNDO_HTML = '' + wysihtml5.INVISIBLE_SPACE + '',
REDO_HTML = '' + wysihtml5.INVISIBLE_SPACE + '',
dom = wysihtml5.dom;
function cleanTempElements(doc) {
var tempElement;
while (tempElement = doc.querySelector("._wysihtml5-temp")) {
tempElement.parentNode.removeChild(tempElement);
}
}
wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(
/** @scope wysihtml5.UndoManager.prototype */ {
constructor: function(editor) {
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();
}
},
_observe: function() {
var that = this,
doc = this.composer.sandbox.getDocument(),
lastKey;
// Catch CTRL+Z and CTRL+Y
dom.observe(this.element, "keydown", function(event) {
if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
return;
}
var keyCode = event.keyCode,
isUndo = keyCode === Z_KEY && !event.shiftKey,
isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
if (isUndo) {
that.undo();
event.preventDefault();
} else if (isRedo) {
that.redo();
event.preventDefault();
}
});
// Catch delete and backspace
dom.observe(this.element, "keydown", function(event) {
var keyCode = event.keyCode;
if (keyCode === lastKey) {
return;
}
lastKey = keyCode;
if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
that.transact();
}
});
// Now this is very hacky:
// These days browsers don't offer a undo/redo event which we could hook into
// to be notified when the user hits undo/redo in the contextmenu.
// Therefore we simply insert two elements as soon as the contextmenu gets opened.
// The last element being inserted will be immediately be removed again by a exexCommand("undo")
// => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu
// => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu
if (wysihtml5.browser.hasUndoInContextMenu()) {
var interval, observed, cleanUp = function() {
cleanTempElements(doc);
clearInterval(interval);
};
dom.observe(this.element, "contextmenu", function() {
cleanUp();
that.composer.selection.executeAndRestoreSimple(function() {
if (that.element.lastChild) {
that.composer.selection.setAfter(that.element.lastChild);
}
// enable undo button in context menu
doc.execCommand("insertHTML", false, UNDO_HTML);
// enable redo button in context menu
doc.execCommand("insertHTML", false, REDO_HTML);
doc.execCommand("undo", false, null);
});
interval = setInterval(function() {
if (doc.getElementById("_wysihtml5-redo")) {
cleanUp();
that.redo();
} else if (!doc.getElementById("_wysihtml5-undo")) {
cleanUp();
that.undo();
}
}, 400);
if (!observed) {
observed = true;
dom.observe(document, "mousedown", cleanUp);
dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp);
}
});
}
this.editor
.observe("newword:composer", function() {
that.transact();
})
.observe("beforecommand:composer", function() {
that.transact();
});
},
transact: function() {
var previousHtml = this.history[this.position - 1],
currentHtml = this.composer.getValue();
if (currentHtml == previousHtml) {
return;
}
var length = this.history.length = this.position;
if (length > MAX_HISTORY_ENTRIES) {
this.history.shift();
this.position--;
}
this.position++;
this.history.push(currentHtml);
},
undo: function() {
this.transact();
if (this.position <= 1) {
return;
}
this.set(this.history[--this.position - 1]);
this.editor.fire("undo:composer");
},
redo: function() {
if (this.position >= this.history.length) {
return;
}
this.set(this.history[++this.position - 1]);
this.editor.fire("redo:composer");
},
set: function(html) {
this.composer.setValue(html);
this.editor.focus(true);
}
});
})(wysihtml5);
/**
* TODO: the following methods still need unit test coverage
*/
wysihtml5.views.View = Base.extend(
/** @scope wysihtml5.views.View.prototype */ {
constructor: function(parent, textareaElement, config) {
this.parent = parent;
this.element = textareaElement;
this.config = config;
this._observeViewChange();
},
_observeViewChange: function() {
var that = this;
this.parent.observe("beforeload", function() {
that.parent.observe("change_view", function(view) {
if (view === that.name) {
that.parent.currentView = that;
that.show();
// Using tiny delay here to make sure that the placeholder is set before focusing
setTimeout(function() { that.focus(); }, 0);
} else {
that.hide();
}
});
});
},
focus: function() {
if (this.element.ownerDocument.querySelector(":focus") === this.element) {
return;
}
try { this.element.focus(); } catch(e) {}
},
hide: function() {
this.element.style.display = "none";
},
show: function() {
this.element.style.display = "";
},
disable: function() {
this.element.setAttribute("disabled", "disabled");
},
enable: function() {
this.element.removeAttribute("disabled");
}
});(function(wysihtml5) {
var dom = wysihtml5.dom,
browser = wysihtml5.browser;
wysihtml5.views.Composer = wysihtml5.views.View.extend(
/** @scope wysihtml5.views.Composer.prototype */ {
name: "composer",
// Needed for firefox in order to display a proper caret in an empty contentEditable
CARET_HACK: "
",
constructor: function(parent, textareaElement, config) {
this.base(parent, textareaElement, config);
this.textarea = this.parent.textarea;
this._initSandbox();
},
clear: function() {
this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK;
},
getValue: function(parse) {
var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element);
if (parse) {
value = this.parent.parse(value);
}
// Replace all "zero width no breaking space" chars
// which are used as hacks to enable some functionalities
// Also remove all CARET hacks that somehow got left
value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by("");
return value;
},
setValue: function(html, parse) {
if (parse) {
html = this.parent.parse(html);
}
this.element.innerHTML = html;
},
show: function() {
this.iframe.style.display = this._displayStyle || "";
// Firefox needs this, otherwise contentEditable becomes uneditable
this.disable();
this.enable();
},
hide: function() {
this._displayStyle = dom.getStyle("display").from(this.iframe);
if (this._displayStyle === "none") {
this._displayStyle = null;
}
this.iframe.style.display = "none";
},
disable: function() {
this.element.removeAttribute("contentEditable");
this.base();
},
enable: function() {
this.element.setAttribute("contentEditable", "true");
this.base();
},
focus: function(setToEnd) {
// IE 8 fires the focus event after .focus()
// This is needed by our simulate_placeholder.js to work
// therefore we clear it ourselves this time
if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
this.clear();
}
this.base();
var lastChild = this.element.lastChild;
if (setToEnd && lastChild) {
if (lastChild.nodeName === "BR") {
this.selection.setBefore(this.element.lastChild);
} else {
this.selection.setAfter(this.element.lastChild);
}
}
},
getTextContent: function() {
return dom.getTextContent(this.element);
},
hasPlaceholderSet: function() {
return this.getTextContent() == this.textarea.element.getAttribute("placeholder");
},
isEmpty: function() {
var innerHTML = this.element.innerHTML,
elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea";
return innerHTML === "" ||
innerHTML === this.CARET_HACK ||
this.hasPlaceholderSet() ||
(this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue));
},
_initSandbox: function() {
var that = this;
this.sandbox = new dom.Sandbox(function() {
that._create();
}, {
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: function() {
var that = this;
this.doc = this.sandbox.getDocument();
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
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);
}
// Simulate html5 placeholder attribute on contentEditable element
var placeholderText = typeof(this.config.placeholder) === "string"
? this.config.placeholder
: this.textarea.element.getAttribute("placeholder");
if (placeholderText) {
dom.simulatePlaceholder(this.parent, this, placeholderText);
}
// Make sure that the browser avoids using inline styles whenever possible
this.commands.exec("styleWithCSS", false);
this._initAutoLinking();
this._initObjectResizing();
this._initUndoManager();
// Simulate html5 autofocus on contentEditable element
if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) {
setTimeout(function() { that.focus(); }, 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");
},
_initAutoLinking: function() {
var that = this,
supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
supportsAutoLinking = browser.doesAutoLinkingInContentEditable();
if (supportsDisablingOfAutoLinking) {
this.commands.exec("autoUrlDetect", false);
}
if (!this.config.autoLink) {
return;
}
// 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);
});
});
}
// Assuming we have the following:
// #
// If a user now changes the url in the innerHTML we want to make sure that
// it's synchronized with the href attribute (as long as the innerHTML is still a url)
var // Use a live NodeList to check whether there are any links in the document
links = this.sandbox.getDocument().getElementsByTagName("a"),
// The autoLink helper method reveals a reg exp to detect correct urls
urlRegExp = dom.autoLink.URL_REG_EXP,
getTextContent = function(element) {
var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim();
if (textContent.substr(0, 4) === "www.") {
textContent = "http://" + textContent;
}
return textContent;
};
dom.observe(this.element, "keydown", function(event) {
if (!links.length) {
return;
}
var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),
textContent;
if (!link) {
return;
}
textContent = getTextContent(link);
// keydown is fired before the actual content is changed
// therefore we set a timeout to change the href
setTimeout(function() {
var newTextContent = getTextContent(link);
if (newTextContent === textContent) {
return;
}
// Only set href when new href looks like a valid url
if (newTextContent.match(urlRegExp)) {
link.setAttribute("href", newTextContent);
}
}, 0);
});
},
_initObjectResizing: function() {
var properties = ["width", "height"],
propertiesLength = properties.length,
element = this.element;
this.commands.exec("enableObjectResizing", this.config.allowObjectResizing);
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 backspace, 46 => delete
parent = target.parentNode;
// delete the
parent.removeChild(target);
// and it's parent too if it hasn't got any other child nodes
if (parent.nodeName === "A" && !parent.firstChild) {
parent.parentNode.removeChild(parent);
}
setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);
event.preventDefault();
}
});
// --------- Show url in tooltip when hovering links or images ---------
var titlePrefixes = {
IMG: "Image: ",
A: "Link: "
};
dom.observe(element, "mouseover", function(event) {
var target = event.target,
nodeName = target.nodeName,
title;
if (nodeName !== "A" && nodeName !== "IMG") {
return;
}
var hasTitle = target.hasAttribute("title");
if(!hasTitle){
title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
target.setAttribute("title", title);
}
});
};
})(wysihtml5);/**
* Class that takes care that the value of the composer and the textarea is always in sync
*/
(function(wysihtml5) {
var INTERVAL = 400;
wysihtml5.views.Synchronizer = Base.extend(
/** @scope wysihtml5.views.Synchronizer.prototype */ {
constructor: function(editor, textarea, composer) {
this.editor = editor;
this.textarea = textarea;
this.composer = composer;
this._observe();
},
/**
* Sync html from composer to textarea
* Takes care of placeholders
* @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
*/
fromComposerToTextarea: function(shouldParseHtml) {
this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml);
},
/**
* Sync value of textarea to composer
* Takes care of placeholders
* @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
*/
fromTextareaToComposer: function(shouldParseHtml) {
var textareaValue = this.textarea.getValue();
if (textareaValue) {
this.composer.setValue(textareaValue, shouldParseHtml);
} else {
this.composer.clear();
this.editor.fire("set_placeholder");
}
},
/**
* Invoke syncing based on view state
* @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
*/
sync: function(shouldParseHtml) {
if (this.editor.currentView.name === "textarea") {
this.fromTextareaToComposer(shouldParseHtml);
} else {
this.fromComposerToTextarea(shouldParseHtml);
}
},
/**
* Initializes interval-based syncing
* also makes sure that on-submit the composer's content is synced with the textarea
* immediately when the form gets submitted
*/
_observe: function() {
var interval,
that = this,
form = this.textarea.element.form,
startInterval = function() {
interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
},
stopInterval = function() {
clearInterval(interval);
interval = null;
};
startInterval();
if (form) {
// If the textarea is in a form make sure that after onreset and onsubmit the composer
// has the correct state
wysihtml5.dom.observe(form, "submit", function() {
that.sync(true);
});
wysihtml5.dom.observe(form, "reset", function() {
setTimeout(function() { that.fromTextareaToComposer(); }, 0);
});
}
this.editor.observe("change_view", function(view) {
if (view === "composer" && !interval) {
that.fromTextareaToComposer(true);
startInterval();
} else if (view === "textarea") {
that.fromComposerToTextarea(true);
stopInterval();
}
});
this.editor.observe("destroy:composer", stopInterval);
}
});
})(wysihtml5);
wysihtml5.views.Textarea = wysihtml5.views.View.extend(
/** @scope wysihtml5.views.Textarea.prototype */ {
name: "textarea",
constructor: function(parent, textareaElement, config) {
this.base(parent, textareaElement, config);
this._observe();
},
clear: function() {
this.element.value = "";
},
getValue: function(parse) {
var value = this.isEmpty() ? "" : this.element.value;
if (parse) {
value = this.parent.parse(value);
}
return value;
},
setValue: function(html, parse) {
if (parse) {
html = this.parent.parse(html);
}
this.element.value = html;
},
hasPlaceholderSet: function() {
var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),
placeholderText = this.element.getAttribute("placeholder") || null,
value = this.element.value,
isEmpty = !value;
return (supportsPlaceholder && isEmpty) || (value === placeholderText);
},
isEmpty: function() {
return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
},
_observe: function() {
var element = this.element,
parent = this.parent,
eventMapping = {
focusin: "focus",
focusout: "blur"
},
/**
* Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
* This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
*/
events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
parent.observe("beforeload", function() {
wysihtml5.dom.observe(element, events, function(event) {
var eventName = eventMapping[event.type] || event.type;
parent.fire(eventName).fire(eventName + ":textarea");
});
wysihtml5.dom.observe(element, ["paste", "drop"], function() {
setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
});
});
}
});/**
* Toolbar Dialog
*
* @param {Element} link The toolbar link which causes the dialog to show up
* @param {Element} container The dialog container
*
* @example
*
* insert an image
*
*
*
*
* URL:
*
*
* Alternative text:
*
*
*
*
Based on dropways/deskapp · MIT License · Copyright (c) 2018 DeskApp
Photographs replaced with artwork generated in the page, so this is one HTML file with no external images and no build step.