You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@annotator.apache.org by ge...@apache.org on 2020/02/13 15:47:23 UTC

[incubator-annotator] 01/01: Add highlightRange to dom package

This is an automated email from the ASF dual-hosted git repository.

gerben pushed a commit to branch add-highlight-range
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit 18eb1d5043787aa62e91217cc5664e3f7b4029a1
Author: Gerben <ge...@treora.com>
AuthorDate: Thu Feb 13 16:42:32 2020 +0100

    Add highlightRange to dom package
    
    Code is copied from https://www.npmjs.com/package/dom-highlight-range v3.0.1
---
 packages/dom/src/highlight-range.js | 106 ++++++++++++++++++++++++++++++++++++
 packages/dom/src/index.js           |   1 +
 2 files changed, 107 insertions(+)

diff --git a/packages/dom/src/highlight-range.js b/packages/dom/src/highlight-range.js
new file mode 100644
index 0000000..09e22d5
--- /dev/null
+++ b/packages/dom/src/highlight-range.js
@@ -0,0 +1,106 @@
+// Wrap each text node in a given DOM Range with a <mark> or other element.
+// Breaks start and/or end node if needed.
+// Returns a function that cleans up the created highlight (not a perfect undo: split text nodes are
+// not merged again).
+//
+// Parameters:
+// - range: a DOM Range object. Note that as highlighting modifies the DOM, the range may be
+//   unusable afterwards
+// - tagName: the element used to wrap text nodes. Defaults to 'mark'.
+// - attributes: an Object defining any attributes to be set on the wrapper elements.
+export function highlightRange(range, tagName = 'mark', attributes = {}) {
+  if (range.collapsed) return;
+
+  // First put all nodes in an array (splits start and end nodes if needed)
+  const nodes = textNodesInRange(range);
+
+  // Highlight each node
+  const highlightElements = [];
+  for (const node of nodes) {
+    const highlightElement = wrapNodeInHighlight(node, tagName, attributes);
+    highlightElements.push(highlightElement);
+  }
+
+  // Return a function that cleans up the highlightElements.
+  function removeHighlights() {
+    // Remove each of the created highlightElements.
+    for (const highlightIdx in highlightElements) {
+      removeHighlight(highlightElements[highlightIdx]);
+    }
+  }
+  return removeHighlights;
+}
+
+// Return an array of the text nodes in the range. Split the start and end nodes if required.
+function textNodesInRange(range) {
+  // If the start or end node is a text node and only partly in the range, split it.
+  if (range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) {
+    const endOffset = range.endOffset; // (this may get lost when the splitting the node)
+    const createdNode = range.startContainer.splitText(range.startOffset);
+    if (range.endContainer === range.startContainer) {
+      // If the end was in the same container, it will now be in the newly created node.
+      range.setEnd(createdNode, endOffset - range.startOffset);
+    }
+    range.setStart(createdNode, 0);
+  }
+  if (
+    range.endContainer.nodeType === Node.TEXT_NODE
+    && range.endOffset < range.endContainer.length
+  ) {
+    range.endContainer.splitText(range.endOffset);
+  }
+
+  // Collect the text nodes.
+  const walker = range.startContainer.ownerDocument.createTreeWalker(
+    range.commonAncestorContainer,
+    NodeFilter.SHOW_TEXT,
+    node => range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT,
+  );
+  walker.currentNode = range.startContainer;
+
+  // // Optimise by skipping nodes that are explicitly outside the range.
+  // const NodeTypesWithCharacterOffset = [
+  //  Node.TEXT_NODE,
+  //  Node.PROCESSING_INSTRUCTION_NODE,
+  //  Node.COMMENT_NODE,
+  // ];
+  // if (!NodeTypesWithCharacterOffset.includes(range.startContainer.nodeType)) {
+  //   if (range.startOffset < range.startContainer.childNodes.length) {
+  //     walker.currentNode = range.startContainer.childNodes[range.startOffset];
+  //   } else {
+  //     walker.nextSibling(); // TODO verify this is correct.
+  //   }
+  // }
+
+  const nodes = [];
+  if (walker.currentNode.nodeType === Node.TEXT_NODE)
+    nodes.push(walker.currentNode);
+  while (walker.nextNode() && range.comparePoint(walker.currentNode, 0) !== 1)
+    nodes.push(walker.currentNode);
+  return nodes;
+}
+
+// Replace [node] with <tagName ...attributes>[node]</tagName>
+function wrapNodeInHighlight(node, tagName, attributes) {
+  const highlightElement = node.ownerDocument.createElement(tagName);
+  Object.keys(attributes).forEach(key => {
+    highlightElement.setAttribute(key, attributes[key]);
+  });
+  const tempRange = node.ownerDocument.createRange();
+  tempRange.selectNode(node);
+  tempRange.surroundContents(highlightElement);
+  return highlightElement;
+}
+
+// Remove a highlight element created with wrapNodeInHighlight.
+function removeHighlight(highlightElement) {
+  if (highlightElement.childNodes.length === 1) {
+    highlightElement.parentNode.replaceChild(highlightElement.firstChild, highlightElement);
+  } else {
+    // If the highlight somehow contains multiple nodes now, move them all.
+    while (highlightElement.firstChild) {
+      highlightElement.parentNode.insertBefore(highlightElement.firstChild, highlightElement);
+    }
+    highlightElement.remove();
+  }
+}
diff --git a/packages/dom/src/index.js b/packages/dom/src/index.js
index e54a806..3d7ca58 100644
--- a/packages/dom/src/index.js
+++ b/packages/dom/src/index.js
@@ -21,3 +21,4 @@
 export * from './css';
 export * from './range';
 export * from './text-quote';
+export * from './highlight-range';