Skip to content

Commit 18ede9b

Browse files
committed
Comments: Added inline comment marker/highlight logic
1 parent 2e7544a commit 18ede9b

File tree

5 files changed

+126
-6
lines changed

5 files changed

+126
-6
lines changed

resources/js/components/page-comment.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Component} from './component';
2-
import {getLoading, htmlToDom} from '../services/dom.ts';
2+
import {findTargetNodeAndOffset, getLoading, hashElement, htmlToDom} from '../services/dom.ts';
33
import {buildForInput} from '../wysiwyg-tinymce/config';
4+
import {el} from "../wysiwyg/utils/dom";
45

56
export class PageComment extends Component {
67

@@ -46,6 +47,7 @@ export class PageComment extends Component {
4647
this.input = this.$refs.input as HTMLInputElement;
4748

4849
this.setupListeners();
50+
this.positionForReference();
4951
}
5052

5153
protected setupListeners(): void {
@@ -135,4 +137,47 @@ export class PageComment extends Component {
135137
return loading;
136138
}
137139

140+
protected positionForReference() {
141+
if (!this.commentContentRef) {
142+
return;
143+
}
144+
145+
const [refId, refHash, refRange] = this.commentContentRef.split(':');
146+
const refEl = document.getElementById(refId);
147+
if (!refEl) {
148+
// TODO - Show outdated marker for comment
149+
return;
150+
}
151+
152+
const actualHash = hashElement(refEl);
153+
if (actualHash !== refHash) {
154+
// TODO - Show outdated marker for comment
155+
return;
156+
}
157+
158+
const refElBounds = refEl.getBoundingClientRect();
159+
let bounds = refElBounds;
160+
const [rangeStart, rangeEnd] = refRange.split('-');
161+
if (rangeStart && rangeEnd) {
162+
const range = new Range();
163+
const relStart = findTargetNodeAndOffset(refEl, Number(rangeStart));
164+
const relEnd = findTargetNodeAndOffset(refEl, Number(rangeEnd));
165+
if (relStart && relEnd) {
166+
range.setStart(relStart.node, relStart.offset);
167+
range.setEnd(relEnd.node, relEnd.offset);
168+
bounds = range.getBoundingClientRect();
169+
}
170+
}
171+
172+
const relLeft = bounds.left - refElBounds.left;
173+
const relTop = bounds.top - refElBounds.top;
174+
// TODO - Extract to class, Use theme color
175+
const marker = el('div', {
176+
class: 'content-comment-highlight',
177+
style: `left: ${relLeft}px; top: ${relTop}px; width: ${bounds.width}px; height: ${bounds.height}px;`
178+
}, ['']);
179+
180+
refEl.style.position = 'relative';
181+
refEl.append(marker);
182+
}
138183
}

resources/js/components/pointer.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as DOM from '../services/dom.ts';
22
import {Component} from './component';
33
import {copyTextToClipboard} from '../services/clipboard.ts';
4-
import {cyrb53} from "../services/util";
5-
import {normalizeNodeTextOffsetToParent} from "../services/dom.ts";
4+
import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom.ts";
65
import {PageComments} from "./page-comments";
76

87
export class Pointer extends Component {
@@ -183,9 +182,8 @@ export class Pointer extends Component {
183182
return;
184183
}
185184

186-
const normalisedElemHtml = this.targetElement.outerHTML.replace(/\s{2,}/g, '');
187185
const refId = this.targetElement.id;
188-
const hash = cyrb53(normalisedElemHtml);
186+
const hash = hashElement(this.targetElement);
189187
let range = '';
190188
if (this.targetSelectionRange) {
191189
const commonContainer = this.targetSelectionRange.commonAncestorContainer;

resources/js/services/dom.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {cyrb53} from "./util";
2+
13
/**
24
* Check if the given param is a HTMLElement
35
*/
@@ -181,6 +183,9 @@ export function htmlToDom(html: string): HTMLElement {
181183
return firstChild;
182184
}
183185

186+
/**
187+
* For the given node and offset, return an adjusted offset that's relative to the given parent element.
188+
*/
184189
export function normalizeNodeTextOffsetToParent(node: Node, offset: number, parentElement: HTMLElement): number {
185190
if (!parentElement.contains(node)) {
186191
throw new Error('ParentElement must be a prent of element');
@@ -201,3 +206,54 @@ export function normalizeNodeTextOffsetToParent(node: Node, offset: number, pare
201206

202207
return normalizedOffset;
203208
}
209+
210+
/**
211+
* Find the target child node and adjusted offset based on a parent node and text offset.
212+
* Returns null if offset not found within the given parent node.
213+
*/
214+
export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number): ({node: Node, offset: number}|null) {
215+
if (offset === 0) {
216+
return { node: parentNode, offset: 0 };
217+
}
218+
219+
let currentOffset = 0;
220+
let currentNode = null;
221+
222+
for (let i = 0; i < parentNode.childNodes.length; i++) {
223+
currentNode = parentNode.childNodes[i];
224+
225+
if (currentNode.nodeType === Node.TEXT_NODE) {
226+
// For text nodes, count the length of their content
227+
// Returns if within range
228+
const textLength = currentNode.textContent.length;
229+
if (currentOffset + textLength >= offset) {
230+
return {
231+
node: currentNode,
232+
offset: offset - currentOffset
233+
};
234+
}
235+
236+
currentOffset += textLength;
237+
} else if (currentNode.nodeType === Node.ELEMENT_NODE) {
238+
// Otherwise, if an element, track the text length and search within
239+
// if in range for the target offset
240+
const elementTextLength = currentNode.textContent.length;
241+
if (currentOffset + elementTextLength >= offset) {
242+
return findTargetNodeAndOffset(currentNode, offset - currentOffset);
243+
}
244+
245+
currentOffset += elementTextLength;
246+
}
247+
}
248+
249+
// Return null if not found within range
250+
return null;
251+
}
252+
253+
/**
254+
* Create a hash for the given HTML element.
255+
*/
256+
export function hashElement(element: HTMLElement): string {
257+
const normalisedElemHtml = element.outerHTML.replace(/\s{2,}/g, '');
258+
return cyrb53(normalisedElemHtml);
259+
}

resources/js/services/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,5 +164,5 @@ export function cyrb53(str: string, seed: number = 0): string {
164164
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
165165
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
166166
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
167-
return (4294967296 * (2097151 & h2) + (h1 >>> 0)) as string;
167+
return String((4294967296 * (2097151 & h2) + (h1 >>> 0)));
168168
}

resources/sass/_pages.scss

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,27 @@ body.tox-fullscreen, body.markdown-fullscreen {
219219
}
220220
}
221221

222+
// Page inline comments
223+
.content-comment-highlight {
224+
position: absolute;
225+
left: 0;
226+
top: 0;
227+
width: 0;
228+
height: 0;
229+
user-select: none;
230+
pointer-events: none;
231+
&:after {
232+
content: '';
233+
position: absolute;
234+
left: 0;
235+
top: 0;
236+
width: 100%;
237+
height: 100%;
238+
background-color: var(--color-primary);
239+
opacity: 0.25;
240+
}
241+
}
242+
222243
// Page editor sidebar toolbox
223244
.floating-toolbox {
224245
@include mixins.lightDark(background-color, #FFF, #222);

0 commit comments

Comments
 (0)