]> BookStack Code Mirror - bookstack/blob - resources/assets/js/components/page-display.js
Prevented normal users from changing own email
[bookstack] / resources / assets / js / components / page-display.js
1 import Clipboard from "clipboard/dist/clipboard.min";
2 import Code from "../services/code";
3
4 class PageDisplay {
5
6     constructor(elem) {
7         this.elem = elem;
8         this.pageId = elem.getAttribute('page-display');
9
10         Code.highlight();
11         this.setupPointer();
12         this.setupStickySidebar();
13         this.setupNavHighlighting();
14
15         // Check the hash on load
16         if (window.location.hash) {
17             let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
18             this.goToText(text);
19         }
20
21         // Sidebar page nav click event
22         $('.sidebar-page-nav').on('click', 'a', event => {
23             window.components['tri-layout'][0].showContent();
24             this.goToText(event.target.getAttribute('href').substr(1));
25         });
26     }
27
28     goToText(text) {
29         let idElem = document.getElementById(text);
30         $('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', '');
31         if (idElem !== null) {
32             window.scrollAndHighlight(idElem);
33         } else {
34             $('.page-content').find(':contains("' + text + '")').smoothScrollTo();
35         }
36     }
37
38     setupPointer() {
39         if (document.getElementById('pointer') === null) return;
40         // Set up pointer
41         let $pointer = $('#pointer').detach();
42         let pointerShowing = false;
43         let $pointerInner = $pointer.children('div.pointer').first();
44         let isSelection = false;
45         let pointerModeLink = true;
46         let pointerSectionId = '';
47
48         // Select all contents on input click
49         $pointer.on('click', 'input', event => {
50             $(this).select();
51             event.stopPropagation();
52         });
53
54         $pointer.on('click focus', event => {
55             event.stopPropagation();
56         });
57
58         // Pointer mode toggle
59         $pointer.on('click', 'span.icon', event => {
60             event.stopPropagation();
61             let $icon = $(event.currentTarget);
62             pointerModeLink = !pointerModeLink;
63             $icon.find('[data-icon="include"]').toggle(!pointerModeLink);
64             $icon.find('[data-icon="link"]').toggle(pointerModeLink);
65             updatePointerContent();
66         });
67
68         // Set up clipboard
69         let clipboard = new Clipboard($pointer[0].querySelector('button'));
70
71         // Hide pointer when clicking away
72         $(document.body).find('*').on('click focus', event => {
73             if (!pointerShowing || isSelection) return;
74             $pointer.detach();
75             pointerShowing = false;
76         });
77
78         let updatePointerContent = ($elem) => {
79             let inputText = pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${pointerSectionId}`) : `{{@${this.pageId}#${pointerSectionId}}}`;
80             if (pointerModeLink && inputText.indexOf('http') !== 0) inputText = window.location.protocol + "//" + window.location.host + inputText;
81
82             $pointer.find('input').val(inputText);
83
84             // update anchor if present
85             const $editAnchor = $pointer.find('#pointer-edit');
86             if ($editAnchor.length !== 0 && $elem) {
87                 const editHref = $editAnchor.data('editHref');
88                 const element = $elem[0];
89                 const elementId = element.id;
90
91                 // get the first 50 characters.
92                 let queryContent = element.textContent && element.textContent.substring(0, 50);
93                 $editAnchor[0].href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
94             }
95         };
96
97         // Show pointer when selecting a single block of tagged content
98         $('.page-content [id^="bkmrk"]').on('mouseup keyup', function (e) {
99             e.stopPropagation();
100             let selection = window.getSelection();
101             if (selection.toString().length === 0) return;
102
103             // Show pointer and set link
104             let $elem = $(this);
105             pointerSectionId = $elem.attr('id');
106             updatePointerContent($elem);
107
108             $elem.before($pointer);
109             $pointer.show();
110             pointerShowing = true;
111
112             // Set pointer to sit near mouse-up position
113             let pointerLeftOffset = (e.pageX - $elem.offset().left - ($pointerInner.width() / 2));
114             if (pointerLeftOffset < 0) pointerLeftOffset = 0;
115             let pointerLeftOffsetPercent = (pointerLeftOffset / $elem.width()) * 100;
116             $pointerInner.css('left', pointerLeftOffsetPercent + '%');
117
118             isSelection = true;
119             setTimeout(() => {
120                 isSelection = false;
121             }, 100);
122         });
123     }
124
125     setupStickySidebar() {
126         // Make the sidebar stick in view on scroll
127         const $window = $(window);
128         const $sidebar = $("#sidebar .scroll-body");
129         const $sidebarContainer = $sidebar.parent();
130         const sidebarHeight = $sidebar.height() + 32;
131
132         // Check the page is scrollable and the content is taller than the tree
133         const pageScrollable = ($(document).height() > ($window.height() + 40)) && (sidebarHeight < $('.page-content').height());
134
135         // Get current tree's width and header height
136         const headerHeight = $("#header").height() + $(".toolbar").height();
137         let isFixed = $window.scrollTop() > headerHeight;
138
139         // Fix the tree as a sidebar
140         function stickTree() {
141             $sidebar.width($sidebarContainer.width() + 15);
142             $sidebar.addClass("fixed");
143             isFixed = true;
144         }
145
146         // Un-fix the tree back into position
147         function unstickTree() {
148             $sidebar.css('width', 'auto');
149             $sidebar.removeClass("fixed");
150             isFixed = false;
151         }
152
153         // Checks if the tree stickiness state should change
154         function checkTreeStickiness(skipCheck) {
155             let shouldBeFixed = $window.scrollTop() > headerHeight;
156             if (shouldBeFixed && (!isFixed || skipCheck)) {
157                 stickTree();
158             } else if (!shouldBeFixed && (isFixed || skipCheck)) {
159                 unstickTree();
160             }
161         }
162         // The event ran when the window scrolls
163         function windowScrollEvent() {
164             checkTreeStickiness(false);
165         }
166
167         // If the page is scrollable and the window is wide enough listen to scroll events
168         // and evaluate tree stickiness.
169         if (pageScrollable && $window.width() > 1000) {
170             $window.on('scroll', windowScrollEvent);
171             checkTreeStickiness(true);
172         }
173
174         // Handle window resizing and switch between desktop/mobile views
175         $window.on('resize', event => {
176             if (pageScrollable && $window.width() > 1000) {
177                 $window.on('scroll', windowScrollEvent);
178                 checkTreeStickiness(true);
179             } else {
180                 $window.off('scroll', windowScrollEvent);
181                 unstickTree();
182             }
183         });
184     }
185
186     setupNavHighlighting() {
187         // Check if support is present for IntersectionObserver
188         if (!('IntersectionObserver' in window) ||
189             !('IntersectionObserverEntry' in window) ||
190             !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
191             return;
192         }
193
194         let pageNav = document.querySelector('.sidebar-page-nav');
195
196         // fetch all the headings.
197         let headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
198         // if headings are present, add observers.
199         if (headings.length > 0 && pageNav !== null) {
200             addNavObserver(headings);
201         }
202
203         function addNavObserver(headings) {
204             // Setup the intersection observer.
205             let intersectOpts = {
206                 rootMargin: '0px 0px 0px 0px',
207                 threshold: 1.0
208             };
209             let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
210
211             // observe each heading
212             for (let heading of headings) {
213                 pageNavObserver.observe(heading);
214             }
215         }
216
217         function headingVisibilityChange(entries, observer) {
218             for (let entry of entries) {
219                 let isVisible = (entry.intersectionRatio === 1);
220                 toggleAnchorHighlighting(entry.target.id, isVisible);
221             }
222         }
223
224         function toggleAnchorHighlighting(elementId, shouldHighlight) {
225             const anchorsToHighlight = pageNav.querySelectorAll('a[href="#' + elementId + '"]');
226             for (let anchor of anchorsToHighlight) {
227                 anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
228             }
229         }
230     }
231 }
232
233 export default PageDisplay;