rangy-rails 1.3alpha.804.0 → 1.3.1.pre.dev

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,498 +1,619 @@
1
1
  /**
2
2
  * Highlighter module for Rangy, a cross-browser JavaScript range and selection library
3
- * http://code.google.com/p/rangy/
3
+ * https://github.com/timdown/rangy
4
4
  *
5
- * Depends on Rangy core, TextRange and CssClassApplier modules.
5
+ * Depends on Rangy core, ClassApplier and optionally TextRange modules.
6
6
  *
7
- * Copyright 2013, Tim Down
7
+ * Copyright 2015, Tim Down
8
8
  * Licensed under the MIT license.
9
- * Version: 1.3alpha.804
10
- * Build date: 8 December 2013
9
+ * Version: 1.3.1-dev
10
+ * Build date: 20 May 2015
11
11
  */
12
- rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) {
13
- var dom = api.dom;
14
- var contains = dom.arrayContains;
15
- var getBody = dom.getBody;
16
-
17
- // Puts highlights in order, last in document first.
18
- function compareHighlights(h1, h2) {
19
- return h1.characterRange.start - h2.characterRange.start;
12
+ (function(factory, root) {
13
+ if (typeof define == "function" && define.amd) {
14
+ // AMD. Register as an anonymous module with a dependency on Rangy.
15
+ define(["./rangy-core"], factory);
16
+ } else if (typeof module != "undefined" && typeof exports == "object") {
17
+ // Node/CommonJS style
18
+ module.exports = factory( require("rangy") );
19
+ } else {
20
+ // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
21
+ factory(root.rangy);
20
22
  }
23
+ })(function(rangy) {
24
+ rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) {
25
+ var dom = api.dom;
26
+ var contains = dom.arrayContains;
27
+ var getBody = dom.getBody;
28
+ var createOptions = api.util.createOptions;
29
+ var forEach = api.util.forEach;
30
+ var nextHighlightId = 1;
31
+
32
+ // Puts highlights in order, last in document first.
33
+ function compareHighlights(h1, h2) {
34
+ return h1.characterRange.start - h2.characterRange.start;
35
+ }
21
36
 
22
- var forEach = [].forEach ?
23
- function(arr, func) {
24
- arr.forEach(func);
25
- } :
26
- function(arr, func) {
27
- for (var i = 0, len = arr.length; i < len; ++i) {
28
- func( arr[i] );
29
- }
30
- };
37
+ function getContainerElement(doc, id) {
38
+ return id ? doc.getElementById(id) : getBody(doc);
39
+ }
31
40
 
32
- var nextHighlightId = 1;
41
+ /*----------------------------------------------------------------------------------------------------------------*/
33
42
 
34
- /*----------------------------------------------------------------------------------------------------------------*/
43
+ var highlighterTypes = {};
35
44
 
36
- var highlighterTypes = {};
45
+ function HighlighterType(type, converterCreator) {
46
+ this.type = type;
47
+ this.converterCreator = converterCreator;
48
+ }
37
49
 
38
- function HighlighterType(type, converterCreator) {
39
- this.type = type;
40
- this.converterCreator = converterCreator;
41
- }
50
+ HighlighterType.prototype.create = function() {
51
+ var converter = this.converterCreator();
52
+ converter.type = this.type;
53
+ return converter;
54
+ };
55
+
56
+ function registerHighlighterType(type, converterCreator) {
57
+ highlighterTypes[type] = new HighlighterType(type, converterCreator);
58
+ }
42
59
 
43
- HighlighterType.prototype.create = function() {
44
- var converter = this.converterCreator();
45
- converter.type = this.type;
46
- return converter;
47
- };
60
+ function getConverter(type) {
61
+ var highlighterType = highlighterTypes[type];
62
+ if (highlighterType instanceof HighlighterType) {
63
+ return highlighterType.create();
64
+ } else {
65
+ throw new Error("Highlighter type '" + type + "' is not valid");
66
+ }
67
+ }
48
68
 
49
- function registerHighlighterType(type, converterCreator) {
50
- highlighterTypes[type] = new HighlighterType(type, converterCreator);
51
- }
69
+ api.registerHighlighterType = registerHighlighterType;
52
70
 
53
- function getConverter(type) {
54
- var highlighterType = highlighterTypes[type];
55
- if (highlighterType instanceof HighlighterType) {
56
- return highlighterType.create();
57
- } else {
58
- throw new Error("Highlighter type '" + type + "' is not valid");
71
+ /*----------------------------------------------------------------------------------------------------------------*/
72
+
73
+ function CharacterRange(start, end) {
74
+ this.start = start;
75
+ this.end = end;
59
76
  }
60
- }
61
77
 
62
- api.registerHighlighterType = registerHighlighterType;
78
+ CharacterRange.prototype = {
79
+ intersects: function(charRange) {
80
+ return this.start < charRange.end && this.end > charRange.start;
81
+ },
63
82
 
64
- /*----------------------------------------------------------------------------------------------------------------*/
83
+ isContiguousWith: function(charRange) {
84
+ return this.start == charRange.end || this.end == charRange.start;
85
+ },
65
86
 
66
- function CharacterRange(start, end) {
67
- this.start = start;
68
- this.end = end;
69
- }
87
+ union: function(charRange) {
88
+ return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end));
89
+ },
70
90
 
71
- CharacterRange.prototype = {
72
- intersects: function(charRange) {
73
- return this.start < charRange.end && this.end > charRange.start;
74
- },
75
-
76
- union: function(charRange) {
77
- return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end));
78
- },
79
-
80
- intersection: function(charRange) {
81
- return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end));
82
- },
83
-
84
- toString: function() {
85
- return "[CharacterRange(" + this.start + ", " + this.end + ")]";
86
- }
87
- };
88
-
89
- CharacterRange.fromCharacterRange = function(charRange) {
90
- return new CharacterRange(charRange.start, charRange.end);
91
- };
92
-
93
- /*----------------------------------------------------------------------------------------------------------------*/
94
-
95
- var textContentConverter = {
96
- rangeToCharacterRange: function(range, containerNode) {
97
- var bookmark = range.getBookmark(containerNode);
98
- return new CharacterRange(bookmark.start, bookmark.end);
99
- },
100
-
101
- characterRangeToRange: function(doc, characterRange, containerNode) {
102
- var range = api.createRange(doc);
103
- range.moveToBookmark({
104
- start: characterRange.start,
105
- end: characterRange.end,
106
- containerNode: containerNode
107
- });
108
-
109
- return range;
110
- },
111
-
112
- serializeSelection: function(selection, containerNode) {
113
- var ranges = selection.getAllRanges(), rangeCount = ranges.length;
114
- var rangeInfos = [];
115
-
116
- var backward = rangeCount == 1 && selection.isBackward();
117
-
118
- for (var i = 0, len = ranges.length; i < len; ++i) {
119
- rangeInfos[i] = {
120
- characterRange: this.rangeToCharacterRange(ranges[i], containerNode),
121
- backward: backward
122
- };
123
- }
91
+ intersection: function(charRange) {
92
+ return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end));
93
+ },
94
+
95
+ getComplements: function(charRange) {
96
+ var ranges = [];
97
+ if (this.start >= charRange.start) {
98
+ if (this.end <= charRange.end) {
99
+ return [];
100
+ }
101
+ ranges.push(new CharacterRange(charRange.end, this.end));
102
+ } else {
103
+ ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start)));
104
+ if (this.end > charRange.end) {
105
+ ranges.push(new CharacterRange(charRange.end, this.end));
106
+ }
107
+ }
108
+ return ranges;
109
+ },
124
110
 
