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:
		
							parent
							
								
									fa566f156a
								
							
						
					
					
						commit
						8d159f77e4
					
				| 
						 | 
					@ -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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue