Deskapp - Login

nos-user · · HTML

About

Deskapp - Login. A landing page in a single HTML file, with calendar, modal, admin, dashboard. Open it in the browser, or copy it into a NOS workspace and edit it.

More like this

Document text

Login To DeskApp

I'm Manager
I'm Employee
Remember
Map placeholder

Open source ❤️

DOWNLOAD

Available in the following technologies:

Download
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.
' ).interpolate(templateVars); }, /** * Method to unset/override existing variables * @example * // Make cookie unreadable and unwritable * this._unset(document, "cookie", "", true); */ _unset: function(object, property, value, setter) { try { object[property] = value; } catch(e) {} try { object.__defineGetter__(property, function() { return value; }); } catch(e) {} if (setter) { try { object.__defineSetter__(property, function() {}); } catch(e) {} } if (!wysihtml5.browser.crashesWhenDefineProperty(property)) { try { var config = { get: function() { return value; } }; if (setter) { config.set = function() {}; } Object.defineProperty(object, property, config); } catch(e) {} } } }); })(wysihtml5); (function() { var mapping = { "className": "class" }; wysihtml5.dom.setAttributes = function(attributes) { return { on: function(element) { for (var i in attributes) { element.setAttribute(mapping[i] || i, attributes[i]); } } } }; })();wysihtml5.dom.setStyles = function(styles) { return { on: function(element) { var style = element.style; if (typeof(styles) === "string") { style.cssText += ";" + styles; return; } for (var i in styles) { if (i === "float") { style.cssFloat = styles[i]; style.styleFloat = styles[i]; } else { style[i] = styles[i]; } } } }; };/** * Simulate HTML5 placeholder attribute * * Needed since * - div[contentEditable] elements don't support it * - older browsers (such as IE8 and Firefox 3.6) don't support it at all * * @param {Object} parent Instance of main wysihtml5.Editor class * @param {Element} view Instance of wysihtml5.views.* class * @param {String} placeholderText * * @example * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar"); */ (function(dom) { dom.simulatePlaceholder = function(editor, view, placeholderText) { var CLASS_NAME = "placeholder", unset = function() { if (view.hasPlaceholderSet()) { view.clear(); } dom.removeClass(view.element, CLASS_NAME); }, set = function() { if (view.isEmpty()) { view.setValue(placeholderText); dom.addClass(view.element, CLASS_NAME); } }; editor .observe("set_placeholder", set) .observe("unset_placeholder", unset) .observe("focus:composer", unset) .observe("paste:composer", unset) .observe("blur:composer", set); set(); }; })(wysihtml5.dom); (function(dom) { var documentElement = document.documentElement; if ("textContent" in documentElement) { dom.setTextContent = function(element, text) { element.textContent = text; }; dom.getTextContent = function(element) { return element.textContent; }; } else if ("innerText" in documentElement) { dom.setTextContent = function(element, text) { element.innerText = text; }; dom.getTextContent = function(element) { return element.innerText; }; } else { dom.setTextContent = function(element, text) { element.nodeValue = text; }; dom.getTextContent = function(element) { return element.nodeValue; }; } })(wysihtml5.dom); /** * Fix most common html formatting misbehaviors of browsers implementation when inserting * content via copy & paste contentEditable * * @author Christopher Blum */ wysihtml5.quirks.cleanPastedHTML = (function() { // TODO: We probably need more rules here var defaultRules = { // When pasting underlined links into a contentEditable, IE thinks, it has to insert to keep the styling "a u": wysihtml5.dom.replaceWithChildNodes }; function cleanPastedHTML(elementOrHtml, rules, context) { rules = rules || defaultRules; context = context || elementOrHtml.ownerDocument || document; var element, isString = typeof(elementOrHtml) === "string", method, matches, matchesLength, i, j = 0; if (isString) { element = wysihtml5.dom.getAsDom(elementOrHtml, context); } else { element = elementOrHtml; } for (i in rules) { matches = element.querySelectorAll(i); method = rules[i]; matchesLength = matches.length; for (; j 

" || innerHTML == "

 

 