125
- return rangeInfos;
126
- },
127
-
128
- restoreSelection: function(selection, savedSelection, containerNode) {
129
- selection.removeAllRanges();
130
- var doc = selection.win.document;
131
- for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) {
132
- rangeInfo = savedSelection[i];
133
- characterRange = rangeInfo.characterRange;
134
- range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode);
135
- selection.addRange(range, rangeInfo.backward);
111
+ toString: function() {
112
+ return "[CharacterRange(" + this.start + ", " + this.end + ")]";
136
113
  }
137
- }
138
- };
114
+ };
139
115
 
140
- registerHighlighterType("textContent", function() {
141
- return textContentConverter;
142
- });
116
+ CharacterRange.fromCharacterRange = function(charRange) {
117
+ return new CharacterRange(charRange.start, charRange.end);
118
+ };
143
119
 
144
- /*----------------------------------------------------------------------------------------------------------------*/
120
+ /*----------------------------------------------------------------------------------------------------------------*/
145
121
 
146
- // Lazily load the TextRange-based converter so that the dependency is only checked when required.
147
- registerHighlighterType("TextRange", (function() {
148
- var converter;
122
+ var textContentConverter = {
123
+ rangeToCharacterRange: function(range, containerNode) {
124
+ var bookmark = range.getBookmark(containerNode);
125
+ return new CharacterRange(bookmark.start, bookmark.end);
126
+ },
149
127
 
150
- return function() {
151
- if (!converter) {
152
- // Test that textRangeModule exists and is supported
153
- var textRangeModule = api.modules.TextRange;
154
- if (!textRangeModule) {
155
- throw new Error("TextRange module is missing.");
156
- } else if (!textRangeModule.supported) {
157
- throw new Error("TextRange module is present but not supported.");
128
+ characterRangeToRange: function(doc, characterRange, containerNode) {
129
+ var range = api.createRange(doc);
130
+ range.moveToBookmark({
131
+ start: characterRange.start,
132
+ end: characterRange.end,
133
+ containerNode: containerNode
134
+ });
135
+
136
+ return range;
137
+ },
138
+
139
+ serializeSelection: function(selection, containerNode) {
140
+ var ranges = selection.getAllRanges(), rangeCount = ranges.length;
141
+ var rangeInfos = [];
142
+
143
+ var backward = rangeCount == 1 && selection.isBackward();
144
+
145
+ for (var i = 0, len = ranges.length; i < len; ++i) {
146
+ rangeInfos[i] = {
147
+ characterRange: this.rangeToCharacterRange(ranges[i], containerNode),
148
+ backward: backward
149
+ };
158
150
  }
159
151
 
160
- converter = {
161
- rangeToCharacterRange: function(range, containerNode) {
162
- return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) );
163
- },
152
+ return rangeInfos;
153
+ },
154
+
155
+ restoreSelection: function(selection, savedSelection, containerNode) {
156
+ selection.removeAllRanges();
157
+ var doc = selection.win.document;
158
+ for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) {
159
+ rangeInfo = savedSelection[i];
160
+ characterRange = rangeInfo.characterRange;
161
+ range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode);
162
+ selection.addRange(range, rangeInfo.backward);
163
+ }
164
+ }
165
+ };
166
+
167
+ registerHighlighterType("textContent", function() {
168
+ return textContentConverter;
169
+ });
164
170
 
165
- characterRangeToRange: function(doc, characterRange, containerNode) {
166
- var range = api.createRange(doc);
167
- range.selectCharacters(containerNode, characterRange.start, characterRange.end);
168
- return range;
169
- },
171
+ /*----------------------------------------------------------------------------------------------------------------*/
170
172
 
171
- serializeSelection: function(selection, containerNode) {
172
- return selection.saveCharacterRanges(containerNode);
173
- },
173
+ // Lazily load the TextRange-based converter so that the dependency is only checked when required.
174
+ registerHighlighterType("TextRange", (function() {
175
+ var converter;
174
176
 
175
- restoreSelection: function(selection, savedSelection, containerNode) {
176
- selection.restoreCharacterRanges(containerNode, savedSelection);
177
+ return function() {
178
+ if (!converter) {
179
+ // Test that textRangeModule exists and is supported
180
+ var textRangeModule = api.modules.TextRange;
181
+ if (!textRangeModule) {
182
+ throw new Error("TextRange module is missing.");
183
+ } else if (!textRangeModule.supported) {
184
+ throw new Error("TextRange module is present but not supported.");
177
185
  }
178
- };
179
- }
180
186
 
181
- return converter;
182
- };
183
- })());
187
+ converter = {
188
+ rangeToCharacterRange: function(range, containerNode) {
189
+ return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) );
190
+ },
184
191
 
185
- /*----------------------------------------------------------------------------------------------------------------*/
192
+ characterRangeToRange: function(doc, characterRange, containerNode) {
193
+ var range = api.createRange(doc);
194
+ range.selectCharacters(containerNode, characterRange.start, characterRange.end);
195
+ return range;
196
+ },
186
197
 
187
- function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) {
188
- if (id) {
189
- this.id = id;
190
- nextHighlightId = Math.max(nextHighlightId, id + 1);
191
- } else {
192
- this.id = nextHighlightId++;
193
- }
194
- this.characterRange = characterRange;
195
- this.doc = doc;
196
- this.classApplier = classApplier;
197
- this.converter = converter;
198
- this.containerElementId = containerElementId || null;
199
- this.applied = false;
200
- }
198
+ serializeSelection: function(selection, containerNode) {
199
+ return selection.saveCharacterRanges(containerNode);
200
+ },
201
+
202
+ restoreSelection: function(selection, savedSelection, containerNode) {
203
+ selection.restoreCharacterRanges(containerNode, savedSelection);
204
+ }
205
+ };
206
+ }
207
+
208
+ return converter;
209
+ };
210
+ })());
201
211
 
