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