Subversion Repositories Integrator Subversion

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 espaco 1
/**
2
 * @license wysihtml5 v0.3.0
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",
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 || isWebKit,
3561
        "insertOrderedList":    isIE || isOpera || isWebKit
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
        lineBreaks        = element.querySelectorAll("br"),
4152
        lineBreaksLength  = lineBreaks.length,
4153
        childNodes,
4154
        childNodesLength,
4155
        childNode,
4156
        lineBreak,
4157
        parentNode,
4158
        isBlockElement,
4159
        isLineBreak,
4160
        currentListItem,
4161
        i;
4162
 
4163
    // First find <br> at the end of inline elements and move them behind them
4164
    for (i=0; i<lineBreaksLength; i++) {
4165
      lineBreak = lineBreaks[i];
4166
      while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) {
4167
        if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") {
4168
          parentNode.removeChild(lineBreak);
4169
          break;
4170
        }
4171
        wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode);
4172
      }
4173
    }
4174
 
4175
    childNodes        = wysihtml5.lang.array(element.childNodes).get();
4176
    childNodesLength  = childNodes.length;
4177
 
4178
    for (i=0; i<childNodesLength; i++) {
4179
      currentListItem   = currentListItem || _createListItem(doc, list);
4180
      childNode         = childNodes[i];
4181
      isBlockElement    = wysihtml5.dom.getStyle("display").from(childNode) === "block";
4182
      isLineBreak       = childNode.nodeName === "BR";
4183
 
4184
      if (isBlockElement) {
4185
        // Append blockElement to current <li> if empty, otherwise create a new one
4186
        currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;
4187
        currentListItem.appendChild(childNode);
4188
        currentListItem = null;
4189
        continue;
4190
      }
4191
 
4192
      if (isLineBreak) {
4193
        // Only create a new list item in the next iteration when the current one has already content
4194
        currentListItem = currentListItem.firstChild ? null : currentListItem;
4195
        continue;
4196
      }
4197
 
4198
      currentListItem.appendChild(childNode);
4199
    }
4200
 
4201
    element.parentNode.replaceChild(list, element);
4202
    return list;
4203
  }
4204
 
4205
  return convertToList;
4206
})();/**
4207
 * Copy a set of attributes from one element to another
4208
 *
4209
 * @param {Array} attributesToCopy List of attributes which should be copied
4210
 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
4211
 *    copy the attributes from., this again returns an object which provides a method named "to" which can be invoked
4212
 *    with the element where to copy the attributes to (see example)
4213
 *
4214
 * @example
4215
 *    var textarea    = document.querySelector("textarea"),
4216
 *        div         = document.querySelector("div[contenteditable=true]"),
4217
 *        anotherDiv  = document.querySelector("div.preview");
4218
 *    wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
4219
 *
4220
 */
4221
wysihtml5.dom.copyAttributes = function(attributesToCopy) {
4222
  return {
4223
    from: function(elementToCopyFrom) {
4224
      return {
4225
        to: function(elementToCopyTo) {
4226
          var attribute,
4227
              i         = 0,
4228
              length    = attributesToCopy.length;
4229
          for (; i<length; i++) {
4230
            attribute = attributesToCopy[i];
4231
            if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") {
4232
              elementToCopyTo[attribute] = elementToCopyFrom[attribute];
4233
            }
4234
          }
4235
          return { andTo: arguments.callee };
4236
        }
4237
      };
4238
    }
4239
  };
4240
};/**
4241
 * Copy a set of styles from one element to another
4242
 * Please note that this only works properly across browsers when the element from which to copy the styles
4243
 * is in the dom
4244
 *
4245
 * Interesting article on how to copy styles
4246
 *
4247
 * @param {Array} stylesToCopy List of styles which should be copied
4248
 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
4249
 *    copy the styles from., this again returns an object which provides a method named "to" which can be invoked
4250
 *    with the element where to copy the styles to (see example)
4251
 *
4252
 * @example
4253
 *    var textarea    = document.querySelector("textarea"),
4254
 *        div         = document.querySelector("div[contenteditable=true]"),
4255
 *        anotherDiv  = document.querySelector("div.preview");
4256
 *    wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);
4257
 *
4258
 */
4259
(function(dom) {
4260
 
4261
  /**
4262
   * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set
4263
   * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then
4264
   * its computed css width will be 198px
4265
   */
4266
  var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];
4267
 
4268
  var shouldIgnoreBoxSizingBorderBox = function(element) {
4269
    if (hasBoxSizingBorderBox(element)) {
4270
       return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
4271
    }
4272
    return false;
4273
  };
4274
 
4275
  var hasBoxSizingBorderBox = function(element) {
4276
    var i       = 0,
4277
        length  = BOX_SIZING_PROPERTIES.length;
4278
    for (; i<length; i++) {
4279
      if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
4280
        return BOX_SIZING_PROPERTIES[i];
4281
      }
4282
    }
4283
  };
4284
 
4285
  dom.copyStyles = function(stylesToCopy) {
4286
    return {
4287
      from: function(element) {
4288
        if (shouldIgnoreBoxSizingBorderBox(element)) {
4289
          stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
4290
        }
4291
 
4292
        var cssText = "",
4293
            length  = stylesToCopy.length,
4294
            i       = 0,
4295
            property;
4296
        for (; i<length; i++) {
4297
          property = stylesToCopy[i];
4298
          cssText += property + ":" + dom.getStyle(property).from(element) + ";";
4299
        }
4300
 
4301
        return {
4302
          to: function(element) {
4303
            dom.setStyles(cssText).on(element);
4304
            return { andTo: arguments.callee };
4305
          }
4306
        };
4307
      }
4308
    };
4309
  };
4310
})(wysihtml5.dom);/**
4311
 * Event Delegation
4312
 *
4313
 * @example
4314
 *    wysihtml5.dom.delegate(document.body, "a", "click", function() {
4315
 *      // foo
4316
 *    });
4317
 */
4318
(function(wysihtml5) {
4319
 
4320
  wysihtml5.dom.delegate = function(container, selector, eventName, handler) {
4321
    return wysihtml5.dom.observe(container, eventName, function(event) {
4322
      var target    = event.target,
4323
          match     = wysihtml5.lang.array(container.querySelectorAll(selector));
4324
 
4325
      while (target && target !== container) {
4326
        if (match.contains(target)) {
4327
          handler.call(target, event);
4328
          break;
4329
        }
4330
        target = target.parentNode;
4331
      }
4332
    });
4333
  };
4334
 
4335
})(wysihtml5);/**
4336
 * Returns the given html wrapped in a div element
4337
 *
4338
 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
4339
 * when inserted via innerHTML
4340
 *
4341
 * @param {String} html The html which should be wrapped in a dom element
4342
 * @param {Obejct} [context] Document object of the context the html belongs to
4343
 *
4344
 * @example
4345
 *    wysihtml5.dom.getAsDom("<article>foo</article>");
4346
 */
4347
wysihtml5.dom.getAsDom = (function() {
4348
 
4349
  var _innerHTMLShiv = function(html, context) {
4350
    var tempElement = context.createElement("div");
4351
    tempElement.style.display = "none";
4352
    context.body.appendChild(tempElement);
4353
    // IE throws an exception when trying to insert <frameset></frameset> via innerHTML
4354
    try { tempElement.innerHTML = html; } catch(e) {}
4355
    context.body.removeChild(tempElement);
4356
    return tempElement;
4357
  };
4358
 
4359
  /**
4360
   * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
4361
   */
4362
  var _ensureHTML5Compatibility = function(context) {
4363
    if (context._wysihtml5_supportsHTML5Tags) {
4364
      return;
4365
    }
4366
    for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {
4367
      context.createElement(HTML5_ELEMENTS[i]);
4368
    }
4369
    context._wysihtml5_supportsHTML5Tags = true;
4370
  };
4371
 
4372
 
4373
  /**
4374
   * List of html5 tags
4375
   * taken from http://simon.html5.org/html5-elements
4376
   */
4377
  var HTML5_ELEMENTS = [
4378
    "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption",
4379
    "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress",
4380
    "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr"
4381
  ];
4382
 
4383
  return function(html, context) {
4384
    context = context || document;
4385
    var tempElement;
4386
    if (typeof(html) === "object" && html.nodeType) {
4387
      tempElement = context.createElement("div");
4388
      tempElement.appendChild(html);
4389
    } else if (wysihtml5.browser.supportsHTML5Tags(context)) {
4390
      tempElement = context.createElement("div");
4391
      tempElement.innerHTML = html;
4392
    } else {
4393
      _ensureHTML5Compatibility(context);
4394
      tempElement = _innerHTMLShiv(html, context);
4395
    }
4396
    return tempElement;
4397
  };
4398
})();/**
4399
 * Walks the dom tree from the given node up until it finds a match
4400
 * Designed for optimal performance.
4401
 *
4402
 * @param {Element} node The from which to check the parent nodes
4403
 * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp)
4404
 * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
4405
 * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
4406
 * @example
4407
 *    var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] });
4408
 *    // ... or ...
4409
 *    var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" });
4410
 *    // ... or ...
4411
 *    var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g });
4412
 */
4413
wysihtml5.dom.getParentElement = (function() {
4414
 
4415
  function _isSameNodeName(nodeName, desiredNodeNames) {
4416
    if (!desiredNodeNames || !desiredNodeNames.length) {
4417
      return true;
4418
    }
4419
 
4420
    if (typeof(desiredNodeNames) === "string") {
4421
      return nodeName === desiredNodeNames;
4422
    } else {
4423
      return wysihtml5.lang.array(desiredNodeNames).contains(nodeName);
4424
    }
4425
  }
4426
 
4427
  function _isElement(node) {
4428
    return node.nodeType === wysihtml5.ELEMENT_NODE;
4429
  }
4430
 
4431
  function _hasClassName(element, className, classRegExp) {
4432
    var classNames = (element.className || "").match(classRegExp) || [];
4433
    if (!className) {
4434
      return !!classNames.length;
4435
    }
4436
    return classNames[classNames.length - 1] === className;
4437
  }
4438
 
4439
  function _getParentElementWithNodeName(node, nodeName, levels) {
4440
    while (levels-- && node && node.nodeName !== "BODY") {
4441
      if (_isSameNodeName(node.nodeName, nodeName)) {
4442
        return node;
4443
      }
4444
      node = node.parentNode;
4445
    }
4446
    return null;
4447
  }
4448
 
4449
  function _getParentElementWithNodeNameAndClassName(node, nodeName, className, classRegExp, levels) {
4450
    while (levels-- && node && node.nodeName !== "BODY") {
4451
      if (_isElement(node) &&
4452
          _isSameNodeName(node.nodeName, nodeName) &&
4453
          _hasClassName(node, className, classRegExp)) {
4454
        return node;
4455
      }
4456
      node = node.parentNode;
4457
    }
4458
    return null;
4459
  }
4460
 
4461
  return function(node, matchingSet, levels) {
4462
    levels = levels || 50; // Go max 50 nodes upwards from current node
4463
    if (matchingSet.className || matchingSet.classRegExp) {
4464
      return _getParentElementWithNodeNameAndClassName(
4465
        node, matchingSet.nodeName, matchingSet.className, matchingSet.classRegExp, levels
4466
      );
4467
    } else {
4468
      return _getParentElementWithNodeName(
4469
        node, matchingSet.nodeName, levels
4470
      );
4471
    }
4472
  };
4473
})();
4474
/**
4475
 * Get element's style for a specific css property
4476
 *
4477
 * @param {Element} element The element on which to retrieve the style
4478
 * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
4479
 *
4480
 * @example
4481
 *    wysihtml5.dom.getStyle("display").from(document.body);
4482
 *    // => "block"
4483
 */
4484
wysihtml5.dom.getStyle = (function() {
4485
  var stylePropertyMapping = {
4486
        "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
4487
      },
4488
      REG_EXP_CAMELIZE = /\-[a-z]/g;
4489
 
4490
  function camelize(str) {
4491
    return str.replace(REG_EXP_CAMELIZE, function(match) {
4492
      return match.charAt(1).toUpperCase();
4493
    });
4494
  }
4495
 
4496
  return function(property) {
4497
    return {
4498
      from: function(element) {
4499
        if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
4500
          return;
4501
        }
4502
 
4503
        var doc               = element.ownerDocument,
4504
            camelizedProperty = stylePropertyMapping[property] || camelize(property),
4505
            style             = element.style,
4506
            currentStyle      = element.currentStyle,
4507
            styleValue        = style[camelizedProperty];
4508
        if (styleValue) {
4509
          return styleValue;
4510
        }
4511
 
4512
        // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
4513
        // window.getComputedStyle, since it returns css property values in their original unit:
4514
        // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
4515
        // gives you the original "50%".
4516
        // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
4517
        if (currentStyle) {
4518
          try {
4519
                return currentStyle[camelizedProperty];
4520
          } catch(e) {
4521
            //ie will occasionally fail for unknown reasons. swallowing exception
4522
          }
4523
        }
4524
 
4525
        var win                 = doc.defaultView || doc.parentWindow,
4526
            needsOverflowReset  = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
4527
            originalOverflow,
4528
            returnValue;
4529
 
4530
        if (win.getComputedStyle) {
4531
          // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
4532
          // therfore we remove and restore the scrollbar and calculate the value in between
4533
          if (needsOverflowReset) {
4534
            originalOverflow = style.overflow;
4535
            style.overflow = "hidden";
4536
          }
4537
          returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
4538
          if (needsOverflowReset) {
4539
            style.overflow = originalOverflow || "";
4540
          }
4541
          return returnValue;
4542
        }
4543
      }
4544
    };
4545
  };
4546
})();/**
4547
 * High performant way to check whether an element with a specific tag name is in the given document
4548
 * Optimized for being heavily executed
4549
 * Unleashes the power of live node lists
4550
 *
4551
 * @param {Object} doc The document object of the context where to check
4552
 * @param {String} tagName Upper cased tag name
4553
 * @example
4554
 *    wysihtml5.dom.hasElementWithTagName(document, "IMG");
4555
 */
4556
wysihtml5.dom.hasElementWithTagName = (function() {
4557
  var LIVE_CACHE          = {},
4558
      DOCUMENT_IDENTIFIER = 1;
4559
 
4560
  function _getDocumentIdentifier(doc) {
4561
    return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
4562
  }
4563
 
4564
  return function(doc, tagName) {
4565
    var key         = _getDocumentIdentifier(doc) + ":" + tagName,
4566
        cacheEntry  = LIVE_CACHE[key];
4567
    if (!cacheEntry) {
4568
      cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
4569
    }
4570
 
4571
    return cacheEntry.length > 0;
4572
  };
4573
})();/**
4574
 * High performant way to check whether an element with a specific class name is in the given document
4575
 * Optimized for being heavily executed
4576
 * Unleashes the power of live node lists
4577
 *
4578
 * @param {Object} doc The document object of the context where to check
4579
 * @param {String} tagName Upper cased tag name
4580
 * @example
4581
 *    wysihtml5.dom.hasElementWithClassName(document, "foobar");
4582
 */
4583
(function(wysihtml5) {
4584
  var LIVE_CACHE          = {},
4585
      DOCUMENT_IDENTIFIER = 1;
4586
 
4587
  function _getDocumentIdentifier(doc) {
4588
    return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
4589
  }
4590
 
4591
  wysihtml5.dom.hasElementWithClassName = function(doc, className) {
4592
    // getElementsByClassName is not supported by IE<9
4593
    // but is sometimes mocked via library code (which then doesn't return live node lists)
4594
    if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) {
4595
      return !!doc.querySelector("." + className);
4596
    }
4597
 
4598
    var key         = _getDocumentIdentifier(doc) + ":" + className,
4599
        cacheEntry  = LIVE_CACHE[key];
4600
    if (!cacheEntry) {
4601
      cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
4602
    }
4603
 
4604
    return cacheEntry.length > 0;
4605
  };
4606
})(wysihtml5);
4607
wysihtml5.dom.insert = function(elementToInsert) {
4608
  return {
4609
    after: function(element) {
4610
      element.parentNode.insertBefore(elementToInsert, element.nextSibling);
4611
    },
4612
 
4613
    before: function(element) {
4614
      element.parentNode.insertBefore(elementToInsert, element);
4615
    },
4616
 
4617
    into: function(element) {
4618
      element.appendChild(elementToInsert);
4619
    }
4620
  };
4621
};wysihtml5.dom.insertCSS = function(rules) {
4622
  rules = rules.join("\n");
4623
 
4624
  return {
4625
    into: function(doc) {
4626
      var head         = doc.head || doc.getElementsByTagName("head")[0],
4627
          styleElement = doc.createElement("style");
4628
 
4629
      styleElement.type = "text/css";
4630
 
4631
      if (styleElement.styleSheet) {
4632
        styleElement.styleSheet.cssText = rules;
4633
      } else {
4634
        styleElement.appendChild(doc.createTextNode(rules));
4635
      }
4636
 
4637
      if (head) {
4638
        head.appendChild(styleElement);
4639
      }
4640
    }
4641
  };
4642
};/**
4643
 * Method to set dom events
4644
 *
4645
 * @example
4646
 *    wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
4647
 */
4648
wysihtml5.dom.observe = function(element, eventNames, handler) {
4649
  eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
4650
 
4651
  var handlerWrapper,
4652
      eventName,
4653
      i       = 0,
4654
      length  = eventNames.length;
4655
 
4656
  for (; i<length; i++) {
4657
    eventName = eventNames[i];
4658
    if (element.addEventListener) {
4659
      element.addEventListener(eventName, handler, false);
4660
    } else {
4661
      handlerWrapper = function(event) {
4662
        if (!("target" in event)) {
4663
          event.target = event.srcElement;
4664
        }
4665
        event.preventDefault = event.preventDefault || function() {
4666
          this.returnValue = false;
4667
        };
4668
        event.stopPropagation = event.stopPropagation || function() {
4669
          this.cancelBubble = true;
4670
        };
4671
        handler.call(element, event);
4672
      };
4673
      element.attachEvent("on" + eventName, handlerWrapper);
4674
    }
4675
  }
4676
 
4677
  return {
4678
    stop: function() {
4679
      var eventName,
4680
          i       = 0,
4681
          length  = eventNames.length;
4682
      for (; i<length; i++) {
4683
        eventName = eventNames[i];
4684
        if (element.removeEventListener) {
4685
          element.removeEventListener(eventName, handler, false);
4686
        } else {
4687
          element.detachEvent("on" + eventName, handlerWrapper);
4688
        }
4689
      }
4690
    }
4691
  };
4692
};
4693
/**
4694
 * HTML Sanitizer
4695
 * Rewrites the HTML based on given rules
4696
 *
4697
 * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized
4698
 * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will
4699
 *    be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the
4700
 *    desired substitution.
4701
 * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing
4702
 *
4703
 * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
4704
 *
4705
 * @example
4706
 *    var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';
4707
 *    wysihtml5.dom.parse(userHTML, {
4708
 *      tags {
4709
 *        p:      "div",      // Rename p tags to div tags
4710
 *        font:   "span"      // Rename font tags to span tags
4711
 *        div:    true,       // Keep them, also possible (same result when passing: "div" or true)
4712
 *        script: undefined   // Remove script elements
4713
 *      }
4714
 *    });
4715
 *    // => <div><div><span>foo bar</span></div></div>
4716
 *
4717
 *    var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';
4718
 *    wysihtml5.dom.parse(userHTML);
4719
 *    // => '<span><span><span><span>I'm a table!</span></span></span></span>'
4720
 *
4721
 *    var userHTML = '<div>foobar<br>foobar</div>';
4722
 *    wysihtml5.dom.parse(userHTML, {
4723
 *      tags: {
4724
 *        div: undefined,
4725
 *        br:  true
4726
 *      }
4727
 *    });
4728
 *    // => ''
4729
 *
4730
 *    var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
4731
 *    wysihtml5.dom.parse(userHTML, {
4732
 *      classes: {
4733
 *        red:    1,
4734
 *        green:  1
4735
 *      },
4736
 *      tags: {
4737
 *        div: {
4738
 *          rename_tag:     "p"
4739
 *        }
4740
 *      }
4741
 *    });
4742
 *    // => '<p class="red">foo</p><p>bar</p>'
4743
 */