202
- Highlight.prototype = {
203
- getContainerElement: function() {
204
- return this.containerElementId ? this.doc.getElementById(this.containerElementId) : getBody(this.doc);
205
- },
206
-
207
- getRange: function() {
208
- return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement());
209
- },
210
-
211
- fromRange: function(range) {
212
- this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement());
213
- },
214
-
215
- getText: function() {
216
- return this.getRange().toString();
217
- },
218
-
219
- containsElement: function(el) {
220
- return this.getRange().containsNodeContents(el.firstChild);
221
- },
222
-
223
- unapply: function() {
224
- this.classApplier.undoToRange(this.getRange());
212
+ /*----------------------------------------------------------------------------------------------------------------*/
213
+
214
+ function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) {
215
+ if (id) {
216
+ this.id = id;
217
+ nextHighlightId = Math.max(nextHighlightId, id + 1);
218
+ } else {
219
+ this.id = nextHighlightId++;
220
+ }
221
+ this.characterRange = characterRange;
222
+ this.doc = doc;
223
+ this.classApplier = classApplier;
224
+ this.converter = converter;
225
+ this.containerElementId = containerElementId || null;
225
226
  this.applied = false;
226
- },
227
-
228
- apply: function() {
229
- this.classApplier.applyToRange(this.getRange());
230
- this.applied = true;
231
- },
232
-
233
- getHighlightElements: function() {
234
- return this.classApplier.getElementsWithClassIntersectingRange(this.getRange());
235
- },
236
-
237
- toString: function() {
238
- return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.cssClass + ", character range: " +
239
- this.characterRange.start + " - " + this.characterRange.end + ")]";
240
227
  }
