Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
16345 anikendra 1
/**
2
 * @license wysihtml5 v0.3.0_rc2
3
 * https://github.com/xing/wysihtml5
4
 *
5
 * Author: Christopher Blum (https://github.com/tiff)
6
 *
7
 * Copyright (C) 2012 XING AG
8
 * Licensed under the MIT license (MIT)
9
 *
10
 */
11
var wysihtml5 = {
12
  version: "0.3.0_rc2",
13
 
14
  // namespaces
15
  commands:   {},
16
  dom:        {},
17
  quirks:     {},
18
  toolbar:    {},
19
  lang:       {},
20
  selection:  {},
21
  views:      {},
22
 
23
  INVISIBLE_SPACE: "\uFEFF",
24
 
25
  EMPTY_FUNCTION: function() {},
26
 
27
  ELEMENT_NODE: 1,
28
  TEXT_NODE:    3,
29
 
30
  BACKSPACE_KEY:  8,
31
  ENTER_KEY:      13,
32
  ESCAPE_KEY:     27,
33
  SPACE_KEY:      32,
34
  DELETE_KEY:     46
35
};/**
36
 * @license Rangy, a cross-browser JavaScript range and selection library
37
 * http://code.google.com/p/rangy/
38
 *
39
 * Copyright 2011, Tim Down
40
 * Licensed under the MIT license.
41
 * Version: 1.2.2
42
 * Build date: 13 November 2011
43
 */
44
window['rangy'] = (function() {
45
 
46
 
47
    var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
48
 
49
    var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
50
        "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];
51
 
52
    var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
53
        "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
54
        "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
55
 
56
    var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
57
 
58
    // Subset of TextRange's full set of methods that we're interested in
59
    var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",
60
        "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];
61
 
62
    /*----------------------------------------------------------------------------------------------------------------*/
63
 
64
    // Trio of functions taken from Peter Michaux's article:
65
    // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
66
    function isHostMethod(o, p) {
67
        var t = typeof o[p];
68
        return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
69
    }
70
 
71
    function isHostObject(o, p) {
72
        return !!(typeof o[p] == OBJECT && o[p]);
73
    }
74
 
75
    function isHostProperty(o, p) {
76
        return typeof o[p] != UNDEFINED;
77
    }
78
 
79
    // Creates a convenience function to save verbose repeated calls to tests functions
80
    function createMultiplePropertyTest(testFunc) {
81
        return function(o, props) {
82
            var i = props.length;
83
            while (i--) {
84
                if (!testFunc(o, props[i])) {
85
                    return false;
86
                }
87
            }
88
            return true;
89
        };
90
    }
91
 
92
    // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
93
    var areHostMethods = createMultiplePropertyTest(isHostMethod);
94
    var areHostObjects = createMultiplePropertyTest(isHostObject);
95
    var areHostProperties = createMultiplePropertyTest(isHostProperty);
96
 
97
    function isTextRange(range) {
98
        return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
99
    }
100
 
101
    var api = {
102
        version: "1.2.2",
103
        initialized: false,
104
        supported: true,
105
 
106
        util: {
107
            isHostMethod: isHostMethod,
108
            isHostObject: isHostObject,
109
            isHostProperty: isHostProperty,
110
            areHostMethods: areHostMethods,
111
            areHostObjects: areHostObjects,
112
            areHostProperties: areHostProperties,
113
            isTextRange: isTextRange
114
        },
115
 
116
        features: {},
117
 
118
        modules: {},
119
        config: {
120
            alertOnWarn: false,
121
            preferTextRange: false
122
        }
123
    };
124
 
125
    function fail(reason) {
126
        window.alert("Rangy not supported in your browser. Reason: " + reason);
127
        api.initialized = true;
128
        api.supported = false;
129
    }
130
 
131
    api.fail = fail;
132
 
133
    function warn(msg) {
134
        var warningMessage = "Rangy warning: " + msg;
135
        if (api.config.alertOnWarn) {
136
            window.alert(warningMessage);
137
        } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {
138
            window.console.log(warningMessage);
139
        }
140
    }
141
 
142
    api.warn = warn;
143
 
144
    if ({}.hasOwnProperty) {
145
        api.util.extend = function(o, props) {
146
            for (var i in props) {
147
                if (props.hasOwnProperty(i)) {
148
                    o[i] = props[i];
149
                }
150
            }
151
        };
152
    } else {
153
        fail("hasOwnProperty not supported");
154
    }
155
 
156
    var initListeners = [];
157
    var moduleInitializers = [];
158
 
159
    // Initialization
160
    function init() {
161
        if (api.initialized) {
162
            return;
163
        }
164
        var testRange;
165
        var implementsDomRange = false, implementsTextRange = false;
166
 
167
        // First, perform basic feature tests
168
 
169
        if (isHostMethod(document, "createRange")) {
170
            testRange = document.createRange();
171
            if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
172
                implementsDomRange = true;
173
            }
174
            testRange.detach();
175
        }
176
 
177
        var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
178
 
179
        if (body && isHostMethod(body, "createTextRange")) {
180
            testRange = body.createTextRange();
181
            if (isTextRange(testRange)) {
182
                implementsTextRange = true;
183
            }
184
        }
185
 
186
        if (!implementsDomRange && !implementsTextRange) {
187
            fail("Neither Range nor TextRange are implemented");
188
        }
189
 
190
        api.initialized = true;
191
        api.features = {
192
            implementsDomRange: implementsDomRange,
193
            implementsTextRange: implementsTextRange
194
        };
195
 
196
        // Initialize modules and call init listeners
197
        var allListeners = moduleInitializers.concat(initListeners);
198
        for (var i = 0, len = allListeners.length; i < len; ++i) {
199
            try {
200
                allListeners[i](api);
201
            } catch (ex) {
202
                if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
203
                    window.console.log("Init listener threw an exception. Continuing.", ex);
204
                }
205
 
206
            }
207
        }
208
    }
209
 
210
    // Allow external scripts to initialize this library in case it's loaded after the document has loaded
211
    api.init = init;
212
 
213
    // Execute listener immediately if already initialized
214
    api.addInitListener = function(listener) {
215
        if (api.initialized) {
216
            listener(api);
217
        } else {
218
            initListeners.push(listener);
219
        }
220
    };
221
 
222
    var createMissingNativeApiListeners = [];
223
 
224
    api.addCreateMissingNativeApiListener = function(listener) {
225
        createMissingNativeApiListeners.push(listener);
226
    };
227
 
228
    function createMissingNativeApi(win) {
229
        win = win || window;
230
        init();
231
 
232
        // Notify listeners
233
        for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {
234
            createMissingNativeApiListeners[i](win);
235
        }
236
    }
237
 
238
    api.createMissingNativeApi = createMissingNativeApi;
239
 
240
    /**
241
     * @constructor
242
     */
243
    function Module(name) {
244
        this.name = name;
245
        this.initialized = false;
246
        this.supported = false;
247
    }
248
 
249
    Module.prototype.fail = function(reason) {
250
        this.initialized = true;
251
        this.supported = false;
252
 
253
        throw new Error("Module '" + this.name + "' failed to load: " + reason);
254
    };
255
 
256
    Module.prototype.warn = function(msg) {
257
        api.warn("Module " + this.name + ": " + msg);
258
    };
259
 
260
    Module.prototype.createError = function(msg) {
261
        return new Error("Error in Rangy " + this.name + " module: " + msg);
262
    };
263
 
264
    api.createModule = function(name, initFunc) {
265
        var module = new Module(name);
266
        api.modules[name] = module;
267
 
268
        moduleInitializers.push(function(api) {
269
            initFunc(api, module);
270
            module.initialized = true;
271
            module.supported = true;
272
        });
273
    };
274
 
275
    api.requireModules = function(modules) {
276
        for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {
277
            moduleName = modules[i];
278
            module = api.modules[moduleName];
279
            if (!module || !(module instanceof Module)) {
280
                throw new Error("Module '" + moduleName + "' not found");
281
            }
282
            if (!module.supported) {
283
                throw new Error("Module '" + moduleName + "' not supported");
284
            }
285
        }
286
    };
287
 
288
    /*----------------------------------------------------------------------------------------------------------------*/
289
 
290
    // Wait for document to load before running tests
291
 
292
    var docReady = false;
293
 
294
    var loadHandler = function(e) {
295
 
296
        if (!docReady) {
297
            docReady = true;
298
            if (!api.initialized) {
299
                init();
300
            }
301
        }
302
    };
303
 
304
    // Test whether we have window and document objects that we will need
305
    if (typeof window == UNDEFINED) {
306
        fail("No window found");
307
        return;
308
    }
309
    if (typeof document == UNDEFINED) {
310
        fail("No document found");
311
        return;
312
    }
313
 
314
    if (isHostMethod(document, "addEventListener")) {
315
        document.addEventListener("DOMContentLoaded", loadHandler, false);
316
    }
317
 
318
    // Add a fallback in case the DOMContentLoaded event isn't supported
319
    if (isHostMethod(window, "addEventListener")) {
320
        window.addEventListener("load", loadHandler, false);
321
    } else if (isHostMethod(window, "attachEvent")) {
322
        window.attachEvent("onload", loadHandler);
323
    } else {
324
        fail("Window does not have required addEventListener or attachEvent method");
325
    }
326
 
327
    return api;
328
})();
329
rangy.createModule("DomUtil", function(api, module) {
330
 
331
    var UNDEF = "undefined";
332
    var util = api.util;
333
 
334
    // Perform feature tests
335
    if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
336
        module.fail("document missing a Node creation method");
337
    }
338
 
339
    if (!util.isHostMethod(document, "getElementsByTagName")) {
340
        module.fail("document missing getElementsByTagName method");
341
    }
342
 
343
    var el = document.createElement("div");
344
    if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
345
            !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
346
        module.fail("Incomplete Element implementation");
347
    }
348
 
349
    // innerHTML is required for Range's createContextualFragment method
350
    if (!util.isHostProperty(el, "innerHTML")) {
351
        module.fail("Element is missing innerHTML property");
352
    }
353
 
354
    var textNode = document.createTextNode("test");
355
    if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
356
            !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
357
            !util.areHostProperties(textNode, ["data"]))) {
358
        module.fail("Incomplete Text Node implementation");
359
    }
360
 
361
    /*----------------------------------------------------------------------------------------------------------------*/
362
 
363
    // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
364
    // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
365
    // contains just the document as a single element and the value searched for is the document.
366
    var arrayContains = /*Array.prototype.indexOf ?
367
        function(arr, val) {
368
            return arr.indexOf(val) > -1;
369
        }:*/
370
 
371
        function(arr, val) {
372
            var i = arr.length;
373
            while (i--) {
374
                if (arr[i] === val) {
375
                    return true;
376
                }
377
            }
378
            return false;
379
        };
380
 
381
    // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
382
    function isHtmlNamespace(node) {
383
        var ns;
384
        return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
385
    }
386
 
387
    function parentElement(node) {
388
        var parent = node.parentNode;
389
        return (parent.nodeType == 1) ? parent : null;
390
    }
391
 
392
    function getNodeIndex(node) {
393
        var i = 0;
394
        while( (node = node.previousSibling) ) {
395
            i++;
396
        }
397
        return i;
398
    }
399
 
400
    function getNodeLength(node) {
401
        var childNodes;
402
        return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);
403
    }
404
 
405
    function getCommonAncestor(node1, node2) {
406
        var ancestors = [], n;
407
        for (n = node1; n; n = n.parentNode) {
408
            ancestors.push(n);
409
        }
410
 
411
        for (n = node2; n; n = n.parentNode) {
412
            if (arrayContains(ancestors, n)) {
413
                return n;
414
            }
415
        }
416
 
417
        return null;
418
    }
419
 
420
    function isAncestorOf(ancestor, descendant, selfIsAncestor) {
421
        var n = selfIsAncestor ? descendant : descendant.parentNode;
422
        while (n) {
423
            if (n === ancestor) {
424
                return true;
425
            } else {
426
                n = n.parentNode;
427
            }
428
        }
429
        return false;
430
    }
431
 
432
    function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
433
        var p, n = selfIsAncestor ? node : node.parentNode;
434
        while (n) {
435
            p = n.parentNode;
436
            if (p === ancestor) {
437
                return n;
438
            }
439
            n = p;
440
        }
441
        return null;
442
    }
443
 
444
    function isCharacterDataNode(node) {
445
        var t = node.nodeType;
446
        return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
447
    }
448
 
449
    function insertAfter(node, precedingNode) {
450
        var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
451
        if (nextNode) {
452
            parent.insertBefore(node, nextNode);
453
        } else {
454
            parent.appendChild(node);
455
        }
456
        return node;
457
    }
458
 
459
    // Note that we cannot use splitText() because it is bugridden in IE 9.
460
    function splitDataNode(node, index) {
461
        var newNode = node.cloneNode(false);
462
        newNode.deleteData(0, index);
463
        node.deleteData(index, node.length - index);
464
        insertAfter(newNode, node);
465
        return newNode;
466
    }
467
 
468
    function getDocument(node) {
469
        if (node.nodeType == 9) {
470
            return node;
471
        } else if (typeof node.ownerDocument != UNDEF) {
472
            return node.ownerDocument;
473
        } else if (typeof node.document != UNDEF) {
474
            return node.document;
475
        } else if (node.parentNode) {
476
            return getDocument(node.parentNode);
477
        } else {
478
            throw new Error("getDocument: no document found for node");
479
        }
480
    }
481
 
482
    function getWindow(node) {
483
        var doc = getDocument(node);
484
        if (typeof doc.defaultView != UNDEF) {
485
            return doc.defaultView;
486
        } else if (typeof doc.parentWindow != UNDEF) {
487
            return doc.parentWindow;
488
        } else {
489
            throw new Error("Cannot get a window object for node");
490
        }
491
    }
492
 
493
    function getIframeDocument(iframeEl) {
494
        if (typeof iframeEl.contentDocument != UNDEF) {
495
            return iframeEl.contentDocument;
496
        } else if (typeof iframeEl.contentWindow != UNDEF) {
497
            return iframeEl.contentWindow.document;
498
        } else {
499
            throw new Error("getIframeWindow: No Document object found for iframe element");
500
        }
501
    }
502
 
503
    function getIframeWindow(iframeEl) {
504
        if (typeof iframeEl.contentWindow != UNDEF) {
505
            return iframeEl.contentWindow;
506
        } else if (typeof iframeEl.contentDocument != UNDEF) {
507
            return iframeEl.contentDocument.defaultView;
508
        } else {
509
            throw new Error("getIframeWindow: No Window object found for iframe element");
510
        }
511
    }
512
 
513
    function getBody(doc) {
514
        return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
515
    }
516
 
517
    function getRootContainer(node) {
518
        var parent;
519
        while ( (parent = node.parentNode) ) {
520
            node = parent;
521
        }
522
        return node;
523
    }
524
 
525
    function comparePoints(nodeA, offsetA, nodeB, offsetB) {
526
        // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
527
        var nodeC, root, childA, childB, n;
528
        if (nodeA == nodeB) {
529
 
530
            // Case 1: nodes are the same
531
            return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
532
        } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
533
 
534
            // Case 2: node C (container B or an ancestor) is a child node of A
535
            return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
536
        } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
537
 
538
            // Case 3: node C (container A or an ancestor) is a child node of B
539
            return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
540
        } else {
541
 
542
            // Case 4: containers are siblings or descendants of siblings
543
            root = getCommonAncestor(nodeA, nodeB);
544
            childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
545
            childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
546
 
547
            if (childA === childB) {
548
                // This shouldn't be possible
549
 
550
                throw new Error("comparePoints got to case 4 and childA and childB are the same!");
551
            } else {
552
                n = root.firstChild;
553
                while (n) {
554
                    if (n === childA) {
555
                        return -1;
556
                    } else if (n === childB) {
557
                        return 1;
558
                    }
559
                    n = n.nextSibling;
560
                }
561
                throw new Error("Should not be here!");
562
            }
563
        }
564
    }
565
 
566
    function fragmentFromNodeChildren(node) {
567
        var fragment = getDocument(node).createDocumentFragment(), child;
568
        while ( (child = node.firstChild) ) {
569
            fragment.appendChild(child);
570
        }
571
        return fragment;
572
    }
573
 
574
    function inspectNode(node) {
575
        if (!node) {
576
            return "[No node]";
577
        }
578
        if (isCharacterDataNode(node)) {
579
            return '"' + node.data + '"';
580
        } else if (node.nodeType == 1) {
581
            var idAttr = node.id ? ' id="' + node.id + '"' : "";
582
            return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";
583
        } else {
584
            return node.nodeName;
585
        }
586
    }
587
 
588
    /**
589
     * @constructor
590
     */
591
    function NodeIterator(root) {
592
        this.root = root;
593
        this._next = root;
594
    }
595
 
596
    NodeIterator.prototype = {
597
        _current: null,
598
 
599
        hasNext: function() {
600
            return !!this._next;
601
        },
602
 
603
        next: function() {
604
            var n = this._current = this._next;
605
            var child, next;
606
            if (this._current) {
607
                child = n.firstChild;
608
                if (child) {
609
                    this._next = child;
610
                } else {
611
                    next = null;
612
                    while ((n !== this.root) && !(next = n.nextSibling)) {
613
                        n = n.parentNode;
614
                    }
615
                    this._next = next;
616
                }
617
            }
618
            return this._current;
619
        },
620
 
621
        detach: function() {
622
            this._current = this._next = this.root = null;
623
        }
624
    };
625
 
626
    function createIterator(root) {
627
        return new NodeIterator(root);
628
    }
629
 
630
    /**
631
     * @constructor
632
     */
633
    function DomPosition(node, offset) {
634
        this.node = node;
635
        this.offset = offset;
636
    }
637
 
638
    DomPosition.prototype = {
639
        equals: function(pos) {
640
            return this.node === pos.node & this.offset == pos.offset;
641
        },
642
 
643
        inspect: function() {
644
            return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
645
        }
646
    };
647
 
648
    /**
649
     * @constructor
650
     */
651
    function DOMException(codeName) {
652
        this.code = this[codeName];
653
        this.codeName = codeName;
654
        this.message = "DOMException: " + this.codeName;
655
    }
656
 
657
    DOMException.prototype = {
658
        INDEX_SIZE_ERR: 1,
659
        HIERARCHY_REQUEST_ERR: 3,
660
        WRONG_DOCUMENT_ERR: 4,
661
        NO_MODIFICATION_ALLOWED_ERR: 7,
662
        NOT_FOUND_ERR: 8,
663
        NOT_SUPPORTED_ERR: 9,
664
        INVALID_STATE_ERR: 11
665
    };
666
 
667
    DOMException.prototype.toString = function() {
668
        return this.message;
669
    };
670
 
671
    api.dom = {
672
        arrayContains: arrayContains,
673
        isHtmlNamespace: isHtmlNamespace,
674
        parentElement: parentElement,
675
        getNodeIndex: getNodeIndex,
676
        getNodeLength: getNodeLength,
677
        getCommonAncestor: getCommonAncestor,
678
        isAncestorOf: isAncestorOf,
679
        getClosestAncestorIn: getClosestAncestorIn,
680
        isCharacterDataNode: isCharacterDataNode,
681
        insertAfter: insertAfter,
682
        splitDataNode: splitDataNode,
683
        getDocument: getDocument,
684
        getWindow: getWindow,
685
        getIframeWindow: getIframeWindow,
686
        getIframeDocument: getIframeDocument,
687
        getBody: getBody,
688
        getRootContainer: getRootContainer,
689
        comparePoints: comparePoints,
690
        inspectNode: inspectNode,
691
        fragmentFromNodeChildren: fragmentFromNodeChildren,
692
        createIterator: createIterator,
693
        DomPosition: DomPosition
694
    };
695
 
696
    api.DOMException = DOMException;
697
});rangy.createModule("DomRange", function(api, module) {
698
    api.requireModules( ["DomUtil"] );
699
 
700
 
701
    var dom = api.dom;
702
    var DomPosition = dom.DomPosition;
703
    var DOMException = api.DOMException;
704
 
705
    /*----------------------------------------------------------------------------------------------------------------*/
706
 
707
    // Utility functions
708
 
709
    function isNonTextPartiallySelected(node, range) {
710
        return (node.nodeType != 3) &&
711
               (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
712
    }
713
 
714
    function getRangeDocument(range) {
715
        return dom.getDocument(range.startContainer);
716
    }
717
 
718
    function dispatchEvent(range, type, args) {
719
        var listeners = range._listeners[type];
720
        if (listeners) {
721
            for (var i = 0, len = listeners.length; i < len; ++i) {
722
                listeners[i].call(range, {target: range, args: args});
723
            }
724
        }
725
    }
726
 
727
    function getBoundaryBeforeNode(node) {
728
        return new DomPosition(node.parentNode, dom.getNodeIndex(node));
729
    }
730
 
731
    function getBoundaryAfterNode(node) {
732
        return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
733
    }
734
 
735
    function insertNodeAtPosition(node, n, o) {
736
        var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
737
        if (dom.isCharacterDataNode(n)) {
738
            if (o == n.length) {
739
                dom.insertAfter(node, n);
740
            } else {
741
                n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
742
            }
743
        } else if (o >= n.childNodes.length) {
744
            n.appendChild(node);
745
        } else {
746
            n.insertBefore(node, n.childNodes[o]);
747
        }
748
        return firstNodeInserted;
749
    }
750
 
751
    function cloneSubtree(iterator) {
752
        var partiallySelected;
753
        for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
754
            partiallySelected = iterator.isPartiallySelectedSubtree();
755
 
756
            node = node.cloneNode(!partiallySelected);
757
            if (partiallySelected) {
758
                subIterator = iterator.getSubtreeIterator();
759
                node.appendChild(cloneSubtree(subIterator));
760
                subIterator.detach(true);
761
            }
762
 
763
            if (node.nodeType == 10) { // DocumentType
764
                throw new DOMException("HIERARCHY_REQUEST_ERR");
765
            }
766
            frag.appendChild(node);
767
        }
768
        return frag;
769
    }
770
 
771
    function iterateSubtree(rangeIterator, func, iteratorState) {
772
        var it, n;
773
        iteratorState = iteratorState || { stop: false };
774
        for (var node, subRangeIterator; node = rangeIterator.next(); ) {
775
            //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
776
            if (rangeIterator.isPartiallySelectedSubtree()) {
777
                // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
778
                // node selected by the Range.
779
                if (func(node) === false) {
780
                    iteratorState.stop = true;
781
                    return;
782
                } else {
783
                    subRangeIterator = rangeIterator.getSubtreeIterator();
784
                    iterateSubtree(subRangeIterator, func, iteratorState);
785
                    subRangeIterator.detach(true);
786
                    if (iteratorState.stop) {
787
                        return;
788
                    }
789
                }
790
            } else {
791
                // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
792
                // descendant
793
                it = dom.createIterator(node);
794
                while ( (n = it.next()) ) {
795
                    if (func(n) === false) {
796
                        iteratorState.stop = true;
797
                        return;
798
                    }
799
                }
800
            }
801
        }
802
    }
803
 
804
    function deleteSubtree(iterator) {
805
        var subIterator;
806
        while (iterator.next()) {
807
            if (iterator.isPartiallySelectedSubtree()) {
808
                subIterator = iterator.getSubtreeIterator();
809
                deleteSubtree(subIterator);
810
                subIterator.detach(true);
811
            } else {
812
                iterator.remove();
813
            }
814
        }
815
    }
816
 
817
    function extractSubtree(iterator) {
818
 
819
        for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
820
 
821
 
822
            if (iterator.isPartiallySelectedSubtree()) {
823
                node = node.cloneNode(false);
824
                subIterator = iterator.getSubtreeIterator();
825
                node.appendChild(extractSubtree(subIterator));
826
                subIterator.detach(true);
827
            } else {
828
                iterator.remove();
829
            }
830
            if (node.nodeType == 10) { // DocumentType
831
                throw new DOMException("HIERARCHY_REQUEST_ERR");
832
            }
833
            frag.appendChild(node);
834
        }
835
        return frag;
836
    }
837
 
838
    function getNodesInRange(range, nodeTypes, filter) {
839
        //log.info("getNodesInRange, " + nodeTypes.join(","));
840
        var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
841
        var filterExists = !!filter;
842
        if (filterNodeTypes) {
843
            regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
844
        }
845
 
846
        var nodes = [];
847
        iterateSubtree(new RangeIterator(range, false), function(node) {
848
            if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
849
                nodes.push(node);
850
            }
851
        });
852
        return nodes;
853
    }
854
 
855
    function inspect(range) {
856
        var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
857
        return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
858
                dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
859
    }
860
 
861
    /*----------------------------------------------------------------------------------------------------------------*/
862
 
863
    // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
864
 
865
    /**
866
     * @constructor
867
     */
868
    function RangeIterator(range, clonePartiallySelectedTextNodes) {
869
        this.range = range;
870
        this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
871
 
872
 
873
 
874
        if (!range.collapsed) {
875
            this.sc = range.startContainer;
876
            this.so = range.startOffset;
877
            this.ec = range.endContainer;
878
            this.eo = range.endOffset;
879
            var root = range.commonAncestorContainer;
880
 
881
            if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
882
                this.isSingleCharacterDataNode = true;
883
                this._first = this._last = this._next = this.sc;
884
            } else {
885
                this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
886
                    this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
887
                this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
888
                    this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
889
            }
890
 
891
        }
892
    }
893
 
894
    RangeIterator.prototype = {
895
        _current: null,
896
        _next: null,
897
        _first: null,
898
        _last: null,
899
        isSingleCharacterDataNode: false,
900
 
901
        reset: function() {
902
            this._current = null;
903
            this._next = this._first;
904
        },
905
 
906
        hasNext: function() {
907
            return !!this._next;
908
        },
909
 
910
        next: function() {
911
            // Move to next node
912
            var current = this._current = this._next;
913
            if (current) {
914
                this._next = (current !== this._last) ? current.nextSibling : null;
915
 
916
                // Check for partially selected text nodes
917
                if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
918
                    if (current === this.ec) {
919
 
920
                        (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
921
                    }
922
                    if (this._current === this.sc) {
923
 
924
                        (current = current.cloneNode(true)).deleteData(0, this.so);
925
                    }
926
                }
927
            }
928
 
929
            return current;
930
        },
931
 
932
        remove: function() {
933
            var current = this._current, start, end;
934
 
935
            if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
936
                start = (current === this.sc) ? this.so : 0;
937
                end = (current === this.ec) ? this.eo : current.length;
938
                if (start != end) {
939
                    current.deleteData(start, end - start);
940
                }
941
            } else {
942
                if (current.parentNode) {
943
                    current.parentNode.removeChild(current);
944
                } else {
945
 
946
                }
947
            }
948
        },
949
 
950
        // Checks if the current node is partially selected
951
        isPartiallySelectedSubtree: function() {
952
            var current = this._current;
953
            return isNonTextPartiallySelected(current, this.range);
954
        },
955
 
956
        getSubtreeIterator: function() {
957
            var subRange;
958
            if (this.isSingleCharacterDataNode) {
959
                subRange = this.range.cloneRange();
960
                subRange.collapse();
961
            } else {
962
                subRange = new Range(getRangeDocument(this.range));
963
                var current = this._current;
964
                var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);
965
 
966
                if (dom.isAncestorOf(current, this.sc, true)) {
967
                    startContainer = this.sc;
968
                    startOffset = this.so;
969
                }
970
                if (dom.isAncestorOf(current, this.ec, true)) {
971
                    endContainer = this.ec;
972
                    endOffset = this.eo;
973
                }
974
 
975
                updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
976
            }
977
            return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
978
        },
979
 
980
        detach: function(detachRange) {
981
            if (detachRange) {
982
                this.range.detach();
983
            }
984
            this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
985
        }
986
    };
987
 
988
    /*----------------------------------------------------------------------------------------------------------------*/
989
 
990
    // Exceptions
991
 
992
    /**
993
     * @constructor
994
     */
995
    function RangeException(codeName) {
996
        this.code = this[codeName];
997
        this.codeName = codeName;
998
        this.message = "RangeException: " + this.codeName;
999
    }
1000
 
1001
    RangeException.prototype = {
1002
        BAD_BOUNDARYPOINTS_ERR: 1,
1003
        INVALID_NODE_TYPE_ERR: 2
1004
    };
1005
 
1006
    RangeException.prototype.toString = function() {
1007
        return this.message;
1008
    };
1009
 
1010
    /*----------------------------------------------------------------------------------------------------------------*/
1011
 
1012
    /**
1013
     * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
1014
     * TODO: Look into making this a proper iterator, not requiring preloading everything first
1015
     * @constructor
1016
     */
1017
    function RangeNodeIterator(range, nodeTypes, filter) {
1018
        this.nodes = getNodesInRange(range, nodeTypes, filter);
1019
        this._next = this.nodes[0];
1020
        this._position = 0;
1021
    }
1022
 
1023
    RangeNodeIterator.prototype = {
1024
        _current: null,
1025
 
1026
        hasNext: function() {
1027
            return !!this._next;
1028
        },
1029
 
1030
        next: function() {
1031
            this._current = this._next;
1032
            this._next = this.nodes[ ++this._position ];
1033
            return this._current;
1034
        },
1035
 
1036
        detach: function() {
1037
            this._current = this._next = this.nodes = null;
1038
        }
1039
    };
1040
 
1041
    var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1042
    var rootContainerNodeTypes = [2, 9, 11];
1043
    var readonlyNodeTypes = [5, 6, 10, 12];
1044
    var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1045
    var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1046
 
1047
    function createAncestorFinder(nodeTypes) {
1048
        return function(node, selfIsAncestor) {
1049
            var t, n = selfIsAncestor ? node : node.parentNode;
1050
            while (n) {
1051
                t = n.nodeType;
1052
                if (dom.arrayContains(nodeTypes, t)) {
1053
                    return n;
1054
                }
1055
                n = n.parentNode;
1056
            }
1057
            return null;
1058
        };
1059
    }
1060
 
1061
    var getRootContainer = dom.getRootContainer;
1062
    var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1063
    var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1064
    var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1065
 
1066
    function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1067
        if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1068
            throw new RangeException("INVALID_NODE_TYPE_ERR");
1069
        }
1070
    }
1071
 
1072
    function assertNotDetached(range) {
1073
        if (!range.startContainer) {
1074
            throw new DOMException("INVALID_STATE_ERR");
1075
        }
1076
    }
1077
 
1078
    function assertValidNodeType(node, invalidTypes) {
1079
        if (!dom.arrayContains(invalidTypes, node.nodeType)) {
1080
            throw new RangeException("INVALID_NODE_TYPE_ERR");
1081
        }
1082
    }
1083
 
1084
    function assertValidOffset(node, offset) {
1085
        if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1086
            throw new DOMException("INDEX_SIZE_ERR");
1087
        }
1088
    }
1089
 
1090
    function assertSameDocumentOrFragment(node1, node2) {
1091
        if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1092
            throw new DOMException("WRONG_DOCUMENT_ERR");
1093
        }
1094
    }
1095
 
1096
    function assertNodeNotReadOnly(node) {
1097
        if (getReadonlyAncestor(node, true)) {
1098
            throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1099
        }
1100
    }
1101
 
1102
    function assertNode(node, codeName) {
1103
        if (!node) {
1104
            throw new DOMException(codeName);
1105
        }
1106
    }
1107
 
1108
    function isOrphan(node) {
1109
        return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
1110
    }
1111
 
1112
    function isValidOffset(node, offset) {
1113
        return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
1114
    }
1115
 
1116
    function assertRangeValid(range) {
1117
        assertNotDetached(range);
1118
        if (isOrphan(range.startContainer) || isOrphan(range.endContainer) ||
1119
                !isValidOffset(range.startContainer, range.startOffset) ||
1120
                !isValidOffset(range.endContainer, range.endOffset)) {
1121
            throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
1122
        }
1123
    }
1124
 
1125
    /*----------------------------------------------------------------------------------------------------------------*/
1126
 
1127
    // Test the browser's innerHTML support to decide how to implement createContextualFragment
1128
    var styleEl = document.createElement("style");
1129
    var htmlParsingConforms = false;
1130
    try {
1131
        styleEl.innerHTML = "<b>x</b>";
1132
        htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1133
    } catch (e) {
1134
        // IE 6 and 7 throw
1135
    }
1136
 
1137
    api.features.htmlParsingConforms = htmlParsingConforms;
1138
 
1139
    var createContextualFragment = htmlParsingConforms ?
1140
 
1141
        // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1142
        // discussion and base code for this implementation at issue 67.
1143
        // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1144
        // Thanks to Aleks Williams.
1145
        function(fragmentStr) {
1146
            // "Let node the context object's start's node."
1147
            var node = this.startContainer;
1148
            var doc = dom.getDocument(node);
1149
 
1150
            // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1151
            // exception and abort these steps."
1152
            if (!node) {
1153
                throw new DOMException("INVALID_STATE_ERR");
1154
            }
1155
 
1156
            // "Let element be as follows, depending on node's interface:"
1157
            // Document, Document Fragment: null
1158
            var el = null;
1159
 
1160
            // "Element: node"
1161
            if (node.nodeType == 1) {
1162
                el = node;
1163
 
1164
            // "Text, Comment: node's parentElement"
1165
            } else if (dom.isCharacterDataNode(node)) {
1166
                el = dom.parentElement(node);
1167
            }
1168
 
1169
            // "If either element is null or element's ownerDocument is an HTML document
1170
            // and element's local name is "html" and element's namespace is the HTML
1171
            // namespace"
1172
            if (el === null || (
1173
                el.nodeName == "HTML"
1174
                && dom.isHtmlNamespace(dom.getDocument(el).documentElement)
1175
                && dom.isHtmlNamespace(el)
1176
            )) {
1177
 
1178
            // "let element be a new Element with "body" as its local name and the HTML
1179
            // namespace as its namespace.""
1180
                el = doc.createElement("body");
1181
            } else {
1182
                el = el.cloneNode(false);
1183
            }
1184
 
1185
            // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1186
            // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1187
            // "In either case, the algorithm must be invoked with fragment as the input
1188
            // and element as the context element."
1189
            el.innerHTML = fragmentStr;
1190
 
1191
            // "If this raises an exception, then abort these steps. Otherwise, let new
1192
            // children be the nodes returned."
1193
 
1194
            // "Let fragment be a new DocumentFragment."
1195
            // "Append all new children to fragment."
1196
            // "Return fragment."
1197
            return dom.fragmentFromNodeChildren(el);
1198
        } :
1199
 
1200
        // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1201
        // previous versions of Rangy used (with the exception of using a body element rather than a div)
1202
        function(fragmentStr) {
1203
            assertNotDetached(this);
1204
            var doc = getRangeDocument(this);
1205
            var el = doc.createElement("body");
1206
            el.innerHTML = fragmentStr;
1207
 
1208
            return dom.fragmentFromNodeChildren(el);
1209
        };
1210
 
1211
    /*----------------------------------------------------------------------------------------------------------------*/
1212
 
1213
    var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1214
        "commonAncestorContainer"];
1215
 
1216
    var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1217
    var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1218
 
1219
    function RangePrototype() {}
1220
 
1221
    RangePrototype.prototype = {
1222
        attachListener: function(type, listener) {
1223
            this._listeners[type].push(listener);
1224
        },
1225
 
1226
        compareBoundaryPoints: function(how, range) {
1227
            assertRangeValid(this);
1228
            assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1229
 
1230
            var nodeA, offsetA, nodeB, offsetB;
1231
            var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1232
            var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1233
            nodeA = this[prefixA + "Container"];
1234
            offsetA = this[prefixA + "Offset"];
1235
            nodeB = range[prefixB + "Container"];
1236
            offsetB = range[prefixB + "Offset"];
1237
            return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
1238
        },
1239
 
1240
        insertNode: function(node) {
1241
            assertRangeValid(this);
1242
            assertValidNodeType(node, insertableNodeTypes);
1243
            assertNodeNotReadOnly(this.startContainer);
1244
 
1245
            if (dom.isAncestorOf(node, this.startContainer, true)) {
1246
                throw new DOMException("HIERARCHY_REQUEST_ERR");
1247
            }
1248
 
1249
            // No check for whether the container of the start of the Range is of a type that does not allow
1250
            // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1251
            // to add the node
1252
 
1253
            var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1254
            this.setStartBefore(firstNodeInserted);
1255
        },
1256
 
1257
        cloneContents: function() {
1258
            assertRangeValid(this);
1259
 
1260
            var clone, frag;
1261
            if (this.collapsed) {
1262
                return getRangeDocument(this).createDocumentFragment();
1263
            } else {
1264
                if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
1265
                    clone = this.startContainer.cloneNode(true);
1266
                    clone.data = clone.data.slice(this.startOffset, this.endOffset);
1267
                    frag = getRangeDocument(this).createDocumentFragment();
1268
                    frag.appendChild(clone);
1269
                    return frag;
1270
                } else {
1271
                    var iterator = new RangeIterator(this, true);
1272
                    clone = cloneSubtree(iterator);
1273
                    iterator.detach();
1274
                }
1275
                return clone;
1276
            }
1277
        },
1278
 
1279
        canSurroundContents: function() {
1280
            assertRangeValid(this);
1281
            assertNodeNotReadOnly(this.startContainer);
1282
            assertNodeNotReadOnly(this.endContainer);
1283
 
1284
            // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1285
            // no non-text nodes.
1286
            var iterator = new RangeIterator(this, true);
1287
            var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1288
                    (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1289
            iterator.detach();
1290
            return !boundariesInvalid;
1291
        },
1292
 
1293
        surroundContents: function(node) {
1294
            assertValidNodeType(node, surroundNodeTypes);
1295
 
1296
            if (!this.canSurroundContents()) {
1297
                throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
1298
            }
1299
 
1300
            // Extract the contents
1301
            var content = this.extractContents();
1302
 
1303
            // Clear the children of the node
1304
            if (node.hasChildNodes()) {
1305
                while (node.lastChild) {
1306
                    node.removeChild(node.lastChild);
1307
                }
1308
            }
1309
 
1310
            // Insert the new node and add the extracted contents
1311
            insertNodeAtPosition(node, this.startContainer, this.startOffset);
1312
            node.appendChild(content);
1313
 
1314
            this.selectNode(node);
1315
        },
1316
 
1317
        cloneRange: function() {
1318
            assertRangeValid(this);
1319
            var range = new Range(getRangeDocument(this));
1320
            var i = rangeProperties.length, prop;
1321
            while (i--) {
1322
                prop = rangeProperties[i];
1323
                range[prop] = this[prop];
1324
            }
1325
            return range;
1326
        },
1327
 
1328
        toString: function() {
1329
            assertRangeValid(this);
1330
            var sc = this.startContainer;
1331
            if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
1332
                return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1333
            } else {
1334
                var textBits = [], iterator = new RangeIterator(this, true);
1335
 
1336
                iterateSubtree(iterator, function(node) {
1337
                    // Accept only text or CDATA nodes, not comments
1338
 
1339
                    if (node.nodeType == 3 || node.nodeType == 4) {
1340
                        textBits.push(node.data);
1341
                    }
1342
                });
1343
                iterator.detach();
1344
                return textBits.join("");
1345
            }
1346
        },