4744
wysihtml5.dom.parse = (function() {
4745
 
4746
  /**
4747
   * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
4748
   * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the
4749
   * node isn't closed
4750
   *
4751
   * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
4752
   */
4753
  var NODE_TYPE_MAPPING = {
4754
        "1": _handleElement,
4755
        "3": _handleText
4756
      },
4757
      // Rename unknown tags to this
4758
      DEFAULT_NODE_NAME   = "span",
4759
      WHITE_SPACE_REG_EXP = /\s+/,
4760
      defaultRules        = { tags: {}, classes: {} },
4761
      currentRules        = {};
4762
 
4763
  /**
4764
   * Iterates over all childs of the element, recreates them, appends them into a document fragment
4765
   * which later replaces the entire body content
4766
   */
4767
  function parse(elementOrHtml, rules, context, cleanUp) {
4768
    wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules).get();
4769
 
4770
    context           = context || elementOrHtml.ownerDocument || document;
4771
    var fragment      = context.createDocumentFragment(),
4772
        isString      = typeof(elementOrHtml) === "string",
4773
        element,
4774
        newNode,
4775
        firstChild;
4776
 
4777
    if (isString) {
4778
      element = wysihtml5.dom.getAsDom(elementOrHtml, context);
4779
    } else {
4780
      element = elementOrHtml;
4781
    }
4782
 
4783
    while (element.firstChild) {
4784
      firstChild  = element.firstChild;
4785
      element.removeChild(firstChild);
4786
      newNode = _convert(firstChild, cleanUp);
4787
      if (newNode) {
4788
        fragment.appendChild(newNode);
4789
      }
4790
    }
4791
 
4792
    // Clear element contents
4793
    element.innerHTML = "";
4794
 
4795
    // Insert new DOM tree
4796
    element.appendChild(fragment);
4797
 
4798
    return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element;
4799
  }
4800
 
4801
  function _convert(oldNode, cleanUp) {
4802
    var oldNodeType     = oldNode.nodeType,
4803
        oldChilds       = oldNode.childNodes,
4804
        oldChildsLength = oldChilds.length,
4805
        newNode,
4806
        method          = NODE_TYPE_MAPPING[oldNodeType],
4807
        i               = 0;
4808
 
4809
    newNode = method && method(oldNode);
4810
 
4811
    if (!newNode) {
4812
      return null;
4813
    }
4814
 
4815
    for (i=0; i<oldChildsLength; i++) {
4816
      newChild = _convert(oldChilds[i], cleanUp);
4817
      if (newChild) {
4818
        newNode.appendChild(newChild);
4819
      }
4820
    }
4821
 
4822
    // Cleanup senseless <span> elements
4823
    if (cleanUp &&
4824
        newNode.childNodes.length <= 1 &&
4825
        newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
4826
        !newNode.attributes.length) {
4827
      return newNode.firstChild;
4828
    }
4829
 
4830
    return newNode;
4831
  }
4832
 
4833
  function _handleElement(oldNode) {
4834
    var rule,
4835
        newNode,
4836
        endTag,
4837
        tagRules    = currentRules.tags,
4838
        nodeName    = oldNode.nodeName.toLowerCase(),
4839
        scopeName   = oldNode.scopeName;
4840
 
4841
    /**
4842
     * We already parsed that element
4843
     * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
4844
     */
4845
    if (oldNode._wysihtml5) {
4846
      return null;
4847
    }
4848
    oldNode._wysihtml5 = 1;
4849
 
4850
    if (oldNode.className === "wysihtml5-temp") {
4851
      return null;
4852
    }
4853
 
4854
    /**
4855
     * IE is the only browser who doesn't include the namespace in the
4856
     * nodeName, that's why we have to prepend it by ourselves
4857
     * scopeName is a proprietary IE feature
4858
     * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
4859
     */
4860
    if (scopeName && scopeName != "HTML") {
4861
      nodeName = scopeName + ":" + nodeName;
4862
    }
4863
 
4864
    /**
4865
     * Repair node
4866
     * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
4867
     * 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
4868
     */
4869
    if ("outerHTML" in oldNode) {
4870
      if (!wysihtml5.browser.autoClosesUnclosedTags() &&
4871
          oldNode.nodeName === "P" &&
4872
          oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
4873
        nodeName = "div";
4874
      }
4875
    }
4876
 
4877
    if (nodeName in tagRules) {
4878
      rule = tagRules[nodeName];
4879
      if (!rule || rule.remove) {
4880
        return null;
4881
      }
4882
 
4883
      rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
4884
    } else if (oldNode.firstChild) {
4885
      rule = { rename_tag: DEFAULT_NODE_NAME };
4886
    } else {
4887
      // Remove empty unknown elements
4888
      return null;
4889
    }
4890
 
4891
    newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName);
4892
    _handleAttributes(oldNode, newNode, rule);
4893
 
4894
    oldNode = null;
4895
    return newNode;
4896
  }
4897
 
4898
  function _handleAttributes(oldNode, newNode, rule) {
4899
    var attributes          = {},                         // fresh new set of attributes to set on newNode
4900
        setClass            = rule.set_class,             // classes to set
4901
        addClass            = rule.add_class,             // add classes based on existing attributes
4902
        setAttributes       = rule.set_attributes,        // attributes to set on the current node
4903
        checkAttributes     = rule.check_attributes,      // check/convert values of attributes
4904
        allowedClasses      = currentRules.classes,
4905
        i                   = 0,
4906
        classes             = [],
4907
        newClasses          = [],
4908
        newUniqueClasses    = [],
4909
        oldClasses          = [],
4910
        classesLength,
4911
        newClassesLength,
4912
        currentClass,
4913
        newClass,
4914
        attributeName,
4915
        newAttributeValue,
4916
        method;
4917
 
4918
    if (setAttributes) {
4919
      attributes = wysihtml5.lang.object(setAttributes).clone();
4920
    }
4921
 
4922
    if (checkAttributes) {
4923
      for (attributeName in checkAttributes) {
4924
        method = attributeCheckMethods[checkAttributes[attributeName]];
4925
        if (!method) {
4926
          continue;
4927
        }
4928
        newAttributeValue = method(_getAttribute(oldNode, attributeName));
4929
        if (typeof(newAttributeValue) === "string") {
4930
          attributes[attributeName] = newAttributeValue;
4931
        }
4932
      }
4933
    }
4934
 
4935
    if (setClass) {
4936
      classes.push(setClass);
4937
    }
4938
 
4939
    if (addClass) {
4940
      for (attributeName in addClass) {
4941
        method = addClassMethods[addClass[attributeName]];
4942
        if (!method) {
4943
          continue;
4944
        }
4945
        newClass = method(_getAttribute(oldNode, attributeName));
4946
        if (typeof(newClass) === "string") {
4947
          classes.push(newClass);
4948
        }
4949
      }
4950
    }
4951
 
4952
    // make sure that wysihtml5 temp class doesn't get stripped out
4953
    allowedClasses["_wysihtml5-temp-placeholder"] = 1;
4954
 
4955
    // add old classes last
4956
    oldClasses = oldNode.getAttribute("class");
4957
    if (oldClasses) {
4958
      classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
4959
    }
4960
    classesLength = classes.length;
4961
    for (; i<classesLength; i++) {
4962
      currentClass = classes[i];
4963
      if (allowedClasses[currentClass]) {
4964
        newClasses.push(currentClass);
4965
      }
4966
    }
4967
 
4968
    // remove duplicate entries and preserve class specificity
4969
    newClassesLength = newClasses.length;
4970
    while (newClassesLength--) {
4971
      currentClass = newClasses[newClassesLength];
4972
      if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) {
4973
        newUniqueClasses.unshift(currentClass);
4974
      }
4975
    }
4976
 
4977
    if (newUniqueClasses.length) {
4978
      attributes["class"] = newUniqueClasses.join(" ");
4979
    }
4980
 
4981
    // set attributes on newNode
4982
    for (attributeName in attributes) {
4983
      // Setting attributes can cause a js error in IE under certain circumstances
4984
      // eg. on a <img> under https when it's new attribute value is non-https
4985
      // TODO: Investigate this further and check for smarter handling
4986
      try {
4987
        newNode.setAttribute(attributeName, attributes[attributeName]);
4988
      } catch(e) {}
4989
    }
4990
 
4991
    // IE8 sometimes loses the width/height attributes when those are set before the "src"
4992
    // so we make sure to set them again
4993
    if (attributes.src) {
4994
      if (typeof(attributes.width) !== "undefined") {
4995
        newNode.setAttribute("width", attributes.width);
4996
      }
4997
      if (typeof(attributes.height) !== "undefined") {
4998
        newNode.setAttribute("height", attributes.height);
4999
      }
5000
    }
5001
  }
5002
 
5003
  /**
5004
   * IE gives wrong results for hasAttribute/getAttribute, for example:
5005
   *    var td = document.createElement("td");
5006
   *    td.getAttribute("rowspan"); // => "1" in IE
5007
   *
5008
   * Therefore we have to check the element's outerHTML for the attribute
5009
   */
5010
  var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly();
5011
  function _getAttribute(node, attributeName) {
5012
    attributeName = attributeName.toLowerCase();
5013
    var nodeName = node.nodeName;
5014
    if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) {
5015
      // Get 'src' attribute value via object property since this will always contain the
5016
      // full absolute url (http://...)
5017
      // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
5018
      // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
5019
      return node.src;
5020
    } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
5021
      // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
5022
      var outerHTML      = node.outerHTML.toLowerCase(),
5023
          // TODO: This might not work for attributes without value: <input disabled>
5024
          hasAttribute   = outerHTML.indexOf(" " + attributeName +  "=") != -1;
5025
 
5026
      return hasAttribute ? node.getAttribute(attributeName) : null;
5027
    } else{
5028
      return node.getAttribute(attributeName);
5029
    }
5030
  }
5031
 
5032
  /**
5033
   * Check whether the given node is a proper loaded image
5034
   * FIXME: Returns undefined when unknown (Chrome, Safari)
5035
   */
5036
  function _isLoadedImage(node) {
5037
    try {
5038
      return node.complete && !node.mozMatchesSelector(":-moz-broken");
5039
    } catch(e) {
5040
      if (node.complete && node.readyState === "complete") {
5041
        return true;
5042
      }
5043
    }
5044
  }
5045
 
5046
  function _handleText(oldNode) {
5047
    return oldNode.ownerDocument.createTextNode(oldNode.data);
5048
  }
5049
 
5050
 
5051
  // ------------ attribute checks ------------ \\
5052
  var attributeCheckMethods = {
5053
    url: (function() {
5054
      var REG_EXP = /^https?:\/\//i;
5055
      return function(attributeValue) {
5056
        if (!attributeValue || !attributeValue.match(REG_EXP)) {
5057
          return null;
5058
        }
5059
        return attributeValue.replace(REG_EXP, function(match) {
5060
          return match.toLowerCase();
5061
        });
5062
      };
5063
    })(),
5064
 
5065
    alt: (function() {
5066
      var REG_EXP = /[^ a-z0-9_\-]/gi;
5067
      return function(attributeValue) {
5068
        if (!attributeValue) {
5069
          return "";
5070
        }
5071
        return attributeValue.replace(REG_EXP, "");
5072
      };
5073
    })(),
5074
 
5075
    numbers: (function() {
5076
      var REG_EXP = /\D/g;
5077
      return function(attributeValue) {
5078
        attributeValue = (attributeValue || "").replace(REG_EXP, "");
5079
        return attributeValue || null;
5080
      };
5081
    })()
5082
  };
5083
 
5084
  // ------------ class converter (converts an html attribute to a class name) ------------ \\
5085
  var addClassMethods = {
5086
    align_img: (function() {
5087
      var mapping = {
5088
        left:   "wysiwyg-float-left",
5089
        right:  "wysiwyg-float-right"
5090
      };
5091
      return function(attributeValue) {
5092
        return mapping[String(attributeValue).toLowerCase()];
5093
      };
5094
    })(),
5095
 
5096
    align_text: (function() {
5097
      var mapping = {
5098
        left:     "wysiwyg-text-align-left",
5099
        right:    "wysiwyg-text-align-right",
5100
        center:   "wysiwyg-text-align-center",
5101
        justify:  "wysiwyg-text-align-justify"
5102
      };
5103
      return function(attributeValue) {
5104
        return mapping[String(attributeValue).toLowerCase()];
5105
      };
5106
    })(),
5107
 
5108
    clear_br: (function() {
5109
      var mapping = {
5110
        left:   "wysiwyg-clear-left",
5111
        right:  "wysiwyg-clear-right",
5112
        both:   "wysiwyg-clear-both",
5113
        all:    "wysiwyg-clear-both"
5114
      };
5115
      return function(attributeValue) {
5116
        return mapping[String(attributeValue).toLowerCase()];
5117
      };
5118
    })(),
5119
 
5120
    size_font: (function() {
5121
      var mapping = {
5122
        "1": "wysiwyg-font-size-xx-small",
5123
        "2": "wysiwyg-font-size-small",
5124
        "3": "wysiwyg-font-size-medium",
5125
        "4": "wysiwyg-font-size-large",
5126
        "5": "wysiwyg-font-size-x-large",
5127
        "6": "wysiwyg-font-size-xx-large",
5128
        "7": "wysiwyg-font-size-xx-large",
5129
        "-": "wysiwyg-font-size-smaller",
5130
        "+": "wysiwyg-font-size-larger"
5131
      };
5132
      return function(attributeValue) {
5133
        return mapping[String(attributeValue).charAt(0)];
5134
      };
5135
    })()
5136
  };
5137
 
5138
  return parse;
5139
})();/**
5140
 * Checks for empty text node childs and removes them
5141
 *
5142
 * @param {Element} node The element in which to cleanup
5143
 * @example
5144
 *    wysihtml5.dom.removeEmptyTextNodes(element);
5145
 */
5146
wysihtml5.dom.removeEmptyTextNodes = function(node) {
5147
  var childNode,
5148
      childNodes        = wysihtml5.lang.array(node.childNodes).get(),
5149
      childNodesLength  = childNodes.length,
5150
      i                 = 0;
5151
  for (; i<childNodesLength; i++) {
5152
    childNode = childNodes[i];
5153
    if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {
5154
      childNode.parentNode.removeChild(childNode);
5155
    }
5156
  }
5157
};
5158
/**
5159
 * Renames an element (eg. a <div> to a <p>) and keeps its childs
5160
 *
5161
 * @param {Element} element The list element which should be renamed
5162
 * @param {Element} newNodeName The desired tag name
5163
 *
5164
 * @example
5165
 *    <!-- Assume the following dom: -->
5166
 *    <ul id="list">
5167
 *      <li>eminem</li>
5168
 *      <li>dr. dre</li>
5169
 *      <li>50 Cent</li>
5170
 *    </ul>
5171
 *
5172
 *    <script>
5173
 *      wysihtml5.dom.renameElement(document.getElementById("list"), "ol");
5174
 *    </script>
5175
 *
5176
 *    <!-- Will result in: -->
5177
 *    <ol>
5178
 *      <li>eminem</li>
5179
 *      <li>dr. dre</li>
5180
 *      <li>50 Cent</li>
5181
 *    </ol>
5182
 */
5183
wysihtml5.dom.renameElement = function(element, newNodeName) {
5184
  var newElement = element.ownerDocument.createElement(newNodeName),
5185
      firstChild;
5186
  while (firstChild = element.firstChild) {
5187
    newElement.appendChild(firstChild);
5188
  }
5189
  wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
5190
  element.parentNode.replaceChild(newElement, element);
5191
  return newElement;
5192
};/**
5193
 * Takes an element, removes it and replaces it with it's childs
5194
 *
5195
 * @param {Object} node The node which to replace with it's child nodes
5196
 * @example
5197
 *    <div id="foo">
5198
 *      <span>hello</span>
5199
 *    </div>
5200
 *    <script>
5201
 *      // Remove #foo and replace with it's children
5202
 *      wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo"));
5203
 *    </script>
5204
 */
5205
wysihtml5.dom.replaceWithChildNodes = function(node) {
5206
  if (!node.parentNode) {
5207
    return;
5208
  }
5209
 
5210
  if (!node.firstChild) {
5211
    node.parentNode.removeChild(node);
5212
    return;
5213
  }
5214
 
5215
  var fragment = node.ownerDocument.createDocumentFragment();
5216
  while (node.firstChild) {
5217
    fragment.appendChild(node.firstChild);
5218
  }
5219
  node.parentNode.replaceChild(fragment, node);
5220
  node = fragment = null;
5221
};
5222
/**
5223
 * Unwraps an unordered/ordered list
5224
 *
5225
 * @param {Element} element The list element which should be unwrapped
5226
 *
5227
 * @example
5228
 *    <!-- Assume the following dom: -->
5229
 *    <ul id="list">
5230
 *      <li>eminem</li>
5231
 *      <li>dr. dre</li>
5232
 *      <li>50 Cent</li>
5233
 *    </ul>
5234
 *
5235
 *    <script>
5236
 *      wysihtml5.dom.resolveList(document.getElementById("list"));
5237
 *    </script>
5238
 *
5239
 *    <!-- Will result in: -->
5240
 *    eminem<br>
5241
 *    dr. dre<br>
5242
 *    50 Cent<br>
5243
 */
5244
(function(dom) {
5245
  function _isBlockElement(node) {
5246
    return dom.getStyle("display").from(node) === "block";
5247
  }
5248
 
5249
  function _isLineBreak(node) {
5250
    return node.nodeName === "BR";
5251
  }
5252
 
5253
  function _appendLineBreak(element) {
5254
    var lineBreak = element.ownerDocument.createElement("br");
5255
    element.appendChild(lineBreak);
5256
  }
5257
 
5258
  function resolveList(list) {
5259
    if (list.nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") {
5260
      return;
5261
    }
5262
 
5263
    var doc             = list.ownerDocument,
5264
        fragment        = doc.createDocumentFragment(),
5265
        previousSibling = list.previousElementSibling || list.previousSibling,
5266
        firstChild,
5267
        lastChild,
5268
        isLastChild,
5269
        shouldAppendLineBreak,
5270
        listItem;
5271
 
5272
    if (previousSibling && !_isBlockElement(previousSibling)) {
5273
      _appendLineBreak(fragment);
5274
    }
5275
 
5276
    while (listItem = list.firstChild) {
5277
      lastChild = listItem.lastChild;
5278
      while (firstChild = listItem.firstChild) {
5279
        isLastChild           = firstChild === lastChild;
5280
        // This needs to be done before appending it to the fragment, as it otherwise will loose style information
5281
        shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
5282
        fragment.appendChild(firstChild);
5283
        if (shouldAppendLineBreak) {
5284
          _appendLineBreak(fragment);
5285
        }
5286
      }
5287
 
5288
      listItem.parentNode.removeChild(listItem);
5289
    }
5290
    list.parentNode.replaceChild(fragment, list);
5291
  }
5292
 
5293
  dom.resolveList = resolveList;
5294
})(wysihtml5.dom);/**
5295
 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
5296
 *
5297
 * Browser Compatibility:
5298
 *  - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
5299
 *  - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
5300
 *
5301
 * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
5302
 *    - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
5303
 *    - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
5304
 *    - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
5305
 *    - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
5306
 *      can do anything as if the sandbox attribute wasn't set
5307
 *
5308
 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
5309
 * @param {Object} [config] Optional parameters
5310
 *
5311
 * @example
5312
 *    new wysihtml5.dom.Sandbox(function(sandbox) {
5313
 *      sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
5314
 *    });
5315
 */
5316
(function(wysihtml5) {
5317
  var /**
5318
       * Default configuration
5319
       */
5320
      doc                 = document,
5321
      /**
5322
       * Properties to unset/protect on the window object
5323
       */
5324
      windowProperties    = [
5325
        "parent", "top", "opener", "frameElement", "frames",
5326
        "localStorage", "globalStorage", "sessionStorage", "indexedDB"
5327
      ],
5328
      /**
5329
       * Properties on the window object which are set to an empty function
5330
       */
5331
      windowProperties2   = [
5332
        "open", "close", "openDialog", "showModalDialog",
5333
        "alert", "confirm", "prompt",
5334
        "openDatabase", "postMessage",
5335
        "XMLHttpRequest", "XDomainRequest"
5336
      ],
5337
      /**
5338
       * Properties to unset/protect on the document object
5339
       */
5340
      documentProperties  = [
5341
        "referrer",
5342
        "write", "open", "close"
5343
      ];
5344
 
5345
  wysihtml5.dom.Sandbox = Base.extend(
5346
    /** @scope wysihtml5.dom.Sandbox.prototype */ {
5347
 
5348
    constructor: function(readyCallback, config) {
5349
      this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
5350
      this.config   = wysihtml5.lang.object({}).merge(config).get();
5351
      this.iframe   = this._createIframe();
5352
    },
5353
 
5354
    insertInto: function(element) {
5355
      if (typeof(element) === "string") {
5356
        element = doc.getElementById(element);
5357
      }
5358
 
5359
      element.appendChild(this.iframe);
5360
    },
5361
 
5362
    getIframe: function() {
5363
      return this.iframe;
5364
    },
5365
 
5366
    getWindow: function() {
5367
      this._readyError();
5368
    },
5369
 
5370
    getDocument: function() {
5371
      this._readyError();
5372
    },
5373
 
5374
    destroy: function() {
5375
      var iframe = this.getIframe();
5376
      iframe.parentNode.removeChild(iframe);
5377
    },
5378
 
5379
    _readyError: function() {
5380
      throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
5381
    },
5382
 
5383
    /**
5384
     * Creates the sandbox iframe
5385
     *
5386
     * Some important notes:
5387
     *  - We can't use HTML5 sandbox for now:
5388
     *    setting it causes that the iframe's dom can't be accessed from the outside
5389
     *    Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
5390
     *    But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
5391
     *    In order to make this happen we need to set the "allow-scripts" flag.
5392
     *    A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
5393
     *  - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
5394
     *  - IE needs to have the security="restricted" attribute set before the iframe is
5395
     *    inserted into the dom tree
5396
     *  - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
5397
     *    though it supports it
5398
     *  - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
5399
     *  - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
5400
     *    on the onreadystatechange event
5401
     */
5402
    _createIframe: function() {
5403
      var that   = this,
5404
          iframe = doc.createElement("iframe");
5405
      iframe.className = "wysihtml5-sandbox";
5406
      wysihtml5.dom.setAttributes({
5407
        "security":           "restricted",
5408
        "allowtransparency":  "true",
5409
        "frameborder":        0,
5410
        "width":              0,
5411
        "height":             0,
5412
        "marginwidth":        0,
5413
        "marginheight":       0
5414
      }).on(iframe);
5415
 
5416
      // Setting the src like this prevents ssl warnings in IE6
5417
      if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
5418
        iframe.src = "javascript:'<html></html>'";
5419
      }
5420
 
5421
      iframe.onload = function() {
5422
        iframe.onreadystatechange = iframe.onload = null;
5423
        that._onLoadIframe(iframe);
5424
      };
5425
 
5426
      iframe.onreadystatechange = function() {
5427
        if (/loaded|complete/.test(iframe.readyState)) {
5428
          iframe.onreadystatechange = iframe.onload = null;
5429
          that._onLoadIframe(iframe);
5430
        }
5431
      };
5432
 
5433
      return iframe;
5434
    },
5435
 
5436
    /**
5437
     * Callback for when the iframe has finished loading
5438
     */
5439
    _onLoadIframe: function(iframe) {
5440
      // don't resume when the iframe got unloaded (eg. by removing it from the dom)
5441
      if (!wysihtml5.dom.contains(doc.documentElement, iframe)) {
5442
        return;
5443
      }
5444
 
5445
      var that           = this,
5446
          iframeWindow   = iframe.contentWindow,
5447
          iframeDocument = iframe.contentWindow.document,
5448
          charset        = doc.characterSet || doc.charset || "utf-8",
5449
          sandboxHtml    = this._getHtml({
5450
            charset:      charset,
5451
            stylesheets:  this.config.stylesheets
5452
          });
5453
 
5454
      // Create the basic dom tree including proper DOCTYPE and charset
5455
      iframeDocument.open("text/html", "replace");
5456
      iframeDocument.write(sandboxHtml);
5457
      iframeDocument.close();
5458
 
5459
      this.getWindow = function() { return iframe.contentWindow; };
5460
      this.getDocument = function() { return iframe.contentWindow.document; };
5461
 
5462
      // Catch js errors and pass them to the parent's onerror event
5463
      // addEventListener("error") doesn't work properly in some browsers
5464
      // TODO: apparently this doesn't work in IE9!
5465
      iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
5466
        throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);
5467
      };
5468
 
5469
      if (!wysihtml5.browser.supportsSandboxedIframes()) {
5470
        // Unset a bunch of sensitive variables
5471
        // Please note: This isn't hack safe!  
5472
        // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
5473
        // IE is secure though, which is the most important thing, since IE is the only browser, who
5474
        // takes over scripts & styles into contentEditable elements when copied from external websites
5475
        // or applications (Microsoft Word, ...)
5476
        var i, length;
5477
        for (i=0, length=windowProperties.length; i<length; i++) {
5478
          this._unset(iframeWindow, windowProperties[i]);
5479
        }
5480
        for (i=0, length=windowProperties2.length; i<length; i++) {
5481
          this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION);
5482
        }
5483
        for (i=0, length=documentProperties.length; i<length; i++) {
5484
          this._unset(iframeDocument, documentProperties[i]);
5485
        }
5486
        // This doesn't work in Safari 5 
5487
        // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
5488
        this._unset(iframeDocument, "cookie", "", true);
5489
      }
5490
 
5491
      this.loaded = true;
5492
 
5493
      // Trigger the callback
5494
      setTimeout(function() { that.callback(that); }, 0);
5495
    },
