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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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);