1347
 
1348
        // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1349
        // been removed from Mozilla.
1350
 
1351
        compareNode: function(node) {
1352
            assertRangeValid(this);
1353
 
1354
            var parent = node.parentNode;
1355
            var nodeIndex = dom.getNodeIndex(node);
1356
 
1357
            if (!parent) {
1358
                throw new DOMException("NOT_FOUND_ERR");
1359
            }
1360
 
1361
            var startComparison = this.comparePoint(parent, nodeIndex),
1362
                endComparison = this.comparePoint(parent, nodeIndex + 1);
1363
 
1364
            if (startComparison < 0) { // Node starts before
1365
                return (endComparison > 0) ? n_b_a : n_b;
1366
            } else {
1367
                return (endComparison > 0) ? n_a : n_i;
1368
            }
1369
        },
1370
 
1371
        comparePoint: function(node, offset) {
1372
            assertRangeValid(this);
1373
            assertNode(node, "HIERARCHY_REQUEST_ERR");
1374
            assertSameDocumentOrFragment(node, this.startContainer);
1375
 
1376
            if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1377
                return -1;
1378
            } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1379
                return 1;
1380
            }
1381
            return 0;
1382
        },
1383
 
1384
        createContextualFragment: createContextualFragment,
1385
 
1386
        toHtml: function() {
1387
            assertRangeValid(this);
1388
            var container = getRangeDocument(this).createElement("div");
1389
            container.appendChild(this.cloneContents());
1390
            return container.innerHTML;
1391
        },
1392
 
1393
        // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1394
        // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1395
        intersectsNode: function(node, touchingIsIntersecting) {
1396
            assertRangeValid(this);
1397
            assertNode(node, "NOT_FOUND_ERR");
1398
            if (dom.getDocument(node) !== getRangeDocument(this)) {
1399
                return false;
1400
            }
1401
 
1402
            var parent = node.parentNode, offset = dom.getNodeIndex(node);
1403
            assertNode(parent, "NOT_FOUND_ERR");
1404
 
1405
            var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
1406
                endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1407
 
1408
            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1409
        },
1410
 
1411
 
1412
        isPointInRange: function(node, offset) {
1413
            assertRangeValid(this);
1414
            assertNode(node, "HIERARCHY_REQUEST_ERR");
1415
            assertSameDocumentOrFragment(node, this.startContainer);
1416
 
1417
            return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1418
                   (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1419
        },
1420
 
1421
        // The methods below are non-standard and invented by me.
1422
 
1423
        // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1424
        intersectsRange: function(range, touchingIsIntersecting) {
1425
            assertRangeValid(this);
1426
 
1427
            if (getRangeDocument(range) != getRangeDocument(this)) {
1428
                throw new DOMException("WRONG_DOCUMENT_ERR");
1429
            }
1430
 
1431
            var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),
1432
                endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);
1433
 
1434
            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1435
        },
1436
 
1437
        intersection: function(range) {
1438
            if (this.intersectsRange(range)) {
1439
                var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1440
                    endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1441
 
1442
                var intersectionRange = this.cloneRange();
1443
 
1444
                if (startComparison == -1) {
1445
                    intersectionRange.setStart(range.startContainer, range.startOffset);
1446
                }
1447
                if (endComparison == 1) {
1448
                    intersectionRange.setEnd(range.endContainer, range.endOffset);
1449
                }
1450
                return intersectionRange;
1451
            }
1452
            return null;
1453
        },
1454
 
1455
        union: function(range) {
1456
            if (this.intersectsRange(range, true)) {
1457
                var unionRange = this.cloneRange();
1458
                if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1459
                    unionRange.setStart(range.startContainer, range.startOffset);
1460
                }
1461
                if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1462
                    unionRange.setEnd(range.endContainer, range.endOffset);
1463
                }
1464
                return unionRange;
1465
            } else {
1466
                throw new RangeException("Ranges do not intersect");
1467
            }
1468
        },
1469
 
1470
        containsNode: function(node, allowPartial) {
1471
            if (allowPartial) {
1472
                return this.intersectsNode(node, false);
1473
            } else {
1474
                return this.compareNode(node) == n_i;
1475
            }
1476
        },
1477
 
1478
        containsNodeContents: function(node) {
1479
            return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0;
1480
        },
1481
 
1482
        containsRange: function(range) {
1483
            return this.intersection(range).equals(range);
1484
        },
1485
 
1486
        containsNodeText: function(node) {
1487
            var nodeRange = this.cloneRange();
1488
            nodeRange.selectNode(node);
1489
            var textNodes = nodeRange.getNodes([3]);
1490
            if (textNodes.length > 0) {
1491
                nodeRange.setStart(textNodes[0], 0);
1492
                var lastTextNode = textNodes.pop();
1493
                nodeRange.setEnd(lastTextNode, lastTextNode.length);
1494
                var contains = this.containsRange(nodeRange);
1495
                nodeRange.detach();
1496
                return contains;
1497
            } else {
1498
                return this.containsNodeContents(node);
1499
            }
1500
        },
1501
 
1502
        createNodeIterator: function(nodeTypes, filter) {
1503
            assertRangeValid(this);
1504
            return new RangeNodeIterator(this, nodeTypes, filter);
1505
        },
1506
 
1507
        getNodes: function(nodeTypes, filter) {
1508
            assertRangeValid(this);
1509
            return getNodesInRange(this, nodeTypes, filter);
1510
        },
1511
 
1512
        getDocument: function() {
1513
            return getRangeDocument(this);
1514
        },
1515
 
1516
        collapseBefore: function(node) {
1517
            assertNotDetached(this);
1518
 
1519
            this.setEndBefore(node);
1520
            this.collapse(false);
1521
        },
1522
 
1523
        collapseAfter: function(node) {
1524
            assertNotDetached(this);
1525
 
1526
            this.setStartAfter(node);
1527
            this.collapse(true);
1528
        },
1529
 
1530
        getName: function() {
1531
            return "DomRange";
1532
        },
1533
 
1534
        equals: function(range) {
1535
            return Range.rangesEqual(this, range);
1536
        },
1537
 
1538
        inspect: function() {
1539
            return inspect(this);
1540
        }
1541
    };
1542
 
1543
    function copyComparisonConstantsToObject(obj) {
1544
        obj.START_TO_START = s2s;
1545
        obj.START_TO_END = s2e;
1546
        obj.END_TO_END = e2e;
1547
        obj.END_TO_START = e2s;
1548
 
1549
        obj.NODE_BEFORE = n_b;
1550
        obj.NODE_AFTER = n_a;
1551
        obj.NODE_BEFORE_AND_AFTER = n_b_a;
1552
        obj.NODE_INSIDE = n_i;
1553
    }
1554
 
1555
    function copyComparisonConstants(constructor) {
1556
        copyComparisonConstantsToObject(constructor);
1557
        copyComparisonConstantsToObject(constructor.prototype);
1558
    }
1559
 
1560
    function createRangeContentRemover(remover, boundaryUpdater) {
1561
        return function() {
1562
            assertRangeValid(this);
1563
 
1564
            var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1565
 
1566
            var iterator = new RangeIterator(this, true);
1567
 
1568
            // Work out where to position the range after content removal
1569
            var node, boundary;
1570
            if (sc !== root) {
1571
                node = dom.getClosestAncestorIn(sc, root, true);
1572
                boundary = getBoundaryAfterNode(node);
1573
                sc = boundary.node;
1574
                so = boundary.offset;
1575
            }
1576
 
1577
            // Check none of the range is read-only
1578
            iterateSubtree(iterator, assertNodeNotReadOnly);
1579
 
1580
            iterator.reset();
1581
 
1582
            // Remove the content
1583
            var returnValue = remover(iterator);
1584
            iterator.detach();
1585
 
1586
            // Move to the new position
1587
            boundaryUpdater(this, sc, so, sc, so);
1588
 
1589
            return returnValue;
1590
        };
1591
    }
1592
 
1593
    function createPrototypeRange(constructor, boundaryUpdater, detacher) {
1594
        function createBeforeAfterNodeSetter(isBefore, isStart) {
1595
            return function(node) {
1596
                assertNotDetached(this);
1597
                assertValidNodeType(node, beforeAfterNodeTypes);
1598
                assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1599
 
1600
                var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1601
                (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1602
            };
1603
        }
1604
 
1605
        function setRangeStart(range, node, offset) {
1606
            var ec = range.endContainer, eo = range.endOffset;
1607
            if (node !== range.startContainer || offset !== range.startOffset) {
1608
                // Check the root containers of the range and the new boundary, and also check whether the new boundary
1609
                // is after the current end. In either case, collapse the range to the new position
1610
                if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {
1611
                    ec = node;
1612
                    eo = offset;
1613
                }
1614
                boundaryUpdater(range, node, offset, ec, eo);
1615
            }
1616
        }
1617
 
1618
        function setRangeEnd(range, node, offset) {
1619
            var sc = range.startContainer, so = range.startOffset;
1620
            if (node !== range.endContainer || offset !== range.endOffset) {
1621
                // Check the root containers of the range and the new boundary, and also check whether the new boundary
1622
                // is after the current end. In either case, collapse the range to the new position
1623
                if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {
1624
                    sc = node;
1625
                    so = offset;
1626
                }
1627
                boundaryUpdater(range, sc, so, node, offset);
1628
            }
1629
        }
1630
 
1631
        function setRangeStartAndEnd(range, node, offset) {
1632
            if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {
1633
                boundaryUpdater(range, node, offset, node, offset);
1634
            }
1635
        }
1636
 
1637
        constructor.prototype = new RangePrototype();
1638
 
1639
        api.util.extend(constructor.prototype, {
1640
            setStart: function(node, offset) {
1641
                assertNotDetached(this);
1642
                assertNoDocTypeNotationEntityAncestor(node, true);
1643
                assertValidOffset(node, offset);
1644
 
1645
                setRangeStart(this, node, offset);
1646
            },
1647
 
1648
            setEnd: function(node, offset) {
1649
                assertNotDetached(this);
1650
                assertNoDocTypeNotationEntityAncestor(node, true);
1651
                assertValidOffset(node, offset);
1652
 
1653
                setRangeEnd(this, node, offset);
1654
            },
1655
 
1656
            setStartBefore: createBeforeAfterNodeSetter(true, true),
1657
            setStartAfter: createBeforeAfterNodeSetter(false, true),
1658
            setEndBefore: createBeforeAfterNodeSetter(true, false),
1659
            setEndAfter: createBeforeAfterNodeSetter(false, false),
1660
 
1661
            collapse: function(isStart) {
1662
                assertRangeValid(this);
1663
                if (isStart) {
1664
                    boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1665
                } else {
1666
                    boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
1667
                }
1668
            },
1669
 
1670
            selectNodeContents: function(node) {
1671
                // This doesn't seem well specified: the spec talks only about selecting the node's contents, which
1672
                // could be taken to mean only its children. However, browsers implement this the same as selectNode for
1673
                // text nodes, so I shall do likewise
1674
                assertNotDetached(this);
1675
                assertNoDocTypeNotationEntityAncestor(node, true);
1676
 
1677
                boundaryUpdater(this, node, 0, node, dom.getNodeLength(node));
1678
            },
1679
 
1680
            selectNode: function(node) {
1681
                assertNotDetached(this);
1682
                assertNoDocTypeNotationEntityAncestor(node, false);
1683
                assertValidNodeType(node, beforeAfterNodeTypes);
1684
 
1685
                var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
1686
                boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
1687
            },
1688
 
1689
            extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
1690
 
1691
            deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
1692
 
1693
            canSurroundContents: function() {
1694
                assertRangeValid(this);
1695
                assertNodeNotReadOnly(this.startContainer);
1696
                assertNodeNotReadOnly(this.endContainer);
1697
 
1698
                // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1699
                // no non-text nodes.
1700
                var iterator = new RangeIterator(this, true);
1701
                var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1702
                        (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1703
                iterator.detach();
1704
                return !boundariesInvalid;
1705
            },
1706
 
1707
            detach: function() {
1708
                detacher(this);
1709
            },
1710
 
1711
            splitBoundaries: function() {
1712
                assertRangeValid(this);
1713
 
1714
 
1715
                var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
1716
                var startEndSame = (sc === ec);
1717
 
1718
                if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1719
                    dom.splitDataNode(ec, eo);
1720
 
1721
                }
1722
 
1723
                if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1724
 
1725
                    sc = dom.splitDataNode(sc, so);
1726
                    if (startEndSame) {
1727
                        eo -= so;
1728
                        ec = sc;
1729
                    } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
1730
                        eo++;
1731
                    }
1732
                    so = 0;
1733
 
1734
                }
1735
                boundaryUpdater(this, sc, so, ec, eo);
1736
            },
1737
 
1738
            normalizeBoundaries: function() {
1739
                assertRangeValid(this);
1740
 
1741
                var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
1742
 
1743
                var mergeForward = function(node) {
1744
                    var sibling = node.nextSibling;
1745
                    if (sibling && sibling.nodeType == node.nodeType) {
1746
                        ec = node;
1747
                        eo = node.length;
1748
                        node.appendData(sibling.data);
1749
                        sibling.parentNode.removeChild(sibling);
1750
                    }
1751
                };
1752
 
1753
                var mergeBackward = function(node) {
1754
                    var sibling = node.previousSibling;
1755
                    if (sibling && sibling.nodeType == node.nodeType) {
1756
                        sc = node;
1757
                        var nodeLength = node.length;
1758
                        so = sibling.length;
1759
                        node.insertData(0, sibling.data);
1760
                        sibling.parentNode.removeChild(sibling);
1761
                        if (sc == ec) {
1762
                            eo += so;
1763
                            ec = sc;
1764
                        } else if (ec == node.parentNode) {
1765
                            var nodeIndex = dom.getNodeIndex(node);
1766
                            if (eo == nodeIndex) {
1767
                                ec = node;
1768
                                eo = nodeLength;
1769
                            } else if (eo > nodeIndex) {
1770
                                eo--;
1771
                            }
1772
                        }
1773
                    }
1774
                };
1775
 
1776
                var normalizeStart = true;
1777
 
1778
                if (dom.isCharacterDataNode(ec)) {
1779
                    if (ec.length == eo) {
1780
                        mergeForward(ec);
1781
                    }
1782
                } else {
1783
                    if (eo > 0) {
1784
                        var endNode = ec.childNodes[eo - 1];
1785
                        if (endNode && dom.isCharacterDataNode(endNode)) {
1786
                            mergeForward(endNode);
1787
                        }
1788
                    }
1789
                    normalizeStart = !this.collapsed;
1790
                }
1791
 
1792
                if (normalizeStart) {
1793
                    if (dom.isCharacterDataNode(sc)) {
1794
                        if (so == 0) {
1795
                            mergeBackward(sc);
1796
                        }
1797
                    } else {
1798
                        if (so < sc.childNodes.length) {
1799
                            var startNode = sc.childNodes[so];
1800
                            if (startNode && dom.isCharacterDataNode(startNode)) {
1801
                                mergeBackward(startNode);
1802
                            }
1803
                        }
1804
                    }
1805
                } else {
1806
                    sc = ec;
1807
                    so = eo;
1808
                }
1809
 
1810
                boundaryUpdater(this, sc, so, ec, eo);
1811
            },
1812
 
1813
            collapseToPoint: function(node, offset) {
1814
                assertNotDetached(this);
1815
 
1816
                assertNoDocTypeNotationEntityAncestor(node, true);
1817
                assertValidOffset(node, offset);
1818
 
1819
                setRangeStartAndEnd(this, node, offset);
1820
            }
1821
        });
1822
 
1823
        copyComparisonConstants(constructor);
1824
    }
1825
 
1826
    /*----------------------------------------------------------------------------------------------------------------*/
1827
 
1828
    // Updates commonAncestorContainer and collapsed after boundary change
1829
    function updateCollapsedAndCommonAncestor(range) {
1830
        range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
1831
        range.commonAncestorContainer = range.collapsed ?
1832
            range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
1833
    }
1834
 
1835
    function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
1836
        var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
1837
        var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);
1838
 
1839
        range.startContainer = startContainer;
1840
        range.startOffset = startOffset;
1841
        range.endContainer = endContainer;
1842
        range.endOffset = endOffset;
1843
 
1844
        updateCollapsedAndCommonAncestor(range);
1845
        dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});
1846
    }
1847
 
1848
    function detach(range) {
1849
        assertNotDetached(range);
1850
        range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
1851
        range.collapsed = range.commonAncestorContainer = null;
1852
        dispatchEvent(range, "detach", null);
1853
        range._listeners = null;
1854
    }
1855
 
1856
    /**
1857
     * @constructor
1858
     */
1859
    function Range(doc) {
1860
        this.startContainer = doc;
1861
        this.startOffset = 0;
1862
        this.endContainer = doc;
1863
        this.endOffset = 0;
1864
        this._listeners = {
1865
            boundarychange: [],
1866
            detach: []
1867
        };
1868
        updateCollapsedAndCommonAncestor(this);
1869
    }
1870
 
1871
    createPrototypeRange(Range, updateBoundaries, detach);
1872
 
1873
    api.rangePrototype = RangePrototype.prototype;
1874
 
1875
    Range.rangeProperties = rangeProperties;
1876
    Range.RangeIterator = RangeIterator;
1877
    Range.copyComparisonConstants = copyComparisonConstants;
1878
    Range.createPrototypeRange = createPrototypeRange;
1879
    Range.inspect = inspect;
1880
    Range.getRangeDocument = getRangeDocument;
1881
    Range.rangesEqual = function(r1, r2) {
1882
        return r1.startContainer === r2.startContainer &&
1883
               r1.startOffset === r2.startOffset &&
1884
               r1.endContainer === r2.endContainer &&
1885
               r1.endOffset === r2.endOffset;
1886
    };
1887
 
1888
    api.DomRange = Range;
1889
    api.RangeException = RangeException;
1890
});rangy.createModule("WrappedRange", function(api, module) {
1891
    api.requireModules( ["DomUtil", "DomRange"] );
1892
 
1893
    /**
1894
     * @constructor
1895
     */
1896
    var WrappedRange;
1897
    var dom = api.dom;
1898
    var DomPosition = dom.DomPosition;
1899
    var DomRange = api.DomRange;
1900
 
1901
 
1902
 
1903
    /*----------------------------------------------------------------------------------------------------------------*/
1904
 
1905
    /*
1906
    This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
1907
    method. For example, in the following (where pipes denote the selection boundaries):
1908
 
1909
    <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
1910
 
1911
    var range = document.selection.createRange();
1912
    alert(range.parentElement().id); // Should alert "ul" but alerts "b"
1913
 
1914
    This method returns the common ancestor node of the following:
1915
    - the parentElement() of the textRange
1916
    - the parentElement() of the textRange after calling collapse(true)
1917
    - the parentElement() of the textRange after calling collapse(false)
1918
     */
1919
    function getTextRangeContainerElement(textRange) {
1920
        var parentEl = textRange.parentElement();
1921
 
1922
        var range = textRange.duplicate();
1923
        range.collapse(true);
1924
        var startEl = range.parentElement();
1925
        range = textRange.duplicate();
1926
        range.collapse(false);
1927
        var endEl = range.parentElement();
1928
        var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
1929
 
1930
        return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
1931
    }
1932
 
1933
    function textRangeIsCollapsed(textRange) {
1934
        return textRange.compareEndPoints("StartToEnd", textRange) == 0;
1935
    }
1936
 
1937
    // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as
1938
    // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has
1939
    // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling
1940
    // for inputs and images, plus optimizations.
1941
    function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {
1942
        var workingRange = textRange.duplicate();
1943
 
1944
        workingRange.collapse(isStart);
1945
        var containerElement = workingRange.parentElement();
1946
 
1947
        // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
1948
        // check for that
1949
        // TODO: Find out when. Workaround for wholeRangeContainerElement may break this
1950
        if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {
1951
            containerElement = wholeRangeContainerElement;
1952
 
1953
        }
1954
 
1955
 
1956
 
1957
        // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
1958
        // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
1959
        if (!containerElement.canHaveHTML) {
1960
            return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
1961
        }
1962
 
1963
        var workingNode = dom.getDocument(containerElement).createElement("span");
1964
        var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
1965
        var previousNode, nextNode, boundaryPosition, boundaryNode;
1966
 
1967
        // Move the working range through the container's children, starting at the end and working backwards, until the
1968
        // working range reaches or goes past the boundary we're interested in
1969
        do {
1970
            containerElement.insertBefore(workingNode, workingNode.previousSibling);
1971
            workingRange.moveToElementText(workingNode);
1972
        } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
1973
                workingNode.previousSibling);
1974
 
1975
        // We've now reached or gone past the boundary of the text range we're interested in
1976
        // so have identified the node we want
1977
        boundaryNode = workingNode.nextSibling;
1978
 
1979
        if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {
1980
            // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the
1981
            // node containing the text range's boundary, so we move the end of the working range to the boundary point
1982
            // and measure the length of its text to get the boundary's offset within the node.
1983
            workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
1984
 
1985
 
1986
            var offset;
1987
 
1988
            if (/[\r\n]/.test(boundaryNode.data)) {
1989
                /*
1990
                For the particular case of a boundary within a text node containing line breaks (within a <pre> element,
1991
                for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:
1992
 
1993
                - Each line break is represented as \r in the text node's data/nodeValue properties
1994
                - Each line break is represented as \r\n in the TextRange's 'text' property
1995
                - The 'text' property of the TextRange does not contain trailing line breaks
1996
 
1997
                To get round the problem presented by the final fact above, we can use the fact that TextRange's
1998
                moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
1999
                the same as the number of characters it was instructed to move. The simplest approach is to use this to
2000
                store the characters moved when moving both the start and end of the range to the start of the document
2001
                body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
2002
                However, this is extremely slow when the document is large and the range is near the end of it. Clearly
2003
                doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
2004
                problem.
2005
 
2006
                Another approach that works is to use moveStart() to move the start boundary of the range up to the end
2007
                boundary one character at a time and incrementing a counter with the value returned by the moveStart()
2008
                call. However, the check for whether the start boundary has reached the end boundary is expensive, so
2009
                this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
2010
                the range within the document).
2011
 
2012
                The method below is a hybrid of the two methods above. It uses the fact that a string containing the
2013
                TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
2014
                text of the TextRange, so the start of the range is moved that length initially and then a character at
2015
                a time to make up for any trailing line breaks not contained in the 'text' property. This has good
2016
                performance in most situations compared to the previous two methods.
2017
                */
2018
                var tempRange = workingRange.duplicate();
2019
                var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2020
 
2021
                offset = tempRange.moveStart("character", rangeLength);
2022
                while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2023
                    offset++;
2024
                    tempRange.moveStart("character", 1);
2025
                }
2026
            } else {
2027
                offset = workingRange.text.length;
2028
            }
2029
            boundaryPosition = new DomPosition(boundaryNode, offset);
2030
        } else {
2031
 
2032
 
2033
            // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2034
            // a position within that, and likewise for a start boundary preceding a character data node
2035
            previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2036
            nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2037
 
2038
 
2039
 
2040
            if (nextNode && dom.isCharacterDataNode(nextNode)) {
2041
                boundaryPosition = new DomPosition(nextNode, 0);
2042
            } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
2043
                boundaryPosition = new DomPosition(previousNode, previousNode.length);
2044
            } else {
2045
                boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2046
            }
2047
        }
2048
 
2049
        // Clean up
2050
        workingNode.parentNode.removeChild(workingNode);
2051
 
2052
        return boundaryPosition;
2053
    }
2054
 
2055
    // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
2056
    // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2057
    // (http://code.google.com/p/ierange/)
2058
    function createBoundaryTextRange(boundaryPosition, isStart) {
2059
        var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2060
        var doc = dom.getDocument(boundaryPosition.node);
2061
        var workingNode, childNodes, workingRange = doc.body.createTextRange();
2062
        var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);
2063
 
2064
        if (nodeIsDataNode) {
2065
            boundaryNode = boundaryPosition.node;
2066
            boundaryParent = boundaryNode.parentNode;
2067
        } else {
2068
            childNodes = boundaryPosition.node.childNodes;
2069
            boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2070
            boundaryParent = boundaryPosition.node;
2071
        }
2072
 
2073
        // Position the range immediately before the node containing the boundary
2074
        workingNode = doc.createElement("span");
2075
 
2076
        // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
2077
        // element rather than immediately before or after it, which is what we want
2078
        workingNode.innerHTML = "&#feff;";
2079
 
2080
        // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
2081
        // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
2082
        if (boundaryNode) {
2083
            boundaryParent.insertBefore(workingNode, boundaryNode);
2084
        } else {
2085
            boundaryParent.appendChild(workingNode);
2086
        }
2087
 
2088
        workingRange.moveToElementText(workingNode);
2089
        workingRange.collapse(!isStart);
2090
 
2091
        // Clean up
2092
        boundaryParent.removeChild(workingNode);
2093
 
2094
        // Move the working range to the text offset, if required
2095
        if (nodeIsDataNode) {
2096
            workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2097
        }
2098
 
2099
        return workingRange;
2100
    }
2101
 
2102
    /*----------------------------------------------------------------------------------------------------------------*/
2103
 
2104
    if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
2105
        // This is a wrapper around the browser's native DOM Range. It has two aims:
2106
        // - Provide workarounds for specific browser bugs
2107
        // - provide convenient extensions, which are inherited from Rangy's DomRange
2108
 
2109
        (function() {
2110
            var rangeProto;
2111
            var rangeProperties = DomRange.rangeProperties;
2112
            var canSetRangeStartAfterEnd;
2113
 
2114
            function updateRangeProperties(range) {
2115
                var i = rangeProperties.length, prop;
2116
                while (i--) {
2117
                    prop = rangeProperties[i];
2118
                    range[prop] = range.nativeRange[prop];
2119
                }
2120
            }
2121
 
2122
            function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
2123
                var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2124
                var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2125
 
2126
                // Always set both boundaries for the benefit of IE9 (see issue 35)
2127
                if (startMoved || endMoved) {
2128
                    range.setEnd(endContainer, endOffset);
2129
                    range.setStart(startContainer, startOffset);
2130
                }
2131
            }
2132
 
2133
            function detach(range) {
2134
                range.nativeRange.detach();
2135
                range.detached = true;
2136
                var i = rangeProperties.length, prop;
2137
                while (i--) {
2138
                    prop = rangeProperties[i];
2139
                    range[prop] = null;
2140
                }
2141
            }
2142
 
2143
            var createBeforeAfterNodeSetter;
2144
 
2145
            WrappedRange = function(range) {
2146
                if (!range) {
2147
                    throw new Error("Range must be specified");
2148
                }
2149
                this.nativeRange = range;
2150
                updateRangeProperties(this);
2151
            };
2152
 
2153
            DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
2154
 
2155
            rangeProto = WrappedRange.prototype;
2156
 
2157
            rangeProto.selectNode = function(node) {
2158
                this.nativeRange.selectNode(node);
2159
                updateRangeProperties(this);
2160
            };
2161
 
2162
            rangeProto.deleteContents = function() {
2163
                this.nativeRange.deleteContents();
2164
                updateRangeProperties(this);
2165
            };
2166
 
2167
            rangeProto.extractContents = function() {
2168
                var frag = this.nativeRange.extractContents();
2169
                updateRangeProperties(this);
2170
                return frag;
2171
            };
2172
 
2173
            rangeProto.cloneContents = function() {
2174
                return this.nativeRange.cloneContents();
2175
            };
2176
 
2177
            // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
2178
            // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
2179
            // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
2180
            // insertNode, which works but is almost certainly slower than the native implementation.
2181
/*
2182
            rangeProto.insertNode = function(node) {
2183
                this.nativeRange.insertNode(node);
2184
                updateRangeProperties(this);
2185
            };
2186
*/
2187
 
2188
            rangeProto.surroundContents = function(node) {
2189
                this.nativeRange.surroundContents(node);
2190
                updateRangeProperties(this);
2191
            };
2192
 
2193
            rangeProto.collapse = function(isStart) {
2194
                this.nativeRange.collapse(isStart);
2195
                updateRangeProperties(this);
2196
            };
2197
 
2198
            rangeProto.cloneRange = function() {
2199
                return new WrappedRange(this.nativeRange.cloneRange());
2200
            };
2201
 
2202
            rangeProto.refresh = function() {
2203
                updateRangeProperties(this);
2204
            };
2205
 
2206
            rangeProto.toString = function() {
2207
                return this.nativeRange.toString();
2208
            };
2209
 
2210
            // Create test range and node for feature detection
2211
 
2212
            var testTextNode = document.createTextNode("test");
2213
            dom.getBody(document).appendChild(testTextNode);
2214
            var range = document.createRange();
2215
 
2216
            /*--------------------------------------------------------------------------------------------------------*/
2217
 
2218
            // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2219
            // correct for it
2220
 
2221
            range.setStart(testTextNode, 0);
2222
            range.setEnd(testTextNode, 0);
2223
 
2224
            try {
2225
                range.setStart(testTextNode, 1);
2226
                canSetRangeStartAfterEnd = true;
2227
 
2228
                rangeProto.setStart = function(node, offset) {
2229
                    this.nativeRange.setStart(node, offset);
2230
                    updateRangeProperties(this);
2231
                };
2232
 
2233
                rangeProto.setEnd = function(node, offset) {
2234
                    this.nativeRange.setEnd(node, offset);
2235
                    updateRangeProperties(this);
2236
                };
2237
 
2238
                createBeforeAfterNodeSetter = function(name) {
2239
                    return function(node) {
2240
                        this.nativeRange[name](node);
2241
                        updateRangeProperties(this);
2242
                    };
2243
                };
2244
 
2245
            } catch(ex) {
2246
 
2247
 
2248
                canSetRangeStartAfterEnd = false;
2249
 
2250
                rangeProto.setStart = function(node, offset) {
2251
                    try {
2252
                        this.nativeRange.setStart(node, offset);
2253
                    } catch (ex) {
2254
                        this.nativeRange.setEnd(node, offset);
2255
                        this.nativeRange.setStart(node, offset);
2256
                    }
2257
                    updateRangeProperties(this);
2258
                };
2259
 
2260
                rangeProto.setEnd = function(node, offset) {
2261
                    try {
2262
                        this.nativeRange.setEnd(node, offset);
2263
                    } catch (ex) {
2264
                        this.nativeRange.setStart(node, offset);
2265
                        this.nativeRange.setEnd(node, offset);
2266
                    }
2267
                    updateRangeProperties(this);
2268
                };
2269
 
2270
                createBeforeAfterNodeSetter = function(name, oppositeName) {
2271
                    return function(node) {
2272
                        try {
2273
                            this.nativeRange[name](node);
2274
                        } catch (ex) {
2275
                            this.nativeRange[oppositeName](node);
2276
                            this.nativeRange[name](node);
2277
                        }
2278
                        updateRangeProperties(this);
2279
                    };
2280
                };
2281
            }
2282
 
2283
            rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2284
            rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2285
            rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2286
            rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2287
 
2288
            /*--------------------------------------------------------------------------------------------------------*/
2289
 
2290
            // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
2291
            // the 0th character of the text node
2292
            range.selectNodeContents(testTextNode);
2293
            if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
2294
                    range.startOffset == 0 && range.endOffset == testTextNode.length) {
2295
                rangeProto.selectNodeContents = function(node) {
2296
                    this.nativeRange.selectNodeContents(node);
2297
                    updateRangeProperties(this);
2298
                };
2299
            } else {
2300
                rangeProto.selectNodeContents = function(node) {
2301
                    this.setStart(node, 0);
2302
                    this.setEnd(node, DomRange.getEndOffset(node));
2303
                };
2304
            }
2305
 
2306
            /*--------------------------------------------------------------------------------------------------------*/
2307
 
2308
            // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
2309
            // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2310
 
2311
            range.selectNodeContents(testTextNode);
2312
            range.setEnd(testTextNode, 3);
2313
 
2314
            var range2 = document.createRange();
2315
            range2.selectNodeContents(testTextNode);
2316
            range2.setEnd(testTextNode, 4);
2317
            range2.setStart(testTextNode, 2);
2318
 
2319
            if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &
2320
                    range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2321
                // This is the wrong way round, so correct for it
2322
 
2323
 
2324
                rangeProto.compareBoundaryPoints = function(type, range) {
2325
                    range = range.nativeRange || range;
2326
                    if (type == range.START_TO_END) {
2327
                        type = range.END_TO_START;
2328
                    } else if (type == range.END_TO_START) {
2329
                        type = range.START_TO_END;
2330
                    }
2331
                    return this.nativeRange.compareBoundaryPoints(type, range);
2332
                };
2333
            } else {
2334
                rangeProto.compareBoundaryPoints = function(type, range) {
2335
                    return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2336
                };
2337
            }
2338
 
2339
            /*--------------------------------------------------------------------------------------------------------*/
2340
 
2341
            // Test for existence of createContextualFragment and delegate to it if it exists
2342
            if (api.util.isHostMethod(range, "createContextualFragment")) {
2343
                rangeProto.createContextualFragment = function(fragmentStr) {
2344
                    return this.nativeRange.createContextualFragment(fragmentStr);
2345
                };
2346
            }
2347
 
2348
            /*--------------------------------------------------------------------------------------------------------*/
2349
 
2350
            // Clean up
2351
            dom.getBody(document).removeChild(testTextNode);
2352
            range.detach();
2353
            range2.detach();
2354
        })();
2355
 
2356
        api.createNativeRange = function(doc) {
2357
            doc = doc || document;
2358
            return doc.createRange();
2359
        };
2360
    } else if (api.features.implementsTextRange) {
2361
        // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2362
        // prototype
2363
 
2364
        WrappedRange = function(textRange) {
2365
            this.textRange = textRange;
2366
            this.refresh();
2367
        };
2368
 
2369
        WrappedRange.prototype = new DomRange(document);
2370
 
2371
        WrappedRange.prototype.refresh = function() {
2372
            var start, end;
2373
 
2374
            // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2375
            var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2376
 
2377
            if (textRangeIsCollapsed(this.textRange)) {
2378
                end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);
2379
            } else {
2380
 
2381
                start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2382
                end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
2383
            }
2384
 
2385
            this.setStart(start.node, start.offset);
2386
            this.setEnd(end.node, end.offset);
2387
        };
2388
 
2389
        DomRange.copyComparisonConstants(WrappedRange);
2390
 
2391
        // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
2392
        var globalObj = (function() { return this; })();
2393
        if (typeof globalObj.Range == "undefined") {
2394
            globalObj.Range = WrappedRange;
2395
        }
2396
 
2397
        api.createNativeRange = function(doc) {
2398
            doc = doc || document;
2399
            return doc.body.createTextRange();
2400
        };
2401
    }
2402
 
2403
    if (api.features.implementsTextRange) {
2404
        WrappedRange.rangeToTextRange = function(range) {
2405
            if (range.collapsed) {
2406
                var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2407
 
2408
 
2409
 
2410
                return tr;
2411
 
2412
                //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2413
            } else {
2414
                var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2415
                var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
2416
                var textRange = dom.getDocument(range.startContainer).body.createTextRange();
2417
                textRange.setEndPoint("StartToStart", startRange);
2418
                textRange.setEndPoint("EndToEnd", endRange);
2419
                return textRange;
2420
            }
2421
        };
2422
    }
2423
 
2424
    WrappedRange.prototype.getName = function() {
2425
        return "WrappedRange";
2426
    };
2427
 
2428
    api.WrappedRange = WrappedRange;
2429
 
2430
    api.createRange = function(doc) {
2431
        doc = doc || document;
2432
        return new WrappedRange(api.createNativeRange(doc));
2433
    };
2434
 
2435
    api.createRangyRange = function(doc) {
2436
        doc = doc || document;
2437
        return new DomRange(doc);
2438
    };
2439
 
2440
    api.createIframeRange = function(iframeEl) {
2441
        return api.createRange(dom.getIframeDocument(iframeEl));
2442
    };
2443
 
2444
    api.createIframeRangyRange = function(iframeEl) {
2445
        return api.createRangyRange(dom.getIframeDocument(iframeEl));
2446
    };
2447
 
2448
    api.addCreateMissingNativeApiListener(function(win) {
2449
        var doc = win.document;
2450
        if (typeof doc.createRange == "undefined") {
2451
            doc.createRange = function() {
2452
                return api.createRange(this);
2453
            };
2454
        }
2455
        doc = win = null;
2456
    });
