rangy-rails 1.3alpha.772.0

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