123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621 |
- /**
- * Highlighter module for Rangy, a cross-browser JavaScript range and selection library
- * https://github.com/timdown/rangy
- *
- * Depends on Rangy core, ClassApplier and optionally TextRange modules.
- *
- * Copyright 2015, Tim Down
- * Licensed under the MIT license.
- * Version: 1.3.0
- * Build date: 10 May 2015
- */
- (function(factory, root) {
- if (typeof define == "function" && define.amd) {
- // AMD. Register as an anonymous module with a dependency on Rangy.
- define(["./rangy-core"], factory);
- } else if (typeof module != "undefined" && typeof exports == "object") {
- // Node/CommonJS style
- module.exports = factory( require("rangy") );
- } else {
- // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
- factory(root.rangy);
- }
- })(function(rangy) {
- rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) {
- var dom = api.dom;
- var contains = dom.arrayContains;
- var getBody = dom.getBody;
- var createOptions = api.util.createOptions;
- var forEach = api.util.forEach;
- var nextHighlightId = 1;
- // Puts highlights in order, last in document first.
- function compareHighlights(h1, h2) {
- return h1.characterRange.start - h2.characterRange.start;
- }
- function getContainerElement(doc, id) {
- return id ? doc.getElementById(id) : getBody(doc);
- }
- /*----------------------------------------------------------------------------------------------------------------*/
- 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;
- },
- isContiguousWith: 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));
- },
- getComplements: function(charRange) {
- var ranges = [];
- if (this.start >= charRange.start) {
- if (this.end <= charRange.end) {
- return [];
- }
- ranges.push(new CharacterRange(charRange.end, this.end));
- } else {
- ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start)));
- if (this.end > charRange.end) {
- ranges.push(new CharacterRange(charRange.end, this.end));
- }
- }
- return ranges;
- },
- 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 getContainerElement(this.doc, this.containerElementId);
- },
- 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.className + ", 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.className] = 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;
- 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, options) {
- var i, len, j;
- var highlights = this.highlights;
- var converter = this.converter;
- var doc = this.doc;
- var highlightsToRemove = [];
- var classApplier = className ? this.classAppliers[className] : null;
- options = createOptions(options, {
- containerElementId: null,
- exclusive: true
- });
- var containerElementId = options.containerElementId;
- var exclusive = options.exclusive;
- 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);
- }
- }
- var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight;
- for (i = 0, len = charRanges.length; i < len; ++i) {
- charRange = charRanges[i];
- highlightsToKeep = [];
- // Restrict character range to container element, if it exists
- if (containerElementCharRange) {
- charRange = charRange.intersection(containerElementCharRange);
- }
- // Ignore empty ranges
- if (charRange.start == charRange.end) {
- continue;
- }
- // 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) {
- removeHighlight = false;
- if (containerElementId == highlights[j].containerElementId) {
- highlightCharRange = highlights[j].characterRange;
- isSameClassApplier = (classApplier == highlights[j].classApplier);
- splitHighlight = !isSameClassApplier && exclusive;
- // Replace the existing highlight if it needs to be:
- // 1. merged (isSameClassApplier)
- // 2. partially or entirely erased (className === null)
- // 3. partially or entirely replaced (isSameClassApplier == false && exclusive == true)
- if ( (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) &&
- (isSameClassApplier || splitHighlight) ) {
- // Remove existing highlights, keeping the unselected parts
- if (splitHighlight) {
- forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) {
- highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) );
- });
- }
- removeHighlight = true;
- if (isSameClassApplier) {
- charRange = highlightCharRange.union(charRange);
- }
- }
- }
- if (removeHighlight) {
- highlightsToRemove.push(highlights[j]);
- highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId);
- } else {
- highlightsToKeep.push(highlights[j]);
- }
- }
- // Add new range
- if (classApplier) {
- highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId));
- }
- this.highlights = highlights = highlightsToKeep;
- }
- // 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, options) {
- var selCharRanges = [];
- var converter = this.converter;
- options = createOptions(options, {
- containerElement: null,
- exclusive: true
- });
- var containerElement = options.containerElement;
- 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(className, selCharRanges, {
- containerElementId: containerElementId,
- exclusive: options.exclusive
- });
- },
- highlightSelection: function(className, options) {
- var converter = this.converter;
- var classApplier = className ? this.classAppliers[className] : false;
- options = createOptions(options, {
- containerElementId: null,
- selection: api.getSelection(this.doc),
- exclusive: true
- });
- var containerElementId = options.containerElementId;
- var exclusive = options.exclusive;
- var selection = options.selection;
- var doc = selection.win.document;
- var containerElement = getContainerElement(doc, containerElementId);
- if (!classApplier && className !== false) {
- 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: containerElementId,
- exclusive: exclusive
- });
- // Restore selection
- converter.restoreSelection(selection, serializedSelection, containerElement);
- return newHighlights;
- },
- unhighlightSelection: function(selection) {
- selection = selection || api.getSelection(this.doc);
- var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() );
- this.removeHighlights(intersectingHighlights);
- selection.removeAllRanges();
- return intersectingHighlights;
- },
- getHighlightsInSelection: function(selection) {
- selection = selection || api.getSelection(this.doc);
- return this.getIntersectingHighlights(selection.getAllRanges());
- },
- selectionOverlapsHighlight: function(selection) {
- return this.getHighlightsInSelection(selection).length > 0;
- },
- serialize: function(options) {
- var highlighter = this;
- var highlights = highlighter.highlights;
- var serializedType, serializedHighlights, convertType, serializationConverter;
- highlights.sort(compareHighlights);
- options = createOptions(options, {
- serializeHighlightText: false,
- type: highlighter.converter.type
- });
- serializedType = options.type;
- convertType = (serializedType != highlighter.converter.type);
- if (convertType) {
- serializationConverter = getConverter(serializedType);
- }
- serializedHighlights = ["type:" + serializedType];
- forEach(highlights, function(highlight) {
- var characterRange = highlight.characterRange;
- var containerElement;
- // Convert to the current Highlighter's type, if different from the serialization type
- if (convertType) {
- containerElement = highlight.getContainerElement();
- characterRange = serializationConverter.rangeToCharacterRange(
- highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement),
- containerElement
- );
- }
- var parts = [
- characterRange.start,
- characterRange.end,
- highlight.id,
- highlight.classApplier.className,
- highlight.containerElementId
- ];
- if (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;
- // Convert to the current Highlighter's type, if different from the serialization type
- if (convertType) {
- containerElement = getContainerElement(this.doc, containerElementId);
- characterRange = this.converter.rangeToCharacterRange(
- serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement),
- containerElement
- );
- }
- classApplier = this.classAppliers[ parts[3] ];
- if (!classApplier) {
- throw new Error("No class applier found for class '" + 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);
- };
- });
-
- return rangy;
- }, this);
|