2457
});rangy.createModule("WrappedSelection", function(api, module) {
2458
    // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
2459
    // spec (http://html5.org/specs/dom-range.html)
2460
 
2461
    api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
2462
 
2463
    api.config.checkSelectionRanges = true;
2464
 
2465
    var BOOLEAN = "boolean",
2466
        windowPropertyName = "_rangySelection",
2467
        dom = api.dom,
2468
        util = api.util,
2469
        DomRange = api.DomRange,
2470
        WrappedRange = api.WrappedRange,
2471
        DOMException = api.DOMException,
2472
        DomPosition = dom.DomPosition,
2473
        getSelection,
2474
        selectionIsCollapsed,
2475
        CONTROL = "Control";
2476
 
2477
 
2478
 
2479
    function getWinSelection(winParam) {
2480
        return (winParam || window).getSelection();
2481
    }
2482
 
2483
    function getDocSelection(winParam) {
2484
        return (winParam || window).document.selection;
2485
    }
2486
 
2487
    // Test for the Range/TextRange and Selection features required
2488
    // Test for ability to retrieve selection
2489
    var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
2490
        implementsDocSelection = api.util.isHostObject(document, "selection");
2491
 
2492
    var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
2493
 
2494
    if (useDocumentSelection) {
2495
        getSelection = getDocSelection;
2496
        api.isSelectionValid = function(winParam) {
2497
            var doc = (winParam || window).document, nativeSel = doc.selection;
2498
 
2499
            // Check whether the selection TextRange is actually contained within the correct document
2500
            return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
2501
        };
2502
    } else if (implementsWinGetSelection) {
2503
        getSelection = getWinSelection;
2504
        api.isSelectionValid = function() {
2505
            return true;
2506
        };
2507
    } else {
2508
        module.fail("Neither document.selection or window.getSelection() detected.");
2509
    }
2510
 
2511
    api.getNativeSelection = getSelection;
2512
 
2513
    var testSelection = getSelection();
2514
    var testRange = api.createNativeRange(document);
2515
    var body = dom.getBody(document);
2516
 
2517
    // Obtaining a range from a selection
2518
    var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
2519
                                     util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
2520
    api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
2521
 
2522
    // Test for existence of native selection extend() method
2523
    var selectionHasExtend = util.isHostMethod(testSelection, "extend");
2524
    api.features.selectionHasExtend = selectionHasExtend;
2525
 
2526
    // Test if rangeCount exists
2527
    var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
2528
    api.features.selectionHasRangeCount = selectionHasRangeCount;
2529
 
2530
    var selectionSupportsMultipleRanges = false;
2531
    var collapsedNonEditableSelectionsSupported = true;
2532
 
2533
    if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
2534
            typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
2535
 
2536
        (function() {
2537
            var iframe = document.createElement("iframe");
2538
            body.appendChild(iframe);
2539
 
2540
            var iframeDoc = dom.getIframeDocument(iframe);
2541
            iframeDoc.open();
2542
            iframeDoc.write("<html><head></head><body>12</body></html>");
2543
            iframeDoc.close();
2544
 
2545
            var sel = dom.getIframeWindow(iframe).getSelection();
2546
            var docEl = iframeDoc.documentElement;
2547
            var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
2548
 
2549
            // Test whether the native selection will allow a collapsed selection within a non-editable element
2550
            var r1 = iframeDoc.createRange();
2551
            r1.setStart(textNode, 1);
2552
            r1.collapse(true);
2553
            sel.addRange(r1);
2554
            collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
2555
            sel.removeAllRanges();
2556
 
2557
            // Test whether the native selection is capable of supporting multiple ranges
2558
            var r2 = r1.cloneRange();
2559
            r1.setStart(textNode, 0);
2560
            r2.setEnd(textNode, 2);
2561
            sel.addRange(r1);
2562
            sel.addRange(r2);
2563
 
2564
            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
2565
 
2566
            // Clean up
2567
            r1.detach();
2568
            r2.detach();
2569
 
2570
            body.removeChild(iframe);
2571
        })();
2572
    }
2573
 
2574
    api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
2575
    api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
2576
 
2577
    // ControlRanges
2578
    var implementsControlRange = false, testControlRange;
2579
 
2580
    if (body && util.isHostMethod(body, "createControlRange")) {
2581
        testControlRange = body.createControlRange();
2582
        if (util.areHostProperties(testControlRange, ["item", "add"])) {
2583
            implementsControlRange = true;
2584
        }
2585
    }
2586
    api.features.implementsControlRange = implementsControlRange;
2587
 
2588
    // Selection collapsedness
2589
    if (selectionHasAnchorAndFocus) {
2590
        selectionIsCollapsed = function(sel) {
2591
            return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
2592
        };
2593
    } else {
2594
        selectionIsCollapsed = function(sel) {
2595
            return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
2596
        };
2597
    }
2598
 
2599
    function updateAnchorAndFocusFromRange(sel, range, backwards) {
2600
        var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";
2601
        sel.anchorNode = range[anchorPrefix + "Container"];
2602
        sel.anchorOffset = range[anchorPrefix + "Offset"];
2603
        sel.focusNode = range[focusPrefix + "Container"];
2604
        sel.focusOffset = range[focusPrefix + "Offset"];
2605
    }
2606
 
2607
    function updateAnchorAndFocusFromNativeSelection(sel) {
2608
        var nativeSel = sel.nativeSelection;
2609
        sel.anchorNode = nativeSel.anchorNode;
2610
        sel.anchorOffset = nativeSel.anchorOffset;
2611
        sel.focusNode = nativeSel.focusNode;
2612
        sel.focusOffset = nativeSel.focusOffset;
2613
    }
2614
 
2615
    function updateEmptySelection(sel) {
2616
        sel.anchorNode = sel.focusNode = null;
2617
        sel.anchorOffset = sel.focusOffset = 0;
2618
        sel.rangeCount = 0;
2619
        sel.isCollapsed = true;
2620
        sel._ranges.length = 0;
2621
    }
2622
 
2623
    function getNativeRange(range) {
2624
        var nativeRange;
2625
        if (range instanceof DomRange) {
2626
            nativeRange = range._selectionNativeRange;
2627
            if (!nativeRange) {
2628
                nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
2629
                nativeRange.setEnd(range.endContainer, range.endOffset);
2630
                nativeRange.setStart(range.startContainer, range.startOffset);
2631
                range._selectionNativeRange = nativeRange;
2632
                range.attachListener("detach", function() {
2633
 
2634
                    this._selectionNativeRange = null;
2635
                });
2636
            }
2637
        } else if (range instanceof WrappedRange) {
2638
            nativeRange = range.nativeRange;
2639
        } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
2640
            nativeRange = range;
2641
        }
2642
        return nativeRange;
2643
    }
2644
 
2645
    function rangeContainsSingleElement(rangeNodes) {
2646
        if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
2647
            return false;
2648
        }
2649
        for (var i = 1, len = rangeNodes.length; i < len; ++i) {
2650
            if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
2651
                return false;
2652
            }
2653
        }
2654
        return true;
2655
    }
2656
 
2657
    function getSingleElementFromRange(range) {
2658
        var nodes = range.getNodes();
2659
        if (!rangeContainsSingleElement(nodes)) {
2660
            throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
2661
        }
2662
        return nodes[0];
2663
    }
2664
 
2665
    function isTextRange(range) {
2666
        return !!range && typeof range.text != "undefined";
2667
    }
2668
 
2669
    function updateFromTextRange(sel, range) {
2670
        // Create a Range from the selected TextRange
2671
        var wrappedRange = new WrappedRange(range);
2672
        sel._ranges = [wrappedRange];
2673
 
2674
        updateAnchorAndFocusFromRange(sel, wrappedRange, false);
2675
        sel.rangeCount = 1;
2676
        sel.isCollapsed = wrappedRange.collapsed;
2677
    }
2678
 
2679
    function updateControlSelection(sel) {
2680
        // Update the wrapped selection based on what's now in the native selection
2681
        sel._ranges.length = 0;
2682
        if (sel.docSelection.type == "None") {
2683
            updateEmptySelection(sel);
2684
        } else {
2685
            var controlRange = sel.docSelection.createRange();
2686
            if (isTextRange(controlRange)) {
2687
                // This case (where the selection type is "Control" and calling createRange() on the selection returns
2688
                // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
2689
                // ControlRange have been removed from the ControlRange and removed from the document.
2690
                updateFromTextRange(sel, controlRange);
2691
            } else {
2692
                sel.rangeCount = controlRange.length;
2693
                var range, doc = dom.getDocument(controlRange.item(0));
2694
                for (var i = 0; i < sel.rangeCount; ++i) {
2695
                    range = api.createRange(doc);
2696
                    range.selectNode(controlRange.item(i));
2697
                    sel._ranges.push(range);
2698
                }
2699
                sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
2700
                updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
2701
            }
2702
        }
2703
    }
2704
 
2705
    function addRangeToControlSelection(sel, range) {
2706
        var controlRange = sel.docSelection.createRange();
2707
        var rangeElement = getSingleElementFromRange(range);
2708
 
2709
        // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
2710
        // contained by the supplied range
2711
        var doc = dom.getDocument(controlRange.item(0));
2712
        var newControlRange = dom.getBody(doc).createControlRange();
2713
        for (var i = 0, len = controlRange.length; i < len; ++i) {
2714
            newControlRange.add(controlRange.item(i));
2715
        }
2716
        try {
2717
            newControlRange.add(rangeElement);
2718
        } catch (ex) {
2719
            throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
2720
        }
2721
        newControlRange.select();
2722
 
2723
        // Update the wrapped selection based on what's now in the native selection
2724
        updateControlSelection(sel);
2725
    }
2726
 
2727
    var getSelectionRangeAt;
2728
 
2729
    if (util.isHostMethod(testSelection,  "getRangeAt")) {
2730
        getSelectionRangeAt = function(sel, index) {
2731
            try {
2732
                return sel.getRangeAt(index);
2733
            } catch(ex) {
2734
                return null;
2735
            }
2736
        };
2737
    } else if (selectionHasAnchorAndFocus) {
2738
        getSelectionRangeAt = function(sel) {
2739
            var doc = dom.getDocument(sel.anchorNode);
2740
            var range = api.createRange(doc);
2741
            range.setStart(sel.anchorNode, sel.anchorOffset);
2742
            range.setEnd(sel.focusNode, sel.focusOffset);
2743
 
2744
            // Handle the case when the selection was selected backwards (from the end to the start in the
2745
            // document)
2746
            if (range.collapsed !== this.isCollapsed) {
2747
                range.setStart(sel.focusNode, sel.focusOffset);
2748
                range.setEnd(sel.anchorNode, sel.anchorOffset);
2749
            }
2750
 
2751
            return range;
2752
        };
2753
    }
2754
 
2755
    /**
2756
     * @constructor
2757
     */
2758
    function WrappedSelection(selection, docSelection, win) {
2759
        this.nativeSelection = selection;
2760
        this.docSelection = docSelection;
2761
        this._ranges = [];
2762
        this.win = win;
2763
        this.refresh();
2764
    }
2765
 
2766
    api.getSelection = function(win) {
2767
        win = win || window;
2768
        var sel = win[windowPropertyName];
2769
        var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
2770
        if (sel) {
2771
            sel.nativeSelection = nativeSel;
2772
            sel.docSelection = docSel;
2773
            sel.refresh(win);
2774
        } else {
2775
            sel = new WrappedSelection(nativeSel, docSel, win);
2776
            win[windowPropertyName] = sel;
2777
        }
2778
        return sel;
2779
    };
2780
 
2781
    api.getIframeSelection = function(iframeEl) {
2782
        return api.getSelection(dom.getIframeWindow(iframeEl));
2783
    };
2784
 
2785
    var selProto = WrappedSelection.prototype;
2786
 
2787
    function createControlSelection(sel, ranges) {
2788
        // Ensure that the selection becomes of type "Control"
2789
        var doc = dom.getDocument(ranges[0].startContainer);
2790
        var controlRange = dom.getBody(doc).createControlRange();
2791
        for (var i = 0, el; i < rangeCount; ++i) {
2792
            el = getSingleElementFromRange(ranges[i]);
2793
            try {
2794
                controlRange.add(el);
2795
            } catch (ex) {
2796
                throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
2797
            }
2798
        }
2799
        controlRange.select();
2800
 
2801
        // Update the wrapped selection based on what's now in the native selection
2802
        updateControlSelection(sel);
2803
    }
2804
 
2805
    // Selecting a range
2806
    if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
2807
        selProto.removeAllRanges = function() {
2808
            this.nativeSelection.removeAllRanges();
2809
            updateEmptySelection(this);
2810
        };
2811
 
2812
        var addRangeBackwards = function(sel, range) {
2813
            var doc = DomRange.getRangeDocument(range);
2814
            var endRange = api.createRange(doc);
2815
            endRange.collapseToPoint(range.endContainer, range.endOffset);
2816
            sel.nativeSelection.addRange(getNativeRange(endRange));
2817
            sel.nativeSelection.extend(range.startContainer, range.startOffset);
2818
            sel.refresh();
2819
        };
2820
 
2821
        if (selectionHasRangeCount) {
2822
            selProto.addRange = function(range, backwards) {
2823
                if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
2824
                    addRangeToControlSelection(this, range);
2825
                } else {
2826
                    if (backwards && selectionHasExtend) {
2827
                        addRangeBackwards(this, range);
2828
                    } else {
2829
                        var previousRangeCount;
2830
                        if (selectionSupportsMultipleRanges) {
2831
                            previousRangeCount = this.rangeCount;
2832
                        } else {
2833
                            this.removeAllRanges();
2834
                            previousRangeCount = 0;
2835
                        }
2836
                        this.nativeSelection.addRange(getNativeRange(range));
2837
 
2838
                        // Check whether adding the range was successful
2839
                        this.rangeCount = this.nativeSelection.rangeCount;
2840
 
2841
                        if (this.rangeCount == previousRangeCount + 1) {
2842
                            // The range was added successfully
2843
 
2844
                            // Check whether the range that we added to the selection is reflected in the last range extracted from
2845
                            // the selection
2846
                            if (api.config.checkSelectionRanges) {
2847
                                var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
2848
                                if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {
2849
                                    // Happens in WebKit with, for example, a selection placed at the start of a text node
2850
                                    range = new WrappedRange(nativeRange);
2851
                                }
2852
                            }
2853
                            this._ranges[this.rangeCount - 1] = range;
2854
                            updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));
2855
                            this.isCollapsed = selectionIsCollapsed(this);
2856
                        } else {
2857
                            // The range was not added successfully. The simplest thing is to refresh
2858
                            this.refresh();
2859
                        }
2860
                    }
2861
                }
2862
            };
2863
        } else {
2864
            selProto.addRange = function(range, backwards) {
2865
                if (backwards && selectionHasExtend) {
2866
                    addRangeBackwards(this, range);
2867
                } else {
2868
                    this.nativeSelection.addRange(getNativeRange(range));
2869
                    this.refresh();
2870
                }
2871
            };
2872
        }
2873
 
2874
        selProto.setRanges = function(ranges) {
2875
            if (implementsControlRange && ranges.length > 1) {
2876
                createControlSelection(this, ranges);
2877
            } else {
2878
                this.removeAllRanges();
2879
                for (var i = 0, len = ranges.length; i < len; ++i) {
2880
                    this.addRange(ranges[i]);
2881
                }
2882
            }
2883
        };
2884
    } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
2885
               implementsControlRange && useDocumentSelection) {
2886
 
2887
        selProto.removeAllRanges = function() {
2888
            // Added try/catch as fix for issue #21
2889
            try {
2890
                this.docSelection.empty();
2891
 
2892
                // Check for empty() not working (issue #24)
2893
                if (this.docSelection.type != "None") {
2894
                    // Work around failure to empty a control selection by instead selecting a TextRange and then
2895
                    // calling empty()
2896
                    var doc;
2897
                    if (this.anchorNode) {
2898
                        doc = dom.getDocument(this.anchorNode);
2899
                    } else if (this.docSelection.type == CONTROL) {
2900
                        var controlRange = this.docSelection.createRange();
2901
                        if (controlRange.length) {
2902
                            doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
2903
                        }
2904
                    }
2905
                    if (doc) {
2906
                        var textRange = doc.body.createTextRange();
2907
                        textRange.select();
2908
                        this.docSelection.empty();
2909
                    }
2910
                }
2911
            } catch(ex) {}
2912
            updateEmptySelection(this);
2913
        };
2914
 
2915
        selProto.addRange = function(range) {
2916
            if (this.docSelection.type == CONTROL) {
2917
                addRangeToControlSelection(this, range);
2918
            } else {
2919
                WrappedRange.rangeToTextRange(range).select();
2920
                this._ranges[0] = range;
2921
                this.rangeCount = 1;
2922
                this.isCollapsed = this._ranges[0].collapsed;
2923
                updateAnchorAndFocusFromRange(this, range, false);
2924
            }
2925
        };
2926
 
2927
        selProto.setRanges = function(ranges) {
2928
            this.removeAllRanges();
2929
            var rangeCount = ranges.length;
2930
            if (rangeCount > 1) {
2931
                createControlSelection(this, ranges);
2932
            } else if (rangeCount) {
2933
                this.addRange(ranges[0]);
2934
            }
2935
        };
2936
    } else {
2937
        module.fail("No means of selecting a Range or TextRange was found");
2938
        return false;
2939
    }
2940
 
2941
    selProto.getRangeAt = function(index) {
2942
        if (index < 0 || index >= this.rangeCount) {
2943
            throw new DOMException("INDEX_SIZE_ERR");
2944
        } else {
2945
            return this._ranges[index];
2946
        }
2947
    };
2948
 
2949
    var refreshSelection;
2950
 
2951
    if (useDocumentSelection) {
2952
        refreshSelection = function(sel) {
2953
            var range;
2954
            if (api.isSelectionValid(sel.win)) {
2955
                range = sel.docSelection.createRange();
2956
            } else {
2957
                range = dom.getBody(sel.win.document).createTextRange();
2958
                range.collapse(true);
2959
            }
2960
 
2961
 
2962
            if (sel.docSelection.type == CONTROL) {
2963
                updateControlSelection(sel);
2964
            } else if (isTextRange(range)) {
2965
                updateFromTextRange(sel, range);
2966
            } else {
2967
                updateEmptySelection(sel);
2968
            }
2969
        };
2970
    } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {
2971
        refreshSelection = function(sel) {
2972
            if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
2973
                updateControlSelection(sel);
2974
            } else {
2975
                sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
2976
                if (sel.rangeCount) {
2977
                    for (var i = 0, len = sel.rangeCount; i < len; ++i) {
2978
                        sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
2979
                    }
2980
                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));
2981
                    sel.isCollapsed = selectionIsCollapsed(sel);
2982
                } else {
2983
                    updateEmptySelection(sel);
2984
                }
2985
            }
2986
        };
2987
    } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {
2988
        refreshSelection = function(sel) {
2989
            var range, nativeSel = sel.nativeSelection;
2990
            if (nativeSel.anchorNode) {
2991
                range = getSelectionRangeAt(nativeSel, 0);
2992
                sel._ranges = [range];
2993
                sel.rangeCount = 1;
2994
                updateAnchorAndFocusFromNativeSelection(sel);
2995
                sel.isCollapsed = selectionIsCollapsed(sel);
2996
            } else {
2997
                updateEmptySelection(sel);
2998
            }
2999
        };
3000
    } else {
3001
        module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3002
        return false;
3003
    }
3004
 
3005
    selProto.refresh = function(checkForChanges) {
3006
        var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3007
        refreshSelection(this);
3008
        if (checkForChanges) {
3009
            var i = oldRanges.length;
3010
            if (i != this._ranges.length) {
3011
                return false;
3012
            }
3013
            while (i--) {
3014
                if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
3015
                    return false;
3016
                }
3017
            }
3018
            return true;
3019
        }
3020
    };
3021
 
3022
    // Removal of a single range
3023
    var removeRangeManually = function(sel, range) {
3024
        var ranges = sel.getAllRanges(), removed = false;
3025
        sel.removeAllRanges();
3026
        for (var i = 0, len = ranges.length; i < len; ++i) {
3027
            if (removed || range !== ranges[i]) {
3028
                sel.addRange(ranges[i]);
3029
            } else {
3030
                // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
3031
                // times. removeRange should only remove the first instance, so the following ensures only the first
3032
                // instance is removed
3033
                removed = true;
3034
            }
3035
        }
3036
        if (!sel.rangeCount) {
3037
            updateEmptySelection(sel);
3038
        }
3039
    };
3040
 
3041
    if (implementsControlRange) {
3042
        selProto.removeRange = function(range) {
3043
            if (this.docSelection.type == CONTROL) {
3044
                var controlRange = this.docSelection.createRange();
3045
                var rangeElement = getSingleElementFromRange(range);
3046
 
3047
                // Create a new ControlRange containing all the elements in the selected ControlRange minus the
3048
                // element contained by the supplied range
3049
                var doc = dom.getDocument(controlRange.item(0));
3050
                var newControlRange = dom.getBody(doc).createControlRange();
3051
                var el, removed = false;
3052
                for (var i = 0, len = controlRange.length; i < len; ++i) {
3053
                    el = controlRange.item(i);
3054
                    if (el !== rangeElement || removed) {
3055
                        newControlRange.add(controlRange.item(i));
3056
                    } else {
3057
                        removed = true;
3058
                    }
3059
                }
3060
                newControlRange.select();
3061
 
3062
                // Update the wrapped selection based on what's now in the native selection
3063
                updateControlSelection(this);
3064
            } else {
3065
                removeRangeManually(this, range);
3066
            }
3067
        };
3068
    } else {
3069
        selProto.removeRange = function(range) {
3070
            removeRangeManually(this, range);
3071
        };
3072
    }
3073
 
3074
    // Detecting if a selection is backwards
3075
    var selectionIsBackwards;
3076
    if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
3077
        selectionIsBackwards = function(sel) {
3078
            var backwards = false;
3079
            if (sel.anchorNode) {
3080
                backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
3081
            }
3082
            return backwards;
3083
        };
3084
 
3085
        selProto.isBackwards = function() {
3086
            return selectionIsBackwards(this);
3087
        };
3088
    } else {
3089
        selectionIsBackwards = selProto.isBackwards = function() {
3090
            return false;
3091
        };
3092
    }
3093
 
3094
    // Selection text
3095
    // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
3096
    selProto.toString = function() {
3097
 
3098
        var rangeTexts = [];
3099
        for (var i = 0, len = this.rangeCount; i < len; ++i) {
3100
            rangeTexts[i] = "" + this._ranges[i];
3101
        }
3102
        return rangeTexts.join("");
3103
    };
3104
 
3105
    function assertNodeInSameDocument(sel, node) {
3106
        if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
3107
            throw new DOMException("WRONG_DOCUMENT_ERR");
3108
        }
3109
    }
3110
 
3111
    // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
3112
    selProto.collapse = function(node, offset) {
3113
        assertNodeInSameDocument(this, node);
3114
        var range = api.createRange(dom.getDocument(node));
3115
        range.collapseToPoint(node, offset);
3116
        this.removeAllRanges();
3117
        this.addRange(range);
3118
        this.isCollapsed = true;
3119
    };
3120
 
3121
    selProto.collapseToStart = function() {
3122
        if (this.rangeCount) {
3123
            var range = this._ranges[0];
3124
            this.collapse(range.startContainer, range.startOffset);
3125
        } else {
3126
            throw new DOMException("INVALID_STATE_ERR");
3127
        }
3128
    };
3129
 
3130
    selProto.collapseToEnd = function() {
3131
        if (this.rangeCount) {
3132
            var range = this._ranges[this.rangeCount - 1];
3133
            this.collapse(range.endContainer, range.endOffset);
3134
        } else {
3135
            throw new DOMException("INVALID_STATE_ERR");
3136
        }
3137
    };
3138
 
3139
    // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
3140
    // never used by Rangy.
3141
    selProto.selectAllChildren = function(node) {
3142
        assertNodeInSameDocument(this, node);
3143
        var range = api.createRange(dom.getDocument(node));
3144
        range.selectNodeContents(node);
3145
        this.removeAllRanges();
3146
        this.addRange(range);
3147
    };
3148
 
3149
    selProto.deleteFromDocument = function() {
3150
        // Sepcial behaviour required for Control selections
3151
        if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3152
            var controlRange = this.docSelection.createRange();
3153
            var element;
3154
            while (controlRange.length) {
3155
                element = controlRange.item(0);
3156
                controlRange.remove(element);
3157
                element.parentNode.removeChild(element);
3158
            }
3159
            this.refresh();
3160
        } else if (this.rangeCount) {
3161
            var ranges = this.getAllRanges();
3162
            this.removeAllRanges();
3163
            for (var i = 0, len = ranges.length; i < len; ++i) {
3164
                ranges[i].deleteContents();
3165
            }
3166
            // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each
3167
            // range. Firefox moves the selection to where the final selected range was, so we emulate that
3168
            this.addRange(ranges[len - 1]);
3169
        }
3170
    };
3171
 
3172
    // The following are non-standard extensions
3173
    selProto.getAllRanges = function() {
3174
        return this._ranges.slice(0);
3175
    };
3176
 
3177
    selProto.setSingleRange = function(range) {
3178
        this.setRanges( [range] );
3179
    };
3180
 
3181
    selProto.containsNode = function(node, allowPartial) {
3182
        for (var i = 0, len = this._ranges.length; i < len; ++i) {
3183
            if (this._ranges[i].containsNode(node, allowPartial)) {
3184
                return true;
3185
            }
3186
        }
3187
        return false;
3188
    };
3189
 
3190
    selProto.toHtml = function() {
3191
        var html = "";
3192
        if (this.rangeCount) {
3193
            var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
3194
            for (var i = 0, len = this._ranges.length; i < len; ++i) {
3195
                container.appendChild(this._ranges[i].cloneContents());
3196
            }
3197
            html = container.innerHTML;
3198
        }
3199
        return html;
3200
    };
3201
 
3202
    function inspect(sel) {
3203
        var rangeInspects = [];
3204
        var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
3205
        var focus = new DomPosition(sel.focusNode, sel.focusOffset);
3206
        var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
3207
 
3208
        if (typeof sel.rangeCount != "undefined") {
3209
            for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3210
                rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
3211
            }
3212
        }
3213
        return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
3214
                ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
3215
 
3216
    }
3217
 
3218
    selProto.getName = function() {
3219
        return "WrappedSelection";
3220
    };
3221
 
3222
    selProto.inspect = function() {
3223
        return inspect(this);
3224
    };
3225
 
3226
    selProto.detach = function() {
3227
        this.win[windowPropertyName] = null;
3228
        this.win = this.anchorNode = this.focusNode = null;
3229
    };
3230
 
3231
    WrappedSelection.inspect = inspect;
3232
 
3233
    api.Selection = WrappedSelection;
3234
 
3235
    api.selectionPrototype = selProto;
3236
 
3237
    api.addCreateMissingNativeApiListener(function(win) {
3238
        if (typeof win.getSelection == "undefined") {
3239
            win.getSelection = function() {
3240
                return api.getSelection(this);
3241
            };
3242
        }
3243
        win = null;
3244
    });
3245
});
3246
/*
3247
	Base.js, version 1.1a
3248
	Copyright 2006-2010, Dean Edwards
3249
	License: http://www.opensource.org/licenses/mit-license.php
3250
*/
3251
 
3252
var Base = function() {
3253
	// dummy
3254
};
3255
 
3256
Base.extend = function(_instance, _static) { // subclass
3257
	var extend = Base.prototype.extend;
3258
 
3259
	// build the prototype
3260
	Base._prototyping = true;
3261
	var proto = new this;
3262
	extend.call(proto, _instance);
3263
  proto.base = function() {
3264
    // call this method from any other method to invoke that method's ancestor
3265
  };
3266
	delete Base._prototyping;
3267
 
3268
	// create the wrapper for the constructor function
3269
	//var constructor = proto.constructor.valueOf(); //-dean
3270
	var constructor = proto.constructor;
3271
	var klass = proto.constructor = function() {
3272
		if (!Base._prototyping) {
3273
			if (this._constructing || this.constructor == klass) { // instantiation
3274
				this._constructing = true;
3275
				constructor.apply(this, arguments);
3276
				delete this._constructing;
3277
			} else if (arguments[0] != null) { // casting
3278
				return (arguments[0].extend || extend).call(arguments[0], proto);
3279
			}
3280
		}
3281
	};
3282
 
3283
	// build the class interface
3284
	klass.ancestor = this;
3285
	klass.extend = this.extend;
3286
	klass.forEach = this.forEach;
3287
	klass.implement = this.implement;
3288
	klass.prototype = proto;
3289
	klass.toString = this.toString;
3290
	klass.valueOf = function(type) {
3291
		//return (type == "object") ? klass : constructor; //-dean
3292
		return (type == "object") ? klass : constructor.valueOf();
3293
	};
3294
	extend.call(klass, _static);
3295
	// class initialisation
3296
	if (typeof klass.init == "function") klass.init();
3297
	return klass;
3298
};
3299
 
3300
Base.prototype = {	
3301
	extend: function(source, value) {
3302
		if (arguments.length > 1) { // extending with a name/value pair
3303
			var ancestor = this[source];
3304
			if (ancestor && (typeof value == "function") && // overriding a method?
3305
				// the valueOf() comparison is to avoid circular references
3306
				(!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
3307
				/\bbase\b/.test(value)) {
3308
				// get the underlying method
3309
				var method = value.valueOf();
3310
				// override
3311
				value = function() {
3312
					var previous = this.base || Base.prototype.base;
3313
					this.base = ancestor;
3314
					var returnValue = method.apply(this, arguments);
3315
					this.base = previous;
3316
					return returnValue;
3317
				};
3318
				// point to the underlying method
3319
				value.valueOf = function(type) {
3320
					return (type == "object") ? value : method;
3321
				};
3322
				value.toString = Base.toString;
3323
			}
3324
			this[source] = value;
3325
		} else if (source) { // extending with an object literal
3326
			var extend = Base.prototype.extend;
3327
			// if this object has a customised extend method then use it
3328
			if (!Base._prototyping && typeof this != "function") {
3329
				extend = this.extend || extend;
3330
			}
3331
			var proto = {toSource: null};
3332
			// do the "toString" and other methods manually
3333
			var hidden = ["constructor", "toString", "valueOf"];
3334
			// if we are prototyping then include the constructor
3335
			var i = Base._prototyping ? 0 : 1;
3336
			while (key = hidden[i++]) {
3337
				if (source[key] != proto[key]) {
3338
					extend.call(this, key, source[key]);
3339
 
3340
				}
3341
			}
3342
			// copy each of the source object's properties to this object
3343
			for (var key in source) {
3344
				if (!proto[key]) extend.call(this, key, source[key]);
3345
			}
3346
		}
3347
		return this;
3348
	}
3349
};
3350
 
3351
// initialise
3352
Base = Base.extend({
3353
	constructor: function() {
3354
		this.extend(arguments[0]);
3355
	}
3356
}, {
3357
	ancestor: Object,
3358
	version: "1.1",
3359
 
3360
	forEach: function(object, block, context) {
3361
		for (var key in object) {
3362
			if (this.prototype[key] === undefined) {
3363
				block.call(context, object[key], key, object);
3364
			}
3365
		}
3366
	},
3367
 
3368
	implement: function() {
3369
		for (var i = 0; i < arguments.length; i++) {
3370
			if (typeof arguments[i] == "function") {
3371
				// if it's a function, call it
3372
				arguments[i](this.prototype);
3373
			} else {
3374
				// add the interface using the extend method
3375
				this.prototype.extend(arguments[i]);
3376
			}
3377
		}
3378
		return this;
3379
	},
3380
 
3381
	toString: function() {
3382
		return String(this.valueOf());
3383
	}
3384
});/**
3385
 * Detect browser support for specific features
3386
 */
3387
wysihtml5.browser = (function() {
3388
  var userAgent   = navigator.userAgent,
3389
      testElement = document.createElement("div"),
3390
      // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
3391
      isIE        = userAgent.indexOf("MSIE")         !== -1 && userAgent.indexOf("Opera") === -1,
3392
      isGecko     = userAgent.indexOf("Gecko")        !== -1 && userAgent.indexOf("KHTML") === -1,
3393
      isWebKit    = userAgent.indexOf("AppleWebKit/") !== -1,
3394
      isChrome    = userAgent.indexOf("Chrome/")      !== -1,
3395
      isOpera     = userAgent.indexOf("Opera/")       !== -1;
3396
 
3397
  function iosVersion(userAgent) {
3398
    return ((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1];
3399
  }
3400
 
3401
  return {
3402
    // Static variable needed, publicly accessible, to be able override it in unit tests
3403
    USER_AGENT: userAgent,
3404
 
3405
    /**
3406
     * Exclude browsers that are not capable of displaying and handling
3407
     * contentEditable as desired:
3408
     *    - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
3409
     *    - IE < 8 create invalid markup and crash randomly from time to time
3410
     *
3411
     * @return {Boolean}
3412
     */
3413
    supported: function() {
3414
      var userAgent                   = this.USER_AGENT.toLowerCase(),
3415
          // Essential for making html elements editable
3416
          hasContentEditableSupport   = "contentEditable" in testElement,
3417
          // Following methods are needed in order to interact with the contentEditable area
3418
          hasEditingApiSupport        = document.execCommand && document.queryCommandSupported && document.queryCommandState,
3419
          // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
3420
          hasQuerySelectorSupport     = document.querySelector && document.querySelectorAll,
3421
          // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
3422
          isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
3423
 
3424
      return hasContentEditableSupport
3425
        && hasEditingApiSupport
3426
        && hasQuerySelectorSupport
3427
        && !isIncompatibleMobileBrowser;
3428
    },
3429
 
3430
    isTouchDevice: function() {
3431
      return this.supportsEvent("touchmove");
3432
    },
3433
 
3434
    isIos: function() {
3435
      var userAgent = this.USER_AGENT.toLowerCase();
3436
      return userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1;
3437
    },
3438
 
3439
    /**
3440
     * Whether the browser supports sandboxed iframes
3441
     * Currently only IE 6+ offers such feature <iframe security="restricted">
3442
     *
3443
     * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
3444
     * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
3445
     *
3446
     * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
3447
     */
3448
    supportsSandboxedIframes: function() {
3449
      return isIE;
3450
    },
3451
 
3452
    /**
3453
     * IE6+7 throw a mixed content warning when the src of an iframe
3454
     * is empty/unset or about:blank
3455
     * window.querySelector is implemented as of IE8
3456
     */
3457
    throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
3458
      return !("querySelector" in document);
3459
    },
3460
 
3461
    /**
3462
     * Whether the caret is correctly displayed in contentEditable elements
3463
     * Firefox sometimes shows a huge caret in the beginning after focusing
3464
     */
3465
    displaysCaretInEmptyContentEditableCorrectly: function() {
3466
      return !isGecko;
3467
    },
3468
 
3469
    /**
3470
     * Opera and IE are the only browsers who offer the css value
3471
     * in the original unit, thx to the currentStyle object
3472
     * All other browsers provide the computed style in px via window.getComputedStyle
3473
     */
3474
    hasCurrentStyleProperty: function() {
3475
      return "currentStyle" in testElement;
3476
    },
3477
 
3478
    /**
3479
     * Whether the browser inserts a <br> when pressing enter in a contentEditable element
3480
     */
3481
    insertsLineBreaksOnReturn: function() {
3482
      return isGecko;
3483
    },
3484
 
3485
    supportsPlaceholderAttributeOn: function(element) {
3486
      return "placeholder" in element;
3487
    },
3488
 
3489
    supportsEvent: function(eventName) {
3490
      return "on" + eventName in testElement || (function() {
3491
        testElement.setAttribute("on" + eventName, "return;");
3492
        return typeof(testElement["on" + eventName]) === "function";
3493
      })();
3494
    },
3495
 
3496
    /**
3497
     * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
3498
     */
3499
    supportsEventsInIframeCorrectly: function() {
3500
      return !isOpera;
3501
    },
3502
 
3503
    /**
3504
     * Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled
3505
     * with event.preventDefault
3506
     * Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs
3507
     * to be cancelled
3508
     */
3509
    firesOnDropOnlyWhenOnDragOverIsCancelled: function() {
3510
      return isWebKit || isGecko;
3511
    },
3512
 
3513
    /**
3514
     * Whether the browser supports the event.dataTransfer property in a proper way
3515
     */
3516
    supportsDataTransfer: function() {
3517
      try {
3518
        // Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does)
3519
        return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData;
3520
      } catch(e) {
3521
        return false;
3522
      }
3523
    },
3524
 
3525
    /**
3526
     * Everything below IE9 doesn't know how to treat HTML5 tags
3527
     *
3528
     * @param {Object} context The document object on which to check HTML5 support
3529
     *
3530
     * @example
3531
     *    wysihtml5.browser.supportsHTML5Tags(document);
3532
     */
3533
    supportsHTML5Tags: function(context) {
3534
      var element = context.createElement("div"),
3535
          html5   = "<article>foo</article>";
3536
      element.innerHTML = html5;
3537
      return element.innerHTML.toLowerCase() === html5;
3538
    },
3539
 
3540
    /**
3541
     * Checks whether a document supports a certain queryCommand
3542
     * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree
3543
     * in oder to report correct results
3544
     *
3545
     * @param {Object} doc Document object on which to check for a query command
3546
     * @param {String} command The query command to check for
3547
     * @return {Boolean}
3548
     *
3549
     * @example
3550
     *    wysihtml5.browser.supportsCommand(document, "bold");
3551
     */
3552
    supportsCommand: (function() {
3553
      // Following commands are supported but contain bugs in some browsers
3554
      var buggyCommands = {
3555
        // formatBlock fails with some tags (eg. <blockquote>)
3556
        "formatBlock":          isIE,
3557
         // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
3558
         // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
3559
         // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
3560
        "insertUnorderedList":  isIE || isOpera,
3561
        "insertOrderedList":    isIE || isOpera
3562
      };
3563
 
3564
      // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands
3565
      var supported = {
3566
        "insertHTML": isGecko
3567
      };
3568
 
3569
      return function(doc, command) {
3570
        var isBuggy = buggyCommands[command];
3571
        if (!isBuggy) {
3572
          // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
3573
          try {
3574
            return doc.queryCommandSupported(command);
3575
          } catch(e1) {}
3576
 
3577
          try {
3578
            return doc.queryCommandEnabled(command);
3579
          } catch(e2) {
3580
            return !!supported[command];
3581
          }
3582
        }
3583
        return false;
3584
      };
3585
    })(),
3586
 
3587
    /**
3588
     * IE: URLs starting with:
3589
     *    www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,
3590
     *    nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:
3591
     * will automatically be auto-linked when either the user inserts them via copy&paste or presses the
3592
     * space bar when the caret is directly after such an url.
3593
     * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll
3594
     * (related blog post on msdn
3595
     * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
3596
     */
3597
    doesAutoLinkingInContentEditable: function() {
3598
      return isIE;
3599
    },
3600
 
3601
    /**
3602
     * As stated above, IE auto links urls typed into contentEditable elements
3603
     * Since IE9 it's possible to prevent this behavior
3604
     */
3605
    canDisableAutoLinking: function() {
3606
      return this.supportsCommand(document, "AutoUrlDetect");
3607
    },
3608
 
3609
    /**
3610
     * IE leaves an empty paragraph in the contentEditable element after clearing it
3611
     * Chrome/Safari sometimes an empty <div>
3612
     */
3613
    clearsContentEditableCorrectly: function() {
3614
      return isGecko || isOpera || isWebKit;
3615
    },
3616
 
3617
    /**
3618
     * IE gives wrong results for getAttribute
3619
     */
3620
    supportsGetAttributeCorrectly: function() {
3621
      var td = document.createElement("td");
3622
      return td.getAttribute("rowspan") != "1";
3623
    },
3624
 
3625
    /**
3626
     * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.
3627
     * Chrome and Safari both don't support this
3628
     */
3629
    canSelectImagesInContentEditable: function() {
3630
      return isGecko || isIE || isOpera;
3631
    },
3632
 
3633
    /**
3634
     * When the caret is in an empty list (<ul><li>|</li></ul>) which is the first child in an contentEditable container
3635
     * pressing backspace doesn't remove the entire list as done in other browsers
3636
     */
3637
    clearsListsInContentEditableCorrectly: function() {
3638
      return isGecko || isIE || isWebKit;
3639
    },
3640
 
3641
    /**
3642
     * All browsers except Safari and Chrome automatically scroll the range/caret position into view
3643
     */
3644
    autoScrollsToCaret: function() {
3645
      return !isWebKit;
3646
    },
3647
 
3648
    /**
3649
     * Check whether the browser automatically closes tags that don't need to be opened
3650
     */
3651
    autoClosesUnclosedTags: function() {
3652
      var clonedTestElement = testElement.cloneNode(false),
3653
          returnValue,
3654
          innerHTML;
3655
 
3656
      clonedTestElement.innerHTML = "<p><div></div>";
3657
      innerHTML                   = clonedTestElement.innerHTML.toLowerCase();
3658
      returnValue                 = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";
3659
 
3660
      // Cache result by overwriting current function
3661
      this.autoClosesUnclosedTags = function() { return returnValue; };
3662
 
3663
      return returnValue;
3664
    },
3665
 
3666
    /**
3667
     * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
3668
     */
3669
    supportsNativeGetElementsByClassName: function() {
3670
      return String(document.getElementsByClassName).indexOf("[native code]") !== -1;
3671
    },
3672
 
3673
    /**
3674
     * As of now (19.04.2011) only supported by Firefox 4 and Chrome
3675
     * See https://developer.mozilla.org/en/DOM/Selection/modify
3676
     */
3677
    supportsSelectionModify: function() {
3678
      return "getSelection" in window && "modify" in window.getSelection();
3679
    },
3680
 
3681
    /**
3682
     * Whether the browser supports the classList object for fast className manipulation
3683
     * See https://developer.mozilla.org/en/DOM/element.classList
3684
     */
3685
    supportsClassList: function() {
3686
      return "classList" in testElement;
3687
    },
3688
 
3689
    /**
3690
     * Opera needs a white space after a <br> in order to position the caret correctly
3691
     */
3692
    needsSpaceAfterLineBreak: function() {
3693
      return isOpera;
3694
    },
3695
 
3696
    /**
3697
     * Whether the browser supports the speech api on the given element
3698
     * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
3699
     *
3700
     * @example
3701
     *    var input = document.createElement("input");
3702
     *    if (wysihtml5.browser.supportsSpeechApiOn(input)) {
3703
     *      // ...
3704
     *    }
3705
     */
3706
    supportsSpeechApiOn: function(input) {
3707
      var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [, 0];
3708
      return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);
3709
    },
3710
 
3711
    /**
3712
     * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest
3713
     * See https://connect.microsoft.com/ie/feedback/details/650112
3714
     * or try the POC http://tifftiff.de/ie9_crash/
3715
     */
3716
    crashesWhenDefineProperty: function(property) {
3717
      return isIE && (property === "XMLHttpRequest" || property === "XDomainRequest");
3718
    },
3719
 
3720
    /**
3721
     * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element
3722
     */
3723
    doesAsyncFocus: function() {
3724
      return isIE;
3725
    },
3726
 
3727
    /**
3728
     * 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 document
3729
     */
3730
    hasProblemsSettingCaretAfterImg: function() {
3731
      return isIE;
3732
    },
3733
 
3734
    hasUndoInContextMenu: function() {
3735
      return isGecko || isChrome || isOpera;
3736
    }
3737
  };
3738
})();wysihtml5.lang.array = function(arr) {
3739
  return {
3740
    /**
3741
     * Check whether a given object exists in an array
3742
     *
3743
     * @example
3744
     *    wysihtml5.lang.array([1, 2]).contains(1);
3745
     *    // => true
3746
     */
3747
    contains: function(needle) {
3748
      if (arr.indexOf) {
3749
        return arr.indexOf(needle) !== -1;
3750
      } else {
3751
        for (var i=0, length=arr.length; i<length; i++) {
3752
          if (arr[i] === needle) { return true; }
3753
        }
3754
        return false;
3755
      }
3756
    },
3757
 
3758
    /**
3759
     * Substract one array from another
3760
     *
3761
     * @example
3762
     *    wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]);
3763
     *    // => [1, 2]
3764
     */
3765
    without: function(arrayToSubstract) {
3766
      arrayToSubstract = wysihtml5.lang.array(arrayToSubstract);
3767
      var newArr  = [],
3768
          i       = 0,
3769
          length  = arr.length;
3770
      for (; i<length; i++) {
3771
        if (!arrayToSubstract.contains(arr[i])) {
3772
          newArr.push(arr[i]);
3773
        }
3774
      }
3775
      return newArr;
3776
    },
3777
 
3778
    /**
3779
     * Return a clean native array
3780
     * 
3781
     * Following will convert a Live NodeList to a proper Array
3782
     * @example
3783
     *    var childNodes = wysihtml5.lang.array(document.body.childNodes).get();
3784
     */
3785
    get: function() {
3786
      var i        = 0,
3787
          length   = arr.length,
3788
          newArray = [];
3789
      for (; i<length; i++) {
3790
        newArray.push(arr[i]);
3791
      }
3792
      return newArray;
3793
    }
3794
  };
3795
};wysihtml5.lang.Dispatcher = Base.extend(
3796
  /** @scope wysihtml5.lang.Dialog.prototype */ {
3797
  observe: function(eventName, handler) {
3798
    this.events = this.events || {};
3799
    this.events[eventName] = this.events[eventName] || [];
3800
    this.events[eventName].push(handler);
3801
    return this;
3802
  },
3803
 
3804
  on: function() {
3805
    return this.observe.apply(this, wysihtml5.lang.array(arguments).get());
3806
  },
3807
 
3808
  fire: function(eventName, payload) {
3809
    this.events = this.events || {};
3810
    var handlers = this.events[eventName] || [],
3811
        i        = 0;
3812
    for (; i<handlers.length; i++) {
3813
      handlers[i].call(this, payload);
3814
    }
3815
    return this;
3816
  },
3817
 
3818
  stopObserving: function(eventName, handler) {
3819
    this.events = this.events || {};
3820
    var i = 0,
3821
        handlers,
3822
        newHandlers;
3823
    if (eventName) {
3824
      handlers    = this.events[eventName] || [],
3825
      newHandlers = [];
3826
      for (; i<handlers.length; i++) {
3827
        if (handlers[i] !== handler && handler) {
3828
          newHandlers.push(handlers[i]);
3829
        }
3830
      }
3831
      this.events[eventName] = newHandlers;
3832
    } else {
3833
      // Clean up all events
3834
      this.events = {};
3835
    }
3836
    return this;
3837
  }
3838
});wysihtml5.lang.object = function(obj) {
3839
  return {
3840
    /**
3841
     * @example
3842
     *    wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();
3843
     *    // => { foo: 1, bar: 2, baz: 3 }
3844
     */
3845
    merge: function(otherObj) {
3846
      for (var i in otherObj) {
3847
        obj[i] = otherObj[i];
3848
      }
3849
      return this;
3850
    },
3851
 
3852
    get: function() {
3853
      return obj;
3854
    },
3855
 
3856
    /**
3857
     * @example
3858
     *    wysihtml5.lang.object({ foo: 1 }).clone();
3859
     *    // => { foo: 1 }
3860
     */
3861
    clone: function() {
3862
      var newObj = {},
3863
          i;
3864
      for (i in obj) {
3865
        newObj[i] = obj[i];
3866
      }
3867
      return newObj;
3868
    },
3869
 
3870
    /**
3871
     * @example
3872
     *    wysihtml5.lang.object([]).isArray();
3873
     *    // => true
3874
     */
3875
    isArray: function() {
3876
      return Object.prototype.toString.call(obj) === "[object Array]";
3877
    }
3878
  };
3879
};(function() {
3880
  var WHITE_SPACE_START = /^\s+/,
3881
      WHITE_SPACE_END   = /\s+$/;
3882
  wysihtml5.lang.string = function(str) {
3883
    str = String(str);
3884
    return {
3885
      /**
3886
       * @example
3887
       *    wysihtml5.lang.string("   foo   ").trim();
3888
       *    // => "foo"
3889
       */
3890
      trim: function() {
3891
        return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, "");
3892
      },
3893
 
3894
      /**
3895
       * @example
3896
       *    wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" });
3897
       *    // => "Hello Christopher"
3898
       */
3899
      interpolate: function(vars) {
3900
        for (var i in vars) {
3901
          str = this.replace("#{" + i + "}").by(vars[i]);
3902
        }
3903
        return str;
3904
      },
3905
 
3906
      /**
3907
       * @example
3908
       *    wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans");
3909
       *    // => "Hello Hans"
3910
       */
3911
      replace: function(search) {
3912
        return {
3913
          by: function(replace) {
3914
            return str.split(search).join(replace);
3915
          }
3916
        }
3917
      }
3918
    };
3919
  };
3920
})();/**
3921
 * Find urls in descendant text nodes of an element and auto-links them
3922
 * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/
3923
 *
3924
 * @param {Element} element Container element in which to search for urls
3925
 *
3926
 * @example
3927
 *    <div id="text-container">Please click here: www.google.com</div>
3928
 *    <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script>
3929
 */
