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