5496
 
5497
    _getHtml: function(templateVars) {
5498
      var stylesheets = templateVars.stylesheets,
5499
          html        = "",
5500
          i           = 0,
5501
          length;
5502
      stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
5503
      if (stylesheets) {
5504
        length = stylesheets.length;
5505
        for (; i<length; i++) {
5506
          html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
5507
        }
5508
      }
5509
      templateVars.stylesheets = html;
5510
 
5511
      return wysihtml5.lang.string(
5512
        '<!DOCTYPE html><html><head>'
5513
        + '<meta charset="#{charset}">#{stylesheets}</head>'
5514
        + '<body></body></html>'
5515
      ).interpolate(templateVars);
5516
    },
5517
 
5518
    /**
5519
     * Method to unset/override existing variables
5520
     * @example
5521
     *    // Make cookie unreadable and unwritable
5522
     *    this._unset(document, "cookie", "", true);
5523
     */
5524
    _unset: function(object, property, value, setter) {
5525
      try { object[property] = value; } catch(e) {}
5526
 
5527
      try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
5528
      if (setter) {
5529
        try { object.__defineSetter__(property, function() {}); } catch(e) {}
5530
      }
5531
 
5532
      if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {
5533
        try {
5534
          var config = {
5535
            get: function() { return value; }
5536
          };
5537
          if (setter) {
5538
            config.set = function() {};
5539
          }
5540
          Object.defineProperty(object, property, config);
5541
        } catch(e) {}
5542
      }
5543
    }
5544
  });
5545
})(wysihtml5);
5546
(function() {
5547
  var mapping = {
5548
    "className": "class"
5549
  };
5550
  wysihtml5.dom.setAttributes = function(attributes) {
5551
    return {
5552
      on: function(element) {
5553
        for (var i in attributes) {
5554
          element.setAttribute(mapping[i] || i, attributes[i]);
5555
        }
5556
      }
5557
    }
5558
  };
5559
})();wysihtml5.dom.setStyles = function(styles) {
5560
  return {
5561
    on: function(element) {
5562
      var style = element.style;
5563
      if (typeof(styles) === "string") {
5564
        style.cssText += ";" + styles;
5565
        return;
5566
      }
5567
      for (var i in styles) {
5568
        if (i === "float") {
5569
          style.cssFloat = styles[i];
5570
          style.styleFloat = styles[i];
5571
        } else {
5572
          style[i] = styles[i];
5573
        }
5574
      }
5575
    }
5576
  };
5577
};/**
5578
 * Simulate HTML5 placeholder attribute
5579
 *
5580
 * Needed since
5581
 *    - div[contentEditable] elements don't support it
5582
 *    - older browsers (such as IE8 and Firefox 3.6) don't support it at all
5583
 *
5584
 * @param {Object} parent Instance of main wysihtml5.Editor class
5585
 * @param {Element} view Instance of wysihtml5.views.* class
5586
 * @param {String} placeholderText
5587
 *
5588
 * @example
5589
 *    wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
5590
 */
5591
(function(dom) {
5592
  dom.simulatePlaceholder = function(editor, view, placeholderText) {
5593
    var CLASS_NAME = "placeholder",
5594
        unset = function() {
5595
          if (view.hasPlaceholderSet()) {
5596
            view.clear();
5597
          }
5598
          dom.removeClass(view.element, CLASS_NAME);
5599
        },
5600
        set = function() {
5601
          if (view.isEmpty()) {
5602
            view.setValue(placeholderText);
5603
            dom.addClass(view.element, CLASS_NAME);
5604
          }
5605
        };
5606
 
5607
    editor
5608
      .observe("set_placeholder", set)
5609
      .observe("unset_placeholder", unset)
5610
      .observe("focus:composer", unset)
5611
      .observe("paste:composer", unset)
5612
      .observe("blur:composer", set);
5613
 
5614
    set();
5615
  };
5616
})(wysihtml5.dom);
5617
(function(dom) {
5618
  var documentElement = document.documentElement;
5619
  if ("textContent" in documentElement) {
5620
    dom.setTextContent = function(element, text) {
5621
      element.textContent = text;
5622
    };
5623
 
5624
    dom.getTextContent = function(element) {
5625
      return element.textContent;
5626
    };
5627
  } else if ("innerText" in documentElement) {
5628
    dom.setTextContent = function(element, text) {
5629
      element.innerText = text;
5630
    };
5631
 
5632
    dom.getTextContent = function(element) {
5633
      return element.innerText;
5634
    };
5635
  } else {
5636
    dom.setTextContent = function(element, text) {
5637
      element.nodeValue = text;
5638
    };
5639
 
5640
    dom.getTextContent = function(element) {
5641
      return element.nodeValue;
5642
    };
5643
  }
5644
})(wysihtml5.dom);
5645
 
5646
/**
5647
 * Fix most common html formatting misbehaviors of browsers implementation when inserting
5648
 * content via copy & paste contentEditable
5649
 *
5650
 * @author Christopher Blum
5651
 */
5652
wysihtml5.quirks.cleanPastedHTML = (function() {
5653
  // TODO: We probably need more rules here
5654
  var defaultRules = {
5655
    // When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling
5656
    "a u": wysihtml5.dom.replaceWithChildNodes
5657
  };
5658
 
5659
  function cleanPastedHTML(elementOrHtml, rules, context) {
5660
    rules   = rules || defaultRules;
5661
    context = context || elementOrHtml.ownerDocument || document;
5662
 
5663
    var element,
5664
        isString = typeof(elementOrHtml) === "string",
5665
        method,
5666
        matches,
5667
        matchesLength,
5668
        i,
5669
        j = 0;
5670
    if (isString) {
5671
      element = wysihtml5.dom.getAsDom(elementOrHtml, context);
5672
    } else {
5673
      element = elementOrHtml;
5674
    }
5675
 
5676
    for (i in rules) {
5677
      matches       = element.querySelectorAll(i);
5678
      method        = rules[i];
5679
      matchesLength = matches.length;
5680
      for (; j<matchesLength; j++) {
5681
        method(matches[j]);
5682
      }
5683
    }
5684
 
5685
    matches = elementOrHtml = rules = null;
5686
 
5687
    return isString ? element.innerHTML : element;
5688
  }
5689
 
5690
  return cleanPastedHTML;
5691
})();/**
5692
 * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
5693
 *
5694
 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
5695
 * @exaple
5696
 *    wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
5697
 */
5698
(function(wysihtml5) {
5699
  var dom = wysihtml5.dom;
5700
 
5701
  wysihtml5.quirks.ensureProperClearing = (function() {
5702
    var clearIfNecessary = function(event) {
5703
      var element = this;
5704
      setTimeout(function() {
5705
        var innerHTML = element.innerHTML.toLowerCase();
5706
        if (innerHTML == "<p>&nbsp;</p>" ||
5707
            innerHTML == "<p>&nbsp;</p><p>&nbsp;</p>") {
5708
          element.innerHTML = "";
5709
        }
5710
      }, 0);
5711
    };
5712
 
5713
    return function(composer) {
5714
      dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
5715
    };
5716
  })();
5717
 
5718
 
5719
 
5720
  /**
5721
   * 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
5722
   *
5723
   * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
5724
   * @exaple
5725
   *    wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
5726
   */
5727
  wysihtml5.quirks.ensureProperClearingOfLists = (function() {
5728
    var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"];
5729
 
5730
    var clearIfNecessary = function(element, contentEditableElement) {
5731
      if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) {
5732
        return;
5733
      }
5734
 
5735
      var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI });
5736
      if (!list) {
5737
        return;
5738
      }
5739
 
5740
      var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild;
5741
      if (!listIsFirstChildOfContentEditable) {
5742
        return;
5743
      }
5744
 
5745
      var hasOnlyOneListItem = list.childNodes.length <= 1;
5746
      if (!hasOnlyOneListItem) {
5747
        return;
5748
      }
5749
 
5750
      var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true;
5751
      if (!onlyListItemIsEmpty) {
5752
        return;
5753
      }
5754
 
5755
      list.parentNode.removeChild(list);
5756
    };
5757
 
5758
    return function(composer) {
5759
      dom.observe(composer.element, "keydown", function(event) {
5760
        if (event.keyCode !== wysihtml5.BACKSPACE_KEY) {
5761
          return;
5762
        }
5763
 
5764
        var element = composer.selection.getSelectedNode();
5765
        clearIfNecessary(element, composer.element);
5766
      });
5767
    };
5768
  })();
5769
 
5770
})(wysihtml5);
5771
// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
5772
//
5773
// In Firefox this:
5774
//      var d = document.createElement("div");
5775
//      d.innerHTML ='<a href="~"></a>';
5776
//      d.innerHTML;
5777
// will result in:
5778
//      <a href="%7E"></a>
5779
// which is wrong
5780
(function(wysihtml5) {
5781
  var TILDE_ESCAPED = "%7E";
5782
  wysihtml5.quirks.getCorrectInnerHTML = function(element) {
5783
    var innerHTML = element.innerHTML;
5784
    if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
5785
      return innerHTML;
5786
    }
5787
 
5788
    var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
5789
        url,
5790
        urlToSearch,
5791
        length,
5792
        i;
5793
    for (i=0, length=elementsWithTilde.length; i<length; i++) {
5794
      url         = elementsWithTilde[i].href || elementsWithTilde[i].src;
5795
      urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED);
5796
      innerHTML   = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url);
5797
    }
5798
    return innerHTML;
5799
  };
5800
})(wysihtml5);/**
5801
 * Some browsers don't insert line breaks when hitting return in a contentEditable element
5802
 *    - Opera & IE insert new <p> on return
5803
 *    - Chrome & Safari insert new <div> on return
5804
 *    - Firefox inserts <br> on return (yippie!)
5805
 *
5806
 * @param {Element} element
5807
 *
5808
 * @example
5809
 *    wysihtml5.quirks.insertLineBreakOnReturn(element);
5810
 */
5811
(function(wysihtml5) {
5812
  var dom                                           = wysihtml5.dom,
5813
      USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS  = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"],
5814
      LIST_TAGS                                     = ["UL", "OL", "MENU"];
5815
 
5816
  wysihtml5.quirks.insertLineBreakOnReturn = function(composer) {
5817
    function unwrap(selectedNode) {
5818
      var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2);
5819
      if (!parentElement) {
5820
        return;
5821
      }
5822
 
5823
      var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE);
5824
      dom.insert(invisibleSpace).before(parentElement);
5825
      dom.replaceWithChildNodes(parentElement);
5826
      composer.selection.selectNode(invisibleSpace);
5827
    }
5828
 
5829
    function keyDown(event) {
5830
      var keyCode = event.keyCode;
5831
      if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) {
5832
        return;
5833
      }
5834
 
5835
      var element         = event.target,
5836
          selectedNode    = composer.selection.getSelectedNode(),
5837
          blockElement    = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4);
5838
      if (blockElement) {
5839
        // Some browsers create <p> elements after leaving a list
5840
        // check after keydown of backspace and return whether a <p> got inserted and unwrap it
5841
        if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) {
5842
          setTimeout(function() {
5843
            var selectedNode = composer.selection.getSelectedNode(),
5844
                list,
5845
                div;
5846
            if (!selectedNode) {
5847
              return;
5848
            }
5849
 
5850
            list = dom.getParentElement(selectedNode, {
5851
              nodeName: LIST_TAGS
5852
            }, 2);
5853
 
5854
            if (list) {
5855
              return;
5856
            }
5857
 
5858
            unwrap(selectedNode);
5859
          }, 0);
5860
        } else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) {
5861
          setTimeout(function() {
5862
            unwrap(composer.selection.getSelectedNode());
5863
          }, 0);
5864
        }
5865
        return;
5866
      }
5867
 
5868
      if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) {
5869
        composer.commands.exec("insertLineBreak");
5870
        event.preventDefault();
5871
      }
5872
    }
5873
 
5874
    // keypress doesn't fire when you hit backspace
5875
    dom.observe(composer.element.ownerDocument, "keydown", keyDown);
5876
  };
5877
})(wysihtml5);/**
5878
 * Force rerendering of a given element
5879
 * Needed to fix display misbehaviors of IE
5880
 *
5881
 * @param {Element} element The element object which needs to be rerendered
5882
 * @example
5883
 *    wysihtml5.quirks.redraw(document.body);
5884
 */
5885
(function(wysihtml5) {
5886
  var CLASS_NAME = "wysihtml5-quirks-redraw";
5887
 
5888
  wysihtml5.quirks.redraw = function(element) {
5889
    wysihtml5.dom.addClass(element, CLASS_NAME);
5890
    wysihtml5.dom.removeClass(element, CLASS_NAME);
5891
 
5892
    // Following hack is needed for firefox to make sure that image resize handles are properly removed
5893
    try {
5894
      var doc = element.ownerDocument;
5895
      doc.execCommand("italic", false, null);
5896
      doc.execCommand("italic", false, null);
5897
    } catch(e) {}
5898
  };
5899
})(wysihtml5);/**
5900
 * Selection API
5901
 *
5902
 * @example
5903
 *    var selection = new wysihtml5.Selection(editor);
5904
 */
5905
(function(wysihtml5) {
5906
  var dom = wysihtml5.dom;
5907
 
5908
  function _getCumulativeOffsetTop(element) {
5909
    var top = 0;
5910
    if (element.parentNode) {
5911
      do {
5912
        top += element.offsetTop || 0;
5913
        element = element.offsetParent;
5914
      } while (element);
5915
    }
5916
    return top;
5917
  }
5918
 
5919
  wysihtml5.Selection = Base.extend(
5920
    /** @scope wysihtml5.Selection.prototype */ {
5921
    constructor: function(editor) {
5922
      // Make sure that our external range library is initialized
5923
      window.rangy.init();
5924
 
5925
      this.editor   = editor;
5926
      this.composer = editor.composer;
5927
      this.doc      = this.composer.doc;
5928
    },
5929
 
5930
    /**
5931
     * Get the current selection as a bookmark to be able to later restore it
5932
     *
5933
     * @return {Object} An object that represents the current selection
5934
     */
5935
    getBookmark: function() {
5936
      var range = this.getRange();
5937
      return range && range.cloneRange();
5938
    },
5939
 
5940
    /**
5941
     * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark
5942
     *
5943
     * @param {Object} bookmark An object that represents the current selection
5944
     */
5945
    setBookmark: function(bookmark) {
5946
      if (!bookmark) {
5947
        return;
5948
      }
5949
 
5950
      this.setSelection(bookmark);
5951
    },
5952
 
5953
    /**
5954
     * Set the caret in front of the given node
5955
     *
5956
     * @param {Object} node The element or text node where to position the caret in front of
5957
     * @example
5958
     *    selection.setBefore(myElement);
5959
     */
5960
    setBefore: function(node) {
5961
      var range = rangy.createRange(this.doc);
5962
      range.setStartBefore(node);
5963
      range.setEndBefore(node);
5964
      return this.setSelection(range);
5965
    },
5966
 
5967
    /**
5968
     * Set the caret after the given node
5969
     *
5970
     * @param {Object} node The element or text node where to position the caret in front of
5971
     * @example
5972
     *    selection.setBefore(myElement);
5973
     */
5974
    setAfter: function(node) {
5975
      var range = rangy.createRange(this.doc);
5976
      range.setStartAfter(node);
5977
      range.setEndAfter(node);
5978
      return this.setSelection(range);
5979
    },
5980
 
5981
    /**
5982
     * Ability to select/mark nodes
5983
     *
5984
     * @param {Element} node The node/element to select
5985
     * @example
5986
     *    selection.selectNode(document.getElementById("my-image"));
5987
     */
5988
    selectNode: function(node) {
5989
      var range           = rangy.createRange(this.doc),
5990
          isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
5991
          canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
5992
          content         = isElement ? node.innerHTML : node.data,
5993
          isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE),
5994
          displayStyle    = dom.getStyle("display").from(node),
5995
          isBlockElement  = (displayStyle === "block" || displayStyle === "list-item");
5996
 
5997
      if (isEmpty && isElement && canHaveHTML) {
5998
        // Make sure that caret is visible in node by inserting a zero width no breaking space
5999
        try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
6000
      }
6001
 
6002
      if (canHaveHTML) {
6003
        range.selectNodeContents(node);
6004
      } else {
6005
        range.selectNode(node);
6006
      }
6007
 
6008
      if (canHaveHTML && isEmpty && isElement) {
6009
        range.collapse(isBlockElement);
6010
      } else if (canHaveHTML && isEmpty) {
6011
        range.setStartAfter(node);
6012
        range.setEndAfter(node);
6013
      }
6014
 
6015
      this.setSelection(range);
6016
    },
6017
 
6018
    /**
6019
     * Get the node which contains the selection
6020
     *
6021
     * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
6022
     * @return {Object} The node that contains the caret
6023
     * @example
6024
     *    var nodeThatContainsCaret = selection.getSelectedNode();
6025
     */
6026
    getSelectedNode: function(controlRange) {
6027
      var selection,
6028
          range;
6029
 
6030
      if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
6031
        range = this.doc.selection.createRange();
6032
        if (range && range.length) {
6033
          return range.item(0);
6034
        }
6035
      }
6036
 
6037
      selection = this.getSelection(this.doc);
6038
      if (selection.focusNode === selection.anchorNode) {
6039
        return selection.focusNode;
6040
      } else {
6041
        range = this.getRange(this.doc);
6042
        return range ? range.commonAncestorContainer : this.doc.body;
6043
      }
6044
    },
6045
 
6046
    executeAndRestore: function(method, restoreScrollPosition) {
6047
      var body                  = this.doc.body,
6048
          oldScrollTop          = restoreScrollPosition && body.scrollTop,
6049
          oldScrollLeft         = restoreScrollPosition && body.scrollLeft,
6050
          className             = "_wysihtml5-temp-placeholder",
6051
          placeholderHTML       = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>',
6052
          range                 = this.getRange(this.doc),
6053
          newRange;
6054
 
6055
      // Nothing selected, execute and say goodbye
6056
      if (!range) {
6057
        method(body, body);
6058
        return;
6059
      }
6060
 
6061
      var node = range.createContextualFragment(placeholderHTML);
6062
      range.insertNode(node);
6063
 
6064
      // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
6065
      try {
6066
        method(range.startContainer, range.endContainer);
6067
      } catch(e3) {
6068
        setTimeout(function() { throw e3; }, 0);
6069
      }
6070
 
6071
      caretPlaceholder = this.doc.querySelector("." + className);
6072
      if (caretPlaceholder) {
6073
        newRange = rangy.createRange(this.doc);
6074
        newRange.selectNode(caretPlaceholder);
6075
        newRange.deleteContents();
6076
        this.setSelection(newRange);
6077
      } else {
6078
        // fallback for when all hell breaks loose
6079
        body.focus();
6080
      }
6081
 
6082
      if (restoreScrollPosition) {
6083
        body.scrollTop  = oldScrollTop;
6084
        body.scrollLeft = oldScrollLeft;
6085
      }
6086
 
6087
      // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
6088
      try {
6089
        caretPlaceholder.parentNode.removeChild(caretPlaceholder);
6090
      } catch(e4) {}
6091
    },
6092
 
6093
    /**
6094
     * Different approach of preserving the selection (doesn't modify the dom)
6095
     * Takes all text nodes in the selection and saves the selection position in the first and last one
6096
     */
6097
    executeAndRestoreSimple: function(method) {
6098
      var range = this.getRange(),
6099
          body  = this.doc.body,
6100
          newRange,
6101
          firstNode,
6102
          lastNode,
6103
          textNodes,
6104
          rangeBackup;
6105
 
6106
      // Nothing selected, execute and say goodbye
6107
      if (!range) {
6108
        method(body, body);
6109
        return;
6110
      }
6111
 
6112
      textNodes = range.getNodes([3]);
6113
      firstNode = textNodes[0] || range.startContainer;
6114
      lastNode  = textNodes[textNodes.length - 1] || range.endContainer;
6115
 
6116
      rangeBackup = {
6117
        collapsed:      range.collapsed,
6118
        startContainer: firstNode,
6119
        startOffset:    firstNode === range.startContainer ? range.startOffset : 0,
6120
        endContainer:   lastNode,
6121
        endOffset:      lastNode === range.endContainer ? range.endOffset : lastNode.length
6122
      };
6123
 
6124
      try {
6125
        method(range.startContainer, range.endContainer);
6126
      } catch(e) {
6127
        setTimeout(function() { throw e; }, 0);
6128
      }
6129
 
6130
      newRange = rangy.createRange(this.doc);
6131
      try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {}
6132
      try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {}
6133
      try { this.setSelection(newRange); } catch(e3) {}
6134
    },
6135
 
6136
    /**
6137
     * Insert html at the caret position and move the cursor after the inserted html
6138
     *
6139
     * @param {String} html HTML string to insert
6140
     * @example
6141
     *    selection.insertHTML("<p>foobar</p>");
6142
     */
6143
    insertHTML: function(html) {
6144
      var range     = rangy.createRange(this.doc),
6145
          node      = range.createContextualFragment(html),
6146
          lastChild = node.lastChild;
6147
      this.insertNode(node);
6148
      if (lastChild) {
6149
        this.setAfter(lastChild);
6150
      }
6151
    },
6152
 
6153
    /**
6154
     * Insert a node at the caret position and move the cursor behind it
6155
     *
6156
     * @param {Object} node HTML string to insert
6157
     * @example
6158
     *    selection.insertNode(document.createTextNode("foobar"));
6159
     */
6160
    insertNode: function(node) {
6161
      var range = this.getRange();
6162
      if (range) {
6163
        range.insertNode(node);
6164
      }
6165
    },
6166
 
6167
    /**
6168
     * Wraps current selection with the given node
6169
     *
6170
     * @param {Object} node The node to surround the selected elements with
6171
     */
6172
    surround: function(node) {
6173
      var range = this.getRange();
6174
      if (!range) {
6175
        return;
6176
      }
6177
 
6178
      try {
6179
        // This only works when the range boundaries are not overlapping other elements
6180
        range.surroundContents(node);
6181
        this.selectNode(node);
6182
      } catch(e) {
6183
        // fallback
6184
        node.appendChild(range.extractContents());
6185
        range.insertNode(node);
6186
      }
6187
    },
6188
 
6189
    /**
6190
     * Scroll the current caret position into the view
6191
     * FIXME: This is a bit hacky, there might be a smarter way of doing this
6192
     *
6193
     * @example
6194
     *    selection.scrollIntoView();
6195
     */
6196
    scrollIntoView: function() {
6197
      var doc           = this.doc,
6198
          hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
6199
          tempElement   = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() {
6200
            var element = doc.createElement("span");
6201
            // The element needs content in order to be able to calculate it's position properly
6202
            element.innerHTML = wysihtml5.INVISIBLE_SPACE;
6203
            return element;
6204
          })(),
6205
          offsetTop;
6206
 
6207
      if (hasScrollBars) {
6208
        this.insertNode(tempElement);
6209
        offsetTop = _getCumulativeOffsetTop(tempElement);
6210
        tempElement.parentNode.removeChild(tempElement);
6211
        if (offsetTop > doc.body.scrollTop) {
6212
          doc.body.scrollTop = offsetTop;
6213
        }
6214
      }
6215
    },
6216
 
6217
    /**
6218
     * Select line where the caret is in
6219
     */
6220
    selectLine: function() {
6221
      if (wysihtml5.browser.supportsSelectionModify()) {
6222
        this._selectLine_W3C();
6223
      } else if (this.doc.selection) {
6224
        this._selectLine_MSIE();
6225
      }
6226
    },
6227
 
6228
    /**
6229
     * See https://developer.mozilla.org/en/DOM/Selection/modify
6230
     */
6231
    _selectLine_W3C: function() {
6232
      var win       = this.doc.defaultView,
6233
          selection = win.getSelection();
6234
      selection.modify("extend", "left", "lineboundary");
6235
      selection.modify("extend", "right", "lineboundary");
6236
    },
6237
 
6238
    _selectLine_MSIE: function() {
6239
      var range       = this.doc.selection.createRange(),
6240
          rangeTop    = range.boundingTop,
6241
          rangeHeight = range.boundingHeight,
6242
          scrollWidth = this.doc.body.scrollWidth,
6243
          rangeBottom,
6244
          rangeEnd,
6245
          measureNode,
6246
          i,
6247
          j;
6248
 
6249
      if (!range.moveToPoint) {
6250
        return;
6251
      }
6252
 
6253
      if (rangeTop === 0) {
6254
        // Don't know why, but when the selection ends at the end of a line
6255
        // range.boundingTop is 0
6256
        measureNode = this.doc.createElement("span");
6257
        this.insertNode(measureNode);
6258
        rangeTop = measureNode.offsetTop;
6259
        measureNode.parentNode.removeChild(measureNode);
6260
      }
6261
 
6262
      rangeTop += 1;
6263
 
6264
      for (i=-10; i<scrollWidth; i+=2) {
6265
        try {
6266
          range.moveToPoint(i, rangeTop);
6267
          break;
6268
        } catch(e1) {}
6269
      }
6270
 
6271
      // Investigate the following in order to handle multi line selections
6272
      // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);
6273
      rangeBottom = rangeTop;
6274
      rangeEnd = this.doc.selection.createRange();
6275
      for (j=scrollWidth; j>=0; j--) {
6276
        try {
6277
          rangeEnd.moveToPoint(j, rangeBottom);
6278
          break;
6279
        } catch(e2) {}
6280
      }
6281
 
6282
      range.setEndPoint("EndToEnd", rangeEnd);
6283
      range.select();
6284
    },
6285
 
6286
    getText: function() {
6287
      var selection = this.getSelection();
6288
      return selection ? selection.toString() : "";
6289
    },
6290
 
6291
    getNodes: function(nodeType, filter) {
6292
      var range = this.getRange();
6293
      if (range) {
6294
        return range.getNodes([nodeType], filter);
6295
      } else {
6296
        return [];
6297
      }
6298
    },
6299
 
6300
    getRange: function() {
6301
      var selection = this.getSelection();
6302
      return selection && selection.rangeCount && selection.getRangeAt(0);
6303
    },
6304
 
6305
    getSelection: function() {
6306
      return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);
6307
    },
