1 import * as DOM from '../services/dom.ts';
2 import {Component} from './component';
3 import {copyTextToClipboard} from '../services/clipboard.ts';
4 import {el} from "../wysiwyg/utils/dom";
5 import {cyrb53} from "../services/util";
6 import {normalizeNodeTextOffsetToParent} from "../services/dom.ts";
8 export class Pointer extends Component {
11 this.container = this.$el;
12 this.pointer = this.$refs.pointer;
13 this.linkInput = this.$refs.linkInput;
14 this.linkButton = this.$refs.linkButton;
15 this.includeInput = this.$refs.includeInput;
16 this.includeButton = this.$refs.includeButton;
17 this.sectionModeButton = this.$refs.sectionModeButton;
18 this.commentButton = this.$refs.commentButton;
19 this.modeToggles = this.$manyRefs.modeToggle;
20 this.modeSections = this.$manyRefs.modeSection;
21 this.pageId = this.$opts.pageId;
25 this.isMakingSelection = false;
26 this.targetElement = null;
27 this.targetSelectionRange = null;
29 this.setupListeners();
33 // Copy on copy button click
34 this.includeButton.addEventListener('click', () => copyTextToClipboard(this.includeInput.value));
35 this.linkButton.addEventListener('click', () => copyTextToClipboard(this.linkInput.value));
37 // Select all contents on input click
38 DOM.onSelect([this.includeInput, this.linkInput], event => {
39 event.target.select();
40 event.stopPropagation();
43 // Prevent closing pointer when clicked or focused
44 DOM.onEvents(this.pointer, ['click', 'focus'], event => {
45 event.stopPropagation();
48 // Hide pointer when clicking away
49 DOM.onEvents(document.body, ['click', 'focus'], () => {
50 if (!this.showing || this.isMakingSelection) return;
54 // Hide pointer on escape press
55 DOM.onEscapePress(this.pointer, this.hidePointer.bind(this));
57 // Show pointer when selecting a single block of tagged content
58 const pageContent = document.querySelector('.page-content');
59 DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => {
60 event.stopPropagation();
61 const targetEl = event.target.closest('[id^="bkmrk"]');
62 if (targetEl && window.getSelection().toString().length > 0) {
63 this.showPointerAtTarget(targetEl, event.pageX, false);
67 // Start section selection mode on button press
68 DOM.onSelect(this.sectionModeButton, this.enterSectionSelectMode.bind(this));
70 // Toggle between pointer modes
71 DOM.onSelect(this.modeToggles, event => {
72 for (const section of this.modeSections) {
73 const show = !section.contains(event.target);
74 section.toggleAttribute('hidden', !show);
77 this.modeToggles.find(b => b !== event.target).focus();
80 if (this.commentButton) {
81 DOM.onSelect(this.commentButton, this.createCommentAtPointer.bind(this));
86 this.pointer.style.display = null;
88 this.targetElement = null;
89 this.targetSelectionRange = null;
93 * Move and display the pointer at the given element, targeting the given screen x-position if possible.
94 * @param {Element} element
95 * @param {Number} xPosition
96 * @param {Boolean} keyboardMode
98 showPointerAtTarget(element, xPosition, keyboardMode) {
99 this.targetElement = element;
100 this.targetSelectionRange = window.getSelection()?.getRangeAt(0);
101 this.updateDomForTarget(element);
103 this.pointer.style.display = 'block';
104 const targetBounds = element.getBoundingClientRect();
105 const pointerBounds = this.pointer.getBoundingClientRect();
107 const xTarget = Math.min(Math.max(xPosition, targetBounds.left), targetBounds.right);
108 const xOffset = xTarget - (pointerBounds.width / 2);
109 const yOffset = (targetBounds.top - pointerBounds.height) - 16;
111 this.pointer.style.left = `${xOffset}px`;
112 this.pointer.style.top = `${yOffset}px`;
115 this.isMakingSelection = true;
118 this.isMakingSelection = false;
121 const scrollListener = () => {
123 window.removeEventListener('scroll', scrollListener, {passive: true});
126 element.parentElement.insertBefore(this.pointer, element);
128 window.addEventListener('scroll', scrollListener, {passive: true});
133 * Update the pointer inputs/content for the given target element.
134 * @param {?Element} element
136 updateDomForTarget(element) {
137 const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`);
138 const includeTag = `{{@${this.pageId}#${element.id}}}`;
140 this.linkInput.value = permaLink;
141 this.includeInput.value = includeTag;
143 // Update anchor if present
144 const editAnchor = this.pointer.querySelector('#pointer-edit');
145 if (editAnchor && element) {
146 const {editHref} = editAnchor.dataset;
147 const elementId = element.id;
149 // Get the first 50 characters.
150 const queryContent = element.textContent && element.textContent.substring(0, 50);
151 editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
155 enterSectionSelectMode() {
156 const sections = Array.from(document.querySelectorAll('.page-content [id^="bkmrk"]'));
157 for (const section of sections) {
158 section.setAttribute('tabindex', '0');
163 DOM.onEnterPress(sections, event => {
164 this.showPointerAtTarget(event.target, 0, true);
165 this.pointer.focus();
169 createCommentAtPointer(event) {
170 if (!this.targetElement) {
174 const normalisedElemHtml = this.targetElement.outerHTML.replace(/\s{2,}/g, '');
175 const refId = this.targetElement.id;
176 const hash = cyrb53(normalisedElemHtml);
178 if (this.targetSelectionRange) {
179 const commonContainer = this.targetSelectionRange.commonAncestorContainer;
180 if (this.targetElement.contains(commonContainer)) {
181 const start = normalizeNodeTextOffsetToParent(
182 this.targetSelectionRange.startContainer,
183 this.targetSelectionRange.startOffset,
186 const end = normalizeNodeTextOffsetToParent(
187 this.targetSelectionRange.endContainer,
188 this.targetSelectionRange.endOffset,
191 range = `${start}-${end}`;
195 const reference = `${refId}:${hash}:${range}`;
196 console.log(reference);