@farberg/reveal-template 1.1.17 → 1.1.19
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/package.json
CHANGED
|
@@ -1,154 +1,383 @@
|
|
|
1
1
|
/*
|
|
2
2
|
Create a zip from existing files for download and display a tree of files
|
|
3
3
|
|
|
4
|
-
<pre class="dirtree"
|
|
4
|
+
Attributes on <pre class="dirtree">:
|
|
5
|
+
data-zipname="archive.zip" — name of the downloaded zip (default: download.zip)
|
|
6
|
+
data-line-height="1.6" — line spacing (default: 1.85)
|
|
7
|
+
data-width="auto" — container width, e.g. "400px", "60%", "auto" (default: auto)
|
|
8
|
+
|
|
9
|
+
<pre class="dirtree" data-zipname="k8s-Exercise.zip" data-line-height="1.6" data-width="480px">
|
|
5
10
|
code/k8s-demo-app/01-mariadb-deployment.yaml
|
|
6
11
|
code/k8s-demo-app/02-k8s-mariadb-service.yaml
|
|
7
|
-
code/k8s-demo-app/03-memcached-deployment-and-service.yaml
|
|
8
|
-
code/k8s-demo-app/04-app-deployment-minikube.yaml
|
|
9
|
-
code/k8s-demo-app/04-app-service-and-ingress.yaml
|
|
10
12
|
code/k8s-demo-app/Dockerfile
|
|
11
13
|
code/k8s-demo-app/package.json
|
|
12
|
-
code/k8s-demo-app/server.js
|
|
13
14
|
</pre>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
17
|
const dir_tree = {
|
|
20
18
|
id: 'dir_tree',
|
|
21
19
|
init: (deck) => {
|
|
22
20
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
function injectStyles() {
|
|
22
|
+
if (document.getElementById('dirtree-styles')) return;
|
|
23
|
+
const style = document.createElement('style');
|
|
24
|
+
style.id = 'dirtree-styles';
|
|
25
|
+
style.textContent = `
|
|
26
|
+
.dirtree-container {
|
|
27
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
|
|
28
|
+
background: transparent;
|
|
29
|
+
border-radius: 10px;
|
|
30
|
+
padding: 38px 18px 14px 18px;
|
|
31
|
+
border: 1px solid #d0d7de;
|
|
32
|
+
display: inline-block;
|
|
33
|
+
min-width: 280px;
|
|
34
|
+
max-width: 90%;
|
|
35
|
+
text-align: left;
|
|
36
|
+
position: relative !important;
|
|
37
|
+
}
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
ul.dirtree {
|
|
40
|
+
list-style: none !important;
|
|
41
|
+
padding: 0 !important;
|
|
42
|
+
margin: 0 !important;
|
|
43
|
+
}
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
ul.dirtree ul.dirtree {
|
|
46
|
+
padding-left: 0 !important;
|
|
47
|
+
}
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
li.dirtree {
|
|
50
|
+
color: #1f2328;
|
|
51
|
+
position: relative;
|
|
52
|
+
white-space: nowrap;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
37
55
|
}
|
|
38
56
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
li.dirtree::before,
|
|
58
|
+
li.dirtree::after {
|
|
59
|
+
display: none !important;
|
|
60
|
+
content: none !important;
|
|
61
|
+
border: none !important;
|
|
42
62
|
}
|
|
43
63
|
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
li.dirtree.root {
|
|
65
|
+
color: #0969da;
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
margin-bottom: 2px;
|
|
68
|
+
}
|
|
46
69
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
70
|
+
.dirtree-dir-name {
|
|
71
|
+
color: #0969da;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
a.dirtree-file-link {
|
|
75
|
+
color: #116329;
|
|
76
|
+
text-decoration: none;
|
|
77
|
+
transition: color 0.15s;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
a.dirtree-file-link:hover {
|
|
81
|
+
color: #6639ba;
|
|
82
|
+
text-decoration: underline;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Copy button — hidden by default, shown on li hover */
|
|
86
|
+
.dirtree-copy-btn {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
margin-left: 8px;
|
|
90
|
+
padding: 0px 6px;
|
|
91
|
+
background: transparent;
|
|
92
|
+
border: 1px solid #d0d7de;
|
|
93
|
+
border-radius: 4px;
|
|
94
|
+
color: #57606a;
|
|
95
|
+
font-size: 0.78em;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
opacity: 0 !important;
|
|
98
|
+
pointer-events: none !important;
|
|
99
|
+
transition: opacity 0.15s, background 0.15s, color 0.15s;
|
|
100
|
+
user-select: none;
|
|
101
|
+
line-height: 1.6;
|
|
102
|
+
font-family: inherit;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
li.dirtree:hover > .dirtree-copy-btn {
|
|
106
|
+
opacity: 1 !important;
|
|
107
|
+
pointer-events: auto !important;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.dirtree-copy-btn:hover {
|
|
111
|
+
background: #f6f8fa;
|
|
112
|
+
border-color: #8c959f;
|
|
113
|
+
color: #1f2328;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.dirtree-copy-btn.copied {
|
|
117
|
+
color: #116329;
|
|
118
|
+
border-color: #116329;
|
|
119
|
+
background: #dafbe1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Download button */
|
|
123
|
+
.dirtree-download-btn {
|
|
124
|
+
position: absolute !important;
|
|
125
|
+
top: 10px !important;
|
|
126
|
+
right: 14px !important;
|
|
127
|
+
padding: 2px 10px;
|
|
128
|
+
background: transparent;
|
|
129
|
+
border: 1px solid #d0d7de;
|
|
130
|
+
border-radius: 6px;
|
|
131
|
+
color: #cf222e !important;
|
|
132
|
+
font-size: 0.85em;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
text-decoration: none !important;
|
|
135
|
+
transition: background 0.15s, border-color 0.15s;
|
|
136
|
+
font-family: inherit;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.dirtree-download-btn:hover {
|
|
140
|
+
background: #fff5f5 !important;
|
|
141
|
+
border-color: #cf222e !important;
|
|
142
|
+
color: #cf222e !important;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.dirtree-prefix {
|
|
146
|
+
color: #8c959f;
|
|
147
|
+
user-select: none;
|
|
148
|
+
white-space: pre;
|
|
149
|
+
flex-shrink: 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.dirtree-icon {
|
|
153
|
+
margin-right: 4px;
|
|
154
|
+
flex-shrink: 0;
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
document.head.appendChild(style);
|
|
158
|
+
}
|
|
51
159
|
|
|
52
|
-
|
|
53
|
-
|
|
160
|
+
function getFileIcon(name, ext) {
|
|
161
|
+
if (name.toLowerCase() === 'dockerfile') return '🐳';
|
|
162
|
+
const icons = {
|
|
163
|
+
js: '📜', ts: '📜', jsx: '📜', tsx: '📜',
|
|
164
|
+
json: '📋', yaml: '📋', yml: '📋', toml: '📋', xml: '📋',
|
|
165
|
+
md: '📝', txt: '📝', rst: '📝',
|
|
166
|
+
html: '🌐', css: '🎨', scss: '🎨',
|
|
167
|
+
sh: '⚙️', bash: '⚙️', zsh: '⚙️',
|
|
168
|
+
py: '🐍', rb: '💎', go: '🔷', rs: '🦀', java: '☕',
|
|
169
|
+
png: '🖼️', jpg: '🖼️', jpeg: '🖼️', svg: '🖼️', gif: '🖼️',
|
|
170
|
+
zip: '📦', tar: '📦', gz: '📦',
|
|
171
|
+
sql: '🗄️',
|
|
172
|
+
};
|
|
173
|
+
return icons[ext] || '📄';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function writeToClipboard(text) {
|
|
177
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
178
|
+
return navigator.clipboard.writeText(text);
|
|
54
179
|
}
|
|
55
|
-
|
|
180
|
+
// Fallback for HTTP / non-secure contexts
|
|
181
|
+
const ta = document.createElement('textarea');
|
|
182
|
+
ta.value = text;
|
|
183
|
+
ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
|
|
184
|
+
document.body.appendChild(ta);
|
|
185
|
+
ta.focus();
|
|
186
|
+
ta.select();
|
|
187
|
+
try {
|
|
188
|
+
document.execCommand('copy');
|
|
189
|
+
return Promise.resolve();
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return Promise.reject(e);
|
|
192
|
+
} finally {
|
|
193
|
+
document.body.removeChild(ta);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
56
196
|
|
|
197
|
+
function makeCopyButton(filePath) {
|
|
198
|
+
const btn = document.createElement('button');
|
|
199
|
+
btn.classList.add('dirtree-copy-btn');
|
|
200
|
+
btn.title = 'Copy file contents';
|
|
201
|
+
btn.textContent = 'copy';
|
|
202
|
+
btn.addEventListener('click', e => {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
btn.textContent = '…';
|
|
205
|
+
fetch(filePath, { credentials: 'include' })
|
|
206
|
+
.then(r => {
|
|
207
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
208
|
+
return r.text();
|
|
209
|
+
})
|
|
210
|
+
.then(text => writeToClipboard(text))
|
|
211
|
+
.then(() => {
|
|
212
|
+
btn.textContent = '✓ copied';
|
|
213
|
+
btn.classList.add('copied');
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
btn.textContent = 'copy';
|
|
216
|
+
btn.classList.remove('copied');
|
|
217
|
+
}, 1500);
|
|
218
|
+
})
|
|
219
|
+
.catch(() => {
|
|
220
|
+
btn.textContent = '✗ error';
|
|
221
|
+
setTimeout(() => { btn.textContent = 'copy'; }, 1500);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
return btn;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getFileList(el) {
|
|
228
|
+
return el.innerText.split('\n').filter(e => e.trim().length > 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function createTree(files) {
|
|
232
|
+
function getPath(tree, path) {
|
|
233
|
+
let node = tree[path[0]];
|
|
234
|
+
if (!node) { node = {}; tree[path[0]] = node; }
|
|
235
|
+
const rest = path.slice(1);
|
|
236
|
+
return rest.length > 0 ? getPath(node, rest) : node;
|
|
237
|
+
}
|
|
238
|
+
const tree = {};
|
|
239
|
+
for (const file of files) {
|
|
240
|
+
const parts = file.split('/');
|
|
241
|
+
const filename = parts.pop();
|
|
242
|
+
getPath(tree, parts)[filename] = filename;
|
|
243
|
+
}
|
|
244
|
+
return tree;
|
|
57
245
|
}
|
|
58
246
|
|
|
59
247
|
function download(files, zipName) {
|
|
60
|
-
Promise.all(files.map(
|
|
61
|
-
.then(
|
|
62
|
-
|
|
63
|
-
const hasUnauthorized = promises.some(p => p.status === 401);
|
|
64
|
-
if (hasUnauthorized) {
|
|
65
|
-
console.log("Authentication required (dir-tree), reloading page");
|
|
248
|
+
Promise.all(files.map(f => fetch(f, { credentials: 'include' })))
|
|
249
|
+
.then(responses => {
|
|
250
|
+
if (responses.some(r => r.status === 401)) {
|
|
66
251
|
window.location.reload();
|
|
67
252
|
return;
|
|
68
253
|
}
|
|
69
|
-
return Promise.all(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
254
|
+
return Promise.all(responses.map(r => r.text())).then(values => {
|
|
255
|
+
const zip = new JSZip();
|
|
256
|
+
values.forEach((text, i) => zip.file(files[i], text));
|
|
257
|
+
zip.generateAsync({ type: 'blob' }).then(blob => saveAs(blob, zipName));
|
|
258
|
+
});
|
|
259
|
+
});
|
|
79
260
|
}
|
|
80
261
|
|
|
81
|
-
function displayTree(el, treeNode, path,
|
|
82
|
-
|
|
83
|
-
for (let name of Object.getOwnPropertyNames(treeNode)) {
|
|
84
|
-
const o = treeNode[name]
|
|
85
|
-
const isDir = typeof o === "object"
|
|
262
|
+
function displayTree(el, treeNode, path, isRoot, files, zipName, ancestorLast) {
|
|
263
|
+
const names = Object.getOwnPropertyNames(treeNode);
|
|
86
264
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
265
|
+
names.forEach((name, index) => {
|
|
266
|
+
const o = treeNode[name];
|
|
267
|
+
const isDir = typeof o === 'object';
|
|
268
|
+
const isLast = index === names.length - 1;
|
|
91
269
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
li.innerHTML = name + " "
|
|
270
|
+
const li = document.createElement('li');
|
|
271
|
+
li.classList.add('dirtree');
|
|
95
272
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
a.innerHTML = `(download ${zipName})`
|
|
99
|
-
a.href = "#zip/" + zipName
|
|
100
|
-
a.style = "color: #e10217";
|
|
101
|
-
a.addEventListener('click', e => {
|
|
102
|
-
e.preventDefault()
|
|
103
|
-
download(files, zipName)
|
|
104
|
-
})
|
|
273
|
+
if (isRoot) {
|
|
274
|
+
li.classList.add('root');
|
|
105
275
|
|
|
276
|
+
const icon = document.createElement('span');
|
|
277
|
+
icon.classList.add('dirtree-icon');
|
|
278
|
+
icon.textContent = '📁';
|
|
106
279
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
280
|
+
const nameSpan = document.createElement('span');
|
|
281
|
+
nameSpan.classList.add('dirtree-dir-name');
|
|
282
|
+
nameSpan.textContent = name;
|
|
110
283
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
284
|
+
li.appendChild(icon);
|
|
285
|
+
li.appendChild(nameSpan);
|
|
286
|
+
el.appendChild(li);
|
|
114
287
|
|
|
115
|
-
|
|
288
|
+
const ul = document.createElement('ul');
|
|
289
|
+
ul.classList.add('dirtree');
|
|
290
|
+
li.appendChild(ul);
|
|
291
|
+
displayTree(ul, o, `${path}/${name}`, false, files, zipName, []);
|
|
116
292
|
|
|
117
293
|
} else {
|
|
294
|
+
const contPrefix = ancestorLast.map(last => last ? ' ' : '│ ').join('');
|
|
295
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
296
|
+
|
|
297
|
+
const prefixSpan = document.createElement('span');
|
|
298
|
+
prefixSpan.classList.add('dirtree-prefix');
|
|
299
|
+
prefixSpan.textContent = contPrefix + connector;
|
|
300
|
+
li.appendChild(prefixSpan);
|
|
301
|
+
|
|
302
|
+
if (isDir) {
|
|
303
|
+
const icon = document.createElement('span');
|
|
304
|
+
icon.classList.add('dirtree-icon');
|
|
305
|
+
icon.textContent = '📂';
|
|
118
306
|
|
|
119
|
-
|
|
120
|
-
|
|
307
|
+
const nameSpan = document.createElement('span');
|
|
308
|
+
nameSpan.classList.add('dirtree-dir-name');
|
|
309
|
+
nameSpan.textContent = name;
|
|
121
310
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
311
|
+
li.appendChild(icon);
|
|
312
|
+
li.appendChild(nameSpan);
|
|
313
|
+
el.appendChild(li);
|
|
125
314
|
|
|
126
|
-
|
|
315
|
+
const ul = document.createElement('ul');
|
|
316
|
+
ul.classList.add('dirtree');
|
|
317
|
+
li.appendChild(ul);
|
|
318
|
+
displayTree(ul, o, `${path}/${name}`, false, files, zipName, [...ancestorLast, isLast]);
|
|
319
|
+
|
|
320
|
+
} else {
|
|
321
|
+
const ext = name.includes('.') ? name.split('.').pop().toLowerCase() : '';
|
|
322
|
+
|
|
323
|
+
const icon = document.createElement('span');
|
|
324
|
+
icon.classList.add('dirtree-icon');
|
|
325
|
+
icon.textContent = getFileIcon(name, ext);
|
|
326
|
+
|
|
327
|
+
const finalPath = `${path}/${o}`.replace(/^\//, '');
|
|
328
|
+
const a = document.createElement('a');
|
|
329
|
+
a.classList.add('dirtree-file-link');
|
|
330
|
+
a.href = finalPath;
|
|
331
|
+
a.textContent = o;
|
|
332
|
+
|
|
333
|
+
li.appendChild(icon);
|
|
334
|
+
li.appendChild(a);
|
|
335
|
+
li.appendChild(makeCopyButton(finalPath));
|
|
336
|
+
el.appendChild(li);
|
|
337
|
+
}
|
|
127
338
|
}
|
|
128
|
-
}
|
|
339
|
+
});
|
|
129
340
|
}
|
|
130
341
|
|
|
131
342
|
deck.on('ready', () => {
|
|
132
|
-
|
|
133
|
-
const zipName = el.getAttribute("data-zipname") || "download.zip"
|
|
134
|
-
const stripPrefix = el.getAttribute("data-strip-prefix")
|
|
343
|
+
injectStyles();
|
|
135
344
|
|
|
136
|
-
|
|
137
|
-
const
|
|
345
|
+
for (const el of deck.getRevealElement().querySelectorAll('pre.dirtree')) {
|
|
346
|
+
const zipName = el.getAttribute('data-zipname') || 'download.zip';
|
|
347
|
+
const lineHeight = el.getAttribute('data-line-height') || '1.85';
|
|
348
|
+
const width = el.getAttribute('data-width') || 'auto';
|
|
138
349
|
|
|
139
|
-
const
|
|
140
|
-
|
|
350
|
+
const files = getFileList(el);
|
|
351
|
+
const tree = createTree(files);
|
|
141
352
|
|
|
142
|
-
|
|
353
|
+
const container = document.createElement('div');
|
|
354
|
+
container.classList.add('dirtree-container');
|
|
355
|
+
container.style.lineHeight = lineHeight;
|
|
356
|
+
if (width !== 'auto') container.style.width = width;
|
|
143
357
|
|
|
144
|
-
|
|
145
|
-
|
|
358
|
+
// Forward font-size (and other inline styles) from the <pre>
|
|
359
|
+
if (el.getAttribute('style')) container.setAttribute('style', el.getAttribute('style'));
|
|
360
|
+
// Re-apply data-driven styles on top (inline style from <pre> must not override these)
|
|
361
|
+
container.style.lineHeight = lineHeight;
|
|
362
|
+
if (width !== 'auto') container.style.width = width;
|
|
363
|
+
|
|
364
|
+
const btn = document.createElement('a');
|
|
365
|
+
btn.classList.add('dirtree-download-btn');
|
|
366
|
+
btn.textContent = '⬇ ' + zipName;
|
|
367
|
+
btn.href = '#zip/' + zipName;
|
|
368
|
+
btn.addEventListener('click', e => { e.preventDefault(); download(files, zipName); });
|
|
369
|
+
container.appendChild(btn);
|
|
146
370
|
|
|
147
|
-
|
|
371
|
+
const ul = document.createElement('ul');
|
|
372
|
+
ul.classList.add('dirtree');
|
|
373
|
+
container.appendChild(ul);
|
|
148
374
|
|
|
375
|
+
displayTree(ul, tree, '', true, files, zipName, []);
|
|
149
376
|
|
|
377
|
+
el.parentElement.replaceChild(container, el);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
150
380
|
}
|
|
151
381
|
}
|
|
152
382
|
|
|
153
|
-
|
|
154
|
-
export default dir_tree;
|
|
383
|
+
export default dir_tree;
|
|
@@ -67,10 +67,91 @@ function extractBeginEndSnippet(code, beginMarker, endMarker) {
|
|
|
67
67
|
return out.trim();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function injectStyles() {
|
|
71
|
+
const style = document.createElement('style');
|
|
72
|
+
style.textContent = `
|
|
73
|
+
pre.with-copy-btn {
|
|
74
|
+
position: relative;
|
|
75
|
+
}
|
|
76
|
+
.copy-code-btn {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: 0.4em;
|
|
79
|
+
right: 0.4em;
|
|
80
|
+
padding: 0.2em 0.5em;
|
|
81
|
+
font-size: 0.75em;
|
|
82
|
+
font-family: sans-serif;
|
|
83
|
+
background: rgba(255, 255, 255, 0.15);
|
|
84
|
+
color: #ccc;
|
|
85
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
86
|
+
border-radius: 4px;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
opacity: 0;
|
|
89
|
+
transition: opacity 0.15s ease, background 0.15s ease;
|
|
90
|
+
z-index: 10;
|
|
91
|
+
line-height: 1.4;
|
|
92
|
+
}
|
|
93
|
+
pre.with-copy-btn:hover .copy-code-btn {
|
|
94
|
+
opacity: 1;
|
|
95
|
+
}
|
|
96
|
+
.copy-code-btn:hover {
|
|
97
|
+
background: rgba(255, 255, 255, 0.3);
|
|
98
|
+
color: #fff;
|
|
99
|
+
}
|
|
100
|
+
.copy-code-btn.copied {
|
|
101
|
+
color: #4caf50;
|
|
102
|
+
border-color: #4caf50;
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
document.head.appendChild(style);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeToClipboard(text) {
|
|
109
|
+
if (navigator.clipboard && navigator.clipboard.writeText)
|
|
110
|
+
return navigator.clipboard.writeText(text);
|
|
111
|
+
const ta = document.createElement('textarea');
|
|
112
|
+
ta.value = text;
|
|
113
|
+
ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
|
|
114
|
+
document.body.appendChild(ta);
|
|
115
|
+
ta.focus();
|
|
116
|
+
ta.select();
|
|
117
|
+
try {
|
|
118
|
+
document.execCommand('copy');
|
|
119
|
+
return Promise.resolve();
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return Promise.reject(e);
|
|
122
|
+
} finally {
|
|
123
|
+
document.body.removeChild(ta);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function addCopyButton(preEl) {
|
|
128
|
+
preEl.classList.add('with-copy-btn');
|
|
129
|
+
const btn = document.createElement('button');
|
|
130
|
+
btn.className = 'copy-code-btn';
|
|
131
|
+
btn.textContent = 'copy';
|
|
132
|
+
btn.addEventListener('click', () => {
|
|
133
|
+
const code = preEl.querySelector('code');
|
|
134
|
+
const text = code ? code.innerText : preEl.innerText;
|
|
135
|
+
writeToClipboard(text).then(() => {
|
|
136
|
+
btn.textContent = '✓ copied';
|
|
137
|
+
btn.classList.add('copied');
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
btn.textContent = 'copy';
|
|
140
|
+
btn.classList.remove('copied');
|
|
141
|
+
}, 1500);
|
|
142
|
+
}).catch(() => {
|
|
143
|
+
btn.textContent = '✗ error';
|
|
144
|
+
setTimeout(() => { btn.textContent = 'copy'; }, 1500);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
preEl.appendChild(btn);
|
|
148
|
+
}
|
|
149
|
+
|
|
70
150
|
export default () => {
|
|
71
151
|
return {
|
|
72
152
|
id: 'show_code_snippets',
|
|
73
153
|
init: (deck) => {
|
|
154
|
+
injectStyles();
|
|
74
155
|
|
|
75
156
|
deck.on('ready', async () => {
|
|
76
157
|
const highlightPlugin = deck.getPlugin("highlight")
|
|
@@ -89,6 +170,7 @@ export default () => {
|
|
|
89
170
|
for (let el of deck.getRevealElement().querySelectorAll("pre code[class]")) {
|
|
90
171
|
if (!el.classList.contains("mermaid")) {
|
|
91
172
|
highlightPlugin.hljs.highlightElement(el);
|
|
173
|
+
addCopyButton(el.closest('pre'));
|
|
92
174
|
}
|
|
93
175
|
}
|
|
94
176
|
|
|
@@ -132,8 +214,10 @@ export default () => {
|
|
|
132
214
|
code = outdent(code)
|
|
133
215
|
|
|
134
216
|
const newEl = showCode(el, language, code, showLink ? url : null, outdent)
|
|
135
|
-
if (language !== 'mermaid')
|
|
217
|
+
if (language !== 'mermaid') {
|
|
136
218
|
highlightPlugin.hljs.highlightElement(newEl.querySelector('code') ?? newEl)
|
|
219
|
+
addCopyButton(newEl)
|
|
220
|
+
}
|
|
137
221
|
} catch (err) {
|
|
138
222
|
console.error(`show-code-snippets: failed to load ${url}:`, err)
|
|
139
223
|
showError(el, `Failed to load ${url}: ${err.message}`)
|