Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
loeasy68
GitHub Repository: loeasy68/loeasy68.github.io
Path: blob/main/website/GAUSS/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.js
2946 views
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);
9522
9523