[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/editor/atto/yui/src/rangy/js/ -> rangy-highlighter.js (source)

   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);


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1