6308
 
6309
    setSelection: function(range) {
6310
      var win       = this.doc.defaultView || this.doc.parentWindow,
6311
          selection = rangy.getSelection(win);
6312
      return selection.setSingleRange(range);
6313
    }
6314
  });
6315
 
6316
})(wysihtml5);
6317
/**
6318
 * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license.
6319
 * http://code.google.com/p/rangy/
6320
 *
6321
 * changed in order to be able ...
6322
 *    - to use custom tags
6323
 *    - to detect and replace similar css classes via reg exp
6324
 */
6325
(function(wysihtml5, rangy) {
6326
  var defaultTagName = "span";
6327
 
6328
  var REG_EXP_WHITE_SPACE = /\s+/g;
6329
 
6330
  function hasClass(el, cssClass, regExp) {
6331
    if (!el.className) {
6332
      return false;
6333
    }
6334
 
6335
    var matchingClassNames = el.className.match(regExp) || [];
6336
    return matchingClassNames[matchingClassNames.length - 1] === cssClass;
6337
  }
6338
 
6339
  function addClass(el, cssClass, regExp) {
6340
    if (el.className) {
6341
      removeClass(el, regExp);
6342
      el.className += " " + cssClass;
6343
    } else {
6344
      el.className = cssClass;
6345
    }
6346
  }
6347
 
6348
  function removeClass(el, regExp) {
6349
    if (el.className) {
6350
      el.className = el.className.replace(regExp, "");
6351
    }
6352
  }
6353
 
6354
  function hasSameClasses(el1, el2) {
6355
    return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");
6356
  }
6357
 
6358
  function replaceWithOwnChildren(el) {
6359
    var parent = el.parentNode;
6360
    while (el.firstChild) {
6361
      parent.insertBefore(el.firstChild, el);
6362
    }
6363
    parent.removeChild(el);
6364
  }
6365
 
6366
  function elementsHaveSameNonClassAttributes(el1, el2) {
6367
    if (el1.attributes.length != el2.attributes.length) {
6368
      return false;
6369
    }
6370
    for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
6371
      attr1 = el1.attributes[i];
6372
      name = attr1.name;
6373
      if (name != "class") {
6374
        attr2 = el2.attributes.getNamedItem(name);
6375
        if (attr1.specified != attr2.specified) {
6376
          return false;
6377
        }
6378
        if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
6379
          return false;
6380
        }
6381
      }
6382
    }
6383
    return true;
6384
  }
6385
 
6386
  function isSplitPoint(node, offset) {
6387
    if (rangy.dom.isCharacterDataNode(node)) {
6388
      if (offset == 0) {
6389
        return !!node.previousSibling;
6390
      } else if (offset == node.length) {
6391
        return !!node.nextSibling;
6392
      } else {
6393
        return true;
6394
      }
6395
    }
6396
 
6397
    return offset > 0 && offset < node.childNodes.length;
6398
  }
6399
 
6400
  function splitNodeAt(node, descendantNode, descendantOffset) {
6401
    var newNode;
6402
    if (rangy.dom.isCharacterDataNode(descendantNode)) {
6403
      if (descendantOffset == 0) {
6404
        descendantOffset = rangy.dom.getNodeIndex(descendantNode);
6405
        descendantNode = descendantNode.parentNode;
6406
      } else if (descendantOffset == descendantNode.length) {
6407
        descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;
6408
        descendantNode = descendantNode.parentNode;
6409
      } else {
6410
        newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);
6411
      }
6412
    }
6413
    if (!newNode) {
6414
      newNode = descendantNode.cloneNode(false);
6415
      if (newNode.id) {
6416
        newNode.removeAttribute("id");
6417
      }
6418
      var child;
6419
      while ((child = descendantNode.childNodes[descendantOffset])) {
6420
        newNode.appendChild(child);
6421
      }
6422
      rangy.dom.insertAfter(newNode, descendantNode);
6423
    }
6424
    return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode));
6425
  }
6426
 
6427
  function Merge(firstNode) {
6428
    this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
6429
    this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
6430
    this.textNodes = [this.firstTextNode];
6431
  }
6432
 
6433
  Merge.prototype = {
6434
    doMerge: function() {
6435
      var textBits = [], textNode, parent, text;
6436
      for (var i = 0, len = this.textNodes.length; i < len; ++i) {
6437
        textNode = this.textNodes[i];
6438
        parent = textNode.parentNode;
6439
        textBits[i] = textNode.data;
6440
        if (i) {
6441
          parent.removeChild(textNode);
6442
          if (!parent.hasChildNodes()) {
6443
            parent.parentNode.removeChild(parent);
6444
          }
6445
        }
6446
      }
6447
      this.firstTextNode.data = text = textBits.join("");
6448
      return text;
6449
    },
6450
 
6451
    getLength: function() {
6452
      var i = this.textNodes.length, len = 0;
6453
      while (i--) {
6454
        len += this.textNodes[i].length;
6455
      }
6456
      return len;
6457
    },
6458
 
6459
    toString: function() {
6460
      var textBits = [];
6461
      for (var i = 0, len = this.textNodes.length; i < len; ++i) {
6462
        textBits[i] = "'" + this.textNodes[i].data + "'";
6463
      }
6464
      return "[Merge(" + textBits.join(",") + ")]";
6465
    }
6466
  };
6467
 
6468
  function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) {
6469
    this.tagNames = tagNames || [defaultTagName];
6470
    this.cssClass = cssClass || "";
6471
    this.similarClassRegExp = similarClassRegExp;
6472
    this.normalize = normalize;
6473
    this.applyToAnyTagName = false;
6474
  }
6475
 
6476
  HTMLApplier.prototype = {
6477
    getAncestorWithClass: function(node) {
6478
      var cssClassMatch;
6479
      while (node) {
6480
        cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true;
6481
        if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {
6482
          return node;
6483
        }
6484
        node = node.parentNode;
6485
      }
6486
      return false;
6487
    },
6488
 
6489
    // Normalizes nodes after applying a CSS class to a Range.
6490
    postApply: function(textNodes, range) {
6491
      var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
6492
 
6493
      var merges = [], currentMerge;
6494
 
6495
      var rangeStartNode = firstNode, rangeEndNode = lastNode;
6496
      var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
6497
 
6498
      var textNode, precedingTextNode;
6499
 
6500
      for (var i = 0, len = textNodes.length; i < len; ++i) {
6501
        textNode = textNodes[i];
6502
        precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false);
6503
        if (precedingTextNode) {
6504
          if (!currentMerge) {
6505
            currentMerge = new Merge(precedingTextNode);
6506
            merges.push(currentMerge);
6507
          }
6508
          currentMerge.textNodes.push(textNode);
6509
          if (textNode === firstNode) {
6510
            rangeStartNode = currentMerge.firstTextNode;
6511
            rangeStartOffset = rangeStartNode.length;
6512
          }
6513
          if (textNode === lastNode) {
6514
            rangeEndNode = currentMerge.firstTextNode;
6515
            rangeEndOffset = currentMerge.getLength();
6516
          }
6517
        } else {
6518
          currentMerge = null;
6519
        }
6520
      }
6521
 
6522
      // Test whether the first node after the range needs merging
6523
      var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
6524
      if (nextTextNode) {
6525
        if (!currentMerge) {
6526
          currentMerge = new Merge(lastNode);
6527
          merges.push(currentMerge);
6528
        }
6529
        currentMerge.textNodes.push(nextTextNode);
6530
      }
6531
 
6532
      // Do the merges
6533
      if (merges.length) {
6534
        for (i = 0, len = merges.length; i < len; ++i) {
6535
          merges[i].doMerge();
6536
        }
6537
        // Set the range boundaries
6538
        range.setStart(rangeStartNode, rangeStartOffset);
6539
        range.setEnd(rangeEndNode, rangeEndOffset);
6540
      }
6541
    },
6542
 
6543
    getAdjacentMergeableTextNode: function(node, forward) {
6544
        var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
6545
        var el = isTextNode ? node.parentNode : node;
6546
        var adjacentNode;
6547
        var propName = forward ? "nextSibling" : "previousSibling";
6548
        if (isTextNode) {
6549
          // Can merge if the node's previous/next sibling is a text node
6550
          adjacentNode = node[propName];
6551
          if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
6552
            return adjacentNode;
6553
          }
6554
        } else {
6555
          // Compare element with its sibling
6556
          adjacentNode = el[propName];
6557
          if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
6558
            return adjacentNode[forward ? "firstChild" : "lastChild"];
6559
          }
6560
        }
6561
        return null;
6562
    },
6563
 
6564
    areElementsMergeable: function(el1, el2) {
6565
      return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
6566
        && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
6567
        && hasSameClasses(el1, el2)
6568
        && elementsHaveSameNonClassAttributes(el1, el2);
6569
    },
6570
 
6571
    createContainer: function(doc) {
6572
      var el = doc.createElement(this.tagNames[0]);
6573
      if (this.cssClass) {
6574
        el.className = this.cssClass;
6575
      }
6576
      return el;
6577
    },
6578
 
6579
    applyToTextNode: function(textNode) {
6580
      var parent = textNode.parentNode;
6581
      if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
6582
        if (this.cssClass) {
6583
          addClass(parent, this.cssClass, this.similarClassRegExp);
6584
        }
6585
      } else {
6586
        var el = this.createContainer(rangy.dom.getDocument(textNode));
6587
        textNode.parentNode.insertBefore(el, textNode);
6588
        el.appendChild(textNode);
6589
      }
6590
    },
6591
 
6592
    isRemovable: function(el) {
6593
      return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass;
6594
    },
6595
 
6596
    undoToTextNode: function(textNode, range, ancestorWithClass) {
6597
      if (!range.containsNode(ancestorWithClass)) {
6598
        // Split out the portion of the ancestor from which we can remove the CSS class
6599
        var ancestorRange = range.cloneRange();
6600
        ancestorRange.selectNode(ancestorWithClass);
6601
 
6602
        if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) {
6603
          splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset);
6604
          range.setEndAfter(ancestorWithClass);
6605
        }
6606
        if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
6607
          ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset);
6608
        }
6609
      }
6610
 
6611
      if (this.similarClassRegExp) {
6612
        removeClass(ancestorWithClass, this.similarClassRegExp);
6613
      }
6614
      if (this.isRemovable(ancestorWithClass)) {
6615
        replaceWithOwnChildren(ancestorWithClass);
6616
      }
6617
    },
6618
 
6619
    applyToRange: function(range) {
6620
        var textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6621
        if (!textNodes.length) {
6622
          try {
6623
            var node = this.createContainer(range.endContainer.ownerDocument);
6624
            range.surroundContents(node);
6625
            this.selectNode(range, node);
6626
            return;
6627
          } catch(e) {}
6628
        }
6629
 
6630
        range.splitBoundaries();
6631
        textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6632
 
6633
        if (textNodes.length) {
6634
          var textNode;
6635
 
6636
          for (var i = 0, len = textNodes.length; i < len; ++i) {
6637
            textNode = textNodes[i];
6638
            if (!this.getAncestorWithClass(textNode)) {
6639
              this.applyToTextNode(textNode);
6640
            }
6641
          }
6642
 
6643
          range.setStart(textNodes[0], 0);
6644
          textNode = textNodes[textNodes.length - 1];
6645
          range.setEnd(textNode, textNode.length);
6646
 
6647
          if (this.normalize) {
6648
            this.postApply(textNodes, range);
6649
          }
6650
        }
6651
    },
6652
 
6653
    undoToRange: function(range) {
6654
      var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass;
6655
      if (textNodes.length) {
6656
        range.splitBoundaries();
6657
        textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6658
      } else {
6659
        var doc = range.endContainer.ownerDocument,
6660
            node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
6661
        range.insertNode(node);
6662
        range.selectNode(node);
6663
        textNodes = [node];
6664
      }
6665
 
6666
      for (var i = 0, len = textNodes.length; i < len; ++i) {
6667
        textNode = textNodes[i];
6668
        ancestorWithClass = this.getAncestorWithClass(textNode);
6669
        if (ancestorWithClass) {
6670
          this.undoToTextNode(textNode, range, ancestorWithClass);
6671
        }
6672
      }
6673
 
6674
      if (len == 1) {
6675
        this.selectNode(range, textNodes[0]);
6676
      } else {
6677
        range.setStart(textNodes[0], 0);
6678
        textNode = textNodes[textNodes.length - 1];
6679
        range.setEnd(textNode, textNode.length);
6680
 
6681
        if (this.normalize) {
6682
          this.postApply(textNodes, range);
6683
        }
6684
      }
6685
    },
6686
 
6687
    selectNode: function(range, node) {
6688
      var isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
6689
          canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : true,
6690
          content         = isElement ? node.innerHTML : node.data,
6691
          isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE);
6692
 
6693
      if (isEmpty && isElement && canHaveHTML) {
6694
        // Make sure that caret is visible in node by inserting a zero width no breaking space
6695
        try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
6696
      }
6697
      range.selectNodeContents(node);
6698
      if (isEmpty && isElement) {
6699
        range.collapse(false);
6700
      } else if (isEmpty) {
6701
        range.setStartAfter(node);
6702
        range.setEndAfter(node);
6703
      }
6704
    },
6705
 
6706
    getTextSelectedByRange: function(textNode, range) {
6707
      var textRange = range.cloneRange();
6708
      textRange.selectNodeContents(textNode);
6709
 
6710
      var intersectionRange = textRange.intersection(range);
6711
      var text = intersectionRange ? intersectionRange.toString() : "";
6712
      textRange.detach();
6713
 
6714
      return text;
6715
    },
6716
 
6717
    isAppliedToRange: function(range) {
6718
      var ancestors = [],
6719
          ancestor,
6720
          textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6721
      if (!textNodes.length) {
6722
        ancestor = this.getAncestorWithClass(range.startContainer);
6723
        return ancestor ? [ancestor] : false;
6724
      }
6725
 
6726
      for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
6727
        selectedText = this.getTextSelectedByRange(textNodes[i], range);
6728
        ancestor = this.getAncestorWithClass(textNodes[i]);
6729
        if (selectedText != "" && !ancestor) {
6730
          return false;
6731
        } else {
6732
          ancestors.push(ancestor);
6733
        }
6734
      }
6735
      return ancestors;
6736
    },
6737
 
6738
    toggleRange: function(range) {
6739
      if (this.isAppliedToRange(range)) {
6740
        this.undoToRange(range);
6741
      } else {
6742
        this.applyToRange(range);
6743
      }
6744
    }
6745
  };
6746
 
6747
  wysihtml5.selection.HTMLApplier = HTMLApplier;
6748
 
6749
})(wysihtml5, rangy);/**
6750
 * Rich Text Query/Formatting Commands
6751
 *
6752
 * @example
6753
 *    var commands = new wysihtml5.Commands(editor);
6754
 */
6755
wysihtml5.Commands = Base.extend(
6756
  /** @scope wysihtml5.Commands.prototype */ {
6757
  constructor: function(editor) {
6758
    this.editor   = editor;
6759
    this.composer = editor.composer;
6760
    this.doc      = this.composer.doc;
6761
  },
6762
 
6763
  /**
6764
   * Check whether the browser supports the given command
6765
   *
6766
   * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
6767
   * @example
6768
   *    commands.supports("createLink");
6769
   */
6770
  support: function(command) {
6771
    return wysihtml5.browser.supportsCommand(this.doc, command);
6772
  },
6773
 
6774
  /**
6775
   * Check whether the browser supports the given command
6776
   *
6777
   * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
6778
   * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
6779
   * @example
6780
   *    commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
6781
   */
6782
  exec: function(command, value) {
6783
    var obj     = wysihtml5.commands[command],
6784
        args    = wysihtml5.lang.array(arguments).get(),
6785
        method  = obj && obj.exec,
6786
        result  = null;
6787
 
6788
    this.editor.fire("beforecommand:composer");
6789
 
6790
    if (method) {
6791
      args.unshift(this.composer);
6792
      result = method.apply(obj, args);
6793
    } else {
6794
      try {
6795
        // try/catch for buggy firefox
6796
        result = this.doc.execCommand(command, false, value);
6797
      } catch(e) {}
6798
    }
6799
 
6800
    this.editor.fire("aftercommand:composer");
6801
    return result;
6802
  },
6803
 
6804
  /**
6805
   * Check whether the current command is active
6806
   * If the caret is within a bold text, then calling this with command "bold" should return true
6807
   *
6808
   * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
6809
   * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
6810
   * @return {Boolean} Whether the command is active
6811
   * @example
6812
   *    var isCurrentSelectionBold = commands.state("bold");
6813
   */
6814
  state: function(command, commandValue) {
6815
    var obj     = wysihtml5.commands[command],
6816
        args    = wysihtml5.lang.array(arguments).get(),
6817
        method  = obj && obj.state;
6818
    if (method) {
6819
      args.unshift(this.composer);
6820
      return method.apply(obj, args);
6821
    } else {
6822
      try {
6823
        // try/catch for buggy firefox
6824
        return this.doc.queryCommandState(command);
6825
      } catch(e) {
6826
        return false;
6827
      }
6828
    }
6829
  },
6830
 
6831
  /**
6832
   * Get the current command's value
6833
   *
6834
   * @param {String} command The command string which to check (eg. "formatBlock")
6835
   * @return {String} The command value
6836
   * @example
6837
   *    var currentBlockElement = commands.value("formatBlock");
6838
   */
6839
  value: function(command) {
6840
    var obj     = wysihtml5.commands[command],
6841
        method  = obj && obj.value;
6842
    if (method) {
6843
      return method.call(obj, this.composer, command);
6844
    } else {
6845
      try {
6846
        // try/catch for buggy firefox
6847
        return this.doc.queryCommandValue(command);
6848
      } catch(e) {
6849
        return null;
6850
      }
6851
    }
6852
  }
6853
});
6854
(function(wysihtml5) {
6855
  var undef;
6856
 
6857
  wysihtml5.commands.bold = {
6858
    exec: function(composer, command) {
6859
      return wysihtml5.commands.formatInline.exec(composer, command, "b");
6860
    },
6861
 
6862
    state: function(composer, command, color) {
6863
      // element.ownerDocument.queryCommandState("bold") results:
6864
      // firefox: only <b>
6865
      // chrome:  <b>, <strong>, <h1>, <h2>, ...
6866
      // ie:      <b>, <strong>
6867
      // opera:   <b>, <strong>
6868
      return wysihtml5.commands.formatInline.state(composer, command, "b");
6869
    },
6870
 
6871
    value: function() {
6872
      return undef;
6873
    }
6874
  };
6875
})(wysihtml5);
6876
 