3930
(function(wysihtml5) {
3931
  var /**
3932
       * Don't auto-link urls that are contained in the following elements:
3933
       */
3934
      IGNORE_URLS_IN        = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
3935
      /**
3936
       * revision 1:
3937
       *    /(\S+\.{1}[^\s\,\.\!]+)/g
3938
       *
3939
       * revision 2:
3940
       *    /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
3941
       *
3942
       * put this in the beginning if you don't wan't to match within a word
3943
       *    (^|[\>\(\{\[\s\>])
3944
       */
3945
      URL_REG_EXP           = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
3946
      TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
3947
      MAX_DISPLAY_LENGTH    = 100,
3948
      BRACKETS              = { ")": "(", "]": "[", "}": "{" };
3949
 
3950
  function autoLink(element) {
3951
    if (_hasParentThatShouldBeIgnored(element)) {
3952
      return element;
3953
    }
3954
 
3955
    if (element === element.ownerDocument.documentElement) {
3956
      element = element.ownerDocument.body;
3957
    }
3958
 
3959
    return _parseNode(element);
3960
  }
3961
 
3962
  /**
3963
   * This is basically a rebuild of
3964
   * the rails auto_link_urls text helper
3965
   */
3966
  function _convertUrlsToLinks(str) {
3967
    return str.replace(URL_REG_EXP, function(match, url) {
3968
      var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
3969
          opening     = BRACKETS[punctuation];
3970
      url = url.replace(TRAILING_CHAR_REG_EXP, "");
3971
 
3972
      if (url.split(opening).length > url.split(punctuation).length) {
3973
        url = url + punctuation;
3974
        punctuation = "";
3975
      }
3976
      var realUrl    = url,
3977
          displayUrl = url;
3978
      if (url.length > MAX_DISPLAY_LENGTH) {
3979
        displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
3980
      }
3981
      // Add http prefix if necessary
3982
      if (realUrl.substr(0, 4) === "www.") {
3983
        realUrl = "http://" + realUrl;
3984
      }
3985
 
3986
      return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;
3987
    });
3988
  }
3989
 
3990
  /**
3991
   * Creates or (if already cached) returns a temp element
3992
   * for the given document object
3993
   */
3994
  function _getTempElement(context) {
3995
    var tempElement = context._wysihtml5_tempElement;
3996
    if (!tempElement) {
3997
      tempElement = context._wysihtml5_tempElement = context.createElement("div");
3998
    }
3999
    return tempElement;
4000
  }
4001
 
4002
  /**
4003
   * Replaces the original text nodes with the newly auto-linked dom tree
4004
   */
4005
  function _wrapMatchesInNode(textNode) {
4006
    var parentNode  = textNode.parentNode,
4007
        tempElement = _getTempElement(parentNode.ownerDocument);
4008
 
4009
    // We need to insert an empty/temporary <span /> to fix IE quirks
4010
    // Elsewise IE would strip white space in the beginning
4011
    tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(textNode.data);
4012
    tempElement.removeChild(tempElement.firstChild);
4013
 
4014
    while (tempElement.firstChild) {
4015
      // inserts tempElement.firstChild before textNode
4016
      parentNode.insertBefore(tempElement.firstChild, textNode);
4017
    }
4018
    parentNode.removeChild(textNode);
4019
  }
4020
 
4021
  function _hasParentThatShouldBeIgnored(node) {
4022
    var nodeName;
4023
    while (node.parentNode) {
4024
      node = node.parentNode;
4025
      nodeName = node.nodeName;
4026
      if (IGNORE_URLS_IN.contains(nodeName)) {
4027
        return true;
4028
      } else if (nodeName === "body") {
4029
        return false;
4030
      }
4031
    }
4032
    return false;
4033
  }
4034
 
4035
  function _parseNode(element) {
4036
    if (IGNORE_URLS_IN.contains(element.nodeName)) {
4037
      return;
4038
    }
4039
 
4040
    if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) {
4041
      _wrapMatchesInNode(element);
4042
      return;
4043
    }
4044
 
4045
    var childNodes        = wysihtml5.lang.array(element.childNodes).get(),
4046
        childNodesLength  = childNodes.length,
4047
        i                 = 0;
4048
 
4049
    for (; i<childNodesLength; i++) {
4050
      _parseNode(childNodes[i]);
4051
    }
4052
 
4053
    return element;
4054
  }
4055
 
4056
  wysihtml5.dom.autoLink = autoLink;
4057
 
4058
  // Reveal url reg exp to the outside
4059
  wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP;
4060
})(wysihtml5);(function(wysihtml5) {
4061
  var supportsClassList = wysihtml5.browser.supportsClassList(),
4062
      api               = wysihtml5.dom;
4063
 
4064
  api.addClass = function(element, className) {
4065
    if (supportsClassList) {
4066
      return element.classList.add(className);
4067
    }
4068
    if (api.hasClass(element, className)) {
4069
      return;
4070
    }
4071
    element.className += " " + className;
4072
  };
4073
 
4074
  api.removeClass = function(element, className) {
4075
    if (supportsClassList) {
4076
      return element.classList.remove(className);
4077
    }
4078
 
4079
    element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " ");
4080
  };
4081
 
4082
  api.hasClass = function(element, className) {
4083
    if (supportsClassList) {
4084
      return element.classList.contains(className);
4085
    }
4086
 
4087
    var elementClassName = element.className;
4088
    return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
4089
  };
4090
})(wysihtml5);
4091
wysihtml5.dom.contains = (function() {
4092
  var documentElement = document.documentElement;
4093
  if (documentElement.contains) {
4094
    return function(container, element) {
4095
      if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
4096
        element = element.parentNode;
4097
      }
4098
      return container !== element && container.contains(element);
4099
    };
4100
  } else if (documentElement.compareDocumentPosition) {
4101
    return function(container, element) {
4102
      // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
4103
      return !!(container.compareDocumentPosition(element) & 16);
4104
    };
4105
  }
4106
})();/**
4107
 * Converts an HTML fragment/element into a unordered/ordered list
4108
 *
4109
 * @param {Element} element The element which should be turned into a list
4110
 * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
4111
 * @return {Element} The created list
4112
 *
4113
 * @example
4114
 *    <!-- Assume the following dom: -->
4115
 *    <span id="pseudo-list">
4116
 *      eminem<br>
4117
 *      dr. dre
4118
 *      <div>50 Cent</div>
4119
 *    </span>
4120
 *
4121
 *    <script>
4122
 *      wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul");
4123
 *    </script>
4124
 *
4125
 *    <!-- Will result in: -->
4126
 *    <ul>
4127
 *      <li>eminem</li>
4128
 *      <li>dr. dre</li>
4129
 *      <li>50 Cent</li>
4130
 *    </ul>
4131
 */
4132
wysihtml5.dom.convertToList = (function() {
4133
  function _createListItem(doc, list) {
4134
    var listItem = doc.createElement("li");
4135
    list.appendChild(listItem);
4136
    return listItem;
4137
  }
4138
 
4139
  function _createList(doc, type) {
4140
    return doc.createElement(type);
4141
  }
4142
 
4143
  function convertToList(element, listType) {
4144
    if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
4145
      // Already a list
4146
      return element;
4147
    }
4148
 
4149
    var doc               = element.ownerDocument,
4150
        list              = _createList(doc, listType),
4151
        childNodes        = wysihtml5.lang.array(element.childNodes).get(),
4152
        childNodesLength  = childNodes.length,
4153
        childNode,
4154
        isBlockElement,
4155
        isLineBreak,
4156
        currentListItem,
4157
        i                 = 0;
4158
    for (; i<childNodesLength; i++) {
4159
      currentListItem = currentListItem || _createListItem(doc, list);
4160
      childNode       = childNodes[i];
4161
      isBlockElement  = wysihtml5.dom.getStyle("display").from(childNode) === "block";
4162
      isLineBreak     = childNode.nodeName === "BR";
4163
 
4164
      if (isBlockElement) {
4165
        // Append blockElement to current <li> if empty, otherwise create a new one
4166
        currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;
4167
        currentListItem.appendChild(childNode);
4168
        currentListItem = null;
4169
        continue;
4170
      }
4171
 
4172
      if (isLineBreak) {
4173
        // Only create a new list item in the next iteration when the current one has already content
4174
        currentListItem = currentListItem.firstChild ? null : currentListItem;
4175
        continue;
4176
      }
4177
 
4178
      currentListItem.appendChild(childNode);
4179
    }
4180
 
4181
    element.parentNode.replaceChild(list, element);
4182
    return list;
4183
  }
4184
 
4185
  return convertToList;
4186
})();/**
4187
 * Copy a set of attributes from one element to another
4188
 *
4189
 * @param {Array} attributesToCopy List of attributes which should be copied
4190
 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
4191
 *    copy the attributes from., this again returns an object which provides a method named "to" which can be invoked 
4192
 *    with the element where to copy the attributes to (see example)
4193
 *
4194
 * @example
4195
 *    var textarea    = document.querySelector("textarea"),
4196
 *        div         = document.querySelector("div[contenteditable=true]"),
4197
 *        anotherDiv  = document.querySelector("div.preview");
4198
 *    wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
4199
 *
4200
 */
4201
wysihtml5.dom.copyAttributes = function(attributesToCopy) {
4202
  return {
4203
    from: function(elementToCopyFrom) {
4204
      return {
4205
        to: function(elementToCopyTo) {
4206
          var attribute,
4207
              i         = 0,
4208
              length    = attributesToCopy.length;
4209
          for (; i<length; i++) {
4210
            attribute = attributesToCopy[i];
4211
            if (elementToCopyFrom[attribute]) {
4212
              elementToCopyTo[attribute] = elementToCopyFrom[attribute];
4213
            }
4214
          }
4215
          return { andTo: arguments.callee };
4216
        }
4217
      };
4218
    }
4219
  };
4220
};/**
4221
 * Copy a set of styles from one element to another
4222
 * Please note that this only works properly across browsers when the element from which to copy the styles
4223
 * is in the dom
4224
 *
4225
 * Interesting article on how to copy styles
4226
 *
4227
 * @param {Array} stylesToCopy List of styles which should be copied
4228
 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
4229
 *    copy the styles from., this again returns an object which provides a method named "to" which can be invoked 
4230
 *    with the element where to copy the styles to (see example)
4231
 *
4232
 * @example
4233
 *    var textarea    = document.querySelector("textarea"),
4234
 *        div         = document.querySelector("div[contenteditable=true]"),
4235
 *        anotherDiv  = document.querySelector("div.preview");
4236
 *    wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);
4237
 *
4238
 */
4239
(function(dom) {
4240
 
4241
  /**
4242
   * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set
4243
   * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then 
4244
   * its computed css width will be 198px
4245
   */
4246
  var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];
4247
 
4248
  var shouldIgnoreBoxSizingBorderBox = function(element) {
4249
    if (hasBoxSizingBorderBox(element)) {
4250
       return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
4251
    }
4252
    return false;
4253
  };
4254
 
4255
  var hasBoxSizingBorderBox = function(element) {
4256
    var i       = 0,
4257
        length  = BOX_SIZING_PROPERTIES.length;
4258
    for (; i<length; i++) {
4259
      if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
4260
        return BOX_SIZING_PROPERTIES[i];
4261
      }
4262
    }
4263
  };
4264
 
4265
  dom.copyStyles = function(stylesToCopy) {
4266
    return {
4267
      from: function(element) {
4268
        if (shouldIgnoreBoxSizingBorderBox(element)) {
4269
          stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
4270
        }
4271
 
4272
        var cssText = "",
4273
            length  = stylesToCopy.length,
4274
            i       = 0,
4275
            property;
4276
        for (; i<length; i++) {
4277
          property = stylesToCopy[i];
4278
          cssText += property + ":" + dom.getStyle(property).from(element) + ";";
4279
        }
4280
 
4281
        return {
4282
          to: function(element) {
4283
            dom.setStyles(cssText).on(element);
4284
            return { andTo: arguments.callee };
4285
          }
4286
        };
4287
      }
4288
    };
4289
  };
4290
})(wysihtml5.dom);/**
4291
 * Event Delegation
4292
 *
4293
 * @example
4294
 *    wysihtml5.dom.delegate(document.body, "a", "click", function() {
4295
 *      // foo
4296
 *    });
4297
 */
4298
(function(wysihtml5) {
4299
 
4300
  wysihtml5.dom.delegate = function(container, selector, eventName, handler) {
4301
    return wysihtml5.dom.observe(container, eventName, function(event) {
4302
      var target    = event.target,
4303
          match     = wysihtml5.lang.array(container.querySelectorAll(selector));
4304
 
4305
      while (target && target !== container) {
4306
        if (match.contains(target)) {
4307
          handler.call(target, event);
4308
          break;
4309
        }
4310
        target = target.parentNode;
4311
      }
4312
    });
4313
  };
4314
 
4315
})(wysihtml5);/**
4316
 * Returns the given html wrapped in a div element
4317
 *
4318
 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
4319
 * when inserted via innerHTML
4320
 * 
4321
 * @param {String} html The html which should be wrapped in a dom element
4322
 * @param {Obejct} [context] Document object of the context the html belongs to
4323
 *
4324
 * @example
4325
 *    wysihtml5.dom.getAsDom("<article>foo</article>");
4326
 */
4327
wysihtml5.dom.getAsDom = (function() {
4328
 
4329
  var _innerHTMLShiv = function(html, context) {
4330
    var tempElement = context.createElement("div");
4331
    tempElement.style.display = "none";
4332
    context.body.appendChild(tempElement);
4333
    // IE throws an exception when trying to insert <frameset></frameset> via innerHTML
4334
    try { tempElement.innerHTML = html; } catch(e) {}
4335
    context.body.removeChild(tempElement);
4336
    return tempElement;
4337
  };
4338
 
4339
  /**
4340
   * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
4341
   */
4342
  var _ensureHTML5Compatibility = function(context) {
4343
    if (context._wysihtml5_supportsHTML5Tags) {
4344
      return;
4345
    }
4346
    for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {
4347
      context.createElement(HTML5_ELEMENTS[i]);
4348
    }
4349
    context._wysihtml5_supportsHTML5Tags = true;
4350
  };
4351
 
4352
 
4353
  /**
4354
   * List of html5 tags
4355
   * taken from http://simon.html5.org/html5-elements
4356
   */
4357
  var HTML5_ELEMENTS = [
4358
    "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption",
4359
    "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress",
4360
    "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr"
4361
  ];
4362
 
4363
  return function(html, context) {
4364
    context = context || document;
4365
    var tempElement;
4366
    if (typeof(html) === "object" && html.nodeType) {
4367
      tempElement = context.createElement("div");
4368
      tempElement.appendChild(html);
4369
    } else if (wysihtml5.browser.supportsHTML5Tags(context)) {
4370
      tempElement = context.createElement("div");
4371
      tempElement.innerHTML = html;
4372
    } else {
4373
      _ensureHTML5Compatibility(context);
4374
      tempElement = _innerHTMLShiv(html, context);
4375
    }
4376
    return tempElement;
4377
  };
4378
})();/**
4379
 * Walks the dom tree from the given node up until it finds a match
4380
 * Designed for optimal performance.
4381
 *
4382
 * @param {Element} node The from which to check the parent nodes
4383
 * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp)
4384
 * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
4385
 * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
4386
 * @example
4387
 *    var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] });
4388
 *    // ... or ...
4389
 *    var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" });
4390
 *    // ... or ...
4391
 *    var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g });
4392
 */
4393
wysihtml5.dom.getParentElement = (function() {
4394
 
4395
  function _isSameNodeName(nodeName, desiredNodeNames) {
4396
    if (!desiredNodeNames || !desiredNodeNames.length) {
4397
      return true;
4398
    }
4399
 
4400
    if (typeof(desiredNodeNames) === "string") {
4401
      return nodeName === desiredNodeNames;
4402
    } else {
4403
      return wysihtml5.lang.array(desiredNodeNames).contains(nodeName);
4404
    }
4405
  }
4406
 
4407
  function _isElement(node) {
4408
    return node.nodeType === wysihtml5.ELEMENT_NODE;
4409
  }
4410
 
4411
  function _hasClassName(element, className, classRegExp) {
4412
    var classNames = (element.className || "").match(classRegExp) || [];
4413
    if (!className) {
4414
      return !!classNames.length;
4415
    }
4416
    return classNames[classNames.length - 1] === className;
4417
  }
4418
 
4419
  function _getParentElementWithNodeName(node, nodeName, levels) {
4420
    while (levels-- && node && node.nodeName !== "BODY") {
4421
      if (_isSameNodeName(node.nodeName, nodeName)) {
4422
        return node;
4423
      }
4424
      node = node.parentNode;
4425
    }
4426
    return null;
4427
  }
4428
 
4429
  function _getParentElementWithNodeNameAndClassName(node, nodeName, className, classRegExp, levels) {
4430
    while (levels-- && node && node.nodeName !== "BODY") {
4431
      if (_isElement(node) &&
4432
          _isSameNodeName(node.nodeName, nodeName) &&
4433
          _hasClassName(node, className, classRegExp)) {
4434
        return node;
4435
      }
4436
      node = node.parentNode;
4437
    }
4438
    return null;
4439
  }
4440
 
4441
  return function(node, matchingSet, levels) {
4442
    levels = levels || 50; // Go max 50 nodes upwards from current node
4443
    if (matchingSet.className || matchingSet.classRegExp) {
4444
      return _getParentElementWithNodeNameAndClassName(
4445
        node, matchingSet.nodeName, matchingSet.className, matchingSet.classRegExp, levels
4446
      );
4447
    } else {
4448
      return _getParentElementWithNodeName(
4449
        node, matchingSet.nodeName, levels
4450
      );
4451
    }
4452
  };
4453
})();
4454
/**
4455
 * Get element's style for a specific css property
4456
 *
4457
 * @param {Element} element The element on which to retrieve the style
4458
 * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
4459
 *
4460
 * @example
4461
 *    wysihtml5.dom.getStyle("display").from(document.body);
4462
 *    // => "block"
4463
 */
4464
wysihtml5.dom.getStyle = (function() {
4465
  var stylePropertyMapping = {
4466
        "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
4467
      },
4468
      REG_EXP_CAMELIZE = /\-[a-z]/g;
4469
 
4470
  function camelize(str) {
4471
    return str.replace(REG_EXP_CAMELIZE, function(match) {
4472
      return match.charAt(1).toUpperCase();
4473
    });
4474
  }
4475
 
4476
  return function(property) {
4477
    return {
4478
      from: function(element) {
4479
        if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
4480
          return;
4481
        }
4482
 
4483
        var doc               = element.ownerDocument,
4484
            camelizedProperty = stylePropertyMapping[property] || camelize(property),
4485
            style             = element.style,
4486
            currentStyle      = element.currentStyle,
4487
            styleValue        = style[camelizedProperty];
4488
        if (styleValue) {
4489
          return styleValue;
4490
        }
4491
 
4492
        // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
4493
        // window.getComputedStyle, since it returns css property values in their original unit:
4494
        // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
4495
        // gives you the original "50%".
4496
        // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
4497
        if (currentStyle) {
4498
          try {
4499
                return currentStyle[camelizedProperty];
4500
          } catch(e) {
4501
            //ie will occasionally fail for unknown reasons. swallowing exception
4502
          }
4503
        }
4504
 
4505
        var win                 = doc.defaultView || doc.parentWindow,
4506
            needsOverflowReset  = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
4507
            originalOverflow,
4508
            returnValue;
4509
 
4510
        if (win.getComputedStyle) {
4511
          // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
4512
          // therfore we remove and restore the scrollbar and calculate the value in between
4513
          if (needsOverflowReset) {
4514
            originalOverflow = style.overflow;
4515
            style.overflow = "hidden";
4516
          }
4517
          returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
4518
          if (needsOverflowReset) {
4519
            style.overflow = originalOverflow || "";
4520
          }
4521
          return returnValue;
4522
        }
4523
      }
4524
    };
4525
  };
4526
})();/**
4527
 * High performant way to check whether an element with a specific tag name is in the given document
4528
 * Optimized for being heavily executed
4529
 * Unleashes the power of live node lists
4530
 *
4531
 * @param {Object} doc The document object of the context where to check
4532
 * @param {String} tagName Upper cased tag name
4533
 * @example
4534
 *    wysihtml5.dom.hasElementWithTagName(document, "IMG");
4535
 */
4536
wysihtml5.dom.hasElementWithTagName = (function() {
4537
  var LIVE_CACHE          = {},
4538
      DOCUMENT_IDENTIFIER = 1;
4539
 
4540
  function _getDocumentIdentifier(doc) {
4541
    return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
4542
  }
4543
 
4544
  return function(doc, tagName) {
4545
    var key         = _getDocumentIdentifier(doc) + ":" + tagName,
4546
        cacheEntry  = LIVE_CACHE[key];
4547
    if (!cacheEntry) {
4548
      cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
4549
    }
4550
 
4551
    return cacheEntry.length > 0;
4552
  };
4553
})();/**
4554
 * High performant way to check whether an element with a specific class name is in the given document
4555
 * Optimized for being heavily executed
4556
 * Unleashes the power of live node lists
4557
 *
4558
 * @param {Object} doc The document object of the context where to check
4559
 * @param {String} tagName Upper cased tag name
4560
 * @example
4561
 *    wysihtml5.dom.hasElementWithClassName(document, "foobar");
4562
 */
4563
(function(wysihtml5) {
4564
  var LIVE_CACHE          = {},
4565
      DOCUMENT_IDENTIFIER = 1;
4566
 
4567
  function _getDocumentIdentifier(doc) {
4568
    return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
4569
  }
4570
 
4571
  wysihtml5.dom.hasElementWithClassName = function(doc, className) {
4572
    // getElementsByClassName is not supported by IE<9
4573
    // but is sometimes mocked via library code (which then doesn't return live node lists)
4574
    if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) {
4575
      return !!doc.querySelector("." + className);
4576
    }
4577
 
4578
    var key         = _getDocumentIdentifier(doc) + ":" + className,
4579
        cacheEntry  = LIVE_CACHE[key];
4580
    if (!cacheEntry) {
4581
      cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
4582
    }
4583
 
4584
    return cacheEntry.length > 0;
4585
  };
4586
})(wysihtml5);
4587
wysihtml5.dom.insert = function(elementToInsert) {
4588
  return {
4589
    after: function(element) {
4590
      element.parentNode.insertBefore(elementToInsert, element.nextSibling);
4591
    },
4592
 
4593
    before: function(element) {
4594
      element.parentNode.insertBefore(elementToInsert, element);
4595
    },
4596
 
4597
    into: function(element) {
4598
      element.appendChild(elementToInsert);
4599
    }
4600
  };
4601
};wysihtml5.dom.insertCSS = function(rules) {
4602
  rules = rules.join("\n");
4603
 
4604
  return {
4605
    into: function(doc) {
4606
      var head         = doc.head || doc.getElementsByTagName("head")[0],
4607
          styleElement = doc.createElement("style");
4608
 
4609
      styleElement.type = "text/css";
4610
 
4611
      if (styleElement.styleSheet) {
4612
        styleElement.styleSheet.cssText = rules;
4613
      } else {
4614
        styleElement.appendChild(doc.createTextNode(rules));
4615
      }
4616
 
4617
      if (head) {
4618
        head.appendChild(styleElement);
4619
      }
4620
    }
4621
  };
4622
};/**
4623
 * Method to set dom events
4624
 *
4625
 * @example
4626
 *    wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
4627
 */
4628
wysihtml5.dom.observe = function(element, eventNames, handler) {
4629
  eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
4630
 
4631
  var handlerWrapper,
4632
      eventName,
4633
      i       = 0,
4634
      length  = eventNames.length;
4635
 
4636
  for (; i<length; i++) {
4637
    eventName = eventNames[i];
4638
    if (element.addEventListener) {
4639
      element.addEventListener(eventName, handler, false);
4640
    } else {
4641
      handlerWrapper = function(event) {
4642
        if (!("target" in event)) {
4643
          event.target = event.srcElement;
4644
        }
4645
        event.preventDefault = event.preventDefault || function() {
4646
          this.returnValue = false;
4647
        };
4648
        event.stopPropagation = event.stopPropagation || function() {
4649
          this.cancelBubble = true;
4650
        };
4651
        handler.call(element, event);
4652
      };
4653
      element.attachEvent("on" + eventName, handlerWrapper);
4654
    }
4655
  }
4656
 
4657
  return {
4658
    stop: function() {
4659
      var eventName,
4660
          i       = 0,
4661
          length  = eventNames.length;
4662
      for (; i<length; i++) {
4663
        eventName = eventNames[i];
4664
        if (element.removeEventListener) {
4665
          element.removeEventListener(eventName, handler, false);
4666
        } else {
4667
          element.detachEvent("on" + eventName, handlerWrapper);
4668
        }
4669
      }
4670
    }
4671
  };
4672
};
4673
/**
4674
 * HTML Sanitizer
4675
 * Rewrites the HTML based on given rules
4676
 *
4677
 * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized
4678
 * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will
4679
 *    be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the
4680
 *    desired substitution.
4681
 * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing
4682
 *
4683
 * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
4684
 *
4685
 * @example
4686
 *    var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';
4687
 *    wysihtml5.dom.parse(userHTML, {
4688
 *      tags {
4689
 *        p:      "div",      // Rename p tags to div tags
4690
 *        font:   "span"      // Rename font tags to span tags
4691
 *        div:    true,       // Keep them, also possible (same result when passing: "div" or true)
4692
 *        script: undefined   // Remove script elements
4693
 *      }
4694
 *    });
4695
 *    // => <div><div><span>foo bar</span></div></div>
4696
 *
4697
 *    var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';
4698
 *    wysihtml5.dom.parse(userHTML);
4699
 *    // => '<span><span><span><span>I'm a table!</span></span></span></span>'
4700
 *
4701
 *    var userHTML = '<div>foobar<br>foobar</div>';
4702
 *    wysihtml5.dom.parse(userHTML, {
4703
 *      tags: {
4704
 *        div: undefined,
4705
 *        br:  true
4706
 *      }
4707
 *    });
4708
 *    // => ''
4709
 *
4710
 *    var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
4711
 *    wysihtml5.dom.parse(userHTML, {
4712
 *      classes: {
4713
 *        red:    1,
4714
 *        green:  1
4715
 *      },
4716
 *      tags: {
4717
 *        div: {
4718
 *          rename_tag:     "p"
4719
 *        }
4720
 *      }
4721
 *    });
4722
 *    // => '<p class="red">foo</p><p>bar</p>'
4723
 */
4724
wysihtml5.dom.parse = (function() {
4725
 
4726
  /**
4727
   * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
4728
   * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the
4729
   * node isn't closed
4730
   *
4731
   * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
4732
   */
4733
  var NODE_TYPE_MAPPING = {
4734
        "1": _handleElement,
4735
        "3": _handleText
4736
      },
4737
      // Rename unknown tags to this
4738
      DEFAULT_NODE_NAME   = "span",
4739
      WHITE_SPACE_REG_EXP = /\s+/,
4740
      defaultRules        = { tags: {}, classes: {} },
4741
      currentRules        = {};
4742
 
4743
  /**
4744
   * Iterates over all childs of the element, recreates them, appends them into a document fragment
4745
   * which later replaces the entire body content
4746
   */
4747
  function parse(elementOrHtml, rules, context, cleanUp) {
4748
    wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules).get();
4749
 
4750
    context           = context || elementOrHtml.ownerDocument || document;
4751
    var fragment      = context.createDocumentFragment(),
4752
        isString      = typeof(elementOrHtml) === "string",
4753
        element,
4754
        newNode,
4755
        firstChild;
4756
 
4757
    if (isString) {
4758
      element = wysihtml5.dom.getAsDom(elementOrHtml, context);
4759
    } else {
4760
      element = elementOrHtml;
4761
    }
4762
 
4763
    while (element.firstChild) {
4764
      firstChild  = element.firstChild;
4765
      element.removeChild(firstChild);
4766
      newNode = _convert(firstChild, cleanUp);
4767
      if (newNode) {
4768
        fragment.appendChild(newNode);
4769
      }
4770
    }
4771
 
4772
    // Clear element contents
4773
    element.innerHTML = "";
4774
 
4775
    // Insert new DOM tree
4776
    element.appendChild(fragment);
4777
 
4778
    return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element;
4779
  }
4780
 
4781
  function _convert(oldNode, cleanUp) {
4782
    var oldNodeType     = oldNode.nodeType,
4783
        oldChilds       = oldNode.childNodes,
4784
        oldChildsLength = oldChilds.length,
4785
        newNode,
4786
        method          = NODE_TYPE_MAPPING[oldNodeType],
4787
        i               = 0;
4788
 
4789
    newNode = method && method(oldNode);
4790
 
4791
    if (!newNode) {
4792
      return null;
4793
    }
4794
 
4795
    for (i=0; i<oldChildsLength; i++) {
4796
      newChild = _convert(oldChilds[i], cleanUp);
4797
      if (newChild) {
4798
        newNode.appendChild(newChild);
4799
      }
4800
    }
4801
 
4802
    // Cleanup senseless <span> elements
4803
    if (cleanUp &&
4804
        newNode.childNodes.length <= 1 &&
4805
        newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
4806
        !newNode.attributes.length) {
4807
      return newNode.firstChild;
4808
    }
4809
 
4810
    return newNode;
4811
  }
4812
 
4813
  function _handleElement(oldNode) {
4814
    var rule,
4815
        newNode,
4816
        endTag,
4817
        tagRules    = currentRules.tags,
4818
        nodeName    = oldNode.nodeName.toLowerCase(),
4819
        scopeName   = oldNode.scopeName;
4820
 
4821
    /**
4822
     * We already parsed that element
4823
     * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
4824
     */
4825
    if (oldNode._wysihtml5) {
4826
      return null;
4827
    }
4828
    oldNode._wysihtml5 = 1;
4829
 
4830
    if (oldNode.className === "wysihtml5-temp") {
4831
      return null;
4832
    }
4833
 
4834
    /**
4835
     * IE is the only browser who doesn't include the namespace in the
4836
     * nodeName, that's why we have to prepend it by ourselves
4837
     * scopeName is a proprietary IE feature
4838
     * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
4839
     */
4840
    if (scopeName && scopeName != "HTML") {
4841
      nodeName = scopeName + ":" + nodeName;
4842
    }
4843
 
4844
    /**
4845
     * Repair node
4846
     * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
4847
     * A <p> doesn't need to be closed according HTML4-5 spec, we simply replace it with a <div> to preserve its content and layout
4848
     */
4849
    if ("outerHTML" in oldNode) {
4850
      if (!wysihtml5.browser.autoClosesUnclosedTags() &&
4851
          oldNode.nodeName === "P" &&
4852
          oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
4853
        nodeName = "div";
4854
      }
4855
    }
4856
 
4857
    if (nodeName in tagRules) {
4858
      rule = tagRules[nodeName];
4859
      if (!rule || rule.remove) {
4860
        return null;
4861
      }
4862
 
4863
      rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
4864
    } else if (oldNode.firstChild) {
4865
      rule = { rename_tag: DEFAULT_NODE_NAME };
4866
    } else {
4867
      // Remove empty unknown elements
4868
      return null;
4869
    }
4870
 
4871
    newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName);
4872
    _handleAttributes(oldNode, newNode, rule);
4873
 
4874
    oldNode = null;
4875
    return newNode;
4876
  }
