AutorÃa | Ultima modificación | Ver Log |
/*** 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 2022, Tim Down* Licensed under the MIT license.* Version: 1.3.1* Build date: 17 August 2022*/(function(factory, root) {// 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 supportedvar 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 overlapvar 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 existsif (containerElementCharRange) {charRange = charRange.intersection(containerElementCharRange);}// Ignore empty rangesif (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 rangefor (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 partsif (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 rangeif (classApplier) {highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId));}this.highlights = highlights = highlightsToKeep;}// Remove the old highlightsforEach(highlightsToRemove, function(highlightToRemove) {highlightToRemove.unapply();});// Apply new highlightsvar 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,exclusive: true});var containerElementId = options.containerElementId;var exclusive = options.exclusive;var selection = options.selection || api.getSelection(this.doc);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 rangesvar serializedSelection = converter.serializeSelection(selection, containerElement);// Create an array of selected character rangesvar selCharRanges = [];forEach(serializedSelection, function(rangeInfo) {selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) );});var newHighlights = this.highlightCharacterRanges(className, selCharRanges, {containerElementId: containerElementId,exclusive: exclusive});// Restore selectionconverter.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 typeif (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 typeif (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);