mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-24 14:41:25 +08:00
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:
@ -1,6 +1,9 @@
|
|||||||
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 {el} from "../wysiwyg/utils/dom";
|
||||||
|
import {cyrb53} from "../services/util";
|
||||||
|
import {normalizeNodeTextOffsetToParent} from "../services/dom.ts";
|
||||||
|
|
||||||
export class Pointer extends Component {
|
export class Pointer extends Component {
|
||||||
|
|
||||||
@ -12,13 +15,16 @@ export class Pointer extends Component {
|
|||||||
this.includeInput = this.$refs.includeInput;
|
this.includeInput = this.$refs.includeInput;
|
||||||
this.includeButton = this.$refs.includeButton;
|
this.includeButton = this.$refs.includeButton;
|
||||||
this.sectionModeButton = this.$refs.sectionModeButton;
|
this.sectionModeButton = this.$refs.sectionModeButton;
|
||||||
|
this.commentButton = this.$refs.commentButton;
|
||||||
this.modeToggles = this.$manyRefs.modeToggle;
|
this.modeToggles = this.$manyRefs.modeToggle;
|
||||||
this.modeSections = this.$manyRefs.modeSection;
|
this.modeSections = this.$manyRefs.modeSection;
|
||||||
this.pageId = this.$opts.pageId;
|
this.pageId = this.$opts.pageId;
|
||||||
|
|
||||||
// Instance variables
|
// Instance variables
|
||||||
this.showing = false;
|
this.showing = false;
|
||||||
this.isSelection = false;
|
this.isMakingSelection = false;
|
||||||
|
this.targetElement = null;
|
||||||
|
this.targetSelectionRange = null;
|
||||||
|
|
||||||
this.setupListeners();
|
this.setupListeners();
|
||||||
}
|
}
|
||||||
@ -41,7 +47,7 @@ export class Pointer extends Component {
|
|||||||
|
|
||||||
// Hide pointer when clicking away
|
// Hide pointer when clicking away
|
||||||
DOM.onEvents(document.body, ['click', 'focus'], () => {
|
DOM.onEvents(document.body, ['click', 'focus'], () => {
|
||||||
if (!this.showing || this.isSelection) return;
|
if (!this.showing || this.isMakingSelection) return;
|
||||||
this.hidePointer();
|
this.hidePointer();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -70,11 +76,17 @@ export class Pointer extends Component {
|
|||||||
|
|
||||||
this.modeToggles.find(b => b !== event.target).focus();
|
this.modeToggles.find(b => b !== event.target).focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.commentButton) {
|
||||||
|
DOM.onSelect(this.commentButton, this.createCommentAtPointer.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hidePointer() {
|
hidePointer() {
|
||||||
this.pointer.style.display = null;
|
this.pointer.style.display = null;
|
||||||
this.showing = false;
|
this.showing = false;
|
||||||
|
this.targetElement = null;
|
||||||
|
this.targetSelectionRange = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,7 +96,9 @@ export class Pointer extends Component {
|
|||||||
* @param {Boolean} keyboardMode
|
* @param {Boolean} keyboardMode
|
||||||
*/
|
*/
|
||||||
showPointerAtTarget(element, xPosition, 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';
|
this.pointer.style.display = 'block';
|
||||||
const targetBounds = element.getBoundingClientRect();
|
const targetBounds = element.getBoundingClientRect();
|
||||||
@ -98,10 +112,10 @@ export class Pointer extends Component {
|
|||||||
this.pointer.style.top = `${yOffset}px`;
|
this.pointer.style.top = `${yOffset}px`;
|
||||||
|
|
||||||
this.showing = true;
|
this.showing = true;
|
||||||
this.isSelection = true;
|
this.isMakingSelection = true;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.isSelection = false;
|
this.isMakingSelection = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
const scrollListener = () => {
|
const scrollListener = () => {
|
||||||
@ -119,7 +133,7 @@ export class Pointer extends Component {
|
|||||||
* Update the pointer inputs/content for the given target element.
|
* Update the pointer inputs/content for the given target element.
|
||||||
* @param {?Element} element
|
* @param {?Element} element
|
||||||
*/
|
*/
|
||||||
updateForTarget(element) {
|
updateDomForTarget(element) {
|
||||||
const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`);
|
const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`);
|
||||||
const includeTag = `{{@${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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -178,3 +178,24 @@ export function htmlToDom(html: string): HTMLElement {
|
|||||||
|
|
||||||
return firstChild;
|
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;
|
||||||
|
}
|
||||||
|
@ -145,3 +145,24 @@ export function importVersioned(moduleName: string): Promise<object> {
|
|||||||
const importPath = window.baseUrl(`dist/${moduleName}.js?version=${getVersion()}`);
|
const importPath = window.baseUrl(`dist/${moduleName}.js?version=${getVersion()}`);
|
||||||
return import(importPath);
|
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;
|
||||||
|
}
|
@ -183,7 +183,6 @@ body.tox-fullscreen, body.markdown-fullscreen {
|
|||||||
}
|
}
|
||||||
input, button, a {
|
input, button, a {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 0;
|
|
||||||
height: 28px;
|
height: 28px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
@ -194,17 +193,19 @@ body.tox-fullscreen, body.markdown-fullscreen {
|
|||||||
border: 1px solid #DDD;
|
border: 1px solid #DDD;
|
||||||
@include mixins.lightDark(border-color, #ddd, #000);
|
@include mixins.lightDark(border-color, #ddd, #000);
|
||||||
color: #666;
|
color: #666;
|
||||||
width: 160px;
|
width: 180px;
|
||||||
z-index: 40;
|
z-index: 58;
|
||||||
padding: 5px 10px;
|
padding: 5px;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
.text-button {
|
.text-button {
|
||||||
@include mixins.lightDark(color, #444, #AAA);
|
@include mixins.lightDark(color, #444, #AAA);
|
||||||
}
|
}
|
||||||
.input-group .button {
|
.input-group .button {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
margin: 0 0 0 -4px;
|
margin: 0 0 0 -5px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
a.button {
|
a.button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -6,14 +6,14 @@
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-label="{{ trans('entities.pages_pointer_label') }}"
|
aria-label="{{ trans('entities.pages_pointer_label') }}"
|
||||||
class="pointer-container">
|
class="pointer-container">
|
||||||
<div class="pointer flex-container-row items-center justify-space-between p-s anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
|
<div class="pointer flex-container-row items-center justify-space-between gap-xs p-xs anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
|
||||||
<div refs="pointer@mode-section" class="flex-container-row items-center gap-s">
|
<div refs="pointer@mode-section" class="flex-container-row items-center gap-xs">
|
||||||
<button refs="pointer@mode-toggle"
|
<button refs="pointer@mode-toggle"
|
||||||
title="{{ trans('entities.pages_pointer_toggle_link') }}"
|
title="{{ trans('entities.pages_pointer_toggle_link') }}"
|
||||||
class="text-button icon px-xs">@icon('link')</button>
|
class="text-button icon px-xs">@icon('link')</button>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input refs="pointer@link-input" aria-label="{{ trans('entities.pages_pointer_permalink') }}" readonly="readonly" type="text" id="pointer-url" placeholder="url">
|
<input refs="pointer@link-input" aria-label="{{ trans('entities.pages_pointer_permalink') }}" readonly="readonly" type="text" id="pointer-url" placeholder="url">
|
||||||
<button refs="pointer@link-button" class="button outline icon" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
|
<button refs="pointer@link-button" class="button outline icon px-xs" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div refs="pointer@mode-section" hidden class="flex-container-row items-center gap-s">
|
<div refs="pointer@mode-section" hidden class="flex-container-row items-center gap-s">
|
||||||
@ -22,13 +22,20 @@
|
|||||||
class="text-button icon px-xs">@icon('include')</button>
|
class="text-button icon px-xs">@icon('include')</button>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input refs="pointer@include-input" aria-label="{{ trans('entities.pages_pointer_include_tag') }}" readonly="readonly" type="text" id="pointer-include" placeholder="include">
|
<input refs="pointer@include-input" aria-label="{{ trans('entities.pages_pointer_include_tag') }}" readonly="readonly" type="text" id="pointer-include" placeholder="include">
|
||||||
<button refs="pointer@include-button" class="button outline icon" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
|
<button refs="pointer@include-button" class="button outline icon px-xs" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
@if(userCan('page-update', $page))
|
@if(userCan('page-update', $page))
|
||||||
<a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
|
<a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
|
||||||
class="button primary outline icon heading-edit-icon ml-s px-s" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
|
class="button primary outline icon heading-edit-icon px-xs" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
|
||||||
@endif
|
@endif
|
||||||
|
@if($commentTree->enabled() && userCan('comment-create-all'))
|
||||||
|
<button type="button"
|
||||||
|
refs="pointer@comment-button"
|
||||||
|
class="button primary outline icon px-xs m-none" title="{{ trans('entities.comment_add')}}">@icon('comment')</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user