4877
 
4878
  function _handleAttributes(oldNode, newNode, rule) {
4879
    var attributes          = {},                         // fresh new set of attributes to set on newNode
4880
        setClass            = rule.set_class,             // classes to set
4881
        addClass            = rule.add_class,             // add classes based on existing attributes
4882
        setAttributes       = rule.set_attributes,        // attributes to set on the current node
4883
        checkAttributes     = rule.check_attributes,      // check/convert values of attributes
4884
        allowedClasses      = currentRules.classes,
4885
        i                   = 0,
4886
        classes             = [],
4887
        newClasses          = [],
4888
        newUniqueClasses    = [],
4889
        oldClasses          = [],
4890
        classesLength,
4891
        newClassesLength,
4892
        currentClass,
4893
        newClass,
4894
        attributeName,
4895
        newAttributeValue,
4896
        method;
4897
 
4898
    if (setAttributes) {
4899
      attributes = wysihtml5.lang.object(setAttributes).clone();
4900
    }
4901
 
4902
    if (checkAttributes) {
4903
      for (attributeName in checkAttributes) {
4904
        method = attributeCheckMethods[checkAttributes[attributeName]];
4905
        if (!method) {
4906
          continue;
4907
        }
4908
        newAttributeValue = method(_getAttribute(oldNode, attributeName));
4909
        if (typeof(newAttributeValue) === "string") {
4910
          attributes[attributeName] = newAttributeValue;
4911
        }
4912
      }
4913
    }
4914
 
4915
    if (setClass) {
4916
      classes.push(setClass);
4917
    }
4918
 
4919
    if (addClass) {
4920
      for (attributeName in addClass) {
4921
        method = addClassMethods[addClass[attributeName]];
4922
        if (!method) {
4923
          continue;
4924
        }
4925
        newClass = method(_getAttribute(oldNode, attributeName));
4926
        if (typeof(newClass) === "string") {
4927
          classes.push(newClass);
4928
        }
4929
      }
4930
    }
4931
 
4932
    // make sure that wysihtml5 temp class doesn't get stripped out
4933
    allowedClasses["_wysihtml5-temp-placeholder"] = 1;
4934
 
4935
    // add old classes last
4936
    oldClasses = oldNode.getAttribute("class");
4937
    if (oldClasses) {
4938
      classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
4939
    }
4940
    classesLength = classes.length;
4941
    for (; i<classesLength; i++) {
4942
      currentClass = classes[i];
4943
      if (allowedClasses[currentClass]) {
4944
        newClasses.push(currentClass);
4945
      }
4946
    }
4947
 
4948
    // remove duplicate entries and preserve class specificity
4949
    newClassesLength = newClasses.length;
4950
    while (newClassesLength--) {
4951
      currentClass = newClasses[newClassesLength];
4952
      if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) {
4953
        newUniqueClasses.unshift(currentClass);
4954
      }
4955
    }
4956
 
4957
    if (newUniqueClasses.length) {
4958
      attributes["class"] = newUniqueClasses.join(" ");
4959
    }
4960
 
4961
    // set attributes on newNode
4962
    for (attributeName in attributes) {
4963
      // Setting attributes can cause a js error in IE under certain circumstances
4964
      // eg. on a <img> under https when it's new attribute value is non-https
4965
      // TODO: Investigate this further and check for smarter handling
4966
      try {
4967
        newNode.setAttribute(attributeName, attributes[attributeName]);
4968
      } catch(e) {}
4969
    }
4970
 
4971
    // IE8 sometimes loses the width/height attributes when those are set before the "src"
4972
    // so we make sure to set them again
4973
    if (attributes.src) {
4974
      if (typeof(attributes.width) !== "undefined") {
4975
        newNode.setAttribute("width", attributes.width);
4976
      }
4977
      if (typeof(attributes.height) !== "undefined") {
4978
        newNode.setAttribute("height", attributes.height);
4979
      }
4980
    }
4981
  }
4982
 
4983
  /**
4984
   * IE gives wrong results for hasAttribute/getAttribute, for example:
4985
   *    var td = document.createElement("td");
4986
   *    td.getAttribute("rowspan"); // => "1" in IE
4987
   *
4988
   * Therefore we have to check the element's outerHTML for the attribute
4989
   */
4990
  var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly();
4991
  function _getAttribute(node, attributeName) {
4992
    attributeName = attributeName.toLowerCase();
4993
    var nodeName = node.nodeName;
4994
    if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) {
4995
      // Get 'src' attribute value via object property since this will always contain the
4996
      // full absolute url (http://...)
4997
      // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
4998
      // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
4999
      return node.src;
5000
    } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
5001
      // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
5002
      var outerHTML      = node.outerHTML.toLowerCase(),
5003
          // TODO: This might not work for attributes without value: <input disabled>
5004
          hasAttribute   = outerHTML.indexOf(" " + attributeName +  "=") != -1;
5005
 
5006
      return hasAttribute ? node.getAttribute(attributeName) : null;
5007
    } else{
5008
      return node.getAttribute(attributeName);
5009
    }
5010
  }
5011
 
5012
  /**
5013
   * Check whether the given node is a proper loaded image
5014
   * FIXME: Returns undefined when unknown (Chrome, Safari)
5015
   */
5016
  function _isLoadedImage(node) {
5017
    try {
5018
      return node.complete && !node.mozMatchesSelector(":-moz-broken");
5019
    } catch(e) {
5020
      if (node.complete && node.readyState === "complete") {
5021
        return true;
5022
      }
5023
    }
5024
  }
5025
 
5026
  function _handleText(oldNode) {
5027
    return oldNode.ownerDocument.createTextNode(oldNode.data);
5028
  }
5029
 
5030
 
5031
  // ------------ attribute checks ------------ \\
5032
  var attributeCheckMethods = {
5033
    url: (function() {
5034
      var REG_EXP = /^https?:\/\//i;
5035
      return function(attributeValue) {
5036
        if (!attributeValue || !attributeValue.match(REG_EXP)) {
5037
          return null;
5038
        }
5039
        return attributeValue.replace(REG_EXP, function(match) {
5040
          return match.toLowerCase();
5041
        });
5042
      };
5043
    })(),
5044
 
5045
    alt: (function() {
5046
      var REG_EXP = /[^ a-z0-9_\-]/gi;
5047
      return function(attributeValue) {
5048
        if (!attributeValue) {
5049
          return "";
5050
        }
5051
        return attributeValue.replace(REG_EXP, "");
5052
      };
5053
    })(),
5054
 
5055
    numbers: (function() {
5056
      var REG_EXP = /\D/g;
5057
      return function(attributeValue) {
5058
        attributeValue = (attributeValue || "").replace(REG_EXP, "");
5059
        return attributeValue || null;
5060
      };
5061
    })()
5062
  };
5063
 
5064
  // ------------ class converter (converts an html attribute to a class name) ------------ \\
5065
  var addClassMethods = {
5066
    align_img: (function() {
5067
      var mapping = {
5068
        left:   "wysiwyg-float-left",
5069
        right:  "wysiwyg-float-right"
5070
      };
5071
      return function(attributeValue) {
5072
        return mapping[String(attributeValue).toLowerCase()];
5073
      };
5074
    })(),
5075
 
5076
    align_text: (function() {
5077
      var mapping = {
5078
        left:     "wysiwyg-text-align-left",
5079
        right:    "wysiwyg-text-align-right",
5080
        center:   "wysiwyg-text-align-center",
5081
        justify:  "wysiwyg-text-align-justify"
5082
      };
5083
      return function(attributeValue) {
5084
        return mapping[String(attributeValue).toLowerCase()];
5085
      };
5086
    })(),
5087
 
5088
    clear_br: (function() {
5089
      var mapping = {
5090
        left:   "wysiwyg-clear-left",
5091
        right:  "wysiwyg-clear-right",
5092
        both:   "wysiwyg-clear-both",
5093
        all:    "wysiwyg-clear-both"
5094
      };
5095
      return function(attributeValue) {
5096
        return mapping[String(attributeValue).toLowerCase()];
5097
      };
5098
    })(),
5099
 
5100
    size_font: (function() {
5101
      var mapping = {
5102
        "1": "wysiwyg-font-size-xx-small",
5103
        "2": "wysiwyg-font-size-small",
5104
        "3": "wysiwyg-font-size-medium",
5105
        "4": "wysiwyg-font-size-large",
5106
        "5": "wysiwyg-font-size-x-large",
5107
        "6": "wysiwyg-font-size-xx-large",
5108
        "7": "wysiwyg-font-size-xx-large",
5109
        "-": "wysiwyg-font-size-smaller",
5110
        "+": "wysiwyg-font-size-larger"
5111
      };
5112
      return function(attributeValue) {
5113
        return mapping[String(attributeValue).charAt(0)];
5114
      };
5115
    })()
5116
  };
5117
 
5118
  return parse;
5119
})();/**
5120
 * Checks for empty text node childs and removes them
5121
 *
5122
 * @param {Element} node The element in which to cleanup
5123
 * @example
5124
 *    wysihtml5.dom.removeEmptyTextNodes(element);
5125
 */
5126
wysihtml5.dom.removeEmptyTextNodes = function(node) {
5127
  var childNode,
5128
      childNodes        = wysihtml5.lang.array(node.childNodes).get(),
5129
      childNodesLength  = childNodes.length,
5130
      i                 = 0;
5131
  for (; i<childNodesLength; i++) {
5132
    childNode = childNodes[i];
5133
    if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {
5134
      childNode.parentNode.removeChild(childNode);
5135
    }
5136
  }
5137
};
5138
/**
5139
 * Renames an element (eg. a <div> to a <p>) and keeps its childs
5140
 *
5141
 * @param {Element} element The list element which should be renamed
5142
 * @param {Element} newNodeName The desired tag name
5143
 *
5144
 * @example
5145
 *    <!-- Assume the following dom: -->
5146
 *    <ul id="list">
5147
 *      <li>eminem</li>
5148
 *      <li>dr. dre</li>
5149
 *      <li>50 Cent</li>
5150
 *    </ul>
5151
 *
5152
 *    <script>
5153
 *      wysihtml5.dom.renameElement(document.getElementById("list"), "ol");
5154
 *    </script>
5155
 *
5156
 *    <!-- Will result in: -->
5157
 *    <ol>
5158
 *      <li>eminem</li>
5159
 *      <li>dr. dre</li>
5160
 *      <li>50 Cent</li>
5161
 *    </ol>
5162
 */
5163
wysihtml5.dom.renameElement = function(element, newNodeName) {
5164
  var newElement = element.ownerDocument.createElement(newNodeName),
5165
      firstChild;
5166
  while (firstChild = element.firstChild) {
5167
    newElement.appendChild(firstChild);
5168
  }
5169
  wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
5170
  element.parentNode.replaceChild(newElement, element);
5171
  return newElement;
5172
};/**
5173
 * Takes an element, removes it and replaces it with it's childs
5174
 * 
5175
 * @param {Object} node The node which to replace with it's child nodes
5176
 * @example
5177
 *    <div id="foo">
5178
 *      <span>hello</span>
5179
 *    </div>
5180
 *    <script>
5181
 *      // Remove #foo and replace with it's children
5182
 *      wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo"));
5183
 *    </script>
5184
 */
5185
wysihtml5.dom.replaceWithChildNodes = function(node) {
5186
  if (!node.parentNode) {
5187
    return;
5188
  }
5189
 
5190
  if (!node.firstChild) {
5191
    node.parentNode.removeChild(node);
5192
    return;
5193
  }
5194
 
5195
  var fragment = node.ownerDocument.createDocumentFragment();
5196
  while (node.firstChild) {
5197
    fragment.appendChild(node.firstChild);
5198
  }
5199
  node.parentNode.replaceChild(fragment, node);
5200
  node = fragment = null;
5201
};
5202
/**
5203
 * Unwraps an unordered/ordered list
5204
 *
5205
 * @param {Element} element The list element which should be unwrapped
5206
 *
5207
 * @example
5208
 *    <!-- Assume the following dom: -->
5209
 *    <ul id="list">
5210
 *      <li>eminem</li>
5211
 *      <li>dr. dre</li>
5212
 *      <li>50 Cent</li>
5213
 *    </ul>
5214
 *
5215
 *    <script>
5216
 *      wysihtml5.dom.resolveList(document.getElementById("list"));
5217
 *    </script>
5218
 *
5219
 *    <!-- Will result in: -->
5220
 *    eminem<br>
5221
 *    dr. dre<br>
5222
 *    50 Cent<br>
5223
 */
5224
(function(dom) {
5225
  function _isBlockElement(node) {
5226
    return dom.getStyle("display").from(node) === "block";
5227
  }
5228
 
5229
  function _isLineBreak(node) {
5230
    return node.nodeName === "BR";
5231
  }
5232
 
5233
  function _appendLineBreak(element) {
5234
    var lineBreak = element.ownerDocument.createElement("br");
5235
    element.appendChild(lineBreak);
5236
  }
5237
 
5238
  function resolveList(list) {
5239
    if (list.nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") {
5240
      return;
5241
    }
5242
 
5243
    var doc             = list.ownerDocument,
5244
        fragment        = doc.createDocumentFragment(),
5245
        previousSibling = list.previousSibling,
5246
        firstChild,
5247
        lastChild,
5248
        isLastChild,
5249
        shouldAppendLineBreak,
5250
        listItem;
5251
 
5252
    if (previousSibling && !_isBlockElement(previousSibling)) {
5253
      _appendLineBreak(fragment);
5254
    }
5255
 
5256
    while (listItem = list.firstChild) {
5257
      lastChild = listItem.lastChild;
5258
      while (firstChild = listItem.firstChild) {
5259
        isLastChild           = firstChild === lastChild;
5260
        // This needs to be done before appending it to the fragment, as it otherwise will loose style information
5261
        shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
5262
        fragment.appendChild(firstChild);
5263
        if (shouldAppendLineBreak) {
5264
          _appendLineBreak(fragment);
5265
        }
5266
      }
5267
 
5268
      listItem.parentNode.removeChild(listItem);
5269
    }
5270
    list.parentNode.replaceChild(fragment, list);
5271
  }
5272
 
5273
  dom.resolveList = resolveList;
5274
})(wysihtml5.dom);/**
5275
 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
5276
 *
5277
 * Browser Compatibility:
5278
 *  - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
5279
 *  - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
5280
 *
5281
 * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
5282
 *    - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
5283
 *    - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
5284
 *    - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
5285
 *    - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
5286
 *      can do anything as if the sandbox attribute wasn't set
5287
 *
5288
 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
5289
 * @param {Object} [config] Optional parameters
5290
 *
5291
 * @example
5292
 *    new wysihtml5.dom.Sandbox(function(sandbox) {
5293
 *      sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
5294
 *    });
5295
 */
5296
(function(wysihtml5) {
5297
  var /**
5298
       * Default configuration
5299
       */
5300
      doc                 = document,
5301
      /**
5302
       * Properties to unset/protect on the window object
5303
       */
5304
      windowProperties    = [
5305
        "parent", "top", "opener", "frameElement", "frames",
5306
        "localStorage", "globalStorage", "sessionStorage", "indexedDB"
5307
      ],
5308
      /**
5309
       * Properties on the window object which are set to an empty function
5310
       */
5311
      windowProperties2   = [
5312
        "open", "close", "openDialog", "showModalDialog",
5313
        "alert", "confirm", "prompt",
5314
        "openDatabase", "postMessage",
5315
        "XMLHttpRequest", "XDomainRequest"
5316
      ],
5317
      /**
5318
       * Properties to unset/proetect on the document object
5319
       */
5320
      documentProperties  = [
5321
        "referrer",
5322
        "write", "open", "close"
5323
      ];
5324
 
5325
  wysihtml5.dom.Sandbox = Base.extend(
5326
    /** @scope wysihtml5.dom.Sandbox.prototype */ {
5327
 
5328
    constructor: function(readyCallback, config) {
5329
      this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
5330
      this.config   = wysihtml5.lang.object({}).merge(config).get();
5331
      this.iframe   = this._createIframe();
5332
    },
5333
 
5334
    insertInto: function(element) {
5335
      if (typeof(element) === "string") {
5336
        element = doc.getElementById(element);
5337
      }
5338
 
5339
      element.appendChild(this.iframe);
5340
    },
5341
 
5342
    getIframe: function() {
5343
      return this.iframe;
5344
    },
5345
 
5346
    getWindow: function() {
5347
      this._readyError();
5348
    },
5349
 
5350
    getDocument: function() {
5351
      this._readyError();
5352
    },
5353
 
5354
    destroy: function() {
5355
      var iframe = this.getIframe();
5356
      iframe.parentNode.removeChild(iframe);
5357
    },
5358
 
5359
    _readyError: function() {
5360
      throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
5361
    },
5362
 
5363
    /**
5364
     * Creates the sandbox iframe
5365
     *
5366
     * Some important notes:
5367
     *  - We can't use HTML5 sandbox for now:
5368
     *    setting it causes that the iframe's dom can't be accessed from the outside
5369
     *    Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
5370
     *    But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
5371
     *    In order to make this happen we need to set the "allow-scripts" flag.
5372
     *    A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
5373
     *  - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
5374
     *  - IE needs to have the security="restricted" attribute set before the iframe is 
5375
     *    inserted into the dom tree
5376
     *  - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
5377
     *    though it supports it
5378
     *  - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
5379
     *  - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
5380
     *    on the onreadystatechange event
5381
     */
5382
    _createIframe: function() {
5383
      var that   = this,
5384
          iframe = doc.createElement("iframe");
5385
      iframe.className = "wysihtml5-sandbox";
5386
      wysihtml5.dom.setAttributes({
5387
        "security":           "restricted",
5388
        "allowtransparency":  "true",
5389
        "frameborder":        0,
5390
        "width":              0,
5391
        "height":             0,
5392
        "marginwidth":        0,
5393
        "marginheight":       0
5394
      }).on(iframe);
5395
 
5396
      // Setting the src like this prevents ssl warnings in IE6
5397
      if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
5398
        iframe.src = "javascript:'<html></html>'";
5399
      }
5400
 
5401
      iframe.onload = function() {
5402
        iframe.onreadystatechange = iframe.onload = null;
5403
        that._onLoadIframe(iframe);
5404
      };
5405
 
5406
      iframe.onreadystatechange = function() {
5407
        if (/loaded|complete/.test(iframe.readyState)) {
5408
          iframe.onreadystatechange = iframe.onload = null;
5409
          that._onLoadIframe(iframe);
5410
        }
5411
      };
5412
 
5413
      return iframe;
5414
    },
5415
 
5416
    /**
5417
     * Callback for when the iframe has finished loading
5418
     */
5419
    _onLoadIframe: function(iframe) {
5420
      // don't resume when the iframe got unloaded (eg. by removing it from the dom)
5421
      if (!wysihtml5.dom.contains(doc.documentElement, iframe)) {
5422
        return;
5423
      }
5424
 
5425
      var that           = this,
5426
          iframeWindow   = iframe.contentWindow,
5427
          iframeDocument = iframe.contentWindow.document,
5428
          charset        = doc.characterSet || doc.charset || "utf-8",
5429
          sandboxHtml    = this._getHtml({
5430
            charset:      charset,
5431
            stylesheets:  this.config.stylesheets
5432
          });
5433
 
5434
      // Create the basic dom tree including proper DOCTYPE and charset
5435
      iframeDocument.open("text/html", "replace");
5436
      iframeDocument.write(sandboxHtml);
5437
      iframeDocument.close();
5438
 
5439
      this.getWindow = function() { return iframe.contentWindow; };
5440
      this.getDocument = function() { return iframe.contentWindow.document; };
5441
 
5442
      // Catch js errors and pass them to the parent's onerror event
5443
      // addEventListener("error") doesn't work properly in some browsers
5444
      // TODO: apparently this doesn't work in IE9!
5445
      iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
5446
        throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);
5447
      };
5448
 
5449
      if (!wysihtml5.browser.supportsSandboxedIframes()) {
5450
        // Unset a bunch of sensitive variables
5451
        // Please note: This isn't hack safe!  
5452
        // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
5453
        // IE is secure though, which is the most important thing, since IE is the only browser, who
5454
        // takes over scripts & styles into contentEditable elements when copied from external websites
5455
        // or applications (Microsoft Word, ...)
5456
        var i, length;
5457
        for (i=0, length=windowProperties.length; i<length; i++) {
5458
          this._unset(iframeWindow, windowProperties[i]);
5459
        }
5460
        for (i=0, length=windowProperties2.length; i<length; i++) {
5461
          this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION);
5462
        }
5463
        for (i=0, length=documentProperties.length; i<length; i++) {
5464
          this._unset(iframeDocument, documentProperties[i]);
5465
        }
5466
        // This doesn't work in Safari 5 
5467
        // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
5468
        this._unset(iframeDocument, "cookie", "", true);
5469
      }
5470
 
5471
      this.loaded = true;
5472
 
5473
      // Trigger the callback
5474
      setTimeout(function() { that.callback(that); }, 0);
5475
    },
5476
 
5477
    _getHtml: function(templateVars) {
5478
      var stylesheets = templateVars.stylesheets,
5479
          html        = "",
5480
          i           = 0,
5481
          length;
5482
      stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
5483
      if (stylesheets) {
5484
        length = stylesheets.length;
5485
        for (; i<length; i++) {
5486
          html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
5487
        }
5488
      }
5489
      templateVars.stylesheets = html;
5490
 
5491
      return wysihtml5.lang.string(
5492
        '<!DOCTYPE html><html><head>'
5493
        + '<meta charset="#{charset}">#{stylesheets}</head>'
5494
        + '<body></body></html>'
5495
      ).interpolate(templateVars);
5496
    },
5497
 
5498
    /**
5499
     * Method to unset/override existing variables
5500
     * @example
5501
     *    // Make cookie unreadable and unwritable
5502
     *    this._unset(document, "cookie", "", true);
5503
     */
5504
    _unset: function(object, property, value, setter) {
5505
      try { object[property] = value; } catch(e) {}
5506
 
5507
      try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
5508
      if (setter) {
5509
        try { object.__defineSetter__(property, function() {}); } catch(e) {}
5510
      }
5511
 
5512
      if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {
5513
        try {
5514
          var config = {
5515
            get: function() { return value; }
5516
          };
5517
          if (setter) {
5518
            config.set = function() {};
5519
          }
5520
          Object.defineProperty(object, property, config);
5521
        } catch(e) {}
5522
      }
5523
    }
5524
  });
5525
})(wysihtml5);
5526
(function() {
5527
  var mapping = {
5528
    "className": "class"
5529
  };
5530
  wysihtml5.dom.setAttributes = function(attributes) {
5531
    return {
5532
      on: function(element) {
5533
        for (var i in attributes) {
5534
          element.setAttribute(mapping[i] || i, attributes[i]);
5535
        }
5536
      }
5537
    }
5538
  };
5539
})();wysihtml5.dom.setStyles = function(styles) {
5540
  return {
5541
    on: function(element) {
5542
      var style = element.style;
5543
      if (typeof(styles) === "string") {
5544
        style.cssText += ";" + styles;
5545
        return;
5546
      }
5547
      for (var i in styles) {
5548
        if (i === "float") {
5549
          style.cssFloat = styles[i];
5550
          style.styleFloat = styles[i];
5551
        } else {
5552
          style[i] = styles[i];
5553
        }
5554
      }
5555
    }
5556
  };
5557
};/**
5558
 * Simulate HTML5 placeholder attribute
5559
 *
5560
 * Needed since
5561
 *    - div[contentEditable] elements don't support it
5562
 *    - older browsers (such as IE8 and Firefox 3.6) don't support it at all
5563
 *
5564
 * @param {Object} parent Instance of main wysihtml5.Editor class
5565
 * @param {Element} view Instance of wysihtml5.views.* class
5566
 * @param {String} placeholderText
5567
 *
5568
 * @example
5569
 *    wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
5570
 */
5571
(function(dom) {
5572
  dom.simulatePlaceholder = function(editor, view, placeholderText) {
5573
    var CLASS_NAME = "placeholder",
5574
        unset = function() {
5575
          if (view.hasPlaceholderSet()) {
5576
            view.clear();
5577
          }
5578
          dom.removeClass(view.element, CLASS_NAME);
5579
        },
5580
        set = function() {
5581
          if (view.isEmpty()) {
5582
            view.setValue(placeholderText);
5583
            dom.addClass(view.element, CLASS_NAME);
5584
          }
5585
        };
5586
 
5587
    editor
5588
      .observe("set_placeholder", set)
5589
      .observe("unset_placeholder", unset)
5590
      .observe("focus:composer", unset)
5591
      .observe("paste:composer", unset)
5592
      .observe("blur:composer", set);
5593
 
5594
    set();
5595
  };
5596
})(wysihtml5.dom);
5597
(function(dom) {
5598
  var documentElement = document.documentElement;
5599
  if ("textContent" in documentElement) {
5600
    dom.setTextContent = function(element, text) {
5601
      element.textContent = text;
5602
    };
5603
 
5604
    dom.getTextContent = function(element) {
5605
      return element.textContent;
5606
    };
5607
  } else if ("innerText" in documentElement) {
5608
    dom.setTextContent = function(element, text) {
5609
      element.innerText = text;
5610
    };
5611
 
5612
    dom.getTextContent = function(element) {
5613
      return element.innerText;
5614
    };
5615
  } else {
5616
    dom.setTextContent = function(element, text) {
5617
      element.nodeValue = text;
5618
    };
5619
 
5620
    dom.getTextContent = function(element) {
5621
      return element.nodeValue;
5622
    };
5623
  }
5624
})(wysihtml5.dom);
5625
 
5626
/**
5627
 * Fix most common html formatting misbehaviors of browsers implementation when inserting
5628
 * content via copy & paste contentEditable
5629
 *
5630
 * @author Christopher Blum
5631
 */
5632
wysihtml5.quirks.cleanPastedHTML = (function() {
5633
  // TODO: We probably need more rules here
5634
  var defaultRules = {
5635
    // When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling
5636
    "a u": wysihtml5.dom.replaceWithChildNodes
5637
  };
5638
 
5639
  function cleanPastedHTML(elementOrHtml, rules, context) {
5640
    rules   = rules || defaultRules;
5641
    context = context || elementOrHtml.ownerDocument || document;
5642
 
5643
    var element,
5644
        isString = typeof(elementOrHtml) === "string",
5645
        method,
5646
        matches,
5647
        matchesLength,
5648
        i,
5649
        j = 0;
5650
    if (isString) {
5651
      element = wysihtml5.dom.getAsDom(elementOrHtml, context);
5652
    } else {
5653
      element = elementOrHtml;
5654
    }
5655
 
5656
    for (i in rules) {
5657
      matches       = element.querySelectorAll(i);
5658
      method        = rules[i];
5659
      matchesLength = matches.length;
5660
      for (; j<matchesLength; j++) {
5661
        method(matches[j]);
5662
      }
5663
    }
5664
 
5665
    matches = elementOrHtml = rules = null;
5666
 
5667
    return isString ? element.innerHTML : element;
5668
  }
5669
 
5670
  return cleanPastedHTML;
5671
})();/**
5672
 * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
5673
 *
5674
 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
5675
 * @exaple
5676
 *    wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
5677
 */
5678
(function(wysihtml5) {
5679
  var dom = wysihtml5.dom;
5680
 
5681
  wysihtml5.quirks.ensureProperClearing = (function() {
5682
    var clearIfNecessary = function(event) {
5683
      var element = this;
5684
      setTimeout(function() {
5685
        var innerHTML = element.innerHTML.toLowerCase();
5686
        if (innerHTML == "<p>&nbsp;</p>" ||
5687
            innerHTML == "<p>&nbsp;</p><p>&nbsp;</p>") {
5688
          element.innerHTML = "";
5689
        }
5690
      }, 0);
5691
    };
5692
 
5693
    return function(composer) {
5694
      dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
5695
    };
5696
  })();
5697
 
5698
 
5699
 
5700
  /**
5701
   * 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 backspace
5702
   *
5703
   * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
5704
   * @exaple
5705
   *    wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
5706
   */
5707
  wysihtml5.quirks.ensureProperClearingOfLists = (function() {
5708
    var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"];
5709
 
5710
    var clearIfNecessary = function(element, contentEditableElement) {
5711
      if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) {
5712
        return;
5713
      }
5714
 
5715
      var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI });
5716
      if (!list) {
5717
        return;
5718
      }
5719
 
5720
      var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild;
5721
      if (!listIsFirstChildOfContentEditable) {
5722
        return;
5723
      }
5724
 
5725
      var hasOnlyOneListItem = list.childNodes.length <= 1;
5726
      if (!hasOnlyOneListItem) {
5727
        return;
5728
      }
5729
 
5730
      var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true;
5731
      if (!onlyListItemIsEmpty) {
5732
        return;
5733
      }
5734
 
5735
      list.parentNode.removeChild(list);
5736
    };
5737
 
5738
    return function(composer) {
5739
      dom.observe(composer.element, "keydown", function(event) {
5740
        if (event.keyCode !== wysihtml5.BACKSPACE_KEY) {
5741
          return;
5742
        }
5743
 
5744
        var element = composer.selection.getSelectedNode();
5745
        clearIfNecessary(element, composer.element);
5746
      });
5747
    };
5748
  })();
5749
 
5750
})(wysihtml5);
5751
// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
5752
//
5753
// In Firefox this:
5754
//      var d = document.createElement("div");
5755
//      d.innerHTML ='<a href="~"></a>';
5756
//      d.innerHTML;
5757
// will result in:
5758
//      <a href="%7E"></a>
5759
// which is wrong
5760
(function(wysihtml5) {
5761
  var TILDE_ESCAPED = "%7E";
5762
  wysihtml5.quirks.getCorrectInnerHTML = function(element) {
5763
    var innerHTML = element.innerHTML;
5764
    if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
5765
      return innerHTML;
5766
    }
5767
 
5768
    var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
5769
        url,
5770
        urlToSearch,
5771
        length,
5772
        i;
5773
    for (i=0, length=elementsWithTilde.length; i<length; i++) {
5774
      url         = elementsWithTilde[i].href || elementsWithTilde[i].src;
5775
      urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED);
5776
      innerHTML   = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url);
5777
    }
5778
    return innerHTML;
5779
  };
5780
})(wysihtml5);/**
5781
 * Some browsers don't insert line breaks when hitting return in a contentEditable element
5782
 *    - Opera & IE insert new <p> on return
5783
 *    - Chrome & Safari insert new <div> on return
5784
 *    - Firefox inserts <br> on return (yippie!)
5785
 *
5786
 * @param {Element} element
5787
 *
5788
 * @example
5789
 *    wysihtml5.quirks.insertLineBreakOnReturn(element);
5790
 */
5791
(function(wysihtml5) {
5792
  var dom                                           = wysihtml5.dom,
5793
      USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS  = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"],
5794
      LIST_TAGS                                     = ["UL", "OL", "MENU"];
5795
 
5796
  wysihtml5.quirks.insertLineBreakOnReturn = function(composer) {
5797
    function unwrap(selectedNode) {
5798
      var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2);
5799
      if (!parentElement) {
5800
        return;
5801
      }
5802
 
5803
      var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE);
5804
      dom.insert(invisibleSpace).before(parentElement);
5805
      dom.replaceWithChildNodes(parentElement);
5806
      composer.selection.selectNode(invisibleSpace);
5807
    }
5808
 
5809
    function keyDown(event) {
5810
      var keyCode = event.keyCode;
5811
      if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) {
5812
        return;
5813
      }
5814
 
5815
      var element         = event.target,
5816
          selectedNode    = composer.selection.getSelectedNode(),
5817
          blockElement    = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4);
5818
      if (blockElement) {
5819
        // Some browsers create <p> elements after leaving a list
5820
        // check after keydown of backspace and return whether a <p> got inserted and unwrap it
5821
        if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) {
5822
          setTimeout(function() {
5823
            var selectedNode = composer.selection.getSelectedNode(),
5824
                list,
5825
                div;
5826
            if (!selectedNode) {
5827
              return;
5828
            }
5829
 
5830
            list = dom.getParentElement(selectedNode, {
5831
              nodeName: LIST_TAGS
5832
            }, 2);
5833
 
5834
            if (list) {
5835
              return;
5836
            }
5837
 
5838
            unwrap(selectedNode);
5839
          }, 0);
5840
        } else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) {
5841
          setTimeout(function() {
5842
            unwrap(composer.selection.getSelectedNode());
5843
          }, 0);
5844
        } 
5845
        return;
5846
      }
5847
 
5848
      if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) {
5849
        composer.commands.exec("insertLineBreak");
5850
        event.preventDefault();
5851
      }
5852
    }
5853
 
5854
    // keypress doesn't fire when you hit backspace
5855
    dom.observe(composer.element.ownerDocument, "keydown", keyDown);
5856
  };
5857
})(wysihtml5);/**
5858
 * Force rerendering of a given element
5859
 * Needed to fix display misbehaviors of IE
5860
 *
5861
 * @param {Element} element The element object which needs to be rerendered
5862
 * @example
5863
 *    wysihtml5.quirks.redraw(document.body);
5864
 */
5865
(function(wysihtml5) {
5866
  var CLASS_NAME = "wysihtml5-quirks-redraw";
5867
 
5868
  wysihtml5.quirks.redraw = function(element) {
5869
    wysihtml5.dom.addClass(element, CLASS_NAME);
5870
    wysihtml5.dom.removeClass(element, CLASS_NAME);
5871
 
5872
    // Following hack is needed for firefox to make sure that image resize handles are properly removed
5873
    try {
5874
      var doc = element.ownerDocument;
5875
      doc.execCommand("italic", false, null);
5876
      doc.execCommand("italic", false, null);
5877
    } catch(e) {}
5878
  };
5879
})(wysihtml5);/**
5880
 * Selection API
5881
 *
5882
 * @example
5883
 *    var selection = new wysihtml5.Selection(editor);
5884
 */