241
- };
242
228
 
243
- /*----------------------------------------------------------------------------------------------------------------*/
229
+ Highlight.prototype = {
230
+ getContainerElement: function() {
231
+ return getContainerElement(this.doc, this.containerElementId);
232
+ },
244
233
 
245
- function Highlighter(doc, type) {
246
- type = type || "textContent";
247
- this.doc = doc || document;
248
- this.classAppliers = {};
249
- this.highlights = [];
250
- this.converter = getConverter(type);
251
- }
234
+ getRange: function() {
235
+ return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement());
236
+ },
252
237
 
253
- Highlighter.prototype = {
254
- addClassApplier: function(classApplier) {
255
- this.classAppliers[classApplier.cssClass] = classApplier;
256
- },
238
+ fromRange: function(range) {
239
+ this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement());
240
+ },
257
241
 
258
- getHighlightForElement: function(el) {
259
- var highlights = this.highlights;
260
- for (var i = 0, len = highlights.length; i < len; ++i) {
261
- if (highlights[i].containsElement(el)) {
262
- return highlights[i];
263
- }
264
- }
265
- return null;
266
- },
267
-
268
- removeHighlights: function(highlights) {
269
- for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) {
270
- highlight = this.highlights[i];
271
- if (contains(highlights, highlight)) {
272
- highlight.unapply();
273
- this.highlights.splice(i--, 1);
274
- }
242
+ getText: function() {
243
+ return this.getRange().toString();
244
+ },
245
+
246
+ containsElement: function(el) {
247
+ return this.getRange().containsNodeContents(el.firstChild);
248
+ },
249
+
250
+ unapply: function() {
251
+ this.classApplier.undoToRange(this.getRange());
252
+ this.applied = false;
253
+ },
254
+
255
+ apply: function() {
256
+ this.classApplier.applyToRange(this.getRange());
257
+ this.applied = true;
258
+ },
259
+
260
+ getHighlightElements: function() {
261
+ return this.classApplier.getElementsWithClassIntersectingRange(this.getRange());
262
+ },
263
+
264
+ toString: function() {
265
+ return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " +
266
+ this.characterRange.start + " - " + this.characterRange.end + ")]";
275
267
  }
276
- },
268
+ };
277
269
 
278
- removeAllHighlights: function() {
279
- this.removeHighlights(this.highlights);
280
- },
270
+ /*----------------------------------------------------------------------------------------------------------------*/
281
271
 
