mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-20 20:31:25 +08:00
Comments: Added inline comment marker/highlight logic
Some checks failed
test-migrations / build (8.3) (push) Has been cancelled
analyse-php / build (push) Has been cancelled
lint-js / build (push) Has been cancelled
lint-php / build (push) Has been cancelled
test-js / build (push) Has been cancelled
test-migrations / build (8.2) (push) Has been cancelled
test-migrations / build (8.4) (push) Has been cancelled
test-php / build (8.2) (push) Has been cancelled
test-php / build (8.3) (push) Has been cancelled
test-php / build (8.4) (push) Has been cancelled
Some checks failed
test-migrations / build (8.3) (push) Has been cancelled
analyse-php / build (push) Has been cancelled
lint-js / build (push) Has been cancelled
lint-php / build (push) Has been cancelled
test-js / build (push) Has been cancelled
test-migrations / build (8.2) (push) Has been cancelled
test-migrations / build (8.4) (push) Has been cancelled
test-php / build (8.2) (push) Has been cancelled
test-php / build (8.3) (push) Has been cancelled
test-php / build (8.4) (push) Has been cancelled
This commit is contained in:
@ -1,6 +1,7 @@
|
|||||||
import {Component} from './component';
|
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 {buildForInput} from '../wysiwyg-tinymce/config';
|
||||||
|
import {el} from "../wysiwyg/utils/dom";
|
||||||
|
|
||||||
export class PageComment extends Component {
|
export class PageComment extends Component {
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ export class PageComment extends Component {
|
|||||||
this.input = this.$refs.input as HTMLInputElement;
|
this.input = this.$refs.input as HTMLInputElement;
|
||||||
|
|
||||||
this.setupListeners();
|
this.setupListeners();
|
||||||
|
this.positionForReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupListeners(): void {
|
protected setupListeners(): void {
|
||||||
@ -135,4 +137,47 @@ export class PageComment extends Component {
|
|||||||
return loading;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import * as DOM from '../services/dom.ts';
|
import * as DOM from '../services/dom.ts';
|
||||||
import {Component} from './component';
|
import {Component} from './component';
|
||||||
import {copyTextToClipboard} from '../services/clipboard.ts';
|
import {copyTextToClipboard} from '../services/clipboard.ts';
|
||||||
import {cyrb53} from "../services/util";
|
import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom.ts";
|
||||||
import {normalizeNodeTextOffsetToParent} from "../services/dom.ts";
|
|
||||||
import {PageComments} from "./page-comments";
|
import {PageComments} from "./page-comments";
|
||||||
|
|
||||||
export class Pointer extends Component {
|
export class Pointer extends Component {
|
||||||
@ -183,9 +182,8 @@ export class Pointer extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalisedElemHtml = this.targetElement.outerHTML.replace(/\s{2,}/g, '');
|
|
||||||
const refId = this.targetElement.id;
|
const refId = this.targetElement.id;
|
||||||
const hash = cyrb53(normalisedElemHtml);
|
const hash = hashElement(this.targetElement);
|
||||||
let range = '';
|
let range = '';
|
||||||
if (this.targetSelectionRange) {
|
if (this.targetSelectionRange) {
|
||||||
const commonContainer = this.targetSelectionRange.commonAncestorContainer;
|
const commonContainer = this.targetSelectionRange.commonAncestorContainer;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import {cyrb53} from "./util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given param is a HTMLElement
|
* Check if the given param is a HTMLElement
|
||||||
*/
|
*/
|
||||||
@ -181,6 +183,9 @@ export function htmlToDom(html: string): HTMLElement {
|
|||||||
return firstChild;
|
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 {
|
export function normalizeNodeTextOffsetToParent(node: Node, offset: number, parentElement: HTMLElement): number {
|
||||||
if (!parentElement.contains(node)) {
|
if (!parentElement.contains(node)) {
|
||||||
throw new Error('ParentElement must be a prent of element');
|
throw new Error('ParentElement must be a prent of element');
|
||||||
@ -201,3 +206,54 @@ export function normalizeNodeTextOffsetToParent(node: Node, offset: number, pare
|
|||||||
|
|
||||||
return normalizedOffset;
|
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);
|
||||||
|
}
|
@ -164,5 +164,5 @@ export function cyrb53(str: string, seed: number = 0): string {
|
|||||||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
||||||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||||
return (4294967296 * (2097151 & h2) + (h1 >>> 0)) as string;
|
return String((4294967296 * (2097151 & h2) + (h1 >>> 0)));
|
||||||
}
|
}
|
@ -219,6 +219,27 @@ body.tox-fullscreen, body.markdown-fullscreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Page editor sidebar toolbox
|
||||||
.floating-toolbox {
|
.floating-toolbox {
|
||||||
@include mixins.lightDark(background-color, #FFF, #222);
|
@include mixins.lightDark(background-color, #FFF, #222);
|
||||||
|
Reference in New Issue
Block a user