Comments: Started logic for content references

Adds button for comments to pointer.
Adds logic to generate a content reference point.
This commit is contained in:
Dan Brown
2025-04-18 15:01:57 +01:00
parent fa566f156a
commit 8d159f77e4
5 changed files with 113 additions and 19 deletions

View File

@ -1,6 +1,9 @@
import * as DOM from '../services/dom.ts';
import {Component} from './component';
import {copyTextToClipboard} from '../services/clipboard.ts';
import {el} from "../wysiwyg/utils/dom";
import {cyrb53} from "../services/util";
import {normalizeNodeTextOffsetToParent} from "../services/dom.ts";
export class Pointer extends Component {
@ -12,13 +15,16 @@ export class Pointer extends Component {
this.includeInput = this.$refs.includeInput;
this.includeButton = this.$refs.includeButton;
this.sectionModeButton = this.$refs.sectionModeButton;
this.commentButton = this.$refs.commentButton;
this.modeToggles = this.$manyRefs.modeToggle;
this.modeSections = this.$manyRefs.modeSection;
this.pageId = this.$opts.pageId;
// Instance variables
this.showing = false;
this.isSelection = false;
this.isMakingSelection = false;
this.targetElement = null;
this.targetSelectionRange = null;
this.setupListeners();
}
@ -41,7 +47,7 @@ export class Pointer extends Component {
// Hide pointer when clicking away
DOM.onEvents(document.body, ['click', 'focus'], () => {
if (!this.showing || this.isSelection) return;
if (!this.showing || this.isMakingSelection) return;
this.hidePointer();
});
@ -70,11 +76,17 @@ export class Pointer extends Component {
this.modeToggles.find(b => b !== event.target).focus();
});
if (this.commentButton) {
DOM.onSelect(this.commentButton, this.createCommentAtPointer.bind(this));
}
}
hidePointer() {
this.pointer.style.display = null;
this.showing = false;
this.targetElement = null;
this.targetSelectionRange = null;
}
/**
@ -84,7 +96,9 @@ export class Pointer extends Component {
* @param {Boolean} keyboardMode
*/
showPointerAtTarget(element, xPosition, keyboardMode) {
this.updateForTarget(element);
this.targetElement = element;
this.targetSelectionRange = window.getSelection()?.getRangeAt(0);
this.updateDomForTarget(element);
this.pointer.style.display = 'block';
const targetBounds = element.getBoundingClientRect();
@ -98,10 +112,10 @@ export class Pointer extends Component {
this.pointer.style.top = `${yOffset}px`;
this.showing = true;
this.isSelection = true;
this.isMakingSelection = true;
setTimeout(() => {
this.isSelection = false;
this.isMakingSelection = false;
}, 100);
const scrollListener = () => {
@ -119,7 +133,7 @@ export class Pointer extends Component {
* Update the pointer inputs/content for the given target element.
* @param {?Element} element
*/
updateForTarget(element) {
updateDomForTarget(element) {
const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`);
const includeTag = `{{@${this.pageId}#${element.id}}}`;
@ -152,4 +166,34 @@ export class Pointer extends Component {
});
}
createCommentAtPointer(event) {
if (!this.targetElement) {
return;
}
const normalisedElemHtml = this.targetElement.outerHTML.replace(/\s{2,}/g, '');
const refId = this.targetElement.id;
const hash = cyrb53(normalisedElemHtml);
let range = '';
if (this.targetSelectionRange) {
const commonContainer = this.targetSelectionRange.commonAncestorContainer;
if (this.targetElement.contains(commonContainer)) {
const start = normalizeNodeTextOffsetToParent(
this.targetSelectionRange.startContainer,
this.targetSelectionRange.startOffset,
this.targetElement
);
const end = normalizeNodeTextOffsetToParent(
this.targetSelectionRange.endContainer,
this.targetSelectionRange.endOffset,
this.targetElement
);
range = `${start}-${end}`;
}
}
const reference = `${refId}:${hash}:${range}`;
console.log(reference);
}
}

View File

@ -178,3 +178,24 @@ export function htmlToDom(html: string): HTMLElement {
return firstChild;
}
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;
}

View File

@ -144,4 +144,25 @@ function getVersion(): string {
export function importVersioned(moduleName: string): Promise<object> {
const importPath = window.baseUrl(`dist/${moduleName}.js?version=${getVersion()}`);
return import(importPath);
}
/*
cyrb53 (c) 2018 bryc (github.com/bryc)
License: Public domain (or MIT if needed). Attribution appreciated.
A fast and simple 53-bit string hash function with decent collision resistance.
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
Taken from: https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
*/
export function cyrb53(str: string, seed: number = 0): string {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for(let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
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;
}