import {Component} from './component';
-import {getLoading, htmlToDom} from '../services/dom.ts';
+import {findTargetNodeAndOffset, getLoading, hashElement, htmlToDom} from '../services/dom.ts';
import {buildForInput} from '../wysiwyg-tinymce/config';
+import {el} from "../wysiwyg/utils/dom";
export class PageComment extends Component {
this.input = this.$refs.input as HTMLInputElement;
this.setupListeners();
+ this.positionForReference();
}
protected setupListeners(): void {
return loading;
}
+ protected positionForReference() {
+ if (!this.commentContentRef) {
+ return;
+ }
+
+ const [refId, refHash, refRange] = this.commentContentRef.split(':');
+ const refEl = document.getElementById(refId);
+ if (!refEl) {
+ // TODO - Show outdated marker for comment
+ return;
+ }
+
+ const actualHash = hashElement(refEl);
+ if (actualHash !== refHash) {
+ // TODO - Show outdated marker for comment
+ return;
+ }
+
+ const refElBounds = refEl.getBoundingClientRect();
+ let bounds = refElBounds;
+ const [rangeStart, rangeEnd] = refRange.split('-');
+ if (rangeStart && rangeEnd) {
+ const range = new Range();
+ const relStart = findTargetNodeAndOffset(refEl, Number(rangeStart));
+ const relEnd = findTargetNodeAndOffset(refEl, Number(rangeEnd));
+ if (relStart && relEnd) {
+ range.setStart(relStart.node, relStart.offset);
+ range.setEnd(relEnd.node, relEnd.offset);
+ bounds = range.getBoundingClientRect();
+ }
+ }
+
+ const relLeft = bounds.left - refElBounds.left;
+ const relTop = bounds.top - refElBounds.top;
+ // TODO - Extract to class, Use theme color
+ const marker = el('div', {
+ class: 'content-comment-highlight',
+ style: `left: ${relLeft}px; top: ${relTop}px; width: ${bounds.width}px; height: ${bounds.height}px;`
+ }, ['']);
+
+ refEl.style.position = 'relative';
+ refEl.append(marker);
+ }
}
import * as DOM from '../services/dom.ts';
import {Component} from './component';
import {copyTextToClipboard} from '../services/clipboard.ts';
-import {cyrb53} from "../services/util";
-import {normalizeNodeTextOffsetToParent} from "../services/dom.ts";
+import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom.ts";
import {PageComments} from "./page-comments";
export class Pointer extends Component {
return;
}
- const normalisedElemHtml = this.targetElement.outerHTML.replace(/\s{2,}/g, '');
const refId = this.targetElement.id;
- const hash = cyrb53(normalisedElemHtml);
+ const hash = hashElement(this.targetElement);
let range = '';
if (this.targetSelectionRange) {
const commonContainer = this.targetSelectionRange.commonAncestorContainer;
+import {cyrb53} from "./util";
+
/**
* Check if the given param is a 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');
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, offset - currentOffset);
+ }
+
+ currentOffset += elementTextLength;
+ }
+ }
+
+ // Return null if not found within range
+ return null;
+}
+
+/**
+ * Create a hash for the given HTML element.
+ */
+export function hashElement(element: HTMLElement): string {
+ const normalisedElemHtml = element.outerHTML.replace(/\s{2,}/g, '');
+ return cyrb53(normalisedElemHtml);
+}
\ No newline at end of file
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
- return (4294967296 * (2097151 & h2) + (h1 >>> 0)) as string;
+ return String((4294967296 * (2097151 & h2) + (h1 >>> 0)));
}
\ No newline at end of file
}
}
+// Page inline comments
+.content-comment-highlight {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 0;
+ height: 0;
+ user-select: none;
+ pointer-events: none;
+ &:after {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-color: var(--color-primary);
+ opacity: 0.25;
+ }
+}
+
// Page editor sidebar toolbox
.floating-toolbox {
@include mixins.lightDark(background-color, #FFF, #222);