]> BookStack Code Mirror - bookstack/blob - resources/assets/js/components/page-display.js
Adds edit icon to each header in the page.
[bookstack] / resources / assets / js / components / page-display.js
1 import Clipboard from "clipboard";
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         this.setupEditOnHeader();
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         $('.sidebar-page-nav').on('click', 'a', event => {
24             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 = () => {
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
85         // Show pointer when selecting a single block of tagged content
86         $('.page-content [id^="bkmrk"]').on('mouseup keyup', function (e) {
87             e.stopPropagation();
88             let selection = window.getSelection();
89             if (selection.toString().length === 0) return;
90
91             // Show pointer and set link
92             let $elem = $(this);
93             pointerSectionId = $elem.attr('id');
94             updatePointerContent();
95
96             $elem.before($pointer);
97             $pointer.show();
98             pointerShowing = true;
99
100             // Set pointer to sit near mouse-up position
101             let pointerLeftOffset = (e.pageX - $elem.offset().left - ($pointerInner.width() / 2));
102             if (pointerLeftOffset < 0) pointerLeftOffset = 0;
103             let pointerLeftOffsetPercent = (pointerLeftOffset / $elem.width()) * 100;
104             $pointerInner.css('left', pointerLeftOffsetPercent + '%');
105
106             isSelection = true;
107             setTimeout(() => {
108                 isSelection = false;
109             }, 100);
110         });
111     }
112
113     setupStickySidebar() {
114         // Make the sidebar stick in view on scroll
115         let $window = $(window);
116         let $sidebar = $("#sidebar .scroll-body");
117         let $bookTreeParent = $sidebar.parent();
118
119         // Check the page is scrollable and the content is taller than the tree
120         let pageScrollable = ($(document).height() > $window.height()) && ($sidebar.height() < $('.page-content').height());
121
122         // Get current tree's width and header height
123         let headerHeight = $("#header").height() + $(".toolbar").height();
124         let isFixed = $window.scrollTop() > headerHeight;
125
126         // Fix the tree as a sidebar
127         function stickTree() {
128             $sidebar.width($bookTreeParent.width() + 15);
129             $sidebar.addClass("fixed");
130             isFixed = true;
131         }
132
133         // Un-fix the tree back into position
134         function unstickTree() {
135             $sidebar.css('width', 'auto');
136             $sidebar.removeClass("fixed");
137             isFixed = false;
138         }
139
140         // Checks if the tree stickiness state should change
141         function checkTreeStickiness(skipCheck) {
142             let shouldBeFixed = $window.scrollTop() > headerHeight;
143             if (shouldBeFixed && (!isFixed || skipCheck)) {
144                 stickTree();
145             } else if (!shouldBeFixed && (isFixed || skipCheck)) {
146                 unstickTree();
147             }
148         }
149         // The event ran when the window scrolls
150         function windowScrollEvent() {
151             checkTreeStickiness(false);
152         }
153
154         // If the page is scrollable and the window is wide enough listen to scroll events
155         // and evaluate tree stickiness.
156         if (pageScrollable && $window.width() > 1000) {
157             $window.on('scroll', windowScrollEvent);
158             checkTreeStickiness(true);
159         }
160
161         // Handle window resizing and switch between desktop/mobile views
162         $window.on('resize', event => {
163             if (pageScrollable && $window.width() > 1000) {
164                 $window.on('scroll', windowScrollEvent);
165                 checkTreeStickiness(true);
166             } else {
167                 $window.off('scroll', windowScrollEvent);
168                 unstickTree();
169             }
170         });
171     }
172
173     setupNavHighlighting() {
174         // Check if support is present for IntersectionObserver
175         if (!'IntersectionObserver' in window ||
176             !'IntersectionObserverEntry' in window ||
177             !'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
178             return;
179         }
180
181         let pageNav = document.querySelector('.sidebar-page-nav');
182
183         // fetch all the headings.
184         let headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
185         // if headings are present, add observers.
186         if (headings.length > 0 && pageNav !== null) {
187             addNavObserver(headings);
188         }
189
190         function addNavObserver(headings) {
191             // Setup the intersection observer.
192             let intersectOpts = {
193                 rootMargin: '0px 0px 0px 0px',
194                 threshold: 1.0
195             };
196             let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
197
198             // observe each heading
199             for (let i = 0; i !== headings.length; ++i) {
200                 pageNavObserver.observe(headings[i]);
201             }
202         }
203
204         function headingVisibilityChange(entries, observer) {
205             for (let entry of entries) {
206                 let isVisible = (entry.intersectionRatio === 1);
207                 toggleAnchorHighlighting(entry.target.id, isVisible);
208             }
209         }
210
211         function toggleAnchorHighlighting(elementId, shouldHighlight) {
212             let anchorsToHighlight = pageNav.querySelectorAll('a[href="#' + elementId + '"]');
213             for (let i = 0; i < anchorsToHighlight.length; i++) {
214                 // Change below to use classList.toggle when IE support is dropped.
215                 if (shouldHighlight) {
216                     anchorsToHighlight[i].classList.add('current-heading');
217                 } else {
218                     anchorsToHighlight[i].classList.remove('current-heading');
219                 }
220             }
221         }
222     }
223     setupEditOnHeader() {
224         const headingEditIcon = document.querySelector('.heading-edit-icon');
225         if (headingEditIcon.length === 0) {
226             // user does not have permission to edit.
227             return;
228         }
229
230         // Create a clone of the edit icon without the hidden class
231         const visibleHeadingEditIcon = headingEditIcon.cloneNode(true);
232         visibleHeadingEditIcon.style.display = '';
233
234         const headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
235
236         // add an edit icon to each header.
237         for (let i = 0; i !== headings.length; ++i) {
238             const currHeading = headings[i];
239             const headingId = currHeading.id;
240
241             let editIcon = visibleHeadingEditIcon.cloneNode(true);
242             editIcon.href += `#${headingId}`;
243
244             currHeading.appendChild(editIcon);
245         }
246     }
247
248 }
249
250 module.exports = PageDisplay;