6877
(function(wysihtml5) {
6878
  var undef,
6879
      NODE_NAME = "A",
6880
      dom       = wysihtml5.dom;
6881
 
6882
  function _removeFormat(composer, anchors) {
6883
    var length  = anchors.length,
6884
        i       = 0,
6885
        anchor,
6886
        codeElement,
6887
        textContent;
6888
    for (; i<length; i++) {
6889
      anchor      = anchors[i];
6890
      codeElement = dom.getParentElement(anchor, { nodeName: "code" });
6891
      textContent = dom.getTextContent(anchor);
6892
 
6893
      // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking
6894
      // else replace <a> with its childNodes
6895
      if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {
6896
        // <code> element is used to prevent later auto-linking of the content
6897
        codeElement = dom.renameElement(anchor, "code");
6898
      } else {
6899
        dom.replaceWithChildNodes(anchor);
6900
      }
6901
    }
6902
  }
6903
 
6904
  function _format(composer, attributes) {
6905
    var doc             = composer.doc,
6906
        tempClass       = "_wysihtml5-temp-" + (+new Date()),
6907
        tempClassRegExp = /non-matching-class/g,
6908
        i               = 0,
6909
        length,
6910
        anchors,
6911
        anchor,
6912
        hasElementChild,
6913
        isEmpty,
6914
        elementToSetCaretAfter,
6915
        textContent,
6916
        whiteSpace,
6917
        j;
6918
    wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp);
6919
    anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);
6920
    length  = anchors.length;
6921
    for (; i<length; i++) {
6922
      anchor = anchors[i];
6923
      anchor.removeAttribute("class");
6924
      for (j in attributes) {
6925
        anchor.setAttribute(j, attributes[j]);
6926
      }
6927
    }
6928
 
6929
    elementToSetCaretAfter = anchor;
6930
    if (length === 1) {
6931
      textContent = dom.getTextContent(anchor);
6932
      hasElementChild = !!anchor.querySelector("*");
6933
      isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE;
6934
      if (!hasElementChild && isEmpty) {
6935
        dom.setTextContent(anchor, attributes.text || anchor.href);
6936
        whiteSpace = doc.createTextNode(" ");
6937
        composer.selection.setAfter(anchor);
6938
        composer.selection.insertNode(whiteSpace);
6939
        elementToSetCaretAfter = whiteSpace;
6940
      }
6941
    }
6942
    composer.selection.setAfter(elementToSetCaretAfter);
6943
  }
6944
 
6945
  wysihtml5.commands.createLink = {
6946
    /**
6947
     * TODO: Use HTMLApplier or formatInline here
6948
     *
6949
     * Turns selection into a link
6950
     * If selection is already a link, it removes the link and wraps it with a <code> element
6951
     * The <code> element is needed to avoid auto linking
6952
     *
6953
     * @example
6954
     *    // either ...
6955
     *    wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de");
6956
     *    // ... or ...
6957
     *    wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" });
6958
     */
6959
    exec: function(composer, command, value) {
6960
      var anchors = this.state(composer, command);
6961
      if (anchors) {
6962
        // Selection contains links
6963
        composer.selection.executeAndRestore(function() {
6964
          _removeFormat(composer, anchors);
6965
        });
6966
      } else {
6967
        // Create links
6968
        value = typeof(value) === "object" ? value : { href: value };
6969
        _format(composer, value);
6970
      }
6971
    },
6972
 
6973
    state: function(composer, command) {
6974
      return wysihtml5.commands.formatInline.state(composer, command, "A");
6975
    },
6976
 
6977
    value: function() {
6978
      return undef;
6979
    }
6980
  };
6981
})(wysihtml5);/**
6982
 * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags
6983
 * which we don't want
6984
 * Instead we set a css class
6985
 */
6986
(function(wysihtml5) {
6987
  var undef,
6988
      REG_EXP = /wysiwyg-font-size-[a-z\-]+/g;
6989
 
6990
  wysihtml5.commands.fontSize = {
6991
    exec: function(composer, command, size) {
6992
      return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
6993
    },
6994
 
6995
    state: function(composer, command, size) {
6996
      return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
6997
    },
6998
 
6999
    value: function() {
7000
      return undef;
7001
    }
7002
  };
7003
})(wysihtml5);
7004
/**
7005
 * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
7006
 * which we don't want
7007
 * Instead we set a css class
7008
 */
7009
(function(wysihtml5) {
7010
  var undef,
7011
      REG_EXP = /wysiwyg-color-[a-z]+/g;
7012
 
7013
  wysihtml5.commands.foreColor = {
7014
    exec: function(composer, command, color) {
7015
      return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
7016
    },
7017
 
7018
    state: function(composer, command, color) {
7019
      return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
7020
    },
7021
 
7022
    value: function() {
7023
      return undef;
7024
    }
7025
  };
7026
})(wysihtml5);(function(wysihtml5) {
7027
  var undef,
7028
      dom                     = wysihtml5.dom,
7029
      DEFAULT_NODE_NAME       = "DIV",
7030
      // Following elements are grouped
7031
      // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
7032
      // instead of creating a H4 within a H1 which would result in semantically invalid html
7033
      BLOCK_ELEMENTS_GROUP    = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", DEFAULT_NODE_NAME];
7034
 
7035
  /**
7036
   * Remove similiar classes (based on classRegExp)
7037
   * and add the desired class name
7038
   */
7039
  function _addClass(element, className, classRegExp) {
7040
    if (element.className) {
7041
      _removeClass(element, classRegExp);
7042
      element.className += " " + className;
7043
    } else {
7044
      element.className = className;
7045
    }
7046
  }
7047
 
7048
  function _removeClass(element, classRegExp) {
7049
    element.className = element.className.replace(classRegExp, "");
7050
  }
7051
 
7052
  /**
7053
   * Check whether given node is a text node and whether it's empty
7054
   */
7055
  function _isBlankTextNode(node) {
7056
    return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim();
7057
  }
7058
 
7059
  /**
7060
   * Returns previous sibling node that is not a blank text node
7061
   */
7062
  function _getPreviousSiblingThatIsNotBlank(node) {
7063
    var previousSibling = node.previousSibling;
7064
    while (previousSibling && _isBlankTextNode(previousSibling)) {
7065
      previousSibling = previousSibling.previousSibling;
7066
    }
7067
    return previousSibling;
7068
  }
7069
 
7070
  /**
7071
   * Returns next sibling node that is not a blank text node
7072
   */
7073
  function _getNextSiblingThatIsNotBlank(node) {
7074
    var nextSibling = node.nextSibling;
7075
    while (nextSibling && _isBlankTextNode(nextSibling)) {
7076
      nextSibling = nextSibling.nextSibling;
7077
    }
7078
    return nextSibling;
7079
  }
7080
 
7081
  /**
7082
   * Adds line breaks before and after the given node if the previous and next siblings
7083
   * aren't already causing a visual line break (block element or <br>)
7084
   */
7085
  function _addLineBreakBeforeAndAfter(node) {
7086
    var doc             = node.ownerDocument,
7087
        nextSibling     = _getNextSiblingThatIsNotBlank(node),
7088
        previousSibling = _getPreviousSiblingThatIsNotBlank(node);
7089
 
7090
    if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
7091
      node.parentNode.insertBefore(doc.createElement("br"), nextSibling);
7092
    }
7093
    if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
7094
      node.parentNode.insertBefore(doc.createElement("br"), node);
7095
    }
7096
  }
7097
 
7098
  /**
7099
   * Removes line breaks before and after the given node
7100
   */
7101
  function _removeLineBreakBeforeAndAfter(node) {
7102
    var nextSibling     = _getNextSiblingThatIsNotBlank(node),
7103
        previousSibling = _getPreviousSiblingThatIsNotBlank(node);
7104
 
7105
    if (nextSibling && _isLineBreak(nextSibling)) {
7106
      nextSibling.parentNode.removeChild(nextSibling);
7107
    }
7108
    if (previousSibling && _isLineBreak(previousSibling)) {
7109
      previousSibling.parentNode.removeChild(previousSibling);
7110
    }
7111
  }
7112
 
7113
  function _removeLastChildIfLineBreak(node) {
7114
    var lastChild = node.lastChild;
7115
    if (lastChild && _isLineBreak(lastChild)) {
7116
      lastChild.parentNode.removeChild(lastChild);
7117
    }
7118
  }
7119
 
7120
  function _isLineBreak(node) {
7121
    return node.nodeName === "BR";
7122
  }
7123
 
7124
  /**
7125
   * Checks whether the elment causes a visual line break
7126
   * (<br> or block elements)
7127
   */
7128
  function _isLineBreakOrBlockElement(element) {
7129
    if (_isLineBreak(element)) {
7130
      return true;
7131
    }
7132
 
7133
    if (dom.getStyle("display").from(element) === "block") {
7134
      return true;
7135
    }
7136
 
7137
    return false;
7138
  }
7139
 
7140
  /**
7141
   * Execute native query command
7142
   * and if necessary modify the inserted node's className
7143
   */
7144
  function _execCommand(doc, command, nodeName, className) {
7145
    if (className) {
7146
      var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {
7147
        var target = event.target,
7148
            displayStyle;
7149
        if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
7150
          return;
7151
        }
7152
        displayStyle = dom.getStyle("display").from(target);
7153
        if (displayStyle.substr(0, 6) !== "inline") {
7154
          // Make sure that only block elements receive the given class
7155
          target.className += " " + className;
7156
        }
7157
      });
7158
    }
7159
    doc.execCommand(command, false, nodeName);
7160
    if (eventListener) {
7161
      eventListener.stop();
7162
    }
7163
  }
7164
 
7165
  function _selectLineAndWrap(composer, element) {
7166
    composer.selection.selectLine();
7167
    composer.selection.surround(element);
7168
    _removeLineBreakBeforeAndAfter(element);
7169
    _removeLastChildIfLineBreak(element);
7170
    composer.selection.selectNode(element);
7171
  }
7172
 
7173
  function _hasClasses(element) {
7174
    return !!wysihtml5.lang.string(element.className).trim();
7175
  }
7176
 
7177
  wysihtml5.commands.formatBlock = {
7178
    exec: function(composer, command, nodeName, className, classRegExp) {
7179
      var doc          = composer.doc,
7180
          blockElement = this.state(composer, command, nodeName, className, classRegExp),
7181
          selectedNode;
7182
 
7183
      nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
7184
 
7185
      if (blockElement) {
7186
        composer.selection.executeAndRestoreSimple(function() {
7187
          if (classRegExp) {
7188
            _removeClass(blockElement, classRegExp);
7189
          }
7190
          var hasClasses = _hasClasses(blockElement);
7191
          if (!hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) {
7192
            // Insert a line break afterwards and beforewards when there are siblings
7193
            // that are not of type line break or block element
7194
            _addLineBreakBeforeAndAfter(blockElement);
7195
            dom.replaceWithChildNodes(blockElement);
7196
          } else if (hasClasses) {
7197
            // Make sure that styling is kept by renaming the element to <div> and copying over the class name
7198
            dom.renameElement(blockElement, DEFAULT_NODE_NAME);
7199
          }
7200
        });
7201
        return;
7202
      }
7203
 
7204
      // Find similiar block element and rename it (<h2 class="foo"></h2>  =>  <h1 class="foo"></h1>)
7205
      if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {
7206
        selectedNode = composer.selection.getSelectedNode();
7207
        blockElement = dom.getParentElement(selectedNode, {
7208
          nodeName: BLOCK_ELEMENTS_GROUP
7209
        });
7210
 
7211
        if (blockElement) {
7212
          composer.selection.executeAndRestoreSimple(function() {
7213
            // Rename current block element to new block element and add class
7214
            if (nodeName) {
7215
              blockElement = dom.renameElement(blockElement, nodeName);
7216
            }
7217
            if (className) {
7218
              _addClass(blockElement, className, classRegExp);
7219
            }
7220
          });
7221
          return;
7222
        }
7223
      }
7224
 
7225
      if (composer.commands.support(command)) {
7226
        _execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className);
7227
        return;
7228
      }
7229
 
7230
      blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME);
7231
      if (className) {
7232
        blockElement.className = className;
7233
      }
7234
      _selectLineAndWrap(composer, blockElement);
7235
    },
7236
 
7237
    state: function(composer, command, nodeName, className, classRegExp) {
7238
      nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
7239
      var selectedNode = composer.selection.getSelectedNode();
7240
      return dom.getParentElement(selectedNode, {
7241
        nodeName:     nodeName,
7242
        className:    className,
7243
        classRegExp:  classRegExp
7244
      });
7245
    },
7246
 
7247
    value: function() {
7248
      return undef;
7249
    }
7250
  };
7251
})(wysihtml5);/**
7252
 * formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
7253
 *
7254
 *   #1 caret in unformatted text:
7255
 *      abcdefg|
7256
 *   output:
7257
 *      abcdefg<b>|</b>
7258
 *  
7259
 *   #2 unformatted text selected:
7260
 *      abc|deg|h
7261
 *   output:
7262
 *      abc<b>|deg|</b>h
7263
 *  
7264
 *   #3 unformatted text selected across boundaries:
7265
 *      ab|c <span>defg|h</span>
7266
 *   output:
7267
 *      ab<b>|c </b><span><b>defg</b>|h</span>
7268
 *
7269
 *   #4 formatted text entirely selected
7270
 *      <b>|abc|</b>
7271
 *   output:
7272
 *      |abc|
7273
 *
7274
 *   #5 formatted text partially selected
7275
 *      <b>ab|c|</b>
7276
 *   output:
7277
 *      <b>ab</b>|c|
7278
 *
7279
 *   #6 formatted text selected across boundaries
7280
 *      <span>ab|c</span> <b>de|fgh</b>
7281
 *   output:
7282
 *      <span>ab|c</span> de|<b>fgh</b>
7283
 */
7284
(function(wysihtml5) {
7285
  var undef,
7286
      // Treat <b> as <strong> and vice versa
7287
      ALIAS_MAPPING = {
7288
        "strong": "b",
7289
        "em":     "i",
7290
        "b":      "strong",
7291
        "i":      "em"
7292
      },
7293
      htmlApplier = {};
7294
 
7295
  function _getTagNames(tagName) {
7296
    var alias = ALIAS_MAPPING[tagName];
7297
    return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
7298
  }
7299
 
7300
  function _getApplier(tagName, className, classRegExp) {
7301
    var identifier = tagName + ":" + className;
7302
    if (!htmlApplier[identifier]) {
7303
      htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true);
7304
    }
7305
    return htmlApplier[identifier];
7306
  }
7307
 
7308
  wysihtml5.commands.formatInline = {
7309
    exec: function(composer, command, tagName, className, classRegExp) {
7310
      var range = composer.selection.getRange();
7311
      if (!range) {
7312
        return false;
7313
      }
7314
      _getApplier(tagName, className, classRegExp).toggleRange(range);
7315
      composer.selection.setSelection(range);
7316
    },
7317
 
7318
    state: function(composer, command, tagName, className, classRegExp) {
7319
      var doc           = composer.doc,
7320
          aliasTagName  = ALIAS_MAPPING[tagName] || tagName,
7321
          range;
7322
 
7323
      // Check whether the document contains a node with the desired tagName
7324
      if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&
7325
          !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
7326
        return false;
7327
      }
7328
 
7329
       // Check whether the document contains a node with the desired className
7330
      if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
7331
         return false;
7332
      }
7333
 
7334
      range = composer.selection.getRange();
7335
      if (!range) {
7336
        return false;
7337
      }
7338
 
7339
      return _getApplier(tagName, className, classRegExp).isAppliedToRange(range);
7340
    },
7341
 
7342
    value: function() {
7343
      return undef;
7344
    }
7345
  };
7346
})(wysihtml5);(function(wysihtml5) {
7347
  var undef;
7348
 
7349
  wysihtml5.commands.insertHTML = {
7350
    exec: function(composer, command, html) {
7351
      if (composer.commands.support(command)) {
7352
        composer.doc.execCommand(command, false, html);
7353
      } else {
7354
        composer.selection.insertHTML(html);
7355
      }
7356
    },
7357
 
7358
    state: function() {
7359
      return false;
7360
    },
7361
 
7362
    value: function() {
7363
      return undef;
7364
    }
7365
  };
7366
})(wysihtml5);(function(wysihtml5) {
7367
  var NODE_NAME = "IMG";
7368
 
7369
  wysihtml5.commands.insertImage = {
7370
    /**
7371
     * Inserts an <img>
7372
     * If selection is already an image link, it removes it
7373
     *
7374
     * @example
7375
     *    // either ...
7376
     *    wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg");
7377
     *    // ... or ...
7378
     *    wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" });
7379
     */
7380
    exec: function(composer, command, value) {
7381
      value = typeof(value) === "object" ? value : { src: value };
7382
 
7383
      var doc     = composer.doc,
7384
          image   = this.state(composer),
7385
          textNode,
7386
          i,
7387
          parent;
7388
 
7389
      if (image) {
7390
        // Image already selected, set the caret before it and delete it
7391
        composer.selection.setBefore(image);
7392
        parent = image.parentNode;
7393
        parent.removeChild(image);
7394
 
7395
        // and it's parent <a> too if it hasn't got any other relevant child nodes
7396
        wysihtml5.dom.removeEmptyTextNodes(parent);
7397
        if (parent.nodeName === "A" && !parent.firstChild) {
7398
          composer.selection.setAfter(parent);
7399
          parent.parentNode.removeChild(parent);
7400
        }
7401
 
7402
        // firefox and ie sometimes don't remove the image handles, even though the image got removed
7403
        wysihtml5.quirks.redraw(composer.element);
7404
        return;
7405
      }
7406
 
7407
      image = doc.createElement(NODE_NAME);
7408
 
7409
      for (i in value) {
7410
        image[i] = value[i];
7411
      }
7412
 
7413
      composer.selection.insertNode(image);
7414
      if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) {
7415
        textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
7416
        composer.selection.insertNode(textNode);
7417
        composer.selection.setAfter(textNode);
7418
      } else {
7419
        composer.selection.setAfter(image);
7420
      }
7421
    },
7422
 
7423
    state: function(composer) {
7424
      var doc = composer.doc,
7425
          selectedNode,
7426
          text,
7427
          imagesInSelection;
7428
 
7429
      if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {
7430
        return false;
7431
      }
7432
 
7433
      selectedNode = composer.selection.getSelectedNode();
7434
      if (!selectedNode) {
7435
        return false;
7436
      }
7437
 
7438
      if (selectedNode.nodeName === NODE_NAME) {
7439
        // This works perfectly in IE
7440
        return selectedNode;
7441
      }
7442
 
7443
      if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
7444
        return false;
7445
      }
7446
 
7447
      text = composer.selection.getText();
7448
      text = wysihtml5.lang.string(text).trim();
7449
      if (text) {
7450
        return false;
7451
      }
7452
 
7453
      imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) {
7454
        return node.nodeName === "IMG";
7455
      });
7456
 
7457
      if (imagesInSelection.length !== 1) {
7458
        return false;
7459
      }
7460
 
7461
      return imagesInSelection[0];
7462
    },
7463
 
7464
    value: function(composer) {
7465
      var image = this.state(composer);
7466
      return image && image.src;
7467
    }
7468
  };
7469
})(wysihtml5);(function(wysihtml5) {
7470
  var undef,
7471
      LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : "");
7472
 
7473
  wysihtml5.commands.insertLineBreak = {
7474
    exec: function(composer, command) {
7475
      if (composer.commands.support(command)) {
7476
        composer.doc.execCommand(command, false, null);
7477
        if (!wysihtml5.browser.autoScrollsToCaret()) {
7478
          composer.selection.scrollIntoView();
7479
        }
7480
      } else {
7481
        composer.commands.exec("insertHTML", LINE_BREAK);
7482
      }
7483
    },
7484
 
7485
    state: function() {
7486
      return false;
7487
    },
7488
 
7489
    value: function() {
7490
      return undef;
7491
    }
7492
  };
7493
})(wysihtml5);(function(wysihtml5) {
7494
  var undef;
7495
 
7496
  wysihtml5.commands.insertOrderedList = {
7497
    exec: function(composer, command) {
7498
      var doc           = composer.doc,
7499
          selectedNode  = composer.selection.getSelectedNode(),
7500
          list          = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }),
7501
          otherList     = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }),
7502
          tempClassName =  "_wysihtml5-temp-" + new Date().getTime(),
7503
          isEmpty,
7504
          tempElement;
7505
 
7506
      if (composer.commands.support(command)) {
7507
        doc.execCommand(command, false, null);
7508
        return;
7509
      }
7510
 
7511
      if (list) {
7512
        // Unwrap list
7513
        // <ol><li>foo</li><li>bar</li></ol>
7514
        // becomes:
7515
        // foo<br>bar<br>
7516
        composer.selection.executeAndRestoreSimple(function() {
7517
          wysihtml5.dom.resolveList(list);
7518
        });
7519
      } else if (otherList) {
7520
        // Turn an unordered list into an ordered list
7521
        // <ul><li>foo</li><li>bar</li></ul>
7522
        // becomes:
7523
        // <ol><li>foo</li><li>bar</li></ol>
7524
        composer.selection.executeAndRestoreSimple(function() {
7525
          wysihtml5.dom.renameElement(otherList, "ol");
7526
        });
7527
      } else {
7528
        // Create list
7529
        composer.commands.exec("formatBlock", "div", tempClassName);
7530
        tempElement = doc.querySelector("." + tempClassName);
7531
        isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
7532
        composer.selection.executeAndRestoreSimple(function() {
7533
          list = wysihtml5.dom.convertToList(tempElement, "ol");
7534
        });
7535
        if (isEmpty) {
7536
          composer.selection.selectNode(list.querySelector("li"));
7537
        }
7538
      }