282
- getIntersectingHighlights: function(ranges) {
283
- // Test each range against each of the highlighted ranges to see whether they overlap
284
- var intersectingHighlights = [], highlights = this.highlights, converter = this.converter;
285
- forEach(ranges, function(range) {
286
- //var selCharRange = converter.rangeToCharacterRange(range);
287
- forEach(highlights, function(highlight) {
288
- if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) {
289
- intersectingHighlights.push(highlight);
272
+ function Highlighter(doc, type) {
273
+ type = type || "textContent";
274
+ this.doc = doc || document;
275
+ this.classAppliers = {};
276
+ this.highlights = [];
277
+ this.converter = getConverter(type);
278
+ }
279
+
280
+ Highlighter.prototype = {
281
+ addClassApplier: function(classApplier) {
282
+ this.classAppliers[classApplier.className] = classApplier;
283
+ },
284
+
285
+ getHighlightForElement: function(el) {
286
+ var highlights = this.highlights;
287
+ for (var i = 0, len = highlights.length; i < len; ++i) {
288
+ if (highlights[i].containsElement(el)) {
289
+ return highlights[i];
290
290
  }
291
- });
292
- });
293
-
294
- return intersectingHighlights;
295
- },
296
-
297
- highlightCharacterRanges: function(className, charRanges, containerElementId) {
298
- var i, len, j;
299
- var highlights = this.highlights;
300
- var converter = this.converter;
301
- var doc = this.doc;
302
- var highlightsToRemove = [];
303
- var classApplier = this.classAppliers[className];
304
- containerElementId = containerElementId || null;
305
-
306
- var containerElement, containerElementRange, containerElementCharRange;
307
- if (containerElementId) {
308
- containerElement = this.doc.getElementById(containerElementId);
309
- if (containerElement) {
310
- containerElementRange = api.createRange(this.doc);
311
- containerElementRange.selectNodeContents(containerElement);
312
- containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length);
313
- containerElementRange.detach();
314
291
  }
315
- }
292
+ return null;
293
+ },
294
+
295
+ removeHighlights: function(highlights) {
296
+ for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) {
297
+ highlight = this.highlights[i];
298
+ if (contains(highlights, highlight)) {
299
+ highlight.unapply();
300
+ this.highlights.splice(i--, 1);
301
+ }
302
+ }
303
+ },
304
+
305
+ removeAllHighlights: function() {
306
+ this.removeHighlights(this.highlights);
307
+ },
308
+
309
+ getIntersectingHighlights: function(ranges) {
310
+ // Test each range against each of the highlighted ranges to see whether they overlap
311
+ var intersectingHighlights = [], highlights = this.highlights;
312
+ forEach(ranges, function(range) {
313
+ //var selCharRange = converter.rangeToCharacterRange(range);
314
+ forEach(highlights, function(highlight) {
315
+ if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) {
316
+ intersectingHighlights.push(highlight);
317
+ }
318
+ });
319
+ });
320
+
321
+ return intersectingHighlights;
322
+ },
316
323
 
317
- var charRange, highlightCharRange, merged;
318
- for (i = 0, len = charRanges.length; i < len; ++i) {
319
- charRange = charRanges[i];
320
- merged = false;
324
+ highlightCharacterRanges: function(className, charRanges, options) {
325
+ var i, len, j;
326
+ var highlights = this.highlights;
327
+ var converter = this.converter;
328
+ var doc = this.doc;
329
+ var highlightsToRemove = [];
330
+ var classApplier = className ? this.classAppliers[className] : null;
321
331
 
322
- // Restrict character range to container element, if it exists
323
- if (containerElementCharRange) {
324
- charRange = charRange.intersection(containerElementCharRange);
332
+ options = createOptions(options, {
333
+ containerElementId: null,
334
+ exclusive: true
335
+ });
336
+
337
+ var containerElementId = options.containerElementId;
338
+ var exclusive = options.exclusive;
339
+
340
+ var containerElement, containerElementRange, containerElementCharRange;
341
+ if (containerElementId) {
342
+ containerElement = this.doc.getElementById(containerElementId);
343
+ if (containerElement) {
344
+ containerElementRange = api.createRange(this.doc);
345
+ containerElementRange.selectNodeContents(containerElement);
346
+ containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length);
347
+ }
325
348
  }
326
349
 
327
- // Check for intersection with existing highlights. For each intersection, create a new highlight
328
- // which is the union of the highlight range and the selected range
329
- for (j = 0; j < highlights.length; ++j) {
330
- if (containerElementId == highlights[j].containerElementId) {
331
- highlightCharRange = highlights[j].characterRange;
350
+ var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight;
351
+
352
+ for (i = 0, len = charRanges.length; i < len; ++i) {
353
+ charRange = charRanges[i];
354
+ highlightsToKeep = [];
355
+
356
+ // Restrict character range to container element, if it exists
357
+ if (containerElementCharRange) {
358
+ charRange = charRange.intersection(containerElementCharRange);
359
+ }
360
+
361
+ // Ignore empty ranges
362
+ if (charRange.start == charRange.end) {
363
+ continue;
364
+ }
365
+
366
+ // Check for intersection with existing highlights. For each intersection, create a new highlight
367
+ // which is the union of the highlight range and the selected range
368
+ for (j = 0; j < highlights.length; ++j) {
369
+ removeHighlight = false;
370
+
371
+ if (containerElementId == highlights[j].containerElementId) {
372
+ highlightCharRange = highlights[j].characterRange;
373
+ isSameClassApplier = (classApplier == highlights[j].classApplier);
374
+ splitHighlight = !isSameClassApplier && exclusive;
375
+
376
+ // Replace the existing highlight if it needs to be:
377
+ // 1. merged (isSameClassApplier)
378
+ // 2. partially or entirely erased (className === null)
379
+ // 3. partially or entirely replaced (isSameClassApplier == false && exclusive == true)
380
+ if ( (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) &&
381
+ (isSameClassApplier || splitHighlight) ) {
382
+
383
+ // Remove existing highlights, keeping the unselected parts
384
+ if (splitHighlight) {
385
+ forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) {
386
+ highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) );
387
+ });
388
+ }
389
+
390
+ removeHighlight = true;
391
+ if (isSameClassApplier) {
392
+ charRange = highlightCharRange.union(charRange);
393
+ }
394
+ }
395
+ }
332
396
 
333
- if (highlightCharRange.intersects(charRange)) {
334
- // Replace the existing highlight in the list of current highlights and add it to the list for
335
- // removal
397
+ if (removeHighlight) {
336
398
  highlightsToRemove.push(highlights[j]);
337
399
  highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId);
400
+ } else {
401
+ highlightsToKeep.push(highlights[j]);
338
402
  }
339
403
  }
340
- }
341
404
 
