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