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 +4 -4
- data/README.md +149 -0
- data/lib/squire-rails/version.rb +1 -1
- data/vendor/assets/javascripts/squire/squire-raw.js +3466 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 398337563890132e5830d98979f048918d241a65
|
4
|
+
data.tar.gz: 369080a22ada52156865c0761f168479f4a0ff49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
|
70
|
+
<span id="removeItalic">Unitalic</span>
|
71
|
+
<span id="underline">Underline</span>
|
72
|
+
<span id="removeUnderline">Deunderline</span>
|
73
|
+
|
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
|
+
|
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
|
+
|
91
|
+
<span id="insertImage" class="prompt">Insert image</span>
|
92
|
+
<span id="setHTML" class="prompt">Set HTML</span>
|
93
|
+
|
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
|
+
```
|
data/lib/squire-rails/version.rb
CHANGED
@@ -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 ) );
|