342
- if (!merged) {
343
- highlights.push( new Highlight(doc, charRange, classApplier, converter, null, containerElementId) );
344
- }
345
- }
346
-
347
- // Remove the old highlights
348
- forEach(highlightsToRemove, function(highlightToRemove) {
349
- highlightToRemove.unapply();
350
- });
351
-
352
- // Apply new highlights
353
- var newHighlights = [];
354
- forEach(highlights, function(highlight) {
355
- if (!highlight.applied) {
356
- highlight.apply();
357
- newHighlights.push(highlight);
405
+ // Add new range
406
+ if (classApplier) {
407
+ highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId));
408
+ }
409
+ this.highlights = highlights = highlightsToKeep;
358
410
  }
359
- });
360
-
361
- return newHighlights;
362
- },
363
-
364
- highlightRanges: function(className, ranges, containerElement) {
365
- var selCharRanges = [];
366
- var converter = this.converter;
367
- var containerElementId = containerElement ? containerElement.id : null;
368
- var containerElementRange;
369
- if (containerElement) {
370
- containerElementRange = api.createRange(containerElement);
371
- containerElementRange.selectNodeContents(containerElement);
372
- }
373
-
374
- forEach(ranges, function(range) {
375
- var scopedRange = containerElement ? containerElementRange.intersection(range) : range;
376
- selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) );
377
- });
378
-
379
- return this.highlightCharacterRanges(selCharRanges, ranges, containerElementId);
380
- },
381
-
382
- highlightSelection: function(className, selection, containerElementId) {
383
- var converter = this.converter;
384
- selection = selection || api.getSelection();
385
- var classApplier = this.classAppliers[className];
386
- var doc = selection.win.document;
387
- var containerElement = containerElementId ? doc.getElementById(containerElementId) : getBody(doc);
388
-
389
- if (!classApplier) {
390
- throw new Error("No class applier found for class '" + className + "'");
391
- }
392
411
 
393
- // Store the existing selection as character ranges
394
- var serializedSelection = converter.serializeSelection(selection, containerElement);
395
-
396
- // Create an array of selected character ranges
397
- var selCharRanges = [];
398
- forEach(serializedSelection, function(rangeInfo) {
399
- selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) );
400
- });
401
-
402
- var newHighlights = this.highlightCharacterRanges(className, selCharRanges, containerElementId);
403
-
404
- // Restore selection
405
- converter.restoreSelection(selection, serializedSelection, containerElement);
406
-
407
- return newHighlights;
408
- },
409
-
410
- unhighlightSelection: function(selection) {
411
- selection = selection || api.getSelection();
412
- var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() );
413
- this.removeHighlights(intersectingHighlights);
414
- selection.removeAllRanges();
415
- return intersectingHighlights;
416
- },
417
-
418
- getHighlightsInSelection: function(selection) {
419
- selection = selection || api.getSelection();
420
- return this.getIntersectingHighlights(selection.getAllRanges());
421
- },
422
-
423
- selectionOverlapsHighlight: function(selection) {
424
- return this.getHighlightsInSelection(selection).length > 0;
425
- },
426
-
427
- serialize: function(options) {
428
- var highlights = this.highlights;
429
- highlights.sort(compareHighlights);
430
- var serializedHighlights = ["type:" + this.converter.type];
431
-
432
- forEach(highlights, function(highlight) {
433
- var characterRange = highlight.characterRange;
434
- var parts = [
435
- characterRange.start,
436
- characterRange.end,
437
- highlight.id,
438
- highlight.classApplier.cssClass,
439
- highlight.containerElementId
440
- ];
441
- if (options && options.serializeHighlightText) {
442
- parts.push(highlight.getText());
412
+ // Remove the old highlights
413
+ forEach(highlightsToRemove, function(highlightToRemove) {
414
+ highlightToRemove.unapply();
415
+ });
416
+
417
+ // Apply new highlights
418
+ var newHighlights = [];
419
+ forEach(highlights, function(highlight) {
420
+ if (!highlight.applied) {
421
+ highlight.apply();
422
+ newHighlights.push(highlight);
423
+ }
424
+ });
425
+
426
+ return newHighlights;
427
+ },
428
+
429
+ highlightRanges: function(className, ranges, options) {
430
+ var selCharRanges = [];
431
+ var converter = this.converter;
432
+
433
+ options = createOptions(options, {
434
+ containerElement: null,
435
+ exclusive: true
436
+ });
437
+
438
+ var containerElement = options.containerElement;
439
+ var containerElementId = containerElement ? containerElement.id : null;
440
+ var containerElementRange;
441
+ if (containerElement) {
442
+ containerElementRange = api.createRange(containerElement);
443
+ containerElementRange.selectNodeContents(containerElement);
443
444
  }
444
- serializedHighlights.push( parts.join("$") );
445
- });
446
-
447
- return serializedHighlights.join("|");
448
- },
449
-
450
- deserialize: function(serialized) {
451
- var serializedHighlights = serialized.split("|");
452
- var highlights = [];
453
-
454
- var firstHighlight = serializedHighlights[0];
455
- var regexResult;
456
- var serializationType, serializationConverter, convertType = false;
457
- if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) {
458
- serializationType = regexResult[1];
459
- if (serializationType != this.converter.type) {
460
- serializationConverter = getConverter(serializationType);
461
- convertType = true;
445
+
446
+ forEach(ranges, function(range) {
447
+ var scopedRange = containerElement ? containerElementRange.intersection(range) : range;
448
+ selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) );
449
+ });
450
+
451
+ return this.highlightCharacterRanges(className, selCharRanges, {
452
+ containerElementId: containerElementId,
453
+ exclusive: options.exclusive
454
+ });
455
+ },
456
+
457
+ highlightSelection: function(className, options) {
458
+ var converter = this.converter;
459
+ var classApplier = className ? this.classAppliers[className] : false;
460
+
461
+ options = createOptions(options, {
462
+ containerElementId: null,
463
+ exclusive: true
464
+ });
465
+
466
+ var containerElementId = options.containerElementId;
467
+ var exclusive = options.exclusive;
468
+ var selection = options.selection || api.getSelection(this.doc);
469
+ var doc = selection.win.document;
470
+ var containerElement = getContainerElement(doc, containerElementId);
471
+
472
+ if (!classApplier && className !== false) {
473
+ throw new Error("No class applier found for class '" + className + "'");
462
474
  }
463
- serializedHighlights.shift();
464
- } else {
465
- throw new Error("Serialized highlights are invalid.");
466
- }
467
-
468
- var classApplier, highlight, characterRange, containerElementId, containerElement;
469
475
 
