]> BookStack Code Mirror - bookstack/blob - resources/assets/js/components/page-display.js
Added locale and text direction to html templates
[bookstack] / resources / assets / js / components / page-display.js
1 import Clipboard from "clipboard/dist/clipboard.min";
2 import Code from "../services/code";
3 import * as DOM from "../services/dom";
4 import {scrollAndHighlightElement} from "../services/util";
5
6 class PageDisplay {
7
8     constructor(elem) {
9         this.elem = elem;
10         this.pageId = elem.getAttribute('page-display');
11
12         Code.highlight();
13         this.setupPointer();
14         this.setupNavHighlighting();
15
16         // Check the hash on load
17         if (window.location.hash) {
18             let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
19             this.goToText(text);
20         }
21
22         // Sidebar page nav click event
23         const sidebarPageNav = document.querySelector('.sidebar-page-nav');
24         if (sidebarPageNav) {
25             DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {
26                 window.components['tri-layout'][0].showContent();
27                 this.goToText(child.getAttribute('href').substr(1));
28             });
29         }
30     }
31
32     goToText(text) {
33         const idElem = document.getElementById(text);
34
35         DOM.forEach('.page-content [data-highlighted]', elem => {
36             elem.removeAttribute('data-highlighted');
37             elem.style.backgroundColor = null;
38         });
39
40         if (idElem !== null) {
41             scrollAndHighlightElement(idElem);
42         } else {
43             const textElem = DOM.findText('.page-content > div > *', text);
44             if (textElem) {
45                 scrollAndHighlightElement(textElem);
46             }
47         }
48     }
49
50     setupPointer() {
51         let pointer = document.getElementById('pointer');
52         if (!pointer) {
53             return;
54         }
55
56         // Set up pointer
57         pointer = pointer.parentNode.removeChild(pointer);
58         const pointerInner = pointer.querySelector('div.pointer');
59
60         // Instance variables
61         let pointerShowing = false;
62         let isSelection = false;
63         let pointerModeLink = true;
64         let pointerSectionId = '';
65
66         // Select all contents on input click
67         DOM.onChildEvent(pointer, 'input', 'click', (event, input) => {
68             input.select();
69             event.stopPropagation();
70         });
71
72         // Prevent closing pointer when clicked or focused
73         DOM.onEvents(pointer, ['click', 'focus'], event => {
74             event.stopPropagation();
75         });
76
77         // Pointer mode toggle
78         DOM.onChildEvent(pointer, 'span.icon', 'click', (event, icon) => {
79             event.stopPropagation();
80             pointerModeLink = !pointerModeLink;
81             icon.querySelector('[data-icon="include"]').style.display = (!pointerModeLink) ? 'inline' : 'none';
82             icon.querySelector('[data-icon="link"]').style.display = (pointerModeLink) ? 'inline' : 'none';
83             updatePointerContent();
84         });
85
86         // Set up clipboard
87         new Clipboard(pointer.querySelector('button'));
88
89         // Hide pointer when clicking away
90         DOM.onEvents(document.body, ['click', 'focus'], event => {
91             if (!pointerShowing || isSelection) return;
92             pointer = pointer.parentElement.removeChild(pointer);
93             pointerShowing = false;
94         });
95
96         let updatePointerContent = (element) => {
97             let inputText = pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${pointerSectionId}`) : `{{@${this.pageId}#${pointerSectionId}}}`;
98             if (pointerModeLink && !inputText.startsWith('http')) {
99                 inputText = window.location.protocol + "//" + window.location.host + inputText;
100             }
101
102             pointer.querySelector('input').value = inputText;
103
104             // Update anchor if present
105             const editAnchor = pointer.querySelector('#pointer-edit');
106             if (editAnchor && element) {
107                 const editHref = editAnchor.dataset.editHref;
108                 const elementId = element.id;
109
110                 // get the first 50 characters.
111                 const queryContent = element.textContent && element.textContent.substring(0, 50);
112                 editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
113             }
114         };
115
116         // Show pointer when selecting a single block of tagged content
117         DOM.forEach('.page-content [id^="bkmrk"]', bookMarkElem => {
118             DOM.onEvents(bookMarkElem, ['mouseup', 'keyup'], event => {
119                 event.stopPropagation();
120                 let selection = window.getSelection();
121                 if (selection.toString().length === 0) return;
122
123                 // Show pointer and set link
124                 pointerSectionId = bookMarkElem.id;
125                 updatePointerContent(bookMarkElem);
126
127                 bookMarkElem.parentNode.insertBefore(pointer, bookMarkElem);
128                 pointer.style.display = 'block';
129                 pointerShowing = true;
130                 isSelection = true;
131
132                 // Set pointer to sit near mouse-up position
133                 requestAnimationFrame(() => {
134                     const bookMarkBounds = bookMarkElem.getBoundingClientRect();
135                     let pointerLeftOffset = (event.pageX - bookMarkBounds.left - 164);
136                     if (pointerLeftOffset < 0) {
137                         pointerLeftOffset = 0
138                     }
139                     const pointerLeftOffsetPercent = (pointerLeftOffset / bookMarkBounds.width) * 100;
140
141                     pointerInner.style.left = pointerLeftOffsetPercent + '%';
142
143                     setTimeout(() => {
144                         isSelection = false;
145                     }, 100);
146                 });
147
148             });
149         });
150     }
151
152     setupNavHighlighting() {
153         // Check if support is present for IntersectionObserver
154         if (!('IntersectionObserver' in window) ||
155             !('IntersectionObserverEntry' in window) ||
156             !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
157             return;
158         }
159
160         let pageNav = document.querySelector('.sidebar-page-nav');
161
162         // fetch all the headings.
163         let headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
164         // if headings are present, add observers.
165         if (headings.length > 0 && pageNav !== null) {
166             addNavObserver(headings);
167         }
168
169         function addNavObserver(headings) {
170             // Setup the intersection observer.
171             let intersectOpts = {
172                 rootMargin: '0px 0px 0px 0px',
173                 threshold: 1.0
174             };
175             let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
176
177             // observe each heading
178             for (let heading of headings) {
179                 pageNavObserver.observe(heading);
180             }
181         }
182
183         function headingVisibilityChange(entries, observer) {
184             for (let entry of entries) {
185                 let isVisible = (entry.intersectionRatio === 1);
186                 toggleAnchorHighlighting(entry.target.id, isVisible);
187             }
188         }
189
190         function toggleAnchorHighlighting(elementId, shouldHighlight) {
191             DOM.forEach('a[href="#' + elementId + '"]', anchor => {
192                 anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
193             });
194         }
195     }
196 }
197
198 export default PageDisplay;