rangy-rails 1.3alpha.772.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ });