]> BookStack Code Mirror - bookstack/blobdiff - resources/js/services/dom.ts
Tests: Updated comment test to account for new editor usage
[bookstack] / resources / js / services / dom.ts
index c88827bac40a1788b89295152799b62e9871425f..c3817536c85422c8d0e480cbd05f267be3f6633f 100644 (file)
@@ -1,3 +1,5 @@
+import {cyrb53} from "./util";
+
 /**
  * Check if the given param is a HTMLElement
  */
@@ -44,9 +46,11 @@ export function forEach(selector: string, callback: (el: Element) => any) {
 /**
  * Helper to listen to multiple DOM events
  */
-export function onEvents(listenerElement: Element, events: string[], callback: (e: Event) => any): void {
-    for (const eventName of events) {
-        listenerElement.addEventListener(eventName, callback);
+export function onEvents(listenerElement: Element|null, events: string[], callback: (e: Event) => any): void {
+    if (listenerElement) {
+        for (const eventName of events) {
+            listenerElement.addEventListener(eventName, callback);
+        }
     }
 }
 
@@ -178,3 +182,78 @@ export function htmlToDom(html: string): HTMLElement {
 
     return firstChild;
 }
+
+/**
+ * For the given node and offset, return an adjusted offset that's relative to the given parent element.
+ */
+export function normalizeNodeTextOffsetToParent(node: Node, offset: number, parentElement: HTMLElement): number {
+    if (!parentElement.contains(node)) {
+        throw new Error('ParentElement must be a prent of element');
+    }
+
+    let normalizedOffset = offset;
+    let currentNode: Node|null = node.nodeType === Node.TEXT_NODE ?
+        node : node.childNodes[offset];
+
+    while (currentNode !== parentElement && currentNode) {
+        if (currentNode.previousSibling) {
+            currentNode = currentNode.previousSibling;
+            normalizedOffset += (currentNode.textContent?.length || 0);
+        } else {
+            currentNode = currentNode.parentNode;
+        }
+    }
+
+    return normalizedOffset;
+}
+
+/**
+ * Find the target child node and adjusted offset based on a parent node and text offset.
+ * Returns null if offset not found within the given parent node.
+ */
+export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number): ({node: Node, offset: number}|null) {
+    if (offset === 0) {
+        return { node: parentNode, offset: 0 };
+    }
+
+    let currentOffset = 0;
+    let currentNode = null;
+
+    for (let i = 0; i < parentNode.childNodes.length; i++) {
+        currentNode = parentNode.childNodes[i];
+
+        if (currentNode.nodeType === Node.TEXT_NODE) {
+            // For text nodes, count the length of their content
+            // Returns if within range
+            const textLength = (currentNode.textContent || '').length;
+            if (currentOffset + textLength >= offset) {
+                return {
+                    node: currentNode,
+                    offset: offset - currentOffset
+                };
+            }
+
+            currentOffset += textLength;
+        } else if (currentNode.nodeType === Node.ELEMENT_NODE) {
+            // Otherwise, if an element, track the text length and search within
+            // if in range for the target offset
+            const elementTextLength = (currentNode.textContent || '').length;
+            if (currentOffset + elementTextLength >= offset) {
+                return findTargetNodeAndOffset(currentNode as HTMLElement, offset - currentOffset);
+            }
+
+            currentOffset += elementTextLength;
+        }
+    }
+
+    // Return null if not found within range
+    return null;
+}
+
+/**
+ * Create a hash for the given HTML element content.
+ */
+export function hashElement(element: HTMLElement): string {
+    const normalisedElemText = (element.textContent || '').replace(/\s{2,}/g, '');
+    return cyrb53(normalisedElemText);
+}
\ No newline at end of file