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