5885
(function(wysihtml5) {
5886
  var dom = wysihtml5.dom;
5887
 
5888
  function _getCumulativeOffsetTop(element) {
5889
    var top = 0;
5890
    if (element.parentNode) {
5891
      do {
5892
        top += element.offsetTop || 0;
5893
        element = element.offsetParent;
5894
      } while (element);
5895
    }
5896
    return top;
5897
  }
5898
 
5899
  wysihtml5.Selection = Base.extend(
5900
    /** @scope wysihtml5.Selection.prototype */ {
5901
    constructor: function(editor) {
5902
      // Make sure that our external range library is initialized
5903
      window.rangy.init();
5904
 
5905
      this.editor   = editor;
5906
      this.composer = editor.composer;
5907
      this.doc      = this.composer.doc;
5908
    },
5909
 
5910
    /**
5911
     * Get the current selection as a bookmark to be able to later restore it
5912
     *
5913
     * @return {Object} An object that represents the current selection
5914
     */
5915
    getBookmark: function() {
5916
      var range = this.getRange();
5917
      return range && range.cloneRange();
5918
    },
5919
 
5920
    /**
5921
     * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark
5922
     *
5923
     * @param {Object} bookmark An object that represents the current selection
5924
     */
5925
    setBookmark: function(bookmark) {
5926
      if (!bookmark) {
5927
        return;
5928
      }
5929
 
5930
      this.setSelection(bookmark);
5931
    },
5932
 
5933
    /**
5934
     * Set the caret in front of the given node
5935
     *
5936
     * @param {Object} node The element or text node where to position the caret in front of
5937
     * @example
5938
     *    selection.setBefore(myElement);
5939
     */
5940
    setBefore: function(node) {
5941
      var range = rangy.createRange(this.doc);
5942
      range.setStartBefore(node);
5943
      range.setEndBefore(node);
5944
      return this.setSelection(range);
5945
    },
5946
 
5947
    /**
5948
     * Set the caret after the given node
5949
     *
5950
     * @param {Object} node The element or text node where to position the caret in front of
5951
     * @example
5952
     *    selection.setBefore(myElement);
5953
     */
5954
    setAfter: function(node) {
5955
      var range = rangy.createRange(this.doc);
5956
      range.setStartAfter(node);
5957
      range.setEndAfter(node);
5958
      return this.setSelection(range);
5959
    },
5960
 
5961
    /**
5962
     * Ability to select/mark nodes
5963
     *
5964
     * @param {Element} node The node/element to select
5965
     * @example
5966
     *    selection.selectNode(document.getElementById("my-image"));
5967
     */
5968
    selectNode: function(node) {
5969
      var range           = rangy.createRange(this.doc),
5970
          isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
5971
          canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
5972
          content         = isElement ? node.innerHTML : node.data,
5973
          isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE),
5974
          displayStyle    = dom.getStyle("display").from(node),
5975
          isBlockElement  = (displayStyle === "block" || displayStyle === "list-item");
5976
 
5977
      if (isEmpty && isElement && canHaveHTML) {
5978
        // Make sure that caret is visible in node by inserting a zero width no breaking space
5979
        try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
5980
      }
5981
 
5982
      if (canHaveHTML) {
5983
        range.selectNodeContents(node);
5984
      } else {
5985
        range.selectNode(node);
5986
      }
5987
 
5988
      if (canHaveHTML && isEmpty && isElement) {
5989
        range.collapse(isBlockElement);
5990
      } else if (canHaveHTML && isEmpty) {
5991
        range.setStartAfter(node);
5992
        range.setEndAfter(node);
5993
      }
5994
 
5995
      this.setSelection(range);
5996
    },
5997
 
5998
    /**
5999
     * Get the node which contains the selection
6000
     *
6001
     * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
6002
     * @return {Object} The node that contains the caret
6003
     * @example
6004
     *    var nodeThatContainsCaret = selection.getSelectedNode();
6005
     */
6006
    getSelectedNode: function(controlRange) {
6007
      var selection,
6008
          range;
6009
 
6010
      if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
6011
        range = this.doc.selection.createRange();
6012
        if (range && range.length) {
6013
          return range.item(0);
6014
        }
6015
      }
6016
 
6017
      selection = this.getSelection(this.doc);
6018
      if (selection.focusNode === selection.anchorNode) {
6019
        return selection.focusNode;
6020
      } else {
6021
        range = this.getRange(this.doc);
6022
        return range ? range.commonAncestorContainer : this.doc.body;
6023
      }
6024
    },
6025
 
6026
    executeAndRestore: function(method, restoreScrollPosition) {
6027
      var body                  = this.doc.body,
6028
          oldScrollTop          = restoreScrollPosition && body.scrollTop,
6029
          oldScrollLeft         = restoreScrollPosition && body.scrollLeft,
6030
          className             = "_wysihtml5-temp-placeholder",
6031
          placeholderHTML       = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>',
6032
          range                 = this.getRange(this.doc),
6033
          newRange;
6034
 
6035
      // Nothing selected, execute and say goodbye
6036
      if (!range) {
6037
        method(body, body);
6038
        return;
6039
      }
6040
 
6041
      var node = range.createContextualFragment(placeholderHTML);
6042
      range.insertNode(node);
6043
 
6044
      // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
6045
      try {
6046
        method(range.startContainer, range.endContainer);
6047
      } catch(e3) {
6048
        setTimeout(function() { throw e3; }, 0);
6049
      }
6050
 
6051
      caretPlaceholder = this.doc.querySelector("." + className);
6052
      if (caretPlaceholder) {
6053
        newRange = rangy.createRange(this.doc);
6054
        newRange.selectNode(caretPlaceholder);
6055
        newRange.deleteContents();
6056
        this.setSelection(newRange);
6057
      } else {
6058
        // fallback for when all hell breaks loose
6059
        body.focus();
6060
      }
6061
 
6062
      if (restoreScrollPosition) {
6063
        body.scrollTop  = oldScrollTop;
6064
        body.scrollLeft = oldScrollLeft;
6065
      }
6066
 
6067
      // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
6068
      try {
6069
        caretPlaceholder.parentNode.removeChild(caretPlaceholder);
6070
      } catch(e4) {}
6071
    },
6072
 
6073
    /**
6074
     * Different approach of preserving the selection (doesn't modify the dom)
6075
     * Takes all text nodes in the selection and saves the selection position in the first and last one
6076
     */
6077
    executeAndRestoreSimple: function(method) {
6078
      var range = this.getRange(),
6079
          body  = this.doc.body,
6080
          newRange,
6081
          firstNode,
6082
          lastNode,
6083
          textNodes,
6084
          rangeBackup;
6085
 
6086
      // Nothing selected, execute and say goodbye
6087
      if (!range) {
6088
        method(body, body);
6089
        return;
6090
      }
6091
 
6092
      textNodes = range.getNodes([3]);
6093
      firstNode = textNodes[0] || range.startContainer;
6094
      lastNode  = textNodes[textNodes.length - 1] || range.endContainer;
6095
 
6096
      rangeBackup = {
6097
        collapsed:      range.collapsed,
6098
        startContainer: firstNode,
6099
        startOffset:    firstNode === range.startContainer ? range.startOffset : 0,
6100
        endContainer:   lastNode,
6101
        endOffset:      lastNode === range.endContainer ? range.endOffset : lastNode.length
6102
      };
6103
 
6104
      try {
6105
        method(range.startContainer, range.endContainer);
6106
      } catch(e) {
6107
        setTimeout(function() { throw e; }, 0);
6108
      }
6109
 
6110
      newRange = rangy.createRange(this.doc);
6111
      try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {}
6112
      try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {}
6113
      try { this.setSelection(newRange); } catch(e3) {}
6114
    },
6115
 
6116
    /**
6117
     * Insert html at the caret position and move the cursor after the inserted html
6118
     *
6119
     * @param {String} html HTML string to insert
6120
     * @example
6121
     *    selection.insertHTML("<p>foobar</p>");
6122
     */
6123
    insertHTML: function(html) {
6124
      var range     = rangy.createRange(this.doc),
6125
          node      = range.createContextualFragment(html),
6126
          lastChild = node.lastChild;
6127
      this.insertNode(node);
6128
      if (lastChild) {
6129
        this.setAfter(lastChild);
6130
      }
6131
    },
6132
 
6133
    /**
6134
     * Insert a node at the caret position and move the cursor behind it
6135
     *
6136
     * @param {Object} node HTML string to insert
6137
     * @example
6138
     *    selection.insertNode(document.createTextNode("foobar"));
6139
     */
6140
    insertNode: function(node) {
6141
      var range = this.getRange();
6142
      if (range) {
6143
        range.insertNode(node);
6144
      }
6145
    },
6146
 
6147
    /**
6148
     * Wraps current selection with the given node
6149
     *
6150
     * @param {Object} node The node to surround the selected elements with
6151
     */
6152
    surround: function(node) {
6153
      var range = this.getRange();
6154
      if (!range) {
6155
        return;
6156
      }
6157
 
6158
      try {
6159
        // This only works when the range boundaries are not overlapping other elements
6160
        range.surroundContents(node);
6161
        this.selectNode(node);
6162
      } catch(e) {
6163
        // fallback
6164
        node.appendChild(range.extractContents());
6165
        range.insertNode(node);
6166
      }
6167
    },
6168
 
6169
    /**
6170
     * Scroll the current caret position into the view
6171
     * FIXME: This is a bit hacky, there might be a smarter way of doing this
6172
     *
6173
     * @example
6174
     *    selection.scrollIntoView();
6175
     */
6176
    scrollIntoView: function() {
6177
      var doc           = this.doc,
6178
          hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
6179
          tempElement   = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() {
6180
            var element = doc.createElement("span");
6181
            // The element needs content in order to be able to calculate it's position properly
6182
            element.innerHTML = wysihtml5.INVISIBLE_SPACE;
6183
            return element;
6184
          })(),
6185
          offsetTop;
6186
 
6187
      if (hasScrollBars) {
6188
        this.insertNode(tempElement);
6189
        offsetTop = _getCumulativeOffsetTop(tempElement);
6190
        tempElement.parentNode.removeChild(tempElement);
6191
        if (offsetTop > doc.body.scrollTop) {
6192
          doc.body.scrollTop = offsetTop;
6193
        }
6194
      }
6195
    },
6196
 
6197
    /**
6198
     * Select line where the caret is in
6199
     */
6200
    selectLine: function() {
6201
      if (wysihtml5.browser.supportsSelectionModify()) {
6202
        this._selectLine_W3C();
6203
      } else if (this.doc.selection) {
6204
        this._selectLine_MSIE();
6205
      }
6206
    },
6207
 
6208
    /**
6209
     * See https://developer.mozilla.org/en/DOM/Selection/modify
6210
     */
6211
    _selectLine_W3C: function() {
6212
      var win       = this.doc.defaultView,
6213
          selection = win.getSelection();
6214
      selection.modify("extend", "left", "lineboundary");
6215
      selection.modify("extend", "right", "lineboundary");
6216
    },
6217
 
6218
    _selectLine_MSIE: function() {
6219
      var range       = this.doc.selection.createRange(),
6220
          rangeTop    = range.boundingTop,
6221
          rangeHeight = range.boundingHeight,
6222
          scrollWidth = this.doc.body.scrollWidth,
6223
          rangeBottom,
6224
          rangeEnd,
6225
          measureNode,
6226
          i,
6227
          j;
6228
 
6229
      if (!range.moveToPoint) {
6230
        return;
6231
      }
6232
 
6233
      if (rangeTop === 0) {
6234
        // Don't know why, but when the selection ends at the end of a line
6235
        // range.boundingTop is 0
6236
        measureNode = this.doc.createElement("span");
6237
        this.insertNode(measureNode);
6238
        rangeTop = measureNode.offsetTop;
6239
        measureNode.parentNode.removeChild(measureNode);
6240
      }
6241
 
6242
      rangeTop += 1;
6243
 
6244
      for (i=-10; i<scrollWidth; i+=2) {
6245
        try {
6246
          range.moveToPoint(i, rangeTop);
6247
          break;
6248
        } catch(e1) {}
6249
      }
6250
 
6251
      // Investigate the following in order to handle multi line selections
6252
      // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);
6253
      rangeBottom = rangeTop;
6254
      rangeEnd = this.doc.selection.createRange();
6255
      for (j=scrollWidth; j>=0; j--) {
6256
        try {
6257
          rangeEnd.moveToPoint(j, rangeBottom);
6258
          break;
6259
        } catch(e2) {}
6260
      }
6261
 
6262
      range.setEndPoint("EndToEnd", rangeEnd);
6263
      range.select();
6264
    },
6265
 
6266
    getText: function() {
6267
      var selection = this.getSelection();
6268
      return selection ? selection.toString() : "";
6269
    },
6270
 
6271
    getNodes: function(nodeType, filter) {
6272
      var range = this.getRange();
6273
      if (range) {
6274
        return range.getNodes([nodeType], filter);
6275
      } else {
6276
        return [];
6277
      }
6278
    },
6279
 
6280
    getRange: function() {
6281
      var selection = this.getSelection();
6282
      return selection && selection.rangeCount && selection.getRangeAt(0);
6283
    },
6284
 
6285
    getSelection: function() {
6286
      return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);
6287
    },
6288
 
6289
    setSelection: function(range) {
6290
      var win       = this.doc.defaultView || this.doc.parentWindow,
6291
          selection = rangy.getSelection(win);
6292
      return selection.setSingleRange(range);
6293
    }
6294
  });
6295
 
6296
})(wysihtml5);
6297
/**
6298
 * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license.
6299
 * http://code.google.com/p/rangy/
6300
 *
6301
 * changed in order to be able ...
6302
 *    - to use custom tags
6303
 *    - to detect and replace similar css classes via reg exp
6304
 */
6305
(function(wysihtml5, rangy) {
6306
  var defaultTagName = "span";
6307
 
6308
  var REG_EXP_WHITE_SPACE = /\s+/g;
6309
 
6310
  function hasClass(el, cssClass, regExp) {
6311
    if (!el.className) {
6312
      return false;
6313
    }
6314
 
6315
    var matchingClassNames = el.className.match(regExp) || [];
6316
    return matchingClassNames[matchingClassNames.length - 1] === cssClass;
6317
  }
6318
 
6319
  function addClass(el, cssClass, regExp) {
6320
    if (el.className) {
6321
      removeClass(el, regExp);
6322
      el.className += " " + cssClass;
6323
    } else {
6324
      el.className = cssClass;
6325
    }
6326
  }
6327
 
6328
  function removeClass(el, regExp) {
6329
    if (el.className) {
6330
      el.className = el.className.replace(regExp, "");
6331
    }
6332
  }
6333
 
6334
  function hasSameClasses(el1, el2) {
6335
    return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");
6336
  }
6337
 
6338
  function replaceWithOwnChildren(el) {
6339
    var parent = el.parentNode;
6340
    while (el.firstChild) {
6341
      parent.insertBefore(el.firstChild, el);
6342
    }
6343
    parent.removeChild(el);
6344
  }
6345
 
6346
  function elementsHaveSameNonClassAttributes(el1, el2) {
6347
    if (el1.attributes.length != el2.attributes.length) {
6348
      return false;
6349
    }
6350
    for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
6351
      attr1 = el1.attributes[i];
6352
      name = attr1.name;
6353
      if (name != "class") {
6354
        attr2 = el2.attributes.getNamedItem(name);
6355
        if (attr1.specified != attr2.specified) {
6356
          return false;
6357
        }
6358
        if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
6359
          return false;
6360
        }
6361
      }
6362
    }
6363
    return true;
6364
  }
6365
 
6366
  function isSplitPoint(node, offset) {
6367
    if (rangy.dom.isCharacterDataNode(node)) {
6368
      if (offset == 0) {
6369
        return !!node.previousSibling;
6370
      } else if (offset == node.length) {
6371
        return !!node.nextSibling;
6372
      } else {
6373
        return true;
6374
      }
6375
    }
6376
 
6377
    return offset > 0 && offset < node.childNodes.length;
6378
  }
6379
 
6380
  function splitNodeAt(node, descendantNode, descendantOffset) {
6381
    var newNode;
6382
    if (rangy.dom.isCharacterDataNode(descendantNode)) {
6383
      if (descendantOffset == 0) {
6384
        descendantOffset = rangy.dom.getNodeIndex(descendantNode);
6385
        descendantNode = descendantNode.parentNode;
6386
      } else if (descendantOffset == descendantNode.length) {
6387
        descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;
6388
        descendantNode = descendantNode.parentNode;
6389
      } else {
6390
        newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);
6391
      }
6392
    }
6393
    if (!newNode) {
6394
      newNode = descendantNode.cloneNode(false);
6395
      if (newNode.id) {
6396
        newNode.removeAttribute("id");
6397
      }
6398
      var child;
6399
      while ((child = descendantNode.childNodes[descendantOffset])) {
6400
        newNode.appendChild(child);
6401
      }
6402
      rangy.dom.insertAfter(newNode, descendantNode);
6403
    }
6404
    return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode));
6405
  }
6406
 
6407
  function Merge(firstNode) {
6408
    this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
6409
    this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
6410
    this.textNodes = [this.firstTextNode];
6411
  }
6412
 
6413
  Merge.prototype = {
6414
    doMerge: function() {
6415
      var textBits = [], textNode, parent, text;
6416
      for (var i = 0, len = this.textNodes.length; i < len; ++i) {
6417
        textNode = this.textNodes[i];
6418
        parent = textNode.parentNode;
6419
        textBits[i] = textNode.data;
6420
        if (i) {
6421
          parent.removeChild(textNode);
6422
          if (!parent.hasChildNodes()) {
6423
            parent.parentNode.removeChild(parent);
6424
          }
6425
        }
6426
      }
6427
      this.firstTextNode.data = text = textBits.join("");
6428
      return text;
6429
    },
6430
 
6431
    getLength: function() {
6432
      var i = this.textNodes.length, len = 0;
6433
      while (i--) {
6434
        len += this.textNodes[i].length;
6435
      }
6436
      return len;
6437
    },
6438
 
6439
    toString: function() {
6440
      var textBits = [];
6441
      for (var i = 0, len = this.textNodes.length; i < len; ++i) {
6442
        textBits[i] = "'" + this.textNodes[i].data + "'";
6443
      }
6444
      return "[Merge(" + textBits.join(",") + ")]";
6445
    }
6446
  };
6447
 
6448
  function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) {
6449
    this.tagNames = tagNames || [defaultTagName];
6450
    this.cssClass = cssClass || "";
6451
    this.similarClassRegExp = similarClassRegExp;
6452
    this.normalize = normalize;
6453
    this.applyToAnyTagName = false;
6454
  }
6455
 
6456
  HTMLApplier.prototype = {
6457
    getAncestorWithClass: function(node) {
6458
      var cssClassMatch;
6459
      while (node) {
6460
        cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true;
6461
        if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {
6462
          return node;
6463
        }
6464
        node = node.parentNode;
6465
      }
6466
      return false;
6467
    },
6468
 
6469
    // Normalizes nodes after applying a CSS class to a Range.
6470
    postApply: function(textNodes, range) {
6471
      var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
6472
 
6473
      var merges = [], currentMerge;
6474
 
6475
      var rangeStartNode = firstNode, rangeEndNode = lastNode;
6476
      var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
6477
 
6478
      var textNode, precedingTextNode;
6479
 
6480
      for (var i = 0, len = textNodes.length; i < len; ++i) {
6481
        textNode = textNodes[i];
6482
        precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false);
6483
        if (precedingTextNode) {
6484
          if (!currentMerge) {
6485
            currentMerge = new Merge(precedingTextNode);
6486
            merges.push(currentMerge);
6487
          }
6488
          currentMerge.textNodes.push(textNode);
6489
          if (textNode === firstNode) {
6490
            rangeStartNode = currentMerge.firstTextNode;
6491
            rangeStartOffset = rangeStartNode.length;
6492
          }
6493
          if (textNode === lastNode) {
6494
            rangeEndNode = currentMerge.firstTextNode;
6495
            rangeEndOffset = currentMerge.getLength();
6496
          }
6497
        } else {
6498
          currentMerge = null;
6499
        }
6500
      }
6501
 
6502
      // Test whether the first node after the range needs merging
6503
      var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
6504
      if (nextTextNode) {
6505
        if (!currentMerge) {
6506
          currentMerge = new Merge(lastNode);
6507
          merges.push(currentMerge);
6508
        }
6509
        currentMerge.textNodes.push(nextTextNode);
6510
      }
6511
 
6512
      // Do the merges
6513
      if (merges.length) {
6514
        for (i = 0, len = merges.length; i < len; ++i) {
6515
          merges[i].doMerge();
6516
        }
6517
        // Set the range boundaries
6518
        range.setStart(rangeStartNode, rangeStartOffset);
6519
        range.setEnd(rangeEndNode, rangeEndOffset);
6520
      }
6521
    },
6522
 
6523
    getAdjacentMergeableTextNode: function(node, forward) {
6524
        var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
6525
        var el = isTextNode ? node.parentNode : node;
6526
        var adjacentNode;
6527
        var propName = forward ? "nextSibling" : "previousSibling";
6528
        if (isTextNode) {
6529
          // Can merge if the node's previous/next sibling is a text node
6530
          adjacentNode = node[propName];
6531
          if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
6532
            return adjacentNode;
6533
          }
6534
        } else {
6535
          // Compare element with its sibling
6536
          adjacentNode = el[propName];
6537
          if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
6538
            return adjacentNode[forward ? "firstChild" : "lastChild"];
6539
          }
6540
        }
6541
        return null;
6542
    },
6543
 
6544
    areElementsMergeable: function(el1, el2) {
6545
      return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
6546
        && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
6547
        && hasSameClasses(el1, el2)
6548
        && elementsHaveSameNonClassAttributes(el1, el2);
6549
    },
6550
 
6551
    createContainer: function(doc) {
6552
      var el = doc.createElement(this.tagNames[0]);
6553
      if (this.cssClass) {
6554
        el.className = this.cssClass;
6555
      }
6556
      return el;
6557
    },
6558
 
6559
    applyToTextNode: function(textNode) {
6560
      var parent = textNode.parentNode;
6561
      if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
6562
        if (this.cssClass) {
6563
          addClass(parent, this.cssClass, this.similarClassRegExp);
6564
        }
6565
      } else {
6566
        var el = this.createContainer(rangy.dom.getDocument(textNode));
6567
        textNode.parentNode.insertBefore(el, textNode);
6568
        el.appendChild(textNode);
6569
      }
6570
    },
6571
 
6572
    isRemovable: function(el) {
6573
      return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass;
6574
    },
6575
 
6576
    undoToTextNode: function(textNode, range, ancestorWithClass) {
6577
      if (!range.containsNode(ancestorWithClass)) {
6578
        // Split out the portion of the ancestor from which we can remove the CSS class
6579
        var ancestorRange = range.cloneRange();
6580
        ancestorRange.selectNode(ancestorWithClass);
6581
 
6582
        if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) {
6583
          splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset);
6584
          range.setEndAfter(ancestorWithClass);
6585
        }
6586
        if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
6587
          ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset);
6588
        }
6589
      }
6590
 
6591
      if (this.similarClassRegExp) {
6592
        removeClass(ancestorWithClass, this.similarClassRegExp);
6593
      }
6594
      if (this.isRemovable(ancestorWithClass)) {
6595
        replaceWithOwnChildren(ancestorWithClass);
6596
      }
6597
    },
6598
 
6599
    applyToRange: function(range) {
6600
        var textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6601
        if (!textNodes.length) {
6602
          try {
6603
            var node = this.createContainer(range.endContainer.ownerDocument);
6604
            range.surroundContents(node);
6605
            this.selectNode(range, node);
6606
            return;
6607
          } catch(e) {}
6608
        }
6609
 
6610
        range.splitBoundaries();
6611
        textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6612
 
6613
        if (textNodes.length) {
6614
          var textNode;
6615
 
6616
          for (var i = 0, len = textNodes.length; i < len; ++i) {
6617
            textNode = textNodes[i];
6618
            if (!this.getAncestorWithClass(textNode)) {
6619
              this.applyToTextNode(textNode);
6620
            }
6621
          }
6622
 
6623
          range.setStart(textNodes[0], 0);
6624
          textNode = textNodes[textNodes.length - 1];
6625
          range.setEnd(textNode, textNode.length);
6626
 
6627
          if (this.normalize) {
6628
            this.postApply(textNodes, range);
6629
          }
6630
        }
6631
    },
6632
 
6633
    undoToRange: function(range) {
6634
      var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass;
6635
      if (textNodes.length) {
6636
        range.splitBoundaries();
6637
        textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6638
      } else {
6639
        var doc = range.endContainer.ownerDocument,
6640
            node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
6641
        range.insertNode(node);
6642
        range.selectNode(node);
6643
        textNodes = [node];
6644
      }
6645
 
6646
      for (var i = 0, len = textNodes.length; i < len; ++i) {
6647
        textNode = textNodes[i];
6648
        ancestorWithClass = this.getAncestorWithClass(textNode);
6649
        if (ancestorWithClass) {
6650
          this.undoToTextNode(textNode, range, ancestorWithClass);
6651
        }
6652
      }
6653
 
6654
      if (len == 1) {
6655
        this.selectNode(range, textNodes[0]);
6656
      } else {
6657
        range.setStart(textNodes[0], 0);
6658
        textNode = textNodes[textNodes.length - 1];
6659
        range.setEnd(textNode, textNode.length);
6660
 
6661
        if (this.normalize) {
6662
          this.postApply(textNodes, range);
6663
        }
6664
      }
6665
    },
6666
 
6667
    selectNode: function(range, node) {
6668
      var isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
6669
          canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : true,
6670
          content         = isElement ? node.innerHTML : node.data,
6671
          isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE);
6672
 
6673
      if (isEmpty && isElement && canHaveHTML) {
6674
        // Make sure that caret is visible in node by inserting a zero width no breaking space
6675
        try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
6676
      }
6677
      range.selectNodeContents(node);
6678
      if (isEmpty && isElement) {
6679
        range.collapse(false);
6680
      } else if (isEmpty) {
6681
        range.setStartAfter(node);
6682
        range.setEndAfter(node);
6683
      }
6684
    },
6685
 
6686
    getTextSelectedByRange: function(textNode, range) {
6687
      var textRange = range.cloneRange();
6688
      textRange.selectNodeContents(textNode);
6689
 
6690
      var intersectionRange = textRange.intersection(range);
6691
      var text = intersectionRange ? intersectionRange.toString() : "";
6692
      textRange.detach();
6693
 
6694
      return text;
6695
    },
6696
 
6697
    isAppliedToRange: function(range) {
6698
      var ancestors = [],
6699
          ancestor,
6700
          textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6701
      if (!textNodes.length) {
6702
        ancestor = this.getAncestorWithClass(range.startContainer);
6703
        return ancestor ? [ancestor] : false;
6704
      }
6705
 
6706
      for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
6707
        selectedText = this.getTextSelectedByRange(textNodes[i], range);
6708
        ancestor = this.getAncestorWithClass(textNodes[i]);
6709
        if (selectedText != "" && !ancestor) {
6710
          return false;
6711
        } else {
6712
          ancestors.push(ancestor);
6713
        }
6714
      }
6715
      return ancestors;
6716
    },
6717
 
6718
    toggleRange: function(range) {
6719
      if (this.isAppliedToRange(range)) {
6720
        this.undoToRange(range);
6721
      } else {
6722
        this.applyToRange(range);
6723
      }
6724
    }
6725
  };
6726
 
6727
  wysihtml5.selection.HTMLApplier = HTMLApplier;
6728
 
6729
})(wysihtml5, rangy);/**
6730
 * Rich Text Query/Formatting Commands
6731
 * 
6732
 * @example
6733
 *    var commands = new wysihtml5.Commands(editor);
6734
 */
6735
wysihtml5.Commands = Base.extend(
6736
  /** @scope wysihtml5.Commands.prototype */ {
6737
  constructor: function(editor) {
6738
    this.editor   = editor;
6739
    this.composer = editor.composer;
6740
    this.doc      = this.composer.doc;
6741
  },
6742
 
6743
  /**
6744
   * Check whether the browser supports the given command
6745
   *
6746
   * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
6747
   * @example
6748
   *    commands.supports("createLink");
6749
   */
6750
  support: function(command) {
6751
    return wysihtml5.browser.supportsCommand(this.doc, command);
6752
  },
6753
 
6754
  /**
6755
   * Check whether the browser supports the given command
6756
   *
6757
   * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
6758
   * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
6759
   * @example
6760
   *    commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
6761
   */
6762
  exec: function(command, value) {
6763
    var obj     = wysihtml5.commands[command],
6764
        method  = obj && obj.exec;
6765
 
6766
    this.editor.fire("beforecommand:composer");
6767
 
6768
    if (method) {
6769
      return method.call(obj, this.composer, command, value);
6770
    } else {
6771
      try {
6772
        // try/catch for buggy firefox
6773
        return this.doc.execCommand(command, false, value);
6774
      } catch(e) {}
6775
    }
6776
 
6777
    this.editor.fire("aftercommand:composer");
6778
  },
6779
 
6780
  /**
6781
   * Check whether the current command is active
6782
   * If the caret is within a bold text, then calling this with command "bold" should return true
6783
   *
6784
   * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
6785
   * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
6786
   * @return {Boolean} Whether the command is active
6787
   * @example
6788
   *    var isCurrentSelectionBold = commands.state("bold");
6789
   */
6790
  state: function(command, commandValue) {
6791
    var obj     = wysihtml5.commands[command],
6792
        method  = obj && obj.state;
6793
    if (method) {
6794
      return method.call(obj, this.composer, command, commandValue);
6795
    } else {
6796
      try {
6797
        // try/catch for buggy firefox
6798
        return this.doc.queryCommandState(command);
6799
      } catch(e) {
6800
        return false;
6801
      }
6802
    }
6803
  },
6804
 
6805
  /**
6806
   * Get the current command's value
6807
   *
6808
   * @param {String} command The command string which to check (eg. "formatBlock")
6809
   * @return {String} The command value
6810
   * @example
6811
   *    var currentBlockElement = commands.value("formatBlock");
6812
   */
6813
  value: function(command) {
6814
    var obj     = wysihtml5.commands[command],
6815
        method  = obj && obj.value;
6816
    if (method) {
6817
      return method.call(obj, this.composer, command);
6818
    } else {
6819
      try {
6820
        // try/catch for buggy firefox
6821
        return this.doc.queryCommandValue(command);
6822
      } catch(e) {
6823
        return null;
6824
      }
6825
    }
6826
  }
6827
});(function(wysihtml5) {
6828
  var undef;
6829
 
6830
  wysihtml5.commands.bold = {
6831
    exec: function(composer, command) {
6832
      return wysihtml5.commands.formatInline.exec(composer, command, "b");
6833
    },
6834
 
6835
    state: function(composer, command, color) {
6836
      // element.ownerDocument.queryCommandState("bold") results:
6837
      // firefox: only <b>
6838
      // chrome:  <b>, <strong>, <h1>, <h2>, ...
6839
      // ie:      <b>, <strong>
6840
      // opera:   <b>, <strong>
6841
      return wysihtml5.commands.formatInline.state(composer, command, "b");
6842
    },
6843
 
6844
    value: function() {
6845
      return undef;
6846
    }
6847
  };
6848
})(wysihtml5);
6849
 
6850
(function(wysihtml5) {
6851
  var undef,
6852
      NODE_NAME = "A",
6853
      dom       = wysihtml5.dom;
6854
 
6855
  function _removeFormat(composer, anchors) {
6856
    var length  = anchors.length,
6857
        i       = 0,
6858
        anchor,
6859
        codeElement,
6860
        textContent;
6861
    for (; i<length; i++) {
6862
      anchor      = anchors[i];
6863
      codeElement = dom.getParentElement(anchor, { nodeName: "code" });
6864
      textContent = dom.getTextContent(anchor);
6865
 
6866
      // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking
6867
      // else replace <a> with its childNodes
6868
      if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {
6869
        // <code> element is used to prevent later auto-linking of the content
6870
        codeElement = dom.renameElement(anchor, "code");
6871
      } else {
6872
        dom.replaceWithChildNodes(anchor);
6873
      }
6874
    }
6875
  }
6876
 
6877
  function _format(composer, attributes) {
6878
    var doc             = composer.doc,
6879
        tempClass       = "_wysihtml5-temp-" + (+new Date()),
6880
        tempClassRegExp = /non-matching-class/g,
6881
        i               = 0,
6882
        length,
6883
        anchors,
6884
        anchor,
6885
        hasElementChild,
6886
        isEmpty,
6887
        elementToSetCaretAfter,
6888
        textContent,
6889
        whiteSpace,
6890
        j;
6891
    wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp);
6892
    anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);
6893
    length  = anchors.length;
6894
    for (; i<length; i++) {
6895
      anchor = anchors[i];
6896
      anchor.removeAttribute("class");
6897
      for (j in attributes) {
6898
        anchor.setAttribute(j, attributes[j]);
6899
      }
6900
    }
6901
 
6902
    elementToSetCaretAfter = anchor;
6903
    if (length === 1) {
6904
      textContent = dom.getTextContent(anchor);
6905
      hasElementChild = !!anchor.querySelector("*");
6906
      isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE;
6907
      if (!hasElementChild && isEmpty) {
6908
        dom.setTextContent(anchor, anchor.href);
6909
        whiteSpace = doc.createTextNode(" ");
6910
        composer.selection.setAfter(anchor);
6911
        composer.selection.insertNode(whiteSpace);
6912
        elementToSetCaretAfter = whiteSpace;
6913
      }
6914
    }
6915
    composer.selection.setAfter(elementToSetCaretAfter);
6916
  }
6917
 
6918
  wysihtml5.commands.createLink = {
6919
    /**
6920
     * TODO: Use HTMLApplier or formatInline here
6921
     *
6922
     * Turns selection into a link
6923
     * If selection is already a link, it removes the link and wraps it with a <code> element
6924
     * The <code> element is needed to avoid auto linking
6925
     * 
6926
     * @example
6927
     *    // either ...
6928
     *    wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de");
6929
     *    // ... or ...
6930
     *    wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" });
6931
     */
6932
    exec: function(composer, command, value) {
6933
      var anchors = this.state(composer, command);
6934
      if (anchors) {
6935
        // Selection contains links
6936
        composer.selection.executeAndRestore(function() {
6937
          _removeFormat(composer, anchors);
6938
        });
6939
      } else {
6940
        // Create links
6941
        value = typeof(value) === "object" ? value : { href: value };
6942
        _format(composer, value);
6943
      }
6944
    },
6945
 
6946
    state: function(composer, command) {
6947
      return wysihtml5.commands.formatInline.state(composer, command, "A");
6948
    },
6949
 
6950
    value: function() {
6951
      return undef;
6952
    }
6953
  };
6954
})(wysihtml5);/**
6955
 * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags
6956
 * which we don't want
6957
 * Instead we set a css class
6958
 */
6959
(function(wysihtml5) {
6960
  var undef,
6961
      REG_EXP = /wysiwyg-font-size-[a-z]+/g;
6962
 
6963
  wysihtml5.commands.fontSize = {
6964
    exec: function(composer, command, size) {
6965
      return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
6966
    },
6967
 
6968
    state: function(composer, command, size) {
6969
      return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
6970
    },
6971
 
6972
    value: function() {
6973
      return undef;
6974
    }
6975
  };
6976
})(wysihtml5);
6977
/**
6978
 * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
6979
 * which we don't want
6980
 * Instead we set a css class
6981
 */
6982
(function(wysihtml5) {
6983
  var undef,
6984
      REG_EXP = /wysiwyg-color-[a-z]+/g;
6985
 
6986
  wysihtml5.commands.foreColor = {
6987
    exec: function(composer, command, color) {
6988
      return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
6989
    },
6990
 
6991
    state: function(composer, command, color) {
6992
      return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
6993
    },
6994
 
6995
    value: function() {
6996
      return undef;
6997
    }
6998
  };
6999
})(wysihtml5);(function(wysihtml5) {
7000
  var undef,
7001
      dom                     = wysihtml5.dom,
7002
      DEFAULT_NODE_NAME       = "DIV",
7003
      // Following elements are grouped
7004
      // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
7005
      // instead of creating a H4 within a H1 which would result in semantically invalid html
7006
      BLOCK_ELEMENTS_GROUP    = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", DEFAULT_NODE_NAME];
7007
 
7008
  /**
7009
   * Remove similiar classes (based on classRegExp)
7010
   * and add the desired class name
7011
   */
7012
  function _addClass(element, className, classRegExp) {
7013
    if (element.className) {
7014
      _removeClass(element, classRegExp);
7015
      element.className += " " + className;
7016
    } else {
7017
      element.className = className;
7018
    }
7019
  }
7020
 
7021
  function _removeClass(element, classRegExp) {
7022
    element.className = element.className.replace(classRegExp, "");
7023
  }
7024
 
7025
  /**
7026
   * Check whether given node is a text node and whether it's empty
7027
   */
7028
  function _isBlankTextNode(node) {
7029
    return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim();
7030
  }
7031
 
7032
  /**
7033
   * Returns previous sibling node that is not a blank text node
7034
   */
7035
  function _getPreviousSiblingThatIsNotBlank(node) {
7036
    var previousSibling = node.previousSibling;
7037
    while (previousSibling && _isBlankTextNode(previousSibling)) {
7038
      previousSibling = previousSibling.previousSibling;
7039
    }
7040
    return previousSibling;
7041
  }
7042
 
7043
  /**
7044
   * Returns next sibling node that is not a blank text node
7045
   */
7046
  function _getNextSiblingThatIsNotBlank(node) {
7047
    var nextSibling = node.nextSibling;
7048
    while (nextSibling && _isBlankTextNode(nextSibling)) {
7049
      nextSibling = nextSibling.nextSibling;
7050
    }
7051
    return nextSibling;
7052
  }
7053
 
7054
  /**
7055
   * Adds line breaks before and after the given node if the previous and next siblings
7056
   * aren't already causing a visual line break (block element or <br>)
7057
   */
7058
  function _addLineBreakBeforeAndAfter(node) {
7059
    var doc             = node.ownerDocument,
7060
        nextSibling     = _getNextSiblingThatIsNotBlank(node),
7061
        previousSibling = _getPreviousSiblingThatIsNotBlank(node);
7062
 
7063
    if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
7064
      node.parentNode.insertBefore(doc.createElement("br"), nextSibling);
7065
    }
7066
    if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
7067
      node.parentNode.insertBefore(doc.createElement("br"), node);
7068
    }
7069
  }
7070
 
7071
  /**
7072
   * Removes line breaks before and after the given node
7073
   */
7074
  function _removeLineBreakBeforeAndAfter(node) {
7075
    var nextSibling     = _getNextSiblingThatIsNotBlank(node),
7076
        previousSibling = _getPreviousSiblingThatIsNotBlank(node);
7077
 
7078
    if (nextSibling && _isLineBreak(nextSibling)) {
7079
      nextSibling.parentNode.removeChild(nextSibling);
7080
    }
7081
    if (previousSibling && _isLineBreak(previousSibling)) {
7082
      previousSibling.parentNode.removeChild(previousSibling);
7083
    }
7084
  }
7085
 
7086
  function _removeLastChildIfLineBreak(node) {
7087
    var lastChild = node.lastChild;
7088
    if (lastChild && _isLineBreak(lastChild)) {
7089
      lastChild.parentNode.removeChild(lastChild);
7090
    }
7091
  }
7092
 
7093
  function _isLineBreak(node) {
7094
    return node.nodeName === "BR";
7095
  }
7096
 
7097
  /**
7098
   * Checks whether the elment causes a visual line break
7099
   * (<br> or block elements)
7100
   */
7101
  function _isLineBreakOrBlockElement(element) {
7102
    if (_isLineBreak(element)) {
7103
      return true;
7104
    }
7105
 
7106
    if (dom.getStyle("display").from(element) === "block") {
7107
      return true;
7108
    }
7109
 
7110
    return false;
7111
  }
7112
 
7113
  /**
7114
   * Execute native query command
7115
   * and if necessary modify the inserted node's className
7116
   */
7117
  function _execCommand(doc, command, nodeName, className) {
7118
    if (className) {
7119
      var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {
7120
        var target = event.target,
7121
            displayStyle;
7122
        if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
7123
          return;
7124
        }
7125
        displayStyle = dom.getStyle("display").from(target);
7126
        if (displayStyle.substr(0, 6) !== "inline") {
7127
          // Make sure that only block elements receive the given class
7128
          target.className += " " + className;
7129
        }
7130
      });
7131
    }
7132
    doc.execCommand(command, false, nodeName);
7133
    if (eventListener) {
7134
      eventListener.stop();
7135
    }
7136
  }
7137
 
7138
  function _selectLineAndWrap(composer, element) {
7139
    composer.selection.selectLine();
7140
    composer.selection.surround(element);
7141
    _removeLineBreakBeforeAndAfter(element);
7142
    _removeLastChildIfLineBreak(element);
7143
    composer.selection.selectNode(element);
7144
  }
7145
 
7146
  function _hasClasses(element) {
7147
    return !!wysihtml5.lang.string(element.className).trim();
7148
  }
7149
 