7539
    },
7540
 
7541
    state: function(composer) {
7542
      var selectedNode = composer.selection.getSelectedNode();
7543
      return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" });
7544
    },
7545
 
7546
    value: function() {
7547
      return undef;
7548
    }
7549
  };
7550
})(wysihtml5);(function(wysihtml5) {
7551
  var undef;
7552
 
7553
  wysihtml5.commands.insertUnorderedList = {
7554
    exec: function(composer, command) {
7555
      var doc           = composer.doc,
7556
          selectedNode  = composer.selection.getSelectedNode(),
7557
          list          = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }),
7558
          otherList     = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }),
7559
          tempClassName =  "_wysihtml5-temp-" + new Date().getTime(),
7560
          isEmpty,
7561
          tempElement;
7562
 
7563
      if (composer.commands.support(command)) {
7564
        doc.execCommand(command, false, null);
7565
        return;
7566
      }
7567
 
7568
      if (list) {
7569
        // Unwrap list
7570
        // <ul><li>foo</li><li>bar</li></ul>
7571
        // becomes:
7572
        // foo<br>bar<br>
7573
        composer.selection.executeAndRestoreSimple(function() {
7574
          wysihtml5.dom.resolveList(list);
7575
        });
7576
      } else if (otherList) {
7577
        // Turn an ordered list into an unordered list
7578
        // <ol><li>foo</li><li>bar</li></ol>
7579
        // becomes:
7580
        // <ul><li>foo</li><li>bar</li></ul>
7581
        composer.selection.executeAndRestoreSimple(function() {
7582
          wysihtml5.dom.renameElement(otherList, "ul");
7583
        });
7584
      } else {
7585
        // Create list
7586
        composer.commands.exec("formatBlock", "div", tempClassName);
7587
        tempElement = doc.querySelector("." + tempClassName);
7588
        isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
7589
        composer.selection.executeAndRestoreSimple(function() {
7590
          list = wysihtml5.dom.convertToList(tempElement, "ul");
7591
        });
7592
        if (isEmpty) {
7593
          composer.selection.selectNode(list.querySelector("li"));
7594
        }
7595
      }
7596
    },
7597
 
7598
    state: function(composer) {
7599
      var selectedNode = composer.selection.getSelectedNode();
7600
      return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" });
7601
    },
7602
 
7603
    value: function() {
7604
      return undef;
7605
    }
7606
  };
7607
})(wysihtml5);(function(wysihtml5) {
7608
  var undef;
7609
 
7610
  wysihtml5.commands.italic = {
7611
    exec: function(composer, command) {
7612
      return wysihtml5.commands.formatInline.exec(composer, command, "i");
7613
    },
7614
 
7615
    state: function(composer, command, color) {
7616
      // element.ownerDocument.queryCommandState("italic") results:
7617
      // firefox: only <i>
7618
      // chrome:  <i>, <em>, <blockquote>, ...
7619
      // ie:      <i>, <em>
7620
      // opera:   only <i>
7621
      return wysihtml5.commands.formatInline.state(composer, command, "i");
7622
    },
7623
 
7624
    value: function() {
7625
      return undef;
7626
    }
7627
  };
7628
})(wysihtml5);(function(wysihtml5) {
7629
  var undef,
7630
      CLASS_NAME  = "wysiwyg-text-align-center",
7631
      REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
7632
 
7633
  wysihtml5.commands.justifyCenter = {
7634
    exec: function(composer, command) {
7635
      return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7636
    },
7637
 
7638
    state: function(composer, command) {
7639
      return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7640
    },
7641
 
7642
    value: function() {
7643
      return undef;
7644
    }
7645
  };
7646
})(wysihtml5);(function(wysihtml5) {
7647
  var undef,
7648
      CLASS_NAME  = "wysiwyg-text-align-left",
7649
      REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
7650
 
7651
  wysihtml5.commands.justifyLeft = {
7652
    exec: function(composer, command) {
7653
      return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7654
    },
7655
 
7656
    state: function(composer, command) {
7657
      return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7658
    },
7659
 
7660
    value: function() {
7661
      return undef;
7662
    }
7663
  };
7664
})(wysihtml5);(function(wysihtml5) {
7665
  var undef,
7666
      CLASS_NAME  = "wysiwyg-text-align-right",
7667
      REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
7668
 
7669
  wysihtml5.commands.justifyRight = {
7670
    exec: function(composer, command) {
7671
      return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7672
    },
7673
 
7674
    state: function(composer, command) {
7675
      return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7676
    },
7677
 
7678
    value: function() {
7679
      return undef;
7680
    }
7681
  };
7682
})(wysihtml5);(function(wysihtml5) {
7683
  var undef;
7684
  wysihtml5.commands.underline = {
7685
    exec: function(composer, command) {
7686
      return wysihtml5.commands.formatInline.exec(composer, command, "u");
7687
    },
7688
 
7689
    state: function(composer, command) {
7690
      return wysihtml5.commands.formatInline.state(composer, command, "u");
7691
    },
7692
 
7693
    value: function() {
7694
      return undef;
7695
    }
7696
  };
7697
})(wysihtml5);/**
7698
 * Undo Manager for wysihtml5
7699
 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
7700
 */
7701
(function(wysihtml5) {
7702
  var Z_KEY               = 90,
7703
      Y_KEY               = 89,
7704
      BACKSPACE_KEY       = 8,
7705
      DELETE_KEY          = 46,
7706
      MAX_HISTORY_ENTRIES = 40,
7707
      UNDO_HTML           = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
7708
      REDO_HTML           = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
7709
      dom                 = wysihtml5.dom;
7710
 
7711
  function cleanTempElements(doc) {
7712
    var tempElement;
7713
    while (tempElement = doc.querySelector("._wysihtml5-temp")) {
7714
      tempElement.parentNode.removeChild(tempElement);
7715
    }
7716
  }
7717
 
7718
  wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(
7719
    /** @scope wysihtml5.UndoManager.prototype */ {
7720
    constructor: function(editor) {
7721
      this.editor = editor;
7722
      this.composer = editor.composer;
7723
      this.element = this.composer.element;
7724
      this.history = [this.composer.getValue()];
7725
      this.position = 1;
7726
 
7727
      // Undo manager currently only supported in browsers who have the insertHTML command (not IE)
7728
      if (this.composer.commands.support("insertHTML")) {
7729
        this._observe();
7730
      }
7731
    },
7732
 
7733
    _observe: function() {
7734
      var that      = this,
7735
          doc       = this.composer.sandbox.getDocument(),
7736
          lastKey;
7737
 
7738
      // Catch CTRL+Z and CTRL+Y
7739
      dom.observe(this.element, "keydown", function(event) {
7740
        if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
7741
          return;
7742
        }
7743
 
7744
        var keyCode = event.keyCode,
7745
            isUndo = keyCode === Z_KEY && !event.shiftKey,
7746
            isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
7747
 
7748
        if (isUndo) {
7749
          that.undo();
7750
          event.preventDefault();
7751
        } else if (isRedo) {
7752
          that.redo();
7753
          event.preventDefault();
7754
        }
7755
      });
7756
 
7757
      // Catch delete and backspace
7758
      dom.observe(this.element, "keydown", function(event) {
7759
        var keyCode = event.keyCode;
7760
        if (keyCode === lastKey) {
7761
          return;
7762
        }
7763
 
7764
        lastKey = keyCode;
7765
 
7766
        if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
7767
          that.transact();
7768
        }
7769
      });
7770
 
7771
      // Now this is very hacky:
7772
      // These days browsers don't offer a undo/redo event which we could hook into
7773
      // to be notified when the user hits undo/redo in the contextmenu.
7774
      // Therefore we simply insert two elements as soon as the contextmenu gets opened.
7775
      // The last element being inserted will be immediately be removed again by a exexCommand("undo")
7776
      //  => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu
7777
      //  => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu
7778
      if (wysihtml5.browser.hasUndoInContextMenu()) {
7779
        var interval, observed, cleanUp = function() {
7780
          cleanTempElements(doc);
7781
          clearInterval(interval);
7782
        };
7783
 
7784
        dom.observe(this.element, "contextmenu", function() {
7785
          cleanUp();
7786
          that.composer.selection.executeAndRestoreSimple(function() {
7787
            if (that.element.lastChild) {
7788
              that.composer.selection.setAfter(that.element.lastChild);
7789
            }
7790
 
7791
            // enable undo button in context menu
7792
            doc.execCommand("insertHTML", false, UNDO_HTML);
7793
            // enable redo button in context menu
7794
            doc.execCommand("insertHTML", false, REDO_HTML);
7795
            doc.execCommand("undo", false, null);
7796
          });
7797
 
7798
          interval = setInterval(function() {
7799
            if (doc.getElementById("_wysihtml5-redo")) {
7800
              cleanUp();
7801
              that.redo();
7802
            } else if (!doc.getElementById("_wysihtml5-undo")) {
7803
              cleanUp();
7804
              that.undo();
7805
            }
7806
          }, 400);
7807
 
7808
          if (!observed) {
7809
            observed = true;
7810
            dom.observe(document, "mousedown", cleanUp);
7811
            dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp);
7812
          }
7813
        });
7814
      }
7815
 
7816
      this.editor
7817
        .observe("newword:composer", function() {
7818
          that.transact();
7819
        })
7820
 
7821
        .observe("beforecommand:composer", function() {
7822
          that.transact();
7823
        });
7824
    },
7825
 
7826
    transact: function() {
7827
      var previousHtml  = this.history[this.position - 1],
7828
          currentHtml   = this.composer.getValue();
7829
 
7830
      if (currentHtml == previousHtml) {
7831
        return;
7832
      }
7833
 
7834
      var length = this.history.length = this.position;
7835
      if (length > MAX_HISTORY_ENTRIES) {
7836
        this.history.shift();
7837
        this.position--;
7838
      }
7839
 
7840
      this.position++;
7841
      this.history.push(currentHtml);
7842
    },
7843
 
7844
    undo: function() {
7845
      this.transact();
7846
 
7847
      if (this.position <= 1) {
7848
        return;
7849
      }
7850
 
7851
      this.set(this.history[--this.position - 1]);
7852
      this.editor.fire("undo:composer");
7853
    },
7854
 
7855
    redo: function() {
7856
      if (this.position >= this.history.length) {
7857
        return;
7858
      }
7859
 
7860
      this.set(this.history[++this.position - 1]);
7861
      this.editor.fire("redo:composer");
7862
    },
7863
 
7864
    set: function(html) {
7865
      this.composer.setValue(html);
7866
      this.editor.focus(true);
7867
    }
7868
  });
7869
})(wysihtml5);
7870
/**
7871
 * TODO: the following methods still need unit test coverage
7872
 */
7873
wysihtml5.views.View = Base.extend(
7874
  /** @scope wysihtml5.views.View.prototype */ {
7875
  constructor: function(parent, textareaElement, config) {
7876
    this.parent   = parent;
7877
    this.element  = textareaElement;
7878
    this.config   = config;
7879
 
7880
    this._observeViewChange();
7881
  },
7882
 
7883
  _observeViewChange: function() {
7884
    var that = this;
7885
    this.parent.observe("beforeload", function() {
7886
      that.parent.observe("change_view", function(view) {
7887
        if (view === that.name) {
7888
          that.parent.currentView = that;
7889
          that.show();
7890
          // Using tiny delay here to make sure that the placeholder is set before focusing
7891
          setTimeout(function() { that.focus(); }, 0);
7892
        } else {
7893
          that.hide();
7894
        }
7895
      });
7896
    });
7897
  },
7898
 
7899
  focus: function() {
7900
    if (this.element.ownerDocument.querySelector(":focus") === this.element) {
7901
      return;
7902
    }
7903
 
7904
    try { this.element.focus(); } catch(e) {}
7905
  },
7906
 
7907
  hide: function() {
7908
    this.element.style.display = "none";
7909
  },
7910
 
7911
  show: function() {
7912
    this.element.style.display = "";
7913
  },
7914
 
7915
  disable: function() {
7916
    this.element.setAttribute("disabled", "disabled");
7917
  },
7918
 
7919
  enable: function() {
7920
    this.element.removeAttribute("disabled");
7921
  }
7922
});(function(wysihtml5) {
7923
  var dom       = wysihtml5.dom,
7924
      browser   = wysihtml5.browser;
7925
 
7926
  wysihtml5.views.Composer = wysihtml5.views.View.extend(
7927
    /** @scope wysihtml5.views.Composer.prototype */ {
7928
    name: "composer",
7929
 
7930
    // Needed for firefox in order to display a proper caret in an empty contentEditable
7931
    CARET_HACK: "<br>",
7932
 
7933
    constructor: function(parent, textareaElement, config) {
7934
      this.base(parent, textareaElement, config);
7935
      this.textarea = this.parent.textarea;
7936
      this._initSandbox();
7937
    },
7938
 
7939
    clear: function() {
7940
      this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK;
7941
    },
7942
 
7943
    getValue: function(parse) {
7944
      var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element);
7945
 
7946
      if (parse) {
7947
        value = this.parent.parse(value);
7948
      }
7949
 
7950
      // Replace all "zero width no breaking space" chars
7951
      // which are used as hacks to enable some functionalities
7952
      // Also remove all CARET hacks that somehow got left
7953
      value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by("");
7954
 
7955
      return value;
7956
    },
7957
 
7958
    setValue: function(html, parse) {
7959
      if (parse) {
7960
        html = this.parent.parse(html);
7961
      }
7962
      this.element.innerHTML = html;
7963
    },
7964
 
7965
    show: function() {
7966
      this.iframe.style.display = this._displayStyle || "";
7967
 
7968
      // Firefox needs this, otherwise contentEditable becomes uneditable
7969
      this.disable();
7970
      this.enable();
7971
    },
7972
 
7973
    hide: function() {
7974
      this._displayStyle = dom.getStyle("display").from(this.iframe);
7975
      if (this._displayStyle === "none") {
7976
        this._displayStyle = null;
7977
      }
7978
      this.iframe.style.display = "none";
7979
    },
7980
 
7981
    disable: function() {
7982
      this.element.removeAttribute("contentEditable");
7983
      this.base();
7984
    },
7985
 
7986
    enable: function() {
7987
      this.element.setAttribute("contentEditable", "true");
7988
      this.base();
7989
    },
7990
 
7991
    focus: function(setToEnd) {
7992
      // IE 8 fires the focus event after .focus()
7993
      // This is needed by our simulate_placeholder.js to work
7994
      // therefore we clear it ourselves this time
7995
      if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
7996
        this.clear();
7997
      }
7998
 
7999
      this.base();
8000
 
8001
      var lastChild = this.element.lastChild;
8002
      if (setToEnd && lastChild) {
8003
        if (lastChild.nodeName === "BR") {
8004
          this.selection.setBefore(this.element.lastChild);
8005
        } else {
8006
          this.selection.setAfter(this.element.lastChild);
8007
        }
8008
      }
8009
    },
8010
 
8011
    getTextContent: function() {
8012
      return dom.getTextContent(this.element);
8013
    },
8014
 
8015
    hasPlaceholderSet: function() {
8016
      return this.getTextContent() == this.textarea.element.getAttribute("placeholder");
8017
    },
8018
 
8019
    isEmpty: function() {
8020
      var innerHTML               = this.element.innerHTML,
8021
          elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea";
8022
      return innerHTML === ""              ||
8023
             innerHTML === this.CARET_HACK ||
8024
             this.hasPlaceholderSet()      ||
8025
             (this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue));
8026
    },
8027
 
8028
    _initSandbox: function() {
8029
      var that = this;
8030
 
8031
      this.sandbox = new dom.Sandbox(function() {
8032
        that._create();
8033
      }, {
8034
        stylesheets:  this.config.stylesheets
8035
      });
8036
      this.iframe  = this.sandbox.getIframe();
8037
 
8038
      // Create hidden field which tells the server after submit, that the user used an wysiwyg editor
8039
      var hiddenField = document.createElement("input");
8040
      hiddenField.type   = "hidden";
8041
      hiddenField.name   = "_wysihtml5_mode";
8042
      hiddenField.value  = 1;
8043
 
8044
      // Store reference to current wysihtml5 instance on the textarea element
8045
      var textareaElement = this.textarea.element;
8046
      dom.insert(this.iframe).after(textareaElement);
8047
      dom.insert(hiddenField).after(textareaElement);
8048
    },
8049
 
8050
    _create: function() {
8051
      var that = this;
8052
 
8053
      this.doc                = this.sandbox.getDocument();
8054
      this.element            = this.doc.body;
8055
      this.textarea           = this.parent.textarea;
8056
      this.element.innerHTML  = this.textarea.getValue(true);
8057
      this.enable();
8058
 
8059
      // Make sure our selection handler is ready
8060
      this.selection = new wysihtml5.Selection(this.parent);
8061
 
8062
      // Make sure commands dispatcher is ready
8063
      this.commands  = new wysihtml5.Commands(this.parent);
8064
 
8065
      dom.copyAttributes([
8066
        "className", "spellcheck", "title", "lang", "dir", "accessKey"
8067
      ]).from(this.textarea.element).to(this.element);
8068
 
8069
      dom.addClass(this.element, this.config.composerClassName);
8070
 
8071
      // Make the editor look like the original textarea, by syncing styles
8072
      if (this.config.style) {
8073
        this.style();
8074
      }
8075
 
8076
      this.observe();
8077
 
8078
      var name = this.config.name;
8079
      if (name) {
8080
        dom.addClass(this.element, name);
8081
        dom.addClass(this.iframe, name);
8082
      }
8083
 
8084
      // Simulate html5 placeholder attribute on contentEditable element
8085
      var placeholderText = typeof(this.config.placeholder) === "string"
8086
        ? this.config.placeholder
8087
        : this.textarea.element.getAttribute("placeholder");
8088
      if (placeholderText) {
8089
        dom.simulatePlaceholder(this.parent, this, placeholderText);
8090
      }
8091
 
8092
      // Make sure that the browser avoids using inline styles whenever possible
8093
      this.commands.exec("styleWithCSS", false);
8094
 
8095
      this._initAutoLinking();
8096
      this._initObjectResizing();
8097
      this._initUndoManager();
8098
 
8099
      // Simulate html5 autofocus on contentEditable element
8100
      if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) {
8101
        setTimeout(function() { that.focus(); }, 100);
8102
      }
8103
 
8104
      wysihtml5.quirks.insertLineBreakOnReturn(this);
8105
 
8106
      // IE sometimes leaves a single paragraph, which can't be removed by the user
8107
      if (!browser.clearsContentEditableCorrectly()) {
8108
        wysihtml5.quirks.ensureProperClearing(this);
8109
      }
8110
 
8111
      if (!browser.clearsListsInContentEditableCorrectly()) {
8112
        wysihtml5.quirks.ensureProperClearingOfLists(this);
8113
      }
8114
 
8115
      // Set up a sync that makes sure that textarea and editor have the same content
8116
      if (this.initSync && this.config.sync) {
8117
        this.initSync();
8118
      }
8119
 
8120
      // Okay hide the textarea, we are ready to go
8121
      this.textarea.hide();
8122
 
8123
      // Fire global (before-)load event
8124
      this.parent.fire("beforeload").fire("load");
8125
    },
8126
 
8127
    _initAutoLinking: function() {
8128
      var that                           = this,
8129
          supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
8130
          supportsAutoLinking            = browser.doesAutoLinkingInContentEditable();
8131
      if (supportsDisablingOfAutoLinking) {
8132
        this.commands.exec("autoUrlDetect", false);
8133
      }
8134
 
8135
      if (!this.config.autoLink) {
8136
        return;
8137
      }
8138
 
8139
      // Only do the auto linking by ourselves when the browser doesn't support auto linking
8140
      // OR when he supports auto linking but we were able to turn it off (IE9+)
8141
      if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
8142
        this.parent.observe("newword:composer", function() {
8143
          that.selection.executeAndRestore(function(startContainer, endContainer) {
8144
            dom.autoLink(endContainer.parentNode);
8145
          });
8146
        });
8147
      }
8148
 
8149
      // Assuming we have the following:
8150
      //  <a href="http://www.google.de">http://www.google.de</a>
8151
      // If a user now changes the url in the innerHTML we want to make sure that
8152
      // it's synchronized with the href attribute (as long as the innerHTML is still a url)
8153
      var // Use a live NodeList to check whether there are any links in the document
8154
          links           = this.sandbox.getDocument().getElementsByTagName("a"),
8155
          // The autoLink helper method reveals a reg exp to detect correct urls
8156
          urlRegExp       = dom.autoLink.URL_REG_EXP,
8157
          getTextContent  = function(element) {
8158
            var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim();
8159
            if (textContent.substr(0, 4) === "www.") {
8160
              textContent = "http://" + textContent;
8161
            }
8162
            return textContent;
8163
          };
8164
 
8165
      dom.observe(this.element, "keydown", function(event) {
8166
        if (!links.length) {
8167
          return;
8168
        }
8169
 
8170
        var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
8171
            link         = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),
8172
            textContent;
8173
 
8174
        if (!link) {
8175
          return;
8176
        }
8177
 
8178
        textContent = getTextContent(link);
8179
        // keydown is fired before the actual content is changed
8180
        // therefore we set a timeout to change the href
8181
        setTimeout(function() {
8182
          var newTextContent = getTextContent(link);
8183
          if (newTextContent === textContent) {
8184
            return;
8185
          }
8186
 
8187
          // Only set href when new href looks like a valid url
8188
          if (newTextContent.match(urlRegExp)) {
8189
            link.setAttribute("href", newTextContent);
8190
          }
8191
        }, 0);
8192
      });
8193
    },
8194
 
