]> BookStack Code Mirror - bookstack/blob - resources/js/components/pointer.js
Merge pull request #3853 from BookStackApp/component_refactor
[bookstack] / resources / js / components / pointer.js
1 import * as DOM from "../services/dom";
2 import Clipboard from "clipboard/dist/clipboard.min";
3 import {Component} from "./component";
4
5
6 export class Pointer extends Component {
7
8     setup() {
9         this.container = this.$el;
10         this.pageId = this.$opts.pageId;
11
12         // Instance variables
13         this.showing = false;
14         this.isSelection = false;
15         this.pointerModeLink = true;
16         this.pointerSectionId = '';
17
18         this.setupListeners();
19
20         // Set up clipboard
21         new Clipboard(this.container.querySelector('button'));
22     }
23
24     setupListeners() {
25         // Select all contents on input click
26         DOM.onChildEvent(this.container, 'input', 'click', (event, input) => {
27             input.select();
28             event.stopPropagation();
29         });
30
31         // Prevent closing pointer when clicked or focused
32         DOM.onEvents(this.container, ['click', 'focus'], event => {
33             event.stopPropagation();
34         });
35
36         // Pointer mode toggle
37         DOM.onChildEvent(this.container, 'span.icon', 'click', (event, icon) => {
38             event.stopPropagation();
39             this.pointerModeLink = !this.pointerModeLink;
40             icon.querySelector('[data-icon="include"]').style.display = (!this.pointerModeLink) ? 'inline' : 'none';
41             icon.querySelector('[data-icon="link"]').style.display = (this.pointerModeLink) ? 'inline' : 'none';
42             this.updateForTarget();
43         });
44
45         // Hide pointer when clicking away
46         DOM.onEvents(document.body, ['click', 'focus'], event => {
47             if (!this.showing || this.isSelection) return;
48             this.hidePointer();
49         });
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) {
57                 this.showPointerAtTarget(targetEl, event.pageX);
58             }
59         });
60     }
61
62     hidePointer() {
63         this.container.style.display = null;
64         this.showing = false;
65     }
66
67     /**
68      * Move and display the pointer at the given element, targeting the given screen x-position if possible.
69      * @param {Element} element
70      * @param {Number} xPosition
71      */
72     showPointerAtTarget(element, xPosition) {
73         const selection = window.getSelection();
74         if (selection.toString().length === 0) return;
75
76         // Show pointer and set link
77         this.pointerSectionId = element.id;
78         this.updateForTarget(element);
79
80         this.container.style.display = 'block';
81         const targetBounds = element.getBoundingClientRect();
82         const pointerBounds = this.container.getBoundingClientRect();
83
84         const xTarget = Math.min(Math.max(xPosition, targetBounds.left), targetBounds.right);
85         const xOffset = xTarget - (pointerBounds.width / 2);
86         const yOffset = (targetBounds.top - pointerBounds.height) - 16;
87
88         this.container.style.left = `${xOffset}px`;
89         this.container.style.top = `${yOffset}px`;
90
91         this.showing = true;
92         this.isSelection = true;
93
94         setTimeout(() => {
95             this.isSelection = false;
96         }, 100);
97
98         const scrollListener = () => {
99             this.hidePointer();
100             window.removeEventListener('scroll', scrollListener, {passive: true});
101         };
102         window.addEventListener('scroll', scrollListener, {passive: true});
103     }
104
105     /**
106      * Update the pointer inputs/content for the given target element.
107      * @param {?Element} element
108      */
109     updateForTarget(element) {
110         let inputText = this.pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${this.pointerSectionId}`) : `{{@${this.pageId}#${this.pointerSectionId}}}`;
111         if (this.pointerModeLink && !inputText.startsWith('http')) {
112             inputText = window.location.protocol + "//" + window.location.host + inputText;
113         }
114
115         this.container.querySelector('input').value = inputText;
116
117         // Update anchor if present
118         const editAnchor = this.container.querySelector('#pointer-edit');
119         if (editAnchor && element) {
120             const editHref = editAnchor.dataset.editHref;
121             const elementId = element.id;
122
123             // get the first 50 characters.
124             const queryContent = element.textContent && element.textContent.substring(0, 50);
125             editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
126         }
127     }
128 }