7150
  wysihtml5.commands.formatBlock = {
7151
    exec: function(composer, command, nodeName, className, classRegExp) {
7152
      var doc          = composer.doc,
7153
          blockElement = this.state(composer, command, nodeName, className, classRegExp),
7154
          selectedNode;
7155
 
7156
      nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
7157
 
7158
      if (blockElement) {
7159
        composer.selection.executeAndRestoreSimple(function() {
7160
          if (classRegExp) {
7161
            _removeClass(blockElement, classRegExp);
7162
          }
7163
          var hasClasses = _hasClasses(blockElement);
7164
          if (!hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) {
7165
            // Insert a line break afterwards and beforewards when there are siblings
7166
            // that are not of type line break or block element
7167
            _addLineBreakBeforeAndAfter(blockElement);
7168
            dom.replaceWithChildNodes(blockElement);
7169
          } else if (hasClasses) {
7170
            // Make sure that styling is kept by renaming the element to <div> and copying over the class name
7171
            dom.renameElement(blockElement, DEFAULT_NODE_NAME);
7172
          }
7173
        });
7174
        return;
7175
      }
7176
 
7177
      // Find similiar block element and rename it (<h2 class="foo"></h2>  =>  <h1 class="foo"></h1>)
7178
      if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {
7179
        selectedNode = composer.selection.getSelectedNode();
7180
        blockElement = dom.getParentElement(selectedNode, {
7181
          nodeName: BLOCK_ELEMENTS_GROUP
7182
        });
7183
 
7184
        if (blockElement) {
7185
          composer.selection.executeAndRestoreSimple(function() {
7186
            // Rename current block element to new block element and add class
7187
            if (nodeName) {
7188
              blockElement = dom.renameElement(blockElement, nodeName);
7189
            }
7190
            if (className) {
7191
              _addClass(blockElement, className, classRegExp);
7192
            }
7193
          });
7194
          return;
7195
        }
7196
      }
7197
 
7198
      if (composer.commands.support(command)) {
7199
        _execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className);
7200
        return;
7201
      }
7202
 
7203
      blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME);
7204
      if (className) {
7205
        blockElement.className = className;
7206
      }
7207
      _selectLineAndWrap(composer, blockElement);
7208
    },
7209
 
7210
    state: function(composer, command, nodeName, className, classRegExp) {
7211
      nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
7212
      var selectedNode = composer.selection.getSelectedNode();
7213
      return dom.getParentElement(selectedNode, {
7214
        nodeName:     nodeName,
7215
        className:    className,
7216
        classRegExp:  classRegExp
7217
      });
7218
    },
7219
 
7220
    value: function() {
7221
      return undef;
7222
    }
7223
  };
7224
})(wysihtml5);/**
7225
 * formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
7226
 *
7227
 *   #1 caret in unformatted text:
7228
 *      abcdefg|
7229
 *   output:
7230
 *      abcdefg<b>|</b>
7231
 *   
7232
 *   #2 unformatted text selected:
7233
 *      abc|deg|h
7234
 *   output:
7235
 *      abc<b>|deg|</b>h
7236
 *   
7237
 *   #3 unformatted text selected across boundaries:
7238
 *      ab|c <span>defg|h</span>
7239
 *   output:
7240
 *      ab<b>|c </b><span><b>defg</b>|h</span>
7241
 *
7242
 *   #4 formatted text entirely selected
7243
 *      <b>|abc|</b>
7244
 *   output:
7245
 *      |abc|
7246
 *
7247
 *   #5 formatted text partially selected
7248
 *      <b>ab|c|</b>
7249
 *   output:
7250
 *      <b>ab</b>|c|
7251
 *
7252
 *   #6 formatted text selected across boundaries
7253
 *      <span>ab|c</span> <b>de|fgh</b>
7254
 *   output:
7255
 *      <span>ab|c</span> de|<b>fgh</b>
7256
 */
7257
(function(wysihtml5) {
7258
  var undef,
7259
      // Treat <b> as <strong> and vice versa
7260
      ALIAS_MAPPING = {
7261
        "strong": "b",
7262
        "em":     "i",
7263
        "b":      "strong",
7264
        "i":      "em"
7265
      },
7266
      htmlApplier = {};
7267
 
7268
  function _getTagNames(tagName) {
7269
    var alias = ALIAS_MAPPING[tagName];
7270
    return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
7271
  }
7272
 
7273
  function _getApplier(tagName, className, classRegExp) {
7274
    var identifier = tagName + ":" + className;
7275
    if (!htmlApplier[identifier]) {
7276
      htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true);
7277
    }
7278
    return htmlApplier[identifier];
7279
  }
7280
 
7281
  wysihtml5.commands.formatInline = {
7282
    exec: function(composer, command, tagName, className, classRegExp) {
7283
      var range = composer.selection.getRange();
7284
      if (!range) {
7285
        return false;
7286
      }
7287
      _getApplier(tagName, className, classRegExp).toggleRange(range);
7288
      composer.selection.setSelection(range);
7289
    },
7290
 
7291
    state: function(composer, command, tagName, className, classRegExp) {
7292
      var doc           = composer.doc,
7293
          aliasTagName  = ALIAS_MAPPING[tagName] || tagName,
7294
          range;
7295
 
7296
      // Check whether the document contains a node with the desired tagName
7297
      if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&
7298
          !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
7299
        return false;
7300
      }
7301
 
7302
       // Check whether the document contains a node with the desired className
7303
      if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
7304
         return false;
7305
      }
7306
 
7307
      range = composer.selection.getRange();
7308
      if (!range) {
7309
        return false;
7310
      }
7311
 
7312
      return _getApplier(tagName, className, classRegExp).isAppliedToRange(range);
7313
    },
7314
 
7315
    value: function() {
7316
      return undef;
7317
    }
7318
  };
7319
})(wysihtml5);(function(wysihtml5) {
7320
  var undef;
7321
 
7322
  wysihtml5.commands.insertHTML = {
7323
    exec: function(composer, command, html) {
7324
      if (composer.commands.support(command)) {
7325
        composer.doc.execCommand(command, false, html);
7326
      } else {
7327
        composer.selection.insertHTML(html);
7328
      }
7329
    },
7330
 
7331
    state: function() {
7332
      return false;
7333
    },
7334
 
7335
    value: function() {
7336
      return undef;
7337
    }
7338
  };
7339
})(wysihtml5);(function(wysihtml5) {
7340
  var NODE_NAME = "IMG";
7341
 
7342
  wysihtml5.commands.insertImage = {
7343
    /**
7344
     * Inserts an <img>
7345
     * If selection is already an image link, it removes it
7346
     * 
7347
     * @example
7348
     *    // either ...
7349
     *    wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg");
7350
     *    // ... or ...
7351
     *    wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" });
7352
     */
7353
    exec: function(composer, command, value) {
7354
      value = typeof(value) === "object" ? value : { src: value };
7355
 
7356
      var doc     = composer.doc,
7357
          image   = this.state(composer),
7358
          textNode,
7359
          i,
7360
          parent;
7361
 
7362
      if (image) {
7363
        // Image already selected, set the caret before it and delete it
7364
        composer.selection.setBefore(image);
7365
        parent = image.parentNode;
7366
        parent.removeChild(image);
7367
 
7368
        // and it's parent <a> too if it hasn't got any other relevant child nodes
7369
        wysihtml5.dom.removeEmptyTextNodes(parent);
7370
        if (parent.nodeName === "A" && !parent.firstChild) {
7371
          composer.selection.setAfter(parent);
7372
          parent.parentNode.removeChild(parent);
7373
        }
7374
 
7375
        // firefox and ie sometimes don't remove the image handles, even though the image got removed
7376
        wysihtml5.quirks.redraw(composer.element);
7377
        return;
7378
      }
7379
 
7380
      image = doc.createElement(NODE_NAME);
7381
 
7382
      for (i in value) {
7383
        image[i] = value[i];
7384
      }
7385
 
7386
      composer.selection.insertNode(image);
7387
      if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) {
7388
        textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
7389
        composer.selection.insertNode(textNode);
7390
        composer.selection.setAfter(textNode);
7391
      } else {
7392
        composer.selection.setAfter(image);
7393
      }
7394
    },
7395
 
7396
    state: function(composer) {
7397
      var doc = composer.doc,
7398
          selectedNode,
7399
          text,
7400
          imagesInSelection;
7401
 
7402
      if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {
7403
        return false;
7404
      }
7405
 
7406
      selectedNode = composer.selection.getSelectedNode();
7407
      if (!selectedNode) {
7408
        return false;
7409
      }
7410
 
7411
      if (selectedNode.nodeName === NODE_NAME) {
7412
        // This works perfectly in IE
7413
        return selectedNode;
7414
      }
7415
 
7416
      if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
7417
        return false;
7418
      }
7419
 
7420
      text = composer.selection.getText();
7421
      text = wysihtml5.lang.string(text).trim();
7422
      if (text) {
7423
        return false;
7424
      }
7425
 
7426
      imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) {
7427
        return node.nodeName === "IMG";
7428
      });
7429
 
7430
      if (imagesInSelection.length !== 1) {
7431
        return false;
7432
      }
7433
 
7434
      return imagesInSelection[0];
7435
    },
7436
 
7437
    value: function(composer) {
7438
      var image = this.state(composer);
7439
      return image && image.src;
7440
    }
7441
  };
7442
})(wysihtml5);(function(wysihtml5) {
7443
  var undef,
7444
      LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : "");
7445
 
7446
  wysihtml5.commands.insertLineBreak = {
7447
    exec: function(composer, command) {
7448
      if (composer.commands.support(command)) {
7449
        composer.doc.execCommand(command, false, null);
7450
        if (!wysihtml5.browser.autoScrollsToCaret()) {
7451
          composer.selection.scrollIntoView();
7452
        }
7453
      } else {
7454
        composer.commands.exec("insertHTML", LINE_BREAK);
7455
      }
7456
    },
7457
 
7458
    state: function() {
7459
      return false;
7460
    },
7461
 
7462
    value: function() {
7463
      return undef;
7464
    }
7465
  };
7466
})(wysihtml5);(function(wysihtml5) {
7467
  var undef;
7468
 
7469
  wysihtml5.commands.insertOrderedList = {
7470
    exec: function(composer, command) {
7471
      var doc = composer.doc,
7472
          selectedNode,
7473
          isEmpty,
7474
          tempElement,
7475
          list;
7476
 
7477
      if (composer.commands.support(command)) {
7478
        doc.execCommand(command, false, null);
7479
      } else {
7480
        selectedNode = composer.selection.getSelectedNode();
7481
        list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: ["UL", "OL"] }, 4);
7482
        if (!list) {
7483
          tempElement = doc.createElement("span");
7484
          composer.selection.surround(tempElement);
7485
          isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
7486
          composer.selection.executeAndRestoreSimple(function() {
7487
            list = wysihtml5.dom.convertToList(tempElement, "ol");
7488
          });
7489
 
7490
          if (isEmpty) {
7491
            composer.selection.selectNode(list.querySelector("li"));
7492
          }
7493
          return;
7494
        }
7495
 
7496
        composer.selection.executeAndRestoreSimple(function() {
7497
          if (list.nodeName === "OL") {
7498
            // Unwrap list
7499
            // <ol><li>foo</li><li>bar</li></ol>
7500
            // becomes:
7501
            // foo<br>bar<br>
7502
            wysihtml5.dom.resolveList(list);
7503
          } else if (list.nodeName === "UL" || list.nodeName === "MENU") {
7504
            // Turn an unordered list into an ordered list
7505
            // <ul><li>foo</li><li>bar</li></ul>
7506
            // becomes:
7507
            // <ol><li>foo</li><li>bar</li></ol>
7508
            wysihtml5.dom.renameElement(list, "ol");
7509
          }
7510
        });
7511
      }
7512
    },
7513
 
7514
    state: function(composer, command) {
7515
      try {
7516
        return composer.doc.queryCommandState(command);
7517
      } catch(e) {
7518
        return false;
7519
      }
7520
    },
7521
 
7522
    value: function() {
7523
      return undef;
7524
    }
7525
  };
7526
})(wysihtml5);(function(wysihtml5) {
7527
  var undef;
7528
 
7529
  wysihtml5.commands.insertUnorderedList = {
7530
    exec: function(composer, command) {
7531
      var doc = composer.doc,
7532
          selectedNode,
7533
          isEmpty,
7534
          tempElement,
7535
          list;
7536
 
7537
      if (composer.commands.support(command)) {
7538
        doc.execCommand(command, false, null);
7539
      } else {
7540
        selectedNode = composer.selection.getSelectedNode();
7541
        list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: ["UL", "OL"] });
7542
 
7543
        if (!list) {
7544
          tempElement = doc.createElement("span");
7545
          composer.selection.surround(tempElement);
7546
          isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
7547
          composer.selection.executeAndRestoreSimple(function() {
7548
            list = wysihtml5.dom.convertToList(tempElement, "ul");
7549
          });
7550
 
7551
          if (isEmpty) {
7552
            composer.selection.selectNode(list.querySelector("li"));
7553
          }
7554
          return;
7555
        }
7556
 
7557
        composer.selection.executeAndRestoreSimple(function() {
7558
          if (list.nodeName === "UL") {
7559
            // Unwrap list
7560
            // <ul><li>foo</li><li>bar</li></ul>
7561
            // becomes:
7562
            // foo<br>bar<br>
7563
            wysihtml5.dom.resolveList(list);
7564
          } else if (list.nodeName === "OL" || list.nodeName === "MENU") {
7565
            // Turn an ordered list into an unordered list
7566
            // <ol><li>foo</li><li>bar</li></ol>
7567
            // becomes:
7568
            // <ul><li>foo</li><li>bar</li></ul>
7569
            wysihtml5.dom.renameElement(list, "ul");
7570
          }
7571
        });
7572
      }
7573
    },
7574
 
7575
    state: function(composer, command) {
7576
      try {
7577
        return composer.doc.queryCommandState(command);
7578
      } catch(e) {
7579
        return false;
7580
      }
7581
    },
7582
 
7583
    value: function() {
7584
      return undef;
7585
    }
7586
  };
7587
})(wysihtml5);(function(wysihtml5) {
7588
  var undef;
7589
 
7590
  wysihtml5.commands.italic = {
7591
    exec: function(composer, command) {
7592
      return wysihtml5.commands.formatInline.exec(composer, command, "i");
7593
    },
7594
 
7595
    state: function(composer, command, color) {
7596
      // element.ownerDocument.queryCommandState("italic") results:
7597
      // firefox: only <i>
7598
      // chrome:  <i>, <em>, <blockquote>, ...
7599
      // ie:      <i>, <em>
7600
      // opera:   only <i>
7601
      return wysihtml5.commands.formatInline.state(composer, command, "i");
7602
    },
7603
 
7604
    value: function() {
7605
      return undef;
7606
    }
7607
  };
7608
})(wysihtml5);(function(wysihtml5) {
7609
  var undef,
7610
      CLASS_NAME  = "wysiwyg-text-align-center",
7611
      REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
7612
 
7613
  wysihtml5.commands.justifyCenter = {
7614
    exec: function(composer, command) {
7615
      return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7616
    },
7617
 
7618
    state: function(composer, command) {
7619
      return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7620
    },
7621
 
7622
    value: function() {
7623
      return undef;
7624
    }
7625
  };
7626
})(wysihtml5);(function(wysihtml5) {
7627
  var undef,
7628
      CLASS_NAME  = "wysiwyg-text-align-left",
7629
      REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
7630
 
7631
  wysihtml5.commands.justifyLeft = {
7632
    exec: function(composer, command) {
7633
      return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7634
    },
7635
 
7636
    state: function(composer, command) {
7637
      return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7638
    },
7639
 
7640
    value: function() {
7641
      return undef;
7642
    }
7643
  };
7644
})(wysihtml5);(function(wysihtml5) {
7645
  var undef,
7646
      CLASS_NAME  = "wysiwyg-text-align-right",
7647
      REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
7648
 
7649
  wysihtml5.commands.justifyRight = {
7650
    exec: function(composer, command) {
7651
      return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7652
    },
7653
 
7654
    state: function(composer, command) {
7655
      return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7656
    },
7657
 
7658
    value: function() {
7659
      return undef;
7660
    }
7661
  };
7662
})(wysihtml5);(function(wysihtml5) {
7663
  var undef,
7664
      REG_EXP     = /wysiwyg-text-decoration-underline/g,
7665
      CLASS_NAME  = "wysiwyg-text-decoration-underline";
7666
 
7667
  wysihtml5.commands.underline = {
7668
    exec: function(composer, command) {
7669
      return wysihtml5.commands.formatInline.exec(composer, command, "span", CLASS_NAME, REG_EXP);
7670
    },
7671
 
7672
    state: function(composer, command) {
7673
      return wysihtml5.commands.formatInline.state(composer, command, "span", CLASS_NAME, REG_EXP);
7674
    },
7675
 
7676
    value: function() {
7677
      return undef;
7678
    }
7679
  };
7680
})(wysihtml5);/**
7681
 * Undo Manager for wysihtml5
7682
 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
7683
 */
7684
(function(wysihtml5) {
7685
  var Z_KEY               = 90,
7686
      Y_KEY               = 89,
7687
      BACKSPACE_KEY       = 8,
7688
      DELETE_KEY          = 46,
7689
      MAX_HISTORY_ENTRIES = 40,
7690
      UNDO_HTML           = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
7691
      REDO_HTML           = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
7692
      dom                 = wysihtml5.dom;
7693
 
7694
  function cleanTempElements(doc) {
7695
    var tempElement;
7696
    while (tempElement = doc.querySelector("._wysihtml5-temp")) {
7697
      tempElement.parentNode.removeChild(tempElement);
7698
    }
7699
  }
7700
 
7701
  wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(
7702
    /** @scope wysihtml5.UndoManager.prototype */ {
7703
    constructor: function(editor) {
7704
      this.editor = editor;
7705
      this.composer = editor.composer;
7706
      this.element = this.composer.element;
7707
      this.history = [this.composer.getValue()];
7708
      this.position = 1;
7709
 
7710
      // Undo manager currently only supported in browsers who have the insertHTML command (not IE)
7711
      if (this.composer.commands.support("insertHTML")) {
7712
        this._observe();
7713
      }
7714
    },
7715
 
7716
    _observe: function() {
7717
      var that      = this,
7718
          doc       = this.composer.sandbox.getDocument(),
7719
          lastKey;
7720
 
7721
      // Catch CTRL+Z and CTRL+Y
7722
      dom.observe(this.element, "keydown", function(event) {
7723
        if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
7724
          return;
7725
        }
7726
 
7727
        var keyCode = event.keyCode,
7728
            isUndo = keyCode === Z_KEY && !event.shiftKey,
7729
            isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
7730
 
7731
        if (isUndo) {
7732
          that.undo();
7733
          event.preventDefault();
7734
        } else if (isRedo) {
7735
          that.redo();
7736
          event.preventDefault();
7737
        }
7738
      });
7739
 
7740
      // Catch delete and backspace
7741
      dom.observe(this.element, "keydown", function(event) {
7742
        var keyCode = event.keyCode;
7743
        if (keyCode === lastKey) {
7744
          return;
7745
        }
7746
 
7747
        lastKey = keyCode;
7748
 
7749
        if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
7750
          that.transact();
7751
        }
7752
      });
7753
 
7754
      // Now this is very hacky:
7755
      // These days browsers don't offer a undo/redo event which we could hook into
7756
      // to be notified when the user hits undo/redo in the contextmenu.
7757
      // Therefore we simply insert two elements as soon as the contextmenu gets opened.
7758
      // The last element being inserted will be immediately be removed again by a exexCommand("undo")
7759
      //  => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu
7760
      //  => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu
7761
      if (wysihtml5.browser.hasUndoInContextMenu()) {
7762
        var interval, observed, cleanUp = function() {
7763
          cleanTempElements(doc);
7764
          clearInterval(interval);
7765
        };
7766
 
7767
        dom.observe(this.element, "contextmenu", function() {
7768
          cleanUp();
7769
          that.composer.selection.executeAndRestoreSimple(function() {
7770
            if (that.element.lastChild) {
7771
              that.composer.selection.setAfter(that.element.lastChild);
7772
            }
7773
 
7774
            // enable undo button in context menu
7775
            doc.execCommand("insertHTML", false, UNDO_HTML);
7776
            // enable redo button in context menu
7777
            doc.execCommand("insertHTML", false, REDO_HTML);
7778
            doc.execCommand("undo", false, null);
7779
          });
7780
 
7781
          interval = setInterval(function() {
7782
            if (doc.getElementById("_wysihtml5-redo")) {
7783
              cleanUp();
7784
              that.redo();
7785
            } else if (!doc.getElementById("_wysihtml5-undo")) {
7786
              cleanUp();
7787
              that.undo();
7788
            }
7789
          }, 400);
7790
 
7791
          if (!observed) {
7792
            observed = true;
7793
            dom.observe(document, "mousedown", cleanUp);
7794
            dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp);
7795
          }
7796
        });
7797
      }
7798
 
7799
      this.editor
7800
        .observe("newword:composer", function() {
7801
          that.transact();
7802
        })
7803
 
7804
        .observe("beforecommand:composer", function() {
7805
          that.transact();
7806
        });
7807
    },
7808
 
7809
    transact: function() {
7810
      var previousHtml  = this.history[this.position - 1],
7811
          currentHtml   = this.composer.getValue();
7812
 
7813
      if (currentHtml == previousHtml) {
7814
        return;
7815
      }
7816
 
7817
      var length = this.history.length = this.position;
7818
      if (length > MAX_HISTORY_ENTRIES) {
7819
        this.history.shift();
7820
        this.position--;
7821
      }
7822
 
7823
      this.position++;
7824
      this.history.push(currentHtml);
7825
    },
7826
 
7827
    undo: function() {
7828
      this.transact();
7829
 
7830
      if (this.position <= 1) {
7831
        return;
7832
      }
7833
 
7834
      this.set(this.history[--this.position - 1]);
7835
      this.editor.fire("undo:composer");
7836
    },
7837
 
7838
    redo: function() {
7839
      if (this.position >= this.history.length) {
7840
        return;
7841
      }
7842
 
7843
      this.set(this.history[++this.position - 1]);
7844
      this.editor.fire("redo:composer");
7845
    },
7846
 
7847
    set: function(html) {
7848
      this.composer.setValue(html);
7849
      this.editor.focus(true);
7850
    }
7851
  });
7852
})(wysihtml5);
7853
/**
7854
 * TODO: the following methods still need unit test coverage
7855
 */
7856
wysihtml5.views.View = Base.extend(
7857
  /** @scope wysihtml5.views.View.prototype */ {
7858
  constructor: function(parent, textareaElement, config) {
7859
    this.parent   = parent;
7860
    this.element  = textareaElement;
7861
    this.config   = config;
7862
 
7863
    this._observeViewChange();
7864
  },
7865
 
7866
  _observeViewChange: function() {
7867
    var that = this;
7868
    this.parent.observe("beforeload", function() {
7869
      that.parent.observe("change_view", function(view) {
7870
        if (view === that.name) {
7871
          that.parent.currentView = that;
7872
          that.show();
7873
          // Using tiny delay here to make sure that the placeholder is set before focusing
7874
          setTimeout(function() { that.focus(); }, 0);
7875
        } else {
7876
          that.hide();
7877
        }
7878
      });
7879
    });
7880
  },
7881
 
7882
  focus: function() {
7883
    if (this.element.ownerDocument.querySelector(":focus") === this.element) {
7884
      return;
7885
    }
7886
 
7887
    try { this.element.focus(); } catch(e) {}
7888
  },
7889
 
7890
  hide: function() {
7891
    this.element.style.display = "none";
7892
  },
7893
 
7894
  show: function() {
7895
    this.element.style.display = "";
7896
  },
7897
 
7898
  disable: function() {
7899
    this.element.setAttribute("disabled", "disabled");
7900
  },
7901
 
7902
  enable: function() {
7903
    this.element.removeAttribute("disabled");
7904
  }
7905
});(function(wysihtml5) {
7906
  var dom       = wysihtml5.dom,
7907
      browser   = wysihtml5.browser;
7908
 
7909
  wysihtml5.views.Composer = wysihtml5.views.View.extend(
7910
    /** @scope wysihtml5.views.Composer.prototype */ {
7911
    name: "composer",
7912
 
7913
    // Needed for firefox in order to display a proper caret in an empty contentEditable
7914
    CARET_HACK: "<br>",
7915
 
7916
    constructor: function(parent, textareaElement, config) {
7917
      this.base(parent, textareaElement, config);
7918
      this.textarea = this.parent.textarea;
7919
      this._initSandbox();
7920
    },
7921
 
7922
    clear: function() {
7923
      this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK;
7924
    },
7925
 
7926
    getValue: function(parse) {
7927
      var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element);
7928
 
7929
      if (parse) {
7930
        value = this.parent.parse(value);
7931
      }
7932
 
7933
      // Replace all "zero width no breaking space" chars
7934
      // which are used as hacks to enable some functionalities
7935
      // Also remove all CARET hacks that somehow got left
7936
      value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by("");
7937
 
7938
      return value;
7939
    },
7940
 
7941
    setValue: function(html, parse) {
7942
      if (parse) {
7943
        html = this.parent.parse(html);
7944
      }
7945
      this.element.innerHTML = html;
7946
    },
7947
 
7948
    show: function() {
7949
      this.iframe.style.display = this._displayStyle || "";
7950
 
7951
      // Firefox needs this, otherwise contentEditable becomes uneditable
7952
      this.disable();
7953
      this.enable();
7954
    },
7955
 
7956
    hide: function() {
7957
      this._displayStyle = dom.getStyle("display").from(this.iframe);
7958
      if (this._displayStyle === "none") {
7959
        this._displayStyle = null;
7960
      }
7961
      this.iframe.style.display = "none";
7962
    },
7963
 
7964
    disable: function() {
7965
      this.element.removeAttribute("contentEditable");
7966
      this.base();
7967
    },
7968
 
7969
    enable: function() {
7970
      this.element.setAttribute("contentEditable", "true");
7971
      this.base();
7972
    },
7973
 
7974
    focus: function(setToEnd) {
7975
      // IE 8 fires the focus event after .focus()
7976
      // This is needed by our simulate_placeholder.js to work
7977
      // therefore we clear it ourselves this time
7978
      if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
7979
        this.clear();
7980
      }
7981
 
7982
      this.base();
7983
 
7984
      var lastChild = this.element.lastChild;
7985
      if (setToEnd && lastChild) {
7986
        if (lastChild.nodeName === "BR") {
7987
          this.selection.setBefore(this.element.lastChild);
7988
        } else {
7989
          this.selection.setAfter(this.element.lastChild);
7990
        }
7991
      }
7992
    },
7993
 
7994
    getTextContent: function() {
7995
      return dom.getTextContent(this.element);
7996
    },
7997
 
7998
    hasPlaceholderSet: function() {
7999
      return this.getTextContent() == this.textarea.element.getAttribute("placeholder");
8000
    },
8001
 
8002
    isEmpty: function() {
8003
      var innerHTML               = this.element.innerHTML,
8004
          elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea";
8005
      return innerHTML === ""              || 
8006
             innerHTML === this.CARET_HACK ||
8007
             this.hasPlaceholderSet()      ||
8008
             (this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue));
8009
    },
8010
 
8011
    _initSandbox: function() {
8012
      var that = this;
8013
 
8014
      this.sandbox = new dom.Sandbox(function() {
8015
        that._create();
8016
      }, {
8017
        stylesheets:  this.config.stylesheets
8018
      });
8019
      this.iframe  = this.sandbox.getIframe();
8020
 
8021
      // Create hidden field which tells the server after submit, that the user used an wysiwyg editor
8022
      var hiddenField = document.createElement("input");
8023
      hiddenField.type   = "hidden";
8024
      hiddenField.name   = "_wysihtml5_mode";
8025
      hiddenField.value  = 1;
8026
 
8027
      // Store reference to current wysihtml5 instance on the textarea element
8028
      var textareaElement = this.textarea.element;
8029
      dom.insert(this.iframe).after(textareaElement);
8030
      dom.insert(hiddenField).after(textareaElement);
8031
    },
8032
 
8033
    _create: function() {
8034
      var that = this;
8035
 
8036
      this.doc                = this.sandbox.getDocument();
8037
      this.element            = this.doc.body;
8038
      this.textarea           = this.parent.textarea;
8039
      this.element.innerHTML  = this.textarea.getValue(true);
8040
      this.enable();
8041
 
8042
      // Make sure our selection handler is ready
8043
      this.selection = new wysihtml5.Selection(this.parent);
8044
 
8045
      // Make sure commands dispatcher is ready
8046
      this.commands  = new wysihtml5.Commands(this.parent);
8047
 
8048
      dom.copyAttributes([
8049
        "className", "spellcheck", "title", "lang", "dir", "accessKey"
8050
      ]).from(this.textarea.element).to(this.element);
8051
 
8052
      dom.addClass(this.element, this.config.composerClassName);
8053
 
8054
      // Make the editor look like the original textarea, by syncing styles
8055
      if (this.config.style) {
8056
        this.style();
8057
      }
8058
 
8059
      this.observe();
8060
 
8061
      var name = this.config.name;
8062
      if (name) {
8063
        dom.addClass(this.element, name);
8064
        dom.addClass(this.iframe, name);
8065
      }
8066
 
8067
      // Simulate html5 placeholder attribute on contentEditable element
8068
      var placeholderText = typeof(this.config.placeholder) === "string"
8069
        ? this.config.placeholder
8070
        : this.textarea.element.getAttribute("placeholder");
8071
      if (placeholderText) {
8072
        dom.simulatePlaceholder(this.parent, this, placeholderText);
8073
      }
8074
 
8075
      // Make sure that the browser avoids using inline styles whenever possible
8076
      this.commands.exec("styleWithCSS", false);
8077
 
8078
      this._initAutoLinking();
8079
      this._initObjectResizing();
8080
      this._initUndoManager();
8081
 
8082
      // Simulate html5 autofocus on contentEditable element
8083
      if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) {
8084
        setTimeout(function() { that.focus(); }, 100);
8085
      }
8086
 
8087
      wysihtml5.quirks.insertLineBreakOnReturn(this);
8088
 
8089
      // IE sometimes leaves a single paragraph, which can't be removed by the user
8090
      if (!browser.clearsContentEditableCorrectly()) {
8091
        wysihtml5.quirks.ensureProperClearing(this);
8092
      }
8093
 
8094
      if (!browser.clearsListsInContentEditableCorrectly()) {
8095
        wysihtml5.quirks.ensureProperClearingOfLists(this);
8096
      }
8097
 
8098
      // Set up a sync that makes sure that textarea and editor have the same content
8099
      if (this.initSync && this.config.sync) {
8100
        this.initSync();
8101
      }
8102
 
8103
      // Okay hide the textarea, we are ready to go
8104
      this.textarea.hide();
8105
 
8106
      // Fire global (before-)load event
8107
      this.parent.fire("beforeload").fire("load");
8108
    },
8109
 
8110
    _initAutoLinking: function() {
8111
      var that                           = this,
8112
          supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
8113
          supportsAutoLinking            = browser.doesAutoLinkingInContentEditable();
8114
      if (supportsDisablingOfAutoLinking) {
8115
        this.commands.exec("autoUrlDetect", false);
8116
      }
8117
 
8118
      if (!this.config.autoLink) {
8119
        return;
8120
      }
8121
 
8122
      // Only do the auto linking by ourselves when the browser doesn't support auto linking
8123
      // OR when he supports auto linking but we were able to turn it off (IE9+)
8124
      if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
8125
        this.parent.observe("newword:composer", function() {
8126
          that.selection.executeAndRestore(function(startContainer, endContainer) {
8127
            dom.autoLink(endContainer.parentNode);
8128
          });
8129
        });
8130
      }
8131
 
8132
      // Assuming we have the following:
8133
      //  <a href="http://www.google.de">http://www.google.de</a>
8134
      // If a user now changes the url in the innerHTML we want to make sure that
8135
      // it's synchronized with the href attribute (as long as the innerHTML is still a url)
8136
      var // Use a live NodeList to check whether there are any links in the document
8137
          links           = this.sandbox.getDocument().getElementsByTagName("a"),
8138
          // The autoLink helper method reveals a reg exp to detect correct urls
8139
          urlRegExp       = dom.autoLink.URL_REG_EXP,
8140
          getTextContent  = function(element) {
8141
            var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim();
8142
            if (textContent.substr(0, 4) === "www.") {
8143
              textContent = "http://" + textContent;
8144
            }
8145
            return textContent;
8146
          };
8147
 
8148
      dom.observe(this.element, "keydown", function(event) {
8149
        if (!links.length) {
8150
          return;
8151
        }
8152
 
8153
        var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
8154
            link         = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),
8155
            textContent;
8156
 
8157
        if (!link) {
8158
          return;
8159
        }
8160
 
8161
        textContent = getTextContent(link);
8162
        // keydown is fired before the actual content is changed
8163
        // therefore we set a timeout to change the href
8164
        setTimeout(function() {
8165
          var newTextContent = getTextContent(link);
8166
          if (newTextContent === textContent) {
8167
            return;
8168
          }
8169
 
8170
          // Only set href when new href looks like a valid url
8171
          if (newTextContent.match(urlRegExp)) {
8172
            link.setAttribute("href", newTextContent);
8173
          }
8174
        }, 0);
8175
      });
8176
    },
8177
 
8178
    _initObjectResizing: function() {
8179
      var properties        = ["width", "height"],
8180
          propertiesLength  = properties.length,
8181
          element           = this.element;
8182
 
8183
      this.commands.exec("enableObjectResizing", this.config.allowObjectResizing);
8184
 
8185
      if (this.config.allowObjectResizing) {
8186
         // IE sets inline styles after resizing objects
8187
         // The following lines make sure that the width/height css properties
8188
         // are copied over to the width/height attributes
8189
        if (browser.supportsEvent("resizeend")) {
8190
          dom.observe(element, "resizeend", function(event) {
8191
            var target = event.target || event.srcElement,
8192
                style  = target.style,
8193
                i      = 0,
8194
                property;
8195
            for(; i<propertiesLength; i++) {
8196
              property = properties[i];
8197
              if (style[property]) {
8198
                target.setAttribute(property, parseInt(style[property], 10));
8199
                style[property] = "";
8200
              }
8201
            }
8202
            // After resizing IE sometimes forgets to remove the old resize handles
8203
            wysihtml5.quirks.redraw(element);
8204
          });
8205
        }
8206
      } else {
8207
        if (browser.supportsEvent("resizestart")) {
8208
          dom.observe(element, "resizestart", function(event) { event.preventDefault(); });
8209
        }
8210
      }
8211
    },
8212
 
8213
    _initUndoManager: function() {
8214
      new wysihtml5.UndoManager(this.parent);
8215
    }
8216
  });
8217
})(wysihtml5);(function(wysihtml5) {
8218
  var dom             = wysihtml5.dom,
8219
      doc             = document,
8220
      win             = window,
8221
      HOST_TEMPLATE   = doc.createElement("div"),
8222
      /**
8223
       * Styles to copy from textarea to the composer element
8224
       */
8225
      TEXT_FORMATTING = [
8226
        "background-color",
8227
        "color", "cursor",
8228
        "font-family", "font-size", "font-style", "font-variant", "font-weight",
8229
        "line-height", "letter-spacing",
8230
        "text-align", "text-decoration", "text-indent", "text-rendering",
8231
        "word-break", "word-wrap", "word-spacing"
8232
      ],
8233
      /**
8234
       * Styles to copy from textarea to the iframe
8235
       */
8236
      BOX_FORMATTING = [
8237
        "background-color",
8238
        "border-collapse",
8239
        "border-bottom-color", "border-bottom-style", "border-bottom-width",
8240
        "border-left-color", "border-left-style", "border-left-width",
8241
        "border-right-color", "border-right-style", "border-right-width",
8242
        "border-top-color", "border-top-style", "border-top-width",
8243
        "clear", "display", "float",
8244
        "margin-bottom", "margin-left", "margin-right", "margin-top",
8245
        "outline-color", "outline-offset", "outline-width", "outline-style",
8246
        "padding-left", "padding-right", "padding-top", "padding-bottom",
8247
        "position", "top", "left", "right", "bottom", "z-index",
8248
        "vertical-align", "text-align",
8249
        "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing",
8250
        "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow",
8251
        "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius",
8252
        "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius",
8253
        "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius",
8254
        "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius",
8255
        "width", "height"
8256
      ],
8257
      /**
8258
       * Styles to sync while the window gets resized
8259
       */
8260
      RESIZE_STYLE = [
8261
        "width", "height",
8262
        "top", "left", "right", "bottom"
8263
      ],
8264
      ADDITIONAL_CSS_RULES = [
8265
        "html             { height: 100%; }",
8266
        "body             { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }",
8267
        "._wysihtml5-temp { display: none; }",
8268
        wysihtml5.browser.isGecko ?
8269
          "body.placeholder { color: graytext !important; }" : 
8270
          "body.placeholder { color: #a9a9a9 !important; }",
8271
        "body[disabled]   { background-color: #eee !important; color: #999 !important; cursor: default !important; }",
8272
        // Ensure that user see's broken images and can delete them
8273
        "img:-moz-broken  { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
8274
      ];
8275
 
8276
  /**
8277
   * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
8278
   * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
8279
   *
8280
   * Other browsers need a more hacky way: (pssst don't tell my mama)
8281
   * In order to prevent the element being scrolled into view when focusing it, we simply
8282
   * move it out of the scrollable area, focus it, and reset it's position
8283
   */
8284
  var focusWithoutScrolling = function(element) {
8285
    if (element.setActive) {
8286
      // Following line could cause a js error when the textarea is invisible
8287
      // See https://github.com/xing/wysihtml5/issues/9
8288
      try { element.setActive(); } catch(e) {}
8289
    } else {
8290
      var elementStyle = element.style,
8291
          originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
8292
          originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
8293
          originalStyles = {
8294
            position:         elementStyle.position,
8295
            top:              elementStyle.top,
8296
            left:             elementStyle.left,
8297
            WebkitUserSelect: elementStyle.WebkitUserSelect
8298
          };
8299
 
8300
      dom.setStyles({
8301
        position:         "absolute",
8302
        top:              "-99999px",
8303
        left:             "-99999px",
8304
        // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
8305
        WebkitUserSelect: "none"
8306
      }).on(element);
8307
 
8308
      element.focus();
8309
 
8310
      dom.setStyles(originalStyles).on(element);
8311
 
8312
      if (win.scrollTo) {
8313
        // Some browser extensions unset this method to prevent annoyances
8314
        // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
8315
        // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
8316
        win.scrollTo(originalScrollLeft, originalScrollTop);
8317
      }
8318
    }
8319
  };
8320
 
8321
 
8322
  wysihtml5.views.Composer.prototype.style = function() {
8323
    var that                  = this,
8324
        originalActiveElement = doc.querySelector(":focus"),
8325
        textareaElement       = this.textarea.element,
8326
        hasPlaceholder        = textareaElement.hasAttribute("placeholder"),
8327
        originalPlaceholder   = hasPlaceholder && textareaElement.getAttribute("placeholder");
8328
    this.focusStylesHost      = this.focusStylesHost  || HOST_TEMPLATE.cloneNode(false);
8329
    this.blurStylesHost       = this.blurStylesHost   || HOST_TEMPLATE.cloneNode(false);
8330
 
8331
    // Remove placeholder before copying (as the placeholder has an affect on the computed style)
8332
    if (hasPlaceholder) {
8333
      textareaElement.removeAttribute("placeholder");
8334
    }
8335
 
8336
    if (textareaElement === originalActiveElement) {
8337
      textareaElement.blur();
8338
    }
8339
 
8340
    // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
8341
    dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost);
8342
 
8343
    // --------- editor styles ---------
8344
    dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
8345
 
8346
    // --------- apply standard rules ---------
8347
    dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
8348
 
8349
    // --------- :focus styles ---------
8350
    focusWithoutScrolling(textareaElement);
8351
    dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
8352
    dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
8353
 
8354
    // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
8355
    // this is needed for when the change_view event is fired where the iframe is hidden and then
8356
    // the blur event fires and re-displays it
8357
    var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]);