8195
    _initObjectResizing: function() {
8196
      var properties        = ["width", "height"],
8197
          propertiesLength  = properties.length,
8198
          element           = this.element;
8199
 
8200
      this.commands.exec("enableObjectResizing", this.config.allowObjectResizing);
8201
 
8202
      if (this.config.allowObjectResizing) {
8203
         // IE sets inline styles after resizing objects
8204
         // The following lines make sure that the width/height css properties
8205
         // are copied over to the width/height attributes
8206
        if (browser.supportsEvent("resizeend")) {
8207
          dom.observe(element, "resizeend", function(event) {
8208
            var target = event.target || event.srcElement,
8209
                style  = target.style,
8210
                i      = 0,
8211
                property;
8212
            for(; i<propertiesLength; i++) {
8213
              property = properties[i];
8214
              if (style[property]) {
8215
                target.setAttribute(property, parseInt(style[property], 10));
8216
                style[property] = "";
8217
              }
8218
            }
8219
            // After resizing IE sometimes forgets to remove the old resize handles
8220
            wysihtml5.quirks.redraw(element);
8221
          });
8222
        }
8223
      } else {
8224
        if (browser.supportsEvent("resizestart")) {
8225
          dom.observe(element, "resizestart", function(event) { event.preventDefault(); });
8226
        }
8227
      }
8228
    },
8229
 
8230
    _initUndoManager: function() {
8231
      new wysihtml5.UndoManager(this.parent);
8232
    }
8233
  });
8234
})(wysihtml5);(function(wysihtml5) {
8235
  var dom             = wysihtml5.dom,
8236
      doc             = document,
8237
      win             = window,
8238
      HOST_TEMPLATE   = doc.createElement("div"),
8239
      /**
8240
       * Styles to copy from textarea to the composer element
8241
       */
8242
      TEXT_FORMATTING = [
8243
        "background-color",
8244
        "color", "cursor",
8245
        "font-family", "font-size", "font-style", "font-variant", "font-weight",
8246
        "line-height", "letter-spacing",
8247
        "text-align", "text-decoration", "text-indent", "text-rendering",
8248
        "word-break", "word-wrap", "word-spacing"
8249
      ],
8250
      /**
8251
       * Styles to copy from textarea to the iframe
8252
       */
8253
      BOX_FORMATTING = [
8254
        "background-color",
8255
        "border-collapse",
8256
        "border-bottom-color", "border-bottom-style", "border-bottom-width",
8257
        "border-left-color", "border-left-style", "border-left-width",
8258
        "border-right-color", "border-right-style", "border-right-width",
8259
        "border-top-color", "border-top-style", "border-top-width",
8260
        "clear", "display", "float",
8261
        "margin-bottom", "margin-left", "margin-right", "margin-top",
8262
        "outline-color", "outline-offset", "outline-width", "outline-style",
8263
        "padding-left", "padding-right", "padding-top", "padding-bottom",
8264
        "position", "top", "left", "right", "bottom", "z-index",
8265
        "vertical-align", "text-align",
8266
        "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing",
8267
        "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow",
8268
        "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius",
8269
        "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius",
8270
        "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius",
8271
        "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius",
8272
        "width", "height"
8273
      ],
8274
      /**
8275
       * Styles to sync while the window gets resized
8276
       */
8277
      RESIZE_STYLE = [
8278
        "width", "height",
8279
        "top", "left", "right", "bottom"
8280
      ],
8281
      ADDITIONAL_CSS_RULES = [
8282
        "html             { height: 100%; }",
8283
        "body             { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }",
8284
        "._wysihtml5-temp { display: none; }",
8285
        wysihtml5.browser.isGecko ?
8286
          "body.placeholder { color: graytext !important; }" :
8287
          "body.placeholder { color: #a9a9a9 !important; }",
8288
        "body[disabled]   { background-color: #eee !important; color: #999 !important; cursor: default !important; }",
8289
        // Ensure that user see's broken images and can delete them
8290
        "img:-moz-broken  { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
8291
      ];
8292
 
8293
  /**
8294
   * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
8295
   * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
8296
   *
8297
   * Other browsers need a more hacky way: (pssst don't tell my mama)
8298
   * In order to prevent the element being scrolled into view when focusing it, we simply
8299
   * move it out of the scrollable area, focus it, and reset it's position
8300
   */
8301
  var focusWithoutScrolling = function(element) {
8302
    if (element.setActive) {
8303
      // Following line could cause a js error when the textarea is invisible
8304
      // See https://github.com/xing/wysihtml5/issues/9
8305
      try { element.setActive(); } catch(e) {}
8306
    } else {
8307
      var elementStyle = element.style,
8308
          originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
8309
          originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
8310
          originalStyles = {
8311
            position:         elementStyle.position,
8312
            top:              elementStyle.top,
8313
            left:             elementStyle.left,
8314
            WebkitUserSelect: elementStyle.WebkitUserSelect
8315
          };
8316
 
8317
      dom.setStyles({
8318
        position:         "absolute",
8319
        top:              "-99999px",
8320
        left:             "-99999px",
8321
        // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
8322
        WebkitUserSelect: "none"
8323
      }).on(element);
8324
 
8325
      element.focus();
8326
 
8327
      dom.setStyles(originalStyles).on(element);
8328
 
8329
      if (win.scrollTo) {
8330
        // Some browser extensions unset this method to prevent annoyances
8331
        // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
8332
        // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
8333
        win.scrollTo(originalScrollLeft, originalScrollTop);
8334
      }
8335
    }
8336
  };
8337
 
8338
 
8339
  wysihtml5.views.Composer.prototype.style = function() {
8340
    var that                  = this,
8341
        originalActiveElement = doc.querySelector(":focus"),
8342
        textareaElement       = this.textarea.element,
8343
        hasPlaceholder        = textareaElement.hasAttribute("placeholder"),
8344
        originalPlaceholder   = hasPlaceholder && textareaElement.getAttribute("placeholder");
8345
    this.focusStylesHost      = this.focusStylesHost  || HOST_TEMPLATE.cloneNode(false);
8346
    this.blurStylesHost       = this.blurStylesHost   || HOST_TEMPLATE.cloneNode(false);
8347
 
8348
    // Remove placeholder before copying (as the placeholder has an affect on the computed style)
8349
    if (hasPlaceholder) {
8350
      textareaElement.removeAttribute("placeholder");
8351
    }
8352
 
8353
    if (textareaElement === originalActiveElement) {
8354
      textareaElement.blur();
8355
    }
8356
 
8357
    // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
8358
    dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost);
8359
 
8360
    // --------- editor styles ---------
8361
    dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
8362
 
8363
    // --------- apply standard rules ---------
8364
    dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
8365
 
8366
    // --------- :focus styles ---------
8367
    focusWithoutScrolling(textareaElement);
8368
    dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
8369
    dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
8370
 
8371
    // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
8372
    // this is needed for when the change_view event is fired where the iframe is hidden and then
8373
    // the blur event fires and re-displays it
8374
    var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]);
8375
 
8376
    // --------- restore focus ---------
8377
    if (originalActiveElement) {
8378
      originalActiveElement.focus();
8379
    } else {
8380
      textareaElement.blur();
8381
    }
8382
 
8383
    // --------- restore placeholder ---------
8384
    if (hasPlaceholder) {
8385
      textareaElement.setAttribute("placeholder", originalPlaceholder);
8386
    }
8387
 
8388
    // When copying styles, we only get the computed style which is never returned in percent unit
8389
    // Therefore we've to recalculate style onresize
8390
    if (!wysihtml5.browser.hasCurrentStyleProperty()) {
8391
      var winObserver = dom.observe(win, "resize", function() {
8392
        // Remove event listener if composer doesn't exist anymore
8393
        if (!dom.contains(document.documentElement, that.iframe)) {
8394
          winObserver.stop();
8395
          return;
8396
        }
8397
        var originalTextareaDisplayStyle = dom.getStyle("display").from(textareaElement),
8398
            originalComposerDisplayStyle = dom.getStyle("display").from(that.iframe);
8399
        textareaElement.style.display = "";
8400
        that.iframe.style.display = "none";
8401
        dom.copyStyles(RESIZE_STYLE)
8402
          .from(textareaElement)
8403
          .to(that.iframe)
8404
          .andTo(that.focusStylesHost)
8405
          .andTo(that.blurStylesHost);
8406
        that.iframe.style.display = originalComposerDisplayStyle;
8407
        textareaElement.style.display = originalTextareaDisplayStyle;
8408
      });
8409
    }
8410
 
8411
    // --------- Sync focus/blur styles ---------
8412
    this.parent.observe("focus:composer", function() {
8413
      dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe);
8414
      dom.copyStyles(TEXT_FORMATTING)     .from(that.focusStylesHost).to(that.element);
8415
    });
8416
 
8417
    this.parent.observe("blur:composer", function() {
8418
      dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe);
8419
      dom.copyStyles(TEXT_FORMATTING)     .from(that.blurStylesHost).to(that.element);
8420
    });
8421
 
8422
    return this;
8423
  };
8424
})(wysihtml5);/**
8425
 * Taking care of events
8426
 *  - Simulating 'change' event on contentEditable element
8427
 *  - Handling drag & drop logic
8428
 *  - Catch paste events
8429
 *  - Dispatch proprietary newword:composer event
8430
 *  - Keyboard shortcuts
8431
 */
8432
(function(wysihtml5) {
8433
  var dom       = wysihtml5.dom,
8434
      browser   = wysihtml5.browser,
8435
      /**
8436
       * Map keyCodes to query commands
8437
       */
8438
      shortcuts = {
8439
        "66": "bold",     // B
8440
        "73": "italic",   // I
8441
        "85": "underline" // U
8442
      };
8443
 
8444
  wysihtml5.views.Composer.prototype.observe = function() {
8445
    var that                = this,
8446
        state               = this.getValue(),
8447
        iframe              = this.sandbox.getIframe(),
8448
        element             = this.element,
8449
        focusBlurElement    = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(),
8450
        // Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same
8451
        pasteEvents         = browser.supportsEvent("drop") ? ["drop", "paste"] : ["dragdrop", "paste"];
8452
 
8453
    // --------- destroy:composer event ---------
8454
    dom.observe(iframe, "DOMNodeRemoved", function() {
8455
      clearInterval(domNodeRemovedInterval);
8456
      that.parent.fire("destroy:composer");
8457
    });
8458
 
8459
    // DOMNodeRemoved event is not supported in IE 8
8460
    var domNodeRemovedInterval = setInterval(function() {
8461
      if (!dom.contains(document.documentElement, iframe)) {
8462
        clearInterval(domNodeRemovedInterval);
8463
        that.parent.fire("destroy:composer");
8464
      }
8465
    }, 250);
8466
 
8467
 
8468
    // --------- Focus & blur logic ---------
8469
    dom.observe(focusBlurElement, "focus", function() {
8470
      that.parent.fire("focus").fire("focus:composer");
8471
 
8472
      // Delay storing of state until all focus handler are fired
8473
      // especially the one which resets the placeholder
8474
      setTimeout(function() { state = that.getValue(); }, 0);
8475
    });
8476
 
8477
    dom.observe(focusBlurElement, "blur", function() {
8478
      if (state !== that.getValue()) {
8479
        that.parent.fire("change").fire("change:composer");
8480
      }
8481
      that.parent.fire("blur").fire("blur:composer");
8482
    });
8483
 
8484
    if (wysihtml5.browser.isIos()) {
8485
      // When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus
8486
      // but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible)
8487
      // We prevent that by focusing a temporary input element which immediately loses focus
8488
      dom.observe(element, "blur", function() {
8489
        var input = element.ownerDocument.createElement("input"),
8490
            originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop,
8491
            originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
8492
        try {
8493
          that.selection.insertNode(input);
8494
        } catch(e) {
8495
          element.appendChild(input);
8496
        }
8497
        input.focus();
8498
        input.parentNode.removeChild(input);
8499
 
8500
        window.scrollTo(originalScrollLeft, originalScrollTop);
8501
      });
8502
    }
8503
 
8504
    // --------- Drag & Drop logic ---------
8505
    dom.observe(element, "dragenter", function() {
8506
      that.parent.fire("unset_placeholder");
8507
    });
8508
 
8509
    if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) {
8510
      dom.observe(element, ["dragover", "dragenter"], function(event) {
8511
        event.preventDefault();
8512
      });
8513
    }
8514
 
8515
    dom.observe(element, pasteEvents, function(event) {
8516
      var dataTransfer = event.dataTransfer,
8517
          data;
8518
 
8519
      if (dataTransfer && browser.supportsDataTransfer()) {
8520
        data = dataTransfer.getData("text/html") || dataTransfer.getData("text/plain");
8521
      }
8522
      if (data) {
8523
        element.focus();
8524
        that.commands.exec("insertHTML", data);
8525
        that.parent.fire("paste").fire("paste:composer");
8526
        event.stopPropagation();
8527
        event.preventDefault();
8528
      } else {
8529
        setTimeout(function() {
8530
          that.parent.fire("paste").fire("paste:composer");
8531
        }, 0);
8532
      }
8533
    });
8534
 
8535
    // --------- neword event ---------
8536
    dom.observe(element, "keyup", function(event) {
8537
      var keyCode = event.keyCode;
8538
      if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) {
8539
        that.parent.fire("newword:composer");
8540
      }
8541
    });
8542
 
8543
    this.parent.observe("paste:composer", function() {
8544
      setTimeout(function() { that.parent.fire("newword:composer"); }, 0);
8545
    });
8546
 
8547
    // --------- Make sure that images are selected when clicking on them ---------
8548
    if (!browser.canSelectImagesInContentEditable()) {
8549
      dom.observe(element, "mousedown", function(event) {
8550
        var target = event.target;
8551
        if (target.nodeName === "IMG") {
8552
          that.selection.selectNode(target);
8553
          event.preventDefault();
8554
        }
8555
      });
8556
    }
8557
 
8558
    // --------- Shortcut logic ---------
8559
    dom.observe(element, "keydown", function(event) {
8560
      var keyCode  = event.keyCode,
8561
          command  = shortcuts[keyCode];
8562
      if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
8563
        that.commands.exec(command);
8564
        event.preventDefault();
8565
      }
8566
    });
8567
 
8568
    // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor ---------
8569
    dom.observe(element, "keydown", function(event) {
8570
      var target  = that.selection.getSelectedNode(true),
8571
          keyCode = event.keyCode,
8572
          parent;
8573
      if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete
8574
        parent = target.parentNode;
8575
        // delete the <img>
8576
        parent.removeChild(target);
8577
        // and it's parent <a> too if it hasn't got any other child nodes
8578
        if (parent.nodeName === "A" && !parent.firstChild) {
8579
          parent.parentNode.removeChild(parent);
8580
        }
8581
 
8582
        setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);
8583
        event.preventDefault();
8584
      }
8585
    });
8586
 
8587
    // --------- Show url in tooltip when hovering links or images ---------
8588
    var titlePrefixes = {
8589
      IMG: "Image: ",
8590
      A:   "Link: "
8591
    };
8592
 
8593
    dom.observe(element, "mouseover", function(event) {
8594
      var target   = event.target,
8595
          nodeName = target.nodeName,
8596
          title;
8597
      if (nodeName !== "A" && nodeName !== "IMG") {
8598
        return;
8599
      }
8600
      var hasTitle = target.hasAttribute("title");
8601
      if(!hasTitle){
8602
        title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
8603
        target.setAttribute("title", title);
8604
      }
8605
    });
8606
  };
8607
})(wysihtml5);/**
8608
 * Class that takes care that the value of the composer and the textarea is always in sync
8609
 */
8610
(function(wysihtml5) {
8611
  var INTERVAL = 400;
8612
 
8613
  wysihtml5.views.Synchronizer = Base.extend(
8614
    /** @scope wysihtml5.views.Synchronizer.prototype */ {
8615
 
8616
    constructor: function(editor, textarea, composer) {
8617
      this.editor   = editor;
8618
      this.textarea = textarea;
8619
      this.composer = composer;
8620
 
8621
      this._observe();
8622
    },
8623
 
8624
    /**
8625
     * Sync html from composer to textarea
8626
     * Takes care of placeholders
8627
     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
8628
     */
8629
    fromComposerToTextarea: function(shouldParseHtml) {
8630
      this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml);
8631
    },
8632
 
8633
    /**
8634
     * Sync value of textarea to composer
8635
     * Takes care of placeholders
8636
     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
8637
     */
8638
    fromTextareaToComposer: function(shouldParseHtml) {
8639
      var textareaValue = this.textarea.getValue();
8640
      if (textareaValue) {
8641
        this.composer.setValue(textareaValue, shouldParseHtml);
8642
      } else {
8643
        this.composer.clear();
8644
        this.editor.fire("set_placeholder");
8645
      }
8646
    },
8647
 
8648
    /**
8649
     * Invoke syncing based on view state
8650
     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
8651
     */
8652
    sync: function(shouldParseHtml) {
8653
      if (this.editor.currentView.name === "textarea") {
8654
        this.fromTextareaToComposer(shouldParseHtml);
8655
      } else {
8656
        this.fromComposerToTextarea(shouldParseHtml);
8657
      }
8658
    },
8659
 
8660
    /**
8661
     * Initializes interval-based syncing
8662
     * also makes sure that on-submit the composer's content is synced with the textarea
8663
     * immediately when the form gets submitted
8664
     */
8665
    _observe: function() {
8666
      var interval,
8667
          that          = this,
8668
          form          = this.textarea.element.form,
8669
          startInterval = function() {
8670
            interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
8671
          },
8672
          stopInterval  = function() {
8673
            clearInterval(interval);
8674
            interval = null;
8675
          };
8676
 
8677
      startInterval();
8678
 
8679
      if (form) {
8680
        // If the textarea is in a form make sure that after onreset and onsubmit the composer
8681
        // has the correct state
8682
        wysihtml5.dom.observe(form, "submit", function() {
8683
          that.sync(true);
8684
        });
8685
        wysihtml5.dom.observe(form, "reset", function() {
8686
          setTimeout(function() { that.fromTextareaToComposer(); }, 0);
8687
        });
8688
      }
8689
 
8690
      this.editor.observe("change_view", function(view) {
8691
        if (view === "composer" && !interval) {
8692
          that.fromTextareaToComposer(true);
8693
          startInterval();
8694
        } else if (view === "textarea") {
8695
          that.fromComposerToTextarea(true);
8696
          stopInterval();
8697
        }
8698
      });
8699
 
8700
      this.editor.observe("destroy:composer", stopInterval);
8701
    }
8702
  });
8703
})(wysihtml5);
8704
wysihtml5.views.Textarea = wysihtml5.views.View.extend(
8705
  /** @scope wysihtml5.views.Textarea.prototype */ {
8706
  name: "textarea",
8707
 
8708
  constructor: function(parent, textareaElement, config) {
8709
    this.base(parent, textareaElement, config);
8710
 
8711
    this._observe();
8712
  },
8713
 
8714
  clear: function() {
8715
    this.element.value = "";
8716
  },
8717
 
8718
  getValue: function(parse) {
8719
    var value = this.isEmpty() ? "" : this.element.value;
8720
    if (parse) {
8721
      value = this.parent.parse(value);
8722
    }
8723
    return value;
8724
  },
8725
 
8726
  setValue: function(html, parse) {
8727
    if (parse) {
8728
      html = this.parent.parse(html);
8729
    }
8730
    this.element.value = html;
8731
  },
8732
 
8733
  hasPlaceholderSet: function() {
8734
    var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),
8735
        placeholderText     = this.element.getAttribute("placeholder") || null,
8736
        value               = this.element.value,
8737
        isEmpty             = !value;
8738
    return (supportsPlaceholder && isEmpty) || (value === placeholderText);
8739
  },
8740
 
8741
  isEmpty: function() {
8742
    return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
8743
  },
8744
 
8745
  _observe: function() {
8746
    var element = this.element,
8747
        parent  = this.parent,
8748
        eventMapping = {
8749
          focusin:  "focus",
8750
          focusout: "blur"
8751
        },
8752
        /**
8753
         * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
8754
         * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
8755
         */
8756
        events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
8757
 
8758
    parent.observe("beforeload", function() {
8759
      wysihtml5.dom.observe(element, events, function(event) {
8760
        var eventName = eventMapping[event.type] || event.type;
8761
        parent.fire(eventName).fire(eventName + ":textarea");
8762
      });
8763
 
8764
      wysihtml5.dom.observe(element, ["paste", "drop"], function() {
8765
        setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
8766
      });
8767
    });
8768
  }
8769
});/**
8770
 * Toolbar Dialog
8771
 *
8772
 * @param {Element} link The toolbar link which causes the dialog to show up
8773
 * @param {Element} container The dialog container
8774
 *
8775
 * @example
8776
 *    <!-- Toolbar link -->
8777
 *    <a data-wysihtml5-command="insertImage">insert an image</a>
8778
 *
8779
 *    <!-- Dialog -->
8780
 *    <div data-wysihtml5-dialog="insertImage" style="display: none;">
8781
 *      <label>
8782
 *        URL: <input data-wysihtml5-dialog-field="src" value="http://">
8783
 *      </label>
8784
 *      <label>
8785
 *        Alternative text: <input data-wysihtml5-dialog-field="alt" value="">
8786
 *      </label>
8787
 *    </div>
8788
 *
8789
 *    <script>
8790
 *      var dialog = new wysihtml5.toolbar.Dialog(
8791
 *        document.querySelector("[data-wysihtml5-command='insertImage']"),
8792
 *        document.querySelector("[data-wysihtml5-dialog='insertImage']")
8793
 *      );
8794
 *      dialog.observe("save", function(attributes) {
8795
 *        // do something
8796
 *      });
8797
 *    </script>
8798
 */
8799
(function(wysihtml5) {
8800
  var dom                     = wysihtml5.dom,
8801
      CLASS_NAME_OPENED       = "wysihtml5-command-dialog-opened",
8802
      SELECTOR_FORM_ELEMENTS  = "input, select, textarea",
8803
      SELECTOR_FIELDS         = "[data-wysihtml5-dialog-field]",
8804
      ATTRIBUTE_FIELDS        = "data-wysihtml5-dialog-field";
8805
 
8806
 
8807
  wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend(
8808
    /** @scope wysihtml5.toolbar.Dialog.prototype */ {
8809
    constructor: function(link, container) {
8810
      this.link       = link;
8811
      this.container  = container;
8812
    },
8813
 
8814
    _observe: function() {
8815
      if (this._observed) {
8816
        return;
8817
      }
8818
 
8819
      var that = this,
8820
          callbackWrapper = function(event) {
8821
            var attributes = that._serialize();
8822
            if (attributes == that.elementToChange) {
8823
              that.fire("edit", attributes);
8824
            } else {
8825
              that.fire("save", attributes);
8826
            }
8827
            that.hide();
8828
            event.preventDefault();
8829
            event.stopPropagation();
8830
          };
8831
 
8832
      dom.observe(that.link, "click", function(event) {
8833
        if (dom.hasClass(that.link, CLASS_NAME_OPENED)) {
8834
          setTimeout(function() { that.hide(); }, 0);
8835
        }
8836
      });
8837
 
8838
      dom.observe(this.container, "keydown", function(event) {
8839
        var keyCode = event.keyCode;
8840
        if (keyCode === wysihtml5.ENTER_KEY) {
8841
          callbackWrapper(event);
8842
        }
8843
        if (keyCode === wysihtml5.ESCAPE_KEY) {
8844
          that.hide();
8845
        }
8846
      });
8847
 
8848
      dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper);
8849
 
8850
      dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) {
8851
        that.fire("cancel");
8852
        that.hide();
8853
        event.preventDefault();
8854
        event.stopPropagation();
8855
      });
8856
 
8857
      var formElements  = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS),
8858
          i             = 0,
8859
          length        = formElements.length,
8860
          _clearInterval = function() { clearInterval(that.interval); };
8861
      for (; i<length; i++) {
8862
        dom.observe(formElements[i], "change", _clearInterval);
8863
      }
8864
 
8865
      this._observed = true;
8866
    },
