50 Cent
+ * + * + * + * + * + *foo bar
+ * + * var userHTML = 'I'm a table! |
foobar
foobar
foo
bar
'; + * wysihtml5.dom.parse(userHTML, { + * classes: { + * red: 1, + * green: 1 + * }, + * tags: { + * p: { + * rename_tag: "p" + * } + * } + * }); + * // => 'foo
bar
' + */ +wysihtml5.dom.parse = (function() { + + /** + * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML + * new DOMParser().parseFromString('doesn't need to be closed according HTML4-5 spec, we simply replace it with a
to preserve its content and layout + */ + if ("outerHTML" in oldNode) { + if (!wysihtml5.browser.autoClosesUnclosedTags() && + oldNode.nodeName === "P" && + oldNode.outerHTML.slice(-4).toLowerCase() !== "
") { + nodeName = "p"; + } + } + + if (nodeName in tagRules) { + rule = tagRules[nodeName]; + if (!rule || rule.remove) { + return null; + } + + rule = typeof(rule) === "string" ? { rename_tag: rule } : rule; + } else if (oldNode.firstChild) { + rule = { rename_tag: DEFAULT_NODE_NAME }; + } else { + // Remove empty unknown elements + return null; + } + + newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName); + _handleAttributes(oldNode, newNode, rule); + + oldNode = null; + return newNode; + } + + function _handleAttributes(oldNode, newNode, rule) { + var attributes = {}, // fresh new set of attributes to set on newNode + setClass = rule.set_class, // classes to set + addClass = rule.add_class, // add classes based on existing attributes + setAttributes = rule.set_attributes, // attributes to set on the current node + checkAttributes = rule.check_attributes, // check/convert values of attributes + allowedClasses = currentRules.classes, + i = 0, + classes = [], + newClasses = [], + newUniqueClasses = [], + oldClasses = [], + classesLength, + newClassesLength, + currentClass, + newClass, + attributeName, + newAttributeValue, + method; + + if (setAttributes) { + attributes = wysihtml5.lang.object(setAttributes).clone(); + } + + if (checkAttributes) { + for (attributeName in checkAttributes) { + method = attributeCheckMethods[checkAttributes[attributeName]]; + if (!method) { + continue; + } + newAttributeValue = method(_getAttribute(oldNode, attributeName)); + if (typeof(newAttributeValue) === "string") { + attributes[attributeName] = newAttributeValue; + } + } + } + + if (setClass) { + classes.push(setClass); + } + + if (addClass) { + for (attributeName in addClass) { + method = addClassMethods[addClass[attributeName]]; + if (!method) { + continue; + } + newClass = method(_getAttribute(oldNode, attributeName)); + if (typeof(newClass) === "string") { + classes.push(newClass); + } + } + } + + // make sure that wysihtml5 temp class doesn't get stripped out + allowedClasses["_wysihtml5-temp-placeholder"] = 1; + + // add old classes last + oldClasses = oldNode.getAttribute("class"); + if (oldClasses) { + classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); + } + classesLength = classes.length; + for (; i) and keeps its childs + * + * @param {Element} element The list element which should be renamed + * @param {Element} newNodeName The desired tag name + * + * @example + * + *
+ * hello + *
+ * + */ +wysihtml5.dom.replaceWithChildNodes = function(node) { + if (!node.parentNode) { + return; + } + + if (!node.firstChild) { + node.parentNode.removeChild(node); + return; + } + + var fragment = node.ownerDocument.createDocumentFragment(); + while (node.firstChild) { + fragment.appendChild(node.firstChild); + } + node.parentNode.replaceChild(fragment, node); + node = fragment = null; +}; +/** + * Unwraps an unordered/ordered list + * + * @param {Element} element The list element which should be unwrapped + * + * @example + * + *
") { + element.innerHTML = ""; + } + }, 0); + }; + + return function(composer) { + wysihtml5.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); + }; +})(); +// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 +// +// In Firefox this: +// var d = document.createElement("p"); +// 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
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, + tolerance = 5, // px + 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.documentElement.offsetHeight - tolerance)) { + 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 https://developer.mozilla.org/en/DOM/Selection/modify + */ + _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, + 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 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", "http://www.google.de");
+ * // ... or ...
+ * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", 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");
+ }
+ };
+})(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-[0-9a-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 REG_EXP = /wysiwyg-color-[0-9a-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);
+ }
+ };
+})(wysihtml5);(function(wysihtml5) {
+ var dom = wysihtml5.dom,
+ // Following elements are grouped
+ // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
+ // instead of creating a H4 within a H1 which would result in semantically invalid html
+ BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", "DIV"];
+
+ /**
+ * 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, wysihtml5.browser.displaysCaretInEmptyContentEditableCorrectly());
+ }
+
+ 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),
+ useLineBreaks = composer.config.useLineBreaks,
+ defaultNodeName = useLineBreaks ? "DIV" : "P",
+ selectedNode;
+
+ nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
+
+ if (blockElement) {
+ composer.selection.executeAndRestoreSimple(function() {
+ if (classRegExp) {
+ _removeClass(blockElement, classRegExp);
+ }
+ var hasClasses = _hasClasses(blockElement);
+ if (!hasClasses && (useLineBreaks || nodeName === "P")) {
+ // Insert a line break afterwards and beforewards when there are siblings
+ // that are not of type line break or block element
+ _addLineBreakBeforeAndAfter(blockElement);
+ dom.replaceWithChildNodes(blockElement);
+ } else {
+ // Make sure that styling is kept by renaming the element to a or
and copying over the class name
+ dom.renameElement(blockElement, nodeName === "P" ? "DIV" : defaultNodeName);
+ }
+ });
+ return;
+ }
+
+ // 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.executeAndRestore(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 || defaultNodeName, className);
+ return;
+ }
+
+ blockElement = doc.createElement(nodeName || defaultNodeName);
+ 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
+ });
+ }
+ };
+})(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 // 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);
+ }
+ };
+})(wysihtml5);wysihtml5.commands.insertHTML = {
+ exec: function(composer, command, html) {
+ if (composer.commands.support(command)) {
+ composer.doc.execCommand(command, false, html);
+ } else {
+ composer.selection.insertHTML(html);
+ }
+ },
+
+ state: function() {
+ return false;
+ }
+};
+(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", "http://www.google.de/logo.jpg");
+ * // ... or ...
+ * wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", 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) {
+ if (i === "className") {
+ i = "class";
+ }
+ image.setAttribute(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];
+ }
+ };
+})(wysihtml5);(function(wysihtml5) {
+ var 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;
+ }
+ };
+})(wysihtml5);wysihtml5.commands.insertOrderedList = {
+ exec: function(composer, command) {
+ var doc = composer.doc,
+ selectedNode = composer.selection.getSelectedNode(),
+ list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }),
+ otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }),
+ tempClassName = "_wysihtml5-temp-" + new Date().getTime(),
+ isEmpty,
+ tempElement;
+
+ if (!list && !otherList && composer.commands.support(command)) {
+ doc.execCommand(command, false, null);
+ return;
+ }
+
+ if (list) {
+ // Unwrap list
+ // - foo
- bar
+ // becomes:
+ // foo
bar
+ composer.selection.executeAndRestore(function() {
+ wysihtml5.dom.resolveList(list, composer.config.useLineBreaks);
+ });
+ } else if (otherList) {
+ // Turn an unordered list into an ordered list
+ // - foo
- bar
+ // becomes:
+ // - foo
- bar
+ composer.selection.executeAndRestore(function() {
+ wysihtml5.dom.renameElement(otherList, "ol");
+ });
+ } else {
+ // Create list
+ composer.commands.exec("formatBlock", "p", tempClassName);
+ tempElement = doc.querySelector("." + tempClassName);
+ isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE || tempElement.innerHTML === "
";
+ composer.selection.executeAndRestore(function() {
+ list = wysihtml5.dom.convertToList(tempElement, "ol");
+ });
+ if (isEmpty) {
+ composer.selection.selectNode(list.querySelector("li"), true);
+ }
+ }
+ },
+
+ state: function(composer) {
+ var selectedNode = composer.selection.getSelectedNode();
+ return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" });
+ }
+};wysihtml5.commands.insertUnorderedList = {
+ exec: function(composer, command) {
+ var doc = composer.doc,
+ selectedNode = composer.selection.getSelectedNode(),
+ list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }),
+ otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }),
+ tempClassName = "_wysihtml5-temp-" + new Date().getTime(),
+ isEmpty,
+ tempElement;
+
+ if (!list && !otherList && composer.commands.support(command)) {
+ doc.execCommand(command, false, null);
+ return;
+ }
+
+ if (list) {
+ // Unwrap list
+ // - foo
- bar
+ // becomes:
+ // foo
bar
+ composer.selection.executeAndRestore(function() {
+ wysihtml5.dom.resolveList(list, composer.config.useLineBreaks);
+ });
+ } else if (otherList) {
+ // Turn an ordered list into an unordered list
+ // - foo
- bar
+ // becomes:
+ // - foo
- bar
+ composer.selection.executeAndRestore(function() {
+ wysihtml5.dom.renameElement(otherList, "ul");
+ });
+ } else {
+ // Create list
+ composer.commands.exec("formatBlock", "p", tempClassName);
+ tempElement = doc.querySelector("." + tempClassName);
+ isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE || tempElement.innerHTML === "
";
+ composer.selection.executeAndRestore(function() {
+ list = wysihtml5.dom.convertToList(tempElement, "ul");
+ });
+ if (isEmpty) {
+ composer.selection.selectNode(list.querySelector("li"), true);
+ }
+ }
+ },
+
+ state: function(composer) {
+ var selectedNode = composer.selection.getSelectedNode();
+ return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" });
+ }
+};wysihtml5.commands.italic = {
+ exec: function(composer, command) {
+ return wysihtml5.commands.formatInline.exec(composer, command, "i");
+ },
+
+ state: function(composer, command) {
+ // element.ownerDocument.queryCommandState("italic") results:
+ // firefox: only
+ // chrome: , , , ...
+ // ie: ,
+ // opera: only
+ return wysihtml5.commands.formatInline.state(composer, command, "i");
+ }
+};(function(wysihtml5) {
+ var CLASS_NAME = "wysiwyg-text-align-center",
+ REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
+
+ wysihtml5.commands.justifyCenter = {
+ exec: function(composer, command) {
+ 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);
+ }
+ };
+})(wysihtml5);(function(wysihtml5) {
+ var CLASS_NAME = "wysiwyg-text-align-left",
+ REG_EXP = /wysiwyg-text-align-[0-9a-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);
+ }
+ };
+})(wysihtml5);(function(wysihtml5) {
+ var CLASS_NAME = "wysiwyg-text-align-right",
+ REG_EXP = /wysiwyg-text-align-[0-9a-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);
+ }
+ };
+})(wysihtml5);(function(wysihtml5) {
+ var CLASS_NAME = "wysiwyg-text-align-justify",
+ REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
+
+ wysihtml5.commands.justifyFull = {
+ exec: function(composer, command) {
+ return wysihtml5.commands.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);
+ }
+ };
+})(wysihtml5);
+wysihtml5.commands.redo = {
+ exec: function(composer) {
+ return composer.undoManager.redo();
+ },
+
+ state: function(composer) {
+ return false;
+ }
+};wysihtml5.commands.underline = {
+ exec: function(composer, command) {
+ return wysihtml5.commands.formatInline.exec(composer, command, "u");
+ },
+
+ state: function(composer, command) {
+ return wysihtml5.commands.formatInline.state(composer, command, "u");
+ }
+};wysihtml5.commands.undo = {
+ exec: function(composer) {
+ return composer.undoManager.undo();
+ },
+
+ state: function(composer) {
+ return false;
+ }
+};/**
+ * Undo Manager for wysihtml5
+ * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
+ */
+(function(wysihtml5) {
+ var Z_KEY = 90,
+ Y_KEY = 89,
+ BACKSPACE_KEY = 8,
+ DELETE_KEY = 46,
+ MAX_HISTORY_ENTRIES = 25,
+ DATA_ATTR_NODE = "data-wysihtml5-selection-node",
+ DATA_ATTR_OFFSET = "data-wysihtml5-selection-offset",
+ UNDO_HTML = '' + wysihtml5.INVISIBLE_SPACE + '',
+ REDO_HTML = '' + wysihtml5.INVISIBLE_SPACE + '',
+ dom = wysihtml5.dom;
+
+ 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.position = 0;
+ this.historyStr = [];
+ this.historyDom = [];
+
+ this.transact();
+
+ 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
+ .on("newword:composer", function() {
+ that.transact();
+ })
+
+ .on("beforecommand:composer", function() {
+ that.transact();
+ });
+ },
+
+ transact: function() {
+ var previousHtml = this.historyStr[this.position - 1],
+ currentHtml = this.composer.getValue();
+
+ if (currentHtml === previousHtml) {
+ return;
+ }
+
+ var length = this.historyStr.length = this.historyDom.length = this.position;
+ if (length > MAX_HISTORY_ENTRIES) {
+ this.historyStr.shift();
+ this.historyDom.shift();
+ this.position--;
+ }
+
+ this.position++;
+
+ var range = this.composer.selection.getRange(),
+ node = range.startContainer || this.element,
+ offset = range.startOffset || 0,
+ element,
+ position;
+
+ if (node.nodeType === wysihtml5.ELEMENT_NODE) {
+ element = node;
+ } else {
+ element = node.parentNode;
+ position = this.getChildNodeIndex(element, node);
+ }
+
+ element.setAttribute(DATA_ATTR_OFFSET, offset);
+ if (typeof(position) !== "undefined") {
+ element.setAttribute(DATA_ATTR_NODE, position);
+ }
+
+ var clone = this.element.cloneNode(!!currentHtml);
+ this.historyDom.push(clone);
+ this.historyStr.push(currentHtml);
+
+ element.removeAttribute(DATA_ATTR_OFFSET);
+ element.removeAttribute(DATA_ATTR_NODE);
+ },
+
+ undo: function() {
+ this.transact();
+
+ if (!this.undoPossible()) {
+ return;
+ }
+
+ this.set(this.historyDom[--this.position - 1]);
+ this.editor.fire("undo:composer");
+ },
+
+ redo: function() {
+ if (!this.redoPossible()) {
+ return;
+ }
+
+ this.set(this.historyDom[++this.position - 1]);
+ this.editor.fire("redo:composer");
+ },
+
+ undoPossible: function() {
+ return this.position > 1;
+ },
+
+ redoPossible: function() {
+ return this.position < this.historyStr.length;
+ },
+
+ set: function(historyEntry) {
+ this.element.innerHTML = "";
+
+ var i = 0,
+ childNodes = historyEntry.childNodes,
+ length = historyEntry.childNodes.length;
+
+ for (; i",
+
+ 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);
+ }
+
+ try {
+ this.element.innerHTML = html;
+ } catch (e) {
+ this.element.innerText = html;
+ }
+ },
+
+ show: function() {
+ this.iframe.style.display = this._displayStyle || "";
+
+ if (!this.textarea.element.disabled) {
+ // 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.parent.fire("disable:composer");
+ this.element.removeAttribute("contentEditable");
+ },
+
+ enable: function() {
+ this.parent.fire("enable:composer");
+ this.element.setAttribute("contentEditable", "true");
+ },
+
+ 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") && this.placeholderSet;
+ },
+
+ isEmpty: function() {
+ var innerHTML = this.element.innerHTML.toLowerCase();
+ return innerHTML === "" ||
+ innerHTML === "
" ||
+ innerHTML === "" ||
+ innerHTML === "
" ||
+ this.hasPlaceholderSet();
+ },
+
+ _initSandbox: function() {
+ var that = this;
+
+ this.sandbox = new dom.Sandbox(function() {
+ that._create();
+ }, {
+ stylesheets: this.config.stylesheets
+ });
+ this.iframe = this.sandbox.getIframe();
+
+ var textareaElement = this.textarea.element;
+ dom.insert(this.iframe).after(textareaElement);
+
+ // Create hidden field which tells the server after submit, that the user used an wysiwyg editor
+ if (textareaElement.form) {
+ var hiddenField = document.createElement("input");
+ hiddenField.type = "hidden";
+ hiddenField.name = "_wysihtml5_mode";
+ hiddenField.value = 1;
+ dom.insert(hiddenField).after(textareaElement);
+ }
+ },
+
+ _create: function() {
+ 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);
+
+ // 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);
+ }
+
+ this.enable();
+
+ if (this.textarea.element.disabled) {
+ this.disable();
+ }
+
+ // Simulate html5 placeholder attribute on contentEditable element
+ var placeholderText = typeof(this.config.placeholder) === "string"
+ ? this.config.placeholder
+ : 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();
+ this._initLineBreaking();
+
+ // Simulate html5 autofocus on contentEditable element
+ // This doesn't work on IOS (5.1.1)
+ if ((this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) {
+ setTimeout(function() { that.focus(true); }, 100);
+ }
+
+ // IE sometimes leaves a single paragraph, which can't be removed by the user
+ if (!browser.clearsContentEditableCorrectly()) {
+ wysihtml5.quirks.ensureProperClearing(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.on("newword:composer", function() {
+ if (dom.getTextContent(that.element).match(dom.autoLink.URL_REG_EXP)) {
+ that.selection.executeAndRestore(function(startContainer, endContainer) {
+ dom.autoLink(endContainer.parentNode);
+ });
+ }
+ });
+
+ dom.observe(this.element, "blur", function() {
+ dom.autoLink(that.element);
+ });
+ }
+
+ // Assuming we have the following:
+ // http://www.google.de
+ // 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() {
+ this.commands.exec("enableObjectResizing", true);
+
+ // 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")) {
+ var properties = ["width", "height"],
+ propertiesLength = properties.length,
+ element = this.element;
+
+ dom.observe(element, "resizeend", function(event) {
+ var target = event.target || event.srcElement,
+ style = target.style,
+ i = 0,
+ property;
+
+ if (target.nodeName !== "IMG") {
+ return;
+ }
+
+ for (; i p:first-child { margin-top: 0; }",
+ "._wysihtml5-temp { display: none; }",
+ wysihtml5.browser.isGecko ?
+ "body.placeholder { color: graytext !important; }" :
+ "body.placeholder { color: #a9a9a9 !important; }",
+ // Ensure that user see's broken images and can delete them
+ "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
+ ];
+
+ /**
+ * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
+ * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
+ *
+ * Other browsers need a more hacky way: (pssst don't tell my mama)
+ * In order to prevent the element being scrolled into view when focusing it, we simply
+ * move it out of the scrollable area, focus it, and reset it's position
+ */
+ var focusWithoutScrolling = function(element) {
+ if (element.setActive) {
+ // Following line could cause a js error when the textarea is invisible
+ // See https://github.com/xing/wysihtml5/issues/9
+ try { element.setActive(); } catch(e) {}
+ } else {
+ var elementStyle = element.style,
+ originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
+ originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
+ originalStyles = {
+ position: elementStyle.position,
+ top: elementStyle.top,
+ left: elementStyle.left,
+ WebkitUserSelect: elementStyle.WebkitUserSelect
+ };
+
+ dom.setStyles({
+ position: "absolute",
+ top: "-99999px",
+ left: "-99999px",
+ // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
+ WebkitUserSelect: "none"
+ }).on(element);
+
+ element.focus();
+
+ dom.setStyles(originalStyles).on(element);
+
+ if (win.scrollTo) {
+ // Some browser extensions unset this method to prevent annoyances
+ // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
+ // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
+ win.scrollTo(originalScrollLeft, originalScrollTop);
+ }
+ }
+ };
+
+
+ wysihtml5.views.Composer.prototype.style = function() {
+ var that = this,
+ originalActiveElement = doc.querySelector(":focus"),
+ textareaElement = this.textarea.element,
+ hasPlaceholder = textareaElement.hasAttribute("placeholder"),
+ originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"),
+ originalDisplayValue = textareaElement.style.display,
+ originalDisabled = textareaElement.disabled,
+ displayValueForCopying;
+
+ this.focusStylesHost = HOST_TEMPLATE.cloneNode(false);
+ this.blurStylesHost = HOST_TEMPLATE.cloneNode(false);
+ this.disabledStylesHost = HOST_TEMPLATE.cloneNode(false);
+
+ // Remove placeholder before copying (as the placeholder has an affect on the computed style)
+ if (hasPlaceholder) {
+ textareaElement.removeAttribute("placeholder");
+ }
+
+ if (textareaElement === originalActiveElement) {
+ textareaElement.blur();
+ }
+
+ // enable for copying styles
+ textareaElement.disabled = false;
+
+ // set textarea to display="none" to get cascaded styles via getComputedStyle
+ textareaElement.style.display = displayValueForCopying = "none";
+
+ if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") ||
+ (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) {
+ textareaElement.style.display = displayValueForCopying = originalDisplayValue;
+ }
+
+ // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
+ dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost);
+
+ // --------- editor styles ---------
+ dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
+
+ // --------- apply standard rules ---------
+ dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
+
+ // --------- :disabled styles ---------
+ textareaElement.disabled = true;
+ dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
+ dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
+ textareaElement.disabled = originalDisabled;
+
+ // --------- :focus styles ---------
+ textareaElement.style.display = originalDisplayValue;
+ focusWithoutScrolling(textareaElement);
+ textareaElement.style.display = displayValueForCopying;
+
+ dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
+ dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
+
+ // reset textarea
+ textareaElement.style.display = originalDisplayValue;
+
+ dom.copyStyles(["display"]).from(textareaElement).to(this.iframe);
+
+ // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
+ // this is needed for when the change_view event is fired where the iframe is hidden and then
+ // the blur event fires and re-displays it
+ var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]);
+
+ // --------- restore focus ---------
+ if (originalActiveElement) {
+ originalActiveElement.focus();
+ } else {
+ textareaElement.blur();
+ }
+
+ // --------- restore placeholder ---------
+ if (hasPlaceholder) {
+ textareaElement.setAttribute("placeholder", originalPlaceholder);
+ }
+
+ // --------- Sync focus/blur styles ---------
+ this.parent.on("focus:composer", function() {
+ dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe);
+ dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element);
+ });
+
+ this.parent.on("blur:composer", function() {
+ dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe);
+ dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element);
+ });
+
+ this.parent.observe("disable:composer", function() {
+ dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.iframe);
+ dom.copyStyles(TEXT_FORMATTING) .from(that.disabledStylesHost).to(that.element);
+ });
+
+ this.parent.observe("enable:composer", function() {
+ dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe);
+ dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element);
+ });
+
+ return this;
+ };
+})(wysihtml5);/**
+ * Taking care of events
+ * - Simulating 'change' event on contentEditable element
+ * - Handling drag & drop logic
+ * - Catch paste events
+ * - Dispatch proprietary newword:composer event
+ * - Keyboard shortcuts
+ */
+(function(wysihtml5) {
+ var dom = wysihtml5.dom,
+ browser = wysihtml5.browser,
+ /**
+ * Map keyCodes to query commands
+ */
+ shortcuts = {
+ "66": "bold", // B
+ "73": "italic", // I
+ "85": "underline" // U
+ };
+
+ wysihtml5.views.Composer.prototype.observe = function() {
+ var that = this,
+ state = this.getValue(),
+ iframe = this.sandbox.getIframe(),
+ element = this.element,
+ focusBlurElement = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(),
+ pasteEvents = ["drop", "paste"];
+
+ // --------- destroy:composer event ---------
+ dom.observe(iframe, "DOMNodeRemoved", function() {
+ clearInterval(domNodeRemovedInterval);
+ that.parent.fire("destroy:composer");
+ });
+
+ // DOMNodeRemoved event is not supported in IE 8
+ var domNodeRemovedInterval = setInterval(function() {
+ if (!dom.contains(document.documentElement, iframe)) {
+ clearInterval(domNodeRemovedInterval);
+ that.parent.fire("destroy:composer");
+ }
+ }, 250);
+
+ // --------- Focus & blur logic ---------
+ dom.observe(focusBlurElement, "focus", function() {
+ that.parent.fire("focus").fire("focus:composer");
+
+ // Delay storing of state until all focus handler are fired
+ // especially the one which resets the placeholder
+ setTimeout(function() { state = that.getValue(); }, 0);
+ });
+
+ dom.observe(focusBlurElement, "blur", function() {
+ if (state !== that.getValue()) {
+ that.parent.fire("change").fire("change:composer");
+ }
+ that.parent.fire("blur").fire("blur:composer");
+ });
+
+ // --------- Drag & Drop logic ---------
+ dom.observe(element, "dragenter", function() {
+ that.parent.fire("unset_placeholder");
+ });
+
+ dom.observe(element, pasteEvents, function() {
+ setTimeout(function() {
+ that.parent.fire("paste").fire("paste:composer");
+ }, 0);
+ });
+
+ // --------- neword event ---------
+ dom.observe(element, "keyup", function(event) {
+ var keyCode = event.keyCode;
+ if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) {
+ that.parent.fire("newword:composer");
+ }
+ });
+
+ this.parent.on("paste:composer", function() {
+ setTimeout(function() { that.parent.fire("newword:composer"); }, 0);
+ });
+
+ // --------- Make sure that images are selected when clicking on them ---------
+ if (!browser.canSelectImagesInContentEditable()) {
+ dom.observe(element, "mousedown", function(event) {
+ var target = event.target;
+ if (target.nodeName === "IMG") {
+ that.selection.selectNode(target);
+ event.preventDefault();
+ }
+ });
+ }
+
+ if (browser.hasHistoryIssue() && browser.supportsSelectionModify()) {
+ dom.observe(element, "keydown", function(event) {
+ if (!event.metaKey && !event.ctrlKey) {
+ return;
+ }
+
+ var keyCode = event.keyCode,
+ win = element.ownerDocument.defaultView,
+ selection = win.getSelection();
+
+ if (keyCode === 37 || keyCode === 39) {
+ if (keyCode === 37) {
+ selection.modify("extend", "left", "lineboundary");
+ if (!event.shiftKey) {
+ selection.collapseToStart();
+ }
+ }
+ if (keyCode === 39) {
+ selection.modify("extend", "right", "lineboundary");
+ if (!event.shiftKey) {
+ selection.collapseToEnd();
+ }
+ }
+ event.preventDefault();
+ }
+ });
+ }
+
+ // --------- Shortcut logic ---------
+ dom.observe(element, "keydown", function(event) {
+ var keyCode = event.keyCode,
+ command = shortcuts[keyCode];
+ if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
+ that.commands.exec(command);
+ event.preventDefault();
+ }
+ });
+
+ // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor ---------
+ dom.observe(element, "keydown", function(event) {
+ var target = that.selection.getSelectedNode(true),
+ keyCode = event.keyCode,
+ parent;
+ if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete
+ parent = target.parentNode;
+ // delete the
+ 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();
+ }
+ });
+
+ // --------- IE 8+9 focus the editor when the iframe is clicked (without actually firing the 'focus' event on the ) ---------
+ if (browser.hasIframeFocusIssue()) {
+ dom.observe(this.iframe, "focus", function() {
+ setTimeout(function() {
+ if (that.doc.querySelector(":focus") !== that.element) {
+ that.focus();
+ }
+ }, 0);
+ });
+
+ dom.observe(this.element, "blur", function() {
+ setTimeout(function() {
+ that.selection.getSelection().removeAllRanges();
+ }, 0);
+ });
+ }
+
+ // --------- Show url in tooltip when hovering links or images ---------
+ var titlePrefixes = {
+ IMG: "Image: ",
+ 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.on("change_view", function(view) {
+ if (view === "composer" && !interval) {
+ that.fromTextareaToComposer(true);
+ startInterval();
+ } else if (view === "textarea") {
+ that.fromComposerToTextarea(true);
+ stopInterval();
+ }
+ });
+
+ this.editor.on("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.on("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
+ *
+ *
+ *
+ *
+ *
+ */
+(function(wysihtml5) {
+ var dom = wysihtml5.dom,
+ CLASS_NAME_OPENED = "wysihtml5-command-dialog-opened",
+ SELECTOR_FORM_ELEMENTS = "input, select, textarea",
+ SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]",
+ ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field";
+
+
+ wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend(
+ /** @scope wysihtml5.toolbar.Dialog.prototype */ {
+ constructor: function(link, container) {
+ this.link = link;
+ this.container = container;
+ },
+
+ _observe: function() {
+ if (this._observed) {
+ return;
+ }
+
+ var that = this,
+ callbackWrapper = function(event) {
+ var attributes = that._serialize();
+ if (attributes == that.elementToChange) {
+ that.fire("edit", attributes);
+ } else {
+ that.fire("save", attributes);
+ }
+ that.hide();
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ dom.observe(that.link, "click", function() {
+ if (dom.hasClass(that.link, CLASS_NAME_OPENED)) {
+ setTimeout(function() { that.hide(); }, 0);
+ }
+ });
+
+ dom.observe(this.container, "keydown", function(event) {
+ var keyCode = event.keyCode;
+ if (keyCode === wysihtml5.ENTER_KEY) {
+ callbackWrapper(event);
+ }
+ if (keyCode === wysihtml5.ESCAPE_KEY) {
+ that.hide();
+ }
+ });
+
+ dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper);
+
+ dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) {
+ that.fire("cancel");
+ that.hide();
+ event.preventDefault();
+ event.stopPropagation();
+ });
+
+ var formElements = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS),
+ i = 0,
+ length = formElements.length,
+ _clearInterval = function() { clearInterval(that.interval); };
+ for (; ivalue style in an object which
+ * then gets returned
+ */
+ _serialize: function() {
+ var data = this.elementToChange || {},
+ fields = this.container.querySelectorAll(SELECTOR_FIELDS),
+ length = fields.length,
+ i = 0;
+ for (; ifoo
+ *
+ * and we have the following dialog:
+ *
+ *
+ *
+ * after calling _interpolate() the dialog will look like this
+ *
+ *
+ *
+ * Basically it adopted the attribute values into the corresponding input fields
+ *
+ */
+ _interpolate: function(avoidHiddenFields) {
+ var field,
+ fieldName,
+ newValue,
+ focusedElement = document.querySelector(":focus"),
+ fields = this.container.querySelectorAll(SELECTOR_FIELDS),
+ length = fields.length,
+ i = 0;
+ for (; i= 11
+ *
+ * Note that it sends the recorded audio to the google speech recognition api:
+ * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec
+ *
+ * Current HTML5 draft can be found here
+ * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
+ *
+ * "Accessing Google Speech API Chrome 11"
+ * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
+ */
+(function(wysihtml5) {
+ var dom = wysihtml5.dom;
+
+ var linkStyles = {
+ position: "relative"
+ };
+
+ var wrapperStyles = {
+ left: 0,
+ margin: 0,
+ opacity: 0,
+ overflow: "hidden",
+ padding: 0,
+ position: "absolute",
+ top: 0,
+ zIndex: 1
+ };
+
+ var inputStyles = {
+ cursor: "inherit",
+ fontSize: "50px",
+ height: "50px",
+ marginTop: "-25px",
+ outline: 0,
+ padding: 0,
+ position: "absolute",
+ right: "-4px",
+ top: "50%"
+ };
+
+ var inputAttributes = {
+ "x-webkit-speech": "",
+ "speech": ""
+ };
+
+ wysihtml5.toolbar.Speech = function(parent, link) {
+ var input = document.createElement("input");
+ if (!wysihtml5.browser.supportsSpeechApiOn(input)) {
+ link.style.display = "none";
+ return;
+ }
+ var lang = parent.editor.textarea.element.getAttribute("lang");
+ if (lang) {
+ inputAttributes.lang = lang;
+ }
+
+ var wrapper = document.createElement("p");
+
+ wysihtml5.lang.object(wrapperStyles).merge({
+ width: link.offsetWidth + "px",
+ height: link.offsetHeight + "px"
+ });
+
+ dom.insert(input).into(wrapper);
+ dom.insert(wrapper).into(link);
+
+ dom.setStyles(inputStyles).on(input);
+ dom.setAttributes(inputAttributes).on(input);
+
+ dom.setStyles(wrapperStyles).on(wrapper);
+ dom.setStyles(linkStyles).on(link);
+
+ var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange";
+ dom.observe(input, eventName, function() {
+ parent.execCommand("insertText", input.value);
+ input.value = "";
+ });
+
+ dom.observe(input, "click", function(event) {
+ if (dom.hasClass(link, "wysihtml5-command-disabled")) {
+ event.preventDefault();
+ }
+
+ event.stopPropagation();
+ });
+ };
+})(wysihtml5);/**
+ * Toolbar
+ *
+ * @param {Object} parent Reference to instance of Editor instance
+ * @param {Element} container Reference to the toolbar container element
+ *
+ * @example
+ *
+ * insert link
+ * insert h1
+ *
+ *
+ *
+ */
+(function(wysihtml5) {
+ var CLASS_NAME_COMMAND_DISABLED = "wysihtml5-command-disabled",
+ CLASS_NAME_COMMANDS_DISABLED = "wysihtml5-commands-disabled",
+ CLASS_NAME_COMMAND_ACTIVE = "wysihtml5-command-active",
+ CLASS_NAME_ACTION_ACTIVE = "wysihtml5-action-active",
+ dom = wysihtml5.dom;
+
+ wysihtml5.toolbar.Toolbar = Base.extend(
+ /** @scope wysihtml5.toolbar.Toolbar.prototype */ {
+ constructor: function(editor, container) {
+ this.editor = editor;
+ this.container = typeof(container) === "string" ? document.getElementById(container) : container;
+ this.composer = editor.composer;
+
+ this._getLinks("command");
+ this._getLinks("action");
+
+ this._observe();
+ this.show();
+
+ var speechInputLinks = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),
+ length = speechInputLinks.length,
+ i = 0;
+ for (; i element or wrap current selection in
+ * toolbar.execCommand("formatBlock", "blockquote");
+ */
+ execCommand: function(command, commandValue) {
+ if (this.commandsDisabled) {
+ return;
+ }
+
+ var commandObj = this.commandMapping[command + ":" + commandValue];
+
+ // Show dialog when available
+ if (commandObj && commandObj.dialog && !commandObj.state) {
+ commandObj.dialog.show();
+ } else {
+ this._execCommand(command, commandValue);
+ }
+ },
+
+ _execCommand: function(command, commandValue) {
+ // Make sure that composer is focussed (false => don't move caret to the end)
+ this.editor.focus(false);
+
+ this.composer.commands.exec(command, commandValue);
+ this._updateLinkStates();
+ },
+
+ execAction: function(action) {
+ var editor = this.editor;
+ if (action === "change_view") {
+ if (editor.currentView === editor.textarea) {
+ editor.fire("change_view", "composer");
+ } else {
+ editor.fire("change_view", "textarea");
+ }
+ }
+ },
+
+ _observe: function() {
+ var that = this,
+ editor = this.editor,
+ container = this.container,
+ links = this.commandLinks.concat(this.actionLinks),
+ length = links.length,
+ i = 0;
+
+ for (; i for line breaks, set this to false to use
+ useLineBreaks: true,
+ // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
+ stylesheets: [],
+ // Placeholder text to use, defaults to the placeholder attribute on the textarea element
+ placeholderText: undef,
+ // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5)
+ supportTouchDevices: true
+ };
+
+ wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend(
+ /** @scope wysihtml5.Editor.prototype */ {
+ constructor: function(textareaElement, config) {
+ this.textareaElement = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement;
+ this.config = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();
+ this.textarea = new wysihtml5.views.Textarea(this, this.textareaElement, this.config);
+ this.currentView = this.textarea;
+ this._isCompatible = wysihtml5.browser.supported();
+
+ // Sort out unsupported/unwanted browsers here
+ if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) {
+ var that = this;
+ setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
+ return;
+ }
+
+ // Add class name to body, to indicate that the editor is supported
+ wysihtml5.dom.addClass(document.body, this.config.bodyClassName);
+
+ this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config);
+ this.currentView = this.composer;
+
+ if (typeof(this.config.parser) === "function") {
+ this._initParser();
+ }
+
+ this.on("beforeload", function() {
+ this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer);
+ if (this.config.toolbar) {
+ this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar);
+ }
+ });
+
+ try {
+ console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5");
+ } catch(e) {}
+ },
+
+ isCompatible: function() {
+ return this._isCompatible;
+ },
+
+ clear: function() {
+ this.currentView.clear();
+ return this;
+ },
+
+ getValue: function(parse) {
+ return this.currentView.getValue(parse);
+ },
+
+ setValue: function(html, parse) {
+ this.fire("unset_placeholder");
+
+ if (!html) {
+ return this.clear();
+ }
+
+ this.currentView.setValue(html, parse);
+ return this;
+ },
+
+ focus: function(setToEnd) {
+ this.currentView.focus(setToEnd);
+ return this;
+ },
+
+ /**
+ * Deactivate editor (make it readonly)
+ */
+ disable: function() {
+ this.currentView.disable();
+ return this;
+ },
+
+ /**
+ * Activate editor
+ */
+ enable: function() {
+ this.currentView.enable();
+ return this;
+ },
+
+ isEmpty: function() {
+ return this.currentView.isEmpty();
+ },
+
+ hasPlaceholderSet: function() {
+ return this.currentView.hasPlaceholderSet();
+ },
+
+ parse: function(htmlOrElement) {
+ var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true);
+ if (typeof(htmlOrElement) === "object") {
+ wysihtml5.quirks.redraw(htmlOrElement);
+ }
+ return returnValue;
+ },
+
+ /**
+ * Prepare html parser logic
+ * - Observes for paste and drop
+ */
+ _initParser: function() {
+ this.on("paste:composer", function() {
+ var keepScrollPosition = true,
+ that = this;
+ that.composer.selection.executeAndRestore(function() {
+ wysihtml5.quirks.cleanPastedHTML(that.composer.element);
+ that.parse(that.composer.element);
+ }, keepScrollPosition);
+ });
+ }
+ });
+})(wysihtml5);
diff --git a/app/assets/javascripts/vendor/wysihtml5-parser.js b/app/assets/javascripts/vendor/wysihtml5-parser.js
new file mode 100644
index 0000000..bcbea6b
--- /dev/null
+++ b/app/assets/javascripts/vendor/wysihtml5-parser.js
@@ -0,0 +1,553 @@
+/**
+ * Full HTML5 compatibility rule set
+ * These rules define which tags and CSS classes are supported and which tags should be specially treated.
+ *
+ * Examples based on this rule set:
+ *
+ * foo
+ * ... becomes ...
+ * foo
+ *
+ *
+ * ... becomes ...
+ *
+ *
+ *
foo
+ * ... becomes ...
+ * foo
+ *
+ *
+ * ... becomes ...
+ * foo
+ *
+ * foo
bar
+ * ... becomes ...
+ * foo
bar
+ *
+ * hello
+ * ... becomes ...
+ * hello
+ *
+ * hello
+ * ... becomes ...
+ * hello
+ */
+var wysihtml5ParserRules = {
+ /**
+ * CSS Class white-list
+ * Following CSS classes won't be removed when parsed by the wysihtml5 HTML parser
+ */
+ "classes": {
+ "wysiwyg-clear-both": 1,
+ "wysiwyg-clear-left": 1,
+ "wysiwyg-clear-right": 1,
+ "wysiwyg-color-aqua": 1,
+ "wysiwyg-color-black": 1,
+ "wysiwyg-color-blue": 1,
+ "wysiwyg-color-fuchsia": 1,
+ "wysiwyg-color-gray": 1,
+ "wysiwyg-color-green": 1,
+ "wysiwyg-color-lime": 1,
+ "wysiwyg-color-maroon": 1,
+ "wysiwyg-color-navy": 1,
+ "wysiwyg-color-olive": 1,
+ "wysiwyg-color-purple": 1,
+ "wysiwyg-color-red": 1,
+ "wysiwyg-color-silver": 1,
+ "wysiwyg-color-teal": 1,
+ "wysiwyg-color-white": 1,
+ "wysiwyg-color-yellow": 1,
+ "wysiwyg-float-left": 1,
+ "wysiwyg-float-right": 1,
+ "wysiwyg-font-size-large": 1,
+ "wysiwyg-font-size-larger": 1,
+ "wysiwyg-font-size-medium": 1,
+ "wysiwyg-font-size-small": 1,
+ "wysiwyg-font-size-smaller": 1,
+ "wysiwyg-font-size-x-large": 1,
+ "wysiwyg-font-size-x-small": 1,
+ "wysiwyg-font-size-xx-large": 1,
+ "wysiwyg-font-size-xx-small": 1,
+ "wysiwyg-text-align-center": 1,
+ "wysiwyg-text-align-justify": 1,
+ "wysiwyg-text-align-left": 1,
+ "wysiwyg-text-align-right": 1
+ },
+ /**
+ * Tag list
+ *
+ * The following options are available:
+ *
+ * - add_class: converts and deletes the given HTML4 attribute (align, clear, ...) via the given method to a css class
+ * The following methods are implemented in wysihtml5.dom.parse:
+ * - align_text: converts align attribute values (right/left/center/justify) to their corresponding css class "wysiwyg-text-align-*")
+ * foo
... becomes ... class="wysiwyg-text-align-center">foo
+ * - clear_br: converts clear attribute values left/right/all/both to their corresponding css class "wysiwyg-clear-*"
+ *
... becomes ...
+ * - align_img: converts align attribute values (right/left) on
to their corresponding css class "wysiwyg-float-*"
+ *
+ * - remove: removes the element and its content
+ *
+ * - rename_tag: renames the element to the given tag
+ *
+ * - set_class: adds the given class to the element (note: make sure that the class is in the "classes" white list above)
+ *
+ * - set_attributes: sets/overrides the given attributes
+ *
+ * - check_attributes: checks the given HTML attribute via the given method
+ * - url: allows only valid urls (starting with http:// or https://)
+ * - src: allows something like "/foobar.jpg", "http://google.com", ...
+ * - href: allows something like "mailto:bert@foo.com", "http://google.com", "/foobar.jpg"
+ * - alt: strips unwanted characters. if the attribute is not set, then it gets set (to ensure valid and compatible HTML)
+ * - numbers: ensures that the attribute only contains numeric characters
+ */
+ "tags": {
+ "tr": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "strike": {
+ "remove": 1
+ },
+ "form": {
+ "rename_tag": "div"
+ },
+ "rt": {
+ "rename_tag": "span"
+ },
+ "code": {},
+ "acronym": {
+ "rename_tag": "span"
+ },
+ "br": {
+ "add_class": {
+ "clear": "clear_br"
+ }
+ },
+ "details": {
+ "rename_tag": "div"
+ },
+ "h4": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "em": {},
+ "title": {
+ "remove": 1
+ },
+ "multicol": {
+ "rename_tag": "div"
+ },
+ "figure": {
+ "rename_tag": "div"
+ },
+ "xmp": {
+ "rename_tag": "span"
+ },
+ "small": {
+ "rename_tag": "span",
+ "set_class": "wysiwyg-font-size-smaller"
+ },
+ "area": {
+ "remove": 1
+ },
+ "time": {
+ "rename_tag": "span"
+ },
+ "dir": {
+ "rename_tag": "ul"
+ },
+ "bdi": {
+ "rename_tag": "span"
+ },
+ "command": {
+ "remove": 1
+ },
+ "ul": {},
+ "progress": {
+ "rename_tag": "span"
+ },
+ "dfn": {
+ "rename_tag": "span"
+ },
+ "iframe": {
+ "remove": 1
+ },
+ "figcaption": {
+ "rename_tag": "div"
+ },
+ "a": {
+ "check_attributes": {
+ "href": "url" // if you compiled master manually then change this from 'url' to 'href'
+ },
+ "set_attributes": {
+ "rel": "nofollow",
+ "target": "_blank"
+ }
+ },
+ "img": {
+ "check_attributes": {
+ "width": "numbers",
+ "alt": "alt",
+ "src": "url", // if you compiled master manually then change this from 'url' to 'src'
+ "height": "numbers"
+ },
+ "add_class": {
+ "align": "align_img"
+ }
+ },
+ "rb": {
+ "rename_tag": "span"
+ },
+ "footer": {
+ "rename_tag": "div"
+ },
+ "noframes": {
+ "remove": 1
+ },
+ "abbr": {
+ "rename_tag": "span"
+ },
+ "u": {},
+ "bgsound": {
+ "remove": 1
+ },
+ "sup": {
+ "rename_tag": "span"
+ },
+ "address": {
+ "rename_tag": "div"
+ },
+ "basefont": {
+ "remove": 1
+ },
+ "nav": {
+ "rename_tag": "div"
+ },
+ "h1": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "head": {
+ "remove": 1
+ },
+ "tbody": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "dd": {
+ "rename_tag": "div"
+ },
+ "s": {
+ "rename_tag": "span"
+ },
+ "li": {},
+ "td": {
+ "check_attributes": {
+ "rowspan": "numbers",
+ "colspan": "numbers"
+ },
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "object": {
+ "remove": 1
+ },
+ "div": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "option": {
+ "rename_tag": "span"
+ },
+ "select": {
+ "rename_tag": "span"
+ },
+ "i": {},
+ "track": {
+ "remove": 1
+ },
+ "wbr": {
+ "remove": 1
+ },
+ "fieldset": {
+ "rename_tag": "div"
+ },
+ "big": {
+ "rename_tag": "span",
+ "set_class": "wysiwyg-font-size-larger"
+ },
+ "button": {
+ "rename_tag": "span"
+ },
+ "noscript": {
+ "remove": 1
+ },
+ "svg": {
+ "remove": 1
+ },
+ "input": {
+ "remove": 1
+ },
+ "table": {},
+ "keygen": {
+ "remove": 1
+ },
+ "h5": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "meta": {
+ "remove": 1
+ },
+ "map": {
+ "rename_tag": "div"
+ },
+ "isindex": {
+ "remove": 1
+ },
+ "mark": {
+ "rename_tag": "span"
+ },
+ "caption": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "tfoot": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "base": {
+ "remove": 1
+ },
+ "video": {
+ "remove": 1
+ },
+ "strong": {},
+ "canvas": {
+ "remove": 1
+ },
+ "output": {
+ "rename_tag": "span"
+ },
+ "marquee": {
+ "rename_tag": "span"
+ },
+ "b": {},
+ "q": {
+ "check_attributes": {
+ "cite": "url"
+ }
+ },
+ "applet": {
+ "remove": 1
+ },
+ "span": {},
+ "rp": {
+ "rename_tag": "span"
+ },
+ "spacer": {
+ "remove": 1
+ },
+ "source": {
+ "remove": 1
+ },
+ "aside": {
+ "rename_tag": "div"
+ },
+ "frame": {
+ "remove": 1
+ },
+ "section": {
+ "rename_tag": "div"
+ },
+ "body": {
+ "rename_tag": "div"
+ },
+ "ol": {},
+ "nobr": {
+ "rename_tag": "span"
+ },
+ "html": {
+ "rename_tag": "div"
+ },
+ "summary": {
+ "rename_tag": "span"
+ },
+ "var": {
+ "rename_tag": "span"
+ },
+ "del": {
+ "remove": 1
+ },
+ "blockquote": {
+ "check_attributes": {
+ "cite": "url"
+ }
+ },
+ "style": {
+ "remove": 1
+ },
+ "device": {
+ "remove": 1
+ },
+ "meter": {
+ "rename_tag": "span"
+ },
+ "h3": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "textarea": {
+ "rename_tag": "span"
+ },
+ "embed": {
+ "remove": 1
+ },
+ "hgroup": {
+ "rename_tag": "div"
+ },
+ "font": {
+ "rename_tag": "span",
+ "add_class": {
+ "size": "size_font"
+ }
+ },
+ "tt": {
+ "rename_tag": "span"
+ },
+ "noembed": {
+ "remove": 1
+ },
+ "thead": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "blink": {
+ "rename_tag": "span"
+ },
+ "plaintext": {
+ "rename_tag": "span"
+ },
+ "xml": {
+ "remove": 1
+ },
+ "h6": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "param": {
+ "remove": 1
+ },
+ "th": {
+ "check_attributes": {
+ "rowspan": "numbers",
+ "colspan": "numbers"
+ },
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "legend": {
+ "rename_tag": "span"
+ },
+ "hr": {},
+ "label": {
+ "rename_tag": "span"
+ },
+ "dl": {
+ "rename_tag": "div"
+ },
+ "kbd": {
+ "rename_tag": "span"
+ },
+ "listing": {
+ "rename_tag": "div"
+ },
+ "dt": {
+ "rename_tag": "span"
+ },
+ "nextid": {
+ "remove": 1
+ },
+ "pre": {},
+ "center": {
+ "rename_tag": "div",
+ "set_class": "wysiwyg-text-align-center"
+ },
+ "audio": {
+ "remove": 1
+ },
+ "datalist": {
+ "rename_tag": "span"
+ },
+ "samp": {
+ "rename_tag": "span"
+ },
+ "col": {
+ "remove": 1
+ },
+ "article": {
+ "rename_tag": "div"
+ },
+ "cite": {},
+ "link": {
+ "remove": 1
+ },
+ "script": {
+ "remove": 1
+ },
+ "bdo": {
+ "rename_tag": "span"
+ },
+ "menu": {
+ "rename_tag": "ul"
+ },
+ "colgroup": {
+ "remove": 1
+ },
+ "ruby": {
+ "rename_tag": "span"
+ },
+ "h2": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "ins": {
+ "rename_tag": "span"
+ },
+ "p": {
+ "add_class": {
+ "align": "align_text"
+ }
+ },
+ "sub": {
+ "rename_tag": "span"
+ },
+ "comment": {
+ "remove": 1
+ },
+ "frameset": {
+ "remove": 1
+ },
+ "optgroup": {
+ "rename_tag": "span"
+ },
+ "header": {
+ "rename_tag": "div"
+ }
+ }
+};
\ No newline at end of file
diff --git a/app/assets/javascripts/wysihtml5/parser_rules.js b/app/assets/javascripts/wysihtml5/parser_rules.js
deleted file mode 100755
index 26cd7a8..0000000
--- a/app/assets/javascripts/wysihtml5/parser_rules.js
+++ /dev/null
@@ -1,551 +0,0 @@
-/**
- * Full HTML5 compatibility rule set
- * These rules define which tags and css classes are supported and which tags should be specially treated.
- *
- * Examples based on this rule set:
- *
- * foo
- * ... becomes ...
- * foo
- *
- *
- * ... becomes ...
- *
- *
- * foo
- * ... becomes ...
- * foo
- *
- *
- * ... becomes ...
- * foo
- *
- * foo
bar
- * ... becomes ...
- * foo
bar
- *
- * hello
- * ... becomes ...
- * hello
- *
- * hello
- * ... becomes ...
- * hello
- */
-var wysihtml5ParserRules = {
- /**
- * CSS Class white-list
- * Following css classes won't be removed when parsed by the wysihtml5 html parser
- */
- "classes": {
- "wysiwyg-clear-both": 1,
- "wysiwyg-clear-left": 1,
- "wysiwyg-clear-right": 1,
- "wysiwyg-color-aqua": 1,
- "wysiwyg-color-black": 1,
- "wysiwyg-color-blue": 1,
- "wysiwyg-color-fuchsia": 1,
- "wysiwyg-color-gray": 1,
- "wysiwyg-color-green": 1,
- "wysiwyg-color-lime": 1,
- "wysiwyg-color-maroon": 1,
- "wysiwyg-color-navy": 1,
- "wysiwyg-color-olive": 1,
- "wysiwyg-color-purple": 1,
- "wysiwyg-color-red": 1,
- "wysiwyg-color-silver": 1,
- "wysiwyg-color-teal": 1,
- "wysiwyg-color-white": 1,
- "wysiwyg-color-yellow": 1,
- "wysiwyg-float-left": 1,
- "wysiwyg-float-right": 1,
- "wysiwyg-font-size-large": 1,
- "wysiwyg-font-size-larger": 1,
- "wysiwyg-font-size-medium": 1,
- "wysiwyg-font-size-small": 1,
- "wysiwyg-font-size-smaller": 1,
- "wysiwyg-font-size-x-large": 1,
- "wysiwyg-font-size-x-small": 1,
- "wysiwyg-font-size-xx-large": 1,
- "wysiwyg-font-size-xx-small": 1,
- "wysiwyg-text-align-center": 1,
- "wysiwyg-text-align-justify": 1,
- "wysiwyg-text-align-left": 1,
- "wysiwyg-text-align-right": 1
- },
- /**
- * Tag list
- *
- * Following options are available:
- *
- * - add_class: converts and deletes the given HTML4 attribute (align, clear, ...) via the given method to a css class
- * The following methods are implemented in wysihtml5.dom.parse:
- * - align_text: converts align attribute values (right/left/center/justify) to their corresponding css class "wysiwyg-text-align-*")
- foo
... becomes ... class="wysiwyg-text-align-center">foo
- * - clear_br: converts clear attribute values left/right/all/both to their corresponding css class "wysiwyg-clear-*"
- *
... becomes ...
- * - align_img: converts align attribute values (right/left) on
to their corresponding css class "wysiwyg-float-*"
- *
- * - remove: removes the element and it's content
- *
- * - rename_tag: renames the element to the given tag
- *
- * - set_class: adds the given class to the element (note: make sure that the class is in the "classes" white list above)
- *
- * - set_attributes: sets/overrides the given attributes
- *
- * - check_attributes: checks the given HTML attribute via the given method
- * - url: checks whether the given string is an url, deletes the attribute if not
- * - alt: strips unwanted characters. if the attribute is not set, then it gets set (to ensure valid and compatible HTML)
- * - numbers: ensures that the attribute only contains numeric characters
- */
- "tags": {
- "tr": {
- "add_class": {
- "align": "align_text"
- }
- },
- "strike": {
- "remove": 1
- },
- "form": {
- "rename_tag": "div"
- },
- "rt": {
- "rename_tag": "span"
- },
- "code": {},
- "acronym": {
- "rename_tag": "span"
- },
- "br": {
- "add_class": {
- "clear": "clear_br"
- }
- },
- "details": {
- "rename_tag": "div"
- },
- "h4": {
- "add_class": {
- "align": "align_text"
- }
- },
- "em": {},
- "title": {
- "remove": 1
- },
- "multicol": {
- "rename_tag": "div"
- },
- "figure": {
- "rename_tag": "div"
- },
- "xmp": {
- "rename_tag": "span"
- },
- "small": {
- "rename_tag": "span",
- "set_class": "wysiwyg-font-size-smaller"
- },
- "area": {
- "remove": 1
- },
- "time": {
- "rename_tag": "span"
- },
- "dir": {
- "rename_tag": "ul"
- },
- "bdi": {
- "rename_tag": "span"
- },
- "command": {
- "remove": 1
- },
- "ul": {},
- "progress": {
- "rename_tag": "span"
- },
- "dfn": {
- "rename_tag": "span"
- },
- "iframe": {
- "remove": 1
- },
- "figcaption": {
- "rename_tag": "div"
- },
- "a": {
- "check_attributes": {
- "href": "url"
- },
- "set_attributes": {
- "rel": "nofollow",
- "target": "_blank"
- }
- },
- "img": {
- "check_attributes": {
- "width": "numbers",
- "alt": "alt",
- "src": "url",
- "height": "numbers"
- },
- "add_class": {
- "align": "align_img"
- }
- },
- "rb": {
- "rename_tag": "span"
- },
- "footer": {
- "rename_tag": "div"
- },
- "noframes": {
- "remove": 1
- },
- "abbr": {
- "rename_tag": "span"
- },
- "u": {},
- "bgsound": {
- "remove": 1
- },
- "sup": {
- "rename_tag": "span"
- },
- "address": {
- "rename_tag": "div"
- },
- "basefont": {
- "remove": 1
- },
- "nav": {
- "rename_tag": "div"
- },
- "h1": {
- "add_class": {
- "align": "align_text"
- }
- },
- "head": {
- "remove": 1
- },
- "tbody": {
- "add_class": {
- "align": "align_text"
- }
- },
- "dd": {
- "rename_tag": "div"
- },
- "s": {
- "rename_tag": "span"
- },
- "li": {},
- "td": {
- "check_attributes": {
- "rowspan": "numbers",
- "colspan": "numbers"
- },
- "add_class": {
- "align": "align_text"
- }
- },
- "object": {
- "remove": 1
- },
- "div": {
- "add_class": {
- "align": "align_text"
- }
- },
- "option": {
- "rename_tag": "span"
- },
- "select": {
- "rename_tag": "span"
- },
- "i": {},
- "track": {
- "remove": 1
- },
- "wbr": {
- "remove": 1
- },
- "fieldset": {
- "rename_tag": "div"
- },
- "big": {
- "rename_tag": "span",
- "set_class": "wysiwyg-font-size-larger"
- },
- "button": {
- "rename_tag": "span"
- },
- "noscript": {
- "remove": 1
- },
- "svg": {
- "remove": 1
- },
- "input": {
- "remove": 1
- },
- "table": {},
- "keygen": {
- "remove": 1
- },
- "h5": {
- "add_class": {
- "align": "align_text"
- }
- },
- "meta": {
- "remove": 1
- },
- "map": {
- "rename_tag": "div"
- },
- "isindex": {
- "remove": 1
- },
- "mark": {
- "rename_tag": "span"
- },
- "caption": {
- "add_class": {
- "align": "align_text"
- }
- },
- "tfoot": {
- "add_class": {
- "align": "align_text"
- }
- },
- "base": {
- "remove": 1
- },
- "video": {
- "remove": 1
- },
- "strong": {},
- "canvas": {
- "remove": 1
- },
- "output": {
- "rename_tag": "span"
- },
- "marquee": {
- "rename_tag": "span"
- },
- "b": {},
- "q": {
- "check_attributes": {
- "cite": "url"
- }
- },
- "applet": {
- "remove": 1
- },
- "span": {},
- "rp": {
- "rename_tag": "span"
- },
- "spacer": {
- "remove": 1
- },
- "source": {
- "remove": 1
- },
- "aside": {
- "rename_tag": "div"
- },
- "frame": {
- "remove": 1
- },
- "section": {
- "rename_tag": "div"
- },
- "body": {
- "rename_tag": "div"
- },
- "ol": {},
- "nobr": {
- "rename_tag": "span"
- },
- "html": {
- "rename_tag": "div"
- },
- "summary": {
- "rename_tag": "span"
- },
- "var": {
- "rename_tag": "span"
- },
- "del": {
- "remove": 1
- },
- "blockquote": {
- "check_attributes": {
- "cite": "url"
- }
- },
- "style": {
- "remove": 1
- },
- "device": {
- "remove": 1
- },
- "meter": {
- "rename_tag": "span"
- },
- "h3": {
- "add_class": {
- "align": "align_text"
- }
- },
- "textarea": {
- "rename_tag": "span"
- },
- "embed": {
- "remove": 1
- },
- "hgroup": {
- "rename_tag": "div"
- },
- "font": {
- "rename_tag": "span",
- "add_class": {
- "size": "size_font"
- }
- },
- "tt": {
- "rename_tag": "span"
- },
- "noembed": {
- "remove": 1
- },
- "thead": {
- "add_class": {
- "align": "align_text"
- }
- },
- "blink": {
- "rename_tag": "span"
- },
- "plaintext": {
- "rename_tag": "span"
- },
- "xml": {
- "remove": 1
- },
- "h6": {
- "add_class": {
- "align": "align_text"
- }
- },
- "param": {
- "remove": 1
- },
- "th": {
- "check_attributes": {
- "rowspan": "numbers",
- "colspan": "numbers"
- },
- "add_class": {
- "align": "align_text"
- }
- },
- "legend": {
- "rename_tag": "span"
- },
- "hr": {},
- "label": {
- "rename_tag": "span"
- },
- "dl": {
- "rename_tag": "div"
- },
- "kbd": {
- "rename_tag": "span"
- },
- "listing": {
- "rename_tag": "div"
- },
- "dt": {
- "rename_tag": "span"
- },
- "nextid": {
- "remove": 1
- },
- "pre": {},
- "center": {
- "rename_tag": "div",
- "set_class": "wysiwyg-text-align-center"
- },
- "audio": {
- "remove": 1
- },
- "datalist": {
- "rename_tag": "span"
- },
- "samp": {
- "rename_tag": "span"
- },
- "col": {
- "remove": 1
- },
- "article": {
- "rename_tag": "div"
- },
- "cite": {},
- "link": {
- "remove": 1
- },
- "script": {
- "remove": 1
- },
- "bdo": {
- "rename_tag": "span"
- },
- "menu": {
- "rename_tag": "ul"
- },
- "colgroup": {
- "remove": 1
- },
- "ruby": {
- "rename_tag": "span"
- },
- "h2": {
- "add_class": {
- "align": "align_text"
- }
- },
- "ins": {
- "rename_tag": "span"
- },
- "p": {
- "add_class": {
- "align": "align_text"
- }
- },
- "sub": {
- "rename_tag": "span"
- },
- "comment": {
- "remove": 1
- },
- "frameset": {
- "remove": 1
- },
- "optgroup": {
- "rename_tag": "span"
- },
- "header": {
- "rename_tag": "div"
- }
- }
-};
\ No newline at end of file
diff --git a/app/assets/javascripts/wysihtml5/wysihtml5.js b/app/assets/javascripts/wysihtml5/wysihtml5.js
deleted file mode 100755
index 3881fec..0000000
--- a/app/assets/javascripts/wysihtml5/wysihtml5.js
+++ /dev/null
@@ -1,9610 +0,0 @@
-/**
- * @license wysihtml5 v0.3.0
- * https://github.com/xing/wysihtml5
- *
- * Author: Christopher Blum (https://github.com/tiff)
- *
- * Copyright (C) 2012 XING AG
- * Licensed under the MIT license (MIT)
- *
- */
-var wysihtml5 = {
- version: "0.3.0",
-
- // namespaces
- commands: {},
- dom: {},
- quirks: {},
- toolbar: {},
- lang: {},
- selection: {},
- views: {},
-
- INVISIBLE_SPACE: "\uFEFF",
-
- EMPTY_FUNCTION: function() {},
-
- ELEMENT_NODE: 1,
- TEXT_NODE: 3,
-
- BACKSPACE_KEY: 8,
- ENTER_KEY: 13,
- ESCAPE_KEY: 27,
- SPACE_KEY: 32,
- DELETE_KEY: 46
-};/**
- * @license Rangy, a cross-browser JavaScript range and selection library
- * http://code.google.com/p/rangy/
- *
- * Copyright 2011, Tim Down
- * Licensed under the MIT license.
- * Version: 1.2.2
- * Build date: 13 November 2011
- */
-window['rangy'] = (function() {
-
-
- var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
-
- var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
- "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];
-
- var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
- "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
- "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
-
- var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
-
- // Subset of TextRange's full set of methods that we're interested in
- var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",
- "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- // Trio of functions taken from Peter Michaux's article:
- // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
- function isHostMethod(o, p) {
- var t = typeof o[p];
- return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
- }
-
- function isHostObject(o, p) {
- return !!(typeof o[p] == OBJECT && o[p]);
- }
-
- function isHostProperty(o, p) {
- return typeof o[p] != UNDEFINED;
- }
-
- // Creates a convenience function to save verbose repeated calls to tests functions
- function createMultiplePropertyTest(testFunc) {
- return function(o, props) {
- var i = props.length;
- while (i--) {
- if (!testFunc(o, props[i])) {
- return false;
- }
- }
- return true;
- };
- }
-
- // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
- var areHostMethods = createMultiplePropertyTest(isHostMethod);
- var areHostObjects = createMultiplePropertyTest(isHostObject);
- var areHostProperties = createMultiplePropertyTest(isHostProperty);
-
- function isTextRange(range) {
- return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
- }
-
- var api = {
- version: "1.2.2",
- initialized: false,
- supported: true,
-
- util: {
- isHostMethod: isHostMethod,
- isHostObject: isHostObject,
- isHostProperty: isHostProperty,
- areHostMethods: areHostMethods,
- areHostObjects: areHostObjects,
- areHostProperties: areHostProperties,
- isTextRange: isTextRange
- },
-
- features: {},
-
- modules: {},
- config: {
- alertOnWarn: false,
- preferTextRange: false
- }
- };
-
- function fail(reason) {
- window.alert("Rangy not supported in your browser. Reason: " + reason);
- api.initialized = true;
- api.supported = false;
- }
-
- api.fail = fail;
-
- function warn(msg) {
- var warningMessage = "Rangy warning: " + msg;
- if (api.config.alertOnWarn) {
- window.alert(warningMessage);
- } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {
- window.console.log(warningMessage);
- }
- }
-
- api.warn = warn;
-
- if ({}.hasOwnProperty) {
- api.util.extend = function(o, props) {
- for (var i in props) {
- if (props.hasOwnProperty(i)) {
- o[i] = props[i];
- }
- }
- };
- } else {
- fail("hasOwnProperty not supported");
- }
-
- var initListeners = [];
- var moduleInitializers = [];
-
- // Initialization
- function init() {
- if (api.initialized) {
- return;
- }
- var testRange;
- var implementsDomRange = false, implementsTextRange = false;
-
- // First, perform basic feature tests
-
- if (isHostMethod(document, "createRange")) {
- testRange = document.createRange();
- if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
- implementsDomRange = true;
- }
- testRange.detach();
- }
-
- var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
-
- if (body && isHostMethod(body, "createTextRange")) {
- testRange = body.createTextRange();
- if (isTextRange(testRange)) {
- implementsTextRange = true;
- }
- }
-
- if (!implementsDomRange && !implementsTextRange) {
- fail("Neither Range nor TextRange are implemented");
- }
-
- api.initialized = true;
- api.features = {
- implementsDomRange: implementsDomRange,
- implementsTextRange: implementsTextRange
- };
-
- // Initialize modules and call init listeners
- var allListeners = moduleInitializers.concat(initListeners);
- for (var i = 0, len = allListeners.length; i < len; ++i) {
- try {
- allListeners[i](api);
- } catch (ex) {
- if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
- window.console.log("Init listener threw an exception. Continuing.", ex);
- }
-
- }
- }
- }
-
- // Allow external scripts to initialize this library in case it's loaded after the document has loaded
- api.init = init;
-
- // Execute listener immediately if already initialized
- api.addInitListener = function(listener) {
- if (api.initialized) {
- listener(api);
- } else {
- initListeners.push(listener);
- }
- };
-
- var createMissingNativeApiListeners = [];
-
- api.addCreateMissingNativeApiListener = function(listener) {
- createMissingNativeApiListeners.push(listener);
- };
-
- function createMissingNativeApi(win) {
- win = win || window;
- init();
-
- // Notify listeners
- for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {
- createMissingNativeApiListeners[i](win);
- }
- }
-
- api.createMissingNativeApi = createMissingNativeApi;
-
- /**
- * @constructor
- */
- function Module(name) {
- this.name = name;
- this.initialized = false;
- this.supported = false;
- }
-
- Module.prototype.fail = function(reason) {
- this.initialized = true;
- this.supported = false;
-
- throw new Error("Module '" + this.name + "' failed to load: " + reason);
- };
-
- Module.prototype.warn = function(msg) {
- api.warn("Module " + this.name + ": " + msg);
- };
-
- Module.prototype.createError = function(msg) {
- return new Error("Error in Rangy " + this.name + " module: " + msg);
- };
-
- api.createModule = function(name, initFunc) {
- var module = new Module(name);
- api.modules[name] = module;
-
- moduleInitializers.push(function(api) {
- initFunc(api, module);
- module.initialized = true;
- module.supported = true;
- });
- };
-
- api.requireModules = function(modules) {
- for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {
- moduleName = modules[i];
- module = api.modules[moduleName];
- if (!module || !(module instanceof Module)) {
- throw new Error("Module '" + moduleName + "' not found");
- }
- if (!module.supported) {
- throw new Error("Module '" + moduleName + "' not supported");
- }
- }
- };
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- // Wait for document to load before running tests
-
- var docReady = false;
-
- var loadHandler = function(e) {
-
- if (!docReady) {
- docReady = true;
- if (!api.initialized) {
- init();
- }
- }
- };
-
- // Test whether we have window and document objects that we will need
- if (typeof window == UNDEFINED) {
- fail("No window found");
- return;
- }
- if (typeof document == UNDEFINED) {
- fail("No document found");
- return;
- }
-
- if (isHostMethod(document, "addEventListener")) {
- document.addEventListener("DOMContentLoaded", loadHandler, false);
- }
-
- // Add a fallback in case the DOMContentLoaded event isn't supported
- if (isHostMethod(window, "addEventListener")) {
- window.addEventListener("load", loadHandler, false);
- } else if (isHostMethod(window, "attachEvent")) {
- window.attachEvent("onload", loadHandler);
- } else {
- fail("Window does not have required addEventListener or attachEvent method");
- }
-
- return api;
-})();
-rangy.createModule("DomUtil", function(api, module) {
-
- var UNDEF = "undefined";
- var util = api.util;
-
- // Perform feature tests
- if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
- module.fail("document missing a Node creation method");
- }
-
- if (!util.isHostMethod(document, "getElementsByTagName")) {
- module.fail("document missing getElementsByTagName method");
- }
-
- var el = document.createElement("div");
- if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
- !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
- module.fail("Incomplete Element implementation");
- }
-
- // innerHTML is required for Range's createContextualFragment method
- if (!util.isHostProperty(el, "innerHTML")) {
- module.fail("Element is missing innerHTML property");
- }
-
- var textNode = document.createTextNode("test");
- if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
- !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
- !util.areHostProperties(textNode, ["data"]))) {
- module.fail("Incomplete Text Node implementation");
- }
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
- // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
- // contains just the document as a single element and the value searched for is the document.
- var arrayContains = /*Array.prototype.indexOf ?
- function(arr, val) {
- return arr.indexOf(val) > -1;
- }:*/
-
- function(arr, val) {
- var i = arr.length;
- while (i--) {
- if (arr[i] === val) {
- return true;
- }
- }
- return false;
- };
-
- // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
- function isHtmlNamespace(node) {
- var ns;
- return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
- }
-
- function parentElement(node) {
- var parent = node.parentNode;
- return (parent.nodeType == 1) ? parent : null;
- }
-
- function getNodeIndex(node) {
- var i = 0;
- while( (node = node.previousSibling) ) {
- i++;
- }
- return i;
- }
-
- function getNodeLength(node) {
- var childNodes;
- return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);
- }
-
- function getCommonAncestor(node1, node2) {
- var ancestors = [], n;
- for (n = node1; n; n = n.parentNode) {
- ancestors.push(n);
- }
-
- for (n = node2; n; n = n.parentNode) {
- if (arrayContains(ancestors, n)) {
- return n;
- }
- }
-
- return null;
- }
-
- function isAncestorOf(ancestor, descendant, selfIsAncestor) {
- var n = selfIsAncestor ? descendant : descendant.parentNode;
- while (n) {
- if (n === ancestor) {
- return true;
- } else {
- n = n.parentNode;
- }
- }
- return false;
- }
-
- function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
- var p, n = selfIsAncestor ? node : node.parentNode;
- while (n) {
- p = n.parentNode;
- if (p === ancestor) {
- return n;
- }
- n = p;
- }
- return null;
- }
-
- function isCharacterDataNode(node) {
- var t = node.nodeType;
- return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
- }
-
- function insertAfter(node, precedingNode) {
- var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
- if (nextNode) {
- parent.insertBefore(node, nextNode);
- } else {
- parent.appendChild(node);
- }
- return node;
- }
-
- // Note that we cannot use splitText() because it is bugridden in IE 9.
- function splitDataNode(node, index) {
- var newNode = node.cloneNode(false);
- newNode.deleteData(0, index);
- node.deleteData(index, node.length - index);
- insertAfter(newNode, node);
- return newNode;
- }
-
- function getDocument(node) {
- if (node.nodeType == 9) {
- return node;
- } else if (typeof node.ownerDocument != UNDEF) {
- return node.ownerDocument;
- } else if (typeof node.document != UNDEF) {
- return node.document;
- } else if (node.parentNode) {
- return getDocument(node.parentNode);
- } else {
- throw new Error("getDocument: no document found for node");
- }
- }
-
- function getWindow(node) {
- var doc = getDocument(node);
- if (typeof doc.defaultView != UNDEF) {
- return doc.defaultView;
- } else if (typeof doc.parentWindow != UNDEF) {
- return doc.parentWindow;
- } else {
- throw new Error("Cannot get a window object for node");
- }
- }
-
- function getIframeDocument(iframeEl) {
- if (typeof iframeEl.contentDocument != UNDEF) {
- return iframeEl.contentDocument;
- } else if (typeof iframeEl.contentWindow != UNDEF) {
- return iframeEl.contentWindow.document;
- } else {
- throw new Error("getIframeWindow: No Document object found for iframe element");
- }
- }
-
- function getIframeWindow(iframeEl) {
- if (typeof iframeEl.contentWindow != UNDEF) {
- return iframeEl.contentWindow;
- } else if (typeof iframeEl.contentDocument != UNDEF) {
- return iframeEl.contentDocument.defaultView;
- } else {
- throw new Error("getIframeWindow: No Window object found for iframe element");
- }
- }
-
- function getBody(doc) {
- return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
- }
-
- function getRootContainer(node) {
- var parent;
- while ( (parent = node.parentNode) ) {
- node = parent;
- }
- return node;
- }
-
- function comparePoints(nodeA, offsetA, nodeB, offsetB) {
- // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
- var nodeC, root, childA, childB, n;
- if (nodeA == nodeB) {
-
- // Case 1: nodes are the same
- return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
- } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
-
- // Case 2: node C (container B or an ancestor) is a child node of A
- return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
- } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
-
- // Case 3: node C (container A or an ancestor) is a child node of B
- return getNodeIndex(nodeC) < offsetB ? -1 : 1;
- } else {
-
- // Case 4: containers are siblings or descendants of siblings
- root = getCommonAncestor(nodeA, nodeB);
- childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
- childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
-
- if (childA === childB) {
- // This shouldn't be possible
-
- throw new Error("comparePoints got to case 4 and childA and childB are the same!");
- } else {
- n = root.firstChild;
- while (n) {
- if (n === childA) {
- return -1;
- } else if (n === childB) {
- return 1;
- }
- n = n.nextSibling;
- }
- throw new Error("Should not be here!");
- }
- }
- }
-
- function fragmentFromNodeChildren(node) {
- var fragment = getDocument(node).createDocumentFragment(), child;
- while ( (child = node.firstChild) ) {
- fragment.appendChild(child);
- }
- return fragment;
- }
-
- function inspectNode(node) {
- if (!node) {
- return "[No node]";
- }
- if (isCharacterDataNode(node)) {
- return '"' + node.data + '"';
- } else if (node.nodeType == 1) {
- var idAttr = node.id ? ' id="' + node.id + '"' : "";
- return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";
- } else {
- return node.nodeName;
- }
- }
-
- /**
- * @constructor
- */
- function NodeIterator(root) {
- this.root = root;
- this._next = root;
- }
-
- NodeIterator.prototype = {
- _current: null,
-
- hasNext: function() {
- return !!this._next;
- },
-
- next: function() {
- var n = this._current = this._next;
- var child, next;
- if (this._current) {
- child = n.firstChild;
- if (child) {
- this._next = child;
- } else {
- next = null;
- while ((n !== this.root) && !(next = n.nextSibling)) {
- n = n.parentNode;
- }
- this._next = next;
- }
- }
- return this._current;
- },
-
- detach: function() {
- this._current = this._next = this.root = null;
- }
- };
-
- function createIterator(root) {
- return new NodeIterator(root);
- }
-
- /**
- * @constructor
- */
- function DomPosition(node, offset) {
- this.node = node;
- this.offset = offset;
- }
-
- DomPosition.prototype = {
- equals: function(pos) {
- return this.node === pos.node & this.offset == pos.offset;
- },
-
- inspect: function() {
- return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
- }
- };
-
- /**
- * @constructor
- */
- function DOMException(codeName) {
- this.code = this[codeName];
- this.codeName = codeName;
- this.message = "DOMException: " + this.codeName;
- }
-
- DOMException.prototype = {
- INDEX_SIZE_ERR: 1,
- HIERARCHY_REQUEST_ERR: 3,
- WRONG_DOCUMENT_ERR: 4,
- NO_MODIFICATION_ALLOWED_ERR: 7,
- NOT_FOUND_ERR: 8,
- NOT_SUPPORTED_ERR: 9,
- INVALID_STATE_ERR: 11
- };
-
- DOMException.prototype.toString = function() {
- return this.message;
- };
-
- api.dom = {
- arrayContains: arrayContains,
- isHtmlNamespace: isHtmlNamespace,
- parentElement: parentElement,
- getNodeIndex: getNodeIndex,
- getNodeLength: getNodeLength,
- getCommonAncestor: getCommonAncestor,
- isAncestorOf: isAncestorOf,
- getClosestAncestorIn: getClosestAncestorIn,
- isCharacterDataNode: isCharacterDataNode,
- insertAfter: insertAfter,
- splitDataNode: splitDataNode,
- getDocument: getDocument,
- getWindow: getWindow,
- getIframeWindow: getIframeWindow,
- getIframeDocument: getIframeDocument,
- getBody: getBody,
- getRootContainer: getRootContainer,
- comparePoints: comparePoints,
- inspectNode: inspectNode,
- fragmentFromNodeChildren: fragmentFromNodeChildren,
- createIterator: createIterator,
- DomPosition: DomPosition
- };
-
- api.DOMException = DOMException;
-});rangy.createModule("DomRange", function(api, module) {
- api.requireModules( ["DomUtil"] );
-
-
- var dom = api.dom;
- var DomPosition = dom.DomPosition;
- var DOMException = api.DOMException;
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- // Utility functions
-
- function isNonTextPartiallySelected(node, range) {
- return (node.nodeType != 3) &&
- (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
- }
-
- function getRangeDocument(range) {
- return dom.getDocument(range.startContainer);
- }
-
- function dispatchEvent(range, type, args) {
- var listeners = range._listeners[type];
- if (listeners) {
- for (var i = 0, len = listeners.length; i < len; ++i) {
- listeners[i].call(range, {target: range, args: args});
- }
- }
- }
-
- function getBoundaryBeforeNode(node) {
- return new DomPosition(node.parentNode, dom.getNodeIndex(node));
- }
-
- function getBoundaryAfterNode(node) {
- return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
- }
-
- function insertNodeAtPosition(node, n, o) {
- var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
- if (dom.isCharacterDataNode(n)) {
- if (o == n.length) {
- dom.insertAfter(node, n);
- } else {
- n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
- }
- } else if (o >= n.childNodes.length) {
- n.appendChild(node);
- } else {
- n.insertBefore(node, n.childNodes[o]);
- }
- return firstNodeInserted;
- }
-
- function cloneSubtree(iterator) {
- var partiallySelected;
- for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
- partiallySelected = iterator.isPartiallySelectedSubtree();
-
- node = node.cloneNode(!partiallySelected);
- if (partiallySelected) {
- subIterator = iterator.getSubtreeIterator();
- node.appendChild(cloneSubtree(subIterator));
- subIterator.detach(true);
- }
-
- if (node.nodeType == 10) { // DocumentType
- throw new DOMException("HIERARCHY_REQUEST_ERR");
- }
- frag.appendChild(node);
- }
- return frag;
- }
-
- function iterateSubtree(rangeIterator, func, iteratorState) {
- var it, n;
- iteratorState = iteratorState || { stop: false };
- for (var node, subRangeIterator; node = rangeIterator.next(); ) {
- //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
- if (rangeIterator.isPartiallySelectedSubtree()) {
- // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
- // node selected by the Range.
- if (func(node) === false) {
- iteratorState.stop = true;
- return;
- } else {
- subRangeIterator = rangeIterator.getSubtreeIterator();
- iterateSubtree(subRangeIterator, func, iteratorState);
- subRangeIterator.detach(true);
- if (iteratorState.stop) {
- return;
- }
- }
- } else {
- // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
- // descendant
- it = dom.createIterator(node);
- while ( (n = it.next()) ) {
- if (func(n) === false) {
- iteratorState.stop = true;
- return;
- }
- }
- }
- }
- }
-
- function deleteSubtree(iterator) {
- var subIterator;
- while (iterator.next()) {
- if (iterator.isPartiallySelectedSubtree()) {
- subIterator = iterator.getSubtreeIterator();
- deleteSubtree(subIterator);
- subIterator.detach(true);
- } else {
- iterator.remove();
- }
- }
- }
-
- function extractSubtree(iterator) {
-
- for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
-
-
- if (iterator.isPartiallySelectedSubtree()) {
- node = node.cloneNode(false);
- subIterator = iterator.getSubtreeIterator();
- node.appendChild(extractSubtree(subIterator));
- subIterator.detach(true);
- } else {
- iterator.remove();
- }
- if (node.nodeType == 10) { // DocumentType
- throw new DOMException("HIERARCHY_REQUEST_ERR");
- }
- frag.appendChild(node);
- }
- return frag;
- }
-
- function getNodesInRange(range, nodeTypes, filter) {
- //log.info("getNodesInRange, " + nodeTypes.join(","));
- var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
- var filterExists = !!filter;
- if (filterNodeTypes) {
- regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
- }
-
- var nodes = [];
- iterateSubtree(new RangeIterator(range, false), function(node) {
- if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
- nodes.push(node);
- }
- });
- return nodes;
- }
-
- function inspect(range) {
- var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
- return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
- dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
- }
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
-
- /**
- * @constructor
- */
- function RangeIterator(range, clonePartiallySelectedTextNodes) {
- this.range = range;
- this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
-
-
-
- if (!range.collapsed) {
- this.sc = range.startContainer;
- this.so = range.startOffset;
- this.ec = range.endContainer;
- this.eo = range.endOffset;
- var root = range.commonAncestorContainer;
-
- if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
- this.isSingleCharacterDataNode = true;
- this._first = this._last = this._next = this.sc;
- } else {
- this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
- this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
- this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
- this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
- }
-
- }
- }
-
- RangeIterator.prototype = {
- _current: null,
- _next: null,
- _first: null,
- _last: null,
- isSingleCharacterDataNode: false,
-
- reset: function() {
- this._current = null;
- this._next = this._first;
- },
-
- hasNext: function() {
- return !!this._next;
- },
-
- next: function() {
- // Move to next node
- var current = this._current = this._next;
- if (current) {
- this._next = (current !== this._last) ? current.nextSibling : null;
-
- // Check for partially selected text nodes
- if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
- if (current === this.ec) {
-
- (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
- }
- if (this._current === this.sc) {
-
- (current = current.cloneNode(true)).deleteData(0, this.so);
- }
- }
- }
-
- return current;
- },
-
- remove: function() {
- var current = this._current, start, end;
-
- if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
- start = (current === this.sc) ? this.so : 0;
- end = (current === this.ec) ? this.eo : current.length;
- if (start != end) {
- current.deleteData(start, end - start);
- }
- } else {
- if (current.parentNode) {
- current.parentNode.removeChild(current);
- } else {
-
- }
- }
- },
-
- // Checks if the current node is partially selected
- isPartiallySelectedSubtree: function() {
- var current = this._current;
- return isNonTextPartiallySelected(current, this.range);
- },
-
- getSubtreeIterator: function() {
- var subRange;
- if (this.isSingleCharacterDataNode) {
- subRange = this.range.cloneRange();
- subRange.collapse();
- } else {
- subRange = new Range(getRangeDocument(this.range));
- var current = this._current;
- var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);
-
- if (dom.isAncestorOf(current, this.sc, true)) {
- startContainer = this.sc;
- startOffset = this.so;
- }
- if (dom.isAncestorOf(current, this.ec, true)) {
- endContainer = this.ec;
- endOffset = this.eo;
- }
-
- updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
- }
- return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
- },
-
- detach: function(detachRange) {
- if (detachRange) {
- this.range.detach();
- }
- this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
- }
- };
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- // Exceptions
-
- /**
- * @constructor
- */
- function RangeException(codeName) {
- this.code = this[codeName];
- this.codeName = codeName;
- this.message = "RangeException: " + this.codeName;
- }
-
- RangeException.prototype = {
- BAD_BOUNDARYPOINTS_ERR: 1,
- INVALID_NODE_TYPE_ERR: 2
- };
-
- RangeException.prototype.toString = function() {
- return this.message;
- };
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- /**
- * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
- * TODO: Look into making this a proper iterator, not requiring preloading everything first
- * @constructor
- */
- function RangeNodeIterator(range, nodeTypes, filter) {
- this.nodes = getNodesInRange(range, nodeTypes, filter);
- this._next = this.nodes[0];
- this._position = 0;
- }
-
- RangeNodeIterator.prototype = {
- _current: null,
-
- hasNext: function() {
- return !!this._next;
- },
-
- next: function() {
- this._current = this._next;
- this._next = this.nodes[ ++this._position ];
- return this._current;
- },
-
- detach: function() {
- this._current = this._next = this.nodes = null;
- }
- };
-
- var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
- var rootContainerNodeTypes = [2, 9, 11];
- var readonlyNodeTypes = [5, 6, 10, 12];
- var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
- var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
-
- function createAncestorFinder(nodeTypes) {
- return function(node, selfIsAncestor) {
- var t, n = selfIsAncestor ? node : node.parentNode;
- while (n) {
- t = n.nodeType;
- if (dom.arrayContains(nodeTypes, t)) {
- return n;
- }
- n = n.parentNode;
- }
- return null;
- };
- }
-
- var getRootContainer = dom.getRootContainer;
- var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
- var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
- var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
-
- function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
- if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
- throw new RangeException("INVALID_NODE_TYPE_ERR");
- }
- }
-
- function assertNotDetached(range) {
- if (!range.startContainer) {
- throw new DOMException("INVALID_STATE_ERR");
- }
- }
-
- function assertValidNodeType(node, invalidTypes) {
- if (!dom.arrayContains(invalidTypes, node.nodeType)) {
- throw new RangeException("INVALID_NODE_TYPE_ERR");
- }
- }
-
- function assertValidOffset(node, offset) {
- if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
- throw new DOMException("INDEX_SIZE_ERR");
- }
- }
-
- function assertSameDocumentOrFragment(node1, node2) {
- if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
- throw new DOMException("WRONG_DOCUMENT_ERR");
- }
- }
-
- function assertNodeNotReadOnly(node) {
- if (getReadonlyAncestor(node, true)) {
- throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
- }
- }
-
- function assertNode(node, codeName) {
- if (!node) {
- throw new DOMException(codeName);
- }
- }
-
- function isOrphan(node) {
- return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
- }
-
- function isValidOffset(node, offset) {
- return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
- }
-
- function assertRangeValid(range) {
- assertNotDetached(range);
- if (isOrphan(range.startContainer) || isOrphan(range.endContainer) ||
- !isValidOffset(range.startContainer, range.startOffset) ||
- !isValidOffset(range.endContainer, range.endOffset)) {
- throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
- }
- }
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- // Test the browser's innerHTML support to decide how to implement createContextualFragment
- var styleEl = document.createElement("style");
- var htmlParsingConforms = false;
- try {
- styleEl.innerHTML = "x";
- htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
- } catch (e) {
- // IE 6 and 7 throw
- }
-
- api.features.htmlParsingConforms = htmlParsingConforms;
-
- var createContextualFragment = htmlParsingConforms ?
-
- // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
- // discussion and base code for this implementation at issue 67.
- // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
- // Thanks to Aleks Williams.
- function(fragmentStr) {
- // "Let node the context object's start's node."
- var node = this.startContainer;
- var doc = dom.getDocument(node);
-
- // "If the context object's start's node is null, raise an INVALID_STATE_ERR
- // exception and abort these steps."
- if (!node) {
- throw new DOMException("INVALID_STATE_ERR");
- }
-
- // "Let element be as follows, depending on node's interface:"
- // Document, Document Fragment: null
- var el = null;
-
- // "Element: node"
- if (node.nodeType == 1) {
- el = node;
-
- // "Text, Comment: node's parentElement"
- } else if (dom.isCharacterDataNode(node)) {
- el = dom.parentElement(node);
- }
-
- // "If either element is null or element's ownerDocument is an HTML document
- // and element's local name is "html" and element's namespace is the HTML
- // namespace"
- if (el === null || (
- el.nodeName == "HTML"
- && dom.isHtmlNamespace(dom.getDocument(el).documentElement)
- && dom.isHtmlNamespace(el)
- )) {
-
- // "let element be a new Element with "body" as its local name and the HTML
- // namespace as its namespace.""
- el = doc.createElement("body");
- } else {
- el = el.cloneNode(false);
- }
-
- // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
- // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
- // "In either case, the algorithm must be invoked with fragment as the input
- // and element as the context element."
- el.innerHTML = fragmentStr;
-
- // "If this raises an exception, then abort these steps. Otherwise, let new
- // children be the nodes returned."
-
- // "Let fragment be a new DocumentFragment."
- // "Append all new children to fragment."
- // "Return fragment."
- return dom.fragmentFromNodeChildren(el);
- } :
-
- // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
- // previous versions of Rangy used (with the exception of using a body element rather than a div)
- function(fragmentStr) {
- assertNotDetached(this);
- var doc = getRangeDocument(this);
- var el = doc.createElement("body");
- el.innerHTML = fragmentStr;
-
- return dom.fragmentFromNodeChildren(el);
- };
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
- "commonAncestorContainer"];
-
- var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
- var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
-
- function RangePrototype() {}
-
- RangePrototype.prototype = {
- attachListener: function(type, listener) {
- this._listeners[type].push(listener);
- },
-
- compareBoundaryPoints: function(how, range) {
- assertRangeValid(this);
- assertSameDocumentOrFragment(this.startContainer, range.startContainer);
-
- var nodeA, offsetA, nodeB, offsetB;
- var prefixA = (how == e2s || how == s2s) ? "start" : "end";
- var prefixB = (how == s2e || how == s2s) ? "start" : "end";
- nodeA = this[prefixA + "Container"];
- offsetA = this[prefixA + "Offset"];
- nodeB = range[prefixB + "Container"];
- offsetB = range[prefixB + "Offset"];
- return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
- },
-
- insertNode: function(node) {
- assertRangeValid(this);
- assertValidNodeType(node, insertableNodeTypes);
- assertNodeNotReadOnly(this.startContainer);
-
- if (dom.isAncestorOf(node, this.startContainer, true)) {
- throw new DOMException("HIERARCHY_REQUEST_ERR");
- }
-
- // No check for whether the container of the start of the Range is of a type that does not allow
- // children of the type of node: the browser's DOM implementation should do this for us when we attempt
- // to add the node
-
- var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
- this.setStartBefore(firstNodeInserted);
- },
-
- cloneContents: function() {
- assertRangeValid(this);
-
- var clone, frag;
- if (this.collapsed) {
- return getRangeDocument(this).createDocumentFragment();
- } else {
- if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
- clone = this.startContainer.cloneNode(true);
- clone.data = clone.data.slice(this.startOffset, this.endOffset);
- frag = getRangeDocument(this).createDocumentFragment();
- frag.appendChild(clone);
- return frag;
- } else {
- var iterator = new RangeIterator(this, true);
- clone = cloneSubtree(iterator);
- iterator.detach();
- }
- return clone;
- }
- },
-
- canSurroundContents: function() {
- assertRangeValid(this);
- assertNodeNotReadOnly(this.startContainer);
- assertNodeNotReadOnly(this.endContainer);
-
- // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
- // no non-text nodes.
- var iterator = new RangeIterator(this, true);
- var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
- (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
- iterator.detach();
- return !boundariesInvalid;
- },
-
- surroundContents: function(node) {
- assertValidNodeType(node, surroundNodeTypes);
-
- if (!this.canSurroundContents()) {
- throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
- }
-
- // Extract the contents
- var content = this.extractContents();
-
- // Clear the children of the node
- if (node.hasChildNodes()) {
- while (node.lastChild) {
- node.removeChild(node.lastChild);
- }
- }
-
- // Insert the new node and add the extracted contents
- insertNodeAtPosition(node, this.startContainer, this.startOffset);
- node.appendChild(content);
-
- this.selectNode(node);
- },
-
- cloneRange: function() {
- assertRangeValid(this);
- var range = new Range(getRangeDocument(this));
- var i = rangeProperties.length, prop;
- while (i--) {
- prop = rangeProperties[i];
- range[prop] = this[prop];
- }
- return range;
- },
-
- toString: function() {
- assertRangeValid(this);
- var sc = this.startContainer;
- if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
- return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
- } else {
- var textBits = [], iterator = new RangeIterator(this, true);
-
- iterateSubtree(iterator, function(node) {
- // Accept only text or CDATA nodes, not comments
-
- if (node.nodeType == 3 || node.nodeType == 4) {
- textBits.push(node.data);
- }
- });
- iterator.detach();
- return textBits.join("");
- }
- },
-
- // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
- // been removed from Mozilla.
-
- compareNode: function(node) {
- assertRangeValid(this);
-
- var parent = node.parentNode;
- var nodeIndex = dom.getNodeIndex(node);
-
- if (!parent) {
- throw new DOMException("NOT_FOUND_ERR");
- }
-
- var startComparison = this.comparePoint(parent, nodeIndex),
- endComparison = this.comparePoint(parent, nodeIndex + 1);
-
- if (startComparison < 0) { // Node starts before
- return (endComparison > 0) ? n_b_a : n_b;
- } else {
- return (endComparison > 0) ? n_a : n_i;
- }
- },
-
- comparePoint: function(node, offset) {
- assertRangeValid(this);
- assertNode(node, "HIERARCHY_REQUEST_ERR");
- assertSameDocumentOrFragment(node, this.startContainer);
-
- if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
- return -1;
- } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
- return 1;
- }
- return 0;
- },
-
- createContextualFragment: createContextualFragment,
-
- toHtml: function() {
- assertRangeValid(this);
- var container = getRangeDocument(this).createElement("div");
- container.appendChild(this.cloneContents());
- return container.innerHTML;
- },
-
- // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
- // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
- intersectsNode: function(node, touchingIsIntersecting) {
- assertRangeValid(this);
- assertNode(node, "NOT_FOUND_ERR");
- if (dom.getDocument(node) !== getRangeDocument(this)) {
- return false;
- }
-
- var parent = node.parentNode, offset = dom.getNodeIndex(node);
- assertNode(parent, "NOT_FOUND_ERR");
-
- var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
- endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
-
- return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
- },
-
-
- isPointInRange: function(node, offset) {
- assertRangeValid(this);
- assertNode(node, "HIERARCHY_REQUEST_ERR");
- assertSameDocumentOrFragment(node, this.startContainer);
-
- return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
- (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
- },
-
- // The methods below are non-standard and invented by me.
-
- // Sharing a boundary start-to-end or end-to-start does not count as intersection.
- intersectsRange: function(range, touchingIsIntersecting) {
- assertRangeValid(this);
-
- if (getRangeDocument(range) != getRangeDocument(this)) {
- throw new DOMException("WRONG_DOCUMENT_ERR");
- }
-
- var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),
- endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);
-
- return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
- },
-
- intersection: function(range) {
- if (this.intersectsRange(range)) {
- var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
- endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
-
- var intersectionRange = this.cloneRange();
-
- if (startComparison == -1) {
- intersectionRange.setStart(range.startContainer, range.startOffset);
- }
- if (endComparison == 1) {
- intersectionRange.setEnd(range.endContainer, range.endOffset);
- }
- return intersectionRange;
- }
- return null;
- },
-
- union: function(range) {
- if (this.intersectsRange(range, true)) {
- var unionRange = this.cloneRange();
- if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
- unionRange.setStart(range.startContainer, range.startOffset);
- }
- if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
- unionRange.setEnd(range.endContainer, range.endOffset);
- }
- return unionRange;
- } else {
- throw new RangeException("Ranges do not intersect");
- }
- },
-
- containsNode: function(node, allowPartial) {
- if (allowPartial) {
- return this.intersectsNode(node, false);
- } else {
- return this.compareNode(node) == n_i;
- }
- },
-
- containsNodeContents: function(node) {
- return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0;
- },
-
- containsRange: function(range) {
- return this.intersection(range).equals(range);
- },
-
- containsNodeText: function(node) {
- var nodeRange = this.cloneRange();
- nodeRange.selectNode(node);
- var textNodes = nodeRange.getNodes([3]);
- if (textNodes.length > 0) {
- nodeRange.setStart(textNodes[0], 0);
- var lastTextNode = textNodes.pop();
- nodeRange.setEnd(lastTextNode, lastTextNode.length);
- var contains = this.containsRange(nodeRange);
- nodeRange.detach();
- return contains;
- } else {
- return this.containsNodeContents(node);
- }
- },
-
- createNodeIterator: function(nodeTypes, filter) {
- assertRangeValid(this);
- return new RangeNodeIterator(this, nodeTypes, filter);
- },
-
- getNodes: function(nodeTypes, filter) {
- assertRangeValid(this);
- return getNodesInRange(this, nodeTypes, filter);
- },
-
- getDocument: function() {
- return getRangeDocument(this);
- },
-
- collapseBefore: function(node) {
- assertNotDetached(this);
-
- this.setEndBefore(node);
- this.collapse(false);
- },
-
- collapseAfter: function(node) {
- assertNotDetached(this);
-
- this.setStartAfter(node);
- this.collapse(true);
- },
-
- getName: function() {
- return "DomRange";
- },
-
- equals: function(range) {
- return Range.rangesEqual(this, range);
- },
-
- inspect: function() {
- return inspect(this);
- }
- };
-
- function copyComparisonConstantsToObject(obj) {
- obj.START_TO_START = s2s;
- obj.START_TO_END = s2e;
- obj.END_TO_END = e2e;
- obj.END_TO_START = e2s;
-
- obj.NODE_BEFORE = n_b;
- obj.NODE_AFTER = n_a;
- obj.NODE_BEFORE_AND_AFTER = n_b_a;
- obj.NODE_INSIDE = n_i;
- }
-
- function copyComparisonConstants(constructor) {
- copyComparisonConstantsToObject(constructor);
- copyComparisonConstantsToObject(constructor.prototype);
- }
-
- function createRangeContentRemover(remover, boundaryUpdater) {
- return function() {
- assertRangeValid(this);
-
- var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
-
- var iterator = new RangeIterator(this, true);
-
- // Work out where to position the range after content removal
- var node, boundary;
- if (sc !== root) {
- node = dom.getClosestAncestorIn(sc, root, true);
- boundary = getBoundaryAfterNode(node);
- sc = boundary.node;
- so = boundary.offset;
- }
-
- // Check none of the range is read-only
- iterateSubtree(iterator, assertNodeNotReadOnly);
-
- iterator.reset();
-
- // Remove the content
- var returnValue = remover(iterator);
- iterator.detach();
-
- // Move to the new position
- boundaryUpdater(this, sc, so, sc, so);
-
- return returnValue;
- };
- }
-
- function createPrototypeRange(constructor, boundaryUpdater, detacher) {
- function createBeforeAfterNodeSetter(isBefore, isStart) {
- return function(node) {
- assertNotDetached(this);
- assertValidNodeType(node, beforeAfterNodeTypes);
- assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
-
- var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
- (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
- };
- }
-
- function setRangeStart(range, node, offset) {
- var ec = range.endContainer, eo = range.endOffset;
- if (node !== range.startContainer || offset !== range.startOffset) {
- // Check the root containers of the range and the new boundary, and also check whether the new boundary
- // is after the current end. In either case, collapse the range to the new position
- if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {
- ec = node;
- eo = offset;
- }
- boundaryUpdater(range, node, offset, ec, eo);
- }
- }
-
- function setRangeEnd(range, node, offset) {
- var sc = range.startContainer, so = range.startOffset;
- if (node !== range.endContainer || offset !== range.endOffset) {
- // Check the root containers of the range and the new boundary, and also check whether the new boundary
- // is after the current end. In either case, collapse the range to the new position
- if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {
- sc = node;
- so = offset;
- }
- boundaryUpdater(range, sc, so, node, offset);
- }
- }
-
- function setRangeStartAndEnd(range, node, offset) {
- if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {
- boundaryUpdater(range, node, offset, node, offset);
- }
- }
-
- constructor.prototype = new RangePrototype();
-
- api.util.extend(constructor.prototype, {
- setStart: function(node, offset) {
- assertNotDetached(this);
- assertNoDocTypeNotationEntityAncestor(node, true);
- assertValidOffset(node, offset);
-
- setRangeStart(this, node, offset);
- },
-
- setEnd: function(node, offset) {
- assertNotDetached(this);
- assertNoDocTypeNotationEntityAncestor(node, true);
- assertValidOffset(node, offset);
-
- setRangeEnd(this, node, offset);
- },
-
- setStartBefore: createBeforeAfterNodeSetter(true, true),
- setStartAfter: createBeforeAfterNodeSetter(false, true),
- setEndBefore: createBeforeAfterNodeSetter(true, false),
- setEndAfter: createBeforeAfterNodeSetter(false, false),
-
- collapse: function(isStart) {
- assertRangeValid(this);
- if (isStart) {
- boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
- } else {
- boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
- }
- },
-
- selectNodeContents: function(node) {
- // This doesn't seem well specified: the spec talks only about selecting the node's contents, which
- // could be taken to mean only its children. However, browsers implement this the same as selectNode for
- // text nodes, so I shall do likewise
- assertNotDetached(this);
- assertNoDocTypeNotationEntityAncestor(node, true);
-
- boundaryUpdater(this, node, 0, node, dom.getNodeLength(node));
- },
-
- selectNode: function(node) {
- assertNotDetached(this);
- assertNoDocTypeNotationEntityAncestor(node, false);
- assertValidNodeType(node, beforeAfterNodeTypes);
-
- var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
- boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
- },
-
- extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
-
- deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
-
- canSurroundContents: function() {
- assertRangeValid(this);
- assertNodeNotReadOnly(this.startContainer);
- assertNodeNotReadOnly(this.endContainer);
-
- // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
- // no non-text nodes.
- var iterator = new RangeIterator(this, true);
- var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
- (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
- iterator.detach();
- return !boundariesInvalid;
- },
-
- detach: function() {
- detacher(this);
- },
-
- splitBoundaries: function() {
- assertRangeValid(this);
-
-
- var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
- var startEndSame = (sc === ec);
-
- if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
- dom.splitDataNode(ec, eo);
-
- }
-
- if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {
-
- sc = dom.splitDataNode(sc, so);
- if (startEndSame) {
- eo -= so;
- ec = sc;
- } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
- eo++;
- }
- so = 0;
-
- }
- boundaryUpdater(this, sc, so, ec, eo);
- },
-
- normalizeBoundaries: function() {
- assertRangeValid(this);
-
- var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
-
- var mergeForward = function(node) {
- var sibling = node.nextSibling;
- if (sibling && sibling.nodeType == node.nodeType) {
- ec = node;
- eo = node.length;
- node.appendData(sibling.data);
- sibling.parentNode.removeChild(sibling);
- }
- };
-
- var mergeBackward = function(node) {
- var sibling = node.previousSibling;
- if (sibling && sibling.nodeType == node.nodeType) {
- sc = node;
- var nodeLength = node.length;
- so = sibling.length;
- node.insertData(0, sibling.data);
- sibling.parentNode.removeChild(sibling);
- if (sc == ec) {
- eo += so;
- ec = sc;
- } else if (ec == node.parentNode) {
- var nodeIndex = dom.getNodeIndex(node);
- if (eo == nodeIndex) {
- ec = node;
- eo = nodeLength;
- } else if (eo > nodeIndex) {
- eo--;
- }
- }
- }
- };
-
- var normalizeStart = true;
-
- if (dom.isCharacterDataNode(ec)) {
- if (ec.length == eo) {
- mergeForward(ec);
- }
- } else {
- if (eo > 0) {
- var endNode = ec.childNodes[eo - 1];
- if (endNode && dom.isCharacterDataNode(endNode)) {
- mergeForward(endNode);
- }
- }
- normalizeStart = !this.collapsed;
- }
-
- if (normalizeStart) {
- if (dom.isCharacterDataNode(sc)) {
- if (so == 0) {
- mergeBackward(sc);
- }
- } else {
- if (so < sc.childNodes.length) {
- var startNode = sc.childNodes[so];
- if (startNode && dom.isCharacterDataNode(startNode)) {
- mergeBackward(startNode);
- }
- }
- }
- } else {
- sc = ec;
- so = eo;
- }
-
- boundaryUpdater(this, sc, so, ec, eo);
- },
-
- collapseToPoint: function(node, offset) {
- assertNotDetached(this);
-
- assertNoDocTypeNotationEntityAncestor(node, true);
- assertValidOffset(node, offset);
-
- setRangeStartAndEnd(this, node, offset);
- }
- });
-
- copyComparisonConstants(constructor);
- }
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- // Updates commonAncestorContainer and collapsed after boundary change
- function updateCollapsedAndCommonAncestor(range) {
- range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
- range.commonAncestorContainer = range.collapsed ?
- range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
- }
-
- function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
- var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
- var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);
-
- range.startContainer = startContainer;
- range.startOffset = startOffset;
- range.endContainer = endContainer;
- range.endOffset = endOffset;
-
- updateCollapsedAndCommonAncestor(range);
- dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});
- }
-
- function detach(range) {
- assertNotDetached(range);
- range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
- range.collapsed = range.commonAncestorContainer = null;
- dispatchEvent(range, "detach", null);
- range._listeners = null;
- }
-
- /**
- * @constructor
- */
- function Range(doc) {
- this.startContainer = doc;
- this.startOffset = 0;
- this.endContainer = doc;
- this.endOffset = 0;
- this._listeners = {
- boundarychange: [],
- detach: []
- };
- updateCollapsedAndCommonAncestor(this);
- }
-
- createPrototypeRange(Range, updateBoundaries, detach);
-
- api.rangePrototype = RangePrototype.prototype;
-
- Range.rangeProperties = rangeProperties;
- Range.RangeIterator = RangeIterator;
- Range.copyComparisonConstants = copyComparisonConstants;
- Range.createPrototypeRange = createPrototypeRange;
- Range.inspect = inspect;
- Range.getRangeDocument = getRangeDocument;
- Range.rangesEqual = function(r1, r2) {
- return r1.startContainer === r2.startContainer &&
- r1.startOffset === r2.startOffset &&
- r1.endContainer === r2.endContainer &&
- r1.endOffset === r2.endOffset;
- };
-
- api.DomRange = Range;
- api.RangeException = RangeException;
-});rangy.createModule("WrappedRange", function(api, module) {
- api.requireModules( ["DomUtil", "DomRange"] );
-
- /**
- * @constructor
- */
- var WrappedRange;
- var dom = api.dom;
- var DomPosition = dom.DomPosition;
- var DomRange = api.DomRange;
-
-
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- /*
- This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
- method. For example, in the following (where pipes denote the selection boundaries):
-
- - | a
- b |
-
- var range = document.selection.createRange();
- alert(range.parentElement().id); // Should alert "ul" but alerts "b"
-
- This method returns the common ancestor node of the following:
- - the parentElement() of the textRange
- - the parentElement() of the textRange after calling collapse(true)
- - the parentElement() of the textRange after calling collapse(false)
- */
- function getTextRangeContainerElement(textRange) {
- var parentEl = textRange.parentElement();
-
- var range = textRange.duplicate();
- range.collapse(true);
- var startEl = range.parentElement();
- range = textRange.duplicate();
- range.collapse(false);
- var endEl = range.parentElement();
- var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
-
- return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
- }
-
- function textRangeIsCollapsed(textRange) {
- return textRange.compareEndPoints("StartToEnd", textRange) == 0;
- }
-
- // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as
- // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has
- // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling
- // for inputs and images, plus optimizations.
- function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {
- var workingRange = textRange.duplicate();
-
- workingRange.collapse(isStart);
- var containerElement = workingRange.parentElement();
-
- // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
- // check for that
- // TODO: Find out when. Workaround for wholeRangeContainerElement may break this
- if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {
- containerElement = wholeRangeContainerElement;
-
- }
-
-
-
- // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
- // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
- if (!containerElement.canHaveHTML) {
- return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
- }
-
- var workingNode = dom.getDocument(containerElement).createElement("span");
- var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
- var previousNode, nextNode, boundaryPosition, boundaryNode;
-
- // Move the working range through the container's children, starting at the end and working backwards, until the
- // working range reaches or goes past the boundary we're interested in
- do {
- containerElement.insertBefore(workingNode, workingNode.previousSibling);
- workingRange.moveToElementText(workingNode);
- } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
- workingNode.previousSibling);
-
- // We've now reached or gone past the boundary of the text range we're interested in
- // so have identified the node we want
- boundaryNode = workingNode.nextSibling;
-
- if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {
- // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the
- // node containing the text range's boundary, so we move the end of the working range to the boundary point
- // and measure the length of its text to get the boundary's offset within the node.
- workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
-
-
- var offset;
-
- if (/[\r\n]/.test(boundaryNode.data)) {
- /*
- For the particular case of a boundary within a text node containing line breaks (within a element,
- for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:
-
- - Each line break is represented as \r in the text node's data/nodeValue properties
- - Each line break is represented as \r\n in the TextRange's 'text' property
- - The 'text' property of the TextRange does not contain trailing line breaks
-
- To get round the problem presented by the final fact above, we can use the fact that TextRange's
- moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
- the same as the number of characters it was instructed to move. The simplest approach is to use this to
- store the characters moved when moving both the start and end of the range to the start of the document
- body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
- However, this is extremely slow when the document is large and the range is near the end of it. Clearly
- doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
- problem.
-
- Another approach that works is to use moveStart() to move the start boundary of the range up to the end
- boundary one character at a time and incrementing a counter with the value returned by the moveStart()
- call. However, the check for whether the start boundary has reached the end boundary is expensive, so
- this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
- the range within the document).
-
- The method below is a hybrid of the two methods above. It uses the fact that a string containing the
- TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
- text of the TextRange, so the start of the range is moved that length initially and then a character at
- a time to make up for any trailing line breaks not contained in the 'text' property. This has good
- performance in most situations compared to the previous two methods.
- */
- var tempRange = workingRange.duplicate();
- var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
-
- offset = tempRange.moveStart("character", rangeLength);
- while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
- offset++;
- tempRange.moveStart("character", 1);
- }
- } else {
- offset = workingRange.text.length;
- }
- boundaryPosition = new DomPosition(boundaryNode, offset);
- } else {
-
-
- // If the boundary immediately follows a character data node and this is the end boundary, we should favour
- // a position within that, and likewise for a start boundary preceding a character data node
- previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
- nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
-
-
-
- if (nextNode && dom.isCharacterDataNode(nextNode)) {
- boundaryPosition = new DomPosition(nextNode, 0);
- } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
- boundaryPosition = new DomPosition(previousNode, previousNode.length);
- } else {
- boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
- }
- }
-
- // Clean up
- workingNode.parentNode.removeChild(workingNode);
-
- return boundaryPosition;
- }
-
- // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
- // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
- // (http://code.google.com/p/ierange/)
- function createBoundaryTextRange(boundaryPosition, isStart) {
- var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
- var doc = dom.getDocument(boundaryPosition.node);
- var workingNode, childNodes, workingRange = doc.body.createTextRange();
- var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);
-
- if (nodeIsDataNode) {
- boundaryNode = boundaryPosition.node;
- boundaryParent = boundaryNode.parentNode;
- } else {
- childNodes = boundaryPosition.node.childNodes;
- boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
- boundaryParent = boundaryPosition.node;
- }
-
- // Position the range immediately before the node containing the boundary
- workingNode = doc.createElement("span");
-
- // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
- // element rather than immediately before or after it, which is what we want
- workingNode.innerHTML = "feff;";
-
- // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
- // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
- if (boundaryNode) {
- boundaryParent.insertBefore(workingNode, boundaryNode);
- } else {
- boundaryParent.appendChild(workingNode);
- }
-
- workingRange.moveToElementText(workingNode);
- workingRange.collapse(!isStart);
-
- // Clean up
- boundaryParent.removeChild(workingNode);
-
- // Move the working range to the text offset, if required
- if (nodeIsDataNode) {
- workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
- }
-
- return workingRange;
- }
-
- /*----------------------------------------------------------------------------------------------------------------*/
-
- if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
- // This is a wrapper around the browser's native DOM Range. It has two aims:
- // - Provide workarounds for specific browser bugs
- // - provide convenient extensions, which are inherited from Rangy's DomRange
-
- (function() {
- var rangeProto;
- var rangeProperties = DomRange.rangeProperties;
- var canSetRangeStartAfterEnd;
-
- function updateRangeProperties(range) {
- var i = rangeProperties.length, prop;
- while (i--) {
- prop = rangeProperties[i];
- range[prop] = range.nativeRange[prop];
- }
- }
-
- function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
- var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
- var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
-
- // Always set both boundaries for the benefit of IE9 (see issue 35)
- if (startMoved || endMoved) {
- range.setEnd(endContainer, endOffset);
- range.setStart(startContainer, startOffset);
- }
- }
-
- function detach(range) {
- range.nativeRange.detach();
- range.detached = true;
- var i = rangeProperties.length, prop;
- while (i--) {
- prop = rangeProperties[i];
- range[prop] = null;
- }
- }
-
- var createBeforeAfterNodeSetter;
-
- WrappedRange = function(range) {
- if (!range) {
- throw new Error("Range must be specified");
- }
- this.nativeRange = range;
- updateRangeProperties(this);
- };
-
- DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
-
- rangeProto = WrappedRange.prototype;
-
- rangeProto.selectNode = function(node) {
- this.nativeRange.selectNode(node);
- updateRangeProperties(this);
- };
-
- rangeProto.deleteContents = function() {
- this.nativeRange.deleteContents();
- updateRangeProperties(this);
- };
-
- rangeProto.extractContents = function() {
- var frag = this.nativeRange.extractContents();
- updateRangeProperties(this);
- return frag;
- };
-
- rangeProto.cloneContents = function() {
- return this.nativeRange.cloneContents();
- };
-
- // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
- // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
- // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
- // insertNode, which works but is almost certainly slower than the native implementation.
-/*
- rangeProto.insertNode = function(node) {
- this.nativeRange.insertNode(node);
- updateRangeProperties(this);
- };
-*/
-
- rangeProto.surroundContents = function(node) {
- this.nativeRange.surroundContents(node);
- updateRangeProperties(this);
- };
-
- rangeProto.collapse = function(isStart) {
- this.nativeRange.collapse(isStart);
- updateRangeProperties(this);
- };
-
- rangeProto.cloneRange = function() {
- return new WrappedRange(this.nativeRange.cloneRange());
- };
-
- rangeProto.refresh = function() {
- updateRangeProperties(this);
- };
-
- rangeProto.toString = function() {
- return this.nativeRange.toString();
- };
-
- // Create test range and node for feature detection
-
- var testTextNode = document.createTextNode("test");
- dom.getBody(document).appendChild(testTextNode);
- var range = document.createRange();
-
- /*--------------------------------------------------------------------------------------------------------*/
-
- // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
- // correct for it
-
- range.setStart(testTextNode, 0);
- range.setEnd(testTextNode, 0);
-
- try {
- range.setStart(testTextNode, 1);
- canSetRangeStartAfterEnd = true;
-
- rangeProto.setStart = function(node, offset) {
- this.nativeRange.setStart(node, offset);
- updateRangeProperties(this);
- };
-
- rangeProto.setEnd = function(node, offset) {
- this.nativeRange.setEnd(node, offset);
- updateRangeProperties(this);
- };
-
- createBeforeAfterNodeSetter = function(name) {
- return function(node) {
- this.nativeRange[name](node);
- updateRangeProperties(this);
- };
- };
-
- } catch(ex) {
-
-
- canSetRangeStartAfterEnd = false;
-
- rangeProto.setStart = function(node, offset) {
- try {
- this.nativeRange.setStart(node, offset);
- } catch (ex) {
- this.nativeRange.setEnd(node, offset);
- this.nativeRange.setStart(node, offset);
- }
- updateRangeProperties(this);
- };
-
- rangeProto.setEnd = function(node, offset) {
- try {
- this.nativeRange.setEnd(node, offset);
- } catch (ex) {
- this.nativeRange.setStart(node, offset);
- this.nativeRange.setEnd(node, offset);
- }
- updateRangeProperties(this);
- };
-
- createBeforeAfterNodeSetter = function(name, oppositeName) {
- return function(node) {
- try {
- this.nativeRange[name](node);
- } catch (ex) {
- this.nativeRange[oppositeName](node);
- this.nativeRange[name](node);
- }
- updateRangeProperties(this);
- };
- };
- }
-
- rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
- rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
- rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
- rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
-
- /*--------------------------------------------------------------------------------------------------------*/
-
- // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
- // the 0th character of the text node
- range.selectNodeContents(testTextNode);
- if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
- range.startOffset == 0 && range.endOffset == testTextNode.length) {
- rangeProto.selectNodeContents = function(node) {
- this.nativeRange.selectNodeContents(node);
- updateRangeProperties(this);
- };
- } else {
- rangeProto.selectNodeContents = function(node) {
- this.setStart(node, 0);
- this.setEnd(node, DomRange.getEndOffset(node));
- };
- }
-
- /*--------------------------------------------------------------------------------------------------------*/
-
- // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
- // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
-
- range.selectNodeContents(testTextNode);
- range.setEnd(testTextNode, 3);
-
- var range2 = document.createRange();
- range2.selectNodeContents(testTextNode);
- range2.setEnd(testTextNode, 4);
- range2.setStart(testTextNode, 2);
-
- if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &
- range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
- // This is the wrong way round, so correct for it
-
-
- rangeProto.compareBoundaryPoints = function(type, range) {
- range = range.nativeRange || range;
- if (type == range.START_TO_END) {
- type = range.END_TO_START;
- } else if (type == range.END_TO_START) {
- type = range.START_TO_END;
- }
- return this.nativeRange.compareBoundaryPoints(type, range);
- };
- } else {
- rangeProto.compareBoundaryPoints = function(type, range) {
- return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
- };
- }
-
- /*--------------------------------------------------------------------------------------------------------*/
-
- // Test for existence of createContextualFragment and delegate to it if it exists
- if (api.util.isHostMethod(range, "createContextualFragment")) {
- rangeProto.createContextualFragment = function(fragmentStr) {
- return this.nativeRange.createContextualFragment(fragmentStr);
- };
- }
-
- /*--------------------------------------------------------------------------------------------------------*/
-
- // Clean up
- dom.getBody(document).removeChild(testTextNode);
- range.detach();
- range2.detach();
- })();
-
- api.createNativeRange = function(doc) {
- doc = doc || document;
- return doc.createRange();
- };
- } else if (api.features.implementsTextRange) {
- // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
- // prototype
-
- WrappedRange = function(textRange) {
- this.textRange = textRange;
- this.refresh();
- };
-
- WrappedRange.prototype = new DomRange(document);
-
- WrappedRange.prototype.refresh = function() {
- var start, end;
-
- // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
- var rangeContainerElement = getTextRangeContainerElement(this.textRange);
-
- if (textRangeIsCollapsed(this.textRange)) {
- end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);
- } else {
-
- start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
- end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
- }
-
- this.setStart(start.node, start.offset);
- this.setEnd(end.node, end.offset);
- };
-
- DomRange.copyComparisonConstants(WrappedRange);
-
- // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
- var globalObj = (function() { return this; })();
- if (typeof globalObj.Range == "undefined") {
- globalObj.Range = WrappedRange;
- }
-
- api.createNativeRange = function(doc) {
- doc = doc || document;
- return doc.body.createTextRange();
- };
- }
-
- if (api.features.implementsTextRange) {
- WrappedRange.rangeToTextRange = function(range) {
- if (range.collapsed) {
- var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
-
-
-
- return tr;
-
- //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
- } else {
- var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
- var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
- var textRange = dom.getDocument(range.startContainer).body.createTextRange();
- textRange.setEndPoint("StartToStart", startRange);
- textRange.setEndPoint("EndToEnd", endRange);
- return textRange;
- }
- };
- }
-
- WrappedRange.prototype.getName = function() {
- return "WrappedRange";
- };
-
- api.WrappedRange = WrappedRange;
-
- api.createRange = function(doc) {
- doc = doc || document;
- return new WrappedRange(api.createNativeRange(doc));
- };
-
- api.createRangyRange = function(doc) {
- doc = doc || document;
- return new DomRange(doc);
- };
-
- api.createIframeRange = function(iframeEl) {
- return api.createRange(dom.getIframeDocument(iframeEl));
- };
-
- api.createIframeRangyRange = function(iframeEl) {
- return api.createRangyRange(dom.getIframeDocument(iframeEl));
- };
-
- api.addCreateMissingNativeApiListener(function(win) {
- var doc = win.document;
- if (typeof doc.createRange == "undefined") {
- doc.createRange = function() {
- return api.createRange(this);
- };
- }
- doc = win = null;
- });
-});rangy.createModule("WrappedSelection", function(api, module) {
- // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
- // spec (http://html5.org/specs/dom-range.html)
-
- api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
-
- api.config.checkSelectionRanges = true;
-
- var BOOLEAN = "boolean",
- windowPropertyName = "_rangySelection",
- dom = api.dom,
- util = api.util,
- DomRange = api.DomRange,
- WrappedRange = api.WrappedRange,
- DOMException = api.DOMException,
- DomPosition = dom.DomPosition,
- getSelection,
- selectionIsCollapsed,
- CONTROL = "Control";
-
-
-
- function getWinSelection(winParam) {
- return (winParam || window).getSelection();
- }
-
- function getDocSelection(winParam) {
- return (winParam || window).document.selection;
- }
-
- // Test for the Range/TextRange and Selection features required
- // Test for ability to retrieve selection
- var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
- implementsDocSelection = api.util.isHostObject(document, "selection");
-
- var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
-
- if (useDocumentSelection) {
- getSelection = getDocSelection;
- api.isSelectionValid = function(winParam) {
- var doc = (winParam || window).document, nativeSel = doc.selection;
-
- // Check whether the selection TextRange is actually contained within the correct document
- return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
- };
- } else if (implementsWinGetSelection) {
- getSelection = getWinSelection;
- api.isSelectionValid = function() {
- return true;
- };
- } else {
- module.fail("Neither document.selection or window.getSelection() detected.");
- }
-
- api.getNativeSelection = getSelection;
-
- var testSelection = getSelection();
- var testRange = api.createNativeRange(document);
- var body = dom.getBody(document);
-
- // Obtaining a range from a selection
- var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
- util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
- api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
-
- // Test for existence of native selection extend() method
- var selectionHasExtend = util.isHostMethod(testSelection, "extend");
- api.features.selectionHasExtend = selectionHasExtend;
-
- // Test if rangeCount exists
- var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
- api.features.selectionHasRangeCount = selectionHasRangeCount;
-
- var selectionSupportsMultipleRanges = false;
- var collapsedNonEditableSelectionsSupported = true;
-
- if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
- typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
-
- (function() {
- var iframe = document.createElement("iframe");
- body.appendChild(iframe);
-
- var iframeDoc = dom.getIframeDocument(iframe);
- iframeDoc.open();
- iframeDoc.write("12");
- iframeDoc.close();
-
- var sel = dom.getIframeWindow(iframe).getSelection();
- var docEl = iframeDoc.documentElement;
- var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
-
- // Test whether the native selection will allow a collapsed selection within a non-editable element
- var r1 = iframeDoc.createRange();
- r1.setStart(textNode, 1);
- r1.collapse(true);
- sel.addRange(r1);
- collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
- sel.removeAllRanges();
-
- // Test whether the native selection is capable of supporting multiple ranges
- var r2 = r1.cloneRange();
- r1.setStart(textNode, 0);
- r2.setEnd(textNode, 2);
- sel.addRange(r1);
- sel.addRange(r2);
-
- selectionSupportsMultipleRanges = (sel.rangeCount == 2);
-
- // Clean up
- r1.detach();
- r2.detach();
-
- body.removeChild(iframe);
- })();
- }
-
- api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
- api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
-
- // ControlRanges
- var implementsControlRange = false, testControlRange;
-
- if (body && util.isHostMethod(body, "createControlRange")) {
- testControlRange = body.createControlRange();
- if (util.areHostProperties(testControlRange, ["item", "add"])) {
- implementsControlRange = true;
- }
- }
- api.features.implementsControlRange = implementsControlRange;
-
- // Selection collapsedness
- if (selectionHasAnchorAndFocus) {
- selectionIsCollapsed = function(sel) {
- return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
- };
- } else {
- selectionIsCollapsed = function(sel) {
- return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
- };
- }
-
- function updateAnchorAndFocusFromRange(sel, range, backwards) {
- var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";
- sel.anchorNode = range[anchorPrefix + "Container"];
- sel.anchorOffset = range[anchorPrefix + "Offset"];
- sel.focusNode = range[focusPrefix + "Container"];
- sel.focusOffset = range[focusPrefix + "Offset"];
- }
-
- function updateAnchorAndFocusFromNativeSelection(sel) {
- var nativeSel = sel.nativeSelection;
- sel.anchorNode = nativeSel.anchorNode;
- sel.anchorOffset = nativeSel.anchorOffset;
- sel.focusNode = nativeSel.focusNode;
- sel.focusOffset = nativeSel.focusOffset;
- }
-
- function updateEmptySelection(sel) {
- sel.anchorNode = sel.focusNode = null;
- sel.anchorOffset = sel.focusOffset = 0;
- sel.rangeCount = 0;
- sel.isCollapsed = true;
- sel._ranges.length = 0;
- }
-
- function getNativeRange(range) {
- var nativeRange;
- if (range instanceof DomRange) {
- nativeRange = range._selectionNativeRange;
- if (!nativeRange) {
- nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
- nativeRange.setEnd(range.endContainer, range.endOffset);
- nativeRange.setStart(range.startContainer, range.startOffset);
- range._selectionNativeRange = nativeRange;
- range.attachListener("detach", function() {
-
- this._selectionNativeRange = null;
- });
- }
- } else if (range instanceof WrappedRange) {
- nativeRange = range.nativeRange;
- } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
- nativeRange = range;
- }
- return nativeRange;
- }
-
- function rangeContainsSingleElement(rangeNodes) {
- if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
- return false;
- }
- for (var i = 1, len = rangeNodes.length; i < len; ++i) {
- if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
- return false;
- }
- }
- return true;
- }
-
- function getSingleElementFromRange(range) {
- var nodes = range.getNodes();
- if (!rangeContainsSingleElement(nodes)) {
- throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
- }
- return nodes[0];
- }
-
- function isTextRange(range) {
- return !!range && typeof range.text != "undefined";
- }
-
- function updateFromTextRange(sel, range) {
- // Create a Range from the selected TextRange
- var wrappedRange = new WrappedRange(range);
- sel._ranges = [wrappedRange];
-
- updateAnchorAndFocusFromRange(sel, wrappedRange, false);
- sel.rangeCount = 1;
- sel.isCollapsed = wrappedRange.collapsed;
- }
-
- function updateControlSelection(sel) {
- // Update the wrapped selection based on what's now in the native selection
- sel._ranges.length = 0;
- if (sel.docSelection.type == "None") {
- updateEmptySelection(sel);
- } else {
- var controlRange = sel.docSelection.createRange();
- if (isTextRange(controlRange)) {
- // This case (where the selection type is "Control" and calling createRange() on the selection returns
- // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
- // ControlRange have been removed from the ControlRange and removed from the document.
- updateFromTextRange(sel, controlRange);
- } else {
- sel.rangeCount = controlRange.length;
- var range, doc = dom.getDocument(controlRange.item(0));
- for (var i = 0; i < sel.rangeCount; ++i) {
- range = api.createRange(doc);
- range.selectNode(controlRange.item(i));
- sel._ranges.push(range);
- }
- sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
- updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
- }
- }
- }
-
- function addRangeToControlSelection(sel, range) {
- var controlRange = sel.docSelection.createRange();
- var rangeElement = getSingleElementFromRange(range);
-
- // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
- // contained by the supplied range
- var doc = dom.getDocument(controlRange.item(0));
- var newControlRange = dom.getBody(doc).createControlRange();
- for (var i = 0, len = controlRange.length; i < len; ++i) {
- newControlRange.add(controlRange.item(i));
- }
- try {
- newControlRange.add(rangeElement);
- } catch (ex) {
- throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
- }
- newControlRange.select();
-
- // Update the wrapped selection based on what's now in the native selection
- updateControlSelection(sel);
- }
-
- var getSelectionRangeAt;
-
- if (util.isHostMethod(testSelection, "getRangeAt")) {
- getSelectionRangeAt = function(sel, index) {
- try {
- return sel.getRangeAt(index);
- } catch(ex) {
- return null;
- }
- };
- } else if (selectionHasAnchorAndFocus) {
- getSelectionRangeAt = function(sel) {
- var doc = dom.getDocument(sel.anchorNode);
- var range = api.createRange(doc);
- range.setStart(sel.anchorNode, sel.anchorOffset);
- range.setEnd(sel.focusNode, sel.focusOffset);
-
- // Handle the case when the selection was selected backwards (from the end to the start in the
- // document)
- if (range.collapsed !== this.isCollapsed) {
- range.setStart(sel.focusNode, sel.focusOffset);
- range.setEnd(sel.anchorNode, sel.anchorOffset);
- }
-
- return range;
- };
- }
-
- /**
- * @constructor
- */
- function WrappedSelection(selection, docSelection, win) {
- this.nativeSelection = selection;
- this.docSelection = docSelection;
- this._ranges = [];
- this.win = win;
- this.refresh();
- }
-
- api.getSelection = function(win) {
- win = win || window;
- var sel = win[windowPropertyName];
- var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
- if (sel) {
- sel.nativeSelection = nativeSel;
- sel.docSelection = docSel;
- sel.refresh(win);
- } else {
- sel = new WrappedSelection(nativeSel, docSel, win);
- win[windowPropertyName] = sel;
- }
- return sel;
- };
-
- api.getIframeSelection = function(iframeEl) {
- return api.getSelection(dom.getIframeWindow(iframeEl));
- };
-
- var selProto = WrappedSelection.prototype;
-
- function createControlSelection(sel, ranges) {
- // Ensure that the selection becomes of type "Control"
- var doc = dom.getDocument(ranges[0].startContainer);
- var controlRange = dom.getBody(doc).createControlRange();
- for (var i = 0, el; i < rangeCount; ++i) {
- el = getSingleElementFromRange(ranges[i]);
- try {
- controlRange.add(el);
- } catch (ex) {
- throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
- }
- }
- controlRange.select();
-
- // Update the wrapped selection based on what's now in the native selection
- updateControlSelection(sel);
- }
-
- // Selecting a range
- if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
- selProto.removeAllRanges = function() {
- this.nativeSelection.removeAllRanges();
- updateEmptySelection(this);
- };
-
- var addRangeBackwards = function(sel, range) {
- var doc = DomRange.getRangeDocument(range);
- var endRange = api.createRange(doc);
- endRange.collapseToPoint(range.endContainer, range.endOffset);
- sel.nativeSelection.addRange(getNativeRange(endRange));
- sel.nativeSelection.extend(range.startContainer, range.startOffset);
- sel.refresh();
- };
-
- if (selectionHasRangeCount) {
- selProto.addRange = function(range, backwards) {
- if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
- addRangeToControlSelection(this, range);
- } else {
- if (backwards && selectionHasExtend) {
- addRangeBackwards(this, range);
- } else {
- var previousRangeCount;
- if (selectionSupportsMultipleRanges) {
- previousRangeCount = this.rangeCount;
- } else {
- this.removeAllRanges();
- previousRangeCount = 0;
- }
- this.nativeSelection.addRange(getNativeRange(range));
-
- // Check whether adding the range was successful
- this.rangeCount = this.nativeSelection.rangeCount;
-
- if (this.rangeCount == previousRangeCount + 1) {
- // The range was added successfully
-
- // Check whether the range that we added to the selection is reflected in the last range extracted from
- // the selection
- if (api.config.checkSelectionRanges) {
- var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
- if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {
- // Happens in WebKit with, for example, a selection placed at the start of a text node
- range = new WrappedRange(nativeRange);
- }
- }
- this._ranges[this.rangeCount - 1] = range;
- updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));
- this.isCollapsed = selectionIsCollapsed(this);
- } else {
- // The range was not added successfully. The simplest thing is to refresh
- this.refresh();
- }
- }
- }
- };
- } else {
- selProto.addRange = function(range, backwards) {
- if (backwards && selectionHasExtend) {
- addRangeBackwards(this, range);
- } else {
- this.nativeSelection.addRange(getNativeRange(range));
- this.refresh();
- }
- };
- }
-
- selProto.setRanges = function(ranges) {
- if (implementsControlRange && ranges.length > 1) {
- createControlSelection(this, ranges);
- } else {
- this.removeAllRanges();
- for (var i = 0, len = ranges.length; i < len; ++i) {
- this.addRange(ranges[i]);
- }
- }
- };
- } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
- implementsControlRange && useDocumentSelection) {
-
- selProto.removeAllRanges = function() {
- // Added try/catch as fix for issue #21
- try {
- this.docSelection.empty();
-
- // Check for empty() not working (issue #24)
- if (this.docSelection.type != "None") {
- // Work around failure to empty a control selection by instead selecting a TextRange and then
- // calling empty()
- var doc;
- if (this.anchorNode) {
- doc = dom.getDocument(this.anchorNode);
- } else if (this.docSelection.type == CONTROL) {
- var controlRange = this.docSelection.createRange();
- if (controlRange.length) {
- doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
- }
- }
- if (doc) {
- var textRange = doc.body.createTextRange();
- textRange.select();
- this.docSelection.empty();
- }
- }
- } catch(ex) {}
- updateEmptySelection(this);
- };
-
- selProto.addRange = function(range) {
- if (this.docSelection.type == CONTROL) {
- addRangeToControlSelection(this, range);
- } else {
- WrappedRange.rangeToTextRange(range).select();
- this._ranges[0] = range;
- this.rangeCount = 1;
- this.isCollapsed = this._ranges[0].collapsed;
- updateAnchorAndFocusFromRange(this, range, false);
- }
- };
-
- selProto.setRanges = function(ranges) {
- this.removeAllRanges();
- var rangeCount = ranges.length;
- if (rangeCount > 1) {
- createControlSelection(this, ranges);
- } else if (rangeCount) {
- this.addRange(ranges[0]);
- }
- };
- } else {
- module.fail("No means of selecting a Range or TextRange was found");
- return false;
- }
-
- selProto.getRangeAt = function(index) {
- if (index < 0 || index >= this.rangeCount) {
- throw new DOMException("INDEX_SIZE_ERR");
- } else {
- return this._ranges[index];
- }
- };
-
- var refreshSelection;
-
- if (useDocumentSelection) {
- refreshSelection = function(sel) {
- var range;
- if (api.isSelectionValid(sel.win)) {
- range = sel.docSelection.createRange();
- } else {
- range = dom.getBody(sel.win.document).createTextRange();
- range.collapse(true);
- }
-
-
- if (sel.docSelection.type == CONTROL) {
- updateControlSelection(sel);
- } else if (isTextRange(range)) {
- updateFromTextRange(sel, range);
- } else {
- updateEmptySelection(sel);
- }
- };
- } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {
- refreshSelection = function(sel) {
- if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
- updateControlSelection(sel);
- } else {
- sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
- if (sel.rangeCount) {
- for (var i = 0, len = sel.rangeCount; i < len; ++i) {
- sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
- }
- updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));
- sel.isCollapsed = selectionIsCollapsed(sel);
- } else {
- updateEmptySelection(sel);
- }
- }
- };
- } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {
- refreshSelection = function(sel) {
- var range, nativeSel = sel.nativeSelection;
- if (nativeSel.anchorNode) {
- range = getSelectionRangeAt(nativeSel, 0);
- sel._ranges = [range];
- sel.rangeCount = 1;
- updateAnchorAndFocusFromNativeSelection(sel);
- sel.isCollapsed = selectionIsCollapsed(sel);
- } else {
- updateEmptySelection(sel);
- }
- };
- } else {
- module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
- return false;
- }
-
- selProto.refresh = function(checkForChanges) {
- var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
- refreshSelection(this);
- if (checkForChanges) {
- var i = oldRanges.length;
- if (i != this._ranges.length) {
- return false;
- }
- while (i--) {
- if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
- return false;
- }
- }
- return true;
- }
- };
-
- // Removal of a single range
- var removeRangeManually = function(sel, range) {
- var ranges = sel.getAllRanges(), removed = false;
- sel.removeAllRanges();
- for (var i = 0, len = ranges.length; i < len; ++i) {
- if (removed || range !== ranges[i]) {
- sel.addRange(ranges[i]);
- } else {
- // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
- // times. removeRange should only remove the first instance, so the following ensures only the first
- // instance is removed
- removed = true;
- }
- }
- if (!sel.rangeCount) {
- updateEmptySelection(sel);
- }
- };
-
- if (implementsControlRange) {
- selProto.removeRange = function(range) {
- if (this.docSelection.type == CONTROL) {
- var controlRange = this.docSelection.createRange();
- var rangeElement = getSingleElementFromRange(range);
-
- // Create a new ControlRange containing all the elements in the selected ControlRange minus the
- // element contained by the supplied range
- var doc = dom.getDocument(controlRange.item(0));
- var newControlRange = dom.getBody(doc).createControlRange();
- var el, removed = false;
- for (var i = 0, len = controlRange.length; i < len; ++i) {
- el = controlRange.item(i);
- if (el !== rangeElement || removed) {
- newControlRange.add(controlRange.item(i));
- } else {
- removed = true;
- }
- }
- newControlRange.select();
-
- // Update the wrapped selection based on what's now in the native selection
- updateControlSelection(this);
- } else {
- removeRangeManually(this, range);
- }
- };
- } else {
- selProto.removeRange = function(range) {
- removeRangeManually(this, range);
- };
- }
-
- // Detecting if a selection is backwards
- var selectionIsBackwards;
- if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
- selectionIsBackwards = function(sel) {
- var backwards = false;
- if (sel.anchorNode) {
- backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
- }
- return backwards;
- };
-
- selProto.isBackwards = function() {
- return selectionIsBackwards(this);
- };
- } else {
- selectionIsBackwards = selProto.isBackwards = function() {
- return false;
- };
- }
-
- // Selection text
- // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
- selProto.toString = function() {
-
- var rangeTexts = [];
- for (var i = 0, len = this.rangeCount; i < len; ++i) {
- rangeTexts[i] = "" + this._ranges[i];
- }
- return rangeTexts.join("");
- };
-
- function assertNodeInSameDocument(sel, node) {
- if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
- throw new DOMException("WRONG_DOCUMENT_ERR");
- }
- }
-
- // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
- selProto.collapse = function(node, offset) {
- assertNodeInSameDocument(this, node);
- var range = api.createRange(dom.getDocument(node));
- range.collapseToPoint(node, offset);
- this.removeAllRanges();
- this.addRange(range);
- this.isCollapsed = true;
- };
-
- selProto.collapseToStart = function() {
- if (this.rangeCount) {
- var range = this._ranges[0];
- this.collapse(range.startContainer, range.startOffset);
- } else {
- throw new DOMException("INVALID_STATE_ERR");
- }
- };
-
- selProto.collapseToEnd = function() {
- if (this.rangeCount) {
- var range = this._ranges[this.rangeCount - 1];
- this.collapse(range.endContainer, range.endOffset);
- } else {
- throw new DOMException("INVALID_STATE_ERR");
- }
- };
-
- // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
- // never used by Rangy.
- selProto.selectAllChildren = function(node) {
- assertNodeInSameDocument(this, node);
- var range = api.createRange(dom.getDocument(node));
- range.selectNodeContents(node);
- this.removeAllRanges();
- this.addRange(range);
- };
-
- selProto.deleteFromDocument = function() {
- // Sepcial behaviour required for Control selections
- if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
- var controlRange = this.docSelection.createRange();
- var element;
- while (controlRange.length) {
- element = controlRange.item(0);
- controlRange.remove(element);
- element.parentNode.removeChild(element);
- }
- this.refresh();
- } else if (this.rangeCount) {
- var ranges = this.getAllRanges();
- this.removeAllRanges();
- for (var i = 0, len = ranges.length; i < len; ++i) {
- ranges[i].deleteContents();
- }
- // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each
- // range. Firefox moves the selection to where the final selected range was, so we emulate that
- this.addRange(ranges[len - 1]);
- }
- };
-
- // The following are non-standard extensions
- selProto.getAllRanges = function() {
- return this._ranges.slice(0);
- };
-
- selProto.setSingleRange = function(range) {
- this.setRanges( [range] );
- };
-
- selProto.containsNode = function(node, allowPartial) {
- for (var i = 0, len = this._ranges.length; i < len; ++i) {
- if (this._ranges[i].containsNode(node, allowPartial)) {
- return true;
- }
- }
- return false;
- };
-
- selProto.toHtml = function() {
- var html = "";
- if (this.rangeCount) {
- var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
- for (var i = 0, len = this._ranges.length; i < len; ++i) {
- container.appendChild(this._ranges[i].cloneContents());
- }
- html = container.innerHTML;
- }
- return html;
- };
-
- function inspect(sel) {
- var rangeInspects = [];
- var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
- var focus = new DomPosition(sel.focusNode, sel.focusOffset);
- var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
-
- if (typeof sel.rangeCount != "undefined") {
- for (var i = 0, len = sel.rangeCount; i < len; ++i) {
- rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
- }
- }
- return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
- ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
-
- }
-
- selProto.getName = function() {
- return "WrappedSelection";
- };
-
- selProto.inspect = function() {
- return inspect(this);
- };
-
- selProto.detach = function() {
- this.win[windowPropertyName] = null;
- this.win = this.anchorNode = this.focusNode = null;
- };
-
- WrappedSelection.inspect = inspect;
-
- api.Selection = WrappedSelection;
-
- api.selectionPrototype = selProto;
-
- api.addCreateMissingNativeApiListener(function(win) {
- if (typeof win.getSelection == "undefined") {
- win.getSelection = function() {
- return api.getSelection(this);
- };
- }
- win = null;
- });
-});
-/*
- Base.js, version 1.1a
- Copyright 2006-2010, Dean Edwards
- License: http://www.opensource.org/licenses/mit-license.php
-*/
-
-var Base = function() {
- // dummy
-};
-
-Base.extend = function(_instance, _static) { // subclass
- var extend = Base.prototype.extend;
-
- // build the prototype
- Base._prototyping = true;
- var proto = new this;
- extend.call(proto, _instance);
- proto.base = function() {
- // call this method from any other method to invoke that method's ancestor
- };
- delete Base._prototyping;
-
- // create the wrapper for the constructor function
- //var constructor = proto.constructor.valueOf(); //-dean
- var constructor = proto.constructor;
- var klass = proto.constructor = function() {
- if (!Base._prototyping) {
- if (this._constructing || this.constructor == klass) { // instantiation
- this._constructing = true;
- constructor.apply(this, arguments);
- delete this._constructing;
- } else if (arguments[0] != null) { // casting
- return (arguments[0].extend || extend).call(arguments[0], proto);
- }
- }
- };
-
- // build the class interface
- klass.ancestor = this;
- klass.extend = this.extend;
- klass.forEach = this.forEach;
- klass.implement = this.implement;
- klass.prototype = proto;
- klass.toString = this.toString;
- klass.valueOf = function(type) {
- //return (type == "object") ? klass : constructor; //-dean
- return (type == "object") ? klass : constructor.valueOf();
- };
- extend.call(klass, _static);
- // class initialisation
- if (typeof klass.init == "function") klass.init();
- return klass;
-};
-
-Base.prototype = {
- extend: function(source, value) {
- if (arguments.length > 1) { // extending with a name/value pair
- var ancestor = this[source];
- if (ancestor && (typeof value == "function") && // overriding a method?
- // the valueOf() comparison is to avoid circular references
- (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
- /\bbase\b/.test(value)) {
- // get the underlying method
- var method = value.valueOf();
- // override
- value = function() {
- var previous = this.base || Base.prototype.base;
- this.base = ancestor;
- var returnValue = method.apply(this, arguments);
- this.base = previous;
- return returnValue;
- };
- // point to the underlying method
- value.valueOf = function(type) {
- return (type == "object") ? value : method;
- };
- value.toString = Base.toString;
- }
- this[source] = value;
- } else if (source) { // extending with an object literal
- var extend = Base.prototype.extend;
- // if this object has a customised extend method then use it
- if (!Base._prototyping && typeof this != "function") {
- extend = this.extend || extend;
- }
- var proto = {toSource: null};
- // do the "toString" and other methods manually
- var hidden = ["constructor", "toString", "valueOf"];
- // if we are prototyping then include the constructor
- var i = Base._prototyping ? 0 : 1;
- while (key = hidden[i++]) {
- if (source[key] != proto[key]) {
- extend.call(this, key, source[key]);
-
- }
- }
- // copy each of the source object's properties to this object
- for (var key in source) {
- if (!proto[key]) extend.call(this, key, source[key]);
- }
- }
- return this;
- }
-};
-
-// initialise
-Base = Base.extend({
- constructor: function() {
- this.extend(arguments[0]);
- }
-}, {
- ancestor: Object,
- version: "1.1",
-
- forEach: function(object, block, context) {
- for (var key in object) {
- if (this.prototype[key] === undefined) {
- block.call(context, object[key], key, object);
- }
- }
- },
-
- implement: function() {
- for (var i = 0; i < arguments.length; i++) {
- if (typeof arguments[i] == "function") {
- // if it's a function, call it
- arguments[i](this.prototype);
- } else {
- // add the interface using the extend method
- this.prototype.extend(arguments[i]);
- }
- }
- return this;
- },
-
- toString: function() {
- return String(this.valueOf());
- }
-});/**
- * Detect browser support for specific features
- */
-wysihtml5.browser = (function() {
- var userAgent = navigator.userAgent,
- testElement = document.createElement("div"),
- // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
- isIE = userAgent.indexOf("MSIE") !== -1 && userAgent.indexOf("Opera") === -1,
- isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1,
- isWebKit = userAgent.indexOf("AppleWebKit/") !== -1,
- isChrome = userAgent.indexOf("Chrome/") !== -1,
- isOpera = userAgent.indexOf("Opera/") !== -1;
-
- function iosVersion(userAgent) {
- return ((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1];
- }
-
- return {
- // Static variable needed, publicly accessible, to be able override it in unit tests
- USER_AGENT: userAgent,
-
- /**
- * Exclude browsers that are not capable of displaying and handling
- * contentEditable as desired:
- * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
- * - IE < 8 create invalid markup and crash randomly from time to time
- *
- * @return {Boolean}
- */
- supported: function() {
- var userAgent = this.USER_AGENT.toLowerCase(),
- // Essential for making html elements editable
- hasContentEditableSupport = "contentEditable" in testElement,
- // Following methods are needed in order to interact with the contentEditable area
- hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState,
- // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
- hasQuerySelectorSupport = document.querySelector && document.querySelectorAll,
- // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
- isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
-
- return hasContentEditableSupport
- && hasEditingApiSupport
- && hasQuerySelectorSupport
- && !isIncompatibleMobileBrowser;
- },
-
- isTouchDevice: function() {
- return this.supportsEvent("touchmove");
- },
-
- isIos: function() {
- var userAgent = this.USER_AGENT.toLowerCase();
- return userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1;
- },
-
- /**
- * Whether the browser supports sandboxed iframes
- * Currently only IE 6+ offers such feature