mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
initial
This commit is contained in:
356
src/editor/value_explorer.js
Normal file
356
src/editor/value_explorer.js
Normal file
@@ -0,0 +1,356 @@
|
||||
// TODO large arrays/objects
|
||||
// TODO maps, sets
|
||||
// TODO show Errors in red
|
||||
|
||||
import {el, stringify, scrollIntoViewIfNeeded} from './domutils.js'
|
||||
|
||||
const displayed_entries = object => {
|
||||
if(Array.isArray(object)) {
|
||||
return object.map((v, i) => [i, v])
|
||||
} else {
|
||||
const result = Object.entries(object)
|
||||
return (object instanceof Error)
|
||||
? [['message', object.message], ...result]
|
||||
: result
|
||||
}
|
||||
}
|
||||
|
||||
const is_expandable = v => typeof(v) == 'object'
|
||||
&& v != null
|
||||
&& displayed_entries(v).length != 0
|
||||
|
||||
|
||||
export const stringify_for_header = v => {
|
||||
const type = typeof(v)
|
||||
|
||||
if(v === null) {
|
||||
return 'null'
|
||||
} else if(v === undefined) {
|
||||
return 'undefined'
|
||||
} else if(type == 'function') {
|
||||
// TODO clickable link, 'fn', cursive
|
||||
return 'fn ' + v.name
|
||||
} else if(v instanceof Error) {
|
||||
return v.toString()
|
||||
} else if(type == 'object') {
|
||||
if(Array.isArray(v)) {
|
||||
if(v.length == 0) {
|
||||
return '[]'
|
||||
} else {
|
||||
return '[…]'
|
||||
}
|
||||
} else {
|
||||
if(displayed_entries(v).length == 0) {
|
||||
return '{}'
|
||||
} else {
|
||||
return '{…}'
|
||||
}
|
||||
}
|
||||
} else if(type == 'string') {
|
||||
return JSON.stringify(v)
|
||||
} else {
|
||||
return v.toString()
|
||||
}
|
||||
}
|
||||
|
||||
const header = object => {
|
||||
if(typeof(object) == 'undefined') {
|
||||
return 'undefined'
|
||||
} else if(object == null) {
|
||||
return 'null'
|
||||
} else if(typeof(object) == 'object') {
|
||||
if(object instanceof Error) {
|
||||
return object.toString()
|
||||
} else if(Array.isArray(object)) {
|
||||
return '['
|
||||
+ object
|
||||
.map(stringify_for_header)
|
||||
.join(', ')
|
||||
+ ']'
|
||||
} else {
|
||||
const inner = displayed_entries(object)
|
||||
.map(([k,v]) => {
|
||||
const value = stringify_for_header(v)
|
||||
return `${k}: ${value}`
|
||||
})
|
||||
.join(', ')
|
||||
return `{${inner}}`
|
||||
}
|
||||
} else if(typeof(object) == 'function') {
|
||||
// TODO clickable link, 'fn', cursive
|
||||
return 'fn ' + object.name
|
||||
} else if(typeof(object) == 'string') {
|
||||
return JSON.stringify(object)
|
||||
} else {
|
||||
return object.toString()
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
const get_path = (o, path) => {
|
||||
if(path.length == 0) {
|
||||
return o
|
||||
} else {
|
||||
const [start, ...rest] = path
|
||||
return get_path(o[start], rest)
|
||||
}
|
||||
}
|
||||
|
||||
export class ValueExplorer {
|
||||
|
||||
constructor({
|
||||
container,
|
||||
event_target = container,
|
||||
scroll_to_element,
|
||||
on_escape = () => {},
|
||||
} = {}
|
||||
) {
|
||||
this.container = container
|
||||
this.scroll_to_element = scroll_to_element
|
||||
this.on_escape = on_escape
|
||||
|
||||
event_target.addEventListener('keydown', (e) => {
|
||||
|
||||
/*
|
||||
Right -
|
||||
- does not has children - nothing
|
||||
- has children - first click expands, second jumps to first element
|
||||
|
||||
Left -
|
||||
- root - nothing
|
||||
- not root collapse node, goes to parent if already collapsed
|
||||
|
||||
Up - goes to prev visible element
|
||||
Down - goes to next visible element
|
||||
|
||||
Click - select and toggles expand
|
||||
*/
|
||||
|
||||
const current_object = get_path(this.value, this.current_path)
|
||||
|
||||
if(e.key == 'ArrowDown' || e.key == 'j'){
|
||||
// Do not scroll
|
||||
e.preventDefault()
|
||||
|
||||
if(is_expandable(current_object) && this.is_expanded(this.current_path)) {
|
||||
this.select_path(this.current_path.concat(
|
||||
displayed_entries(current_object)[0][0]
|
||||
))
|
||||
} else {
|
||||
const next = p => {
|
||||
if(p.length == 0) {
|
||||
return null
|
||||
}
|
||||
const parent = p.slice(0, p.length - 1)
|
||||
const children = displayed_entries(get_path(this.value, parent))
|
||||
const child_index = children.findIndex(([k,v]) =>
|
||||
k == p[p.length - 1]
|
||||
)
|
||||
const next_child = children[child_index + 1]
|
||||
if(next_child == null) {
|
||||
return next(parent)
|
||||
} else {
|
||||
return [...parent, next_child[0]]
|
||||
}
|
||||
}
|
||||
|
||||
const next_path = next(this.current_path)
|
||||
if(next_path != null) {
|
||||
this.select_path(next_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(e.key == 'ArrowUp' || e.key == 'k'){
|
||||
// Do not scroll
|
||||
e.preventDefault()
|
||||
|
||||
if(this.current_path.length == 0) {
|
||||
this.on_escape()
|
||||
return
|
||||
}
|
||||
const parent = this.current_path.slice(0, this.current_path.length - 1)
|
||||
const children = displayed_entries(get_path(this.value, parent))
|
||||
const child_index = children.findIndex(([k,v]) =>
|
||||
k == this.current_path[this.current_path.length - 1]
|
||||
)
|
||||
const next_child = children[child_index - 1]
|
||||
if(next_child == null) {
|
||||
this.select_path(parent)
|
||||
} else {
|
||||
const last = p => {
|
||||
if(!is_expandable(get_path(this.value, p)) || !this.is_expanded(p)) {
|
||||
return p
|
||||
} else {
|
||||
const children = displayed_entries(get_path(this.value, p))
|
||||
.map(([k,v]) => k)
|
||||
return last([...p, children[children.length - 1]])
|
||||
|
||||
}
|
||||
}
|
||||
this.select_path(last([...parent, next_child[0]]))
|
||||
}
|
||||
}
|
||||
|
||||
if(e.key == 'ArrowLeft' || e.key == 'h'){
|
||||
// Do not scroll
|
||||
e.preventDefault()
|
||||
|
||||
const is_expanded = this.is_expanded(this.current_path)
|
||||
if(!is_expandable(current_object) || !is_expanded) {
|
||||
if(this.current_path.length != 0) {
|
||||
const parent = this.current_path.slice(0, this.current_path.length - 1)
|
||||
this.select_path(parent)
|
||||
} else {
|
||||
this.on_escape()
|
||||
}
|
||||
} else {
|
||||
this.toggle_expanded()
|
||||
}
|
||||
}
|
||||
|
||||
if(e.key == 'ArrowRight' || e.key == 'l'){
|
||||
// Do not scroll
|
||||
e.preventDefault()
|
||||
|
||||
if(is_expandable(current_object)) {
|
||||
const is_expanded = this.is_expanded(this.current_path)
|
||||
if(!is_expanded) {
|
||||
this.toggle_expanded()
|
||||
} else {
|
||||
const children = displayed_entries(get_path(this.value, this.current_path))
|
||||
this.select_path(
|
||||
[
|
||||
...this.current_path,
|
||||
children[0][0],
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get_node_data(path, node_data = this.node_data) {
|
||||
if(path.length == 0) {
|
||||
return node_data
|
||||
} else {
|
||||
const [start, ...rest] = path
|
||||
return this.get_node_data(rest, node_data.children[start])
|
||||
}
|
||||
}
|
||||
|
||||
is_expanded(path) {
|
||||
return this.get_node_data(path).is_expanded
|
||||
}
|
||||
|
||||
on_click(path) {
|
||||
this.select_path(path)
|
||||
this.toggle_expanded()
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.container.innerHTML = ''
|
||||
this.node_data = {is_expanded: true}
|
||||
}
|
||||
|
||||
render(value) {
|
||||
this.clear()
|
||||
this.value = value
|
||||
const path = []
|
||||
this.container.appendChild(this.render_value_explorer_node(null, value, path, this.node_data))
|
||||
this.select_path(path)
|
||||
}
|
||||
|
||||
select_path(current_path) {
|
||||
if(this.current_path != null) {
|
||||
this.set_active(this.current_path, false)
|
||||
}
|
||||
this.current_path = current_path
|
||||
this.set_active(this.current_path, true)
|
||||
// Check that was already added to document
|
||||
if(document.contains(this.container)) {
|
||||
const target = this.get_node_data(current_path).el.getElementsByClassName('value_explorer_header')[0]
|
||||
if(this.scroll_to_element == null) {
|
||||
scrollIntoViewIfNeeded(this.container.parentNode, target)
|
||||
} else {
|
||||
this.scroll_to_element(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set_active(path, is_active) {
|
||||
const el = this.get_node_data(path).el.getElementsByClassName('value_explorer_header')[0]
|
||||
if(is_active) {
|
||||
el.classList.add('active')
|
||||
} else {
|
||||
el.classList.remove('active')
|
||||
}
|
||||
}
|
||||
|
||||
set_expanded(fn) {
|
||||
if(typeof(fn) == 'boolean') {
|
||||
return this.set_expanded(() => fn)
|
||||
}
|
||||
const val = this.is_expanded(this.current_path)
|
||||
const data = this.get_node_data(this.current_path)
|
||||
data.is_expanded = fn(data.is_expanded)
|
||||
const prev_dom_node = data.el
|
||||
const key = this.current_path.length == 0
|
||||
? null
|
||||
: this.current_path[this.current_path.length - 1]
|
||||
const value = get_path(this.value, this.current_path)
|
||||
const next = this.render_value_explorer_node(key, value, this.current_path, data)
|
||||
prev_dom_node.parentNode.replaceChild(next, prev_dom_node)
|
||||
}
|
||||
|
||||
toggle_expanded() {
|
||||
this.set_expanded(e => !e)
|
||||
this.set_active(this.current_path, true)
|
||||
}
|
||||
|
||||
render_value_explorer_node(key, value, path, node_data) {
|
||||
|
||||
const is_exp = is_expandable(value)
|
||||
const is_expanded = is_exp && node_data.is_expanded
|
||||
|
||||
node_data.children = {}
|
||||
|
||||
const result = el('div', 'value_explorer_node',
|
||||
|
||||
el('span', {
|
||||
class: 'value_explorer_header',
|
||||
click: this.on_click.bind(this, path),
|
||||
},
|
||||
is_exp
|
||||
? (is_expanded ? '▼' : '▶')
|
||||
: '\xa0',
|
||||
|
||||
key == null
|
||||
? null
|
||||
: el('span', 'value_explorer_key', key.toString(), ': '),
|
||||
|
||||
key == null || !is_exp || !is_expanded
|
||||
// Full header
|
||||
? header(value)
|
||||
// Short header
|
||||
: Array.isArray(value)
|
||||
? 'Array(' + value.length + ')'
|
||||
: ''
|
||||
),
|
||||
|
||||
(is_exp && is_expanded)
|
||||
? displayed_entries(value).map(([k,v]) => {
|
||||
node_data.children[k] = {}
|
||||
return this.render_value_explorer_node(k, v, [...path, k], node_data.children[k])
|
||||
})
|
||||
: []
|
||||
)
|
||||
|
||||
node_data.el = result
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user