@jsenv/plugin-explorer 1.0.0

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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @jsenv/plugin-explorer [![npm package](https://img.shields.io/npm/v/@jsenv/plugin-explorer.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/plugin-explorer)
2
+
3
+ [Documentation](<https://github.com/jsenv/core/wiki/A)-Getting-started>) · [Changelog](./CHANGELOG.md)
@@ -0,0 +1,509 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Exploring</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta charset="utf-8">
7
+ <link rel="icon" href="data:;base64,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">
8
+ <style>
9
+ * {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ [data-force-hide] {
14
+ display: none !important;
15
+ }
16
+
17
+ html, body {
18
+ flex-direction: column;
19
+ flex: 1;
20
+ margin: 0;
21
+ padding: 0;
22
+ display: flex;
23
+ }
24
+
25
+ html {
26
+ height: 100%;
27
+ font-family: arial, sans;
28
+ }
29
+
30
+ button:focus, a:focus, input:focus, [data-contains-hidden-input] input:focus + * {
31
+ outline-offset: -2px;
32
+ outline-style: none;
33
+ }
34
+
35
+ [data-contains-hidden-input] input:focus + * {
36
+ outline-offset: -2px;
37
+ outline-width: 4px;
38
+ }
39
+
40
+ html[data-last-interaction="keyboard"] button:focus, html[data-last-interaction="keyboard"] a:focus, html[data-last-interaction="keyboard"] input:focus, html[data-last-interaction="keyboard"] [data-contains-hidden-input] input:focus + * {
41
+ outline-style: auto;
42
+ }
43
+
44
+ html[data-last-interaction="keyboard"] [data-contains-hidden-input] input:focus + * {
45
+ outline-color: #0096ff;
46
+ }
47
+
48
+ @-moz-document url-prefix() {
49
+ html[data-last-interaction="keyboard"] button:focus, html[data-last-interaction="keyboard"] a:focus, html[data-last-interaction="keyboard"] input:focus, html[data-last-interaction="keyboard"] [data-input-customized] input:focus + * {
50
+ outline-offset: 0;
51
+ outline: 2px solid #0096ff;
52
+ }
53
+ }
54
+
55
+ ::-moz-focus-inner {
56
+ border: 0;
57
+ }
58
+
59
+ main {
60
+ flex-direction: column;
61
+ flex: 1;
62
+ display: flex;
63
+ }
64
+
65
+ main > * {
66
+ min-width: 100%;
67
+ flex: 1;
68
+ }
69
+
70
+ hr {
71
+ color: #000;
72
+ background-color: #000;
73
+ border-color: #000;
74
+ margin: 15px 0;
75
+ }
76
+
77
+ article[data-page="file-list"] {
78
+ color: #97a0a0;
79
+ background-color: #204143;
80
+ }
81
+
82
+ nav, #explorables-message {
83
+ background: #1f262c;
84
+ }
85
+
86
+ #explorables nav {
87
+ overflow: hidden;
88
+ }
89
+
90
+ #explorables-message {
91
+ overflow: auto;
92
+ }
93
+
94
+ article h2 {
95
+ margin: 0;
96
+ display: flex;
97
+ }
98
+
99
+ article h2 svg {
100
+ margin-right: 15px;
101
+ }
102
+
103
+ #main_html_file {
104
+ padding: 25px 25px 0;
105
+ }
106
+
107
+ #main_file_link {
108
+ color: #e7f2f3;
109
+ }
110
+
111
+ h2 {
112
+ color: #24b1b0;
113
+ margin: 20px 0;
114
+ display: inline-block;
115
+ }
116
+
117
+ h4 {
118
+ color: #97a0a0;
119
+ padding: 0 25px;
120
+ font-size: 15px;
121
+ font-weight: normal;
122
+ }
123
+
124
+ #explorables {
125
+ word-break: break-word;
126
+ }
127
+
128
+ #explorables-header {
129
+ width: 100%;
130
+ position: -webkit-sticky;
131
+ position: sticky;
132
+ top: 0;
133
+ }
134
+
135
+ #explorables_header_and_menu {
136
+ background-color: #204143;
137
+ padding-top: 25px;
138
+ padding-left: 25px;
139
+ padding-right: 25px;
140
+ position: relative;
141
+ }
142
+
143
+ #explorables_header_bottom_spacing {
144
+ height: 20px;
145
+ background: #1f262c;
146
+ }
147
+
148
+ #directory_relative_url {
149
+ color: orange;
150
+ }
151
+
152
+ #explorables ul {
153
+ margin: 0;
154
+ padding: 0 25px 25px;
155
+ }
156
+
157
+ #explorables li {
158
+ margin: 10px 0;
159
+ list-style-type: none;
160
+ }
161
+
162
+ #explorables li:first-child {
163
+ margin-top: 0;
164
+ }
165
+
166
+ #explorables li .execution-link {
167
+ width: 100%;
168
+ word-break: break-all;
169
+ }
170
+
171
+ .execution-link {
172
+ color: #e7f2f3;
173
+ -webkit-text-decoration: unset;
174
+ -webkit-text-decoration: unset;
175
+ text-decoration: unset;
176
+ background: #204143;
177
+ padding: 6px 12px;
178
+ font-family: monospace;
179
+ font-size: 14px;
180
+ display: inline-block;
181
+ }
182
+
183
+ #main_file_icon {
184
+ fill: #24b1b0;
185
+ }
186
+
187
+ #explorables_icon {
188
+ width: 25px;
189
+ height: 25px;
190
+ stroke: none;
191
+ fill: #24b1b0;
192
+ }
193
+
194
+ #explorables fieldset {
195
+ border: none;
196
+ margin: 0;
197
+ padding: 0;
198
+ }
199
+
200
+ #explorables fieldset label {
201
+ padding: 0;
202
+ display: inline-block;
203
+ }
204
+
205
+ #explorables fieldset input {
206
+ opacity: 0;
207
+ position: absolute;
208
+ }
209
+
210
+ #explorables fieldset label input + * {
211
+ min-width: 8em;
212
+ text-align: center;
213
+ background-color: #204143;
214
+ border-radius: .1em;
215
+ padding: .7em 1em;
216
+ display: block;
217
+ }
218
+
219
+ #explorables fieldset input:checked + * {
220
+ color: orange;
221
+ background: #1f262c;
222
+ }
223
+
224
+ .menu-wrapper {
225
+ max-width: 100%;
226
+ margin-top: 25px;
227
+ transition: all .3s;
228
+ position: relative;
229
+ overflow: hidden;
230
+ }
231
+
232
+ .menu {
233
+ box-sizing: border-box;
234
+ white-space: nowrap;
235
+ -webkit-overflow-scrolling: touch;
236
+ overflow-x: auto;
237
+ overflow-y: hidden;
238
+ }
239
+
240
+ .menu .item {
241
+ height: 100%;
242
+ box-sizing: border-box;
243
+ padding: 1em;
244
+ display: inline-block;
245
+ }
246
+
247
+ .paddle {
248
+ height: 36px;
249
+ color: #1f262c;
250
+ background: #24b1b0;
251
+ border: none;
252
+ position: absolute;
253
+ bottom: 0;
254
+ }
255
+
256
+ .left-paddle {
257
+ left: 0;
258
+ }
259
+
260
+ .right-paddle {
261
+ right: 0;
262
+ }
263
+
264
+ .hidden {
265
+ display: none;
266
+ }
267
+ </style>
268
+ </head>
269
+
270
+ <body>
271
+ <main></main>
272
+ <div style="display: none">
273
+ <article data-page="file-list">
274
+ <!--
275
+ <section id="main_html_file">
276
+ <h2>
277
+ <svg id="main_file_icon" viewBox="0 0 16 16" width="25px" height="25px">
278
+ <path
279
+ d="M15.45,7L14,5.551V2c0-0.55-0.45-1-1-1h-1c-0.55,0-1,0.45-1,1v0.553L9,0.555C8.727,0.297,8.477,0,8,0S7.273,0.297,7,0.555 L0.55,7C0.238,7.325,0,7.562,0,8c0,0.563,0.432,1,1,1h1v6c0,0.55,0.45,1,1,1h3v-5c0-0.55,0.45-1,1-1h2c0.55,0,1,0.45,1,1v5h3 c0.55,0,1-0.45,1-1V9h1c0.568,0,1-0.437,1-1C16,7.562,15.762,7.325,15.45,7z"
280
+ />
281
+ </svg>
282
+ <span
283
+ >Main html file:
284
+ <a id="main_file_link" href="javascript:void(0)">${mainFileRelativeUrl}</a></span
285
+ >
286
+ </h2>
287
+ <div>
288
+ <iframe id="main_file_iframe" src="about:blank"></iframe>
289
+ </div>
290
+ </section>
291
+ <hr />
292
+ !-->
293
+ <section id="explorables">
294
+ <div id="explorables-header">
295
+ <div id="explorables_header_and_menu">
296
+ <h2>
297
+ <svg id="explorables_icon" viewBox="0 0 24 24" width="32" height="32">
298
+ <path d="M0 0h24v24H0V0z" fill="none"></path>
299
+ <path d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"></path>
300
+ </svg>
301
+ <span>File(s) to explore in
302
+ <span id="directory_relative_url" title="${directoryUrl}">${directoryName}</span></span>
303
+ </h2>
304
+ <div class="menu-wrapper">
305
+ <fieldset id="filter-group-set" class="menu"></fieldset>
306
+ </div>
307
+ <div class="paddles">
308
+ <button class="left-paddle paddle hidden">&lt;</button>
309
+ <button class="right-paddle paddle">&gt;</button>
310
+ </div>
311
+ </div>
312
+ <div id="explorables_header_bottom_spacing"></div>
313
+ </div>
314
+ <div>
315
+ <div id="explorables-message">
316
+ <h4 style="margin-top: 0">${message}</h4>
317
+ </div>
318
+ <nav>
319
+ <ul></ul>
320
+ </nav>
321
+ </div>
322
+ </section>
323
+ </article>
324
+ </div>
325
+ <script no-supervisor="">// eslint-disable-next-line no-undef
326
+ const {
327
+ rootDirectoryUrl,
328
+ groups,
329
+ files
330
+ } = SERVER_PARAMS;
331
+ const groupPreference = {
332
+ has: () => localStorage.hasOwnProperty("explorer_active_group"),
333
+ get: () => localStorage.hasOwnProperty("explorer_active_group") ? JSON.parse(localStorage.getItem("explorer_active_group")) : undefined,
334
+ set: value => localStorage.setItem("explorer_active_group", JSON.stringify(value))
335
+ };
336
+ const directoryNameFromUrl = directoryUrl => {
337
+ const slashLastIndex = directoryUrl.lastIndexOf("/",
338
+ // ignore last slash
339
+ directoryUrl.length - 2);
340
+ if (slashLastIndex === -1) return "";
341
+ return directoryUrl.slice(slashLastIndex + 1);
342
+ };
343
+ const urlToVisitFromRelativeUrl = relativeUrl => {
344
+ return "".concat(window.origin, "/").concat(relativeUrl);
345
+ };
346
+ const fileListElement = document.querySelector("[data-page=\"file-list\"]").cloneNode(true);
347
+ const directoryName = directoryNameFromUrl(rootDirectoryUrl);
348
+ const span = fileListElement.querySelector("#directory_relative_url");
349
+ span.title = rootDirectoryUrl;
350
+ span.textContent = directoryName;
351
+ const h4 = fileListElement.querySelector("h4");
352
+ const ul = fileListElement.querySelector("ul");
353
+ ul.innerHTML = files.map(file => "<li>\n <a\n class=\"execution-link\"\n data-relative-url=".concat(file.relativeUrl, "\n href=").concat(urlToVisitFromRelativeUrl(file.relativeUrl), "\n >\n ").concat(file.relativeUrl, "\n </a>\n </li>")).join("");
354
+ const groupFieldset = fileListElement.querySelector("#filter-group-set");
355
+ const groupNames = Object.keys(groups);
356
+ groupFieldset.innerHTML = groupNames.map(key => "<label data-contains-hidden-input class=\"item\">\n <input type=\"radio\" name=\"filter-group\" value=\"".concat(key, "\"/>\n <span>").concat(key, "</span>\n </label>")).join("");
357
+ const groupFromLocalStorage = groupPreference.get();
358
+ const currentGroup = groupFromLocalStorage && groupNames.includes(groupFromLocalStorage) ? groupFromLocalStorage : groupNames[0];
359
+ Array.from(groupFieldset.querySelectorAll("input")).forEach(inputRadio => {
360
+ inputRadio.checked = inputRadio.value === currentGroup;
361
+ inputRadio.onchange = () => {
362
+ if (inputRadio.checked) {
363
+ groupPreference.set(inputRadio.value);
364
+ enableGroup(inputRadio.value);
365
+ }
366
+ };
367
+ });
368
+ const enableGroup = groupName => {
369
+ const arrayOfElementToShow = [];
370
+ const arrayOfElementToHide = [];
371
+ files.forEach(file => {
372
+ const fileLink = fileListElement.querySelector("a[data-relative-url=\"".concat(file.relativeUrl, "\"]"));
373
+ const fileLi = fileLink.parentNode;
374
+ if (file.meta[groupName]) {
375
+ arrayOfElementToShow.push(fileLi);
376
+ } else {
377
+ arrayOfElementToHide.push(fileLi);
378
+ }
379
+ });
380
+ arrayOfElementToShow.forEach(element => {
381
+ element.removeAttribute("data-force-hide");
382
+ });
383
+ arrayOfElementToHide.forEach(element => {
384
+ element.setAttribute("data-force-hide", "");
385
+ });
386
+ h4.innerHTML = arrayOfElementToShow.length === 0 ? "No file found.\n Config for this section: <pre>".concat(JSON.stringify(groups[groupName], null, " "), "</pre>") : "".concat(arrayOfElementToShow.length, " files found. Click on the one you want to execute");
387
+ };
388
+ enableGroup(currentGroup);
389
+ document.querySelector("main").appendChild(fileListElement);</script>
390
+ <script no-supervisor="">// make menu scrollable
391
+ const getMenuWrapperSize = () => {
392
+ return document.querySelector(".menu-wrapper").getBoundingClientRect().width;
393
+ };
394
+ const getMenuSize = () => {
395
+ return document.querySelector(".menu").getBoundingClientRect().width;
396
+ };
397
+ const getMenuPosition = () => {
398
+ return document.querySelector(".menu-wrapper").scrollLeft;
399
+ };
400
+ let menuWrapperSize = getMenuWrapperSize();
401
+ let menuSize = getMenuSize();
402
+ const menuVisibleSize = menuWrapperSize;
403
+ let menuInvisibleSize = menuSize - menuVisibleSize;
404
+ const scrollDuration = 300;
405
+ const leftPaddle = document.querySelector(".left-paddle");
406
+ const rightPaddle = document.querySelector(".right-paddle");
407
+ const handleMenuScroll = () => {
408
+ menuInvisibleSize = menuSize - menuWrapperSize;
409
+ const menuPosition = getMenuPosition();
410
+ const menuEndOffset = menuInvisibleSize;
411
+ // show & hide the paddles, depending on scroll position
412
+ if (menuPosition <= 0 && menuEndOffset <= 0) {
413
+ // hide both paddles if the window is large enough to display all tabs
414
+ leftPaddle.classList.add("hidden");
415
+ rightPaddle.classList.add("hidden");
416
+ } else if (menuPosition <= 0) {
417
+ leftPaddle.classList.add("hidden");
418
+ rightPaddle.classList.remove("hidden");
419
+ } else if (menuPosition < Math.floor(menuEndOffset)) {
420
+ // show both paddles in the middle
421
+ leftPaddle.classList.remove("hidden");
422
+ rightPaddle.classList.remove("hidden");
423
+ } else if (menuPosition >= Math.floor(menuEndOffset)) {
424
+ leftPaddle.classList.remove("hidden");
425
+ rightPaddle.classList.add("hidden");
426
+ }
427
+ };
428
+ handleMenuScroll();
429
+ window.onresize = () => {
430
+ menuWrapperSize = getMenuWrapperSize();
431
+ menuSize = getMenuSize();
432
+ handleMenuScroll();
433
+ };
434
+ // finally, what happens when we are actually scrolling the menu
435
+ document.querySelector(".menu-wrapper").onscroll = () => {
436
+ handleMenuScroll();
437
+ };
438
+ const startJavaScriptAnimation = ({
439
+ duration = 300,
440
+ timingFunction = t => t,
441
+ onProgress = () => {},
442
+ onCancel = () => {},
443
+ onComplete = () => {}
444
+ }) => {
445
+ duration = parseInt(duration, 10);
446
+ const startMs = performance.now();
447
+ let currentRequestAnimationFrameId;
448
+ let done = false;
449
+ let rawProgress = 0;
450
+ let progress = 0;
451
+ const handler = () => {
452
+ currentRequestAnimationFrameId = null;
453
+ const nowMs = performance.now();
454
+ rawProgress = Math.min((nowMs - startMs) / duration, 1);
455
+ progress = timingFunction(rawProgress);
456
+ done = rawProgress === 1;
457
+ onProgress({
458
+ done,
459
+ rawProgress,
460
+ progress
461
+ });
462
+ if (done) {
463
+ onComplete();
464
+ } else {
465
+ currentRequestAnimationFrameId = window.requestAnimationFrame(handler);
466
+ }
467
+ };
468
+ handler();
469
+ const stop = () => {
470
+ if (currentRequestAnimationFrameId) {
471
+ window.cancelAnimationFrame(currentRequestAnimationFrameId);
472
+ currentRequestAnimationFrameId = null;
473
+ }
474
+ if (!done) {
475
+ done = true;
476
+ onCancel({
477
+ rawProgress,
478
+ progress
479
+ });
480
+ }
481
+ };
482
+ return stop;
483
+ };
484
+ rightPaddle.onclick = () => {
485
+ const scrollStart = document.querySelector(".menu-wrapper").scrollLeft;
486
+ const scrollEnd = scrollStart + menuWrapperSize;
487
+ startJavaScriptAnimation({
488
+ duration: scrollDuration,
489
+ onProgress: ({
490
+ progress
491
+ }) => {
492
+ document.querySelector(".menu-wrapper").scrollLeft = scrollStart + (scrollEnd - scrollStart) * progress;
493
+ }
494
+ });
495
+ };
496
+ leftPaddle.onclick = () => {
497
+ const scrollStart = document.querySelector(".menu-wrapper").scrollLeft;
498
+ const scrollEnd = scrollStart - menuWrapperSize;
499
+ startJavaScriptAnimation({
500
+ duration: scrollDuration,
501
+ onProgress: ({
502
+ progress
503
+ }) => {
504
+ document.querySelector(".menu-wrapper").scrollLeft = scrollStart + (scrollEnd - scrollStart) * progress;
505
+ }
506
+ });
507
+ };</script>
508
+ </body>
509
+ </html>
@@ -0,0 +1,70 @@
1
+ import { collectFiles } from "@jsenv/filesystem";
2
+
3
+ const explorerHtmlFileUrl = String(new URL("./html/explorer.html", import.meta.url));
4
+ const jsenvPluginExplorer = ({
5
+ pathname = "/",
6
+ groups = {
7
+ src: {
8
+ "./**/*.html": true,
9
+ "./**/*.test.html": false
10
+ },
11
+ tests: {
12
+ "./**/*.test.html": true
13
+ }
14
+ }
15
+ } = {}) => {
16
+ return {
17
+ name: "jsenv:explorer",
18
+ appliesDuring: "dev",
19
+ resolveReference: {
20
+ http_request: reference => {
21
+ if (reference.specifier === pathname || reference.specifier === "__explorer__") {
22
+ return explorerHtmlFileUrl;
23
+ }
24
+ return null;
25
+ }
26
+ },
27
+ transformUrlContent: {
28
+ html: async (urlInfo, context) => {
29
+ if (urlInfo.url !== explorerHtmlFileUrl) {
30
+ return null;
31
+ }
32
+ let html = urlInfo.content;
33
+ if (html.includes("SERVER_PARAMS")) {
34
+ const associationsForExplorable = {};
35
+ Object.keys(groups).forEach(groupName => {
36
+ const groupConfig = groups[groupName];
37
+ associationsForExplorable[groupName] = {
38
+ "**/.jsenv/": false,
39
+ // avoid visting .jsenv directory in jsenv itself
40
+ ...groupConfig
41
+ };
42
+ });
43
+ const matchingFileResultArray = await collectFiles({
44
+ directoryUrl: context.rootDirectoryUrl,
45
+ associations: associationsForExplorable,
46
+ predicate: meta => Object.keys(meta).some(group => Boolean(meta[group]))
47
+ });
48
+ const files = matchingFileResultArray.map(({
49
+ relativeUrl,
50
+ meta
51
+ }) => ({
52
+ relativeUrl,
53
+ meta
54
+ }));
55
+ html = html.replace("SERVER_PARAMS", JSON.stringify({
56
+ rootDirectoryUrl: context.rootDirectoryUrl,
57
+ groups,
58
+ files
59
+ }, null, " "));
60
+ Object.assign(urlInfo.headers, {
61
+ "cache-control": "no-store"
62
+ });
63
+ }
64
+ return html;
65
+ }
66
+ }
67
+ };
68
+ };
69
+
70
+ export { explorerHtmlFileUrl, jsenvPluginExplorer };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@jsenv/plugin-explorer",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/jsenv/core",
8
+ "directory": "packages/jsenv-plugin-explorer"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "type": "module",
14
+ "exports": {
15
+ ".": {
16
+ "import": {
17
+ "development": "./src/jsenv_plugin_explorer.js",
18
+ "default": "./dist/jsenv_plugin_explorer.js"
19
+ }
20
+ },
21
+ "./*": "./*"
22
+ },
23
+ "main": "./src/main.js",
24
+ "files": [
25
+ "/dist/",
26
+ "/src/"
27
+ ],
28
+ "dependencies": {
29
+ "@jsenv/filesystem": "4.2.3"
30
+ },
31
+ "scripts": {
32
+ "eslint": "npx eslint . --ext=.js,.mjs,.cjs,.html",
33
+ "build": "node ./scripts/build.mjs",
34
+ "prepublishOnly": "npm run build"
35
+ }
36
+ }
@@ -0,0 +1,289 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ [data-force-hide] {
6
+ display: none !important;
7
+ }
8
+
9
+ html,
10
+ body {
11
+ margin: 0;
12
+ padding: 0;
13
+ display: flex;
14
+ flex-direction: column;
15
+ flex: 1;
16
+ }
17
+
18
+ html {
19
+ font-family: arial, sans;
20
+ height: 100%;
21
+ }
22
+
23
+ button:focus,
24
+ a:focus,
25
+ input:focus,
26
+ *[data-contains-hidden-input] input:focus + * {
27
+ outline-style: none;
28
+ outline-offset: -2px;
29
+ }
30
+
31
+ *[data-contains-hidden-input] input:focus + * {
32
+ outline-width: 4px;
33
+ outline-offset: -2px;
34
+ }
35
+
36
+ html[data-last-interaction="keyboard"] button:focus,
37
+ html[data-last-interaction="keyboard"] a:focus,
38
+ html[data-last-interaction="keyboard"] input:focus,
39
+ html[data-last-interaction="keyboard"]
40
+ *[data-contains-hidden-input]
41
+ input:focus
42
+ + * {
43
+ outline-style: auto;
44
+ }
45
+
46
+ html[data-last-interaction="keyboard"]
47
+ *[data-contains-hidden-input]
48
+ input:focus
49
+ + * {
50
+ outline-color: rgb(0, 150, 255);
51
+ }
52
+
53
+ /*
54
+ Don't try to replace @-moz-document url-prefix() this by
55
+ -moz-outline or -moz-outline-color
56
+ Because firefox would take into account outline instead of -moz-outline
57
+ :moz-focus-ring
58
+ Because for some element we set the focus outline on a div which would not match :focusring
59
+ */
60
+ @-moz-document url-prefix() {
61
+ html[data-last-interaction="keyboard"] button:focus,
62
+ html[data-last-interaction="keyboard"] a:focus,
63
+ html[data-last-interaction="keyboard"] input:focus,
64
+ html[data-last-interaction="keyboard"]
65
+ *[data-input-customized]
66
+ input:focus
67
+ + * {
68
+ outline-width: 2px;
69
+ outline-offset: 0;
70
+ outline-style: solid;
71
+ /*
72
+ force a blue color for firefox otherwise
73
+ it uses outline-color: inherit
74
+ making it unpredictible and sometimes hard to see
75
+ */
76
+ outline-color: rgb(0, 150, 255);
77
+ }
78
+ }
79
+ ::-moz-focus-inner {
80
+ border: 0;
81
+ }
82
+
83
+ main {
84
+ display: flex;
85
+ flex-direction: column;
86
+ flex: 1;
87
+ }
88
+
89
+ main > * {
90
+ min-width: 100%;
91
+ flex: 1;
92
+ }
93
+
94
+ hr {
95
+ color: black;
96
+ background-color: black;
97
+ border-color: black;
98
+ margin: 15px 0;
99
+ }
100
+
101
+ article[data-page="file-list"] {
102
+ background-color: #204143;
103
+ color: #97a0a0;
104
+ }
105
+
106
+ nav,
107
+ #explorables-message {
108
+ background: #1f262c;
109
+ }
110
+
111
+ #explorables nav {
112
+ /* overflow hidden to ensure it wraps li which have margins
113
+ otherwise the background color is not the one from <nav> */
114
+ overflow: hidden;
115
+ }
116
+
117
+ #explorables-message {
118
+ overflow: auto; /* propagate background to margins cause by h4 inside */
119
+ }
120
+
121
+ article h2 {
122
+ margin: 0;
123
+ display: flex;
124
+ }
125
+
126
+ article h2 svg {
127
+ margin-right: 15px;
128
+ }
129
+
130
+ #main_html_file {
131
+ padding: 25px;
132
+ padding-bottom: 0;
133
+ }
134
+
135
+ #main_file_link {
136
+ color: #e7f2f3;
137
+ }
138
+
139
+ h2 {
140
+ margin: 20px 0;
141
+ color: #24b1b0;
142
+ display: inline-block;
143
+ }
144
+
145
+ h4 {
146
+ font-weight: normal;
147
+ font-size: 15px;
148
+ color: #97a0a0;
149
+ padding: 0 25px;
150
+ }
151
+
152
+ #explorables {
153
+ word-break: break-word;
154
+ }
155
+
156
+ #explorables-header {
157
+ /* padding-bottom: 10px; */
158
+ width: 100%;
159
+ position: sticky;
160
+ top: 0;
161
+ }
162
+
163
+ #explorables_header_and_menu {
164
+ padding-left: 25px;
165
+ padding-right: 25px;
166
+ padding-top: 25px;
167
+ background-color: #204143;
168
+ position: relative;
169
+ }
170
+
171
+ #explorables_header_bottom_spacing {
172
+ height: 20px;
173
+ background: #1f262c;
174
+ }
175
+
176
+ #directory_relative_url {
177
+ color: orange;
178
+ }
179
+
180
+ #explorables ul {
181
+ padding: 0px 25px 25px 25px;
182
+ margin: 0;
183
+ }
184
+
185
+ #explorables li {
186
+ list-style-type: none;
187
+ margin: 10px 0;
188
+ }
189
+
190
+ #explorables li:first-child {
191
+ margin-top: 0;
192
+ }
193
+
194
+ #explorables li .execution-link {
195
+ width: 100%;
196
+ word-break: break-all;
197
+ }
198
+
199
+ .execution-link {
200
+ background: #204143;
201
+ padding: 6px 12px;
202
+ font-size: 14px;
203
+ display: inline-block;
204
+ color: #e7f2f3;
205
+ font-family: monospace;
206
+ text-decoration: unset;
207
+ }
208
+
209
+ /* icons */
210
+ #main_file_icon {
211
+ fill: #24b1b0;
212
+ }
213
+
214
+ #explorables_icon {
215
+ width: 25px;
216
+ height: 25px;
217
+ stroke: none;
218
+ fill: #24b1b0;
219
+ }
220
+
221
+ #explorables fieldset {
222
+ border: none;
223
+ padding: 0;
224
+ margin: 0;
225
+ }
226
+
227
+ #explorables fieldset label {
228
+ display: inline-block;
229
+ padding: 0;
230
+ }
231
+
232
+ #explorables fieldset input {
233
+ opacity: 0;
234
+ position: absolute;
235
+ }
236
+
237
+ #explorables fieldset label input + * {
238
+ display: block;
239
+ padding: 0.7em 1em;
240
+ background-color: #204143;
241
+ border-radius: 0.1em;
242
+ min-width: 8em;
243
+ text-align: center;
244
+ }
245
+
246
+ #explorables fieldset input:checked + * {
247
+ color: orange;
248
+ background: #1f262c;
249
+ }
250
+
251
+ /* scrolable menu */
252
+ .menu-wrapper {
253
+ position: relative;
254
+ max-width: 100%;
255
+ overflow-x: hidden;
256
+ overflow-y: hidden;
257
+ transition: 300ms;
258
+ margin-top: 25px;
259
+ }
260
+ .menu {
261
+ box-sizing: border-box;
262
+ white-space: nowrap;
263
+ overflow-x: auto;
264
+ overflow-y: hidden;
265
+ -webkit-overflow-scrolling: touch;
266
+ }
267
+ .menu .item {
268
+ display: inline-block;
269
+ height: 100%;
270
+ padding: 1em;
271
+ box-sizing: border-box;
272
+ }
273
+ .paddle {
274
+ position: absolute;
275
+ height: 36px;
276
+ bottom: 0;
277
+ border: none;
278
+ background: #24b1b0;
279
+ color: #1f262c;
280
+ }
281
+ .left-paddle {
282
+ left: 0;
283
+ }
284
+ .right-paddle {
285
+ right: 0;
286
+ }
287
+ .hidden {
288
+ display: none;
289
+ }
@@ -0,0 +1,316 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Exploring</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta charset="utf-8" />
7
+ <link rel="icon" href="./explorer_favicon.png?inline" />
8
+ <link rel="stylesheet" href="./explorer.css?inline" />
9
+ </head>
10
+
11
+ <body>
12
+ <main></main>
13
+ <div style="display: none">
14
+ <article data-page="file-list">
15
+ <!--
16
+ <section id="main_html_file">
17
+ <h2>
18
+ <svg id="main_file_icon" viewBox="0 0 16 16" width="25px" height="25px">
19
+ <path
20
+ d="M15.45,7L14,5.551V2c0-0.55-0.45-1-1-1h-1c-0.55,0-1,0.45-1,1v0.553L9,0.555C8.727,0.297,8.477,0,8,0S7.273,0.297,7,0.555 L0.55,7C0.238,7.325,0,7.562,0,8c0,0.563,0.432,1,1,1h1v6c0,0.55,0.45,1,1,1h3v-5c0-0.55,0.45-1,1-1h2c0.55,0,1,0.45,1,1v5h3 c0.55,0,1-0.45,1-1V9h1c0.568,0,1-0.437,1-1C16,7.562,15.762,7.325,15.45,7z"
21
+ />
22
+ </svg>
23
+ <span
24
+ >Main html file:
25
+ <a id="main_file_link" href="javascript:void(0)">${mainFileRelativeUrl}</a></span
26
+ >
27
+ </h2>
28
+ <div>
29
+ <iframe id="main_file_iframe" src="about:blank"></iframe>
30
+ </div>
31
+ </section>
32
+ <hr />
33
+ !-->
34
+ <section id="explorables">
35
+ <div id="explorables-header">
36
+ <div id="explorables_header_and_menu">
37
+ <h2>
38
+ <svg
39
+ id="explorables_icon"
40
+ viewBox="0 0 24 24"
41
+ width="32"
42
+ height="32"
43
+ >
44
+ <path d="M0 0h24v24H0V0z" fill="none" />
45
+ <path
46
+ d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"
47
+ />
48
+ </svg>
49
+ <span
50
+ >File(s) to explore in
51
+ <span id="directory_relative_url" title="${directoryUrl}"
52
+ >${directoryName}</span
53
+ ></span
54
+ >
55
+ </h2>
56
+ <div class="menu-wrapper">
57
+ <fieldset id="filter-group-set" class="menu"></fieldset>
58
+ </div>
59
+ <div class="paddles">
60
+ <button class="left-paddle paddle hidden">&#60;</button>
61
+ <button class="right-paddle paddle">&#62;</button>
62
+ </div>
63
+ </div>
64
+ <div id="explorables_header_bottom_spacing"></div>
65
+ </div>
66
+ <div>
67
+ <div id="explorables-message">
68
+ <h4 style="margin-top: 0">${message}</h4>
69
+ </div>
70
+ <nav>
71
+ <ul></ul>
72
+ </nav>
73
+ </div>
74
+ </section>
75
+ </article>
76
+ </div>
77
+ <script no-supervisor>
78
+ // eslint-disable-next-line no-undef
79
+ const { rootDirectoryUrl, groups, files } = SERVER_PARAMS;
80
+
81
+ const groupPreference = {
82
+ has: () => localStorage.hasOwnProperty("explorer_active_group"),
83
+ get: () =>
84
+ localStorage.hasOwnProperty("explorer_active_group")
85
+ ? JSON.parse(localStorage.getItem("explorer_active_group"))
86
+ : undefined,
87
+ set: (value) =>
88
+ localStorage.setItem("explorer_active_group", JSON.stringify(value)),
89
+ };
90
+ const directoryNameFromUrl = (directoryUrl) => {
91
+ const slashLastIndex = directoryUrl.lastIndexOf(
92
+ "/",
93
+ // ignore last slash
94
+ directoryUrl.length - 2,
95
+ );
96
+ if (slashLastIndex === -1) return "";
97
+ return directoryUrl.slice(slashLastIndex + 1);
98
+ };
99
+ const urlToVisitFromRelativeUrl = (relativeUrl) => {
100
+ return `${window.origin}/${relativeUrl}`;
101
+ };
102
+
103
+ const fileListElement = document
104
+ .querySelector(`[data-page="file-list"]`)
105
+ .cloneNode(true);
106
+ const directoryName = directoryNameFromUrl(rootDirectoryUrl);
107
+ const span = fileListElement.querySelector("#directory_relative_url");
108
+ span.title = rootDirectoryUrl;
109
+ span.textContent = directoryName;
110
+
111
+ const h4 = fileListElement.querySelector("h4");
112
+ const ul = fileListElement.querySelector("ul");
113
+ ul.innerHTML = files
114
+ .map(
115
+ (file) =>
116
+ `<li>
117
+ <a
118
+ class="execution-link"
119
+ data-relative-url=${file.relativeUrl}
120
+ href=${urlToVisitFromRelativeUrl(file.relativeUrl)}
121
+ >
122
+ ${file.relativeUrl}
123
+ </a>
124
+ </li>`,
125
+ )
126
+ .join("");
127
+
128
+ const groupFieldset = fileListElement.querySelector("#filter-group-set");
129
+ const groupNames = Object.keys(groups);
130
+ groupFieldset.innerHTML = groupNames
131
+ .map(
132
+ (key) => `<label data-contains-hidden-input class="item">
133
+ <input type="radio" name="filter-group" value="${key}"/>
134
+ <span>${key}</span>
135
+ </label>`,
136
+ )
137
+ .join("");
138
+
139
+ const groupFromLocalStorage = groupPreference.get();
140
+ const currentGroup =
141
+ groupFromLocalStorage && groupNames.includes(groupFromLocalStorage)
142
+ ? groupFromLocalStorage
143
+ : groupNames[0];
144
+ Array.from(groupFieldset.querySelectorAll("input")).forEach(
145
+ (inputRadio) => {
146
+ inputRadio.checked = inputRadio.value === currentGroup;
147
+ inputRadio.onchange = () => {
148
+ if (inputRadio.checked) {
149
+ groupPreference.set(inputRadio.value);
150
+ enableGroup(inputRadio.value);
151
+ }
152
+ };
153
+ },
154
+ );
155
+
156
+ const enableGroup = (groupName) => {
157
+ const arrayOfElementToShow = [];
158
+ const arrayOfElementToHide = [];
159
+ files.forEach((file) => {
160
+ const fileLink = fileListElement.querySelector(
161
+ `a[data-relative-url="${file.relativeUrl}"]`,
162
+ );
163
+ const fileLi = fileLink.parentNode;
164
+ if (file.meta[groupName]) {
165
+ arrayOfElementToShow.push(fileLi);
166
+ } else {
167
+ arrayOfElementToHide.push(fileLi);
168
+ }
169
+ });
170
+ arrayOfElementToShow.forEach((element) => {
171
+ element.removeAttribute("data-force-hide");
172
+ });
173
+ arrayOfElementToHide.forEach((element) => {
174
+ element.setAttribute("data-force-hide", "");
175
+ });
176
+
177
+ h4.innerHTML =
178
+ arrayOfElementToShow.length === 0
179
+ ? `No file found.
180
+ Config for this section: <pre>${JSON.stringify(
181
+ groups[groupName],
182
+ null,
183
+ " ",
184
+ )}</pre>`
185
+ : `${arrayOfElementToShow.length} files found. Click on the one you want to execute`;
186
+ };
187
+ enableGroup(currentGroup);
188
+
189
+ document.querySelector("main").appendChild(fileListElement);
190
+ </script>
191
+ <script no-supervisor>
192
+ // make menu scrollable
193
+ const getMenuWrapperSize = () => {
194
+ return document.querySelector(".menu-wrapper").getBoundingClientRect()
195
+ .width;
196
+ };
197
+ const getMenuSize = () => {
198
+ return document.querySelector(".menu").getBoundingClientRect().width;
199
+ };
200
+ const getMenuPosition = () => {
201
+ return document.querySelector(".menu-wrapper").scrollLeft;
202
+ };
203
+
204
+ let menuWrapperSize = getMenuWrapperSize();
205
+ let menuSize = getMenuSize();
206
+ const menuVisibleSize = menuWrapperSize;
207
+ let menuInvisibleSize = menuSize - menuVisibleSize;
208
+ const scrollDuration = 300;
209
+ const leftPaddle = document.querySelector(".left-paddle");
210
+ const rightPaddle = document.querySelector(".right-paddle");
211
+
212
+ const handleMenuScroll = () => {
213
+ menuInvisibleSize = menuSize - menuWrapperSize;
214
+ const menuPosition = getMenuPosition();
215
+ const menuEndOffset = menuInvisibleSize;
216
+ // show & hide the paddles, depending on scroll position
217
+ if (menuPosition <= 0 && menuEndOffset <= 0) {
218
+ // hide both paddles if the window is large enough to display all tabs
219
+ leftPaddle.classList.add("hidden");
220
+ rightPaddle.classList.add("hidden");
221
+ } else if (menuPosition <= 0) {
222
+ leftPaddle.classList.add("hidden");
223
+ rightPaddle.classList.remove("hidden");
224
+ } else if (menuPosition < Math.floor(menuEndOffset)) {
225
+ // show both paddles in the middle
226
+ leftPaddle.classList.remove("hidden");
227
+ rightPaddle.classList.remove("hidden");
228
+ } else if (menuPosition >= Math.floor(menuEndOffset)) {
229
+ leftPaddle.classList.remove("hidden");
230
+ rightPaddle.classList.add("hidden");
231
+ }
232
+ };
233
+ handleMenuScroll();
234
+
235
+ window.onresize = () => {
236
+ menuWrapperSize = getMenuWrapperSize();
237
+ menuSize = getMenuSize();
238
+ handleMenuScroll();
239
+ };
240
+ // finally, what happens when we are actually scrolling the menu
241
+ document.querySelector(".menu-wrapper").onscroll = () => {
242
+ handleMenuScroll();
243
+ };
244
+
245
+ const startJavaScriptAnimation = ({
246
+ duration = 300,
247
+ timingFunction = (t) => t,
248
+ onProgress = () => {},
249
+ onCancel = () => {},
250
+ onComplete = () => {},
251
+ }) => {
252
+ duration = parseInt(duration, 10);
253
+ const startMs = performance.now();
254
+ let currentRequestAnimationFrameId;
255
+ let done = false;
256
+ let rawProgress = 0;
257
+ let progress = 0;
258
+ const handler = () => {
259
+ currentRequestAnimationFrameId = null;
260
+ const nowMs = performance.now();
261
+ rawProgress = Math.min((nowMs - startMs) / duration, 1);
262
+ progress = timingFunction(rawProgress);
263
+ done = rawProgress === 1;
264
+ onProgress({
265
+ done,
266
+ rawProgress,
267
+ progress,
268
+ });
269
+ if (done) {
270
+ onComplete();
271
+ } else {
272
+ currentRequestAnimationFrameId =
273
+ window.requestAnimationFrame(handler);
274
+ }
275
+ };
276
+ handler();
277
+ const stop = () => {
278
+ if (currentRequestAnimationFrameId) {
279
+ window.cancelAnimationFrame(currentRequestAnimationFrameId);
280
+ currentRequestAnimationFrameId = null;
281
+ }
282
+ if (!done) {
283
+ done = true;
284
+ onCancel({
285
+ rawProgress,
286
+ progress,
287
+ });
288
+ }
289
+ };
290
+ return stop;
291
+ };
292
+ rightPaddle.onclick = () => {
293
+ const scrollStart = document.querySelector(".menu-wrapper").scrollLeft;
294
+ const scrollEnd = scrollStart + menuWrapperSize;
295
+ startJavaScriptAnimation({
296
+ duration: scrollDuration,
297
+ onProgress: ({ progress }) => {
298
+ document.querySelector(".menu-wrapper").scrollLeft =
299
+ scrollStart + (scrollEnd - scrollStart) * progress;
300
+ },
301
+ });
302
+ };
303
+ leftPaddle.onclick = () => {
304
+ const scrollStart = document.querySelector(".menu-wrapper").scrollLeft;
305
+ const scrollEnd = scrollStart - menuWrapperSize;
306
+ startJavaScriptAnimation({
307
+ duration: scrollDuration,
308
+ onProgress: ({ progress }) => {
309
+ document.querySelector(".menu-wrapper").scrollLeft =
310
+ scrollStart + (scrollEnd - scrollStart) * progress;
311
+ },
312
+ });
313
+ };
314
+ </script>
315
+ </body>
316
+ </html>
Binary file
@@ -0,0 +1,81 @@
1
+ import { collectFiles } from "@jsenv/filesystem";
2
+
3
+ export const explorerHtmlFileUrl = String(
4
+ new URL("./client/explorer.html", import.meta.url),
5
+ );
6
+
7
+ export const jsenvPluginExplorer = ({
8
+ pathname = "/",
9
+ groups = {
10
+ src: {
11
+ "./**/*.html": true,
12
+ "./**/*.test.html": false,
13
+ },
14
+ tests: {
15
+ "./**/*.test.html": true,
16
+ },
17
+ },
18
+ } = {}) => {
19
+ return {
20
+ name: "jsenv:explorer",
21
+ appliesDuring: "dev",
22
+ resolveReference: {
23
+ http_request: (reference) => {
24
+ if (
25
+ reference.specifier === pathname ||
26
+ reference.specifier === "__explorer__"
27
+ ) {
28
+ return explorerHtmlFileUrl;
29
+ }
30
+ return null;
31
+ },
32
+ },
33
+ transformUrlContent: {
34
+ html: async (urlInfo, context) => {
35
+ if (urlInfo.url !== explorerHtmlFileUrl) {
36
+ return null;
37
+ }
38
+ let html = urlInfo.content;
39
+ if (html.includes("SERVER_PARAMS")) {
40
+ const associationsForExplorable = {};
41
+ Object.keys(groups).forEach((groupName) => {
42
+ const groupConfig = groups[groupName];
43
+ associationsForExplorable[groupName] = {
44
+ "**/.jsenv/": false, // avoid visting .jsenv directory in jsenv itself
45
+ ...groupConfig,
46
+ };
47
+ });
48
+ const matchingFileResultArray = await collectFiles({
49
+ directoryUrl: context.rootDirectoryUrl,
50
+ associations: associationsForExplorable,
51
+ predicate: (meta) =>
52
+ Object.keys(meta).some((group) => Boolean(meta[group])),
53
+ });
54
+ const files = matchingFileResultArray.map(
55
+ ({ relativeUrl, meta }) => ({
56
+ relativeUrl,
57
+ meta,
58
+ }),
59
+ );
60
+
61
+ html = html.replace(
62
+ "SERVER_PARAMS",
63
+ JSON.stringify(
64
+ {
65
+ rootDirectoryUrl: context.rootDirectoryUrl,
66
+ groups,
67
+ files,
68
+ },
69
+ null,
70
+ " ",
71
+ ),
72
+ );
73
+ Object.assign(urlInfo.headers, {
74
+ "cache-control": "no-store",
75
+ });
76
+ }
77
+ return html;
78
+ },
79
+ },
80
+ };
81
+ };