| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- /**
- * Highlighter module for Rangy, a cross-browser JavaScript range and selection library
- * http://code.google.com/p/rangy/
- *
- * Depends on Rangy core, TextRange and CssClassApplier modules.
- *
- * Copyright 2013, Tim Down
- * Licensed under the MIT license.
- * Version: 1.3alpha.804
- * Build date: 8 December 2013
- */
- rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) {
- var dom = api.dom;
- var contains = dom.arrayContains;
- var getBody = dom.getBody;
- // Puts highlights in order, last in document first.
- function compareHighlights(h1, h2) {
- return h1.characterRange.start - h2.characterRange.start;
- }
- var forEach = [].forEach ?
- function(arr, func) {
- arr.forEach(func);
- } :
- function(arr, func) {
- for (var i = 0, len = arr.length; i < len; ++i) {
- func( arr[i] );
- }
- };
- var nextHighlightId = 1;
- /*----------------------------------------------------------------------------------------------------------------*/
- var highlighterTypes = {};
- function HighlighterType(type, converterCreator) {
- this.type = type;
- this.converterCreator = converterCreator;
- }
- HighlighterType.prototype.create = function() {
- var converter = this.converterCreator();
- converter.type = this.type;
- return converter;
- };
- function registerHighlighterType(type, converterCreator) {
- highlighterTypes[type] = new HighlighterType(type, converterCreator);
- }
- function getConverter(type) {
- var highlighterType = highlighterTypes[type];
- if (highlighterType instanceof HighlighterType) {
- return highlighterType.create();
- } else {
- throw new Error("Highlighter type '" + type + "' is not valid");
- }
- }
- api.registerHighlighterType = registerHighlighterType;
- /*----------------------------------------------------------------------------------------------------------------*/
- function CharacterRange(start, end) {
- this.start = start;
- this.end = end;
- }
- CharacterRange.prototype = {
- intersects: function(charRange) {
- return this.start < charRange.end && this.end > charRange.start;
- },
- union: function(charRange) {
- return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end));
- },
-
- intersection: function(charRange) {
- return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end));
- },
- toString: function() {
- return "[CharacterRange(" + this.start + ", " + this.end + ")]";
- }
- };
- CharacterRange.fromCharacterRange = function(charRange) {
- return new CharacterRange(charRange.start, charRange.end);
- };
- /*----------------------------------------------------------------------------------------------------------------*/
- var textContentConverter = {
- rangeToCharacterRange: function(range, containerNode) {
- var bookmark = range.getBookmark(containerNode);
- return new CharacterRange(bookmark.start, bookmark.end);
- },
- characterRangeToRange: function(doc, characterRange, containerNode) {
- var range = api.createRange(doc);
- range.moveToBookmark({
- start: characterRange.start,
- end: characterRange.end,
- containerNode: containerNode
- });
- return range;
- },
- serializeSelection: function(selection, containerNode) {
- var ranges = selection.getAllRanges(), rangeCount = ranges.length;
- var rangeInfos = [];
- var backward = rangeCount == 1 && selection.isBackward();
- for (var i = 0, len = ranges.length; i < len; ++i) {
- rangeInfos[i] = {
- characterRange: this.rangeToCharacterRange(ranges[i], containerNode),
- backward: backward
- };
- }
- return rangeInfos;
- },
- restoreSelection: function(selection, savedSelection, containerNode) {
- selection.removeAllRanges();
- var doc = selection.win.document;
- for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) {
- rangeInfo = savedSelection[i];
- characterRange = rangeInfo.characterRange;
- range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode);
- selection.addRange(range, rangeInfo.backward);
- }
- }
- };
- registerHighlighterType("textContent", function() {
- return textContentConverter;
- });
- /*----------------------------------------------------------------------------------------------------------------*/
- // Lazily load the TextRange-based converter so that the dependency is only checked when required.
- registerHighlighterType("TextRange", (function() {
- var converter;
- return function() {
- if (!converter) {
- // Test that textRangeModule exists and is supported
- var textRangeModule = api.modules.TextRange;
- if (!textRangeModule) {
- throw new Error("TextRange module is missing.");
- } else if (!textRangeModule.supported) {
- throw new Error("TextRange module is present but not supported.");
- }
- converter = {
- rangeToCharacterRange: function(range, containerNode) {
- return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) );
- },
- characterRangeToRange: function(doc, characterRange, containerNode) {
- var range = api.createRange(doc);
- range.selectCharacters(containerNode, characterRange.start, characterRange.end);
- return range;
- },
- serializeSelection: function(selection, containerNode) {
- return selection.saveCharacterRanges(containerNode);
- },
- restoreSelection: function(selection, savedSelection, containerNode) {
- selection.restoreCharacterRanges(containerNode, savedSelection);
- }
- };
- }
- return converter;
- };
- })());
- /*----------------------------------------------------------------------------------------------------------------*/
- function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) {
- if (id) {
- this.id = id;
- nextHighlightId = Math.max(nextHighlightId, id + 1);
- } else {
- this.id = nextHighlightId++;
- }
- this.characterRange = characterRange;
- this.doc = doc;
- this.classApplier = classApplier;
- this.converter = converter;
- this.containerElementId = containerElementId || null;
- this.applied = false;
- }
- Highlight.prototype = {
- getContainerElement: function() {
- return this.containerElementId ? this.doc.getElementById(this.containerElementId) : getBody(this.doc);
- },
-
- getRange: function() {
- return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement());
- },
- fromRange: function(range) {
- this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement());
- },
-
- getText: function() {
- return this.getRange().toString();
- },
- containsElement: function(el) {
- return this.getRange().containsNodeContents(el.firstChild);
- },
- unapply: function() {
- this.classApplier.undoToRange(this.getRange());
- this.applied = false;
- },
- apply: function() {
- this.classApplier.applyToRange(this.getRange());
- this.applied = true;
- },
-
- getHighlightElements: function() {
- return this.classApplier.getElementsWithClassIntersectingRange(this.getRange());
- },
- toString: function() {
- return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.cssClass + ", character range: " +
- this.characterRange.start + " - " + this.characterRange.end + ")]";
- }
- };
- /*----------------------------------------------------------------------------------------------------------------*/
- function Highlighter(doc, type) {
- type = type || "textContent";
- this.doc = doc || document;
- this.classAppliers = {};
- this.highlights = [];
- this.converter = getConverter(type);
- }
- Highlighter.prototype = {
- addClassApplier: function(classApplier) {
- this.classAppliers[classApplier.cssClass] = classApplier;
- },
- getHighlightForElement: function(el) {
- var highlights = this.highlights;
- for (var i = 0, len = highlights.length; i < len; ++i) {
- if (highlights[i].containsElement(el)) {
- return highlights[i];
- }
- }
- return null;
- },
- removeHighlights: function(highlights) {
- for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) {
- highlight = this.highlights[i];
- if (contains(highlights, highlight)) {
- highlight.unapply();
- this.highlights.splice(i--, 1);
- }
- }
- },
- removeAllHighlights: function() {
- this.removeHighlights(this.highlights);
- },
- getIntersectingHighlights: function(ranges) {
- // Test each range against each of the highlighted ranges to see whether they overlap
- var intersectingHighlights = [], highlights = this.highlights, converter = this.converter;
- forEach(ranges, function(range) {
- //var selCharRange = converter.rangeToCharacterRange(range);
- forEach(highlights, function(highlight) {
- if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) {
- intersectingHighlights.push(highlight);
- }
- });
- });
- return intersectingHighlights;
- },
-
- highlightCharacterRanges: function(className, charRanges, containerElementId) {
- var i, len, j;
- var highlights = this.highlights;
- var converter = this.converter;
- var doc = this.doc;
- var highlightsToRemove = [];
- var classApplier = this.classAppliers[className];
- containerElementId = containerElementId || null;
- var containerElement, containerElementRange, containerElementCharRange;
- if (containerElementId) {
- containerElement = this.doc.getElementById(containerElementId);
- if (containerElement) {
- containerElementRange = api.createRange(this.doc);
- containerElementRange.selectNodeContents(containerElement);
- containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length);
- containerElementRange.detach();
- }
- }
- var charRange, highlightCharRange, merged;
- for (i = 0, len = charRanges.length; i < len; ++i) {
- charRange = charRanges[i];
- merged = false;
- // Restrict character range to container element, if it exists
- if (containerElementCharRange) {
- charRange = charRange.intersection(containerElementCharRange);
- }
- // Check for intersection with existing highlights. For each intersection, create a new highlight
- // which is the union of the highlight range and the selected range
- for (j = 0; j < highlights.length; ++j) {
- if (containerElementId == highlights[j].containerElementId) {
- highlightCharRange = highlights[j].characterRange;
- if (highlightCharRange.intersects(charRange)) {
- // Replace the existing highlight in the list of current highlights and add it to the list for
- // removal
- highlightsToRemove.push(highlights[j]);
- highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId);
- }
- }
- }
- if (!merged) {
- highlights.push( new Highlight(doc, charRange, classApplier, converter, null, containerElementId) );
- }
- }
-
- // Remove the old highlights
- forEach(highlightsToRemove, function(highlightToRemove) {
- highlightToRemove.unapply();
- });
- // Apply new highlights
- var newHighlights = [];
- forEach(highlights, function(highlight) {
- if (!highlight.applied) {
- highlight.apply();
- newHighlights.push(highlight);
- }
- });
-
- return newHighlights;
- },
- highlightRanges: function(className, ranges, containerElement) {
- var selCharRanges = [];
- var converter = this.converter;
- var containerElementId = containerElement ? containerElement.id : null;
- var containerElementRange;
- if (containerElement) {
- containerElementRange = api.createRange(containerElement);
- containerElementRange.selectNodeContents(containerElement);
- }
- forEach(ranges, function(range) {
- var scopedRange = containerElement ? containerElementRange.intersection(range) : range;
- selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) );
- });
-
- return this.highlightCharacterRanges(selCharRanges, ranges, containerElementId);
- },
- highlightSelection: function(className, selection, containerElementId) {
- var converter = this.converter;
- selection = selection || api.getSelection();
- var classApplier = this.classAppliers[className];
- var doc = selection.win.document;
- var containerElement = containerElementId ? doc.getElementById(containerElementId) : getBody(doc);
- if (!classApplier) {
- throw new Error("No class applier found for class '" + className + "'");
- }
- // Store the existing selection as character ranges
- var serializedSelection = converter.serializeSelection(selection, containerElement);
- // Create an array of selected character ranges
- var selCharRanges = [];
- forEach(serializedSelection, function(rangeInfo) {
- selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) );
- });
-
- var newHighlights = this.highlightCharacterRanges(className, selCharRanges, containerElementId);
- // Restore selection
- converter.restoreSelection(selection, serializedSelection, containerElement);
- return newHighlights;
- },
- unhighlightSelection: function(selection) {
- selection = selection || api.getSelection();
- var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() );
- this.removeHighlights(intersectingHighlights);
- selection.removeAllRanges();
- return intersectingHighlights;
- },
- getHighlightsInSelection: function(selection) {
- selection = selection || api.getSelection();
- return this.getIntersectingHighlights(selection.getAllRanges());
- },
- selectionOverlapsHighlight: function(selection) {
- return this.getHighlightsInSelection(selection).length > 0;
- },
- serialize: function(options) {
- var highlights = this.highlights;
- highlights.sort(compareHighlights);
- var serializedHighlights = ["type:" + this.converter.type];
- forEach(highlights, function(highlight) {
- var characterRange = highlight.characterRange;
- var parts = [
- characterRange.start,
- characterRange.end,
- highlight.id,
- highlight.classApplier.cssClass,
- highlight.containerElementId
- ];
- if (options && options.serializeHighlightText) {
- parts.push(highlight.getText());
- }
- serializedHighlights.push( parts.join("$") );
- });
- return serializedHighlights.join("|");
- },
- deserialize: function(serialized) {
- var serializedHighlights = serialized.split("|");
- var highlights = [];
- var firstHighlight = serializedHighlights[0];
- var regexResult;
- var serializationType, serializationConverter, convertType = false;
- if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) {
- serializationType = regexResult[1];
- if (serializationType != this.converter.type) {
- serializationConverter = getConverter(serializationType);
- convertType = true;
- }
- serializedHighlights.shift();
- } else {
- throw new Error("Serialized highlights are invalid.");
- }
-
- var classApplier, highlight, characterRange, containerElementId, containerElement;
- for (var i = serializedHighlights.length, parts; i-- > 0; ) {
- parts = serializedHighlights[i].split("$");
- characterRange = new CharacterRange(+parts[0], +parts[1]);
- containerElementId = parts[4] || null;
- containerElement = containerElementId ? this.doc.getElementById(containerElementId) : getBody(this.doc);
- // Convert to the current Highlighter's type, if different from the serialization type
- if (convertType) {
- characterRange = this.converter.rangeToCharacterRange(
- serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement),
- containerElement
- );
- }
- classApplier = this.classAppliers[parts[3]];
- highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId);
- highlight.apply();
- highlights.push(highlight);
- }
- this.highlights = highlights;
- }
- };
- api.Highlighter = Highlighter;
- api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) {
- return new Highlighter(doc, rangeCharacterOffsetConverterType);
- };
- });
|