8867
 
8868
    /**
8869
     * Grabs all fields in the dialog and puts them in key=>value style in an object which
8870
     * then gets returned
8871
     */
8872
    _serialize: function() {
8873
      var data    = this.elementToChange || {},
8874
          fields  = this.container.querySelectorAll(SELECTOR_FIELDS),
8875
          length  = fields.length,
8876
          i       = 0;
8877
      for (; i<length; i++) {
8878
        data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
8879
      }
8880
      return data;
8881
    },
8882
 
8883
    /**
8884
     * Takes the attributes of the "elementToChange"
8885
     * and inserts them in their corresponding dialog input fields
8886
     *
8887
     * Assume the "elementToChange" looks like this:
8888
     *    <a href="http://www.google.com" target="_blank">foo</a>
8889
     *
8890
     * and we have the following dialog:
8891
     *    <input type="text" data-wysihtml5-dialog-field="href" value="">
8892
     *    <input type="text" data-wysihtml5-dialog-field="target" value="">
8893
     *
8894
     * after calling _interpolate() the dialog will look like this
8895
     *    <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com">
8896
     *    <input type="text" data-wysihtml5-dialog-field="target" value="_blank">
8897
     *
8898
     * Basically it adopted the attribute values into the corresponding input fields
8899
     *
8900
     */
8901
    _interpolate: function(avoidHiddenFields) {
8902
      var field,
8903
          fieldName,
8904
          newValue,
8905
          focusedElement = document.querySelector(":focus"),
8906
          fields         = this.container.querySelectorAll(SELECTOR_FIELDS),
8907
          length         = fields.length,
8908
          i              = 0;
8909
      for (; i<length; i++) {
8910
        field = fields[i];
8911
 
8912
        // Never change elements where the user is currently typing in
8913
        if (field === focusedElement) {
8914
          continue;
8915
        }
8916
 
8917
        // Don't update hidden fields
8918
        // See https://github.com/xing/wysihtml5/pull/14
8919
        if (avoidHiddenFields && field.type === "hidden") {
8920
          continue;
8921
        }
8922
 
8923
        fieldName = field.getAttribute(ATTRIBUTE_FIELDS);
8924
        newValue  = this.elementToChange ? (this.elementToChange[fieldName] || "") : field.defaultValue;
8925
        field.value = newValue;
8926
      }
8927
    },
8928
 
8929
    /**
8930
     * Show the dialog element
8931
     */
8932
    show: function(elementToChange) {
8933
      var that        = this,
8934
          firstField  = this.container.querySelector(SELECTOR_FORM_ELEMENTS);
8935
      this.elementToChange = elementToChange;
8936
      this._observe();
8937
      this._interpolate();
8938
      if (elementToChange) {
8939
        this.interval = setInterval(function() { that._interpolate(true); }, 500);
8940
      }
8941
      dom.addClass(this.link, CLASS_NAME_OPENED);
8942
      this.container.style.display = "";
8943
      this.fire("show");
8944
      if (firstField && !elementToChange) {
8945
        try {
8946
          firstField.focus();
8947
        } catch(e) {}
8948
      }
8949
    },
8950
 
8951
    /**
8952
     * Hide the dialog element
8953
     */
8954
    hide: function() {
8955
      clearInterval(this.interval);
8956
      this.elementToChange = null;
8957
      dom.removeClass(this.link, CLASS_NAME_OPENED);
8958
      this.container.style.display = "none";
8959
      this.fire("hide");
8960
    }
8961
  });
8962
})(wysihtml5);
8963
/**
8964
 * Converts speech-to-text and inserts this into the editor
8965
 * As of now (2011/03/25) this only is supported in Chrome >= 11
8966
 *
8967
 * Note that it sends the recorded audio to the google speech recognition api:
8968
 * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec
8969
 *
8970
 * Current HTML5 draft can be found here
8971
 * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
8972
 *
8973
 * "Accessing Google Speech API Chrome 11"
8974
 * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
8975
 */
8976
(function(wysihtml5) {
8977
  var dom = wysihtml5.dom;
8978
 
8979
  var linkStyles = {
8980
    position: "relative"
8981
  };
8982
 
8983
  var wrapperStyles = {
8984
    left:     0,
8985
    margin:   0,
8986
    opacity:  0,
8987
    overflow: "hidden",
8988
    padding:  0,
8989
    position: "absolute",
8990
    top:      0,
8991
    zIndex:   1
8992
  };
8993
 
8994
  var inputStyles = {
8995
    cursor:     "inherit",
8996
    fontSize:   "50px",
8997
    height:     "50px",
8998
    marginTop:  "-25px",
8999
    outline:    0,
9000
    padding:    0,
9001
    position:   "absolute",
9002
    right:      "-4px",
9003
    top:        "50%"
9004
  };
9005
 
9006
  var inputAttributes = {
9007
    "x-webkit-speech": "",
9008
    "speech":          ""
9009
  };
9010
 
9011
  wysihtml5.toolbar.Speech = function(parent, link) {
9012
    var input = document.createElement("input");
9013
    if (!wysihtml5.browser.supportsSpeechApiOn(input)) {
9014
      link.style.display = "none";
9015
      return;
9016
    }
9017
 
9018
    var wrapper = document.createElement("div");
9019
 
9020
    wysihtml5.lang.object(wrapperStyles).merge({
9021
      width:  link.offsetWidth  + "px",
9022
      height: link.offsetHeight + "px"
9023
    });
9024
 
9025
    dom.insert(input).into(wrapper);
9026
    dom.insert(wrapper).into(link);
9027
 
9028
    dom.setStyles(inputStyles).on(input);
9029
    dom.setAttributes(inputAttributes).on(input)
9030
 
9031
    dom.setStyles(wrapperStyles).on(wrapper);
9032
    dom.setStyles(linkStyles).on(link);
9033
 
9034
    var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange";
9035
    dom.observe(input, eventName, function() {
9036
      parent.execCommand("insertText", input.value);
9037
      input.value = "";
9038
    });
9039
 
9040
    dom.observe(input, "click", function(event) {
9041
      if (dom.hasClass(link, "wysihtml5-command-disabled")) {
9042
        event.preventDefault();
9043
      }
9044
 
9045
      event.stopPropagation();
9046
    });
9047
  };
9048
})(wysihtml5);/**
9049
 * Toolbar
9050
 *
9051
 * @param {Object} parent Reference to instance of Editor instance
9052
 * @param {Element} container Reference to the toolbar container element
9053
 *
9054
 * @example
9055
 *    <div id="toolbar">
9056
 *      <a data-wysihtml5-command="createLink">insert link</a>
9057
 *      <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a>
9058
 *    </div>
9059
 *
9060
 *    <script>
9061
 *      var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar"));
9062
 *    </script>
9063
 */
9064
(function(wysihtml5) {
9065
  var CLASS_NAME_COMMAND_DISABLED   = "wysihtml5-command-disabled",
9066
      CLASS_NAME_COMMANDS_DISABLED  = "wysihtml5-commands-disabled",
9067
      CLASS_NAME_COMMAND_ACTIVE     = "wysihtml5-command-active",
9068
      CLASS_NAME_ACTION_ACTIVE      = "wysihtml5-action-active",
9069
      dom                           = wysihtml5.dom;
9070
 
9071
  wysihtml5.toolbar.Toolbar = Base.extend(
9072
    /** @scope wysihtml5.toolbar.Toolbar.prototype */ {
9073
    constructor: function(editor, container) {
9074
      this.editor     = editor;
9075
      this.container  = typeof(container) === "string" ? document.getElementById(container) : container;
9076
      this.composer   = editor.composer;
9077
 
9078
      this._getLinks("command");
9079
      this._getLinks("action");
9080
 
9081
      this._observe();
9082
      this.show();
9083
 
9084
      var speechInputLinks  = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),
9085
          length            = speechInputLinks.length,
9086
          i                 = 0;
9087
      for (; i<length; i++) {
9088
        new wysihtml5.toolbar.Speech(this, speechInputLinks[i]);
9089
      }
9090
    },
9091
 
9092
    _getLinks: function(type) {
9093
      var links   = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(),
9094
          length  = links.length,
9095
          i       = 0,
9096
          mapping = this[type + "Mapping"] = {},
9097
          link,
9098
          group,
9099
          name,
9100
          value,
9101
          dialog;
9102
      for (; i<length; i++) {
9103
        link    = links[i];
9104
        name    = link.getAttribute("data-wysihtml5-" + type);
9105
        value   = link.getAttribute("data-wysihtml5-" + type + "-value");
9106
        group   = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']");
9107
        dialog  = this._getDialog(link, name);
9108
 
9109
        mapping[name + ":" + value] = {
9110
          link:   link,
9111
          group:  group,
9112
          name:   name,
9113
          value:  value,
9114
          dialog: dialog,
9115
          state:  false
9116
        };
9117
      }
9118
    },
9119
 
9120
    _getDialog: function(link, command) {
9121
      var that          = this,
9122
          dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"),
9123
          dialog,
9124
          caretBookmark;
9125
 
9126
      if (dialogElement) {
9127
        dialog = new wysihtml5.toolbar.Dialog(link, dialogElement);
9128
 
9129
        dialog.observe("show", function() {
9130
          caretBookmark = that.composer.selection.getBookmark();
9131
 
9132
          that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
9133
        });
9134
 
9135
        dialog.observe("save", function(attributes) {
9136
          if (caretBookmark) {
9137
            that.composer.selection.setBookmark(caretBookmark);
9138
          }
9139
          that._execCommand(command, attributes);
9140
 
9141
          that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
9142
        });
9143
 
9144
        dialog.observe("cancel", function() {
9145
          that.editor.focus(false);
9146
          that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
9147
        });
9148
      }
9149
      return dialog;
9150
    },
9151
 
9152
    /**
9153
     * @example
9154
     *    var toolbar = new wysihtml5.Toolbar();
9155
     *    // Insert a <blockquote> element or wrap current selection in <blockquote>
9156
     *    toolbar.execCommand("formatBlock", "blockquote");
9157
     */
9158
    execCommand: function(command, commandValue) {
9159
      if (this.commandsDisabled) {
9160
        return;
9161
      }
9162
 
9163
      var commandObj = this.commandMapping[command + ":" + commandValue];
9164
 
9165
      // Show dialog when available
9166
      if (commandObj && commandObj.dialog && !commandObj.state) {
9167
        commandObj.dialog.show();
9168
      } else {
9169
        this._execCommand(command, commandValue);
9170
      }
9171
    },
9172
 
9173
    _execCommand: function(command, commandValue) {
9174
      // Make sure that composer is focussed (false => don't move caret to the end)
9175
      this.editor.focus(false);
9176
 
9177
      this.composer.commands.exec(command, commandValue);
9178
      this._updateLinkStates();
9179
    },
9180
 
9181
    execAction: function(action) {
9182
      var editor = this.editor;
9183
      switch(action) {
9184
        case "change_view":
9185
          if (editor.currentView === editor.textarea) {
9186
            editor.fire("change_view", "composer");
9187
          } else {
9188
            editor.fire("change_view", "textarea");
9189
          }
9190
          break;
9191
      }
9192
    },
9193
 
9194
    _observe: function() {
9195
      var that      = this,
9196
          editor    = this.editor,
9197
          container = this.container,
9198
          links     = this.commandLinks.concat(this.actionLinks),
9199
          length    = links.length,
9200
          i         = 0;
9201
 
9202
      for (; i<length; i++) {
9203
        // 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied
9204
        // (you know, a:link { ... } doesn't match anchors with missing href attribute)
9205
        dom.setAttributes({
9206
          href:         "javascript:;",
9207
          unselectable: "on"
9208
        }).on(links[i]);
9209
      }
9210
 
9211
      // Needed for opera
9212
      dom.delegate(container, "[data-wysihtml5-command]", "mousedown", function(event) { event.preventDefault(); });
9213
 
9214
      dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) {
9215
        var link          = this,
9216
            command       = link.getAttribute("data-wysihtml5-command"),
9217
            commandValue  = link.getAttribute("data-wysihtml5-command-value");
9218
        that.execCommand(command, commandValue);
9219
        event.preventDefault();
9220
      });
9221
 
9222
      dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) {
9223
        var action = this.getAttribute("data-wysihtml5-action");
9224
        that.execAction(action);
9225
        event.preventDefault();
9226
      });
9227
 
9228
      editor.observe("focus:composer", function() {
9229
        that.bookmark = null;
9230
        clearInterval(that.interval);
9231
        that.interval = setInterval(function() { that._updateLinkStates(); }, 500);
9232
      });
9233
 
9234
      editor.observe("blur:composer", function() {
9235
        clearInterval(that.interval);
9236
      });
9237
 
9238
      editor.observe("destroy:composer", function() {
9239
        clearInterval(that.interval);
9240
      });
9241
 
9242
      editor.observe("change_view", function(currentView) {
9243
        // Set timeout needed in order to let the blur event fire first
9244
        setTimeout(function() {
9245
          that.commandsDisabled = (currentView !== "composer");
9246
          that._updateLinkStates();
9247
          if (that.commandsDisabled) {
9248
            dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED);
9249
          } else {
9250
            dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED);
9251
          }
9252
        }, 0);
9253
      });
9254
    },
9255
 
9256
    _updateLinkStates: function() {
9257
      var element           = this.composer.element,
9258
          commandMapping    = this.commandMapping,
9259
          actionMapping     = this.actionMapping,
9260
          i,
9261
          state,
9262
          action,
9263
          command;
9264
      // every millisecond counts... this is executed quite often
9265
      for (i in commandMapping) {
9266
        command = commandMapping[i];
9267
        if (this.commandsDisabled) {
9268
          state = false;
9269
          dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
9270
          if (command.group) {
9271
            dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
9272
          }
9273
          if (command.dialog) {
9274
            command.dialog.hide();
9275
          }
9276
        } else {
9277
          state = this.composer.commands.state(command.name, command.value);
9278
          if (wysihtml5.lang.object(state).isArray()) {
9279
            // Grab first and only object/element in state array, otherwise convert state into boolean
9280
            // to avoid showing a dialog for multiple selected elements which may have different attributes
9281
            // eg. when two links with different href are selected, the state will be an array consisting of both link elements
9282
            // but the dialog interface can only update one
9283
            state = state.length === 1 ? state[0] : true;
9284
          }
9285
          dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED);
9286
          if (command.group) {
9287
            dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED);
9288
          }
9289
        }
9290
 
9291
        if (command.state === state) {
9292
          continue;
9293
        }
9294
 
9295
        command.state = state;
9296
        if (state) {
9297
          dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
9298
          if (command.group) {
9299
            dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
9300
          }
9301
          if (command.dialog) {
9302
            if (typeof(state) === "object") {
9303
              command.dialog.show(state);
9304
            } else {
9305
              command.dialog.hide();
9306
            }
9307
          }
9308
        } else {
9309
          dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
9310
          if (command.group) {
9311
            dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
9312
          }
9313
          if (command.dialog) {
9314
            command.dialog.hide();
9315
          }
9316
        }
9317
      }
9318
 
9319
      for (i in actionMapping) {
9320
        action = actionMapping[i];
9321
 
9322
        if (action.name === "change_view") {
9323
          action.state = this.editor.currentView === this.editor.textarea;
9324
          if (action.state) {
9325
            dom.addClass(action.link, CLASS_NAME_ACTION_ACTIVE);
9326
          } else {
9327
            dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE);
9328
          }
9329
        }
9330
      }
9331
    },
9332
 
9333
    show: function() {
9334
      this.container.style.display = "";
9335
    },
9336
 
9337
    hide: function() {
9338
      this.container.style.display = "none";
9339
    }
9340
  });
9341
 
9342
})(wysihtml5);
9343
/**
9344
 * WYSIHTML5 Editor
9345
 *
9346
 * @param {Element} textareaElement Reference to the textarea which should be turned into a rich text interface
9347
 * @param {Object} [config] See defaultConfig object below for explanation of each individual config option
9348
 *
9349
 * @events
9350
 *    load
9351
 *    beforeload (for internal use only)
9352
 *    focus
9353
 *    focus:composer
9354
 *    focus:textarea
9355
 *    blur
9356
 *    blur:composer
9357
 *    blur:textarea
9358
 *    change
9359
 *    change:composer
9360
 *    change:textarea
9361
 *    paste
9362
 *    paste:composer
9363
 *    paste:textarea
9364
 *    newword:composer
9365
 *    destroy:composer
9366
 *    undo:composer
9367
 *    redo:composer
9368
 *    beforecommand:composer
9369
 *    aftercommand:composer
9370
 *    change_view
9371
 */
9372
(function(wysihtml5) {
9373
  var undef;
9374
 
9375
  var defaultConfig = {
9376
    // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body 
9377
    name:                 undef,
9378
    // Whether the editor should look like the textarea (by adopting styles)
9379
    style:                true,
9380
    // Id of the toolbar element, pass falsey value if you don't want any toolbar logic
9381
    toolbar:              undef,
9382
    // Whether urls, entered by the user should automatically become clickable-links
9383
    autoLink:             true,
9384
    // Object which includes parser rules to apply when html gets inserted via copy & paste
9385
    // See parser_rules/*.js for examples
9386
    parserRules:          { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} },
9387
    // Parser method to use when the user inserts content via copy & paste
9388
    parser:               wysihtml5.dom.parse,
9389
    // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
9390
    composerClassName:    "wysihtml5-editor",
9391
    // Class name to add to the body when the wysihtml5 editor is supported
9392
    bodyClassName:        "wysihtml5-supported",
9393
    // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
9394
    stylesheets:          [],
9395
    // Placeholder text to use, defaults to the placeholder attribute on the textarea element
9396
    placeholderText:      undef,
9397
    // Whether the composer should allow the user to manually resize images, tables etc.
9398
    allowObjectResizing:  true,
9399
    // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5)
9400
    supportTouchDevices:  true
9401
  };
9402
 
9403
  wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend(
9404
    /** @scope wysihtml5.Editor.prototype */ {
9405
    constructor: function(textareaElement, config) {
9406
      this.textareaElement  = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement;
9407
      this.config           = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();
9408
      this.textarea         = new wysihtml5.views.Textarea(this, this.textareaElement, this.config);
9409
      this.currentView      = this.textarea;
9410
      this._isCompatible    = wysihtml5.browser.supported();
9411
 
9412
      // Sort out unsupported/unwanted browsers here
9413
      if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) {
9414
        var that = this;
9415
        setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
9416
        return;
9417
      }
9418
 
9419
      // Add class name to body, to indicate that the editor is supported
9420
      wysihtml5.dom.addClass(document.body, this.config.bodyClassName);
9421
 
9422
      this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config);
9423
      this.currentView = this.composer;
9424
 
9425
      if (typeof(this.config.parser) === "function") {
9426
        this._initParser();
9427
      }
9428
 
9429
      this.observe("beforeload", function() {
9430
        this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer);
9431
        if (this.config.toolbar) {
9432
          this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar);
9433
        }
9434
      });
9435
 
9436
      try {
9437
        console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5");
9438
      } catch(e) {}
9439
    },
9440
 
9441
    isCompatible: function() {
9442
      return this._isCompatible;
9443
    },
9444
 
9445
    clear: function() {
9446
      this.currentView.clear();
9447
      return this;
9448
    },
9449
 
9450
    getValue: function(parse) {
9451
      return this.currentView.getValue(parse);
9452
    },
9453
 
9454
    setValue: function(html, parse) {
9455
      if (!html) {
9456
        return this.clear();
9457
      }
9458
      this.currentView.setValue(html, parse);
9459
      return this;
9460
    },
9461
 
9462
    focus: function(setToEnd) {
9463
      this.currentView.focus(setToEnd);
9464
      return this;
9465
    },
9466
 
9467
    /**
9468
     * Deactivate editor (make it readonly)
9469
     */
9470
    disable: function() {
9471
      this.currentView.disable();
9472
      return this;
9473
    },
9474
 
9475
    /**
9476
     * Activate editor
9477
     */
9478
    enable: function() {
9479
      this.currentView.enable();
9480
      return this;
9481
    },
9482
 
9483
    isEmpty: function() {
9484
      return this.currentView.isEmpty();
9485
    },
9486
 
9487
    hasPlaceholderSet: function() {
9488
      return this.currentView.hasPlaceholderSet();
9489
    },
9490
 
9491
    parse: function(htmlOrElement) {
9492
      var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true);
9493
      if (typeof(htmlOrElement) === "object") {
9494
        wysihtml5.quirks.redraw(htmlOrElement);
9495
      }
9496
      return returnValue;
9497
    },
9498
 
9499
    /**
9500
     * Prepare html parser logic
9501
     *  - Observes for paste and drop
9502
     */
9503
    _initParser: function() {
9504
      this.observe("paste:composer", function() {
9505
        var keepScrollPosition  = true,
9506
            that                = this;
9507
        that.composer.selection.executeAndRestore(function() {
9508
          wysihtml5.quirks.cleanPastedHTML(that.composer.element);
9509
          that.parse(that.composer.element);
9510
        }, keepScrollPosition);
9511
      });
9512
 
9513
      this.observe("paste:textarea", function() {
9514
        var value   = this.textarea.getValue(),
9515
            newValue;
9516
        newValue = this.parse(value);
9517
        this.textarea.setValue(newValue);
9518
      });
9519
    }
9520
  });
9521
})(wysihtml5);