diff --git a/index.html b/index.html
index 14a374f..f633ea8 100644
--- a/index.html
+++ b/index.html
@@ -200,16 +200,10 @@
}
.allow_file_access {
- height: 100%;
- padding: 10px;
display: flex;
flex-direction: column;
- justify-content: center;
- align-items: center;
- text-align: center;
}
.allow_file_access .subtitle {
- padding: 10px;
font-size: 0.8em;
}
diff --git a/src/cmd.js b/src/cmd.js
index bafde88..57d4284 100644
--- a/src/cmd.js
+++ b/src/cmd.js
@@ -808,7 +808,7 @@ const clear_io_trace = state => {
return run_code({...state, io_trace: null})
}
-const do_load_dir = (state, dir) => {
+const load_files = (state, dir) => {
const collect_files = dir => dir.kind == 'file'
? [dir]
: dir.children.map(collect_files).flat()
@@ -820,7 +820,7 @@ const do_load_dir = (state, dir) => {
return {
...state,
project_dir: dir,
- files: {...files, ...state.files},
+ files: {...files, '': state.files['']},
}
}
@@ -842,24 +842,29 @@ const apply_entrypoint_settings = (state, entrypoint_settings) => {
}
}
-const load_dir = (state, dir, entrypoint_settings) => {
+const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => {
// Clear parse cache and rerun code
- const with_dir = do_load_dir(state, dir)
+ const with_dir = load_files(state, dir)
return run_code({
...(
entrypoint_settings == null
? with_dir
: apply_entrypoint_settings(with_dir, entrypoint_settings)
),
+
+ has_file_system_access,
+
// remove cache. We have to clear cache because imports of modules that are
// not available because project_dir is not available have errors and the
// errors are cached
parse_result: null,
+
+ external_imports_cache: null,
})
}
const create_file = (state, dir, current_module) => {
- return {...load_dir(state, dir), current_module}
+ return {...load_dir(state, dir, true), current_module}
}
const open_run_window = (state, globals) => {
@@ -877,7 +882,7 @@ const open_run_window = (state, globals) => {
const get_initial_state = (state, entrypoint_settings) => {
const with_files = state.project_dir == null
? state
- : do_load_dir(state, state.project_dir)
+ : load_files(state, state.project_dir)
const with_settings = apply_entrypoint_settings(with_files, entrypoint_settings)
diff --git a/src/editor/domutils.js b/src/editor/domutils.js
index 7e53282..9558319 100644
--- a/src/editor/domutils.js
+++ b/src/editor/domutils.js
@@ -21,7 +21,7 @@ export function el(tag, className, ...children){
const append = child => {
if(typeof(child) == 'undefined') {
throw new Error('illegal state')
- } else if(child !== null) {
+ } else if(child !== null && child !== false) {
result.appendChild(
typeof(child) == 'string'
? document.createTextNode(child)
diff --git a/src/editor/files.js b/src/editor/files.js
index 6931224..3fe0c79 100644
--- a/src/editor/files.js
+++ b/src/editor/files.js
@@ -1,7 +1,13 @@
import {el} from './domutils.js'
import {map_find} from '../utils.js'
-import {load_dir, create_file} from '../filesystem.js'
-import {exec, get_state, open_directory, reload_run_window} from '../index.js'
+import {open_dir, create_file} from '../filesystem.js'
+import {
+ exec,
+ get_state,
+ open_directory,
+ reload_run_window,
+ close_directory,
+} from '../index.js'
const is_html = path => path.endsWith('.htm') || path.endsWith('.html')
const is_js = path => path == '' || path.endsWith('.js') || path.endsWith('.mjs')
@@ -25,27 +31,56 @@ export class Files {
reload_run_window(get_state())
}
- render(state) {
- if(state.project_dir == null) {
- this.el.innerHTML = ''
- this.el.appendChild(
- el('div', 'allow_file_access',
- el('a', {
- href: 'javascript:void(0)',
- click: open_directory,
- },
- `Allow access to local project folder`,
- ),
- el('div', 'subtitle', `Your files will never leave your device`)
- )
- )
- } else {
- this.render_files(state)
- }
- }
- render_files(state) {
- const children = [
+ render(state) {
+ const file_actions = state.has_file_system_access
+ ? el('div', 'file_actions',
+ el('a', {
+ 'class': 'file_action',
+ href: 'javascript: void(0)',
+ click: this.create_file.bind(this, false),
+ },
+ 'New file'
+ ),
+
+ el('a', {
+ 'class': 'file_action',
+ href: 'javascript: void(0)',
+ click: this.create_file.bind(this, true),
+ },
+ 'New dir'
+ ),
+
+ el('a', {
+ 'class': 'file_action',
+ href: 'javascript: void(0)',
+ click: close_directory,
+ },
+ 'Revoke access'
+ ),
+
+ el('a', {
+ href: 'https://github.com/leporello-js/leporello-js#selecting-entrypoint-module',
+ target: '__blank',
+ "class": 'select_entrypoint_title',
+ title: 'Select entrypoint',
+ },
+ 'Entry point'
+ ),
+ )
+ : el('div', 'file_actions',
+ el('div', 'file_action allow_file_access',
+ el('a', {
+ href: 'javascript: void(0)',
+ click: open_directory,
+ }, 'Allow access to local project folder'),
+ el('span', 'subtitle', `Your files will never leave your device`)
+ ),
+ )
+
+
+
+ const file_elements = [
this.render_file({name: '*scratch*', path: ''}, state),
this.render_file(state.project_dir, state),
]
@@ -54,41 +89,19 @@ export class Files {
if(files == null) {
this.el.innerHTML = ''
+ this.el.appendChild(file_actions)
this.el.appendChild(
- el('div', 'file_actions',
- el('a', {
- 'class': 'file_action',
- href: 'javascript: void(0)',
- click: this.create_file.bind(this, false),
- },
- 'Create file'
- ),
- el('a', {
- 'class': 'file_action',
- href: 'javascript: void(0)',
- click: this.create_file.bind(this, true),
- }, 'Create dir'),
- el('a', {
- href: 'https://github.com/leporello-js/leporello-js#selecting-entrypoint-module',
- target: '__blank',
- "class": 'select_entrypoint_title',
- title: 'Select entrypoint',
- }, 'Entry point'),
- )
- )
- this.el.appendChild(
- el('div', 'files',
- children
- )
+ el('div', 'files', file_elements)
)
} else {
// Replace to preserve scroll position
- files.replaceChildren(...children)
+ this.el.replaceChild(file_actions, this.el.children[0])
+ files.replaceChildren(...file_elements)
}
}
render_select_entrypoint(file, state) {
- if(file.kind == 'directory') {
+ if(!state.has_file_system_access || file.kind == 'directory') {
return null
} else if(is_js(file.path)) {
return el('span', 'select_entrypoint',
@@ -189,9 +202,9 @@ export class Files {
await create_file(path, is_dir)
// Reload all files for simplicity
- load_dir(false).then(dir => {
+ open_dir(false).then(dir => {
if(is_dir) {
- exec('load_dir', dir)
+ exec('load_dir', dir, true)
} else {
exec('create_file', dir, path)
}
@@ -205,8 +218,15 @@ export class Files {
this.active_el = e.currentTarget.parentElement
e.currentTarget.classList.add('active')
this.active_file = file
+
if(file.kind != 'directory') {
- exec('change_current_module', file.path)
+ if(get_state().has_file_system_access) {
+ exec('change_current_module', file.path)
+ } else {
+ // in examples mode, on click file we also change entrypoint for
+ // simplicity
+ exec('change_entrypoint', file.path)
+ }
}
}
}
diff --git a/src/effects.js b/src/effects.js
index 629ce6f..531d9d5 100644
--- a/src/effects.js
+++ b/src/effects.js
@@ -1,4 +1,5 @@
import {write_file} from './filesystem.js'
+import {write_example} from './examples.js'
import {color_file} from './color.js'
import {
root_calltree_node,
@@ -293,6 +294,12 @@ export const EFFECTS = {
localStorage[key] = value
},
- write: (state, [name, contents], ui) => write_file(name, contents),
+ write: (state, [name, contents], ui) => {
+ if(state.has_file_system_access) {
+ write_file(name, contents)
+ } else {
+ write_example(name, contents)
+ }
+ }
}
diff --git a/src/examples.js b/src/examples.js
new file mode 100644
index 0000000..c2b2a0c
--- /dev/null
+++ b/src/examples.js
@@ -0,0 +1,57 @@
+export const write_example = (name, contents) => {
+ localStorage['examples_' + name] = contents
+}
+
+const read_example = name => {
+ return localStorage['examples_' + name]
+}
+
+const list = [
+ 'github_api/index.js',
+ 'ethers/block_by_timestamp.js',
+ 'ethers/index.js',
+ // TODO for html5 example, open run window or hint that it should be opened
+]
+.map(l => l.split('/'))
+
+
+const get_children = path => {
+ const children = list.filter(l => path.every((elem, i) => elem == l[i] ))
+ const files = children.filter(c => c.length == path.length + 1)
+ const dirs = [...new Set(children
+ .filter(c => c.length != path.length + 1)
+ .map(c => c[path.length])
+ )]
+ return Promise.all(files.map(async f => {
+ const name = f[path.length]
+ const filepath = f.slice(0, path.length + 1).join('/')
+ return {
+ name,
+ path: filepath,
+ kind: 'file',
+ contents:
+ read_example(filepath) ??
+ await fetch(globalThis.location.origin + '/docs/examples/'+ filepath)
+ .then(r => r.text()),
+ }
+ })
+ .concat(dirs.map(async d => {
+ const p = [...path, d]
+ return {
+ name: d,
+ path: p.join('/'),
+ kind: 'directory',
+ children: await get_children(p),
+ }
+ })))
+}
+
+export const examples_promise = get_children([]).then(children => {
+ return {
+ kind: 'directory',
+ name: 'examples',
+ path: null,
+ children,
+ }
+})
+
diff --git a/src/filesystem.js b/src/filesystem.js
index a7bc2b0..b865620 100644
--- a/src/filesystem.js
+++ b/src/filesystem.js
@@ -15,11 +15,10 @@ const send_message = (message) => {
});
}
-globalThis.clear_directory_handle = () => {
+export const close_dir = () => {
send_message({type: 'SET_DIR_HANDLE', data: null})
clearInterval(keepalive_interval_id)
keepalive_interval_id = null
- window.location.reload()
}
let dir_handle
@@ -120,7 +119,7 @@ const read_file = async handle => {
return await file_data.text()
}
-const do_load_dir = async (handle, path) => {
+const do_open_dir = async (handle, path) => {
if(handle.kind == 'directory') {
const children = []
for await (let [name, h] of handle) {
@@ -134,7 +133,7 @@ const do_load_dir = async (handle, path) => {
kind: 'directory',
children: (await Promise.all(
children.map(c =>
- do_load_dir(c, path == null ? c.name : path + '/' + c.name)
+ do_open_dir(c, path == null ? c.name : path + '/' + c.name)
)
)).sort((a, b) => a.name.localeCompare(b.name))
}
@@ -159,7 +158,7 @@ export const create_file = (path, is_dir) => {
)
}
-export const load_dir = async (should_request_access) => {
+export const open_dir = async (should_request_access) => {
let handle
if(should_request_access) {
handle = await request_directory_handle()
@@ -171,5 +170,5 @@ export const load_dir = async (should_request_access) => {
} else {
keep_service_worker_alive()
}
- return do_load_dir(handle, null)
+ return do_open_dir(handle, null)
}
diff --git a/src/index.js b/src/index.js
index d48c475..89f3b7c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,12 +1,22 @@
import {UI} from './editor/ui.js'
import {EFFECTS, render_initial_state, apply_side_effects} from './effects.js'
-import {load_dir, init_window_service_worker} from './filesystem.js'
+import {
+ open_dir,
+ close_dir,
+ init_window_service_worker
+} from './filesystem.js'
+import {examples_promise} from './examples.js'
-const EXAMPLE = `const fib = n =>
- n == 0 || n == 1
- ? n
- : fib(n - 1) + fib(n - 2)
-fib(6)`
+const EXAMPLE = `function fib(n) {
+ if(n == 0 || n == 1) {
+ return n
+ } else {
+ return fib(n - 1) + fib(n - 2)
+ }
+}
+
+fib(6)
+`
const set_error_handler = (w, with_unhandled_rejection = true) => {
@@ -131,38 +141,56 @@ export const reload_run_window = state => {
globalThis.run_window.location = get_html_url(state)
}
+
const read_modules = async () => {
const default_module = {'': localStorage.code || EXAMPLE}
- const project_dir = await load_dir(false)
+ const project_dir = await open_dir(false)
if(project_dir == null) {
- // Single anonymous module
return {
+ project_dir: await examples_promise,
files: default_module,
+ has_file_system_access: false,
}
} else {
return {
project_dir,
files: default_module,
+ has_file_system_access: true,
}
}
}
-const get_entrypoint_settings = () => ({
- current_module: localStorage.current_module ?? '',
- entrypoint: localStorage.entrypoint ?? '',
- html_file: localStorage.html_file ?? '',
-})
+const get_entrypoint_settings = () => {
+
+ const params = new URLSearchParams(window.location.search)
+
+ const entrypoint = null
+ ?? params.get('entrypoint')
+ ?? localStorage.entrypoint
+ ?? ''
+
+ return {
+ current_module: params.get('entrypoint') ?? localStorage.current_module ?? '',
+ entrypoint,
+ html_file: localStorage.html_file ?? '',
+ }
+}
export const open_directory = () => {
if(globalThis.showDirectoryPicker == null) {
throw new Error('Your browser is not supporting File System Access API')
}
- load_dir(true).then(dir => {
- exec('load_dir', dir, get_entrypoint_settings())
+ open_dir(true).then(dir => {
+ exec('load_dir', dir, true, get_entrypoint_settings())
})
}
+export const close_directory = async () => {
+ close_dir()
+ exec('load_dir', await examples_promise, false, get_entrypoint_settings())
+}
+
let COMMANDS
let ui