diff --git a/resources/js/components/pointer.js b/resources/js/components/pointer.js index 292b923e5..997df329a 100644 --- a/resources/js/components/pointer.js +++ b/resources/js/components/pointer.js @@ -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); + } + } diff --git a/resources/js/services/dom.ts b/resources/js/services/dom.ts index c88827bac..779b48547 100644 --- a/resources/js/services/dom.ts +++ b/resources/js/services/dom.ts @@ -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; +} diff --git a/resources/js/services/util.ts b/resources/js/services/util.ts index c5a5d2db8..1a6fa55b6 100644 --- a/resources/js/services/util.ts +++ b/resources/js/services/util.ts @@ -144,4 +144,25 @@ function getVersion(): string { export function importVersioned(moduleName: string): Promise { 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; } \ No newline at end of file diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index 45e58ffc8..de7837057 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -183,7 +183,6 @@ body.tox-fullscreen, body.markdown-fullscreen { } input, button, a { position: relative; - border-radius: 0; height: 28px; font-size: 12px; vertical-align: top; @@ -194,17 +193,19 @@ body.tox-fullscreen, body.markdown-fullscreen { border: 1px solid #DDD; @include mixins.lightDark(border-color, #ddd, #000); color: #666; - width: 160px; - z-index: 40; - padding: 5px 10px; + width: 180px; + z-index: 58; + padding: 5px; + border-radius: 0; } .text-button { @include mixins.lightDark(color, #444, #AAA); } .input-group .button { line-height: 1; - margin: 0 0 0 -4px; + margin: 0 0 0 -5px; box-shadow: none; + border-radius: 0; } a.button { margin: 0; diff --git a/resources/views/pages/parts/pointer.blade.php b/resources/views/pages/parts/pointer.blade.php index 56f36cb75..77fc76382 100644 --- a/resources/views/pages/parts/pointer.blade.php +++ b/resources/views/pages/parts/pointer.blade.php @@ -6,14 +6,14 @@ tabindex="-1" aria-label="{{ trans('entities.pages_pointer_label') }}" class="pointer-container"> -
-
+
+
- +
- @if(userCan('page-update', $page)) - @icon('edit') - @endif +
+ @if(userCan('page-update', $page)) + @icon('edit') + @endif + @if($commentTree->enabled() && userCan('comment-create-all')) + + @endif +