Path: blob/main/website/GAUSS/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.js
2946 views
/**1* @license wysihtml5 v0.3.02* https://github.com/xing/wysihtml53*4* Author: Christopher Blum (https://github.com/tiff)5*6* Copyright (C) 2012 XING AG7* Licensed under the MIT license (MIT)8*9*/10var wysihtml5 = {11version: "0.3.0",1213// namespaces14commands: {},15dom: {},16quirks: {},17toolbar: {},18lang: {},19selection: {},20views: {},2122INVISIBLE_SPACE: "\uFEFF",2324EMPTY_FUNCTION: function() {},2526ELEMENT_NODE: 1,27TEXT_NODE: 3,2829BACKSPACE_KEY: 8,30ENTER_KEY: 13,31ESCAPE_KEY: 27,32SPACE_KEY: 32,33DELETE_KEY: 4634};/**35* @license Rangy, a cross-browser JavaScript range and selection library36* http://code.google.com/p/rangy/37*38* Copyright 2011, Tim Down39* Licensed under the MIT license.40* Version: 1.2.241* Build date: 13 November 201142*/43window['rangy'] = (function() {444546var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";4748var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",49"commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];5051var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",52"setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",53"extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];5455var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];5657// Subset of TextRange's full set of methods that we're interested in58var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",59"moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];6061/*----------------------------------------------------------------------------------------------------------------*/6263// Trio of functions taken from Peter Michaux's article:64// http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting65function isHostMethod(o, p) {66var t = typeof o[p];67return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";68}6970function isHostObject(o, p) {71return !!(typeof o[p] == OBJECT && o[p]);72}7374function isHostProperty(o, p) {75return typeof o[p] != UNDEFINED;76}7778// Creates a convenience function to save verbose repeated calls to tests functions79function createMultiplePropertyTest(testFunc) {80return function(o, props) {81var i = props.length;82while (i--) {83if (!testFunc(o, props[i])) {84return false;85}86}87return true;88};89}9091// Next trio of functions are a convenience to save verbose repeated calls to previous two functions92var areHostMethods = createMultiplePropertyTest(isHostMethod);93var areHostObjects = createMultiplePropertyTest(isHostObject);94var areHostProperties = createMultiplePropertyTest(isHostProperty);9596function isTextRange(range) {97return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);98}99100var api = {101version: "1.2.2",102initialized: false,103supported: true,104105util: {106isHostMethod: isHostMethod,107isHostObject: isHostObject,108isHostProperty: isHostProperty,109areHostMethods: areHostMethods,110areHostObjects: areHostObjects,111areHostProperties: areHostProperties,112isTextRange: isTextRange113},114115features: {},116117modules: {},118config: {119alertOnWarn: false,120preferTextRange: false121}122};123124function fail(reason) {125window.alert("Rangy not supported in your browser. Reason: " + reason);126api.initialized = true;127api.supported = false;128}129130api.fail = fail;131132function warn(msg) {133var warningMessage = "Rangy warning: " + msg;134if (api.config.alertOnWarn) {135window.alert(warningMessage);136} else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {137window.console.log(warningMessage);138}139}140141api.warn = warn;142143if ({}.hasOwnProperty) {144api.util.extend = function(o, props) {145for (var i in props) {146if (props.hasOwnProperty(i)) {147o[i] = props[i];148}149}150};151} else {152fail("hasOwnProperty not supported");153}154155var initListeners = [];156var moduleInitializers = [];157158// Initialization159function init() {160if (api.initialized) {161return;162}163var testRange;164var implementsDomRange = false, implementsTextRange = false;165166// First, perform basic feature tests167168if (isHostMethod(document, "createRange")) {169testRange = document.createRange();170if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {171implementsDomRange = true;172}173testRange.detach();174}175176var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];177178if (body && isHostMethod(body, "createTextRange")) {179testRange = body.createTextRange();180if (isTextRange(testRange)) {181implementsTextRange = true;182}183}184185if (!implementsDomRange && !implementsTextRange) {186fail("Neither Range nor TextRange are implemented");187}188189api.initialized = true;190api.features = {191implementsDomRange: implementsDomRange,192implementsTextRange: implementsTextRange193};194195// Initialize modules and call init listeners196var allListeners = moduleInitializers.concat(initListeners);197for (var i = 0, len = allListeners.length; i < len; ++i) {198try {199allListeners[i](api);200} catch (ex) {201if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {202window.console.log("Init listener threw an exception. Continuing.", ex);203}204205}206}207}208209// Allow external scripts to initialize this library in case it's loaded after the document has loaded210api.init = init;211212// Execute listener immediately if already initialized213api.addInitListener = function(listener) {214if (api.initialized) {215listener(api);216} else {217initListeners.push(listener);218}219};220221var createMissingNativeApiListeners = [];222223api.addCreateMissingNativeApiListener = function(listener) {224createMissingNativeApiListeners.push(listener);225};226227function createMissingNativeApi(win) {228win = win || window;229init();230231// Notify listeners232for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {233createMissingNativeApiListeners[i](win);234}235}236237api.createMissingNativeApi = createMissingNativeApi;238239/**240* @constructor241*/242function Module(name) {243this.name = name;244this.initialized = false;245this.supported = false;246}247248Module.prototype.fail = function(reason) {249this.initialized = true;250this.supported = false;251252throw new Error("Module '" + this.name + "' failed to load: " + reason);253};254255Module.prototype.warn = function(msg) {256api.warn("Module " + this.name + ": " + msg);257};258259Module.prototype.createError = function(msg) {260return new Error("Error in Rangy " + this.name + " module: " + msg);261};262263api.createModule = function(name, initFunc) {264var module = new Module(name);265api.modules[name] = module;266267moduleInitializers.push(function(api) {268initFunc(api, module);269module.initialized = true;270module.supported = true;271});272};273274api.requireModules = function(modules) {275for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {276moduleName = modules[i];277module = api.modules[moduleName];278if (!module || !(module instanceof Module)) {279throw new Error("Module '" + moduleName + "' not found");280}281if (!module.supported) {282throw new Error("Module '" + moduleName + "' not supported");283}284}285};286287/*----------------------------------------------------------------------------------------------------------------*/288289// Wait for document to load before running tests290291var docReady = false;292293var loadHandler = function(e) {294295if (!docReady) {296docReady = true;297if (!api.initialized) {298init();299}300}301};302303// Test whether we have window and document objects that we will need304if (typeof window == UNDEFINED) {305fail("No window found");306return;307}308if (typeof document == UNDEFINED) {309fail("No document found");310return;311}312313if (isHostMethod(document, "addEventListener")) {314document.addEventListener("DOMContentLoaded", loadHandler, false);315}316317// Add a fallback in case the DOMContentLoaded event isn't supported318if (isHostMethod(window, "addEventListener")) {319window.addEventListener("load", loadHandler, false);320} else if (isHostMethod(window, "attachEvent")) {321window.attachEvent("onload", loadHandler);322} else {323fail("Window does not have required addEventListener or attachEvent method");324}325326return api;327})();328rangy.createModule("DomUtil", function(api, module) {329330var UNDEF = "undefined";331var util = api.util;332333// Perform feature tests334if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {335module.fail("document missing a Node creation method");336}337338if (!util.isHostMethod(document, "getElementsByTagName")) {339module.fail("document missing getElementsByTagName method");340}341342var el = document.createElement("div");343if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||344!util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {345module.fail("Incomplete Element implementation");346}347348// innerHTML is required for Range's createContextualFragment method349if (!util.isHostProperty(el, "innerHTML")) {350module.fail("Element is missing innerHTML property");351}352353var textNode = document.createTextNode("test");354if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||355!util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||356!util.areHostProperties(textNode, ["data"]))) {357module.fail("Incomplete Text Node implementation");358}359360/*----------------------------------------------------------------------------------------------------------------*/361362// Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been363// able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that364// contains just the document as a single element and the value searched for is the document.365var arrayContains = /*Array.prototype.indexOf ?366function(arr, val) {367return arr.indexOf(val) > -1;368}:*/369370function(arr, val) {371var i = arr.length;372while (i--) {373if (arr[i] === val) {374return true;375}376}377return false;378};379380// Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI381function isHtmlNamespace(node) {382var ns;383return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");384}385386function parentElement(node) {387var parent = node.parentNode;388return (parent.nodeType == 1) ? parent : null;389}390391function getNodeIndex(node) {392var i = 0;393while( (node = node.previousSibling) ) {394i++;395}396return i;397}398399function getNodeLength(node) {400var childNodes;401return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);402}403404function getCommonAncestor(node1, node2) {405var ancestors = [], n;406for (n = node1; n; n = n.parentNode) {407ancestors.push(n);408}409410for (n = node2; n; n = n.parentNode) {411if (arrayContains(ancestors, n)) {412return n;413}414}415416return null;417}418419function isAncestorOf(ancestor, descendant, selfIsAncestor) {420var n = selfIsAncestor ? descendant : descendant.parentNode;421while (n) {422if (n === ancestor) {423return true;424} else {425n = n.parentNode;426}427}428return false;429}430431function getClosestAncestorIn(node, ancestor, selfIsAncestor) {432var p, n = selfIsAncestor ? node : node.parentNode;433while (n) {434p = n.parentNode;435if (p === ancestor) {436return n;437}438n = p;439}440return null;441}442443function isCharacterDataNode(node) {444var t = node.nodeType;445return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment446}447448function insertAfter(node, precedingNode) {449var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;450if (nextNode) {451parent.insertBefore(node, nextNode);452} else {453parent.appendChild(node);454}455return node;456}457458// Note that we cannot use splitText() because it is bugridden in IE 9.459function splitDataNode(node, index) {460var newNode = node.cloneNode(false);461newNode.deleteData(0, index);462node.deleteData(index, node.length - index);463insertAfter(newNode, node);464return newNode;465}466467function getDocument(node) {468if (node.nodeType == 9) {469return node;470} else if (typeof node.ownerDocument != UNDEF) {471return node.ownerDocument;472} else if (typeof node.document != UNDEF) {473return node.document;474} else if (node.parentNode) {475return getDocument(node.parentNode);476} else {477throw new Error("getDocument: no document found for node");478}479}480481function getWindow(node) {482var doc = getDocument(node);483if (typeof doc.defaultView != UNDEF) {484return doc.defaultView;485} else if (typeof doc.parentWindow != UNDEF) {486return doc.parentWindow;487} else {488throw new Error("Cannot get a window object for node");489}490}491492function getIframeDocument(iframeEl) {493if (typeof iframeEl.contentDocument != UNDEF) {494return iframeEl.contentDocument;495} else if (typeof iframeEl.contentWindow != UNDEF) {496return iframeEl.contentWindow.document;497} else {498throw new Error("getIframeWindow: No Document object found for iframe element");499}500}501502function getIframeWindow(iframeEl) {503if (typeof iframeEl.contentWindow != UNDEF) {504return iframeEl.contentWindow;505} else if (typeof iframeEl.contentDocument != UNDEF) {506return iframeEl.contentDocument.defaultView;507} else {508throw new Error("getIframeWindow: No Window object found for iframe element");509}510}511512function getBody(doc) {513return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];514}515516function getRootContainer(node) {517var parent;518while ( (parent = node.parentNode) ) {519node = parent;520}521return node;522}523524function comparePoints(nodeA, offsetA, nodeB, offsetB) {525// See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing526var nodeC, root, childA, childB, n;527if (nodeA == nodeB) {528529// Case 1: nodes are the same530return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;531} else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {532533// Case 2: node C (container B or an ancestor) is a child node of A534return offsetA <= getNodeIndex(nodeC) ? -1 : 1;535} else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {536537// Case 3: node C (container A or an ancestor) is a child node of B538return getNodeIndex(nodeC) < offsetB ? -1 : 1;539} else {540541// Case 4: containers are siblings or descendants of siblings542root = getCommonAncestor(nodeA, nodeB);543childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);544childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);545546if (childA === childB) {547// This shouldn't be possible548549throw new Error("comparePoints got to case 4 and childA and childB are the same!");550} else {551n = root.firstChild;552while (n) {553if (n === childA) {554return -1;555} else if (n === childB) {556return 1;557}558n = n.nextSibling;559}560throw new Error("Should not be here!");561}562}563}564565function fragmentFromNodeChildren(node) {566var fragment = getDocument(node).createDocumentFragment(), child;567while ( (child = node.firstChild) ) {568fragment.appendChild(child);569}570return fragment;571}572573function inspectNode(node) {574if (!node) {575return "[No node]";576}577if (isCharacterDataNode(node)) {578return '"' + node.data + '"';579} else if (node.nodeType == 1) {580var idAttr = node.id ? ' id="' + node.id + '"' : "";581return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";582} else {583return node.nodeName;584}585}586587/**588* @constructor589*/590function NodeIterator(root) {591this.root = root;592this._next = root;593}594595NodeIterator.prototype = {596_current: null,597598hasNext: function() {599return !!this._next;600},601602next: function() {603var n = this._current = this._next;604var child, next;605if (this._current) {606child = n.firstChild;607if (child) {608this._next = child;609} else {610next = null;611while ((n !== this.root) && !(next = n.nextSibling)) {612n = n.parentNode;613}614this._next = next;615}616}617return this._current;618},619620detach: function() {621this._current = this._next = this.root = null;622}623};624625function createIterator(root) {626return new NodeIterator(root);627}628629/**630* @constructor631*/632function DomPosition(node, offset) {633this.node = node;634this.offset = offset;635}636637DomPosition.prototype = {638equals: function(pos) {639return this.node === pos.node & this.offset == pos.offset;640},641642inspect: function() {643return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";644}645};646647/**648* @constructor649*/650function DOMException(codeName) {651this.code = this[codeName];652this.codeName = codeName;653this.message = "DOMException: " + this.codeName;654}655656DOMException.prototype = {657INDEX_SIZE_ERR: 1,658HIERARCHY_REQUEST_ERR: 3,659WRONG_DOCUMENT_ERR: 4,660NO_MODIFICATION_ALLOWED_ERR: 7,661NOT_FOUND_ERR: 8,662NOT_SUPPORTED_ERR: 9,663INVALID_STATE_ERR: 11664};665666DOMException.prototype.toString = function() {667return this.message;668};669670api.dom = {671arrayContains: arrayContains,672isHtmlNamespace: isHtmlNamespace,673parentElement: parentElement,674getNodeIndex: getNodeIndex,675getNodeLength: getNodeLength,676getCommonAncestor: getCommonAncestor,677isAncestorOf: isAncestorOf,678getClosestAncestorIn: getClosestAncestorIn,679isCharacterDataNode: isCharacterDataNode,680insertAfter: insertAfter,681splitDataNode: splitDataNode,682getDocument: getDocument,683getWindow: getWindow,684getIframeWindow: getIframeWindow,685getIframeDocument: getIframeDocument,686getBody: getBody,687getRootContainer: getRootContainer,688comparePoints: comparePoints,689inspectNode: inspectNode,690fragmentFromNodeChildren: fragmentFromNodeChildren,691createIterator: createIterator,692DomPosition: DomPosition693};694695api.DOMException = DOMException;696});rangy.createModule("DomRange", function(api, module) {697api.requireModules( ["DomUtil"] );698699700var dom = api.dom;701var DomPosition = dom.DomPosition;702var DOMException = api.DOMException;703704/*----------------------------------------------------------------------------------------------------------------*/705706// Utility functions707708function isNonTextPartiallySelected(node, range) {709return (node.nodeType != 3) &&710(dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));711}712713function getRangeDocument(range) {714return dom.getDocument(range.startContainer);715}716717function dispatchEvent(range, type, args) {718var listeners = range._listeners[type];719if (listeners) {720for (var i = 0, len = listeners.length; i < len; ++i) {721listeners[i].call(range, {target: range, args: args});722}723}724}725726function getBoundaryBeforeNode(node) {727return new DomPosition(node.parentNode, dom.getNodeIndex(node));728}729730function getBoundaryAfterNode(node) {731return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);732}733734function insertNodeAtPosition(node, n, o) {735var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;736if (dom.isCharacterDataNode(n)) {737if (o == n.length) {738dom.insertAfter(node, n);739} else {740n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));741}742} else if (o >= n.childNodes.length) {743n.appendChild(node);744} else {745n.insertBefore(node, n.childNodes[o]);746}747return firstNodeInserted;748}749750function cloneSubtree(iterator) {751var partiallySelected;752for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {753partiallySelected = iterator.isPartiallySelectedSubtree();754755node = node.cloneNode(!partiallySelected);756if (partiallySelected) {757subIterator = iterator.getSubtreeIterator();758node.appendChild(cloneSubtree(subIterator));759subIterator.detach(true);760}761762if (node.nodeType == 10) { // DocumentType763throw new DOMException("HIERARCHY_REQUEST_ERR");764}765frag.appendChild(node);766}767return frag;768}769770function iterateSubtree(rangeIterator, func, iteratorState) {771var it, n;772iteratorState = iteratorState || { stop: false };773for (var node, subRangeIterator; node = rangeIterator.next(); ) {774//log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));775if (rangeIterator.isPartiallySelectedSubtree()) {776// The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the777// node selected by the Range.778if (func(node) === false) {779iteratorState.stop = true;780return;781} else {782subRangeIterator = rangeIterator.getSubtreeIterator();783iterateSubtree(subRangeIterator, func, iteratorState);784subRangeIterator.detach(true);785if (iteratorState.stop) {786return;787}788}789} else {790// The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its791// descendant792it = dom.createIterator(node);793while ( (n = it.next()) ) {794if (func(n) === false) {795iteratorState.stop = true;796return;797}798}799}800}801}802803function deleteSubtree(iterator) {804var subIterator;805while (iterator.next()) {806if (iterator.isPartiallySelectedSubtree()) {807subIterator = iterator.getSubtreeIterator();808deleteSubtree(subIterator);809subIterator.detach(true);810} else {811iterator.remove();812}813}814}815816function extractSubtree(iterator) {817818for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {819820821if (iterator.isPartiallySelectedSubtree()) {822node = node.cloneNode(false);823subIterator = iterator.getSubtreeIterator();824node.appendChild(extractSubtree(subIterator));825subIterator.detach(true);826} else {827iterator.remove();828}829if (node.nodeType == 10) { // DocumentType830throw new DOMException("HIERARCHY_REQUEST_ERR");831}832frag.appendChild(node);833}834return frag;835}836837function getNodesInRange(range, nodeTypes, filter) {838//log.info("getNodesInRange, " + nodeTypes.join(","));839var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;840var filterExists = !!filter;841if (filterNodeTypes) {842regex = new RegExp("^(" + nodeTypes.join("|") + ")$");843}844845var nodes = [];846iterateSubtree(new RangeIterator(range, false), function(node) {847if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {848nodes.push(node);849}850});851return nodes;852}853854function inspect(range) {855var name = (typeof range.getName == "undefined") ? "Range" : range.getName();856return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +857dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";858}859860/*----------------------------------------------------------------------------------------------------------------*/861862// RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)863864/**865* @constructor866*/867function RangeIterator(range, clonePartiallySelectedTextNodes) {868this.range = range;869this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;870871872873if (!range.collapsed) {874this.sc = range.startContainer;875this.so = range.startOffset;876this.ec = range.endContainer;877this.eo = range.endOffset;878var root = range.commonAncestorContainer;879880if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {881this.isSingleCharacterDataNode = true;882this._first = this._last = this._next = this.sc;883} else {884this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?885this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);886this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?887this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);888}889890}891}892893RangeIterator.prototype = {894_current: null,895_next: null,896_first: null,897_last: null,898isSingleCharacterDataNode: false,899900reset: function() {901this._current = null;902this._next = this._first;903},904905hasNext: function() {906return !!this._next;907},908909next: function() {910// Move to next node911var current = this._current = this._next;912if (current) {913this._next = (current !== this._last) ? current.nextSibling : null;914915// Check for partially selected text nodes916if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {917if (current === this.ec) {918919(current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);920}921if (this._current === this.sc) {922923(current = current.cloneNode(true)).deleteData(0, this.so);924}925}926}927928return current;929},930931remove: function() {932var current = this._current, start, end;933934if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {935start = (current === this.sc) ? this.so : 0;936end = (current === this.ec) ? this.eo : current.length;937if (start != end) {938current.deleteData(start, end - start);939}940} else {941if (current.parentNode) {942current.parentNode.removeChild(current);943} else {944945}946}947},948949// Checks if the current node is partially selected950isPartiallySelectedSubtree: function() {951var current = this._current;952return isNonTextPartiallySelected(current, this.range);953},954955getSubtreeIterator: function() {956var subRange;957if (this.isSingleCharacterDataNode) {958subRange = this.range.cloneRange();959subRange.collapse();960} else {961subRange = new Range(getRangeDocument(this.range));962var current = this._current;963var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);964965if (dom.isAncestorOf(current, this.sc, true)) {966startContainer = this.sc;967startOffset = this.so;968}969if (dom.isAncestorOf(current, this.ec, true)) {970endContainer = this.ec;971endOffset = this.eo;972}973974updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);975}976return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);977},978979detach: function(detachRange) {980if (detachRange) {981this.range.detach();982}983this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;984}985};986987/*----------------------------------------------------------------------------------------------------------------*/988989// Exceptions990991/**992* @constructor993*/994function RangeException(codeName) {995this.code = this[codeName];996this.codeName = codeName;997this.message = "RangeException: " + this.codeName;998}9991000RangeException.prototype = {1001BAD_BOUNDARYPOINTS_ERR: 1,1002INVALID_NODE_TYPE_ERR: 21003};10041005RangeException.prototype.toString = function() {1006return this.message;1007};10081009/*----------------------------------------------------------------------------------------------------------------*/10101011/**1012* Currently iterates through all nodes in the range on creation until I think of a decent way to do it1013* TODO: Look into making this a proper iterator, not requiring preloading everything first1014* @constructor1015*/1016function RangeNodeIterator(range, nodeTypes, filter) {1017this.nodes = getNodesInRange(range, nodeTypes, filter);1018this._next = this.nodes[0];1019this._position = 0;1020}10211022RangeNodeIterator.prototype = {1023_current: null,10241025hasNext: function() {1026return !!this._next;1027},10281029next: function() {1030this._current = this._next;1031this._next = this.nodes[ ++this._position ];1032return this._current;1033},10341035detach: function() {1036this._current = this._next = this.nodes = null;1037}1038};10391040var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];1041var rootContainerNodeTypes = [2, 9, 11];1042var readonlyNodeTypes = [5, 6, 10, 12];1043var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];1044var surroundNodeTypes = [1, 3, 4, 5, 7, 8];10451046function createAncestorFinder(nodeTypes) {1047return function(node, selfIsAncestor) {1048var t, n = selfIsAncestor ? node : node.parentNode;1049while (n) {1050t = n.nodeType;1051if (dom.arrayContains(nodeTypes, t)) {1052return n;1053}1054n = n.parentNode;1055}1056return null;1057};1058}10591060var getRootContainer = dom.getRootContainer;1061var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );1062var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);1063var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );10641065function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {1066if (getDocTypeNotationEntityAncestor(node, allowSelf)) {1067throw new RangeException("INVALID_NODE_TYPE_ERR");1068}1069}10701071function assertNotDetached(range) {1072if (!range.startContainer) {1073throw new DOMException("INVALID_STATE_ERR");1074}1075}10761077function assertValidNodeType(node, invalidTypes) {1078if (!dom.arrayContains(invalidTypes, node.nodeType)) {1079throw new RangeException("INVALID_NODE_TYPE_ERR");1080}1081}10821083function assertValidOffset(node, offset) {1084if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {1085throw new DOMException("INDEX_SIZE_ERR");1086}1087}10881089function assertSameDocumentOrFragment(node1, node2) {1090if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {1091throw new DOMException("WRONG_DOCUMENT_ERR");1092}1093}10941095function assertNodeNotReadOnly(node) {1096if (getReadonlyAncestor(node, true)) {1097throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");1098}1099}11001101function assertNode(node, codeName) {1102if (!node) {1103throw new DOMException(codeName);1104}1105}11061107function isOrphan(node) {1108return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);1109}11101111function isValidOffset(node, offset) {1112return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);1113}11141115function assertRangeValid(range) {1116assertNotDetached(range);1117if (isOrphan(range.startContainer) || isOrphan(range.endContainer) ||1118!isValidOffset(range.startContainer, range.startOffset) ||1119!isValidOffset(range.endContainer, range.endOffset)) {1120throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");1121}1122}11231124/*----------------------------------------------------------------------------------------------------------------*/11251126// Test the browser's innerHTML support to decide how to implement createContextualFragment1127var styleEl = document.createElement("style");1128var htmlParsingConforms = false;1129try {1130styleEl.innerHTML = "<b>x</b>";1131htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node1132} catch (e) {1133// IE 6 and 7 throw1134}11351136api.features.htmlParsingConforms = htmlParsingConforms;11371138var createContextualFragment = htmlParsingConforms ?11391140// Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See1141// discussion and base code for this implementation at issue 67.1142// Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface1143// Thanks to Aleks Williams.1144function(fragmentStr) {1145// "Let node the context object's start's node."1146var node = this.startContainer;1147var doc = dom.getDocument(node);11481149// "If the context object's start's node is null, raise an INVALID_STATE_ERR1150// exception and abort these steps."1151if (!node) {1152throw new DOMException("INVALID_STATE_ERR");1153}11541155// "Let element be as follows, depending on node's interface:"1156// Document, Document Fragment: null1157var el = null;11581159// "Element: node"1160if (node.nodeType == 1) {1161el = node;11621163// "Text, Comment: node's parentElement"1164} else if (dom.isCharacterDataNode(node)) {1165el = dom.parentElement(node);1166}11671168// "If either element is null or element's ownerDocument is an HTML document1169// and element's local name is "html" and element's namespace is the HTML1170// namespace"1171if (el === null || (1172el.nodeName == "HTML"1173&& dom.isHtmlNamespace(dom.getDocument(el).documentElement)1174&& dom.isHtmlNamespace(el)1175)) {11761177// "let element be a new Element with "body" as its local name and the HTML1178// namespace as its namespace.""1179el = doc.createElement("body");1180} else {1181el = el.cloneNode(false);1182}11831184// "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."1185// "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."1186// "In either case, the algorithm must be invoked with fragment as the input1187// and element as the context element."1188el.innerHTML = fragmentStr;11891190// "If this raises an exception, then abort these steps. Otherwise, let new1191// children be the nodes returned."11921193// "Let fragment be a new DocumentFragment."1194// "Append all new children to fragment."1195// "Return fragment."1196return dom.fragmentFromNodeChildren(el);1197} :11981199// In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that1200// previous versions of Rangy used (with the exception of using a body element rather than a div)1201function(fragmentStr) {1202assertNotDetached(this);1203var doc = getRangeDocument(this);1204var el = doc.createElement("body");1205el.innerHTML = fragmentStr;12061207return dom.fragmentFromNodeChildren(el);1208};12091210/*----------------------------------------------------------------------------------------------------------------*/12111212var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",1213"commonAncestorContainer"];12141215var s2s = 0, s2e = 1, e2e = 2, e2s = 3;1216var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;12171218function RangePrototype() {}12191220RangePrototype.prototype = {1221attachListener: function(type, listener) {1222this._listeners[type].push(listener);1223},12241225compareBoundaryPoints: function(how, range) {1226assertRangeValid(this);1227assertSameDocumentOrFragment(this.startContainer, range.startContainer);12281229var nodeA, offsetA, nodeB, offsetB;1230var prefixA = (how == e2s || how == s2s) ? "start" : "end";1231var prefixB = (how == s2e || how == s2s) ? "start" : "end";1232nodeA = this[prefixA + "Container"];1233offsetA = this[prefixA + "Offset"];1234nodeB = range[prefixB + "Container"];1235offsetB = range[prefixB + "Offset"];1236return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);1237},12381239insertNode: function(node) {1240assertRangeValid(this);1241assertValidNodeType(node, insertableNodeTypes);1242assertNodeNotReadOnly(this.startContainer);12431244if (dom.isAncestorOf(node, this.startContainer, true)) {1245throw new DOMException("HIERARCHY_REQUEST_ERR");1246}12471248// No check for whether the container of the start of the Range is of a type that does not allow1249// children of the type of node: the browser's DOM implementation should do this for us when we attempt1250// to add the node12511252var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);1253this.setStartBefore(firstNodeInserted);1254},12551256cloneContents: function() {1257assertRangeValid(this);12581259var clone, frag;1260if (this.collapsed) {1261return getRangeDocument(this).createDocumentFragment();1262} else {1263if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {1264clone = this.startContainer.cloneNode(true);1265clone.data = clone.data.slice(this.startOffset, this.endOffset);1266frag = getRangeDocument(this).createDocumentFragment();1267frag.appendChild(clone);1268return frag;1269} else {1270var iterator = new RangeIterator(this, true);1271clone = cloneSubtree(iterator);1272iterator.detach();1273}1274return clone;1275}1276},12771278canSurroundContents: function() {1279assertRangeValid(this);1280assertNodeNotReadOnly(this.startContainer);1281assertNodeNotReadOnly(this.endContainer);12821283// Check if the contents can be surrounded. Specifically, this means whether the range partially selects1284// no non-text nodes.1285var iterator = new RangeIterator(this, true);1286var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||1287(iterator._last && isNonTextPartiallySelected(iterator._last, this)));1288iterator.detach();1289return !boundariesInvalid;1290},12911292surroundContents: function(node) {1293assertValidNodeType(node, surroundNodeTypes);12941295if (!this.canSurroundContents()) {1296throw new RangeException("BAD_BOUNDARYPOINTS_ERR");1297}12981299// Extract the contents1300var content = this.extractContents();13011302// Clear the children of the node1303if (node.hasChildNodes()) {1304while (node.lastChild) {1305node.removeChild(node.lastChild);1306}1307}13081309// Insert the new node and add the extracted contents1310insertNodeAtPosition(node, this.startContainer, this.startOffset);1311node.appendChild(content);13121313this.selectNode(node);1314},13151316cloneRange: function() {1317assertRangeValid(this);1318var range = new Range(getRangeDocument(this));1319var i = rangeProperties.length, prop;1320while (i--) {1321prop = rangeProperties[i];1322range[prop] = this[prop];1323}1324return range;1325},13261327toString: function() {1328assertRangeValid(this);1329var sc = this.startContainer;1330if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {1331return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";1332} else {1333var textBits = [], iterator = new RangeIterator(this, true);13341335iterateSubtree(iterator, function(node) {1336// Accept only text or CDATA nodes, not comments13371338if (node.nodeType == 3 || node.nodeType == 4) {1339textBits.push(node.data);1340}1341});1342iterator.detach();1343return textBits.join("");1344}1345},13461347// The methods below are all non-standard. The following batch were introduced by Mozilla but have since1348// been removed from Mozilla.13491350compareNode: function(node) {1351assertRangeValid(this);13521353var parent = node.parentNode;1354var nodeIndex = dom.getNodeIndex(node);13551356if (!parent) {1357throw new DOMException("NOT_FOUND_ERR");1358}13591360var startComparison = this.comparePoint(parent, nodeIndex),1361endComparison = this.comparePoint(parent, nodeIndex + 1);13621363if (startComparison < 0) { // Node starts before1364return (endComparison > 0) ? n_b_a : n_b;1365} else {1366return (endComparison > 0) ? n_a : n_i;1367}1368},13691370comparePoint: function(node, offset) {1371assertRangeValid(this);1372assertNode(node, "HIERARCHY_REQUEST_ERR");1373assertSameDocumentOrFragment(node, this.startContainer);13741375if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {1376return -1;1377} else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {1378return 1;1379}1380return 0;1381},13821383createContextualFragment: createContextualFragment,13841385toHtml: function() {1386assertRangeValid(this);1387var container = getRangeDocument(this).createElement("div");1388container.appendChild(this.cloneContents());1389return container.innerHTML;1390},13911392// touchingIsIntersecting determines whether this method considers a node that borders a range intersects1393// with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)1394intersectsNode: function(node, touchingIsIntersecting) {1395assertRangeValid(this);1396assertNode(node, "NOT_FOUND_ERR");1397if (dom.getDocument(node) !== getRangeDocument(this)) {1398return false;1399}14001401var parent = node.parentNode, offset = dom.getNodeIndex(node);1402assertNode(parent, "NOT_FOUND_ERR");14031404var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),1405endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);14061407return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;1408},140914101411isPointInRange: function(node, offset) {1412assertRangeValid(this);1413assertNode(node, "HIERARCHY_REQUEST_ERR");1414assertSameDocumentOrFragment(node, this.startContainer);14151416return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&1417(dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);1418},14191420// The methods below are non-standard and invented by me.14211422// Sharing a boundary start-to-end or end-to-start does not count as intersection.1423intersectsRange: function(range, touchingIsIntersecting) {1424assertRangeValid(this);14251426if (getRangeDocument(range) != getRangeDocument(this)) {1427throw new DOMException("WRONG_DOCUMENT_ERR");1428}14291430var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),1431endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);14321433return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;1434},14351436intersection: function(range) {1437if (this.intersectsRange(range)) {1438var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),1439endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);14401441var intersectionRange = this.cloneRange();14421443if (startComparison == -1) {1444intersectionRange.setStart(range.startContainer, range.startOffset);1445}1446if (endComparison == 1) {1447intersectionRange.setEnd(range.endContainer, range.endOffset);1448}1449return intersectionRange;1450}1451return null;1452},14531454union: function(range) {1455if (this.intersectsRange(range, true)) {1456var unionRange = this.cloneRange();1457if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {1458unionRange.setStart(range.startContainer, range.startOffset);1459}1460if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {1461unionRange.setEnd(range.endContainer, range.endOffset);1462}1463return unionRange;1464} else {1465throw new RangeException("Ranges do not intersect");1466}1467},14681469containsNode: function(node, allowPartial) {1470if (allowPartial) {1471return this.intersectsNode(node, false);1472} else {1473return this.compareNode(node) == n_i;1474}1475},14761477containsNodeContents: function(node) {1478return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0;1479},14801481containsRange: function(range) {1482return this.intersection(range).equals(range);1483},14841485containsNodeText: function(node) {1486var nodeRange = this.cloneRange();1487nodeRange.selectNode(node);1488var textNodes = nodeRange.getNodes([3]);1489if (textNodes.length > 0) {1490nodeRange.setStart(textNodes[0], 0);1491var lastTextNode = textNodes.pop();1492nodeRange.setEnd(lastTextNode, lastTextNode.length);1493var contains = this.containsRange(nodeRange);1494nodeRange.detach();1495return contains;1496} else {1497return this.containsNodeContents(node);1498}1499},15001501createNodeIterator: function(nodeTypes, filter) {1502assertRangeValid(this);1503return new RangeNodeIterator(this, nodeTypes, filter);1504},15051506getNodes: function(nodeTypes, filter) {1507assertRangeValid(this);1508return getNodesInRange(this, nodeTypes, filter);1509},15101511getDocument: function() {1512return getRangeDocument(this);1513},15141515collapseBefore: function(node) {1516assertNotDetached(this);15171518this.setEndBefore(node);1519this.collapse(false);1520},15211522collapseAfter: function(node) {1523assertNotDetached(this);15241525this.setStartAfter(node);1526this.collapse(true);1527},15281529getName: function() {1530return "DomRange";1531},15321533equals: function(range) {1534return Range.rangesEqual(this, range);1535},15361537inspect: function() {1538return inspect(this);1539}1540};15411542function copyComparisonConstantsToObject(obj) {1543obj.START_TO_START = s2s;1544obj.START_TO_END = s2e;1545obj.END_TO_END = e2e;1546obj.END_TO_START = e2s;15471548obj.NODE_BEFORE = n_b;1549obj.NODE_AFTER = n_a;1550obj.NODE_BEFORE_AND_AFTER = n_b_a;1551obj.NODE_INSIDE = n_i;1552}15531554function copyComparisonConstants(constructor) {1555copyComparisonConstantsToObject(constructor);1556copyComparisonConstantsToObject(constructor.prototype);1557}15581559function createRangeContentRemover(remover, boundaryUpdater) {1560return function() {1561assertRangeValid(this);15621563var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;15641565var iterator = new RangeIterator(this, true);15661567// Work out where to position the range after content removal1568var node, boundary;1569if (sc !== root) {1570node = dom.getClosestAncestorIn(sc, root, true);1571boundary = getBoundaryAfterNode(node);1572sc = boundary.node;1573so = boundary.offset;1574}15751576// Check none of the range is read-only1577iterateSubtree(iterator, assertNodeNotReadOnly);15781579iterator.reset();15801581// Remove the content1582var returnValue = remover(iterator);1583iterator.detach();15841585// Move to the new position1586boundaryUpdater(this, sc, so, sc, so);15871588return returnValue;1589};1590}15911592function createPrototypeRange(constructor, boundaryUpdater, detacher) {1593function createBeforeAfterNodeSetter(isBefore, isStart) {1594return function(node) {1595assertNotDetached(this);1596assertValidNodeType(node, beforeAfterNodeTypes);1597assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);15981599var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);1600(isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);1601};1602}16031604function setRangeStart(range, node, offset) {1605var ec = range.endContainer, eo = range.endOffset;1606if (node !== range.startContainer || offset !== range.startOffset) {1607// Check the root containers of the range and the new boundary, and also check whether the new boundary1608// is after the current end. In either case, collapse the range to the new position1609if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {1610ec = node;1611eo = offset;1612}1613boundaryUpdater(range, node, offset, ec, eo);1614}1615}16161617function setRangeEnd(range, node, offset) {1618var sc = range.startContainer, so = range.startOffset;1619if (node !== range.endContainer || offset !== range.endOffset) {1620// Check the root containers of the range and the new boundary, and also check whether the new boundary1621// is after the current end. In either case, collapse the range to the new position1622if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {1623sc = node;1624so = offset;1625}1626boundaryUpdater(range, sc, so, node, offset);1627}1628}16291630function setRangeStartAndEnd(range, node, offset) {1631if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {1632boundaryUpdater(range, node, offset, node, offset);1633}1634}16351636constructor.prototype = new RangePrototype();16371638api.util.extend(constructor.prototype, {1639setStart: function(node, offset) {1640assertNotDetached(this);1641assertNoDocTypeNotationEntityAncestor(node, true);1642assertValidOffset(node, offset);16431644setRangeStart(this, node, offset);1645},16461647setEnd: function(node, offset) {1648assertNotDetached(this);1649assertNoDocTypeNotationEntityAncestor(node, true);1650assertValidOffset(node, offset);16511652setRangeEnd(this, node, offset);1653},16541655setStartBefore: createBeforeAfterNodeSetter(true, true),1656setStartAfter: createBeforeAfterNodeSetter(false, true),1657setEndBefore: createBeforeAfterNodeSetter(true, false),1658setEndAfter: createBeforeAfterNodeSetter(false, false),16591660collapse: function(isStart) {1661assertRangeValid(this);1662if (isStart) {1663boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);1664} else {1665boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);1666}1667},16681669selectNodeContents: function(node) {1670// This doesn't seem well specified: the spec talks only about selecting the node's contents, which1671// could be taken to mean only its children. However, browsers implement this the same as selectNode for1672// text nodes, so I shall do likewise1673assertNotDetached(this);1674assertNoDocTypeNotationEntityAncestor(node, true);16751676boundaryUpdater(this, node, 0, node, dom.getNodeLength(node));1677},16781679selectNode: function(node) {1680assertNotDetached(this);1681assertNoDocTypeNotationEntityAncestor(node, false);1682assertValidNodeType(node, beforeAfterNodeTypes);16831684var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);1685boundaryUpdater(this, start.node, start.offset, end.node, end.offset);1686},16871688extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),16891690deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),16911692canSurroundContents: function() {1693assertRangeValid(this);1694assertNodeNotReadOnly(this.startContainer);1695assertNodeNotReadOnly(this.endContainer);16961697// Check if the contents can be surrounded. Specifically, this means whether the range partially selects1698// no non-text nodes.1699var iterator = new RangeIterator(this, true);1700var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||1701(iterator._last && isNonTextPartiallySelected(iterator._last, this)));1702iterator.detach();1703return !boundariesInvalid;1704},17051706detach: function() {1707detacher(this);1708},17091710splitBoundaries: function() {1711assertRangeValid(this);171217131714var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;1715var startEndSame = (sc === ec);17161717if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {1718dom.splitDataNode(ec, eo);17191720}17211722if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {17231724sc = dom.splitDataNode(sc, so);1725if (startEndSame) {1726eo -= so;1727ec = sc;1728} else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {1729eo++;1730}1731so = 0;17321733}1734boundaryUpdater(this, sc, so, ec, eo);1735},17361737normalizeBoundaries: function() {1738assertRangeValid(this);17391740var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;17411742var mergeForward = function(node) {1743var sibling = node.nextSibling;1744if (sibling && sibling.nodeType == node.nodeType) {1745ec = node;1746eo = node.length;1747node.appendData(sibling.data);1748sibling.parentNode.removeChild(sibling);1749}1750};17511752var mergeBackward = function(node) {1753var sibling = node.previousSibling;1754if (sibling && sibling.nodeType == node.nodeType) {1755sc = node;1756var nodeLength = node.length;1757so = sibling.length;1758node.insertData(0, sibling.data);1759sibling.parentNode.removeChild(sibling);1760if (sc == ec) {1761eo += so;1762ec = sc;1763} else if (ec == node.parentNode) {1764var nodeIndex = dom.getNodeIndex(node);1765if (eo == nodeIndex) {1766ec = node;1767eo = nodeLength;1768} else if (eo > nodeIndex) {1769eo--;1770}1771}1772}1773};17741775var normalizeStart = true;17761777if (dom.isCharacterDataNode(ec)) {1778if (ec.length == eo) {1779mergeForward(ec);1780}1781} else {1782if (eo > 0) {1783var endNode = ec.childNodes[eo - 1];1784if (endNode && dom.isCharacterDataNode(endNode)) {1785mergeForward(endNode);1786}1787}1788normalizeStart = !this.collapsed;1789}17901791if (normalizeStart) {1792if (dom.isCharacterDataNode(sc)) {1793if (so == 0) {1794mergeBackward(sc);1795}1796} else {1797if (so < sc.childNodes.length) {1798var startNode = sc.childNodes[so];1799if (startNode && dom.isCharacterDataNode(startNode)) {1800mergeBackward(startNode);1801}1802}1803}1804} else {1805sc = ec;1806so = eo;1807}18081809boundaryUpdater(this, sc, so, ec, eo);1810},18111812collapseToPoint: function(node, offset) {1813assertNotDetached(this);18141815assertNoDocTypeNotationEntityAncestor(node, true);1816assertValidOffset(node, offset);18171818setRangeStartAndEnd(this, node, offset);1819}1820});18211822copyComparisonConstants(constructor);1823}18241825/*----------------------------------------------------------------------------------------------------------------*/18261827// Updates commonAncestorContainer and collapsed after boundary change1828function updateCollapsedAndCommonAncestor(range) {1829range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);1830range.commonAncestorContainer = range.collapsed ?1831range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);1832}18331834function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {1835var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);1836var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);18371838range.startContainer = startContainer;1839range.startOffset = startOffset;1840range.endContainer = endContainer;1841range.endOffset = endOffset;18421843updateCollapsedAndCommonAncestor(range);1844dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});1845}18461847function detach(range) {1848assertNotDetached(range);1849range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;1850range.collapsed = range.commonAncestorContainer = null;1851dispatchEvent(range, "detach", null);1852range._listeners = null;1853}18541855/**1856* @constructor1857*/1858function Range(doc) {1859this.startContainer = doc;1860this.startOffset = 0;1861this.endContainer = doc;1862this.endOffset = 0;1863this._listeners = {1864boundarychange: [],1865detach: []1866};1867updateCollapsedAndCommonAncestor(this);1868}18691870createPrototypeRange(Range, updateBoundaries, detach);18711872api.rangePrototype = RangePrototype.prototype;18731874Range.rangeProperties = rangeProperties;1875Range.RangeIterator = RangeIterator;1876Range.copyComparisonConstants = copyComparisonConstants;1877Range.createPrototypeRange = createPrototypeRange;1878Range.inspect = inspect;1879Range.getRangeDocument = getRangeDocument;1880Range.rangesEqual = function(r1, r2) {1881return r1.startContainer === r2.startContainer &&1882r1.startOffset === r2.startOffset &&1883r1.endContainer === r2.endContainer &&1884r1.endOffset === r2.endOffset;1885};18861887api.DomRange = Range;1888api.RangeException = RangeException;1889});rangy.createModule("WrappedRange", function(api, module) {1890api.requireModules( ["DomUtil", "DomRange"] );18911892/**1893* @constructor1894*/1895var WrappedRange;1896var dom = api.dom;1897var DomPosition = dom.DomPosition;1898var DomRange = api.DomRange;1899190019011902/*----------------------------------------------------------------------------------------------------------------*/19031904/*1905This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()1906method. For example, in the following (where pipes denote the selection boundaries):19071908<ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>19091910var range = document.selection.createRange();1911alert(range.parentElement().id); // Should alert "ul" but alerts "b"19121913This method returns the common ancestor node of the following:1914- the parentElement() of the textRange1915- the parentElement() of the textRange after calling collapse(true)1916- the parentElement() of the textRange after calling collapse(false)1917*/1918function getTextRangeContainerElement(textRange) {1919var parentEl = textRange.parentElement();19201921var range = textRange.duplicate();1922range.collapse(true);1923var startEl = range.parentElement();1924range = textRange.duplicate();1925range.collapse(false);1926var endEl = range.parentElement();1927var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);19281929return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);1930}19311932function textRangeIsCollapsed(textRange) {1933return textRange.compareEndPoints("StartToEnd", textRange) == 0;1934}19351936// Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as1937// an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has1938// grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling1939// for inputs and images, plus optimizations.1940function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {1941var workingRange = textRange.duplicate();19421943workingRange.collapse(isStart);1944var containerElement = workingRange.parentElement();19451946// Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so1947// check for that1948// TODO: Find out when. Workaround for wholeRangeContainerElement may break this1949if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {1950containerElement = wholeRangeContainerElement;19511952}1953195419551956// Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and1957// similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx1958if (!containerElement.canHaveHTML) {1959return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));1960}19611962var workingNode = dom.getDocument(containerElement).createElement("span");1963var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";1964var previousNode, nextNode, boundaryPosition, boundaryNode;19651966// Move the working range through the container's children, starting at the end and working backwards, until the1967// working range reaches or goes past the boundary we're interested in1968do {1969containerElement.insertBefore(workingNode, workingNode.previousSibling);1970workingRange.moveToElementText(workingNode);1971} while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&1972workingNode.previousSibling);19731974// We've now reached or gone past the boundary of the text range we're interested in1975// so have identified the node we want1976boundaryNode = workingNode.nextSibling;19771978if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {1979// This is a character data node (text, comment, cdata). The working range is collapsed at the start of the1980// node containing the text range's boundary, so we move the end of the working range to the boundary point1981// and measure the length of its text to get the boundary's offset within the node.1982workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);198319841985var offset;19861987if (/[\r\n]/.test(boundaryNode.data)) {1988/*1989For the particular case of a boundary within a text node containing line breaks (within a <pre> element,1990for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:19911992- Each line break is represented as \r in the text node's data/nodeValue properties1993- Each line break is represented as \r\n in the TextRange's 'text' property1994- The 'text' property of the TextRange does not contain trailing line breaks19951996To get round the problem presented by the final fact above, we can use the fact that TextRange's1997moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily1998the same as the number of characters it was instructed to move. The simplest approach is to use this to1999store the characters moved when moving both the start and end of the range to the start of the document2000body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).2001However, this is extremely slow when the document is large and the range is near the end of it. Clearly2002doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same2003problem.20042005Another approach that works is to use moveStart() to move the start boundary of the range up to the end2006boundary one character at a time and incrementing a counter with the value returned by the moveStart()2007call. However, the check for whether the start boundary has reached the end boundary is expensive, so2008this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of2009the range within the document).20102011The method below is a hybrid of the two methods above. It uses the fact that a string containing the2012TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the2013text of the TextRange, so the start of the range is moved that length initially and then a character at2014a time to make up for any trailing line breaks not contained in the 'text' property. This has good2015performance in most situations compared to the previous two methods.2016*/2017var tempRange = workingRange.duplicate();2018var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;20192020offset = tempRange.moveStart("character", rangeLength);2021while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {2022offset++;2023tempRange.moveStart("character", 1);2024}2025} else {2026offset = workingRange.text.length;2027}2028boundaryPosition = new DomPosition(boundaryNode, offset);2029} else {203020312032// If the boundary immediately follows a character data node and this is the end boundary, we should favour2033// a position within that, and likewise for a start boundary preceding a character data node2034previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;2035nextNode = (isCollapsed || isStart) && workingNode.nextSibling;2036203720382039if (nextNode && dom.isCharacterDataNode(nextNode)) {2040boundaryPosition = new DomPosition(nextNode, 0);2041} else if (previousNode && dom.isCharacterDataNode(previousNode)) {2042boundaryPosition = new DomPosition(previousNode, previousNode.length);2043} else {2044boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));2045}2046}20472048// Clean up2049workingNode.parentNode.removeChild(workingNode);20502051return boundaryPosition;2052}20532054// Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.2055// This function started out as an optimized version of code found in Tim Cameron Ryan's IERange2056// (http://code.google.com/p/ierange/)2057function createBoundaryTextRange(boundaryPosition, isStart) {2058var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;2059var doc = dom.getDocument(boundaryPosition.node);2060var workingNode, childNodes, workingRange = doc.body.createTextRange();2061var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);20622063if (nodeIsDataNode) {2064boundaryNode = boundaryPosition.node;2065boundaryParent = boundaryNode.parentNode;2066} else {2067childNodes = boundaryPosition.node.childNodes;2068boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;2069boundaryParent = boundaryPosition.node;2070}20712072// Position the range immediately before the node containing the boundary2073workingNode = doc.createElement("span");20742075// Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the2076// element rather than immediately before or after it, which is what we want2077workingNode.innerHTML = "&#feff;";20782079// insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report2080// for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=122081if (boundaryNode) {2082boundaryParent.insertBefore(workingNode, boundaryNode);2083} else {2084boundaryParent.appendChild(workingNode);2085}20862087workingRange.moveToElementText(workingNode);2088workingRange.collapse(!isStart);20892090// Clean up2091boundaryParent.removeChild(workingNode);20922093// Move the working range to the text offset, if required2094if (nodeIsDataNode) {2095workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);2096}20972098return workingRange;2099}21002101/*----------------------------------------------------------------------------------------------------------------*/21022103if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {2104// This is a wrapper around the browser's native DOM Range. It has two aims:2105// - Provide workarounds for specific browser bugs2106// - provide convenient extensions, which are inherited from Rangy's DomRange21072108(function() {2109var rangeProto;2110var rangeProperties = DomRange.rangeProperties;2111var canSetRangeStartAfterEnd;21122113function updateRangeProperties(range) {2114var i = rangeProperties.length, prop;2115while (i--) {2116prop = rangeProperties[i];2117range[prop] = range.nativeRange[prop];2118}2119}21202121function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {2122var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);2123var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);21242125// Always set both boundaries for the benefit of IE9 (see issue 35)2126if (startMoved || endMoved) {2127range.setEnd(endContainer, endOffset);2128range.setStart(startContainer, startOffset);2129}2130}21312132function detach(range) {2133range.nativeRange.detach();2134range.detached = true;2135var i = rangeProperties.length, prop;2136while (i--) {2137prop = rangeProperties[i];2138range[prop] = null;2139}2140}21412142var createBeforeAfterNodeSetter;21432144WrappedRange = function(range) {2145if (!range) {2146throw new Error("Range must be specified");2147}2148this.nativeRange = range;2149updateRangeProperties(this);2150};21512152DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);21532154rangeProto = WrappedRange.prototype;21552156rangeProto.selectNode = function(node) {2157this.nativeRange.selectNode(node);2158updateRangeProperties(this);2159};21602161rangeProto.deleteContents = function() {2162this.nativeRange.deleteContents();2163updateRangeProperties(this);2164};21652166rangeProto.extractContents = function() {2167var frag = this.nativeRange.extractContents();2168updateRangeProperties(this);2169return frag;2170};21712172rangeProto.cloneContents = function() {2173return this.nativeRange.cloneContents();2174};21752176// TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still2177// present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for2178// insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of2179// insertNode, which works but is almost certainly slower than the native implementation.2180/*2181rangeProto.insertNode = function(node) {2182this.nativeRange.insertNode(node);2183updateRangeProperties(this);2184};2185*/21862187rangeProto.surroundContents = function(node) {2188this.nativeRange.surroundContents(node);2189updateRangeProperties(this);2190};21912192rangeProto.collapse = function(isStart) {2193this.nativeRange.collapse(isStart);2194updateRangeProperties(this);2195};21962197rangeProto.cloneRange = function() {2198return new WrappedRange(this.nativeRange.cloneRange());2199};22002201rangeProto.refresh = function() {2202updateRangeProperties(this);2203};22042205rangeProto.toString = function() {2206return this.nativeRange.toString();2207};22082209// Create test range and node for feature detection22102211var testTextNode = document.createTextNode("test");2212dom.getBody(document).appendChild(testTextNode);2213var range = document.createRange();22142215/*--------------------------------------------------------------------------------------------------------*/22162217// Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and2218// correct for it22192220range.setStart(testTextNode, 0);2221range.setEnd(testTextNode, 0);22222223try {2224range.setStart(testTextNode, 1);2225canSetRangeStartAfterEnd = true;22262227rangeProto.setStart = function(node, offset) {2228this.nativeRange.setStart(node, offset);2229updateRangeProperties(this);2230};22312232rangeProto.setEnd = function(node, offset) {2233this.nativeRange.setEnd(node, offset);2234updateRangeProperties(this);2235};22362237createBeforeAfterNodeSetter = function(name) {2238return function(node) {2239this.nativeRange[name](node);2240updateRangeProperties(this);2241};2242};22432244} catch(ex) {224522462247canSetRangeStartAfterEnd = false;22482249rangeProto.setStart = function(node, offset) {2250try {2251this.nativeRange.setStart(node, offset);2252} catch (ex) {2253this.nativeRange.setEnd(node, offset);2254this.nativeRange.setStart(node, offset);2255}2256updateRangeProperties(this);2257};22582259rangeProto.setEnd = function(node, offset) {2260try {2261this.nativeRange.setEnd(node, offset);2262} catch (ex) {2263this.nativeRange.setStart(node, offset);2264this.nativeRange.setEnd(node, offset);2265}2266updateRangeProperties(this);2267};22682269createBeforeAfterNodeSetter = function(name, oppositeName) {2270return function(node) {2271try {2272this.nativeRange[name](node);2273} catch (ex) {2274this.nativeRange[oppositeName](node);2275this.nativeRange[name](node);2276}2277updateRangeProperties(this);2278};2279};2280}22812282rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");2283rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");2284rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");2285rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");22862287/*--------------------------------------------------------------------------------------------------------*/22882289// Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to2290// the 0th character of the text node2291range.selectNodeContents(testTextNode);2292if (range.startContainer == testTextNode && range.endContainer == testTextNode &&2293range.startOffset == 0 && range.endOffset == testTextNode.length) {2294rangeProto.selectNodeContents = function(node) {2295this.nativeRange.selectNodeContents(node);2296updateRangeProperties(this);2297};2298} else {2299rangeProto.selectNodeContents = function(node) {2300this.setStart(node, 0);2301this.setEnd(node, DomRange.getEndOffset(node));2302};2303}23042305/*--------------------------------------------------------------------------------------------------------*/23062307// Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants2308// START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=2073823092310range.selectNodeContents(testTextNode);2311range.setEnd(testTextNode, 3);23122313var range2 = document.createRange();2314range2.selectNodeContents(testTextNode);2315range2.setEnd(testTextNode, 4);2316range2.setStart(testTextNode, 2);23172318if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &2319range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {2320// This is the wrong way round, so correct for it232123222323rangeProto.compareBoundaryPoints = function(type, range) {2324range = range.nativeRange || range;2325if (type == range.START_TO_END) {2326type = range.END_TO_START;2327} else if (type == range.END_TO_START) {2328type = range.START_TO_END;2329}2330return this.nativeRange.compareBoundaryPoints(type, range);2331};2332} else {2333rangeProto.compareBoundaryPoints = function(type, range) {2334return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);2335};2336}23372338/*--------------------------------------------------------------------------------------------------------*/23392340// Test for existence of createContextualFragment and delegate to it if it exists2341if (api.util.isHostMethod(range, "createContextualFragment")) {2342rangeProto.createContextualFragment = function(fragmentStr) {2343return this.nativeRange.createContextualFragment(fragmentStr);2344};2345}23462347/*--------------------------------------------------------------------------------------------------------*/23482349// Clean up2350dom.getBody(document).removeChild(testTextNode);2351range.detach();2352range2.detach();2353})();23542355api.createNativeRange = function(doc) {2356doc = doc || document;2357return doc.createRange();2358};2359} else if (api.features.implementsTextRange) {2360// This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a2361// prototype23622363WrappedRange = function(textRange) {2364this.textRange = textRange;2365this.refresh();2366};23672368WrappedRange.prototype = new DomRange(document);23692370WrappedRange.prototype.refresh = function() {2371var start, end;23722373// TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.2374var rangeContainerElement = getTextRangeContainerElement(this.textRange);23752376if (textRangeIsCollapsed(this.textRange)) {2377end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);2378} else {23792380start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);2381end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);2382}23832384this.setStart(start.node, start.offset);2385this.setEnd(end.node, end.offset);2386};23872388DomRange.copyComparisonConstants(WrappedRange);23892390// Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work2391var globalObj = (function() { return this; })();2392if (typeof globalObj.Range == "undefined") {2393globalObj.Range = WrappedRange;2394}23952396api.createNativeRange = function(doc) {2397doc = doc || document;2398return doc.body.createTextRange();2399};2400}24012402if (api.features.implementsTextRange) {2403WrappedRange.rangeToTextRange = function(range) {2404if (range.collapsed) {2405var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);2406240724082409return tr;24102411//return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);2412} else {2413var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);2414var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);2415var textRange = dom.getDocument(range.startContainer).body.createTextRange();2416textRange.setEndPoint("StartToStart", startRange);2417textRange.setEndPoint("EndToEnd", endRange);2418return textRange;2419}2420};2421}24222423WrappedRange.prototype.getName = function() {2424return "WrappedRange";2425};24262427api.WrappedRange = WrappedRange;24282429api.createRange = function(doc) {2430doc = doc || document;2431return new WrappedRange(api.createNativeRange(doc));2432};24332434api.createRangyRange = function(doc) {2435doc = doc || document;2436return new DomRange(doc);2437};24382439api.createIframeRange = function(iframeEl) {2440return api.createRange(dom.getIframeDocument(iframeEl));2441};24422443api.createIframeRangyRange = function(iframeEl) {2444return api.createRangyRange(dom.getIframeDocument(iframeEl));2445};24462447api.addCreateMissingNativeApiListener(function(win) {2448var doc = win.document;2449if (typeof doc.createRange == "undefined") {2450doc.createRange = function() {2451return api.createRange(this);2452};2453}2454doc = win = null;2455});2456});rangy.createModule("WrappedSelection", function(api, module) {2457// This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range2458// spec (http://html5.org/specs/dom-range.html)24592460api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );24612462api.config.checkSelectionRanges = true;24632464var BOOLEAN = "boolean",2465windowPropertyName = "_rangySelection",2466dom = api.dom,2467util = api.util,2468DomRange = api.DomRange,2469WrappedRange = api.WrappedRange,2470DOMException = api.DOMException,2471DomPosition = dom.DomPosition,2472getSelection,2473selectionIsCollapsed,2474CONTROL = "Control";2475247624772478function getWinSelection(winParam) {2479return (winParam || window).getSelection();2480}24812482function getDocSelection(winParam) {2483return (winParam || window).document.selection;2484}24852486// Test for the Range/TextRange and Selection features required2487// Test for ability to retrieve selection2488var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),2489implementsDocSelection = api.util.isHostObject(document, "selection");24902491var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);24922493if (useDocumentSelection) {2494getSelection = getDocSelection;2495api.isSelectionValid = function(winParam) {2496var doc = (winParam || window).document, nativeSel = doc.selection;24972498// Check whether the selection TextRange is actually contained within the correct document2499return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);2500};2501} else if (implementsWinGetSelection) {2502getSelection = getWinSelection;2503api.isSelectionValid = function() {2504return true;2505};2506} else {2507module.fail("Neither document.selection or window.getSelection() detected.");2508}25092510api.getNativeSelection = getSelection;25112512var testSelection = getSelection();2513var testRange = api.createNativeRange(document);2514var body = dom.getBody(document);25152516// Obtaining a range from a selection2517var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&2518util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));2519api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;25202521// Test for existence of native selection extend() method2522var selectionHasExtend = util.isHostMethod(testSelection, "extend");2523api.features.selectionHasExtend = selectionHasExtend;25242525// Test if rangeCount exists2526var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");2527api.features.selectionHasRangeCount = selectionHasRangeCount;25282529var selectionSupportsMultipleRanges = false;2530var collapsedNonEditableSelectionsSupported = true;25312532if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&2533typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {25342535(function() {2536var iframe = document.createElement("iframe");2537body.appendChild(iframe);25382539var iframeDoc = dom.getIframeDocument(iframe);2540iframeDoc.open();2541iframeDoc.write("<html><head></head><body>12</body></html>");2542iframeDoc.close();25432544var sel = dom.getIframeWindow(iframe).getSelection();2545var docEl = iframeDoc.documentElement;2546var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;25472548// Test whether the native selection will allow a collapsed selection within a non-editable element2549var r1 = iframeDoc.createRange();2550r1.setStart(textNode, 1);2551r1.collapse(true);2552sel.addRange(r1);2553collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);2554sel.removeAllRanges();25552556// Test whether the native selection is capable of supporting multiple ranges2557var r2 = r1.cloneRange();2558r1.setStart(textNode, 0);2559r2.setEnd(textNode, 2);2560sel.addRange(r1);2561sel.addRange(r2);25622563selectionSupportsMultipleRanges = (sel.rangeCount == 2);25642565// Clean up2566r1.detach();2567r2.detach();25682569body.removeChild(iframe);2570})();2571}25722573api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;2574api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;25752576// ControlRanges2577var implementsControlRange = false, testControlRange;25782579if (body && util.isHostMethod(body, "createControlRange")) {2580testControlRange = body.createControlRange();2581if (util.areHostProperties(testControlRange, ["item", "add"])) {2582implementsControlRange = true;2583}2584}2585api.features.implementsControlRange = implementsControlRange;25862587// Selection collapsedness2588if (selectionHasAnchorAndFocus) {2589selectionIsCollapsed = function(sel) {2590return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;2591};2592} else {2593selectionIsCollapsed = function(sel) {2594return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;2595};2596}25972598function updateAnchorAndFocusFromRange(sel, range, backwards) {2599var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";2600sel.anchorNode = range[anchorPrefix + "Container"];2601sel.anchorOffset = range[anchorPrefix + "Offset"];2602sel.focusNode = range[focusPrefix + "Container"];2603sel.focusOffset = range[focusPrefix + "Offset"];2604}26052606function updateAnchorAndFocusFromNativeSelection(sel) {2607var nativeSel = sel.nativeSelection;2608sel.anchorNode = nativeSel.anchorNode;2609sel.anchorOffset = nativeSel.anchorOffset;2610sel.focusNode = nativeSel.focusNode;2611sel.focusOffset = nativeSel.focusOffset;2612}26132614function updateEmptySelection(sel) {2615sel.anchorNode = sel.focusNode = null;2616sel.anchorOffset = sel.focusOffset = 0;2617sel.rangeCount = 0;2618sel.isCollapsed = true;2619sel._ranges.length = 0;2620}26212622function getNativeRange(range) {2623var nativeRange;2624if (range instanceof DomRange) {2625nativeRange = range._selectionNativeRange;2626if (!nativeRange) {2627nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));2628nativeRange.setEnd(range.endContainer, range.endOffset);2629nativeRange.setStart(range.startContainer, range.startOffset);2630range._selectionNativeRange = nativeRange;2631range.attachListener("detach", function() {26322633this._selectionNativeRange = null;2634});2635}2636} else if (range instanceof WrappedRange) {2637nativeRange = range.nativeRange;2638} else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {2639nativeRange = range;2640}2641return nativeRange;2642}26432644function rangeContainsSingleElement(rangeNodes) {2645if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {2646return false;2647}2648for (var i = 1, len = rangeNodes.length; i < len; ++i) {2649if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {2650return false;2651}2652}2653return true;2654}26552656function getSingleElementFromRange(range) {2657var nodes = range.getNodes();2658if (!rangeContainsSingleElement(nodes)) {2659throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");2660}2661return nodes[0];2662}26632664function isTextRange(range) {2665return !!range && typeof range.text != "undefined";2666}26672668function updateFromTextRange(sel, range) {2669// Create a Range from the selected TextRange2670var wrappedRange = new WrappedRange(range);2671sel._ranges = [wrappedRange];26722673updateAnchorAndFocusFromRange(sel, wrappedRange, false);2674sel.rangeCount = 1;2675sel.isCollapsed = wrappedRange.collapsed;2676}26772678function updateControlSelection(sel) {2679// Update the wrapped selection based on what's now in the native selection2680sel._ranges.length = 0;2681if (sel.docSelection.type == "None") {2682updateEmptySelection(sel);2683} else {2684var controlRange = sel.docSelection.createRange();2685if (isTextRange(controlRange)) {2686// This case (where the selection type is "Control" and calling createRange() on the selection returns2687// a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected2688// ControlRange have been removed from the ControlRange and removed from the document.2689updateFromTextRange(sel, controlRange);2690} else {2691sel.rangeCount = controlRange.length;2692var range, doc = dom.getDocument(controlRange.item(0));2693for (var i = 0; i < sel.rangeCount; ++i) {2694range = api.createRange(doc);2695range.selectNode(controlRange.item(i));2696sel._ranges.push(range);2697}2698sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;2699updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);2700}2701}2702}27032704function addRangeToControlSelection(sel, range) {2705var controlRange = sel.docSelection.createRange();2706var rangeElement = getSingleElementFromRange(range);27072708// Create a new ControlRange containing all the elements in the selected ControlRange plus the element2709// contained by the supplied range2710var doc = dom.getDocument(controlRange.item(0));2711var newControlRange = dom.getBody(doc).createControlRange();2712for (var i = 0, len = controlRange.length; i < len; ++i) {2713newControlRange.add(controlRange.item(i));2714}2715try {2716newControlRange.add(rangeElement);2717} catch (ex) {2718throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");2719}2720newControlRange.select();27212722// Update the wrapped selection based on what's now in the native selection2723updateControlSelection(sel);2724}27252726var getSelectionRangeAt;27272728if (util.isHostMethod(testSelection, "getRangeAt")) {2729getSelectionRangeAt = function(sel, index) {2730try {2731return sel.getRangeAt(index);2732} catch(ex) {2733return null;2734}2735};2736} else if (selectionHasAnchorAndFocus) {2737getSelectionRangeAt = function(sel) {2738var doc = dom.getDocument(sel.anchorNode);2739var range = api.createRange(doc);2740range.setStart(sel.anchorNode, sel.anchorOffset);2741range.setEnd(sel.focusNode, sel.focusOffset);27422743// Handle the case when the selection was selected backwards (from the end to the start in the2744// document)2745if (range.collapsed !== this.isCollapsed) {2746range.setStart(sel.focusNode, sel.focusOffset);2747range.setEnd(sel.anchorNode, sel.anchorOffset);2748}27492750return range;2751};2752}27532754/**2755* @constructor2756*/2757function WrappedSelection(selection, docSelection, win) {2758this.nativeSelection = selection;2759this.docSelection = docSelection;2760this._ranges = [];2761this.win = win;2762this.refresh();2763}27642765api.getSelection = function(win) {2766win = win || window;2767var sel = win[windowPropertyName];2768var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;2769if (sel) {2770sel.nativeSelection = nativeSel;2771sel.docSelection = docSel;2772sel.refresh(win);2773} else {2774sel = new WrappedSelection(nativeSel, docSel, win);2775win[windowPropertyName] = sel;2776}2777return sel;2778};27792780api.getIframeSelection = function(iframeEl) {2781return api.getSelection(dom.getIframeWindow(iframeEl));2782};27832784var selProto = WrappedSelection.prototype;27852786function createControlSelection(sel, ranges) {2787// Ensure that the selection becomes of type "Control"2788var doc = dom.getDocument(ranges[0].startContainer);2789var controlRange = dom.getBody(doc).createControlRange();2790for (var i = 0, el; i < rangeCount; ++i) {2791el = getSingleElementFromRange(ranges[i]);2792try {2793controlRange.add(el);2794} catch (ex) {2795throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");2796}2797}2798controlRange.select();27992800// Update the wrapped selection based on what's now in the native selection2801updateControlSelection(sel);2802}28032804// Selecting a range2805if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {2806selProto.removeAllRanges = function() {2807this.nativeSelection.removeAllRanges();2808updateEmptySelection(this);2809};28102811var addRangeBackwards = function(sel, range) {2812var doc = DomRange.getRangeDocument(range);2813var endRange = api.createRange(doc);2814endRange.collapseToPoint(range.endContainer, range.endOffset);2815sel.nativeSelection.addRange(getNativeRange(endRange));2816sel.nativeSelection.extend(range.startContainer, range.startOffset);2817sel.refresh();2818};28192820if (selectionHasRangeCount) {2821selProto.addRange = function(range, backwards) {2822if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {2823addRangeToControlSelection(this, range);2824} else {2825if (backwards && selectionHasExtend) {2826addRangeBackwards(this, range);2827} else {2828var previousRangeCount;2829if (selectionSupportsMultipleRanges) {2830previousRangeCount = this.rangeCount;2831} else {2832this.removeAllRanges();2833previousRangeCount = 0;2834}2835this.nativeSelection.addRange(getNativeRange(range));28362837// Check whether adding the range was successful2838this.rangeCount = this.nativeSelection.rangeCount;28392840if (this.rangeCount == previousRangeCount + 1) {2841// The range was added successfully28422843// Check whether the range that we added to the selection is reflected in the last range extracted from2844// the selection2845if (api.config.checkSelectionRanges) {2846var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);2847if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {2848// Happens in WebKit with, for example, a selection placed at the start of a text node2849range = new WrappedRange(nativeRange);2850}2851}2852this._ranges[this.rangeCount - 1] = range;2853updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));2854this.isCollapsed = selectionIsCollapsed(this);2855} else {2856// The range was not added successfully. The simplest thing is to refresh2857this.refresh();2858}2859}2860}2861};2862} else {2863selProto.addRange = function(range, backwards) {2864if (backwards && selectionHasExtend) {2865addRangeBackwards(this, range);2866} else {2867this.nativeSelection.addRange(getNativeRange(range));2868this.refresh();2869}2870};2871}28722873selProto.setRanges = function(ranges) {2874if (implementsControlRange && ranges.length > 1) {2875createControlSelection(this, ranges);2876} else {2877this.removeAllRanges();2878for (var i = 0, len = ranges.length; i < len; ++i) {2879this.addRange(ranges[i]);2880}2881}2882};2883} else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&2884implementsControlRange && useDocumentSelection) {28852886selProto.removeAllRanges = function() {2887// Added try/catch as fix for issue #212888try {2889this.docSelection.empty();28902891// Check for empty() not working (issue #24)2892if (this.docSelection.type != "None") {2893// Work around failure to empty a control selection by instead selecting a TextRange and then2894// calling empty()2895var doc;2896if (this.anchorNode) {2897doc = dom.getDocument(this.anchorNode);2898} else if (this.docSelection.type == CONTROL) {2899var controlRange = this.docSelection.createRange();2900if (controlRange.length) {2901doc = dom.getDocument(controlRange.item(0)).body.createTextRange();2902}2903}2904if (doc) {2905var textRange = doc.body.createTextRange();2906textRange.select();2907this.docSelection.empty();2908}2909}2910} catch(ex) {}2911updateEmptySelection(this);2912};29132914selProto.addRange = function(range) {2915if (this.docSelection.type == CONTROL) {2916addRangeToControlSelection(this, range);2917} else {2918WrappedRange.rangeToTextRange(range).select();2919this._ranges[0] = range;2920this.rangeCount = 1;2921this.isCollapsed = this._ranges[0].collapsed;2922updateAnchorAndFocusFromRange(this, range, false);2923}2924};29252926selProto.setRanges = function(ranges) {2927this.removeAllRanges();2928var rangeCount = ranges.length;2929if (rangeCount > 1) {2930createControlSelection(this, ranges);2931} else if (rangeCount) {2932this.addRange(ranges[0]);2933}2934};2935} else {2936module.fail("No means of selecting a Range or TextRange was found");2937return false;2938}29392940selProto.getRangeAt = function(index) {2941if (index < 0 || index >= this.rangeCount) {2942throw new DOMException("INDEX_SIZE_ERR");2943} else {2944return this._ranges[index];2945}2946};29472948var refreshSelection;29492950if (useDocumentSelection) {2951refreshSelection = function(sel) {2952var range;2953if (api.isSelectionValid(sel.win)) {2954range = sel.docSelection.createRange();2955} else {2956range = dom.getBody(sel.win.document).createTextRange();2957range.collapse(true);2958}295929602961if (sel.docSelection.type == CONTROL) {2962updateControlSelection(sel);2963} else if (isTextRange(range)) {2964updateFromTextRange(sel, range);2965} else {2966updateEmptySelection(sel);2967}2968};2969} else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {2970refreshSelection = function(sel) {2971if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {2972updateControlSelection(sel);2973} else {2974sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;2975if (sel.rangeCount) {2976for (var i = 0, len = sel.rangeCount; i < len; ++i) {2977sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));2978}2979updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));2980sel.isCollapsed = selectionIsCollapsed(sel);2981} else {2982updateEmptySelection(sel);2983}2984}2985};2986} else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {2987refreshSelection = function(sel) {2988var range, nativeSel = sel.nativeSelection;2989if (nativeSel.anchorNode) {2990range = getSelectionRangeAt(nativeSel, 0);2991sel._ranges = [range];2992sel.rangeCount = 1;2993updateAnchorAndFocusFromNativeSelection(sel);2994sel.isCollapsed = selectionIsCollapsed(sel);2995} else {2996updateEmptySelection(sel);2997}2998};2999} else {3000module.fail("No means of obtaining a Range or TextRange from the user's selection was found");3001return false;3002}30033004selProto.refresh = function(checkForChanges) {3005var oldRanges = checkForChanges ? this._ranges.slice(0) : null;3006refreshSelection(this);3007if (checkForChanges) {3008var i = oldRanges.length;3009if (i != this._ranges.length) {3010return false;3011}3012while (i--) {3013if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {3014return false;3015}3016}3017return true;3018}3019};30203021// Removal of a single range3022var removeRangeManually = function(sel, range) {3023var ranges = sel.getAllRanges(), removed = false;3024sel.removeAllRanges();3025for (var i = 0, len = ranges.length; i < len; ++i) {3026if (removed || range !== ranges[i]) {3027sel.addRange(ranges[i]);3028} else {3029// According to the draft WHATWG Range spec, the same range may be added to the selection multiple3030// times. removeRange should only remove the first instance, so the following ensures only the first3031// instance is removed3032removed = true;3033}3034}3035if (!sel.rangeCount) {3036updateEmptySelection(sel);3037}3038};30393040if (implementsControlRange) {3041selProto.removeRange = function(range) {3042if (this.docSelection.type == CONTROL) {3043var controlRange = this.docSelection.createRange();3044var rangeElement = getSingleElementFromRange(range);30453046// Create a new ControlRange containing all the elements in the selected ControlRange minus the3047// element contained by the supplied range3048var doc = dom.getDocument(controlRange.item(0));3049var newControlRange = dom.getBody(doc).createControlRange();3050var el, removed = false;3051for (var i = 0, len = controlRange.length; i < len; ++i) {3052el = controlRange.item(i);3053if (el !== rangeElement || removed) {3054newControlRange.add(controlRange.item(i));3055} else {3056removed = true;3057}3058}3059newControlRange.select();30603061// Update the wrapped selection based on what's now in the native selection3062updateControlSelection(this);3063} else {3064removeRangeManually(this, range);3065}3066};3067} else {3068selProto.removeRange = function(range) {3069removeRangeManually(this, range);3070};3071}30723073// Detecting if a selection is backwards3074var selectionIsBackwards;3075if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {3076selectionIsBackwards = function(sel) {3077var backwards = false;3078if (sel.anchorNode) {3079backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);3080}3081return backwards;3082};30833084selProto.isBackwards = function() {3085return selectionIsBackwards(this);3086};3087} else {3088selectionIsBackwards = selProto.isBackwards = function() {3089return false;3090};3091}30923093// Selection text3094// This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation3095selProto.toString = function() {30963097var rangeTexts = [];3098for (var i = 0, len = this.rangeCount; i < len; ++i) {3099rangeTexts[i] = "" + this._ranges[i];3100}3101return rangeTexts.join("");3102};31033104function assertNodeInSameDocument(sel, node) {3105if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {3106throw new DOMException("WRONG_DOCUMENT_ERR");3107}3108}31093110// No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used3111selProto.collapse = function(node, offset) {3112assertNodeInSameDocument(this, node);3113var range = api.createRange(dom.getDocument(node));3114range.collapseToPoint(node, offset);3115this.removeAllRanges();3116this.addRange(range);3117this.isCollapsed = true;3118};31193120selProto.collapseToStart = function() {3121if (this.rangeCount) {3122var range = this._ranges[0];3123this.collapse(range.startContainer, range.startOffset);3124} else {3125throw new DOMException("INVALID_STATE_ERR");3126}3127};31283129selProto.collapseToEnd = function() {3130if (this.rangeCount) {3131var range = this._ranges[this.rangeCount - 1];3132this.collapse(range.endContainer, range.endOffset);3133} else {3134throw new DOMException("INVALID_STATE_ERR");3135}3136};31373138// The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is3139// never used by Rangy.3140selProto.selectAllChildren = function(node) {3141assertNodeInSameDocument(this, node);3142var range = api.createRange(dom.getDocument(node));3143range.selectNodeContents(node);3144this.removeAllRanges();3145this.addRange(range);3146};31473148selProto.deleteFromDocument = function() {3149// Sepcial behaviour required for Control selections3150if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {3151var controlRange = this.docSelection.createRange();3152var element;3153while (controlRange.length) {3154element = controlRange.item(0);3155controlRange.remove(element);3156element.parentNode.removeChild(element);3157}3158this.refresh();3159} else if (this.rangeCount) {3160var ranges = this.getAllRanges();3161this.removeAllRanges();3162for (var i = 0, len = ranges.length; i < len; ++i) {3163ranges[i].deleteContents();3164}3165// The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each3166// range. Firefox moves the selection to where the final selected range was, so we emulate that3167this.addRange(ranges[len - 1]);3168}3169};31703171// The following are non-standard extensions3172selProto.getAllRanges = function() {3173return this._ranges.slice(0);3174};31753176selProto.setSingleRange = function(range) {3177this.setRanges( [range] );3178};31793180selProto.containsNode = function(node, allowPartial) {3181for (var i = 0, len = this._ranges.length; i < len; ++i) {3182if (this._ranges[i].containsNode(node, allowPartial)) {3183return true;3184}3185}3186return false;3187};31883189selProto.toHtml = function() {3190var html = "";3191if (this.rangeCount) {3192var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");3193for (var i = 0, len = this._ranges.length; i < len; ++i) {3194container.appendChild(this._ranges[i].cloneContents());3195}3196html = container.innerHTML;3197}3198return html;3199};32003201function inspect(sel) {3202var rangeInspects = [];3203var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);3204var focus = new DomPosition(sel.focusNode, sel.focusOffset);3205var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";32063207if (typeof sel.rangeCount != "undefined") {3208for (var i = 0, len = sel.rangeCount; i < len; ++i) {3209rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));3210}3211}3212return "[" + name + "(Ranges: " + rangeInspects.join(", ") +3213")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";32143215}32163217selProto.getName = function() {3218return "WrappedSelection";3219};32203221selProto.inspect = function() {3222return inspect(this);3223};32243225selProto.detach = function() {3226this.win[windowPropertyName] = null;3227this.win = this.anchorNode = this.focusNode = null;3228};32293230WrappedSelection.inspect = inspect;32313232api.Selection = WrappedSelection;32333234api.selectionPrototype = selProto;32353236api.addCreateMissingNativeApiListener(function(win) {3237if (typeof win.getSelection == "undefined") {3238win.getSelection = function() {3239return api.getSelection(this);3240};3241}3242win = null;3243});3244});3245/*3246Base.js, version 1.1a3247Copyright 2006-2010, Dean Edwards3248License: http://www.opensource.org/licenses/mit-license.php3249*/32503251var Base = function() {3252// dummy3253};32543255Base.extend = function(_instance, _static) { // subclass3256var extend = Base.prototype.extend;32573258// build the prototype3259Base._prototyping = true;3260var proto = new this;3261extend.call(proto, _instance);3262proto.base = function() {3263// call this method from any other method to invoke that method's ancestor3264};3265delete Base._prototyping;32663267// create the wrapper for the constructor function3268//var constructor = proto.constructor.valueOf(); //-dean3269var constructor = proto.constructor;3270var klass = proto.constructor = function() {3271if (!Base._prototyping) {3272if (this._constructing || this.constructor == klass) { // instantiation3273this._constructing = true;3274constructor.apply(this, arguments);3275delete this._constructing;3276} else if (arguments[0] != null) { // casting3277return (arguments[0].extend || extend).call(arguments[0], proto);3278}3279}3280};32813282// build the class interface3283klass.ancestor = this;3284klass.extend = this.extend;3285klass.forEach = this.forEach;3286klass.implement = this.implement;3287klass.prototype = proto;3288klass.toString = this.toString;3289klass.valueOf = function(type) {3290//return (type == "object") ? klass : constructor; //-dean3291return (type == "object") ? klass : constructor.valueOf();3292};3293extend.call(klass, _static);3294// class initialisation3295if (typeof klass.init == "function") klass.init();3296return klass;3297};32983299Base.prototype = {3300extend: function(source, value) {3301if (arguments.length > 1) { // extending with a name/value pair3302var ancestor = this[source];3303if (ancestor && (typeof value == "function") && // overriding a method?3304// the valueOf() comparison is to avoid circular references3305(!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&3306/\bbase\b/.test(value)) {3307// get the underlying method3308var method = value.valueOf();3309// override3310value = function() {3311var previous = this.base || Base.prototype.base;3312this.base = ancestor;3313var returnValue = method.apply(this, arguments);3314this.base = previous;3315return returnValue;3316};3317// point to the underlying method3318value.valueOf = function(type) {3319return (type == "object") ? value : method;3320};3321value.toString = Base.toString;3322}3323this[source] = value;3324} else if (source) { // extending with an object literal3325var extend = Base.prototype.extend;3326// if this object has a customised extend method then use it3327if (!Base._prototyping && typeof this != "function") {3328extend = this.extend || extend;3329}3330var proto = {toSource: null};3331// do the "toString" and other methods manually3332var hidden = ["constructor", "toString", "valueOf"];3333// if we are prototyping then include the constructor3334var i = Base._prototyping ? 0 : 1;3335while (key = hidden[i++]) {3336if (source[key] != proto[key]) {3337extend.call(this, key, source[key]);33383339}3340}3341// copy each of the source object's properties to this object3342for (var key in source) {3343if (!proto[key]) extend.call(this, key, source[key]);3344}3345}3346return this;3347}3348};33493350// initialise3351Base = Base.extend({3352constructor: function() {3353this.extend(arguments[0]);3354}3355}, {3356ancestor: Object,3357version: "1.1",33583359forEach: function(object, block, context) {3360for (var key in object) {3361if (this.prototype[key] === undefined) {3362block.call(context, object[key], key, object);3363}3364}3365},33663367implement: function() {3368for (var i = 0; i < arguments.length; i++) {3369if (typeof arguments[i] == "function") {3370// if it's a function, call it3371arguments[i](this.prototype);3372} else {3373// add the interface using the extend method3374this.prototype.extend(arguments[i]);3375}3376}3377return this;3378},33793380toString: function() {3381return String(this.valueOf());3382}3383});/**3384* Detect browser support for specific features3385*/3386wysihtml5.browser = (function() {3387var userAgent = navigator.userAgent,3388testElement = document.createElement("div"),3389// Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect3390isIE = userAgent.indexOf("MSIE") !== -1 && userAgent.indexOf("Opera") === -1,3391isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1,3392isWebKit = userAgent.indexOf("AppleWebKit/") !== -1,3393isChrome = userAgent.indexOf("Chrome/") !== -1,3394isOpera = userAgent.indexOf("Opera/") !== -1;33953396function iosVersion(userAgent) {3397return ((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1];3398}33993400return {3401// Static variable needed, publicly accessible, to be able override it in unit tests3402USER_AGENT: userAgent,34033404/**3405* Exclude browsers that are not capable of displaying and handling3406* contentEditable as desired:3407* - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable3408* - IE < 8 create invalid markup and crash randomly from time to time3409*3410* @return {Boolean}3411*/3412supported: function() {3413var userAgent = this.USER_AGENT.toLowerCase(),3414// Essential for making html elements editable3415hasContentEditableSupport = "contentEditable" in testElement,3416// Following methods are needed in order to interact with the contentEditable area3417hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState,3418// document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+3419hasQuerySelectorSupport = document.querySelector && document.querySelectorAll,3420// contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)3421isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;34223423return hasContentEditableSupport3424&& hasEditingApiSupport3425&& hasQuerySelectorSupport3426&& !isIncompatibleMobileBrowser;3427},34283429isTouchDevice: function() {3430return this.supportsEvent("touchmove");3431},34323433isIos: function() {3434var userAgent = this.USER_AGENT.toLowerCase();3435return userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1;3436},34373438/**3439* Whether the browser supports sandboxed iframes3440* Currently only IE 6+ offers such feature <iframe security="restricted">3441*3442* http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx3443* http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx3444*3445* HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)3446*/3447supportsSandboxedIframes: function() {3448return isIE;3449},34503451/**3452* IE6+7 throw a mixed content warning when the src of an iframe3453* is empty/unset or about:blank3454* window.querySelector is implemented as of IE83455*/3456throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {3457return !("querySelector" in document);3458},34593460/**3461* Whether the caret is correctly displayed in contentEditable elements3462* Firefox sometimes shows a huge caret in the beginning after focusing3463*/3464displaysCaretInEmptyContentEditableCorrectly: function() {3465return !isGecko;3466},34673468/**3469* Opera and IE are the only browsers who offer the css value3470* in the original unit, thx to the currentStyle object3471* All other browsers provide the computed style in px via window.getComputedStyle3472*/3473hasCurrentStyleProperty: function() {3474return "currentStyle" in testElement;3475},34763477/**3478* Whether the browser inserts a <br> when pressing enter in a contentEditable element3479*/3480insertsLineBreaksOnReturn: function() {3481return isGecko;3482},34833484supportsPlaceholderAttributeOn: function(element) {3485return "placeholder" in element;3486},34873488supportsEvent: function(eventName) {3489return "on" + eventName in testElement || (function() {3490testElement.setAttribute("on" + eventName, "return;");3491return typeof(testElement["on" + eventName]) === "function";3492})();3493},34943495/**3496* Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe3497*/3498supportsEventsInIframeCorrectly: function() {3499return !isOpera;3500},35013502/**3503* Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled3504* with event.preventDefault3505* Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs3506* to be cancelled3507*/3508firesOnDropOnlyWhenOnDragOverIsCancelled: function() {3509return isWebKit || isGecko;3510},35113512/**3513* Whether the browser supports the event.dataTransfer property in a proper way3514*/3515supportsDataTransfer: function() {3516try {3517// Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does)3518return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData;3519} catch(e) {3520return false;3521}3522},35233524/**3525* Everything below IE9 doesn't know how to treat HTML5 tags3526*3527* @param {Object} context The document object on which to check HTML5 support3528*3529* @example3530* wysihtml5.browser.supportsHTML5Tags(document);3531*/3532supportsHTML5Tags: function(context) {3533var element = context.createElement("div"),3534html5 = "<article>foo</article>";3535element.innerHTML = html5;3536return element.innerHTML.toLowerCase() === html5;3537},35383539/**3540* Checks whether a document supports a certain queryCommand3541* In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree3542* in oder to report correct results3543*3544* @param {Object} doc Document object on which to check for a query command3545* @param {String} command The query command to check for3546* @return {Boolean}3547*3548* @example3549* wysihtml5.browser.supportsCommand(document, "bold");3550*/3551supportsCommand: (function() {3552// Following commands are supported but contain bugs in some browsers3553var buggyCommands = {3554// formatBlock fails with some tags (eg. <blockquote>)3555"formatBlock": isIE,3556// When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets3557// converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)3558// IE and Opera act a bit different here as they convert the entire content of the current block element into a list3559"insertUnorderedList": isIE || isOpera || isWebKit,3560"insertOrderedList": isIE || isOpera || isWebKit3561};35623563// Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands3564var supported = {3565"insertHTML": isGecko3566};35673568return function(doc, command) {3569var isBuggy = buggyCommands[command];3570if (!isBuggy) {3571// Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled3572try {3573return doc.queryCommandSupported(command);3574} catch(e1) {}35753576try {3577return doc.queryCommandEnabled(command);3578} catch(e2) {3579return !!supported[command];3580}3581}3582return false;3583};3584})(),35853586/**3587* IE: URLs starting with:3588* www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,3589* nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:3590* will automatically be auto-linked when either the user inserts them via copy&paste or presses the3591* space bar when the caret is directly after such an url.3592* This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll3593* (related blog post on msdn3594* http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).3595*/3596doesAutoLinkingInContentEditable: function() {3597return isIE;3598},35993600/**3601* As stated above, IE auto links urls typed into contentEditable elements3602* Since IE9 it's possible to prevent this behavior3603*/3604canDisableAutoLinking: function() {3605return this.supportsCommand(document, "AutoUrlDetect");3606},36073608/**3609* IE leaves an empty paragraph in the contentEditable element after clearing it3610* Chrome/Safari sometimes an empty <div>3611*/3612clearsContentEditableCorrectly: function() {3613return isGecko || isOpera || isWebKit;3614},36153616/**3617* IE gives wrong results for getAttribute3618*/3619supportsGetAttributeCorrectly: function() {3620var td = document.createElement("td");3621return td.getAttribute("rowspan") != "1";3622},36233624/**3625* When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.3626* Chrome and Safari both don't support this3627*/3628canSelectImagesInContentEditable: function() {3629return isGecko || isIE || isOpera;3630},36313632/**3633* When the caret is in an empty list (<ul><li>|</li></ul>) which is the first child in an contentEditable container3634* pressing backspace doesn't remove the entire list as done in other browsers3635*/3636clearsListsInContentEditableCorrectly: function() {3637return isGecko || isIE || isWebKit;3638},36393640/**3641* All browsers except Safari and Chrome automatically scroll the range/caret position into view3642*/3643autoScrollsToCaret: function() {3644return !isWebKit;3645},36463647/**3648* Check whether the browser automatically closes tags that don't need to be opened3649*/3650autoClosesUnclosedTags: function() {3651var clonedTestElement = testElement.cloneNode(false),3652returnValue,3653innerHTML;36543655clonedTestElement.innerHTML = "<p><div></div>";3656innerHTML = clonedTestElement.innerHTML.toLowerCase();3657returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";36583659// Cache result by overwriting current function3660this.autoClosesUnclosedTags = function() { return returnValue; };36613662return returnValue;3663},36643665/**3666* Whether the browser supports the native document.getElementsByClassName which returns live NodeLists3667*/3668supportsNativeGetElementsByClassName: function() {3669return String(document.getElementsByClassName).indexOf("[native code]") !== -1;3670},36713672/**3673* As of now (19.04.2011) only supported by Firefox 4 and Chrome3674* See https://developer.mozilla.org/en/DOM/Selection/modify3675*/3676supportsSelectionModify: function() {3677return "getSelection" in window && "modify" in window.getSelection();3678},36793680/**3681* Whether the browser supports the classList object for fast className manipulation3682* See https://developer.mozilla.org/en/DOM/element.classList3683*/3684supportsClassList: function() {3685return "classList" in testElement;3686},36873688/**3689* Opera needs a white space after a <br> in order to position the caret correctly3690*/3691needsSpaceAfterLineBreak: function() {3692return isOpera;3693},36943695/**3696* Whether the browser supports the speech api on the given element3697* See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/3698*3699* @example3700* var input = document.createElement("input");3701* if (wysihtml5.browser.supportsSpeechApiOn(input)) {3702* // ...3703* }3704*/3705supportsSpeechApiOn: function(input) {3706var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [, 0];3707return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);3708},37093710/**3711* IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest3712* See https://connect.microsoft.com/ie/feedback/details/6501123713* or try the POC http://tifftiff.de/ie9_crash/3714*/3715crashesWhenDefineProperty: function(property) {3716return isIE && (property === "XMLHttpRequest" || property === "XDomainRequest");3717},37183719/**3720* IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element3721*/3722doesAsyncFocus: function() {3723return isIE;3724},37253726/**3727* In IE it's impssible for the user and for the selection library to set the caret after an <img> when it's the lastChild in the document3728*/3729hasProblemsSettingCaretAfterImg: function() {3730return isIE;3731},37323733hasUndoInContextMenu: function() {3734return isGecko || isChrome || isOpera;3735}3736};3737})();wysihtml5.lang.array = function(arr) {3738return {3739/**3740* Check whether a given object exists in an array3741*3742* @example3743* wysihtml5.lang.array([1, 2]).contains(1);3744* // => true3745*/3746contains: function(needle) {3747if (arr.indexOf) {3748return arr.indexOf(needle) !== -1;3749} else {3750for (var i=0, length=arr.length; i<length; i++) {3751if (arr[i] === needle) { return true; }3752}3753return false;3754}3755},37563757/**3758* Substract one array from another3759*3760* @example3761* wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]);3762* // => [1, 2]3763*/3764without: function(arrayToSubstract) {3765arrayToSubstract = wysihtml5.lang.array(arrayToSubstract);3766var newArr = [],3767i = 0,3768length = arr.length;3769for (; i<length; i++) {3770if (!arrayToSubstract.contains(arr[i])) {3771newArr.push(arr[i]);3772}3773}3774return newArr;3775},37763777/**3778* Return a clean native array3779*3780* Following will convert a Live NodeList to a proper Array3781* @example3782* var childNodes = wysihtml5.lang.array(document.body.childNodes).get();3783*/3784get: function() {3785var i = 0,3786length = arr.length,3787newArray = [];3788for (; i<length; i++) {3789newArray.push(arr[i]);3790}3791return newArray;3792}3793};3794};wysihtml5.lang.Dispatcher = Base.extend(3795/** @scope wysihtml5.lang.Dialog.prototype */ {3796observe: function(eventName, handler) {3797this.events = this.events || {};3798this.events[eventName] = this.events[eventName] || [];3799this.events[eventName].push(handler);3800return this;3801},38023803on: function() {3804return this.observe.apply(this, wysihtml5.lang.array(arguments).get());3805},38063807fire: function(eventName, payload) {3808this.events = this.events || {};3809var handlers = this.events[eventName] || [],3810i = 0;3811for (; i<handlers.length; i++) {3812handlers[i].call(this, payload);3813}3814return this;3815},38163817stopObserving: function(eventName, handler) {3818this.events = this.events || {};3819var i = 0,3820handlers,3821newHandlers;3822if (eventName) {3823handlers = this.events[eventName] || [],3824newHandlers = [];3825for (; i<handlers.length; i++) {3826if (handlers[i] !== handler && handler) {3827newHandlers.push(handlers[i]);3828}3829}3830this.events[eventName] = newHandlers;3831} else {3832// Clean up all events3833this.events = {};3834}3835return this;3836}3837});wysihtml5.lang.object = function(obj) {3838return {3839/**3840* @example3841* wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();3842* // => { foo: 1, bar: 2, baz: 3 }3843*/3844merge: function(otherObj) {3845for (var i in otherObj) {3846obj[i] = otherObj[i];3847}3848return this;3849},38503851get: function() {3852return obj;3853},38543855/**3856* @example3857* wysihtml5.lang.object({ foo: 1 }).clone();3858* // => { foo: 1 }3859*/3860clone: function() {3861var newObj = {},3862i;3863for (i in obj) {3864newObj[i] = obj[i];3865}3866return newObj;3867},38683869/**3870* @example3871* wysihtml5.lang.object([]).isArray();3872* // => true3873*/3874isArray: function() {3875return Object.prototype.toString.call(obj) === "[object Array]";3876}3877};3878};(function() {3879var WHITE_SPACE_START = /^\s+/,3880WHITE_SPACE_END = /\s+$/;3881wysihtml5.lang.string = function(str) {3882str = String(str);3883return {3884/**3885* @example3886* wysihtml5.lang.string(" foo ").trim();3887* // => "foo"3888*/3889trim: function() {3890return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, "");3891},38923893/**3894* @example3895* wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" });3896* // => "Hello Christopher"3897*/3898interpolate: function(vars) {3899for (var i in vars) {3900str = this.replace("#{" + i + "}").by(vars[i]);3901}3902return str;3903},39043905/**3906* @example3907* wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans");3908* // => "Hello Hans"3909*/3910replace: function(search) {3911return {3912by: function(replace) {3913return str.split(search).join(replace);3914}3915}3916}3917};3918};3919})();/**3920* Find urls in descendant text nodes of an element and auto-links them3921* Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/3922*3923* @param {Element} element Container element in which to search for urls3924*3925* @example3926* <div id="text-container">Please click here: www.google.com</div>3927* <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script>3928*/3929(function(wysihtml5) {3930var /**3931* Don't auto-link urls that are contained in the following elements:3932*/3933IGNORE_URLS_IN = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),3934/**3935* revision 1:3936* /(\S+\.{1}[^\s\,\.\!]+)/g3937*3938* revision 2:3939* /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim3940*3941* put this in the beginning if you don't wan't to match within a word3942* (^|[\>\(\{\[\s\>])3943*/3944URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi,3945TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,3946MAX_DISPLAY_LENGTH = 100,3947BRACKETS = { ")": "(", "]": "[", "}": "{" };39483949function autoLink(element) {3950if (_hasParentThatShouldBeIgnored(element)) {3951return element;3952}39533954if (element === element.ownerDocument.documentElement) {3955element = element.ownerDocument.body;3956}39573958return _parseNode(element);3959}39603961/**3962* This is basically a rebuild of3963* the rails auto_link_urls text helper3964*/3965function _convertUrlsToLinks(str) {3966return str.replace(URL_REG_EXP, function(match, url) {3967var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",3968opening = BRACKETS[punctuation];3969url = url.replace(TRAILING_CHAR_REG_EXP, "");39703971if (url.split(opening).length > url.split(punctuation).length) {3972url = url + punctuation;3973punctuation = "";3974}3975var realUrl = url,3976displayUrl = url;3977if (url.length > MAX_DISPLAY_LENGTH) {3978displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";3979}3980// Add http prefix if necessary3981if (realUrl.substr(0, 4) === "www.") {3982realUrl = "http://" + realUrl;3983}39843985return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;3986});3987}39883989/**3990* Creates or (if already cached) returns a temp element3991* for the given document object3992*/3993function _getTempElement(context) {3994var tempElement = context._wysihtml5_tempElement;3995if (!tempElement) {3996tempElement = context._wysihtml5_tempElement = context.createElement("div");3997}3998return tempElement;3999}40004001/**4002* Replaces the original text nodes with the newly auto-linked dom tree4003*/4004function _wrapMatchesInNode(textNode) {4005var parentNode = textNode.parentNode,4006tempElement = _getTempElement(parentNode.ownerDocument);40074008// We need to insert an empty/temporary <span /> to fix IE quirks4009// Elsewise IE would strip white space in the beginning4010tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(textNode.data);4011tempElement.removeChild(tempElement.firstChild);40124013while (tempElement.firstChild) {4014// inserts tempElement.firstChild before textNode4015parentNode.insertBefore(tempElement.firstChild, textNode);4016}4017parentNode.removeChild(textNode);4018}40194020function _hasParentThatShouldBeIgnored(node) {4021var nodeName;4022while (node.parentNode) {4023node = node.parentNode;4024nodeName = node.nodeName;4025if (IGNORE_URLS_IN.contains(nodeName)) {4026return true;4027} else if (nodeName === "body") {4028return false;4029}4030}4031return false;4032}40334034function _parseNode(element) {4035if (IGNORE_URLS_IN.contains(element.nodeName)) {4036return;4037}40384039if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) {4040_wrapMatchesInNode(element);4041return;4042}40434044var childNodes = wysihtml5.lang.array(element.childNodes).get(),4045childNodesLength = childNodes.length,4046i = 0;40474048for (; i<childNodesLength; i++) {4049_parseNode(childNodes[i]);4050}40514052return element;4053}40544055wysihtml5.dom.autoLink = autoLink;40564057// Reveal url reg exp to the outside4058wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP;4059})(wysihtml5);(function(wysihtml5) {4060var supportsClassList = wysihtml5.browser.supportsClassList(),4061api = wysihtml5.dom;40624063api.addClass = function(element, className) {4064if (supportsClassList) {4065return element.classList.add(className);4066}4067if (api.hasClass(element, className)) {4068return;4069}4070element.className += " " + className;4071};40724073api.removeClass = function(element, className) {4074if (supportsClassList) {4075return element.classList.remove(className);4076}40774078element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " ");4079};40804081api.hasClass = function(element, className) {4082if (supportsClassList) {4083return element.classList.contains(className);4084}40854086var elementClassName = element.className;4087return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));4088};4089})(wysihtml5);4090wysihtml5.dom.contains = (function() {4091var documentElement = document.documentElement;4092if (documentElement.contains) {4093return function(container, element) {4094if (element.nodeType !== wysihtml5.ELEMENT_NODE) {4095element = element.parentNode;4096}4097return container !== element && container.contains(element);4098};4099} else if (documentElement.compareDocumentPosition) {4100return function(container, element) {4101// https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition4102return !!(container.compareDocumentPosition(element) & 16);4103};4104}4105})();/**4106* Converts an HTML fragment/element into a unordered/ordered list4107*4108* @param {Element} element The element which should be turned into a list4109* @param {String} listType The list type in which to convert the tree (either "ul" or "ol")4110* @return {Element} The created list4111*4112* @example4113* <!-- Assume the following dom: -->4114* <span id="pseudo-list">4115* eminem<br>4116* dr. dre4117* <div>50 Cent</div>4118* </span>4119*4120* <script>4121* wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul");4122* </script>4123*4124* <!-- Will result in: -->4125* <ul>4126* <li>eminem</li>4127* <li>dr. dre</li>4128* <li>50 Cent</li>4129* </ul>4130*/4131wysihtml5.dom.convertToList = (function() {4132function _createListItem(doc, list) {4133var listItem = doc.createElement("li");4134list.appendChild(listItem);4135return listItem;4136}41374138function _createList(doc, type) {4139return doc.createElement(type);4140}41414142function convertToList(element, listType) {4143if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {4144// Already a list4145return element;4146}41474148var doc = element.ownerDocument,4149list = _createList(doc, listType),4150lineBreaks = element.querySelectorAll("br"),4151lineBreaksLength = lineBreaks.length,4152childNodes,4153childNodesLength,4154childNode,4155lineBreak,4156parentNode,4157isBlockElement,4158isLineBreak,4159currentListItem,4160i;41614162// First find <br> at the end of inline elements and move them behind them4163for (i=0; i<lineBreaksLength; i++) {4164lineBreak = lineBreaks[i];4165while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) {4166if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") {4167parentNode.removeChild(lineBreak);4168break;4169}4170wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode);4171}4172}41734174childNodes = wysihtml5.lang.array(element.childNodes).get();4175childNodesLength = childNodes.length;41764177for (i=0; i<childNodesLength; i++) {4178currentListItem = currentListItem || _createListItem(doc, list);4179childNode = childNodes[i];4180isBlockElement = wysihtml5.dom.getStyle("display").from(childNode) === "block";4181isLineBreak = childNode.nodeName === "BR";41824183if (isBlockElement) {4184// Append blockElement to current <li> if empty, otherwise create a new one4185currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;4186currentListItem.appendChild(childNode);4187currentListItem = null;4188continue;4189}41904191if (isLineBreak) {4192// Only create a new list item in the next iteration when the current one has already content4193currentListItem = currentListItem.firstChild ? null : currentListItem;4194continue;4195}41964197currentListItem.appendChild(childNode);4198}41994200element.parentNode.replaceChild(list, element);4201return list;4202}42034204return convertToList;4205})();/**4206* Copy a set of attributes from one element to another4207*4208* @param {Array} attributesToCopy List of attributes which should be copied4209* @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to4210* copy the attributes from., this again returns an object which provides a method named "to" which can be invoked4211* with the element where to copy the attributes to (see example)4212*4213* @example4214* var textarea = document.querySelector("textarea"),4215* div = document.querySelector("div[contenteditable=true]"),4216* anotherDiv = document.querySelector("div.preview");4217* wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);4218*4219*/4220wysihtml5.dom.copyAttributes = function(attributesToCopy) {4221return {4222from: function(elementToCopyFrom) {4223return {4224to: function(elementToCopyTo) {4225var attribute,4226i = 0,4227length = attributesToCopy.length;4228for (; i<length; i++) {4229attribute = attributesToCopy[i];4230if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") {4231elementToCopyTo[attribute] = elementToCopyFrom[attribute];4232}4233}4234return { andTo: arguments.callee };4235}4236};4237}4238};4239};/**4240* Copy a set of styles from one element to another4241* Please note that this only works properly across browsers when the element from which to copy the styles4242* is in the dom4243*4244* Interesting article on how to copy styles4245*4246* @param {Array} stylesToCopy List of styles which should be copied4247* @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to4248* copy the styles from., this again returns an object which provides a method named "to" which can be invoked4249* with the element where to copy the styles to (see example)4250*4251* @example4252* var textarea = document.querySelector("textarea"),4253* div = document.querySelector("div[contenteditable=true]"),4254* anotherDiv = document.querySelector("div.preview");4255* wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);4256*4257*/4258(function(dom) {42594260/**4261* Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set4262* So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then4263* its computed css width will be 198px4264*/4265var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];42664267var shouldIgnoreBoxSizingBorderBox = function(element) {4268if (hasBoxSizingBorderBox(element)) {4269return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;4270}4271return false;4272};42734274var hasBoxSizingBorderBox = function(element) {4275var i = 0,4276length = BOX_SIZING_PROPERTIES.length;4277for (; i<length; i++) {4278if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {4279return BOX_SIZING_PROPERTIES[i];4280}4281}4282};42834284dom.copyStyles = function(stylesToCopy) {4285return {4286from: function(element) {4287if (shouldIgnoreBoxSizingBorderBox(element)) {4288stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);4289}42904291var cssText = "",4292length = stylesToCopy.length,4293i = 0,4294property;4295for (; i<length; i++) {4296property = stylesToCopy[i];4297cssText += property + ":" + dom.getStyle(property).from(element) + ";";4298}42994300return {4301to: function(element) {4302dom.setStyles(cssText).on(element);4303return { andTo: arguments.callee };4304}4305};4306}4307};4308};4309})(wysihtml5.dom);/**4310* Event Delegation4311*4312* @example4313* wysihtml5.dom.delegate(document.body, "a", "click", function() {4314* // foo4315* });4316*/4317(function(wysihtml5) {43184319wysihtml5.dom.delegate = function(container, selector, eventName, handler) {4320return wysihtml5.dom.observe(container, eventName, function(event) {4321var target = event.target,4322match = wysihtml5.lang.array(container.querySelectorAll(selector));43234324while (target && target !== container) {4325if (match.contains(target)) {4326handler.call(target, event);4327break;4328}4329target = target.parentNode;4330}4331});4332};43334334})(wysihtml5);/**4335* Returns the given html wrapped in a div element4336*4337* Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly4338* when inserted via innerHTML4339*4340* @param {String} html The html which should be wrapped in a dom element4341* @param {Obejct} [context] Document object of the context the html belongs to4342*4343* @example4344* wysihtml5.dom.getAsDom("<article>foo</article>");4345*/4346wysihtml5.dom.getAsDom = (function() {43474348var _innerHTMLShiv = function(html, context) {4349var tempElement = context.createElement("div");4350tempElement.style.display = "none";4351context.body.appendChild(tempElement);4352// IE throws an exception when trying to insert <frameset></frameset> via innerHTML4353try { tempElement.innerHTML = html; } catch(e) {}4354context.body.removeChild(tempElement);4355return tempElement;4356};43574358/**4359* Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element4360*/4361var _ensureHTML5Compatibility = function(context) {4362if (context._wysihtml5_supportsHTML5Tags) {4363return;4364}4365for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {4366context.createElement(HTML5_ELEMENTS[i]);4367}4368context._wysihtml5_supportsHTML5Tags = true;4369};437043714372/**4373* List of html5 tags4374* taken from http://simon.html5.org/html5-elements4375*/4376var HTML5_ELEMENTS = [4377"abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption",4378"figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress",4379"rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr"4380];43814382return function(html, context) {4383context = context || document;4384var tempElement;4385if (typeof(html) === "object" && html.nodeType) {4386tempElement = context.createElement("div");4387tempElement.appendChild(html);4388} else if (wysihtml5.browser.supportsHTML5Tags(context)) {4389tempElement = context.createElement("div");4390tempElement.innerHTML = html;4391} else {4392_ensureHTML5Compatibility(context);4393tempElement = _innerHTMLShiv(html, context);4394}4395return tempElement;4396};4397})();/**4398* Walks the dom tree from the given node up until it finds a match4399* Designed for optimal performance.4400*4401* @param {Element} node The from which to check the parent nodes4402* @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp)4403* @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)4404* @return {null|Element} Returns the first element that matched the desiredNodeName(s)4405* @example4406* var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] });4407* // ... or ...4408* var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" });4409* // ... or ...4410* var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g });4411*/4412wysihtml5.dom.getParentElement = (function() {44134414function _isSameNodeName(nodeName, desiredNodeNames) {4415if (!desiredNodeNames || !desiredNodeNames.length) {4416return true;4417}44184419if (typeof(desiredNodeNames) === "string") {4420return nodeName === desiredNodeNames;4421} else {4422return wysihtml5.lang.array(desiredNodeNames).contains(nodeName);4423}4424}44254426function _isElement(node) {4427return node.nodeType === wysihtml5.ELEMENT_NODE;4428}44294430function _hasClassName(element, className, classRegExp) {4431var classNames = (element.className || "").match(classRegExp) || [];4432if (!className) {4433return !!classNames.length;4434}4435return classNames[classNames.length - 1] === className;4436}44374438function _getParentElementWithNodeName(node, nodeName, levels) {4439while (levels-- && node && node.nodeName !== "BODY") {4440if (_isSameNodeName(node.nodeName, nodeName)) {4441return node;4442}4443node = node.parentNode;4444}4445return null;4446}44474448function _getParentElementWithNodeNameAndClassName(node, nodeName, className, classRegExp, levels) {4449while (levels-- && node && node.nodeName !== "BODY") {4450if (_isElement(node) &&4451_isSameNodeName(node.nodeName, nodeName) &&4452_hasClassName(node, className, classRegExp)) {4453return node;4454}4455node = node.parentNode;4456}4457return null;4458}44594460return function(node, matchingSet, levels) {4461levels = levels || 50; // Go max 50 nodes upwards from current node4462if (matchingSet.className || matchingSet.classRegExp) {4463return _getParentElementWithNodeNameAndClassName(4464node, matchingSet.nodeName, matchingSet.className, matchingSet.classRegExp, levels4465);4466} else {4467return _getParentElementWithNodeName(4468node, matchingSet.nodeName, levels4469);4470}4471};4472})();4473/**4474* Get element's style for a specific css property4475*4476* @param {Element} element The element on which to retrieve the style4477* @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)4478*4479* @example4480* wysihtml5.dom.getStyle("display").from(document.body);4481* // => "block"4482*/4483wysihtml5.dom.getStyle = (function() {4484var stylePropertyMapping = {4485"float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"4486},4487REG_EXP_CAMELIZE = /\-[a-z]/g;44884489function camelize(str) {4490return str.replace(REG_EXP_CAMELIZE, function(match) {4491return match.charAt(1).toUpperCase();4492});4493}44944495return function(property) {4496return {4497from: function(element) {4498if (element.nodeType !== wysihtml5.ELEMENT_NODE) {4499return;4500}45014502var doc = element.ownerDocument,4503camelizedProperty = stylePropertyMapping[property] || camelize(property),4504style = element.style,4505currentStyle = element.currentStyle,4506styleValue = style[camelizedProperty];4507if (styleValue) {4508return styleValue;4509}45104511// currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant4512// window.getComputedStyle, since it returns css property values in their original unit:4513// If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle4514// gives you the original "50%".4515// Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio4516if (currentStyle) {4517try {4518return currentStyle[camelizedProperty];4519} catch(e) {4520//ie will occasionally fail for unknown reasons. swallowing exception4521}4522}45234524var win = doc.defaultView || doc.parentWindow,4525needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",4526originalOverflow,4527returnValue;45284529if (win.getComputedStyle) {4530// Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars4531// therfore we remove and restore the scrollbar and calculate the value in between4532if (needsOverflowReset) {4533originalOverflow = style.overflow;4534style.overflow = "hidden";4535}4536returnValue = win.getComputedStyle(element, null).getPropertyValue(property);4537if (needsOverflowReset) {4538style.overflow = originalOverflow || "";4539}4540return returnValue;4541}4542}4543};4544};4545})();/**4546* High performant way to check whether an element with a specific tag name is in the given document4547* Optimized for being heavily executed4548* Unleashes the power of live node lists4549*4550* @param {Object} doc The document object of the context where to check4551* @param {String} tagName Upper cased tag name4552* @example4553* wysihtml5.dom.hasElementWithTagName(document, "IMG");4554*/4555wysihtml5.dom.hasElementWithTagName = (function() {4556var LIVE_CACHE = {},4557DOCUMENT_IDENTIFIER = 1;45584559function _getDocumentIdentifier(doc) {4560return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);4561}45624563return function(doc, tagName) {4564var key = _getDocumentIdentifier(doc) + ":" + tagName,4565cacheEntry = LIVE_CACHE[key];4566if (!cacheEntry) {4567cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);4568}45694570return cacheEntry.length > 0;4571};4572})();/**4573* High performant way to check whether an element with a specific class name is in the given document4574* Optimized for being heavily executed4575* Unleashes the power of live node lists4576*4577* @param {Object} doc The document object of the context where to check4578* @param {String} tagName Upper cased tag name4579* @example4580* wysihtml5.dom.hasElementWithClassName(document, "foobar");4581*/4582(function(wysihtml5) {4583var LIVE_CACHE = {},4584DOCUMENT_IDENTIFIER = 1;45854586function _getDocumentIdentifier(doc) {4587return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);4588}45894590wysihtml5.dom.hasElementWithClassName = function(doc, className) {4591// getElementsByClassName is not supported by IE<94592// but is sometimes mocked via library code (which then doesn't return live node lists)4593if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) {4594return !!doc.querySelector("." + className);4595}45964597var key = _getDocumentIdentifier(doc) + ":" + className,4598cacheEntry = LIVE_CACHE[key];4599if (!cacheEntry) {4600cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);4601}46024603return cacheEntry.length > 0;4604};4605})(wysihtml5);4606wysihtml5.dom.insert = function(elementToInsert) {4607return {4608after: function(element) {4609element.parentNode.insertBefore(elementToInsert, element.nextSibling);4610},46114612before: function(element) {4613element.parentNode.insertBefore(elementToInsert, element);4614},46154616into: function(element) {4617element.appendChild(elementToInsert);4618}4619};4620};wysihtml5.dom.insertCSS = function(rules) {4621rules = rules.join("\n");46224623return {4624into: function(doc) {4625var head = doc.head || doc.getElementsByTagName("head")[0],4626styleElement = doc.createElement("style");46274628styleElement.type = "text/css";46294630if (styleElement.styleSheet) {4631styleElement.styleSheet.cssText = rules;4632} else {4633styleElement.appendChild(doc.createTextNode(rules));4634}46354636if (head) {4637head.appendChild(styleElement);4638}4639}4640};4641};/**4642* Method to set dom events4643*4644* @example4645* wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });4646*/4647wysihtml5.dom.observe = function(element, eventNames, handler) {4648eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;46494650var handlerWrapper,4651eventName,4652i = 0,4653length = eventNames.length;46544655for (; i<length; i++) {4656eventName = eventNames[i];4657if (element.addEventListener) {4658element.addEventListener(eventName, handler, false);4659} else {4660handlerWrapper = function(event) {4661if (!("target" in event)) {4662event.target = event.srcElement;4663}4664event.preventDefault = event.preventDefault || function() {4665this.returnValue = false;4666};4667event.stopPropagation = event.stopPropagation || function() {4668this.cancelBubble = true;4669};4670handler.call(element, event);4671};4672element.attachEvent("on" + eventName, handlerWrapper);4673}4674}46754676return {4677stop: function() {4678var eventName,4679i = 0,4680length = eventNames.length;4681for (; i<length; i++) {4682eventName = eventNames[i];4683if (element.removeEventListener) {4684element.removeEventListener(eventName, handler, false);4685} else {4686element.detachEvent("on" + eventName, handlerWrapper);4687}4688}4689}4690};4691};4692/**4693* HTML Sanitizer4694* Rewrites the HTML based on given rules4695*4696* @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized4697* @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will4698* be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the4699* desired substitution.4700* @param {Object} context Document object in which to parse the html, needed to sandbox the parsing4701*4702* @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.4703*4704* @example4705* var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';4706* wysihtml5.dom.parse(userHTML, {4707* tags {4708* p: "div", // Rename p tags to div tags4709* font: "span" // Rename font tags to span tags4710* div: true, // Keep them, also possible (same result when passing: "div" or true)4711* script: undefined // Remove script elements4712* }4713* });4714* // => <div><div><span>foo bar</span></div></div>4715*4716* var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';4717* wysihtml5.dom.parse(userHTML);4718* // => '<span><span><span><span>I'm a table!</span></span></span></span>'4719*4720* var userHTML = '<div>foobar<br>foobar</div>';4721* wysihtml5.dom.parse(userHTML, {4722* tags: {4723* div: undefined,4724* br: true4725* }4726* });4727* // => ''4728*4729* var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';4730* wysihtml5.dom.parse(userHTML, {4731* classes: {4732* red: 1,4733* green: 14734* },4735* tags: {4736* div: {4737* rename_tag: "p"4738* }4739* }4740* });4741* // => '<p class="red">foo</p><p>bar</p>'4742*/4743wysihtml5.dom.parse = (function() {47444745/**4746* It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML4747* new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the4748* node isn't closed4749*4750* Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.4751*/4752var NODE_TYPE_MAPPING = {4753"1": _handleElement,4754"3": _handleText4755},4756// Rename unknown tags to this4757DEFAULT_NODE_NAME = "span",4758WHITE_SPACE_REG_EXP = /\s+/,4759defaultRules = { tags: {}, classes: {} },4760currentRules = {};47614762/**4763* Iterates over all childs of the element, recreates them, appends them into a document fragment4764* which later replaces the entire body content4765*/4766function parse(elementOrHtml, rules, context, cleanUp) {4767wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules).get();47684769context = context || elementOrHtml.ownerDocument || document;4770var fragment = context.createDocumentFragment(),4771isString = typeof(elementOrHtml) === "string",4772element,4773newNode,4774firstChild;47754776if (isString) {4777element = wysihtml5.dom.getAsDom(elementOrHtml, context);4778} else {4779element = elementOrHtml;4780}47814782while (element.firstChild) {4783firstChild = element.firstChild;4784element.removeChild(firstChild);4785newNode = _convert(firstChild, cleanUp);4786if (newNode) {4787fragment.appendChild(newNode);4788}4789}47904791// Clear element contents4792element.innerHTML = "";47934794// Insert new DOM tree4795element.appendChild(fragment);47964797return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element;4798}47994800function _convert(oldNode, cleanUp) {4801var oldNodeType = oldNode.nodeType,4802oldChilds = oldNode.childNodes,4803oldChildsLength = oldChilds.length,4804newNode,4805method = NODE_TYPE_MAPPING[oldNodeType],4806i = 0;48074808newNode = method && method(oldNode);48094810if (!newNode) {4811return null;4812}48134814for (i=0; i<oldChildsLength; i++) {4815newChild = _convert(oldChilds[i], cleanUp);4816if (newChild) {4817newNode.appendChild(newChild);4818}4819}48204821// Cleanup senseless <span> elements4822if (cleanUp &&4823newNode.childNodes.length <= 1 &&4824newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&4825!newNode.attributes.length) {4826return newNode.firstChild;4827}48284829return newNode;4830}48314832function _handleElement(oldNode) {4833var rule,4834newNode,4835endTag,4836tagRules = currentRules.tags,4837nodeName = oldNode.nodeName.toLowerCase(),4838scopeName = oldNode.scopeName;48394840/**4841* We already parsed that element4842* ignore it! (yes, this sometimes happens in IE8 when the html is invalid)4843*/4844if (oldNode._wysihtml5) {4845return null;4846}4847oldNode._wysihtml5 = 1;48484849if (oldNode.className === "wysihtml5-temp") {4850return null;4851}48524853/**4854* IE is the only browser who doesn't include the namespace in the4855* nodeName, that's why we have to prepend it by ourselves4856* scopeName is a proprietary IE feature4857* read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx4858*/4859if (scopeName && scopeName != "HTML") {4860nodeName = scopeName + ":" + nodeName;4861}48624863/**4864* Repair node4865* IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags4866* A <p> doesn't need to be closed according HTML4-5 spec, we simply replace it with a <div> to preserve its content and layout4867*/4868if ("outerHTML" in oldNode) {4869if (!wysihtml5.browser.autoClosesUnclosedTags() &&4870oldNode.nodeName === "P" &&4871oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {4872nodeName = "div";4873}4874}48754876if (nodeName in tagRules) {4877rule = tagRules[nodeName];4878if (!rule || rule.remove) {4879return null;4880}48814882rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;4883} else if (oldNode.firstChild) {4884rule = { rename_tag: DEFAULT_NODE_NAME };4885} else {4886// Remove empty unknown elements4887return null;4888}48894890newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName);4891_handleAttributes(oldNode, newNode, rule);48924893oldNode = null;4894return newNode;4895}48964897function _handleAttributes(oldNode, newNode, rule) {4898var attributes = {}, // fresh new set of attributes to set on newNode4899setClass = rule.set_class, // classes to set4900addClass = rule.add_class, // add classes based on existing attributes4901setAttributes = rule.set_attributes, // attributes to set on the current node4902checkAttributes = rule.check_attributes, // check/convert values of attributes4903allowedClasses = currentRules.classes,4904i = 0,4905classes = [],4906newClasses = [],4907newUniqueClasses = [],4908oldClasses = [],4909classesLength,4910newClassesLength,4911currentClass,4912newClass,4913attributeName,4914newAttributeValue,4915method;49164917if (setAttributes) {4918attributes = wysihtml5.lang.object(setAttributes).clone();4919}49204921if (checkAttributes) {4922for (attributeName in checkAttributes) {4923method = attributeCheckMethods[checkAttributes[attributeName]];4924if (!method) {4925continue;4926}4927newAttributeValue = method(_getAttribute(oldNode, attributeName));4928if (typeof(newAttributeValue) === "string") {4929attributes[attributeName] = newAttributeValue;4930}4931}4932}49334934if (setClass) {4935classes.push(setClass);4936}49374938if (addClass) {4939for (attributeName in addClass) {4940method = addClassMethods[addClass[attributeName]];4941if (!method) {4942continue;4943}4944newClass = method(_getAttribute(oldNode, attributeName));4945if (typeof(newClass) === "string") {4946classes.push(newClass);4947}4948}4949}49504951// make sure that wysihtml5 temp class doesn't get stripped out4952allowedClasses["_wysihtml5-temp-placeholder"] = 1;49534954// add old classes last4955oldClasses = oldNode.getAttribute("class");4956if (oldClasses) {4957classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));4958}4959classesLength = classes.length;4960for (; i<classesLength; i++) {4961currentClass = classes[i];4962if (allowedClasses[currentClass]) {4963newClasses.push(currentClass);4964}4965}49664967// remove duplicate entries and preserve class specificity4968newClassesLength = newClasses.length;4969while (newClassesLength--) {4970currentClass = newClasses[newClassesLength];4971if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) {4972newUniqueClasses.unshift(currentClass);4973}4974}49754976if (newUniqueClasses.length) {4977attributes["class"] = newUniqueClasses.join(" ");4978}49794980// set attributes on newNode4981for (attributeName in attributes) {4982// Setting attributes can cause a js error in IE under certain circumstances4983// eg. on a <img> under https when it's new attribute value is non-https4984// TODO: Investigate this further and check for smarter handling4985try {4986newNode.setAttribute(attributeName, attributes[attributeName]);4987} catch(e) {}4988}49894990// IE8 sometimes loses the width/height attributes when those are set before the "src"4991// so we make sure to set them again4992if (attributes.src) {4993if (typeof(attributes.width) !== "undefined") {4994newNode.setAttribute("width", attributes.width);4995}4996if (typeof(attributes.height) !== "undefined") {4997newNode.setAttribute("height", attributes.height);4998}4999}5000}50015002/**5003* IE gives wrong results for hasAttribute/getAttribute, for example:5004* var td = document.createElement("td");5005* td.getAttribute("rowspan"); // => "1" in IE5006*5007* Therefore we have to check the element's outerHTML for the attribute5008*/5009var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly();5010function _getAttribute(node, attributeName) {5011attributeName = attributeName.toLowerCase();5012var nodeName = node.nodeName;5013if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) {5014// Get 'src' attribute value via object property since this will always contain the5015// full absolute url (http://...)5016// this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host5017// will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)5018return node.src;5019} else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {5020// Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML5021var outerHTML = node.outerHTML.toLowerCase(),5022// TODO: This might not work for attributes without value: <input disabled>5023hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1;50245025return hasAttribute ? node.getAttribute(attributeName) : null;5026} else{5027return node.getAttribute(attributeName);5028}5029}50305031/**5032* Check whether the given node is a proper loaded image5033* FIXME: Returns undefined when unknown (Chrome, Safari)5034*/5035function _isLoadedImage(node) {5036try {5037return node.complete && !node.mozMatchesSelector(":-moz-broken");5038} catch(e) {5039if (node.complete && node.readyState === "complete") {5040return true;5041}5042}5043}50445045function _handleText(oldNode) {5046return oldNode.ownerDocument.createTextNode(oldNode.data);5047}504850495050// ------------ attribute checks ------------ \\5051var attributeCheckMethods = {5052url: (function() {5053var REG_EXP = /^https?:\/\//i;5054return function(attributeValue) {5055if (!attributeValue || !attributeValue.match(REG_EXP)) {5056return null;5057}5058return attributeValue.replace(REG_EXP, function(match) {5059return match.toLowerCase();5060});5061};5062})(),50635064alt: (function() {5065var REG_EXP = /[^ a-z0-9_\-]/gi;5066return function(attributeValue) {5067if (!attributeValue) {5068return "";5069}5070return attributeValue.replace(REG_EXP, "");5071};5072})(),50735074numbers: (function() {5075var REG_EXP = /\D/g;5076return function(attributeValue) {5077attributeValue = (attributeValue || "").replace(REG_EXP, "");5078return attributeValue || null;5079};5080})()5081};50825083// ------------ class converter (converts an html attribute to a class name) ------------ \\5084var addClassMethods = {5085align_img: (function() {5086var mapping = {5087left: "wysiwyg-float-left",5088right: "wysiwyg-float-right"5089};5090return function(attributeValue) {5091return mapping[String(attributeValue).toLowerCase()];5092};5093})(),50945095align_text: (function() {5096var mapping = {5097left: "wysiwyg-text-align-left",5098right: "wysiwyg-text-align-right",5099center: "wysiwyg-text-align-center",5100justify: "wysiwyg-text-align-justify"5101};5102return function(attributeValue) {5103return mapping[String(attributeValue).toLowerCase()];5104};5105})(),51065107clear_br: (function() {5108var mapping = {5109left: "wysiwyg-clear-left",5110right: "wysiwyg-clear-right",5111both: "wysiwyg-clear-both",5112all: "wysiwyg-clear-both"5113};5114return function(attributeValue) {5115return mapping[String(attributeValue).toLowerCase()];5116};5117})(),51185119size_font: (function() {5120var mapping = {5121"1": "wysiwyg-font-size-xx-small",5122"2": "wysiwyg-font-size-small",5123"3": "wysiwyg-font-size-medium",5124"4": "wysiwyg-font-size-large",5125"5": "wysiwyg-font-size-x-large",5126"6": "wysiwyg-font-size-xx-large",5127"7": "wysiwyg-font-size-xx-large",5128"-": "wysiwyg-font-size-smaller",5129"+": "wysiwyg-font-size-larger"5130};5131return function(attributeValue) {5132return mapping[String(attributeValue).charAt(0)];5133};5134})()5135};51365137return parse;5138})();/**5139* Checks for empty text node childs and removes them5140*5141* @param {Element} node The element in which to cleanup5142* @example5143* wysihtml5.dom.removeEmptyTextNodes(element);5144*/5145wysihtml5.dom.removeEmptyTextNodes = function(node) {5146var childNode,5147childNodes = wysihtml5.lang.array(node.childNodes).get(),5148childNodesLength = childNodes.length,5149i = 0;5150for (; i<childNodesLength; i++) {5151childNode = childNodes[i];5152if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {5153childNode.parentNode.removeChild(childNode);5154}5155}5156};5157/**5158* Renames an element (eg. a <div> to a <p>) and keeps its childs5159*5160* @param {Element} element The list element which should be renamed5161* @param {Element} newNodeName The desired tag name5162*5163* @example5164* <!-- Assume the following dom: -->5165* <ul id="list">5166* <li>eminem</li>5167* <li>dr. dre</li>5168* <li>50 Cent</li>5169* </ul>5170*5171* <script>5172* wysihtml5.dom.renameElement(document.getElementById("list"), "ol");5173* </script>5174*5175* <!-- Will result in: -->5176* <ol>5177* <li>eminem</li>5178* <li>dr. dre</li>5179* <li>50 Cent</li>5180* </ol>5181*/5182wysihtml5.dom.renameElement = function(element, newNodeName) {5183var newElement = element.ownerDocument.createElement(newNodeName),5184firstChild;5185while (firstChild = element.firstChild) {5186newElement.appendChild(firstChild);5187}5188wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement);5189element.parentNode.replaceChild(newElement, element);5190return newElement;5191};/**5192* Takes an element, removes it and replaces it with it's childs5193*5194* @param {Object} node The node which to replace with it's child nodes5195* @example5196* <div id="foo">5197* <span>hello</span>5198* </div>5199* <script>5200* // Remove #foo and replace with it's children5201* wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo"));5202* </script>5203*/5204wysihtml5.dom.replaceWithChildNodes = function(node) {5205if (!node.parentNode) {5206return;5207}52085209if (!node.firstChild) {5210node.parentNode.removeChild(node);5211return;5212}52135214var fragment = node.ownerDocument.createDocumentFragment();5215while (node.firstChild) {5216fragment.appendChild(node.firstChild);5217}5218node.parentNode.replaceChild(fragment, node);5219node = fragment = null;5220};5221/**5222* Unwraps an unordered/ordered list5223*5224* @param {Element} element The list element which should be unwrapped5225*5226* @example5227* <!-- Assume the following dom: -->5228* <ul id="list">5229* <li>eminem</li>5230* <li>dr. dre</li>5231* <li>50 Cent</li>5232* </ul>5233*5234* <script>5235* wysihtml5.dom.resolveList(document.getElementById("list"));5236* </script>5237*5238* <!-- Will result in: -->5239* eminem<br>5240* dr. dre<br>5241* 50 Cent<br>5242*/5243(function(dom) {5244function _isBlockElement(node) {5245return dom.getStyle("display").from(node) === "block";5246}52475248function _isLineBreak(node) {5249return node.nodeName === "BR";5250}52515252function _appendLineBreak(element) {5253var lineBreak = element.ownerDocument.createElement("br");5254element.appendChild(lineBreak);5255}52565257function resolveList(list) {5258if (list.nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") {5259return;5260}52615262var doc = list.ownerDocument,5263fragment = doc.createDocumentFragment(),5264previousSibling = list.previousElementSibling || list.previousSibling,5265firstChild,5266lastChild,5267isLastChild,5268shouldAppendLineBreak,5269listItem;52705271if (previousSibling && !_isBlockElement(previousSibling)) {5272_appendLineBreak(fragment);5273}52745275while (listItem = list.firstChild) {5276lastChild = listItem.lastChild;5277while (firstChild = listItem.firstChild) {5278isLastChild = firstChild === lastChild;5279// This needs to be done before appending it to the fragment, as it otherwise will loose style information5280shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);5281fragment.appendChild(firstChild);5282if (shouldAppendLineBreak) {5283_appendLineBreak(fragment);5284}5285}52865287listItem.parentNode.removeChild(listItem);5288}5289list.parentNode.replaceChild(fragment, list);5290}52915292dom.resolveList = resolveList;5293})(wysihtml5.dom);/**5294* Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way5295*5296* Browser Compatibility:5297* - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"5298* - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)5299*5300* Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:5301* - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")5302* - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)5303* - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire5304* - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe5305* can do anything as if the sandbox attribute wasn't set5306*5307* @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready5308* @param {Object} [config] Optional parameters5309*5310* @example5311* new wysihtml5.dom.Sandbox(function(sandbox) {5312* sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';5313* });5314*/5315(function(wysihtml5) {5316var /**5317* Default configuration5318*/5319doc = document,5320/**5321* Properties to unset/protect on the window object5322*/5323windowProperties = [5324"parent", "top", "opener", "frameElement", "frames",5325"localStorage", "globalStorage", "sessionStorage", "indexedDB"5326],5327/**5328* Properties on the window object which are set to an empty function5329*/5330windowProperties2 = [5331"open", "close", "openDialog", "showModalDialog",5332"alert", "confirm", "prompt",5333"openDatabase", "postMessage",5334"XMLHttpRequest", "XDomainRequest"5335],5336/**5337* Properties to unset/protect on the document object5338*/5339documentProperties = [5340"referrer",5341"write", "open", "close"5342];53435344wysihtml5.dom.Sandbox = Base.extend(5345/** @scope wysihtml5.dom.Sandbox.prototype */ {53465347constructor: function(readyCallback, config) {5348this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;5349this.config = wysihtml5.lang.object({}).merge(config).get();5350this.iframe = this._createIframe();5351},53525353insertInto: function(element) {5354if (typeof(element) === "string") {5355element = doc.getElementById(element);5356}53575358element.appendChild(this.iframe);5359},53605361getIframe: function() {5362return this.iframe;5363},53645365getWindow: function() {5366this._readyError();5367},53685369getDocument: function() {5370this._readyError();5371},53725373destroy: function() {5374var iframe = this.getIframe();5375iframe.parentNode.removeChild(iframe);5376},53775378_readyError: function() {5379throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");5380},53815382/**5383* Creates the sandbox iframe5384*5385* Some important notes:5386* - We can't use HTML5 sandbox for now:5387* setting it causes that the iframe's dom can't be accessed from the outside5388* Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom5389* But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.5390* In order to make this happen we need to set the "allow-scripts" flag.5391* A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.5392* - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)5393* - IE needs to have the security="restricted" attribute set before the iframe is5394* inserted into the dom tree5395* - Believe it or not but in IE "security" in document.createElement("iframe") is false, even5396* though it supports it5397* - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore5398* - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely5399* on the onreadystatechange event5400*/5401_createIframe: function() {5402var that = this,5403iframe = doc.createElement("iframe");5404iframe.className = "wysihtml5-sandbox";5405wysihtml5.dom.setAttributes({5406"security": "restricted",5407"allowtransparency": "true",5408"frameborder": 0,5409"width": 0,5410"height": 0,5411"marginwidth": 0,5412"marginheight": 05413}).on(iframe);54145415// Setting the src like this prevents ssl warnings in IE65416if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {5417iframe.src = "javascript:'<html></html>'";5418}54195420iframe.onload = function() {5421iframe.onreadystatechange = iframe.onload = null;5422that._onLoadIframe(iframe);5423};54245425iframe.onreadystatechange = function() {5426if (/loaded|complete/.test(iframe.readyState)) {5427iframe.onreadystatechange = iframe.onload = null;5428that._onLoadIframe(iframe);5429}5430};54315432return iframe;5433},54345435/**5436* Callback for when the iframe has finished loading5437*/5438_onLoadIframe: function(iframe) {5439// don't resume when the iframe got unloaded (eg. by removing it from the dom)5440if (!wysihtml5.dom.contains(doc.documentElement, iframe)) {5441return;5442}54435444var that = this,5445iframeWindow = iframe.contentWindow,5446iframeDocument = iframe.contentWindow.document,5447charset = doc.characterSet || doc.charset || "utf-8",5448sandboxHtml = this._getHtml({5449charset: charset,5450stylesheets: this.config.stylesheets5451});54525453// Create the basic dom tree including proper DOCTYPE and charset5454iframeDocument.open("text/html", "replace");5455iframeDocument.write(sandboxHtml);5456iframeDocument.close();54575458this.getWindow = function() { return iframe.contentWindow; };5459this.getDocument = function() { return iframe.contentWindow.document; };54605461// Catch js errors and pass them to the parent's onerror event5462// addEventListener("error") doesn't work properly in some browsers5463// TODO: apparently this doesn't work in IE9!5464iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {5465throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);5466};54675468if (!wysihtml5.browser.supportsSandboxedIframes()) {5469// Unset a bunch of sensitive variables5470// Please note: This isn't hack safe!5471// It more or less just takes care of basic attacks and prevents accidental theft of sensitive information5472// IE is secure though, which is the most important thing, since IE is the only browser, who5473// takes over scripts & styles into contentEditable elements when copied from external websites5474// or applications (Microsoft Word, ...)5475var i, length;5476for (i=0, length=windowProperties.length; i<length; i++) {5477this._unset(iframeWindow, windowProperties[i]);5478}5479for (i=0, length=windowProperties2.length; i<length; i++) {5480this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION);5481}5482for (i=0, length=documentProperties.length; i<length; i++) {5483this._unset(iframeDocument, documentProperties[i]);5484}5485// This doesn't work in Safari 55486// See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit5487this._unset(iframeDocument, "cookie", "", true);5488}54895490this.loaded = true;54915492// Trigger the callback5493setTimeout(function() { that.callback(that); }, 0);5494},54955496_getHtml: function(templateVars) {5497var stylesheets = templateVars.stylesheets,5498html = "",5499i = 0,5500length;5501stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;5502if (stylesheets) {5503length = stylesheets.length;5504for (; i<length; i++) {5505html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';5506}5507}5508templateVars.stylesheets = html;55095510return wysihtml5.lang.string(5511'<!DOCTYPE html><html><head>'5512+ '<meta charset="#{charset}">#{stylesheets}</head>'5513+ '<body></body></html>'5514).interpolate(templateVars);5515},55165517/**5518* Method to unset/override existing variables5519* @example5520* // Make cookie unreadable and unwritable5521* this._unset(document, "cookie", "", true);5522*/5523_unset: function(object, property, value, setter) {5524try { object[property] = value; } catch(e) {}55255526try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}5527if (setter) {5528try { object.__defineSetter__(property, function() {}); } catch(e) {}5529}55305531if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {5532try {5533var config = {5534get: function() { return value; }5535};5536if (setter) {5537config.set = function() {};5538}5539Object.defineProperty(object, property, config);5540} catch(e) {}5541}5542}5543});5544})(wysihtml5);5545(function() {5546var mapping = {5547"className": "class"5548};5549wysihtml5.dom.setAttributes = function(attributes) {5550return {5551on: function(element) {5552for (var i in attributes) {5553element.setAttribute(mapping[i] || i, attributes[i]);5554}5555}5556}5557};5558})();wysihtml5.dom.setStyles = function(styles) {5559return {5560on: function(element) {5561var style = element.style;5562if (typeof(styles) === "string") {5563style.cssText += ";" + styles;5564return;5565}5566for (var i in styles) {5567if (i === "float") {5568style.cssFloat = styles[i];5569style.styleFloat = styles[i];5570} else {5571style[i] = styles[i];5572}5573}5574}5575};5576};/**5577* Simulate HTML5 placeholder attribute5578*5579* Needed since5580* - div[contentEditable] elements don't support it5581* - older browsers (such as IE8 and Firefox 3.6) don't support it at all5582*5583* @param {Object} parent Instance of main wysihtml5.Editor class5584* @param {Element} view Instance of wysihtml5.views.* class5585* @param {String} placeholderText5586*5587* @example5588* wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");5589*/5590(function(dom) {5591dom.simulatePlaceholder = function(editor, view, placeholderText) {5592var CLASS_NAME = "placeholder",5593unset = function() {5594if (view.hasPlaceholderSet()) {5595view.clear();5596}5597dom.removeClass(view.element, CLASS_NAME);5598},5599set = function() {5600if (view.isEmpty()) {5601view.setValue(placeholderText);5602dom.addClass(view.element, CLASS_NAME);5603}5604};56055606editor5607.observe("set_placeholder", set)5608.observe("unset_placeholder", unset)5609.observe("focus:composer", unset)5610.observe("paste:composer", unset)5611.observe("blur:composer", set);56125613set();5614};5615})(wysihtml5.dom);5616(function(dom) {5617var documentElement = document.documentElement;5618if ("textContent" in documentElement) {5619dom.setTextContent = function(element, text) {5620element.textContent = text;5621};56225623dom.getTextContent = function(element) {5624return element.textContent;5625};5626} else if ("innerText" in documentElement) {5627dom.setTextContent = function(element, text) {5628element.innerText = text;5629};56305631dom.getTextContent = function(element) {5632return element.innerText;5633};5634} else {5635dom.setTextContent = function(element, text) {5636element.nodeValue = text;5637};56385639dom.getTextContent = function(element) {5640return element.nodeValue;5641};5642}5643})(wysihtml5.dom);56445645/**5646* Fix most common html formatting misbehaviors of browsers implementation when inserting5647* content via copy & paste contentEditable5648*5649* @author Christopher Blum5650*/5651wysihtml5.quirks.cleanPastedHTML = (function() {5652// TODO: We probably need more rules here5653var defaultRules = {5654// When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling5655"a u": wysihtml5.dom.replaceWithChildNodes5656};56575658function cleanPastedHTML(elementOrHtml, rules, context) {5659rules = rules || defaultRules;5660context = context || elementOrHtml.ownerDocument || document;56615662var element,5663isString = typeof(elementOrHtml) === "string",5664method,5665matches,5666matchesLength,5667i,5668j = 0;5669if (isString) {5670element = wysihtml5.dom.getAsDom(elementOrHtml, context);5671} else {5672element = elementOrHtml;5673}56745675for (i in rules) {5676matches = element.querySelectorAll(i);5677method = rules[i];5678matchesLength = matches.length;5679for (; j<matchesLength; j++) {5680method(matches[j]);5681}5682}56835684matches = elementOrHtml = rules = null;56855686return isString ? element.innerHTML : element;5687}56885689return cleanPastedHTML;5690})();/**5691* IE and Opera leave an empty paragraph in the contentEditable element after clearing it5692*5693* @param {Object} contentEditableElement The contentEditable element to observe for clearing events5694* @exaple5695* wysihtml5.quirks.ensureProperClearing(myContentEditableElement);5696*/5697(function(wysihtml5) {5698var dom = wysihtml5.dom;56995700wysihtml5.quirks.ensureProperClearing = (function() {5701var clearIfNecessary = function(event) {5702var element = this;5703setTimeout(function() {5704var innerHTML = element.innerHTML.toLowerCase();5705if (innerHTML == "<p> </p>" ||5706innerHTML == "<p> </p><p> </p>") {5707element.innerHTML = "";5708}5709}, 0);5710};57115712return function(composer) {5713dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);5714};5715})();5716571757185719/**5720* In Opera when the caret is in the first and only item of a list (<ul><li>|</li></ul>) and the list is the first child of the contentEditable element, it's impossible to delete the list by hitting backspace5721*5722* @param {Object} contentEditableElement The contentEditable element to observe for clearing events5723* @exaple5724* wysihtml5.quirks.ensureProperClearing(myContentEditableElement);5725*/5726wysihtml5.quirks.ensureProperClearingOfLists = (function() {5727var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"];57285729var clearIfNecessary = function(element, contentEditableElement) {5730if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) {5731return;5732}57335734var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI });5735if (!list) {5736return;5737}57385739var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild;5740if (!listIsFirstChildOfContentEditable) {5741return;5742}57435744var hasOnlyOneListItem = list.childNodes.length <= 1;5745if (!hasOnlyOneListItem) {5746return;5747}57485749var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true;5750if (!onlyListItemIsEmpty) {5751return;5752}57535754list.parentNode.removeChild(list);5755};57565757return function(composer) {5758dom.observe(composer.element, "keydown", function(event) {5759if (event.keyCode !== wysihtml5.BACKSPACE_KEY) {5760return;5761}57625763var element = composer.selection.getSelectedNode();5764clearIfNecessary(element, composer.element);5765});5766};5767})();57685769})(wysihtml5);5770// See https://bugzilla.mozilla.org/show_bug.cgi?id=6643985771//5772// In Firefox this:5773// var d = document.createElement("div");5774// d.innerHTML ='<a href="~"></a>';5775// d.innerHTML;5776// will result in:5777// <a href="%7E"></a>5778// which is wrong5779(function(wysihtml5) {5780var TILDE_ESCAPED = "%7E";5781wysihtml5.quirks.getCorrectInnerHTML = function(element) {5782var innerHTML = element.innerHTML;5783if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {5784return innerHTML;5785}57865787var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),5788url,5789urlToSearch,5790length,5791i;5792for (i=0, length=elementsWithTilde.length; i<length; i++) {5793url = elementsWithTilde[i].href || elementsWithTilde[i].src;5794urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED);5795innerHTML = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url);5796}5797return innerHTML;5798};5799})(wysihtml5);/**5800* Some browsers don't insert line breaks when hitting return in a contentEditable element5801* - Opera & IE insert new <p> on return5802* - Chrome & Safari insert new <div> on return5803* - Firefox inserts <br> on return (yippie!)5804*5805* @param {Element} element5806*5807* @example5808* wysihtml5.quirks.insertLineBreakOnReturn(element);5809*/5810(function(wysihtml5) {5811var dom = wysihtml5.dom,5812USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"],5813LIST_TAGS = ["UL", "OL", "MENU"];58145815wysihtml5.quirks.insertLineBreakOnReturn = function(composer) {5816function unwrap(selectedNode) {5817var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2);5818if (!parentElement) {5819return;5820}58215822var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE);5823dom.insert(invisibleSpace).before(parentElement);5824dom.replaceWithChildNodes(parentElement);5825composer.selection.selectNode(invisibleSpace);5826}58275828function keyDown(event) {5829var keyCode = event.keyCode;5830if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) {5831return;5832}58335834var element = event.target,5835selectedNode = composer.selection.getSelectedNode(),5836blockElement = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4);5837if (blockElement) {5838// Some browsers create <p> elements after leaving a list5839// check after keydown of backspace and return whether a <p> got inserted and unwrap it5840if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) {5841setTimeout(function() {5842var selectedNode = composer.selection.getSelectedNode(),5843list,5844div;5845if (!selectedNode) {5846return;5847}58485849list = dom.getParentElement(selectedNode, {5850nodeName: LIST_TAGS5851}, 2);58525853if (list) {5854return;5855}58565857unwrap(selectedNode);5858}, 0);5859} else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) {5860setTimeout(function() {5861unwrap(composer.selection.getSelectedNode());5862}, 0);5863}5864return;5865}58665867if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) {5868composer.commands.exec("insertLineBreak");5869event.preventDefault();5870}5871}58725873// keypress doesn't fire when you hit backspace5874dom.observe(composer.element.ownerDocument, "keydown", keyDown);5875};5876})(wysihtml5);/**5877* Force rerendering of a given element5878* Needed to fix display misbehaviors of IE5879*5880* @param {Element} element The element object which needs to be rerendered5881* @example5882* wysihtml5.quirks.redraw(document.body);5883*/5884(function(wysihtml5) {5885var CLASS_NAME = "wysihtml5-quirks-redraw";58865887wysihtml5.quirks.redraw = function(element) {5888wysihtml5.dom.addClass(element, CLASS_NAME);5889wysihtml5.dom.removeClass(element, CLASS_NAME);58905891// Following hack is needed for firefox to make sure that image resize handles are properly removed5892try {5893var doc = element.ownerDocument;5894doc.execCommand("italic", false, null);5895doc.execCommand("italic", false, null);5896} catch(e) {}5897};5898})(wysihtml5);/**5899* Selection API5900*5901* @example5902* var selection = new wysihtml5.Selection(editor);5903*/5904(function(wysihtml5) {5905var dom = wysihtml5.dom;59065907function _getCumulativeOffsetTop(element) {5908var top = 0;5909if (element.parentNode) {5910do {5911top += element.offsetTop || 0;5912element = element.offsetParent;5913} while (element);5914}5915return top;5916}59175918wysihtml5.Selection = Base.extend(5919/** @scope wysihtml5.Selection.prototype */ {5920constructor: function(editor) {5921// Make sure that our external range library is initialized5922window.rangy.init();59235924this.editor = editor;5925this.composer = editor.composer;5926this.doc = this.composer.doc;5927},59285929/**5930* Get the current selection as a bookmark to be able to later restore it5931*5932* @return {Object} An object that represents the current selection5933*/5934getBookmark: function() {5935var range = this.getRange();5936return range && range.cloneRange();5937},59385939/**5940* Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark5941*5942* @param {Object} bookmark An object that represents the current selection5943*/5944setBookmark: function(bookmark) {5945if (!bookmark) {5946return;5947}59485949this.setSelection(bookmark);5950},59515952/**5953* Set the caret in front of the given node5954*5955* @param {Object} node The element or text node where to position the caret in front of5956* @example5957* selection.setBefore(myElement);5958*/5959setBefore: function(node) {5960var range = rangy.createRange(this.doc);5961range.setStartBefore(node);5962range.setEndBefore(node);5963return this.setSelection(range);5964},59655966/**5967* Set the caret after the given node5968*5969* @param {Object} node The element or text node where to position the caret in front of5970* @example5971* selection.setBefore(myElement);5972*/5973setAfter: function(node) {5974var range = rangy.createRange(this.doc);5975range.setStartAfter(node);5976range.setEndAfter(node);5977return this.setSelection(range);5978},59795980/**5981* Ability to select/mark nodes5982*5983* @param {Element} node The node/element to select5984* @example5985* selection.selectNode(document.getElementById("my-image"));5986*/5987selectNode: function(node) {5988var range = rangy.createRange(this.doc),5989isElement = node.nodeType === wysihtml5.ELEMENT_NODE,5990canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),5991content = isElement ? node.innerHTML : node.data,5992isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE),5993displayStyle = dom.getStyle("display").from(node),5994isBlockElement = (displayStyle === "block" || displayStyle === "list-item");59955996if (isEmpty && isElement && canHaveHTML) {5997// Make sure that caret is visible in node by inserting a zero width no breaking space5998try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}5999}60006001if (canHaveHTML) {6002range.selectNodeContents(node);6003} else {6004range.selectNode(node);6005}60066007if (canHaveHTML && isEmpty && isElement) {6008range.collapse(isBlockElement);6009} else if (canHaveHTML && isEmpty) {6010range.setStartAfter(node);6011range.setEndAfter(node);6012}60136014this.setSelection(range);6015},60166017/**6018* Get the node which contains the selection6019*6020* @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"6021* @return {Object} The node that contains the caret6022* @example6023* var nodeThatContainsCaret = selection.getSelectedNode();6024*/6025getSelectedNode: function(controlRange) {6026var selection,6027range;60286029if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {6030range = this.doc.selection.createRange();6031if (range && range.length) {6032return range.item(0);6033}6034}60356036selection = this.getSelection(this.doc);6037if (selection.focusNode === selection.anchorNode) {6038return selection.focusNode;6039} else {6040range = this.getRange(this.doc);6041return range ? range.commonAncestorContainer : this.doc.body;6042}6043},60446045executeAndRestore: function(method, restoreScrollPosition) {6046var body = this.doc.body,6047oldScrollTop = restoreScrollPosition && body.scrollTop,6048oldScrollLeft = restoreScrollPosition && body.scrollLeft,6049className = "_wysihtml5-temp-placeholder",6050placeholderHTML = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>',6051range = this.getRange(this.doc),6052newRange;60536054// Nothing selected, execute and say goodbye6055if (!range) {6056method(body, body);6057return;6058}60596060var node = range.createContextualFragment(placeholderHTML);6061range.insertNode(node);60626063// Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder6064try {6065method(range.startContainer, range.endContainer);6066} catch(e3) {6067setTimeout(function() { throw e3; }, 0);6068}60696070caretPlaceholder = this.doc.querySelector("." + className);6071if (caretPlaceholder) {6072newRange = rangy.createRange(this.doc);6073newRange.selectNode(caretPlaceholder);6074newRange.deleteContents();6075this.setSelection(newRange);6076} else {6077// fallback for when all hell breaks loose6078body.focus();6079}60806081if (restoreScrollPosition) {6082body.scrollTop = oldScrollTop;6083body.scrollLeft = oldScrollLeft;6084}60856086// Remove it again, just to make sure that the placeholder is definitely out of the dom tree6087try {6088caretPlaceholder.parentNode.removeChild(caretPlaceholder);6089} catch(e4) {}6090},60916092/**6093* Different approach of preserving the selection (doesn't modify the dom)6094* Takes all text nodes in the selection and saves the selection position in the first and last one6095*/6096executeAndRestoreSimple: function(method) {6097var range = this.getRange(),6098body = this.doc.body,6099newRange,6100firstNode,6101lastNode,6102textNodes,6103rangeBackup;61046105// Nothing selected, execute and say goodbye6106if (!range) {6107method(body, body);6108return;6109}61106111textNodes = range.getNodes([3]);6112firstNode = textNodes[0] || range.startContainer;6113lastNode = textNodes[textNodes.length - 1] || range.endContainer;61146115rangeBackup = {6116collapsed: range.collapsed,6117startContainer: firstNode,6118startOffset: firstNode === range.startContainer ? range.startOffset : 0,6119endContainer: lastNode,6120endOffset: lastNode === range.endContainer ? range.endOffset : lastNode.length6121};61226123try {6124method(range.startContainer, range.endContainer);6125} catch(e) {6126setTimeout(function() { throw e; }, 0);6127}61286129newRange = rangy.createRange(this.doc);6130try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {}6131try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {}6132try { this.setSelection(newRange); } catch(e3) {}6133},61346135/**6136* Insert html at the caret position and move the cursor after the inserted html6137*6138* @param {String} html HTML string to insert6139* @example6140* selection.insertHTML("<p>foobar</p>");6141*/6142insertHTML: function(html) {6143var range = rangy.createRange(this.doc),6144node = range.createContextualFragment(html),6145lastChild = node.lastChild;6146this.insertNode(node);6147if (lastChild) {6148this.setAfter(lastChild);6149}6150},61516152/**6153* Insert a node at the caret position and move the cursor behind it6154*6155* @param {Object} node HTML string to insert6156* @example6157* selection.insertNode(document.createTextNode("foobar"));6158*/6159insertNode: function(node) {6160var range = this.getRange();6161if (range) {6162range.insertNode(node);6163}6164},61656166/**6167* Wraps current selection with the given node6168*6169* @param {Object} node The node to surround the selected elements with6170*/6171surround: function(node) {6172var range = this.getRange();6173if (!range) {6174return;6175}61766177try {6178// This only works when the range boundaries are not overlapping other elements6179range.surroundContents(node);6180this.selectNode(node);6181} catch(e) {6182// fallback6183node.appendChild(range.extractContents());6184range.insertNode(node);6185}6186},61876188/**6189* Scroll the current caret position into the view6190* FIXME: This is a bit hacky, there might be a smarter way of doing this6191*6192* @example6193* selection.scrollIntoView();6194*/6195scrollIntoView: function() {6196var doc = this.doc,6197hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,6198tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() {6199var element = doc.createElement("span");6200// The element needs content in order to be able to calculate it's position properly6201element.innerHTML = wysihtml5.INVISIBLE_SPACE;6202return element;6203})(),6204offsetTop;62056206if (hasScrollBars) {6207this.insertNode(tempElement);6208offsetTop = _getCumulativeOffsetTop(tempElement);6209tempElement.parentNode.removeChild(tempElement);6210if (offsetTop > doc.body.scrollTop) {6211doc.body.scrollTop = offsetTop;6212}6213}6214},62156216/**6217* Select line where the caret is in6218*/6219selectLine: function() {6220if (wysihtml5.browser.supportsSelectionModify()) {6221this._selectLine_W3C();6222} else if (this.doc.selection) {6223this._selectLine_MSIE();6224}6225},62266227/**6228* See https://developer.mozilla.org/en/DOM/Selection/modify6229*/6230_selectLine_W3C: function() {6231var win = this.doc.defaultView,6232selection = win.getSelection();6233selection.modify("extend", "left", "lineboundary");6234selection.modify("extend", "right", "lineboundary");6235},62366237_selectLine_MSIE: function() {6238var range = this.doc.selection.createRange(),6239rangeTop = range.boundingTop,6240rangeHeight = range.boundingHeight,6241scrollWidth = this.doc.body.scrollWidth,6242rangeBottom,6243rangeEnd,6244measureNode,6245i,6246j;62476248if (!range.moveToPoint) {6249return;6250}62516252if (rangeTop === 0) {6253// Don't know why, but when the selection ends at the end of a line6254// range.boundingTop is 06255measureNode = this.doc.createElement("span");6256this.insertNode(measureNode);6257rangeTop = measureNode.offsetTop;6258measureNode.parentNode.removeChild(measureNode);6259}62606261rangeTop += 1;62626263for (i=-10; i<scrollWidth; i+=2) {6264try {6265range.moveToPoint(i, rangeTop);6266break;6267} catch(e1) {}6268}62696270// Investigate the following in order to handle multi line selections6271// rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);6272rangeBottom = rangeTop;6273rangeEnd = this.doc.selection.createRange();6274for (j=scrollWidth; j>=0; j--) {6275try {6276rangeEnd.moveToPoint(j, rangeBottom);6277break;6278} catch(e2) {}6279}62806281range.setEndPoint("EndToEnd", rangeEnd);6282range.select();6283},62846285getText: function() {6286var selection = this.getSelection();6287return selection ? selection.toString() : "";6288},62896290getNodes: function(nodeType, filter) {6291var range = this.getRange();6292if (range) {6293return range.getNodes([nodeType], filter);6294} else {6295return [];6296}6297},62986299getRange: function() {6300var selection = this.getSelection();6301return selection && selection.rangeCount && selection.getRangeAt(0);6302},63036304getSelection: function() {6305return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);6306},63076308setSelection: function(range) {6309var win = this.doc.defaultView || this.doc.parentWindow,6310selection = rangy.getSelection(win);6311return selection.setSingleRange(range);6312}6313});63146315})(wysihtml5);6316/**6317* Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license.6318* http://code.google.com/p/rangy/6319*6320* changed in order to be able ...6321* - to use custom tags6322* - to detect and replace similar css classes via reg exp6323*/6324(function(wysihtml5, rangy) {6325var defaultTagName = "span";63266327var REG_EXP_WHITE_SPACE = /\s+/g;63286329function hasClass(el, cssClass, regExp) {6330if (!el.className) {6331return false;6332}63336334var matchingClassNames = el.className.match(regExp) || [];6335return matchingClassNames[matchingClassNames.length - 1] === cssClass;6336}63376338function addClass(el, cssClass, regExp) {6339if (el.className) {6340removeClass(el, regExp);6341el.className += " " + cssClass;6342} else {6343el.className = cssClass;6344}6345}63466347function removeClass(el, regExp) {6348if (el.className) {6349el.className = el.className.replace(regExp, "");6350}6351}63526353function hasSameClasses(el1, el2) {6354return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");6355}63566357function replaceWithOwnChildren(el) {6358var parent = el.parentNode;6359while (el.firstChild) {6360parent.insertBefore(el.firstChild, el);6361}6362parent.removeChild(el);6363}63646365function elementsHaveSameNonClassAttributes(el1, el2) {6366if (el1.attributes.length != el2.attributes.length) {6367return false;6368}6369for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {6370attr1 = el1.attributes[i];6371name = attr1.name;6372if (name != "class") {6373attr2 = el2.attributes.getNamedItem(name);6374if (attr1.specified != attr2.specified) {6375return false;6376}6377if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {6378return false;6379}6380}6381}6382return true;6383}63846385function isSplitPoint(node, offset) {6386if (rangy.dom.isCharacterDataNode(node)) {6387if (offset == 0) {6388return !!node.previousSibling;6389} else if (offset == node.length) {6390return !!node.nextSibling;6391} else {6392return true;6393}6394}63956396return offset > 0 && offset < node.childNodes.length;6397}63986399function splitNodeAt(node, descendantNode, descendantOffset) {6400var newNode;6401if (rangy.dom.isCharacterDataNode(descendantNode)) {6402if (descendantOffset == 0) {6403descendantOffset = rangy.dom.getNodeIndex(descendantNode);6404descendantNode = descendantNode.parentNode;6405} else if (descendantOffset == descendantNode.length) {6406descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;6407descendantNode = descendantNode.parentNode;6408} else {6409newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);6410}6411}6412if (!newNode) {6413newNode = descendantNode.cloneNode(false);6414if (newNode.id) {6415newNode.removeAttribute("id");6416}6417var child;6418while ((child = descendantNode.childNodes[descendantOffset])) {6419newNode.appendChild(child);6420}6421rangy.dom.insertAfter(newNode, descendantNode);6422}6423return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode));6424}64256426function Merge(firstNode) {6427this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);6428this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;6429this.textNodes = [this.firstTextNode];6430}64316432Merge.prototype = {6433doMerge: function() {6434var textBits = [], textNode, parent, text;6435for (var i = 0, len = this.textNodes.length; i < len; ++i) {6436textNode = this.textNodes[i];6437parent = textNode.parentNode;6438textBits[i] = textNode.data;6439if (i) {6440parent.removeChild(textNode);6441if (!parent.hasChildNodes()) {6442parent.parentNode.removeChild(parent);6443}6444}6445}6446this.firstTextNode.data = text = textBits.join("");6447return text;6448},64496450getLength: function() {6451var i = this.textNodes.length, len = 0;6452while (i--) {6453len += this.textNodes[i].length;6454}6455return len;6456},64576458toString: function() {6459var textBits = [];6460for (var i = 0, len = this.textNodes.length; i < len; ++i) {6461textBits[i] = "'" + this.textNodes[i].data + "'";6462}6463return "[Merge(" + textBits.join(",") + ")]";6464}6465};64666467function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) {6468this.tagNames = tagNames || [defaultTagName];6469this.cssClass = cssClass || "";6470this.similarClassRegExp = similarClassRegExp;6471this.normalize = normalize;6472this.applyToAnyTagName = false;6473}64746475HTMLApplier.prototype = {6476getAncestorWithClass: function(node) {6477var cssClassMatch;6478while (node) {6479cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true;6480if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {6481return node;6482}6483node = node.parentNode;6484}6485return false;6486},64876488// Normalizes nodes after applying a CSS class to a Range.6489postApply: function(textNodes, range) {6490var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];64916492var merges = [], currentMerge;64936494var rangeStartNode = firstNode, rangeEndNode = lastNode;6495var rangeStartOffset = 0, rangeEndOffset = lastNode.length;64966497var textNode, precedingTextNode;64986499for (var i = 0, len = textNodes.length; i < len; ++i) {6500textNode = textNodes[i];6501precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false);6502if (precedingTextNode) {6503if (!currentMerge) {6504currentMerge = new Merge(precedingTextNode);6505merges.push(currentMerge);6506}6507currentMerge.textNodes.push(textNode);6508if (textNode === firstNode) {6509rangeStartNode = currentMerge.firstTextNode;6510rangeStartOffset = rangeStartNode.length;6511}6512if (textNode === lastNode) {6513rangeEndNode = currentMerge.firstTextNode;6514rangeEndOffset = currentMerge.getLength();6515}6516} else {6517currentMerge = null;6518}6519}65206521// Test whether the first node after the range needs merging6522var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);6523if (nextTextNode) {6524if (!currentMerge) {6525currentMerge = new Merge(lastNode);6526merges.push(currentMerge);6527}6528currentMerge.textNodes.push(nextTextNode);6529}65306531// Do the merges6532if (merges.length) {6533for (i = 0, len = merges.length; i < len; ++i) {6534merges[i].doMerge();6535}6536// Set the range boundaries6537range.setStart(rangeStartNode, rangeStartOffset);6538range.setEnd(rangeEndNode, rangeEndOffset);6539}6540},65416542getAdjacentMergeableTextNode: function(node, forward) {6543var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);6544var el = isTextNode ? node.parentNode : node;6545var adjacentNode;6546var propName = forward ? "nextSibling" : "previousSibling";6547if (isTextNode) {6548// Can merge if the node's previous/next sibling is a text node6549adjacentNode = node[propName];6550if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {6551return adjacentNode;6552}6553} else {6554// Compare element with its sibling6555adjacentNode = el[propName];6556if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {6557return adjacentNode[forward ? "firstChild" : "lastChild"];6558}6559}6560return null;6561},65626563areElementsMergeable: function(el1, el2) {6564return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())6565&& rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())6566&& hasSameClasses(el1, el2)6567&& elementsHaveSameNonClassAttributes(el1, el2);6568},65696570createContainer: function(doc) {6571var el = doc.createElement(this.tagNames[0]);6572if (this.cssClass) {6573el.className = this.cssClass;6574}6575return el;6576},65776578applyToTextNode: function(textNode) {6579var parent = textNode.parentNode;6580if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {6581if (this.cssClass) {6582addClass(parent, this.cssClass, this.similarClassRegExp);6583}6584} else {6585var el = this.createContainer(rangy.dom.getDocument(textNode));6586textNode.parentNode.insertBefore(el, textNode);6587el.appendChild(textNode);6588}6589},65906591isRemovable: function(el) {6592return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass;6593},65946595undoToTextNode: function(textNode, range, ancestorWithClass) {6596if (!range.containsNode(ancestorWithClass)) {6597// Split out the portion of the ancestor from which we can remove the CSS class6598var ancestorRange = range.cloneRange();6599ancestorRange.selectNode(ancestorWithClass);66006601if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) {6602splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset);6603range.setEndAfter(ancestorWithClass);6604}6605if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {6606ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset);6607}6608}66096610if (this.similarClassRegExp) {6611removeClass(ancestorWithClass, this.similarClassRegExp);6612}6613if (this.isRemovable(ancestorWithClass)) {6614replaceWithOwnChildren(ancestorWithClass);6615}6616},66176618applyToRange: function(range) {6619var textNodes = range.getNodes([wysihtml5.TEXT_NODE]);6620if (!textNodes.length) {6621try {6622var node = this.createContainer(range.endContainer.ownerDocument);6623range.surroundContents(node);6624this.selectNode(range, node);6625return;6626} catch(e) {}6627}66286629range.splitBoundaries();6630textNodes = range.getNodes([wysihtml5.TEXT_NODE]);66316632if (textNodes.length) {6633var textNode;66346635for (var i = 0, len = textNodes.length; i < len; ++i) {6636textNode = textNodes[i];6637if (!this.getAncestorWithClass(textNode)) {6638this.applyToTextNode(textNode);6639}6640}66416642range.setStart(textNodes[0], 0);6643textNode = textNodes[textNodes.length - 1];6644range.setEnd(textNode, textNode.length);66456646if (this.normalize) {6647this.postApply(textNodes, range);6648}6649}6650},66516652undoToRange: function(range) {6653var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass;6654if (textNodes.length) {6655range.splitBoundaries();6656textNodes = range.getNodes([wysihtml5.TEXT_NODE]);6657} else {6658var doc = range.endContainer.ownerDocument,6659node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);6660range.insertNode(node);6661range.selectNode(node);6662textNodes = [node];6663}66646665for (var i = 0, len = textNodes.length; i < len; ++i) {6666textNode = textNodes[i];6667ancestorWithClass = this.getAncestorWithClass(textNode);6668if (ancestorWithClass) {6669this.undoToTextNode(textNode, range, ancestorWithClass);6670}6671}66726673if (len == 1) {6674this.selectNode(range, textNodes[0]);6675} else {6676range.setStart(textNodes[0], 0);6677textNode = textNodes[textNodes.length - 1];6678range.setEnd(textNode, textNode.length);66796680if (this.normalize) {6681this.postApply(textNodes, range);6682}6683}6684},66856686selectNode: function(range, node) {6687var isElement = node.nodeType === wysihtml5.ELEMENT_NODE,6688canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true,6689content = isElement ? node.innerHTML : node.data,6690isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE);66916692if (isEmpty && isElement && canHaveHTML) {6693// Make sure that caret is visible in node by inserting a zero width no breaking space6694try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}6695}6696range.selectNodeContents(node);6697if (isEmpty && isElement) {6698range.collapse(false);6699} else if (isEmpty) {6700range.setStartAfter(node);6701range.setEndAfter(node);6702}6703},67046705getTextSelectedByRange: function(textNode, range) {6706var textRange = range.cloneRange();6707textRange.selectNodeContents(textNode);67086709var intersectionRange = textRange.intersection(range);6710var text = intersectionRange ? intersectionRange.toString() : "";6711textRange.detach();67126713return text;6714},67156716isAppliedToRange: function(range) {6717var ancestors = [],6718ancestor,6719textNodes = range.getNodes([wysihtml5.TEXT_NODE]);6720if (!textNodes.length) {6721ancestor = this.getAncestorWithClass(range.startContainer);6722return ancestor ? [ancestor] : false;6723}67246725for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {6726selectedText = this.getTextSelectedByRange(textNodes[i], range);6727ancestor = this.getAncestorWithClass(textNodes[i]);6728if (selectedText != "" && !ancestor) {6729return false;6730} else {6731ancestors.push(ancestor);6732}6733}6734return ancestors;6735},67366737toggleRange: function(range) {6738if (this.isAppliedToRange(range)) {6739this.undoToRange(range);6740} else {6741this.applyToRange(range);6742}6743}6744};67456746wysihtml5.selection.HTMLApplier = HTMLApplier;67476748})(wysihtml5, rangy);/**6749* Rich Text Query/Formatting Commands6750*6751* @example6752* var commands = new wysihtml5.Commands(editor);6753*/6754wysihtml5.Commands = Base.extend(6755/** @scope wysihtml5.Commands.prototype */ {6756constructor: function(editor) {6757this.editor = editor;6758this.composer = editor.composer;6759this.doc = this.composer.doc;6760},67616762/**6763* Check whether the browser supports the given command6764*6765* @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")6766* @example6767* commands.supports("createLink");6768*/6769support: function(command) {6770return wysihtml5.browser.supportsCommand(this.doc, command);6771},67726773/**6774* Check whether the browser supports the given command6775*6776* @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")6777* @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)6778* @example6779* commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");6780*/6781exec: function(command, value) {6782var obj = wysihtml5.commands[command],6783args = wysihtml5.lang.array(arguments).get(),6784method = obj && obj.exec,6785result = null;67866787this.editor.fire("beforecommand:composer");67886789if (method) {6790args.unshift(this.composer);6791result = method.apply(obj, args);6792} else {6793try {6794// try/catch for buggy firefox6795result = this.doc.execCommand(command, false, value);6796} catch(e) {}6797}67986799this.editor.fire("aftercommand:composer");6800return result;6801},68026803/**6804* Check whether the current command is active6805* If the caret is within a bold text, then calling this with command "bold" should return true6806*6807* @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")6808* @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)6809* @return {Boolean} Whether the command is active6810* @example6811* var isCurrentSelectionBold = commands.state("bold");6812*/6813state: function(command, commandValue) {6814var obj = wysihtml5.commands[command],6815args = wysihtml5.lang.array(arguments).get(),6816method = obj && obj.state;6817if (method) {6818args.unshift(this.composer);6819return method.apply(obj, args);6820} else {6821try {6822// try/catch for buggy firefox6823return this.doc.queryCommandState(command);6824} catch(e) {6825return false;6826}6827}6828},68296830/**6831* Get the current command's value6832*6833* @param {String} command The command string which to check (eg. "formatBlock")6834* @return {String} The command value6835* @example6836* var currentBlockElement = commands.value("formatBlock");6837*/6838value: function(command) {6839var obj = wysihtml5.commands[command],6840method = obj && obj.value;6841if (method) {6842return method.call(obj, this.composer, command);6843} else {6844try {6845// try/catch for buggy firefox6846return this.doc.queryCommandValue(command);6847} catch(e) {6848return null;6849}6850}6851}6852});6853(function(wysihtml5) {6854var undef;68556856wysihtml5.commands.bold = {6857exec: function(composer, command) {6858return wysihtml5.commands.formatInline.exec(composer, command, "b");6859},68606861state: function(composer, command, color) {6862// element.ownerDocument.queryCommandState("bold") results:6863// firefox: only <b>6864// chrome: <b>, <strong>, <h1>, <h2>, ...6865// ie: <b>, <strong>6866// opera: <b>, <strong>6867return wysihtml5.commands.formatInline.state(composer, command, "b");6868},68696870value: function() {6871return undef;6872}6873};6874})(wysihtml5);68756876(function(wysihtml5) {6877var undef,6878NODE_NAME = "A",6879dom = wysihtml5.dom;68806881function _removeFormat(composer, anchors) {6882var length = anchors.length,6883i = 0,6884anchor,6885codeElement,6886textContent;6887for (; i<length; i++) {6888anchor = anchors[i];6889codeElement = dom.getParentElement(anchor, { nodeName: "code" });6890textContent = dom.getTextContent(anchor);68916892// if <a> contains url-like text content, rename it to <code> to prevent re-autolinking6893// else replace <a> with its childNodes6894if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {6895// <code> element is used to prevent later auto-linking of the content6896codeElement = dom.renameElement(anchor, "code");6897} else {6898dom.replaceWithChildNodes(anchor);6899}6900}6901}69026903function _format(composer, attributes) {6904var doc = composer.doc,6905tempClass = "_wysihtml5-temp-" + (+new Date()),6906tempClassRegExp = /non-matching-class/g,6907i = 0,6908length,6909anchors,6910anchor,6911hasElementChild,6912isEmpty,6913elementToSetCaretAfter,6914textContent,6915whiteSpace,6916j;6917wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp);6918anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);6919length = anchors.length;6920for (; i<length; i++) {6921anchor = anchors[i];6922anchor.removeAttribute("class");6923for (j in attributes) {6924anchor.setAttribute(j, attributes[j]);6925}6926}69276928elementToSetCaretAfter = anchor;6929if (length === 1) {6930textContent = dom.getTextContent(anchor);6931hasElementChild = !!anchor.querySelector("*");6932isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE;6933if (!hasElementChild && isEmpty) {6934dom.setTextContent(anchor, attributes.text || anchor.href);6935whiteSpace = doc.createTextNode(" ");6936composer.selection.setAfter(anchor);6937composer.selection.insertNode(whiteSpace);6938elementToSetCaretAfter = whiteSpace;6939}6940}6941composer.selection.setAfter(elementToSetCaretAfter);6942}69436944wysihtml5.commands.createLink = {6945/**6946* TODO: Use HTMLApplier or formatInline here6947*6948* Turns selection into a link6949* If selection is already a link, it removes the link and wraps it with a <code> element6950* The <code> element is needed to avoid auto linking6951*6952* @example6953* // either ...6954* wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de");6955* // ... or ...6956* wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" });6957*/6958exec: function(composer, command, value) {6959var anchors = this.state(composer, command);6960if (anchors) {6961// Selection contains links6962composer.selection.executeAndRestore(function() {6963_removeFormat(composer, anchors);6964});6965} else {6966// Create links6967value = typeof(value) === "object" ? value : { href: value };6968_format(composer, value);6969}6970},69716972state: function(composer, command) {6973return wysihtml5.commands.formatInline.state(composer, command, "A");6974},69756976value: function() {6977return undef;6978}6979};6980})(wysihtml5);/**6981* document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags6982* which we don't want6983* Instead we set a css class6984*/6985(function(wysihtml5) {6986var undef,6987REG_EXP = /wysiwyg-font-size-[a-z\-]+/g;69886989wysihtml5.commands.fontSize = {6990exec: function(composer, command, size) {6991return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);6992},69936994state: function(composer, command, size) {6995return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);6996},69976998value: function() {6999return undef;7000}7001};7002})(wysihtml5);7003/**7004* document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags7005* which we don't want7006* Instead we set a css class7007*/7008(function(wysihtml5) {7009var undef,7010REG_EXP = /wysiwyg-color-[a-z]+/g;70117012wysihtml5.commands.foreColor = {7013exec: function(composer, command, color) {7014return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);7015},70167017state: function(composer, command, color) {7018return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);7019},70207021value: function() {7022return undef;7023}7024};7025})(wysihtml5);(function(wysihtml5) {7026var undef,7027dom = wysihtml5.dom,7028DEFAULT_NODE_NAME = "DIV",7029// Following elements are grouped7030// when the caret is within a H1 and the H4 is invoked, the H1 should turn into H47031// instead of creating a H4 within a H1 which would result in semantically invalid html7032BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", DEFAULT_NODE_NAME];70337034/**7035* Remove similiar classes (based on classRegExp)7036* and add the desired class name7037*/7038function _addClass(element, className, classRegExp) {7039if (element.className) {7040_removeClass(element, classRegExp);7041element.className += " " + className;7042} else {7043element.className = className;7044}7045}70467047function _removeClass(element, classRegExp) {7048element.className = element.className.replace(classRegExp, "");7049}70507051/**7052* Check whether given node is a text node and whether it's empty7053*/7054function _isBlankTextNode(node) {7055return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim();7056}70577058/**7059* Returns previous sibling node that is not a blank text node7060*/7061function _getPreviousSiblingThatIsNotBlank(node) {7062var previousSibling = node.previousSibling;7063while (previousSibling && _isBlankTextNode(previousSibling)) {7064previousSibling = previousSibling.previousSibling;7065}7066return previousSibling;7067}70687069/**7070* Returns next sibling node that is not a blank text node7071*/7072function _getNextSiblingThatIsNotBlank(node) {7073var nextSibling = node.nextSibling;7074while (nextSibling && _isBlankTextNode(nextSibling)) {7075nextSibling = nextSibling.nextSibling;7076}7077return nextSibling;7078}70797080/**7081* Adds line breaks before and after the given node if the previous and next siblings7082* aren't already causing a visual line break (block element or <br>)7083*/7084function _addLineBreakBeforeAndAfter(node) {7085var doc = node.ownerDocument,7086nextSibling = _getNextSiblingThatIsNotBlank(node),7087previousSibling = _getPreviousSiblingThatIsNotBlank(node);70887089if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {7090node.parentNode.insertBefore(doc.createElement("br"), nextSibling);7091}7092if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {7093node.parentNode.insertBefore(doc.createElement("br"), node);7094}7095}70967097/**7098* Removes line breaks before and after the given node7099*/7100function _removeLineBreakBeforeAndAfter(node) {7101var nextSibling = _getNextSiblingThatIsNotBlank(node),7102previousSibling = _getPreviousSiblingThatIsNotBlank(node);71037104if (nextSibling && _isLineBreak(nextSibling)) {7105nextSibling.parentNode.removeChild(nextSibling);7106}7107if (previousSibling && _isLineBreak(previousSibling)) {7108previousSibling.parentNode.removeChild(previousSibling);7109}7110}71117112function _removeLastChildIfLineBreak(node) {7113var lastChild = node.lastChild;7114if (lastChild && _isLineBreak(lastChild)) {7115lastChild.parentNode.removeChild(lastChild);7116}7117}71187119function _isLineBreak(node) {7120return node.nodeName === "BR";7121}71227123/**7124* Checks whether the elment causes a visual line break7125* (<br> or block elements)7126*/7127function _isLineBreakOrBlockElement(element) {7128if (_isLineBreak(element)) {7129return true;7130}71317132if (dom.getStyle("display").from(element) === "block") {7133return true;7134}71357136return false;7137}71387139/**7140* Execute native query command7141* and if necessary modify the inserted node's className7142*/7143function _execCommand(doc, command, nodeName, className) {7144if (className) {7145var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {7146var target = event.target,7147displayStyle;7148if (target.nodeType !== wysihtml5.ELEMENT_NODE) {7149return;7150}7151displayStyle = dom.getStyle("display").from(target);7152if (displayStyle.substr(0, 6) !== "inline") {7153// Make sure that only block elements receive the given class7154target.className += " " + className;7155}7156});7157}7158doc.execCommand(command, false, nodeName);7159if (eventListener) {7160eventListener.stop();7161}7162}71637164function _selectLineAndWrap(composer, element) {7165composer.selection.selectLine();7166composer.selection.surround(element);7167_removeLineBreakBeforeAndAfter(element);7168_removeLastChildIfLineBreak(element);7169composer.selection.selectNode(element);7170}71717172function _hasClasses(element) {7173return !!wysihtml5.lang.string(element.className).trim();7174}71757176wysihtml5.commands.formatBlock = {7177exec: function(composer, command, nodeName, className, classRegExp) {7178var doc = composer.doc,7179blockElement = this.state(composer, command, nodeName, className, classRegExp),7180selectedNode;71817182nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;71837184if (blockElement) {7185composer.selection.executeAndRestoreSimple(function() {7186if (classRegExp) {7187_removeClass(blockElement, classRegExp);7188}7189var hasClasses = _hasClasses(blockElement);7190if (!hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) {7191// Insert a line break afterwards and beforewards when there are siblings7192// that are not of type line break or block element7193_addLineBreakBeforeAndAfter(blockElement);7194dom.replaceWithChildNodes(blockElement);7195} else if (hasClasses) {7196// Make sure that styling is kept by renaming the element to <div> and copying over the class name7197dom.renameElement(blockElement, DEFAULT_NODE_NAME);7198}7199});7200return;7201}72027203// Find similiar block element and rename it (<h2 class="foo"></h2> => <h1 class="foo"></h1>)7204if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {7205selectedNode = composer.selection.getSelectedNode();7206blockElement = dom.getParentElement(selectedNode, {7207nodeName: BLOCK_ELEMENTS_GROUP7208});72097210if (blockElement) {7211composer.selection.executeAndRestoreSimple(function() {7212// Rename current block element to new block element and add class7213if (nodeName) {7214blockElement = dom.renameElement(blockElement, nodeName);7215}7216if (className) {7217_addClass(blockElement, className, classRegExp);7218}7219});7220return;7221}7222}72237224if (composer.commands.support(command)) {7225_execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className);7226return;7227}72287229blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME);7230if (className) {7231blockElement.className = className;7232}7233_selectLineAndWrap(composer, blockElement);7234},72357236state: function(composer, command, nodeName, className, classRegExp) {7237nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;7238var selectedNode = composer.selection.getSelectedNode();7239return dom.getParentElement(selectedNode, {7240nodeName: nodeName,7241className: className,7242classRegExp: classRegExp7243});7244},72457246value: function() {7247return undef;7248}7249};7250})(wysihtml5);/**7251* formatInline scenarios for tag "B" (| = caret, |foo| = selected text)7252*7253* #1 caret in unformatted text:7254* abcdefg|7255* output:7256* abcdefg<b>|</b>7257*7258* #2 unformatted text selected:7259* abc|deg|h7260* output:7261* abc<b>|deg|</b>h7262*7263* #3 unformatted text selected across boundaries:7264* ab|c <span>defg|h</span>7265* output:7266* ab<b>|c </b><span><b>defg</b>|h</span>7267*7268* #4 formatted text entirely selected7269* <b>|abc|</b>7270* output:7271* |abc|7272*7273* #5 formatted text partially selected7274* <b>ab|c|</b>7275* output:7276* <b>ab</b>|c|7277*7278* #6 formatted text selected across boundaries7279* <span>ab|c</span> <b>de|fgh</b>7280* output:7281* <span>ab|c</span> de|<b>fgh</b>7282*/7283(function(wysihtml5) {7284var undef,7285// Treat <b> as <strong> and vice versa7286ALIAS_MAPPING = {7287"strong": "b",7288"em": "i",7289"b": "strong",7290"i": "em"7291},7292htmlApplier = {};72937294function _getTagNames(tagName) {7295var alias = ALIAS_MAPPING[tagName];7296return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];7297}72987299function _getApplier(tagName, className, classRegExp) {7300var identifier = tagName + ":" + className;7301if (!htmlApplier[identifier]) {7302htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true);7303}7304return htmlApplier[identifier];7305}73067307wysihtml5.commands.formatInline = {7308exec: function(composer, command, tagName, className, classRegExp) {7309var range = composer.selection.getRange();7310if (!range) {7311return false;7312}7313_getApplier(tagName, className, classRegExp).toggleRange(range);7314composer.selection.setSelection(range);7315},73167317state: function(composer, command, tagName, className, classRegExp) {7318var doc = composer.doc,7319aliasTagName = ALIAS_MAPPING[tagName] || tagName,7320range;73217322// Check whether the document contains a node with the desired tagName7323if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&7324!wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {7325return false;7326}73277328// Check whether the document contains a node with the desired className7329if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {7330return false;7331}73327333range = composer.selection.getRange();7334if (!range) {7335return false;7336}73377338return _getApplier(tagName, className, classRegExp).isAppliedToRange(range);7339},73407341value: function() {7342return undef;7343}7344};7345})(wysihtml5);(function(wysihtml5) {7346var undef;73477348wysihtml5.commands.insertHTML = {7349exec: function(composer, command, html) {7350if (composer.commands.support(command)) {7351composer.doc.execCommand(command, false, html);7352} else {7353composer.selection.insertHTML(html);7354}7355},73567357state: function() {7358return false;7359},73607361value: function() {7362return undef;7363}7364};7365})(wysihtml5);(function(wysihtml5) {7366var NODE_NAME = "IMG";73677368wysihtml5.commands.insertImage = {7369/**7370* Inserts an <img>7371* If selection is already an image link, it removes it7372*7373* @example7374* // either ...7375* wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg");7376* // ... or ...7377* wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" });7378*/7379exec: function(composer, command, value) {7380value = typeof(value) === "object" ? value : { src: value };73817382var doc = composer.doc,7383image = this.state(composer),7384textNode,7385i,7386parent;73877388if (image) {7389// Image already selected, set the caret before it and delete it7390composer.selection.setBefore(image);7391parent = image.parentNode;7392parent.removeChild(image);73937394// and it's parent <a> too if it hasn't got any other relevant child nodes7395wysihtml5.dom.removeEmptyTextNodes(parent);7396if (parent.nodeName === "A" && !parent.firstChild) {7397composer.selection.setAfter(parent);7398parent.parentNode.removeChild(parent);7399}74007401// firefox and ie sometimes don't remove the image handles, even though the image got removed7402wysihtml5.quirks.redraw(composer.element);7403return;7404}74057406image = doc.createElement(NODE_NAME);74077408for (i in value) {7409image[i] = value[i];7410}74117412composer.selection.insertNode(image);7413if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) {7414textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);7415composer.selection.insertNode(textNode);7416composer.selection.setAfter(textNode);7417} else {7418composer.selection.setAfter(image);7419}7420},74217422state: function(composer) {7423var doc = composer.doc,7424selectedNode,7425text,7426imagesInSelection;74277428if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {7429return false;7430}74317432selectedNode = composer.selection.getSelectedNode();7433if (!selectedNode) {7434return false;7435}74367437if (selectedNode.nodeName === NODE_NAME) {7438// This works perfectly in IE7439return selectedNode;7440}74417442if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {7443return false;7444}74457446text = composer.selection.getText();7447text = wysihtml5.lang.string(text).trim();7448if (text) {7449return false;7450}74517452imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) {7453return node.nodeName === "IMG";7454});74557456if (imagesInSelection.length !== 1) {7457return false;7458}74597460return imagesInSelection[0];7461},74627463value: function(composer) {7464var image = this.state(composer);7465return image && image.src;7466}7467};7468})(wysihtml5);(function(wysihtml5) {7469var undef,7470LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : "");74717472wysihtml5.commands.insertLineBreak = {7473exec: function(composer, command) {7474if (composer.commands.support(command)) {7475composer.doc.execCommand(command, false, null);7476if (!wysihtml5.browser.autoScrollsToCaret()) {7477composer.selection.scrollIntoView();7478}7479} else {7480composer.commands.exec("insertHTML", LINE_BREAK);7481}7482},74837484state: function() {7485return false;7486},74877488value: function() {7489return undef;7490}7491};7492})(wysihtml5);(function(wysihtml5) {7493var undef;74947495wysihtml5.commands.insertOrderedList = {7496exec: function(composer, command) {7497var doc = composer.doc,7498selectedNode = composer.selection.getSelectedNode(),7499list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }),7500otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }),7501tempClassName = "_wysihtml5-temp-" + new Date().getTime(),7502isEmpty,7503tempElement;75047505if (composer.commands.support(command)) {7506doc.execCommand(command, false, null);7507return;7508}75097510if (list) {7511// Unwrap list7512// <ol><li>foo</li><li>bar</li></ol>7513// becomes:7514// foo<br>bar<br>7515composer.selection.executeAndRestoreSimple(function() {7516wysihtml5.dom.resolveList(list);7517});7518} else if (otherList) {7519// Turn an unordered list into an ordered list7520// <ul><li>foo</li><li>bar</li></ul>7521// becomes:7522// <ol><li>foo</li><li>bar</li></ol>7523composer.selection.executeAndRestoreSimple(function() {7524wysihtml5.dom.renameElement(otherList, "ol");7525});7526} else {7527// Create list7528composer.commands.exec("formatBlock", "div", tempClassName);7529tempElement = doc.querySelector("." + tempClassName);7530isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;7531composer.selection.executeAndRestoreSimple(function() {7532list = wysihtml5.dom.convertToList(tempElement, "ol");7533});7534if (isEmpty) {7535composer.selection.selectNode(list.querySelector("li"));7536}7537}7538},75397540state: function(composer) {7541var selectedNode = composer.selection.getSelectedNode();7542return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" });7543},75447545value: function() {7546return undef;7547}7548};7549})(wysihtml5);(function(wysihtml5) {7550var undef;75517552wysihtml5.commands.insertUnorderedList = {7553exec: function(composer, command) {7554var doc = composer.doc,7555selectedNode = composer.selection.getSelectedNode(),7556list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }),7557otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }),7558tempClassName = "_wysihtml5-temp-" + new Date().getTime(),7559isEmpty,7560tempElement;75617562if (composer.commands.support(command)) {7563doc.execCommand(command, false, null);7564return;7565}75667567if (list) {7568// Unwrap list7569// <ul><li>foo</li><li>bar</li></ul>7570// becomes:7571// foo<br>bar<br>7572composer.selection.executeAndRestoreSimple(function() {7573wysihtml5.dom.resolveList(list);7574});7575} else if (otherList) {7576// Turn an ordered list into an unordered list7577// <ol><li>foo</li><li>bar</li></ol>7578// becomes:7579// <ul><li>foo</li><li>bar</li></ul>7580composer.selection.executeAndRestoreSimple(function() {7581wysihtml5.dom.renameElement(otherList, "ul");7582});7583} else {7584// Create list7585composer.commands.exec("formatBlock", "div", tempClassName);7586tempElement = doc.querySelector("." + tempClassName);7587isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;7588composer.selection.executeAndRestoreSimple(function() {7589list = wysihtml5.dom.convertToList(tempElement, "ul");7590});7591if (isEmpty) {7592composer.selection.selectNode(list.querySelector("li"));7593}7594}7595},75967597state: function(composer) {7598var selectedNode = composer.selection.getSelectedNode();7599return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" });7600},76017602value: function() {7603return undef;7604}7605};7606})(wysihtml5);(function(wysihtml5) {7607var undef;76087609wysihtml5.commands.italic = {7610exec: function(composer, command) {7611return wysihtml5.commands.formatInline.exec(composer, command, "i");7612},76137614state: function(composer, command, color) {7615// element.ownerDocument.queryCommandState("italic") results:7616// firefox: only <i>7617// chrome: <i>, <em>, <blockquote>, ...7618// ie: <i>, <em>7619// opera: only <i>7620return wysihtml5.commands.formatInline.state(composer, command, "i");7621},76227623value: function() {7624return undef;7625}7626};7627})(wysihtml5);(function(wysihtml5) {7628var undef,7629CLASS_NAME = "wysiwyg-text-align-center",7630REG_EXP = /wysiwyg-text-align-[a-z]+/g;76317632wysihtml5.commands.justifyCenter = {7633exec: function(composer, command) {7634return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);7635},76367637state: function(composer, command) {7638return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);7639},76407641value: function() {7642return undef;7643}7644};7645})(wysihtml5);(function(wysihtml5) {7646var undef,7647CLASS_NAME = "wysiwyg-text-align-left",7648REG_EXP = /wysiwyg-text-align-[a-z]+/g;76497650wysihtml5.commands.justifyLeft = {7651exec: function(composer, command) {7652return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);7653},76547655state: function(composer, command) {7656return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);7657},76587659value: function() {7660return undef;7661}7662};7663})(wysihtml5);(function(wysihtml5) {7664var undef,7665CLASS_NAME = "wysiwyg-text-align-right",7666REG_EXP = /wysiwyg-text-align-[a-z]+/g;76677668wysihtml5.commands.justifyRight = {7669exec: function(composer, command) {7670return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);7671},76727673state: function(composer, command) {7674return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);7675},76767677value: function() {7678return undef;7679}7680};7681})(wysihtml5);(function(wysihtml5) {7682var undef;7683wysihtml5.commands.underline = {7684exec: function(composer, command) {7685return wysihtml5.commands.formatInline.exec(composer, command, "u");7686},76877688state: function(composer, command) {7689return wysihtml5.commands.formatInline.state(composer, command, "u");7690},76917692value: function() {7693return undef;7694}7695};7696})(wysihtml5);/**7697* Undo Manager for wysihtml57698* slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface7699*/7700(function(wysihtml5) {7701var Z_KEY = 90,7702Y_KEY = 89,7703BACKSPACE_KEY = 8,7704DELETE_KEY = 46,7705MAX_HISTORY_ENTRIES = 40,7706UNDO_HTML = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',7707REDO_HTML = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',7708dom = wysihtml5.dom;77097710function cleanTempElements(doc) {7711var tempElement;7712while (tempElement = doc.querySelector("._wysihtml5-temp")) {7713tempElement.parentNode.removeChild(tempElement);7714}7715}77167717wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(7718/** @scope wysihtml5.UndoManager.prototype */ {7719constructor: function(editor) {7720this.editor = editor;7721this.composer = editor.composer;7722this.element = this.composer.element;7723this.history = [this.composer.getValue()];7724this.position = 1;77257726// Undo manager currently only supported in browsers who have the insertHTML command (not IE)7727if (this.composer.commands.support("insertHTML")) {7728this._observe();7729}7730},77317732_observe: function() {7733var that = this,7734doc = this.composer.sandbox.getDocument(),7735lastKey;77367737// Catch CTRL+Z and CTRL+Y7738dom.observe(this.element, "keydown", function(event) {7739if (event.altKey || (!event.ctrlKey && !event.metaKey)) {7740return;7741}77427743var keyCode = event.keyCode,7744isUndo = keyCode === Z_KEY && !event.shiftKey,7745isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);77467747if (isUndo) {7748that.undo();7749event.preventDefault();7750} else if (isRedo) {7751that.redo();7752event.preventDefault();7753}7754});77557756// Catch delete and backspace7757dom.observe(this.element, "keydown", function(event) {7758var keyCode = event.keyCode;7759if (keyCode === lastKey) {7760return;7761}77627763lastKey = keyCode;77647765if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {7766that.transact();7767}7768});77697770// Now this is very hacky:7771// These days browsers don't offer a undo/redo event which we could hook into7772// to be notified when the user hits undo/redo in the contextmenu.7773// Therefore we simply insert two elements as soon as the contextmenu gets opened.7774// The last element being inserted will be immediately be removed again by a exexCommand("undo")7775// => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu7776// => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu7777if (wysihtml5.browser.hasUndoInContextMenu()) {7778var interval, observed, cleanUp = function() {7779cleanTempElements(doc);7780clearInterval(interval);7781};77827783dom.observe(this.element, "contextmenu", function() {7784cleanUp();7785that.composer.selection.executeAndRestoreSimple(function() {7786if (that.element.lastChild) {7787that.composer.selection.setAfter(that.element.lastChild);7788}77897790// enable undo button in context menu7791doc.execCommand("insertHTML", false, UNDO_HTML);7792// enable redo button in context menu7793doc.execCommand("insertHTML", false, REDO_HTML);7794doc.execCommand("undo", false, null);7795});77967797interval = setInterval(function() {7798if (doc.getElementById("_wysihtml5-redo")) {7799cleanUp();7800that.redo();7801} else if (!doc.getElementById("_wysihtml5-undo")) {7802cleanUp();7803that.undo();7804}7805}, 400);78067807if (!observed) {7808observed = true;7809dom.observe(document, "mousedown", cleanUp);7810dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp);7811}7812});7813}78147815this.editor7816.observe("newword:composer", function() {7817that.transact();7818})78197820.observe("beforecommand:composer", function() {7821that.transact();7822});7823},78247825transact: function() {7826var previousHtml = this.history[this.position - 1],7827currentHtml = this.composer.getValue();78287829if (currentHtml == previousHtml) {7830return;7831}78327833var length = this.history.length = this.position;7834if (length > MAX_HISTORY_ENTRIES) {7835this.history.shift();7836this.position--;7837}78387839this.position++;7840this.history.push(currentHtml);7841},78427843undo: function() {7844this.transact();78457846if (this.position <= 1) {7847return;7848}78497850this.set(this.history[--this.position - 1]);7851this.editor.fire("undo:composer");7852},78537854redo: function() {7855if (this.position >= this.history.length) {7856return;7857}78587859this.set(this.history[++this.position - 1]);7860this.editor.fire("redo:composer");7861},78627863set: function(html) {7864this.composer.setValue(html);7865this.editor.focus(true);7866}7867});7868})(wysihtml5);7869/**7870* TODO: the following methods still need unit test coverage7871*/7872wysihtml5.views.View = Base.extend(7873/** @scope wysihtml5.views.View.prototype */ {7874constructor: function(parent, textareaElement, config) {7875this.parent = parent;7876this.element = textareaElement;7877this.config = config;78787879this._observeViewChange();7880},78817882_observeViewChange: function() {7883var that = this;7884this.parent.observe("beforeload", function() {7885that.parent.observe("change_view", function(view) {7886if (view === that.name) {7887that.parent.currentView = that;7888that.show();7889// Using tiny delay here to make sure that the placeholder is set before focusing7890setTimeout(function() { that.focus(); }, 0);7891} else {7892that.hide();7893}7894});7895});7896},78977898focus: function() {7899if (this.element.ownerDocument.querySelector(":focus") === this.element) {7900return;7901}79027903try { this.element.focus(); } catch(e) {}7904},79057906hide: function() {7907this.element.style.display = "none";7908},79097910show: function() {7911this.element.style.display = "";7912},79137914disable: function() {7915this.element.setAttribute("disabled", "disabled");7916},79177918enable: function() {7919this.element.removeAttribute("disabled");7920}7921});(function(wysihtml5) {7922var dom = wysihtml5.dom,7923browser = wysihtml5.browser;79247925wysihtml5.views.Composer = wysihtml5.views.View.extend(7926/** @scope wysihtml5.views.Composer.prototype */ {7927name: "composer",79287929// Needed for firefox in order to display a proper caret in an empty contentEditable7930CARET_HACK: "<br>",79317932constructor: function(parent, textareaElement, config) {7933this.base(parent, textareaElement, config);7934this.textarea = this.parent.textarea;7935this._initSandbox();7936},79377938clear: function() {7939this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK;7940},79417942getValue: function(parse) {7943var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element);79447945if (parse) {7946value = this.parent.parse(value);7947}79487949// Replace all "zero width no breaking space" chars7950// which are used as hacks to enable some functionalities7951// Also remove all CARET hacks that somehow got left7952value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by("");79537954return value;7955},79567957setValue: function(html, parse) {7958if (parse) {7959html = this.parent.parse(html);7960}7961this.element.innerHTML = html;7962},79637964show: function() {7965this.iframe.style.display = this._displayStyle || "";79667967// Firefox needs this, otherwise contentEditable becomes uneditable7968this.disable();7969this.enable();7970},79717972hide: function() {7973this._displayStyle = dom.getStyle("display").from(this.iframe);7974if (this._displayStyle === "none") {7975this._displayStyle = null;7976}7977this.iframe.style.display = "none";7978},79797980disable: function() {7981this.element.removeAttribute("contentEditable");7982this.base();7983},79847985enable: function() {7986this.element.setAttribute("contentEditable", "true");7987this.base();7988},79897990focus: function(setToEnd) {7991// IE 8 fires the focus event after .focus()7992// This is needed by our simulate_placeholder.js to work7993// therefore we clear it ourselves this time7994if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {7995this.clear();7996}79977998this.base();79998000var lastChild = this.element.lastChild;8001if (setToEnd && lastChild) {8002if (lastChild.nodeName === "BR") {8003this.selection.setBefore(this.element.lastChild);8004} else {8005this.selection.setAfter(this.element.lastChild);8006}8007}8008},80098010getTextContent: function() {8011return dom.getTextContent(this.element);8012},80138014hasPlaceholderSet: function() {8015return this.getTextContent() == this.textarea.element.getAttribute("placeholder");8016},80178018isEmpty: function() {8019var innerHTML = this.element.innerHTML,8020elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea";8021return innerHTML === "" ||8022innerHTML === this.CARET_HACK ||8023this.hasPlaceholderSet() ||8024(this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue));8025},80268027_initSandbox: function() {8028var that = this;80298030this.sandbox = new dom.Sandbox(function() {8031that._create();8032}, {8033stylesheets: this.config.stylesheets8034});8035this.iframe = this.sandbox.getIframe();80368037// Create hidden field which tells the server after submit, that the user used an wysiwyg editor8038var hiddenField = document.createElement("input");8039hiddenField.type = "hidden";8040hiddenField.name = "_wysihtml5_mode";8041hiddenField.value = 1;80428043// Store reference to current wysihtml5 instance on the textarea element8044var textareaElement = this.textarea.element;8045dom.insert(this.iframe).after(textareaElement);8046dom.insert(hiddenField).after(textareaElement);8047},80488049_create: function() {8050var that = this;80518052this.doc = this.sandbox.getDocument();8053this.element = this.doc.body;8054this.textarea = this.parent.textarea;8055this.element.innerHTML = this.textarea.getValue(true);8056this.enable();80578058// Make sure our selection handler is ready8059this.selection = new wysihtml5.Selection(this.parent);80608061// Make sure commands dispatcher is ready8062this.commands = new wysihtml5.Commands(this.parent);80638064dom.copyAttributes([8065"className", "spellcheck", "title", "lang", "dir", "accessKey"8066]).from(this.textarea.element).to(this.element);80678068dom.addClass(this.element, this.config.composerClassName);80698070// Make the editor look like the original textarea, by syncing styles8071if (this.config.style) {8072this.style();8073}80748075this.observe();80768077var name = this.config.name;8078if (name) {8079dom.addClass(this.element, name);8080dom.addClass(this.iframe, name);8081}80828083// Simulate html5 placeholder attribute on contentEditable element8084var placeholderText = typeof(this.config.placeholder) === "string"8085? this.config.placeholder8086: this.textarea.element.getAttribute("placeholder");8087if (placeholderText) {8088dom.simulatePlaceholder(this.parent, this, placeholderText);8089}80908091// Make sure that the browser avoids using inline styles whenever possible8092this.commands.exec("styleWithCSS", false);80938094this._initAutoLinking();8095this._initObjectResizing();8096this._initUndoManager();80978098// Simulate html5 autofocus on contentEditable element8099if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) {8100setTimeout(function() { that.focus(); }, 100);8101}81028103wysihtml5.quirks.insertLineBreakOnReturn(this);81048105// IE sometimes leaves a single paragraph, which can't be removed by the user8106if (!browser.clearsContentEditableCorrectly()) {8107wysihtml5.quirks.ensureProperClearing(this);8108}81098110if (!browser.clearsListsInContentEditableCorrectly()) {8111wysihtml5.quirks.ensureProperClearingOfLists(this);8112}81138114// Set up a sync that makes sure that textarea and editor have the same content8115if (this.initSync && this.config.sync) {8116this.initSync();8117}81188119// Okay hide the textarea, we are ready to go8120this.textarea.hide();81218122// Fire global (before-)load event8123this.parent.fire("beforeload").fire("load");8124},81258126_initAutoLinking: function() {8127var that = this,8128supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),8129supportsAutoLinking = browser.doesAutoLinkingInContentEditable();8130if (supportsDisablingOfAutoLinking) {8131this.commands.exec("autoUrlDetect", false);8132}81338134if (!this.config.autoLink) {8135return;8136}81378138// Only do the auto linking by ourselves when the browser doesn't support auto linking8139// OR when he supports auto linking but we were able to turn it off (IE9+)8140if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {8141this.parent.observe("newword:composer", function() {8142that.selection.executeAndRestore(function(startContainer, endContainer) {8143dom.autoLink(endContainer.parentNode);8144});8145});8146}81478148// Assuming we have the following:8149// <a href="http://www.google.de">http://www.google.de</a>8150// If a user now changes the url in the innerHTML we want to make sure that8151// it's synchronized with the href attribute (as long as the innerHTML is still a url)8152var // Use a live NodeList to check whether there are any links in the document8153links = this.sandbox.getDocument().getElementsByTagName("a"),8154// The autoLink helper method reveals a reg exp to detect correct urls8155urlRegExp = dom.autoLink.URL_REG_EXP,8156getTextContent = function(element) {8157var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim();8158if (textContent.substr(0, 4) === "www.") {8159textContent = "http://" + textContent;8160}8161return textContent;8162};81638164dom.observe(this.element, "keydown", function(event) {8165if (!links.length) {8166return;8167}81688169var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),8170link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),8171textContent;81728173if (!link) {8174return;8175}81768177textContent = getTextContent(link);8178// keydown is fired before the actual content is changed8179// therefore we set a timeout to change the href8180setTimeout(function() {8181var newTextContent = getTextContent(link);8182if (newTextContent === textContent) {8183return;8184}81858186// Only set href when new href looks like a valid url8187if (newTextContent.match(urlRegExp)) {8188link.setAttribute("href", newTextContent);8189}8190}, 0);8191});8192},81938194_initObjectResizing: function() {8195var properties = ["width", "height"],8196propertiesLength = properties.length,8197element = this.element;81988199this.commands.exec("enableObjectResizing", this.config.allowObjectResizing);82008201if (this.config.allowObjectResizing) {8202// IE sets inline styles after resizing objects8203// The following lines make sure that the width/height css properties8204// are copied over to the width/height attributes8205if (browser.supportsEvent("resizeend")) {8206dom.observe(element, "resizeend", function(event) {8207var target = event.target || event.srcElement,8208style = target.style,8209i = 0,8210property;8211for(; i<propertiesLength; i++) {8212property = properties[i];8213if (style[property]) {8214target.setAttribute(property, parseInt(style[property], 10));8215style[property] = "";8216}8217}8218// After resizing IE sometimes forgets to remove the old resize handles8219wysihtml5.quirks.redraw(element);8220});8221}8222} else {8223if (browser.supportsEvent("resizestart")) {8224dom.observe(element, "resizestart", function(event) { event.preventDefault(); });8225}8226}8227},82288229_initUndoManager: function() {8230new wysihtml5.UndoManager(this.parent);8231}8232});8233})(wysihtml5);(function(wysihtml5) {8234var dom = wysihtml5.dom,8235doc = document,8236win = window,8237HOST_TEMPLATE = doc.createElement("div"),8238/**8239* Styles to copy from textarea to the composer element8240*/8241TEXT_FORMATTING = [8242"background-color",8243"color", "cursor",8244"font-family", "font-size", "font-style", "font-variant", "font-weight",8245"line-height", "letter-spacing",8246"text-align", "text-decoration", "text-indent", "text-rendering",8247"word-break", "word-wrap", "word-spacing"8248],8249/**8250* Styles to copy from textarea to the iframe8251*/8252BOX_FORMATTING = [8253"background-color",8254"border-collapse",8255"border-bottom-color", "border-bottom-style", "border-bottom-width",8256"border-left-color", "border-left-style", "border-left-width",8257"border-right-color", "border-right-style", "border-right-width",8258"border-top-color", "border-top-style", "border-top-width",8259"clear", "display", "float",8260"margin-bottom", "margin-left", "margin-right", "margin-top",8261"outline-color", "outline-offset", "outline-width", "outline-style",8262"padding-left", "padding-right", "padding-top", "padding-bottom",8263"position", "top", "left", "right", "bottom", "z-index",8264"vertical-align", "text-align",8265"-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing",8266"-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow",8267"-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius",8268"-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius",8269"-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius",8270"-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius",8271"width", "height"8272],8273/**8274* Styles to sync while the window gets resized8275*/8276RESIZE_STYLE = [8277"width", "height",8278"top", "left", "right", "bottom"8279],8280ADDITIONAL_CSS_RULES = [8281"html { height: 100%; }",8282"body { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }",8283"._wysihtml5-temp { display: none; }",8284wysihtml5.browser.isGecko ?8285"body.placeholder { color: graytext !important; }" :8286"body.placeholder { color: #a9a9a9 !important; }",8287"body[disabled] { background-color: #eee !important; color: #999 !important; cursor: default !important; }",8288// Ensure that user see's broken images and can delete them8289"img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"8290];82918292/**8293* With "setActive" IE offers a smart way of focusing elements without scrolling them into view:8294* http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx8295*8296* Other browsers need a more hacky way: (pssst don't tell my mama)8297* In order to prevent the element being scrolled into view when focusing it, we simply8298* move it out of the scrollable area, focus it, and reset it's position8299*/8300var focusWithoutScrolling = function(element) {8301if (element.setActive) {8302// Following line could cause a js error when the textarea is invisible8303// See https://github.com/xing/wysihtml5/issues/98304try { element.setActive(); } catch(e) {}8305} else {8306var elementStyle = element.style,8307originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,8308originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,8309originalStyles = {8310position: elementStyle.position,8311top: elementStyle.top,8312left: elementStyle.left,8313WebkitUserSelect: elementStyle.WebkitUserSelect8314};83158316dom.setStyles({8317position: "absolute",8318top: "-99999px",8319left: "-99999px",8320// Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother8321WebkitUserSelect: "none"8322}).on(element);83238324element.focus();83258326dom.setStyles(originalStyles).on(element);83278328if (win.scrollTo) {8329// Some browser extensions unset this method to prevent annoyances8330// "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#1008331// Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=18332win.scrollTo(originalScrollLeft, originalScrollTop);8333}8334}8335};833683378338wysihtml5.views.Composer.prototype.style = function() {8339var that = this,8340originalActiveElement = doc.querySelector(":focus"),8341textareaElement = this.textarea.element,8342hasPlaceholder = textareaElement.hasAttribute("placeholder"),8343originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder");8344this.focusStylesHost = this.focusStylesHost || HOST_TEMPLATE.cloneNode(false);8345this.blurStylesHost = this.blurStylesHost || HOST_TEMPLATE.cloneNode(false);83468347// Remove placeholder before copying (as the placeholder has an affect on the computed style)8348if (hasPlaceholder) {8349textareaElement.removeAttribute("placeholder");8350}83518352if (textareaElement === originalActiveElement) {8353textareaElement.blur();8354}83558356// --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------8357dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost);83588359// --------- editor styles ---------8360dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);83618362// --------- apply standard rules ---------8363dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);83648365// --------- :focus styles ---------8366focusWithoutScrolling(textareaElement);8367dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);8368dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);83698370// Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus8371// this is needed for when the change_view event is fired where the iframe is hidden and then8372// the blur event fires and re-displays it8373var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]);83748375// --------- restore focus ---------8376if (originalActiveElement) {8377originalActiveElement.focus();8378} else {8379textareaElement.blur();8380}83818382// --------- restore placeholder ---------8383if (hasPlaceholder) {8384textareaElement.setAttribute("placeholder", originalPlaceholder);8385}83868387// When copying styles, we only get the computed style which is never returned in percent unit8388// Therefore we've to recalculate style onresize8389if (!wysihtml5.browser.hasCurrentStyleProperty()) {8390var winObserver = dom.observe(win, "resize", function() {8391// Remove event listener if composer doesn't exist anymore8392if (!dom.contains(document.documentElement, that.iframe)) {8393winObserver.stop();8394return;8395}8396var originalTextareaDisplayStyle = dom.getStyle("display").from(textareaElement),8397originalComposerDisplayStyle = dom.getStyle("display").from(that.iframe);8398textareaElement.style.display = "";8399that.iframe.style.display = "none";8400dom.copyStyles(RESIZE_STYLE)8401.from(textareaElement)8402.to(that.iframe)8403.andTo(that.focusStylesHost)8404.andTo(that.blurStylesHost);8405that.iframe.style.display = originalComposerDisplayStyle;8406textareaElement.style.display = originalTextareaDisplayStyle;8407});8408}84098410// --------- Sync focus/blur styles ---------8411this.parent.observe("focus:composer", function() {8412dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe);8413dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element);8414});84158416this.parent.observe("blur:composer", function() {8417dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe);8418dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element);8419});84208421return this;8422};8423})(wysihtml5);/**8424* Taking care of events8425* - Simulating 'change' event on contentEditable element8426* - Handling drag & drop logic8427* - Catch paste events8428* - Dispatch proprietary newword:composer event8429* - Keyboard shortcuts8430*/8431(function(wysihtml5) {8432var dom = wysihtml5.dom,8433browser = wysihtml5.browser,8434/**8435* Map keyCodes to query commands8436*/8437shortcuts = {8438"66": "bold", // B8439"73": "italic", // I8440"85": "underline" // U8441};84428443wysihtml5.views.Composer.prototype.observe = function() {8444var that = this,8445state = this.getValue(),8446iframe = this.sandbox.getIframe(),8447element = this.element,8448focusBlurElement = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(),8449// Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same8450pasteEvents = browser.supportsEvent("drop") ? ["drop", "paste"] : ["dragdrop", "paste"];84518452// --------- destroy:composer event ---------8453dom.observe(iframe, "DOMNodeRemoved", function() {8454clearInterval(domNodeRemovedInterval);8455that.parent.fire("destroy:composer");8456});84578458// DOMNodeRemoved event is not supported in IE 88459var domNodeRemovedInterval = setInterval(function() {8460if (!dom.contains(document.documentElement, iframe)) {8461clearInterval(domNodeRemovedInterval);8462that.parent.fire("destroy:composer");8463}8464}, 250);846584668467// --------- Focus & blur logic ---------8468dom.observe(focusBlurElement, "focus", function() {8469that.parent.fire("focus").fire("focus:composer");84708471// Delay storing of state until all focus handler are fired8472// especially the one which resets the placeholder8473setTimeout(function() { state = that.getValue(); }, 0);8474});84758476dom.observe(focusBlurElement, "blur", function() {8477if (state !== that.getValue()) {8478that.parent.fire("change").fire("change:composer");8479}8480that.parent.fire("blur").fire("blur:composer");8481});84828483if (wysihtml5.browser.isIos()) {8484// When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus8485// but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible)8486// We prevent that by focusing a temporary input element which immediately loses focus8487dom.observe(element, "blur", function() {8488var input = element.ownerDocument.createElement("input"),8489originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop,8490originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;8491try {8492that.selection.insertNode(input);8493} catch(e) {8494element.appendChild(input);8495}8496input.focus();8497input.parentNode.removeChild(input);84988499window.scrollTo(originalScrollLeft, originalScrollTop);8500});8501}85028503// --------- Drag & Drop logic ---------8504dom.observe(element, "dragenter", function() {8505that.parent.fire("unset_placeholder");8506});85078508if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) {8509dom.observe(element, ["dragover", "dragenter"], function(event) {8510event.preventDefault();8511});8512}85138514dom.observe(element, pasteEvents, function(event) {8515var dataTransfer = event.dataTransfer,8516data;85178518if (dataTransfer && browser.supportsDataTransfer()) {8519data = dataTransfer.getData("text/html") || dataTransfer.getData("text/plain");8520}8521if (data) {8522element.focus();8523that.commands.exec("insertHTML", data);8524that.parent.fire("paste").fire("paste:composer");8525event.stopPropagation();8526event.preventDefault();8527} else {8528setTimeout(function() {8529that.parent.fire("paste").fire("paste:composer");8530}, 0);8531}8532});85338534// --------- neword event ---------8535dom.observe(element, "keyup", function(event) {8536var keyCode = event.keyCode;8537if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) {8538that.parent.fire("newword:composer");8539}8540});85418542this.parent.observe("paste:composer", function() {8543setTimeout(function() { that.parent.fire("newword:composer"); }, 0);8544});85458546// --------- Make sure that images are selected when clicking on them ---------8547if (!browser.canSelectImagesInContentEditable()) {8548dom.observe(element, "mousedown", function(event) {8549var target = event.target;8550if (target.nodeName === "IMG") {8551that.selection.selectNode(target);8552event.preventDefault();8553}8554});8555}85568557// --------- Shortcut logic ---------8558dom.observe(element, "keydown", function(event) {8559var keyCode = event.keyCode,8560command = shortcuts[keyCode];8561if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {8562that.commands.exec(command);8563event.preventDefault();8564}8565});85668567// --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor ---------8568dom.observe(element, "keydown", function(event) {8569var target = that.selection.getSelectedNode(true),8570keyCode = event.keyCode,8571parent;8572if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete8573parent = target.parentNode;8574// delete the <img>8575parent.removeChild(target);8576// and it's parent <a> too if it hasn't got any other child nodes8577if (parent.nodeName === "A" && !parent.firstChild) {8578parent.parentNode.removeChild(parent);8579}85808581setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);8582event.preventDefault();8583}8584});85858586// --------- Show url in tooltip when hovering links or images ---------8587var titlePrefixes = {8588IMG: "Image: ",8589A: "Link: "8590};85918592dom.observe(element, "mouseover", function(event) {8593var target = event.target,8594nodeName = target.nodeName,8595title;8596if (nodeName !== "A" && nodeName !== "IMG") {8597return;8598}8599var hasTitle = target.hasAttribute("title");8600if(!hasTitle){8601title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));8602target.setAttribute("title", title);8603}8604});8605};8606})(wysihtml5);/**8607* Class that takes care that the value of the composer and the textarea is always in sync8608*/8609(function(wysihtml5) {8610var INTERVAL = 400;86118612wysihtml5.views.Synchronizer = Base.extend(8613/** @scope wysihtml5.views.Synchronizer.prototype */ {86148615constructor: function(editor, textarea, composer) {8616this.editor = editor;8617this.textarea = textarea;8618this.composer = composer;86198620this._observe();8621},86228623/**8624* Sync html from composer to textarea8625* Takes care of placeholders8626* @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea8627*/8628fromComposerToTextarea: function(shouldParseHtml) {8629this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml);8630},86318632/**8633* Sync value of textarea to composer8634* Takes care of placeholders8635* @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer8636*/8637fromTextareaToComposer: function(shouldParseHtml) {8638var textareaValue = this.textarea.getValue();8639if (textareaValue) {8640this.composer.setValue(textareaValue, shouldParseHtml);8641} else {8642this.composer.clear();8643this.editor.fire("set_placeholder");8644}8645},86468647/**8648* Invoke syncing based on view state8649* @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea8650*/8651sync: function(shouldParseHtml) {8652if (this.editor.currentView.name === "textarea") {8653this.fromTextareaToComposer(shouldParseHtml);8654} else {8655this.fromComposerToTextarea(shouldParseHtml);8656}8657},86588659/**8660* Initializes interval-based syncing8661* also makes sure that on-submit the composer's content is synced with the textarea8662* immediately when the form gets submitted8663*/8664_observe: function() {8665var interval,8666that = this,8667form = this.textarea.element.form,8668startInterval = function() {8669interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);8670},8671stopInterval = function() {8672clearInterval(interval);8673interval = null;8674};86758676startInterval();86778678if (form) {8679// If the textarea is in a form make sure that after onreset and onsubmit the composer8680// has the correct state8681wysihtml5.dom.observe(form, "submit", function() {8682that.sync(true);8683});8684wysihtml5.dom.observe(form, "reset", function() {8685setTimeout(function() { that.fromTextareaToComposer(); }, 0);8686});8687}86888689this.editor.observe("change_view", function(view) {8690if (view === "composer" && !interval) {8691that.fromTextareaToComposer(true);8692startInterval();8693} else if (view === "textarea") {8694that.fromComposerToTextarea(true);8695stopInterval();8696}8697});86988699this.editor.observe("destroy:composer", stopInterval);8700}8701});8702})(wysihtml5);8703wysihtml5.views.Textarea = wysihtml5.views.View.extend(8704/** @scope wysihtml5.views.Textarea.prototype */ {8705name: "textarea",87068707constructor: function(parent, textareaElement, config) {8708this.base(parent, textareaElement, config);87098710this._observe();8711},87128713clear: function() {8714this.element.value = "";8715},87168717getValue: function(parse) {8718var value = this.isEmpty() ? "" : this.element.value;8719if (parse) {8720value = this.parent.parse(value);8721}8722return value;8723},87248725setValue: function(html, parse) {8726if (parse) {8727html = this.parent.parse(html);8728}8729this.element.value = html;8730},87318732hasPlaceholderSet: function() {8733var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),8734placeholderText = this.element.getAttribute("placeholder") || null,8735value = this.element.value,8736isEmpty = !value;8737return (supportsPlaceholder && isEmpty) || (value === placeholderText);8738},87398740isEmpty: function() {8741return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();8742},87438744_observe: function() {8745var element = this.element,8746parent = this.parent,8747eventMapping = {8748focusin: "focus",8749focusout: "blur"8750},8751/**8752* Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events8753* This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai8754*/8755events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];87568757parent.observe("beforeload", function() {8758wysihtml5.dom.observe(element, events, function(event) {8759var eventName = eventMapping[event.type] || event.type;8760parent.fire(eventName).fire(eventName + ":textarea");8761});87628763wysihtml5.dom.observe(element, ["paste", "drop"], function() {8764setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);8765});8766});8767}8768});/**8769* Toolbar Dialog8770*8771* @param {Element} link The toolbar link which causes the dialog to show up8772* @param {Element} container The dialog container8773*8774* @example8775* <!-- Toolbar link -->8776* <a data-wysihtml5-command="insertImage">insert an image</a>8777*8778* <!-- Dialog -->8779* <div data-wysihtml5-dialog="insertImage" style="display: none;">8780* <label>8781* URL: <input data-wysihtml5-dialog-field="src" value="http://">8782* </label>8783* <label>8784* Alternative text: <input data-wysihtml5-dialog-field="alt" value="">8785* </label>8786* </div>8787*8788* <script>8789* var dialog = new wysihtml5.toolbar.Dialog(8790* document.querySelector("[data-wysihtml5-command='insertImage']"),8791* document.querySelector("[data-wysihtml5-dialog='insertImage']")8792* );8793* dialog.observe("save", function(attributes) {8794* // do something8795* });8796* </script>8797*/8798(function(wysihtml5) {8799var dom = wysihtml5.dom,8800CLASS_NAME_OPENED = "wysihtml5-command-dialog-opened",8801SELECTOR_FORM_ELEMENTS = "input, select, textarea",8802SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]",8803ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field";880488058806wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend(8807/** @scope wysihtml5.toolbar.Dialog.prototype */ {8808constructor: function(link, container) {8809this.link = link;8810this.container = container;8811},88128813_observe: function() {8814if (this._observed) {8815return;8816}88178818var that = this,8819callbackWrapper = function(event) {8820var attributes = that._serialize();8821if (attributes == that.elementToChange) {8822that.fire("edit", attributes);8823} else {8824that.fire("save", attributes);8825}8826that.hide();8827event.preventDefault();8828event.stopPropagation();8829};88308831dom.observe(that.link, "click", function(event) {8832if (dom.hasClass(that.link, CLASS_NAME_OPENED)) {8833setTimeout(function() { that.hide(); }, 0);8834}8835});88368837dom.observe(this.container, "keydown", function(event) {8838var keyCode = event.keyCode;8839if (keyCode === wysihtml5.ENTER_KEY) {8840callbackWrapper(event);8841}8842if (keyCode === wysihtml5.ESCAPE_KEY) {8843that.hide();8844}8845});88468847dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper);88488849dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) {8850that.fire("cancel");8851that.hide();8852event.preventDefault();8853event.stopPropagation();8854});88558856var formElements = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS),8857i = 0,8858length = formElements.length,8859_clearInterval = function() { clearInterval(that.interval); };8860for (; i<length; i++) {8861dom.observe(formElements[i], "change", _clearInterval);8862}88638864this._observed = true;8865},88668867/**8868* Grabs all fields in the dialog and puts them in key=>value style in an object which8869* then gets returned8870*/8871_serialize: function() {8872var data = this.elementToChange || {},8873fields = this.container.querySelectorAll(SELECTOR_FIELDS),8874length = fields.length,8875i = 0;8876for (; i<length; i++) {8877data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;8878}8879return data;8880},88818882/**8883* Takes the attributes of the "elementToChange"8884* and inserts them in their corresponding dialog input fields8885*8886* Assume the "elementToChange" looks like this:8887* <a href="http://www.google.com" target="_blank">foo</a>8888*8889* and we have the following dialog:8890* <input type="text" data-wysihtml5-dialog-field="href" value="">8891* <input type="text" data-wysihtml5-dialog-field="target" value="">8892*8893* after calling _interpolate() the dialog will look like this8894* <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com">8895* <input type="text" data-wysihtml5-dialog-field="target" value="_blank">8896*8897* Basically it adopted the attribute values into the corresponding input fields8898*8899*/8900_interpolate: function(avoidHiddenFields) {8901var field,8902fieldName,8903newValue,8904focusedElement = document.querySelector(":focus"),8905fields = this.container.querySelectorAll(SELECTOR_FIELDS),8906length = fields.length,8907i = 0;8908for (; i<length; i++) {8909field = fields[i];89108911// Never change elements where the user is currently typing in8912if (field === focusedElement) {8913continue;8914}89158916// Don't update hidden fields8917// See https://github.com/xing/wysihtml5/pull/148918if (avoidHiddenFields && field.type === "hidden") {8919continue;8920}89218922fieldName = field.getAttribute(ATTRIBUTE_FIELDS);8923newValue = this.elementToChange ? (this.elementToChange[fieldName] || "") : field.defaultValue;8924field.value = newValue;8925}8926},89278928/**8929* Show the dialog element8930*/8931show: function(elementToChange) {8932var that = this,8933firstField = this.container.querySelector(SELECTOR_FORM_ELEMENTS);8934this.elementToChange = elementToChange;8935this._observe();8936this._interpolate();8937if (elementToChange) {8938this.interval = setInterval(function() { that._interpolate(true); }, 500);8939}8940dom.addClass(this.link, CLASS_NAME_OPENED);8941this.container.style.display = "";8942this.fire("show");8943if (firstField && !elementToChange) {8944try {8945firstField.focus();8946} catch(e) {}8947}8948},89498950/**8951* Hide the dialog element8952*/8953hide: function() {8954clearInterval(this.interval);8955this.elementToChange = null;8956dom.removeClass(this.link, CLASS_NAME_OPENED);8957this.container.style.display = "none";8958this.fire("hide");8959}8960});8961})(wysihtml5);8962/**8963* Converts speech-to-text and inserts this into the editor8964* As of now (2011/03/25) this only is supported in Chrome >= 118965*8966* Note that it sends the recorded audio to the google speech recognition api:8967* http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec8968*8969* Current HTML5 draft can be found here8970* http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html8971*8972* "Accessing Google Speech API Chrome 11"8973* http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/8974*/8975(function(wysihtml5) {8976var dom = wysihtml5.dom;89778978var linkStyles = {8979position: "relative"8980};89818982var wrapperStyles = {8983left: 0,8984margin: 0,8985opacity: 0,8986overflow: "hidden",8987padding: 0,8988position: "absolute",8989top: 0,8990zIndex: 18991};89928993var inputStyles = {8994cursor: "inherit",8995fontSize: "50px",8996height: "50px",8997marginTop: "-25px",8998outline: 0,8999padding: 0,9000position: "absolute",9001right: "-4px",9002top: "50%"9003};90049005var inputAttributes = {9006"x-webkit-speech": "",9007"speech": ""9008};90099010wysihtml5.toolbar.Speech = function(parent, link) {9011var input = document.createElement("input");9012if (!wysihtml5.browser.supportsSpeechApiOn(input)) {9013link.style.display = "none";9014return;9015}90169017var wrapper = document.createElement("div");90189019wysihtml5.lang.object(wrapperStyles).merge({9020width: link.offsetWidth + "px",9021height: link.offsetHeight + "px"9022});90239024dom.insert(input).into(wrapper);9025dom.insert(wrapper).into(link);90269027dom.setStyles(inputStyles).on(input);9028dom.setAttributes(inputAttributes).on(input)90299030dom.setStyles(wrapperStyles).on(wrapper);9031dom.setStyles(linkStyles).on(link);90329033var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange";9034dom.observe(input, eventName, function() {9035parent.execCommand("insertText", input.value);9036input.value = "";9037});90389039dom.observe(input, "click", function(event) {9040if (dom.hasClass(link, "wysihtml5-command-disabled")) {9041event.preventDefault();9042}90439044event.stopPropagation();9045});9046};9047})(wysihtml5);/**9048* Toolbar9049*9050* @param {Object} parent Reference to instance of Editor instance9051* @param {Element} container Reference to the toolbar container element9052*9053* @example9054* <div id="toolbar">9055* <a data-wysihtml5-command="createLink">insert link</a>9056* <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a>9057* </div>9058*9059* <script>9060* var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar"));9061* </script>9062*/9063(function(wysihtml5) {9064var CLASS_NAME_COMMAND_DISABLED = "wysihtml5-command-disabled",9065CLASS_NAME_COMMANDS_DISABLED = "wysihtml5-commands-disabled",9066CLASS_NAME_COMMAND_ACTIVE = "wysihtml5-command-active",9067CLASS_NAME_ACTION_ACTIVE = "wysihtml5-action-active",9068dom = wysihtml5.dom;90699070wysihtml5.toolbar.Toolbar = Base.extend(9071/** @scope wysihtml5.toolbar.Toolbar.prototype */ {9072constructor: function(editor, container) {9073this.editor = editor;9074this.container = typeof(container) === "string" ? document.getElementById(container) : container;9075this.composer = editor.composer;90769077this._getLinks("command");9078this._getLinks("action");90799080this._observe();9081this.show();90829083var speechInputLinks = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),9084length = speechInputLinks.length,9085i = 0;9086for (; i<length; i++) {9087new wysihtml5.toolbar.Speech(this, speechInputLinks[i]);9088}9089},90909091_getLinks: function(type) {9092var links = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(),9093length = links.length,9094i = 0,9095mapping = this[type + "Mapping"] = {},9096link,9097group,9098name,9099value,9100dialog;9101for (; i<length; i++) {9102link = links[i];9103name = link.getAttribute("data-wysihtml5-" + type);9104value = link.getAttribute("data-wysihtml5-" + type + "-value");9105group = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']");9106dialog = this._getDialog(link, name);91079108mapping[name + ":" + value] = {9109link: link,9110group: group,9111name: name,9112value: value,9113dialog: dialog,9114state: false9115};9116}9117},91189119_getDialog: function(link, command) {9120var that = this,9121dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"),9122dialog,9123caretBookmark;91249125if (dialogElement) {9126dialog = new wysihtml5.toolbar.Dialog(link, dialogElement);91279128dialog.observe("show", function() {9129caretBookmark = that.composer.selection.getBookmark();91309131that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });9132});91339134dialog.observe("save", function(attributes) {9135if (caretBookmark) {9136that.composer.selection.setBookmark(caretBookmark);9137}9138that._execCommand(command, attributes);91399140that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });9141});91429143dialog.observe("cancel", function() {9144that.editor.focus(false);9145that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });9146});9147}9148return dialog;9149},91509151/**9152* @example9153* var toolbar = new wysihtml5.Toolbar();9154* // Insert a <blockquote> element or wrap current selection in <blockquote>9155* toolbar.execCommand("formatBlock", "blockquote");9156*/9157execCommand: function(command, commandValue) {9158if (this.commandsDisabled) {9159return;9160}91619162var commandObj = this.commandMapping[command + ":" + commandValue];91639164// Show dialog when available9165if (commandObj && commandObj.dialog && !commandObj.state) {9166commandObj.dialog.show();9167} else {9168this._execCommand(command, commandValue);9169}9170},91719172_execCommand: function(command, commandValue) {9173// Make sure that composer is focussed (false => don't move caret to the end)9174this.editor.focus(false);91759176this.composer.commands.exec(command, commandValue);9177this._updateLinkStates();9178},91799180execAction: function(action) {9181var editor = this.editor;9182switch(action) {9183case "change_view":9184if (editor.currentView === editor.textarea) {9185editor.fire("change_view", "composer");9186} else {9187editor.fire("change_view", "textarea");9188}9189break;9190}9191},91929193_observe: function() {9194var that = this,9195editor = this.editor,9196container = this.container,9197links = this.commandLinks.concat(this.actionLinks),9198length = links.length,9199i = 0;92009201for (; i<length; i++) {9202// 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied9203// (you know, a:link { ... } doesn't match anchors with missing href attribute)9204dom.setAttributes({9205href: "javascript:;",9206unselectable: "on"9207}).on(links[i]);9208}92099210// Needed for opera9211dom.delegate(container, "[data-wysihtml5-command]", "mousedown", function(event) { event.preventDefault(); });92129213dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) {9214var link = this,9215command = link.getAttribute("data-wysihtml5-command"),9216commandValue = link.getAttribute("data-wysihtml5-command-value");9217that.execCommand(command, commandValue);9218event.preventDefault();9219});92209221dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) {9222var action = this.getAttribute("data-wysihtml5-action");9223that.execAction(action);9224event.preventDefault();9225});92269227editor.observe("focus:composer", function() {9228that.bookmark = null;9229clearInterval(that.interval);9230that.interval = setInterval(function() { that._updateLinkStates(); }, 500);9231});92329233editor.observe("blur:composer", function() {9234clearInterval(that.interval);9235});92369237editor.observe("destroy:composer", function() {9238clearInterval(that.interval);9239});92409241editor.observe("change_view", function(currentView) {9242// Set timeout needed in order to let the blur event fire first9243setTimeout(function() {9244that.commandsDisabled = (currentView !== "composer");9245that._updateLinkStates();9246if (that.commandsDisabled) {9247dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED);9248} else {9249dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED);9250}9251}, 0);9252});9253},92549255_updateLinkStates: function() {9256var element = this.composer.element,9257commandMapping = this.commandMapping,9258actionMapping = this.actionMapping,9259i,9260state,9261action,9262command;9263// every millisecond counts... this is executed quite often9264for (i in commandMapping) {9265command = commandMapping[i];9266if (this.commandsDisabled) {9267state = false;9268dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);9269if (command.group) {9270dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);9271}9272if (command.dialog) {9273command.dialog.hide();9274}9275} else {9276state = this.composer.commands.state(command.name, command.value);9277if (wysihtml5.lang.object(state).isArray()) {9278// Grab first and only object/element in state array, otherwise convert state into boolean9279// to avoid showing a dialog for multiple selected elements which may have different attributes9280// eg. when two links with different href are selected, the state will be an array consisting of both link elements9281// but the dialog interface can only update one9282state = state.length === 1 ? state[0] : true;9283}9284dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED);9285if (command.group) {9286dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED);9287}9288}92899290if (command.state === state) {9291continue;9292}92939294command.state = state;9295if (state) {9296dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);9297if (command.group) {9298dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE);9299}9300if (command.dialog) {9301if (typeof(state) === "object") {9302command.dialog.show(state);9303} else {9304command.dialog.hide();9305}9306}9307} else {9308dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);9309if (command.group) {9310dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);9311}9312if (command.dialog) {9313command.dialog.hide();9314}9315}9316}93179318for (i in actionMapping) {9319action = actionMapping[i];93209321if (action.name === "change_view") {9322action.state = this.editor.currentView === this.editor.textarea;9323if (action.state) {9324dom.addClass(action.link, CLASS_NAME_ACTION_ACTIVE);9325} else {9326dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE);9327}9328}9329}9330},93319332show: function() {9333this.container.style.display = "";9334},93359336hide: function() {9337this.container.style.display = "none";9338}9339});93409341})(wysihtml5);9342/**9343* WYSIHTML5 Editor9344*9345* @param {Element} textareaElement Reference to the textarea which should be turned into a rich text interface9346* @param {Object} [config] See defaultConfig object below for explanation of each individual config option9347*9348* @events9349* load9350* beforeload (for internal use only)9351* focus9352* focus:composer9353* focus:textarea9354* blur9355* blur:composer9356* blur:textarea9357* change9358* change:composer9359* change:textarea9360* paste9361* paste:composer9362* paste:textarea9363* newword:composer9364* destroy:composer9365* undo:composer9366* redo:composer9367* beforecommand:composer9368* aftercommand:composer9369* change_view9370*/9371(function(wysihtml5) {9372var undef;93739374var defaultConfig = {9375// Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body9376name: undef,9377// Whether the editor should look like the textarea (by adopting styles)9378style: true,9379// Id of the toolbar element, pass falsey value if you don't want any toolbar logic9380toolbar: undef,9381// Whether urls, entered by the user should automatically become clickable-links9382autoLink: true,9383// Object which includes parser rules to apply when html gets inserted via copy & paste9384// See parser_rules/*.js for examples9385parserRules: { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} },9386// Parser method to use when the user inserts content via copy & paste9387parser: wysihtml5.dom.parse,9388// Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option9389composerClassName: "wysihtml5-editor",9390// Class name to add to the body when the wysihtml5 editor is supported9391bodyClassName: "wysihtml5-supported",9392// Array (or single string) of stylesheet urls to be loaded in the editor's iframe9393stylesheets: [],9394// Placeholder text to use, defaults to the placeholder attribute on the textarea element9395placeholderText: undef,9396// Whether the composer should allow the user to manually resize images, tables etc.9397allowObjectResizing: true,9398// Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5)9399supportTouchDevices: true9400};94019402wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend(9403/** @scope wysihtml5.Editor.prototype */ {9404constructor: function(textareaElement, config) {9405this.textareaElement = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement;9406this.config = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();9407this.textarea = new wysihtml5.views.Textarea(this, this.textareaElement, this.config);9408this.currentView = this.textarea;9409this._isCompatible = wysihtml5.browser.supported();94109411// Sort out unsupported/unwanted browsers here9412if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) {9413var that = this;9414setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);9415return;9416}94179418// Add class name to body, to indicate that the editor is supported9419wysihtml5.dom.addClass(document.body, this.config.bodyClassName);94209421this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config);9422this.currentView = this.composer;94239424if (typeof(this.config.parser) === "function") {9425this._initParser();9426}94279428this.observe("beforeload", function() {9429this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer);9430if (this.config.toolbar) {9431this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar);9432}9433});94349435try {9436console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5");9437} catch(e) {}9438},94399440isCompatible: function() {9441return this._isCompatible;9442},94439444clear: function() {9445this.currentView.clear();9446return this;9447},94489449getValue: function(parse) {9450return this.currentView.getValue(parse);9451},94529453setValue: function(html, parse) {9454if (!html) {9455return this.clear();9456}9457this.currentView.setValue(html, parse);9458return this;9459},94609461focus: function(setToEnd) {9462this.currentView.focus(setToEnd);9463return this;9464},94659466/**9467* Deactivate editor (make it readonly)9468*/9469disable: function() {9470this.currentView.disable();9471return this;9472},94739474/**9475* Activate editor9476*/9477enable: function() {9478this.currentView.enable();9479return this;9480},94819482isEmpty: function() {9483return this.currentView.isEmpty();9484},94859486hasPlaceholderSet: function() {9487return this.currentView.hasPlaceholderSet();9488},94899490parse: function(htmlOrElement) {9491var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true);9492if (typeof(htmlOrElement) === "object") {9493wysihtml5.quirks.redraw(htmlOrElement);9494}9495return returnValue;9496},94979498/**9499* Prepare html parser logic9500* - Observes for paste and drop9501*/9502_initParser: function() {9503this.observe("paste:composer", function() {9504var keepScrollPosition = true,9505that = this;9506that.composer.selection.executeAndRestore(function() {9507wysihtml5.quirks.cleanPastedHTML(that.composer.element);9508that.parse(that.composer.element);9509}, keepScrollPosition);9510});95119512this.observe("paste:textarea", function() {9513var value = this.textarea.getValue(),9514newValue;9515newValue = this.parse(value);9516this.textarea.setValue(newValue);9517});9518}9519});9520})(wysihtml5);952195229523