@farberg/reveal-template 1.1.23 → 1.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/css/dhbw.css CHANGED
@@ -30,7 +30,7 @@ body {
30
30
 
31
31
  .reveal {
32
32
  font-family: 'Source Sans Pro', Helvetica, sans-serif;
33
- font-size: 30px;
33
+ font-size: 31px;
34
34
  font-weight: normal;
35
35
  /* color: #222; */
36
36
  color: #4e565c;
package/init-reveal.js CHANGED
@@ -10,6 +10,7 @@ import ToggleSolutionsPlugin from './plugins/reveal-plugin-toggle-solutions.js';
10
10
  import DirTreePlugin from './plugins/reveal-plugin-dir-tree.js';
11
11
  import PrefixUrlPlugin from './plugins/reveal-plugin-prefix-with-base-url.js';
12
12
  import AsciinemaPlugin from './plugins/reveal-plugin-asciinema.js';
13
+ import QuickNavPlugin from './plugins/reveal-plugin-quick-nav.js';
13
14
 
14
15
  // All third-party paths below are written as bare-package specifiers
15
16
  // (e.g. "reveal.js/dist/reveal.mjs"). At runtime they are resolved to
@@ -297,7 +298,8 @@ function buildDennisPlugins(deps) {
297
298
  ToggleSolutionsPlugin,
298
299
  DirTreePlugin(deps.fflate),
299
300
  PrefixUrlPlugin,
300
- AsciinemaPlugin(deps.asciinemaPlayer.create)
301
+ AsciinemaPlugin(deps.asciinemaPlayer.create),
302
+ QuickNavPlugin
301
303
  ]
302
304
  }
303
305
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farberg/reveal-template",
3
- "version": "1.1.23",
3
+ "version": "1.1.25",
4
4
  "homepage": "https://github.com/pfisterer/reveal-template",
5
5
  "description": "Reveal.js template for Dennis' lectures",
6
6
  "main": "index.js",
@@ -0,0 +1,319 @@
1
+ /*
2
+ Quick Navigation Overlay
3
+
4
+ Press 'q' to open (or close) an overlay listing all h1 and h2 headings
5
+ across the deck. Each heading is a link that jumps to the slide. A text
6
+ input at the top filters the list as you type. Press ESC (or click the
7
+ backdrop / close button) to dismiss the overlay.
8
+
9
+ The list is rendered in multiple columns so 30-60 slides with headings
10
+ can be scanned at a glance.
11
+ */
12
+
13
+ function collectHeadings(deck) {
14
+ const items = []
15
+ const horizontalSlides = deck.getSlidesElement().querySelectorAll(':scope > section')
16
+
17
+ horizontalSlides.forEach((hSection, h) => {
18
+ const verticals = hSection.querySelectorAll(':scope > section')
19
+ if (verticals.length > 0) {
20
+ verticals.forEach((vSection, v) => extractFromSection(vSection, h, v, items))
21
+ } else {
22
+ extractFromSection(hSection, h, 0, items)
23
+ }
24
+ })
25
+
26
+ return items
27
+ }
28
+
29
+ function extractFromSection(section, h, v, items) {
30
+ const headings = section.querySelectorAll('h1, h2')
31
+ if (headings.length === 0) return
32
+ const slideText = (section.innerText || section.textContent || '').toLowerCase()
33
+ headings.forEach(heading => {
34
+ const text = heading.textContent.trim()
35
+ if (!text) return
36
+ items.push({
37
+ text,
38
+ level: heading.tagName.toLowerCase(),
39
+ searchText: slideText,
40
+ h,
41
+ v
42
+ })
43
+ })
44
+ }
45
+
46
+ function injectStyles() {
47
+ if (document.getElementById('quick-nav-styles')) return
48
+
49
+ const css = `
50
+ #quick-nav-overlay {
51
+ position: fixed;
52
+ inset: 0;
53
+ background: rgba(0,0,0,0.65);
54
+ z-index: 10000;
55
+ display: none;
56
+ font-family: sans-serif;
57
+ }
58
+ #quick-nav-overlay.visible { display: block; }
59
+ #quick-nav-overlay .quick-nav-panel {
60
+ position: absolute;
61
+ top: 3vh;
62
+ left: 50%;
63
+ transform: translateX(-50%);
64
+ width: 92vw;
65
+ max-width: 1500px;
66
+ max-height: 94vh;
67
+ background: #fff;
68
+ border-radius: 8px;
69
+ box-shadow: 0 12px 48px rgba(0,0,0,0.4);
70
+ display: flex;
71
+ flex-direction: column;
72
+ overflow: hidden;
73
+ }
74
+ #quick-nav-overlay .quick-nav-header {
75
+ display: flex;
76
+ align-items: center;
77
+ padding: 12px 16px;
78
+ border-bottom: 1px solid #e0e0e0;
79
+ background: #f7f7f7;
80
+ gap: 12px;
81
+ }
82
+ #quick-nav-overlay .quick-nav-title {
83
+ font-weight: bold;
84
+ color: #555;
85
+ font-size: 13px;
86
+ white-space: nowrap;
87
+ }
88
+ #quick-nav-overlay .quick-nav-search {
89
+ flex: 1;
90
+ font-size: 14px;
91
+ padding: 6px 10px;
92
+ border: 1px solid #ccc;
93
+ border-radius: 4px;
94
+ outline: none;
95
+ }
96
+ #quick-nav-overlay .quick-nav-search:focus {
97
+ border-color: #e2001a;
98
+ box-shadow: 0 0 0 2px rgba(226,0,26,0.2);
99
+ }
100
+ #quick-nav-overlay .quick-nav-count {
101
+ color: #888;
102
+ font-size: 11px;
103
+ white-space: nowrap;
104
+ }
105
+ #quick-nav-overlay .quick-nav-close {
106
+ background: transparent;
107
+ border: none;
108
+ font-size: 26px;
109
+ line-height: 1;
110
+ cursor: pointer;
111
+ color: #555;
112
+ padding: 0 6px;
113
+ }
114
+ #quick-nav-overlay .quick-nav-close:hover { color: #e2001a; }
115
+ #quick-nav-overlay .quick-nav-list {
116
+ flex: 1;
117
+ overflow-y: auto;
118
+ padding: 12px 16px;
119
+ column-count: 2;
120
+ column-gap: 24px;
121
+ column-rule: 1px solid #eee;
122
+ }
123
+ @media (min-width: 1100px) {
124
+ #quick-nav-overlay .quick-nav-list { column-count: 3; }
125
+ }
126
+ @media (min-width: 1600px) {
127
+ #quick-nav-overlay .quick-nav-list { column-count: 4; }
128
+ }
129
+ #quick-nav-overlay .quick-nav-item {
130
+ display: block;
131
+ padding: 3px 6px;
132
+ margin: 1px 0;
133
+ border-radius: 3px;
134
+ color: #222;
135
+ text-decoration: none;
136
+ font-size: 12px;
137
+ line-height: 1.3;
138
+ break-inside: avoid;
139
+ cursor: pointer;
140
+ }
141
+ #quick-nav-overlay .quick-nav-item:hover,
142
+ #quick-nav-overlay .quick-nav-item.active {
143
+ background: #e2001a;
144
+ color: #fff;
145
+ }
146
+ #quick-nav-overlay .quick-nav-h1 {
147
+ font-weight: bold;
148
+ font-size: 13px;
149
+ margin-top: 5px;
150
+ }
151
+ #quick-nav-overlay .quick-nav-h2 {
152
+ padding-left: 16px;
153
+ color: #444;
154
+ }
155
+ #quick-nav-overlay .quick-nav-empty {
156
+ padding: 20px;
157
+ color: #888;
158
+ column-span: all;
159
+ text-align: center;
160
+ }
161
+ `
162
+
163
+ const style = document.createElement('style')
164
+ style.id = 'quick-nav-styles'
165
+ style.textContent = css
166
+ document.head.appendChild(style)
167
+ }
168
+
169
+ function buildOverlay(deck) {
170
+ const overlay = document.createElement('div')
171
+ overlay.id = 'quick-nav-overlay'
172
+ overlay.innerHTML = `
173
+ <div class="quick-nav-panel" role="dialog" aria-label="Quick slide navigation">
174
+ <div class="quick-nav-header">
175
+ <span class="quick-nav-title">Quick Nav</span>
176
+ <input type="text" class="quick-nav-search" placeholder="Full-text search…" autocomplete="off" spellcheck="false" />
177
+ <span class="quick-nav-count"></span>
178
+ <button class="quick-nav-close" title="Close (Esc / q)">×</button>
179
+ </div>
180
+ <div class="quick-nav-list"></div>
181
+ </div>
182
+ `
183
+ document.body.appendChild(overlay)
184
+
185
+ const panel = overlay.querySelector('.quick-nav-panel')
186
+ const input = overlay.querySelector('.quick-nav-search')
187
+ const list = overlay.querySelector('.quick-nav-list')
188
+ const count = overlay.querySelector('.quick-nav-count')
189
+ const closeBtn = overlay.querySelector('.quick-nav-close')
190
+
191
+ let allItems = []
192
+ let filtered = []
193
+ let activeIndex = -1
194
+
195
+ function render() {
196
+ list.innerHTML = ''
197
+ if (filtered.length === 0) {
198
+ const empty = document.createElement('div')
199
+ empty.className = 'quick-nav-empty'
200
+ empty.textContent = 'No matching headings'
201
+ list.appendChild(empty)
202
+ } else {
203
+ filtered.forEach((item, idx) => {
204
+ const a = document.createElement('a')
205
+ a.className = `quick-nav-item quick-nav-${item.level}`
206
+ if (idx === activeIndex) a.classList.add('active')
207
+ a.textContent = item.text
208
+ a.href = '#'
209
+ a.addEventListener('click', e => {
210
+ e.preventDefault()
211
+ jumpTo(item)
212
+ })
213
+ list.appendChild(a)
214
+ })
215
+ }
216
+ count.textContent = `${filtered.length} / ${allItems.length}`
217
+ }
218
+
219
+ function applyFilter() {
220
+ const q = input.value.toLowerCase().trim()
221
+ if (q === '') {
222
+ filtered = allItems
223
+ } else {
224
+ filtered = allItems.filter(i => i.searchText.includes(q))
225
+ }
226
+ activeIndex = filtered.length > 0 ? 0 : -1
227
+ render()
228
+ }
229
+
230
+ function jumpTo(item) {
231
+ hide()
232
+ deck.slide(item.h, item.v)
233
+ }
234
+
235
+ function show() {
236
+ allItems = collectHeadings(deck)
237
+ filtered = allItems
238
+ input.value = ''
239
+ activeIndex = filtered.length > 0 ? 0 : -1
240
+ render()
241
+ overlay.classList.add('visible')
242
+ setTimeout(() => input.focus(), 30)
243
+ }
244
+
245
+ function hide() {
246
+ overlay.classList.remove('visible')
247
+ }
248
+
249
+ function isVisible() {
250
+ return overlay.classList.contains('visible')
251
+ }
252
+
253
+ function toggle() {
254
+ if (isVisible()) hide()
255
+ else show()
256
+ }
257
+
258
+ input.addEventListener('input', applyFilter)
259
+ closeBtn.addEventListener('click', hide)
260
+ overlay.addEventListener('click', e => {
261
+ if (e.target === overlay) hide()
262
+ })
263
+
264
+ overlay.addEventListener('keydown', e => {
265
+ if (e.key === 'Escape') {
266
+ e.preventDefault()
267
+ e.stopPropagation()
268
+ hide()
269
+ return
270
+ }
271
+ if (e.key === 'Enter' && activeIndex >= 0 && filtered[activeIndex]) {
272
+ e.preventDefault()
273
+ jumpTo(filtered[activeIndex])
274
+ return
275
+ }
276
+ if (e.key === 'ArrowDown') {
277
+ e.preventDefault()
278
+ if (filtered.length === 0) return
279
+ activeIndex = (activeIndex + 1) % filtered.length
280
+ render()
281
+ scrollActiveIntoView()
282
+ return
283
+ }
284
+ if (e.key === 'ArrowUp') {
285
+ e.preventDefault()
286
+ if (filtered.length === 0) return
287
+ activeIndex = (activeIndex - 1 + filtered.length) % filtered.length
288
+ render()
289
+ scrollActiveIntoView()
290
+ return
291
+ }
292
+ })
293
+
294
+ function scrollActiveIntoView() {
295
+ const el = list.querySelector('.quick-nav-item.active')
296
+ if (el) el.scrollIntoView({ block: 'nearest' })
297
+ }
298
+
299
+ return { show, hide, toggle, isVisible }
300
+ }
301
+
302
+ export default () => {
303
+
304
+ return {
305
+ id: 'quick_nav',
306
+ init: (deck) => {
307
+ injectStyles()
308
+
309
+ deck.on('ready', () => {
310
+ const overlay = buildOverlay(deck)
311
+
312
+ deck.addKeyBinding(
313
+ { keyCode: 81, key: 'Q', description: 'Quick navigation overlay' },
314
+ () => overlay.toggle()
315
+ )
316
+ })
317
+ }
318
+ }
319
+ }