8358
 
8359
    // --------- restore focus ---------
8360
    if (originalActiveElement) {
8361
      originalActiveElement.focus();
8362
    } else {
8363
      textareaElement.blur();
8364
    }
8365
 
8366
    // --------- restore placeholder ---------
8367
    if (hasPlaceholder) {
8368
      textareaElement.setAttribute("placeholder", originalPlaceholder);
8369
    }
8370
 
8371
    // When copying styles, we only get the computed style which is never returned in percent unit
8372
    // Therefore we've to recalculate style onresize
8373
    if (!wysihtml5.browser.hasCurrentStyleProperty()) {
8374
      dom.observe(win, "resize", function() {
8375
        var originalDisplayStyle = dom.getStyle("display").from(textareaElement);
8376
        textareaElement.style.display = "";
8377
        dom.copyStyles(RESIZE_STYLE)
8378
          .from(textareaElement)
8379
          .to(that.iframe)
8380
          .andTo(that.focusStylesHost)
8381
          .andTo(that.blurStylesHost);
8382
        textareaElement.style.display = originalDisplayStyle;
8383
      });
8384
    }
8385
 
8386
    // --------- Sync focus/blur styles ---------
8387
    this.parent.observe("focus:composer", function() {
8388
      dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe);
8389
      dom.copyStyles(TEXT_FORMATTING)     .from(that.focusStylesHost).to(that.element);
8390
    });
8391
 
8392
    this.parent.observe("blur:composer", function() {
8393
      dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe);
8394
      dom.copyStyles(TEXT_FORMATTING)     .from(that.blurStylesHost).to(that.element);
8395
    });
8396
 
8397
    return this;
8398
  };
8399
})(wysihtml5);/**
8400
 * Taking care of events
8401
 *  - Simulating 'change' event on contentEditable element
8402
 *  - Handling drag & drop logic
8403
 *  - Catch paste events
8404
 *  - Dispatch proprietary newword:composer event
8405
 *  - Keyboard shortcuts
8406
 */
8407
(function(wysihtml5) {
8408
  var dom       = wysihtml5.dom,
8409
      browser   = wysihtml5.browser,
8410
      /**
8411
       * Map keyCodes to query commands
8412
       */
8413
      shortcuts = {
8414
        "66": "bold",     // B
8415
        "73": "italic",   // I
8416
        "85": "underline" // U
8417
      };
8418
 
8419
  wysihtml5.views.Composer.prototype.observe = function() {
8420
    var that                = this,
8421
        state               = this.getValue(),
8422
        iframe              = this.sandbox.getIframe(),
8423
        element             = this.element,
8424
        focusBlurElement    = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(),
8425
        // Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same
8426
        pasteEvents         = browser.supportsEvent("drop") ? ["drop", "paste"] : ["dragdrop", "paste"];
8427
 
8428
    // --------- destroy:composer event ---------
8429
    dom.observe(iframe, "DOMNodeRemoved", function() {
8430
      clearInterval(domNodeRemovedInterval);
8431
      that.parent.fire("destroy:composer");
8432
    });
8433
 
8434
    // DOMNodeRemoved event is not supported in IE 8
8435
    var domNodeRemovedInterval = setInterval(function() {
8436
      if (!dom.contains(document.documentElement, iframe)) {
8437
        clearInterval(domNodeRemovedInterval);
8438
        that.parent.fire("destroy:composer");
8439
      }
8440
    }, 250);
8441
 
8442
 
8443
    // --------- Focus & blur logic ---------
8444
    dom.observe(focusBlurElement, "focus", function() {
8445
      that.parent.fire("focus").fire("focus:composer");
8446
 
8447
      // Delay storing of state until all focus handler are fired
8448
      // especially the one which resets the placeholder
8449
      setTimeout(function() { state = that.getValue(); }, 0);
8450
    });
8451
 
8452
    dom.observe(focusBlurElement, "blur", function() {
8453
      if (state !== that.getValue()) {
8454
        that.parent.fire("change").fire("change:composer");
8455
      }
8456
      that.parent.fire("blur").fire("blur:composer");
8457
    });
8458
 
8459
    if (wysihtml5.browser.isIos()) {
8460
      // When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus
8461
      // but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible)
8462
      // We prevent that by focusing a temporary input element which immediately loses focus
8463
      dom.observe(element, "blur", function() {
8464
        var input = element.ownerDocument.createElement("input"),
8465
            originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop,
8466
            originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
8467
        try {
8468
          that.selection.insertNode(input);
8469
        } catch(e) {
8470
          element.appendChild(input);
8471
        }
8472
        input.focus();
8473
        input.parentNode.removeChild(input);
8474
 
8475
        window.scrollTo(originalScrollLeft, originalScrollTop);
8476
      });
8477
    }
8478
 
8479
    // --------- Drag & Drop logic ---------
8480
    dom.observe(element, "dragenter", function() {
8481
      that.parent.fire("unset_placeholder");
8482
    });
8483
 
8484
    if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) {
8485
      dom.observe(element, ["dragover", "dragenter"], function(event) {
8486
        event.preventDefault();
8487
      });
8488
    }
8489
 
8490
    dom.observe(element, pasteEvents, function(event) {
8491
      var dataTransfer = event.dataTransfer,
8492
          data;
8493
 
8494
      if (dataTransfer && browser.supportsDataTransfer()) {
8495
        data = dataTransfer.getData("text/html") || dataTransfer.getData("text/plain");
8496
      }
8497
      if (data) {
8498
        element.focus();
8499
        that.commands.exec("insertHTML", data);
8500
        that.parent.fire("paste").fire("paste:composer");
8501
        event.stopPropagation();
8502
        event.preventDefault();
8503
      } else {
8504
        setTimeout(function() {
8505
          that.parent.fire("paste").fire("paste:composer");
8506
        }, 0);
8507
      }
8508
    });
8509
 
8510
    // --------- neword event ---------
8511
    dom.observe(element, "keyup", function(event) {
8512
      var keyCode = event.keyCode;
8513
      if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) {
8514
        that.parent.fire("newword:composer");
8515
      }
8516
    });
8517
 
8518
    this.parent.observe("paste:composer", function() {
8519
      setTimeout(function() { that.parent.fire("newword:composer"); }, 0);
8520
    });
8521
 
8522
    // --------- Make sure that images are selected when clicking on them ---------
8523
    if (!browser.canSelectImagesInContentEditable()) {
8524
      dom.observe(element, "mousedown", function(event) {
8525
        var target = event.target;
8526
        if (target.nodeName === "IMG") {
8527
          that.selection.selectNode(target);
8528
          event.preventDefault();
8529
        }
8530
      });
8531
    }
8532
 
8533
    // --------- Shortcut logic ---------
8534
    dom.observe(element, "keydown", function(event) {
8535
      var keyCode  = event.keyCode,
8536
          command  = shortcuts[keyCode];
8537
      if ((event.ctrlKey || event.metaKey) && command) {
8538
        that.commands.exec(command);
8539
        event.preventDefault();
8540
      }
8541
    });
8542
 
8543
    // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor ---------
8544
    dom.observe(element, "keydown", function(event) {
8545
      var target  = that.selection.getSelectedNode(true),
8546
          keyCode = event.keyCode,
8547
          parent;
8548
      if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete
8549
        parent = target.parentNode;
8550
        // delete the <img>
8551
        parent.removeChild(target);
8552
        // and it's parent <a> too if it hasn't got any other child nodes
8553
        if (parent.nodeName === "A" && !parent.firstChild) {
8554
          parent.parentNode.removeChild(parent);
8555
        }
8556
 
8557
        setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);
8558
        event.preventDefault();
8559
      }
8560
    });
8561
 
8562
    // --------- Show url in tooltip when hovering links or images ---------
8563
    var titlePrefixes = {
8564
      IMG: "Image: ",
8565
      A:   "Link: "
8566
    };
8567
 
8568
    dom.observe(element, "mouseover", function(event) {
8569
      var target   = event.target,
8570
          nodeName = target.nodeName,
8571
          title;
8572
      if (nodeName !== "A" && nodeName !== "IMG") {
8573
        return;
8574
      }
8575
 
8576
      title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
8577
      target.setAttribute("title", title);
8578
    });
8579
  };
8580
})(wysihtml5);/**
8581
 * Class that takes care that the value of the composer and the textarea is always in sync
8582
 */
8583
(function(wysihtml5) {
8584
  var INTERVAL = 400;
8585
 
8586
  wysihtml5.views.Synchronizer = Base.extend(
8587
    /** @scope wysihtml5.views.Synchronizer.prototype */ {
8588
 
8589
    constructor: function(editor, textarea, composer) {
8590
      this.editor   = editor;
8591
      this.textarea = textarea;
8592
      this.composer = composer;
8593
 
8594
      this._observe();
8595
    },
8596
 
8597
    /**
8598
     * Sync html from composer to textarea
8599
     * Takes care of placeholders
8600
     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
8601
     */
8602
    fromComposerToTextarea: function(shouldParseHtml) {
8603
      this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml);
8604
    },
8605
 
8606
    /**
8607
     * Sync value of textarea to composer
8608
     * Takes care of placeholders
8609
     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
8610
     */
8611
    fromTextareaToComposer: function(shouldParseHtml) {
8612
      var textareaValue = this.textarea.getValue();
8613
      if (textareaValue) {
8614
        this.composer.setValue(textareaValue, shouldParseHtml);
8615
      } else {
8616
        this.composer.clear();
8617
        this.editor.fire("set_placeholder");
8618
      }
8619
    },
8620
 
8621
    /**
8622
     * Invoke syncing based on view state
8623
     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
8624
     */
8625
    sync: function(shouldParseHtml) {
8626
      if (this.editor.currentView.name === "textarea") {
8627
        this.fromTextareaToComposer(shouldParseHtml);
8628
      } else {
8629
        this.fromComposerToTextarea(shouldParseHtml);
8630
      }
8631
    },
8632
 
8633
    /**
8634
     * Initializes interval-based syncing
8635
     * also makes sure that on-submit the composer's content is synced with the textarea
8636
     * immediately when the form gets submitted
8637
     */
8638
    _observe: function() {
8639
      var interval,
8640
          that          = this,
8641
          form          = this.textarea.element.form,
8642
          startInterval = function() {
8643
            interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
8644
          },
8645
          stopInterval  = function() {
8646
            clearInterval(interval);
8647
            interval = null;
8648
          };
8649
 
8650
      startInterval();
8651
 
8652
      if (form) {
8653
        // If the textarea is in a form make sure that after onreset and onsubmit the composer
8654
        // has the correct state
8655
        wysihtml5.dom.observe(form, "submit", function() {
8656
          that.sync(true);
8657
        });
8658
        wysihtml5.dom.observe(form, "reset", function() {
8659
          setTimeout(function() { that.fromTextareaToComposer(); }, 0);
8660
        });
8661
      }
8662
 
8663
      this.editor.observe("change_view", function(view) {
8664
        if (view === "composer" && !interval) {
8665
          that.fromTextareaToComposer(true);
8666
          startInterval();
8667
        } else if (view === "textarea") {
8668
          that.fromComposerToTextarea(true);
8669
          stopInterval();
8670
        }
8671
      });
8672
 
8673
      this.editor.observe("destroy:composer", stopInterval);
8674
    }
8675
  });
8676
})(wysihtml5);
8677
wysihtml5.views.Textarea = wysihtml5.views.View.extend(
8678
  /** @scope wysihtml5.views.Textarea.prototype */ {
8679
  name: "textarea",
8680
 
8681
  constructor: function(parent, textareaElement, config) {
8682
    this.base(parent, textareaElement, config);
8683
 
8684
    this._observe();
8685
  },
8686
 
8687
  clear: function() {
8688
    this.element.value = "";
8689
  },
8690
 
8691
  getValue: function(parse) {
8692
    var value = this.isEmpty() ? "" : this.element.value;
8693
    if (parse) {
8694
      value = this.parent.parse(value);
8695
    }
8696
    return value;
8697
  },
8698
 
8699
  setValue: function(html, parse) {
8700
    if (parse) {
8701
      html = this.parent.parse(html);
8702
    }
8703
    this.element.value = html;
8704
  },
8705
 
8706
  hasPlaceholderSet: function() {
8707
    var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),
8708
        placeholderText     = this.element.getAttribute("placeholder") || null,
8709
        value               = this.element.value,
8710
        isEmpty             = !value;
8711
    return (supportsPlaceholder && isEmpty) || (value === placeholderText);
8712
  },
8713
 
8714
  isEmpty: function() {
8715
    return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
8716
  },
8717
 
8718
  _observe: function() {
8719
    var element = this.element,
8720
        parent  = this.parent,
8721
        eventMapping = {
8722
          focusin:  "focus",
8723
          focusout: "blur"
8724
        },
8725
        /**
8726
         * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
8727
         * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
8728
         */
8729
        events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
8730
 
8731
    parent.observe("beforeload", function() {
8732
      wysihtml5.dom.observe(element, events, function(event) {
8733
        var eventName = eventMapping[event.type] || event.type;
8734
        parent.fire(eventName).fire(eventName + ":textarea");
8735
      });
8736
 
8737
      wysihtml5.dom.observe(element, ["paste", "drop"], function() {
8738
        setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
8739
      });
8740
    });
8741
  }
8742
});/**
8743
 * Toolbar Dialog
8744
 *
8745
 * @param {Element} link The toolbar link which causes the dialog to show up
8746
 * @param {Element} container The dialog container
8747
 *
8748
 * @example
8749
 *    <!-- Toolbar link -->
8750
 *    <a data-wysihtml5-command="insertImage">insert an image</a>
8751
 *
8752
 *    <!-- Dialog -->
8753
 *    <div data-wysihtml5-dialog="insertImage" style="display: none;">
8754
 *      <label>
8755
 *        URL: <input data-wysihtml5-dialog-field="src" value="http://">
8756
 *      </label>
8757
 *      <label>
8758
 *        Alternative text: <input data-wysihtml5-dialog-field="alt" value="">
8759
 *      </label>
8760
 *    </div>
8761
 *
8762
 *    <script>
8763
 *      var dialog = new wysihtml5.toolbar.Dialog(
8764
 *        document.querySelector("[data-wysihtml5-command='insertImage']"),
8765
 *        document.querySelector("[data-wysihtml5-dialog='insertImage']")
8766
 *      );
8767
 *      dialog.observe("save", function(attributes) {
8768
 *        // do something
8769
 *      });
8770
 *    </script>
8771
 */
8772
(function(wysihtml5) {
8773
  var dom                     = wysihtml5.dom,
8774
      CLASS_NAME_OPENED       = "wysihtml5-command-dialog-opened",
8775
      SELECTOR_FORM_ELEMENTS  = "input, select, textarea",
8776
      SELECTOR_FIELDS         = "[data-wysihtml5-dialog-field]",
8777
      ATTRIBUTE_FIELDS        = "data-wysihtml5-dialog-field";
8778
 
8779
 
8780
  wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend(
8781
    /** @scope wysihtml5.toolbar.Dialog.prototype */ {
8782
    constructor: function(link, container) {
8783
      this.link       = link;
8784
      this.container  = container;
8785
    },
8786
 
8787
    _observe: function() {
8788
      if (this._observed) {
8789
        return;
8790
      }
8791
 
8792
      var that = this,
8793
          callbackWrapper = function(event) {
8794
            var attributes = that._serialize();
8795
            if (attributes == that.elementToChange) {
8796
              that.fire("edit", attributes);
8797
            } else {
8798
              that.fire("save", attributes);
8799
            }
8800
            that.hide();
8801
            event.preventDefault();
8802
            event.stopPropagation();
8803
          };
8804
 
8805
      dom.observe(that.link, "click", function(event) {
8806
        if (dom.hasClass(that.link, CLASS_NAME_OPENED)) {
8807
          setTimeout(function() { that.hide(); }, 0);
8808
        }
8809
      });
8810
 
8811
      dom.observe(this.container, "keydown", function(event) {
8812
        var keyCode = event.keyCode;
8813
        if (keyCode === wysihtml5.ENTER_KEY) {
8814
          callbackWrapper(event);
8815
        }
8816
        if (keyCode === wysihtml5.ESCAPE_KEY) {
8817
          that.hide();
8818
        }
8819
      });
8820
 
8821
      dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper);
8822
 
8823
      dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) {
8824
        that.fire("cancel");
8825
        that.hide();
8826
        event.preventDefault();
8827
        event.stopPropagation();
8828
      });
8829
 
8830
      var formElements  = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS),
8831
          i             = 0,
8832
          length        = formElements.length,
8833
          _clearInterval = function() { clearInterval(that.interval); };
8834
      for (; i<length; i++) {
8835
        dom.observe(formElements[i], "change", _clearInterval);
8836
      }
8837
 
8838
      this._observed = true;
8839
    },
8840
 
8841
    /**
8842
     * Grabs all fields in the dialog and puts them in key=>value style in an object which
8843
     * then gets returned
8844
     */
8845
    _serialize: function() {
8846
      var data    = this.elementToChange || {},
8847
          fields  = this.container.querySelectorAll(SELECTOR_FIELDS),
8848
          length  = fields.length,
8849
          i       = 0;
8850
      for (; i<length; i++) {
8851
        data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
8852
      }
8853
      return data;
8854
    },
8855
 
8856
    /**
8857
     * Takes the attributes of the "elementToChange"
8858
     * and inserts them in their corresponding dialog input fields
8859
     * 
8860
     * Assume the "elementToChange" looks like this:
8861
     *    <a href="http://www.google.com" target="_blank">foo</a>
8862
     *
8863
     * and we have the following dialog:
8864
     *    <input type="text" data-wysihtml5-dialog-field="href" value="">
8865
     *    <input type="text" data-wysihtml5-dialog-field="target" value="">
8866
     * 
8867
     * after calling _interpolate() the dialog will look like this
8868
     *    <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com">
8869
     *    <input type="text" data-wysihtml5-dialog-field="target" value="_blank">
8870
     *
8871
     * Basically it adopted the attribute values into the corresponding input fields
8872
     *
8873
     */
8874
    _interpolate: function(avoidHiddenFields) {
8875
      var field,
8876
          fieldName,
8877
          newValue,
8878
          focusedElement = document.querySelector(":focus"),
8879
          fields         = this.container.querySelectorAll(SELECTOR_FIELDS),
8880
          length         = fields.length,
8881
          i              = 0;
8882
      for (; i<length; i++) {
8883
        field = fields[i];
8884
 
8885
        // Never change elements where the user is currently typing in
8886
        if (field === focusedElement) {
8887
          continue;
8888
        }
8889
 
8890
        // Don't update hidden fields
8891
        // See https://github.com/xing/wysihtml5/pull/14
8892
        if (avoidHiddenFields && field.type === "hidden") {
8893
          continue;
8894
        }
8895
 
8896
        fieldName = field.getAttribute(ATTRIBUTE_FIELDS);
8897
        newValue  = this.elementToChange ? (this.elementToChange[fieldName] || "") : field.defaultValue;
8898
        field.value = newValue;
8899
      }
8900
    },
8901
 
8902
    /**
8903
     * Show the dialog element
8904
     */
8905
    show: function(elementToChange) {
8906
      var that        = this,
8907
          firstField  = this.container.querySelector(SELECTOR_FORM_ELEMENTS);
8908
      this.elementToChange = elementToChange;
8909
      this._observe();
8910
      this._interpolate();
8911
      if (elementToChange) {
8912
        this.interval = setInterval(function() { that._interpolate(true); }, 500);
8913
      }
8914
      dom.addClass(this.link, CLASS_NAME_OPENED);
8915
      this.container.style.display = "";
8916
      this.fire("show");
8917
      if (firstField && !elementToChange) {
8918
        try {
8919
          firstField.focus();
8920
        } catch(e) {}
8921
      }
8922
    },
8923
 
8924
    /**
8925
     * Hide the dialog element
8926
     */
8927
    hide: function() {
8928
      clearInterval(this.interval);
8929
      this.elementToChange = null;
8930
      dom.removeClass(this.link, CLASS_NAME_OPENED);
8931
      this.container.style.display = "none";
8932
      this.fire("hide");
8933
    }
8934
  });
8935
})(wysihtml5);
8936
/**
8937
 * Converts speech-to-text and inserts this into the editor
8938
 * As of now (2011/03/25) this only is supported in Chrome >= 11
8939
 *
8940
 * Note that it sends the recorded audio to the google speech recognition api:
8941
 * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec
8942
 *
8943
 * Current HTML5 draft can be found here
8944
 * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
8945
 * 
8946
 * "Accessing Google Speech API Chrome 11"
8947
 * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
8948
 */
8949
(function(wysihtml5) {
8950
  var dom = wysihtml5.dom;
8951
 
8952
  var linkStyles = {
8953
    position: "relative"
8954
  };
8955
 
8956
  var wrapperStyles = {
8957
    left:     0,
8958
    margin:   0,
8959
    opacity:  0,
8960
    overflow: "hidden",
8961
    padding:  0,
8962
    position: "absolute",
8963
    top:      0,
8964
    zIndex:   1
8965
  };
8966
 
8967
  var inputStyles = {
8968
    cursor:     "inherit",
8969
    fontSize:   "50px",
8970
    height:     "50px",
8971
    marginTop:  "-25px",
8972
    outline:    0,
8973
    padding:    0,
8974
    position:   "absolute",
8975
    right:      "-4px",
8976
    top:        "50%"
8977
  };
8978
 
8979
  var inputAttributes = {
8980
    "x-webkit-speech": "",
8981
    "speech":          ""
8982
  };
8983
 
8984
  wysihtml5.toolbar.Speech = function(parent, link) {
8985
    var input = document.createElement("input");
8986
    if (!wysihtml5.browser.supportsSpeechApiOn(input)) {
8987
      link.style.display = "none";
8988
      return;
8989
    }
8990
 
8991
    var wrapper = document.createElement("div");
8992
 
8993
    wysihtml5.lang.object(wrapperStyles).merge({
8994
      width:  link.offsetWidth  + "px",
8995
      height: link.offsetHeight + "px"
8996
    });
8997
 
8998
    dom.insert(input).into(wrapper);
8999
    dom.insert(wrapper).into(link);
9000
 
9001
    dom.setStyles(inputStyles).on(input);
9002
    dom.setAttributes(inputAttributes).on(input)
9003
 
9004
    dom.setStyles(wrapperStyles).on(wrapper);
9005
    dom.setStyles(linkStyles).on(link);
9006
 
9007
    var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange";
9008
    dom.observe(input, eventName, function() {
9009
      parent.execCommand("insertText", input.value);
9010
      input.value = "";
9011
    });
9012
 
9013
    dom.observe(input, "click", function(event) {
9014
      if (dom.hasClass(link, "wysihtml5-command-disabled")) {
9015
        event.preventDefault();
9016
      }
9017
 
9018
      event.stopPropagation();
9019
    });
9020
  };
9021
})(wysihtml5);/**
9022
 * Toolbar
9023
 *
9024
 * @param {Object} parent Reference to instance of Editor instance
9025
 * @param {Element} container Reference to the toolbar container element
9026
 *
9027
 * @example
9028
 *    <div id="toolbar">
9029
 *      <a data-wysihtml5-command="createLink">insert link</a>
9030
 *      <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a>
9031
 *    </div>
9032
 *
9033
 *    <script>
9034
 *      var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar"));
9035
 *    </script>
9036
 */
9037
(function(wysihtml5) {
9038
  var CLASS_NAME_COMMAND_DISABLED   = "wysihtml5-command-disabled",
9039
      CLASS_NAME_COMMANDS_DISABLED  = "wysihtml5-commands-disabled",
9040
      CLASS_NAME_COMMAND_ACTIVE     = "wysihtml5-command-active",
9041
      dom                           = wysihtml5.dom;
9042
 
9043
  wysihtml5.toolbar.Toolbar = Base.extend(
9044
    /** @scope wysihtml5.toolbar.Toolbar.prototype */ {
9045
    constructor: function(editor, container) {
9046
      this.editor     = editor;
9047
      this.container  = typeof(container) === "string" ? document.getElementById(container) : container;
9048
      this.composer   = editor.composer;
9049
 
9050
      this._getLinks("command");
9051
      this._getLinks("action");
9052
 
9053
      this._observe();
9054
      this.show();
9055
 
9056
      var speechInputLinks  = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),
9057
          length            = speechInputLinks.length,
9058
          i                 = 0;
9059
      for (; i<length; i++) {
9060
        new wysihtml5.toolbar.Speech(this, speechInputLinks[i]);
9061
      }
9062
    },
9063
 
9064
    _getLinks: function(type) {
9065
      var links   = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("a[data-wysihtml5-" + type + "]")).get(),
9066
          length  = links.length,
9067
          i       = 0,
9068
          mapping = this[type + "Mapping"] = {},
9069
          link,
9070
          name,
9071
          value,
9072
          dialog;
9073
      for (; i<length; i++) {
9074
        link    = links[i];
9075
        name    = link.getAttribute("data-wysihtml5-" + type);
9076
        value   = link.getAttribute("data-wysihtml5-" + type + "-value");
9077
        dialog  = this._getDialog(link, name);
9078
 
9079
        mapping[name + ":" + value] = {
9080
          link:   link,
9081
          name:   name,
9082
          value:  value,
9083
          dialog: dialog,
9084
          state:  false
9085
        };
9086
      }
9087
    },
9088
 
9089
    _getDialog: function(link, command) {
9090
      var that          = this,
9091
          dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"),
9092
          dialog,
9093
          caretBookmark;
9094
 
9095
      if (dialogElement) {
9096
        dialog = new wysihtml5.toolbar.Dialog(link, dialogElement);
9097
 
9098
        dialog.observe("show", function() {
9099
          caretBookmark = that.composer.selection.getBookmark();
9100
 
9101
          that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
9102
        });
9103
 
9104
        dialog.observe("save", function(attributes) {
9105
          if (caretBookmark) {
9106
            that.composer.selection.setBookmark(caretBookmark);
9107
          }
9108
          that._execCommand(command, attributes);
9109
 
9110
          that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
9111
        });
9112
 
9113
        dialog.observe("cancel", function() {
9114
          that.editor.focus(false);
9115
          that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
9116
        });
9117
      }
9118
      return dialog;
9119
    },
9120
 
9121
    /**
9122
     * @example
9123
     *    var toolbar = new wysihtml5.Toolbar();
9124
     *    // Insert a <blockquote> element or wrap current selection in <blockquote>
9125
     *    toolbar.execCommand("formatBlock", "blockquote");
9126
     */
9127
    execCommand: function(command, commandValue) {
9128
      if (this.commandsDisabled) {
9129
        return;
9130
      }
9131
 
9132
      var commandObj = this.commandMapping[command + ":" + commandValue];
9133
 
9134
      // Show dialog when available
9135
      if (commandObj && commandObj.dialog && !commandObj.state) {
9136
        commandObj.dialog.show();
9137
      } else {
9138
        this._execCommand(command, commandValue);
9139
      }
9140
    },
9141
 
9142
    _execCommand: function(command, commandValue) {
9143
      // Make sure that composer is focussed (false => don't move caret to the end)
9144
      this.editor.focus(false);
9145
 
9146
      this.composer.commands.exec(command, commandValue);
9147
      this._updateLinkStates();
9148
    },
9149
 
9150
    execAction: function(action) {
9151
      var editor = this.editor;
9152
      switch(action) {
9153
        case "change_view":
9154
          if (editor.currentView === editor.textarea) {
9155
            editor.fire("change_view", "composer");
9156
          } else {
9157
            editor.fire("change_view", "textarea");
9158
          }
9159
          break;
9160
      }
9161
    },
9162
 
9163
    _observe: function() {
9164
      var that      = this,
9165
          editor    = this.editor,
9166
          container = this.container,
9167
          links     = this.commandLinks.concat(this.actionLinks),
9168
          length    = links.length,
9169
          i         = 0;
9170
 
9171
      for (; i<length; i++) {
9172
        // 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied
9173
        // (you know, a:link { ... } doesn't match anchors with missing href attribute)
9174
        dom.setAttributes({
9175
          href:         "javascript:;",
9176
          unselectable: "on"
9177
        }).on(links[i]);
9178
      }
9179
 
9180
      // Needed for opera
9181
      dom.delegate(container, "[data-wysihtml5-command]", "mousedown", function(event) { event.preventDefault(); });
9182
 
9183
      dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) {
9184
        var link          = this,
9185
            command       = link.getAttribute("data-wysihtml5-command"),
9186
            commandValue  = link.getAttribute("data-wysihtml5-command-value");
9187
        that.execCommand(command, commandValue);
9188
        event.preventDefault();
9189
      });
9190
 
9191
      dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) {
9192
        var action = this.getAttribute("data-wysihtml5-action");
9193
        that.execAction(action);
9194
        event.preventDefault();
9195
      });
9196
 
9197
      editor.observe("focus:composer", function() {
9198
        that.bookmark = null;
9199
        clearInterval(that.interval);
9200
        that.interval = setInterval(function() { that._updateLinkStates(); }, 500);
9201
      });
9202
 
9203
      editor.observe("blur:composer", function() {
9204
        clearInterval(that.interval);
9205
      });
9206
 
9207
      editor.observe("destroy:composer", function() {
9208
        clearInterval(that.interval);
9209
      });
9210
 
9211
      editor.observe("change_view", function(currentView) {
9212
        // Set timeout needed in order to let the blur event fire first
9213
        setTimeout(function() {
9214
          that.commandsDisabled = (currentView !== "composer");
9215
          that._updateLinkStates();
9216
          if (that.commandsDisabled) {
9217
            dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED);
9218
          } else {
9219
            dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED);
9220
          }
9221
        }, 0);
9222
      });
9223
    },
9224
 
9225
    _updateLinkStates: function() {
9226
      var element           = this.composer.element,
9227
          commandMapping    = this.commandMapping,
9228
          i,
9229
          state,
9230
          command;
9231
      // every millisecond counts... this is executed quite often
9232
      for (i in commandMapping) {
9233
        command = commandMapping[i];
9234
        if (this.commandsDisabled) {
9235
          state = false;
9236
          dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
9237
          if (command.dialog) {
9238
            command.dialog.hide();
9239
          }
9240
        } else {
9241
          state = this.composer.commands.state(command.name, command.value);
9242
          if (wysihtml5.lang.object(state).isArray()) {
9243
            // Grab first and only object/element in state array, otherwise convert state into boolean
9244
            // to avoid showing a dialog for multiple selected elements which may have different attributes
9245
            // eg. when two links with different href are selected, the state will be an array consisting of both link elements
9246
            // but the dialog interface can only update one
9247
            state = state.length === 1 ? state[0] : true;
9248
          }
9249
          dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED);
9250
        }
9251
 
9252
        if (command.state === state) {
9253
          continue;
9254
        }
9255
 
9256
        command.state = state;
9257
        if (state) {
9258
          dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
9259
          if (command.dialog) {
9260
            if (typeof(state) === "object") {
9261
              command.dialog.show(state);
9262
            } else {
9263
              command.dialog.hide();
9264
            }
9265
          }
9266
        } else {
9267
          dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
9268
          if (command.dialog) {
9269
            command.dialog.hide();
9270
          }
9271
        }
9272
      }
9273
    },
9274
 
9275
    show: function() {
9276
      this.container.style.display = "";
9277
    },
9278
 
9279
    hide: function() {
9280
      this.container.style.display = "none";
9281
    }
9282
  });
9283
 
9284
})(wysihtml5);
9285
/**
9286
 * WYSIHTML5 Editor
9287
 *
9288
 * @param {Element} textareaElement Reference to the textarea which should be turned into a rich text interface
9289
 * @param {Object} [config] See defaultConfig object below for explanation of each individual config option
9290
 *
9291
 * @events
9292
 *    load
9293
 *    beforeload (for internal use only)
9294
 *    focus
9295
 *    focus:composer
9296
 *    focus:textarea
9297
 *    blur
9298
 *    blur:composer
9299
 *    blur:textarea
9300
 *    change
9301
 *    change:composer
9302
 *    change:textarea
9303
 *    paste
9304
 *    paste:composer
9305
 *    paste:textarea
9306
 *    newword:composer
9307
 *    destroy:composer
9308
 *    undo:composer
9309
 *    redo:composer
9310
 *    beforecommand:composer
9311
 *    aftercommand:composer
9312
 *    change_view
9313
 */
9314
(function(wysihtml5) {
9315
  var undef;
9316
 
9317
  var defaultConfig = {
9318
    // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body 
9319
    name:                 undef,
9320
    // Whether the editor should look like the textarea (by adopting styles)
9321
    style:                true,
9322
    // Id of the toolbar element, pass falsey value if you don't want any toolbar logic
9323
    toolbar:              undef,
9324
    // Whether urls, entered by the user should automatically become clickable-links
9325
    autoLink:             true,
9326
    // Object which includes parser rules to apply when html gets inserted via copy & paste
9327
    // See parser_rules/*.js for examples
9328
    parserRules:          { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} },
9329
    // Parser method to use when the user inserts content via copy & paste
9330
    parser:               wysihtml5.dom.parse,
9331
    // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
9332
    composerClassName:    "wysihtml5-editor",
9333
    // Class name to add to the body when the wysihtml5 editor is supported
9334
    bodyClassName:        "wysihtml5-supported",
9335
    // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
9336
    stylesheets:          [],
9337
    // Placeholder text to use, defaults to the placeholder attribute on the textarea element
9338
    placeholderText:      undef,
9339
    // Whether the composer should allow the user to manually resize images, tables etc.
9340
    allowObjectResizing:  true,
9341
    // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5)
9342
    supportTouchDevices:  true
9343
  };
9344
 
9345
  wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend(
9346
    /** @scope wysihtml5.Editor.prototype */ {
9347
    constructor: function(textareaElement, config) {
9348
      this.textareaElement  = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement;
9349
      this.config           = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();
9350
      this.textarea         = new wysihtml5.views.Textarea(this, this.textareaElement, this.config);
9351
      this.currentView      = this.textarea;
9352
      this._isCompatible    = wysihtml5.browser.supported();
9353
 
9354
      // Sort out unsupported/unwanted browsers here
9355
      if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) {
9356
        var that = this;
9357
        setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
9358
        return;
9359
      }
9360
 
9361
      // Add class name to body, to indicate that the editor is supported
9362
      wysihtml5.dom.addClass(document.body, this.config.bodyClassName);
9363
 
9364
      this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config);
9365
      this.currentView = this.composer;
9366
 
9367
      if (typeof(this.config.parser) === "function") {
9368
        this._initParser();
9369
      }
9370
 
9371
      this.observe("beforeload", function() {
9372
        this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer);
9373
        if (this.config.toolbar) {
9374
          this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar);
9375
        }
9376
      });
9377
 
9378
      try {
9379
        console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5");
9380
      } catch(e) {}
9381
    },
9382
 
9383
    isCompatible: function() {
9384
      return this._isCompatible;
9385
    },
9386
 
9387
    clear: function() {
9388
      this.currentView.clear();
9389
      return this;
9390
    },
9391
 
9392
    getValue: function(parse) {
9393
      return this.currentView.getValue(parse);
9394
    },
9395
 
9396
    setValue: function(html, parse) {
9397
      if (!html) {
9398
        return this.clear();
9399
      }
9400
      this.currentView.setValue(html, parse);
9401
      return this;
9402
    },
9403
 
9404
    focus: function(setToEnd) {
9405
      this.currentView.focus(setToEnd);
9406
      return this;
9407
    },
9408
 
9409
    /**
9410
     * Deactivate editor (make it readonly)
9411
     */
9412
    disable: function() {
9413
      this.currentView.disable();
9414
      return this;
9415
    },
9416
 
9417
    /**
9418
     * Activate editor
9419
     */
9420
    enable: function() {
9421
      this.currentView.enable();
9422
      return this;
9423
    },
9424
 
9425
    isEmpty: function() {
9426
      return this.currentView.isEmpty();
9427
    },
9428
 
9429
    hasPlaceholderSet: function() {
9430
      return this.currentView.hasPlaceholderSet();
9431
    },
9432
 
9433
    parse: function(htmlOrElement) {
9434
      var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true);
9435
      if (typeof(htmlOrElement) === "object") {
9436
        wysihtml5.quirks.redraw(htmlOrElement);
9437
      }
9438
      return returnValue;
9439
    },
9440
 
9441
    /**
9442
     * Prepare html parser logic
9443
     *  - Observes for paste and drop
9444
     */
9445
    _initParser: function() {
9446
      this.observe("paste:composer", function() {
9447
        var keepScrollPosition  = true,
9448
            that                = this;
9449
        that.composer.selection.executeAndRestore(function() {
9450
          wysihtml5.quirks.cleanPastedHTML(that.composer.element);
9451
          that.parse(that.composer.element);
9452
        }, keepScrollPosition);
9453
      });
9454
 
9455
      this.observe("paste:textarea", function() {
9456
        var value   = this.textarea.getValue(),
9457
            newValue;
9458
        newValue = this.parse(value);
9459
        this.textarea.setValue(newValue);
9460
      });
9461
    }
9462
  });
9463
})(wysihtml5);