]> BookStack Code Mirror - bookstack/blob - resources/js/components/pointer.js
Merge branch 'fix-ldap-display-name' into development
[bookstack] / resources / js / components / pointer.js
1 import * as DOM from '../services/dom.ts';
2 import {Component} from './component';
3 import {copyTextToClipboard} from '../services/clipboard.ts';
4
5 export class Pointer extends Component {
6
7     setup() {
8         this.container = this.$el;
9         this.pointer = this.$refs.pointer;
10         this.linkInput = this.$refs.linkInput;
11         this.linkButton = this.$refs.linkButton;
12         this.includeInput = this.$refs.includeInput;
13         this.includeButton = this.$refs.includeButton;
14         this.sectionModeButton = this.$refs.sectionModeButton;
15         this.modeToggles = this.$manyRefs.modeToggle;
16         this.modeSections = this.$manyRefs.modeSection;
17         this.pageId = this.$opts.pageId;
18
19         // Instance variables
20         this.showing = false;
21         this.isSelection = false;
22
23         this.setupListeners();
24     }
25
26     setupListeners() {
27         // Copy on copy button click
28         this.includeButton.addEventListener('click', () => copyTextToClipboard(this.includeInput.value));
29         this.linkButton.addEventListener('click', () => copyTextToClipboard(this.linkInput.value));
30
31         // Select all contents on input click
32         DOM.onSelect([this.includeInput, this.linkInput], event => {
33             event.target.select();
34             event.stopPropagation();
35         });
36
37         // Prevent closing pointer when clicked or focused
38         DOM.onEvents(this.pointer, ['click', 'focus'], event => {
39             event.stopPropagation();
40         });
41
42         // Hide pointer when clicking away
43         DOM.onEvents(document.body, ['click', 'focus'], () => {
44             if (!this.showing || this.isSelection) return;
45             this.hidePointer();
46         });
47
48         // Hide pointer on escape press
49         DOM.onEscapePress(this.pointer, this.hidePointer.bind(this));
50
51         // Show pointer when selecting a single block of tagged content
52         const pageContent = document.querySelector('.page-content');
53         DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => {
54             event.stopPropagation();
55             const targetEl = event.target.closest('[id^="bkmrk"]');
56             if (targetEl && window.getSelection().toString().length > 0) {
57                 this.showPointerAtTarget(targetEl, event.pageX, false);
58             }
59         });
60
61         // Start section selection mode on button press
62         DOM.onSelect(this.sectionModeButton, this.enterSectionSelectMode.bind(this));
63
64         // Toggle between pointer modes
65         DOM.onSelect(this.modeToggles, event => {
66             for (const section of this.modeSections) {
67                 const show = !section.contains(event.target);
68                 section.toggleAttribute('hidden', !show);
69             }
70
71             this.modeToggles.find(b => b !== event.target).focus();
72         });
73     }
74
75     hidePointer() {
76         this.pointer.style.display = null;
77         this.showing = false;
78     }
79
80     /**
81      * Move and display the pointer at the given element, targeting the given screen x-position if possible.
82      * @param {Element} element
83      * @param {Number} xPosition
84      * @param {Boolean} keyboardMode
85      */
86     showPointerAtTarget(element, xPosition, keyboardMode) {
87         this.updateForTarget(element);
88
89         this.pointer.style.display = 'block';
90         const targetBounds = element.getBoundingClientRect();
91         const pointerBounds = this.pointer.getBoundingClientRect();
92
93         const xTarget = Math.min(Math.max(xPosition, targetBounds.left), targetBounds.right);
94         const xOffset = xTarget - (pointerBounds.width / 2);
95         const yOffset = (targetBounds.top - pointerBounds.height) - 16;
96
97         this.pointer.style.left = `${xOffset}px`;
98         this.pointer.style.top = `${yOffset}px`;
99
100         this.showing = true;
101         this.isSelection = true;
102
103         setTimeout(() => {
104             this.isSelection = false;
105         }, 100);
106
107         const scrollListener = () => {
108             this.hidePointer();
109             window.removeEventListener('scroll', scrollListener, {passive: true});
110         };
111
112         element.parentElement.insertBefore(this.pointer, element);
113         if (!keyboardMode) {
114             window.addEventListener('scroll', scrollListener, {passive: true});
115         }
116     }
117
118     /**
119      * Update the pointer inputs/content for the given target element.
120      * @param {?Element} element
121      */
122     updateForTarget(element) {
123         const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`);
124         const includeTag = `{{@${this.pageId}#${element.id}}}`;
125
126         this.linkInput.value = permaLink;
127         this.includeInput.value = includeTag;
128
129         // Update anchor if present
130         const editAnchor = this.pointer.querySelector('#pointer-edit');
131         if (editAnchor && element) {
132             const {editHref} = editAnchor.dataset;
133             const elementId = element.id;
134
135             // Get the first 50 characters.
136             const queryContent = element.textContent && element.textContent.substring(0, 50);
137             editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
138         }
139     }
140
141     enterSectionSelectMode() {
142         const sections = Array.from(document.querySelectorAll('.page-content [id^="bkmrk"]'));
143         for (const section of sections) {
144             section.setAttribute('tabindex', '0');
145         }
146
147         sections[0].focus();
148
149         DOM.onEnterPress(sections, event => {
150             this.showPointerAtTarget(event.target, 0, true);
151             this.pointer.focus();
152         });
153     }
154
155 }