470
- for (var i = serializedHighlights.length, parts; i-- > 0; ) {
471
- parts = serializedHighlights[i].split("$");
472
- characterRange = new CharacterRange(+parts[0], +parts[1]);
473
- containerElementId = parts[4] || null;
474
- containerElement = containerElementId ? this.doc.getElementById(containerElementId) : getBody(this.doc);
476
+ // Store the existing selection as character ranges
477
+ var serializedSelection = converter.serializeSelection(selection, containerElement);
478
+
479
+ // Create an array of selected character ranges
480
+ var selCharRanges = [];
481
+ forEach(serializedSelection, function(rangeInfo) {
482
+ selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) );
483
+ });
484
+
485
+ var newHighlights = this.highlightCharacterRanges(className, selCharRanges, {
486
+ containerElementId: containerElementId,
487
+ exclusive: exclusive
488
+ });
489
+
490
+ // Restore selection
491
+ converter.restoreSelection(selection, serializedSelection, containerElement);
492
+
493
+ return newHighlights;
494
+ },
495
+
496
+ unhighlightSelection: function(selection) {
497
+ selection = selection || api.getSelection(this.doc);
498
+ var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() );
499
+ this.removeHighlights(intersectingHighlights);
500
+ selection.removeAllRanges();
501
+ return intersectingHighlights;
502
+ },
503
+
504
+ getHighlightsInSelection: function(selection) {
505
+ selection = selection || api.getSelection(this.doc);
506
+ return this.getIntersectingHighlights(selection.getAllRanges());
507
+ },
508
+
509
+ selectionOverlapsHighlight: function(selection) {
510
+ return this.getHighlightsInSelection(selection).length > 0;
511
+ },
512
+
513
+ serialize: function(options) {
514
+ var highlighter = this;
515
+ var highlights = highlighter.highlights;
516
+ var serializedType, serializedHighlights, convertType, serializationConverter;
517
+
518
+ highlights.sort(compareHighlights);
519
+ options = createOptions(options, {
520
+ serializeHighlightText: false,
521
+ type: highlighter.converter.type
522
+ });
523
+
524
+ serializedType = options.type;
525
+ convertType = (serializedType != highlighter.converter.type);
475
526
 
476
- // Convert to the current Highlighter's type, if different from the serialization type
477
527
  if (convertType) {
478
- characterRange = this.converter.rangeToCharacterRange(
479
- serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement),
480
- containerElement
481
- );
528
+ serializationConverter = getConverter(serializedType);
482
529
  }
