squire-rails 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 449579f33f3d1e116ded395a56b908ac97e7ae26
4
- data.tar.gz: fe723c3a9238e8858def1a4251176c433b1b2804
3
+ metadata.gz: 398337563890132e5830d98979f048918d241a65
4
+ data.tar.gz: 369080a22ada52156865c0761f168479f4a0ff49
5
5
  SHA512:
6
- metadata.gz: bcfda584a17453fcc6acb0d6ec3f33f4596a148697c3e73581c8212401587563cb26b784284ecff2177de1d62a46da9bcb9c6e079f84c6669c3ee4feffd09291
7
- data.tar.gz: f99036bc5f39b2a2d527ba4ea2312eb3f683bbba18be6c3c0b23971cf0146a0b82714a630a2e125d1668a6c441c9964146b0ba0db75ecf75636b97d52a29cfc3
6
+ metadata.gz: 8727b1dcdaa35bb14f3ba5e926254a9d36a55aefcf6932f622a40fde33687df45865669926d93ed3c75b8a45283430710213c65f848e96669fb98ffed3a6ddfb
7
+ data.tar.gz: 12ecb53d2bdeff9eb00fa9b818c9ab166de6702ae954f95e75a6e92c92080d2805cb415bf718b8f3b6567edbb3f05fc12f18c09712267d4cb5c75637e763e7bb
data/README.md ADDED
@@ -0,0 +1,149 @@
1
+ #Squire rails
2
+
3
+
4
+ ##Information
5
+
6
+
7
+ squire-rails gem is made on the basis of the squire.
8
+
9
+ squire-rails is possible following things.
10
+
11
+
12
+ + It calls the javascript of squire
13
+ + Generator of sample source code using the twitter-bootstrap and font-awesome
14
+
15
+
16
+ squire-rais is using such only javascript and css, it will also work in the any version of rails.
17
+
18
+ ##Getting start
19
+ Add to this in Gemfile
20
+
21
+ ```ruby
22
+ gem 'squire-rails'
23
+ ```
24
+
25
+ Run bundle install command
26
+
27
+ ```bash
28
+ bundle exec rake db:migrate
29
+ ```
30
+
31
+ If you want to use only the javascirpt of squire, open the `app/assets/javascripts/applications.js`, writes the following.
32
+
33
+ ```javascript
34
+ //= require squire/squire-raw
35
+ ```
36
+
37
+ How to use the squire, please look at the github of squire.
38
+
39
+ https://github.com/neilj/Squire
40
+
41
+
42
+ ##Basic usage of squire editor
43
+
44
+ Create scaffold
45
+
46
+ ```bash
47
+ rails g scaffold Post title:string description:text
48
+ ```
49
+ And run.
50
+
51
+ ```bash
52
+ bundle exec rake db:migrate
53
+ ```
54
+
55
+ Open the `app/views/posts/_form.html.erb`
56
+ Turn off the display of description.
57
+
58
+ ```ruby
59
+ <%= f.text_area :description, :style =>'display:none' %>
60
+ ```
61
+ Add the following in the form_for instead.
62
+
63
+ ```html
64
+ <div id="squire_action">
65
+ <p>
66
+ <span id="bold">Bold</span>
67
+ <span id="removeBold">Unbold</span>
68
+ <span id="italic">Italic</span>
69
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
70
+ <span id="removeItalic">Unitalic</span>
71
+ <span id="underline">Underline</span>
72
+ <span id="removeUnderline">Deunderline</span>
73
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
74
+ <span id="setFontSize" class="prompt">Font size</span>
75
+ <span id="setFontFace" class="prompt">Font face</span>
76
+ </p>
77
+ <p>
78
+ <span id="setTextColour" class="prompt">Text colour</span>
79
+ <span id="setHighlightColour" class="prompt">Text highlight</span>
80
+ <span id="makeLink" class="prompt">Link</span>
81
+ </p>
82
+ <p>
83
+ <span id="increaseQuoteLevel">Quote</span>
84
+ <span id="decreaseQuoteLevel">Dequote</span>
85
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
86
+ <span id="makeUnorderedList">List</span>
87
+ <span id="removeList">Unlist</span>
88
+ <span id="increaseListLevel">Increase list level</span>
89
+ <span id="decreaseListLevel">Decrease list level</span>
90
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
91
+ <span id="insertImage" class="prompt">Insert image</span>
92
+ <span id="setHTML" class="prompt">Set HTML</span>
93
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
94
+ <span id="undo">Undo</span>
95
+ <span id="redo">Redo</span>
96
+ </p>
97
+ <iframe id="seditor" width="500" height="300"></iframe>
98
+ </div>
99
+ ```
100
+ Open the `app/views/posts/edit.html.erb`, add the following.
101
+
102
+ ```ruby
103
+ <%= javascript_tag do %>
104
+ var $post_description = '<%= raw @post.description.gsub("'", "\\\\'") %>';
105
+ <% end %>
106
+
107
+ ```
108
+ Open the `app/assets/javascripts/posts.js.coffee`, add the following.
109
+
110
+ ```coffeescript
111
+ $(document).on 'ready page:load', ->
112
+ $editor_id = "seditor"
113
+
114
+ if document.getElementById($editor_id)
115
+
116
+ iframe = $('#'+$editor_id)
117
+ iframe[0].contentWindow.editor = new Squire(iframe[0].contentWindow.document)
118
+ editor = iframe[0].contentWindow.editor
119
+
120
+ document.addEventListener 'click', ((e) ->
121
+ id = e.target.id
122
+ value = undefined
123
+ if id and editor and editor[id]
124
+ if e.target.className == 'prompt'
125
+ value = prompt('Value:')
126
+ editor[id] value
127
+ ), false
128
+
129
+ if typeof $post_description != 'undefined'
130
+ editor.setHTML $squire_description
131
+
132
+ $('form').submit ->
133
+ $('#post_description').val(editor.getHTML()).change()
134
+ return
135
+
136
+ ```
137
+
138
+ Open the `app/assets/stylesheets/posts.css.scss` to add the following.
139
+
140
+ ```scss
141
+ #squire_action span {
142
+ cursor: pointer;
143
+ text-decoration: underline;
144
+ }
145
+
146
+ #squire_action p {
147
+ margin: 5px 0;
148
+ }
149
+ ```
@@ -1,3 +1,3 @@
1
1
  module SquireRails
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -0,0 +1,3466 @@
1
+ /* Copyright © 2011-2013 by Neil Jenkins. MIT Licensed. */
2
+
3
+ ( function ( doc, undefined ) {
4
+
5
+ "use strict";
6
+
7
+ var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING
8
+ var ELEMENT_NODE = 1; // Node.ELEMENT_NODE;
9
+ var TEXT_NODE = 3; // Node.TEXT_NODE;
10
+ var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT;
11
+ var SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT;
12
+
13
+ var START_TO_START = 0; // Range.START_TO_START
14
+ var START_TO_END = 1; // Range.START_TO_END
15
+ var END_TO_END = 2; // Range.END_TO_END
16
+ var END_TO_START = 3; // Range.END_TO_START
17
+
18
+ var ZWS = '\u200B';
19
+
20
+ var win = doc.defaultView;
21
+
22
+ var ua = navigator.userAgent;
23
+
24
+ var isIOS = /iP(?:ad|hone|od)/.test( ua );
25
+ var isMac = /Mac OS X/.test( ua );
26
+
27
+ var isGecko = /Gecko\//.test( ua );
28
+ var isIElt11 = /Trident\/[456]\./.test( ua );
29
+ var isPresto = !!win.opera;
30
+ var isWebKit = /WebKit\//.test( ua );
31
+
32
+ var ctrlKey = isMac ? 'meta-' : 'ctrl-';
33
+
34
+ var useTextFixer = isIElt11 || isPresto;
35
+ var cantFocusEmptyTextNodes = isIElt11 || isWebKit;
36
+ var losesSelectionOnBlur = isIElt11;
37
+
38
+ var canObserveMutations = typeof MutationObserver !== 'undefined';
39
+
40
+ // Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space
41
+ var notWS = /[^ \t\r\n]/;
42
+
43
+ var indexOf = Array.prototype.indexOf;
44
+
45
+ /*
46
+ Native TreeWalker is buggy in IE and Opera:
47
+ * IE9/10 sometimes throw errors when calling TreeWalker#nextNode or
48
+ TreeWalker#previousNode. No way to feature detect this.
49
+ * Some versions of Opera have a bug in TreeWalker#previousNode which makes
50
+ it skip to the wrong node.
51
+
52
+ Rather than risk further bugs, it's easiest just to implement our own
53
+ (subset) of the spec in all browsers.
54
+ */
55
+
56
+ var typeToBitArray = {
57
+ // ELEMENT_NODE
58
+ 1: 1,
59
+ // ATTRIBUTE_NODE
60
+ 2: 2,
61
+ // TEXT_NODE
62
+ 3: 4,
63
+ // COMMENT_NODE
64
+ 8: 128,
65
+ // DOCUMENT_NODE
66
+ 9: 256,
67
+ // DOCUMENT_FRAGMENT_NODE
68
+ 11: 1024
69
+ };
70
+
71
+ function TreeWalker ( root, nodeType, filter ) {
72
+ this.root = this.currentNode = root;
73
+ this.nodeType = nodeType;
74
+ this.filter = filter;
75
+ }
76
+
77
+ TreeWalker.prototype.nextNode = function () {
78
+ var current = this.currentNode,
79
+ root = this.root,
80
+ nodeType = this.nodeType,
81
+ filter = this.filter,
82
+ node;
83
+ while ( true ) {
84
+ node = current.firstChild;
85
+ while ( !node && current ) {
86
+ if ( current === root ) {
87
+ break;
88
+ }
89
+ node = current.nextSibling;
90
+ if ( !node ) { current = current.parentNode; }
91
+ }
92
+ if ( !node ) {
93
+ return null;
94
+ }
95
+ if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
96
+ filter( node ) ) {
97
+ this.currentNode = node;
98
+ return node;
99
+ }
100
+ current = node;
101
+ }
102
+ };
103
+
104
+ TreeWalker.prototype.previousNode = function () {
105
+ var current = this.currentNode,
106
+ root = this.root,
107
+ nodeType = this.nodeType,
108
+ filter = this.filter,
109
+ node;
110
+ while ( true ) {
111
+ if ( current === root ) {
112
+ return null;
113
+ }
114
+ node = current.previousSibling;
115
+ if ( node ) {
116
+ while ( current = node.lastChild ) {
117
+ node = current;
118
+ }
119
+ } else {
120
+ node = current.parentNode;
121
+ }
122
+ if ( !node ) {
123
+ return null;
124
+ }
125
+ if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
126
+ filter( node ) ) {
127
+ this.currentNode = node;
128
+ return node;
129
+ }
130
+ current = node;
131
+ }
132
+ };
133
+
134
+ var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TR(?:IKE|ONG)|MALL|AMP)?|U|VAR|WBR)$/;
135
+
136
+ var leafNodeNames = {
137
+ BR: 1,
138
+ IMG: 1,
139
+ INPUT: 1
140
+ };
141
+
142
+ function every ( nodeList, fn ) {
143
+ var l = nodeList.length;
144
+ while ( l-- ) {
145
+ if ( !fn( nodeList[l] ) ) {
146
+ return false;
147
+ }
148
+ }
149
+ return true;
150
+ }
151
+
152
+ // ---
153
+
154
+ function hasTagAttributes ( node, tag, attributes ) {
155
+ if ( node.nodeName !== tag ) {
156
+ return false;
157
+ }
158
+ for ( var attr in attributes ) {
159
+ if ( node.getAttribute( attr ) !== attributes[ attr ] ) {
160
+ return false;
161
+ }
162
+ }
163
+ return true;
164
+ }
165
+ function areAlike ( node, node2 ) {
166
+ return (
167
+ node.nodeType === node2.nodeType &&
168
+ node.nodeName === node2.nodeName &&
169
+ node.className === node2.className &&
170
+ ( ( !node.style && !node2.style ) ||
171
+ node.style.cssText === node2.style.cssText )
172
+ );
173
+ }
174
+
175
+ function isLeaf ( node ) {
176
+ return node.nodeType === ELEMENT_NODE &&
177
+ !!leafNodeNames[ node.nodeName ];
178
+ }
179
+ function isInline ( node ) {
180
+ return inlineNodeNames.test( node.nodeName );
181
+ }
182
+ function isBlock ( node ) {
183
+ return node.nodeType === ELEMENT_NODE &&
184
+ !isInline( node ) && every( node.childNodes, isInline );
185
+ }
186
+ function isContainer ( node ) {
187
+ return node.nodeType === ELEMENT_NODE &&
188
+ !isInline( node ) && !isBlock( node );
189
+ }
190
+
191
+ function getBlockWalker ( node ) {
192
+ var doc = node.ownerDocument,
193
+ walker = new TreeWalker(
194
+ doc.body, SHOW_ELEMENT, isBlock, false );
195
+ walker.currentNode = node;
196
+ return walker;
197
+ }
198
+
199
+ function getPreviousBlock ( node ) {
200
+ return getBlockWalker( node ).previousNode();
201
+ }
202
+ function getNextBlock ( node ) {
203
+ return getBlockWalker( node ).nextNode();
204
+ }
205
+ function getNearest ( node, tag, attributes ) {
206
+ do {
207
+ if ( hasTagAttributes( node, tag, attributes ) ) {
208
+ return node;
209
+ }
210
+ } while ( node = node.parentNode );
211
+ return null;
212
+ }
213
+
214
+ function getPath ( node ) {
215
+ var parent = node.parentNode,
216
+ path, id, className, classNames;
217
+ if ( !parent || node.nodeType !== ELEMENT_NODE ) {
218
+ path = parent ? getPath( parent ) : '';
219
+ } else {
220
+ path = getPath( parent );
221
+ path += ( path ? '>' : '' ) + node.nodeName;
222
+ if ( id = node.id ) {
223
+ path += '#' + id;
224
+ }
225
+ if ( className = node.className.trim() ) {
226
+ classNames = className.split( /\s\s*/ );
227
+ classNames.sort();
228
+ path += '.';
229
+ path += classNames.join( '.' );
230
+ }
231
+ }
232
+ return path;
233
+ }
234
+
235
+ function getLength ( node ) {
236
+ var nodeType = node.nodeType;
237
+ return nodeType === ELEMENT_NODE ?
238
+ node.childNodes.length : node.length || 0;
239
+ }
240
+
241
+ function detach ( node ) {
242
+ var parent = node.parentNode;
243
+ if ( parent ) {
244
+ parent.removeChild( node );
245
+ }
246
+ return node;
247
+ }
248
+ function replaceWith ( node, node2 ) {
249
+ var parent = node.parentNode;
250
+ if ( parent ) {
251
+ parent.replaceChild( node2, node );
252
+ }
253
+ }
254
+ function empty ( node ) {
255
+ var frag = node.ownerDocument.createDocumentFragment(),
256
+ childNodes = node.childNodes,
257
+ l = childNodes ? childNodes.length : 0;
258
+ while ( l-- ) {
259
+ frag.appendChild( node.firstChild );
260
+ }
261
+ return frag;
262
+ }
263
+
264
+ function createElement ( doc, tag, props, children ) {
265
+ var el = doc.createElement( tag ),
266
+ attr, value, i, l;
267
+ if ( props instanceof Array ) {
268
+ children = props;
269
+ props = null;
270
+ }
271
+ if ( props ) {
272
+ for ( attr in props ) {
273
+ value = props[ attr ];
274
+ if ( value !== undefined ) {
275
+ el.setAttribute( attr, props[ attr ] );
276
+ }
277
+ }
278
+ }
279
+ if ( children ) {
280
+ for ( i = 0, l = children.length; i < l; i += 1 ) {
281
+ el.appendChild( children[i] );
282
+ }
283
+ }
284
+ return el;
285
+ }
286
+
287
+ function fixCursor ( node ) {
288
+ // In Webkit and Gecko, block level elements are collapsed and
289
+ // unfocussable if they have no content. To remedy this, a <BR> must be
290
+ // inserted. In Opera and IE, we just need a textnode in order for the
291
+ // cursor to appear.
292
+ var doc = node.ownerDocument,
293
+ root = node,
294
+ fixer, child,
295
+ l, instance;
296
+
297
+ if ( node.nodeName === 'BODY' ) {
298
+ if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
299
+ fixer = doc.createElement( 'DIV' );
300
+ if ( child ) {
301
+ node.replaceChild( fixer, child );
302
+ }
303
+ else {
304
+ node.appendChild( fixer );
305
+ }
306
+ node = fixer;
307
+ fixer = null;
308
+ }
309
+ }
310
+
311
+ if ( isInline( node ) ) {
312
+ child = node.firstChild;
313
+ while ( cantFocusEmptyTextNodes && child &&
314
+ child.nodeType === TEXT_NODE && !child.data ) {
315
+ node.removeChild( child );
316
+ child = node.firstChild;
317
+ }
318
+ if ( !child ) {
319
+ if ( cantFocusEmptyTextNodes ) {
320
+ fixer = doc.createTextNode( ZWS );
321
+ // Find the relevant Squire instance and notify
322
+ l = instances.length;
323
+ while ( l-- ) {
324
+ instance = instances[l];
325
+ if ( instance._doc === doc ) {
326
+ instance._didAddZWS();
327
+ }
328
+ }
329
+ } else {
330
+ fixer = doc.createTextNode( '' );
331
+ }
332
+ }
333
+ } else {
334
+ if ( useTextFixer ) {
335
+ while ( node.nodeType !== TEXT_NODE && !isLeaf( node ) ) {
336
+ child = node.firstChild;
337
+ if ( !child ) {
338
+ fixer = doc.createTextNode( '' );
339
+ break;
340
+ }
341
+ node = child;
342
+ }
343
+ if ( node.nodeType === TEXT_NODE ) {
344
+ // Opera will collapse the block element if it contains
345
+ // just spaces (but not if it contains no data at all).
346
+ if ( /^ +$/.test( node.data ) ) {
347
+ node.data = '';
348
+ }
349
+ } else if ( isLeaf( node ) ) {
350
+ node.parentNode.insertBefore( doc.createTextNode( '' ), node );
351
+ }
352
+ }
353
+ else if ( !node.querySelector( 'BR' ) ) {
354
+ fixer = doc.createElement( 'BR' );
355
+ while ( ( child = node.lastElementChild ) && !isInline( child ) ) {
356
+ node = child;
357
+ }
358
+ }
359
+ }
360
+ if ( fixer ) {
361
+ node.appendChild( fixer );
362
+ }
363
+
364
+ return root;
365
+ }
366
+
367
+ // Recursively examine container nodes and wrap any inline children.
368
+ function fixContainer ( container ) {
369
+ var children = container.childNodes,
370
+ doc = container.ownerDocument,
371
+ wrapper = null,
372
+ i, l, child, isBR;
373
+ for ( i = 0, l = children.length; i < l; i += 1 ) {
374
+ child = children[i];
375
+ isBR = child.nodeName === 'BR';
376
+ if ( !isBR && isInline( child ) ) {
377
+ if ( !wrapper ) { wrapper = createElement( doc, 'DIV' ); }
378
+ wrapper.appendChild( child );
379
+ i -= 1;
380
+ l -= 1;
381
+ } else if ( isBR || wrapper ) {
382
+ if ( !wrapper ) { wrapper = createElement( doc, 'DIV' ); }
383
+ fixCursor( wrapper );
384
+ if ( isBR ) {
385
+ container.replaceChild( wrapper, child );
386
+ } else {
387
+ container.insertBefore( wrapper, child );
388
+ i += 1;
389
+ l += 1;
390
+ }
391
+ wrapper = null;
392
+ }
393
+ if ( isContainer( child ) ) {
394
+ fixContainer( child );
395
+ }
396
+ }
397
+ if ( wrapper ) {
398
+ container.appendChild( fixCursor( wrapper ) );
399
+ }
400
+ return container;
401
+ }
402
+
403
+ function split ( node, offset, stopNode ) {
404
+ var nodeType = node.nodeType,
405
+ parent, clone, next;
406
+ if ( nodeType === TEXT_NODE && node !== stopNode ) {
407
+ return split( node.parentNode, node.splitText( offset ), stopNode );
408
+ }
409
+ if ( nodeType === ELEMENT_NODE ) {
410
+ if ( typeof( offset ) === 'number' ) {
411
+ offset = offset < node.childNodes.length ?
412
+ node.childNodes[ offset ] : null;
413
+ }
414
+ if ( node === stopNode ) {
415
+ return offset;
416
+ }
417
+
418
+ // Clone node without children
419
+ parent = node.parentNode;
420
+ clone = node.cloneNode( false );
421
+
422
+ // Add right-hand siblings to the clone
423
+ while ( offset ) {
424
+ next = offset.nextSibling;
425
+ clone.appendChild( offset );
426
+ offset = next;
427
+ }
428
+
429
+ // Maintain li numbering
430
+ if ( node.nodeName === 'OL' ) {
431
+ clone.start = ( +node.start || 1 ) + node.childNodes.length - 1;
432
+ }
433
+
434
+ // DO NOT NORMALISE. This may undo the fixCursor() call
435
+ // of a node lower down the tree!
436
+
437
+ // We need something in the element in order for the cursor to appear.
438
+ fixCursor( node );
439
+ fixCursor( clone );
440
+
441
+ // Inject clone after original node
442
+ if ( next = node.nextSibling ) {
443
+ parent.insertBefore( clone, next );
444
+ } else {
445
+ parent.appendChild( clone );
446
+ }
447
+
448
+ // Keep on splitting up the tree
449
+ return split( parent, clone, stopNode );
450
+ }
451
+ return offset;
452
+ }
453
+
454
+ function mergeInlines ( node, range ) {
455
+ if ( node.nodeType !== ELEMENT_NODE ) {
456
+ return;
457
+ }
458
+ var children = node.childNodes,
459
+ l = children.length,
460
+ frags = [],
461
+ child, prev, len;
462
+ while ( l-- ) {
463
+ child = children[l];
464
+ prev = l && children[ l - 1 ];
465
+ if ( l && isInline( child ) && areAlike( child, prev ) &&
466
+ !leafNodeNames[ child.nodeName ] ) {
467
+ if ( range.startContainer === child ) {
468
+ range.startContainer = prev;
469
+ range.startOffset += getLength( prev );
470
+ }
471
+ if ( range.endContainer === child ) {
472
+ range.endContainer = prev;
473
+ range.endOffset += getLength( prev );
474
+ }
475
+ if ( range.startContainer === node ) {
476
+ if ( range.startOffset > l ) {
477
+ range.startOffset -= 1;
478
+ }
479
+ else if ( range.startOffset === l ) {
480
+ range.startContainer = prev;
481
+ range.startOffset = getLength( prev );
482
+ }
483
+ }
484
+ if ( range.endContainer === node ) {
485
+ if ( range.endOffset > l ) {
486
+ range.endOffset -= 1;
487
+ }
488
+ else if ( range.endOffset === l ) {
489
+ range.endContainer = prev;
490
+ range.endOffset = getLength( prev );
491
+ }
492
+ }
493
+ detach( child );
494
+ if ( child.nodeType === TEXT_NODE ) {
495
+ prev.appendData( child.data );
496
+ }
497
+ else {
498
+ frags.push( empty( child ) );
499
+ }
500
+ }
501
+ else if ( child.nodeType === ELEMENT_NODE ) {
502
+ len = frags.length;
503
+ while ( len-- ) {
504
+ child.appendChild( frags.pop() );
505
+ }
506
+ mergeInlines( child, range );
507
+ }
508
+ }
509
+ }
510
+
511
+ function mergeWithBlock ( block, next, range ) {
512
+ var container = next,
513
+ last, offset, _range;
514
+ while ( container.parentNode.childNodes.length === 1 ) {
515
+ container = container.parentNode;
516
+ }
517
+ detach( container );
518
+
519
+ offset = block.childNodes.length;
520
+
521
+ // Remove extra <BR> fixer if present.
522
+ last = block.lastChild;
523
+ if ( last && last.nodeName === 'BR' ) {
524
+ block.removeChild( last );
525
+ offset -= 1;
526
+ }
527
+
528
+ _range = {
529
+ startContainer: block,
530
+ startOffset: offset,
531
+ endContainer: block,
532
+ endOffset: offset
533
+ };
534
+
535
+ block.appendChild( empty( next ) );
536
+ mergeInlines( block, _range );
537
+
538
+ range.setStart( _range.startContainer, _range.startOffset );
539
+ range.collapse( true );
540
+
541
+ // Opera inserts a BR if you delete the last piece of text
542
+ // in a block-level element. Unfortunately, it then gets
543
+ // confused when setting the selection subsequently and
544
+ // refuses to accept the range that finishes just before the
545
+ // BR. Removing the BR fixes the bug.
546
+ // Steps to reproduce bug: Type "a-b-c" (where - is return)
547
+ // then backspace twice. The cursor goes to the top instead
548
+ // of after "b".
549
+ if ( isPresto && ( last = block.lastChild ) && last.nodeName === 'BR' ) {
550
+ block.removeChild( last );
551
+ }
552
+ }
553
+
554
+ function mergeContainers ( node ) {
555
+ var prev = node.previousSibling,
556
+ first = node.firstChild,
557
+ doc = node.ownerDocument,
558
+ isListItem = ( node.nodeName === 'LI' ),
559
+ needsFix, block;
560
+
561
+ // Do not merge LIs, unless it only contains a UL
562
+ if ( isListItem && ( !first || !/^[OU]L$/.test( first.nodeName ) ) ) {
563
+ return;
564
+ }
565
+
566
+ if ( prev && areAlike( prev, node ) ) {
567
+ if ( !isContainer( prev ) ) {
568
+ if ( isListItem ) {
569
+ block = doc.createElement( 'DIV' );
570
+ block.appendChild( empty( prev ) );
571
+ prev.appendChild( block );
572
+ } else {
573
+ return;
574
+ }
575
+ }
576
+ detach( node );
577
+ needsFix = !isContainer( node );
578
+ prev.appendChild( empty( node ) );
579
+ if ( needsFix ) {
580
+ fixContainer( prev );
581
+ }
582
+ if ( first ) {
583
+ mergeContainers( first );
584
+ }
585
+ } else if ( isListItem ) {
586
+ prev = doc.createElement( 'DIV' );
587
+ node.insertBefore( prev, first );
588
+ fixCursor( prev );
589
+ }
590
+ }
591
+
592
+ var getNodeBefore = function ( node, offset ) {
593
+ var children = node.childNodes;
594
+ while ( offset && node.nodeType === ELEMENT_NODE ) {
595
+ node = children[ offset - 1 ];
596
+ children = node.childNodes;
597
+ offset = children.length;
598
+ }
599
+ return node;
600
+ };
601
+
602
+ var getNodeAfter = function ( node, offset ) {
603
+ if ( node.nodeType === ELEMENT_NODE ) {
604
+ var children = node.childNodes;
605
+ if ( offset < children.length ) {
606
+ node = children[ offset ];
607
+ } else {
608
+ while ( node && !node.nextSibling ) {
609
+ node = node.parentNode;
610
+ }
611
+ if ( node ) { node = node.nextSibling; }
612
+ }
613
+ }
614
+ return node;
615
+ };
616
+
617
+ // ---
618
+
619
+ var forEachTextNodeInRange = function ( range, fn ) {
620
+ range = range.cloneRange();
621
+ moveRangeBoundariesDownTree( range );
622
+
623
+ var startContainer = range.startContainer,
624
+ endContainer = range.endContainer,
625
+ root = range.commonAncestorContainer,
626
+ walker = new TreeWalker(
627
+ root, SHOW_TEXT, function (/* node */) {
628
+ return true;
629
+ }, false ),
630
+ textnode = walker.currentNode = startContainer;
631
+
632
+ while ( !fn( textnode, range ) &&
633
+ textnode !== endContainer &&
634
+ ( textnode = walker.nextNode() ) ) {}
635
+ };
636
+
637
+ var getTextContentInRange = function ( range ) {
638
+ var textContent = '';
639
+ forEachTextNodeInRange( range, function ( textnode, range ) {
640
+ var value = textnode.data;
641
+ if ( value && ( /\S/.test( value ) ) ) {
642
+ if ( textnode === range.endContainer ) {
643
+ value = value.slice( 0, range.endOffset );
644
+ }
645
+ if ( textnode === range.startContainer ) {
646
+ value = value.slice( range.startOffset );
647
+ }
648
+ textContent += value;
649
+ }
650
+ });
651
+ return textContent;
652
+ };
653
+
654
+ // ---
655
+
656
+ var insertNodeInRange = function ( range, node ) {
657
+ // Insert at start.
658
+ var startContainer = range.startContainer,
659
+ startOffset = range.startOffset,
660
+ endContainer = range.endContainer,
661
+ endOffset = range.endOffset,
662
+ parent, children, childCount, afterSplit;
663
+
664
+ // If part way through a text node, split it.
665
+ if ( startContainer.nodeType === TEXT_NODE ) {
666
+ parent = startContainer.parentNode;
667
+ children = parent.childNodes;
668
+ if ( startOffset === startContainer.length ) {
669
+ startOffset = indexOf.call( children, startContainer ) + 1;
670
+ if ( range.collapsed ) {
671
+ endContainer = parent;
672
+ endOffset = startOffset;
673
+ }
674
+ } else {
675
+ if ( startOffset ) {
676
+ afterSplit = startContainer.splitText( startOffset );
677
+ if ( endContainer === startContainer ) {
678
+ endOffset -= startOffset;
679
+ endContainer = afterSplit;
680
+ }
681
+ else if ( endContainer === parent ) {
682
+ endOffset += 1;
683
+ }
684
+ startContainer = afterSplit;
685
+ }
686
+ startOffset = indexOf.call( children, startContainer );
687
+ }
688
+ startContainer = parent;
689
+ } else {
690
+ children = startContainer.childNodes;
691
+ }
692
+
693
+ childCount = children.length;
694
+
695
+ if ( startOffset === childCount) {
696
+ startContainer.appendChild( node );
697
+ } else {
698
+ startContainer.insertBefore( node, children[ startOffset ] );
699
+ }
700
+
701
+ if ( startContainer === endContainer ) {
702
+ endOffset += children.length - childCount;
703
+ }
704
+
705
+ range.setStart( startContainer, startOffset );
706
+ range.setEnd( endContainer, endOffset );
707
+ };
708
+
709
+ var extractContentsOfRange = function ( range, common ) {
710
+ var startContainer = range.startContainer,
711
+ startOffset = range.startOffset,
712
+ endContainer = range.endContainer,
713
+ endOffset = range.endOffset;
714
+
715
+ if ( !common ) {
716
+ common = range.commonAncestorContainer;
717
+ }
718
+
719
+ if ( common.nodeType === TEXT_NODE ) {
720
+ common = common.parentNode;
721
+ }
722
+
723
+ var endNode = split( endContainer, endOffset, common ),
724
+ startNode = split( startContainer, startOffset, common ),
725
+ frag = common.ownerDocument.createDocumentFragment(),
726
+ next, before, after;
727
+
728
+ // End node will be null if at end of child nodes list.
729
+ while ( startNode !== endNode ) {
730
+ next = startNode.nextSibling;
731
+ frag.appendChild( startNode );
732
+ startNode = next;
733
+ }
734
+
735
+ startContainer = common;
736
+ startOffset = endNode ?
737
+ indexOf.call( common.childNodes, endNode ) :
738
+ common.childNodes.length;
739
+
740
+ // Merge text nodes if adjacent. IE10 in particular will not focus
741
+ // between two text nodes
742
+ after = common.childNodes[ startOffset ];
743
+ before = after && after.previousSibling;
744
+ if ( before &&
745
+ before.nodeType === TEXT_NODE &&
746
+ after.nodeType === TEXT_NODE ) {
747
+ startContainer = before;
748
+ startOffset = before.length;
749
+ before.appendData( after.data );
750
+ detach( after );
751
+ }
752
+
753
+ range.setStart( startContainer, startOffset );
754
+ range.collapse( true );
755
+
756
+ fixCursor( common );
757
+
758
+ return frag;
759
+ };
760
+
761
+ var deleteContentsOfRange = function ( range ) {
762
+ // Move boundaries up as much as possible to reduce need to split.
763
+ moveRangeBoundariesUpTree( range );
764
+
765
+ // Remove selected range
766
+ extractContentsOfRange( range );
767
+
768
+ // Move boundaries back down tree so that they are inside the blocks.
769
+ // If we don't do this, the range may be collapsed to a point between
770
+ // two blocks, so get(Start|End)BlockOfRange will return null.
771
+ moveRangeBoundariesDownTree( range );
772
+
773
+ // If we split into two different blocks, merge the blocks.
774
+ var startBlock = getStartBlockOfRange( range ),
775
+ endBlock = getEndBlockOfRange( range );
776
+ if ( startBlock && endBlock && startBlock !== endBlock ) {
777
+ mergeWithBlock( startBlock, endBlock, range );
778
+ }
779
+
780
+ // Ensure block has necessary children
781
+ if ( startBlock ) {
782
+ fixCursor( startBlock );
783
+ }
784
+
785
+ // Ensure body has a block-level element in it.
786
+ var body = range.endContainer.ownerDocument.body,
787
+ child = body.firstChild;
788
+ if ( !child || child.nodeName === 'BR' ) {
789
+ fixCursor( body );
790
+ range.selectNodeContents( body.firstChild );
791
+ }
792
+ };
793
+
794
+ // ---
795
+
796
+ var insertTreeFragmentIntoRange = function ( range, frag ) {
797
+ // Check if it's all inline content
798
+ var allInline = true,
799
+ children = frag.childNodes,
800
+ l = children.length;
801
+ while ( l-- ) {
802
+ if ( !isInline( children[l] ) ) {
803
+ allInline = false;
804
+ break;
805
+ }
806
+ }
807
+
808
+ // Delete any selected content
809
+ if ( !range.collapsed ) {
810
+ deleteContentsOfRange( range );
811
+ }
812
+
813
+ // Move range down into text ndoes
814
+ moveRangeBoundariesDownTree( range );
815
+
816
+ // If inline, just insert at the current position.
817
+ if ( allInline ) {
818
+ insertNodeInRange( range, frag );
819
+ range.collapse( false );
820
+ }
821
+ // Otherwise, split up to body, insert inline before and after split
822
+ // and insert block in between split, then merge containers.
823
+ else {
824
+ var nodeAfterSplit = split( range.startContainer, range.startOffset,
825
+ range.startContainer.ownerDocument.body ),
826
+ nodeBeforeSplit = nodeAfterSplit.previousSibling,
827
+ startContainer = nodeBeforeSplit,
828
+ startOffset = startContainer.childNodes.length,
829
+ endContainer = nodeAfterSplit,
830
+ endOffset = 0,
831
+ parent = nodeAfterSplit.parentNode,
832
+ child, node;
833
+
834
+ while ( ( child = startContainer.lastChild ) &&
835
+ child.nodeType === ELEMENT_NODE &&
836
+ child.nodeName !== 'BR' ) {
837
+ startContainer = child;
838
+ startOffset = startContainer.childNodes.length;
839
+ }
840
+ while ( ( child = endContainer.firstChild ) &&
841
+ child.nodeType === ELEMENT_NODE &&
842
+ child.nodeName !== 'BR' ) {
843
+ endContainer = child;
844
+ }
845
+ while ( ( child = frag.firstChild ) && isInline( child ) ) {
846
+ startContainer.appendChild( child );
847
+ }
848
+ while ( ( child = frag.lastChild ) && isInline( child ) ) {
849
+ endContainer.insertBefore( child, endContainer.firstChild );
850
+ endOffset += 1;
851
+ }
852
+
853
+ // Fix cursor then insert block(s)
854
+ node = frag;
855
+ while ( node = getNextBlock( node ) ) {
856
+ fixCursor( node );
857
+ }
858
+ parent.insertBefore( frag, nodeAfterSplit );
859
+
860
+ // Remove empty nodes created by split and merge inserted containers
861
+ // with edges of split
862
+ node = nodeAfterSplit.previousSibling;
863
+ if ( !nodeAfterSplit.textContent ) {
864
+ parent.removeChild( nodeAfterSplit );
865
+ } else {
866
+ mergeContainers( nodeAfterSplit );
867
+ }
868
+ if ( !nodeAfterSplit.parentNode ) {
869
+ endContainer = node;
870
+ endOffset = getLength( endContainer );
871
+ }
872
+
873
+ if ( !nodeBeforeSplit.textContent) {
874
+ startContainer = nodeBeforeSplit.nextSibling;
875
+ startOffset = 0;
876
+ parent.removeChild( nodeBeforeSplit );
877
+ } else {
878
+ mergeContainers( nodeBeforeSplit );
879
+ }
880
+
881
+ range.setStart( startContainer, startOffset );
882
+ range.setEnd( endContainer, endOffset );
883
+ moveRangeBoundariesDownTree( range );
884
+ }
885
+ };
886
+
887
+ // ---
888
+
889
+ var isNodeContainedInRange = function ( range, node, partial ) {
890
+ var nodeRange = node.ownerDocument.createRange();
891
+
892
+ nodeRange.selectNode( node );
893
+
894
+ if ( partial ) {
895
+ // Node must not finish before range starts or start after range
896
+ // finishes.
897
+ var nodeEndBeforeStart = ( range.compareBoundaryPoints(
898
+ END_TO_START, nodeRange ) > -1 ),
899
+ nodeStartAfterEnd = ( range.compareBoundaryPoints(
900
+ START_TO_END, nodeRange ) < 1 );
901
+ return ( !nodeEndBeforeStart && !nodeStartAfterEnd );
902
+ }
903
+ else {
904
+ // Node must start after range starts and finish before range
905
+ // finishes
906
+ var nodeStartAfterStart = ( range.compareBoundaryPoints(
907
+ START_TO_START, nodeRange ) < 1 ),
908
+ nodeEndBeforeEnd = ( range.compareBoundaryPoints(
909
+ END_TO_END, nodeRange ) > -1 );
910
+ return ( nodeStartAfterStart && nodeEndBeforeEnd );
911
+ }
912
+ };
913
+
914
+ var moveRangeBoundariesDownTree = function ( range ) {
915
+ var startContainer = range.startContainer,
916
+ startOffset = range.startOffset,
917
+ endContainer = range.endContainer,
918
+ endOffset = range.endOffset,
919
+ child;
920
+
921
+ while ( startContainer.nodeType !== TEXT_NODE ) {
922
+ child = startContainer.childNodes[ startOffset ];
923
+ if ( !child || isLeaf( child ) ) {
924
+ break;
925
+ }
926
+ startContainer = child;
927
+ startOffset = 0;
928
+ }
929
+ if ( endOffset ) {
930
+ while ( endContainer.nodeType !== TEXT_NODE ) {
931
+ child = endContainer.childNodes[ endOffset - 1 ];
932
+ if ( !child || isLeaf( child ) ) {
933
+ break;
934
+ }
935
+ endContainer = child;
936
+ endOffset = getLength( endContainer );
937
+ }
938
+ } else {
939
+ while ( endContainer.nodeType !== TEXT_NODE ) {
940
+ child = endContainer.firstChild;
941
+ if ( !child || isLeaf( child ) ) {
942
+ break;
943
+ }
944
+ endContainer = child;
945
+ }
946
+ }
947
+
948
+ // If collapsed, this algorithm finds the nearest text node positions
949
+ // *outside* the range rather than inside, but also it flips which is
950
+ // assigned to which.
951
+ if ( range.collapsed ) {
952
+ range.setStart( endContainer, endOffset );
953
+ range.setEnd( startContainer, startOffset );
954
+ } else {
955
+ range.setStart( startContainer, startOffset );
956
+ range.setEnd( endContainer, endOffset );
957
+ }
958
+ };
959
+
960
+ var moveRangeBoundariesUpTree = function ( range, common ) {
961
+ var startContainer = range.startContainer,
962
+ startOffset = range.startOffset,
963
+ endContainer = range.endContainer,
964
+ endOffset = range.endOffset,
965
+ parent;
966
+
967
+ if ( !common ) {
968
+ common = range.commonAncestorContainer;
969
+ }
970
+
971
+ while ( startContainer !== common && !startOffset ) {
972
+ parent = startContainer.parentNode;
973
+ startOffset = indexOf.call( parent.childNodes, startContainer );
974
+ startContainer = parent;
975
+ }
976
+
977
+ while ( endContainer !== common &&
978
+ endOffset === getLength( endContainer ) ) {
979
+ parent = endContainer.parentNode;
980
+ endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
981
+ endContainer = parent;
982
+ }
983
+
984
+ range.setStart( startContainer, startOffset );
985
+ range.setEnd( endContainer, endOffset );
986
+ };
987
+
988
+ // Returns the first block at least partially contained by the range,
989
+ // or null if no block is contained by the range.
990
+ var getStartBlockOfRange = function ( range ) {
991
+ var container = range.startContainer,
992
+ block;
993
+
994
+ // If inline, get the containing block.
995
+ if ( isInline( container ) ) {
996
+ block = getPreviousBlock( container );
997
+ } else if ( isBlock( container ) ) {
998
+ block = container;
999
+ } else {
1000
+ block = getNodeBefore( container, range.startOffset );
1001
+ block = getNextBlock( block );
1002
+ }
1003
+ // Check the block actually intersects the range
1004
+ return block && isNodeContainedInRange( range, block, true ) ? block : null;
1005
+ };
1006
+
1007
+ // Returns the last block at least partially contained by the range,
1008
+ // or null if no block is contained by the range.
1009
+ var getEndBlockOfRange = function ( range ) {
1010
+ var container = range.endContainer,
1011
+ block, child;
1012
+
1013
+ // If inline, get the containing block.
1014
+ if ( isInline( container ) ) {
1015
+ block = getPreviousBlock( container );
1016
+ } else if ( isBlock( container ) ) {
1017
+ block = container;
1018
+ } else {
1019
+ block = getNodeAfter( container, range.endOffset );
1020
+ if ( !block ) {
1021
+ block = container.ownerDocument.body;
1022
+ while ( child = block.lastChild ) {
1023
+ block = child;
1024
+ }
1025
+ }
1026
+ block = getPreviousBlock( block );
1027
+
1028
+ }
1029
+ // Check the block actually intersects the range
1030
+ return block && isNodeContainedInRange( range, block, true ) ? block : null;
1031
+ };
1032
+
1033
+ var contentWalker = new TreeWalker( null,
1034
+ SHOW_TEXT|SHOW_ELEMENT,
1035
+ function ( node ) {
1036
+ return node.nodeType === TEXT_NODE ?
1037
+ notWS.test( node.data ) :
1038
+ node.nodeName === 'IMG';
1039
+ }
1040
+ );
1041
+
1042
+ var rangeDoesStartAtBlockBoundary = function ( range ) {
1043
+ var startContainer = range.startContainer,
1044
+ startOffset = range.startOffset;
1045
+
1046
+ // If in the middle or end of a text node, we're not at the boundary.
1047
+ if ( startContainer.nodeType === TEXT_NODE ) {
1048
+ if ( startOffset ) {
1049
+ return false;
1050
+ }
1051
+ contentWalker.currentNode = startContainer;
1052
+ } else {
1053
+ contentWalker.currentNode = getNodeAfter( startContainer, startOffset );
1054
+ }
1055
+
1056
+ // Otherwise, look for any previous content in the same block.
1057
+ contentWalker.root = getStartBlockOfRange( range );
1058
+
1059
+ return !contentWalker.previousNode();
1060
+ };
1061
+
1062
+ var rangeDoesEndAtBlockBoundary = function ( range ) {
1063
+ var endContainer = range.endContainer,
1064
+ endOffset = range.endOffset,
1065
+ length;
1066
+
1067
+ // If in a text node with content, and not at the end, we're not
1068
+ // at the boundary
1069
+ if ( endContainer.nodeType === TEXT_NODE ) {
1070
+ length = endContainer.data.length;
1071
+ if ( length && endOffset < length ) {
1072
+ return false;
1073
+ }
1074
+ contentWalker.currentNode = endContainer;
1075
+ } else {
1076
+ contentWalker.currentNode = getNodeBefore( endContainer, endOffset );
1077
+ }
1078
+
1079
+ // Otherwise, look for any further content in the same block.
1080
+ contentWalker.root = getEndBlockOfRange( range );
1081
+
1082
+ return !contentWalker.nextNode();
1083
+ };
1084
+
1085
+ var expandRangeToBlockBoundaries = function ( range ) {
1086
+ var start = getStartBlockOfRange( range ),
1087
+ end = getEndBlockOfRange( range ),
1088
+ parent;
1089
+
1090
+ if ( start && end ) {
1091
+ parent = start.parentNode;
1092
+ range.setStart( parent, indexOf.call( parent.childNodes, start ) );
1093
+ parent = end.parentNode;
1094
+ range.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 );
1095
+ }
1096
+ };
1097
+
1098
+ var instances = [];
1099
+
1100
+ function Squire ( doc ) {
1101
+ var win = doc.defaultView;
1102
+ var body = doc.body;
1103
+ var mutation;
1104
+
1105
+ this._win = win;
1106
+ this._doc = doc;
1107
+ this._body = body;
1108
+
1109
+ this._events = {};
1110
+
1111
+ this._sel = win.getSelection();
1112
+ this._lastSelection = null;
1113
+
1114
+ // IE loses selection state of iframe on blur, so make sure we
1115
+ // cache it just before it loses focus.
1116
+ if ( losesSelectionOnBlur ) {
1117
+ this.addEventListener( 'beforedeactivate', this.getSelection );
1118
+ }
1119
+
1120
+ this._hasZWS = false;
1121
+
1122
+ this._lastAnchorNode = null;
1123
+ this._lastFocusNode = null;
1124
+ this._path = '';
1125
+
1126
+ this.addEventListener( 'keyup', this._updatePathOnEvent );
1127
+ this.addEventListener( 'mouseup', this._updatePathOnEvent );
1128
+
1129
+ win.addEventListener( 'focus', this, false );
1130
+ win.addEventListener( 'blur', this, false );
1131
+
1132
+ this._undoIndex = -1;
1133
+ this._undoStack = [];
1134
+ this._undoStackLength = 0;
1135
+ this._isInUndoState = false;
1136
+ this._ignoreChange = false;
1137
+
1138
+ if ( canObserveMutations ) {
1139
+ mutation = new MutationObserver( this._docWasChanged.bind( this ) );
1140
+ mutation.observe( body, {
1141
+ childList: true,
1142
+ attributes: true,
1143
+ characterData: true,
1144
+ subtree: true
1145
+ });
1146
+ this._mutation = mutation;
1147
+ } else {
1148
+ this.addEventListener( 'keyup', this._keyUpDetectChange );
1149
+ }
1150
+
1151
+ this.defaultBlockTag = 'DIV';
1152
+ this.defaultBlockProperties = null;
1153
+
1154
+ // IE sometimes fires the beforepaste event twice; make sure it is not run
1155
+ // again before our after paste function is called.
1156
+ this._awaitingPaste = false;
1157
+ this.addEventListener( isIElt11 ? 'beforecut' : 'cut', this._onCut );
1158
+ this.addEventListener( isIElt11 ? 'beforepaste' : 'paste', this._onPaste );
1159
+
1160
+ // Opera does not fire keydown repeatedly.
1161
+ this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
1162
+
1163
+ // Fix IE<10's buggy implementation of Text#splitText.
1164
+ // If the split is at the end of the node, it doesn't insert the newly split
1165
+ // node into the document, and sets its value to undefined rather than ''.
1166
+ // And even if the split is not at the end, the original node is removed
1167
+ // from the document and replaced by another, rather than just having its
1168
+ // data shortened.
1169
+ // We used to feature test for this, but then found the feature test would
1170
+ // sometimes pass, but later on the buggy behaviour would still appear.
1171
+ // I think IE10 does not have the same bug, but it doesn't hurt to replace
1172
+ // its native fn too and then we don't need yet another UA category.
1173
+ if ( isIElt11 ) {
1174
+ win.Text.prototype.splitText = function ( offset ) {
1175
+ var afterSplit = this.ownerDocument.createTextNode(
1176
+ this.data.slice( offset ) ),
1177
+ next = this.nextSibling,
1178
+ parent = this.parentNode,
1179
+ toDelete = this.length - offset;
1180
+ if ( next ) {
1181
+ parent.insertBefore( afterSplit, next );
1182
+ } else {
1183
+ parent.appendChild( afterSplit );
1184
+ }
1185
+ if ( toDelete ) {
1186
+ this.deleteData( offset, toDelete );
1187
+ }
1188
+ return afterSplit;
1189
+ };
1190
+ }
1191
+
1192
+ body.setAttribute( 'contenteditable', 'true' );
1193
+ this.setHTML( '' );
1194
+
1195
+ // Remove Firefox's built-in controls
1196
+ try {
1197
+ doc.execCommand( 'enableObjectResizing', false, 'false' );
1198
+ doc.execCommand( 'enableInlineTableEditing', false, 'false' );
1199
+ } catch ( error ) {}
1200
+
1201
+ instances.push( this );
1202
+ }
1203
+
1204
+ var proto = Squire.prototype;
1205
+
1206
+ proto.createElement = function ( tag, props, children ) {
1207
+ return createElement( this._doc, tag, props, children );
1208
+ };
1209
+
1210
+ proto.createDefaultBlock = function ( children ) {
1211
+ return fixCursor(
1212
+ this.createElement(
1213
+ this.defaultBlockTag, this.defaultBlockProperties, children )
1214
+ );
1215
+ };
1216
+
1217
+ proto.didError = function ( error ) {
1218
+ console.log( error );
1219
+ };
1220
+
1221
+ proto.getDocument = function () {
1222
+ return this._doc;
1223
+ };
1224
+
1225
+ // --- Events ---
1226
+
1227
+ // Subscribing to these events won't automatically add a listener to the
1228
+ // document node, since these events are fired in a custom manner by the
1229
+ // editor code.
1230
+ var customEvents = {
1231
+ focus: 1, blur: 1,
1232
+ pathChange: 1, select: 1, input: 1, undoStateChange: 1
1233
+ };
1234
+
1235
+ proto.fireEvent = function ( type, event ) {
1236
+ var handlers = this._events[ type ],
1237
+ i, l, obj;
1238
+ if ( handlers ) {
1239
+ if ( !event ) {
1240
+ event = {};
1241
+ }
1242
+ if ( event.type !== type ) {
1243
+ event.type = type;
1244
+ }
1245
+ // Clone handlers array, so any handlers added/removed do not affect it.
1246
+ handlers = handlers.slice();
1247
+ for ( i = 0, l = handlers.length; i < l; i += 1 ) {
1248
+ obj = handlers[i];
1249
+ try {
1250
+ if ( obj.handleEvent ) {
1251
+ obj.handleEvent( event );
1252
+ } else {
1253
+ obj.call( this, event );
1254
+ }
1255
+ } catch ( error ) {
1256
+ error.details = 'Squire: fireEvent error. Event type: ' + type;
1257
+ this.didError( error );
1258
+ }
1259
+ }
1260
+ }
1261
+ return this;
1262
+ };
1263
+
1264
+ proto.destroy = function () {
1265
+ var win = this._win,
1266
+ doc = this._doc,
1267
+ events = this._events,
1268
+ type;
1269
+ win.removeEventListener( 'focus', this, false );
1270
+ win.removeEventListener( 'blur', this, false );
1271
+ for ( type in events ) {
1272
+ if ( !customEvents[ type ] ) {
1273
+ doc.removeEventListener( type, this, true );
1274
+ }
1275
+ }
1276
+ if ( this._mutation ) {
1277
+ this._mutation.disconnect();
1278
+ }
1279
+ var l = instances.length;
1280
+ while ( l-- ) {
1281
+ if ( instances[l] === this ) {
1282
+ instances.splice( l, 1 );
1283
+ }
1284
+ }
1285
+ };
1286
+
1287
+ proto.handleEvent = function ( event ) {
1288
+ this.fireEvent( event.type, event );
1289
+ };
1290
+
1291
+ proto.addEventListener = function ( type, fn ) {
1292
+ var handlers = this._events[ type ];
1293
+ if ( !fn ) {
1294
+ this.didError({
1295
+ name: 'Squire: addEventListener with null or undefined fn',
1296
+ message: 'Event type: ' + type
1297
+ });
1298
+ return this;
1299
+ }
1300
+ if ( !handlers ) {
1301
+ handlers = this._events[ type ] = [];
1302
+ if ( !customEvents[ type ] ) {
1303
+ this._doc.addEventListener( type, this, true );
1304
+ }
1305
+ }
1306
+ handlers.push( fn );
1307
+ return this;
1308
+ };
1309
+
1310
+ proto.removeEventListener = function ( type, fn ) {
1311
+ var handlers = this._events[ type ],
1312
+ l;
1313
+ if ( handlers ) {
1314
+ l = handlers.length;
1315
+ while ( l-- ) {
1316
+ if ( handlers[l] === fn ) {
1317
+ handlers.splice( l, 1 );
1318
+ }
1319
+ }
1320
+ if ( !handlers.length ) {
1321
+ delete this._events[ type ];
1322
+ if ( !customEvents[ type ] ) {
1323
+ this._doc.removeEventListener( type, this, false );
1324
+ }
1325
+ }
1326
+ }
1327
+ return this;
1328
+ };
1329
+
1330
+ // --- Selection and Path ---
1331
+
1332
+ proto._createRange =
1333
+ function ( range, startOffset, endContainer, endOffset ) {
1334
+ if ( range instanceof this._win.Range ) {
1335
+ return range.cloneRange();
1336
+ }
1337
+ var domRange = this._doc.createRange();
1338
+ domRange.setStart( range, startOffset );
1339
+ if ( endContainer ) {
1340
+ domRange.setEnd( endContainer, endOffset );
1341
+ } else {
1342
+ domRange.setEnd( range, startOffset );
1343
+ }
1344
+ return domRange;
1345
+ };
1346
+
1347
+ proto.setSelection = function ( range ) {
1348
+ if ( range ) {
1349
+ // iOS bug: if you don't focus the iframe before setting the
1350
+ // selection, you can end up in a state where you type but the input
1351
+ // doesn't get directed into the contenteditable area but is instead
1352
+ // lost in a black hole. Very strange.
1353
+ if ( isIOS ) {
1354
+ this._win.focus();
1355
+ }
1356
+ var sel;
1357
+ if(this._sel === null){
1358
+ sel = this._win.getSelection();
1359
+ }else{
1360
+ sel = this._sel;
1361
+ }
1362
+ sel.removeAllRanges();
1363
+ sel.addRange( range );
1364
+ }
1365
+ return this;
1366
+ };
1367
+
1368
+ proto.getSelection = function () {
1369
+ var sel = this._sel,
1370
+ selection, startContainer, endContainer;
1371
+ if ( sel.rangeCount ) {
1372
+ selection = sel.getRangeAt( 0 ).cloneRange();
1373
+ startContainer = selection.startContainer;
1374
+ endContainer = selection.endContainer;
1375
+ // FF can return the selection as being inside an <img>. WTF?
1376
+ if ( startContainer && isLeaf( startContainer ) ) {
1377
+ selection.setStartBefore( startContainer );
1378
+ }
1379
+ if ( endContainer && isLeaf( endContainer ) ) {
1380
+ selection.setEndBefore( endContainer );
1381
+ }
1382
+ this._lastSelection = selection;
1383
+ } else {
1384
+ selection = this._lastSelection;
1385
+ }
1386
+ if ( !selection ) {
1387
+ selection = this._createRange( this._body.firstChild, 0 );
1388
+ }
1389
+ return selection;
1390
+ };
1391
+
1392
+ proto.getSelectedText = function () {
1393
+ return getTextContentInRange( this.getSelection() );
1394
+ };
1395
+
1396
+ proto.getPath = function () {
1397
+ return this._path;
1398
+ };
1399
+
1400
+ // --- Workaround for browsers that can't focus empty text nodes ---
1401
+
1402
+ // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256
1403
+
1404
+ var removeZWS = function ( root ) {
1405
+ var walker = new TreeWalker( root, SHOW_TEXT, function () {
1406
+ return true;
1407
+ }, false ),
1408
+ node, index;
1409
+ while ( node = walker.nextNode() ) {
1410
+ while ( ( index = node.data.indexOf( ZWS ) ) > -1 ) {
1411
+ node.deleteData( index, 1 );
1412
+ }
1413
+ }
1414
+ };
1415
+
1416
+ proto._didAddZWS = function () {
1417
+ this._hasZWS = true;
1418
+ };
1419
+ proto._removeZWS = function () {
1420
+ if ( !this._hasZWS ) {
1421
+ return;
1422
+ }
1423
+ removeZWS( this._body );
1424
+ this._hasZWS = false;
1425
+ };
1426
+
1427
+ // --- Path change events ---
1428
+
1429
+ proto._updatePath = function ( range, force ) {
1430
+ var anchor = range.startContainer,
1431
+ focus = range.endContainer,
1432
+ newPath;
1433
+ if ( force || anchor !== this._lastAnchorNode ||
1434
+ focus !== this._lastFocusNode ) {
1435
+ this._lastAnchorNode = anchor;
1436
+ this._lastFocusNode = focus;
1437
+ newPath = ( anchor && focus ) ? ( anchor === focus ) ?
1438
+ getPath( focus ) : '(selection)' : '';
1439
+ if ( this._path !== newPath ) {
1440
+ this._path = newPath;
1441
+ this.fireEvent( 'pathChange', { path: newPath } );
1442
+ }
1443
+ }
1444
+ if ( !range.collapsed ) {
1445
+ this.fireEvent( 'select' );
1446
+ }
1447
+ };
1448
+
1449
+ proto._updatePathOnEvent = function () {
1450
+ this._updatePath( this.getSelection() );
1451
+ };
1452
+
1453
+ // --- Focus ---
1454
+
1455
+ proto.focus = function () {
1456
+ // FF seems to need the body to be focussed (at least on first load).
1457
+ // Chrome also now needs body to be focussed in order to show the cursor
1458
+ // (otherwise it is focussed, but the cursor doesn't appear).
1459
+ // Opera (Presto-variant) however will lose the selection if you call this!
1460
+ if ( !isPresto ) {
1461
+ this._body.focus();
1462
+ }
1463
+ this._win.focus();
1464
+ return this;
1465
+ };
1466
+
1467
+ proto.blur = function () {
1468
+ // IE will remove the whole browser window from focus if you call
1469
+ // win.blur() or body.blur(), so instead we call top.focus() to focus
1470
+ // the top frame, thus blurring this frame. This works in everything
1471
+ // except FF, so we need to call body.blur() in that as well.
1472
+ if ( isGecko ) {
1473
+ this._body.blur();
1474
+ }
1475
+ top.focus();
1476
+ return this;
1477
+ };
1478
+
1479
+ // --- Bookmarking ---
1480
+
1481
+ var startSelectionId = 'squire-selection-start';
1482
+ var endSelectionId = 'squire-selection-end';
1483
+
1484
+ proto._saveRangeToBookmark = function ( range ) {
1485
+ var startNode = this.createElement( 'INPUT', {
1486
+ id: startSelectionId,
1487
+ type: 'hidden'
1488
+ }),
1489
+ endNode = this.createElement( 'INPUT', {
1490
+ id: endSelectionId,
1491
+ type: 'hidden'
1492
+ }),
1493
+ temp;
1494
+
1495
+ insertNodeInRange( range, startNode );
1496
+ range.collapse( false );
1497
+ insertNodeInRange( range, endNode );
1498
+
1499
+ // In a collapsed range, the start is sometimes inserted after the end!
1500
+ if ( startNode.compareDocumentPosition( endNode ) &
1501
+ DOCUMENT_POSITION_PRECEDING ) {
1502
+ startNode.id = endSelectionId;
1503
+ endNode.id = startSelectionId;
1504
+ temp = startNode;
1505
+ startNode = endNode;
1506
+ endNode = temp;
1507
+ }
1508
+
1509
+ range.setStartAfter( startNode );
1510
+ range.setEndBefore( endNode );
1511
+ };
1512
+
1513
+ proto._getRangeAndRemoveBookmark = function ( range ) {
1514
+ var doc = this._doc,
1515
+ start = doc.getElementById( startSelectionId ),
1516
+ end = doc.getElementById( endSelectionId );
1517
+
1518
+ if ( start && end ) {
1519
+ var startContainer = start.parentNode,
1520
+ endContainer = end.parentNode,
1521
+ collapsed;
1522
+
1523
+ var _range = {
1524
+ startContainer: startContainer,
1525
+ endContainer: endContainer,
1526
+ startOffset: indexOf.call( startContainer.childNodes, start ),
1527
+ endOffset: indexOf.call( endContainer.childNodes, end )
1528
+ };
1529
+
1530
+ if ( startContainer === endContainer ) {
1531
+ _range.endOffset -= 1;
1532
+ }
1533
+
1534
+ detach( start );
1535
+ detach( end );
1536
+
1537
+ // Merge any text nodes we split
1538
+ mergeInlines( startContainer, _range );
1539
+ if ( startContainer !== endContainer ) {
1540
+ mergeInlines( endContainer, _range );
1541
+ }
1542
+
1543
+ if ( !range ) {
1544
+ range = doc.createRange();
1545
+ }
1546
+ range.setStart( _range.startContainer, _range.startOffset );
1547
+ range.setEnd( _range.endContainer, _range.endOffset );
1548
+ collapsed = range.collapsed;
1549
+
1550
+ moveRangeBoundariesDownTree( range );
1551
+ if ( collapsed ) {
1552
+ range.collapse( true );
1553
+ }
1554
+ }
1555
+ return range || null;
1556
+ };
1557
+
1558
+ // --- Undo ---
1559
+
1560
+ proto._keyUpDetectChange = function ( event ) {
1561
+ var code = event.keyCode;
1562
+ // Presume document was changed if:
1563
+ // 1. A modifier key (other than shift) wasn't held down
1564
+ // 2. The key pressed is not in range 16<=x<=20 (control keys)
1565
+ // 3. The key pressed is not in range 33<=x<=45 (navigation keys)
1566
+ if ( !event.ctrlKey && !event.metaKey && !event.altKey &&
1567
+ ( code < 16 || code > 20 ) &&
1568
+ ( code < 33 || code > 45 ) ) {
1569
+ this._docWasChanged();
1570
+ }
1571
+ };
1572
+
1573
+ proto._docWasChanged = function () {
1574
+ if ( canObserveMutations && this._ignoreChange ) {
1575
+ this._ignoreChange = false;
1576
+ return;
1577
+ }
1578
+ if ( this._isInUndoState ) {
1579
+ this._isInUndoState = false;
1580
+ this.fireEvent( 'undoStateChange', {
1581
+ canUndo: true,
1582
+ canRedo: false
1583
+ });
1584
+ }
1585
+ this.fireEvent( 'input' );
1586
+ };
1587
+
1588
+ // Leaves bookmark
1589
+ proto._recordUndoState = function ( range ) {
1590
+ // Don't record if we're already in an undo state
1591
+ if ( !this._isInUndoState ) {
1592
+ // Advance pointer to new position
1593
+ var undoIndex = this._undoIndex += 1,
1594
+ undoStack = this._undoStack;
1595
+
1596
+ // Truncate stack if longer (i.e. if has been previously undone)
1597
+ if ( undoIndex < this._undoStackLength) {
1598
+ undoStack.length = this._undoStackLength = undoIndex;
1599
+ }
1600
+
1601
+ // Write out data
1602
+ if ( range ) {
1603
+ this._saveRangeToBookmark( range );
1604
+ }
1605
+ undoStack[ undoIndex ] = this._getHTML();
1606
+ this._undoStackLength += 1;
1607
+ this._isInUndoState = true;
1608
+ }
1609
+ };
1610
+
1611
+ proto.undo = function () {
1612
+ // Sanity check: must not be at beginning of the history stack
1613
+ if ( this._undoIndex !== 0 || !this._isInUndoState ) {
1614
+ // Make sure any changes since last checkpoint are saved.
1615
+ this._recordUndoState( this.getSelection() );
1616
+
1617
+ this._undoIndex -= 1;
1618
+ this._setHTML( this._undoStack[ this._undoIndex ] );
1619
+ var range = this._getRangeAndRemoveBookmark();
1620
+ if ( range ) {
1621
+ this.setSelection( range );
1622
+ }
1623
+ this._isInUndoState = true;
1624
+ this.fireEvent( 'undoStateChange', {
1625
+ canUndo: this._undoIndex !== 0,
1626
+ canRedo: true
1627
+ });
1628
+ this.fireEvent( 'input' );
1629
+ }
1630
+ return this;
1631
+ };
1632
+
1633
+ proto.redo = function () {
1634
+ // Sanity check: must not be at end of stack and must be in an undo
1635
+ // state.
1636
+ var undoIndex = this._undoIndex,
1637
+ undoStackLength = this._undoStackLength;
1638
+ if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) {
1639
+ this._undoIndex += 1;
1640
+ this._setHTML( this._undoStack[ this._undoIndex ] );
1641
+ var range = this._getRangeAndRemoveBookmark();
1642
+ if ( range ) {
1643
+ this.setSelection( range );
1644
+ }
1645
+ this.fireEvent( 'undoStateChange', {
1646
+ canUndo: true,
1647
+ canRedo: undoIndex + 2 < undoStackLength
1648
+ });
1649
+ this.fireEvent( 'input' );
1650
+ }
1651
+ return this;
1652
+ };
1653
+
1654
+ // --- Inline formatting ---
1655
+
1656
+ // Looks for matching tag and attributes, so won't work
1657
+ // if <strong> instead of <b> etc.
1658
+ proto.hasFormat = function ( tag, attributes, range ) {
1659
+ // 1. Normalise the arguments and get selection
1660
+ tag = tag.toUpperCase();
1661
+ if ( !attributes ) { attributes = {}; }
1662
+ if ( !range && !( range = this.getSelection() ) ) {
1663
+ return false;
1664
+ }
1665
+
1666
+ // If the common ancestor is inside the tag we require, we definitely
1667
+ // have the format.
1668
+ var root = range.commonAncestorContainer,
1669
+ walker, node;
1670
+ if ( getNearest( root, tag, attributes ) ) {
1671
+ return true;
1672
+ }
1673
+
1674
+ // If common ancestor is a text node and doesn't have the format, we
1675
+ // definitely don't have it.
1676
+ if ( root.nodeType === TEXT_NODE ) {
1677
+ return false;
1678
+ }
1679
+
1680
+ // Otherwise, check each text node at least partially contained within
1681
+ // the selection and make sure all of them have the format we want.
1682
+ walker = new TreeWalker( root, SHOW_TEXT, function ( node ) {
1683
+ return isNodeContainedInRange( range, node, true );
1684
+ }, false );
1685
+
1686
+ var seenNode = false;
1687
+ while ( node = walker.nextNode() ) {
1688
+ if ( !getNearest( node, tag, attributes ) ) {
1689
+ return false;
1690
+ }
1691
+ seenNode = true;
1692
+ }
1693
+
1694
+ return seenNode;
1695
+ };
1696
+
1697
+ proto._addFormat = function ( tag, attributes, range ) {
1698
+ // If the range is collapsed we simply insert the node by wrapping
1699
+ // it round the range and focus it.
1700
+ var el, walker, startContainer, endContainer, startOffset, endOffset,
1701
+ textNode, needsFormat;
1702
+
1703
+ if ( range.collapsed ) {
1704
+ el = fixCursor( this.createElement( tag, attributes ) );
1705
+ insertNodeInRange( range, el );
1706
+ range.setStart( el.firstChild, el.firstChild.length );
1707
+ range.collapse( true );
1708
+ }
1709
+ // Otherwise we find all the textnodes in the range (splitting
1710
+ // partially selected nodes) and if they're not already formatted
1711
+ // correctly we wrap them in the appropriate tag.
1712
+ else {
1713
+ // We don't want to apply formatting twice so we check each text
1714
+ // node to see if it has an ancestor with the formatting already.
1715
+ // Create an iterator to walk over all the text nodes under this
1716
+ // ancestor which are in the range and not already formatted
1717
+ // correctly.
1718
+ walker = new TreeWalker(
1719
+ range.commonAncestorContainer,
1720
+ SHOW_TEXT,
1721
+ function ( node ) {
1722
+ return isNodeContainedInRange( range, node, true );
1723
+ },
1724
+ false
1725
+ );
1726
+
1727
+ // Start at the beginning node of the range and iterate through
1728
+ // all the nodes in the range that need formatting.
1729
+ startContainer = range.startContainer;
1730
+ startOffset = range.startOffset;
1731
+ endContainer = range.endContainer;
1732
+ endOffset = range.endOffset;
1733
+
1734
+ // Make sure we start inside a text node.
1735
+ walker.currentNode = startContainer;
1736
+ if ( startContainer.nodeType !== TEXT_NODE ) {
1737
+ startContainer = walker.nextNode();
1738
+ startOffset = 0;
1739
+ }
1740
+
1741
+ do {
1742
+ textNode = walker.currentNode;
1743
+ needsFormat = !getNearest( textNode, tag, attributes );
1744
+ if ( needsFormat ) {
1745
+ if ( textNode === endContainer &&
1746
+ textNode.length > endOffset ) {
1747
+ textNode.splitText( endOffset );
1748
+ }
1749
+ if ( textNode === startContainer && startOffset ) {
1750
+ textNode = textNode.splitText( startOffset );
1751
+ if ( endContainer === startContainer ) {
1752
+ endContainer = textNode;
1753
+ endOffset -= startOffset;
1754
+ }
1755
+ startContainer = textNode;
1756
+ startOffset = 0;
1757
+ }
1758
+ el = this.createElement( tag, attributes );
1759
+ replaceWith( textNode, el );
1760
+ el.appendChild( textNode );
1761
+ }
1762
+ } while ( walker.nextNode() );
1763
+
1764
+ // Make sure we finish inside a text node. Otherwise offset may have
1765
+ // changed.
1766
+ if ( endContainer.nodeType !== TEXT_NODE ) {
1767
+ endContainer = textNode;
1768
+ endOffset = textNode.length;
1769
+ }
1770
+
1771
+ // Now set the selection to as it was before
1772
+ range = this._createRange(
1773
+ startContainer, startOffset, endContainer, endOffset );
1774
+ }
1775
+ return range;
1776
+ };
1777
+
1778
+ proto._removeFormat = function ( tag, attributes, range, partial ) {
1779
+ // Add bookmark
1780
+ this._saveRangeToBookmark( range );
1781
+
1782
+ // We need a node in the selection to break the surrounding
1783
+ // formatted text.
1784
+ var doc = this._doc,
1785
+ fixer;
1786
+ if ( range.collapsed ) {
1787
+ if ( cantFocusEmptyTextNodes ) {
1788
+ fixer = doc.createTextNode( ZWS );
1789
+ this._didAddZWS();
1790
+ } else {
1791
+ fixer = doc.createTextNode( '' );
1792
+ }
1793
+ insertNodeInRange( range, fixer );
1794
+ }
1795
+
1796
+ // Find block-level ancestor of selection
1797
+ var root = range.commonAncestorContainer;
1798
+ while ( isInline( root ) ) {
1799
+ root = root.parentNode;
1800
+ }
1801
+
1802
+ // Find text nodes inside formatTags that are not in selection and
1803
+ // add an extra tag with the same formatting.
1804
+ var startContainer = range.startContainer,
1805
+ startOffset = range.startOffset,
1806
+ endContainer = range.endContainer,
1807
+ endOffset = range.endOffset,
1808
+ toWrap = [],
1809
+ examineNode = function ( node, exemplar ) {
1810
+ // If the node is completely contained by the range then
1811
+ // we're going to remove all formatting so ignore it.
1812
+ if ( isNodeContainedInRange( range, node, false ) ) {
1813
+ return;
1814
+ }
1815
+
1816
+ var isText = ( node.nodeType === TEXT_NODE ),
1817
+ child, next;
1818
+
1819
+ // If not at least partially contained, wrap entire contents
1820
+ // in a clone of the tag we're removing and we're done.
1821
+ if ( !isNodeContainedInRange( range, node, true ) ) {
1822
+ // Ignore bookmarks and empty text nodes
1823
+ if ( node.nodeName !== 'INPUT' &&
1824
+ ( !isText || node.data ) ) {
1825
+ toWrap.push([ exemplar, node ]);
1826
+ }
1827
+ return;
1828
+ }
1829
+
1830
+ // Split any partially selected text nodes.
1831
+ if ( isText ) {
1832
+ if ( node === endContainer && endOffset !== node.length ) {
1833
+ toWrap.push([ exemplar, node.splitText( endOffset ) ]);
1834
+ }
1835
+ if ( node === startContainer && startOffset ) {
1836
+ node.splitText( startOffset );
1837
+ toWrap.push([ exemplar, node ]);
1838
+ }
1839
+ }
1840
+ // If not a text node, recurse onto all children.
1841
+ // Beware, the tree may be rewritten with each call
1842
+ // to examineNode, hence find the next sibling first.
1843
+ else {
1844
+ for ( child = node.firstChild; child; child = next ) {
1845
+ next = child.nextSibling;
1846
+ examineNode( child, exemplar );
1847
+ }
1848
+ }
1849
+ },
1850
+ formatTags = Array.prototype.filter.call(
1851
+ root.getElementsByTagName( tag ), function ( el ) {
1852
+ return isNodeContainedInRange( range, el, true ) &&
1853
+ hasTagAttributes( el, tag, attributes );
1854
+ }
1855
+ );
1856
+
1857
+ if ( !partial ) {
1858
+ formatTags.forEach( function ( node ) {
1859
+ examineNode( node, node );
1860
+ });
1861
+ }
1862
+
1863
+ // Now wrap unselected nodes in the tag
1864
+ toWrap.forEach( function ( item ) {
1865
+ // [ exemplar, node ] tuple
1866
+ var el = item[0].cloneNode( false ),
1867
+ node = item[1];
1868
+ replaceWith( node, el );
1869
+ el.appendChild( node );
1870
+ });
1871
+ // and remove old formatting tags.
1872
+ formatTags.forEach( function ( el ) {
1873
+ replaceWith( el, empty( el ) );
1874
+ });
1875
+
1876
+ // Merge adjacent inlines:
1877
+ this._getRangeAndRemoveBookmark( range );
1878
+ if ( fixer ) {
1879
+ range.collapse( false );
1880
+ }
1881
+ var _range = {
1882
+ startContainer: range.startContainer,
1883
+ startOffset: range.startOffset,
1884
+ endContainer: range.endContainer,
1885
+ endOffset: range.endOffset
1886
+ };
1887
+ mergeInlines( root, _range );
1888
+ range.setStart( _range.startContainer, _range.startOffset );
1889
+ range.setEnd( _range.endContainer, _range.endOffset );
1890
+
1891
+ return range;
1892
+ };
1893
+
1894
+ proto.changeFormat = function ( add, remove, range, partial ) {
1895
+ // Normalise the arguments and get selection
1896
+ if ( !range && !( range = this.getSelection() ) ) {
1897
+ return;
1898
+ }
1899
+
1900
+ // Save undo checkpoint
1901
+ this._recordUndoState( range );
1902
+ this._getRangeAndRemoveBookmark( range );
1903
+
1904
+ if ( remove ) {
1905
+ range = this._removeFormat( remove.tag.toUpperCase(),
1906
+ remove.attributes || {}, range, partial );
1907
+ }
1908
+ if ( add ) {
1909
+ range = this._addFormat( add.tag.toUpperCase(),
1910
+ add.attributes || {}, range );
1911
+ }
1912
+
1913
+ this.setSelection( range );
1914
+ this._updatePath( range, true );
1915
+
1916
+ // We're not still in an undo state
1917
+ if ( !canObserveMutations ) {
1918
+ this._docWasChanged();
1919
+ }
1920
+
1921
+ return this;
1922
+ };
1923
+
1924
+ // --- Block formatting ---
1925
+
1926
+ var tagAfterSplit = {
1927
+ DT: 'DD',
1928
+ DD: 'DT',
1929
+ LI: 'LI'
1930
+ };
1931
+
1932
+ var splitBlock = function ( self, block, node, offset ) {
1933
+ var splitTag = tagAfterSplit[ block.nodeName ],
1934
+ splitProperties = null,
1935
+ nodeAfterSplit = split( node, offset, block.parentNode );
1936
+
1937
+ if ( !splitTag ) {
1938
+ splitTag = self.defaultBlockTag;
1939
+ splitProperties = self.defaultBlockProperties;
1940
+ }
1941
+
1942
+ // Make sure the new node is the correct type.
1943
+ if ( !hasTagAttributes( nodeAfterSplit, splitTag, splitProperties ) ) {
1944
+ block = createElement( nodeAfterSplit.ownerDocument,
1945
+ splitTag, splitProperties );
1946
+ if ( nodeAfterSplit.dir ) {
1947
+ block.className = nodeAfterSplit.dir === 'rtl' ? 'dir-rtl' : '';
1948
+ block.dir = nodeAfterSplit.dir;
1949
+ }
1950
+ replaceWith( nodeAfterSplit, block );
1951
+ block.appendChild( empty( nodeAfterSplit ) );
1952
+ nodeAfterSplit = block;
1953
+ }
1954
+ return nodeAfterSplit;
1955
+ };
1956
+
1957
+ proto.forEachBlock = function ( fn, mutates, range ) {
1958
+ if ( !range && !( range = this.getSelection() ) ) {
1959
+ return this;
1960
+ }
1961
+
1962
+ // Save undo checkpoint
1963
+ if ( mutates ) {
1964
+ this._recordUndoState( range );
1965
+ this._getRangeAndRemoveBookmark( range );
1966
+ }
1967
+
1968
+ var start = getStartBlockOfRange( range ),
1969
+ end = getEndBlockOfRange( range );
1970
+ if ( start && end ) {
1971
+ do {
1972
+ if ( fn( start ) || start === end ) { break; }
1973
+ } while ( start = getNextBlock( start ) );
1974
+ }
1975
+
1976
+ if ( mutates ) {
1977
+ this.setSelection( range );
1978
+
1979
+ // Path may have changed
1980
+ this._updatePath( range, true );
1981
+
1982
+ // We're not still in an undo state
1983
+ if ( !canObserveMutations ) {
1984
+ this._docWasChanged();
1985
+ }
1986
+ }
1987
+ return this;
1988
+ };
1989
+
1990
+ proto.modifyBlocks = function ( modify, range ) {
1991
+ if ( !range && !( range = this.getSelection() ) ) {
1992
+ return this;
1993
+ }
1994
+
1995
+ // 1. Save undo checkpoint and bookmark selection
1996
+ if ( this._isInUndoState ) {
1997
+ this._saveRangeToBookmark( range );
1998
+ } else {
1999
+ this._recordUndoState( range );
2000
+ }
2001
+
2002
+ // 2. Expand range to block boundaries
2003
+ expandRangeToBlockBoundaries( range );
2004
+
2005
+ // 3. Remove range.
2006
+ var body = this._body,
2007
+ frag;
2008
+ moveRangeBoundariesUpTree( range, body );
2009
+ frag = extractContentsOfRange( range, body );
2010
+
2011
+ // 4. Modify tree of fragment and reinsert.
2012
+ insertNodeInRange( range, modify.call( this, frag ) );
2013
+
2014
+ // 5. Merge containers at edges
2015
+ if ( range.endOffset < range.endContainer.childNodes.length ) {
2016
+ mergeContainers( range.endContainer.childNodes[ range.endOffset ] );
2017
+ }
2018
+ mergeContainers( range.startContainer.childNodes[ range.startOffset ] );
2019
+
2020
+ // 6. Restore selection
2021
+ this._getRangeAndRemoveBookmark( range );
2022
+ this.setSelection( range );
2023
+ this._updatePath( range, true );
2024
+
2025
+ // 7. We're not still in an undo state
2026
+ if ( !canObserveMutations ) {
2027
+ this._docWasChanged();
2028
+ }
2029
+
2030
+ return this;
2031
+ };
2032
+
2033
+ var increaseBlockQuoteLevel = function ( frag ) {
2034
+ return this.createElement( 'BLOCKQUOTE', [
2035
+ frag
2036
+ ]);
2037
+ };
2038
+
2039
+ var decreaseBlockQuoteLevel = function ( frag ) {
2040
+ var blockquotes = frag.querySelectorAll( 'blockquote' );
2041
+ Array.prototype.filter.call( blockquotes, function ( el ) {
2042
+ return !getNearest( el.parentNode, 'BLOCKQUOTE' );
2043
+ }).forEach( function ( el ) {
2044
+ replaceWith( el, empty( el ) );
2045
+ });
2046
+ return frag;
2047
+ };
2048
+
2049
+ var removeBlockQuote = function (/* frag */) {
2050
+ return this.createDefaultBlock([
2051
+ this.createElement( 'INPUT', {
2052
+ id: startSelectionId,
2053
+ type: 'hidden'
2054
+ }),
2055
+ this.createElement( 'INPUT', {
2056
+ id: endSelectionId,
2057
+ type: 'hidden'
2058
+ })
2059
+ ]);
2060
+ };
2061
+
2062
+ var makeList = function ( self, frag, type ) {
2063
+ var walker = getBlockWalker( frag ),
2064
+ node, tag, prev, newLi;
2065
+
2066
+ while ( node = walker.nextNode() ) {
2067
+ tag = node.parentNode.nodeName;
2068
+ if ( tag !== 'LI' ) {
2069
+ newLi = self.createElement( 'LI', {
2070
+ 'class': node.dir === 'rtl' ? 'dir-rtl' : undefined,
2071
+ dir: node.dir || undefined
2072
+ });
2073
+ // Have we replaced the previous block with a new <ul>/<ol>?
2074
+ if ( ( prev = node.previousSibling ) &&
2075
+ prev.nodeName === type ) {
2076
+ prev.appendChild( newLi );
2077
+ }
2078
+ // Otherwise, replace this block with the <ul>/<ol>
2079
+ else {
2080
+ replaceWith(
2081
+ node,
2082
+ self.createElement( type, [
2083
+ newLi
2084
+ ])
2085
+ );
2086
+ }
2087
+ newLi.appendChild( node );
2088
+ } else {
2089
+ node = node.parentNode.parentNode;
2090
+ tag = node.nodeName;
2091
+ if ( tag !== type && ( /^[OU]L$/.test( tag ) ) ) {
2092
+ replaceWith( node,
2093
+ self.createElement( type, [ empty( node ) ] )
2094
+ );
2095
+ }
2096
+ }
2097
+ }
2098
+ };
2099
+
2100
+ var makeUnorderedList = function ( frag ) {
2101
+ makeList( this, frag, 'UL' );
2102
+ return frag;
2103
+ };
2104
+
2105
+ var makeOrderedList = function ( frag ) {
2106
+ makeList( this, frag, 'OL' );
2107
+ return frag;
2108
+ };
2109
+
2110
+ var removeList = function ( frag ) {
2111
+ var lists = frag.querySelectorAll( 'UL, OL' ),
2112
+ i, l, ll, list, listFrag, children, child;
2113
+ for ( i = 0, l = lists.length; i < l; i += 1 ) {
2114
+ list = lists[i];
2115
+ listFrag = empty( list );
2116
+ children = listFrag.childNodes;
2117
+ ll = children.length;
2118
+ while ( ll-- ) {
2119
+ child = children[ll];
2120
+ replaceWith( child, empty( child ) );
2121
+ }
2122
+ fixContainer( listFrag );
2123
+ replaceWith( list, listFrag );
2124
+ }
2125
+ return frag;
2126
+ };
2127
+
2128
+ var increaseListLevel = function ( frag ) {
2129
+ var items = frag.querySelectorAll( 'LI' ),
2130
+ i, l, item,
2131
+ type, newParent;
2132
+ for ( i = 0, l = items.length; i < l; i += 1 ) {
2133
+ item = items[i];
2134
+ if ( !isContainer( item.firstChild ) ) {
2135
+ // type => 'UL' or 'OL'
2136
+ type = item.parentNode.nodeName;
2137
+ newParent = item.previousSibling;
2138
+ if ( !newParent || !( newParent = newParent.lastChild ) ||
2139
+ newParent.nodeName !== type ) {
2140
+ replaceWith(
2141
+ item,
2142
+ this.createElement( 'LI', [
2143
+ newParent = this.createElement( type )
2144
+ ])
2145
+ );
2146
+ }
2147
+ newParent.appendChild( item );
2148
+ }
2149
+ }
2150
+ return frag;
2151
+ };
2152
+
2153
+ var decreaseListLevel = function ( frag ) {
2154
+ var items = frag.querySelectorAll( 'LI' );
2155
+ Array.prototype.filter.call( items, function ( el ) {
2156
+ return !isContainer( el.firstChild );
2157
+ }).forEach( function ( item ) {
2158
+ var parent = item.parentNode,
2159
+ newParent = parent.parentNode,
2160
+ first = item.firstChild,
2161
+ node = first,
2162
+ next;
2163
+ if ( item.previousSibling ) {
2164
+ parent = split( parent, item, newParent );
2165
+ }
2166
+ while ( node ) {
2167
+ next = node.nextSibling;
2168
+ if ( isContainer( node ) ) {
2169
+ break;
2170
+ }
2171
+ newParent.insertBefore( node, parent );
2172
+ node = next;
2173
+ }
2174
+ if ( newParent.nodeName === 'LI' && first.previousSibling ) {
2175
+ split( newParent, first, newParent.parentNode );
2176
+ }
2177
+ while ( item !== frag && !item.childNodes.length ) {
2178
+ parent = item.parentNode;
2179
+ parent.removeChild( item );
2180
+ item = parent;
2181
+ }
2182
+ }, this );
2183
+ fixContainer( frag );
2184
+ return frag;
2185
+ };
2186
+
2187
+ // --- Clean ---
2188
+
2189
+ var linkRegExp = /\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))|([\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,}\b)/i;
2190
+
2191
+ var addLinks = function ( frag ) {
2192
+ var doc = frag.ownerDocument,
2193
+ walker = new TreeWalker( frag, SHOW_TEXT,
2194
+ function ( node ) {
2195
+ return !getNearest( node, 'A' );
2196
+ }, false ),
2197
+ node, data, parent, match, index, endIndex, child;
2198
+ while ( node = walker.nextNode() ) {
2199
+ data = node.data;
2200
+ parent = node.parentNode;
2201
+ while ( match = linkRegExp.exec( data ) ) {
2202
+ index = match.index;
2203
+ endIndex = index + match[0].length;
2204
+ if ( index ) {
2205
+ child = doc.createTextNode( data.slice( 0, index ) );
2206
+ parent.insertBefore( child, node );
2207
+ }
2208
+ child = doc.createElement( 'A' );
2209
+ child.textContent = data.slice( index, endIndex );
2210
+ child.href = match[1] ?
2211
+ /^(?:ht|f)tps?:/.test( match[1] ) ?
2212
+ match[1] :
2213
+ 'http://' + match[1] :
2214
+ 'mailto:' + match[2];
2215
+ parent.insertBefore( child, node );
2216
+ node.data = data = data.slice( endIndex );
2217
+ }
2218
+ }
2219
+ };
2220
+
2221
+ var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/;
2222
+
2223
+ var fontSizes = {
2224
+ 1: 10,
2225
+ 2: 13,
2226
+ 3: 16,
2227
+ 4: 18,
2228
+ 5: 24,
2229
+ 6: 32,
2230
+ 7: 48
2231
+ };
2232
+
2233
+ var spanToSemantic = {
2234
+ backgroundColor: {
2235
+ regexp: notWS,
2236
+ replace: function ( doc, colour ) {
2237
+ return createElement( doc, 'SPAN', {
2238
+ 'class': 'highlight',
2239
+ style: 'background-color: ' + colour
2240
+ });
2241
+ }
2242
+ },
2243
+ color: {
2244
+ regexp: notWS,
2245
+ replace: function ( doc, colour ) {
2246
+ return createElement( doc, 'SPAN', {
2247
+ 'class': 'colour',
2248
+ style: 'color:' + colour
2249
+ });
2250
+ }
2251
+ },
2252
+ fontWeight: {
2253
+ regexp: /^bold/i,
2254
+ replace: function ( doc ) {
2255
+ return createElement( doc, 'B' );
2256
+ }
2257
+ },
2258
+ fontStyle: {
2259
+ regexp: /^italic/i,
2260
+ replace: function ( doc ) {
2261
+ return createElement( doc, 'I' );
2262
+ }
2263
+ },
2264
+ fontFamily: {
2265
+ regexp: notWS,
2266
+ replace: function ( doc, family ) {
2267
+ return createElement( doc, 'SPAN', {
2268
+ 'class': 'font',
2269
+ style: 'font-family:' + family
2270
+ });
2271
+ }
2272
+ },
2273
+ fontSize: {
2274
+ regexp: notWS,
2275
+ replace: function ( doc, size ) {
2276
+ return createElement( doc, 'SPAN', {
2277
+ 'class': 'size',
2278
+ style: 'font-size:' + size
2279
+ });
2280
+ }
2281
+ }
2282
+ };
2283
+
2284
+ var replaceWithTag = function ( tag ) {
2285
+ return function ( node, parent ) {
2286
+ var el = createElement( node.ownerDocument, tag );
2287
+ parent.replaceChild( el, node );
2288
+ el.appendChild( empty( node ) );
2289
+ return el;
2290
+ };
2291
+ };
2292
+
2293
+ var stylesRewriters = {
2294
+ SPAN: function ( span, parent ) {
2295
+ var style = span.style,
2296
+ doc = span.ownerDocument,
2297
+ attr, converter, css, newTreeBottom, newTreeTop, el;
2298
+
2299
+ for ( attr in spanToSemantic ) {
2300
+ converter = spanToSemantic[ attr ];
2301
+ css = style[ attr ];
2302
+ if ( css && converter.regexp.test( css ) ) {
2303
+ el = converter.replace( doc, css );
2304
+ if ( newTreeBottom ) {
2305
+ newTreeBottom.appendChild( el );
2306
+ }
2307
+ newTreeBottom = el;
2308
+ if ( !newTreeTop ) {
2309
+ newTreeTop = el;
2310
+ }
2311
+ }
2312
+ }
2313
+
2314
+ if ( newTreeTop ) {
2315
+ newTreeBottom.appendChild( empty( span ) );
2316
+ parent.replaceChild( newTreeTop, span );
2317
+ }
2318
+
2319
+ return newTreeBottom || span;
2320
+ },
2321
+ STRONG: replaceWithTag( 'B' ),
2322
+ EM: replaceWithTag( 'I' ),
2323
+ STRIKE: replaceWithTag( 'S' ),
2324
+ FONT: function ( node, parent ) {
2325
+ var face = node.face,
2326
+ size = node.size,
2327
+ colour = node.color,
2328
+ doc = node.ownerDocument,
2329
+ fontSpan, sizeSpan, colourSpan,
2330
+ newTreeBottom, newTreeTop;
2331
+ if ( face ) {
2332
+ fontSpan = createElement( doc, 'SPAN', {
2333
+ 'class': 'font',
2334
+ style: 'font-family:' + face
2335
+ });
2336
+ newTreeTop = fontSpan;
2337
+ newTreeBottom = fontSpan;
2338
+ }
2339
+ if ( size ) {
2340
+ sizeSpan = createElement( doc, 'SPAN', {
2341
+ 'class': 'size',
2342
+ style: 'font-size:' + fontSizes[ size ] + 'px'
2343
+ });
2344
+ if ( !newTreeTop ) {
2345
+ newTreeTop = sizeSpan;
2346
+ }
2347
+ if ( newTreeBottom ) {
2348
+ newTreeBottom.appendChild( sizeSpan );
2349
+ }
2350
+ newTreeBottom = sizeSpan;
2351
+ }
2352
+ if ( colour && /^#?([\dA-F]{3}){1,2}$/i.test( colour ) ) {
2353
+ if ( colour.charAt( 0 ) !== '#' ) {
2354
+ colour = '#' + colour;
2355
+ }
2356
+ colourSpan = createElement( doc, 'SPAN', {
2357
+ 'class': 'colour',
2358
+ style: 'color:' + colour
2359
+ });
2360
+ if ( !newTreeTop ) {
2361
+ newTreeTop = colourSpan;
2362
+ }
2363
+ if ( newTreeBottom ) {
2364
+ newTreeBottom.appendChild( colourSpan );
2365
+ }
2366
+ newTreeBottom = colourSpan;
2367
+ }
2368
+ if ( !newTreeTop ) {
2369
+ newTreeTop = newTreeBottom = createElement( doc, 'SPAN' );
2370
+ }
2371
+ parent.replaceChild( newTreeTop, node );
2372
+ newTreeBottom.appendChild( empty( node ) );
2373
+ return newTreeBottom;
2374
+ },
2375
+ TT: function ( node, parent ) {
2376
+ var el = createElement( node.ownerDocument, 'SPAN', {
2377
+ 'class': 'font',
2378
+ style: 'font-family:menlo,consolas,"courier new",monospace'
2379
+ });
2380
+ parent.replaceChild( el, node );
2381
+ el.appendChild( empty( node ) );
2382
+ return el;
2383
+ }
2384
+ };
2385
+
2386
+ var removeEmptyInlines = function ( root ) {
2387
+ var children = root.childNodes,
2388
+ l = children.length,
2389
+ child;
2390
+ while ( l-- ) {
2391
+ child = children[l];
2392
+ if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
2393
+ removeEmptyInlines( child );
2394
+ if ( isInline( child ) && !child.firstChild ) {
2395
+ root.removeChild( child );
2396
+ }
2397
+ } else if ( child.nodeType === TEXT_NODE && !child.data ) {
2398
+ root.removeChild( child );
2399
+ }
2400
+ }
2401
+ };
2402
+
2403
+ /*
2404
+ Two purposes:
2405
+
2406
+ 1. Remove nodes we don't want, such as weird <o:p> tags, comment nodes
2407
+ and whitespace nodes.
2408
+ 2. Convert inline tags into our preferred format.
2409
+ */
2410
+ var cleanTree = function ( node, allowStyles ) {
2411
+ var children = node.childNodes,
2412
+ i, l, child, nodeName, nodeType, rewriter, childLength,
2413
+ data, j, ll;
2414
+ for ( i = 0, l = children.length; i < l; i += 1 ) {
2415
+ child = children[i];
2416
+ nodeName = child.nodeName;
2417
+ nodeType = child.nodeType;
2418
+ rewriter = stylesRewriters[ nodeName ];
2419
+ if ( nodeType === ELEMENT_NODE ) {
2420
+ childLength = child.childNodes.length;
2421
+ if ( rewriter ) {
2422
+ child = rewriter( child, node );
2423
+ } else if ( !allowedBlock.test( nodeName ) &&
2424
+ !isInline( child ) ) {
2425
+ i -= 1;
2426
+ l += childLength - 1;
2427
+ node.replaceChild( empty( child ), child );
2428
+ continue;
2429
+ } else if ( !allowStyles && child.style.cssText ) {
2430
+ child.removeAttribute( 'style' );
2431
+ }
2432
+ if ( childLength ) {
2433
+ cleanTree( child, allowStyles );
2434
+ }
2435
+ } else {
2436
+ if ( nodeType === TEXT_NODE ) {
2437
+ data = child.data;
2438
+ // Use \S instead of notWS, because we want to remove nodes
2439
+ // which are just nbsp, in order to cleanup <div>nbsp<br></div>
2440
+ // construct.
2441
+ if ( /\S/.test( data ) ) {
2442
+ // If the parent node is inline, don't trim this node as
2443
+ // it probably isn't at the end of the block.
2444
+ if ( isInline( node ) ) {
2445
+ continue;
2446
+ }
2447
+ j = 0;
2448
+ ll = data.length;
2449
+ if ( !i || !isInline( children[ i - 1 ] ) ) {
2450
+ while ( j < ll && !notWS.test( data.charAt( j ) ) ) {
2451
+ j += 1;
2452
+ }
2453
+ if ( j ) {
2454
+ child.data = data = data.slice( j );
2455
+ ll -= j;
2456
+ }
2457
+ }
2458
+ if ( i + 1 === l || !isInline( children[ i + 1 ] ) ) {
2459
+ j = ll;
2460
+ while ( j > 0 && !notWS.test( data.charAt( j - 1 ) ) ) {
2461
+ j -= 1;
2462
+ }
2463
+ if ( j < ll ) {
2464
+ child.data = data.slice( 0, j );
2465
+ }
2466
+ }
2467
+ continue;
2468
+ }
2469
+ // If we have just white space, it may still be important if it
2470
+ // separates two inline nodes, e.g. "<a>link</a> <a>link</a>".
2471
+ else if ( i && i + 1 < l &&
2472
+ isInline( children[ i - 1 ] ) &&
2473
+ isInline( children[ i + 1 ] ) ) {
2474
+ child.data = ' ';
2475
+ continue;
2476
+ }
2477
+ }
2478
+ node.removeChild( child );
2479
+ i -= 1;
2480
+ l -= 1;
2481
+ }
2482
+ }
2483
+ return node;
2484
+ };
2485
+
2486
+ var notWSTextNode = function ( node ) {
2487
+ return node.nodeType === ELEMENT_NODE ?
2488
+ node.nodeName === 'BR' :
2489
+ notWS.test( node.data );
2490
+ };
2491
+ var isLineBreak = function ( br ) {
2492
+ var block = br.parentNode,
2493
+ walker;
2494
+ while ( isInline( block ) ) {
2495
+ block = block.parentNode;
2496
+ }
2497
+ walker = new TreeWalker(
2498
+ block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode );
2499
+ walker.currentNode = br;
2500
+ return !!walker.nextNode();
2501
+ };
2502
+
2503
+ // <br> elements are treated specially, and differently depending on the
2504
+ // browser, when in rich text editor mode. When adding HTML from external
2505
+ // sources, we must remove them, replacing the ones that actually affect
2506
+ // line breaks with a split of the block element containing it (and wrapping
2507
+ // any not inside a block). Browsers that want <br> elements at the end of
2508
+ // each block will then have them added back in a later fixCursor method
2509
+ // call.
2510
+ var cleanupBRs = function ( root ) {
2511
+ var brs = root.querySelectorAll( 'BR' ),
2512
+ brBreaksLine = [],
2513
+ l = brs.length,
2514
+ i, br, block;
2515
+
2516
+ // Must calculate whether the <br> breaks a line first, because if we
2517
+ // have two <br>s next to each other, after the first one is converted
2518
+ // to a block split, the second will be at the end of a block and
2519
+ // therefore seem to not be a line break. But in its original context it
2520
+ // was, so we should also convert it to a block split.
2521
+ for ( i = 0; i < l; i += 1 ) {
2522
+ brBreaksLine[i] = isLineBreak( brs[i] );
2523
+ }
2524
+ while ( l-- ) {
2525
+ br = brs[l];
2526
+ // Cleanup may have removed it
2527
+ block = br.parentNode;
2528
+ if ( !block ) { continue; }
2529
+ while ( isInline( block ) ) {
2530
+ block = block.parentNode;
2531
+ }
2532
+ // If this is not inside a block, replace it by wrapping
2533
+ // inlines in a <div>.
2534
+ if ( !isBlock( block ) ) {
2535
+ fixContainer( block );
2536
+ }
2537
+ else {
2538
+ // If it doesn't break a line, just remove it; it's not doing
2539
+ // anything useful. We'll add it back later if required by the
2540
+ // browser. If it breaks a line, split the block or leave it as
2541
+ // appropriate.
2542
+ if ( brBreaksLine[l] ) {
2543
+ // If in a <div>, split, but anywhere else we might change
2544
+ // the formatting too much (e.g. <li> -> to two list items!)
2545
+ // so just play it safe and leave it.
2546
+ if ( block.nodeName !== 'DIV' ) {
2547
+ continue;
2548
+ }
2549
+ split( br.parentNode, br, block.parentNode );
2550
+ }
2551
+ detach( br );
2552
+ }
2553
+ }
2554
+ };
2555
+
2556
+ proto._ensureBottomLine = function () {
2557
+ var body = this._body,
2558
+ last = body.lastChild;
2559
+ if ( !last || last.nodeName !== this.defaultBlockTag || !isBlock( last ) ) {
2560
+ body.appendChild( this.createDefaultBlock() );
2561
+ }
2562
+ };
2563
+
2564
+ // --- Cut and Paste ---
2565
+
2566
+ proto._onCut = function () {
2567
+ // Save undo checkpoint
2568
+ var range = this.getSelection();
2569
+ var self = this;
2570
+ this._recordUndoState( range );
2571
+ this._getRangeAndRemoveBookmark( range );
2572
+ this.setSelection( range );
2573
+ setTimeout( function () {
2574
+ try {
2575
+ // If all content removed, ensure div at start of body.
2576
+ self._ensureBottomLine();
2577
+ } catch ( error ) {
2578
+ self.didError( error );
2579
+ }
2580
+ }, 0 );
2581
+ };
2582
+
2583
+ proto._onPaste = function ( event ) {
2584
+ if ( this._awaitingPaste ) { return; }
2585
+
2586
+ // Treat image paste as a drop of an image file.
2587
+ var clipboardData = event.clipboardData,
2588
+ items = clipboardData && clipboardData.items,
2589
+ fireDrop = false,
2590
+ hasImage = false,
2591
+ l, type;
2592
+ if ( items ) {
2593
+ l = items.length;
2594
+ while ( l-- ) {
2595
+ type = items[l].type;
2596
+ if ( type === 'text/html' ) {
2597
+ hasImage = false;
2598
+ break;
2599
+ }
2600
+ if ( /^image\/.*/.test( type ) ) {
2601
+ hasImage = true;
2602
+ }
2603
+ }
2604
+ if ( hasImage ) {
2605
+ event.preventDefault();
2606
+ this.fireEvent( 'dragover', {
2607
+ dataTransfer: clipboardData,
2608
+ /*jshint loopfunc: true */
2609
+ preventDefault: function () {
2610
+ fireDrop = true;
2611
+ }
2612
+ /*jshint loopfunc: false */
2613
+ });
2614
+ if ( fireDrop ) {
2615
+ this.fireEvent( 'drop', {
2616
+ dataTransfer: clipboardData
2617
+ });
2618
+ }
2619
+ return;
2620
+ }
2621
+ }
2622
+
2623
+ this._awaitingPaste = true;
2624
+
2625
+ var self = this,
2626
+ body = this._body,
2627
+ range = this.getSelection(),
2628
+ startContainer, startOffset, endContainer, endOffset, startBlock;
2629
+
2630
+ // Record undo checkpoint
2631
+ self._recordUndoState( range );
2632
+ self._getRangeAndRemoveBookmark( range );
2633
+
2634
+ // Note current selection. We must do this AFTER recording the undo
2635
+ // checkpoint, as this modifies the DOM.
2636
+ startContainer = range.startContainer;
2637
+ startOffset = range.startOffset;
2638
+ endContainer = range.endContainer;
2639
+ endOffset = range.endOffset;
2640
+ startBlock = getStartBlockOfRange( range );
2641
+
2642
+ // We need to position the pasteArea in the visible portion of the screen
2643
+ // to stop the browser auto-scrolling.
2644
+ var pasteArea = this.createElement( 'DIV', {
2645
+ style: 'position: absolute; overflow: hidden; top:' +
2646
+ ( body.scrollTop +
2647
+ ( startBlock ? startBlock.getBoundingClientRect().top : 0 ) ) +
2648
+ 'px; left: 0; width: 1px; height: 1px;'
2649
+ });
2650
+ body.appendChild( pasteArea );
2651
+ range.selectNodeContents( pasteArea );
2652
+ this.setSelection( range );
2653
+
2654
+ // A setTimeout of 0 means this is added to the back of the
2655
+ // single javascript thread, so it will be executed after the
2656
+ // paste event.
2657
+ setTimeout( function () {
2658
+ try {
2659
+ // Get the pasted content and clean
2660
+ var frag = empty( detach( pasteArea ) ),
2661
+ first = frag.firstChild,
2662
+ range = self._createRange(
2663
+ startContainer, startOffset, endContainer, endOffset );
2664
+
2665
+ // Was anything actually pasted?
2666
+ if ( first ) {
2667
+ // Safari and IE like putting extra divs around things.
2668
+ if ( first === frag.lastChild &&
2669
+ first.nodeName === 'DIV' ) {
2670
+ frag.replaceChild( empty( first ), first );
2671
+ }
2672
+
2673
+ frag.normalize();
2674
+ addLinks( frag );
2675
+ cleanTree( frag, false );
2676
+ cleanupBRs( frag );
2677
+ removeEmptyInlines( frag );
2678
+
2679
+ var node = frag,
2680
+ doPaste = true;
2681
+ while ( node = getNextBlock( node ) ) {
2682
+ fixCursor( node );
2683
+ }
2684
+
2685
+ self.fireEvent( 'willPaste', {
2686
+ fragment: frag,
2687
+ preventDefault: function () {
2688
+ doPaste = false;
2689
+ }
2690
+ });
2691
+
2692
+ // Insert pasted data
2693
+ if ( doPaste ) {
2694
+ insertTreeFragmentIntoRange( range, frag );
2695
+ if ( !canObserveMutations ) {
2696
+ self._docWasChanged();
2697
+ }
2698
+ range.collapse( false );
2699
+ self._ensureBottomLine();
2700
+ }
2701
+ }
2702
+
2703
+ self.setSelection( range );
2704
+ self._updatePath( range, true );
2705
+
2706
+ self._awaitingPaste = false;
2707
+ } catch ( error ) {
2708
+ self.didError( error );
2709
+ }
2710
+ }, 0 );
2711
+ };
2712
+
2713
+ // --- Keyboard interaction ---
2714
+
2715
+ var keys = {
2716
+ 8: 'backspace',
2717
+ 9: 'tab',
2718
+ 13: 'enter',
2719
+ 32: 'space',
2720
+ 37: 'left',
2721
+ 39: 'right',
2722
+ 46: 'delete',
2723
+ 219: '[',
2724
+ 221: ']'
2725
+ };
2726
+
2727
+ var mapKeyTo = function ( method ) {
2728
+ return function ( self, event ) {
2729
+ event.preventDefault();
2730
+ self[ method ]();
2731
+ };
2732
+ };
2733
+
2734
+ var mapKeyToFormat = function ( tag, remove ) {
2735
+ remove = remove || null;
2736
+ return function ( self, event ) {
2737
+ event.preventDefault();
2738
+ var range = self.getSelection();
2739
+ if ( self.hasFormat( tag, null, range ) ) {
2740
+ self.changeFormat( null, { tag: tag }, range );
2741
+ } else {
2742
+ self.changeFormat( { tag: tag }, remove, range );
2743
+ }
2744
+ };
2745
+ };
2746
+
2747
+ // If you delete the content inside a span with a font styling, Webkit will
2748
+ // replace it with a <font> tag (!). If you delete all the text inside a
2749
+ // link in Opera, it won't delete the link. Let's make things consistent. If
2750
+ // you delete all text inside an inline tag, remove the inline tag.
2751
+ var afterDelete = function ( self, range ) {
2752
+ try {
2753
+ if ( !range ) { range = self.getSelection(); }
2754
+ var node = range.startContainer,
2755
+ parent;
2756
+ // Climb the tree from the focus point while we are inside an empty
2757
+ // inline element
2758
+ if ( node.nodeType === TEXT_NODE ) {
2759
+ node = node.parentNode;
2760
+ }
2761
+ parent = node;
2762
+ while ( isInline( parent ) &&
2763
+ ( !parent.textContent || parent.textContent === ZWS ) ) {
2764
+ node = parent;
2765
+ parent = node.parentNode;
2766
+ }
2767
+ // If focussed in empty inline element
2768
+ if ( node !== parent ) {
2769
+ // Move focus to just before empty inline(s)
2770
+ range.setStart( parent,
2771
+ indexOf.call( parent.childNodes, node ) );
2772
+ range.collapse( true );
2773
+ // Remove empty inline(s)
2774
+ parent.removeChild( node );
2775
+ // Fix cursor in block
2776
+ if ( !isBlock( parent ) ) {
2777
+ parent = getPreviousBlock( parent );
2778
+ }
2779
+ fixCursor( parent );
2780
+ // Move cursor into text node
2781
+ moveRangeBoundariesDownTree( range );
2782
+ }
2783
+ self._ensureBottomLine();
2784
+ self.setSelection( range );
2785
+ self._updatePath( range, true );
2786
+ } catch ( error ) {
2787
+ self.didError( error );
2788
+ }
2789
+ };
2790
+
2791
+ var keyHandlers = {
2792
+ enter: function ( self, event, range ) {
2793
+ var block, parent, nodeAfterSplit;
2794
+
2795
+ // We handle this ourselves
2796
+ event.preventDefault();
2797
+
2798
+ // Save undo checkpoint and add any links in the preceding section.
2799
+ // Remove any zws so we don't think there's content in an empty
2800
+ // block.
2801
+ self._recordUndoState( range );
2802
+ addLinks( range.startContainer );
2803
+ self._removeZWS();
2804
+ self._getRangeAndRemoveBookmark( range );
2805
+
2806
+ // Selected text is overwritten, therefore delete the contents
2807
+ // to collapse selection.
2808
+ if ( !range.collapsed ) {
2809
+ deleteContentsOfRange( range );
2810
+ }
2811
+
2812
+ block = getStartBlockOfRange( range );
2813
+
2814
+ // If this is a malformed bit of document or in a table;
2815
+ // just play it safe and insert a <br>.
2816
+ if ( !block || /^T[HD]$/.test( block.nodeName ) ) {
2817
+ insertNodeInRange( range, self.createElement( 'BR' ) );
2818
+ range.collapse( false );
2819
+ self.setSelection( range );
2820
+ self._updatePath( range, true );
2821
+ return;
2822
+ }
2823
+
2824
+ // If in a list, we'll split the LI instead.
2825
+ if ( parent = getNearest( block, 'LI' ) ) {
2826
+ block = parent;
2827
+ }
2828
+
2829
+ if ( !block.textContent ) {
2830
+ // Break list
2831
+ if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) {
2832
+ return self.modifyBlocks( decreaseListLevel, range );
2833
+ }
2834
+ // Break blockquote
2835
+ else if ( getNearest( block, 'BLOCKQUOTE' ) ) {
2836
+ return self.modifyBlocks( removeBlockQuote, range );
2837
+ }
2838
+ }
2839
+
2840
+ // Otherwise, split at cursor point.
2841
+ nodeAfterSplit = splitBlock( self, block,
2842
+ range.startContainer, range.startOffset );
2843
+
2844
+ // Clean up any empty inlines if we hit enter at the beginning of the
2845
+ // block
2846
+ removeZWS( block );
2847
+ removeEmptyInlines( block );
2848
+ fixCursor( block );
2849
+
2850
+ // Focus cursor
2851
+ // If there's a <b>/<i> etc. at the beginning of the split
2852
+ // make sure we focus inside it.
2853
+ while ( nodeAfterSplit.nodeType === ELEMENT_NODE ) {
2854
+ var child = nodeAfterSplit.firstChild,
2855
+ next;
2856
+
2857
+ // Don't continue links over a block break; unlikely to be the
2858
+ // desired outcome.
2859
+ if ( nodeAfterSplit.nodeName === 'A' &&
2860
+ !nodeAfterSplit.textContent ) {
2861
+ replaceWith( nodeAfterSplit, empty( nodeAfterSplit ) );
2862
+ nodeAfterSplit = child;
2863
+ continue;
2864
+ }
2865
+
2866
+ while ( child && child.nodeType === TEXT_NODE && !child.data ) {
2867
+ next = child.nextSibling;
2868
+ if ( !next || next.nodeName === 'BR' ) {
2869
+ break;
2870
+ }
2871
+ detach( child );
2872
+ child = next;
2873
+ }
2874
+
2875
+ // 'BR's essentially don't count; they're a browser hack.
2876
+ // If you try to select the contents of a 'BR', FF will not let
2877
+ // you type anything!
2878
+ if ( !child || child.nodeName === 'BR' ||
2879
+ ( child.nodeType === TEXT_NODE && !isPresto ) ) {
2880
+ break;
2881
+ }
2882
+ nodeAfterSplit = child;
2883
+ }
2884
+ range = self._createRange( nodeAfterSplit, 0 );
2885
+ self.setSelection( range );
2886
+ self._updatePath( range, true );
2887
+
2888
+ // Scroll into view
2889
+ if ( nodeAfterSplit.nodeType === TEXT_NODE ) {
2890
+ nodeAfterSplit = nodeAfterSplit.parentNode;
2891
+ }
2892
+ var doc = self._doc,
2893
+ body = self._body;
2894
+ if ( nodeAfterSplit.offsetTop + nodeAfterSplit.offsetHeight >
2895
+ ( doc.documentElement.scrollTop || body.scrollTop ) +
2896
+ body.offsetHeight ) {
2897
+ nodeAfterSplit.scrollIntoView( false );
2898
+ }
2899
+ },
2900
+ backspace: function ( self, event, range ) {
2901
+ self._removeZWS();
2902
+ // Record undo checkpoint.
2903
+ self._recordUndoState( range );
2904
+ self._getRangeAndRemoveBookmark( range );
2905
+ // If not collapsed, delete contents
2906
+ if ( !range.collapsed ) {
2907
+ event.preventDefault();
2908
+ deleteContentsOfRange( range );
2909
+ afterDelete( self, range );
2910
+ }
2911
+ // If at beginning of block, merge with previous
2912
+ else if ( rangeDoesStartAtBlockBoundary( range ) ) {
2913
+ event.preventDefault();
2914
+ var current = getStartBlockOfRange( range ),
2915
+ previous = current && getPreviousBlock( current );
2916
+ // Must not be at the very beginning of the text area.
2917
+ if ( previous ) {
2918
+ // If not editable, just delete whole block.
2919
+ if ( !previous.isContentEditable ) {
2920
+ detach( previous );
2921
+ return;
2922
+ }
2923
+ // Otherwise merge.
2924
+ mergeWithBlock( previous, current, range );
2925
+ // If deleted line between containers, merge newly adjacent
2926
+ // containers.
2927
+ current = previous.parentNode;
2928
+ while ( current && !current.nextSibling ) {
2929
+ current = current.parentNode;
2930
+ }
2931
+ if ( current && ( current = current.nextSibling ) ) {
2932
+ mergeContainers( current );
2933
+ }
2934
+ self.setSelection( range );
2935
+ }
2936
+ // If at very beginning of text area, allow backspace
2937
+ // to break lists/blockquote.
2938
+ else if ( current ) {
2939
+ // Break list
2940
+ if ( getNearest( current, 'UL' ) ||
2941
+ getNearest( current, 'OL' ) ) {
2942
+ return self.modifyBlocks( decreaseListLevel, range );
2943
+ }
2944
+ // Break blockquote
2945
+ else if ( getNearest( current, 'BLOCKQUOTE' ) ) {
2946
+ return self.modifyBlocks( decreaseBlockQuoteLevel, range );
2947
+ }
2948
+ self.setSelection( range );
2949
+ self._updatePath( range, true );
2950
+ }
2951
+ }
2952
+ // Otherwise, leave to browser but check afterwards whether it has
2953
+ // left behind an empty inline tag.
2954
+ else {
2955
+ self.setSelection( range );
2956
+ setTimeout( function () { afterDelete( self ); }, 0 );
2957
+ }
2958
+ },
2959
+ 'delete': function ( self, event, range ) {
2960
+ self._removeZWS();
2961
+ // Record undo checkpoint.
2962
+ self._recordUndoState( range );
2963
+ self._getRangeAndRemoveBookmark( range );
2964
+ // If not collapsed, delete contents
2965
+ if ( !range.collapsed ) {
2966
+ event.preventDefault();
2967
+ deleteContentsOfRange( range );
2968
+ afterDelete( self, range );
2969
+ }
2970
+ // If at end of block, merge next into this block
2971
+ else if ( rangeDoesEndAtBlockBoundary( range ) ) {
2972
+ event.preventDefault();
2973
+ var current = getStartBlockOfRange( range ),
2974
+ next = current && getNextBlock( current );
2975
+ // Must not be at the very end of the text area.
2976
+ if ( next ) {
2977
+ // If not editable, just delete whole block.
2978
+ if ( !next.isContentEditable ) {
2979
+ detach( next );
2980
+ return;
2981
+ }
2982
+ // Otherwise merge.
2983
+ mergeWithBlock( current, next, range );
2984
+ // If deleted line between containers, merge newly adjacent
2985
+ // containers.
2986
+ next = current.parentNode;
2987
+ while ( next && !next.nextSibling ) {
2988
+ next = next.parentNode;
2989
+ }
2990
+ if ( next && ( next = next.nextSibling ) ) {
2991
+ mergeContainers( next );
2992
+ }
2993
+ self.setSelection( range );
2994
+ self._updatePath( range, true );
2995
+ }
2996
+ }
2997
+ // Otherwise, leave to browser but check afterwards whether it has
2998
+ // left behind an empty inline tag.
2999
+ else {
3000
+ self.setSelection( range );
3001
+ setTimeout( function () { afterDelete( self ); }, 0 );
3002
+ }
3003
+ },
3004
+ tab: function ( self, event, range ) {
3005
+ var node, parent;
3006
+ self._removeZWS();
3007
+ // If no selection and in an empty block
3008
+ if ( range.collapsed &&
3009
+ rangeDoesStartAtBlockBoundary( range ) &&
3010
+ rangeDoesEndAtBlockBoundary( range ) ) {
3011
+ node = getStartBlockOfRange( range );
3012
+ // Iterate through the block's parents
3013
+ while ( parent = node.parentNode ) {
3014
+ // If we find a UL or OL (so are in a list, node must be an LI)
3015
+ if ( parent.nodeName === 'UL' || parent.nodeName === 'OL' ) {
3016
+ // AND the LI is not the first in the list
3017
+ if ( node.previousSibling ) {
3018
+ // Then increase the list level
3019
+ event.preventDefault();
3020
+ self.modifyBlocks( increaseListLevel, range );
3021
+ }
3022
+ break;
3023
+ }
3024
+ node = parent;
3025
+ }
3026
+ event.preventDefault();
3027
+ }
3028
+ },
3029
+ space: function ( self, _, range ) {
3030
+ var node, parent;
3031
+ self._recordUndoState( range );
3032
+ addLinks( range.startContainer );
3033
+ self._getRangeAndRemoveBookmark( range );
3034
+
3035
+ // If the cursor is at the end of a link (<a>foo|</a>) then move it
3036
+ // outside of the link (<a>foo</a>|) so that the space is not part of
3037
+ // the link text.
3038
+ node = range.endContainer;
3039
+ parent = node.parentNode;
3040
+ if ( range.collapsed && parent.nodeName === 'A' &&
3041
+ !node.nextSibling && range.endOffset === getLength( node ) ) {
3042
+ range.setStartAfter( parent );
3043
+ }
3044
+
3045
+ self.setSelection( range );
3046
+ },
3047
+ left: function ( self ) {
3048
+ self._removeZWS();
3049
+ },
3050
+ right: function ( self ) {
3051
+ self._removeZWS();
3052
+ }
3053
+ };
3054
+
3055
+ // Firefox incorrectly handles Cmd-left/Cmd-right on Mac:
3056
+ // it goes back/forward in history! Override to do the right
3057
+ // thing.
3058
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=289384
3059
+ if ( isMac && isGecko && win.getSelection().modify ) {
3060
+ keyHandlers[ 'meta-left' ] = function ( self, event ) {
3061
+ event.preventDefault();
3062
+ self._sel.modify( 'move', 'backward', 'lineboundary' );
3063
+ };
3064
+ keyHandlers[ 'meta-right' ] = function ( self, event ) {
3065
+ event.preventDefault();
3066
+ self._sel.modify( 'move', 'forward', 'lineboundary' );
3067
+ };
3068
+ }
3069
+
3070
+ keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' );
3071
+ keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' );
3072
+ keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' );
3073
+ keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' );
3074
+ keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } );
3075
+ keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } );
3076
+ keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' );
3077
+ keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' );
3078
+ keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
3079
+ keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
3080
+ keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
3081
+ keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
3082
+ keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );
3083
+
3084
+ // Ref: http://unixpapa.com/js/key.html
3085
+ proto._onKey = function ( event ) {
3086
+ var code = event.keyCode,
3087
+ key = keys[ code ],
3088
+ modifiers = '',
3089
+ range = this.getSelection();
3090
+
3091
+ if ( !key ) {
3092
+ key = String.fromCharCode( code ).toLowerCase();
3093
+ // Only reliable for letters and numbers
3094
+ if ( !/^[A-Za-z0-9]$/.test( key ) ) {
3095
+ key = '';
3096
+ }
3097
+ }
3098
+
3099
+ // On keypress, delete and '.' both have event.keyCode 46
3100
+ // Must check event.which to differentiate.
3101
+ if ( isPresto && event.which === 46 ) {
3102
+ key = '.';
3103
+ }
3104
+
3105
+ // Function keys
3106
+ if ( 111 < code && code < 124 ) {
3107
+ key = 'f' + ( code - 111 );
3108
+ }
3109
+
3110
+ // We need to apply the backspace/delete handlers regardless of
3111
+ // control key modifiers.
3112
+ if ( key !== 'backspace' && key !== 'delete' ) {
3113
+ if ( event.altKey ) { modifiers += 'alt-'; }
3114
+ if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
3115
+ if ( event.metaKey ) { modifiers += 'meta-'; }
3116
+ }
3117
+ // However, on Windows, shift-delete is apparently "cut" (WTF right?), so
3118
+ // we want to let the browser handle shift-delete.
3119
+ if ( event.shiftKey ) { modifiers += 'shift-'; }
3120
+
3121
+ key = modifiers + key;
3122
+
3123
+ if ( keyHandlers[ key ] ) {
3124
+ keyHandlers[ key ]( this, event, range );
3125
+ } else if ( key.length === 1 && !range.collapsed ) {
3126
+ // Record undo checkpoint.
3127
+ this._recordUndoState( range );
3128
+ this._getRangeAndRemoveBookmark( range );
3129
+ // Delete the selection
3130
+ deleteContentsOfRange( range );
3131
+ this._ensureBottomLine();
3132
+ this.setSelection( range );
3133
+ this._updatePath( range, true );
3134
+ }
3135
+ };
3136
+
3137
+ // --- Get/Set data ---
3138
+
3139
+ proto._getHTML = function () {
3140
+ return this._body.innerHTML;
3141
+ };
3142
+
3143
+ proto._setHTML = function ( html ) {
3144
+ var node = this._body;
3145
+ node.innerHTML = html;
3146
+ do {
3147
+ fixCursor( node );
3148
+ } while ( node = getNextBlock( node ) );
3149
+ this._ignoreChange = true;
3150
+ };
3151
+
3152
+ proto.getHTML = function ( withBookMark ) {
3153
+ var brs = [],
3154
+ node, fixer, html, l, range;
3155
+ if ( withBookMark && ( range = this.getSelection() ) ) {
3156
+ this._saveRangeToBookmark( range );
3157
+ }
3158
+ if ( useTextFixer ) {
3159
+ node = this._body;
3160
+ while ( node = getNextBlock( node ) ) {
3161
+ if ( !node.textContent && !node.querySelector( 'BR' ) ) {
3162
+ fixer = this.createElement( 'BR' );
3163
+ node.appendChild( fixer );
3164
+ brs.push( fixer );
3165
+ }
3166
+ }
3167
+ }
3168
+ html = this._getHTML().replace( /\u200B/g, '' );
3169
+ if ( useTextFixer ) {
3170
+ l = brs.length;
3171
+ while ( l-- ) {
3172
+ detach( brs[l] );
3173
+ }
3174
+ }
3175
+ if ( range ) {
3176
+ this._getRangeAndRemoveBookmark( range );
3177
+ }
3178
+ return html;
3179
+ };
3180
+
3181
+ proto.setHTML = function ( html ) {
3182
+ var frag = this._doc.createDocumentFragment(),
3183
+ div = this.createElement( 'DIV' ),
3184
+ child;
3185
+
3186
+ // Parse HTML into DOM tree
3187
+ div.innerHTML = html;
3188
+ frag.appendChild( empty( div ) );
3189
+
3190
+ cleanTree( frag, true );
3191
+ cleanupBRs( frag );
3192
+
3193
+ fixContainer( frag );
3194
+
3195
+ // Fix cursor
3196
+ var node = frag;
3197
+ while ( node = getNextBlock( node ) ) {
3198
+ fixCursor( node );
3199
+ }
3200
+
3201
+ // Don't fire an input event
3202
+ this._ignoreChange = true;
3203
+
3204
+ // Remove existing body children
3205
+ var body = this._body;
3206
+ while ( child = body.lastChild ) {
3207
+ body.removeChild( child );
3208
+ }
3209
+
3210
+ // And insert new content
3211
+ body.appendChild( frag );
3212
+ fixCursor( body );
3213
+
3214
+ // Reset the undo stack
3215
+ this._undoIndex = -1;
3216
+ this._undoStack.length = 0;
3217
+ this._undoStackLength = 0;
3218
+ this._isInUndoState = false;
3219
+
3220
+ // Record undo state
3221
+ var range = this._getRangeAndRemoveBookmark() ||
3222
+ this._createRange( body.firstChild, 0 );
3223
+ this._recordUndoState( range );
3224
+ this._getRangeAndRemoveBookmark( range );
3225
+ // IE will also set focus when selecting text so don't use
3226
+ // setSelection. Instead, just store it in lastSelection, so if
3227
+ // anything calls getSelection before first focus, we have a range
3228
+ // to return.
3229
+ if ( losesSelectionOnBlur ) {
3230
+ this._lastSelection = range;
3231
+ } else {
3232
+ this.setSelection( range );
3233
+ }
3234
+ this._updatePath( range, true );
3235
+
3236
+ return this;
3237
+ };
3238
+
3239
+ proto.insertElement = function ( el, range ) {
3240
+ if ( !range ) { range = this.getSelection(); }
3241
+ range.collapse( true );
3242
+ if ( isInline( el ) ) {
3243
+ insertNodeInRange( range, el );
3244
+ range.setStartAfter( el );
3245
+ } else {
3246
+ // Get containing block node.
3247
+ var body = this._body,
3248
+ splitNode = getStartBlockOfRange( range ) || body,
3249
+ parent, nodeAfterSplit;
3250
+ // While at end of container node, move up DOM tree.
3251
+ while ( splitNode !== body && !splitNode.nextSibling ) {
3252
+ splitNode = splitNode.parentNode;
3253
+ }
3254
+ // If in the middle of a container node, split up to body.
3255
+ if ( splitNode !== body ) {
3256
+ parent = splitNode.parentNode;
3257
+ nodeAfterSplit = split( parent, splitNode.nextSibling, body );
3258
+ }
3259
+ if ( nodeAfterSplit ) {
3260
+ body.insertBefore( el, nodeAfterSplit );
3261
+ range.setStart( nodeAfterSplit, 0 );
3262
+ range.setStart( nodeAfterSplit, 0 );
3263
+ moveRangeBoundariesDownTree( range );
3264
+ } else {
3265
+ body.appendChild( el );
3266
+ // Insert blank line below block.
3267
+ body.appendChild( this.createDefaultBlock() );
3268
+ range.setStart( el, 0 );
3269
+ range.setEnd( el, 0 );
3270
+ }
3271
+ this.focus();
3272
+ this.setSelection( range );
3273
+ this._updatePath( range );
3274
+ }
3275
+ return this;
3276
+ };
3277
+
3278
+ proto.insertImage = function ( src ) {
3279
+ var img = this.createElement( 'IMG', {
3280
+ src: src
3281
+ });
3282
+ this.insertElement( img );
3283
+ return img;
3284
+ };
3285
+
3286
+ // --- Formatting ---
3287
+
3288
+ var command = function ( method, arg, arg2 ) {
3289
+ return function () {
3290
+ this[ method ]( arg, arg2 );
3291
+ return this.focus();
3292
+ };
3293
+ };
3294
+
3295
+ proto.addStyles = function ( styles ) {
3296
+ if ( styles ) {
3297
+ var head = this._doc.documentElement.firstChild,
3298
+ style = this.createElement( 'STYLE', {
3299
+ type: 'text/css'
3300
+ });
3301
+ if ( style.styleSheet ) {
3302
+ // IE8: must append to document BEFORE adding styles
3303
+ // or you get the IE7 CSS parser!
3304
+ head.appendChild( style );
3305
+ style.styleSheet.cssText = styles;
3306
+ } else {
3307
+ // Everyone else
3308
+ style.appendChild( this._doc.createTextNode( styles ) );
3309
+ head.appendChild( style );
3310
+ }
3311
+ }
3312
+ return this;
3313
+ };
3314
+
3315
+ proto.bold = command( 'changeFormat', { tag: 'B' } );
3316
+ proto.italic = command( 'changeFormat', { tag: 'I' } );
3317
+ proto.underline = command( 'changeFormat', { tag: 'U' } );
3318
+ proto.strikethrough = command( 'changeFormat', { tag: 'S' } );
3319
+ proto.subscript = command( 'changeFormat', { tag: 'SUB' }, { tag: 'SUP' } );
3320
+ proto.superscript = command( 'changeFormat', { tag: 'SUP' }, { tag: 'SUB' } );
3321
+
3322
+ proto.removeBold = command( 'changeFormat', null, { tag: 'B' } );
3323
+ proto.removeItalic = command( 'changeFormat', null, { tag: 'I' } );
3324
+ proto.removeUnderline = command( 'changeFormat', null, { tag: 'U' } );
3325
+ proto.removeStrikethrough = command( 'changeFormat', null, { tag: 'S' } );
3326
+ proto.removeSubscript = command( 'changeFormat', null, { tag: 'SUB' } );
3327
+ proto.removeSuperscript = command( 'changeFormat', null, { tag: 'SUP' } );
3328
+
3329
+ proto.makeLink = function ( url, attributes ) {
3330
+ var range = this.getSelection();
3331
+ if ( range.collapsed ) {
3332
+ var protocolEnd = url.indexOf( ':' ) + 1;
3333
+ if ( protocolEnd ) {
3334
+ while ( url[ protocolEnd ] === '/' ) { protocolEnd += 1; }
3335
+ }
3336
+ insertNodeInRange(
3337
+ range,
3338
+ this._doc.createTextNode( url.slice( protocolEnd ) )
3339
+ );
3340
+ }
3341
+
3342
+ if ( !attributes ) {
3343
+ attributes = {};
3344
+ }
3345
+ attributes.href = url;
3346
+
3347
+ this.changeFormat({
3348
+ tag: 'A',
3349
+ attributes: attributes
3350
+ }, {
3351
+ tag: 'A'
3352
+ }, range );
3353
+ return this.focus();
3354
+ };
3355
+ proto.removeLink = function () {
3356
+ this.changeFormat( null, {
3357
+ tag: 'A'
3358
+ }, this.getSelection(), true );
3359
+ return this.focus();
3360
+ };
3361
+
3362
+ proto.setFontFace = function ( name ) {
3363
+ this.changeFormat({
3364
+ tag: 'SPAN',
3365
+ attributes: {
3366
+ 'class': 'font',
3367
+ style: 'font-family: ' + name + ', sans-serif;'
3368
+ }
3369
+ }, {
3370
+ tag: 'SPAN',
3371
+ attributes: { 'class': 'font' }
3372
+ });
3373
+ return this.focus();
3374
+ };
3375
+ proto.setFontSize = function ( size ) {
3376
+ this.changeFormat({
3377
+ tag: 'SPAN',
3378
+ attributes: {
3379
+ 'class': 'size',
3380
+ style: 'font-size: ' +
3381
+ ( typeof size === 'number' ? size + 'px' : size )
3382
+ }
3383
+ }, {
3384
+ tag: 'SPAN',
3385
+ attributes: { 'class': 'size' }
3386
+ });
3387
+ return this.focus();
3388
+ };
3389
+
3390
+ proto.setTextColour = function ( colour ) {
3391
+ this.changeFormat({
3392
+ tag: 'SPAN',
3393
+ attributes: {
3394
+ 'class': 'colour',
3395
+ style: 'color: ' + colour
3396
+ }
3397
+ }, {
3398
+ tag: 'SPAN',
3399
+ attributes: { 'class': 'colour' }
3400
+ });
3401
+ return this.focus();
3402
+ };
3403
+
3404
+ proto.setHighlightColour = function ( colour ) {
3405
+ this.changeFormat({
3406
+ tag: 'SPAN',
3407
+ attributes: {
3408
+ 'class': 'highlight',
3409
+ style: 'background-color: ' + colour
3410
+ }
3411
+ }, {
3412
+ tag: 'SPAN',
3413
+ attributes: { 'class': 'highlight' }
3414
+ });
3415
+ return this.focus();
3416
+ };
3417
+
3418
+ proto.setTextAlignment = function ( alignment ) {
3419
+ this.forEachBlock( function ( block ) {
3420
+ block.className = ( block.className
3421
+ .split( /\s+/ )
3422
+ .filter( function ( klass ) {
3423
+ return !( /align/.test( klass ) );
3424
+ })
3425
+ .join( ' ' ) +
3426
+ ' align-' + alignment ).trim();
3427
+ block.style.textAlign = alignment;
3428
+ }, true );
3429
+ return this.focus();
3430
+ };
3431
+
3432
+ proto.setTextDirection = function ( direction ) {
3433
+ this.forEachBlock( function ( block ) {
3434
+ block.className = ( block.className
3435
+ .split( /\s+/ )
3436
+ .filter( function ( klass ) {
3437
+ return !( /dir/.test( klass ) );
3438
+ })
3439
+ .join( ' ' ) +
3440
+ ' dir-' + direction ).trim();
3441
+ block.dir = direction;
3442
+ }, true );
3443
+ return this.focus();
3444
+ };
3445
+
3446
+ proto.increaseQuoteLevel = command( 'modifyBlocks', increaseBlockQuoteLevel );
3447
+ proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
3448
+
3449
+ proto.makeUnorderedList = command( 'modifyBlocks', makeUnorderedList );
3450
+ proto.makeOrderedList = command( 'modifyBlocks', makeOrderedList );
3451
+ proto.removeList = command( 'modifyBlocks', removeList );
3452
+
3453
+ proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel );
3454
+ proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel );
3455
+
3456
+ if ( top !== win ) {
3457
+ win.editor = new Squire( doc );
3458
+ if ( win.onEditorLoad ) {
3459
+ win.onEditorLoad( win.editor );
3460
+ win.onEditorLoad = null;
3461
+ }
3462
+ } else {
3463
+ win.Squire = Squire;
3464
+ }
3465
+
3466
+ }( document ) );