") { element.innerHTML = ""; } }, 0); }; return function(composer) { dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); }; })(); /** * In Opera when the caret is in the first and only item of a list (
  • |
) and the list is the first child of the contentEditable element, it's impossible to delete the list by hitting backspace * * @param {Object} contentEditableElement The contentEditable element to observe for clearing events * @exaple * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); */ wysihtml5.quirks.ensureProperClearingOfLists = (function() { var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"]; var clearIfNecessary = function(element, contentEditableElement) { if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) { return; } var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI }); if (!list) { return; } var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild; if (!listIsFirstChildOfContentEditable) { return; } var hasOnlyOneListItem = list.childNodes.length <= 1; if (!hasOnlyOneListItem) { return; } var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true; if (!onlyListItemIsEmpty) { return; } list.parentNode.removeChild(list); }; return function(composer) { dom.observe(composer.element, "keydown", function(event) { if (event.keyCode !== wysihtml5.BACKSPACE_KEY) { return; } var element = composer.selection.getSelectedNode(); clearIfNecessary(element, composer.element); }); }; })(); })(wysihtml5); // See # // // In Firefox this: // var d = document.createElement("div"); // d.innerHTML ='
'; // d.innerHTML; // will result in: // // which is wrong (function(wysihtml5) { var TILDE_ESCAPED = "%7E"; wysihtml5.quirks.getCorrectInnerHTML = function(element) { var innerHTML = element.innerHTML; if (innerHTML.indexOf(TILDE_ESCAPED) === -1) { return innerHTML; } var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"), url, urlToSearch, length, i; for (i=0, length=elementsWithTilde.length; i on return * - Chrome & Safari insert new
on return * - Firefox inserts
on return (yippie!) * * @param {Element} element * * @example * wysihtml5.quirks.insertLineBreakOnReturn(element); */ (function(wysihtml5) { var dom = wysihtml5.dom, USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"], LIST_TAGS = ["UL", "OL", "MENU"]; wysihtml5.quirks.insertLineBreakOnReturn = function(composer) { function unwrap(selectedNode) { var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2); if (!parentElement) { return; } var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE); dom.insert(invisibleSpace).before(parentElement); dom.replaceWithChildNodes(parentElement); composer.selection.selectNode(invisibleSpace); } function keyDown(event) { var keyCode = event.keyCode; if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) { return; } var element = event.target, selectedNode = composer.selection.getSelectedNode(), blockElement = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4); if (blockElement) { // Some browsers create

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

got inserted and unwrap it if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) { setTimeout(function() { var selectedNode = composer.selection.getSelectedNode(), list, div; if (!selectedNode) { return; } list = dom.getParentElement(selectedNode, { nodeName: LIST_TAGS }, 2); if (list) { return; } unwrap(selectedNode); }, 0); } else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) { setTimeout(function() { unwrap(composer.selection.getSelectedNode()); }, 0); } return; } if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) { composer.commands.exec("insertLineBreak"); event.preventDefault(); } } // keypress doesn't fire when you hit backspace dom.observe(composer.element.ownerDocument, "keydown", keyDown); }; })(wysihtml5);/** * Force rerendering of a given element * Needed to fix display misbehaviors of IE * * @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 //
  1. foo
  2. bar
// becomes: // foo
bar
composer.selection.executeAndRestoreSimple(function() { wysihtml5.dom.resolveList(list); }); } else if (otherList) { // Turn an unordered list into an ordered list //
  • foo
  • bar
// becomes: //
  1. foo
  2. bar
composer.selection.executeAndRestoreSimple(function() { wysihtml5.dom.renameElement(otherList, "ol"); }); } else { // Create list composer.commands.exec("formatBlock", "div", tempClassName); tempElement = doc.querySelector("." + tempClassName); isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; composer.selection.executeAndRestoreSimple(function() { list = wysihtml5.dom.convertToList(tempElement, "ol"); }); if (isEmpty) { composer.selection.selectNode(list.querySelector("li")); } } }, 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 //
  1. foo
  2. bar
// becomes: //
  • foo
  • bar
composer.selection.executeAndRestoreSimple(function() { wysihtml5.dom.renameElement(otherList, "ul"); }); } else { // Create list composer.commands.exec("formatBlock", "div", tempClassName); tempElement = doc.querySelector("." + tempClassName); isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; composer.selection.executeAndRestoreSimple(function() { list = wysihtml5.dom.convertToList(tempElement, "ul"); }); if (isEmpty) { composer.selection.selectNode(list.querySelector("li")); } } }, 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.