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.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +44 -0
- data/Rakefile +1 -0
- data/lib/rangy-rails.rb +12 -0
- data/lib/rangy-rails/version.rb +5 -0
- data/rangy-rails.gemspec +21 -0
- data/vendor/assets/javascripts/rangy-core.js +3644 -0
- data/vendor/assets/javascripts/rangy-cssclassapplier.js +935 -0
- data/vendor/assets/javascripts/rangy-highlighter.js +496 -0
- data/vendor/assets/javascripts/rangy-selectionsaverestore.js +237 -0
- data/vendor/assets/javascripts/rangy-serializer.js +294 -0
- data/vendor/assets/javascripts/rangy-textrange.js +1896 -0
- metadata +61 -0
@@ -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
|
+
});
|