[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 /** 2 * Highlighter module for Rangy, a cross-browser JavaScript range and selection library 3 * https://github.com/timdown/rangy 4 * 5 * Depends on Rangy core, ClassApplier and optionally TextRange modules. 6 * 7 * Copyright 2015, Tim Down 8 * Licensed under the MIT license. 9 * Version: 1.3.0 10 * Build date: 10 May 2015 11 */ 12 (function(factory, root) { 13 // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 14 factory(root.rangy); 15 })(function(rangy) { 16 rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) { 17 var dom = api.dom; 18 var contains = dom.arrayContains; 19 var getBody = dom.getBody; 20 var createOptions = api.util.createOptions; 21 var forEach = api.util.forEach; 22 var nextHighlightId = 1; 23 24 // Puts highlights in order, last in document first. 25 function compareHighlights(h1, h2) { 26 return h1.characterRange.start - h2.characterRange.start; 27 } 28 29 function getContainerElement(doc, id) { 30 return id ? doc.getElementById(id) : getBody(doc); 31 } 32 33 /*----------------------------------------------------------------------------------------------------------------*/ 34 35 var highlighterTypes = {}; 36 37 function HighlighterType(type, converterCreator) { 38 this.type = type; 39 this.converterCreator = converterCreator; 40 } 41 42 HighlighterType.prototype.create = function() { 43 var converter = this.converterCreator(); 44 converter.type = this.type; 45 return converter; 46 }; 47 48 function registerHighlighterType(type, converterCreator) { 49 highlighterTypes[type] = new HighlighterType(type, converterCreator); 50 } 51 52 function getConverter(type) { 53 var highlighterType = highlighterTypes[type]; 54 if (highlighterType instanceof HighlighterType) { 55 return highlighterType.create(); 56 } else { 57 throw new Error("Highlighter type '" + type + "' is not valid"); 58 } 59 } 60 61 api.registerHighlighterType = registerHighlighterType; 62 63 /*----------------------------------------------------------------------------------------------------------------*/ 64 65 function CharacterRange(start, end) { 66 this.start = start; 67 this.end = end; 68 } 69 70 CharacterRange.prototype = { 71 intersects: function(charRange) { 72 return this.start < charRange.end && this.end > charRange.start; 73 }, 74 75 isContiguousWith: function(charRange) { 76 return this.start == charRange.end || this.end == charRange.start; 77 }, 78 79 union: function(charRange) { 80 return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end)); 81 }, 82 83 intersection: function(charRange) { 84 return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end)); 85 }, 86 87 getComplements: function(charRange) { 88 var ranges = []; 89 if (this.start >= charRange.start) { 90 if (this.end <= charRange.end) { 91 return []; 92 } 93 ranges.push(new CharacterRange(charRange.end, this.end)); 94 } else { 95 ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start))); 96 if (this.end > charRange.end) { 97 ranges.push(new CharacterRange(charRange.end, this.end)); 98 } 99 } 100 return ranges; 101 }, 102 103 toString: function() { 104 return "[CharacterRange(" + this.start + ", " + this.end + ")]"; 105 } 106 }; 107 108 CharacterRange.fromCharacterRange = function(charRange) { 109 return new CharacterRange(charRange.start, charRange.end); 110 }; 111 112 /*----------------------------------------------------------------------------------------------------------------*/ 113 114 var textContentConverter = { 115 rangeToCharacterRange: function(range, containerNode) { 116 var bookmark = range.getBookmark(containerNode); 117 return new CharacterRange(bookmark.start, bookmark.end); 118 }, 119 120 characterRangeToRange: function(doc, characterRange, containerNode) { 121 var range = api.createRange(doc); 122 range.moveToBookmark({ 123 start: characterRange.start, 124 end: characterRange.end, 125 containerNode: containerNode 126 }); 127 128 return range; 129 }, 130 131 serializeSelection: function(selection, containerNode) { 132 var ranges = selection.getAllRanges(), rangeCount = ranges.length; 133 var rangeInfos = []; 134 135 var backward = rangeCount == 1 && selection.isBackward(); 136 137 for (var i = 0, len = ranges.length; i < len; ++i) { 138 rangeInfos[i] = { 139 characterRange: this.rangeToCharacterRange(ranges[i], containerNode), 140 backward: backward 141 }; 142 } 143 144 return rangeInfos; 145 }, 146 147 restoreSelection: function(selection, savedSelection, containerNode) { 148 selection.removeAllRanges(); 149 var doc = selection.win.document; 150 for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) { 151 rangeInfo = savedSelection[i]; 152 characterRange = rangeInfo.characterRange; 153 range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode); 154 selection.addRange(range, rangeInfo.backward); 155 } 156 } 157 }; 158 159 registerHighlighterType("textContent", function() { 160 return textContentConverter; 161 }); 162 163 /*----------------------------------------------------------------------------------------------------------------*/ 164 165 // Lazily load the TextRange-based converter so that the dependency is only checked when required. 166 registerHighlighterType("TextRange", (function() { 167 var converter; 168 169 return function() { 170 if (!converter) { 171 // Test that textRangeModule exists and is supported 172 var textRangeModule = api.modules.TextRange; 173 if (!textRangeModule) { 174 throw new Error("TextRange module is missing."); 175 } else if (!textRangeModule.supported) { 176 throw new Error("TextRange module is present but not supported."); 177 } 178 179 converter = { 180 rangeToCharacterRange: function(range, containerNode) { 181 return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) ); 182 }, 183 184 characterRangeToRange: function(doc, characterRange, containerNode) { 185 var range = api.createRange(doc); 186 range.selectCharacters(containerNode, characterRange.start, characterRange.end); 187 return range; 188 }, 189 190 serializeSelection: function(selection, containerNode) { 191 return selection.saveCharacterRanges(containerNode); 192 }, 193 194 restoreSelection: function(selection, savedSelection, containerNode) { 195 selection.restoreCharacterRanges(containerNode, savedSelection); 196 } 197 }; 198 } 199 200 return converter; 201 }; 202 })()); 203 204 /*----------------------------------------------------------------------------------------------------------------*/ 205 206 function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) { 207 if (id) { 208 this.id = id; 209 nextHighlightId = Math.max(nextHighlightId, id + 1); 210 } else { 211 this.id = nextHighlightId++; 212 } 213 this.characterRange = characterRange; 214 this.doc = doc; 215 this.classApplier = classApplier; 216 this.converter = converter; 217 this.containerElementId = containerElementId || null; 218 this.applied = false; 219 } 220 221 Highlight.prototype = { 222 getContainerElement: function() { 223 return getContainerElement(this.doc, this.containerElementId); 224 }, 225 226 getRange: function() { 227 return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement()); 228 }, 229 230 fromRange: function(range) { 231 this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement()); 232 }, 233 234 getText: function() { 235 return this.getRange().toString(); 236 }, 237 238 containsElement: function(el) { 239 return this.getRange().containsNodeContents(el.firstChild); 240 }, 241 242 unapply: function() { 243 this.classApplier.undoToRange(this.getRange()); 244 this.applied = false; 245 }, 246 247 apply: function() { 248 this.classApplier.applyToRange(this.getRange()); 249 this.applied = true; 250 }, 251 252 getHighlightElements: function() { 253 return this.classApplier.getElementsWithClassIntersectingRange(this.getRange()); 254 }, 255 256 toString: function() { 257 return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " + 258 this.characterRange.start + " - " + this.characterRange.end + ")]"; 259 } 260 }; 261 262 /*----------------------------------------------------------------------------------------------------------------*/ 263 264 function Highlighter(doc, type) { 265 type = type || "textContent"; 266 this.doc = doc || document; 267 this.classAppliers = {}; 268 this.highlights = []; 269 this.converter = getConverter(type); 270 } 271 272 Highlighter.prototype = { 273 addClassApplier: function(classApplier) { 274 this.classAppliers[classApplier.className] = classApplier; 275 }, 276 277 getHighlightForElement: function(el) { 278 var highlights = this.highlights; 279 for (var i = 0, len = highlights.length; i < len; ++i) { 280 if (highlights[i].containsElement(el)) { 281 return highlights[i]; 282 } 283 } 284 return null; 285 }, 286 287 removeHighlights: function(highlights) { 288 for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) { 289 highlight = this.highlights[i]; 290 if (contains(highlights, highlight)) { 291 highlight.unapply(); 292 this.highlights.splice(i--, 1); 293 } 294 } 295 }, 296 297 removeAllHighlights: function() { 298 this.removeHighlights(this.highlights); 299 }, 300 301 getIntersectingHighlights: function(ranges) { 302 // Test each range against each of the highlighted ranges to see whether they overlap 303 var intersectingHighlights = [], highlights = this.highlights; 304 forEach(ranges, function(range) { 305 //var selCharRange = converter.rangeToCharacterRange(range); 306 forEach(highlights, function(highlight) { 307 if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) { 308 intersectingHighlights.push(highlight); 309 } 310 }); 311 }); 312 313 return intersectingHighlights; 314 }, 315 316 highlightCharacterRanges: function(className, charRanges, options) { 317 var i, len, j; 318 var highlights = this.highlights; 319 var converter = this.converter; 320 var doc = this.doc; 321 var highlightsToRemove = []; 322 var classApplier = className ? this.classAppliers[className] : null; 323 324 options = createOptions(options, { 325 containerElementId: null, 326 exclusive: true 327 }); 328 329 var containerElementId = options.containerElementId; 330 var exclusive = options.exclusive; 331 332 var containerElement, containerElementRange, containerElementCharRange; 333 if (containerElementId) { 334 containerElement = this.doc.getElementById(containerElementId); 335 if (containerElement) { 336 containerElementRange = api.createRange(this.doc); 337 containerElementRange.selectNodeContents(containerElement); 338 containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length); 339 } 340 } 341 342 var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight; 343 344 for (i = 0, len = charRanges.length; i < len; ++i) { 345 charRange = charRanges[i]; 346 highlightsToKeep = []; 347 348 // Restrict character range to container element, if it exists 349 if (containerElementCharRange) { 350 charRange = charRange.intersection(containerElementCharRange); 351 } 352 353 // Ignore empty ranges 354 if (charRange.start == charRange.end) { 355 continue; 356 } 357 358 // Check for intersection with existing highlights. For each intersection, create a new highlight 359 // which is the union of the highlight range and the selected range 360 for (j = 0; j < highlights.length; ++j) { 361 removeHighlight = false; 362 363 if (containerElementId == highlights[j].containerElementId) { 364 highlightCharRange = highlights[j].characterRange; 365 isSameClassApplier = (classApplier == highlights[j].classApplier); 366 splitHighlight = !isSameClassApplier && exclusive; 367 368 // Replace the existing highlight if it needs to be: 369 // 1. merged (isSameClassApplier) 370 // 2. partially or entirely erased (className === null) 371 // 3. partially or entirely replaced (isSameClassApplier == false && exclusive == true) 372 if ( (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) && 373 (isSameClassApplier || splitHighlight) ) { 374 375 // Remove existing highlights, keeping the unselected parts 376 if (splitHighlight) { 377 forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) { 378 highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) ); 379 }); 380 } 381 382 removeHighlight = true; 383 if (isSameClassApplier) { 384 charRange = highlightCharRange.union(charRange); 385 } 386 } 387 } 388 389 if (removeHighlight) { 390 highlightsToRemove.push(highlights[j]); 391 highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId); 392 } else { 393 highlightsToKeep.push(highlights[j]); 394 } 395 } 396 397 // Add new range 398 if (classApplier) { 399 highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId)); 400 } 401 this.highlights = highlights = highlightsToKeep; 402 } 403 404 // Remove the old highlights 405 forEach(highlightsToRemove, function(highlightToRemove) { 406 highlightToRemove.unapply(); 407 }); 408 409 // Apply new highlights 410 var newHighlights = []; 411 forEach(highlights, function(highlight) { 412 if (!highlight.applied) { 413 highlight.apply(); 414 newHighlights.push(highlight); 415 } 416 }); 417 418 return newHighlights; 419 }, 420 421 highlightRanges: function(className, ranges, options) { 422 var selCharRanges = []; 423 var converter = this.converter; 424 425 options = createOptions(options, { 426 containerElement: null, 427 exclusive: true 428 }); 429 430 var containerElement = options.containerElement; 431 var containerElementId = containerElement ? containerElement.id : null; 432 var containerElementRange; 433 if (containerElement) { 434 containerElementRange = api.createRange(containerElement); 435 containerElementRange.selectNodeContents(containerElement); 436 } 437 438 forEach(ranges, function(range) { 439 var scopedRange = containerElement ? containerElementRange.intersection(range) : range; 440 selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) ); 441 }); 442 443 return this.highlightCharacterRanges(className, selCharRanges, { 444 containerElementId: containerElementId, 445 exclusive: options.exclusive 446 }); 447 }, 448 449 highlightSelection: function(className, options) { 450 var converter = this.converter; 451 var classApplier = className ? this.classAppliers[className] : false; 452 453 options = createOptions(options, { 454 containerElementId: null, 455 selection: api.getSelection(this.doc), 456 exclusive: true 457 }); 458 459 var containerElementId = options.containerElementId; 460 var exclusive = options.exclusive; 461 var selection = options.selection; 462 var doc = selection.win.document; 463 var containerElement = getContainerElement(doc, containerElementId); 464 465 if (!classApplier && className !== false) { 466 throw new Error("No class applier found for class '" + className + "'"); 467 } 468 469 // Store the existing selection as character ranges 470 var serializedSelection = converter.serializeSelection(selection, containerElement); 471 472 // Create an array of selected character ranges 473 var selCharRanges = []; 474 forEach(serializedSelection, function(rangeInfo) { 475 selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) ); 476 }); 477 478 var newHighlights = this.highlightCharacterRanges(className, selCharRanges, { 479 containerElementId: containerElementId, 480 exclusive: exclusive 481 }); 482 483 // Restore selection 484 converter.restoreSelection(selection, serializedSelection, containerElement); 485 486 return newHighlights; 487 }, 488 489 unhighlightSelection: function(selection) { 490 selection = selection || api.getSelection(this.doc); 491 var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() ); 492 this.removeHighlights(intersectingHighlights); 493 selection.removeAllRanges(); 494 return intersectingHighlights; 495 }, 496 497 getHighlightsInSelection: function(selection) { 498 selection = selection || api.getSelection(this.doc); 499 return this.getIntersectingHighlights(selection.getAllRanges()); 500 }, 501 502 selectionOverlapsHighlight: function(selection) { 503 return this.getHighlightsInSelection(selection).length > 0; 504 }, 505 506 serialize: function(options) { 507 var highlighter = this; 508 var highlights = highlighter.highlights; 509 var serializedType, serializedHighlights, convertType, serializationConverter; 510 511 highlights.sort(compareHighlights); 512 options = createOptions(options, { 513 serializeHighlightText: false, 514 type: highlighter.converter.type 515 }); 516 517 serializedType = options.type; 518 convertType = (serializedType != highlighter.converter.type); 519 520 if (convertType) { 521 serializationConverter = getConverter(serializedType); 522 } 523 524 serializedHighlights = ["type:" + serializedType]; 525 526 forEach(highlights, function(highlight) { 527 var characterRange = highlight.characterRange; 528 var containerElement; 529 530 // Convert to the current Highlighter's type, if different from the serialization type 531 if (convertType) { 532 containerElement = highlight.getContainerElement(); 533 characterRange = serializationConverter.rangeToCharacterRange( 534 highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement), 535 containerElement 536 ); 537 } 538 539 var parts = [ 540 characterRange.start, 541 characterRange.end, 542 highlight.id, 543 highlight.classApplier.className, 544 highlight.containerElementId 545 ]; 546 547 if (options.serializeHighlightText) { 548 parts.push(highlight.getText()); 549 } 550 serializedHighlights.push( parts.join("$") ); 551 }); 552 553 return serializedHighlights.join("|"); 554 }, 555 556 deserialize: function(serialized) { 557 var serializedHighlights = serialized.split("|"); 558 var highlights = []; 559 560 var firstHighlight = serializedHighlights[0]; 561 var regexResult; 562 var serializationType, serializationConverter, convertType = false; 563 if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) { 564 serializationType = regexResult[1]; 565 if (serializationType != this.converter.type) { 566 serializationConverter = getConverter(serializationType); 567 convertType = true; 568 } 569 serializedHighlights.shift(); 570 } else { 571 throw new Error("Serialized highlights are invalid."); 572 } 573 574 var classApplier, highlight, characterRange, containerElementId, containerElement; 575 576 for (var i = serializedHighlights.length, parts; i-- > 0; ) { 577 parts = serializedHighlights[i].split("$"); 578 characterRange = new CharacterRange(+parts[0], +parts[1]); 579 containerElementId = parts[4] || null; 580 581 // Convert to the current Highlighter's type, if different from the serialization type 582 if (convertType) { 583 containerElement = getContainerElement(this.doc, containerElementId); 584 characterRange = this.converter.rangeToCharacterRange( 585 serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement), 586 containerElement 587 ); 588 } 589 590 classApplier = this.classAppliers[ parts[3] ]; 591 592 if (!classApplier) { 593 throw new Error("No class applier found for class '" + parts[3] + "'"); 594 } 595 596 highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId); 597 highlight.apply(); 598 highlights.push(highlight); 599 } 600 this.highlights = highlights; 601 } 602 }; 603 604 api.Highlighter = Highlighter; 605 606 api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) { 607 return new Highlighter(doc, rangeCharacterOffsetConverterType); 608 }; 609 }); 610 611 return rangy; 612 }, this);
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |