Files
leporello-js/src/editor/files.js

267 lines
6.9 KiB
JavaScript
Raw Normal View History

2022-09-10 02:48:13 +08:00
import {el} from './domutils.js'
import {map_find} from '../utils.js'
2023-07-02 20:22:41 +03:00
import {open_dir, create_file} from '../filesystem.js'
2023-07-14 03:02:10 +03:00
import {examples} from '../examples.js'
2023-07-02 20:22:41 +03:00
import {
exec,
get_state,
open_directory,
2023-07-11 18:24:28 +03:00
reload_app_window,
2023-07-02 20:22:41 +03:00
close_directory,
} from '../index.js'
2023-07-02 19:21:20 +03:00
const is_html = path => path.endsWith('.htm') || path.endsWith('.html')
const is_js = path => path == '' || path.endsWith('.js') || path.endsWith('.mjs')
2022-09-10 02:48:13 +08:00
export class Files {
constructor(ui) {
this.ui = ui
this.el = el('div', 'files_container')
this.render(get_state())
}
2023-07-02 19:21:20 +03:00
change_entrypoint(e) {
const file = e.target.value
exec('change_entrypoint', file)
this.ui.editor.focus()
}
change_html_file(e) {
const html_file = e.target.value
exec('change_html_file', html_file)
2023-07-11 18:24:28 +03:00
reload_app_window(get_state())
2023-07-02 19:21:20 +03:00
}
2023-07-02 20:22:41 +03:00
render(state) {
2023-07-14 02:41:05 +03:00
this.file_to_el = new Map()
2023-07-02 20:22:41 +03:00
const file_actions = state.has_file_system_access
? el('div', 'file_actions',
2022-09-10 02:48:13 +08:00
el('a', {
2023-07-02 19:21:20 +03:00
'class': 'file_action',
2022-09-10 02:48:13 +08:00
href: 'javascript: void(0)',
click: this.create_file.bind(this, false),
},
2023-07-02 20:22:41 +03:00
'New file'
2022-09-10 02:48:13 +08:00
),
2023-07-02 20:22:41 +03:00
2022-09-10 02:48:13 +08:00
el('a', {
2023-07-02 19:21:20 +03:00
'class': 'file_action',
2022-09-10 02:48:13 +08:00
href: 'javascript: void(0)',
click: this.create_file.bind(this, true),
2023-07-02 20:22:41 +03:00
},
'New dir'
),
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: close_directory,
},
'Revoke access'
),
2023-07-02 19:21:20 +03:00
el('a', {
href: 'https://github.com/leporello-js/leporello-js#selecting-entrypoint-module',
target: '__blank',
"class": 'select_entrypoint_title',
title: 'Select entrypoint',
2023-07-02 20:22:41 +03:00
},
'Entry point'
),
2022-09-10 02:48:13 +08:00
)
2023-07-02 20:22:41 +03:00
: 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`)
),
2022-09-10 02:48:13 +08:00
)
2023-07-02 20:22:41 +03:00
const file_elements = [
this.render_file({name: '*scratch*', path: ''}, state),
this.render_file(state.project_dir, state),
]
const files = this.el.querySelector('.files')
if(files == null) {
this.el.innerHTML = ''
this.el.appendChild(file_actions)
this.el.appendChild(
el('div', 'files', file_elements)
2022-09-10 02:48:13 +08:00
)
} else {
// Replace to preserve scroll position
2023-07-02 20:22:41 +03:00
this.el.replaceChild(file_actions, this.el.children[0])
files.replaceChildren(...file_elements)
2022-09-10 02:48:13 +08:00
}
}
2023-07-14 02:41:05 +03:00
render_current_module(current_module) {
this.current_file = current_module
this.active_el.querySelector('.file_title').classList.remove('active')
this.active_el = this.file_to_el.get(current_module)
this.active_el.querySelector('.file_title').classList.add('active')
}
2023-07-02 19:21:20 +03:00
render_select_entrypoint(file, state) {
2023-07-02 20:22:41 +03:00
if(!state.has_file_system_access || file.kind == 'directory') {
2023-07-02 19:21:20 +03:00
return null
} else if(is_js(file.path)) {
return el('span', 'select_entrypoint',
el('input', {
type: 'radio',
name: 'js_entrypoint',
value: file.path,
checked: state.entrypoint == file.path,
change: e => this.change_entrypoint(e),
click: e => e.stopPropagation(),
})
)
} else if(is_html(file.path)) {
return el('span', 'select_entrypoint',
el('input', {
type: 'radio',
name: 'html_file',
value: file.path,
checked: state.html_file == file.path,
change: e => this.change_html_file(e),
click: e => e.stopPropagation(),
})
)
} else {
return null
}
}
render_file(file, state) {
2023-07-14 02:41:05 +03:00
const result = el('div', 'file',
2022-09-10 02:48:13 +08:00
el('div', {
2023-07-02 19:21:20 +03:00
'class': 'file_title' + (file.path == state.current_module ? ' active' : ''),
2022-09-10 02:48:13 +08:00
click: e => this.on_click(e, file)
},
el('span', 'icon',
file.kind == 'directory'
? '\u{1F4C1}' // folder icon
: '\xa0',
),
file.name,
2023-07-02 19:21:20 +03:00
this.render_select_entrypoint(file, state),
2022-09-10 02:48:13 +08:00
),
file.children == null
? null
2023-07-02 19:21:20 +03:00
: file.children.map(c => this.render_file(c, state))
2022-09-10 02:48:13 +08:00
)
2023-07-14 02:41:05 +03:00
this.file_to_el.set(file.path, result)
2023-07-02 19:21:20 +03:00
if(file.path == state.current_module) {
2022-09-10 02:48:13 +08:00
this.active_el = result
2023-07-14 02:41:05 +03:00
this.current_file = file.path
2022-09-10 02:48:13 +08:00
}
return result
}
async create_file(is_dir) {
let name = prompt(`Enter ${is_dir ? 'directory' : 'file'} name`)
if(name == null) {
return
}
const root = get_state().project_dir
2023-07-14 02:41:05 +03:00
let dir, file
if(this.current_file == '' /* scratch */) {
2022-09-10 02:48:13 +08:00
// Create in root directory
dir = root
} else {
2023-07-14 02:41:05 +03:00
const find_file_with_parent = (dir, parent) => {
if(dir.path == this.current_file) {
return [dir, parent]
}
if(dir.children == null) {
return null
2022-09-10 02:48:13 +08:00
}
2023-07-14 02:41:05 +03:00
return map_find(dir.children, c => find_file_with_parent(c, dir))
}
2022-09-10 02:48:13 +08:00
2023-07-14 02:41:05 +03:00
([file, dir] = find_file_with_parent(root))
2022-09-10 02:48:13 +08:00
2023-07-14 02:41:05 +03:00
if(file.kind == 'directory') {
dir = file
} else {
2022-09-10 02:48:13 +08:00
if(dir == null) {
throw new Error('illegal state')
}
}
}
const path = dir == root ? name : dir.path + '/' + name
await create_file(path, is_dir)
// Reload all files for simplicity
2023-07-02 20:22:41 +03:00
open_dir(false).then(dir => {
2022-09-10 02:48:13 +08:00
if(is_dir) {
2023-07-02 20:22:41 +03:00
exec('load_dir', dir, true)
2022-09-10 02:48:13 +08:00
} else {
exec('create_file', dir, path)
}
})
}
on_click(e, file) {
e.stopPropagation()
2023-07-02 20:22:41 +03:00
2023-07-14 03:02:10 +03:00
if(get_state().has_file_system_access) {
if(file.kind != 'directory') {
2023-07-02 20:22:41 +03:00
exec('change_current_module', file.path)
2023-07-14 03:02:10 +03:00
}
} else {
if(file.path == null) {
// root of examples dir, do nothing
return
}
if(file.path == '') {
exec('change_entrypoint', '')
return
}
const find_node = n =>
n.path == file.path
||
n.children != null && n.children.find(find_node)
// find example dir
const example_dir = get_state().project_dir.children.find(
c => find_node(c) != null
)
// in examples mode, on click file we also change entrypoint for
// simplicity
const example = examples.find(e => e.path == example_dir.path)
exec('change_entrypoint', example.entrypoint)
if(example.with_app_window && !localStorage.onboarding_open_app_window) {
this.ui.toggle_open_app_window_tooltip(true)
2023-07-02 20:22:41 +03:00
}
2022-09-10 02:48:13 +08:00
}
2023-07-14 03:02:10 +03:00
// Note that we call render_current_module AFTER exec('change_entrypoint'),
// because in case we clicked to example dir, entrypoint would be set, and
// rendered active, so file with entrypoint would be active and not dir we
// clicked, which is weird
this.render_current_module(file.path)
2022-09-10 02:48:13 +08:00
}
}