rangy-rails 1.3alpha.772.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +44 -0
- data/Rakefile +1 -0
- data/lib/rangy-rails.rb +12 -0
- data/lib/rangy-rails/version.rb +5 -0
- data/rangy-rails.gemspec +21 -0
- data/vendor/assets/javascripts/rangy-core.js +3644 -0
- data/vendor/assets/javascripts/rangy-cssclassapplier.js +935 -0
- data/vendor/assets/javascripts/rangy-highlighter.js +496 -0
- data/vendor/assets/javascripts/rangy-selectionsaverestore.js +237 -0
- data/vendor/assets/javascripts/rangy-serializer.js +294 -0
- data/vendor/assets/javascripts/rangy-textrange.js +1896 -0
- metadata +61 -0
@@ -0,0 +1,1896 @@
|
|
1
|
+
/**
|
2
|
+
* Text range module for Rangy.
|
3
|
+
* Text-based manipulation and searching of ranges and selections.
|
4
|
+
*
|
5
|
+
* Features
|
6
|
+
*
|
7
|
+
* - Ability to move range boundaries by character or word offsets
|
8
|
+
* - Customizable word tokenizer
|
9
|
+
* - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties
|
10
|
+
* - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case
|
11
|
+
* sensitivity
|
12
|
+
* - Selection and range save/restore as text offsets within a node
|
13
|
+
* - Methods to return visible text within a range or selection
|
14
|
+
* - innerText method for elements
|
15
|
+
*
|
16
|
+
* References
|
17
|
+
*
|
18
|
+
* https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145
|
19
|
+
* http://aryeh.name/spec/innertext/innertext.html
|
20
|
+
* http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html
|
21
|
+
*
|
22
|
+
* Part of Rangy, a cross-browser JavaScript range and selection library
|
23
|
+
* http://code.google.com/p/rangy/
|
24
|
+
*
|
25
|
+
* Depends on Rangy core.
|
26
|
+
*
|
27
|
+
* Copyright 2013, Tim Down
|
28
|
+
* Licensed under the MIT license.
|
29
|
+
* Version: 1.3alpha.772
|
30
|
+
* Build date: 26 February 2013
|
31
|
+
*/
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers.
|
35
|
+
*
|
36
|
+
* First, a <br>: this is relatively simple. For the following HTML:
|
37
|
+
*
|
38
|
+
* 1 <br>2
|
39
|
+
*
|
40
|
+
* - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a
|
41
|
+
* textarea, the space is present) and allow the caret to be placed after it.
|
42
|
+
* - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it.
|
43
|
+
* - Opera does not render the space but has two separate caret positions on either side of the space (left and right
|
44
|
+
* arrow keys show this) and includes the space in the selection.
|
45
|
+
*
|
46
|
+
* The other case is the line break or breaks implied by block elements. For the following HTML:
|
47
|
+
*
|
48
|
+
* <p>1 </p><p>2<p>
|
49
|
+
*
|
50
|
+
* - WebKit does not acknowledge the space in any way
|
51
|
+
* - Firefox, IE and Opera as per <br>
|
52
|
+
*
|
53
|
+
* One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML:
|
54
|
+
*
|
55
|
+
* <p style="white-space: pre-line">1
|
56
|
+
* 2</p>
|
57
|
+
*
|
58
|
+
* - Firefox and WebKit include the space in caret positions
|
59
|
+
* - IE does not support pre-line up to and including version 9
|
60
|
+
* - Opera ignores the space
|
61
|
+
* - Trailing space only renders if there is a non-collapsed character in the line
|
62
|
+
*
|
63
|
+
* Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be
|
64
|
+
* feature-tested
|
65
|
+
*/
|
66
|
+
rangy.createModule("TextRange", function(api, module) {
|
67
|
+
api.requireModules( ["WrappedSelection"] );
|
68
|
+
|
69
|
+
var UNDEF = "undefined";
|
70
|
+
var CHARACTER = "character", WORD = "word";
|
71
|
+
var dom = api.dom, util = api.util;
|
72
|
+
var extend = util.extend;
|
73
|
+
var getBody = dom.getBody;
|
74
|
+
|
75
|
+
|
76
|
+
var spacesRegex = /^[ \t\f\r\n]+$/;
|
77
|
+
var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/;
|
78
|
+
var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/;
|
79
|
+
var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/;
|
80
|
+
var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/;
|
81
|
+
|
82
|
+
var defaultLanguage = "en";
|
83
|
+
|
84
|
+
var isDirectionBackward = api.Selection.isDirectionBackward;
|
85
|
+
|
86
|
+
// Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit,
|
87
|
+
// but not other browsers). Also test whether trailing spaces before <br> elements are collapsed.
|
88
|
+
var trailingSpaceInBlockCollapses = false;
|
89
|
+
var trailingSpaceBeforeBrCollapses = false;
|
90
|
+
var trailingSpaceBeforeLineBreakInPreLineCollapses = true;
|
91
|
+
|
92
|
+
(function() {
|
93
|
+
var el = document.createElement("div");
|
94
|
+
el.contentEditable = "true";
|
95
|
+
el.innerHTML = "<p>1 </p><p></p>";
|
96
|
+
var body = getBody(document);
|
97
|
+
var p = el.firstChild;
|
98
|
+
var sel = api.getSelection();
|
99
|
+
|
100
|
+
body.appendChild(el);
|
101
|
+
sel.collapse(p.lastChild, 2);
|
102
|
+
sel.setStart(p.firstChild, 0);
|
103
|
+
trailingSpaceInBlockCollapses = ("" + sel).length == 1;
|
104
|
+
|
105
|
+
el.innerHTML = "1 <br>";
|
106
|
+
sel.collapse(el, 2);
|
107
|
+
sel.setStart(el.firstChild, 0);
|
108
|
+
trailingSpaceBeforeBrCollapses = ("" + sel).length == 1;
|
109
|
+
body.removeChild(el);
|
110
|
+
|
111
|
+
sel.removeAllRanges();
|
112
|
+
})();
|
113
|
+
|
114
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
115
|
+
|
116
|
+
// This function must create word and non-word tokens for the whole of the text supplied to it
|
117
|
+
function defaultTokenizer(chars, wordOptions) {
|
118
|
+
var word = chars.join(""), result, tokens = [];
|
119
|
+
|
120
|
+
function createTokenFromRange(start, end, isWord) {
|
121
|
+
var tokenChars = chars.slice(start, end);
|
122
|
+
var token = {
|
123
|
+
isWord: isWord,
|
124
|
+
chars: tokenChars,
|
125
|
+
toString: function() {
|
126
|
+
return tokenChars.join("");
|
127
|
+
}
|
128
|
+
};
|
129
|
+
for (var i = 0, len = tokenChars.length; i < len; ++i) {
|
130
|
+
tokenChars[i].token = token;
|
131
|
+
}
|
132
|
+
tokens.push(token);
|
133
|
+
}
|
134
|
+
|
135
|
+
// Match words and mark characters
|
136
|
+
var lastWordEnd = 0, wordStart, wordEnd;
|
137
|
+
while ( (result = wordOptions.wordRegex.exec(word)) ) {
|
138
|
+
wordStart = result.index;
|
139
|
+
wordEnd = wordStart + result[0].length;
|
140
|
+
|
141
|
+
// Create token for non-word characters preceding this word
|
142
|
+
if (wordStart > lastWordEnd) {
|
143
|
+
createTokenFromRange(lastWordEnd, wordStart, false);
|
144
|
+
}
|
145
|
+
|
146
|
+
// Get trailing space characters for word
|
147
|
+
if (wordOptions.includeTrailingSpace) {
|
148
|
+
while (nonLineBreakWhiteSpaceRegex.test(chars[wordEnd])) {
|
149
|
+
++wordEnd;
|
150
|
+
}
|
151
|
+
}
|
152
|
+
createTokenFromRange(wordStart, wordEnd, true);
|
153
|
+
lastWordEnd = wordEnd;
|
154
|
+
}
|
155
|
+
|
156
|
+
// Create token for trailing non-word characters, if any exist
|
157
|
+
if (lastWordEnd < chars.length) {
|
158
|
+
createTokenFromRange(lastWordEnd, chars.length, false);
|
159
|
+
}
|
160
|
+
|
161
|
+
return tokens;
|
162
|
+
}
|
163
|
+
|
164
|
+
var defaultCharacterOptions = {
|
165
|
+
includeBlockContentTrailingSpace: true,
|
166
|
+
includeSpaceBeforeBr: true,
|
167
|
+
includePreLineTrailingSpace: true
|
168
|
+
};
|
169
|
+
|
170
|
+
var defaultCaretCharacterOptions = {
|
171
|
+
includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses,
|
172
|
+
includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses,
|
173
|
+
includePreLineTrailingSpace: true
|
174
|
+
};
|
175
|
+
|
176
|
+
var defaultWordOptions = {
|
177
|
+
"en": {
|
178
|
+
wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi,
|
179
|
+
includeTrailingSpace: false,
|
180
|
+
tokenizer: defaultTokenizer
|
181
|
+
}
|
182
|
+
};
|
183
|
+
|
184
|
+
function createOptions(optionsParam, defaults) {
|
185
|
+
if (!optionsParam) {
|
186
|
+
return defaults;
|
187
|
+
} else {
|
188
|
+
var options = {};
|
189
|
+
extend(options, defaults);
|
190
|
+
extend(options, optionsParam);
|
191
|
+
return options;
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
function createWordOptions(options) {
|
196
|
+
var lang, defaults;
|
197
|
+
if (!options) {
|
198
|
+
return defaultWordOptions[defaultLanguage];
|
199
|
+
} else {
|
200
|
+
lang = options.language || defaultLanguage;
|
201
|
+
defaults = {};
|
202
|
+
extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]);
|
203
|
+
extend(defaults, options);
|
204
|
+
return defaults;
|
205
|
+
}
|
206
|
+
}
|
207
|
+
|
208
|
+
function createCharacterOptions(options) {
|
209
|
+
return createOptions(options, defaultCharacterOptions);
|
210
|
+
}
|
211
|
+
|
212
|
+
function createCaretCharacterOptions(options) {
|
213
|
+
return createOptions(options, defaultCaretCharacterOptions);
|
214
|
+
}
|
215
|
+
|
216
|
+
var defaultFindOptions = {
|
217
|
+
caseSensitive: false,
|
218
|
+
withinRange: null,
|
219
|
+
wholeWordsOnly: false,
|
220
|
+
wrap: false,
|
221
|
+
direction: "forward",
|
222
|
+
wordOptions: null,
|
223
|
+
characterOptions: null
|
224
|
+
};
|
225
|
+
|
226
|
+
var defaultMoveOptions = {
|
227
|
+
wordOptions: null,
|
228
|
+
characterOptions: null
|
229
|
+
};
|
230
|
+
|
231
|
+
var defaultExpandOptions = {
|
232
|
+
wordOptions: null,
|
233
|
+
characterOptions: null,
|
234
|
+
trim: false,
|
235
|
+
trimStart: true,
|
236
|
+
trimEnd: true
|
237
|
+
};
|
238
|
+
|
239
|
+
var defaultWordIteratorOptions = {
|
240
|
+
wordOptions: null,
|
241
|
+
characterOptions: null,
|
242
|
+
direction: "forward"
|
243
|
+
};
|
244
|
+
|
245
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
246
|
+
|
247
|
+
/* DOM utility functions */
|
248
|
+
var getComputedStyleProperty = dom.getComputedStyleProperty;
|
249
|
+
|
250
|
+
// Create cachable versions of DOM functions
|
251
|
+
|
252
|
+
// Test for old IE's incorrect display properties
|
253
|
+
var tableCssDisplayBlock;
|
254
|
+
(function() {
|
255
|
+
var table = document.createElement("table");
|
256
|
+
var body = getBody(document);
|
257
|
+
body.appendChild(table);
|
258
|
+
tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block");
|
259
|
+
body.removeChild(table);
|
260
|
+
})();
|
261
|
+
|
262
|
+
api.features.tableCssDisplayBlock = tableCssDisplayBlock;
|
263
|
+
|
264
|
+
var defaultDisplayValueForTag = {
|
265
|
+
table: "table",
|
266
|
+
caption: "table-caption",
|
267
|
+
colgroup: "table-column-group",
|
268
|
+
col: "table-column",
|
269
|
+
thead: "table-header-group",
|
270
|
+
tbody: "table-row-group",
|
271
|
+
tfoot: "table-footer-group",
|
272
|
+
tr: "table-row",
|
273
|
+
td: "table-cell",
|
274
|
+
th: "table-cell"
|
275
|
+
};
|
276
|
+
|
277
|
+
// Corrects IE's "block" value for table-related elements
|
278
|
+
function getComputedDisplay(el, win) {
|
279
|
+
var display = getComputedStyleProperty(el, "display", win);
|
280
|
+
var tagName = el.tagName.toLowerCase();
|
281
|
+
return (display == "block"
|
282
|
+
&& tableCssDisplayBlock
|
283
|
+
&& defaultDisplayValueForTag.hasOwnProperty(tagName))
|
284
|
+
? defaultDisplayValueForTag[tagName] : display;
|
285
|
+
}
|
286
|
+
|
287
|
+
function isHidden(node) {
|
288
|
+
var ancestors = getAncestorsAndSelf(node);
|
289
|
+
for (var i = 0, len = ancestors.length; i < len; ++i) {
|
290
|
+
if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") {
|
291
|
+
return true;
|
292
|
+
}
|
293
|
+
}
|
294
|
+
|
295
|
+
return false;
|
296
|
+
}
|
297
|
+
|
298
|
+
function isVisibilityHiddenTextNode(textNode) {
|
299
|
+
var el;
|
300
|
+
return textNode.nodeType == 3
|
301
|
+
&& (el = textNode.parentNode)
|
302
|
+
&& getComputedStyleProperty(el, "visibility") == "hidden";
|
303
|
+
}
|
304
|
+
|
305
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
306
|
+
|
307
|
+
|
308
|
+
// "A block node is either an Element whose "display" property does not have
|
309
|
+
// resolved value "inline" or "inline-block" or "inline-table" or "none", or a
|
310
|
+
// Document, or a DocumentFragment."
|
311
|
+
function isBlockNode(node) {
|
312
|
+
return node
|
313
|
+
&& ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node)))
|
314
|
+
|| node.nodeType == 9 || node.nodeType == 11);
|
315
|
+
}
|
316
|
+
|
317
|
+
function getLastDescendantOrSelf(node) {
|
318
|
+
var lastChild = node.lastChild;
|
319
|
+
return lastChild ? getLastDescendantOrSelf(lastChild) : node;
|
320
|
+
}
|
321
|
+
|
322
|
+
function containsPositions(node) {
|
323
|
+
return dom.isCharacterDataNode(node)
|
324
|
+
|| !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName);
|
325
|
+
}
|
326
|
+
|
327
|
+
function getAncestors(node) {
|
328
|
+
var ancestors = [];
|
329
|
+
while (node.parentNode) {
|
330
|
+
ancestors.unshift(node.parentNode);
|
331
|
+
node = node.parentNode;
|
332
|
+
}
|
333
|
+
return ancestors;
|
334
|
+
}
|
335
|
+
|
336
|
+
function getAncestorsAndSelf(node) {
|
337
|
+
return getAncestors(node).concat([node]);
|
338
|
+
}
|
339
|
+
|
340
|
+
// Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
|
341
|
+
function isHtmlNode(node) {
|
342
|
+
var ns;
|
343
|
+
return typeof (ns = node.namespaceURI) == UNDEF || (ns === null || ns == "http://www.w3.org/1999/xhtml");
|
344
|
+
}
|
345
|
+
|
346
|
+
function isHtmlElement(node, tagNames) {
|
347
|
+
if (!node || node.nodeType != 1 || !isHtmlNode(node)) {
|
348
|
+
return false;
|
349
|
+
}
|
350
|
+
switch (typeof tagNames) {
|
351
|
+
case "string":
|
352
|
+
return node.tagName.toLowerCase() == tagNames.toLowerCase();
|
353
|
+
case "object":
|
354
|
+
return new RegExp("^(" + tagNames.join("|S") + ")$", "i").test(node.tagName);
|
355
|
+
default:
|
356
|
+
return true;
|
357
|
+
}
|
358
|
+
}
|
359
|
+
|
360
|
+
function nextNodeDescendants(node) {
|
361
|
+
while (node && !node.nextSibling) {
|
362
|
+
node = node.parentNode;
|
363
|
+
}
|
364
|
+
if (!node) {
|
365
|
+
return null;
|
366
|
+
}
|
367
|
+
return node.nextSibling;
|
368
|
+
}
|
369
|
+
|
370
|
+
function nextNode(node, excludeChildren) {
|
371
|
+
if (!excludeChildren && node.hasChildNodes()) {
|
372
|
+
return node.firstChild;
|
373
|
+
}
|
374
|
+
return nextNodeDescendants(node);
|
375
|
+
}
|
376
|
+
|
377
|
+
function previousNode(node) {
|
378
|
+
var previous = node.previousSibling;
|
379
|
+
if (previous) {
|
380
|
+
node = previous;
|
381
|
+
while (node.hasChildNodes()) {
|
382
|
+
node = node.lastChild;
|
383
|
+
}
|
384
|
+
return node;
|
385
|
+
}
|
386
|
+
var parent = node.parentNode;
|
387
|
+
if (parent && parent.nodeType == 1) {
|
388
|
+
return parent;
|
389
|
+
}
|
390
|
+
return null;
|
391
|
+
}
|
392
|
+
|
393
|
+
|
394
|
+
|
395
|
+
// Adpated from Aryeh's code.
|
396
|
+
// "A whitespace node is either a Text node whose data is the empty string; or
|
397
|
+
// a Text node whose data consists only of one or more tabs (0x0009), line
|
398
|
+
// feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
|
399
|
+
// parent is an Element whose resolved value for "white-space" is "normal" or
|
400
|
+
// "nowrap"; or a Text node whose data consists only of one or more tabs
|
401
|
+
// (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
|
402
|
+
// parent is an Element whose resolved value for "white-space" is "pre-line"."
|
403
|
+
function isWhitespaceNode(node) {
|
404
|
+
if (!node || node.nodeType != 3) {
|
405
|
+
return false;
|
406
|
+
}
|
407
|
+
var text = node.data;
|
408
|
+
if (text === "") {
|
409
|
+
return true;
|
410
|
+
}
|
411
|
+
var parent = node.parentNode;
|
412
|
+
if (!parent || parent.nodeType != 1) {
|
413
|
+
return false;
|
414
|
+
}
|
415
|
+
var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
|
416
|
+
|
417
|
+
return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace))
|
418
|
+
|| (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line");
|
419
|
+
}
|
420
|
+
|
421
|
+
// Adpated from Aryeh's code.
|
422
|
+
// "node is a collapsed whitespace node if the following algorithm returns
|
423
|
+
// true:"
|
424
|
+
function isCollapsedWhitespaceNode(node) {
|
425
|
+
// "If node's data is the empty string, return true."
|
426
|
+
if (node.data === "") {
|
427
|
+
return true;
|
428
|
+
}
|
429
|
+
|
430
|
+
// "If node is not a whitespace node, return false."
|
431
|
+
if (!isWhitespaceNode(node)) {
|
432
|
+
return false;
|
433
|
+
}
|
434
|
+
|
435
|
+
// "Let ancestor be node's parent."
|
436
|
+
var ancestor = node.parentNode;
|
437
|
+
|
438
|
+
// "If ancestor is null, return true."
|
439
|
+
if (!ancestor) {
|
440
|
+
return true;
|
441
|
+
}
|
442
|
+
|
443
|
+
// "If the "display" property of some ancestor of node has resolved value "none", return true."
|
444
|
+
if (isHidden(node)) {
|
445
|
+
return true;
|
446
|
+
}
|
447
|
+
|
448
|
+
return false;
|
449
|
+
}
|
450
|
+
|
451
|
+
function isCollapsedNode(node) {
|
452
|
+
var type = node.nodeType;
|
453
|
+
return type == 7 /* PROCESSING_INSTRUCTION */
|
454
|
+
|| type == 8 /* COMMENT */
|
455
|
+
|| isHidden(node)
|
456
|
+
|| /^(script|style)$/i.test(node.nodeName)
|
457
|
+
|| isVisibilityHiddenTextNode(node)
|
458
|
+
|| isCollapsedWhitespaceNode(node);
|
459
|
+
}
|
460
|
+
|
461
|
+
function isIgnoredNode(node, win) {
|
462
|
+
var type = node.nodeType;
|
463
|
+
return type == 7 /* PROCESSING_INSTRUCTION */
|
464
|
+
|| type == 8 /* COMMENT */
|
465
|
+
|| (type == 1 && getComputedDisplay(node, win) == "none");
|
466
|
+
}
|
467
|
+
|
468
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
469
|
+
|
470
|
+
// Possibly overengineered caching system to prevent repeated DOM calls slowing everything down
|
471
|
+
|
472
|
+
function Cache() {
|
473
|
+
this.store = {};
|
474
|
+
}
|
475
|
+
|
476
|
+
Cache.prototype = {
|
477
|
+
get: function(key) {
|
478
|
+
return this.store.hasOwnProperty(key) ? this.store[key] : null;
|
479
|
+
},
|
480
|
+
|
481
|
+
set: function(key, value) {
|
482
|
+
return this.store[key] = value;
|
483
|
+
}
|
484
|
+
};
|
485
|
+
|
486
|
+
var cachedCount = 0, uncachedCount = 0;
|
487
|
+
|
488
|
+
function createCachingGetter(methodName, func, objProperty) {
|
489
|
+
return function(args) {
|
490
|
+
var cache = this.cache;
|
491
|
+
if (cache.hasOwnProperty(methodName)) {
|
492
|
+
cachedCount++;
|
493
|
+
return cache[methodName];
|
494
|
+
} else {
|
495
|
+
uncachedCount++;
|
496
|
+
var value = func.call(this, objProperty ? this[objProperty] : this, args);
|
497
|
+
cache[methodName] = value;
|
498
|
+
return value;
|
499
|
+
}
|
500
|
+
};
|
501
|
+
}
|
502
|
+
|
503
|
+
api.report = function() {
|
504
|
+
console.log("Cached: " + cachedCount + ", uncached: " + uncachedCount);
|
505
|
+
};
|
506
|
+
|
507
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
508
|
+
|
509
|
+
function NodeWrapper(node, session) {
|
510
|
+
this.node = node;
|
511
|
+
this.session = session;
|
512
|
+
this.cache = new Cache();
|
513
|
+
this.positions = new Cache();
|
514
|
+
}
|
515
|
+
|
516
|
+
var nodeProto = {
|
517
|
+
getPosition: function(offset) {
|
518
|
+
var positions = this.positions;
|
519
|
+
return positions.get(offset) || positions.set(offset, new Position(this, offset));
|
520
|
+
},
|
521
|
+
|
522
|
+
toString: function() {
|
523
|
+
return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]";
|
524
|
+
}
|
525
|
+
};
|
526
|
+
|
527
|
+
NodeWrapper.prototype = nodeProto;
|
528
|
+
|
529
|
+
var EMPTY = "EMPTY",
|
530
|
+
NON_SPACE = "NON_SPACE",
|
531
|
+
UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE",
|
532
|
+
COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE",
|
533
|
+
TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK",
|
534
|
+
TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR",
|
535
|
+
PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK";
|
536
|
+
|
537
|
+
|
538
|
+
extend(nodeProto, {
|
539
|
+
isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"),
|
540
|
+
getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"),
|
541
|
+
getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"),
|
542
|
+
containsPositions: createCachingGetter("containsPositions", containsPositions, "node"),
|
543
|
+
isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"),
|
544
|
+
isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"),
|
545
|
+
getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"),
|
546
|
+
isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"),
|
547
|
+
isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"),
|
548
|
+
next: createCachingGetter("nextPos", nextNode, "node"),
|
549
|
+
previous: createCachingGetter("previous", previousNode, "node"),
|
550
|
+
|
551
|
+
getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) {
|
552
|
+
var spaceRegex = null, collapseSpaces = false;
|
553
|
+
var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace");
|
554
|
+
var preLine = (cssWhitespace == "pre-line");
|
555
|
+
if (preLine) {
|
556
|
+
spaceRegex = spacesMinusLineBreaksRegex;
|
557
|
+
collapseSpaces = true;
|
558
|
+
} else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") {
|
559
|
+
spaceRegex = spacesRegex;
|
560
|
+
collapseSpaces = true;
|
561
|
+
}
|
562
|
+
|
563
|
+
return {
|
564
|
+
node: textNode,
|
565
|
+
text: textNode.data,
|
566
|
+
spaceRegex: spaceRegex,
|
567
|
+
collapseSpaces: collapseSpaces,
|
568
|
+
preLine: preLine
|
569
|
+
};
|
570
|
+
}, "node"),
|
571
|
+
|
572
|
+
hasInnerText: createCachingGetter("hasInnerText", function(el, backward) {
|
573
|
+
var session = this.session;
|
574
|
+
var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1);
|
575
|
+
var firstPosInEl = session.getPosition(el, 0);
|
576
|
+
|
577
|
+
var pos = backward ? posAfterEl : firstPosInEl;
|
578
|
+
var endPos = backward ? firstPosInEl : posAfterEl;
|
579
|
+
|
580
|
+
/*
|
581
|
+
<body><p>X </p><p>Y</p></body>
|
582
|
+
|
583
|
+
Positions:
|
584
|
+
|
585
|
+
body:0:""
|
586
|
+
p:0:""
|
587
|
+
text:0:""
|
588
|
+
text:1:"X"
|
589
|
+
text:2:TRAILING_SPACE_IN_BLOCK
|
590
|
+
text:3:COLLAPSED_SPACE
|
591
|
+
p:1:""
|
592
|
+
body:1:"\n"
|
593
|
+
p:0:""
|
594
|
+
text:0:""
|
595
|
+
text:1:"Y"
|
596
|
+
|
597
|
+
A character is a TRAILING_SPACE_IN_BLOCK iff:
|
598
|
+
|
599
|
+
- There is no uncollapsed character after it within the visible containing block element
|
600
|
+
|
601
|
+
A character is a TRAILING_SPACE_BEFORE_BR iff:
|
602
|
+
|
603
|
+
- There is no uncollapsed character after it preceding a <br> element
|
604
|
+
|
605
|
+
An element has inner text iff
|
606
|
+
|
607
|
+
- It is not hidden
|
608
|
+
- It contains an uncollapsed character
|
609
|
+
|
610
|
+
All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render.
|
611
|
+
*/
|
612
|
+
|
613
|
+
while (pos !== endPos) {
|
614
|
+
pos.prepopulateChar();
|
615
|
+
if (pos.isDefinitelyNonEmpty()) {
|
616
|
+
return true;
|
617
|
+
}
|
618
|
+
pos = backward ? pos.previousVisible() : pos.nextVisible();
|
619
|
+
}
|
620
|
+
|
621
|
+
return false;
|
622
|
+
}, "node"),
|
623
|
+
|
624
|
+
getTrailingSpace: createCachingGetter("trailingSpace", function(el) {
|
625
|
+
if (el.tagName.toLowerCase() == "br") {
|
626
|
+
return "";
|
627
|
+
} else {
|
628
|
+
switch (this.getComputedDisplay()) {
|
629
|
+
case "inline":
|
630
|
+
var child = el.lastChild;
|
631
|
+
while (child) {
|
632
|
+
if (!isIgnoredNode(child)) {
|
633
|
+
return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : "";
|
634
|
+
}
|
635
|
+
child = child.previousSibling;
|
636
|
+
}
|
637
|
+
break;
|
638
|
+
case "inline-block":
|
639
|
+
case "inline-table":
|
640
|
+
case "none":
|
641
|
+
case "table-column":
|
642
|
+
case "table-column-group":
|
643
|
+
break;
|
644
|
+
case "table-cell":
|
645
|
+
return "\t";
|
646
|
+
default:
|
647
|
+
return this.hasInnerText(true) ? "\n" : "";
|
648
|
+
}
|
649
|
+
}
|
650
|
+
return "";
|
651
|
+
}, "node"),
|
652
|
+
|
653
|
+
getLeadingSpace: createCachingGetter("leadingSpace", function(el) {
|
654
|
+
switch (this.getComputedDisplay()) {
|
655
|
+
case "inline":
|
656
|
+
case "inline-block":
|
657
|
+
case "inline-table":
|
658
|
+
case "none":
|
659
|
+
case "table-column":
|
660
|
+
case "table-column-group":
|
661
|
+
case "table-cell":
|
662
|
+
break;
|
663
|
+
default:
|
664
|
+
return this.hasInnerText(false) ? "\n" : "";
|
665
|
+
}
|
666
|
+
return "";
|
667
|
+
}, "node")
|
668
|
+
});
|
669
|
+
|
670
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
671
|
+
|
672
|
+
|
673
|
+
function Position(nodeWrapper, offset) {
|
674
|
+
this.offset = offset;
|
675
|
+
this.nodeWrapper = nodeWrapper;
|
676
|
+
this.node = nodeWrapper.node;
|
677
|
+
this.session = nodeWrapper.session;
|
678
|
+
this.cache = new Cache();
|
679
|
+
}
|
680
|
+
|
681
|
+
function inspectPosition() {
|
682
|
+
return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]";
|
683
|
+
}
|
684
|
+
|
685
|
+
var positionProto = {
|
686
|
+
character: "",
|
687
|
+
characterType: EMPTY,
|
688
|
+
isBr: false,
|
689
|
+
|
690
|
+
/*
|
691
|
+
This method:
|
692
|
+
- Fully populates positions that have characters that can be determined independently of any other characters.
|
693
|
+
- Populates most types of space positions with a provisional character. The character is finalized later.
|
694
|
+
*/
|
695
|
+
prepopulateChar: function() {
|
696
|
+
var pos = this;
|
697
|
+
if (!pos.prepopulatedChar) {
|
698
|
+
var node = pos.node, offset = pos.offset;
|
699
|
+
var visibleChar = "", charType = EMPTY;
|
700
|
+
var finalizedChar = false;
|
701
|
+
if (offset > 0) {
|
702
|
+
if (node.nodeType == 3) {
|
703
|
+
var text = node.data;
|
704
|
+
var textChar = text.charAt(offset - 1);
|
705
|
+
|
706
|
+
var nodeInfo = pos.nodeWrapper.getTextNodeInfo();
|
707
|
+
var spaceRegex = nodeInfo.spaceRegex;
|
708
|
+
if (nodeInfo.collapseSpaces) {
|
709
|
+
if (spaceRegex.test(textChar)) {
|
710
|
+
// "If the character at position is from set, append a single space (U+0020) to newdata and advance
|
711
|
+
// position until the character at position is not from set."
|
712
|
+
|
713
|
+
// We also need to check for the case where we're in a pre-line and we have a space preceding a
|
714
|
+
// line break, because such spaces are collapsed in some browsers
|
715
|
+
if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) {
|
716
|
+
} else if (nodeInfo.preLine && text.charAt(offset) === "\n") {
|
717
|
+
visibleChar = " ";
|
718
|
+
charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK;
|
719
|
+
} else {
|
720
|
+
visibleChar = " ";
|
721
|
+
//pos.checkForFollowingLineBreak = true;
|
722
|
+
charType = COLLAPSIBLE_SPACE;
|
723
|
+
}
|
724
|
+
} else {
|
725
|
+
visibleChar = textChar;
|
726
|
+
charType = NON_SPACE;
|
727
|
+
finalizedChar = true;
|
728
|
+
}
|
729
|
+
} else {
|
730
|
+
visibleChar = textChar;
|
731
|
+
charType = UNCOLLAPSIBLE_SPACE;
|
732
|
+
finalizedChar = true;
|
733
|
+
}
|
734
|
+
} else {
|
735
|
+
var nodePassed = node.childNodes[offset - 1];
|
736
|
+
if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) {
|
737
|
+
if (nodePassed.tagName.toLowerCase() == "br") {
|
738
|
+
visibleChar = "\n";
|
739
|
+
pos.isBr = true;
|
740
|
+
charType = COLLAPSIBLE_SPACE;
|
741
|
+
finalizedChar = false;
|
742
|
+
} else {
|
743
|
+
pos.checkForTrailingSpace = true;
|
744
|
+
}
|
745
|
+
}
|
746
|
+
|
747
|
+
// Check the leading space of the next node for the case when a block element follows an inline
|
748
|
+
// element or text node. In that case, there is an implied line break between the two nodes.
|
749
|
+
if (!visibleChar) {
|
750
|
+
var nextNode = node.childNodes[offset];
|
751
|
+
if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) {
|
752
|
+
pos.checkForLeadingSpace = true;
|
753
|
+
}
|
754
|
+
}
|
755
|
+
}
|
756
|
+
}
|
757
|
+
|
758
|
+
pos.prepopulatedChar = true;
|
759
|
+
pos.character = visibleChar;
|
760
|
+
pos.characterType = charType;
|
761
|
+
pos.isCharInvariant = finalizedChar;
|
762
|
+
}
|
763
|
+
},
|
764
|
+
|
765
|
+
isDefinitelyNonEmpty: function() {
|
766
|
+
var charType = this.characterType;
|
767
|
+
return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE;
|
768
|
+
},
|
769
|
+
|
770
|
+
// Resolve leading and trailing spaces, which may involve prepopulating other positions
|
771
|
+
resolveLeadingAndTrailingSpaces: function() {
|
772
|
+
if (!this.prepopulatedChar) {
|
773
|
+
this.prepopulateChar();
|
774
|
+
}
|
775
|
+
if (this.checkForTrailingSpace) {
|
776
|
+
var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace();
|
777
|
+
if (trailingSpace) {
|
778
|
+
this.isTrailingSpace = true;
|
779
|
+
this.character = trailingSpace;
|
780
|
+
this.characterType = COLLAPSIBLE_SPACE;
|
781
|
+
}
|
782
|
+
this.checkForTrailingSpace = false;
|
783
|
+
}
|
784
|
+
if (this.checkForLeadingSpace) {
|
785
|
+
var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();
|
786
|
+
if (leadingSpace) {
|
787
|
+
this.isLeadingSpace = true;
|
788
|
+
this.character = leadingSpace;
|
789
|
+
this.characterType = COLLAPSIBLE_SPACE;
|
790
|
+
}
|
791
|
+
this.checkForLeadingSpace = false;
|
792
|
+
}
|
793
|
+
},
|
794
|
+
|
795
|
+
getPrecedingUncollapsedPosition: function(characterOptions) {
|
796
|
+
var pos = this, character;
|
797
|
+
while ( (pos = pos.previousVisible()) ) {
|
798
|
+
character = pos.getCharacter(characterOptions);
|
799
|
+
if (character !== "") {
|
800
|
+
return pos;
|
801
|
+
}
|
802
|
+
}
|
803
|
+
|
804
|
+
return null;
|
805
|
+
},
|
806
|
+
|
807
|
+
getCharacter: function(characterOptions) {
|
808
|
+
this.resolveLeadingAndTrailingSpaces();
|
809
|
+
|
810
|
+
// Check if this position's character is invariant (i.e. not dependent on character options) and return it
|
811
|
+
// if so
|
812
|
+
if (this.isCharInvariant) {
|
813
|
+
return this.character;
|
814
|
+
}
|
815
|
+
|
816
|
+
var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace].join("_");
|
817
|
+
var cachedChar = this.cache.get(cacheKey);
|
818
|
+
if (cachedChar !== null) {
|
819
|
+
return cachedChar;
|
820
|
+
}
|
821
|
+
|
822
|
+
// We need to actually get the character
|
823
|
+
var character = "";
|
824
|
+
var collapsible = (this.characterType == COLLAPSIBLE_SPACE);
|
825
|
+
|
826
|
+
var nextPos, previousPos/* = this.getPrecedingUncollapsedPosition(characterOptions)*/;
|
827
|
+
var gotPreviousPos = false;
|
828
|
+
var pos = this;
|
829
|
+
|
830
|
+
function getPreviousPos() {
|
831
|
+
if (!gotPreviousPos) {
|
832
|
+
previousPos = pos.getPrecedingUncollapsedPosition(characterOptions);
|
833
|
+
gotPreviousPos = true;
|
834
|
+
}
|
835
|
+
return previousPos;
|
836
|
+
}
|
837
|
+
|
838
|
+
// Disallow a collapsible space that is followed by a line break or is the last character
|
839
|
+
if (collapsible) {
|
840
|
+
// Disallow a collapsible space that follows a trailing space or line break, or is the first character
|
841
|
+
if (this.character == " " &&
|
842
|
+
(!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n")) {
|
843
|
+
}
|
844
|
+
// Allow a leading line break unless it follows a line break
|
845
|
+
else if (this.character == "\n" && this.isLeadingSpace) {
|
846
|
+
if (getPreviousPos() && previousPos.character != "\n") {
|
847
|
+
character = "\n";
|
848
|
+
} else {
|
849
|
+
}
|
850
|
+
} else {
|
851
|
+
nextPos = this.nextUncollapsed();
|
852
|
+
if (nextPos) {
|
853
|
+
if (nextPos.isBr) {
|
854
|
+
this.type = TRAILING_SPACE_BEFORE_BR;
|
855
|
+
} else if (nextPos.isTrailingSpace && nextPos.character == "\n") {
|
856
|
+
this.type = TRAILING_SPACE_IN_BLOCK;
|
857
|
+
}
|
858
|
+
if (nextPos.character === "\n") {
|
859
|
+
if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) {
|
860
|
+
} else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) {
|
861
|
+
} else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) {
|
862
|
+
} else if (this.character === "\n") {
|
863
|
+
if (nextPos.isTrailingSpace) {
|
864
|
+
if (this.isTrailingSpace) {
|
865
|
+
} else if (this.isBr) {
|
866
|
+
}
|
867
|
+
} else {
|
868
|
+
character = "\n";
|
869
|
+
}
|
870
|
+
} else if (this.character === " ") {
|
871
|
+
character = " ";
|
872
|
+
} else {
|
873
|
+
}
|
874
|
+
} else {
|
875
|
+
character = this.character;
|
876
|
+
}
|
877
|
+
} else {
|
878
|
+
}
|
879
|
+
}
|
880
|
+
}
|
881
|
+
|
882
|
+
// Collapse a br element that is followed by a trailing space
|
883
|
+
else if (this.character === "\n" &&
|
884
|
+
(!(nextPos = this.nextUncollapsed()) || nextPos.isTrailingSpace)) {
|
885
|
+
}
|
886
|
+
|
887
|
+
|
888
|
+
this.cache.set(cacheKey, character);
|
889
|
+
|
890
|
+
return character;
|
891
|
+
},
|
892
|
+
|
893
|
+
equals: function(pos) {
|
894
|
+
return !!pos && this.node === pos.node && this.offset === pos.offset;
|
895
|
+
},
|
896
|
+
|
897
|
+
inspect: inspectPosition,
|
898
|
+
|
899
|
+
toString: function() {
|
900
|
+
return this.character;
|
901
|
+
}
|
902
|
+
};
|
903
|
+
|
904
|
+
Position.prototype = positionProto;
|
905
|
+
|
906
|
+
extend(positionProto, {
|
907
|
+
next: createCachingGetter("nextPos", function(pos) {
|
908
|
+
var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
|
909
|
+
if (!node) {
|
910
|
+
return null;
|
911
|
+
}
|
912
|
+
var nextNode, nextOffset, child;
|
913
|
+
if (offset == nodeWrapper.getLength()) {
|
914
|
+
// Move onto the next node
|
915
|
+
nextNode = node.parentNode;
|
916
|
+
nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0;
|
917
|
+
} else {
|
918
|
+
if (nodeWrapper.isCharacterDataNode()) {
|
919
|
+
nextNode = node;
|
920
|
+
nextOffset = offset + 1;
|
921
|
+
} else {
|
922
|
+
child = node.childNodes[offset];
|
923
|
+
// Go into the children next, if children there are
|
924
|
+
if (session.getNodeWrapper(child).containsPositions()) {
|
925
|
+
nextNode = child;
|
926
|
+
nextOffset = 0;
|
927
|
+
} else {
|
928
|
+
nextNode = node;
|
929
|
+
nextOffset = offset + 1;
|
930
|
+
}
|
931
|
+
}
|
932
|
+
}
|
933
|
+
|
934
|
+
return nextNode ? session.getPosition(nextNode, nextOffset) : null;
|
935
|
+
}),
|
936
|
+
|
937
|
+
previous: createCachingGetter("previous", function(pos) {
|
938
|
+
var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
|
939
|
+
var previousNode, previousOffset, child;
|
940
|
+
if (offset == 0) {
|
941
|
+
previousNode = node.parentNode;
|
942
|
+
previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0;
|
943
|
+
} else {
|
944
|
+
if (nodeWrapper.isCharacterDataNode()) {
|
945
|
+
previousNode = node;
|
946
|
+
previousOffset = offset - 1;
|
947
|
+
} else {
|
948
|
+
child = node.childNodes[offset - 1];
|
949
|
+
// Go into the children next, if children there are
|
950
|
+
if (session.getNodeWrapper(child).containsPositions()) {
|
951
|
+
previousNode = child;
|
952
|
+
previousOffset = dom.getNodeLength(child);
|
953
|
+
} else {
|
954
|
+
previousNode = node;
|
955
|
+
previousOffset = offset - 1;
|
956
|
+
}
|
957
|
+
}
|
958
|
+
}
|
959
|
+
return previousNode ? session.getPosition(previousNode, previousOffset) : null;
|
960
|
+
}),
|
961
|
+
|
962
|
+
/*
|
963
|
+
Next and previous position moving functions that filter out
|
964
|
+
|
965
|
+
- Hidden (CSS visibility/display) elements
|
966
|
+
- Script and style elements
|
967
|
+
*/
|
968
|
+
nextVisible: createCachingGetter("nextVisible", function(pos) {
|
969
|
+
var next = pos.next();
|
970
|
+
if (!next) {
|
971
|
+
return null;
|
972
|
+
}
|
973
|
+
var nodeWrapper = next.nodeWrapper, node = next.node;
|
974
|
+
var newPos = next;
|
975
|
+
if (nodeWrapper.isCollapsed()) {
|
976
|
+
// We're skipping this node and all its descendants
|
977
|
+
newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1);
|
978
|
+
}
|
979
|
+
return newPos;
|
980
|
+
}),
|
981
|
+
|
982
|
+
nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) {
|
983
|
+
var nextPos = pos;
|
984
|
+
while ( (nextPos = nextPos.nextVisible()) ) {
|
985
|
+
nextPos.resolveLeadingAndTrailingSpaces();
|
986
|
+
if (nextPos.character !== "") {
|
987
|
+
return nextPos;
|
988
|
+
}
|
989
|
+
}
|
990
|
+
return null;
|
991
|
+
}),
|
992
|
+
|
993
|
+
previousVisible: createCachingGetter("previousVisible", function(pos) {
|
994
|
+
var previous = pos.previous();
|
995
|
+
if (!previous) {
|
996
|
+
return null;
|
997
|
+
}
|
998
|
+
var nodeWrapper = previous.nodeWrapper, node = previous.node;
|
999
|
+
var newPos = previous;
|
1000
|
+
if (nodeWrapper.isCollapsed()) {
|
1001
|
+
// We're skipping this node and all its descendants
|
1002
|
+
newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex());
|
1003
|
+
}
|
1004
|
+
return newPos;
|
1005
|
+
})
|
1006
|
+
});
|
1007
|
+
|
1008
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
1009
|
+
|
1010
|
+
var currentSession = null;
|
1011
|
+
|
1012
|
+
var Session = (function() {
|
1013
|
+
function createWrapperCache(nodeProperty) {
|
1014
|
+
var cache = new Cache();
|
1015
|
+
|
1016
|
+
return {
|
1017
|
+
get: function(node) {
|
1018
|
+
var wrappersByProperty = cache.get(node[nodeProperty]);
|
1019
|
+
if (wrappersByProperty) {
|
1020
|
+
for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) {
|
1021
|
+
if (wrapper.node === node) {
|
1022
|
+
return wrapper;
|
1023
|
+
}
|
1024
|
+
}
|
1025
|
+
}
|
1026
|
+
return null;
|
1027
|
+
},
|
1028
|
+
|
1029
|
+
set: function(nodeWrapper) {
|
1030
|
+
var property = nodeWrapper.node[nodeProperty];
|
1031
|
+
var wrappersByProperty = cache.get(property) || cache.set(property, []);
|
1032
|
+
wrappersByProperty.push(nodeWrapper);
|
1033
|
+
}
|
1034
|
+
};
|
1035
|
+
}
|
1036
|
+
|
1037
|
+
var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID");
|
1038
|
+
|
1039
|
+
function Session() {
|
1040
|
+
this.initCaches();
|
1041
|
+
}
|
1042
|
+
|
1043
|
+
Session.prototype = {
|
1044
|
+
initCaches: function() {
|
1045
|
+
this.elementCache = uniqueIDSupported ? (function() {
|
1046
|
+
var elementsCache = new Cache();
|
1047
|
+
|
1048
|
+
return {
|
1049
|
+
get: function(el) {
|
1050
|
+
return elementsCache.get(el.uniqueID);
|
1051
|
+
},
|
1052
|
+
|
1053
|
+
set: function(elWrapper) {
|
1054
|
+
elementsCache.set(elWrapper.node.uniqueID, elWrapper);
|
1055
|
+
}
|
1056
|
+
};
|
1057
|
+
})() : createWrapperCache("tagName");
|
1058
|
+
|
1059
|
+
// Store text nodes keyed by data, although we may need to truncate this
|
1060
|
+
this.textNodeCache = createWrapperCache("data");
|
1061
|
+
this.otherNodeCache = createWrapperCache("nodeName");
|
1062
|
+
},
|
1063
|
+
|
1064
|
+
getNodeWrapper: function(node) {
|
1065
|
+
var wrapperCache;
|
1066
|
+
switch (node.nodeType) {
|
1067
|
+
case 1:
|
1068
|
+
wrapperCache = this.elementCache;
|
1069
|
+
break;
|
1070
|
+
case 3:
|
1071
|
+
wrapperCache = this.textNodeCache;
|
1072
|
+
break;
|
1073
|
+
default:
|
1074
|
+
wrapperCache = this.otherNodeCache;
|
1075
|
+
break;
|
1076
|
+
}
|
1077
|
+
|
1078
|
+
var wrapper = wrapperCache.get(node);
|
1079
|
+
if (!wrapper) {
|
1080
|
+
wrapper = new NodeWrapper(node, this);
|
1081
|
+
wrapperCache.set(wrapper);
|
1082
|
+
}
|
1083
|
+
return wrapper;
|
1084
|
+
},
|
1085
|
+
|
1086
|
+
getPosition: function(node, offset) {
|
1087
|
+
return this.getNodeWrapper(node).getPosition(offset);
|
1088
|
+
},
|
1089
|
+
|
1090
|
+
getRangeBoundaryPosition: function(range, isStart) {
|
1091
|
+
var prefix = isStart ? "start" : "end";
|
1092
|
+
return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]);
|
1093
|
+
},
|
1094
|
+
|
1095
|
+
detach: function() {
|
1096
|
+
this.elementCache = this.textNodeCache = this.otherNodeCache = null;
|
1097
|
+
}
|
1098
|
+
};
|
1099
|
+
|
1100
|
+
return Session;
|
1101
|
+
})();
|
1102
|
+
|
1103
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
1104
|
+
|
1105
|
+
function startSession() {
|
1106
|
+
endSession();
|
1107
|
+
return (currentSession = new Session());
|
1108
|
+
}
|
1109
|
+
|
1110
|
+
function getSession() {
|
1111
|
+
return currentSession || startSession();
|
1112
|
+
}
|
1113
|
+
|
1114
|
+
function endSession() {
|
1115
|
+
if (currentSession) {
|
1116
|
+
currentSession.detach();
|
1117
|
+
}
|
1118
|
+
currentSession = null;
|
1119
|
+
}
|
1120
|
+
|
1121
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
1122
|
+
|
1123
|
+
// Extensions to the rangy.dom utility object
|
1124
|
+
|
1125
|
+
extend(dom, {
|
1126
|
+
nextNode: nextNode,
|
1127
|
+
previousNode: previousNode
|
1128
|
+
});
|
1129
|
+
|
1130
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
1131
|
+
|
1132
|
+
function createCharacterIterator(startPos, backward, endPos, characterOptions) {
|
1133
|
+
|
1134
|
+
// Adjust the end position to ensure that it is actually reached
|
1135
|
+
if (endPos) {
|
1136
|
+
if (backward) {
|
1137
|
+
if (isCollapsedNode(endPos.node)) {
|
1138
|
+
endPos = startPos.previousVisible();
|
1139
|
+
}
|
1140
|
+
} else {
|
1141
|
+
if (isCollapsedNode(endPos.node)) {
|
1142
|
+
endPos = endPos.nextVisible();
|
1143
|
+
}
|
1144
|
+
}
|
1145
|
+
}
|
1146
|
+
|
1147
|
+
var pos = startPos, finished = false;
|
1148
|
+
|
1149
|
+
function next() {
|
1150
|
+
var newPos = null, charPos = null;
|
1151
|
+
if (backward) {
|
1152
|
+
charPos = pos;
|
1153
|
+
if (!finished) {
|
1154
|
+
pos = pos.previousVisible();
|
1155
|
+
finished = !pos || (endPos && pos.equals(endPos));
|
1156
|
+
}
|
1157
|
+
} else {
|
1158
|
+
if (!finished) {
|
1159
|
+
charPos = pos = pos.nextVisible();
|
1160
|
+
finished = !pos || (endPos && pos.equals(endPos));
|
1161
|
+
}
|
1162
|
+
}
|
1163
|
+
if (finished) {
|
1164
|
+
pos = null;
|
1165
|
+
}
|
1166
|
+
return charPos;
|
1167
|
+
}
|
1168
|
+
|
1169
|
+
var previousTextPos, returnPreviousTextPos = false;
|
1170
|
+
|
1171
|
+
return {
|
1172
|
+
next: function() {
|
1173
|
+
if (returnPreviousTextPos) {
|
1174
|
+
returnPreviousTextPos = false;
|
1175
|
+
return previousTextPos;
|
1176
|
+
} else {
|
1177
|
+
var pos, character;
|
1178
|
+
while ( (pos = next()) ) {
|
1179
|
+
character = pos.getCharacter(characterOptions);
|
1180
|
+
if (character) {
|
1181
|
+
previousTextPos = pos;
|
1182
|
+
return pos;
|
1183
|
+
}
|
1184
|
+
}
|
1185
|
+
return null;
|
1186
|
+
}
|
1187
|
+
},
|
1188
|
+
|
1189
|
+
rewind: function() {
|
1190
|
+
if (previousTextPos) {
|
1191
|
+
returnPreviousTextPos = true;
|
1192
|
+
} else {
|
1193
|
+
throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
|
1194
|
+
}
|
1195
|
+
},
|
1196
|
+
|
1197
|
+
dispose: function() {
|
1198
|
+
startPos = endPos = null;
|
1199
|
+
}
|
1200
|
+
};
|
1201
|
+
}
|
1202
|
+
|
1203
|
+
var arrayIndexOf = Array.prototype.indexOf ?
|
1204
|
+
function(arr, val) {
|
1205
|
+
return arr.indexOf(val);
|
1206
|
+
} :
|
1207
|
+
function(arr, val) {
|
1208
|
+
for (var i = 0, len = arr.length; i < len; ++i) {
|
1209
|
+
if (arr[i] === val) {
|
1210
|
+
return i;
|
1211
|
+
}
|
1212
|
+
}
|
1213
|
+
return -1;
|
1214
|
+
};
|
1215
|
+
|
1216
|
+
// Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
|
1217
|
+
// is called and there is no more tokenized text
|
1218
|
+
function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
|
1219
|
+
var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
|
1220
|
+
var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
|
1221
|
+
var tokenizer = wordOptions.tokenizer;
|
1222
|
+
|
1223
|
+
// Consumes a word and the whitespace beyond it
|
1224
|
+
function consumeWord(forward) {
|
1225
|
+
var pos, textChar;
|
1226
|
+
var newChars = [], it = forward ? forwardIterator : backwardIterator;
|
1227
|
+
|
1228
|
+
var passedWordBoundary = false, insideWord = false;
|
1229
|
+
|
1230
|
+
while ( (pos = it.next()) ) {
|
1231
|
+
textChar = pos.character;
|
1232
|
+
|
1233
|
+
|
1234
|
+
if (allWhiteSpaceRegex.test(textChar)) {
|
1235
|
+
if (insideWord) {
|
1236
|
+
insideWord = false;
|
1237
|
+
passedWordBoundary = true;
|
1238
|
+
}
|
1239
|
+
} else {
|
1240
|
+
if (passedWordBoundary) {
|
1241
|
+
it.rewind();
|
1242
|
+
break;
|
1243
|
+
} else {
|
1244
|
+
insideWord = true;
|
1245
|
+
}
|
1246
|
+
}
|
1247
|
+
newChars.push(pos);
|
1248
|
+
}
|
1249
|
+
|
1250
|
+
|
1251
|
+
return newChars;
|
1252
|
+
}
|
1253
|
+
|
1254
|
+
// Get initial word surrounding initial position and tokenize it
|
1255
|
+
var forwardChars = consumeWord(true);
|
1256
|
+
var backwardChars = consumeWord(false).reverse();
|
1257
|
+
var tokens = tokenizer(backwardChars.concat(forwardChars), wordOptions);
|
1258
|
+
|
1259
|
+
// Create initial token buffers
|
1260
|
+
var forwardTokensBuffer = forwardChars.length ?
|
1261
|
+
tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];
|
1262
|
+
|
1263
|
+
var backwardTokensBuffer = backwardChars.length ?
|
1264
|
+
tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];
|
1265
|
+
|
1266
|
+
function inspectBuffer(buffer) {
|
1267
|
+
var textPositions = ["[" + buffer.length + "]"];
|
1268
|
+
for (var i = 0; i < buffer.length; ++i) {
|
1269
|
+
textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
|
1270
|
+
}
|
1271
|
+
return textPositions;
|
1272
|
+
}
|
1273
|
+
|
1274
|
+
|
1275
|
+
return {
|
1276
|
+
nextEndToken: function() {
|
1277
|
+
var lastToken, forwardChars;
|
1278
|
+
|
1279
|
+
// If we're down to the last token, consume character chunks until we have a word or run out of
|
1280
|
+
// characters to consume
|
1281
|
+
while ( forwardTokensBuffer.length == 1 &&
|
1282
|
+
!(lastToken = forwardTokensBuffer[0]).isWord &&
|
1283
|
+
(forwardChars = consumeWord(true)).length > 0) {
|
1284
|
+
|
1285
|
+
// Merge trailing non-word into next word and tokenize
|
1286
|
+
forwardTokensBuffer = tokenizer(lastToken.chars.concat(forwardChars), wordOptions);
|
1287
|
+
}
|
1288
|
+
|
1289
|
+
return forwardTokensBuffer.shift();
|
1290
|
+
},
|
1291
|
+
|
1292
|
+
previousStartToken: function() {
|
1293
|
+
var lastToken, backwardChars;
|
1294
|
+
|
1295
|
+
// If we're down to the last token, consume character chunks until we have a word or run out of
|
1296
|
+
// characters to consume
|
1297
|
+
while ( backwardTokensBuffer.length == 1 &&
|
1298
|
+
!(lastToken = backwardTokensBuffer[0]).isWord &&
|
1299
|
+
(backwardChars = consumeWord(false)).length > 0) {
|
1300
|
+
|
1301
|
+
// Merge leading non-word into next word and tokenize
|
1302
|
+
backwardTokensBuffer = tokenizer(backwardChars.reverse().concat(lastToken.chars), wordOptions);
|
1303
|
+
}
|
1304
|
+
|
1305
|
+
return backwardTokensBuffer.pop();
|
1306
|
+
},
|
1307
|
+
|
1308
|
+
dispose: function() {
|
1309
|
+
forwardIterator.dispose();
|
1310
|
+
backwardIterator.dispose();
|
1311
|
+
forwardTokensBuffer = backwardTokensBuffer = null;
|
1312
|
+
}
|
1313
|
+
};
|
1314
|
+
}
|
1315
|
+
|
1316
|
+
function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
|
1317
|
+
var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
|
1318
|
+
if (count !== 0) {
|
1319
|
+
var backward = (count < 0);
|
1320
|
+
|
1321
|
+
switch (unit) {
|
1322
|
+
case CHARACTER:
|
1323
|
+
charIterator = createCharacterIterator(pos, backward, null, characterOptions);
|
1324
|
+
while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
|
1325
|
+
++unitsMoved;
|
1326
|
+
newPos = currentPos;
|
1327
|
+
}
|
1328
|
+
nextPos = currentPos;
|
1329
|
+
charIterator.dispose();
|
1330
|
+
break;
|
1331
|
+
case WORD:
|
1332
|
+
var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
|
1333
|
+
var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;
|
1334
|
+
|
1335
|
+
while ( (token = next()) && unitsMoved < absCount ) {
|
1336
|
+
if (token.isWord) {
|
1337
|
+
++unitsMoved;
|
1338
|
+
newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
|
1339
|
+
}
|
1340
|
+
}
|
1341
|
+
break;
|
1342
|
+
default:
|
1343
|
+
throw new Error("movePositionBy: unit '" + unit + "' not implemented");
|
1344
|
+
}
|
1345
|
+
|
1346
|
+
// Perform any necessary position tweaks
|
1347
|
+
if (backward) {
|
1348
|
+
newPos = newPos.previousVisible();
|
1349
|
+
unitsMoved = -unitsMoved;
|
1350
|
+
} else if (newPos && newPos.isLeadingSpace) {
|
1351
|
+
// Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
|
1352
|
+
// before a block element (for example, the line break between "1" and "2" in the following HTML:
|
1353
|
+
// "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
|
1354
|
+
// corresponds with a different selection position in most browsers from the one we want (i.e. at the
|
1355
|
+
// start of the contents of the block element). We get round this by advancing the position returned to
|
1356
|
+
// the last possible equivalent visible position.
|
1357
|
+
if (unit == WORD) {
|
1358
|
+
charIterator = createCharacterIterator(pos, false, null, characterOptions);
|
1359
|
+
nextPos = charIterator.next();
|
1360
|
+
charIterator.dispose();
|
1361
|
+
}
|
1362
|
+
if (nextPos) {
|
1363
|
+
newPos = nextPos.previousVisible();
|
1364
|
+
}
|
1365
|
+
}
|
1366
|
+
}
|
1367
|
+
|
1368
|
+
|
1369
|
+
return {
|
1370
|
+
position: newPos,
|
1371
|
+
unitsMoved: unitsMoved
|
1372
|
+
};
|
1373
|
+
}
|
1374
|
+
|
1375
|
+
function createRangeCharacterIterator(session, range, characterOptions, backward) {
|
1376
|
+
var rangeStart = session.getRangeBoundaryPosition(range, true);
|
1377
|
+
var rangeEnd = session.getRangeBoundaryPosition(range, false);
|
1378
|
+
var itStart = backward ? rangeEnd : rangeStart;
|
1379
|
+
var itEnd = backward ? rangeStart : rangeEnd;
|
1380
|
+
|
1381
|
+
return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
|
1382
|
+
}
|
1383
|
+
|
1384
|
+
function getRangeCharacters(session, range, characterOptions) {
|
1385
|
+
|
1386
|
+
var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
|
1387
|
+
while ( (pos = it.next()) ) {
|
1388
|
+
chars.push(pos);
|
1389
|
+
}
|
1390
|
+
|
1391
|
+
it.dispose();
|
1392
|
+
return chars;
|
1393
|
+
}
|
1394
|
+
|
1395
|
+
function isWholeWord(startPos, endPos, wordOptions) {
|
1396
|
+
var range = api.createRange(startPos.node);
|
1397
|
+
range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
|
1398
|
+
var returnVal = !range.expand("word", wordOptions);
|
1399
|
+
range.detach();
|
1400
|
+
return returnVal;
|
1401
|
+
}
|
1402
|
+
|
1403
|
+
function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
|
1404
|
+
var backward = isDirectionBackward(findOptions.direction);
|
1405
|
+
var it = createCharacterIterator(
|
1406
|
+
initialPos,
|
1407
|
+
backward,
|
1408
|
+
initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
|
1409
|
+
findOptions
|
1410
|
+
);
|
1411
|
+
var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
|
1412
|
+
var result, insideRegexMatch;
|
1413
|
+
var returnValue = null;
|
1414
|
+
|
1415
|
+
function handleMatch(startIndex, endIndex) {
|
1416
|
+
var startPos = chars[startIndex].previousVisible();
|
1417
|
+
var endPos = chars[endIndex - 1];
|
1418
|
+
var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));
|
1419
|
+
|
1420
|
+
return {
|
1421
|
+
startPos: startPos,
|
1422
|
+
endPos: endPos,
|
1423
|
+
valid: valid
|
1424
|
+
};
|
1425
|
+
}
|
1426
|
+
|
1427
|
+
while ( (pos = it.next()) ) {
|
1428
|
+
currentChar = pos.character;
|
1429
|
+
if (!isRegex && !findOptions.caseSensitive) {
|
1430
|
+
currentChar = currentChar.toLowerCase();
|
1431
|
+
}
|
1432
|
+
|
1433
|
+
if (backward) {
|
1434
|
+
chars.unshift(pos);
|
1435
|
+
text = currentChar + text;
|
1436
|
+
} else {
|
1437
|
+
chars.push(pos);
|
1438
|
+
text += currentChar;
|
1439
|
+
}
|
1440
|
+
|
1441
|
+
if (isRegex) {
|
1442
|
+
result = searchTerm.exec(text);
|
1443
|
+
if (result) {
|
1444
|
+
if (insideRegexMatch) {
|
1445
|
+
// Check whether the match is now over
|
1446
|
+
matchStartIndex = result.index;
|
1447
|
+
matchEndIndex = matchStartIndex + result[0].length;
|
1448
|
+
if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
|
1449
|
+
returnValue = handleMatch(matchStartIndex, matchEndIndex);
|
1450
|
+
break;
|
1451
|
+
}
|
1452
|
+
} else {
|
1453
|
+
insideRegexMatch = true;
|
1454
|
+
}
|
1455
|
+
}
|
1456
|
+
} else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
|
1457
|
+
returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
|
1458
|
+
break;
|
1459
|
+
}
|
1460
|
+
}
|
1461
|
+
|
1462
|
+
// Check whether regex match extends to the end of the range
|
1463
|
+
if (insideRegexMatch) {
|
1464
|
+
returnValue = handleMatch(matchStartIndex, matchEndIndex);
|
1465
|
+
}
|
1466
|
+
it.dispose();
|
1467
|
+
|
1468
|
+
return returnValue;
|
1469
|
+
}
|
1470
|
+
|
1471
|
+
function createEntryPointFunction(func) {
|
1472
|
+
return function() {
|
1473
|
+
var sessionRunning = !!currentSession;
|
1474
|
+
var session = getSession();
|
1475
|
+
var args = [session].concat( util.toArray(arguments) );
|
1476
|
+
var returnValue = func.apply(this, args);
|
1477
|
+
if (!sessionRunning) {
|
1478
|
+
endSession();
|
1479
|
+
}
|
1480
|
+
return returnValue;
|
1481
|
+
};
|
1482
|
+
}
|
1483
|
+
|
1484
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
1485
|
+
|
1486
|
+
// Extensions to the Rangy Range object
|
1487
|
+
|
1488
|
+
function createRangeBoundaryMover(isStart, collapse) {
|
1489
|
+
/*
|
1490
|
+
Unit can be "character" or "word"
|
1491
|
+
Options:
|
1492
|
+
|
1493
|
+
- includeTrailingSpace
|
1494
|
+
- wordRegex
|
1495
|
+
- tokenizer
|
1496
|
+
- collapseSpaceBeforeLineBreak
|
1497
|
+
*/
|
1498
|
+
return createEntryPointFunction(
|
1499
|
+
function(session, unit, count, moveOptions) {
|
1500
|
+
if (typeof count == "undefined") {
|
1501
|
+
count = unit;
|
1502
|
+
unit = CHARACTER;
|
1503
|
+
}
|
1504
|
+
moveOptions = createOptions(moveOptions, defaultMoveOptions);
|
1505
|
+
var characterOptions = createCharacterOptions(moveOptions.characterOptions);
|
1506
|
+
var wordOptions = createWordOptions(moveOptions.wordOptions);
|
1507
|
+
|
1508
|
+
var boundaryIsStart = isStart;
|
1509
|
+
if (collapse) {
|
1510
|
+
boundaryIsStart = (count >= 0);
|
1511
|
+
this.collapse(!boundaryIsStart);
|
1512
|
+
}
|
1513
|
+
var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, characterOptions, wordOptions);
|
1514
|
+
var newPos = moveResult.position;
|
1515
|
+
this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
|
1516
|
+
return moveResult.unitsMoved;
|
1517
|
+
}
|
1518
|
+
);
|
1519
|
+
}
|
1520
|
+
|
1521
|
+
function createRangeTrimmer(isStart) {
|
1522
|
+
return createEntryPointFunction(
|
1523
|
+
function(session, characterOptions) {
|
1524
|
+
characterOptions = createCharacterOptions(characterOptions);
|
1525
|
+
var pos;
|
1526
|
+
var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
|
1527
|
+
var trimCharCount = 0;
|
1528
|
+
while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
|
1529
|
+
++trimCharCount;
|
1530
|
+
}
|
1531
|
+
it.dispose();
|
1532
|
+
var trimmed = (trimCharCount > 0);
|
1533
|
+
if (trimmed) {
|
1534
|
+
this[isStart ? "moveStart" : "moveEnd"](
|
1535
|
+
"character",
|
1536
|
+
isStart ? trimCharCount : -trimCharCount,
|
1537
|
+
{ characterOptions: characterOptions }
|
1538
|
+
);
|
1539
|
+
}
|
1540
|
+
return trimmed;
|
1541
|
+
}
|
1542
|
+
);
|
1543
|
+
}
|
1544
|
+
|
1545
|
+
extend(api.rangePrototype, {
|
1546
|
+
moveStart: createRangeBoundaryMover(true, false),
|
1547
|
+
|
1548
|
+
moveEnd: createRangeBoundaryMover(false, false),
|
1549
|
+
|
1550
|
+
move: createRangeBoundaryMover(true, true),
|
1551
|
+
|
1552
|
+
trimStart: createRangeTrimmer(true),
|
1553
|
+
|
1554
|
+
trimEnd: createRangeTrimmer(false),
|
1555
|
+
|
1556
|
+
trim: createEntryPointFunction(
|
1557
|
+
function(session, characterOptions) {
|
1558
|
+
var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
|
1559
|
+
return startTrimmed || endTrimmed;
|
1560
|
+
}
|
1561
|
+
),
|
1562
|
+
|
1563
|
+
expand: createEntryPointFunction(
|
1564
|
+
function(session, unit, expandOptions) {
|
1565
|
+
var moved = false;
|
1566
|
+
expandOptions = createOptions(expandOptions, defaultExpandOptions);
|
1567
|
+
var characterOptions = createCharacterOptions(expandOptions.characterOptions);
|
1568
|
+
if (!unit) {
|
1569
|
+
unit = CHARACTER;
|
1570
|
+
}
|
1571
|
+
if (unit == WORD) {
|
1572
|
+
var wordOptions = createWordOptions(expandOptions.wordOptions);
|
1573
|
+
var startPos = session.getRangeBoundaryPosition(this, true);
|
1574
|
+
var endPos = session.getRangeBoundaryPosition(this, false);
|
1575
|
+
|
1576
|
+
var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
|
1577
|
+
var startToken = startTokenizedTextProvider.nextEndToken();
|
1578
|
+
var newStartPos = startToken.chars[0].previousVisible();
|
1579
|
+
var endToken, newEndPos;
|
1580
|
+
|
1581
|
+
if (this.collapsed) {
|
1582
|
+
endToken = startToken;
|
1583
|
+
} else {
|
1584
|
+
var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
|
1585
|
+
endToken = endTokenizedTextProvider.previousStartToken();
|
1586
|
+
}
|
1587
|
+
newEndPos = endToken.chars[endToken.chars.length - 1];
|
1588
|
+
|
1589
|
+
if (!newStartPos.equals(startPos)) {
|
1590
|
+
this.setStart(newStartPos.node, newStartPos.offset);
|
1591
|
+
moved = true;
|
1592
|
+
}
|
1593
|
+
if (newEndPos && !newEndPos.equals(endPos)) {
|
1594
|
+
this.setEnd(newEndPos.node, newEndPos.offset);
|
1595
|
+
moved = true;
|
1596
|
+
}
|
1597
|
+
|
1598
|
+
if (expandOptions.trim) {
|
1599
|
+
if (expandOptions.trimStart) {
|
1600
|
+
moved = this.trimStart(characterOptions) || moved;
|
1601
|
+
}
|
1602
|
+
if (expandOptions.trimEnd) {
|
1603
|
+
moved = this.trimEnd(characterOptions) || moved;
|
1604
|
+
}
|
1605
|
+
}
|
1606
|
+
|
1607
|
+
return moved;
|
1608
|
+
} else {
|
1609
|
+
return this.moveEnd(CHARACTER, 1, expandOptions);
|
1610
|
+
}
|
1611
|
+
}
|
1612
|
+
),
|
1613
|
+
|
1614
|
+
text: createEntryPointFunction(
|
1615
|
+
function(session, characterOptions) {
|
1616
|
+
return this.collapsed ?
|
1617
|
+
"" : getRangeCharacters(session, this, createCharacterOptions(characterOptions)).join("");
|
1618
|
+
}
|
1619
|
+
),
|
1620
|
+
|
1621
|
+
selectCharacters: createEntryPointFunction(
|
1622
|
+
function(session, containerNode, startIndex, endIndex, characterOptions) {
|
1623
|
+
var moveOptions = { characterOptions: characterOptions };
|
1624
|
+
if (!containerNode) {
|
1625
|
+
containerNode = getBody( this.getDocument() );
|
1626
|
+
}
|
1627
|
+
this.selectNodeContents(containerNode);
|
1628
|
+
this.collapse(true);
|
1629
|
+
this.moveStart("character", startIndex, moveOptions);
|
1630
|
+
this.collapse(true);
|
1631
|
+
this.moveEnd("character", endIndex - startIndex, moveOptions);
|
1632
|
+
}
|
1633
|
+
),
|
1634
|
+
|
1635
|
+
// Character indexes are relative to the start of node
|
1636
|
+
toCharacterRange: createEntryPointFunction(
|
1637
|
+
function(session, containerNode, characterOptions) {
|
1638
|
+
if (!containerNode) {
|
1639
|
+
containerNode = getBody( this.getDocument() );
|
1640
|
+
}
|
1641
|
+
var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
|
1642
|
+
var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
|
1643
|
+
var rangeBetween = this.cloneRange();
|
1644
|
+
var startIndex, endIndex;
|
1645
|
+
if (rangeStartsBeforeNode) {
|
1646
|
+
rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
|
1647
|
+
startIndex = -rangeBetween.text(characterOptions).length;
|
1648
|
+
} else {
|
1649
|
+
rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
|
1650
|
+
startIndex = rangeBetween.text(characterOptions).length;
|
1651
|
+
}
|
1652
|
+
endIndex = startIndex + this.text(characterOptions).length;
|
1653
|
+
|
1654
|
+
return {
|
1655
|
+
start: startIndex,
|
1656
|
+
end: endIndex
|
1657
|
+
};
|
1658
|
+
}
|
1659
|
+
),
|
1660
|
+
|
1661
|
+
findText: createEntryPointFunction(
|
1662
|
+
function(session, searchTermParam, findOptions) {
|
1663
|
+
// Set up options
|
1664
|
+
findOptions = createOptions(findOptions, defaultFindOptions);
|
1665
|
+
|
1666
|
+
// Create word options if we're matching whole words only
|
1667
|
+
if (findOptions.wholeWordsOnly) {
|
1668
|
+
findOptions.wordOptions = createWordOptions(findOptions.wordOptions);
|
1669
|
+
|
1670
|
+
// We don't ever want trailing spaces for search results
|
1671
|
+
findOptions.wordOptions.includeTrailingSpace = false;
|
1672
|
+
}
|
1673
|
+
|
1674
|
+
var backward = isDirectionBackward(findOptions.direction);
|
1675
|
+
|
1676
|
+
// Create a range representing the search scope if none was provided
|
1677
|
+
var searchScopeRange = findOptions.withinRange;
|
1678
|
+
if (!searchScopeRange) {
|
1679
|
+
searchScopeRange = api.createRange();
|
1680
|
+
searchScopeRange.selectNodeContents(this.getDocument());
|
1681
|
+
}
|
1682
|
+
|
1683
|
+
// Examine and prepare the search term
|
1684
|
+
var searchTerm = searchTermParam, isRegex = false;
|
1685
|
+
if (typeof searchTerm == "string") {
|
1686
|
+
if (!findOptions.caseSensitive) {
|
1687
|
+
searchTerm = searchTerm.toLowerCase();
|
1688
|
+
}
|
1689
|
+
} else {
|
1690
|
+
isRegex = true;
|
1691
|
+
}
|
1692
|
+
|
1693
|
+
var initialPos = session.getRangeBoundaryPosition(this, !backward);
|
1694
|
+
|
1695
|
+
// Adjust initial position if it lies outside the search scope
|
1696
|
+
var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);
|
1697
|
+
|
1698
|
+
if (comparison === -1) {
|
1699
|
+
initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
|
1700
|
+
} else if (comparison === 1) {
|
1701
|
+
initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
|
1702
|
+
}
|
1703
|
+
|
1704
|
+
var pos = initialPos;
|
1705
|
+
var wrappedAround = false;
|
1706
|
+
|
1707
|
+
// Try to find a match and ignore invalid ones
|
1708
|
+
var findResult;
|
1709
|
+
while (true) {
|
1710
|
+
findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);
|
1711
|
+
|
1712
|
+
if (findResult) {
|
1713
|
+
if (findResult.valid) {
|
1714
|
+
this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
|
1715
|
+
return true;
|
1716
|
+
} else {
|
1717
|
+
// We've found a match that is not a whole word, so we carry on searching from the point immediately
|
1718
|
+
// after the match
|
1719
|
+
pos = backward ? findResult.startPos : findResult.endPos;
|
1720
|
+
}
|
1721
|
+
} else if (findOptions.wrap && !wrappedAround) {
|
1722
|
+
// No result found but we're wrapping around and limiting the scope to the unsearched part of the range
|
1723
|
+
searchScopeRange = searchScopeRange.cloneRange();
|
1724
|
+
pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
|
1725
|
+
searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
|
1726
|
+
wrappedAround = true;
|
1727
|
+
} else {
|
1728
|
+
// Nothing found and we can't wrap around, so we're done
|
1729
|
+
return false;
|
1730
|
+
}
|
1731
|
+
}
|
1732
|
+
}
|
1733
|
+
),
|
1734
|
+
|
1735
|
+
pasteHtml: function(html) {
|
1736
|
+
this.deleteContents();
|
1737
|
+
if (html) {
|
1738
|
+
var frag = this.createContextualFragment(html);
|
1739
|
+
var lastChild = frag.lastChild;
|
1740
|
+
this.insertNode(frag);
|
1741
|
+
this.collapseAfter(lastChild);
|
1742
|
+
}
|
1743
|
+
}
|
1744
|
+
});
|
1745
|
+
|
1746
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
1747
|
+
|
1748
|
+
// Extensions to the Rangy Selection object
|
1749
|
+
|
1750
|
+
function createSelectionTrimmer(methodName) {
|
1751
|
+
return createEntryPointFunction(
|
1752
|
+
function(session, characterOptions) {
|
1753
|
+
var trimmed = false;
|
1754
|
+
this.changeEachRange(function(range) {
|
1755
|
+
trimmed = range[methodName](characterOptions) || trimmed;
|
1756
|
+
});
|
1757
|
+
return trimmed;
|
1758
|
+
}
|
1759
|
+
);
|
1760
|
+
}
|
1761
|
+
|
1762
|
+
extend(api.selectionPrototype, {
|
1763
|
+
expand: createEntryPointFunction(
|
1764
|
+
function(session, unit, expandOptions) {
|
1765
|
+
this.changeEachRange(function(range) {
|
1766
|
+
range.expand(unit, expandOptions);
|
1767
|
+
});
|
1768
|
+
}
|
1769
|
+
),
|
1770
|
+
|
1771
|
+
move: createEntryPointFunction(
|
1772
|
+
function(session, unit, count, options) {
|
1773
|
+
var unitsMoved = 0;
|
1774
|
+
if (this.focusNode) {
|
1775
|
+
this.collapse(this.focusNode, this.focusOffset);
|
1776
|
+
var range = this.getRangeAt(0);
|
1777
|
+
if (!options) {
|
1778
|
+
options = {};
|
1779
|
+
}
|
1780
|
+
options.characterOptions = createCaretCharacterOptions(options.characterOptions);
|
1781
|
+
unitsMoved = range.move(unit, count, options);
|
1782
|
+
this.setSingleRange(range);
|
1783
|
+
}
|
1784
|
+
return unitsMoved;
|
1785
|
+
}
|
1786
|
+
),
|
1787
|
+
|
1788
|
+
trimStart: createSelectionTrimmer("trimStart"),
|
1789
|
+
trimEnd: createSelectionTrimmer("trimEnd"),
|
1790
|
+
trim: createSelectionTrimmer("trim"),
|
1791
|
+
|
1792
|
+
selectCharacters: createEntryPointFunction(
|
1793
|
+
function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
|
1794
|
+
var range = api.createRange(containerNode);
|
1795
|
+
range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
|
1796
|
+
this.setSingleRange(range, direction);
|
1797
|
+
}
|
1798
|
+
),
|
1799
|
+
|
1800
|
+
saveCharacterRanges: createEntryPointFunction(
|
1801
|
+
function(session, containerNode, characterOptions) {
|
1802
|
+
var ranges = this.getAllRanges(), rangeCount = ranges.length;
|
1803
|
+
var rangeInfos = [];
|
1804
|
+
|
1805
|
+
var backward = rangeCount == 1 && this.isBackward();
|
1806
|
+
|
1807
|
+
for (var i = 0, len = ranges.length; i < len; ++i) {
|
1808
|
+
rangeInfos[i] = {
|
1809
|
+
characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
|
1810
|
+
backward: backward,
|
1811
|
+
characterOptions: characterOptions
|
1812
|
+
};
|
1813
|
+
}
|
1814
|
+
|
1815
|
+
return rangeInfos;
|
1816
|
+
}
|
1817
|
+
),
|
1818
|
+
|
1819
|
+
restoreCharacterRanges: createEntryPointFunction(
|
1820
|
+
function(session, containerNode, saved) {
|
1821
|
+
this.removeAllRanges();
|
1822
|
+
for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
|
1823
|
+
rangeInfo = saved[i];
|
1824
|
+
characterRange = rangeInfo.characterRange;
|
1825
|
+
range = api.createRange(containerNode);
|
1826
|
+
range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
|
1827
|
+
this.addRange(range, rangeInfo.backward);
|
1828
|
+
}
|
1829
|
+
}
|
1830
|
+
),
|
1831
|
+
|
1832
|
+
text: createEntryPointFunction(
|
1833
|
+
function(session, characterOptions) {
|
1834
|
+
var rangeTexts = [];
|
1835
|
+
for (var i = 0, len = this.rangeCount; i < len; ++i) {
|
1836
|
+
rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
|
1837
|
+
}
|
1838
|
+
return rangeTexts.join("");
|
1839
|
+
}
|
1840
|
+
)
|
1841
|
+
});
|
1842
|
+
|
1843
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
1844
|
+
|
1845
|
+
// Extensions to the core rangy object
|
1846
|
+
|
1847
|
+
api.innerText = function(el, characterOptions) {
|
1848
|
+
var range = api.createRange(el);
|
1849
|
+
range.selectNodeContents(el);
|
1850
|
+
var text = range.text(characterOptions);
|
1851
|
+
range.detach();
|
1852
|
+
return text;
|
1853
|
+
};
|
1854
|
+
|
1855
|
+
api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
|
1856
|
+
var session = getSession();
|
1857
|
+
iteratorOptions = createOptions(iteratorOptions, defaultWordIteratorOptions);
|
1858
|
+
var characterOptions = createCharacterOptions(iteratorOptions.characterOptions);
|
1859
|
+
var wordOptions = createWordOptions(iteratorOptions.wordOptions);
|
1860
|
+
var startPos = session.getPosition(startNode, startOffset);
|
1861
|
+
var tokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
|
1862
|
+
var backward = isDirectionBackward(iteratorOptions.direction);
|
1863
|
+
|
1864
|
+
return {
|
1865
|
+
next: function() {
|
1866
|
+
return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
|
1867
|
+
},
|
1868
|
+
|
1869
|
+
dispose: function() {
|
1870
|
+
tokenizedTextProvider.dispose();
|
1871
|
+
this.next = function() {};
|
1872
|
+
}
|
1873
|
+
};
|
1874
|
+
};
|
1875
|
+
|
1876
|
+
/*----------------------------------------------------------------------------------------------------------------*/
|
1877
|
+
|
1878
|
+
api.noMutation = function(func) {
|
1879
|
+
var session = getSession();
|
1880
|
+
func(session);
|
1881
|
+
endSession();
|
1882
|
+
};
|
1883
|
+
|
1884
|
+
api.noMutation.createEntryPointFunction = createEntryPointFunction;
|
1885
|
+
|
1886
|
+
api.textRange = {
|
1887
|
+
isBlockNode: isBlockNode,
|
1888
|
+
isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,
|
1889
|
+
|
1890
|
+
createPosition: createEntryPointFunction(
|
1891
|
+
function(session, node, offset) {
|
1892
|
+
return session.getPosition(node, offset);
|
1893
|
+
}
|
1894
|
+
)
|
1895
|
+
};
|
1896
|
+
});
|