1 | /*
|
---|
2 | MIT License
|
---|
3 |
|
---|
4 | Copyright (c) 2020 Haiku, Inc.
|
---|
5 |
|
---|
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
|
---|
7 | of this software and associated documentation files (the "Software"), to deal
|
---|
8 | in the Software without restriction, including without limitation the rights
|
---|
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
---|
10 | copies of the Software, and to permit persons to whom the Software is
|
---|
11 | furnished to do so, subject to the following conditions:
|
---|
12 |
|
---|
13 | The above copyright notice and this permission notice shall be included in all
|
---|
14 | copies or substantial portions of the Software.
|
---|
15 |
|
---|
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
---|
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
---|
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
---|
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
---|
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
---|
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
---|
22 | SOFTWARE.
|
---|
23 | */
|
---|
24 |
|
---|
25 | ;(function (window, doc) {
|
---|
26 |
|
---|
27 | const bodyClassList = doc.getElementsByTagName('body')[0].classList;
|
---|
28 | const contentDiv = doc.getElementById('content');
|
---|
29 | const passive = { passive: true };
|
---|
30 |
|
---|
31 | let scrollTimeout = null;
|
---|
32 | let internalHashChange = false;
|
---|
33 | let currentTopic = null;
|
---|
34 |
|
---|
35 | function slides() {
|
---|
36 | return bodyClassList.contains('slides');
|
---|
37 | }
|
---|
38 |
|
---|
39 | function toggleFormat() {
|
---|
40 | processPendingScroll();
|
---|
41 | bodyClassList.toggle('slides');
|
---|
42 | topics.get(currentTopic).slide.scrollIntoView(true);
|
---|
43 | setLocationHash('');
|
---|
44 | }
|
---|
45 |
|
---|
46 | function nextTopic() {
|
---|
47 | processPendingScroll();
|
---|
48 | const topic = topics.get(currentTopic);
|
---|
49 | if (topic != null && topic.next != null) {
|
---|
50 | setTopic(topic.next);
|
---|
51 | topics.get(topic.next).slide.scrollIntoView(true);
|
---|
52 | }
|
---|
53 | }
|
---|
54 |
|
---|
55 | function previousTopic() {
|
---|
56 | processPendingScroll();
|
---|
57 | const topic = topics.get(currentTopic);
|
---|
58 | if (topic != null && topic.prev != null) {
|
---|
59 | setTopic(topic.prev);
|
---|
60 | topics.get(topic.prev).slide.scrollIntoView(true);
|
---|
61 | }
|
---|
62 | }
|
---|
63 |
|
---|
64 | /* The translation tool wants exactly <div id="content">\n<div>
|
---|
65 | * to insert its "translation incomplete" box. Removing the topic
|
---|
66 | * class from the welcome page breaks the slide layout. Adding it
|
---|
67 | * back on load only shows the box in the first slide. Having an
|
---|
68 | * extra div when there's no box adds too much whitespace to the
|
---|
69 | * top of the slides. So we remove it here if it has no content.
|
---|
70 | */
|
---|
71 | {
|
---|
72 | let emptyDiv = doc.querySelector('#content>div:empty');
|
---|
73 | if (emptyDiv != null) {
|
---|
74 | emptyDiv.remove();
|
---|
75 | }
|
---|
76 | }
|
---|
77 |
|
---|
78 | let topics = new Map();
|
---|
79 | {
|
---|
80 | let prev = null;
|
---|
81 | for (let topicDiv of doc.querySelectorAll('div.topic')) {
|
---|
82 | let topic = '#' + topicDiv.querySelector('h1 a').name;
|
---|
83 | topics.set(topic, {
|
---|
84 | slide: topicDiv,
|
---|
85 | navdot: null,
|
---|
86 | next: null,
|
---|
87 | prev: prev,
|
---|
88 | });
|
---|
89 | prev = topic;
|
---|
90 | }
|
---|
91 | for (let dot of doc.querySelectorAll('.navdots a acronym')) {
|
---|
92 | topics.get((new URL(dot.parentNode.href)).hash).navdot = dot;
|
---|
93 | }
|
---|
94 | for (let [k,v] of topics) {
|
---|
95 | prev = v.prev;
|
---|
96 | if (prev !== null) {
|
---|
97 | topics.get(prev).next = k;
|
---|
98 | }
|
---|
99 | }
|
---|
100 | }
|
---|
101 |
|
---|
102 | function setTopic(hash) {
|
---|
103 | if (hash == null || hash.length == 0) {
|
---|
104 | hash = topics.keys().next().value;
|
---|
105 | } else if (!topics.has(hash)) {
|
---|
106 | hash = hash.substring(1);
|
---|
107 | let element = doc.getElementById(hash)
|
---|
108 | || doc.querySelector('[name='+hash+']');
|
---|
109 | if (element != null) {
|
---|
110 | element = element.closest('div.topic');
|
---|
111 | }
|
---|
112 | if (element != null) {
|
---|
113 | hash = '#' + element.querySelector('h1 a').name;
|
---|
114 | }
|
---|
115 | if (!topics.has(hash)) {
|
---|
116 | hash = topics.keys().next().value;
|
---|
117 | }
|
---|
118 | }
|
---|
119 |
|
---|
120 | if (hash === currentTopic) return;
|
---|
121 |
|
---|
122 | let topic;
|
---|
123 | if (currentTopic !== null) {
|
---|
124 | topic = topics.get(currentTopic);
|
---|
125 | topic.slide.classList.remove('current-page');
|
---|
126 | topic.navdot.classList.remove('current-page');
|
---|
127 | }
|
---|
128 | topic = topics.get(hash);
|
---|
129 | topic.slide.classList.add('current-page');
|
---|
130 | topic.navdot.classList.add('current-page');
|
---|
131 | currentTopic = hash;
|
---|
132 | }
|
---|
133 |
|
---|
134 |
|
---|
135 | function checkPosition() {
|
---|
136 | const containerRect = contentDiv.getBoundingClientRect();
|
---|
137 | const minTop = containerRect.top;
|
---|
138 | const maxTop = minTop + (containerRect.bottom - minTop) / 3;
|
---|
139 | let bestValue = -9999;
|
---|
140 | let bestTopic = null;
|
---|
141 | for (let [topic, v] of topics) {
|
---|
142 | const topicRect = v.slide.getBoundingClientRect();
|
---|
143 | if (topicRect.top < maxTop) {
|
---|
144 | if (topicRect.top >= minTop) {
|
---|
145 | if (topicRect.top < bestValue || bestValue < minTop) {
|
---|
146 | bestValue = topicRect.top;
|
---|
147 | bestTopic = topic;
|
---|
148 | }
|
---|
149 | } else if (topicRect.top > bestValue) {
|
---|
150 | bestValue = topicRect.top;
|
---|
151 | bestTopic = topic;
|
---|
152 | }
|
---|
153 | }
|
---|
154 | }
|
---|
155 | if (bestTopic !== null) {
|
---|
156 | setTopic(bestTopic);
|
---|
157 | }
|
---|
158 | }
|
---|
159 |
|
---|
160 | function checkScroll() {
|
---|
161 | scrollTimeout = null;
|
---|
162 | if (!slides()) {
|
---|
163 | checkPosition();
|
---|
164 | }
|
---|
165 | }
|
---|
166 |
|
---|
167 | function processPendingScroll() {
|
---|
168 | if (scrollTimeout != null) {
|
---|
169 | window.clearTimeout(scrollTimeout);
|
---|
170 | checkScroll();
|
---|
171 | }
|
---|
172 | }
|
---|
173 |
|
---|
174 | function positionChangeHandler() {
|
---|
175 | if (scrollTimeout === null) {
|
---|
176 | scrollTimeout = window.setTimeout(checkScroll, 200);
|
---|
177 | }
|
---|
178 | }
|
---|
179 |
|
---|
180 | contentDiv.addEventListener('scroll', positionChangeHandler, passive);
|
---|
181 | window.addEventListener('resize', positionChangeHandler, passive);
|
---|
182 | window.addEventListener('orientationchange', positionChangeHandler, passive);
|
---|
183 |
|
---|
184 |
|
---|
185 | function setLocationHash(hash) {
|
---|
186 | if (window.location.hash != hash) {
|
---|
187 | internalHashChange = true;
|
---|
188 | window.location.hash = hash;
|
---|
189 | }
|
---|
190 | }
|
---|
191 |
|
---|
192 | window.addEventListener('hashchange', function() {
|
---|
193 | if (!internalHashChange) {
|
---|
194 | setTopic(window.location.hash);
|
---|
195 | }
|
---|
196 | internalHashChange = false;
|
---|
197 | }, passive);
|
---|
198 |
|
---|
199 | doc.getElementById('toggle').addEventListener(
|
---|
200 | 'click', toggleFormat, passive);
|
---|
201 | doc.getElementById('prevtopic').addEventListener(
|
---|
202 | 'click', previousTopic, passive);
|
---|
203 | doc.getElementById('nexttopic').addEventListener(
|
---|
204 | 'click', nextTopic, passive);
|
---|
205 | doc.getElementById('prevtopic-bottom').addEventListener(
|
---|
206 | 'click', previousTopic, passive);
|
---|
207 | doc.getElementById('nexttopic-bottom').addEventListener(
|
---|
208 | 'click', nextTopic, passive);
|
---|
209 |
|
---|
210 | for (let element of doc.querySelectorAll('.hide-no-js')) {
|
---|
211 | element.classList.remove('hide-no-js');
|
---|
212 | }
|
---|
213 |
|
---|
214 | /* Body is fixed, with the scrollable element being #content.
|
---|
215 | * So let's scroll #content with events triggered outside.
|
---|
216 | * No fancy inertia or smooth scrolling, though.
|
---|
217 | */
|
---|
218 | {
|
---|
219 | const targetInContent = (e) => e.target.closest('#content') != null;
|
---|
220 | const lines = (l) => 20 * l;
|
---|
221 | const pages = (p) => p * (contentDiv.getBoundingClientRect().height - lines(1));
|
---|
222 |
|
---|
223 | let wantScroll = 0;
|
---|
224 | let lockedScroll = false;
|
---|
225 |
|
---|
226 | function lockScroll() {
|
---|
227 | lockedScroll = true;
|
---|
228 | window.setTimeout(function () {
|
---|
229 | lockedScroll = false;
|
---|
230 | }, 400);
|
---|
231 | }
|
---|
232 |
|
---|
233 | function doScroll() {
|
---|
234 | if (lockedScroll) {
|
---|
235 | wantScroll = 0;
|
---|
236 | return;
|
---|
237 | }
|
---|
238 |
|
---|
239 | let curScroll = contentDiv.scrollTop;
|
---|
240 | contentDiv.scrollTop += wantScroll;
|
---|
241 | if (curScroll == contentDiv.scrollTop) {
|
---|
242 | wantScroll /= lines(3);
|
---|
243 | if (wantScroll > 1) {
|
---|
244 | nextTopic();
|
---|
245 | lockScroll();
|
---|
246 | } else if (wantScroll < -1) {
|
---|
247 | previousTopic();
|
---|
248 | lockScroll();
|
---|
249 | }
|
---|
250 | }
|
---|
251 | wantScroll = 0;
|
---|
252 | }
|
---|
253 |
|
---|
254 | function requestScroll(px) {
|
---|
255 | if (wantScroll === 0) {
|
---|
256 | window.requestAnimationFrame(doScroll);
|
---|
257 | }
|
---|
258 | wantScroll += px;
|
---|
259 | }
|
---|
260 |
|
---|
261 | function forceScroll(px) {
|
---|
262 | lockedScroll = false;
|
---|
263 | requestScroll(px);
|
---|
264 | }
|
---|
265 |
|
---|
266 | doc.addEventListener('wheel', function (event) {
|
---|
267 | if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)
|
---|
268 | return;
|
---|
269 | if (event.target.closest('.lang-menu') != null)
|
---|
270 | return;
|
---|
271 | switch (event.deltaMode) {
|
---|
272 | case event.DOM_DELTA_PIXEL:
|
---|
273 | requestScroll(event.deltaY);
|
---|
274 | break;
|
---|
275 | case event.DOM_DELTA_PAGE:
|
---|
276 | requestScroll(pages(event.deltaY > 0 ? 1 : -1));
|
---|
277 | break;
|
---|
278 | default:
|
---|
279 | requestScroll(lines(event.deltaY));
|
---|
280 | break;
|
---|
281 | }
|
---|
282 | }, passive);
|
---|
283 | doc.addEventListener('keydown', function (event) {
|
---|
284 | if (!targetInContent(event)) {
|
---|
285 | switch (event.key) {
|
---|
286 | case 'PageDown':
|
---|
287 | forceScroll(pages(1));
|
---|
288 | break;
|
---|
289 | case 'PageUp':
|
---|
290 | forceScroll(-pages(1));
|
---|
291 | break;
|
---|
292 | case 'ArrowDown':
|
---|
293 | requestScroll(lines(1));
|
---|
294 | break;
|
---|
295 | case 'ArrowUp':
|
---|
296 | requestScroll(lines(-1));
|
---|
297 | break;
|
---|
298 | }
|
---|
299 | }
|
---|
300 | if (event.ctrlKey || event.metaKey) {
|
---|
301 | switch (event.key) {
|
---|
302 | case 'ArrowLeft':
|
---|
303 | previousTopic();
|
---|
304 | break;
|
---|
305 | case 'ArrowRight':
|
---|
306 | nextTopic();
|
---|
307 | break;
|
---|
308 | case 'm':
|
---|
309 | toggleFormat();
|
---|
310 | break;
|
---|
311 | }
|
---|
312 | }
|
---|
313 | }, passive);
|
---|
314 | }
|
---|
315 |
|
---|
316 | setTopic(window.location.hash);
|
---|
317 |
|
---|
318 | // Change to slide mode, except in the translation tool
|
---|
319 | if (typeof source_strings === "undefined") {
|
---|
320 | toggleFormat();
|
---|
321 | }
|
---|
322 |
|
---|
323 | } (window, document));
|
---|