483
530
 
484
- classApplier = this.classAppliers[parts[3]];
485
- highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId);
486
- highlight.apply();
487
- highlights.push(highlight);
531
+ serializedHighlights = ["type:" + serializedType];
532
+
533
+ forEach(highlights, function(highlight) {
534
+ var characterRange = highlight.characterRange;
535
+ var containerElement;
536
+
537
+ // Convert to the current Highlighter's type, if different from the serialization type
538
+ if (convertType) {
539
+ containerElement = highlight.getContainerElement();
540
+ characterRange = serializationConverter.rangeToCharacterRange(
541
+ highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement),
542
+ containerElement
543
+ );
544
+ }
545
+
546
+ var parts = [
547
+ characterRange.start,
548
+ characterRange.end,
549
+ highlight.id,
550
+ highlight.classApplier.className,
551
+ highlight.containerElementId
552
+ ];
553
+
554
+ if (options.serializeHighlightText) {
555
+ parts.push(highlight.getText());
556
+ }
557
+ serializedHighlights.push( parts.join("$") );
558
+ });
559
+
560
+ return serializedHighlights.join("|");
561
+ },
562
+
563
+ deserialize: function(serialized) {
564
+ var serializedHighlights = serialized.split("|");
565
+ var highlights = [];
566
+
567
+ var firstHighlight = serializedHighlights[0];
568
+ var regexResult;
569
+ var serializationType, serializationConverter, convertType = false;
570
+ if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) {
571
+ serializationType = regexResult[1];
572
+ if (serializationType != this.converter.type) {
573
+ serializationConverter = getConverter(serializationType);
574
+ convertType = true;
575
+ }
576
+ serializedHighlights.shift();
577
+ } else {
578
+ throw new Error("Serialized highlights are invalid.");
579
+ }
580
+
581
+ var classApplier, highlight, characterRange, containerElementId, containerElement;
582
+
583
+ for (var i = serializedHighlights.length, parts; i-- > 0; ) {
584
+ parts = serializedHighlights[i].split("$");
585
+ characterRange = new CharacterRange(+parts[0], +parts[1]);
586
+ containerElementId = parts[4] || null;
587
+
588
+ // Convert to the current Highlighter's type, if different from the serialization type
589
+ if (convertType) {
590
+ containerElement = getContainerElement(this.doc, containerElementId);
591
+ characterRange = this.converter.rangeToCharacterRange(
592
+ serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement),
593
+ containerElement
594
+ );
595
+ }
596
+
597
+ classApplier = this.classAppliers[ parts[3] ];
598
+
599
+ if (!classApplier) {
600
+ throw new Error("No class applier found for class '" + parts[3] + "'");
601
+ }
602
+
603
+ highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId);
604
+ highlight.apply();
605
+ highlights.push(highlight);
606
+ }
607
+ this.highlights = highlights;
488
608
  }
489
- this.highlights = highlights;
490
- }
491
- };
609
+ };
492
610
 
493
- api.Highlighter = Highlighter;
611
+ api.Highlighter = Highlighter;
494
612
 
495
- api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) {
496
- return new Highlighter(doc, rangeCharacterOffsetConverterType);
497
- };
498
- });
613
+ api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) {
614
+ return new Highlighter(doc, rangeCharacterOffsetConverterType);
615
+ };
616
+ });
617
+
618
+ return rangy;
619
+ }, this);