212 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			212 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
// Copyright (C) 2016 The Android Open Source Project
 | 
						|
//
 | 
						|
// Licensed under the Apache License, Version 2.0 (the 'License');
 | 
						|
// you may not use this file except in compliance with the License.
 | 
						|
// You may obtain a copy of the License at
 | 
						|
//
 | 
						|
// http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
//
 | 
						|
// Unless required by applicable law or agreed to in writing, software
 | 
						|
// distributed under the License is distributed on an 'AS IS' BASIS,
 | 
						|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
// See the License for the specific language governing permissions and
 | 
						|
// limitations under the License.
 | 
						|
(function(window) {
 | 
						|
  'use strict';
 | 
						|
 | 
						|
  // Prevent redefinition.
 | 
						|
  if (window.GrAnnotation) { return; }
 | 
						|
 | 
						|
  // TODO(wyatta): refactor this to be <MARK> rather than <HL>.
 | 
						|
  const ANNOTATION_TAG = 'HL';
 | 
						|
 | 
						|
  // Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
 | 
						|
  const REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
 | 
						|
 | 
						|
  const GrAnnotation = {
 | 
						|
 | 
						|
    /**
 | 
						|
     * The DOM API textContent.length calculation is broken when the text
 | 
						|
     * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
 | 
						|
     * @param  {!Text} node text node.
 | 
						|
     * @return {number} The length of the text.
 | 
						|
     */
 | 
						|
    getLength(node) {
 | 
						|
      return this.getStringLength(node.textContent);
 | 
						|
    },
 | 
						|
 | 
						|
    getStringLength(str) {
 | 
						|
      return str.replace(REGEX_ASTRAL_SYMBOL, '_').length;
 | 
						|
    },
 | 
						|
 | 
						|
    /**
 | 
						|
     * Surrounds the element's text at specified range in an ANNOTATION_TAG
 | 
						|
     * element. If the element has child elements, the range is split and
 | 
						|
     * applied as deeply as possible.
 | 
						|
     */
 | 
						|
    annotateElement(parent, offset, length, cssClass) {
 | 
						|
      const nodes = [].slice.apply(parent.childNodes);
 | 
						|
      let nodeLength;
 | 
						|
      let subLength;
 | 
						|
 | 
						|
      for (const node of nodes) {
 | 
						|
        nodeLength = this.getLength(node);
 | 
						|
 | 
						|
        // If the current node is completely before the offset.
 | 
						|
        if (nodeLength <= offset) {
 | 
						|
          offset -= nodeLength;
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
 | 
						|
        // Sublength is the annotation length for the current node.
 | 
						|
        subLength = Math.min(length, nodeLength - offset);
 | 
						|
 | 
						|
        if (node instanceof Text) {
 | 
						|
          this._annotateText(node, offset, subLength, cssClass);
 | 
						|
        } else if (node instanceof HTMLElement) {
 | 
						|
          this.annotateElement(node, offset, subLength, cssClass);
 | 
						|
        }
 | 
						|
 | 
						|
        // If there is still more to annotate, then shift the indices, otherwise
 | 
						|
        // work is done, so break the loop.
 | 
						|
        if (subLength < length) {
 | 
						|
          length -= subLength;
 | 
						|
          offset = 0;
 | 
						|
        } else {
 | 
						|
          break;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    /**
 | 
						|
     * Wraps node in annotation tag with cssClass, replacing the node in DOM.
 | 
						|
     *
 | 
						|
     * @return {!Element} Wrapped node.
 | 
						|
     */
 | 
						|
    wrapInHighlight(node, cssClass) {
 | 
						|
      let hl;
 | 
						|
      if (node.tagName === ANNOTATION_TAG) {
 | 
						|
        hl = node;
 | 
						|
        hl.classList.add(cssClass);
 | 
						|
      } else {
 | 
						|
        hl = document.createElement(ANNOTATION_TAG);
 | 
						|
        hl.className = cssClass;
 | 
						|
        Polymer.dom(node.parentElement).replaceChild(hl, node);
 | 
						|
        Polymer.dom(hl).appendChild(node);
 | 
						|
      }
 | 
						|
      return hl;
 | 
						|
    },
 | 
						|
 | 
						|
    /**
 | 
						|
     * Splits Text Node and wraps it in hl with cssClass.
 | 
						|
     * Wraps trailing part after split, tailing one if opt_firstPart is true.
 | 
						|
     *
 | 
						|
     * @param {!Node} node
 | 
						|
     * @param {number} offset
 | 
						|
     * @param {string} cssClass
 | 
						|
     * @param {boolean=} opt_firstPart
 | 
						|
     */
 | 
						|
    splitAndWrapInHighlight(node, offset, cssClass, opt_firstPart) {
 | 
						|
      if (this.getLength(node) === offset || offset === 0) {
 | 
						|
        return this.wrapInHighlight(node, cssClass);
 | 
						|
      } else {
 | 
						|
        if (opt_firstPart) {
 | 
						|
          this.splitNode(node, offset);
 | 
						|
          // Node points to first part of the Text, second one is sibling.
 | 
						|
        } else {
 | 
						|
          node = this.splitNode(node, offset);
 | 
						|
        }
 | 
						|
        return this.wrapInHighlight(node, cssClass);
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    /**
 | 
						|
     * Splits Node at offset.
 | 
						|
     * If Node is Element, it's cloned and the node at offset is split too.
 | 
						|
     *
 | 
						|
     * @param {!Node} node
 | 
						|
     * @param {number} offset
 | 
						|
     * @return {!Node} Trailing Node.
 | 
						|
     */
 | 
						|
    splitNode(element, offset) {
 | 
						|
      if (element instanceof Text) {
 | 
						|
        return this.splitTextNode(element, offset);
 | 
						|
      }
 | 
						|
      const tail = element.cloneNode(false);
 | 
						|
      element.parentElement.insertBefore(tail, element.nextSibling);
 | 
						|
      // Skip nodes before offset.
 | 
						|
      let node = element.firstChild;
 | 
						|
      while (node &&
 | 
						|
          this.getLength(node) <= offset ||
 | 
						|
          this.getLength(node) === 0) {
 | 
						|
        offset -= this.getLength(node);
 | 
						|
        node = node.nextSibling;
 | 
						|
      }
 | 
						|
      if (this.getLength(node) > offset) {
 | 
						|
        tail.appendChild(this.splitNode(node, offset));
 | 
						|
      }
 | 
						|
      while (node.nextSibling) {
 | 
						|
        tail.appendChild(node.nextSibling);
 | 
						|
      }
 | 
						|
      return tail;
 | 
						|
    },
 | 
						|
 | 
						|
    /**
 | 
						|
     * Node.prototype.splitText Unicode-valid alternative.
 | 
						|
     *
 | 
						|
     * DOM Api for splitText() is broken for Unicode:
 | 
						|
     * https://mathiasbynens.be/notes/javascript-unicode
 | 
						|
     *
 | 
						|
     * @param {!Text} node
 | 
						|
     * @param {number} offset
 | 
						|
     * @return {!Text} Trailing Text Node.
 | 
						|
     */
 | 
						|
    splitTextNode(node, offset) {
 | 
						|
      if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) {
 | 
						|
        // TODO (viktard): Polyfill Array.from for IE10.
 | 
						|
        const head = Array.from(node.textContent);
 | 
						|
        const tail = head.splice(offset);
 | 
						|
        const parent = node.parentNode;
 | 
						|
 | 
						|
        // Split the content of the original node.
 | 
						|
        node.textContent = head.join('');
 | 
						|
 | 
						|
        const tailNode = document.createTextNode(tail.join(''));
 | 
						|
        if (parent) {
 | 
						|
          parent.insertBefore(tailNode, node.nextSibling);
 | 
						|
        }
 | 
						|
        return tailNode;
 | 
						|
      } else {
 | 
						|
        return node.splitText(offset);
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    _annotateText(node, offset, length, cssClass) {
 | 
						|
      const nodeLength = this.getLength(node);
 | 
						|
 | 
						|
      // There are four cases:
 | 
						|
      //  1) Entire node is highlighted.
 | 
						|
      //  2) Highlight is at the start.
 | 
						|
      //  3) Highlight is at the end.
 | 
						|
      //  4) Highlight is in the middle.
 | 
						|
 | 
						|
      if (offset === 0 && nodeLength === length) {
 | 
						|
        // Case 1.
 | 
						|
        this.wrapInHighlight(node, cssClass);
 | 
						|
      } else if (offset === 0) {
 | 
						|
        // Case 2.
 | 
						|
        this.splitAndWrapInHighlight(node, length, cssClass, true);
 | 
						|
      } else if (offset + length === nodeLength) {
 | 
						|
        // Case 3
 | 
						|
        this.splitAndWrapInHighlight(node, offset, cssClass, false);
 | 
						|
      } else {
 | 
						|
        // Case 4
 | 
						|
        this.splitAndWrapInHighlight(this.splitTextNode(node, offset), length,
 | 
						|
            cssClass, true);
 | 
						|
      }
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  window.GrAnnotation = GrAnnotation;
 | 
						|
})(window);
 |