Adds button for comments to pointer.
Adds logic to generate a content reference point.
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 {
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();
}
// 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();
});
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;
}
/**
* @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();
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 = () => {
* 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}}}`;
});
}
+ 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);
+ }
+
}
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;
+}
export function importVersioned(moduleName: string): Promise<object> {
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
}
input, button, a {
position: relative;
- border-radius: 0;
height: 28px;
font-size: 12px;
vertical-align: top;
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;
tabindex="-1"
aria-label="{{ trans('entities.pages_pointer_label') }}"
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 refs="pointer@mode-section" class="flex-container-row items-center gap-s">
+ <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-xs">
<button refs="pointer@mode-toggle"
title="{{ trans('entities.pages_pointer_toggle_link') }}"
class="text-button icon px-xs">@icon('link')</button>
<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">
- <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 refs="pointer@mode-section" hidden class="flex-container-row items-center gap-s">
class="text-button icon px-xs">@icon('include')</button>
<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">
- <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>
- @if(userCan('page-update', $page))
- <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>
- @endif
+ <div>
+ @if(userCan('page-update', $page))
+ <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
+ class="button primary outline icon heading-edit-icon px-xs" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
+ @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>