From 5481e8f999e1d47f6e23ba940e9f21328c345181 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 12 Jul 2025 23:58:01 +0500 Subject: [PATCH 001/130] initialized and transferred the code --- .gitignore | 3 + bundle.js | 543 ++++ index.html | 12 + lib/STATE.js | 0 lib/graph_explorer/entries.json | 770 ++++++ lib/graph_explorer/graph.txt | 98 + lib/graph_explorer/graph_explorer.js | 426 +++ lib/graph_explorer/package.json | 3 + lib/main.js | 1 + package-lock.json | 3710 ++++++++++++++++++++++++++ package.json | 31 + web/boot.js | 19 + web/page.js | 86 + 13 files changed, 5702 insertions(+) create mode 100644 .gitignore create mode 100644 bundle.js create mode 100644 index.html create mode 100644 lib/STATE.js create mode 100644 lib/graph_explorer/entries.json create mode 100644 lib/graph_explorer/graph.txt create mode 100644 lib/graph_explorer/graph_explorer.js create mode 100644 lib/graph_explorer/package.json create mode 100644 lib/main.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 web/boot.js create mode 100644 web/page.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7991f82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules/* +/package-lock.json +/npm-debug.log \ No newline at end of file diff --git a/bundle.js b/bundle.js new file mode 100644 index 0000000..0524df8 --- /dev/null +++ b/bundle.js @@ -0,0 +1,543 @@ +(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i { + vertical_scroll_value = el.scrollTop + horizontal_scroll_value = el.scrollLeft + } + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = `
` + const container = shadow.querySelector('.graph-container') + + let all_entries = {} + let view = [] + const instance_states = {} + + let start_index = 0 + let end_index = 0 + const chunk_size = 50 + const max_rendered_nodes = chunk_size * 3 + const node_height = 22 + + const top_sentinel = document.createElement('div') + const bottom_sentinel = document.createElement('div') + top_sentinel.className = 'sentinel' + bottom_sentinel.className = 'sentinel' + + const observer = new IntersectionObserver(handle_sentinel_intersection, { + root: el, + threshold: 0 + }) + + await sdb.watch(onbatch) + + return el + + async function onbatch(batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + const func = on[type] || fail + func(data, type) + } + } + + function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } + + function on_entries(data) { + all_entries = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + const root_path = '/' + if (all_entries[root_path]) { + if (!instance_states[root_path]) { + instance_states[root_path] = { expanded_subs: true, expanded_hubs: false } + } + build_and_render_view() + } + } + + function inject_style(data) { + const sheet = new CSSStyleSheet() + sheet.replaceSync(data[0]) + shadow.adoptedStyleSheets = [sheet] + } + + function build_and_render_view(focal_instance_path = null) { + const old_view = [...view] + const old_scroll_top = vertical_scroll_value + const old_scroll_left = horizontal_scroll_value + + view = build_view_recursive({ + base_path: '/', + parent_instance_path: '', + depth: 0, + is_last_sub : true, + is_hub: false, + parent_pipe_trail: [], + instance_states, + all_entries + }) + + let focal_index = -1 + if (focal_instance_path) { + focal_index = view.findIndex( + node => node.instance_path === focal_instance_path + ) + } + if (focal_index === -1) { + focal_index = Math.floor(old_scroll_top / node_height) + } + + const old_focal_node = old_view[focal_index] + let new_scroll_top = old_scroll_top + + if (old_focal_node) { + const old_focal_instance_path = old_focal_node.instance_path + const new_focal_index = view.findIndex( + node => node.instance_path === old_focal_instance_path + ) + if (new_focal_index !== -1) { + const scroll_diff = (new_focal_index - focal_index) * node_height + new_scroll_top = old_scroll_top + scroll_diff + } + } + + start_index = Math.max(0, focal_index - Math.floor(chunk_size / 2)) + end_index = start_index + + container.replaceChildren() + container.appendChild(top_sentinel) + container.appendChild(bottom_sentinel) + observer.observe(top_sentinel) + observer.observe(bottom_sentinel) + + render_next_chunk() + + requestAnimationFrame(() => { + el.scrollTop = new_scroll_top + el.scrollLeft = old_scroll_left + }) + } + + function build_view_recursive({ + base_path, + parent_instance_path, + parent_base_path = null, + depth, + is_last_sub, + is_hub, + is_first_hub = false, + parent_pipe_trail, + instance_states, + all_entries + }) { + + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return [] + + if (!instance_states[instance_path]) { + instance_states[instance_path] = { + expanded_subs: false, + expanded_hubs: false + } + } + const state = instance_states[instance_path] + const children_pipe_trail = [...parent_pipe_trail] + let last_pipe = null + + if (depth > 0) { + if (is_hub) { + last_pipe = [...parent_pipe_trail] + if (is_last_sub) { + children_pipe_trail.pop() + children_pipe_trail.push(is_last_sub) + last_pipe.pop() + last_pipe.push(true) + if (is_first_hub) { + last_pipe.pop() + last_pipe.push(false) + } + } + if (is_first_hub) { + children_pipe_trail.pop() + children_pipe_trail.push(false) + } + } + children_pipe_trail.push(!is_last_sub || is_hub) + } + + let current_view = [] + const is_hub_on_top = (base_path === all_entries[parent_base_path]?.hubs?.[0]) || (base_path === '/') + if (state.expanded_hubs && entry.hubs) { + entry.hubs.forEach((hub_path, i, arr) => { + current_view = current_view.concat( + build_view_recursive({ + base_path: hub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub : i === arr.length - 1, + is_hub: true, + is_first_hub: is_hub ? is_hub_on_top : false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries + }) + ) + }) + } + + current_view.push({ + base_path, + instance_path, + depth, + is_last_sub, + is_hub, + pipe_trail: (is_hub && is_last_sub) ? last_pipe : parent_pipe_trail + }) + + if (state.expanded_subs && entry.subs) { + entry.subs.forEach((sub_path, i, arr) => { + current_view = current_view.concat( + build_view_recursive({ + base_path: sub_path, + parent_instance_path: instance_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries + }) + ) + }) + } + return current_view + } + + function handle_sentinel_intersection(entries) { + entries.forEach(entry => { + if (entry.isIntersecting) { + if (entry.target === top_sentinel) render_prev_chunk() + else if (entry.target === bottom_sentinel) render_next_chunk() + } + }) + } + + function render_next_chunk() { + if (end_index >= view.length) return + const fragment = document.createDocumentFragment() + const next_end = Math.min(view.length, end_index + chunk_size) + for (let i = end_index; i < next_end; i++) { + fragment.appendChild(create_node(view[i])) + } + container.insertBefore(fragment, bottom_sentinel) + end_index = next_end + cleanup_dom(false) + } + + function render_prev_chunk() { + if (start_index <= 0) return + const fragment = document.createDocumentFragment() + const prev_start = Math.max(0, start_index - chunk_size) + for (let i = prev_start; i < start_index; i++) { + fragment.appendChild(create_node(view[i])) + } + const old_scroll_height = container.scrollHeight + const old_scroll_top = el.scrollTop + container.insertBefore(fragment, top_sentinel.nextSibling) + start_index = prev_start + cleanup_dom(true) + el.scrollTop = old_scroll_top + (container.scrollHeight - old_scroll_height) + } + + function cleanup_dom(is_scrolling_up) { + const rendered_count = end_index - start_index + if (rendered_count < max_rendered_nodes) return + const to_remove_count = rendered_count - max_rendered_nodes + if (is_scrolling_up) { + for (let i = 0; i < to_remove_count; i++) { + bottom_sentinel.previousElementSibling.remove() + } + end_index -= to_remove_count + } else { + for (let i = 0; i < to_remove_count; i++) { + top_sentinel.nextElementSibling.remove() + } + start_index += to_remove_count + } + } + + function get_prefix(is_last_sub, has_subs, state, is_hub) { + const { expanded_subs, expanded_hubs } = state + if (is_hub) { + if (expanded_subs && expanded_hubs) return '┌┼' + if (expanded_subs) return '┌┬' + if (expanded_hubs) return '┌┴' + return '┌─' + } else if (is_last_sub) { + if (expanded_subs && expanded_hubs) return '└┼' + if (expanded_subs) return '└┬' + if (expanded_hubs) return '└┴' + return '└─' + } else { + if (expanded_subs && expanded_hubs) return '├┼' + if (expanded_subs) return '├┬' + if (expanded_hubs) return '├┴' + return '├─' + } + } + + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail }) { + const entry = all_entries[base_path] + const state = instance_states[instance_path] + const el = document.createElement('div') + el.className = `node type-${entry.type}` + el.dataset.instance_path = instance_path + + const has_hubs = entry.hubs && entry.hubs.length > 0 + const has_subs = entry.subs && entry.subs.length > 0 + + if (depth) { + el.style.paddingLeft = '20px' + } + + if (base_path === '/' && instance_path === '|/') { + const { expanded_subs } = state + const prefix_symbol = expanded_subs ? '🪄┬' : '🪄─' + const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + el.innerHTML = `${prefix_symbol}/🌐` + if (has_subs) { + el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) + el.querySelector('.name').onclick = () => toggle_subs(instance_path) + } + return el + } + + const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub) + const pipe_html = pipe_trail.map(should_pipe => `${should_pipe ? '│' : ' '}`).join('') + + const prefix_class = (!has_hubs || base_path !== '/') ? 'prefix clickable' : 'prefix' + const icon_class = has_subs ? 'icon clickable' : 'icon' + + el.innerHTML = ` + ${pipe_html} + ${prefix_symbol} + + ${entry.name} + ` + if(has_hubs && base_path !== '/') el.querySelector('.prefix').onclick = () => toggle_hubs(instance_path) + if(has_subs) el.querySelector('.icon').onclick = () => toggle_subs(instance_path) + return el + } + + function toggle_subs(instance_path) { + const state = instance_states[instance_path] + if (state) { + state.expanded_subs = !state.expanded_subs + build_and_render_view(instance_path) + } + } + + function toggle_hubs(instance_path) { + const state = instance_states[instance_path] + if (state) { + state.expanded_hubs = !state.expanded_hubs + build_and_render_view(instance_path) + } + } +} + +function fallback_module() { + return { + api: fallback_instance + } + function fallback_instance() { + return { + drive: { + 'entries/': { + 'entries.json': { $ref: 'entries.json' } + }, + 'style/': { + 'theme.css': { + raw: ` + .graph-container { + color: #abb2bf; + background-color: #282c34; + padding: 10px; + height: 500px; /* Or make it flexible */ + overflow: auto; + } + .node { + display: flex; + align-items: center; + white-space: nowrap; + cursor: default; + height: 22px; /* Important for scroll calculation */ + } + .indent { + display: flex; + } + .pipe { + text-align: center; + } + .blank { + width: 10px; + text-align: center; + } + .clickable { + cursor: pointer; + } + .prefix, .icon { + margin-right: 6px; + } + .icon { display: inline-block; text-align: center; } + .name { flex-grow: 1; } + .node.type-root > .icon::before { content: '🌐'; } + .node.type-folder > .icon::before { content: '📁'; } + .node.type-html-file > .icon::before { content: '📄'; } + .node.type-js-file > .icon::before { content: '📜'; } + .node.type-css-file > .icon::before { content: '🎨'; } + .node.type-json-file > .icon::before { content: '📝'; } + .node.type-file > .icon::before { content: '📄'; } + .sentinel { height: 1px; } + ` + } + } + } + } + } +} + +}).call(this)}).call(this,"/lib/graph_explorer/graph_explorer.js") +},{"../STATE":1}],3:[function(require,module,exports){ +const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' +const init_url = location.hash === '#dev' ? 'web/init.js' : prefix + 'src/node_modules/init.js' +const args = arguments + +const has_save = location.hash.includes('#save') +const fetch_opts = has_save ? {} : { cache: 'no-store' } + +if (!has_save) { + localStorage.clear() +} + +fetch(init_url, fetch_opts).then(res => res.text()).then(async source => { + const module = { exports: {} } + const f = new Function('module', 'require', source) + f(module, require) + const init = module.exports + await init(args, prefix) + require('./page') // or whatever is otherwise the main entry of our project +}) + +},{"./page":4}],4:[function(require,module,exports){ +(function (__filename,__dirname){(function (){ +const STATE = require('../lib/STATE') +const statedb = STATE(__filename) +const { sdb } = statedb(fallback_module) + +/****************************************************************************** + PAGE +******************************************************************************/ +const app = require('../lib/graph_explorer') +const sheet = new CSSStyleSheet() +config().then(() => boot({ sid: '' })) + +async function config() { + const path = path => new URL(`../src/node_modules/${path}`, `file://${__dirname}`).href.slice(8) + const html = document.documentElement + const meta = document.createElement('meta') + const font = 'https://fonts.googleapis.com/css?family=Nunito:300,400,700,900|Slackey&display=swap' + const loadFont = `` + html.setAttribute('lang', 'en') + meta.setAttribute('name', 'viewport') + meta.setAttribute('content', 'width=device-width,initial-scale=1.0') + // @TODO: use font api and cache to avoid re-downloading the font data every time + document.head.append(meta) + document.head.innerHTML += loadFont + document.adoptedStyleSheets = [sheet] + await document.fonts.ready // @TODO: investigate why there is a FOUC +} +/****************************************************************************** + PAGE BOOT +******************************************************************************/ +async function boot(opts) { + // ---------------------------------------- + // ID + JSON STATE + // ---------------------------------------- + const on = { + theme: inject + } + const { drive } = sdb + + const subs = await sdb.watch(onbatch, on) + + // ---------------------------------------- + // TEMPLATE + // ---------------------------------------- + const el = document.body + const shopts = { mode: 'closed' } + const shadow = el.attachShadow(shopts) + shadow.adoptedStyleSheets = [sheet] + // ---------------------------------------- + // ELEMENTS + // ---------------------------------------- + { // desktop + shadow.append(await app(subs[0])) + } + // ---------------------------------------- + // INIT + // ---------------------------------------- + + async function onbatch(batch) { + for (const {type, paths} of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + on[type] && on[type](data) + } + } +} +async function inject(data) { + sheet.replaceSync(data.join('\n')) +} + +function fallback_module () { + return { + _: { + '../lib/graph_explorer': { + $: '', + 0: '', + mapping: { + 'style': 'style', + 'entries': 'entries' + } + } + }, + drive: { + 'theme/': { 'style.css': { raw: "body { font-family: 'system-ui'; }" } }, + 'lang/': {} + } + } +} +}).call(this)}).call(this,"/web/page.js","/web") +},{"../lib/STATE":1,"../lib/graph_explorer":2}]},{},[3]); diff --git a/index.html b/index.html new file mode 100644 index 0000000..2a9c6ee --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + + Graph-Explorer + + + + + \ No newline at end of file diff --git a/lib/STATE.js b/lib/STATE.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/graph_explorer/entries.json b/lib/graph_explorer/entries.json new file mode 100644 index 0000000..c9047f6 --- /dev/null +++ b/lib/graph_explorer/entries.json @@ -0,0 +1,770 @@ +{ + "/": { + "name": "root", + "type": "root", + "subs": [ + "/pins", + "/code", + "/data", + "/tasks" + ], + "hubs": [ + null + ] + }, + "/pins": { + "name": "pins", + "type": "folder", + "subs": [], + "hubs": [ + "/" + ] + }, + "/code": { + "name": "code", + "type": "folder", + "subs": [ + "/code/playproject_website", + "/code/theme_widget", + "/code/text_editor" + ], + "hubs": [ + "/" + ] + }, + "/data": { + "name": "data", + "type": "folder", + "subs": [ + "/data/themes" + ], + "hubs": [ + "/" + ] + }, + "/tasks": { + "name": "tasks", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget" + ], + "hubs": [ + "/" + ] + }, + "/code/playproject_website": { + "name": "playproject_website", + "type": "folder", + "subs": [ + "/code/playproject_website/index.html", + "/code/playproject_website/main.js", + "/code/playproject_website/styles.css" + ], + "hubs": [ + "/code" + ] + }, + "/code/playproject_website/index.html": { + "name": "index.html", + "type": "html-file", + "subs": [], + "hubs": [ + "/code/playproject_website" + ] + }, + "/code/playproject_website/main.js": { + "name": "main.js", + "type": "js-file", + "subs": [], + "hubs": [ + "/code/playproject_website" + ] + }, + "/code/playproject_website/styles.css": { + "name": "styles.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/code/playproject_website" + ] + }, + "/code/theme_widget": { + "name": "theme_widget", + "type": "folder", + "subs": [ + "/code/theme_widget/widget.html", + "/code/theme_widget/widget.js", + "/code/theme_widget/theme.css" + ], + "hubs": [ + "/code" + ] + }, + "/code/theme_widget/widget.html": { + "name": "widget.html", + "type": "html-file", + "subs": [], + "hubs": [ + "/code/theme_widget" + ] + }, + "/code/theme_widget/widget.js": { + "name": "widget.js", + "type": "js-file", + "subs": [], + "hubs": [ + "/code/theme_widget" + ] + }, + "/code/theme_widget/theme.css": { + "name": "theme.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/code/theme_widget" + ] + }, + "/code/text_editor": { + "name": "text_editor", + "type": "folder", + "subs": [ + "/code/text_editor/editor.html", + "/code/text_editor/editor.js", + "/code/text_editor/editor.css" + ], + "hubs": [ + "/code" + ] + }, + "/code/text_editor/editor.html": { + "name": "editor.html", + "type": "html-file", + "subs": [], + "hubs": [ + "/code/text_editor" + ] + }, + "/code/text_editor/editor.js": { + "name": "editor.js", + "type": "js-file", + "subs": [], + "hubs": [ + "/code/text_editor" + ] + }, + "/code/text_editor/editor.css": { + "name": "editor.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/code/text_editor" + ] + }, + "/data/themes": { + "name": "themes", + "type": "folder", + "subs": [ + "/data/themes/fantasy.json", + "/data/themes/electro.json", + "/data/themes/light.json", + "/data/themes/night.json" + ], + "hubs": [ + "/data" + ] + }, + "/data/themes/fantasy.json": { + "name": "fantasy.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/data/themes" + ] + }, + "/data/themes/electro.json": { + "name": "electro.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/data/themes" + ] + }, + "/data/themes/light.json": { + "name": "light.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/data/themes" + ] + }, + "/data/themes/night.json": { + "name": "night.json", + "type": "json-file", + "subs": [ + "/data/themes/night.json/page:css", + "/data/themes/night.json/page/header:css", + "/data/themes/night.json/page/header/menu:css", + "/data/themes/night.json/page/projects:css", + "/data/themes/night.json/page/footer:css", + "/data/themes/night.json/page/footer/socials:css" + ], + "hubs": [ + "/data/themes" + ] + }, + "/data/themes/night.json/page:css": { + "name": "page:css", + "type": "file", + "subs": [], + "hubs": [ + "/data/themes/night.json" + ] + }, + "/data/themes/night.json/page/header:css": { + "name": "page/header:css", + "type": "file", + "subs": [], + "hubs": [ + "/data/themes/night.json" + ] + }, + "/data/themes/night.json/page/header/menu:css": { + "name": "page/header/menu:css", + "type": "file", + "subs": [], + "hubs": [ + "/data/themes/night.json/page/header:css" + ] + }, + "/data/themes/night.json/page/projects:css": { + "name": "page/projects:css", + "type": "folder", + "subs": [ + "/data/themes/night.json/page/projects:css/header.css", + "/data/themes/night.json/page/projects:css/1.css" + ], + "hubs": [ + "/data/themes/night.json" + ] + }, + "/data/themes/night.json/page/projects:css/header.css": { + "name": "header.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/data/themes/night.json/page/projects:css" + ] + }, + "/data/themes/night.json/page/projects:css/1.css": { + "name": "1.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/data/themes/night.json/page/projects:css" + ] + }, + "/data/themes/night.json/page/footer:css": { + "name": "page/footer:css", + "type": "file", + "subs": [], + "hubs": [ + "/data/themes/night.json" + ] + }, + "/data/themes/night.json/page/footer/socials:css": { + "name": "page/footer/socials:css", + "type": "file", + "subs": [], + "hubs": [ + "/data/themes/night.json/page/footer:css" + ] + }, + "/tasks/0:theme_widget": { + "name": "0:theme_widget", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/task.json", + "/tasks/0:theme_widget/state", + "/tasks/0:theme_widget/theme-widget", + "/tasks/0:theme_widget/subs" + ], + "hubs": [ + "/tasks" + ] + }, + "/tasks/0:theme_widget/task.json": { + "name": "task.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget" + ] + }, + "/tasks/0:theme_widget/state": { + "name": "state", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/state/session.autosave", + "/tasks/0:theme_widget/state/undo.history" + ], + "hubs": [ + "/tasks/0:theme_widget" + ] + }, + "/tasks/0:theme_widget/state/session.autosave": { + "name": "session.autosave", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/state" + ] + }, + "/tasks/0:theme_widget/state/undo.history": { + "name": "undo.history", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/state" + ] + }, + "/tasks/0:theme_widget/theme-widget": { + "name": "theme-widget", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget" + ] + }, + "/tasks/0:theme_widget/subs": { + "name": "subs", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject", + "/tasks/0:theme_widget/subs/2:text_editor", + "/tasks/0:theme_widget/subs/3:text_editor" + ], + "hubs": [ + "/tasks/0:theme_widget" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject": { + "name": "1:playproject", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject/task.json", + "/tasks/0:theme_widget/subs/1:playproject/state", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ], + "hubs": [ + "/tasks/0:theme_widget/subs" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/task.json": { + "name": "task.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/state": { + "name": "state", + "type": "folder", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io": { + "name": "playproject-io", + "type": "file", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/topnav", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/header", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/supporters", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/footer" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject", + "/data/themes/night.json" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/topnav": { + "name": "topnav", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/header": { + "name": "header", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects": { + "name": "projects", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/projects", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/datdot", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/played", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/smartcontract_codes", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/wizardamigos", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/dat_ecosystem", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/data_shell" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/projects": { + "name": "projects", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css": { + "name": "css", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/light:page:css", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/header.css", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/1.css" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/light:page:css": { + "name": "light:page:css", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/header.css": { + "name": "header.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/1.css": { + "name": "1.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/datdot": { + "name": "datdot", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/played": { + "name": "played", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/smartcontract_codes": { + "name": "smartcontract_codes", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/wizardamigos": { + "name": "wizardamigos", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/dat_ecosystem": { + "name": "dat_ecosystem", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/data_shell": { + "name": "data_shell", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/supporters": { + "name": "supporters", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors": { + "name": "our_contributors", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Nina", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Jam", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Mauve", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Fiona", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Toshi", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ailin", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Kayla", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Tommings", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Santies", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Pepe", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Jannis", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Nora", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Mimi", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Helenphina", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ali", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ibrar", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Cypher" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Nina": { + "name": "Nina", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Jam": { + "name": "Jam", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Mauve": { + "name": "Mauve", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Fiona": { + "name": "Fiona", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Toshi": { + "name": "Toshi", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ailin": { + "name": "Ailin", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Kayla": { + "name": "Kayla", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Tommings": { + "name": "Tommings", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Santies": { + "name": "Santies", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Pepe": { + "name": "Pepe", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Jannis": { + "name": "Jannis", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Nora": { + "name": "Nora", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Mimi": { + "name": "Mimi", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Helenphina": { + "name": "Helenphina", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ali": { + "name": "Ali", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ibrar": { + "name": "Ibrar", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Cypher": { + "name": "Cypher", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/footer": { + "name": "footer", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/2:text_editor": { + "name": "2:text_editor", + "type": "folder", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor": { + "name": "3:text_editor", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/3:text_editor/task.json", + "/tasks/0:theme_widget/subs/3:text_editor/state", + "/tasks/0:theme_widget/subs/3:text_editor/editor" + ], + "hubs": [ + "/tasks/0:theme_widget/subs" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/task.json": { + "name": "task.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/state": { + "name": "state", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/3:text_editor/state/night.json" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/state/night.json": { + "name": "night.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor/state" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/editor": { + "name": "editor", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/3:text_editor/editor/tetxtarea", + "/tasks/0:theme_widget/subs/3:text_editor/editor/toolbar" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/editor/tetxtarea": { + "name": "tetxtarea", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor/editor" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/editor/toolbar": { + "name": "toolbar", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor/editor" + ] + } +} \ No newline at end of file diff --git a/lib/graph_explorer/graph.txt b/lib/graph_explorer/graph.txt new file mode 100644 index 0000000..1fd1a26 --- /dev/null +++ b/lib/graph_explorer/graph.txt @@ -0,0 +1,98 @@ +🪄┬🌐/ + ├─📁pins/ + ├┬📚code/ + │├┬📖playproject_website + ││├─📄index.html + ││├─📄main.js + ││└─📄styles.css + │├┬📖theme_widget + ││├─📄widget.html + ││├─📄widget.js + ││└─📄theme.css + │└┬📖text_editor + │ ├─📄editor.html + │ ├─📄editor.js + │ └─📄editor.css + ├┬📁data/ + ││┌─📁data/ + │└┼📁themes/ + │ ├─🎨fantasy.json + │ ├─🎨electro.json + │ ├─🎨light.json + │ │┌─📁themes/ + │ └┼🎨night.json + │ ├─🔗page:css + │ ├─🔗page/header:css + │ ├─🔗page/header/menu:css + │ │┌─📁themes/ + │ ├┼🔗page/projects:css + │ │├─🖌️header.css + │ │└─🖌️1.css + │ ├─🔗page/footer:css + │ └─🔗page/footer/socials:css + └┬🗄️tasks/ + └┬🗃️0:theme_widget/ + ├┬📄task.json + │├─📥night.json + │├─📓theme_widget + │└─📤night.json + ├┬📂state/ + │├─📄session.autosave + │└─📄undo.history + ├─🧩theme-widget + └┬🗄️subs/ + ├┬🗃️1:playproject + │├─📄task.json + │├─📁state/ + ││┌🇹page + ││├🎨night.json + │└┼🧩playproject-io + │ ├─🧩topnav + │ ├─🧩header + │ ├┬🧩projects + │ ││┌─🇹itemcard + │ ││├─🧩projects + │ ││├┬🔗css + │ ││││┌─🔗night:page/header:css + │ ││││├─🔗light:page/header:css + │ │││├┴🖌️header.css + │ │││└─🖌️1.css + │ ││├─🔗data + │ │├┴🧩datdot + │ │├─🧩played + │ │├─🧩smartcontract_codes + │ │├─🧩wizardamigos + │ │├─🧩dat_ecosystem + │ │└─🧩data_shell + │ ├─🧩supporters + │ │┌─🧩page + │ ├┼🧩our_contributors + │ ││┌─🇹itemcard + │ │├┴🧩Nina + │ │├─🧩Jam + │ │├─🧩Mauve + │ │├─🧩Fiona + │ │├─🧩Toshi + │ │├─🧩Ailin + │ │├─🧩Kayla + │ │├─🧩Tommings + │ │├─🧩Santies + │ │├─🧩Pepe + │ │├─🧩Jannis + │ │├─🧩Nora + │ │├─🧩Mimi + │ │├─🧩Helenphina + │ │├─🧩Ali + │ │├─🧩Ibrar + │ │└─🧩🔨{Cypher}🆔1 + │ └─🧩footer + ├─🗃️2:text_editor + └┬🗃️3:text_editor + ├┬📄task.json + │└─📓text_editor + ├─📂state/ + │┌🇹page + │├🎨night.json + └┼🧩editor + ├─🧩tetxtarea + └─🧩toolbar \ No newline at end of file diff --git a/lib/graph_explorer/graph_explorer.js b/lib/graph_explorer/graph_explorer.js new file mode 100644 index 0000000..672d335 --- /dev/null +++ b/lib/graph_explorer/graph_explorer.js @@ -0,0 +1,426 @@ +const STATE = require('../STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) + +module.exports = graph_explorer + +async function graph_explorer(opts) { + const { sdb } = await get(opts.sid) + const { drive } = sdb + + let vertical_scroll_value = 0 + let horizontal_scroll_value = 0 + + const on = { + entries: on_entries, + style: inject_style + } + + const el = document.createElement('div') + el.className = 'graph-explorer-wrapper' + el.onscroll = () => { + vertical_scroll_value = el.scrollTop + horizontal_scroll_value = el.scrollLeft + } + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = `
` + const container = shadow.querySelector('.graph-container') + + let all_entries = {} + let view = [] + const instance_states = {} + + let start_index = 0 + let end_index = 0 + const chunk_size = 50 + const max_rendered_nodes = chunk_size * 3 + const node_height = 22 + + const top_sentinel = document.createElement('div') + const bottom_sentinel = document.createElement('div') + top_sentinel.className = 'sentinel' + bottom_sentinel.className = 'sentinel' + + const observer = new IntersectionObserver(handle_sentinel_intersection, { + root: el, + threshold: 0 + }) + + await sdb.watch(onbatch) + + return el + + async function onbatch(batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + const func = on[type] || fail + func(data, type) + } + } + + function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } + + function on_entries(data) { + all_entries = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + const root_path = '/' + if (all_entries[root_path]) { + if (!instance_states[root_path]) { + instance_states[root_path] = { expanded_subs: true, expanded_hubs: false } + } + build_and_render_view() + } + } + + function inject_style(data) { + const sheet = new CSSStyleSheet() + sheet.replaceSync(data[0]) + shadow.adoptedStyleSheets = [sheet] + } + + function build_and_render_view(focal_instance_path = null) { + const old_view = [...view] + const old_scroll_top = vertical_scroll_value + const old_scroll_left = horizontal_scroll_value + + view = build_view_recursive({ + base_path: '/', + parent_instance_path: '', + depth: 0, + is_last_sub : true, + is_hub: false, + parent_pipe_trail: [], + instance_states, + all_entries + }) + + let focal_index = -1 + if (focal_instance_path) { + focal_index = view.findIndex( + node => node.instance_path === focal_instance_path + ) + } + if (focal_index === -1) { + focal_index = Math.floor(old_scroll_top / node_height) + } + + const old_focal_node = old_view[focal_index] + let new_scroll_top = old_scroll_top + + if (old_focal_node) { + const old_focal_instance_path = old_focal_node.instance_path + const new_focal_index = view.findIndex( + node => node.instance_path === old_focal_instance_path + ) + if (new_focal_index !== -1) { + const scroll_diff = (new_focal_index - focal_index) * node_height + new_scroll_top = old_scroll_top + scroll_diff + } + } + + start_index = Math.max(0, focal_index - Math.floor(chunk_size / 2)) + end_index = start_index + + container.replaceChildren() + container.appendChild(top_sentinel) + container.appendChild(bottom_sentinel) + observer.observe(top_sentinel) + observer.observe(bottom_sentinel) + + render_next_chunk() + + requestAnimationFrame(() => { + el.scrollTop = new_scroll_top + el.scrollLeft = old_scroll_left + }) + } + + function build_view_recursive({ + base_path, + parent_instance_path, + parent_base_path = null, + depth, + is_last_sub, + is_hub, + is_first_hub = false, + parent_pipe_trail, + instance_states, + all_entries + }) { + + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return [] + + if (!instance_states[instance_path]) { + instance_states[instance_path] = { + expanded_subs: false, + expanded_hubs: false + } + } + const state = instance_states[instance_path] + const children_pipe_trail = [...parent_pipe_trail] + let last_pipe = null + + if (depth > 0) { + if (is_hub) { + last_pipe = [...parent_pipe_trail] + if (is_last_sub) { + children_pipe_trail.pop() + children_pipe_trail.push(is_last_sub) + last_pipe.pop() + last_pipe.push(true) + if (is_first_hub) { + last_pipe.pop() + last_pipe.push(false) + } + } + if (is_first_hub) { + children_pipe_trail.pop() + children_pipe_trail.push(false) + } + } + children_pipe_trail.push(!is_last_sub || is_hub) + } + + let current_view = [] + const is_hub_on_top = (base_path === all_entries[parent_base_path]?.hubs?.[0]) || (base_path === '/') + if (state.expanded_hubs && entry.hubs) { + entry.hubs.forEach((hub_path, i, arr) => { + current_view = current_view.concat( + build_view_recursive({ + base_path: hub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub : i === arr.length - 1, + is_hub: true, + is_first_hub: is_hub ? is_hub_on_top : false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries + }) + ) + }) + } + + current_view.push({ + base_path, + instance_path, + depth, + is_last_sub, + is_hub, + pipe_trail: (is_hub && is_last_sub) ? last_pipe : parent_pipe_trail + }) + + if (state.expanded_subs && entry.subs) { + entry.subs.forEach((sub_path, i, arr) => { + current_view = current_view.concat( + build_view_recursive({ + base_path: sub_path, + parent_instance_path: instance_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries + }) + ) + }) + } + return current_view + } + + function handle_sentinel_intersection(entries) { + entries.forEach(entry => { + if (entry.isIntersecting) { + if (entry.target === top_sentinel) render_prev_chunk() + else if (entry.target === bottom_sentinel) render_next_chunk() + } + }) + } + + function render_next_chunk() { + if (end_index >= view.length) return + const fragment = document.createDocumentFragment() + const next_end = Math.min(view.length, end_index + chunk_size) + for (let i = end_index; i < next_end; i++) { + fragment.appendChild(create_node(view[i])) + } + container.insertBefore(fragment, bottom_sentinel) + end_index = next_end + cleanup_dom(false) + } + + function render_prev_chunk() { + if (start_index <= 0) return + const fragment = document.createDocumentFragment() + const prev_start = Math.max(0, start_index - chunk_size) + for (let i = prev_start; i < start_index; i++) { + fragment.appendChild(create_node(view[i])) + } + const old_scroll_height = container.scrollHeight + const old_scroll_top = el.scrollTop + container.insertBefore(fragment, top_sentinel.nextSibling) + start_index = prev_start + cleanup_dom(true) + el.scrollTop = old_scroll_top + (container.scrollHeight - old_scroll_height) + } + + function cleanup_dom(is_scrolling_up) { + const rendered_count = end_index - start_index + if (rendered_count < max_rendered_nodes) return + const to_remove_count = rendered_count - max_rendered_nodes + if (is_scrolling_up) { + for (let i = 0; i < to_remove_count; i++) { + bottom_sentinel.previousElementSibling.remove() + } + end_index -= to_remove_count + } else { + for (let i = 0; i < to_remove_count; i++) { + top_sentinel.nextElementSibling.remove() + } + start_index += to_remove_count + } + } + + function get_prefix(is_last_sub, has_subs, state, is_hub) { + const { expanded_subs, expanded_hubs } = state + if (is_hub) { + if (expanded_subs && expanded_hubs) return '┌┼' + if (expanded_subs) return '┌┬' + if (expanded_hubs) return '┌┴' + return '┌─' + } else if (is_last_sub) { + if (expanded_subs && expanded_hubs) return '└┼' + if (expanded_subs) return '└┬' + if (expanded_hubs) return '└┴' + return '└─' + } else { + if (expanded_subs && expanded_hubs) return '├┼' + if (expanded_subs) return '├┬' + if (expanded_hubs) return '├┴' + return '├─' + } + } + + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail }) { + const entry = all_entries[base_path] + const state = instance_states[instance_path] + const el = document.createElement('div') + el.className = `node type-${entry.type}` + el.dataset.instance_path = instance_path + + const has_hubs = entry.hubs && entry.hubs.length > 0 + const has_subs = entry.subs && entry.subs.length > 0 + + if (depth) { + el.style.paddingLeft = '20px' + } + + if (base_path === '/' && instance_path === '|/') { + const { expanded_subs } = state + const prefix_symbol = expanded_subs ? '🪄┬' : '🪄─' + const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + el.innerHTML = `${prefix_symbol}/🌐` + if (has_subs) { + el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) + el.querySelector('.name').onclick = () => toggle_subs(instance_path) + } + return el + } + + const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub) + const pipe_html = pipe_trail.map(should_pipe => `${should_pipe ? '│' : ' '}`).join('') + + const prefix_class = (!has_hubs || base_path !== '/') ? 'prefix clickable' : 'prefix' + const icon_class = has_subs ? 'icon clickable' : 'icon' + + el.innerHTML = ` + ${pipe_html} + ${prefix_symbol} + + ${entry.name} + ` + if(has_hubs && base_path !== '/') el.querySelector('.prefix').onclick = () => toggle_hubs(instance_path) + if(has_subs) el.querySelector('.icon').onclick = () => toggle_subs(instance_path) + return el + } + + function toggle_subs(instance_path) { + const state = instance_states[instance_path] + if (state) { + state.expanded_subs = !state.expanded_subs + build_and_render_view(instance_path) + } + } + + function toggle_hubs(instance_path) { + const state = instance_states[instance_path] + if (state) { + state.expanded_hubs = !state.expanded_hubs + build_and_render_view(instance_path) + } + } +} + +function fallback_module() { + return { + api: fallback_instance + } + function fallback_instance() { + return { + drive: { + 'entries/': { + 'entries.json': { $ref: 'entries.json' } + }, + 'style/': { + 'theme.css': { + raw: ` + .graph-container { + color: #abb2bf; + background-color: #282c34; + padding: 10px; + height: 500px; /* Or make it flexible */ + overflow: auto; + } + .node { + display: flex; + align-items: center; + white-space: nowrap; + cursor: default; + height: 22px; /* Important for scroll calculation */ + } + .indent { + display: flex; + } + .pipe { + text-align: center; + } + .blank { + width: 10px; + text-align: center; + } + .clickable { + cursor: pointer; + } + .prefix, .icon { + margin-right: 6px; + } + .icon { display: inline-block; text-align: center; } + .name { flex-grow: 1; } + .node.type-root > .icon::before { content: '🌐'; } + .node.type-folder > .icon::before { content: '📁'; } + .node.type-html-file > .icon::before { content: '📄'; } + .node.type-js-file > .icon::before { content: '📜'; } + .node.type-css-file > .icon::before { content: '🎨'; } + .node.type-json-file > .icon::before { content: '📝'; } + .node.type-file > .icon::before { content: '📄'; } + .sentinel { height: 1px; } + ` + } + } + } + } + } +} diff --git a/lib/graph_explorer/package.json b/lib/graph_explorer/package.json new file mode 100644 index 0000000..b3e053a --- /dev/null +++ b/lib/graph_explorer/package.json @@ -0,0 +1,3 @@ +{ + "main": "graph_explorer.js" +} \ No newline at end of file diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 0000000..ee21e6e --- /dev/null +++ b/lib/main.js @@ -0,0 +1 @@ +module.exports = require('./graph_explorer') diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..72a04af --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3710 @@ +{ + "name": "graph-explorer", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "graph-explorer", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "browserify": "^17.0.1", + "budo": "^11.8.4" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha512-sGwIGMjhYdW26/IhwK2gkWWI8DRCVO6uj3hYgHT+zD+QL1pa37tM3ujhyfcJIYSbsxp7Gxhy7zrRW/1AHm4BmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha512-f2PKUkN5QngiSemowa6Mrk9MPCdtFiOSmibjZ+j1qhLGHHYsqZwmBMRF3IRMVXo8sybDqx2fJl2d/8OphBoWkA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz", + "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.4", + "util": "^0.10.4" + } + }, + "node_modules/assert/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/assert/node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bole": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bole/-/bole-2.0.0.tgz", + "integrity": "sha512-/7aKG4IlOS3Gv15ccrSFiXwXjm5vPAFNfkncSNLYLpq4bH9m9N8Ef4QiTu8NN1lld8p7V1q8l8kOkbExEOv94A==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": ">=1.0.1 <1.1.0-0", + "individual": ">=3.0.0 <3.1.0-0", + "json-stringify-safe": ">=5.0.0 <5.1.0-0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/browser-pack": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", + "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "combine-source-map": "~0.8.0", + "defined": "^1.0.0", + "JSONStream": "^1.0.3", + "safe-buffer": "^5.1.1", + "through2": "^2.0.0", + "umd": "^3.0.0" + }, + "bin": { + "browser-pack": "bin/cmd.js" + } + }, + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.17.0" + } + }, + "node_modules/browserify": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-17.0.1.tgz", + "integrity": "sha512-pxhT00W3ylMhCHwG5yfqtZjNnFuX5h2IJdaBfSo4ChaaBsIp9VLrEMQ1bHV+Xr1uLPXuNDDM1GlJkjli0qkRsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^2.0.0", + "browserify-zlib": "~0.2.0", + "buffer": "~5.2.1", + "cached-path-relative": "^1.0.0", + "concat-stream": "^1.6.0", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.1", + "domain-browser": "^1.2.0", + "duplexer2": "~0.1.2", + "events": "^3.0.0", + "glob": "^7.1.0", + "hasown": "^2.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.2.1", + "JSONStream": "^1.0.3", + "labeled-stream-splicer": "^2.0.0", + "mkdirp-classic": "^0.5.2", + "module-deps": "^6.2.3", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "^1.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum-object": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^3.0.0", + "stream-http": "^3.0.0", + "string_decoder": "^1.1.1", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "0.0.1", + "url": "~0.11.0", + "util": "~0.12.0", + "vm-browserify": "^1.0.0", + "xtend": "^4.0.0" + }, + "bin": { + "browserify": "bin/cmd.js" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "dev": true, + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.5", + "hash-base": "~3.0", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/budo": { + "version": "11.8.4", + "resolved": "https://registry.npmjs.org/budo/-/budo-11.8.4.tgz", + "integrity": "sha512-drUnbk6nAuzQ4xmyWjajvUb85ZhGduXpblY9guD776HmPqWoShlEE8XiYX145v7+ZoqznnShI3QHAObK9YSWnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bole": "^2.0.0", + "browserify": "^17.0.0", + "chokidar": "^3.5.2", + "connect-pushstate": "^1.1.0", + "escape-html": "^1.0.3", + "events": "^1.0.2", + "garnish": "^5.0.0", + "get-ports": "^1.0.2", + "inject-lr-script": "^2.1.0", + "internal-ip": "^3.0.1", + "micromatch": "^4.0.5", + "on-finished": "^2.3.0", + "on-headers": "^1.0.1", + "once": "^1.3.2", + "opn": "^3.0.2", + "path-is-absolute": "^1.0.1", + "pem": "^1.13.2", + "reload-css": "^1.0.0", + "resolve": "^1.1.6", + "serve-static": "^1.10.0", + "simple-html-index": "^1.4.0", + "stacked": "^1.1.1", + "stdout-stream": "^1.4.0", + "strip-ansi": "^3.0.0", + "subarg": "^1.0.0", + "term-color": "^1.0.1", + "url-trim": "^1.0.0", + "watchify-middleware": "^1.9.1", + "ws": "^6.2.2", + "xtend": "^4.0.0" + }, + "bin": { + "budo": "bin/cmd.js" + } + }, + "node_modules/budo/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cached-path-relative": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz", + "integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha512-bIKA54hP8iZhyDT81TOsJiQvR1gW+ZYSXFaZUAvoD4wCHdbHY2actmpTE4x344ZlFqHbvoxKOaESULTZN2gstg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk/node_modules/strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha512-DerhZL7j6i6/nEnVG0qViKXI0OKouvvpsAiaj7c+LfqZZZxdwZtv8+UiA/w4VUJpT8UzX0pR1dcHOii1GbmruQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^0.2.1" + }, + "bin": { + "strip-ansi": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cipher-base": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha512-UlxQ9Vw0b/Bt/KYwCFqdEwsQ1eL8d1gibiFb7lxQJFdvTgc2hIZi6ugsg+kyhzhPV+QEpUiEIwInIAIrgoEkrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/connect-pushstate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/connect-pushstate/-/connect-pushstate-1.1.0.tgz", + "integrity": "sha512-5p2H2+eXkCiqcSZqZbTh5TLcLsl1wub7VKrRnfHyorC+pxXjF6nfswda4YMluYhGMo+33eR/58weorzSi9uSbA==", + "dev": true, + "license": "MIT" + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dash-ast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", + "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/default-gateway": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz", + "integrity": "sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ==", + "dev": true, + "license": "BSD-2-Clause", + "os": [ + "android", + "darwin", + "freebsd", + "linux", + "openbsd", + "sunos", + "win32" + ], + "dependencies": { + "execa": "^0.10.0", + "ip-regex": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deps-sort": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", + "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "JSONStream": "^1.0.3", + "shasum-object": "^1.0.0", + "subarg": "^1.0.0", + "through2": "^2.0.0" + }, + "bin": { + "deps-sort": "bin/cmd.js" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-promisify": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-7.0.0.tgz", + "integrity": "sha512-ginqzK3J90Rd4/Yz7qRrqUeIpe3TwSXTPPZtPne7tGBPeAaQiU8qt4fpKApnxHcq1AwtUdHVg5P77x/yrggG8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/from2-string/-/from2-string-1.1.0.tgz", + "integrity": "sha512-m8vCh+KnXXXBtfF2VUbiYlQ+nczLcntB0BrtNgpmLkHylhObe9WF1b2LZjBBzrZzA6P4mkEla6ZYQoOUTG8cYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.0.3" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/garnish": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/garnish/-/garnish-5.2.0.tgz", + "integrity": "sha512-y0qv1q5ylEtbKW08LGDxmhrmyHGIXH2Jfcz3JPKFikMPDQ0mgBIbtOc2R3fXHYOXOfDfDpx1o5G9rMl4jpU0qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^0.5.1", + "minimist": "^1.1.0", + "pad-left": "^2.0.0", + "pad-right": "^0.2.2", + "prettier-bytes": "^1.0.3", + "pretty-ms": "^2.1.0", + "right-now": "^1.0.0", + "split2": "^0.2.1", + "stdout-stream": "^1.4.0", + "url-trim": "^1.0.0" + }, + "bin": { + "garnish": "bin/cmd.js" + } + }, + "node_modules/get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-ports": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-ports/-/get-ports-1.0.3.tgz", + "integrity": "sha512-XtNFp93OT2wNEX/PkcCJ5+4PR5fxYCK+J2BsfJO8eV7hCYbqROt+8XO6iApJqJ06A2UJMUueDCoJ1Lp5vypuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "map-limit": "0.0.1" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha512-1YsTg1fk2/6JToQhtZkArMkurq8UoWU1Qe0aR3VUHjgij4nOylSWLWAtBXoZ4/dXOmugfLGm1c+QhuD0JyedFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^0.2.0" + }, + "bin": { + "has-ansi": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/htmlescape": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/individual": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz", + "integrity": "sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inject-lr-script": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/inject-lr-script/-/inject-lr-script-2.2.0.tgz", + "integrity": "sha512-lFLjCOg2XP8233AiET5vFePo910vhNIkKHDzUptNhc+4Y7dsp/TNBiusUUpaxzaGd6UDHy0Lozfl9AwmteK6DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resp-modifier": "^6.0.0" + } + }, + "node_modules/inline-source-map": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.3.tgz", + "integrity": "sha512-1aVsPEsJWMJq/pdMU61CDlm1URcW702MTB4w9/zUjMus6H/Py8o7g68Pr9D4I6QluWGt/KdmswuRhaA05xVR1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.5.3" + } + }, + "node_modules/insert-module-globals": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", + "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-node": "^1.5.2", + "combine-source-map": "^0.8.0", + "concat-stream": "^1.6.1", + "is-buffer": "^1.1.0", + "JSONStream": "^1.0.3", + "path-is-absolute": "^1.0.1", + "process": "~0.11.0", + "through2": "^2.0.0", + "undeclared-identifiers": "^1.1.2", + "xtend": "^4.0.0" + }, + "bin": { + "insert-module-globals": "bin/cmd.js" + } + }, + "node_modules/internal-ip": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-3.0.1.tgz", + "integrity": "sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q==", + "dev": true, + "license": "MIT", + "os": [ + "android", + "darwin", + "freebsd", + "linux", + "openbsd", + "sunos", + "win32" + ], + "dependencies": { + "default-gateway": "^2.6.0", + "ipaddr.js": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/labeled-stream-splicer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", + "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "stream-splicer": "^2.0.0" + } + }, + "node_modules/lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/map-limit": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz", + "integrity": "sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "~1.3.0" + } + }, + "node_modules/map-limit/node_modules/once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/module-deps": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", + "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-resolve": "^2.0.0", + "cached-path-relative": "^1.0.2", + "concat-stream": "~1.6.0", + "defined": "^1.0.0", + "detective": "^5.2.0", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "JSONStream": "^1.0.3", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.4.0", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + }, + "bin": { + "module-deps": "bin/cmd.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opn": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz", + "integrity": "sha512-YKyQo/aDk+kLY/ChqYx3DMWW8cbxvZDh+7op1oU60TmLHGWFrn2gPaRWihzDhSwCarAESa9G8dNXzjTGfLx8FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/outpipe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", + "integrity": "sha512-BnNY/RwnDrkmQdUa9U+OfN/Y7AWmKuUPCCd+hbRclZnnANvYpO72zp/a6Q4n829hPbdqEac31XCcsvlEvb+rtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shell-quote": "^1.4.2" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pad-left": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pad-left/-/pad-left-2.1.0.tgz", + "integrity": "sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pad-right": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", + "integrity": "sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha512-mXKF3xkoUt5td2DoxpLmtOmZvko9VfFpwRwkKDHSNvgmpLAeBo18YDhcPbBzJq+QLCHMbGOfzia2cX4U+0v9Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-platform": "~0.11.15" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "dev": true, + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-ms": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz", + "integrity": "sha512-LpH1Cf5EYuVjkBvCDBYvkUPh+iv2bk3FHflxHkpCYT0/FZ1d3N3uJaLiHr4yGuMcFUhv6eAivitTvWZI4B/chg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", + "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "create-hash": "~1.1.3", + "create-hmac": "^1.1.7", + "ripemd160": "=2.0.1", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.11", + "to-buffer": "^1.2.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/pbkdf2/node_modules/create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "node_modules/pbkdf2/node_modules/hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1" + } + }, + "node_modules/pbkdf2/node_modules/ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/pem": { + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/pem/-/pem-1.14.8.tgz", + "integrity": "sha512-ZpbOf4dj9/fQg5tQzTqv4jSKJQsK7tPl0pm4/pvPcZVjZcJg7TMfr3PBk6gJH97lnpJDu4e4v8UUqEz5daipCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es6-promisify": "^7.0.0", + "md5": "^2.3.0", + "os-tmpdir": "^1.0.2", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pem/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plur": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz", + "integrity": "sha512-qSnKBSZeDY8ApxwhfVIwKwF36KVJqb1/9nzYYq3j3vdwocULCXT8f8fQGkiw1Nk9BGfxiDagEe/pwakA+bOBqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prettier-bytes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz", + "integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pretty-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz", + "integrity": "sha512-H2enpsxzDhuzRl3zeSQpQMirn8dB0Z/gxW96j06tMfTviUWvX14gjKb7qd1gtkUyYhDPuoNe00K5PqNvy2oQNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-finite": "^1.0.1", + "parse-ms": "^1.0.0", + "plur": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha512-3ALe0bjBVZtkdWKIcThYpQCLbBMd/+Tbh2CDSrAIDO3UsZ4Xs+tnyjv2MjCOMMgBG+AsUOeuP1cgtY1INISc8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reload-css": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/reload-css/-/reload-css-1.0.2.tgz", + "integrity": "sha512-R4IA5JujRJlvGW5B5qCKrlNgqrrLqsa0uvFaXFWtHcE7dkA9c6c8WbqKpvqQ82hOoWkHjmigZ970jTfsywzMxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "query-string": "^4.2.3" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resp-modifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", + "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/right-now": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/right-now/-/right-now-1.0.0.tgz", + "integrity": "sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shasum-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", + "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-html-index": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/simple-html-index/-/simple-html-index-1.5.0.tgz", + "integrity": "sha512-+okfICS99jb6y4u584/nIt1dqM74+aTT93GM1JhSvy0IYbj6So7zfuaR7z2TIrt87nNpMRZIS504J+mRvGUBeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "from2-string": "^1.1.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/split2/-/split2-0.2.1.tgz", + "integrity": "sha512-D/oTExYAkC9nWleOCTOyNmAuzfAT/6rHGBA9LIK7FVnGo13CSvrKCUzKenwH6U1s2znY9MqH6v0UQTEDa3vJmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "through2": "~0.6.1" + } + }, + "node_modules/split2/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/split2/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/split2/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/split2/node_modules/through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "node_modules/stacked": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stacked/-/stacked-1.1.1.tgz", + "integrity": "sha512-3Kj9zD4TycXGCeOMPg8yv1lIbwYATx+O8dQ/m1MSaixpOnNwdrS2YgLGwyWUF8zO2Q26LHsAr8KKb88flQESnQ==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-browserify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-splicer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", + "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.1.0" + } + }, + "node_modules/supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha512-tdCZ28MnM7k7cJDJc7Eq80A9CsRFAAOZUy41npOZCs++qSjfIy7o5Rh46CBk+Dk5FbKJ33X3Tqg4YrV07N5RaA==", + "dev": true, + "license": "MIT", + "bin": { + "supports-color": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-node": "^1.2.0" + } + }, + "node_modules/term-color": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/term-color/-/term-color-1.0.1.tgz", + "integrity": "sha512-4Ld+sFlAdziaaMabvBU215dxyMotGoz7yN+9GtPE7RhKvzXAmg8tD/nKohJp4v2bMdSsNO3FEIBxFDsXu0Pf8w==", + "dev": true, + "dependencies": { + "ansi-styles": "2.0.1", + "supports-color": "1.3.1" + } + }, + "node_modules/term-color/node_modules/ansi-styles": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.0.1.tgz", + "integrity": "sha512-0zjsXMhnTibRx8YrLgLKb5NvWEcHN/OZEe1NzR8VVrEM6xr7/NyLsoMVelAhaoJhOtpuexaeRGD8MF8Z64+9LQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/term-color/node_modules/supports-color": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz", + "integrity": "sha512-OHbMkscHFRcNWEcW80fYhCrzAjheSIBwJChpFaBqA6zEz53nxumqi6ukciRb/UA0/v2nDNMk28ce/uBbYRDsng==", + "dev": true, + "license": "MIT", + "bin": { + "supports-color": "cli.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha512-PIxwAupJZiYU4JmVZYwXp9FKsHMXb5h0ZEFyuXTAn8WLHOlcij+FEcbrvDsom1o5dr1YggEtFbECvGCW2sT53Q==", + "dev": true, + "dependencies": { + "process": "~0.11.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/umd": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", + "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", + "dev": true, + "license": "MIT", + "bin": { + "umd": "bin/cli.js" + } + }, + "node_modules/undeclared-identifiers": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", + "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "acorn-node": "^1.3.0", + "dash-ast": "^1.0.0", + "get-assigned-identifiers": "^1.2.0", + "simple-concat": "^1.0.0", + "xtend": "^4.0.1" + }, + "bin": { + "undeclared-identifiers": "bin.js" + } + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url-trim": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-trim/-/url-trim-1.0.0.tgz", + "integrity": "sha512-2BggHF+3B6Ni6k57/CAwKPLOLPi3eNx6roldQw8cP+tqsa5v3k3lUvb2EygoXBdJxeGOnvODwFGXtCbTxG7psw==", + "dev": true, + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/watchify/-/watchify-4.0.0.tgz", + "integrity": "sha512-2Z04dxwoOeNxa11qzWumBTgSAohTC0+ScuY7XMenPnH+W2lhTcpEOJP4g2EIG/SWeLadPk47x++Yh+8BqPM/lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.0", + "browserify": "^17.0.0", + "chokidar": "^3.4.0", + "defined": "^1.0.0", + "outpipe": "^1.1.0", + "through2": "^4.0.2", + "xtend": "^4.0.2" + }, + "bin": { + "watchify": "bin/cmd.js" + }, + "engines": { + "node": ">= 8.10.0" + } + }, + "node_modules/watchify-middleware": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/watchify-middleware/-/watchify-middleware-1.9.1.tgz", + "integrity": "sha512-vjD5S1cTJC99ZLvq61AGiR+Es+4Oloo3mTzPvAPArlBlq8w2IJ1qtQheRgf26ihqjZ3qW/IfgLeqxWOyHorHbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "concat-stream": "^1.5.0", + "debounce": "^1.0.0", + "events": "^1.0.2", + "object-assign": "^4.0.1", + "strip-ansi": "^3.0.0", + "watchify": "^4.0.0" + } + }, + "node_modules/watchify-middleware/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/watchify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/watchify/node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8d9942f --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "graph-explorer", + "version": "1.0.0", + "description": "a hierarchical file tree explorer, but with the ability to not just expand sub entries, but also multiple super entries", + "main": "lib/main.js", + "directories": { + "lib": "lib" + }, + "scripts": { + "start": "budo web/boot.js:bundle.js --open --live", + "build": "browserify web/boot.js -o bundle.js", + "start:act": "budo web/boot.js:bundle.js --dir ./ --live --open", + "build:act": "browserify web/boot.js > bundle.js" + }, + "devDependencies": { + "browserify": "^17.0.1", + "budo": "^11.8.4" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ddroid/graph-explorer.git" + }, + "keywords": [], + "author": "ddroid", + "license": "ISC", + "type": "commonjs", + "bugs": { + "url": "https://github.com/ddroid/graph-explorer/issues" + }, + "homepage": "https://github.com/ddroid/graph-explorer#readme" +} diff --git a/web/boot.js b/web/boot.js new file mode 100644 index 0000000..6749e09 --- /dev/null +++ b/web/boot.js @@ -0,0 +1,19 @@ +const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' +const init_url = location.hash === '#dev' ? 'web/init.js' : prefix + 'src/node_modules/init.js' +const args = arguments + +const has_save = location.hash.includes('#save') +const fetch_opts = has_save ? {} : { cache: 'no-store' } + +if (!has_save) { + localStorage.clear() +} + +fetch(init_url, fetch_opts).then(res => res.text()).then(async source => { + const module = { exports: {} } + const f = new Function('module', 'require', source) + f(module, require) + const init = module.exports + await init(args, prefix) + require('./page') // or whatever is otherwise the main entry of our project +}) diff --git a/web/page.js b/web/page.js new file mode 100644 index 0000000..4490dbc --- /dev/null +++ b/web/page.js @@ -0,0 +1,86 @@ +const STATE = require('../lib/STATE') +const statedb = STATE(__filename) +const { sdb } = statedb(fallback_module) + +/****************************************************************************** + PAGE +******************************************************************************/ +const app = require('../lib/graph_explorer') +const sheet = new CSSStyleSheet() +config().then(() => boot({ sid: '' })) + +async function config() { + const path = path => new URL(`../src/node_modules/${path}`, `file://${__dirname}`).href.slice(8) + const html = document.documentElement + const meta = document.createElement('meta') + const font = 'https://fonts.googleapis.com/css?family=Nunito:300,400,700,900|Slackey&display=swap' + const loadFont = `` + html.setAttribute('lang', 'en') + meta.setAttribute('name', 'viewport') + meta.setAttribute('content', 'width=device-width,initial-scale=1.0') + // @TODO: use font api and cache to avoid re-downloading the font data every time + document.head.append(meta) + document.head.innerHTML += loadFont + document.adoptedStyleSheets = [sheet] + await document.fonts.ready // @TODO: investigate why there is a FOUC +} +/****************************************************************************** + PAGE BOOT +******************************************************************************/ +async function boot(opts) { + // ---------------------------------------- + // ID + JSON STATE + // ---------------------------------------- + const on = { + theme: inject + } + const { drive } = sdb + + const subs = await sdb.watch(onbatch, on) + + // ---------------------------------------- + // TEMPLATE + // ---------------------------------------- + const el = document.body + const shopts = { mode: 'closed' } + const shadow = el.attachShadow(shopts) + shadow.adoptedStyleSheets = [sheet] + // ---------------------------------------- + // ELEMENTS + // ---------------------------------------- + { // desktop + shadow.append(await app(subs[0])) + } + // ---------------------------------------- + // INIT + // ---------------------------------------- + + async function onbatch(batch) { + for (const {type, paths} of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + on[type] && on[type](data) + } + } +} +async function inject(data) { + sheet.replaceSync(data.join('\n')) +} + +function fallback_module () { + return { + _: { + '../lib/graph_explorer': { + $: '', + 0: '', + mapping: { + 'style': 'style', + 'entries': 'entries' + } + } + }, + drive: { + 'theme/': { 'style.css': { raw: "body { font-family: 'system-ui'; }" } }, + 'lang/': {} + } + } +} \ No newline at end of file From a6e720eabd60d519260e1e4b76975249f3c9113f Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 13 Jul 2025 19:35:05 +0500 Subject: [PATCH 002/130] changed structure for graph-explorer --- lib/{graph_explorer => }/entries.json | 0 lib/{graph_explorer => }/graph.txt | 0 lib/{graph_explorer => }/graph_explorer.js | 2 +- lib/main.js | 1 - lib/{graph_explorer => }/package.json | 0 package.json | 2 +- 6 files changed, 2 insertions(+), 3 deletions(-) rename lib/{graph_explorer => }/entries.json (100%) rename lib/{graph_explorer => }/graph.txt (100%) rename lib/{graph_explorer => }/graph_explorer.js (99%) delete mode 100644 lib/main.js rename lib/{graph_explorer => }/package.json (100%) diff --git a/lib/graph_explorer/entries.json b/lib/entries.json similarity index 100% rename from lib/graph_explorer/entries.json rename to lib/entries.json diff --git a/lib/graph_explorer/graph.txt b/lib/graph.txt similarity index 100% rename from lib/graph_explorer/graph.txt rename to lib/graph.txt diff --git a/lib/graph_explorer/graph_explorer.js b/lib/graph_explorer.js similarity index 99% rename from lib/graph_explorer/graph_explorer.js rename to lib/graph_explorer.js index 672d335..4eac8f6 100644 --- a/lib/graph_explorer/graph_explorer.js +++ b/lib/graph_explorer.js @@ -1,4 +1,4 @@ -const STATE = require('../STATE') +const STATE = require('./STATE') const statedb = STATE(__filename) const { get } = statedb(fallback_module) diff --git a/lib/main.js b/lib/main.js deleted file mode 100644 index ee21e6e..0000000 --- a/lib/main.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./graph_explorer') diff --git a/lib/graph_explorer/package.json b/lib/package.json similarity index 100% rename from lib/graph_explorer/package.json rename to lib/package.json diff --git a/package.json b/package.json index 8d9942f..d7f8a31 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "graph-explorer", "version": "1.0.0", "description": "a hierarchical file tree explorer, but with the ability to not just expand sub entries, but also multiple super entries", - "main": "lib/main.js", + "main": "lib/graph_explorer.js", "directories": { "lib": "lib" }, From 980f9fc324620096bcebbfecf0ef7e718f001006 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 13 Jul 2025 21:10:23 +0500 Subject: [PATCH 003/130] fixed upscroll bug --- bundle.js | 25 +++++++++++++------------ lib/graph_explorer.js | 19 ++++++++++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/bundle.js b/bundle.js index 0524df8..b602750 100644 --- a/bundle.js +++ b/bundle.js @@ -2,7 +2,7 @@ },{}],2:[function(require,module,exports){ (function (__filename){(function (){ -const STATE = require('../STATE') +const STATE = require('./STATE') const statedb = STATE(__filename) const { get } = statedb(fallback_module) @@ -22,13 +22,14 @@ async function graph_explorer(opts) { const el = document.createElement('div') el.className = 'graph-explorer-wrapper' - el.onscroll = () => { - vertical_scroll_value = el.scrollTop - horizontal_scroll_value = el.scrollLeft - } const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = `
` const container = shadow.querySelector('.graph-container') + container.onscroll = () => { + vertical_scroll_value = container.scrollTop + horizontal_scroll_value = container.scrollLeft + console.log('scroll', vertical_scroll_value, horizontal_scroll_value) + } let all_entries = {} let view = [] @@ -46,7 +47,7 @@ async function graph_explorer(opts) { bottom_sentinel.className = 'sentinel' const observer = new IntersectionObserver(handle_sentinel_intersection, { - root: el, + root: container, threshold: 0 }) @@ -133,8 +134,8 @@ async function graph_explorer(opts) { render_next_chunk() requestAnimationFrame(() => { - el.scrollTop = new_scroll_top - el.scrollLeft = old_scroll_left + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left }) } @@ -264,11 +265,11 @@ async function graph_explorer(opts) { fragment.appendChild(create_node(view[i])) } const old_scroll_height = container.scrollHeight - const old_scroll_top = el.scrollTop + const old_scroll_top = container.scrollTop container.insertBefore(fragment, top_sentinel.nextSibling) start_index = prev_start + container.scrollTop = old_scroll_top + (container.scrollHeight - old_scroll_height) cleanup_dom(true) - el.scrollTop = old_scroll_top + (container.scrollHeight - old_scroll_height) } function cleanup_dom(is_scrolling_up) { @@ -429,8 +430,8 @@ function fallback_module() { } } -}).call(this)}).call(this,"/lib/graph_explorer/graph_explorer.js") -},{"../STATE":1}],3:[function(require,module,exports){ +}).call(this)}).call(this,"/lib/graph_explorer.js") +},{"./STATE":1}],3:[function(require,module,exports){ const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' const init_url = location.hash === '#dev' ? 'web/init.js' : prefix + 'src/node_modules/init.js' const args = arguments diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 4eac8f6..d95a8aa 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -18,13 +18,14 @@ async function graph_explorer(opts) { const el = document.createElement('div') el.className = 'graph-explorer-wrapper' - el.onscroll = () => { - vertical_scroll_value = el.scrollTop - horizontal_scroll_value = el.scrollLeft - } const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = `
` const container = shadow.querySelector('.graph-container') + container.onscroll = () => { + vertical_scroll_value = container.scrollTop + horizontal_scroll_value = container.scrollLeft + console.log('scroll', vertical_scroll_value, horizontal_scroll_value) + } let all_entries = {} let view = [] @@ -42,7 +43,7 @@ async function graph_explorer(opts) { bottom_sentinel.className = 'sentinel' const observer = new IntersectionObserver(handle_sentinel_intersection, { - root: el, + root: container, threshold: 0 }) @@ -129,8 +130,8 @@ async function graph_explorer(opts) { render_next_chunk() requestAnimationFrame(() => { - el.scrollTop = new_scroll_top - el.scrollLeft = old_scroll_left + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left }) } @@ -260,11 +261,11 @@ async function graph_explorer(opts) { fragment.appendChild(create_node(view[i])) } const old_scroll_height = container.scrollHeight - const old_scroll_top = el.scrollTop + const old_scroll_top = container.scrollTop container.insertBefore(fragment, top_sentinel.nextSibling) start_index = prev_start + container.scrollTop = old_scroll_top + (container.scrollHeight - old_scroll_height) cleanup_dom(true) - el.scrollTop = old_scroll_top + (container.scrollHeight - old_scroll_height) } function cleanup_dom(is_scrolling_up) { From 179cdaf94f05329759b39f6a99bb773de7aa1f3b Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 15 Jul 2025 20:01:49 +0500 Subject: [PATCH 004/130] fixed the 2hubs bug --- bundle.js | 40 ++++++++++++++++++++++++++-------------- lib/graph_explorer.js | 40 ++++++++++++++++++++++++++-------------- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/bundle.js b/bundle.js index b602750..4777b10 100644 --- a/bundle.js +++ b/bundle.js @@ -151,11 +151,10 @@ async function graph_explorer(opts) { instance_states, all_entries }) { - const instance_path = `${parent_instance_path}|${base_path}` const entry = all_entries[base_path] if (!entry) return [] - + if (!instance_states[instance_path]) { instance_states[instance_path] = { expanded_subs: false, @@ -163,15 +162,15 @@ async function graph_explorer(opts) { } } const state = instance_states[instance_path] + const is_hub_on_top = (base_path === all_entries[parent_base_path]?.hubs?.[0]) || (base_path === '/') const children_pipe_trail = [...parent_pipe_trail] let last_pipe = null - if (depth > 0) { if (is_hub) { last_pipe = [...parent_pipe_trail] if (is_last_sub) { children_pipe_trail.pop() - children_pipe_trail.push(is_last_sub) + children_pipe_trail.push(true) last_pipe.pop() last_pipe.push(true) if (is_first_hub) { @@ -179,16 +178,21 @@ async function graph_explorer(opts) { last_pipe.push(false) } } + if (is_hub_on_top && !is_last_sub) { + last_pipe.pop() + last_pipe.push(true) + children_pipe_trail.pop() + children_pipe_trail.push(true) + } if (is_first_hub) { children_pipe_trail.pop() children_pipe_trail.push(false) } } - children_pipe_trail.push(!is_last_sub || is_hub) + children_pipe_trail.push(is_hub_on_top || !is_last_sub) } let current_view = [] - const is_hub_on_top = (base_path === all_entries[parent_base_path]?.hubs?.[0]) || (base_path === '/') if (state.expanded_hubs && entry.hubs) { entry.hubs.forEach((hub_path, i, arr) => { current_view = current_view.concat( @@ -214,7 +218,8 @@ async function graph_explorer(opts) { depth, is_last_sub, is_hub, - pipe_trail: (is_hub && is_last_sub) ? last_pipe : parent_pipe_trail + pipe_trail: ((is_hub && is_last_sub) || (is_hub && is_hub_on_top)) ? last_pipe : parent_pipe_trail, + is_hub_on_top }) if (state.expanded_subs && entry.subs) { @@ -289,13 +294,20 @@ async function graph_explorer(opts) { } } - function get_prefix(is_last_sub, has_subs, state, is_hub) { + function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { const { expanded_subs, expanded_hubs } = state if (is_hub) { - if (expanded_subs && expanded_hubs) return '┌┼' - if (expanded_subs) return '┌┬' - if (expanded_hubs) return '┌┴' - return '┌─' + if (is_hub_on_top) { + if (expanded_subs && expanded_hubs) return '┌┼' + if (expanded_subs) return '┌┬' + if (expanded_hubs) return '┌┴' + return '┌─' + } else { + if (expanded_subs && expanded_hubs) return '├┼' + if (expanded_subs) return '├┬' + if (expanded_hubs) return '├┴' + return '├─' + } } else if (is_last_sub) { if (expanded_subs && expanded_hubs) return '└┼' if (expanded_subs) return '└┬' @@ -309,7 +321,7 @@ async function graph_explorer(opts) { } } - function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail }) { + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { const entry = all_entries[base_path] const state = instance_states[instance_path] const el = document.createElement('div') @@ -335,7 +347,7 @@ async function graph_explorer(opts) { return el } - const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub) + const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) const pipe_html = pipe_trail.map(should_pipe => `${should_pipe ? '│' : ' '}`).join('') const prefix_class = (!has_hubs || base_path !== '/') ? 'prefix clickable' : 'prefix' diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index d95a8aa..04daaa5 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -147,11 +147,10 @@ async function graph_explorer(opts) { instance_states, all_entries }) { - const instance_path = `${parent_instance_path}|${base_path}` const entry = all_entries[base_path] if (!entry) return [] - + if (!instance_states[instance_path]) { instance_states[instance_path] = { expanded_subs: false, @@ -159,15 +158,15 @@ async function graph_explorer(opts) { } } const state = instance_states[instance_path] + const is_hub_on_top = (base_path === all_entries[parent_base_path]?.hubs?.[0]) || (base_path === '/') const children_pipe_trail = [...parent_pipe_trail] let last_pipe = null - if (depth > 0) { if (is_hub) { last_pipe = [...parent_pipe_trail] if (is_last_sub) { children_pipe_trail.pop() - children_pipe_trail.push(is_last_sub) + children_pipe_trail.push(true) last_pipe.pop() last_pipe.push(true) if (is_first_hub) { @@ -175,16 +174,21 @@ async function graph_explorer(opts) { last_pipe.push(false) } } + if (is_hub_on_top && !is_last_sub) { + last_pipe.pop() + last_pipe.push(true) + children_pipe_trail.pop() + children_pipe_trail.push(true) + } if (is_first_hub) { children_pipe_trail.pop() children_pipe_trail.push(false) } } - children_pipe_trail.push(!is_last_sub || is_hub) + children_pipe_trail.push(is_hub_on_top || !is_last_sub) } let current_view = [] - const is_hub_on_top = (base_path === all_entries[parent_base_path]?.hubs?.[0]) || (base_path === '/') if (state.expanded_hubs && entry.hubs) { entry.hubs.forEach((hub_path, i, arr) => { current_view = current_view.concat( @@ -210,7 +214,8 @@ async function graph_explorer(opts) { depth, is_last_sub, is_hub, - pipe_trail: (is_hub && is_last_sub) ? last_pipe : parent_pipe_trail + pipe_trail: ((is_hub && is_last_sub) || (is_hub && is_hub_on_top)) ? last_pipe : parent_pipe_trail, + is_hub_on_top }) if (state.expanded_subs && entry.subs) { @@ -285,13 +290,20 @@ async function graph_explorer(opts) { } } - function get_prefix(is_last_sub, has_subs, state, is_hub) { + function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { const { expanded_subs, expanded_hubs } = state if (is_hub) { - if (expanded_subs && expanded_hubs) return '┌┼' - if (expanded_subs) return '┌┬' - if (expanded_hubs) return '┌┴' - return '┌─' + if (is_hub_on_top) { + if (expanded_subs && expanded_hubs) return '┌┼' + if (expanded_subs) return '┌┬' + if (expanded_hubs) return '┌┴' + return '┌─' + } else { + if (expanded_subs && expanded_hubs) return '├┼' + if (expanded_subs) return '├┬' + if (expanded_hubs) return '├┴' + return '├─' + } } else if (is_last_sub) { if (expanded_subs && expanded_hubs) return '└┼' if (expanded_subs) return '└┬' @@ -305,7 +317,7 @@ async function graph_explorer(opts) { } } - function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail }) { + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { const entry = all_entries[base_path] const state = instance_states[instance_path] const el = document.createElement('div') @@ -331,7 +343,7 @@ async function graph_explorer(opts) { return el } - const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub) + const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) const pipe_html = pipe_trail.map(should_pipe => `${should_pipe ? '│' : ' '}`).join('') const prefix_class = (!has_hubs || base_path !== '/') ? 'prefix clickable' : 'prefix' From f80d9fde67d90c2451ab5157f761f9ffdb4a2ba8 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 15 Jul 2025 20:46:49 +0500 Subject: [PATCH 005/130] Added Static Position of list on expanding --- bundle.js | 75 +++++++++++++++++++++++++------------------ lib/graph_explorer.js | 75 +++++++++++++++++++++++++------------------ 2 files changed, 86 insertions(+), 64 deletions(-) diff --git a/bundle.js b/bundle.js index 4777b10..34dd95d 100644 --- a/bundle.js +++ b/bundle.js @@ -28,7 +28,6 @@ async function graph_explorer(opts) { container.onscroll = () => { vertical_scroll_value = container.scrollTop horizontal_scroll_value = container.scrollLeft - console.log('scroll', vertical_scroll_value, horizontal_scroll_value) } let all_entries = {} @@ -43,8 +42,6 @@ async function graph_explorer(opts) { const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') - top_sentinel.className = 'sentinel' - bottom_sentinel.className = 'sentinel' const observer = new IntersectionObserver(handle_sentinel_intersection, { root: container, @@ -87,6 +84,10 @@ async function graph_explorer(opts) { const old_scroll_top = vertical_scroll_value const old_scroll_left = horizontal_scroll_value + const old_focal_index = focal_instance_path + ? old_view.findIndex(node => node.instance_path === focal_instance_path) + : -1 + view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -98,41 +99,46 @@ async function graph_explorer(opts) { all_entries }) - let focal_index = -1 - if (focal_instance_path) { - focal_index = view.findIndex( - node => node.instance_path === focal_instance_path - ) - } - if (focal_index === -1) { - focal_index = Math.floor(old_scroll_top / node_height) - } + const new_focal_index = focal_instance_path + ? view.findIndex(node => node.instance_path === focal_instance_path) + : -1 - const old_focal_node = old_view[focal_index] let new_scroll_top = old_scroll_top - if (old_focal_node) { - const old_focal_instance_path = old_focal_node.instance_path - const new_focal_index = view.findIndex( - node => node.instance_path === old_focal_instance_path - ) - if (new_focal_index !== -1) { - const scroll_diff = (new_focal_index - focal_index) * node_height - new_scroll_top = old_scroll_top + scroll_diff + if (focal_instance_path && old_focal_index !== -1 && new_focal_index !== -1) { + const scroll_diff = (new_focal_index - old_focal_index) * node_height + new_scroll_top = old_scroll_top + scroll_diff + } else { + const old_top_node_index = Math.floor(old_scroll_top / node_height) + const old_top_node = old_view[old_top_node_index] + if (old_top_node) { + const new_top_node_index = view.findIndex(node => node.instance_path === old_top_node.instance_path) + if (new_top_node_index !== -1) { + new_scroll_top = new_top_node_index * node_height + } } } - start_index = Math.max(0, focal_index - Math.floor(chunk_size / 2)) - end_index = start_index + const render_anchor_index = Math.max(0, Math.floor(new_scroll_top / node_height)) + start_index = Math.max(0, render_anchor_index - chunk_size) + end_index = Math.min(view.length, render_anchor_index + chunk_size) + + const fragment = document.createDocumentFragment() + for (let i = start_index; i < end_index; i++) { + fragment.appendChild(create_node(view[i])) + } container.replaceChildren() container.appendChild(top_sentinel) + container.appendChild(fragment) container.appendChild(bottom_sentinel) + + top_sentinel.style.height = `${start_index * node_height}px` + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + observer.observe(top_sentinel) observer.observe(bottom_sentinel) - render_next_chunk() - requestAnimationFrame(() => { container.scrollTop = new_scroll_top container.scrollLeft = old_scroll_left @@ -259,6 +265,7 @@ async function graph_explorer(opts) { } container.insertBefore(fragment, bottom_sentinel) end_index = next_end + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` cleanup_dom(false) } @@ -269,28 +276,33 @@ async function graph_explorer(opts) { for (let i = prev_start; i < start_index; i++) { fragment.appendChild(create_node(view[i])) } - const old_scroll_height = container.scrollHeight - const old_scroll_top = container.scrollTop container.insertBefore(fragment, top_sentinel.nextSibling) start_index = prev_start - container.scrollTop = old_scroll_top + (container.scrollHeight - old_scroll_height) + top_sentinel.style.height = `${start_index * node_height}px` cleanup_dom(true) } function cleanup_dom(is_scrolling_up) { const rendered_count = end_index - start_index - if (rendered_count < max_rendered_nodes) return + if (rendered_count <= max_rendered_nodes) return + const to_remove_count = rendered_count - max_rendered_nodes if (is_scrolling_up) { for (let i = 0; i < to_remove_count; i++) { - bottom_sentinel.previousElementSibling.remove() + if (bottom_sentinel.previousElementSibling && bottom_sentinel.previousElementSibling !== top_sentinel) { + bottom_sentinel.previousElementSibling.remove() + } } end_index -= to_remove_count + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` } else { for (let i = 0; i < to_remove_count; i++) { - top_sentinel.nextElementSibling.remove() + if (top_sentinel.nextElementSibling && top_sentinel.nextElementSibling !== bottom_sentinel) { + top_sentinel.nextElementSibling.remove() + } } start_index += to_remove_count + top_sentinel.style.height = `${start_index * node_height}px` } } @@ -433,7 +445,6 @@ function fallback_module() { .node.type-css-file > .icon::before { content: '🎨'; } .node.type-json-file > .icon::before { content: '📝'; } .node.type-file > .icon::before { content: '📄'; } - .sentinel { height: 1px; } ` } } diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 04daaa5..22b7f59 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -24,7 +24,6 @@ async function graph_explorer(opts) { container.onscroll = () => { vertical_scroll_value = container.scrollTop horizontal_scroll_value = container.scrollLeft - console.log('scroll', vertical_scroll_value, horizontal_scroll_value) } let all_entries = {} @@ -39,8 +38,6 @@ async function graph_explorer(opts) { const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') - top_sentinel.className = 'sentinel' - bottom_sentinel.className = 'sentinel' const observer = new IntersectionObserver(handle_sentinel_intersection, { root: container, @@ -83,6 +80,10 @@ async function graph_explorer(opts) { const old_scroll_top = vertical_scroll_value const old_scroll_left = horizontal_scroll_value + const old_focal_index = focal_instance_path + ? old_view.findIndex(node => node.instance_path === focal_instance_path) + : -1 + view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -94,41 +95,46 @@ async function graph_explorer(opts) { all_entries }) - let focal_index = -1 - if (focal_instance_path) { - focal_index = view.findIndex( - node => node.instance_path === focal_instance_path - ) - } - if (focal_index === -1) { - focal_index = Math.floor(old_scroll_top / node_height) - } + const new_focal_index = focal_instance_path + ? view.findIndex(node => node.instance_path === focal_instance_path) + : -1 - const old_focal_node = old_view[focal_index] let new_scroll_top = old_scroll_top - if (old_focal_node) { - const old_focal_instance_path = old_focal_node.instance_path - const new_focal_index = view.findIndex( - node => node.instance_path === old_focal_instance_path - ) - if (new_focal_index !== -1) { - const scroll_diff = (new_focal_index - focal_index) * node_height - new_scroll_top = old_scroll_top + scroll_diff + if (focal_instance_path && old_focal_index !== -1 && new_focal_index !== -1) { + const scroll_diff = (new_focal_index - old_focal_index) * node_height + new_scroll_top = old_scroll_top + scroll_diff + } else { + const old_top_node_index = Math.floor(old_scroll_top / node_height) + const old_top_node = old_view[old_top_node_index] + if (old_top_node) { + const new_top_node_index = view.findIndex(node => node.instance_path === old_top_node.instance_path) + if (new_top_node_index !== -1) { + new_scroll_top = new_top_node_index * node_height + } } } - start_index = Math.max(0, focal_index - Math.floor(chunk_size / 2)) - end_index = start_index + const render_anchor_index = Math.max(0, Math.floor(new_scroll_top / node_height)) + start_index = Math.max(0, render_anchor_index - chunk_size) + end_index = Math.min(view.length, render_anchor_index + chunk_size) + + const fragment = document.createDocumentFragment() + for (let i = start_index; i < end_index; i++) { + fragment.appendChild(create_node(view[i])) + } container.replaceChildren() container.appendChild(top_sentinel) + container.appendChild(fragment) container.appendChild(bottom_sentinel) + + top_sentinel.style.height = `${start_index * node_height}px` + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + observer.observe(top_sentinel) observer.observe(bottom_sentinel) - render_next_chunk() - requestAnimationFrame(() => { container.scrollTop = new_scroll_top container.scrollLeft = old_scroll_left @@ -255,6 +261,7 @@ async function graph_explorer(opts) { } container.insertBefore(fragment, bottom_sentinel) end_index = next_end + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` cleanup_dom(false) } @@ -265,28 +272,33 @@ async function graph_explorer(opts) { for (let i = prev_start; i < start_index; i++) { fragment.appendChild(create_node(view[i])) } - const old_scroll_height = container.scrollHeight - const old_scroll_top = container.scrollTop container.insertBefore(fragment, top_sentinel.nextSibling) start_index = prev_start - container.scrollTop = old_scroll_top + (container.scrollHeight - old_scroll_height) + top_sentinel.style.height = `${start_index * node_height}px` cleanup_dom(true) } function cleanup_dom(is_scrolling_up) { const rendered_count = end_index - start_index - if (rendered_count < max_rendered_nodes) return + if (rendered_count <= max_rendered_nodes) return + const to_remove_count = rendered_count - max_rendered_nodes if (is_scrolling_up) { for (let i = 0; i < to_remove_count; i++) { - bottom_sentinel.previousElementSibling.remove() + if (bottom_sentinel.previousElementSibling && bottom_sentinel.previousElementSibling !== top_sentinel) { + bottom_sentinel.previousElementSibling.remove() + } } end_index -= to_remove_count + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` } else { for (let i = 0; i < to_remove_count; i++) { - top_sentinel.nextElementSibling.remove() + if (top_sentinel.nextElementSibling && top_sentinel.nextElementSibling !== bottom_sentinel) { + top_sentinel.nextElementSibling.remove() + } } start_index += to_remove_count + top_sentinel.style.height = `${start_index * node_height}px` } } @@ -429,7 +441,6 @@ function fallback_module() { .node.type-css-file > .icon::before { content: '🎨'; } .node.type-json-file > .icon::before { content: '📝'; } .node.type-file > .icon::before { content: '📄'; } - .sentinel { height: 1px; } ` } } From 5dc7c9569f8efb292bee93058e18c2a431b0b3d9 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 15 Jul 2025 21:24:13 +0500 Subject: [PATCH 006/130] Added basic docs --- README.md | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d85c5fe..4a5ad56 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,118 @@ -# graph-explorer -a hierarchical file tree explorer, but with the ability to not just expand sub entries, but also multiple super entries +# `graph-explorer` + +A lightweight, high-performance frontend component for rendering and exploring interactive, hierarchical graph data. It uses a virtual scrolling technique to efficiently display large datasets with thousands of nodes without sacrificing performance. + +## Features + +- **Virtual Scrolling:** Renders only the visible nodes, ensuring smooth scrolling and interaction even with very large graphs. +- **Interactive Exploration:** Allows users to expand and collapse both hierarchical children (`subs`) and related connections (`hubs`). +- **Dynamic Data Loading:** Listens for data updates and re-renders the view accordingly. +## Usage + +Require the `graph_explorer` function and call it with a configuration object. It returns a DOM element that can be appended to the page. + +```javascript +const graph_explorer = require('./graph_explorer.js') + +// Provide `opts` and `Protocol` as parameters +const graphElement = await graph_explorer(opts, protocol); + +// Append the element to your application's body or another container +document.body.appendChild(graphElement); +``` + +## Drive + +The component expects to receive data through datasets in drive. It responds to two types of messages: `entries` and `style`. + +### 1. `entries` + +The `entries` message provides the core graph data. It should be an object where each key is a unique path identifier for a node, and the value is an object describing that node's properties. + +**Example `entries` Object:** + +```json +{ + "/": { + "name": "Root Directory", + "type": "root", + "subs": ["/src", "/assets", "/README.md"], + "hubs": ["/LICENSE"] + }, + "/src": { + "name": "src", + "type": "folder", + "subs": ["/src/index.js", "/src/styles.css"] + }, + "/assets": { + "name": "assets", + "type": "folder", + "subs": [] + }, + "/README.md": { + "name": "README.md", + "type": "file" + }, + "/LICENSE": { + "name": "LICENSE", + "type": "file" + }, + "/src/index.js": { + "name": "index.js", + "type": "js-file" + }, + "/src/styles.css": { + "name": "styles.css", + "type": "css-file" + } +} +``` + +**Node Properties:** + +- `name` (String): The display name of the node. +- `type` (String): A type identifier used for styling (e.g., `folder`, `file`, `js-file`). The component will add a `type-` class to the node element. And these classes can be used to append `.icon::before` css property to show an icon before name. +- `subs` (Array): An array of paths to child nodes. An empty array indicates no children. +- `hubs` (Array): An array of paths to related, non-hierarchical nodes. + +### 2. `style` + +The `style` message provides a string of CSS content that will be injected directly into the component's Shadow DOM. This allows for full control over the visual appearance of the graph, nodes, icons, and tree lines. + +**Example `style` Data:** + +```css +.graph-container { + color: #abb2bf; + background-color: #282c34; + padding: 10px; + height: 100vh; + overflow: auto; +} +.node { + display: flex; + align-items: center; + white-space: nowrap; + cursor: default; + height: 22px; + /* + This height is crucial for virtual scrolling calculations and it should match the height of javascript variable i.e + + const node_height = 22 + + */ +} +.clickable { + cursor: pointer; +} +.node.type-folder > .icon::before { content: '📁'; } +.node.type-js-file > .icon::before { content: '📜'; } +/* these use `type` to inject icon */ +/* ... more custom styles */ +``` + +## How It Works + +The component maintains a complete `view` array representing the flattened, visible graph structure. It uses an `IntersectionObserver` with two sentinel elements at the top and bottom of the scrollable container. + +When a sentinel becomes visible, the component dynamically renders the next or previous "chunk" of nodes and removes nodes that have scrolled far out of view. This ensures that the number of DOM elements remains small and constant, providing excellent performance regardless of the total number of nodes in the graph. From bca5a536cfb056f265a0382876681bf0483758ae Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 15 Jul 2025 21:33:44 +0500 Subject: [PATCH 007/130] fixed mistake --- lib/graph_explorer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 22b7f59..8f5249d 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -191,7 +191,7 @@ async function graph_explorer(opts) { children_pipe_trail.push(false) } } - children_pipe_trail.push(is_hub_on_top || !is_last_sub) + children_pipe_trail.push(is_hub || !is_last_sub) } let current_view = [] From 306f4e49085711da4857c7a4632c5f63ecc57d7a Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 18 Jul 2025 14:52:21 +0500 Subject: [PATCH 008/130] added reset to wand --- lib/graph_explorer.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 8f5249d..a611de9 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -329,6 +329,20 @@ async function graph_explorer(opts) { } } + function reset() { + vertical_scroll_value = 0 + horizontal_scroll_value = 0 + view = [] + Object.keys(instance_states).forEach(k => delete instance_states[k]) + const root_path = '/' + if (all_entries[root_path]) { + instance_states[root_path] = { expanded_subs: true, expanded_hubs: false } + build_and_render_view() + } else { + container.replaceChildren() + } + } + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { const entry = all_entries[base_path] const state = instance_states[instance_path] @@ -345,9 +359,10 @@ async function graph_explorer(opts) { if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state - const prefix_symbol = expanded_subs ? '🪄┬' : '🪄─' + const prefix_symbol = expanded_subs ? '┬' : '─' const prefix_class = has_subs ? 'prefix clickable' : 'prefix' - el.innerHTML = `${prefix_symbol}/🌐` + el.innerHTML = `
🪄
${prefix_symbol}/🌐` + el.querySelector('.wand').onclick = reset if (has_subs) { el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) el.querySelector('.name').onclick = () => toggle_subs(instance_path) From e43f792f817db85b54442d9d87443956e5b39eb4 Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 18 Jul 2025 14:53:07 +0500 Subject: [PATCH 009/130] quick fixes according to feedback --- .gitignore | 3 +- index.html | 18 +- package-lock.json | 3710 --------------------------------------------- web/page.js | 4 +- 4 files changed, 12 insertions(+), 3723 deletions(-) delete mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 7991f82..8bb47fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules/* /package-lock.json -/npm-debug.log \ No newline at end of file +/npm-debug.log +/package-lock.json \ No newline at end of file diff --git a/index.html b/index.html index 2a9c6ee..1f24e14 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,10 @@ - - - - - - Graph-Explorer - - - - + + + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 72a04af..0000000 --- a/package-lock.json +++ /dev/null @@ -1,3710 +0,0 @@ -{ - "name": "graph-explorer", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "graph-explorer", - "version": "1.0.0", - "license": "ISC", - "devDependencies": { - "browserify": "^17.0.1", - "budo": "^11.8.4" - } - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-regex": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", - "integrity": "sha512-sGwIGMjhYdW26/IhwK2gkWWI8DRCVO6uj3hYgHT+zD+QL1pa37tM3ujhyfcJIYSbsxp7Gxhy7zrRW/1AHm4BmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", - "integrity": "sha512-f2PKUkN5QngiSemowa6Mrk9MPCdtFiOSmibjZ+j1qhLGHHYsqZwmBMRF3IRMVXo8sybDqx2fJl2d/8OphBoWkA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/assert": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz", - "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "object.assign": "^4.1.4", - "util": "^0.10.4" - } - }, - "node_modules/assert/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" - }, - "node_modules/assert/node_modules/util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "2.0.3" - } - }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/bole": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bole/-/bole-2.0.0.tgz", - "integrity": "sha512-/7aKG4IlOS3Gv15ccrSFiXwXjm5vPAFNfkncSNLYLpq4bH9m9N8Ef4QiTu8NN1lld8p7V1q8l8kOkbExEOv94A==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": ">=1.0.1 <1.1.0-0", - "individual": ">=3.0.0 <3.1.0-0", - "json-stringify-safe": ">=5.0.0 <5.1.0-0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/browser-pack": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", - "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "combine-source-map": "~0.8.0", - "defined": "^1.0.0", - "JSONStream": "^1.0.3", - "safe-buffer": "^5.1.1", - "through2": "^2.0.0", - "umd": "^3.0.0" - }, - "bin": { - "browser-pack": "bin/cmd.js" - } - }, - "node_modules/browser-resolve": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", - "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.17.0" - } - }, - "node_modules/browserify": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-17.0.1.tgz", - "integrity": "sha512-pxhT00W3ylMhCHwG5yfqtZjNnFuX5h2IJdaBfSo4ChaaBsIp9VLrEMQ1bHV+Xr1uLPXuNDDM1GlJkjli0qkRsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert": "^1.4.0", - "browser-pack": "^6.0.1", - "browser-resolve": "^2.0.0", - "browserify-zlib": "~0.2.0", - "buffer": "~5.2.1", - "cached-path-relative": "^1.0.0", - "concat-stream": "^1.6.0", - "console-browserify": "^1.1.0", - "constants-browserify": "~1.0.0", - "crypto-browserify": "^3.0.0", - "defined": "^1.0.0", - "deps-sort": "^2.0.1", - "domain-browser": "^1.2.0", - "duplexer2": "~0.1.2", - "events": "^3.0.0", - "glob": "^7.1.0", - "hasown": "^2.0.0", - "htmlescape": "^1.1.0", - "https-browserify": "^1.0.0", - "inherits": "~2.0.1", - "insert-module-globals": "^7.2.1", - "JSONStream": "^1.0.3", - "labeled-stream-splicer": "^2.0.0", - "mkdirp-classic": "^0.5.2", - "module-deps": "^6.2.3", - "os-browserify": "~0.3.0", - "parents": "^1.0.1", - "path-browserify": "^1.0.0", - "process": "~0.11.0", - "punycode": "^1.3.2", - "querystring-es3": "~0.2.0", - "read-only-stream": "^2.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.1.4", - "shasum-object": "^1.0.0", - "shell-quote": "^1.6.1", - "stream-browserify": "^3.0.0", - "stream-http": "^3.0.0", - "string_decoder": "^1.1.1", - "subarg": "^1.0.0", - "syntax-error": "^1.1.1", - "through2": "^2.0.0", - "timers-browserify": "^1.0.1", - "tty-browserify": "0.0.1", - "url": "~0.11.0", - "util": "~0.12.0", - "vm-browserify": "^1.0.0", - "xtend": "^4.0.0" - }, - "bin": { - "browserify": "bin/cmd.js" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", - "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^5.2.1", - "randombytes": "^2.1.0", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", - "dev": true, - "license": "ISC", - "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.5", - "hash-base": "~3.0", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.7", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pako": "~1.0.5" - } - }, - "node_modules/budo": { - "version": "11.8.4", - "resolved": "https://registry.npmjs.org/budo/-/budo-11.8.4.tgz", - "integrity": "sha512-drUnbk6nAuzQ4xmyWjajvUb85ZhGduXpblY9guD776HmPqWoShlEE8XiYX145v7+ZoqznnShI3QHAObK9YSWnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bole": "^2.0.0", - "browserify": "^17.0.0", - "chokidar": "^3.5.2", - "connect-pushstate": "^1.1.0", - "escape-html": "^1.0.3", - "events": "^1.0.2", - "garnish": "^5.0.0", - "get-ports": "^1.0.2", - "inject-lr-script": "^2.1.0", - "internal-ip": "^3.0.1", - "micromatch": "^4.0.5", - "on-finished": "^2.3.0", - "on-headers": "^1.0.1", - "once": "^1.3.2", - "opn": "^3.0.2", - "path-is-absolute": "^1.0.1", - "pem": "^1.13.2", - "reload-css": "^1.0.0", - "resolve": "^1.1.6", - "serve-static": "^1.10.0", - "simple-html-index": "^1.4.0", - "stacked": "^1.1.1", - "stdout-stream": "^1.4.0", - "strip-ansi": "^3.0.0", - "subarg": "^1.0.0", - "term-color": "^1.0.1", - "url-trim": "^1.0.0", - "watchify-middleware": "^1.9.1", - "ws": "^6.2.2", - "xtend": "^4.0.0" - }, - "bin": { - "budo": "bin/cmd.js" - } - }, - "node_modules/budo/node_modules/events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cached-path-relative": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz", - "integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chalk": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", - "integrity": "sha512-bIKA54hP8iZhyDT81TOsJiQvR1gW+ZYSXFaZUAvoD4wCHdbHY2actmpTE4x344ZlFqHbvoxKOaESULTZN2gstg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^1.1.0", - "escape-string-regexp": "^1.0.0", - "has-ansi": "^0.1.0", - "strip-ansi": "^0.3.0", - "supports-color": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chalk/node_modules/strip-ansi": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", - "integrity": "sha512-DerhZL7j6i6/nEnVG0qViKXI0OKouvvpsAiaj7c+LfqZZZxdwZtv8+UiA/w4VUJpT8UzX0pR1dcHOii1GbmruQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^0.2.1" - }, - "bin": { - "strip-ansi": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/cipher-base": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", - "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/combine-source-map": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", - "integrity": "sha512-UlxQ9Vw0b/Bt/KYwCFqdEwsQ1eL8d1gibiFb7lxQJFdvTgc2hIZi6ugsg+kyhzhPV+QEpUiEIwInIAIrgoEkrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "convert-source-map": "~1.1.0", - "inline-source-map": "~0.6.0", - "lodash.memoize": "~3.0.3", - "source-map": "~0.5.3" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/connect-pushstate": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/connect-pushstate/-/connect-pushstate-1.1.0.tgz", - "integrity": "sha512-5p2H2+eXkCiqcSZqZbTh5TLcLsl1wub7VKrRnfHyorC+pxXjF6nfswda4YMluYhGMo+33eR/58weorzSi9uSbA==", - "dev": true, - "license": "MIT" - }, - "node_modules/console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "node_modules/constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/crypto-browserify": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", - "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserify-cipher": "^1.0.1", - "browserify-sign": "^4.2.3", - "create-ecdh": "^4.0.4", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "diffie-hellman": "^5.0.3", - "hash-base": "~3.0.4", - "inherits": "^2.0.4", - "pbkdf2": "^3.1.2", - "public-encrypt": "^4.0.3", - "randombytes": "^2.1.0", - "randomfill": "^1.0.4" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/dash-ast": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", - "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/default-gateway": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz", - "integrity": "sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ==", - "dev": true, - "license": "BSD-2-Clause", - "os": [ - "android", - "darwin", - "freebsd", - "linux", - "openbsd", - "sunos", - "win32" - ], - "dependencies": { - "execa": "^0.10.0", - "ip-regex": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/deps-sort": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", - "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "JSONStream": "^1.0.3", - "shasum-object": "^1.0.0", - "subarg": "^1.0.0", - "through2": "^2.0.0" - }, - "bin": { - "deps-sort": "bin/cmd.js" - } - }, - "node_modules/des.js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", - "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4", - "npm": ">=1.2" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-promisify": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-7.0.0.tgz", - "integrity": "sha512-ginqzK3J90Rd4/Yz7qRrqUeIpe3TwSXTPPZtPne7tGBPeAaQiU8qt4fpKApnxHcq1AwtUdHVg5P77x/yrggG8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "node_modules/from2-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/from2-string/-/from2-string-1.1.0.tgz", - "integrity": "sha512-m8vCh+KnXXXBtfF2VUbiYlQ+nczLcntB0BrtNgpmLkHylhObe9WF1b2LZjBBzrZzA6P4mkEla6ZYQoOUTG8cYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "from2": "^2.0.3" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/garnish": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/garnish/-/garnish-5.2.0.tgz", - "integrity": "sha512-y0qv1q5ylEtbKW08LGDxmhrmyHGIXH2Jfcz3JPKFikMPDQ0mgBIbtOc2R3fXHYOXOfDfDpx1o5G9rMl4jpU0qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^0.5.1", - "minimist": "^1.1.0", - "pad-left": "^2.0.0", - "pad-right": "^0.2.2", - "prettier-bytes": "^1.0.3", - "pretty-ms": "^2.1.0", - "right-now": "^1.0.0", - "split2": "^0.2.1", - "stdout-stream": "^1.4.0", - "url-trim": "^1.0.0" - }, - "bin": { - "garnish": "bin/cmd.js" - } - }, - "node_modules/get-assigned-identifiers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", - "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-ports": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-ports/-/get-ports-1.0.3.tgz", - "integrity": "sha512-XtNFp93OT2wNEX/PkcCJ5+4PR5fxYCK+J2BsfJO8eV7hCYbqROt+8XO6iApJqJ06A2UJMUueDCoJ1Lp5vypuDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "map-limit": "0.0.1" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-ansi": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", - "integrity": "sha512-1YsTg1fk2/6JToQhtZkArMkurq8UoWU1Qe0aR3VUHjgij4nOylSWLWAtBXoZ4/dXOmugfLGm1c+QhuD0JyedFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^0.2.0" - }, - "bin": { - "has-ansi": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/htmlescape": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", - "integrity": "sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/individual": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz", - "integrity": "sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==", - "dev": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/inject-lr-script": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/inject-lr-script/-/inject-lr-script-2.2.0.tgz", - "integrity": "sha512-lFLjCOg2XP8233AiET5vFePo910vhNIkKHDzUptNhc+4Y7dsp/TNBiusUUpaxzaGd6UDHy0Lozfl9AwmteK6DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resp-modifier": "^6.0.0" - } - }, - "node_modules/inline-source-map": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.3.tgz", - "integrity": "sha512-1aVsPEsJWMJq/pdMU61CDlm1URcW702MTB4w9/zUjMus6H/Py8o7g68Pr9D4I6QluWGt/KdmswuRhaA05xVR1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "~0.5.3" - } - }, - "node_modules/insert-module-globals": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", - "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn-node": "^1.5.2", - "combine-source-map": "^0.8.0", - "concat-stream": "^1.6.1", - "is-buffer": "^1.1.0", - "JSONStream": "^1.0.3", - "path-is-absolute": "^1.0.1", - "process": "~0.11.0", - "through2": "^2.0.0", - "undeclared-identifiers": "^1.1.2", - "xtend": "^4.0.0" - }, - "bin": { - "insert-module-globals": "bin/cmd.js" - } - }, - "node_modules/internal-ip": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-3.0.1.tgz", - "integrity": "sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q==", - "dev": true, - "license": "MIT", - "os": [ - "android", - "darwin", - "freebsd", - "linux", - "openbsd", - "sunos", - "win32" - ], - "dependencies": { - "default-gateway": "^2.6.0", - "ipaddr.js": "^1.5.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC" - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ], - "license": "MIT" - }, - "node_modules/JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "license": "(MIT OR Apache-2.0)", - "dependencies": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - }, - "bin": { - "JSONStream": "bin.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/labeled-stream-splicer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", - "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "stream-splicer": "^2.0.0" - } - }, - "node_modules/lodash.memoize": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", - "integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/map-limit": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz", - "integrity": "sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "~1.3.0" - } - }, - "node_modules/map-limit/node_modules/once": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true, - "license": "MIT" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/module-deps": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", - "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "browser-resolve": "^2.0.0", - "cached-path-relative": "^1.0.2", - "concat-stream": "~1.6.0", - "defined": "^1.0.0", - "detective": "^5.2.0", - "duplexer2": "^0.1.2", - "inherits": "^2.0.1", - "JSONStream": "^1.0.3", - "parents": "^1.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.4.0", - "stream-combiner2": "^1.1.1", - "subarg": "^1.0.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - }, - "bin": { - "module-deps": "bin/cmd.js" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/opn": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz", - "integrity": "sha512-YKyQo/aDk+kLY/ChqYx3DMWW8cbxvZDh+7op1oU60TmLHGWFrn2gPaRWihzDhSwCarAESa9G8dNXzjTGfLx8FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/outpipe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", - "integrity": "sha512-BnNY/RwnDrkmQdUa9U+OfN/Y7AWmKuUPCCd+hbRclZnnANvYpO72zp/a6Q4n829hPbdqEac31XCcsvlEvb+rtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shell-quote": "^1.4.2" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pad-left": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pad-left/-/pad-left-2.1.0.tgz", - "integrity": "sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "repeat-string": "^1.5.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pad-right": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", - "integrity": "sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==", - "dev": true, - "license": "MIT", - "dependencies": { - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, - "license": "(MIT AND Zlib)" - }, - "node_modules/parents": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", - "integrity": "sha512-mXKF3xkoUt5td2DoxpLmtOmZvko9VfFpwRwkKDHSNvgmpLAeBo18YDhcPbBzJq+QLCHMbGOfzia2cX4U+0v9Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-platform": "~0.11.15" - } - }, - "node_modules/parse-asn1": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", - "dev": true, - "license": "ISC", - "dependencies": { - "asn1.js": "^4.10.1", - "browserify-aes": "^1.2.0", - "evp_bytestokey": "^1.0.3", - "hash-base": "~3.0", - "pbkdf2": "^3.1.2", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-ms": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz", - "integrity": "sha512-LpH1Cf5EYuVjkBvCDBYvkUPh+iv2bk3FHflxHkpCYT0/FZ1d3N3uJaLiHr4yGuMcFUhv6eAivitTvWZI4B/chg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-platform": { - "version": "0.11.15", - "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", - "integrity": "sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pbkdf2": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", - "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "create-hash": "~1.1.3", - "create-hmac": "^1.1.7", - "ripemd160": "=2.0.1", - "safe-buffer": "^5.2.1", - "sha.js": "^2.4.11", - "to-buffer": "^1.2.0" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/pbkdf2/node_modules/create-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", - "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "sha.js": "^2.4.0" - } - }, - "node_modules/pbkdf2/node_modules/hash-base": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", - "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1" - } - }, - "node_modules/pbkdf2/node_modules/ripemd160": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hash-base": "^2.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/pem": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/pem/-/pem-1.14.8.tgz", - "integrity": "sha512-ZpbOf4dj9/fQg5tQzTqv4jSKJQsK7tPl0pm4/pvPcZVjZcJg7TMfr3PBk6gJH97lnpJDu4e4v8UUqEz5daipCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es6-promisify": "^7.0.0", - "md5": "^2.3.0", - "os-tmpdir": "^1.0.2", - "which": "^2.0.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/pem/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plur": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz", - "integrity": "sha512-qSnKBSZeDY8ApxwhfVIwKwF36KVJqb1/9nzYYq3j3vdwocULCXT8f8fQGkiw1Nk9BGfxiDagEe/pwakA+bOBqw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prettier-bytes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz", - "integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/pretty-ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz", - "integrity": "sha512-H2enpsxzDhuzRl3zeSQpQMirn8dB0Z/gxW96j06tMfTviUWvX14gjKb7qd1gtkUyYhDPuoNe00K5PqNvy2oQNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-finite": "^1.0.1", - "parse-ms": "^1.0.0", - "plur": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/read-only-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", - "integrity": "sha512-3ALe0bjBVZtkdWKIcThYpQCLbBMd/+Tbh2CDSrAIDO3UsZ4Xs+tnyjv2MjCOMMgBG+AsUOeuP1cgtY1INISc8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/readable-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reload-css": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/reload-css/-/reload-css-1.0.2.tgz", - "integrity": "sha512-R4IA5JujRJlvGW5B5qCKrlNgqrrLqsa0uvFaXFWtHcE7dkA9c6c8WbqKpvqQ82hOoWkHjmigZ970jTfsywzMxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "query-string": "^4.2.3" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resp-modifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", - "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", - "dev": true, - "dependencies": { - "debug": "^2.2.0", - "minimatch": "^3.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/right-now": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/right-now/-/right-now-1.0.0.tgz", - "integrity": "sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/sha.js": { - "version": "2.4.12", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", - "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", - "dev": true, - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.0" - }, - "bin": { - "sha.js": "bin.js" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/shasum-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", - "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "fast-safe-stringify": "^2.0.7" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-html-index": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/simple-html-index/-/simple-html-index-1.5.0.tgz", - "integrity": "sha512-+okfICS99jb6y4u584/nIt1dqM74+aTT93GM1JhSvy0IYbj6So7zfuaR7z2TIrt87nNpMRZIS504J+mRvGUBeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "from2-string": "^1.1.0" - } - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split2": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/split2/-/split2-0.2.1.tgz", - "integrity": "sha512-D/oTExYAkC9nWleOCTOyNmAuzfAT/6rHGBA9LIK7FVnGo13CSvrKCUzKenwH6U1s2znY9MqH6v0UQTEDa3vJmg==", - "dev": true, - "license": "ISC", - "dependencies": { - "through2": "~0.6.1" - } - }, - "node_modules/split2/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/split2/node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/split2/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/split2/node_modules/through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } - }, - "node_modules/stacked": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stacked/-/stacked-1.1.1.tgz", - "integrity": "sha512-3Kj9zD4TycXGCeOMPg8yv1lIbwYATx+O8dQ/m1MSaixpOnNwdrS2YgLGwyWUF8zO2Q26LHsAr8KKb88flQESnQ==", - "dev": true - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.1" - } - }, - "node_modules/stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "node_modules/stream-browserify/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "node_modules/stream-http": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", - "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", - "dev": true, - "license": "MIT", - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" - } - }, - "node_modules/stream-http/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/stream-splicer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", - "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.2" - } - }, - "node_modules/strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.1.0" - } - }, - "node_modules/supports-color": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", - "integrity": "sha512-tdCZ28MnM7k7cJDJc7Eq80A9CsRFAAOZUy41npOZCs++qSjfIy7o5Rh46CBk+Dk5FbKJ33X3Tqg4YrV07N5RaA==", - "dev": true, - "license": "MIT", - "bin": { - "supports-color": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/syntax-error": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", - "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn-node": "^1.2.0" - } - }, - "node_modules/term-color": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/term-color/-/term-color-1.0.1.tgz", - "integrity": "sha512-4Ld+sFlAdziaaMabvBU215dxyMotGoz7yN+9GtPE7RhKvzXAmg8tD/nKohJp4v2bMdSsNO3FEIBxFDsXu0Pf8w==", - "dev": true, - "dependencies": { - "ansi-styles": "2.0.1", - "supports-color": "1.3.1" - } - }, - "node_modules/term-color/node_modules/ansi-styles": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.0.1.tgz", - "integrity": "sha512-0zjsXMhnTibRx8YrLgLKb5NvWEcHN/OZEe1NzR8VVrEM6xr7/NyLsoMVelAhaoJhOtpuexaeRGD8MF8Z64+9LQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/term-color/node_modules/supports-color": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz", - "integrity": "sha512-OHbMkscHFRcNWEcW80fYhCrzAjheSIBwJChpFaBqA6zEz53nxumqi6ukciRb/UA0/v2nDNMk28ce/uBbYRDsng==", - "dev": true, - "license": "MIT", - "bin": { - "supports-color": "cli.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/timers-browserify": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha512-PIxwAupJZiYU4JmVZYwXp9FKsHMXb5h0ZEFyuXTAn8WLHOlcij+FEcbrvDsom1o5dr1YggEtFbECvGCW2sT53Q==", - "dev": true, - "dependencies": { - "process": "~0.11.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/to-buffer/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/umd": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", - "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", - "dev": true, - "license": "MIT", - "bin": { - "umd": "bin/cli.js" - } - }, - "node_modules/undeclared-identifiers": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", - "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "acorn-node": "^1.3.0", - "dash-ast": "^1.0.0", - "get-assigned-identifiers": "^1.2.0", - "simple-concat": "^1.0.0", - "xtend": "^4.0.1" - }, - "bin": { - "undeclared-identifiers": "bin.js" - } - }, - "node_modules/url": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", - "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^1.4.1", - "qs": "^6.12.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/url-trim": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-trim/-/url-trim-1.0.0.tgz", - "integrity": "sha512-2BggHF+3B6Ni6k57/CAwKPLOLPi3eNx6roldQw8cP+tqsa5v3k3lUvb2EygoXBdJxeGOnvODwFGXtCbTxG7psw==", - "dev": true, - "license": "MIT" - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/watchify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/watchify/-/watchify-4.0.0.tgz", - "integrity": "sha512-2Z04dxwoOeNxa11qzWumBTgSAohTC0+ScuY7XMenPnH+W2lhTcpEOJP4g2EIG/SWeLadPk47x++Yh+8BqPM/lA==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "^3.1.0", - "browserify": "^17.0.0", - "chokidar": "^3.4.0", - "defined": "^1.0.0", - "outpipe": "^1.1.0", - "through2": "^4.0.2", - "xtend": "^4.0.2" - }, - "bin": { - "watchify": "bin/cmd.js" - }, - "engines": { - "node": ">= 8.10.0" - } - }, - "node_modules/watchify-middleware": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/watchify-middleware/-/watchify-middleware-1.9.1.tgz", - "integrity": "sha512-vjD5S1cTJC99ZLvq61AGiR+Es+4Oloo3mTzPvAPArlBlq8w2IJ1qtQheRgf26ihqjZ3qW/IfgLeqxWOyHorHbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "concat-stream": "^1.5.0", - "debounce": "^1.0.0", - "events": "^1.0.2", - "object-assign": "^4.0.1", - "strip-ansi": "^3.0.0", - "watchify": "^4.0.0" - } - }, - "node_modules/watchify-middleware/node_modules/events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/watchify/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/watchify/node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "3" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-limiter": "~1.0.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - } - } -} diff --git a/web/page.js b/web/page.js index 4490dbc..0e85b75 100644 --- a/web/page.js +++ b/web/page.js @@ -5,7 +5,7 @@ const { sdb } = statedb(fallback_module) /****************************************************************************** PAGE ******************************************************************************/ -const app = require('../lib/graph_explorer') +const app = require('..') const sheet = new CSSStyleSheet() config().then(() => boot({ sid: '' })) @@ -69,7 +69,7 @@ async function inject(data) { function fallback_module () { return { _: { - '../lib/graph_explorer': { + '..': { $: '', 0: '', mapping: { From 896e4c673073fa55c2b2b9d1074264844be4af0b Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 18 Jul 2025 15:31:23 +0500 Subject: [PATCH 010/130] added instance selection --- lib/graph_explorer.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index a611de9..1b26e22 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -10,6 +10,7 @@ async function graph_explorer(opts) { let vertical_scroll_value = 0 let horizontal_scroll_value = 0 + let selected_instance_path = null const on = { entries: on_entries, @@ -349,6 +350,7 @@ async function graph_explorer(opts) { const el = document.createElement('div') el.className = `node type-${entry.type}` el.dataset.instance_path = instance_path + if (instance_path === selected_instance_path) el.classList.add('selected') const has_hubs = entry.hubs && entry.hubs.length > 0 const has_subs = entry.subs && entry.subs.length > 0 @@ -361,12 +363,12 @@ async function graph_explorer(opts) { const { expanded_subs } = state const prefix_symbol = expanded_subs ? '┬' : '─' const prefix_class = has_subs ? 'prefix clickable' : 'prefix' - el.innerHTML = `
🪄
${prefix_symbol}/🌐` + el.innerHTML = `
🪄
${prefix_symbol}/🌐` el.querySelector('.wand').onclick = reset if (has_subs) { el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) - el.querySelector('.name').onclick = () => toggle_subs(instance_path) } + el.querySelector('.name').onclick = () => select_node(instance_path, base_path) return el } @@ -380,13 +382,29 @@ async function graph_explorer(opts) { ${pipe_html} ${prefix_symbol} - ${entry.name} + ${entry.name} ` if(has_hubs && base_path !== '/') el.querySelector('.prefix').onclick = () => toggle_hubs(instance_path) if(has_subs) el.querySelector('.icon').onclick = () => toggle_subs(instance_path) + el.querySelector('.name').onclick = () => select_node(instance_path, base_path) return el } + function select_node(instance_path, base_path) { + if (instance_path === selected_instance_path) { + console.log(`entry ${base_path} selected again aka confirmed`) + return + } + const old_selected_path = selected_instance_path + selected_instance_path = instance_path + if (old_selected_path) { + const old_node = shadow.querySelector(`[data-instance_path="${CSS.escape(old_selected_path)}"]`) + if (old_node) old_node.classList.remove('selected') + } + const new_node = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (new_node) new_node.classList.add('selected') + } + function toggle_subs(instance_path) { const state = instance_states[instance_path] if (state) { @@ -431,6 +449,9 @@ function fallback_module() { cursor: default; height: 22px; /* Important for scroll calculation */ } + .node.selected { + background-color: #3a3f4b; + } .indent { display: flex; } From eb9588bdaaec665581d2acb30ffe0446a07d506f Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 18 Jul 2025 20:29:43 +0500 Subject: [PATCH 011/130] drive runtime vars --- bundle.js | 158 +++++++++++++++++++++++++++++++----------- lib/graph_explorer.js | 135 +++++++++++++++++++++++------------- web/page.js | 3 +- 3 files changed, 208 insertions(+), 88 deletions(-) diff --git a/bundle.js b/bundle.js index 34dd95d..8afdddb 100644 --- a/bundle.js +++ b/bundle.js @@ -14,25 +14,19 @@ async function graph_explorer(opts) { let vertical_scroll_value = 0 let horizontal_scroll_value = 0 - - const on = { - entries: on_entries, - style: inject_style - } - + let selected_instance_path = null + let all_entries = {} + let instance_states = {} + let view = [] + const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = `
` const container = shadow.querySelector('.graph-container') - container.onscroll = () => { - vertical_scroll_value = container.scrollTop - horizontal_scroll_value = container.scrollLeft - } - - let all_entries = {} - let view = [] - const instance_states = {} + + let scroll_update_pending = false + container.onscroll = onscroll let start_index = 0 let end_index = 0 @@ -42,34 +36,74 @@ async function graph_explorer(opts) { const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') - + const observer = new IntersectionObserver(handle_sentinel_intersection, { root: container, threshold: 0 }) - + const on = { + entries: on_entries, + style: inject_style, + runtime: on_runtime + } await sdb.watch(onbatch) - + return el async function onbatch(batch) { for (const { type, paths } of batch) { - const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file ? file.raw : null))) const func = on[type] || fail - func(data, type) + func(data, type, paths) } } function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } + async function update_runtime_state (name, value) { + await drive.put(`runtime/${name}.json`, { raw: JSON.stringify(value) }) + } + + function on_runtime (data, type, paths) { + for (let i = 0; i < paths.length; i++) { + const path = paths[i] + if (data[i] === null) continue + const value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] + if (path.endsWith('vertical_scroll_value.json')) { + vertical_scroll_value = value + container.scrollTop = vertical_scroll_value + } else if (path.endsWith('horizontal_scroll_value.json')) { + horizontal_scroll_value = value + container.scrollLeft = horizontal_scroll_value + } else if (path.endsWith('selected_instance_path.json')) { + const old_selected_path = selected_instance_path + selected_instance_path = value + if (old_selected_path) { + const old_node = shadow.querySelector(`[data-instance_path="${CSS.escape(old_selected_path)}"]`) + if (old_node) old_node.classList.remove('selected') + } + if (selected_instance_path) { + const new_node = shadow.querySelector(`[data-instance_path="${CSS.escape(selected_instance_path)}"]`) + if (new_node) new_node.classList.add('selected') + } + } else if (path.endsWith('instance_states.json')) { + instance_states = value + build_and_render_view() + } + } + } + function on_entries(data) { all_entries = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] const root_path = '/' if (all_entries[root_path]) { - if (!instance_states[root_path]) { - instance_states[root_path] = { expanded_subs: true, expanded_hubs: false } + const root_instance_path = '|/' + if (!instance_states[root_instance_path]) { + instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + update_runtime_state('instance_states', instance_states) + } else { + build_and_render_view() } - build_and_render_view() } } @@ -78,7 +112,21 @@ async function graph_explorer(opts) { sheet.replaceSync(data[0]) shadow.adoptedStyleSheets = [sheet] } - + function onscroll() { + if (scroll_update_pending) return + scroll_update_pending = true + requestAnimationFrame(() => { + if (vertical_scroll_value !== container.scrollTop) { + vertical_scroll_value = container.scrollTop + update_runtime_state('vertical_scroll_value', vertical_scroll_value) + } + if (horizontal_scroll_value !== container.scrollLeft) { + horizontal_scroll_value = container.scrollLeft + update_runtime_state('horizontal_scroll_value', horizontal_scroll_value) + } + scroll_update_pending = false + }) + } function build_and_render_view(focal_instance_path = null) { const old_view = [...view] const old_scroll_top = vertical_scroll_value @@ -195,7 +243,7 @@ async function graph_explorer(opts) { children_pipe_trail.push(false) } } - children_pipe_trail.push(is_hub_on_top || !is_last_sub) + children_pipe_trail.push(is_hub || !is_last_sub) } let current_view = [] @@ -333,12 +381,26 @@ async function graph_explorer(opts) { } } + function reset() { + const root_path = '/' + const root_instance_path = '|/' + const new_instance_states = {} + if (all_entries[root_path]) { + new_instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + } + update_runtime_state('vertical_scroll_value', 0) + update_runtime_state('horizontal_scroll_value', 0) + update_runtime_state('selected_instance_path', null) + update_runtime_state('instance_states', new_instance_states) + } + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { const entry = all_entries[base_path] const state = instance_states[instance_path] const el = document.createElement('div') el.className = `node type-${entry.type}` el.dataset.instance_path = instance_path + if (instance_path === selected_instance_path) el.classList.add('selected') const has_hubs = entry.hubs && entry.hubs.length > 0 const has_subs = entry.subs && entry.subs.length > 0 @@ -349,13 +411,14 @@ async function graph_explorer(opts) { if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state - const prefix_symbol = expanded_subs ? '🪄┬' : '🪄─' + const prefix_symbol = expanded_subs ? '┬' : '─' const prefix_class = has_subs ? 'prefix clickable' : 'prefix' - el.innerHTML = `${prefix_symbol}/🌐` + el.innerHTML = `
🪄
${prefix_symbol}/🌐` + el.querySelector('.wand').onclick = reset if (has_subs) { el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) - el.querySelector('.name').onclick = () => toggle_subs(instance_path) } + el.querySelector('.name').onclick = () => select_node(instance_path, base_path) return el } @@ -369,27 +432,32 @@ async function graph_explorer(opts) { ${pipe_html} ${prefix_symbol} - ${entry.name} + ${entry.name} ` if(has_hubs && base_path !== '/') el.querySelector('.prefix').onclick = () => toggle_hubs(instance_path) if(has_subs) el.querySelector('.icon').onclick = () => toggle_subs(instance_path) + el.querySelector('.name').onclick = () => select_node(instance_path, base_path) return el } + function select_node(instance_path, base_path) { + if (instance_path === selected_instance_path) { + console.log(`entry ${base_path} selected again aka confirmed`) + return + } + update_runtime_state('selected_instance_path', instance_path) + } + function toggle_subs(instance_path) { const state = instance_states[instance_path] - if (state) { - state.expanded_subs = !state.expanded_subs - build_and_render_view(instance_path) - } + state.expanded_subs = !state.expanded_subs + update_runtime_state('instance_states', instance_states) } function toggle_hubs(instance_path) { const state = instance_states[instance_path] - if (state) { - state.expanded_hubs = !state.expanded_hubs - build_and_render_view(instance_path) - } + state.expanded_hubs = !state.expanded_hubs + update_runtime_state('instance_states', instance_states) } } @@ -420,6 +488,9 @@ function fallback_module() { cursor: default; height: 22px; /* Important for scroll calculation */ } + .node.selected { + background-color: #3a3f4b; + } .indent { display: flex; } @@ -447,6 +518,12 @@ function fallback_module() { .node.type-file > .icon::before { content: '📄'; } ` } + }, + 'runtime/': { + 'vertical_scroll_value.json': { raw: '0' }, + 'horizontal_scroll_value.json': { raw: '0' }, + 'selected_instance_path.json': { raw: 'null' }, + 'instance_states.json': { raw: '{}' } } } } @@ -484,7 +561,7 @@ const { sdb } = statedb(fallback_module) /****************************************************************************** PAGE ******************************************************************************/ -const app = require('../lib/graph_explorer') +const app = require('..') const sheet = new CSSStyleSheet() config().then(() => boot({ sid: '' })) @@ -548,12 +625,13 @@ async function inject(data) { function fallback_module () { return { _: { - '../lib/graph_explorer': { + '..': { $: '', 0: '', mapping: { 'style': 'style', - 'entries': 'entries' + 'entries': 'entries', + 'runtime': 'runtime' } } }, @@ -564,4 +642,4 @@ function fallback_module () { } } }).call(this)}).call(this,"/web/page.js","/web") -},{"../lib/STATE":1,"../lib/graph_explorer":2}]},{},[3]); +},{"..":2,"../lib/STATE":1}]},{},[3]); diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 1b26e22..bfe22eb 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -11,25 +11,18 @@ async function graph_explorer(opts) { let vertical_scroll_value = 0 let horizontal_scroll_value = 0 let selected_instance_path = null - - const on = { - entries: on_entries, - style: inject_style - } - + let all_entries = {} + let instance_states = {} + let view = [] + const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = `
` const container = shadow.querySelector('.graph-container') - container.onscroll = () => { - vertical_scroll_value = container.scrollTop - horizontal_scroll_value = container.scrollLeft - } - - let all_entries = {} - let view = [] - const instance_states = {} + + let scroll_update_pending = false + container.onscroll = onscroll let start_index = 0 let end_index = 0 @@ -39,34 +32,74 @@ async function graph_explorer(opts) { const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') - + const observer = new IntersectionObserver(handle_sentinel_intersection, { root: container, threshold: 0 }) - + const on = { + entries: on_entries, + style: inject_style, + runtime: on_runtime + } await sdb.watch(onbatch) - + return el async function onbatch(batch) { for (const { type, paths } of batch) { - const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file ? file.raw : null))) const func = on[type] || fail - func(data, type) + func(data, type, paths) } } function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } + async function update_runtime_state (name, value) { + await drive.put(`runtime/${name}.json`, { raw: JSON.stringify(value) }) + } + + function on_runtime (data, type, paths) { + for (let i = 0; i < paths.length; i++) { + const path = paths[i] + if (data[i] === null) continue + const value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] + if (path.endsWith('vertical_scroll_value.json')) { + vertical_scroll_value = value + container.scrollTop = vertical_scroll_value + } else if (path.endsWith('horizontal_scroll_value.json')) { + horizontal_scroll_value = value + container.scrollLeft = horizontal_scroll_value + } else if (path.endsWith('selected_instance_path.json')) { + const old_selected_path = selected_instance_path + selected_instance_path = value + if (old_selected_path) { + const old_node = shadow.querySelector(`[data-instance_path="${CSS.escape(old_selected_path)}"]`) + if (old_node) old_node.classList.remove('selected') + } + if (selected_instance_path) { + const new_node = shadow.querySelector(`[data-instance_path="${CSS.escape(selected_instance_path)}"]`) + if (new_node) new_node.classList.add('selected') + } + } else if (path.endsWith('instance_states.json')) { + instance_states = value + build_and_render_view() + } + } + } + function on_entries(data) { all_entries = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] const root_path = '/' if (all_entries[root_path]) { - if (!instance_states[root_path]) { - instance_states[root_path] = { expanded_subs: true, expanded_hubs: false } + const root_instance_path = '|/' + if (!instance_states[root_instance_path]) { + instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + update_runtime_state('instance_states', instance_states) + } else { + build_and_render_view() } - build_and_render_view() } } @@ -75,7 +108,21 @@ async function graph_explorer(opts) { sheet.replaceSync(data[0]) shadow.adoptedStyleSheets = [sheet] } - + function onscroll() { + if (scroll_update_pending) return + scroll_update_pending = true + requestAnimationFrame(() => { + if (vertical_scroll_value !== container.scrollTop) { + vertical_scroll_value = container.scrollTop + update_runtime_state('vertical_scroll_value', vertical_scroll_value) + } + if (horizontal_scroll_value !== container.scrollLeft) { + horizontal_scroll_value = container.scrollLeft + update_runtime_state('horizontal_scroll_value', horizontal_scroll_value) + } + scroll_update_pending = false + }) + } function build_and_render_view(focal_instance_path = null) { const old_view = [...view] const old_scroll_top = vertical_scroll_value @@ -331,17 +378,16 @@ async function graph_explorer(opts) { } function reset() { - vertical_scroll_value = 0 - horizontal_scroll_value = 0 - view = [] - Object.keys(instance_states).forEach(k => delete instance_states[k]) const root_path = '/' + const root_instance_path = '|/' + const new_instance_states = {} if (all_entries[root_path]) { - instance_states[root_path] = { expanded_subs: true, expanded_hubs: false } - build_and_render_view() - } else { - container.replaceChildren() + new_instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } } + update_runtime_state('vertical_scroll_value', 0) + update_runtime_state('horizontal_scroll_value', 0) + update_runtime_state('selected_instance_path', null) + update_runtime_state('instance_states', new_instance_states) } function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { @@ -395,30 +441,19 @@ async function graph_explorer(opts) { console.log(`entry ${base_path} selected again aka confirmed`) return } - const old_selected_path = selected_instance_path - selected_instance_path = instance_path - if (old_selected_path) { - const old_node = shadow.querySelector(`[data-instance_path="${CSS.escape(old_selected_path)}"]`) - if (old_node) old_node.classList.remove('selected') - } - const new_node = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) - if (new_node) new_node.classList.add('selected') + update_runtime_state('selected_instance_path', instance_path) } function toggle_subs(instance_path) { const state = instance_states[instance_path] - if (state) { - state.expanded_subs = !state.expanded_subs - build_and_render_view(instance_path) - } + state.expanded_subs = !state.expanded_subs + update_runtime_state('instance_states', instance_states) } function toggle_hubs(instance_path) { const state = instance_states[instance_path] - if (state) { - state.expanded_hubs = !state.expanded_hubs - build_and_render_view(instance_path) - } + state.expanded_hubs = !state.expanded_hubs + update_runtime_state('instance_states', instance_states) } } @@ -479,6 +514,12 @@ function fallback_module() { .node.type-file > .icon::before { content: '📄'; } ` } + }, + 'runtime/': { + 'vertical_scroll_value.json': { raw: '0' }, + 'horizontal_scroll_value.json': { raw: '0' }, + 'selected_instance_path.json': { raw: 'null' }, + 'instance_states.json': { raw: '{}' } } } } diff --git a/web/page.js b/web/page.js index 0e85b75..163d890 100644 --- a/web/page.js +++ b/web/page.js @@ -74,7 +74,8 @@ function fallback_module () { 0: '', mapping: { 'style': 'style', - 'entries': 'entries' + 'entries': 'entries', + 'runtime': 'runtime' } } }, From 9e6bb82aa2a87c4031f13c14a4219bd3bee3b0cf Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 22 Jul 2025 14:33:12 +0500 Subject: [PATCH 012/130] drive runtime vars part 2 --- lib/graph_explorer.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index bfe22eb..0ca93c0 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -7,7 +7,7 @@ module.exports = graph_explorer async function graph_explorer(opts) { const { sdb } = await get(opts.sid) const { drive } = sdb - + await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) let vertical_scroll_value = 0 let horizontal_scroll_value = 0 let selected_instance_path = null @@ -57,7 +57,7 @@ async function graph_explorer(opts) { function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } async function update_runtime_state (name, value) { - await drive.put(`runtime/${name}.json`, { raw: JSON.stringify(value) }) + await drive.put(`runtime/${name}.json`, JSON.stringify(value)) } function on_runtime (data, type, paths) { @@ -65,13 +65,9 @@ async function graph_explorer(opts) { const path = paths[i] if (data[i] === null) continue const value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] - if (path.endsWith('vertical_scroll_value.json')) { - vertical_scroll_value = value - container.scrollTop = vertical_scroll_value - } else if (path.endsWith('horizontal_scroll_value.json')) { - horizontal_scroll_value = value - container.scrollLeft = horizontal_scroll_value - } else if (path.endsWith('selected_instance_path.json')) { + if (path.endsWith('vertical_scroll_value.json')) vertical_scroll_value = value + else if (path.endsWith('horizontal_scroll_value.json')) horizontal_scroll_value = value + else if (path.endsWith('selected_instance_path.json')) { const old_selected_path = selected_instance_path selected_instance_path = value if (old_selected_path) { @@ -96,7 +92,6 @@ async function graph_explorer(opts) { const root_instance_path = '|/' if (!instance_states[root_instance_path]) { instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } - update_runtime_state('instance_states', instance_states) } else { build_and_render_view() } From dc974a9509cd8e9950c81a5e847aed1a57bc36be Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 22 Jul 2025 15:03:30 +0500 Subject: [PATCH 013/130] multiple selection while holding ctrl --- bundle.js | 64 ++++++++++++++++++++++--------------------- lib/graph_explorer.js | 51 +++++++++++++++++++--------------- 2 files changed, 62 insertions(+), 53 deletions(-) diff --git a/bundle.js b/bundle.js index 8afdddb..0b888bd 100644 --- a/bundle.js +++ b/bundle.js @@ -11,10 +11,10 @@ module.exports = graph_explorer async function graph_explorer(opts) { const { sdb } = await get(opts.sid) const { drive } = sdb - + await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) let vertical_scroll_value = 0 let horizontal_scroll_value = 0 - let selected_instance_path = null + let selected_instance_paths = [] let all_entries = {} let instance_states = {} let view = [] @@ -61,7 +61,7 @@ async function graph_explorer(opts) { function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } async function update_runtime_state (name, value) { - await drive.put(`runtime/${name}.json`, { raw: JSON.stringify(value) }) + await drive.put(`runtime/${name}.json`, JSON.stringify(value)) } function on_runtime (data, type, paths) { @@ -69,23 +69,15 @@ async function graph_explorer(opts) { const path = paths[i] if (data[i] === null) continue const value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] - if (path.endsWith('vertical_scroll_value.json')) { - vertical_scroll_value = value - container.scrollTop = vertical_scroll_value - } else if (path.endsWith('horizontal_scroll_value.json')) { - horizontal_scroll_value = value - container.scrollLeft = horizontal_scroll_value - } else if (path.endsWith('selected_instance_path.json')) { - const old_selected_path = selected_instance_path - selected_instance_path = value - if (old_selected_path) { - const old_node = shadow.querySelector(`[data-instance_path="${CSS.escape(old_selected_path)}"]`) - if (old_node) old_node.classList.remove('selected') - } - if (selected_instance_path) { - const new_node = shadow.querySelector(`[data-instance_path="${CSS.escape(selected_instance_path)}"]`) - if (new_node) new_node.classList.add('selected') - } + if (path.endsWith('vertical_scroll_value.json')) vertical_scroll_value = value + else if (path.endsWith('horizontal_scroll_value.json')) horizontal_scroll_value = value + else if (path.endsWith('selected_instance_paths.json')) { + selected_instance_paths = value || [] + shadow.querySelectorAll('.node.selected').forEach(n => n.classList.remove('selected')) + selected_instance_paths.forEach(p => { + const node = shadow.querySelector(`[data-instance_path="${CSS.escape(p)}"]`) + if (node) node.classList.add('selected') + }) } else if (path.endsWith('instance_states.json')) { instance_states = value build_and_render_view() @@ -100,7 +92,6 @@ async function graph_explorer(opts) { const root_instance_path = '|/' if (!instance_states[root_instance_path]) { instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } - update_runtime_state('instance_states', instance_states) } else { build_and_render_view() } @@ -390,7 +381,7 @@ async function graph_explorer(opts) { } update_runtime_state('vertical_scroll_value', 0) update_runtime_state('horizontal_scroll_value', 0) - update_runtime_state('selected_instance_path', null) + update_runtime_state('selected_instance_paths', []) update_runtime_state('instance_states', new_instance_states) } @@ -400,7 +391,7 @@ async function graph_explorer(opts) { const el = document.createElement('div') el.className = `node type-${entry.type}` el.dataset.instance_path = instance_path - if (instance_path === selected_instance_path) el.classList.add('selected') + if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') const has_hubs = entry.hubs && entry.hubs.length > 0 const has_subs = entry.subs && entry.subs.length > 0 @@ -418,7 +409,7 @@ async function graph_explorer(opts) { if (has_subs) { el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) } - el.querySelector('.name').onclick = () => select_node(instance_path, base_path) + el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) return el } @@ -436,16 +427,27 @@ async function graph_explorer(opts) { ` if(has_hubs && base_path !== '/') el.querySelector('.prefix').onclick = () => toggle_hubs(instance_path) if(has_subs) el.querySelector('.icon').onclick = () => toggle_subs(instance_path) - el.querySelector('.name').onclick = () => select_node(instance_path, base_path) + el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) return el } - function select_node(instance_path, base_path) { - if (instance_path === selected_instance_path) { - console.log(`entry ${base_path} selected again aka confirmed`) - return + function select_node(ev, instance_path, base_path) { + if (ev.ctrlKey) { + const new_selected_paths = [...selected_instance_paths] + const index = new_selected_paths.indexOf(instance_path) + if (index > -1) { + new_selected_paths.splice(index, 1) + } else { + new_selected_paths.push(instance_path) + } + update_runtime_state('selected_instance_paths', new_selected_paths) + } else { + if (selected_instance_paths.length === 1 && selected_instance_paths[0] === instance_path) { + console.log(`entry ${base_path} selected again aka confirmed`) + return + } + update_runtime_state('selected_instance_paths', [instance_path]) } - update_runtime_state('selected_instance_path', instance_path) } function toggle_subs(instance_path) { @@ -522,7 +524,7 @@ function fallback_module() { 'runtime/': { 'vertical_scroll_value.json': { raw: '0' }, 'horizontal_scroll_value.json': { raw: '0' }, - 'selected_instance_path.json': { raw: 'null' }, + 'selected_instance_paths.json': { raw: '[]' }, 'instance_states.json': { raw: '{}' } } } diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 0ca93c0..4f95525 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -10,7 +10,7 @@ async function graph_explorer(opts) { await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) let vertical_scroll_value = 0 let horizontal_scroll_value = 0 - let selected_instance_path = null + let selected_instance_paths = [] let all_entries = {} let instance_states = {} let view = [] @@ -67,17 +67,13 @@ async function graph_explorer(opts) { const value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] if (path.endsWith('vertical_scroll_value.json')) vertical_scroll_value = value else if (path.endsWith('horizontal_scroll_value.json')) horizontal_scroll_value = value - else if (path.endsWith('selected_instance_path.json')) { - const old_selected_path = selected_instance_path - selected_instance_path = value - if (old_selected_path) { - const old_node = shadow.querySelector(`[data-instance_path="${CSS.escape(old_selected_path)}"]`) - if (old_node) old_node.classList.remove('selected') - } - if (selected_instance_path) { - const new_node = shadow.querySelector(`[data-instance_path="${CSS.escape(selected_instance_path)}"]`) - if (new_node) new_node.classList.add('selected') - } + else if (path.endsWith('selected_instance_paths.json')) { + selected_instance_paths = value || [] + shadow.querySelectorAll('.node.selected').forEach(n => n.classList.remove('selected')) + selected_instance_paths.forEach(p => { + const node = shadow.querySelector(`[data-instance_path="${CSS.escape(p)}"]`) + if (node) node.classList.add('selected') + }) } else if (path.endsWith('instance_states.json')) { instance_states = value build_and_render_view() @@ -381,7 +377,7 @@ async function graph_explorer(opts) { } update_runtime_state('vertical_scroll_value', 0) update_runtime_state('horizontal_scroll_value', 0) - update_runtime_state('selected_instance_path', null) + update_runtime_state('selected_instance_paths', []) update_runtime_state('instance_states', new_instance_states) } @@ -391,7 +387,7 @@ async function graph_explorer(opts) { const el = document.createElement('div') el.className = `node type-${entry.type}` el.dataset.instance_path = instance_path - if (instance_path === selected_instance_path) el.classList.add('selected') + if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') const has_hubs = entry.hubs && entry.hubs.length > 0 const has_subs = entry.subs && entry.subs.length > 0 @@ -409,7 +405,7 @@ async function graph_explorer(opts) { if (has_subs) { el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) } - el.querySelector('.name').onclick = () => select_node(instance_path, base_path) + el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) return el } @@ -427,16 +423,27 @@ async function graph_explorer(opts) { ` if(has_hubs && base_path !== '/') el.querySelector('.prefix').onclick = () => toggle_hubs(instance_path) if(has_subs) el.querySelector('.icon').onclick = () => toggle_subs(instance_path) - el.querySelector('.name').onclick = () => select_node(instance_path, base_path) + el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) return el } - function select_node(instance_path, base_path) { - if (instance_path === selected_instance_path) { - console.log(`entry ${base_path} selected again aka confirmed`) - return + function select_node(ev, instance_path, base_path) { + if (ev.ctrlKey) { + const new_selected_paths = [...selected_instance_paths] + const index = new_selected_paths.indexOf(instance_path) + if (index > -1) { + new_selected_paths.splice(index, 1) + } else { + new_selected_paths.push(instance_path) + } + update_runtime_state('selected_instance_paths', new_selected_paths) + } else { + if (selected_instance_paths.length === 1 && selected_instance_paths[0] === instance_path) { + console.log(`entry ${base_path} selected again aka confirmed`) + return + } + update_runtime_state('selected_instance_paths', [instance_path]) } - update_runtime_state('selected_instance_path', instance_path) } function toggle_subs(instance_path) { @@ -513,7 +520,7 @@ function fallback_module() { 'runtime/': { 'vertical_scroll_value.json': { raw: '0' }, 'horizontal_scroll_value.json': { raw: '0' }, - 'selected_instance_path.json': { raw: 'null' }, + 'selected_instance_paths.json': { raw: '[]' }, 'instance_states.json': { raw: '{}' } } } From 64f450d7b472a8ebf4c2e31f7fd5c91a0795d045 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 22 Jul 2025 15:37:53 +0500 Subject: [PATCH 014/130] added confirmed and uncofirmed instances --- bundle.js | 69 +++++++++++++++++++++++++++++++++++++++---- lib/graph_explorer.js | 69 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 126 insertions(+), 12 deletions(-) diff --git a/bundle.js b/bundle.js index 0b888bd..d390eb8 100644 --- a/bundle.js +++ b/bundle.js @@ -15,6 +15,7 @@ async function graph_explorer(opts) { let vertical_scroll_value = 0 let horizontal_scroll_value = 0 let selected_instance_paths = [] + let confirmed_instance_paths = [] let all_entries = {} let instance_states = {} let view = [] @@ -72,12 +73,15 @@ async function graph_explorer(opts) { if (path.endsWith('vertical_scroll_value.json')) vertical_scroll_value = value else if (path.endsWith('horizontal_scroll_value.json')) horizontal_scroll_value = value else if (path.endsWith('selected_instance_paths.json')) { + const old_paths = [...selected_instance_paths] selected_instance_paths = value || [] - shadow.querySelectorAll('.node.selected').forEach(n => n.classList.remove('selected')) - selected_instance_paths.forEach(p => { - const node = shadow.querySelector(`[data-instance_path="${CSS.escape(p)}"]`) - if (node) node.classList.add('selected') - }) + const changed_paths = [...new Set([...old_paths, ...selected_instance_paths])] + changed_paths.forEach(re_render_node) + } else if (path.endsWith('confirmed_selected.json')) { + const old_paths = [...confirmed_instance_paths] + confirmed_instance_paths = value || [] + const changed_paths = [...new Set([...old_paths, ...confirmed_instance_paths])] + changed_paths.forEach(re_render_node) } else if (path.endsWith('instance_states.json')) { instance_states = value build_and_render_view() @@ -382,9 +386,21 @@ async function graph_explorer(opts) { update_runtime_state('vertical_scroll_value', 0) update_runtime_state('horizontal_scroll_value', 0) update_runtime_state('selected_instance_paths', []) + update_runtime_state('confirmed_selected', []) update_runtime_state('instance_states', new_instance_states) } + function re_render_node (instance_path) { + const node_data = view.find(n => n.instance_path === instance_path) + if (node_data) { + const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (old_node_el) { + const new_node_el = create_node(node_data) + old_node_el.replaceWith(new_node_el) + } + } + } + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { const entry = all_entries[base_path] const state = instance_states[instance_path] @@ -392,6 +408,7 @@ async function graph_explorer(opts) { el.className = `node type-${entry.type}` el.dataset.instance_path = instance_path if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') + if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') const has_hubs = entry.hubs && entry.hubs.length > 0 const has_subs = entry.subs && entry.subs.length > 0 @@ -428,9 +445,41 @@ async function graph_explorer(opts) { if(has_hubs && base_path !== '/') el.querySelector('.prefix').onclick = () => toggle_hubs(instance_path) if(has_subs) el.querySelector('.icon').onclick = () => toggle_subs(instance_path) el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) + + if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { + const checkbox_div = document.createElement('div') + checkbox_div.className = 'confirm-wrapper' + const is_confirmed = confirmed_instance_paths.includes(instance_path) + checkbox_div.innerHTML = `` + checkbox_div.querySelector('input').onchange = (ev) => handle_confirm(ev, instance_path) + el.appendChild(checkbox_div) + } + return el } + function handle_confirm(ev, instance_path) { + const is_checked = ev.target.checked + const new_selected_paths = [...selected_instance_paths] + const new_confirmed_paths = [...confirmed_instance_paths] + + if (is_checked) { + const idx = new_selected_paths.indexOf(instance_path) + if (idx > -1) new_selected_paths.splice(idx, 1) + if (!new_confirmed_paths.includes(instance_path)) { + new_confirmed_paths.push(instance_path) + } + } else { + if (!new_selected_paths.includes(instance_path)) { + new_selected_paths.push(instance_path) + } + const idx = new_confirmed_paths.indexOf(instance_path) + if (idx > -1) new_confirmed_paths.splice(idx, 1) + } + update_runtime_state('selected_instance_paths', new_selected_paths) + update_runtime_state('confirmed_selected', new_confirmed_paths) + } + function select_node(ev, instance_path, base_path) { if (ev.ctrlKey) { const new_selected_paths = [...selected_instance_paths] @@ -491,7 +540,14 @@ function fallback_module() { height: 22px; /* Important for scroll calculation */ } .node.selected { - background-color: #3a3f4b; + background-color: #776346; + } + .node.confirmed { + background-color: #774346; + } + .confirm-wrapper { + margin-left: auto; + padding-left: 10px; } .indent { display: flex; @@ -525,6 +581,7 @@ function fallback_module() { 'vertical_scroll_value.json': { raw: '0' }, 'horizontal_scroll_value.json': { raw: '0' }, 'selected_instance_paths.json': { raw: '[]' }, + 'confirmed_selected.json': { raw: '[]' }, 'instance_states.json': { raw: '{}' } } } diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 4f95525..7e6c13c 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -11,6 +11,7 @@ async function graph_explorer(opts) { let vertical_scroll_value = 0 let horizontal_scroll_value = 0 let selected_instance_paths = [] + let confirmed_instance_paths = [] let all_entries = {} let instance_states = {} let view = [] @@ -68,12 +69,15 @@ async function graph_explorer(opts) { if (path.endsWith('vertical_scroll_value.json')) vertical_scroll_value = value else if (path.endsWith('horizontal_scroll_value.json')) horizontal_scroll_value = value else if (path.endsWith('selected_instance_paths.json')) { + const old_paths = [...selected_instance_paths] selected_instance_paths = value || [] - shadow.querySelectorAll('.node.selected').forEach(n => n.classList.remove('selected')) - selected_instance_paths.forEach(p => { - const node = shadow.querySelector(`[data-instance_path="${CSS.escape(p)}"]`) - if (node) node.classList.add('selected') - }) + const changed_paths = [...new Set([...old_paths, ...selected_instance_paths])] + changed_paths.forEach(re_render_node) + } else if (path.endsWith('confirmed_selected.json')) { + const old_paths = [...confirmed_instance_paths] + confirmed_instance_paths = value || [] + const changed_paths = [...new Set([...old_paths, ...confirmed_instance_paths])] + changed_paths.forEach(re_render_node) } else if (path.endsWith('instance_states.json')) { instance_states = value build_and_render_view() @@ -378,9 +382,21 @@ async function graph_explorer(opts) { update_runtime_state('vertical_scroll_value', 0) update_runtime_state('horizontal_scroll_value', 0) update_runtime_state('selected_instance_paths', []) + update_runtime_state('confirmed_selected', []) update_runtime_state('instance_states', new_instance_states) } + function re_render_node (instance_path) { + const node_data = view.find(n => n.instance_path === instance_path) + if (node_data) { + const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (old_node_el) { + const new_node_el = create_node(node_data) + old_node_el.replaceWith(new_node_el) + } + } + } + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { const entry = all_entries[base_path] const state = instance_states[instance_path] @@ -388,6 +404,7 @@ async function graph_explorer(opts) { el.className = `node type-${entry.type}` el.dataset.instance_path = instance_path if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') + if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') const has_hubs = entry.hubs && entry.hubs.length > 0 const has_subs = entry.subs && entry.subs.length > 0 @@ -424,9 +441,41 @@ async function graph_explorer(opts) { if(has_hubs && base_path !== '/') el.querySelector('.prefix').onclick = () => toggle_hubs(instance_path) if(has_subs) el.querySelector('.icon').onclick = () => toggle_subs(instance_path) el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) + + if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { + const checkbox_div = document.createElement('div') + checkbox_div.className = 'confirm-wrapper' + const is_confirmed = confirmed_instance_paths.includes(instance_path) + checkbox_div.innerHTML = `` + checkbox_div.querySelector('input').onchange = (ev) => handle_confirm(ev, instance_path) + el.appendChild(checkbox_div) + } + return el } + function handle_confirm(ev, instance_path) { + const is_checked = ev.target.checked + const new_selected_paths = [...selected_instance_paths] + const new_confirmed_paths = [...confirmed_instance_paths] + + if (is_checked) { + const idx = new_selected_paths.indexOf(instance_path) + if (idx > -1) new_selected_paths.splice(idx, 1) + if (!new_confirmed_paths.includes(instance_path)) { + new_confirmed_paths.push(instance_path) + } + } else { + if (!new_selected_paths.includes(instance_path)) { + new_selected_paths.push(instance_path) + } + const idx = new_confirmed_paths.indexOf(instance_path) + if (idx > -1) new_confirmed_paths.splice(idx, 1) + } + update_runtime_state('selected_instance_paths', new_selected_paths) + update_runtime_state('confirmed_selected', new_confirmed_paths) + } + function select_node(ev, instance_path, base_path) { if (ev.ctrlKey) { const new_selected_paths = [...selected_instance_paths] @@ -487,7 +536,14 @@ function fallback_module() { height: 22px; /* Important for scroll calculation */ } .node.selected { - background-color: #3a3f4b; + background-color: #776346; + } + .node.confirmed { + background-color: #774346; + } + .confirm-wrapper { + margin-left: auto; + padding-left: 10px; } .indent { display: flex; @@ -521,6 +577,7 @@ function fallback_module() { 'vertical_scroll_value.json': { raw: '0' }, 'horizontal_scroll_value.json': { raw: '0' }, 'selected_instance_paths.json': { raw: '[]' }, + 'confirmed_selected.json': { raw: '[]' }, 'instance_states.json': { raw: '{}' } } } From 9a681c7fba888516870590856f23219e162fe9a4 Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 24 Jul 2025 16:38:07 +0500 Subject: [PATCH 015/130] fixed scroll persistance and removed all of static entry code --- instructions.md | 639 ++++++++++++++++++++++++++++++++++++++++++ lib/graph_explorer.js | 41 ++- 2 files changed, 655 insertions(+), 25 deletions(-) create mode 100644 instructions.md diff --git a/instructions.md b/instructions.md new file mode 100644 index 0000000..1f36761 --- /dev/null +++ b/instructions.md @@ -0,0 +1,639 @@ +Hey! I'm a vanilla JavaScript developer who works on an open-source project named [UI components](https://github.com/ddroid/ui-components), Where I create ui-components for a decentralized app called **'Theme Widget'**. It's not my own project, I'm just a contributor. I want to tell you about how each Nodejs component is created so you can help me with creating some. + +# Guide to create modules: + +Here we would discuss the rules and a deep explanation of the steps for creating a module. + +## Here are some rules: +- We use StandardJS. +- We use snake_case and try to keep variable names concise. +- We use CommonJS. Which consist of `require` keyword for importing external modules. +- Furthermore, we use shadow DOM. +- We handle all the element creation through JavaScript and try to maximize the use of template literals while creating HTML elements. +- We try to keep the code modular by dividing the functionality into multiple functioned which are defined/placed always under the return statement of parent function and are used above, obviously. +- Likewise, we don't use `btn.addEventListner()` syntax. Instead, we use `btn.onclick = onclick_function` etc. +- We don't use JavaScript `classes` or `this` keyword. +- We use a module called `STATE` for state management and persistent browser storage. I Will discuss it in detail in a bit. +- We use bundlers `budo/browserify`. Our index.html is a small file that just includes the bundle.js script. +- Try to keep code as short as possible without compromising the readability and reusability. + +# Structure Explained: +Here is the structure that I would show you step by step. +## `example.js` +First 3 lines for each module are always same: +```js +const STATE = require('STATE') +const statedb = STATE(__filename) +const { sdb, get } = statedb(fallback_module) +``` +As you can see here we just require the `STATE` module and then execute it to create a state database function. This is then passed with a `fallback` function. + +You dont need to get deep into these first 2 lines. + +--- +A `submodule` is a module that is required by our current module. + +A `fallback_module` is a function which returns an object which contains 3 properties: + +- **_** : This defines the `submodules`. If there is no `submodule` used or `required`/`imported` in the current module then It is not defined (meaning it should not exist as a key. It should not be like `_:{}`. Instead, there should be nothing). **It is necessary to define when we do require a external module as a `submodule`.** +- **drive** : It is a place where we can store data which we want to be saved in localStorage. We store all the Styles, SVG's inside the drive. +- **api** : this defines another `fallback` function called `fallback_instance`. It is used to provide a developer who reuses our component with customization api to override our default data which is defined in `fallback_module`'s object. `fallback_instance` has obj returned with 2 properties ⇾ **_** and **drive**. +--- +#### The `_` property is very important. +It represents the submodules and instances. Any number of instances can be made from a single required module. +It is an object that is assigned to `_`. +Unlike `drive` (which has same structure in both `fallback_module` and `fallback_instance`) the stuctural syntax for **`_`** is a little different in `fallback_module` and `fallback_instance`. + +--- +In `fallback_module` we include the required module names as keys, and we assign an object to those keys which define the module by `$` key, This `$` property is mandatory what ever reason. We can create as many instances we want using `0,1,2,3,4,5,....,n` as keys of object that is passed to required module key. But mostly we use `fallback_instance` for creating instances. Anyways an example is: +```js +_: { + '../../module_address' : { + $ : '', // This is a must. we can either assign a string as a override which keeps the default data intact. Or we can specify an override function which would change the default fallbacks for each instance. + 0 : override_func, // we can assign a override to a sigle instance which will change the data only for this particular instance. + 1 : override_func2, // we can use same or different override functions for each instance. + 2 : '', // obviously we can also assign a empty string which would take data from $. and if $ also has and empty string then it defaults to orignal module data. + mapping: { + style: 'style' + } + } +} +``` +I have added the comments for explanation about `overrides`. + +--- +In `fallback_instance` the only difference is that we don't have a $ property for representing the module. + +That's why the `$` inside the `_` property of `fallback_module` is mandatory whether we use `fallback_instance` for creating instances or `fallback_module`. + + +There is another mandatory thing inside the **`_`** which is **`mapping`** property. It is always defined where we create Instances. + +If we create instance at module level then we would add it inside `_` of `fallback_module` but as most of the times we create instances through the `fallback_instance` add mapping there. + +Example: +```js + _: { + $: '', // only if we create module level instances + 0: '', + mapping: { + style: 'style' + } + } +``` +--- +Let's go back to drive. As discussed above that we place the data in **drive** which is supposed to be stored in localStorage of a browser. It is completely optional, we can ignore it if we want. The data we want to be customizable is stored in **api**'s **drive** (`fallback_instance`) and which is supposed to be not is stored in `fallback_module`'s drive. + +Drive is an JavaScript object. It contains datasets of different types or category. These categories can contain multiple files. +```json +drive: { + 'style/': { + 'theme.css': { + raw: ` + .element-class { + display: flex; + align-items: center; + background-color: #212121; + padding: 0.5rem; + // min-width: 456px + }` + } + } +} +``` +Now these datasets like `style/` can contain files and each file contains content using `raw:`. + +Another way of defining the content is by using `$ref:`. This is used when we want to use a file from the same directory as the module file. For example, if we want to require/import --> $ref `cross.svg` from directory of the module, we can do it like this : +```js +drive: { + 'style/': { + 'theme.css': { + '$ref': 'example.svg' + } + } +} +``` +This `$ref` can help a lot in short and clean `fallbacks` + +--- +### Back to where we left +After we have added those 3 lines of code, we can require modules to use them as `submodules` (if any). + +```js +const submodule1 = require('example_submodule') +``` + +Then we export our current module function. +```js +module.exports = module_function +``` +Then we define our function which is always async and always takes one `opts` parameter. +```js +async function module_function (opts) { + // Code +} +``` +Inside the function we start with this line: +```js + const { id, sdb } = await get(opts.sid) +``` +It fetches our `sdb` which is state database. +Now there is also a `sdb` in third line of the module i.e. +```js +const { sdb, get } = statedb(fallback_module) +``` +It is used when we use `fallback_module` to create instances. It is only used when we don't add this `const { id, sdb } = await get(opts.sid)` line to the module function. Most of the time we do add it as it's the backbone of customization `api`. I will share the exceptions in a bit. + +We should only add this line if we use `fallback_instance` to create instances. Which we mostly do. + +--- + +After that we create this object according to the datasets in drive. They will be helpful in executing certain functions when specific dataset is updated or changed. +```js + const on = { + style: inject + } +``` +This has a simple structure where key name is based of dataset and its value is the function we want to execute when that dataset changes. + +--- + +Then we start the real vanilla JavaScript journey for creating some useful HTML. +```js + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+ + +
+
` +``` +As mentioned before that we make the maximum use of literals. We also use Shadow DOM with closed mode. + +We can also define some placeholder elements that we can later replace with a submodule. + +--- +Then the most important line of the `STATE` program comes. +```js + const subs = await sdb.watch(onbatch) +``` +This does two things. + +First is that it is a watch function which is like an event listener. It triggers the `onbatch()` whenever something in the drive changes. We would share the `onbatch()` code later. + +Second it stores `Sid`'s for all the submodules and their instances into the subs array. It gets then from `_` properties of `drive` from both fallbacks (instance and module). These `Sid`'s are passed as parameters to the `submodules`. + +The order of execution of functions by `onbatch` is not random. so we need to so we need to make sure that those functions work independent of order of execution. A strategy we can opt is to create elements at the end after storing all the data into variables and then using those variables for creating elements. + +--- + +After we get the `Sid`'s we can append the required submodules into our HTML elements. +```js + submodule1(subs[0]).then(el => shadow.querySelector('placeholder').replaceWith(el)) + + // to add a click event listener to the buttons: + // const [btn1, btn2, btn3] = shadow.querySelectorAll('button') + // btn1.onclick = () => { console.log('Terminal button clicked') }) +``` +We can also add event listeners if we want at this stage. As mentioned in rules we dont use `element.addEventListner()` syntax. + +--- + Then we return the `el`. The main element to which we have attached the shadow. + ```js + return el + ``` + This is the end of a clean code. We can add the real mess under this return statement. + + --- + +Then we define the functions used under the return statement. +```js + function onbatch (batch) { + for (const { type, data } of batch) { + on[type] && on[type](data) + } + // here we can create some elements after storing data + } + function inject(data) { + const sheet = new CSSStyleSheet() + sheet.replaceSync(data) + shadow.adoptedStyleSheets = [sheet] + } + function iconject(data) { + dricons = data[0] + // using data[0] to retrieve the first file from the dataset. + } + function some_useful_function (){ + // NON state function + } +``` +We add both `STATE` related and actual code related functions here. And finally after those we close our main module delimiters. + +--- +Last but not the least outside the main module function, we define the `fallback_module` + +It is placed at the last as it can be pretty long sometimes. + +```js +function fallback_module () { + return { + api: fallback_instance, + _: { + submodule1: { + $: '' + } + } + } + function fallback_instance () { + return { + _: { + submodule1: { + 0: '', + mapping: { + style: 'style' + } + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .element-class { + display: flex; + align-items: center; + background-color: #212121; + padding: 0.5rem; + // min-width: 456px + } + ` + } + } + } + } + } +} +``` +--- +### Thus, here is the whole combined code: + +```js +const STATE = require('STATE') +const statedb = STATE(__filename) +const { sdb, get } = statedb(fallback_module) + +const submodule1 = require('example_submodule') + +module.exports = module_function + +async function module_function (opts) { + const { id, sdb } = await get(opts.sid) + const on = { + style: inject + } + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+ + +
+
` + const subs = await sdb.watch(onbatch) + submodule1(subs[0]).then(el => shadow.querySelector('placeholder').replaceWith(el)) + + return el + function onbatch (batch) { + for (const { type, data } of batch) { + on[type] && on[type](data) + } + } + function inject(data) { + const sheet = new CSSStyleSheet() + sheet.replaceSync(data) + shadow.adoptedStyleSheets = [sheet] + } + function some_useful_function (){ + // NON state function + } +} +function fallback_module () { + return { + api: fallback_instance, + _: { + submodule1: { + $: '' + } + } + } + function fallback_instance () { + return { + _: { + submodule1: { + 0: '', + mapping: { + style: 'style' + } + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .element-class { + display: flex; + align-items: center; + background-color: #212121; + padding: 0.5rem; + // min-width: 456px + } + ` + } + } + } + } + } +} +``` +# Latest and greatest example +## `tabs.js` + +This is another example which I think does not need much explanation. But still if you have any questions let me know. + +```js +//state Initialization +const STATE = require('STATE') +const statedb = STATE(__filename) +const { sdb, get } = statedb(fallback_module) +// exporting the module +module.exports = component +// actual module +async function component(opts, protocol) { + // getting the state database for the current instance + const { id, sdb } = await get(opts.sid) + // optional getting drive from state database but it does not work currently. will be useful in the future though. + const {drive} = sdb + // on object which contains the functions to be executed when the dataset changes and onbatch is called. + const on = { + variables: onvariables, + style: inject_style, + icons: iconject, + scroll: onscroll + } + // creating the main element and attaching shadow DOM to it. + const div = document.createElement('div') + const shadow = div.attachShadow({ mode: 'closed' }) + // defining the HTML structure of the component using template literals. + shadow.innerHTML = `
` + const entries = shadow.querySelector('.tab-entries') + // Initializing the variables to be used in the element creation. We store the data from drive through the onbatch function in these variables. + // this init variable is used to check if the component is initialized or not. It is set to true when the component is initialized for the first time. So that after that we can just update the component instead of creating it again using the onbatch function data. + let init = false + let variables = [] + let dricons = [] + + // subs for storing the Sid's of submodules and onbatch function which is called when the dataset changes. + const subs = await sdb.watch(onbatch) + // this is just a custom scrolling through drag clicking functionality. + if (entries) { + let is_down = false + let start_x + let scroll_start + + const stop = () => { + is_down = false + entries.classList.remove('grabbing') + update_scroll_position() + } + + const move = x => { + if (!is_down) return + if (entries.scrollWidth <= entries.clientWidth) return stop() + entries.scrollLeft = scroll_start - (x - start_x) * 1.5 + } + + entries.onmousedown = e => { + if (entries.scrollWidth <= entries.clientWidth) return + is_down = true + entries.classList.add('grabbing') + start_x = e.pageX - entries.offsetLeft + scroll_start = entries.scrollLeft + window.onmousemove = e => { + move(e.pageX - entries.offsetLeft) + e.preventDefault() + } + window.onmouseup = () => { + stop() + window.onmousemove = window.onmouseup = null + } + } + + entries.onmouseleave = stop + + entries.ontouchstart = e => { + if (entries.scrollWidth <= entries.clientWidth) return + is_down = true + start_x = e.touches[0].pageX - entries.offsetLeft + scroll_start = entries.scrollLeft + } + ;['ontouchend', 'ontouchcancel'].forEach(ev => { + entries[ev] = stop + }) + + entries.ontouchmove = e => { + move(e.touches[0].pageX - entries.offsetLeft) + e.preventDefault() + } + } + // component function returns the main element. + return div + // All the functions are defined below this return statement. + // this create_btn function is executed using forEach on the variables array. It creates the buttons for each variable in the array. It uses the data from the variables and dricons arrays to create the buttons. + async function create_btn({ name, id }, index) { + const el = document.createElement('div') + el.innerHTML = ` + ${dricons[index + 1]} + ${id} + ${name} + ` + + el.className = 'tabsbtn' + const icon_el = el.querySelector('.icon') + const label_el = el.querySelector('.name') + + label_el.draggable = false + // Event listener for the button click. It uses the protocol function to send a message to the parent component. The parent can further handle the message using the protocol function to route the message to the appropriate destination. + icon_el.onclick = protocol(onmessage)('type','data') + entries.appendChild(el) + return + } + function onmessage(type, data) { + return console.log(type,data) + } + // this function is called when the dataset changes. It calls the functions defined in `on` object. + function onbatch (batch) { + for (const { type, data } of batch) (on[type] || fail)(data, type) + // this condition checks if the component is initialized or not. If not then it creates the buttons using the create_btn function. if the component is already initialized then it can handle the updates to the drive in future. + if (!init) { + // after for loop ends and each of the data is stored in their respective variables, we can create the buttons using the create_btn function. + variables.forEach(create_btn) + init = true + } else { + // TODO: Here we can handle drive updates + // currently waiting for the next STATE module to be released so we can use the drive updates. + } + } + // this function throws an error if the type of data is not valid. It is used to handle the errors in the onbatch function. + function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } + // this function adds styles to shadow DOM. It uses the CSSStyleSheet API to create a new stylesheet and then replaces the existing stylesheet with the new one. + function inject_style(data) { + const sheet = new CSSStyleSheet() + sheet.replaceSync(data) + shadow.adoptedStyleSheets = [sheet] + } + // we simple store the data from the dataset into variables. We can use this data to create the buttons in the create_btn function. + function onvariables(data) { + const vars = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + variables = vars + } + // same here we store the data into dricons for later use. We can use this data to create the buttons in the create_btn function. + function iconject(data) { + dricons = data + } + // waiting for the next STATE module to be released so we can use the drive.put() to update the scroll position. + function update_scroll_position() { + // TODO + } + + function onscroll(data) { + setTimeout(() => { + if (entries) { + entries.scrollLeft = data + } + }, 200) + } +} +// this is the fallback module which is used to create the state database and to provide the default data for the component. +function fallback_module() { + return { + api: fallback_instance, + } + // this is the fallback instance which is used to provide the default data for the instances of a component. this also help in providing an API for csustomization by overriding the default data. + function fallback_instance() { + return { + drive: { + 'icons/': { + 'cross.svg':{ + '$ref': 'cross.svg' + // data is stored through '$ref' functionality + }, + '1.svg': { + '$ref': 'icon.svg' + }, + '2.svg': { + '$ref': 'icon.svg' + }, + '3.svg': { + '$ref': 'icon.svg' + } + }, + 'variables/': { + 'tabs.json': { + '$ref': 'tabs.json' + } + }, + 'scroll/': { + 'position.json': { + raw: '100' + } + }, + 'style/': { + 'theme.css': { + '$ref': 'style.css' + } + } + } + } + } +} +``` + +# Important update related to drive fetch, ignore old code above +### State Management +The `STATE` module provides several key features for state management: + +#### 1. Instance Isolation + - Each instance of a module gets its own isolated state + - State is accessed through the `sdb` interface + - Instances can be created and destroyed independently + +#### 2. sdb Interface +Provides access to following two APIs: + +**sdb.watch(onbatch)** +```js +const subs = await sdb.watch(onbatch) +const { drive } = sdb +async function onbatch(batch){ + for (const {type, paths} of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + on[type] && on[type](data) + } +} +``` +- Modules can watch for state changes +- Changes are batched and processed through the `onbatch` handler +- Different types of changes can be handled separately using `on`. +- `type` refers to the `dataset_type` used in fallbacks. The key names need to match. E.g. see `template.js` +- `paths` refers to the paths to the files inside the dataset. + +**sdb.get_sub** + @TODO +**sdb.drive** +The `sdb.drive` object provides an interface for managing datasets and files attached to the current node. It allows you to list, retrieve, add, and check files within datasets defined in the module's state. + +- **sdb.drive.list(path?)** + - Lists all dataset names (as folders) attached to the current node. + - If a `path` (dataset name) is provided, returns the list of file names within that dataset. + - Example: + ```js + const datasets = sdb.drive.list(); // ['mydata/', 'images/'] + const files = sdb.drive.list('mydata/'); // ['file1.json', 'file2.txt'] + ``` + +- **sdb.drive.get(path)** + - Retrieves a file object from a dataset. + - `path` should be in the format `'dataset_name/filename.ext'`. + - Returns an object: `{ id, name, type, raw }` or `null` if not found. + - Example: + ```js + const file = sdb.drive.get('mydata/file1.json'); + // file: { id: '...', name: 'file1.json', type: 'json', raw: ... } + ``` + +- **sdb.drive.put(path, buffer)** + - Adds a new file to a dataset. + - `path` is `'dataset_name/filename.ext'`. + - `buffer` is the file content (object, string, etc.). + - Returns the created file object: `{ id, name, type, raw }`. + - Example: + ```js + sdb.drive.put('mydata/newfile.txt', 'Hello World'); + ``` + +- **sdb.drive.has(path)** + - Checks if a file exists in a dataset. + - `path` is `'dataset_name/filename.ext'`. + - Returns `true` if the file exists, otherwise `false`. + - Example: + ```js + if (sdb.drive.has('mydata/file1.json')) { /* ... */ } + ``` + +**Notes:** +- Dataset names are defined in the fallback structure and must be unique within a node. +- File types are inferred from the file extension. +- All file operations are isolated to the current node's state and changes are persisted immediately. + diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 7e6c13c..7974326 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -15,6 +15,7 @@ async function graph_explorer(opts) { let all_entries = {} let instance_states = {} let view = [] + let drive_updated_by_scroll = false const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -48,6 +49,10 @@ async function graph_explorer(opts) { return el async function onbatch(batch) { + if (drive_updated_by_scroll) { + drive_updated_by_scroll = false + return + } for (const { type, paths } of batch) { const data = await Promise.all(paths.map(path => drive.get(path).then(file => file ? file.raw : null))) const func = on[type] || fail @@ -109,24 +114,22 @@ async function graph_explorer(opts) { requestAnimationFrame(() => { if (vertical_scroll_value !== container.scrollTop) { vertical_scroll_value = container.scrollTop + drive_updated_by_scroll = true update_runtime_state('vertical_scroll_value', vertical_scroll_value) } if (horizontal_scroll_value !== container.scrollLeft) { horizontal_scroll_value = container.scrollLeft + drive_updated_by_scroll = true update_runtime_state('horizontal_scroll_value', horizontal_scroll_value) } scroll_update_pending = false }) } - function build_and_render_view(focal_instance_path = null) { + function build_and_render_view() { const old_view = [...view] const old_scroll_top = vertical_scroll_value const old_scroll_left = horizontal_scroll_value - const old_focal_index = focal_instance_path - ? old_view.findIndex(node => node.instance_path === focal_instance_path) - : -1 - view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -138,23 +141,13 @@ async function graph_explorer(opts) { all_entries }) - const new_focal_index = focal_instance_path - ? view.findIndex(node => node.instance_path === focal_instance_path) - : -1 - let new_scroll_top = old_scroll_top - - if (focal_instance_path && old_focal_index !== -1 && new_focal_index !== -1) { - const scroll_diff = (new_focal_index - old_focal_index) * node_height - new_scroll_top = old_scroll_top + scroll_diff - } else { - const old_top_node_index = Math.floor(old_scroll_top / node_height) - const old_top_node = old_view[old_top_node_index] - if (old_top_node) { - const new_top_node_index = view.findIndex(node => node.instance_path === old_top_node.instance_path) - if (new_top_node_index !== -1) { - new_scroll_top = new_top_node_index * node_height - } + const old_top_node_index = Math.floor(old_scroll_top / node_height) + const old_top_node = old_view[old_top_node_index] + if (old_top_node) { + const new_top_node_index = view.findIndex(node => node.instance_path === old_top_node.instance_path) + if (new_top_node_index !== -1) { + new_scroll_top = new_top_node_index * node_height } } @@ -487,10 +480,6 @@ async function graph_explorer(opts) { } update_runtime_state('selected_instance_paths', new_selected_paths) } else { - if (selected_instance_paths.length === 1 && selected_instance_paths[0] === instance_path) { - console.log(`entry ${base_path} selected again aka confirmed`) - return - } update_runtime_state('selected_instance_paths', [instance_path]) } } @@ -498,12 +487,14 @@ async function graph_explorer(opts) { function toggle_subs(instance_path) { const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs + build_and_render_view() update_runtime_state('instance_states', instance_states) } function toggle_hubs(instance_path) { const state = instance_states[instance_path] state.expanded_hubs = !state.expanded_hubs + build_and_render_view() update_runtime_state('instance_states', instance_states) } } From 7e5d0d45b9aeb6f4840307c65e0d350f1437dbb4 Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 24 Jul 2025 17:13:13 +0500 Subject: [PATCH 016/130] static entry position while expanding or collapsing --- lib/graph_explorer.js | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 7974326..6135afd 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -22,6 +22,7 @@ async function graph_explorer(opts) { const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = `
` const container = shadow.querySelector('.graph-container') + document.body.style.margin = 0 let scroll_update_pending = false container.onscroll = onscroll @@ -125,7 +126,7 @@ async function graph_explorer(opts) { scroll_update_pending = false }) } - function build_and_render_view() { + function build_and_render_view(focal_instance_path) { const old_view = [...view] const old_scroll_top = vertical_scroll_value const old_scroll_left = horizontal_scroll_value @@ -142,12 +143,24 @@ async function graph_explorer(opts) { }) let new_scroll_top = old_scroll_top - const old_top_node_index = Math.floor(old_scroll_top / node_height) - const old_top_node = old_view[old_top_node_index] - if (old_top_node) { - const new_top_node_index = view.findIndex(node => node.instance_path === old_top_node.instance_path) - if (new_top_node_index !== -1) { - new_scroll_top = new_top_node_index * node_height + + if (focal_instance_path) { + const old_toggled_node_index = old_view.findIndex(node => node.instance_path === focal_instance_path) + const new_toggled_node_index = view.findIndex(node => node.instance_path === focal_instance_path) + + if (old_toggled_node_index !== -1 && new_toggled_node_index !== -1) { + const index_change = new_toggled_node_index - old_toggled_node_index + new_scroll_top = old_scroll_top + (index_change * node_height) + } + } else if (old_view.length > 0) { + const old_top_node_index = Math.floor(old_scroll_top / node_height) + const scroll_offset = old_scroll_top % node_height + const old_top_node = old_view[old_top_node_index] + if (old_top_node) { + const new_top_node_index = view.findIndex(node => node.instance_path === old_top_node.instance_path) + if (new_top_node_index !== -1) { + new_scroll_top = (new_top_node_index * node_height) + scroll_offset + } } } @@ -487,14 +500,14 @@ async function graph_explorer(opts) { function toggle_subs(instance_path) { const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs - build_and_render_view() + build_and_render_view(instance_path) update_runtime_state('instance_states', instance_states) } function toggle_hubs(instance_path) { const state = instance_states[instance_path] state.expanded_hubs = !state.expanded_hubs - build_and_render_view() + build_and_render_view(instance_path) update_runtime_state('instance_states', instance_states) } } From de4a9f8ae315d8b74a6ae612aeb35c16a3a3a98c Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 24 Jul 2025 19:46:23 +0500 Subject: [PATCH 017/130] added commented sections --- lib/graph_explorer.js | 352 +++++++++++++++++++++++------------------- 1 file changed, 191 insertions(+), 161 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 6135afd..828549f 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -5,6 +5,10 @@ const { get } = statedb(fallback_module) module.exports = graph_explorer async function graph_explorer(opts) { +/****************************************************************************** + 1. COMPONENT INITIALIZATION + Set up state, variables, DOM, and watchers. +******************************************************************************/ const { sdb } = await get(opts.sid) const { drive } = sdb await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) @@ -49,6 +53,10 @@ async function graph_explorer(opts) { return el +/****************************************************************************** + 2. STATE AND DATA HANDLING + Functions for processing data from the STATE module. +******************************************************************************/ async function onbatch(batch) { if (drive_updated_by_scroll) { drive_updated_by_scroll = false @@ -63,8 +71,17 @@ async function graph_explorer(opts) { function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } - async function update_runtime_state (name, value) { - await drive.put(`runtime/${name}.json`, JSON.stringify(value)) + function on_entries(data) { + all_entries = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + const root_path = '/' + if (all_entries[root_path]) { + const root_instance_path = '|/' + if (!instance_states[root_instance_path]) { + instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + } else { + build_and_render_view() + } + } } function on_runtime (data, type, paths) { @@ -91,41 +108,20 @@ async function graph_explorer(opts) { } } - function on_entries(data) { - all_entries = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] - const root_path = '/' - if (all_entries[root_path]) { - const root_instance_path = '|/' - if (!instance_states[root_instance_path]) { - instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } - } else { - build_and_render_view() - } - } - } - function inject_style(data) { const sheet = new CSSStyleSheet() sheet.replaceSync(data[0]) shadow.adoptedStyleSheets = [sheet] } - function onscroll() { - if (scroll_update_pending) return - scroll_update_pending = true - requestAnimationFrame(() => { - if (vertical_scroll_value !== container.scrollTop) { - vertical_scroll_value = container.scrollTop - drive_updated_by_scroll = true - update_runtime_state('vertical_scroll_value', vertical_scroll_value) - } - if (horizontal_scroll_value !== container.scrollLeft) { - horizontal_scroll_value = container.scrollLeft - drive_updated_by_scroll = true - update_runtime_state('horizontal_scroll_value', horizontal_scroll_value) - } - scroll_update_pending = false - }) + + async function update_runtime_state (name, value) { + await drive.put(`runtime/${name}.json`, JSON.stringify(value)) } + +/****************************************************************************** + 3. VIEW AND RENDERING LOGIC + Functions for building and rendering the graph view. +******************************************************************************/ function build_and_render_view(focal_instance_path) { const old_view = [...view] const old_scroll_top = vertical_scroll_value @@ -291,118 +287,12 @@ async function graph_explorer(opts) { } return current_view } - - function handle_sentinel_intersection(entries) { - entries.forEach(entry => { - if (entry.isIntersecting) { - if (entry.target === top_sentinel) render_prev_chunk() - else if (entry.target === bottom_sentinel) render_next_chunk() - } - }) - } - - function render_next_chunk() { - if (end_index >= view.length) return - const fragment = document.createDocumentFragment() - const next_end = Math.min(view.length, end_index + chunk_size) - for (let i = end_index; i < next_end; i++) { - fragment.appendChild(create_node(view[i])) - } - container.insertBefore(fragment, bottom_sentinel) - end_index = next_end - bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` - cleanup_dom(false) - } - - function render_prev_chunk() { - if (start_index <= 0) return - const fragment = document.createDocumentFragment() - const prev_start = Math.max(0, start_index - chunk_size) - for (let i = prev_start; i < start_index; i++) { - fragment.appendChild(create_node(view[i])) - } - container.insertBefore(fragment, top_sentinel.nextSibling) - start_index = prev_start - top_sentinel.style.height = `${start_index * node_height}px` - cleanup_dom(true) - } - - function cleanup_dom(is_scrolling_up) { - const rendered_count = end_index - start_index - if (rendered_count <= max_rendered_nodes) return - - const to_remove_count = rendered_count - max_rendered_nodes - if (is_scrolling_up) { - for (let i = 0; i < to_remove_count; i++) { - if (bottom_sentinel.previousElementSibling && bottom_sentinel.previousElementSibling !== top_sentinel) { - bottom_sentinel.previousElementSibling.remove() - } - } - end_index -= to_remove_count - bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` - } else { - for (let i = 0; i < to_remove_count; i++) { - if (top_sentinel.nextElementSibling && top_sentinel.nextElementSibling !== bottom_sentinel) { - top_sentinel.nextElementSibling.remove() - } - } - start_index += to_remove_count - top_sentinel.style.height = `${start_index * node_height}px` - } - } - - function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { - const { expanded_subs, expanded_hubs } = state - if (is_hub) { - if (is_hub_on_top) { - if (expanded_subs && expanded_hubs) return '┌┼' - if (expanded_subs) return '┌┬' - if (expanded_hubs) return '┌┴' - return '┌─' - } else { - if (expanded_subs && expanded_hubs) return '├┼' - if (expanded_subs) return '├┬' - if (expanded_hubs) return '├┴' - return '├─' - } - } else if (is_last_sub) { - if (expanded_subs && expanded_hubs) return '└┼' - if (expanded_subs) return '└┬' - if (expanded_hubs) return '└┴' - return '└─' - } else { - if (expanded_subs && expanded_hubs) return '├┼' - if (expanded_subs) return '├┬' - if (expanded_hubs) return '├┴' - return '├─' - } - } - - function reset() { - const root_path = '/' - const root_instance_path = '|/' - const new_instance_states = {} - if (all_entries[root_path]) { - new_instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } - } - update_runtime_state('vertical_scroll_value', 0) - update_runtime_state('horizontal_scroll_value', 0) - update_runtime_state('selected_instance_paths', []) - update_runtime_state('confirmed_selected', []) - update_runtime_state('instance_states', new_instance_states) - } - - function re_render_node (instance_path) { - const node_data = view.find(n => n.instance_path === instance_path) - if (node_data) { - const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) - if (old_node_el) { - const new_node_el = create_node(node_data) - old_node_el.replaceWith(new_node_el) - } - } - } - + + /****************************************************************************** + 4. NODE CREATION AND GRAPH BUILDING + Functions for creating nodes in the graph. + ******************************************************************************/ + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { const entry = all_entries[base_path] const state = instance_states[instance_path] @@ -439,7 +329,7 @@ async function graph_explorer(opts) { const icon_class = has_subs ? 'icon clickable' : 'icon' el.innerHTML = ` - ${pipe_html} + ${pipe_html} ${prefix_symbol} ${entry.name} @@ -460,6 +350,63 @@ async function graph_explorer(opts) { return el } + function re_render_node (instance_path) { + const node_data = view.find(n => n.instance_path === instance_path) + if (node_data) { + const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (old_node_el) { + const new_node_el = create_node(node_data) + old_node_el.replaceWith(new_node_el) + } + } + } + + function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { + const { expanded_subs, expanded_hubs } = state + if (is_hub) { + if (is_hub_on_top) { + if (expanded_subs && expanded_hubs) return '┌┼' + if (expanded_subs) return '┌┬' + if (expanded_hubs) return '┌┴' + return '┌─' + } else { + if (expanded_subs && expanded_hubs) return '├┼' + if (expanded_subs) return '├┬' + if (expanded_hubs) return '├┴' + return '├─' + } + } else if (is_last_sub) { + if (expanded_subs && expanded_hubs) return '└┼' + if (expanded_subs) return '└┬' + if (expanded_hubs) return '└┴' + return '└─' + } else { + if (expanded_subs && expanded_hubs) return '├┼' + if (expanded_subs) return '├┬' + if (expanded_hubs) return '├┴' + return '├─' + } + } + + /****************************************************************************** + 5. VIEW MANIPULATION + Functions for toggling view states, selecting, confirming nodes and resetting graph. + ******************************************************************************/ + function select_node(ev, instance_path, base_path) { + if (ev.ctrlKey) { + const new_selected_paths = [...selected_instance_paths] + const index = new_selected_paths.indexOf(instance_path) + if (index > -1) { + new_selected_paths.splice(index, 1) + } else { + new_selected_paths.push(instance_path) + } + update_runtime_state('selected_instance_paths', new_selected_paths) + } else { + update_runtime_state('selected_instance_paths', [instance_path]) + } + } + function handle_confirm(ev, instance_path) { const is_checked = ev.target.checked const new_selected_paths = [...selected_instance_paths] @@ -481,22 +428,6 @@ async function graph_explorer(opts) { update_runtime_state('selected_instance_paths', new_selected_paths) update_runtime_state('confirmed_selected', new_confirmed_paths) } - - function select_node(ev, instance_path, base_path) { - if (ev.ctrlKey) { - const new_selected_paths = [...selected_instance_paths] - const index = new_selected_paths.indexOf(instance_path) - if (index > -1) { - new_selected_paths.splice(index, 1) - } else { - new_selected_paths.push(instance_path) - } - update_runtime_state('selected_instance_paths', new_selected_paths) - } else { - update_runtime_state('selected_instance_paths', [instance_path]) - } - } - function toggle_subs(instance_path) { const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs @@ -510,8 +441,107 @@ async function graph_explorer(opts) { build_and_render_view(instance_path) update_runtime_state('instance_states', instance_states) } + + function reset() { + const root_path = '/' + const root_instance_path = '|/' + const new_instance_states = {} + if (all_entries[root_path]) { + new_instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + } + update_runtime_state('vertical_scroll_value', 0) + update_runtime_state('horizontal_scroll_value', 0) + update_runtime_state('selected_instance_paths', []) + update_runtime_state('confirmed_selected', []) + update_runtime_state('instance_states', new_instance_states) + } + +/****************************************************************************** + 6. VIRTUAL SCROLLING + Functions for handling virtual scrolling and DOM cleanup. +******************************************************************************/ + function onscroll() { + if (scroll_update_pending) return + scroll_update_pending = true + requestAnimationFrame(() => { + if (vertical_scroll_value !== container.scrollTop) { + vertical_scroll_value = container.scrollTop + drive_updated_by_scroll = true + update_runtime_state('vertical_scroll_value', vertical_scroll_value) + } + if (horizontal_scroll_value !== container.scrollLeft) { + horizontal_scroll_value = container.scrollLeft + drive_updated_by_scroll = true + update_runtime_state('horizontal_scroll_value', horizontal_scroll_value) + } + scroll_update_pending = false + }) + } + + function handle_sentinel_intersection(entries) { + entries.forEach(entry => { + if (entry.isIntersecting) { + if (entry.target === top_sentinel) render_prev_chunk() + else if (entry.target === bottom_sentinel) render_next_chunk() + } + }) + } + + function render_next_chunk() { + if (end_index >= view.length) return + const fragment = document.createDocumentFragment() + const next_end = Math.min(view.length, end_index + chunk_size) + for (let i = end_index; i < next_end; i++) { + fragment.appendChild(create_node(view[i])) + } + container.insertBefore(fragment, bottom_sentinel) + end_index = next_end + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + cleanup_dom(false) + } + + function render_prev_chunk() { + if (start_index <= 0) return + const fragment = document.createDocumentFragment() + const prev_start = Math.max(0, start_index - chunk_size) + for (let i = prev_start; i < start_index; i++) { + fragment.appendChild(create_node(view[i])) + } + container.insertBefore(fragment, top_sentinel.nextSibling) + start_index = prev_start + top_sentinel.style.height = `${start_index * node_height}px` + cleanup_dom(true) + } + + function cleanup_dom(is_scrolling_up) { + const rendered_count = end_index - start_index + if (rendered_count <= max_rendered_nodes) return + + const to_remove_count = rendered_count - max_rendered_nodes + if (is_scrolling_up) { + for (let i = 0; i < to_remove_count; i++) { + if (bottom_sentinel.previousElementSibling && bottom_sentinel.previousElementSibling !== top_sentinel) { + bottom_sentinel.previousElementSibling.remove() + } + } + end_index -= to_remove_count + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + } else { + for (let i = 0; i < to_remove_count; i++) { + if (top_sentinel.nextElementSibling && top_sentinel.nextElementSibling !== bottom_sentinel) { + top_sentinel.nextElementSibling.remove() + } + } + start_index += to_remove_count + top_sentinel.style.height = `${start_index * node_height}px` + } + } } +/****************************************************************************** + 7. FALLBACK CONFIGURATION + Provides default data and API for the component. +******************************************************************************/ function fallback_module() { return { api: fallback_instance @@ -587,4 +617,4 @@ function fallback_module() { } } } -} +} \ No newline at end of file From 9e47479cedffe0267567c03aeb738646a04defdb Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 24 Jul 2025 19:55:13 +0500 Subject: [PATCH 018/130] reversed the onclicks for toggle hubs and subs --- lib/graph_explorer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 828549f..b062dca 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -325,8 +325,8 @@ async function graph_explorer(opts) { const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) const pipe_html = pipe_trail.map(should_pipe => `${should_pipe ? '│' : ' '}`).join('') - const prefix_class = (!has_hubs || base_path !== '/') ? 'prefix clickable' : 'prefix' - const icon_class = has_subs ? 'icon clickable' : 'icon' + const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + const icon_class = (has_hubs && base_path !== '/') ? 'icon clickable' : 'icon' el.innerHTML = ` ${pipe_html} @@ -334,8 +334,8 @@ async function graph_explorer(opts) { ${entry.name} ` - if(has_hubs && base_path !== '/') el.querySelector('.prefix').onclick = () => toggle_hubs(instance_path) - if(has_subs) el.querySelector('.icon').onclick = () => toggle_subs(instance_path) + if(has_hubs && base_path !== '/') el.querySelector('.icon').onclick = () => toggle_hubs(instance_path) + if(has_subs) el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { From eaf7f4f6a57879d3f4c8800288c3dfea8165d8d7 Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 24 Jul 2025 21:43:48 +0500 Subject: [PATCH 019/130] tweaked styles and bundled --- bundle.js | 399 +++++++++++++++++++++++------------------- lib/graph_explorer.js | 4 +- 2 files changed, 218 insertions(+), 185 deletions(-) diff --git a/bundle.js b/bundle.js index d390eb8..7b628d2 100644 --- a/bundle.js +++ b/bundle.js @@ -9,6 +9,10 @@ const { get } = statedb(fallback_module) module.exports = graph_explorer async function graph_explorer(opts) { +/****************************************************************************** + 1. COMPONENT INITIALIZATION + Set up state, variables, DOM, and watchers. +******************************************************************************/ const { sdb } = await get(opts.sid) const { drive } = sdb await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) @@ -19,12 +23,14 @@ async function graph_explorer(opts) { let all_entries = {} let instance_states = {} let view = [] + let drive_updated_by_scroll = false const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = `
` const container = shadow.querySelector('.graph-container') + document.body.style.margin = 0 let scroll_update_pending = false container.onscroll = onscroll @@ -33,7 +39,7 @@ async function graph_explorer(opts) { let end_index = 0 const chunk_size = 50 const max_rendered_nodes = chunk_size * 3 - const node_height = 22 + const node_height = 19 const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') @@ -51,7 +57,15 @@ async function graph_explorer(opts) { return el +/****************************************************************************** + 2. STATE AND DATA HANDLING + Functions for processing data from the STATE module. +******************************************************************************/ async function onbatch(batch) { + if (drive_updated_by_scroll) { + drive_updated_by_scroll = false + return + } for (const { type, paths } of batch) { const data = await Promise.all(paths.map(path => drive.get(path).then(file => file ? file.raw : null))) const func = on[type] || fail @@ -61,8 +75,17 @@ async function graph_explorer(opts) { function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } - async function update_runtime_state (name, value) { - await drive.put(`runtime/${name}.json`, JSON.stringify(value)) + function on_entries(data) { + all_entries = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + const root_path = '/' + if (all_entries[root_path]) { + const root_instance_path = '|/' + if (!instance_states[root_instance_path]) { + instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + } else { + build_and_render_view() + } + } } function on_runtime (data, type, paths) { @@ -89,48 +112,25 @@ async function graph_explorer(opts) { } } - function on_entries(data) { - all_entries = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] - const root_path = '/' - if (all_entries[root_path]) { - const root_instance_path = '|/' - if (!instance_states[root_instance_path]) { - instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } - } else { - build_and_render_view() - } - } - } - function inject_style(data) { const sheet = new CSSStyleSheet() sheet.replaceSync(data[0]) shadow.adoptedStyleSheets = [sheet] } - function onscroll() { - if (scroll_update_pending) return - scroll_update_pending = true - requestAnimationFrame(() => { - if (vertical_scroll_value !== container.scrollTop) { - vertical_scroll_value = container.scrollTop - update_runtime_state('vertical_scroll_value', vertical_scroll_value) - } - if (horizontal_scroll_value !== container.scrollLeft) { - horizontal_scroll_value = container.scrollLeft - update_runtime_state('horizontal_scroll_value', horizontal_scroll_value) - } - scroll_update_pending = false - }) + + async function update_runtime_state (name, value) { + await drive.put(`runtime/${name}.json`, JSON.stringify(value)) } - function build_and_render_view(focal_instance_path = null) { + +/****************************************************************************** + 3. VIEW AND RENDERING LOGIC + Functions for building and rendering the graph view. +******************************************************************************/ + function build_and_render_view(focal_instance_path) { const old_view = [...view] const old_scroll_top = vertical_scroll_value const old_scroll_left = horizontal_scroll_value - const old_focal_index = focal_instance_path - ? old_view.findIndex(node => node.instance_path === focal_instance_path) - : -1 - view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -142,22 +142,24 @@ async function graph_explorer(opts) { all_entries }) - const new_focal_index = focal_instance_path - ? view.findIndex(node => node.instance_path === focal_instance_path) - : -1 - let new_scroll_top = old_scroll_top - if (focal_instance_path && old_focal_index !== -1 && new_focal_index !== -1) { - const scroll_diff = (new_focal_index - old_focal_index) * node_height - new_scroll_top = old_scroll_top + scroll_diff - } else { + if (focal_instance_path) { + const old_toggled_node_index = old_view.findIndex(node => node.instance_path === focal_instance_path) + const new_toggled_node_index = view.findIndex(node => node.instance_path === focal_instance_path) + + if (old_toggled_node_index !== -1 && new_toggled_node_index !== -1) { + const index_change = new_toggled_node_index - old_toggled_node_index + new_scroll_top = old_scroll_top + (index_change * node_height) + } + } else if (old_view.length > 0) { const old_top_node_index = Math.floor(old_scroll_top / node_height) + const scroll_offset = old_scroll_top % node_height const old_top_node = old_view[old_top_node_index] if (old_top_node) { const new_top_node_index = view.findIndex(node => node.instance_path === old_top_node.instance_path) if (new_top_node_index !== -1) { - new_scroll_top = new_top_node_index * node_height + new_scroll_top = (new_top_node_index * node_height) + scroll_offset } } } @@ -289,118 +291,12 @@ async function graph_explorer(opts) { } return current_view } - - function handle_sentinel_intersection(entries) { - entries.forEach(entry => { - if (entry.isIntersecting) { - if (entry.target === top_sentinel) render_prev_chunk() - else if (entry.target === bottom_sentinel) render_next_chunk() - } - }) - } - - function render_next_chunk() { - if (end_index >= view.length) return - const fragment = document.createDocumentFragment() - const next_end = Math.min(view.length, end_index + chunk_size) - for (let i = end_index; i < next_end; i++) { - fragment.appendChild(create_node(view[i])) - } - container.insertBefore(fragment, bottom_sentinel) - end_index = next_end - bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` - cleanup_dom(false) - } - - function render_prev_chunk() { - if (start_index <= 0) return - const fragment = document.createDocumentFragment() - const prev_start = Math.max(0, start_index - chunk_size) - for (let i = prev_start; i < start_index; i++) { - fragment.appendChild(create_node(view[i])) - } - container.insertBefore(fragment, top_sentinel.nextSibling) - start_index = prev_start - top_sentinel.style.height = `${start_index * node_height}px` - cleanup_dom(true) - } - - function cleanup_dom(is_scrolling_up) { - const rendered_count = end_index - start_index - if (rendered_count <= max_rendered_nodes) return - - const to_remove_count = rendered_count - max_rendered_nodes - if (is_scrolling_up) { - for (let i = 0; i < to_remove_count; i++) { - if (bottom_sentinel.previousElementSibling && bottom_sentinel.previousElementSibling !== top_sentinel) { - bottom_sentinel.previousElementSibling.remove() - } - } - end_index -= to_remove_count - bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` - } else { - for (let i = 0; i < to_remove_count; i++) { - if (top_sentinel.nextElementSibling && top_sentinel.nextElementSibling !== bottom_sentinel) { - top_sentinel.nextElementSibling.remove() - } - } - start_index += to_remove_count - top_sentinel.style.height = `${start_index * node_height}px` - } - } - - function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { - const { expanded_subs, expanded_hubs } = state - if (is_hub) { - if (is_hub_on_top) { - if (expanded_subs && expanded_hubs) return '┌┼' - if (expanded_subs) return '┌┬' - if (expanded_hubs) return '┌┴' - return '┌─' - } else { - if (expanded_subs && expanded_hubs) return '├┼' - if (expanded_subs) return '├┬' - if (expanded_hubs) return '├┴' - return '├─' - } - } else if (is_last_sub) { - if (expanded_subs && expanded_hubs) return '└┼' - if (expanded_subs) return '└┬' - if (expanded_hubs) return '└┴' - return '└─' - } else { - if (expanded_subs && expanded_hubs) return '├┼' - if (expanded_subs) return '├┬' - if (expanded_hubs) return '├┴' - return '├─' - } - } - - function reset() { - const root_path = '/' - const root_instance_path = '|/' - const new_instance_states = {} - if (all_entries[root_path]) { - new_instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } - } - update_runtime_state('vertical_scroll_value', 0) - update_runtime_state('horizontal_scroll_value', 0) - update_runtime_state('selected_instance_paths', []) - update_runtime_state('confirmed_selected', []) - update_runtime_state('instance_states', new_instance_states) - } - - function re_render_node (instance_path) { - const node_data = view.find(n => n.instance_path === instance_path) - if (node_data) { - const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) - if (old_node_el) { - const new_node_el = create_node(node_data) - old_node_el.replaceWith(new_node_el) - } - } - } - + + /****************************************************************************** + 4. NODE CREATION AND GRAPH BUILDING + Functions for creating nodes in the graph. + ******************************************************************************/ + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { const entry = all_entries[base_path] const state = instance_states[instance_path] @@ -433,17 +329,17 @@ async function graph_explorer(opts) { const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) const pipe_html = pipe_trail.map(should_pipe => `${should_pipe ? '│' : ' '}`).join('') - const prefix_class = (!has_hubs || base_path !== '/') ? 'prefix clickable' : 'prefix' - const icon_class = has_subs ? 'icon clickable' : 'icon' + const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + const icon_class = (has_hubs && base_path !== '/') ? 'icon clickable' : 'icon' el.innerHTML = ` - ${pipe_html} + ${pipe_html} ${prefix_symbol} ${entry.name} ` - if(has_hubs && base_path !== '/') el.querySelector('.prefix').onclick = () => toggle_hubs(instance_path) - if(has_subs) el.querySelector('.icon').onclick = () => toggle_subs(instance_path) + if(has_hubs && base_path !== '/') el.querySelector('.icon').onclick = () => toggle_hubs(instance_path) + if(has_subs) el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { @@ -458,6 +354,63 @@ async function graph_explorer(opts) { return el } + function re_render_node (instance_path) { + const node_data = view.find(n => n.instance_path === instance_path) + if (node_data) { + const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (old_node_el) { + const new_node_el = create_node(node_data) + old_node_el.replaceWith(new_node_el) + } + } + } + + function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { + const { expanded_subs, expanded_hubs } = state + if (is_hub) { + if (is_hub_on_top) { + if (expanded_subs && expanded_hubs) return '┌┼' + if (expanded_subs) return '┌┬' + if (expanded_hubs) return '┌┴' + return '┌─' + } else { + if (expanded_subs && expanded_hubs) return '├┼' + if (expanded_subs) return '├┬' + if (expanded_hubs) return '├┴' + return '├─' + } + } else if (is_last_sub) { + if (expanded_subs && expanded_hubs) return '└┼' + if (expanded_subs) return '└┬' + if (expanded_hubs) return '└┴' + return '└─' + } else { + if (expanded_subs && expanded_hubs) return '├┼' + if (expanded_subs) return '├┬' + if (expanded_hubs) return '├┴' + return '├─' + } + } + + /****************************************************************************** + 5. VIEW MANIPULATION + Functions for toggling view states, selecting, confirming nodes and resetting graph. + ******************************************************************************/ + function select_node(ev, instance_path, base_path) { + if (ev.ctrlKey) { + const new_selected_paths = [...selected_instance_paths] + const index = new_selected_paths.indexOf(instance_path) + if (index > -1) { + new_selected_paths.splice(index, 1) + } else { + new_selected_paths.push(instance_path) + } + update_runtime_state('selected_instance_paths', new_selected_paths) + } else { + update_runtime_state('selected_instance_paths', [instance_path]) + } + } + function handle_confirm(ev, instance_path) { const is_checked = ev.target.checked const new_selected_paths = [...selected_instance_paths] @@ -479,39 +432,120 @@ async function graph_explorer(opts) { update_runtime_state('selected_instance_paths', new_selected_paths) update_runtime_state('confirmed_selected', new_confirmed_paths) } - - function select_node(ev, instance_path, base_path) { - if (ev.ctrlKey) { - const new_selected_paths = [...selected_instance_paths] - const index = new_selected_paths.indexOf(instance_path) - if (index > -1) { - new_selected_paths.splice(index, 1) - } else { - new_selected_paths.push(instance_path) - } - update_runtime_state('selected_instance_paths', new_selected_paths) - } else { - if (selected_instance_paths.length === 1 && selected_instance_paths[0] === instance_path) { - console.log(`entry ${base_path} selected again aka confirmed`) - return - } - update_runtime_state('selected_instance_paths', [instance_path]) - } - } - function toggle_subs(instance_path) { const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs + build_and_render_view(instance_path) update_runtime_state('instance_states', instance_states) } function toggle_hubs(instance_path) { const state = instance_states[instance_path] state.expanded_hubs = !state.expanded_hubs + build_and_render_view(instance_path) update_runtime_state('instance_states', instance_states) } + + function reset() { + const root_path = '/' + const root_instance_path = '|/' + const new_instance_states = {} + if (all_entries[root_path]) { + new_instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + } + update_runtime_state('vertical_scroll_value', 0) + update_runtime_state('horizontal_scroll_value', 0) + update_runtime_state('selected_instance_paths', []) + update_runtime_state('confirmed_selected', []) + update_runtime_state('instance_states', new_instance_states) + } + +/****************************************************************************** + 6. VIRTUAL SCROLLING + Functions for handling virtual scrolling and DOM cleanup. +******************************************************************************/ + function onscroll() { + if (scroll_update_pending) return + scroll_update_pending = true + requestAnimationFrame(() => { + if (vertical_scroll_value !== container.scrollTop) { + vertical_scroll_value = container.scrollTop + drive_updated_by_scroll = true + update_runtime_state('vertical_scroll_value', vertical_scroll_value) + } + if (horizontal_scroll_value !== container.scrollLeft) { + horizontal_scroll_value = container.scrollLeft + drive_updated_by_scroll = true + update_runtime_state('horizontal_scroll_value', horizontal_scroll_value) + } + scroll_update_pending = false + }) + } + + function handle_sentinel_intersection(entries) { + entries.forEach(entry => { + if (entry.isIntersecting) { + if (entry.target === top_sentinel) render_prev_chunk() + else if (entry.target === bottom_sentinel) render_next_chunk() + } + }) + } + + function render_next_chunk() { + if (end_index >= view.length) return + const fragment = document.createDocumentFragment() + const next_end = Math.min(view.length, end_index + chunk_size) + for (let i = end_index; i < next_end; i++) { + fragment.appendChild(create_node(view[i])) + } + container.insertBefore(fragment, bottom_sentinel) + end_index = next_end + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + cleanup_dom(false) + } + + function render_prev_chunk() { + if (start_index <= 0) return + const fragment = document.createDocumentFragment() + const prev_start = Math.max(0, start_index - chunk_size) + for (let i = prev_start; i < start_index; i++) { + fragment.appendChild(create_node(view[i])) + } + container.insertBefore(fragment, top_sentinel.nextSibling) + start_index = prev_start + top_sentinel.style.height = `${start_index * node_height}px` + cleanup_dom(true) + } + + function cleanup_dom(is_scrolling_up) { + const rendered_count = end_index - start_index + if (rendered_count <= max_rendered_nodes) return + + const to_remove_count = rendered_count - max_rendered_nodes + if (is_scrolling_up) { + for (let i = 0; i < to_remove_count; i++) { + if (bottom_sentinel.previousElementSibling && bottom_sentinel.previousElementSibling !== top_sentinel) { + bottom_sentinel.previousElementSibling.remove() + } + } + end_index -= to_remove_count + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + } else { + for (let i = 0; i < to_remove_count; i++) { + if (top_sentinel.nextElementSibling && top_sentinel.nextElementSibling !== bottom_sentinel) { + top_sentinel.nextElementSibling.remove() + } + } + start_index += to_remove_count + top_sentinel.style.height = `${start_index * node_height}px` + } + } } +/****************************************************************************** + 7. FALLBACK CONFIGURATION + Provides default data and API for the component. +******************************************************************************/ function fallback_module() { return { api: fallback_instance @@ -537,7 +571,7 @@ function fallback_module() { align-items: center; white-space: nowrap; cursor: default; - height: 22px; /* Important for scroll calculation */ + height: 19px; /* Important for scroll calculation */ } .node.selected { background-color: #776346; @@ -588,7 +622,6 @@ function fallback_module() { } } } - }).call(this)}).call(this,"/lib/graph_explorer.js") },{"./STATE":1}],3:[function(require,module,exports){ const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index b062dca..02fd656 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -35,7 +35,7 @@ async function graph_explorer(opts) { let end_index = 0 const chunk_size = 50 const max_rendered_nodes = chunk_size * 3 - const node_height = 22 + const node_height = 19 const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') @@ -567,7 +567,7 @@ function fallback_module() { align-items: center; white-space: nowrap; cursor: default; - height: 22px; /* Important for scroll calculation */ + height: 19px; /* Important for scroll calculation */ } .node.selected { background-color: #776346; From 50d538c5116ebd098543800437e168387e10f642 Mon Sep 17 00:00:00 2001 From: Ahmad Munir <142005659+ddroid@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:53:35 +0500 Subject: [PATCH 020/130] Delete instructions.md --- instructions.md | 639 ------------------------------------------------ 1 file changed, 639 deletions(-) delete mode 100644 instructions.md diff --git a/instructions.md b/instructions.md deleted file mode 100644 index 1f36761..0000000 --- a/instructions.md +++ /dev/null @@ -1,639 +0,0 @@ -Hey! I'm a vanilla JavaScript developer who works on an open-source project named [UI components](https://github.com/ddroid/ui-components), Where I create ui-components for a decentralized app called **'Theme Widget'**. It's not my own project, I'm just a contributor. I want to tell you about how each Nodejs component is created so you can help me with creating some. - -# Guide to create modules: - -Here we would discuss the rules and a deep explanation of the steps for creating a module. - -## Here are some rules: -- We use StandardJS. -- We use snake_case and try to keep variable names concise. -- We use CommonJS. Which consist of `require` keyword for importing external modules. -- Furthermore, we use shadow DOM. -- We handle all the element creation through JavaScript and try to maximize the use of template literals while creating HTML elements. -- We try to keep the code modular by dividing the functionality into multiple functioned which are defined/placed always under the return statement of parent function and are used above, obviously. -- Likewise, we don't use `btn.addEventListner()` syntax. Instead, we use `btn.onclick = onclick_function` etc. -- We don't use JavaScript `classes` or `this` keyword. -- We use a module called `STATE` for state management and persistent browser storage. I Will discuss it in detail in a bit. -- We use bundlers `budo/browserify`. Our index.html is a small file that just includes the bundle.js script. -- Try to keep code as short as possible without compromising the readability and reusability. - -# Structure Explained: -Here is the structure that I would show you step by step. -## `example.js` -First 3 lines for each module are always same: -```js -const STATE = require('STATE') -const statedb = STATE(__filename) -const { sdb, get } = statedb(fallback_module) -``` -As you can see here we just require the `STATE` module and then execute it to create a state database function. This is then passed with a `fallback` function. - -You dont need to get deep into these first 2 lines. - ---- -A `submodule` is a module that is required by our current module. - -A `fallback_module` is a function which returns an object which contains 3 properties: - -- **_** : This defines the `submodules`. If there is no `submodule` used or `required`/`imported` in the current module then It is not defined (meaning it should not exist as a key. It should not be like `_:{}`. Instead, there should be nothing). **It is necessary to define when we do require a external module as a `submodule`.** -- **drive** : It is a place where we can store data which we want to be saved in localStorage. We store all the Styles, SVG's inside the drive. -- **api** : this defines another `fallback` function called `fallback_instance`. It is used to provide a developer who reuses our component with customization api to override our default data which is defined in `fallback_module`'s object. `fallback_instance` has obj returned with 2 properties ⇾ **_** and **drive**. ---- -#### The `_` property is very important. -It represents the submodules and instances. Any number of instances can be made from a single required module. -It is an object that is assigned to `_`. -Unlike `drive` (which has same structure in both `fallback_module` and `fallback_instance`) the stuctural syntax for **`_`** is a little different in `fallback_module` and `fallback_instance`. - ---- -In `fallback_module` we include the required module names as keys, and we assign an object to those keys which define the module by `$` key, This `$` property is mandatory what ever reason. We can create as many instances we want using `0,1,2,3,4,5,....,n` as keys of object that is passed to required module key. But mostly we use `fallback_instance` for creating instances. Anyways an example is: -```js -_: { - '../../module_address' : { - $ : '', // This is a must. we can either assign a string as a override which keeps the default data intact. Or we can specify an override function which would change the default fallbacks for each instance. - 0 : override_func, // we can assign a override to a sigle instance which will change the data only for this particular instance. - 1 : override_func2, // we can use same or different override functions for each instance. - 2 : '', // obviously we can also assign a empty string which would take data from $. and if $ also has and empty string then it defaults to orignal module data. - mapping: { - style: 'style' - } - } -} -``` -I have added the comments for explanation about `overrides`. - ---- -In `fallback_instance` the only difference is that we don't have a $ property for representing the module. - -That's why the `$` inside the `_` property of `fallback_module` is mandatory whether we use `fallback_instance` for creating instances or `fallback_module`. - - -There is another mandatory thing inside the **`_`** which is **`mapping`** property. It is always defined where we create Instances. - -If we create instance at module level then we would add it inside `_` of `fallback_module` but as most of the times we create instances through the `fallback_instance` add mapping there. - -Example: -```js - _: { - $: '', // only if we create module level instances - 0: '', - mapping: { - style: 'style' - } - } -``` ---- -Let's go back to drive. As discussed above that we place the data in **drive** which is supposed to be stored in localStorage of a browser. It is completely optional, we can ignore it if we want. The data we want to be customizable is stored in **api**'s **drive** (`fallback_instance`) and which is supposed to be not is stored in `fallback_module`'s drive. - -Drive is an JavaScript object. It contains datasets of different types or category. These categories can contain multiple files. -```json -drive: { - 'style/': { - 'theme.css': { - raw: ` - .element-class { - display: flex; - align-items: center; - background-color: #212121; - padding: 0.5rem; - // min-width: 456px - }` - } - } -} -``` -Now these datasets like `style/` can contain files and each file contains content using `raw:`. - -Another way of defining the content is by using `$ref:`. This is used when we want to use a file from the same directory as the module file. For example, if we want to require/import --> $ref `cross.svg` from directory of the module, we can do it like this : -```js -drive: { - 'style/': { - 'theme.css': { - '$ref': 'example.svg' - } - } -} -``` -This `$ref` can help a lot in short and clean `fallbacks` - ---- -### Back to where we left -After we have added those 3 lines of code, we can require modules to use them as `submodules` (if any). - -```js -const submodule1 = require('example_submodule') -``` - -Then we export our current module function. -```js -module.exports = module_function -``` -Then we define our function which is always async and always takes one `opts` parameter. -```js -async function module_function (opts) { - // Code -} -``` -Inside the function we start with this line: -```js - const { id, sdb } = await get(opts.sid) -``` -It fetches our `sdb` which is state database. -Now there is also a `sdb` in third line of the module i.e. -```js -const { sdb, get } = statedb(fallback_module) -``` -It is used when we use `fallback_module` to create instances. It is only used when we don't add this `const { id, sdb } = await get(opts.sid)` line to the module function. Most of the time we do add it as it's the backbone of customization `api`. I will share the exceptions in a bit. - -We should only add this line if we use `fallback_instance` to create instances. Which we mostly do. - ---- - -After that we create this object according to the datasets in drive. They will be helpful in executing certain functions when specific dataset is updated or changed. -```js - const on = { - style: inject - } -``` -This has a simple structure where key name is based of dataset and its value is the function we want to execute when that dataset changes. - ---- - -Then we start the real vanilla JavaScript journey for creating some useful HTML. -```js - const el = document.createElement('div') - const shadow = el.attachShadow({ mode: 'closed' }) - shadow.innerHTML = ` -
-
- - -
-
` -``` -As mentioned before that we make the maximum use of literals. We also use Shadow DOM with closed mode. - -We can also define some placeholder elements that we can later replace with a submodule. - ---- -Then the most important line of the `STATE` program comes. -```js - const subs = await sdb.watch(onbatch) -``` -This does two things. - -First is that it is a watch function which is like an event listener. It triggers the `onbatch()` whenever something in the drive changes. We would share the `onbatch()` code later. - -Second it stores `Sid`'s for all the submodules and their instances into the subs array. It gets then from `_` properties of `drive` from both fallbacks (instance and module). These `Sid`'s are passed as parameters to the `submodules`. - -The order of execution of functions by `onbatch` is not random. so we need to so we need to make sure that those functions work independent of order of execution. A strategy we can opt is to create elements at the end after storing all the data into variables and then using those variables for creating elements. - ---- - -After we get the `Sid`'s we can append the required submodules into our HTML elements. -```js - submodule1(subs[0]).then(el => shadow.querySelector('placeholder').replaceWith(el)) - - // to add a click event listener to the buttons: - // const [btn1, btn2, btn3] = shadow.querySelectorAll('button') - // btn1.onclick = () => { console.log('Terminal button clicked') }) -``` -We can also add event listeners if we want at this stage. As mentioned in rules we dont use `element.addEventListner()` syntax. - ---- - Then we return the `el`. The main element to which we have attached the shadow. - ```js - return el - ``` - This is the end of a clean code. We can add the real mess under this return statement. - - --- - -Then we define the functions used under the return statement. -```js - function onbatch (batch) { - for (const { type, data } of batch) { - on[type] && on[type](data) - } - // here we can create some elements after storing data - } - function inject(data) { - const sheet = new CSSStyleSheet() - sheet.replaceSync(data) - shadow.adoptedStyleSheets = [sheet] - } - function iconject(data) { - dricons = data[0] - // using data[0] to retrieve the first file from the dataset. - } - function some_useful_function (){ - // NON state function - } -``` -We add both `STATE` related and actual code related functions here. And finally after those we close our main module delimiters. - ---- -Last but not the least outside the main module function, we define the `fallback_module` - -It is placed at the last as it can be pretty long sometimes. - -```js -function fallback_module () { - return { - api: fallback_instance, - _: { - submodule1: { - $: '' - } - } - } - function fallback_instance () { - return { - _: { - submodule1: { - 0: '', - mapping: { - style: 'style' - } - } - }, - drive: { - 'style/': { - 'theme.css': { - raw: ` - .element-class { - display: flex; - align-items: center; - background-color: #212121; - padding: 0.5rem; - // min-width: 456px - } - ` - } - } - } - } - } -} -``` ---- -### Thus, here is the whole combined code: - -```js -const STATE = require('STATE') -const statedb = STATE(__filename) -const { sdb, get } = statedb(fallback_module) - -const submodule1 = require('example_submodule') - -module.exports = module_function - -async function module_function (opts) { - const { id, sdb } = await get(opts.sid) - const on = { - style: inject - } - const el = document.createElement('div') - const shadow = el.attachShadow({ mode: 'closed' }) - shadow.innerHTML = ` -
-
- - -
-
` - const subs = await sdb.watch(onbatch) - submodule1(subs[0]).then(el => shadow.querySelector('placeholder').replaceWith(el)) - - return el - function onbatch (batch) { - for (const { type, data } of batch) { - on[type] && on[type](data) - } - } - function inject(data) { - const sheet = new CSSStyleSheet() - sheet.replaceSync(data) - shadow.adoptedStyleSheets = [sheet] - } - function some_useful_function (){ - // NON state function - } -} -function fallback_module () { - return { - api: fallback_instance, - _: { - submodule1: { - $: '' - } - } - } - function fallback_instance () { - return { - _: { - submodule1: { - 0: '', - mapping: { - style: 'style' - } - } - }, - drive: { - 'style/': { - 'theme.css': { - raw: ` - .element-class { - display: flex; - align-items: center; - background-color: #212121; - padding: 0.5rem; - // min-width: 456px - } - ` - } - } - } - } - } -} -``` -# Latest and greatest example -## `tabs.js` - -This is another example which I think does not need much explanation. But still if you have any questions let me know. - -```js -//state Initialization -const STATE = require('STATE') -const statedb = STATE(__filename) -const { sdb, get } = statedb(fallback_module) -// exporting the module -module.exports = component -// actual module -async function component(opts, protocol) { - // getting the state database for the current instance - const { id, sdb } = await get(opts.sid) - // optional getting drive from state database but it does not work currently. will be useful in the future though. - const {drive} = sdb - // on object which contains the functions to be executed when the dataset changes and onbatch is called. - const on = { - variables: onvariables, - style: inject_style, - icons: iconject, - scroll: onscroll - } - // creating the main element and attaching shadow DOM to it. - const div = document.createElement('div') - const shadow = div.attachShadow({ mode: 'closed' }) - // defining the HTML structure of the component using template literals. - shadow.innerHTML = `
` - const entries = shadow.querySelector('.tab-entries') - // Initializing the variables to be used in the element creation. We store the data from drive through the onbatch function in these variables. - // this init variable is used to check if the component is initialized or not. It is set to true when the component is initialized for the first time. So that after that we can just update the component instead of creating it again using the onbatch function data. - let init = false - let variables = [] - let dricons = [] - - // subs for storing the Sid's of submodules and onbatch function which is called when the dataset changes. - const subs = await sdb.watch(onbatch) - // this is just a custom scrolling through drag clicking functionality. - if (entries) { - let is_down = false - let start_x - let scroll_start - - const stop = () => { - is_down = false - entries.classList.remove('grabbing') - update_scroll_position() - } - - const move = x => { - if (!is_down) return - if (entries.scrollWidth <= entries.clientWidth) return stop() - entries.scrollLeft = scroll_start - (x - start_x) * 1.5 - } - - entries.onmousedown = e => { - if (entries.scrollWidth <= entries.clientWidth) return - is_down = true - entries.classList.add('grabbing') - start_x = e.pageX - entries.offsetLeft - scroll_start = entries.scrollLeft - window.onmousemove = e => { - move(e.pageX - entries.offsetLeft) - e.preventDefault() - } - window.onmouseup = () => { - stop() - window.onmousemove = window.onmouseup = null - } - } - - entries.onmouseleave = stop - - entries.ontouchstart = e => { - if (entries.scrollWidth <= entries.clientWidth) return - is_down = true - start_x = e.touches[0].pageX - entries.offsetLeft - scroll_start = entries.scrollLeft - } - ;['ontouchend', 'ontouchcancel'].forEach(ev => { - entries[ev] = stop - }) - - entries.ontouchmove = e => { - move(e.touches[0].pageX - entries.offsetLeft) - e.preventDefault() - } - } - // component function returns the main element. - return div - // All the functions are defined below this return statement. - // this create_btn function is executed using forEach on the variables array. It creates the buttons for each variable in the array. It uses the data from the variables and dricons arrays to create the buttons. - async function create_btn({ name, id }, index) { - const el = document.createElement('div') - el.innerHTML = ` - ${dricons[index + 1]} - ${id} - ${name} - ` - - el.className = 'tabsbtn' - const icon_el = el.querySelector('.icon') - const label_el = el.querySelector('.name') - - label_el.draggable = false - // Event listener for the button click. It uses the protocol function to send a message to the parent component. The parent can further handle the message using the protocol function to route the message to the appropriate destination. - icon_el.onclick = protocol(onmessage)('type','data') - entries.appendChild(el) - return - } - function onmessage(type, data) { - return console.log(type,data) - } - // this function is called when the dataset changes. It calls the functions defined in `on` object. - function onbatch (batch) { - for (const { type, data } of batch) (on[type] || fail)(data, type) - // this condition checks if the component is initialized or not. If not then it creates the buttons using the create_btn function. if the component is already initialized then it can handle the updates to the drive in future. - if (!init) { - // after for loop ends and each of the data is stored in their respective variables, we can create the buttons using the create_btn function. - variables.forEach(create_btn) - init = true - } else { - // TODO: Here we can handle drive updates - // currently waiting for the next STATE module to be released so we can use the drive updates. - } - } - // this function throws an error if the type of data is not valid. It is used to handle the errors in the onbatch function. - function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } - // this function adds styles to shadow DOM. It uses the CSSStyleSheet API to create a new stylesheet and then replaces the existing stylesheet with the new one. - function inject_style(data) { - const sheet = new CSSStyleSheet() - sheet.replaceSync(data) - shadow.adoptedStyleSheets = [sheet] - } - // we simple store the data from the dataset into variables. We can use this data to create the buttons in the create_btn function. - function onvariables(data) { - const vars = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] - variables = vars - } - // same here we store the data into dricons for later use. We can use this data to create the buttons in the create_btn function. - function iconject(data) { - dricons = data - } - // waiting for the next STATE module to be released so we can use the drive.put() to update the scroll position. - function update_scroll_position() { - // TODO - } - - function onscroll(data) { - setTimeout(() => { - if (entries) { - entries.scrollLeft = data - } - }, 200) - } -} -// this is the fallback module which is used to create the state database and to provide the default data for the component. -function fallback_module() { - return { - api: fallback_instance, - } - // this is the fallback instance which is used to provide the default data for the instances of a component. this also help in providing an API for csustomization by overriding the default data. - function fallback_instance() { - return { - drive: { - 'icons/': { - 'cross.svg':{ - '$ref': 'cross.svg' - // data is stored through '$ref' functionality - }, - '1.svg': { - '$ref': 'icon.svg' - }, - '2.svg': { - '$ref': 'icon.svg' - }, - '3.svg': { - '$ref': 'icon.svg' - } - }, - 'variables/': { - 'tabs.json': { - '$ref': 'tabs.json' - } - }, - 'scroll/': { - 'position.json': { - raw: '100' - } - }, - 'style/': { - 'theme.css': { - '$ref': 'style.css' - } - } - } - } - } -} -``` - -# Important update related to drive fetch, ignore old code above -### State Management -The `STATE` module provides several key features for state management: - -#### 1. Instance Isolation - - Each instance of a module gets its own isolated state - - State is accessed through the `sdb` interface - - Instances can be created and destroyed independently - -#### 2. sdb Interface -Provides access to following two APIs: - -**sdb.watch(onbatch)** -```js -const subs = await sdb.watch(onbatch) -const { drive } = sdb -async function onbatch(batch){ - for (const {type, paths} of batch) { - const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) - on[type] && on[type](data) - } -} -``` -- Modules can watch for state changes -- Changes are batched and processed through the `onbatch` handler -- Different types of changes can be handled separately using `on`. -- `type` refers to the `dataset_type` used in fallbacks. The key names need to match. E.g. see `template.js` -- `paths` refers to the paths to the files inside the dataset. - -**sdb.get_sub** - @TODO -**sdb.drive** -The `sdb.drive` object provides an interface for managing datasets and files attached to the current node. It allows you to list, retrieve, add, and check files within datasets defined in the module's state. - -- **sdb.drive.list(path?)** - - Lists all dataset names (as folders) attached to the current node. - - If a `path` (dataset name) is provided, returns the list of file names within that dataset. - - Example: - ```js - const datasets = sdb.drive.list(); // ['mydata/', 'images/'] - const files = sdb.drive.list('mydata/'); // ['file1.json', 'file2.txt'] - ``` - -- **sdb.drive.get(path)** - - Retrieves a file object from a dataset. - - `path` should be in the format `'dataset_name/filename.ext'`. - - Returns an object: `{ id, name, type, raw }` or `null` if not found. - - Example: - ```js - const file = sdb.drive.get('mydata/file1.json'); - // file: { id: '...', name: 'file1.json', type: 'json', raw: ... } - ``` - -- **sdb.drive.put(path, buffer)** - - Adds a new file to a dataset. - - `path` is `'dataset_name/filename.ext'`. - - `buffer` is the file content (object, string, etc.). - - Returns the created file object: `{ id, name, type, raw }`. - - Example: - ```js - sdb.drive.put('mydata/newfile.txt', 'Hello World'); - ``` - -- **sdb.drive.has(path)** - - Checks if a file exists in a dataset. - - `path` is `'dataset_name/filename.ext'`. - - Returns `true` if the file exists, otherwise `false`. - - Example: - ```js - if (sdb.drive.has('mydata/file1.json')) { /* ... */ } - ``` - -**Notes:** -- Dataset names are defined in the fallback structure and must be unique within a node. -- File types are inferred from the file extension. -- All file operations are isolated to the current node's state and changes are persisted immediately. - From 247e8003d55620b6791718c1016e8ebb75b54dff Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 26 Jul 2025 17:51:24 +0500 Subject: [PATCH 021/130] tweaked box drawing logic and styles --- lib/graph_explorer.js | 52 ++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 02fd656..a448e46 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -11,7 +11,9 @@ async function graph_explorer(opts) { ******************************************************************************/ const { sdb } = await get(opts.sid) const { drive } = sdb - await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) + + // await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) + let vertical_scroll_value = 0 let horizontal_scroll_value = 0 let selected_instance_paths = [] @@ -306,12 +308,12 @@ async function graph_explorer(opts) { const has_subs = entry.subs && entry.subs.length > 0 if (depth) { - el.style.paddingLeft = '20px' + el.style.paddingLeft = '17.5px' } if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state - const prefix_symbol = expanded_subs ? '┬' : '─' + const prefix_symbol = expanded_subs ? '┳' : '━' const prefix_class = has_subs ? 'prefix clickable' : 'prefix' el.innerHTML = `
🪄
${prefix_symbol}/🌐` el.querySelector('.wand').onclick = reset @@ -323,7 +325,7 @@ async function graph_explorer(opts) { } const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) - const pipe_html = pipe_trail.map(should_pipe => `${should_pipe ? '│' : ' '}`).join('') + const pipe_html = pipe_trail.map(should_pipe => `${should_pipe ? '┃' : ' '}`).join('') const prefix_class = has_subs ? 'prefix clickable' : 'prefix' const icon_class = (has_hubs && base_path !== '/') ? 'icon clickable' : 'icon' @@ -363,28 +365,28 @@ async function graph_explorer(opts) { function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { const { expanded_subs, expanded_hubs } = state - if (is_hub) { + if (is_hub) { if (is_hub_on_top) { - if (expanded_subs && expanded_hubs) return '┌┼' - if (expanded_subs) return '┌┬' - if (expanded_hubs) return '┌┴' - return '┌─' + if (expanded_subs && expanded_hubs) return '┏╋' + if (expanded_subs) return '┏┳' + if (expanded_hubs) return '┏┻' + return '┏━' } else { - if (expanded_subs && expanded_hubs) return '├┼' - if (expanded_subs) return '├┬' - if (expanded_hubs) return '├┴' - return '├─' + if (expanded_subs && expanded_hubs) return '┣╋' + if (expanded_subs) return '┣┳' + if (expanded_hubs) return '┣┻' + return '┣━' } } else if (is_last_sub) { - if (expanded_subs && expanded_hubs) return '└┼' - if (expanded_subs) return '└┬' - if (expanded_hubs) return '└┴' - return '└─' + if (expanded_subs && expanded_hubs) return '┗╋' + if (expanded_subs) return '┗┳' + if (expanded_hubs) return has_subs ? '┗┻' : '┖┸' + return has_subs ? '┗━' : '┖─' } else { - if (expanded_subs && expanded_hubs) return '├┼' - if (expanded_subs) return '├┬' - if (expanded_hubs) return '├┴' - return '├─' + if (expanded_subs && expanded_hubs) return '┣╋' + if (expanded_subs) return '┣┳' + if (expanded_hubs) return has_subs ? '┣┻' : '┠┸' + return has_subs ? '┣━' : '┠─' } } @@ -428,6 +430,7 @@ async function graph_explorer(opts) { update_runtime_state('selected_instance_paths', new_selected_paths) update_runtime_state('confirmed_selected', new_confirmed_paths) } + function toggle_subs(instance_path) { const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs @@ -555,6 +558,9 @@ function fallback_module() { 'style/': { 'theme.css': { raw: ` + .graph-container, .node { + font-family: monospace; + } .graph-container { color: #abb2bf; background-color: #282c34; @@ -567,7 +573,7 @@ function fallback_module() { align-items: center; white-space: nowrap; cursor: default; - height: 19px; /* Important for scroll calculation */ + height: 16px; /* Important for scroll calculation */ } .node.selected { background-color: #776346; @@ -586,7 +592,7 @@ function fallback_module() { text-align: center; } .blank { - width: 10px; + width: 8.5px; text-align: center; } .clickable { From 901f900edf729f722a6e7608b20ceb86e451d0a3 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 26 Jul 2025 18:23:05 +0500 Subject: [PATCH 022/130] moved prefix to CSS --- lib/graph_explorer.js | 65 ++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index a448e46..1272cb1 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -313,9 +313,9 @@ async function graph_explorer(opts) { if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state - const prefix_symbol = expanded_subs ? '┳' : '━' + const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' const prefix_class = has_subs ? 'prefix clickable' : 'prefix' - el.innerHTML = `
🪄
${prefix_symbol}/🌐` + el.innerHTML = `
🪄
/🌐` el.querySelector('.wand').onclick = reset if (has_subs) { el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) @@ -324,15 +324,15 @@ async function graph_explorer(opts) { return el } - const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) - const pipe_html = pipe_trail.map(should_pipe => `${should_pipe ? '┃' : ' '}`).join('') + const prefix_class_name = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) + const pipe_html = pipe_trail.map(should_pipe => ``).join('') const prefix_class = has_subs ? 'prefix clickable' : 'prefix' const icon_class = (has_hubs && base_path !== '/') ? 'icon clickable' : 'icon' el.innerHTML = ` ${pipe_html} - ${prefix_symbol} + ${entry.name} ` @@ -365,28 +365,28 @@ async function graph_explorer(opts) { function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { const { expanded_subs, expanded_hubs } = state - if (is_hub) { + if (is_hub) { if (is_hub_on_top) { - if (expanded_subs && expanded_hubs) return '┏╋' - if (expanded_subs) return '┏┳' - if (expanded_hubs) return '┏┻' - return '┏━' + if (expanded_subs && expanded_hubs) return 'top-cross' + if (expanded_subs) return 'top-tee-down' + if (expanded_hubs) return 'top-tee-up' + return 'top-line' } else { - if (expanded_subs && expanded_hubs) return '┣╋' - if (expanded_subs) return '┣┳' - if (expanded_hubs) return '┣┻' - return '┣━' + if (expanded_subs && expanded_hubs) return 'middle-cross' + if (expanded_subs) return 'middle-tee-down' + if (expanded_hubs) return 'middle-tee-up' + return 'middle-line' } } else if (is_last_sub) { - if (expanded_subs && expanded_hubs) return '┗╋' - if (expanded_subs) return '┗┳' - if (expanded_hubs) return has_subs ? '┗┻' : '┖┸' - return has_subs ? '┗━' : '┖─' + if (expanded_subs && expanded_hubs) return 'bottom-cross' + if (expanded_subs) return 'bottom-tee-down' + if (expanded_hubs) return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' + return has_subs ? 'bottom-line' : 'bottom-light-line' } else { - if (expanded_subs && expanded_hubs) return '┣╋' - if (expanded_subs) return '┣┳' - if (expanded_hubs) return has_subs ? '┣┻' : '┠┸' - return has_subs ? '┣━' : '┠─' + if (expanded_subs && expanded_hubs) return 'middle-cross' + if (expanded_subs) return 'middle-tee-down' + if (expanded_hubs) return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' + return has_subs ? 'middle-line' : 'middle-light-line' } } @@ -591,6 +591,7 @@ function fallback_module() { .pipe { text-align: center; } + .pipe::before { content: '┃'; } .blank { width: 8.5px; text-align: center; @@ -601,6 +602,24 @@ function fallback_module() { .prefix, .icon { margin-right: 6px; } + .top-cross::before { content: '┏╋'; } + .top-tee-down::before { content: '┏┳'; } + .top-tee-up::before { content: '┏┻'; } + .top-line::before { content: '┏━'; } + .middle-cross::before { content: '┣╋'; } + .middle-tee-down::before { content: '┣┳'; } + .middle-tee-up::before { content: '┣┻'; } + .middle-line::before { content: '┣━'; } + .bottom-cross::before { content: '┗╋'; } + .bottom-tee-down::before { content: '┗┳'; } + .bottom-tee-up::before { content: '┗┻'; } + .bottom-line::before { content: '┗━'; } + .bottom-light-tee-up::before { content: '┖┸'; } + .bottom-light-line::before { content: '┖─'; } + .middle-light-tee-up::before { content: '┠┸'; } + .middle-light-line::before { content: '┠─'; } + .tee-down::before { content: '┳'; } + .line-h::before { content: '━'; } .icon { display: inline-block; text-align: center; } .name { flex-grow: 1; } .node.type-root > .icon::before { content: '🌐'; } @@ -623,4 +642,4 @@ function fallback_module() { } } } -} \ No newline at end of file +} From 7c903ce6a63e38aa8d74230c6f7629606a5c1a78 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 26 Jul 2025 19:14:06 +0500 Subject: [PATCH 023/130] fixed fast scroll issue --- lib/graph_explorer.js | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 1272cb1..0c5a318 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -22,6 +22,7 @@ async function graph_explorer(opts) { let instance_states = {} let view = [] let drive_updated_by_scroll = false + let is_rendering = false const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -44,6 +45,7 @@ async function graph_explorer(opts) { const observer = new IntersectionObserver(handle_sentinel_intersection, { root: container, + rootMargin: '500px 0px', threshold: 0 }) const on = { @@ -481,11 +483,37 @@ async function graph_explorer(opts) { }) } + async function fill_viewport_downwards() { + if (is_rendering) return + is_rendering = true + const container_rect = container.getBoundingClientRect() + let sentinel_rect = bottom_sentinel.getBoundingClientRect() + while (end_index < view.length && sentinel_rect.top < container_rect.bottom + 500) { + render_next_chunk() + await new Promise(resolve => requestAnimationFrame(resolve)) + sentinel_rect = bottom_sentinel.getBoundingClientRect() + } + is_rendering = false + } + + async function fill_viewport_upwards() { + if (is_rendering) return + is_rendering = true + const container_rect = container.getBoundingClientRect() + let sentinel_rect = top_sentinel.getBoundingClientRect() + while (start_index > 0 && sentinel_rect.bottom > container_rect.top - 500) { + render_prev_chunk() + await new Promise(resolve => requestAnimationFrame(resolve)) + sentinel_rect = top_sentinel.getBoundingClientRect() + } + is_rendering = false + } + function handle_sentinel_intersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { - if (entry.target === top_sentinel) render_prev_chunk() - else if (entry.target === bottom_sentinel) render_next_chunk() + if (entry.target === top_sentinel) fill_viewport_upwards() + else if (entry.target === bottom_sentinel) fill_viewport_downwards() } }) } @@ -600,7 +628,7 @@ function fallback_module() { cursor: pointer; } .prefix, .icon { - margin-right: 6px; + margin-right: 2px; } .top-cross::before { content: '┏╋'; } .top-tee-down::before { content: '┏┳'; } From 38ed6a8bfeca6b4bdceb7e9b50677f4f8a41cb79 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 26 Jul 2025 21:40:02 +0500 Subject: [PATCH 024/130] bundled --- bundle.js | 110 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 28 deletions(-) diff --git a/bundle.js b/bundle.js index 7b628d2..c4db243 100644 --- a/bundle.js +++ b/bundle.js @@ -15,7 +15,9 @@ async function graph_explorer(opts) { ******************************************************************************/ const { sdb } = await get(opts.sid) const { drive } = sdb - await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) + + // await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) + let vertical_scroll_value = 0 let horizontal_scroll_value = 0 let selected_instance_paths = [] @@ -24,6 +26,7 @@ async function graph_explorer(opts) { let instance_states = {} let view = [] let drive_updated_by_scroll = false + let is_rendering = false const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -46,6 +49,7 @@ async function graph_explorer(opts) { const observer = new IntersectionObserver(handle_sentinel_intersection, { root: container, + rootMargin: '500px 0px', threshold: 0 }) const on = { @@ -310,14 +314,14 @@ async function graph_explorer(opts) { const has_subs = entry.subs && entry.subs.length > 0 if (depth) { - el.style.paddingLeft = '20px' + el.style.paddingLeft = '17.5px' } if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state - const prefix_symbol = expanded_subs ? '┬' : '─' + const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' const prefix_class = has_subs ? 'prefix clickable' : 'prefix' - el.innerHTML = `
🪄
${prefix_symbol}/🌐` + el.innerHTML = `
🪄
/🌐` el.querySelector('.wand').onclick = reset if (has_subs) { el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) @@ -326,15 +330,15 @@ async function graph_explorer(opts) { return el } - const prefix_symbol = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) - const pipe_html = pipe_trail.map(should_pipe => `${should_pipe ? '│' : ' '}`).join('') + const prefix_class_name = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) + const pipe_html = pipe_trail.map(should_pipe => ``).join('') const prefix_class = has_subs ? 'prefix clickable' : 'prefix' const icon_class = (has_hubs && base_path !== '/') ? 'icon clickable' : 'icon' el.innerHTML = ` ${pipe_html} - ${prefix_symbol} + ${entry.name} ` @@ -369,26 +373,26 @@ async function graph_explorer(opts) { const { expanded_subs, expanded_hubs } = state if (is_hub) { if (is_hub_on_top) { - if (expanded_subs && expanded_hubs) return '┌┼' - if (expanded_subs) return '┌┬' - if (expanded_hubs) return '┌┴' - return '┌─' + if (expanded_subs && expanded_hubs) return 'top-cross' + if (expanded_subs) return 'top-tee-down' + if (expanded_hubs) return 'top-tee-up' + return 'top-line' } else { - if (expanded_subs && expanded_hubs) return '├┼' - if (expanded_subs) return '├┬' - if (expanded_hubs) return '├┴' - return '├─' + if (expanded_subs && expanded_hubs) return 'middle-cross' + if (expanded_subs) return 'middle-tee-down' + if (expanded_hubs) return 'middle-tee-up' + return 'middle-line' } } else if (is_last_sub) { - if (expanded_subs && expanded_hubs) return '└┼' - if (expanded_subs) return '└┬' - if (expanded_hubs) return '└┴' - return '└─' + if (expanded_subs && expanded_hubs) return 'bottom-cross' + if (expanded_subs) return 'bottom-tee-down' + if (expanded_hubs) return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' + return has_subs ? 'bottom-line' : 'bottom-light-line' } else { - if (expanded_subs && expanded_hubs) return '├┼' - if (expanded_subs) return '├┬' - if (expanded_hubs) return '├┴' - return '├─' + if (expanded_subs && expanded_hubs) return 'middle-cross' + if (expanded_subs) return 'middle-tee-down' + if (expanded_hubs) return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' + return has_subs ? 'middle-line' : 'middle-light-line' } } @@ -432,6 +436,7 @@ async function graph_explorer(opts) { update_runtime_state('selected_instance_paths', new_selected_paths) update_runtime_state('confirmed_selected', new_confirmed_paths) } + function toggle_subs(instance_path) { const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs @@ -482,11 +487,37 @@ async function graph_explorer(opts) { }) } + async function fill_viewport_downwards() { + if (is_rendering) return + is_rendering = true + const container_rect = container.getBoundingClientRect() + let sentinel_rect = bottom_sentinel.getBoundingClientRect() + while (end_index < view.length && sentinel_rect.top < container_rect.bottom + 500) { + render_next_chunk() + await new Promise(resolve => requestAnimationFrame(resolve)) + sentinel_rect = bottom_sentinel.getBoundingClientRect() + } + is_rendering = false + } + + async function fill_viewport_upwards() { + if (is_rendering) return + is_rendering = true + const container_rect = container.getBoundingClientRect() + let sentinel_rect = top_sentinel.getBoundingClientRect() + while (start_index > 0 && sentinel_rect.bottom > container_rect.top - 500) { + render_prev_chunk() + await new Promise(resolve => requestAnimationFrame(resolve)) + sentinel_rect = top_sentinel.getBoundingClientRect() + } + is_rendering = false + } + function handle_sentinel_intersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { - if (entry.target === top_sentinel) render_prev_chunk() - else if (entry.target === bottom_sentinel) render_next_chunk() + if (entry.target === top_sentinel) fill_viewport_upwards() + else if (entry.target === bottom_sentinel) fill_viewport_downwards() } }) } @@ -559,6 +590,9 @@ function fallback_module() { 'style/': { 'theme.css': { raw: ` + .graph-container, .node { + font-family: monospace; + } .graph-container { color: #abb2bf; background-color: #282c34; @@ -571,7 +605,7 @@ function fallback_module() { align-items: center; white-space: nowrap; cursor: default; - height: 19px; /* Important for scroll calculation */ + height: 16px; /* Important for scroll calculation */ } .node.selected { background-color: #776346; @@ -589,16 +623,35 @@ function fallback_module() { .pipe { text-align: center; } + .pipe::before { content: '┃'; } .blank { - width: 10px; + width: 8.5px; text-align: center; } .clickable { cursor: pointer; } .prefix, .icon { - margin-right: 6px; + margin-right: 2px; } + .top-cross::before { content: '┏╋'; } + .top-tee-down::before { content: '┏┳'; } + .top-tee-up::before { content: '┏┻'; } + .top-line::before { content: '┏━'; } + .middle-cross::before { content: '┣╋'; } + .middle-tee-down::before { content: '┣┳'; } + .middle-tee-up::before { content: '┣┻'; } + .middle-line::before { content: '┣━'; } + .bottom-cross::before { content: '┗╋'; } + .bottom-tee-down::before { content: '┗┳'; } + .bottom-tee-up::before { content: '┗┻'; } + .bottom-line::before { content: '┗━'; } + .bottom-light-tee-up::before { content: '┖┸'; } + .bottom-light-line::before { content: '┖─'; } + .middle-light-tee-up::before { content: '┠┸'; } + .middle-light-line::before { content: '┠─'; } + .tee-down::before { content: '┳'; } + .line-h::before { content: '━'; } .icon { display: inline-block; text-align: center; } .name { flex-grow: 1; } .node.type-root > .icon::before { content: '🌐'; } @@ -622,6 +675,7 @@ function fallback_module() { } } } + }).call(this)}).call(this,"/lib/graph_explorer.js") },{"./STATE":1}],3:[function(require,module,exports){ const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' From 4e67aae0edd07fcddbba558b08e75b738d8aafcf Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 27 Jul 2025 20:20:24 +0500 Subject: [PATCH 025/130] added error throwing inside console for debugging --- lib/graph_explorer.js | 244 +++++++++++++++++++++++++++++++----------- 1 file changed, 184 insertions(+), 60 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 0c5a318..7105a70 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -12,8 +12,6 @@ async function graph_explorer(opts) { const { sdb } = await get(opts.sid) const { drive } = sdb - // await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) - let vertical_scroll_value = 0 let horizontal_scroll_value = 0 let selected_instance_paths = [] @@ -29,6 +27,7 @@ async function graph_explorer(opts) { const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = `
` const container = shadow.querySelector('.graph-container') + document.body.style.margin = 0 let scroll_update_pending = false @@ -38,7 +37,7 @@ async function graph_explorer(opts) { let end_index = 0 const chunk_size = 50 const max_rendered_nodes = chunk_size * 3 - const node_height = 19 + const node_height = 16 const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') @@ -67,49 +66,119 @@ async function graph_explorer(opts) { return } for (const { type, paths } of batch) { - const data = await Promise.all(paths.map(path => drive.get(path).then(file => file ? file.raw : null))) - const func = on[type] || fail - func(data, type, paths) + if (!paths || paths.length === 0) continue + const data = await Promise.all(paths.map(async (path) => { + try { + const file = await drive.get(path) + return file ? file.raw : null + } catch (e) { + console.error(`Error getting file from drive: ${path}`, e) + return null + } + })) + + const func = on[type] + func ? func(data, type, paths) : fail(data, type) } } - function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } + function fail (data, type) { throw new Error(`Invalid message type: ${type}`, { cause: { data, type } }) } function on_entries(data) { - all_entries = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + if (!data || data[0] === null || data[0] === undefined) { + console.error('Entries data is missing or empty.') + all_entries = {} + return + } + try { + const parsed_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + if (typeof parsed_data !== 'object' || parsed_data === null) { + console.error('Parsed entries data is not a valid object.') + all_entries = {} + return + } + all_entries = parsed_data + } catch (e) { + console.error('Failed to parse entries data:', e) + all_entries = {} + return + } + const root_path = '/' if (all_entries[root_path]) { const root_instance_path = '|/' if (!instance_states[root_instance_path]) { instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } - } else { - build_and_render_view() } + build_and_render_view() + } else { + console.warn('Root path "/" not found in entries. Clearing view.') + view = [] + if (container) container.replaceChildren() } } function on_runtime (data, type, paths) { + let needs_render = false + const render_nodes_needed = new Set() + for (let i = 0; i < paths.length; i++) { const path = paths[i] if (data[i] === null) continue - const value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] - if (path.endsWith('vertical_scroll_value.json')) vertical_scroll_value = value - else if (path.endsWith('horizontal_scroll_value.json')) horizontal_scroll_value = value - else if (path.endsWith('selected_instance_paths.json')) { - const old_paths = [...selected_instance_paths] - selected_instance_paths = value || [] - const changed_paths = [...new Set([...old_paths, ...selected_instance_paths])] - changed_paths.forEach(re_render_node) - } else if (path.endsWith('confirmed_selected.json')) { - const old_paths = [...confirmed_instance_paths] - confirmed_instance_paths = value || [] - const changed_paths = [...new Set([...old_paths, ...confirmed_instance_paths])] - changed_paths.forEach(re_render_node) - } else if (path.endsWith('instance_states.json')) { - instance_states = value - build_and_render_view() + + let value + try { + value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] + } catch (e) { + console.error(`Failed to parse JSON for ${path}:`, e) + continue + } + + switch (true) { + case path.endsWith('vertical_scroll_value.json'): + if (typeof value === 'number') vertical_scroll_value = value + break + case path.endsWith('horizontal_scroll_value.json'): + if (typeof value === 'number') horizontal_scroll_value = value + break + case path.endsWith('selected_instance_paths.json'): { + const old_paths = [...selected_instance_paths] + if (Array.isArray(value)) { + selected_instance_paths = value + } else { + console.warn('selected_instance_paths is not an array, defaulting to empty.', value) + selected_instance_paths = [] + } + const changed_paths = [...new Set([...old_paths, ...selected_instance_paths])] + changed_paths.forEach(p => render_nodes_needed.add(p)) + break + } + case path.endsWith('confirmed_selected.json'): { + const old_paths = [...confirmed_instance_paths] + if (Array.isArray(value)) { + confirmed_instance_paths = value + } else { + console.warn('confirmed_selected is not an array, defaulting to empty.', value) + confirmed_instance_paths = [] + } + const changed_paths = [...new Set([...old_paths, ...confirmed_instance_paths])] + changed_paths.forEach(p => render_nodes_needed.add(p)) + break + } + case path.endsWith('instance_states.json'): + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + instance_states = value + needs_render = true + } else console.warn('instance_states is not a valid object, ignoring.', value) + break } } + + if (needs_render) { + build_and_render_view() + } else if (render_nodes_needed.size > 0) { + render_nodes_needed.forEach(re_render_node) + } } function inject_style(data) { @@ -119,7 +188,11 @@ async function graph_explorer(opts) { } async function update_runtime_state (name, value) { - await drive.put(`runtime/${name}.json`, JSON.stringify(value)) + try { + await drive.put(`runtime/${name}.json`, JSON.stringify(value)) + } catch (e) { + console.error(`Failed to update runtime state for ${name}:`, e) + } } /****************************************************************************** @@ -127,6 +200,10 @@ async function graph_explorer(opts) { Functions for building and rendering the graph view. ******************************************************************************/ function build_and_render_view(focal_instance_path) { + if (Object.keys(all_entries).length === 0) { + console.warn('No entries available to render.') + return + } const old_view = [...view] const old_scroll_top = vertical_scroll_value const old_scroll_left = horizontal_scroll_value @@ -170,7 +247,8 @@ async function graph_explorer(opts) { const fragment = document.createDocumentFragment() for (let i = start_index; i < end_index; i++) { - fragment.appendChild(create_node(view[i])) + if (view[i]) fragment.appendChild(create_node(view[i])) + else console.warn(`Missing node at index ${i} in view.`) } container.replaceChildren() @@ -244,7 +322,7 @@ async function graph_explorer(opts) { } let current_view = [] - if (state.expanded_hubs && entry.hubs) { + if (state.expanded_hubs && Array.isArray(entry.hubs)) { entry.hubs.forEach((hub_path, i, arr) => { current_view = current_view.concat( build_view_recursive({ @@ -273,7 +351,7 @@ async function graph_explorer(opts) { is_hub_on_top }) - if (state.expanded_subs && entry.subs) { + if (state.expanded_subs && Array.isArray(entry.subs)) { entry.subs.forEach((sub_path, i, arr) => { current_view = current_view.concat( build_view_recursive({ @@ -299,15 +377,29 @@ async function graph_explorer(opts) { function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { const entry = all_entries[base_path] - const state = instance_states[instance_path] + if (!entry) { + console.error(`Entry not found for path: ${base_path}. Cannot create node.`) + const err_el = document.createElement('div') + err_el.className = 'node error' + err_el.textContent = `Error: Missing entry for ${base_path}` + return err_el + } + + let state = instance_states[instance_path] + if (!state) { + console.warn(`State not found for instance: ${instance_path}. Using default.`) + state = { expanded_subs: false, expanded_hubs: false } + instance_states[instance_path] = state + } + const el = document.createElement('div') - el.className = `node type-${entry.type}` + el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') - const has_hubs = entry.hubs && entry.hubs.length > 0 - const has_subs = entry.subs && entry.subs.length > 0 + const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 + const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 if (depth) { el.style.paddingLeft = '17.5px' @@ -318,11 +410,18 @@ async function graph_explorer(opts) { const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' const prefix_class = has_subs ? 'prefix clickable' : 'prefix' el.innerHTML = `
🪄
/🌐` - el.querySelector('.wand').onclick = reset + + const wand_el = el.querySelector('.wand') + if (wand_el) wand_el.onclick = reset + if (has_subs) { - el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) + const prefix_el = el.querySelector('.prefix') + if (prefix_el) prefix_el.onclick = () => toggle_subs(instance_path) } - el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) + + const name_el = el.querySelector('.name') + if (name_el) name_el.onclick = (ev) => select_node(ev, instance_path, base_path) + return el } @@ -336,18 +435,29 @@ async function graph_explorer(opts) { ${pipe_html} - ${entry.name} + ${entry.name || base_path} ` - if(has_hubs && base_path !== '/') el.querySelector('.icon').onclick = () => toggle_hubs(instance_path) - if(has_subs) el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) - el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) + + if(has_hubs && base_path !== '/') { + const icon_el = el.querySelector('.icon') + if (icon_el) icon_el.onclick = () => toggle_hubs(instance_path) + } + + if(has_subs) { + const prefix_el = el.querySelector('.prefix') + if (prefix_el) prefix_el.onclick = () => toggle_subs(instance_path) + } + + const name_el = el.querySelector('.name') + if (name_el) name_el.onclick = (ev) => select_node(ev, instance_path, base_path) if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { const checkbox_div = document.createElement('div') checkbox_div.className = 'confirm-wrapper' const is_confirmed = confirmed_instance_paths.includes(instance_path) checkbox_div.innerHTML = `` - checkbox_div.querySelector('input').onchange = (ev) => handle_confirm(ev, instance_path) + const checkbox_input = checkbox_div.querySelector('input') + if (checkbox_input) checkbox_input.onchange = (ev) => handle_confirm(ev, instance_path) el.appendChild(checkbox_div) } @@ -357,15 +467,19 @@ async function graph_explorer(opts) { function re_render_node (instance_path) { const node_data = view.find(n => n.instance_path === instance_path) if (node_data) { - const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) - if (old_node_el) { - const new_node_el = create_node(node_data) - old_node_el.replaceWith(new_node_el) - } + const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (old_node_el) { + const new_node_el = create_node(node_data) + old_node_el.replaceWith(new_node_el) + } } } function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { + if (!state) { + console.error('get_prefix called with invalid state.') + return 'middle-line' + } const { expanded_subs, expanded_hubs } = state if (is_hub) { if (is_hub_on_top) { @@ -396,7 +510,7 @@ async function graph_explorer(opts) { 5. VIEW MANIPULATION Functions for toggling view states, selecting, confirming nodes and resetting graph. ******************************************************************************/ - function select_node(ev, instance_path, base_path) { + function select_node(ev, instance_path) { if (ev.ctrlKey) { const new_selected_paths = [...selected_instance_paths] const index = new_selected_paths.indexOf(instance_path) @@ -412,6 +526,7 @@ async function graph_explorer(opts) { } function handle_confirm(ev, instance_path) { + if (!ev.target) return console.warn('Checkbox event target is missing.') const is_checked = ev.target.checked const new_selected_paths = [...selected_instance_paths] const new_confirmed_paths = [...confirmed_instance_paths] @@ -434,6 +549,10 @@ async function graph_explorer(opts) { } function toggle_subs(instance_path) { + if (!instance_states[instance_path]) { + console.warn(`Toggling subs for non-existent state: ${instance_path}. Creating default state.`) + instance_states[instance_path] = { expanded_subs: false, expanded_hubs: false } + } const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs build_and_render_view(instance_path) @@ -441,6 +560,10 @@ async function graph_explorer(opts) { } function toggle_hubs(instance_path) { + if (!instance_states[instance_path]) { + console.warn(`Toggling hubs for non-existent state: ${instance_path}. Creating default state.`) + instance_states[instance_path] = { expanded_subs: false, expanded_hubs: false } + } const state = instance_states[instance_path] state.expanded_hubs = !state.expanded_hubs build_and_render_view(instance_path) @@ -484,7 +607,7 @@ async function graph_explorer(opts) { } async function fill_viewport_downwards() { - if (is_rendering) return + if (is_rendering || end_index >= view.length) return is_rendering = true const container_rect = container.getBoundingClientRect() let sentinel_rect = bottom_sentinel.getBoundingClientRect() @@ -497,7 +620,7 @@ async function graph_explorer(opts) { } async function fill_viewport_upwards() { - if (is_rendering) return + if (is_rendering || start_index <= 0) return is_rendering = true const container_rect = container.getBoundingClientRect() let sentinel_rect = top_sentinel.getBoundingClientRect() @@ -522,9 +645,7 @@ async function graph_explorer(opts) { if (end_index >= view.length) return const fragment = document.createDocumentFragment() const next_end = Math.min(view.length, end_index + chunk_size) - for (let i = end_index; i < next_end; i++) { - fragment.appendChild(create_node(view[i])) - } + for (let i = end_index; i < next_end; i++) if (view[i]) fragment.appendChild(create_node(view[i])) container.insertBefore(fragment, bottom_sentinel) end_index = next_end bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` @@ -535,9 +656,7 @@ async function graph_explorer(opts) { if (start_index <= 0) return const fragment = document.createDocumentFragment() const prev_start = Math.max(0, start_index - chunk_size) - for (let i = prev_start; i < start_index; i++) { - fragment.appendChild(create_node(view[i])) - } + for (let i = prev_start; i < start_index; i++) if (view[i]) fragment.appendChild(create_node(view[i])) container.insertBefore(fragment, top_sentinel.nextSibling) start_index = prev_start top_sentinel.style.height = `${start_index * node_height}px` @@ -551,16 +670,18 @@ async function graph_explorer(opts) { const to_remove_count = rendered_count - max_rendered_nodes if (is_scrolling_up) { for (let i = 0; i < to_remove_count; i++) { - if (bottom_sentinel.previousElementSibling && bottom_sentinel.previousElementSibling !== top_sentinel) { - bottom_sentinel.previousElementSibling.remove() + const temp = bottom_sentinel.previousElementSibling + if (temp && temp !== top_sentinel) { + temp.remove() } } end_index -= to_remove_count bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` } else { for (let i = 0; i < to_remove_count; i++) { - if (top_sentinel.nextElementSibling && top_sentinel.nextElementSibling !== bottom_sentinel) { - top_sentinel.nextElementSibling.remove() + const temp = top_sentinel.nextElementSibling + if (temp && temp !== bottom_sentinel) { + temp.remove() } } start_index += to_remove_count @@ -603,6 +724,9 @@ function fallback_module() { cursor: default; height: 16px; /* Important for scroll calculation */ } + .node.error { + color: red; + } .node.selected { background-color: #776346; } From 6c9e52d609e6004eafa414f66ca925e0d3d7dcee Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 27 Jul 2025 20:30:16 +0500 Subject: [PATCH 026/130] object based prams --- lib/graph_explorer.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 7105a70..0f94ec6 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -78,13 +78,13 @@ async function graph_explorer(opts) { })) const func = on[type] - func ? func(data, type, paths) : fail(data, type) + func ? func({ data, type, paths }) : fail(data, type) } } function fail (data, type) { throw new Error(`Invalid message type: ${type}`, { cause: { data, type } }) } - function on_entries(data) { + function on_entries({ data }) { if (!data || data[0] === null || data[0] === undefined) { console.error('Entries data is missing or empty.') all_entries = {} @@ -118,7 +118,7 @@ async function graph_explorer(opts) { } } - function on_runtime (data, type, paths) { + function on_runtime ({ data, paths }) { let needs_render = false const render_nodes_needed = new Set() @@ -181,7 +181,7 @@ async function graph_explorer(opts) { } } - function inject_style(data) { + function inject_style({ data }) { const sheet = new CSSStyleSheet() sheet.replaceSync(data[0]) shadow.adoptedStyleSheets = [sheet] @@ -425,7 +425,7 @@ async function graph_explorer(opts) { return el } - const prefix_class_name = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) + const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) const pipe_html = pipe_trail.map(should_pipe => ``).join('') const prefix_class = has_subs ? 'prefix clickable' : 'prefix' @@ -475,7 +475,7 @@ async function graph_explorer(opts) { } } - function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { + function get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) { if (!state) { console.error('get_prefix called with invalid state.') return 'middle-line' From 359256742b8f68fb62961debe03482f6d9f5ba1f Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 27 Jul 2025 22:30:47 +0500 Subject: [PATCH 027/130] added static entry functionality when container not full --- lib/graph_explorer.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 0f94ec6..f030196 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -20,6 +20,7 @@ async function graph_explorer(opts) { let instance_states = {} let view = [] let drive_updated_by_scroll = false + let drive_updated_by_toggle = false let is_rendering = false const el = document.createElement('div') @@ -65,6 +66,10 @@ async function graph_explorer(opts) { drive_updated_by_scroll = false return } + if (drive_updated_by_toggle) { + drive_updated_by_toggle = false + return + } for (const { type, paths } of batch) { if (!paths || paths.length === 0) continue const data = await Promise.all(paths.map(async (path) => { @@ -251,10 +256,14 @@ async function graph_explorer(opts) { else console.warn(`Missing node at index ${i} in view.`) } + const spacer = document.createElement('div') + spacer.className = 'spacer' + container.replaceChildren() container.appendChild(top_sentinel) container.appendChild(fragment) container.appendChild(bottom_sentinel) + container.appendChild(spacer) top_sentinel.style.height = `${start_index * node_height}px` bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` @@ -263,6 +272,15 @@ async function graph_explorer(opts) { observer.observe(bottom_sentinel) requestAnimationFrame(() => { + const container_height = container.clientHeight + const content_height = view.length * node_height + const max_scroll_top = content_height - container_height + + if (new_scroll_top > max_scroll_top) { + const spacer_height = new_scroll_top - max_scroll_top + spacer.style.height = `${spacer_height}px` + } + container.scrollTop = new_scroll_top container.scrollLeft = old_scroll_left }) @@ -556,6 +574,7 @@ async function graph_explorer(opts) { const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs build_and_render_view(instance_path) + drive_updated_by_toggle = true update_runtime_state('instance_states', instance_states) } @@ -567,6 +586,7 @@ async function graph_explorer(opts) { const state = instance_states[instance_path] state.expanded_hubs = !state.expanded_hubs build_and_render_view(instance_path) + drive_updated_by_toggle = true update_runtime_state('instance_states', instance_states) } From 2470b53097856b599109cb1a958f8e296415429f Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 27 Jul 2025 23:28:11 +0500 Subject: [PATCH 028/130] upgraded the static entry logic --- lib/graph_explorer.js | 84 +++++++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index f030196..3256660 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -22,7 +22,11 @@ async function graph_explorer(opts) { let drive_updated_by_scroll = false let drive_updated_by_toggle = false let is_rendering = false - + let spacer_element = null + let spacer_initial_scroll_top = 0 + let spacer_initial_height = 0 + let hub_num = 0 + const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) @@ -204,7 +208,7 @@ async function graph_explorer(opts) { 3. VIEW AND RENDERING LOGIC Functions for building and rendering the graph view. ******************************************************************************/ - function build_and_render_view(focal_instance_path) { + function build_and_render_view(focal_instance_path, hub_toggle = false) { if (Object.keys(all_entries).length === 0) { console.warn('No entries available to render.') return @@ -213,6 +217,11 @@ async function graph_explorer(opts) { const old_scroll_top = vertical_scroll_value const old_scroll_left = horizontal_scroll_value + let existing_spacer_height = 0 + if (spacer_element && spacer_element.parentNode) { + existing_spacer_height = parseFloat(spacer_element.style.height) || 0 + } + view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -256,14 +265,10 @@ async function graph_explorer(opts) { else console.warn(`Missing node at index ${i} in view.`) } - const spacer = document.createElement('div') - spacer.className = 'spacer' - container.replaceChildren() container.appendChild(top_sentinel) container.appendChild(fragment) container.appendChild(bottom_sentinel) - container.appendChild(spacer) top_sentinel.style.height = `${start_index * node_height}px` bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` @@ -271,19 +276,42 @@ async function graph_explorer(opts) { observer.observe(top_sentinel) observer.observe(bottom_sentinel) - requestAnimationFrame(() => { - const container_height = container.clientHeight - const content_height = view.length * node_height - const max_scroll_top = content_height - container_height - - if (new_scroll_top > max_scroll_top) { - const spacer_height = new_scroll_top - max_scroll_top - spacer.style.height = `${spacer_height}px` - } + if (hub_toggle || hub_num > 0) { + spacer_element = document.createElement('div') + spacer_element.className = 'spacer' + container.appendChild(spacer_element) + + if (hub_toggle) { + requestAnimationFrame(() => { + const container_height = container.clientHeight + const content_height = view.length * node_height + const max_scroll_top = content_height - container_height + + if (new_scroll_top > max_scroll_top) { + spacer_initial_height = new_scroll_top - max_scroll_top + spacer_initial_scroll_top = new_scroll_top + spacer_element.style.height = `${spacer_initial_height}px` + } - container.scrollTop = new_scroll_top - container.scrollLeft = old_scroll_left - }) + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + }) + } else { + spacer_element.style.height = `${existing_spacer_height}px` + requestAnimationFrame(() => { + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + }) + } + } else { + spacer_element = null + spacer_initial_height = 0 + spacer_initial_scroll_top = 0 + requestAnimationFrame(() => { + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + }) + } } function build_view_recursive({ @@ -584,8 +612,9 @@ async function graph_explorer(opts) { instance_states[instance_path] = { expanded_subs: false, expanded_hubs: false } } const state = instance_states[instance_path] + state.expanded_hubs ? hub_num-- : hub_num++ state.expanded_hubs = !state.expanded_hubs - build_and_render_view(instance_path) + build_and_render_view(instance_path, true) drive_updated_by_toggle = true update_runtime_state('instance_states', instance_states) } @@ -612,6 +641,23 @@ async function graph_explorer(opts) { if (scroll_update_pending) return scroll_update_pending = true requestAnimationFrame(() => { + if (spacer_element && spacer_initial_height > 0) { + const scroll_delta = spacer_initial_scroll_top - container.scrollTop + + if (scroll_delta > 0) { + const new_height = spacer_initial_height - scroll_delta + if (new_height <= 0) { + spacer_element.remove() + spacer_element = null + spacer_initial_height = 0 + spacer_initial_scroll_top = 0 + hub_num = 0 + } else { + spacer_element.style.height = `${new_height}px` + } + } + } + if (vertical_scroll_value !== container.scrollTop) { vertical_scroll_value = container.scrollTop drive_updated_by_scroll = true From 29aab86c6041c47efef5cad2866cfaed0d01e1f2 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 27 Jul 2025 23:28:36 +0500 Subject: [PATCH 029/130] bundled --- bundle.js | 334 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 262 insertions(+), 72 deletions(-) diff --git a/bundle.js b/bundle.js index c4db243..4fbaeb8 100644 --- a/bundle.js +++ b/bundle.js @@ -16,8 +16,6 @@ async function graph_explorer(opts) { const { sdb } = await get(opts.sid) const { drive } = sdb - // await drive.list('runtime/').forEach(async path => console.log(path, await drive.get('runtime/' + path))) - let vertical_scroll_value = 0 let horizontal_scroll_value = 0 let selected_instance_paths = [] @@ -26,13 +24,19 @@ async function graph_explorer(opts) { let instance_states = {} let view = [] let drive_updated_by_scroll = false + let drive_updated_by_toggle = false let is_rendering = false - + let spacer_element = null + let spacer_initial_scroll_top = 0 + let spacer_initial_height = 0 + let hub_num = 0 + const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = `
` const container = shadow.querySelector('.graph-container') + document.body.style.margin = 0 let scroll_update_pending = false @@ -42,7 +46,7 @@ async function graph_explorer(opts) { let end_index = 0 const chunk_size = 50 const max_rendered_nodes = chunk_size * 3 - const node_height = 19 + const node_height = 16 const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') @@ -70,71 +74,158 @@ async function graph_explorer(opts) { drive_updated_by_scroll = false return } + if (drive_updated_by_toggle) { + drive_updated_by_toggle = false + return + } for (const { type, paths } of batch) { - const data = await Promise.all(paths.map(path => drive.get(path).then(file => file ? file.raw : null))) - const func = on[type] || fail - func(data, type, paths) + if (!paths || paths.length === 0) continue + const data = await Promise.all(paths.map(async (path) => { + try { + const file = await drive.get(path) + return file ? file.raw : null + } catch (e) { + console.error(`Error getting file from drive: ${path}`, e) + return null + } + })) + + const func = on[type] + func ? func({ data, type, paths }) : fail(data, type) } } - function fail (data, type) { throw new Error('invalid message', { cause: { data, type } }) } + function fail (data, type) { throw new Error(`Invalid message type: ${type}`, { cause: { data, type } }) } - function on_entries(data) { - all_entries = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + function on_entries({ data }) { + if (!data || data[0] === null || data[0] === undefined) { + console.error('Entries data is missing or empty.') + all_entries = {} + return + } + try { + const parsed_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + if (typeof parsed_data !== 'object' || parsed_data === null) { + console.error('Parsed entries data is not a valid object.') + all_entries = {} + return + } + all_entries = parsed_data + } catch (e) { + console.error('Failed to parse entries data:', e) + all_entries = {} + return + } + const root_path = '/' if (all_entries[root_path]) { const root_instance_path = '|/' if (!instance_states[root_instance_path]) { instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } - } else { - build_and_render_view() } + build_and_render_view() + } else { + console.warn('Root path "/" not found in entries. Clearing view.') + view = [] + if (container) container.replaceChildren() } } - function on_runtime (data, type, paths) { + function on_runtime ({ data, paths }) { + let needs_render = false + const render_nodes_needed = new Set() + for (let i = 0; i < paths.length; i++) { const path = paths[i] if (data[i] === null) continue - const value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] - if (path.endsWith('vertical_scroll_value.json')) vertical_scroll_value = value - else if (path.endsWith('horizontal_scroll_value.json')) horizontal_scroll_value = value - else if (path.endsWith('selected_instance_paths.json')) { - const old_paths = [...selected_instance_paths] - selected_instance_paths = value || [] - const changed_paths = [...new Set([...old_paths, ...selected_instance_paths])] - changed_paths.forEach(re_render_node) - } else if (path.endsWith('confirmed_selected.json')) { - const old_paths = [...confirmed_instance_paths] - confirmed_instance_paths = value || [] - const changed_paths = [...new Set([...old_paths, ...confirmed_instance_paths])] - changed_paths.forEach(re_render_node) - } else if (path.endsWith('instance_states.json')) { - instance_states = value - build_and_render_view() + + let value + try { + value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] + } catch (e) { + console.error(`Failed to parse JSON for ${path}:`, e) + continue } + + switch (true) { + case path.endsWith('vertical_scroll_value.json'): + if (typeof value === 'number') vertical_scroll_value = value + break + case path.endsWith('horizontal_scroll_value.json'): + if (typeof value === 'number') horizontal_scroll_value = value + break + case path.endsWith('selected_instance_paths.json'): { + const old_paths = [...selected_instance_paths] + if (Array.isArray(value)) { + selected_instance_paths = value + } else { + console.warn('selected_instance_paths is not an array, defaulting to empty.', value) + selected_instance_paths = [] + } + const changed_paths = [...new Set([...old_paths, ...selected_instance_paths])] + changed_paths.forEach(p => render_nodes_needed.add(p)) + break + } + case path.endsWith('confirmed_selected.json'): { + const old_paths = [...confirmed_instance_paths] + if (Array.isArray(value)) { + confirmed_instance_paths = value + } else { + console.warn('confirmed_selected is not an array, defaulting to empty.', value) + confirmed_instance_paths = [] + } + const changed_paths = [...new Set([...old_paths, ...confirmed_instance_paths])] + changed_paths.forEach(p => render_nodes_needed.add(p)) + break + } + case path.endsWith('instance_states.json'): + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + instance_states = value + needs_render = true + } else console.warn('instance_states is not a valid object, ignoring.', value) + break + } + } + + if (needs_render) { + build_and_render_view() + } else if (render_nodes_needed.size > 0) { + render_nodes_needed.forEach(re_render_node) } } - function inject_style(data) { + function inject_style({ data }) { const sheet = new CSSStyleSheet() sheet.replaceSync(data[0]) shadow.adoptedStyleSheets = [sheet] } async function update_runtime_state (name, value) { - await drive.put(`runtime/${name}.json`, JSON.stringify(value)) + try { + await drive.put(`runtime/${name}.json`, JSON.stringify(value)) + } catch (e) { + console.error(`Failed to update runtime state for ${name}:`, e) + } } /****************************************************************************** 3. VIEW AND RENDERING LOGIC Functions for building and rendering the graph view. ******************************************************************************/ - function build_and_render_view(focal_instance_path) { + function build_and_render_view(focal_instance_path, hub_toggle = false) { + if (Object.keys(all_entries).length === 0) { + console.warn('No entries available to render.') + return + } const old_view = [...view] const old_scroll_top = vertical_scroll_value const old_scroll_left = horizontal_scroll_value + let existing_spacer_height = 0 + if (spacer_element && spacer_element.parentNode) { + existing_spacer_height = parseFloat(spacer_element.style.height) || 0 + } + view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -174,7 +265,8 @@ async function graph_explorer(opts) { const fragment = document.createDocumentFragment() for (let i = start_index; i < end_index; i++) { - fragment.appendChild(create_node(view[i])) + if (view[i]) fragment.appendChild(create_node(view[i])) + else console.warn(`Missing node at index ${i} in view.`) } container.replaceChildren() @@ -188,10 +280,42 @@ async function graph_explorer(opts) { observer.observe(top_sentinel) observer.observe(bottom_sentinel) - requestAnimationFrame(() => { - container.scrollTop = new_scroll_top - container.scrollLeft = old_scroll_left - }) + if (hub_toggle || hub_num > 0) { + spacer_element = document.createElement('div') + spacer_element.className = 'spacer' + container.appendChild(spacer_element) + + if (hub_toggle) { + requestAnimationFrame(() => { + const container_height = container.clientHeight + const content_height = view.length * node_height + const max_scroll_top = content_height - container_height + + if (new_scroll_top > max_scroll_top) { + spacer_initial_height = new_scroll_top - max_scroll_top + spacer_initial_scroll_top = new_scroll_top + spacer_element.style.height = `${spacer_initial_height}px` + } + + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + }) + } else { + spacer_element.style.height = `${existing_spacer_height}px` + requestAnimationFrame(() => { + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + }) + } + } else { + spacer_element = null + spacer_initial_height = 0 + spacer_initial_scroll_top = 0 + requestAnimationFrame(() => { + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + }) + } } function build_view_recursive({ @@ -248,7 +372,7 @@ async function graph_explorer(opts) { } let current_view = [] - if (state.expanded_hubs && entry.hubs) { + if (state.expanded_hubs && Array.isArray(entry.hubs)) { entry.hubs.forEach((hub_path, i, arr) => { current_view = current_view.concat( build_view_recursive({ @@ -277,7 +401,7 @@ async function graph_explorer(opts) { is_hub_on_top }) - if (state.expanded_subs && entry.subs) { + if (state.expanded_subs && Array.isArray(entry.subs)) { entry.subs.forEach((sub_path, i, arr) => { current_view = current_view.concat( build_view_recursive({ @@ -303,15 +427,29 @@ async function graph_explorer(opts) { function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { const entry = all_entries[base_path] - const state = instance_states[instance_path] + if (!entry) { + console.error(`Entry not found for path: ${base_path}. Cannot create node.`) + const err_el = document.createElement('div') + err_el.className = 'node error' + err_el.textContent = `Error: Missing entry for ${base_path}` + return err_el + } + + let state = instance_states[instance_path] + if (!state) { + console.warn(`State not found for instance: ${instance_path}. Using default.`) + state = { expanded_subs: false, expanded_hubs: false } + instance_states[instance_path] = state + } + const el = document.createElement('div') - el.className = `node type-${entry.type}` + el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') - const has_hubs = entry.hubs && entry.hubs.length > 0 - const has_subs = entry.subs && entry.subs.length > 0 + const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 + const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 if (depth) { el.style.paddingLeft = '17.5px' @@ -322,15 +460,22 @@ async function graph_explorer(opts) { const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' const prefix_class = has_subs ? 'prefix clickable' : 'prefix' el.innerHTML = `
🪄
/🌐` - el.querySelector('.wand').onclick = reset + + const wand_el = el.querySelector('.wand') + if (wand_el) wand_el.onclick = reset + if (has_subs) { - el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) + const prefix_el = el.querySelector('.prefix') + if (prefix_el) prefix_el.onclick = () => toggle_subs(instance_path) } - el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) + + const name_el = el.querySelector('.name') + if (name_el) name_el.onclick = (ev) => select_node(ev, instance_path, base_path) + return el } - const prefix_class_name = get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) + const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) const pipe_html = pipe_trail.map(should_pipe => ``).join('') const prefix_class = has_subs ? 'prefix clickable' : 'prefix' @@ -340,18 +485,29 @@ async function graph_explorer(opts) { ${pipe_html} - ${entry.name} + ${entry.name || base_path} ` - if(has_hubs && base_path !== '/') el.querySelector('.icon').onclick = () => toggle_hubs(instance_path) - if(has_subs) el.querySelector('.prefix').onclick = () => toggle_subs(instance_path) - el.querySelector('.name').onclick = (ev) => select_node(ev, instance_path, base_path) + + if(has_hubs && base_path !== '/') { + const icon_el = el.querySelector('.icon') + if (icon_el) icon_el.onclick = () => toggle_hubs(instance_path) + } + + if(has_subs) { + const prefix_el = el.querySelector('.prefix') + if (prefix_el) prefix_el.onclick = () => toggle_subs(instance_path) + } + + const name_el = el.querySelector('.name') + if (name_el) name_el.onclick = (ev) => select_node(ev, instance_path, base_path) if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { const checkbox_div = document.createElement('div') checkbox_div.className = 'confirm-wrapper' const is_confirmed = confirmed_instance_paths.includes(instance_path) checkbox_div.innerHTML = `` - checkbox_div.querySelector('input').onchange = (ev) => handle_confirm(ev, instance_path) + const checkbox_input = checkbox_div.querySelector('input') + if (checkbox_input) checkbox_input.onchange = (ev) => handle_confirm(ev, instance_path) el.appendChild(checkbox_div) } @@ -361,15 +517,19 @@ async function graph_explorer(opts) { function re_render_node (instance_path) { const node_data = view.find(n => n.instance_path === instance_path) if (node_data) { - const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) - if (old_node_el) { - const new_node_el = create_node(node_data) - old_node_el.replaceWith(new_node_el) - } + const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (old_node_el) { + const new_node_el = create_node(node_data) + old_node_el.replaceWith(new_node_el) + } } } - function get_prefix(is_last_sub, has_subs, state, is_hub, is_hub_on_top) { + function get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) { + if (!state) { + console.error('get_prefix called with invalid state.') + return 'middle-line' + } const { expanded_subs, expanded_hubs } = state if (is_hub) { if (is_hub_on_top) { @@ -400,7 +560,7 @@ async function graph_explorer(opts) { 5. VIEW MANIPULATION Functions for toggling view states, selecting, confirming nodes and resetting graph. ******************************************************************************/ - function select_node(ev, instance_path, base_path) { + function select_node(ev, instance_path) { if (ev.ctrlKey) { const new_selected_paths = [...selected_instance_paths] const index = new_selected_paths.indexOf(instance_path) @@ -416,6 +576,7 @@ async function graph_explorer(opts) { } function handle_confirm(ev, instance_path) { + if (!ev.target) return console.warn('Checkbox event target is missing.') const is_checked = ev.target.checked const new_selected_paths = [...selected_instance_paths] const new_confirmed_paths = [...confirmed_instance_paths] @@ -438,16 +599,27 @@ async function graph_explorer(opts) { } function toggle_subs(instance_path) { + if (!instance_states[instance_path]) { + console.warn(`Toggling subs for non-existent state: ${instance_path}. Creating default state.`) + instance_states[instance_path] = { expanded_subs: false, expanded_hubs: false } + } const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs build_and_render_view(instance_path) + drive_updated_by_toggle = true update_runtime_state('instance_states', instance_states) } function toggle_hubs(instance_path) { + if (!instance_states[instance_path]) { + console.warn(`Toggling hubs for non-existent state: ${instance_path}. Creating default state.`) + instance_states[instance_path] = { expanded_subs: false, expanded_hubs: false } + } const state = instance_states[instance_path] + state.expanded_hubs ? hub_num-- : hub_num++ state.expanded_hubs = !state.expanded_hubs - build_and_render_view(instance_path) + build_and_render_view(instance_path, true) + drive_updated_by_toggle = true update_runtime_state('instance_states', instance_states) } @@ -473,6 +645,23 @@ async function graph_explorer(opts) { if (scroll_update_pending) return scroll_update_pending = true requestAnimationFrame(() => { + if (spacer_element && spacer_initial_height > 0) { + const scroll_delta = spacer_initial_scroll_top - container.scrollTop + + if (scroll_delta > 0) { + const new_height = spacer_initial_height - scroll_delta + if (new_height <= 0) { + spacer_element.remove() + spacer_element = null + spacer_initial_height = 0 + spacer_initial_scroll_top = 0 + hub_num = 0 + } else { + spacer_element.style.height = `${new_height}px` + } + } + } + if (vertical_scroll_value !== container.scrollTop) { vertical_scroll_value = container.scrollTop drive_updated_by_scroll = true @@ -488,7 +677,7 @@ async function graph_explorer(opts) { } async function fill_viewport_downwards() { - if (is_rendering) return + if (is_rendering || end_index >= view.length) return is_rendering = true const container_rect = container.getBoundingClientRect() let sentinel_rect = bottom_sentinel.getBoundingClientRect() @@ -501,7 +690,7 @@ async function graph_explorer(opts) { } async function fill_viewport_upwards() { - if (is_rendering) return + if (is_rendering || start_index <= 0) return is_rendering = true const container_rect = container.getBoundingClientRect() let sentinel_rect = top_sentinel.getBoundingClientRect() @@ -526,9 +715,7 @@ async function graph_explorer(opts) { if (end_index >= view.length) return const fragment = document.createDocumentFragment() const next_end = Math.min(view.length, end_index + chunk_size) - for (let i = end_index; i < next_end; i++) { - fragment.appendChild(create_node(view[i])) - } + for (let i = end_index; i < next_end; i++) if (view[i]) fragment.appendChild(create_node(view[i])) container.insertBefore(fragment, bottom_sentinel) end_index = next_end bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` @@ -539,9 +726,7 @@ async function graph_explorer(opts) { if (start_index <= 0) return const fragment = document.createDocumentFragment() const prev_start = Math.max(0, start_index - chunk_size) - for (let i = prev_start; i < start_index; i++) { - fragment.appendChild(create_node(view[i])) - } + for (let i = prev_start; i < start_index; i++) if (view[i]) fragment.appendChild(create_node(view[i])) container.insertBefore(fragment, top_sentinel.nextSibling) start_index = prev_start top_sentinel.style.height = `${start_index * node_height}px` @@ -555,16 +740,18 @@ async function graph_explorer(opts) { const to_remove_count = rendered_count - max_rendered_nodes if (is_scrolling_up) { for (let i = 0; i < to_remove_count; i++) { - if (bottom_sentinel.previousElementSibling && bottom_sentinel.previousElementSibling !== top_sentinel) { - bottom_sentinel.previousElementSibling.remove() + const temp = bottom_sentinel.previousElementSibling + if (temp && temp !== top_sentinel) { + temp.remove() } } end_index -= to_remove_count bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` } else { for (let i = 0; i < to_remove_count; i++) { - if (top_sentinel.nextElementSibling && top_sentinel.nextElementSibling !== bottom_sentinel) { - top_sentinel.nextElementSibling.remove() + const temp = top_sentinel.nextElementSibling + if (temp && temp !== bottom_sentinel) { + temp.remove() } } start_index += to_remove_count @@ -607,6 +794,9 @@ function fallback_module() { cursor: default; height: 16px; /* Important for scroll calculation */ } + .node.error { + color: red; + } .node.selected { background-color: #776346; } From 577446e2a8cb4742aa7e46748887dad2d615b114 Mon Sep 17 00:00:00 2001 From: ddroid Date: Mon, 28 Jul 2025 20:36:50 +0500 Subject: [PATCH 030/130] added node_height in drive --- lib/graph_explorer.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 3256660..fb9c5be 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -42,7 +42,7 @@ async function graph_explorer(opts) { let end_index = 0 const chunk_size = 50 const max_rendered_nodes = chunk_size * 3 - const node_height = 16 + let node_height const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') @@ -144,6 +144,9 @@ async function graph_explorer(opts) { } switch (true) { + case path.endsWith('node_height.json'): + node_height = value + break case path.endsWith('vertical_scroll_value.json'): if (typeof value === 'number') vertical_scroll_value = value break @@ -450,6 +453,7 @@ async function graph_explorer(opts) { if (depth) { el.style.paddingLeft = '17.5px' } + el.style.height = `${node_height}px` if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state @@ -788,7 +792,6 @@ function fallback_module() { align-items: center; white-space: nowrap; cursor: default; - height: 16px; /* Important for scroll calculation */ } .node.error { color: red; @@ -851,6 +854,7 @@ function fallback_module() { } }, 'runtime/': { + 'node_height.json': { raw: '16' }, 'vertical_scroll_value.json': { raw: '0' }, 'horizontal_scroll_value.json': { raw: '0' }, 'selected_instance_paths.json': { raw: '[]' }, From 2b5dd052782574ad2074fe6c6f63e233b25fb9bd Mon Sep 17 00:00:00 2001 From: ddroid Date: Mon, 28 Jul 2025 22:38:54 +0500 Subject: [PATCH 031/130] added logic to remove spacer when scrollup --- lib/graph_explorer.js | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index fb9c5be..f19c8e3 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -279,6 +279,12 @@ async function graph_explorer(opts) { observer.observe(top_sentinel) observer.observe(bottom_sentinel) + const set_scroll_and_sync = () => { + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + vertical_scroll_value = container.scrollTop + } + if (hub_toggle || hub_num > 0) { spacer_element = document.createElement('div') spacer_element.className = 'spacer' @@ -295,25 +301,17 @@ async function graph_explorer(opts) { spacer_initial_scroll_top = new_scroll_top spacer_element.style.height = `${spacer_initial_height}px` } - - container.scrollTop = new_scroll_top - container.scrollLeft = old_scroll_left + set_scroll_and_sync() }) } else { spacer_element.style.height = `${existing_spacer_height}px` - requestAnimationFrame(() => { - container.scrollTop = new_scroll_top - container.scrollLeft = old_scroll_left - }) + requestAnimationFrame(set_scroll_and_sync) } } else { spacer_element = null spacer_initial_height = 0 spacer_initial_scroll_top = 0 - requestAnimationFrame(() => { - container.scrollTop = new_scroll_top - container.scrollLeft = old_scroll_left - }) + requestAnimationFrame(set_scroll_and_sync) } } @@ -645,21 +643,14 @@ async function graph_explorer(opts) { if (scroll_update_pending) return scroll_update_pending = true requestAnimationFrame(() => { - if (spacer_element && spacer_initial_height > 0) { - const scroll_delta = spacer_initial_scroll_top - container.scrollTop - - if (scroll_delta > 0) { - const new_height = spacer_initial_height - scroll_delta - if (new_height <= 0) { - spacer_element.remove() - spacer_element = null - spacer_initial_height = 0 - spacer_initial_scroll_top = 0 - hub_num = 0 - } else { - spacer_element.style.height = `${new_height}px` - } - } + const scroll_delta = vertical_scroll_value - container.scrollTop + + if (spacer_element && scroll_delta > 0 && container.scrollTop == 0) { + spacer_element.remove() + spacer_element = null + spacer_initial_height = 0 + spacer_initial_scroll_top = 0 + hub_num = 0 } if (vertical_scroll_value !== container.scrollTop) { From 51d14104db4d076d6dc3537aa0fa4ac2e05b4b73 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 29 Jul 2025 00:21:50 +0500 Subject: [PATCH 032/130] added more comments to sections and in involved stuff --- lib/graph_explorer.js | 80 ++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index f19c8e3..4fc88d2 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -7,7 +7,9 @@ module.exports = graph_explorer async function graph_explorer(opts) { /****************************************************************************** 1. COMPONENT INITIALIZATION - Set up state, variables, DOM, and watchers. + - This sets up the initial state, variables, and the basic DOM structure. + - It also initializes the IntersectionObserver for virtual scrolling and + sets up the watcher for state changes. ******************************************************************************/ const { sdb } = await get(opts.sid) const { drive } = sdb @@ -16,16 +18,15 @@ async function graph_explorer(opts) { let horizontal_scroll_value = 0 let selected_instance_paths = [] let confirmed_instance_paths = [] - let all_entries = {} - let instance_states = {} - let view = [] - let drive_updated_by_scroll = false - let drive_updated_by_toggle = false - let is_rendering = false - let spacer_element = null - let spacer_initial_scroll_top = 0 + let all_entries = {} // Holds the entire graph structure from entries.json. + let instance_states = {} // Holds expansion state {expanded_subs, expanded_hubs} for each node instance. + let view = [] // A flat array representing the visible nodes in the graph. + let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. + let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. + let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. + let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 - let hub_num = 0 + let hub_num = 0 // Counter for expanded hubs. const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -52,20 +53,24 @@ async function graph_explorer(opts) { rootMargin: '500px 0px', threshold: 0 }) + // Define handlers for different data types from the drive, called by `onbatch`. const on = { entries: on_entries, style: inject_style, runtime: on_runtime } + // Start watching for state changes. This is the main trigger for all updates. await sdb.watch(onbatch) return el /****************************************************************************** 2. STATE AND DATA HANDLING - Functions for processing data from the STATE module. + - These functions process incoming data from the STATE module's `sdb.watch`. + - `onbatch` is the primary entry point. ******************************************************************************/ async function onbatch(batch) { + // Prevent feedback loops from scroll or toggle actions. if (drive_updated_by_scroll) { drive_updated_by_scroll = false return @@ -74,6 +79,7 @@ async function graph_explorer(opts) { drive_updated_by_toggle = false return } + for (const { type, paths } of batch) { if (!paths || paths.length === 0) continue const data = await Promise.all(paths.map(async (path) => { @@ -85,7 +91,7 @@ async function graph_explorer(opts) { return null } })) - + // Call the appropriate handler based on `type`. const func = on[type] func ? func({ data, type, paths }) : fail(data, type) } @@ -113,6 +119,7 @@ async function graph_explorer(opts) { return } + // After receiving entries, ensure the root node state is initialized and trigger the first render. const root_path = '/' if (all_entries[root_path]) { const root_instance_path = '|/' @@ -142,7 +149,7 @@ async function graph_explorer(opts) { console.error(`Failed to parse JSON for ${path}:`, e) continue } - + // Handle different runtime state updates based on the path i.e files switch (true) { case path.endsWith('node_height.json'): node_height = value @@ -199,6 +206,7 @@ async function graph_explorer(opts) { shadow.adoptedStyleSheets = [sheet] } + // Helper to persist component state to the drive. async function update_runtime_state (name, value) { try { await drive.put(`runtime/${name}.json`, JSON.stringify(value)) @@ -209,7 +217,9 @@ async function graph_explorer(opts) { /****************************************************************************** 3. VIEW AND RENDERING LOGIC - Functions for building and rendering the graph view. + - These functions build the `view` array and render the DOM. + - `build_and_render_view` is the main orchestrator. + - `build_view_recursive` creates the flat `view` array from the hierarchical data. ******************************************************************************/ function build_and_render_view(focal_instance_path, hub_toggle = false) { if (Object.keys(all_entries).length === 0) { @@ -225,6 +235,7 @@ async function graph_explorer(opts) { existing_spacer_height = parseFloat(spacer_element.style.height) || 0 } + // Recursively build the new `view` array from the graph data. view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -236,9 +247,10 @@ async function graph_explorer(opts) { all_entries }) + // Calculate the new scroll position to maintain the user's viewport. let new_scroll_top = old_scroll_top - if (focal_instance_path) { + // If an action was focused on a specific node (like a toggle), try to keep it in the same position. const old_toggled_node_index = old_view.findIndex(node => node.instance_path === focal_instance_path) const new_toggled_node_index = view.findIndex(node => node.instance_path === focal_instance_path) @@ -247,6 +259,7 @@ async function graph_explorer(opts) { new_scroll_top = old_scroll_top + (index_change * node_height) } } else if (old_view.length > 0) { + // Otherwise, try to keep the topmost visible node in the same position. const old_top_node_index = Math.floor(old_scroll_top / node_height) const scroll_offset = old_scroll_top % node_height const old_top_node = old_view[old_top_node_index] @@ -285,6 +298,7 @@ async function graph_explorer(opts) { vertical_scroll_value = container.scrollTop } + // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. if (hub_toggle || hub_num > 0) { spacer_element = document.createElement('div') spacer_element.className = 'spacer' @@ -315,6 +329,7 @@ async function graph_explorer(opts) { } } + // Traverses the hierarchical `all_entries` data and builds a flat `view` array for rendering. function build_view_recursive({ base_path, parent_instance_path, @@ -339,9 +354,12 @@ async function graph_explorer(opts) { } const state = instance_states[instance_path] const is_hub_on_top = (base_path === all_entries[parent_base_path]?.hubs?.[0]) || (base_path === '/') + + // Calculate the pipe trail for drawing the tree lines. Quite complex logic here. const children_pipe_trail = [...parent_pipe_trail] let last_pipe = null if (depth > 0) { + if (is_hub) { last_pipe = [...parent_pipe_trail] if (is_last_sub) { @@ -369,6 +387,7 @@ async function graph_explorer(opts) { } let current_view = [] + // If hubs are expanded, recursively add them to the view first (they appear above the node). if (state.expanded_hubs && Array.isArray(entry.hubs)) { entry.hubs.forEach((hub_path, i, arr) => { current_view = current_view.concat( @@ -398,6 +417,7 @@ async function graph_explorer(opts) { is_hub_on_top }) + // If subs are expanded, recursively add them to the view (they appear below the node). if (state.expanded_subs && Array.isArray(entry.subs)) { entry.subs.forEach((sub_path, i, arr) => { current_view = current_view.concat( @@ -418,8 +438,9 @@ async function graph_explorer(opts) { } /****************************************************************************** - 4. NODE CREATION AND GRAPH BUILDING - Functions for creating nodes in the graph. + 4. NODE CREATION AND EVENT HANDLING + - `create_node` generates the DOM element for a single node. + - It sets up event handlers for user interactions like selecting or toggling. ******************************************************************************/ function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { @@ -453,6 +474,7 @@ async function graph_explorer(opts) { } el.style.height = `${node_height}px` + // Handle the special case for the root node since its a bit different. if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' @@ -512,6 +534,7 @@ async function graph_explorer(opts) { return el } + // `re_render_node` updates a single node in the DOM, used when only its selection state changes. function re_render_node (instance_path) { const node_data = view.find(n => n.instance_path === instance_path) if (node_data) { @@ -523,6 +546,7 @@ async function graph_explorer(opts) { } } + // `get_prefix` determines which box-drawing character to use for the node's prefix. It gives the name of a specific CSS class. function get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) { if (!state) { console.error('get_prefix called with invalid state.') @@ -555,8 +579,9 @@ async function graph_explorer(opts) { } /****************************************************************************** - 5. VIEW MANIPULATION - Functions for toggling view states, selecting, confirming nodes and resetting graph. + 5. VIEW MANIPULATION & USER ACTIONS + - These functions handle user interactions like selecting, confirming, + toggling, and resetting the graph. ******************************************************************************/ function select_node(ev, instance_path) { if (ev.ctrlKey) { @@ -604,6 +629,7 @@ async function graph_explorer(opts) { const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs build_and_render_view(instance_path) + // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true update_runtime_state('instance_states', instance_states) } @@ -637,7 +663,8 @@ async function graph_explorer(opts) { /****************************************************************************** 6. VIRTUAL SCROLLING - Functions for handling virtual scrolling and DOM cleanup. + - These functions implement virtual scrolling to handle large graphs + efficiently using an IntersectionObserver. ******************************************************************************/ function onscroll() { if (scroll_update_pending) return @@ -645,6 +672,7 @@ async function graph_explorer(opts) { requestAnimationFrame(() => { const scroll_delta = vertical_scroll_value - container.scrollTop + // Handle removal of the scroll spacer. if (spacer_element && scroll_delta > 0 && container.scrollTop == 0) { spacer_element.remove() spacer_element = null @@ -655,7 +683,7 @@ async function graph_explorer(opts) { if (vertical_scroll_value !== container.scrollTop) { vertical_scroll_value = container.scrollTop - drive_updated_by_scroll = true + drive_updated_by_scroll = true // Set flag to prevent render loop. update_runtime_state('vertical_scroll_value', vertical_scroll_value) } if (horizontal_scroll_value !== container.scrollLeft) { @@ -724,12 +752,14 @@ async function graph_explorer(opts) { cleanup_dom(true) } + // Removes nodes from the DOM that are far outside the viewport. function cleanup_dom(is_scrolling_up) { const rendered_count = end_index - start_index if (rendered_count <= max_rendered_nodes) return const to_remove_count = rendered_count - max_rendered_nodes if (is_scrolling_up) { + // If scrolling up, remove nodes from the bottom. for (let i = 0; i < to_remove_count; i++) { const temp = bottom_sentinel.previousElementSibling if (temp && temp !== top_sentinel) { @@ -739,6 +769,7 @@ async function graph_explorer(opts) { end_index -= to_remove_count bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` } else { + // If scrolling down, remove nodes from the top. for (let i = 0; i < to_remove_count; i++) { const temp = top_sentinel.nextElementSibling if (temp && temp !== bottom_sentinel) { @@ -753,7 +784,10 @@ async function graph_explorer(opts) { /****************************************************************************** 7. FALLBACK CONFIGURATION - Provides default data and API for the component. + - This provides the default data and API configuration for the component, + following the pattern described in `instructions.md`. + - It defines the default datasets (`entries`, `style`, `runtime`) and their + initial values. ******************************************************************************/ function fallback_module() { return { @@ -855,4 +889,4 @@ function fallback_module() { } } } -} +} \ No newline at end of file From 038b26ff3596d7703db3ea0b9070341d8e0fa2ca Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 29 Jul 2025 00:22:18 +0500 Subject: [PATCH 033/130] bundled --- bundle.js | 130 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 51 deletions(-) diff --git a/bundle.js b/bundle.js index 4fbaeb8..139dbb5 100644 --- a/bundle.js +++ b/bundle.js @@ -11,7 +11,9 @@ module.exports = graph_explorer async function graph_explorer(opts) { /****************************************************************************** 1. COMPONENT INITIALIZATION - Set up state, variables, DOM, and watchers. + - This sets up the initial state, variables, and the basic DOM structure. + - It also initializes the IntersectionObserver for virtual scrolling and + sets up the watcher for state changes. ******************************************************************************/ const { sdb } = await get(opts.sid) const { drive } = sdb @@ -20,16 +22,15 @@ async function graph_explorer(opts) { let horizontal_scroll_value = 0 let selected_instance_paths = [] let confirmed_instance_paths = [] - let all_entries = {} - let instance_states = {} - let view = [] - let drive_updated_by_scroll = false - let drive_updated_by_toggle = false - let is_rendering = false - let spacer_element = null - let spacer_initial_scroll_top = 0 + let all_entries = {} // Holds the entire graph structure from entries.json. + let instance_states = {} // Holds expansion state {expanded_subs, expanded_hubs} for each node instance. + let view = [] // A flat array representing the visible nodes in the graph. + let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. + let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. + let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. + let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 - let hub_num = 0 + let hub_num = 0 // Counter for expanded hubs. const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -46,7 +47,7 @@ async function graph_explorer(opts) { let end_index = 0 const chunk_size = 50 const max_rendered_nodes = chunk_size * 3 - const node_height = 16 + let node_height const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') @@ -56,20 +57,24 @@ async function graph_explorer(opts) { rootMargin: '500px 0px', threshold: 0 }) + // Define handlers for different data types from the drive, called by `onbatch`. const on = { entries: on_entries, style: inject_style, runtime: on_runtime } + // Start watching for state changes. This is the main trigger for all updates. await sdb.watch(onbatch) return el /****************************************************************************** 2. STATE AND DATA HANDLING - Functions for processing data from the STATE module. + - These functions process incoming data from the STATE module's `sdb.watch`. + - `onbatch` is the primary entry point. ******************************************************************************/ async function onbatch(batch) { + // Prevent feedback loops from scroll or toggle actions. if (drive_updated_by_scroll) { drive_updated_by_scroll = false return @@ -78,6 +83,7 @@ async function graph_explorer(opts) { drive_updated_by_toggle = false return } + for (const { type, paths } of batch) { if (!paths || paths.length === 0) continue const data = await Promise.all(paths.map(async (path) => { @@ -89,7 +95,7 @@ async function graph_explorer(opts) { return null } })) - + // Call the appropriate handler based on `type`. const func = on[type] func ? func({ data, type, paths }) : fail(data, type) } @@ -117,6 +123,7 @@ async function graph_explorer(opts) { return } + // After receiving entries, ensure the root node state is initialized and trigger the first render. const root_path = '/' if (all_entries[root_path]) { const root_instance_path = '|/' @@ -146,8 +153,11 @@ async function graph_explorer(opts) { console.error(`Failed to parse JSON for ${path}:`, e) continue } - + // Handle different runtime state updates based on the path i.e files switch (true) { + case path.endsWith('node_height.json'): + node_height = value + break case path.endsWith('vertical_scroll_value.json'): if (typeof value === 'number') vertical_scroll_value = value break @@ -200,6 +210,7 @@ async function graph_explorer(opts) { shadow.adoptedStyleSheets = [sheet] } + // Helper to persist component state to the drive. async function update_runtime_state (name, value) { try { await drive.put(`runtime/${name}.json`, JSON.stringify(value)) @@ -210,7 +221,9 @@ async function graph_explorer(opts) { /****************************************************************************** 3. VIEW AND RENDERING LOGIC - Functions for building and rendering the graph view. + - These functions build the `view` array and render the DOM. + - `build_and_render_view` is the main orchestrator. + - `build_view_recursive` creates the flat `view` array from the hierarchical data. ******************************************************************************/ function build_and_render_view(focal_instance_path, hub_toggle = false) { if (Object.keys(all_entries).length === 0) { @@ -226,6 +239,7 @@ async function graph_explorer(opts) { existing_spacer_height = parseFloat(spacer_element.style.height) || 0 } + // Recursively build the new `view` array from the graph data. view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -237,9 +251,10 @@ async function graph_explorer(opts) { all_entries }) + // Calculate the new scroll position to maintain the user's viewport. let new_scroll_top = old_scroll_top - if (focal_instance_path) { + // If an action was focused on a specific node (like a toggle), try to keep it in the same position. const old_toggled_node_index = old_view.findIndex(node => node.instance_path === focal_instance_path) const new_toggled_node_index = view.findIndex(node => node.instance_path === focal_instance_path) @@ -248,6 +263,7 @@ async function graph_explorer(opts) { new_scroll_top = old_scroll_top + (index_change * node_height) } } else if (old_view.length > 0) { + // Otherwise, try to keep the topmost visible node in the same position. const old_top_node_index = Math.floor(old_scroll_top / node_height) const scroll_offset = old_scroll_top % node_height const old_top_node = old_view[old_top_node_index] @@ -280,6 +296,13 @@ async function graph_explorer(opts) { observer.observe(top_sentinel) observer.observe(bottom_sentinel) + const set_scroll_and_sync = () => { + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + vertical_scroll_value = container.scrollTop + } + + // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. if (hub_toggle || hub_num > 0) { spacer_element = document.createElement('div') spacer_element.className = 'spacer' @@ -296,28 +319,21 @@ async function graph_explorer(opts) { spacer_initial_scroll_top = new_scroll_top spacer_element.style.height = `${spacer_initial_height}px` } - - container.scrollTop = new_scroll_top - container.scrollLeft = old_scroll_left + set_scroll_and_sync() }) } else { spacer_element.style.height = `${existing_spacer_height}px` - requestAnimationFrame(() => { - container.scrollTop = new_scroll_top - container.scrollLeft = old_scroll_left - }) + requestAnimationFrame(set_scroll_and_sync) } } else { spacer_element = null spacer_initial_height = 0 spacer_initial_scroll_top = 0 - requestAnimationFrame(() => { - container.scrollTop = new_scroll_top - container.scrollLeft = old_scroll_left - }) + requestAnimationFrame(set_scroll_and_sync) } } + // Traverses the hierarchical `all_entries` data and builds a flat `view` array for rendering. function build_view_recursive({ base_path, parent_instance_path, @@ -342,9 +358,12 @@ async function graph_explorer(opts) { } const state = instance_states[instance_path] const is_hub_on_top = (base_path === all_entries[parent_base_path]?.hubs?.[0]) || (base_path === '/') + + // Calculate the pipe trail for drawing the tree lines. Quite complex logic here. const children_pipe_trail = [...parent_pipe_trail] let last_pipe = null if (depth > 0) { + if (is_hub) { last_pipe = [...parent_pipe_trail] if (is_last_sub) { @@ -372,6 +391,7 @@ async function graph_explorer(opts) { } let current_view = [] + // If hubs are expanded, recursively add them to the view first (they appear above the node). if (state.expanded_hubs && Array.isArray(entry.hubs)) { entry.hubs.forEach((hub_path, i, arr) => { current_view = current_view.concat( @@ -401,6 +421,7 @@ async function graph_explorer(opts) { is_hub_on_top }) + // If subs are expanded, recursively add them to the view (they appear below the node). if (state.expanded_subs && Array.isArray(entry.subs)) { entry.subs.forEach((sub_path, i, arr) => { current_view = current_view.concat( @@ -421,8 +442,9 @@ async function graph_explorer(opts) { } /****************************************************************************** - 4. NODE CREATION AND GRAPH BUILDING - Functions for creating nodes in the graph. + 4. NODE CREATION AND EVENT HANDLING + - `create_node` generates the DOM element for a single node. + - It sets up event handlers for user interactions like selecting or toggling. ******************************************************************************/ function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { @@ -454,7 +476,9 @@ async function graph_explorer(opts) { if (depth) { el.style.paddingLeft = '17.5px' } + el.style.height = `${node_height}px` + // Handle the special case for the root node since its a bit different. if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' @@ -514,6 +538,7 @@ async function graph_explorer(opts) { return el } + // `re_render_node` updates a single node in the DOM, used when only its selection state changes. function re_render_node (instance_path) { const node_data = view.find(n => n.instance_path === instance_path) if (node_data) { @@ -525,6 +550,7 @@ async function graph_explorer(opts) { } } + // `get_prefix` determines which box-drawing character to use for the node's prefix. It gives the name of a specific CSS class. function get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) { if (!state) { console.error('get_prefix called with invalid state.') @@ -557,8 +583,9 @@ async function graph_explorer(opts) { } /****************************************************************************** - 5. VIEW MANIPULATION - Functions for toggling view states, selecting, confirming nodes and resetting graph. + 5. VIEW MANIPULATION & USER ACTIONS + - These functions handle user interactions like selecting, confirming, + toggling, and resetting the graph. ******************************************************************************/ function select_node(ev, instance_path) { if (ev.ctrlKey) { @@ -606,6 +633,7 @@ async function graph_explorer(opts) { const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs build_and_render_view(instance_path) + // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true update_runtime_state('instance_states', instance_states) } @@ -639,32 +667,27 @@ async function graph_explorer(opts) { /****************************************************************************** 6. VIRTUAL SCROLLING - Functions for handling virtual scrolling and DOM cleanup. + - These functions implement virtual scrolling to handle large graphs + efficiently using an IntersectionObserver. ******************************************************************************/ function onscroll() { if (scroll_update_pending) return scroll_update_pending = true requestAnimationFrame(() => { - if (spacer_element && spacer_initial_height > 0) { - const scroll_delta = spacer_initial_scroll_top - container.scrollTop - - if (scroll_delta > 0) { - const new_height = spacer_initial_height - scroll_delta - if (new_height <= 0) { - spacer_element.remove() - spacer_element = null - spacer_initial_height = 0 - spacer_initial_scroll_top = 0 - hub_num = 0 - } else { - spacer_element.style.height = `${new_height}px` - } - } + const scroll_delta = vertical_scroll_value - container.scrollTop + + // Handle removal of the scroll spacer. + if (spacer_element && scroll_delta > 0 && container.scrollTop == 0) { + spacer_element.remove() + spacer_element = null + spacer_initial_height = 0 + spacer_initial_scroll_top = 0 + hub_num = 0 } if (vertical_scroll_value !== container.scrollTop) { vertical_scroll_value = container.scrollTop - drive_updated_by_scroll = true + drive_updated_by_scroll = true // Set flag to prevent render loop. update_runtime_state('vertical_scroll_value', vertical_scroll_value) } if (horizontal_scroll_value !== container.scrollLeft) { @@ -733,12 +756,14 @@ async function graph_explorer(opts) { cleanup_dom(true) } + // Removes nodes from the DOM that are far outside the viewport. function cleanup_dom(is_scrolling_up) { const rendered_count = end_index - start_index if (rendered_count <= max_rendered_nodes) return const to_remove_count = rendered_count - max_rendered_nodes if (is_scrolling_up) { + // If scrolling up, remove nodes from the bottom. for (let i = 0; i < to_remove_count; i++) { const temp = bottom_sentinel.previousElementSibling if (temp && temp !== top_sentinel) { @@ -748,6 +773,7 @@ async function graph_explorer(opts) { end_index -= to_remove_count bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` } else { + // If scrolling down, remove nodes from the top. for (let i = 0; i < to_remove_count; i++) { const temp = top_sentinel.nextElementSibling if (temp && temp !== bottom_sentinel) { @@ -762,7 +788,10 @@ async function graph_explorer(opts) { /****************************************************************************** 7. FALLBACK CONFIGURATION - Provides default data and API for the component. + - This provides the default data and API configuration for the component, + following the pattern described in `instructions.md`. + - It defines the default datasets (`entries`, `style`, `runtime`) and their + initial values. ******************************************************************************/ function fallback_module() { return { @@ -792,7 +821,6 @@ function fallback_module() { align-items: center; white-space: nowrap; cursor: default; - height: 16px; /* Important for scroll calculation */ } .node.error { color: red; @@ -855,6 +883,7 @@ function fallback_module() { } }, 'runtime/': { + 'node_height.json': { raw: '16' }, 'vertical_scroll_value.json': { raw: '0' }, 'horizontal_scroll_value.json': { raw: '0' }, 'selected_instance_paths.json': { raw: '[]' }, @@ -865,7 +894,6 @@ function fallback_module() { } } } - }).call(this)}).call(this,"/lib/graph_explorer.js") },{"./STATE":1}],3:[function(require,module,exports){ const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' From ae76c1591310a85b349f6b805e2384bd9bfe60bb Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 30 Jul 2025 17:28:13 +0500 Subject: [PATCH 034/130] Added Search Feature --- lib/graph_explorer.js | 248 ++++++++++++++++++++++++++++++++++++++++-- web/page.js | 3 +- 2 files changed, 238 insertions(+), 13 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 4fc88d2..c95d63f 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -21,6 +21,7 @@ async function graph_explorer(opts) { let all_entries = {} // Holds the entire graph structure from entries.json. let instance_states = {} // Holds expansion state {expanded_subs, expanded_hubs} for each node instance. let view = [] // A flat array representing the visible nodes in the graph. + let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. @@ -31,7 +32,11 @@ async function graph_explorer(opts) { const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) - shadow.innerHTML = `
` + shadow.innerHTML = ` + +
+ ` + const menubar = shadow.querySelector('.menubar') const container = shadow.querySelector('.graph-container') document.body.style.margin = 0 @@ -57,11 +62,12 @@ async function graph_explorer(opts) { const on = { entries: on_entries, style: inject_style, - runtime: on_runtime + runtime: on_runtime, + mode: on_mode } // Start watching for state changes. This is the main trigger for all updates. await sdb.watch(onbatch) - + return el /****************************************************************************** @@ -85,7 +91,9 @@ async function graph_explorer(opts) { const data = await Promise.all(paths.map(async (path) => { try { const file = await drive.get(path) - return file ? file.raw : null + if (!file) return null + if (type === 'mode') return file.raw.replace(/"/g, '') + return file.raw } catch (e) { console.error(`Error getting file from drive: ${path}`, e) return null @@ -93,7 +101,11 @@ async function graph_explorer(opts) { })) // Call the appropriate handler based on `type`. const func = on[type] - func ? func({ data, type, paths }) : fail(data, type) + if (type === 'mode') { + func ? func(data[0]) : fail(data, type) + } else { + func ? func({ data, type, paths }) : fail(data, type) + } } } @@ -200,6 +212,13 @@ async function graph_explorer(opts) { } } + function on_mode (new_mode) { + if (!new_mode || mode === new_mode) return + mode = new_mode + render_menubar() + handle_mode_change() + } + function inject_style({ data }) { const sheet = new CSSStyleSheet() sheet.replaceSync(data[0]) @@ -215,6 +234,14 @@ async function graph_explorer(opts) { } } + async function update_mode_state (value) { + try { + await drive.put('mode/current_mode.json', JSON.stringify(value)) + } catch (e) { + console.error('Failed to update mode state:', e) + } + } + /****************************************************************************** 3. VIEW AND RENDERING LOGIC - These functions build the `view` array and render the DOM. @@ -437,13 +464,13 @@ async function graph_explorer(opts) { return current_view } - /****************************************************************************** +/****************************************************************************** 4. NODE CREATION AND EVENT HANDLING - `create_node` generates the DOM element for a single node. - It sets up event handlers for user interactions like selecting or toggling. - ******************************************************************************/ +******************************************************************************/ - function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top, is_search_match, is_direct_match, is_in_original_view }) { const entry = all_entries[base_path] if (!entry) { console.error(`Entry not found for path: ${base_path}. Cannot create node.`) @@ -463,6 +490,17 @@ async function graph_explorer(opts) { const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path + + if (is_search_match) { + el.classList.add('search-result') + if (is_direct_match) { + el.classList.add('direct-match') + } + if (!is_in_original_view) { + el.classList.add('new-entry') + } + } + if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') @@ -578,12 +616,180 @@ async function graph_explorer(opts) { } } - /****************************************************************************** - 5. VIEW MANIPULATION & USER ACTIONS +/****************************************************************************** + 5. MENUBAR AND SEARCH +******************************************************************************/ + function render_menubar () { + menubar.replaceChildren() // Clear existing menubar + const search_button = document.createElement('button') + search_button.textContent = 'Search' + search_button.onclick = toggle_search_mode + + menubar.appendChild(search_button) + + if (mode === 'search') { + const search_input = document.createElement('input') + search_input.type = 'text' + search_input.placeholder = 'Search entries...' + search_input.className = 'search-input' + search_input.oninput = on_search_input + menubar.appendChild(search_input) + search_input.focus() + } + } + + function handle_mode_change () { + if (mode === 'default') { + menubar.style.display = 'none' + } else { + menubar.style.display = 'flex' + } + if (mode !== 'search') { + build_and_render_view() + } + } + + function toggle_search_mode () { + const new_mode = mode === 'search' ? 'menubar' : 'search' + update_mode_state(new_mode) + } + + function on_search_input (event) { + const query = event.target.value.trim() + perform_search(query) + } + + function perform_search (query) { + if (!query) { + build_and_render_view() + return + } + const original_view = [...view] // Capture original view + const search_view = build_search_view_recursive({ + query, + base_path: '/', + parent_instance_path: '', + depth: 0, + is_last_sub : true, + is_hub: false, + parent_pipe_trail: [], + instance_states: {}, // Use a temporary state for search + all_entries, + original_view + }) + render_search_results(search_view, query) + } + + function build_search_view_recursive({ + query, + base_path, + parent_instance_path, + depth, + is_last_sub, + is_hub, + parent_pipe_trail, + instance_states, + all_entries, + original_view + }) { + const entry = all_entries[base_path] + if (!entry) return [] + + const instance_path = `${parent_instance_path}|${base_path}` + const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) + + let sub_results = [] + if (Array.isArray(entry.subs)) { + const children_pipe_trail = [...parent_pipe_trail] + if (depth > 0) children_pipe_trail.push(!is_last_sub) + + sub_results = entry.subs.map((sub_path, i, arr) => { + return build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries, + original_view + }) + }).flat() + } + + const has_matching_descendant = sub_results.length > 0 + + if (!is_direct_match && !has_matching_descendant) { + return [] + } + + instance_states[instance_path] = { + expanded_subs: has_matching_descendant, + expanded_hubs: false + } + + const is_in_original_view = original_view.some(node => node.instance_path === instance_path) + + const current_node_view = { + base_path, + instance_path, + depth, + is_last_sub, + is_hub, + pipe_trail: parent_pipe_trail, + is_hub_on_top: false, + is_search_match: true, + is_direct_match, + is_in_original_view + } + + return [current_node_view, ...sub_results] + } + + function render_search_results (search_view, query) { + view = search_view + container.replaceChildren() + + if (search_view.length === 0) { + const no_results_el = document.createElement('div') + no_results_el.className = 'no-results' + no_results_el.textContent = `No results for "${query}"` + container.appendChild(no_results_el) + return + } + + const fragment = document.createDocumentFragment() + for (const node_data of search_view) { + fragment.appendChild(create_node(node_data)) + } + container.appendChild(fragment) + } + +/****************************************************************************** + 6. VIEW MANIPULATION & USER ACTIONS - These functions handle user interactions like selecting, confirming, toggling, and resetting the graph. ******************************************************************************/ function select_node(ev, instance_path) { + if (mode === 'search') { + let current_path = instance_path + // Traverse up the tree to expand all parents + while (current_path) { + const parent_path = current_path.substring(0, current_path.lastIndexOf('|')) + if (!parent_path) break // Stop if there's no parent left + + if (!instance_states[parent_path]) { + instance_states[parent_path] = { expanded_subs: false, expanded_hubs: false } + } + instance_states[parent_path].expanded_subs = true + current_path = parent_path + } + drive_updated_by_toggle = true + update_runtime_state('instance_states', instance_states) + } + if (ev.ctrlKey) { const new_selected_paths = [...selected_instance_paths] const index = new_selected_paths.indexOf(instance_path) @@ -662,7 +868,7 @@ async function graph_explorer(opts) { } /****************************************************************************** - 6. VIRTUAL SCROLLING + 7. VIRTUAL SCROLLING - These functions implement virtual scrolling to handle large graphs efficiently using an IntersectionObserver. ******************************************************************************/ @@ -783,7 +989,7 @@ async function graph_explorer(opts) { } /****************************************************************************** - 7. FALLBACK CONFIGURATION + 8. FALLBACK CONFIGURATION - This provides the default data and API configuration for the component, following the pattern described in `instructions.md`. - It defines the default datasets (`entries`, `style`, `runtime`) and their @@ -827,6 +1033,21 @@ function fallback_module() { .node.confirmed { background-color: #774346; } + .node.new-entry { + background-color: #87ceeb; /* sky blue */ + } + .menubar { + display: flex; + padding: 5px; + background-color: #21252b; + border-bottom: 1px solid #181a1f; + } + .search-input { + margin-left: auto; + background-color: #282c34; + color: #abb2bf; + border: 1px solid #181a1f; + } .confirm-wrapper { margin-left: auto; padding-left: 10px; @@ -885,6 +1106,9 @@ function fallback_module() { 'selected_instance_paths.json': { raw: '[]' }, 'confirmed_selected.json': { raw: '[]' }, 'instance_states.json': { raw: '{}' } + }, + 'mode/': { + 'current_mode.json': { raw: '"menubar"' } } } } diff --git a/web/page.js b/web/page.js index 163d890..3ff3c77 100644 --- a/web/page.js +++ b/web/page.js @@ -75,7 +75,8 @@ function fallback_module () { mapping: { 'style': 'style', 'entries': 'entries', - 'runtime': 'runtime' + 'runtime': 'runtime', + 'mode': 'mode' } } }, From dffecec3525fd0350bdbf0d13fcce30ac57e90b5 Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 30 Jul 2025 17:36:10 +0500 Subject: [PATCH 035/130] Fixed the previous search effecting new view issue --- lib/graph_explorer.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index c95d63f..6e6783a 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -664,7 +664,16 @@ async function graph_explorer(opts) { build_and_render_view() return } - const original_view = [...view] // Capture original view + const original_view = build_view_recursive({ + base_path: '/', + parent_instance_path: '', + depth: 0, + is_last_sub : true, + is_hub: false, + parent_pipe_trail: [], + instance_states, + all_entries + }) const search_view = build_search_view_recursive({ query, base_path: '/', From 4d07f05362341b61b3589ffa5e60acd6517c885e Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 30 Jul 2025 18:28:45 +0500 Subject: [PATCH 036/130] Added new different variable for search states --- lib/graph_explorer.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 6e6783a..16aa6e4 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -20,6 +20,7 @@ async function graph_explorer(opts) { let confirmed_instance_paths = [] let all_entries = {} // Holds the entire graph structure from entries.json. let instance_states = {} // Holds expansion state {expanded_subs, expanded_hubs} for each node instance. + let search_state_instances = {} let view = [] // A flat array representing the visible nodes in the graph. let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. @@ -479,12 +480,13 @@ async function graph_explorer(opts) { err_el.textContent = `Error: Missing entry for ${base_path}` return err_el } - - let state = instance_states[instance_path] + + const states = mode === 'search' ? search_state_instances : instance_states + let state = states[instance_path] if (!state) { console.warn(`State not found for instance: ${instance_path}. Using default.`) state = { expanded_subs: false, expanded_hubs: false } - instance_states[instance_path] = state + states[instance_path] = state } const el = document.createElement('div') @@ -674,6 +676,7 @@ async function graph_explorer(opts) { instance_states, all_entries }) + search_state_instances = {} const search_view = build_search_view_recursive({ query, base_path: '/', @@ -682,7 +685,7 @@ async function graph_explorer(opts) { is_last_sub : true, is_hub: false, parent_pipe_trail: [], - instance_states: {}, // Use a temporary state for search + instance_states: search_state_instances, // Use a temporary state for search all_entries, original_view }) From 7e40a5ad0718db89beb02c46570da9b1ce0dbb70 Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 30 Jul 2025 18:40:13 +0500 Subject: [PATCH 037/130] disable toggle expand nodes during search mode --- lib/graph_explorer.js | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 16aa6e4..3ff76d9 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -518,7 +518,7 @@ async function graph_explorer(opts) { if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' - const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' el.innerHTML = `
🪄
/🌐` const wand_el = el.querySelector('.wand') @@ -526,7 +526,13 @@ async function graph_explorer(opts) { if (has_subs) { const prefix_el = el.querySelector('.prefix') - if (prefix_el) prefix_el.onclick = () => toggle_subs(instance_path) + if (prefix_el) { + if (mode !== 'search') { + prefix_el.onclick = () => toggle_subs(instance_path) + } else { + prefix_el.onclick = null + } + } } const name_el = el.querySelector('.name') @@ -538,8 +544,8 @@ async function graph_explorer(opts) { const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) const pipe_html = pipe_trail.map(should_pipe => ``).join('') - const prefix_class = has_subs ? 'prefix clickable' : 'prefix' - const icon_class = (has_hubs && base_path !== '/') ? 'icon clickable' : 'icon' + const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const icon_class = (has_hubs && base_path !== '/') && mode !== 'search' ? 'icon clickable' : 'icon' el.innerHTML = ` ${pipe_html} @@ -548,14 +554,26 @@ async function graph_explorer(opts) { ${entry.name || base_path} ` - if(has_hubs && base_path !== '/') { + if (has_hubs && base_path !== '/') { const icon_el = el.querySelector('.icon') - if (icon_el) icon_el.onclick = () => toggle_hubs(instance_path) + if (icon_el) { + if (mode !== 'search') { + icon_el.onclick = () => toggle_hubs(instance_path) + } else { + icon_el.onclick = null + } + } } - if(has_subs) { + if (has_subs) { const prefix_el = el.querySelector('.prefix') - if (prefix_el) prefix_el.onclick = () => toggle_subs(instance_path) + if (prefix_el) { + if (mode !== 'search') { + prefix_el.onclick = () => toggle_subs(instance_path) + } else { + prefix_el.onclick = null + } + } } const name_el = el.querySelector('.name') From 61e33ef755fd57af9949f10144c03d8a7b24043a Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 30 Jul 2025 19:15:51 +0500 Subject: [PATCH 038/130] bundled --- bundle.js | 303 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 279 insertions(+), 24 deletions(-) diff --git a/bundle.js b/bundle.js index 139dbb5..46e55e6 100644 --- a/bundle.js +++ b/bundle.js @@ -24,7 +24,9 @@ async function graph_explorer(opts) { let confirmed_instance_paths = [] let all_entries = {} // Holds the entire graph structure from entries.json. let instance_states = {} // Holds expansion state {expanded_subs, expanded_hubs} for each node instance. + let search_state_instances = {} let view = [] // A flat array representing the visible nodes in the graph. + let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. @@ -35,7 +37,11 @@ async function graph_explorer(opts) { const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) - shadow.innerHTML = `
` + shadow.innerHTML = ` + +
+ ` + const menubar = shadow.querySelector('.menubar') const container = shadow.querySelector('.graph-container') document.body.style.margin = 0 @@ -61,11 +67,12 @@ async function graph_explorer(opts) { const on = { entries: on_entries, style: inject_style, - runtime: on_runtime + runtime: on_runtime, + mode: on_mode } // Start watching for state changes. This is the main trigger for all updates. await sdb.watch(onbatch) - + return el /****************************************************************************** @@ -89,7 +96,9 @@ async function graph_explorer(opts) { const data = await Promise.all(paths.map(async (path) => { try { const file = await drive.get(path) - return file ? file.raw : null + if (!file) return null + if (type === 'mode') return file.raw.replace(/"/g, '') + return file.raw } catch (e) { console.error(`Error getting file from drive: ${path}`, e) return null @@ -97,7 +106,11 @@ async function graph_explorer(opts) { })) // Call the appropriate handler based on `type`. const func = on[type] - func ? func({ data, type, paths }) : fail(data, type) + if (type === 'mode') { + func ? func(data[0]) : fail(data, type) + } else { + func ? func({ data, type, paths }) : fail(data, type) + } } } @@ -204,6 +217,13 @@ async function graph_explorer(opts) { } } + function on_mode (new_mode) { + if (!new_mode || mode === new_mode) return + mode = new_mode + render_menubar() + handle_mode_change() + } + function inject_style({ data }) { const sheet = new CSSStyleSheet() sheet.replaceSync(data[0]) @@ -219,6 +239,14 @@ async function graph_explorer(opts) { } } + async function update_mode_state (value) { + try { + await drive.put('mode/current_mode.json', JSON.stringify(value)) + } catch (e) { + console.error('Failed to update mode state:', e) + } + } + /****************************************************************************** 3. VIEW AND RENDERING LOGIC - These functions build the `view` array and render the DOM. @@ -441,13 +469,13 @@ async function graph_explorer(opts) { return current_view } - /****************************************************************************** +/****************************************************************************** 4. NODE CREATION AND EVENT HANDLING - `create_node` generates the DOM element for a single node. - It sets up event handlers for user interactions like selecting or toggling. - ******************************************************************************/ +******************************************************************************/ - function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top }) { + function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top, is_search_match, is_direct_match, is_in_original_view }) { const entry = all_entries[base_path] if (!entry) { console.error(`Entry not found for path: ${base_path}. Cannot create node.`) @@ -456,17 +484,29 @@ async function graph_explorer(opts) { err_el.textContent = `Error: Missing entry for ${base_path}` return err_el } - - let state = instance_states[instance_path] + + const states = mode === 'search' ? search_state_instances : instance_states + let state = states[instance_path] if (!state) { console.warn(`State not found for instance: ${instance_path}. Using default.`) state = { expanded_subs: false, expanded_hubs: false } - instance_states[instance_path] = state + states[instance_path] = state } const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path + + if (is_search_match) { + el.classList.add('search-result') + if (is_direct_match) { + el.classList.add('direct-match') + } + if (!is_in_original_view) { + el.classList.add('new-entry') + } + } + if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') @@ -482,7 +522,7 @@ async function graph_explorer(opts) { if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' - const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' el.innerHTML = `
🪄
/🌐` const wand_el = el.querySelector('.wand') @@ -490,7 +530,13 @@ async function graph_explorer(opts) { if (has_subs) { const prefix_el = el.querySelector('.prefix') - if (prefix_el) prefix_el.onclick = () => toggle_subs(instance_path) + if (prefix_el) { + if (mode !== 'search') { + prefix_el.onclick = () => toggle_subs(instance_path) + } else { + prefix_el.onclick = null + } + } } const name_el = el.querySelector('.name') @@ -502,8 +548,8 @@ async function graph_explorer(opts) { const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) const pipe_html = pipe_trail.map(should_pipe => ``).join('') - const prefix_class = has_subs ? 'prefix clickable' : 'prefix' - const icon_class = (has_hubs && base_path !== '/') ? 'icon clickable' : 'icon' + const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const icon_class = (has_hubs && base_path !== '/') && mode !== 'search' ? 'icon clickable' : 'icon' el.innerHTML = ` ${pipe_html} @@ -512,14 +558,26 @@ async function graph_explorer(opts) { ${entry.name || base_path} ` - if(has_hubs && base_path !== '/') { + if (has_hubs && base_path !== '/') { const icon_el = el.querySelector('.icon') - if (icon_el) icon_el.onclick = () => toggle_hubs(instance_path) + if (icon_el) { + if (mode !== 'search') { + icon_el.onclick = () => toggle_hubs(instance_path) + } else { + icon_el.onclick = null + } + } } - if(has_subs) { + if (has_subs) { const prefix_el = el.querySelector('.prefix') - if (prefix_el) prefix_el.onclick = () => toggle_subs(instance_path) + if (prefix_el) { + if (mode !== 'search') { + prefix_el.onclick = () => toggle_subs(instance_path) + } else { + prefix_el.onclick = null + } + } } const name_el = el.querySelector('.name') @@ -582,12 +640,190 @@ async function graph_explorer(opts) { } } - /****************************************************************************** - 5. VIEW MANIPULATION & USER ACTIONS +/****************************************************************************** + 5. MENUBAR AND SEARCH +******************************************************************************/ + function render_menubar () { + menubar.replaceChildren() // Clear existing menubar + const search_button = document.createElement('button') + search_button.textContent = 'Search' + search_button.onclick = toggle_search_mode + + menubar.appendChild(search_button) + + if (mode === 'search') { + const search_input = document.createElement('input') + search_input.type = 'text' + search_input.placeholder = 'Search entries...' + search_input.className = 'search-input' + search_input.oninput = on_search_input + menubar.appendChild(search_input) + search_input.focus() + } + } + + function handle_mode_change () { + if (mode === 'default') { + menubar.style.display = 'none' + } else { + menubar.style.display = 'flex' + } + if (mode !== 'search') { + build_and_render_view() + } + } + + function toggle_search_mode () { + const new_mode = mode === 'search' ? 'menubar' : 'search' + update_mode_state(new_mode) + } + + function on_search_input (event) { + const query = event.target.value.trim() + perform_search(query) + } + + function perform_search (query) { + if (!query) { + build_and_render_view() + return + } + const original_view = build_view_recursive({ + base_path: '/', + parent_instance_path: '', + depth: 0, + is_last_sub : true, + is_hub: false, + parent_pipe_trail: [], + instance_states, + all_entries + }) + search_state_instances = {} + const search_view = build_search_view_recursive({ + query, + base_path: '/', + parent_instance_path: '', + depth: 0, + is_last_sub : true, + is_hub: false, + parent_pipe_trail: [], + instance_states: search_state_instances, // Use a temporary state for search + all_entries, + original_view + }) + render_search_results(search_view, query) + } + + function build_search_view_recursive({ + query, + base_path, + parent_instance_path, + depth, + is_last_sub, + is_hub, + parent_pipe_trail, + instance_states, + all_entries, + original_view + }) { + const entry = all_entries[base_path] + if (!entry) return [] + + const instance_path = `${parent_instance_path}|${base_path}` + const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) + + let sub_results = [] + if (Array.isArray(entry.subs)) { + const children_pipe_trail = [...parent_pipe_trail] + if (depth > 0) children_pipe_trail.push(!is_last_sub) + + sub_results = entry.subs.map((sub_path, i, arr) => { + return build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries, + original_view + }) + }).flat() + } + + const has_matching_descendant = sub_results.length > 0 + + if (!is_direct_match && !has_matching_descendant) { + return [] + } + + instance_states[instance_path] = { + expanded_subs: has_matching_descendant, + expanded_hubs: false + } + + const is_in_original_view = original_view.some(node => node.instance_path === instance_path) + + const current_node_view = { + base_path, + instance_path, + depth, + is_last_sub, + is_hub, + pipe_trail: parent_pipe_trail, + is_hub_on_top: false, + is_search_match: true, + is_direct_match, + is_in_original_view + } + + return [current_node_view, ...sub_results] + } + + function render_search_results (search_view, query) { + view = search_view + container.replaceChildren() + + if (search_view.length === 0) { + const no_results_el = document.createElement('div') + no_results_el.className = 'no-results' + no_results_el.textContent = `No results for "${query}"` + container.appendChild(no_results_el) + return + } + + const fragment = document.createDocumentFragment() + for (const node_data of search_view) { + fragment.appendChild(create_node(node_data)) + } + container.appendChild(fragment) + } + +/****************************************************************************** + 6. VIEW MANIPULATION & USER ACTIONS - These functions handle user interactions like selecting, confirming, toggling, and resetting the graph. ******************************************************************************/ function select_node(ev, instance_path) { + if (mode === 'search') { + let current_path = instance_path + // Traverse up the tree to expand all parents + while (current_path) { + const parent_path = current_path.substring(0, current_path.lastIndexOf('|')) + if (!parent_path) break // Stop if there's no parent left + + if (!instance_states[parent_path]) { + instance_states[parent_path] = { expanded_subs: false, expanded_hubs: false } + } + instance_states[parent_path].expanded_subs = true + current_path = parent_path + } + drive_updated_by_toggle = true + update_runtime_state('instance_states', instance_states) + } + if (ev.ctrlKey) { const new_selected_paths = [...selected_instance_paths] const index = new_selected_paths.indexOf(instance_path) @@ -666,7 +902,7 @@ async function graph_explorer(opts) { } /****************************************************************************** - 6. VIRTUAL SCROLLING + 7. VIRTUAL SCROLLING - These functions implement virtual scrolling to handle large graphs efficiently using an IntersectionObserver. ******************************************************************************/ @@ -787,7 +1023,7 @@ async function graph_explorer(opts) { } /****************************************************************************** - 7. FALLBACK CONFIGURATION + 8. FALLBACK CONFIGURATION - This provides the default data and API configuration for the component, following the pattern described in `instructions.md`. - It defines the default datasets (`entries`, `style`, `runtime`) and their @@ -831,6 +1067,21 @@ function fallback_module() { .node.confirmed { background-color: #774346; } + .node.new-entry { + background-color: #87ceeb; /* sky blue */ + } + .menubar { + display: flex; + padding: 5px; + background-color: #21252b; + border-bottom: 1px solid #181a1f; + } + .search-input { + margin-left: auto; + background-color: #282c34; + color: #abb2bf; + border: 1px solid #181a1f; + } .confirm-wrapper { margin-left: auto; padding-left: 10px; @@ -889,6 +1140,9 @@ function fallback_module() { 'selected_instance_paths.json': { raw: '[]' }, 'confirmed_selected.json': { raw: '[]' }, 'instance_states.json': { raw: '{}' } + }, + 'mode/': { + 'current_mode.json': { raw: '"menubar"' } } } } @@ -995,7 +1249,8 @@ function fallback_module () { mapping: { 'style': 'style', 'entries': 'entries', - 'runtime': 'runtime' + 'runtime': 'runtime', + 'mode': 'mode' } } }, From d5d9603dec78baec6a9890c94c30a7f59999101f Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 31 Jul 2025 11:55:55 +0500 Subject: [PATCH 039/130] Debug the event listners on first click --- lib/graph_explorer.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 3ff76d9..9ca6509 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -664,9 +664,7 @@ async function graph_explorer(opts) { } else { menubar.style.display = 'flex' } - if (mode !== 'search') { - build_and_render_view() - } + build_and_render_view() } function toggle_search_mode () { From b19f37f9c884915b37f28579917aa99fd7c8c4a2 Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 31 Jul 2025 12:30:32 +0500 Subject: [PATCH 040/130] Add automatic switch between modes once an entry is selected --- lib/graph_explorer.js | 48 ++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 9ca6509..e713246 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -23,6 +23,7 @@ async function graph_explorer(opts) { let search_state_instances = {} let view = [] // A flat array representing the visible nodes in the graph. let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. + let previous_mode = 'menubar' let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. @@ -102,11 +103,7 @@ async function graph_explorer(opts) { })) // Call the appropriate handler based on `type`. const func = on[type] - if (type === 'mode') { - func ? func(data[0]) : fail(data, type) - } else { - func ? func({ data, type, paths }) : fail(data, type) - } + func ? func({ data, paths }) : fail(data, type) } } @@ -213,9 +210,30 @@ async function graph_explorer(opts) { } } - function on_mode (new_mode) { - if (!new_mode || mode === new_mode) return - mode = new_mode + function on_mode ({ data, paths }) { + let new_current_mode + let new_previous_mode + + for (let i = 0; i < paths.length; i++) { + const path = paths[i] + const value = data[i] + if (path.endsWith('current_mode.json')) { + new_current_mode = value + } else if (path.endsWith('previous_mode.json')) { + new_previous_mode = value + } + } + + if (new_previous_mode) { + previous_mode = new_previous_mode + } + + if (!new_current_mode || mode === new_current_mode) return + + if (mode && new_current_mode === 'search') { + update_mode_state('previous_mode', mode) + } + mode = new_current_mode render_menubar() handle_mode_change() } @@ -235,11 +253,11 @@ async function graph_explorer(opts) { } } - async function update_mode_state (value) { + async function update_mode_state (name, value) { try { - await drive.put('mode/current_mode.json', JSON.stringify(value)) + await drive.put(`mode/${name}.json`, JSON.stringify(value)) } catch (e) { - console.error('Failed to update mode state:', e) + console.error(`Failed to update mode state for ${name}:`, e) } } @@ -668,8 +686,8 @@ async function graph_explorer(opts) { } function toggle_search_mode () { - const new_mode = mode === 'search' ? 'menubar' : 'search' - update_mode_state(new_mode) + const new_mode = mode === 'search' ? previous_mode : 'search' + update_mode_state('current_mode', new_mode) } function on_search_input (event) { @@ -816,6 +834,7 @@ async function graph_explorer(opts) { } drive_updated_by_toggle = true update_runtime_state('instance_states', instance_states) + update_mode_state('current_mode', previous_mode) } if (ev.ctrlKey) { @@ -1136,7 +1155,8 @@ function fallback_module() { 'instance_states.json': { raw: '{}' } }, 'mode/': { - 'current_mode.json': { raw: '"menubar"' } + 'current_mode.json': { raw: '"default"' }, + 'previous_mode.json': { raw: '"menubar"' } } } } From 54fc97a8007d739107eda0f7cd0ce6af184eb7b7 Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 31 Jul 2025 17:07:07 +0500 Subject: [PATCH 041/130] Fix prefix bugs --- lib/graph_explorer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index e713246..d54a283 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -688,10 +688,12 @@ async function graph_explorer(opts) { function toggle_search_mode () { const new_mode = mode === 'search' ? previous_mode : 'search' update_mode_state('current_mode', new_mode) + search_state_instances = instance_states } function on_search_input (event) { const query = event.target.value.trim() + if (query === '') search_state_instances = instance_states perform_search(query) } @@ -1155,7 +1157,7 @@ function fallback_module() { 'instance_states.json': { raw: '{}' } }, 'mode/': { - 'current_mode.json': { raw: '"default"' }, + 'current_mode.json': { raw: '"menubar"' }, 'previous_mode.json': { raw: '"menubar"' } } } From c628bebda09a9d4ed77caecd14e16c3d64fdc208 Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 31 Jul 2025 17:36:18 +0500 Subject: [PATCH 042/130] Add Drive Persistance across search --- lib/graph_explorer.js | 54 ++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index d54a283..b87d4d5 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -24,8 +24,10 @@ async function graph_explorer(opts) { let view = [] // A flat array representing the visible nodes in the graph. let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. let previous_mode = 'menubar' + let search_query = '' let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. + let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 @@ -87,6 +89,10 @@ async function graph_explorer(opts) { drive_updated_by_toggle = false return } + if (drive_updated_by_search) { + drive_updated_by_search = false + return + } for (const { type, paths } of batch) { if (!paths || paths.length === 0) continue @@ -94,7 +100,6 @@ async function graph_explorer(opts) { try { const file = await drive.get(path) if (!file) return null - if (type === 'mode') return file.raw.replace(/"/g, '') return file.raw } catch (e) { console.error(`Error getting file from drive: ${path}`, e) @@ -213,29 +218,40 @@ async function graph_explorer(opts) { function on_mode ({ data, paths }) { let new_current_mode let new_previous_mode + let new_search_query for (let i = 0; i < paths.length; i++) { - const path = paths[i] - const value = data[i] - if (path.endsWith('current_mode.json')) { - new_current_mode = value - } else if (path.endsWith('previous_mode.json')) { - new_previous_mode = value - } + const path = paths[i] + const raw_data = data[i] + if (raw_data === null) continue + let value + try { + value = JSON.parse(raw_data) + } catch (e) { + console.error(`Failed to parse JSON for ${path}:`, e) + continue + } + if (path.endsWith('current_mode.json')) new_current_mode = value + else if (path.endsWith('previous_mode.json')) new_previous_mode = value + else if (path.endsWith('search_query.json')) new_search_query = value } - if (new_previous_mode) { - previous_mode = new_previous_mode + if (typeof new_search_query === 'string') search_query = new_search_query + if (new_previous_mode) previous_mode = new_previous_mode + if (new_current_mode === 'search' && !search_query) { + search_state_instances = instance_states } - if (!new_current_mode || mode === new_current_mode) return if (mode && new_current_mode === 'search') { - update_mode_state('previous_mode', mode) + update_mode_state('previous_mode', mode) } mode = new_current_mode render_menubar() handle_mode_change() + if (mode === 'search' && search_query) { + perform_search(search_query) + } } function inject_style({ data }) { @@ -671,8 +687,9 @@ async function graph_explorer(opts) { search_input.placeholder = 'Search entries...' search_input.className = 'search-input' search_input.oninput = on_search_input + search_input.value = search_query menubar.appendChild(search_input) - search_input.focus() + requestAnimationFrame(() => search_input.focus()) } } @@ -687,12 +704,20 @@ async function graph_explorer(opts) { function toggle_search_mode () { const new_mode = mode === 'search' ? previous_mode : 'search' + if (mode === 'search') { + search_query = '' + drive_updated_by_search = true + update_mode_state('search_query', '') + } update_mode_state('current_mode', new_mode) search_state_instances = instance_states } function on_search_input (event) { const query = event.target.value.trim() + search_query = query + drive_updated_by_search = true + update_mode_state('search_query', query) if (query === '') search_state_instances = instance_states perform_search(query) } @@ -1158,7 +1183,8 @@ function fallback_module() { }, 'mode/': { 'current_mode.json': { raw: '"menubar"' }, - 'previous_mode.json': { raw: '"menubar"' } + 'previous_mode.json': { raw: '"menubar"' }, + 'search_query.json': { raw: '""' } } } } From 36c53412fbc05e4407c2a36c7695244c3639ec27 Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 31 Jul 2025 17:38:33 +0500 Subject: [PATCH 043/130] bundled --- bundle.js | 84 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/bundle.js b/bundle.js index 46e55e6..a2f27b4 100644 --- a/bundle.js +++ b/bundle.js @@ -27,8 +27,11 @@ async function graph_explorer(opts) { let search_state_instances = {} let view = [] // A flat array representing the visible nodes in the graph. let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. + let previous_mode = 'menubar' + let search_query = '' let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. + let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 @@ -90,6 +93,10 @@ async function graph_explorer(opts) { drive_updated_by_toggle = false return } + if (drive_updated_by_search) { + drive_updated_by_search = false + return + } for (const { type, paths } of batch) { if (!paths || paths.length === 0) continue @@ -97,7 +104,6 @@ async function graph_explorer(opts) { try { const file = await drive.get(path) if (!file) return null - if (type === 'mode') return file.raw.replace(/"/g, '') return file.raw } catch (e) { console.error(`Error getting file from drive: ${path}`, e) @@ -106,11 +112,7 @@ async function graph_explorer(opts) { })) // Call the appropriate handler based on `type`. const func = on[type] - if (type === 'mode') { - func ? func(data[0]) : fail(data, type) - } else { - func ? func({ data, type, paths }) : fail(data, type) - } + func ? func({ data, paths }) : fail(data, type) } } @@ -217,11 +219,43 @@ async function graph_explorer(opts) { } } - function on_mode (new_mode) { - if (!new_mode || mode === new_mode) return - mode = new_mode + function on_mode ({ data, paths }) { + let new_current_mode + let new_previous_mode + let new_search_query + + for (let i = 0; i < paths.length; i++) { + const path = paths[i] + const raw_data = data[i] + if (raw_data === null) continue + let value + try { + value = JSON.parse(raw_data) + } catch (e) { + console.error(`Failed to parse JSON for ${path}:`, e) + continue + } + if (path.endsWith('current_mode.json')) new_current_mode = value + else if (path.endsWith('previous_mode.json')) new_previous_mode = value + else if (path.endsWith('search_query.json')) new_search_query = value + } + + if (typeof new_search_query === 'string') search_query = new_search_query + if (new_previous_mode) previous_mode = new_previous_mode + if (new_current_mode === 'search' && !search_query) { + search_state_instances = instance_states + } + if (!new_current_mode || mode === new_current_mode) return + + if (mode && new_current_mode === 'search') { + update_mode_state('previous_mode', mode) + } + mode = new_current_mode render_menubar() handle_mode_change() + if (mode === 'search' && search_query) { + perform_search(search_query) + } } function inject_style({ data }) { @@ -239,11 +273,11 @@ async function graph_explorer(opts) { } } - async function update_mode_state (value) { + async function update_mode_state (name, value) { try { - await drive.put('mode/current_mode.json', JSON.stringify(value)) + await drive.put(`mode/${name}.json`, JSON.stringify(value)) } catch (e) { - console.error('Failed to update mode state:', e) + console.error(`Failed to update mode state for ${name}:`, e) } } @@ -657,8 +691,9 @@ async function graph_explorer(opts) { search_input.placeholder = 'Search entries...' search_input.className = 'search-input' search_input.oninput = on_search_input + search_input.value = search_query menubar.appendChild(search_input) - search_input.focus() + requestAnimationFrame(() => search_input.focus()) } } @@ -668,18 +703,26 @@ async function graph_explorer(opts) { } else { menubar.style.display = 'flex' } - if (mode !== 'search') { - build_and_render_view() - } + build_and_render_view() } function toggle_search_mode () { - const new_mode = mode === 'search' ? 'menubar' : 'search' - update_mode_state(new_mode) + const new_mode = mode === 'search' ? previous_mode : 'search' + if (mode === 'search') { + search_query = '' + drive_updated_by_search = true + update_mode_state('search_query', '') + } + update_mode_state('current_mode', new_mode) + search_state_instances = instance_states } function on_search_input (event) { const query = event.target.value.trim() + search_query = query + drive_updated_by_search = true + update_mode_state('search_query', query) + if (query === '') search_state_instances = instance_states perform_search(query) } @@ -822,6 +865,7 @@ async function graph_explorer(opts) { } drive_updated_by_toggle = true update_runtime_state('instance_states', instance_states) + update_mode_state('current_mode', previous_mode) } if (ev.ctrlKey) { @@ -1142,7 +1186,9 @@ function fallback_module() { 'instance_states.json': { raw: '{}' } }, 'mode/': { - 'current_mode.json': { raw: '"menubar"' } + 'current_mode.json': { raw: '"menubar"' }, + 'previous_mode.json': { raw: '"menubar"' }, + 'search_query.json': { raw: '""' } } } } From 1494e041d7f040d80027e571fffc0dbbb7e1fb0a Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 15 Aug 2025 16:46:13 +0500 Subject: [PATCH 044/130] formatting using prettier standard --- bundle.js | 423 +++++++++++++++++++++++++++--------------- lib/entries.json | 2 +- lib/graph_explorer.js | 370 +++++++++++++++++++++++------------- web/boot.js | 18 +- web/page.js | 37 ++-- 5 files changed, 551 insertions(+), 299 deletions(-) diff --git a/bundle.js b/bundle.js index a2f27b4..b4cb740 100644 --- a/bundle.js +++ b/bundle.js @@ -8,8 +8,8 @@ const { get } = statedb(fallback_module) module.exports = graph_explorer -async function graph_explorer(opts) { -/****************************************************************************** +async function graph_explorer (opts) { + /****************************************************************************** 1. COMPONENT INITIALIZATION - This sets up the initial state, variables, and the basic DOM structure. - It also initializes the IntersectionObserver for virtual scrolling and @@ -48,7 +48,7 @@ async function graph_explorer(opts) { const container = shadow.querySelector('.graph-container') document.body.style.margin = 0 - + let scroll_update_pending = false container.onscroll = onscroll @@ -60,7 +60,7 @@ async function graph_explorer(opts) { const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') - + const observer = new IntersectionObserver(handle_sentinel_intersection, { root: container, rootMargin: '500px 0px', @@ -78,12 +78,12 @@ async function graph_explorer(opts) { return el -/****************************************************************************** + /****************************************************************************** 2. STATE AND DATA HANDLING - These functions process incoming data from the STATE module's `sdb.watch`. - `onbatch` is the primary entry point. ******************************************************************************/ - async function onbatch(batch) { + async function onbatch (batch) { // Prevent feedback loops from scroll or toggle actions. if (drive_updated_by_scroll) { drive_updated_by_scroll = false @@ -100,32 +100,37 @@ async function graph_explorer(opts) { for (const { type, paths } of batch) { if (!paths || paths.length === 0) continue - const data = await Promise.all(paths.map(async (path) => { - try { - const file = await drive.get(path) - if (!file) return null - return file.raw - } catch (e) { - console.error(`Error getting file from drive: ${path}`, e) - return null - } - })) + const data = await Promise.all( + paths.map(async path => { + try { + const file = await drive.get(path) + if (!file) return null + return file.raw + } catch (e) { + console.error(`Error getting file from drive: ${path}`, e) + return null + } + }) + ) // Call the appropriate handler based on `type`. const func = on[type] func ? func({ data, paths }) : fail(data, type) } } - function fail (data, type) { throw new Error(`Invalid message type: ${type}`, { cause: { data, type } }) } + function fail (data, type) { + throw new Error(`Invalid message type: ${type}`, { cause: { data, type } }) + } - function on_entries({ data }) { + function on_entries ({ data }) { if (!data || data[0] === null || data[0] === undefined) { console.error('Entries data is missing or empty.') all_entries = {} return } try { - const parsed_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + const parsed_data = + typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] if (typeof parsed_data !== 'object' || parsed_data === null) { console.error('Parsed entries data is not a valid object.') all_entries = {} @@ -137,13 +142,16 @@ async function graph_explorer(opts) { all_entries = {} return } - + // After receiving entries, ensure the root node state is initialized and trigger the first render. const root_path = '/' if (all_entries[root_path]) { const root_instance_path = '|/' if (!instance_states[root_instance_path]) { - instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + instance_states[root_instance_path] = { + expanded_subs: true, + expanded_hubs: false + } } build_and_render_view() } else { @@ -160,7 +168,7 @@ async function graph_explorer(opts) { for (let i = 0; i < paths.length; i++) { const path = paths[i] if (data[i] === null) continue - + let value try { value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] @@ -184,10 +192,15 @@ async function graph_explorer(opts) { if (Array.isArray(value)) { selected_instance_paths = value } else { - console.warn('selected_instance_paths is not an array, defaulting to empty.', value) + console.warn( + 'selected_instance_paths is not an array, defaulting to empty.', + value + ) selected_instance_paths = [] } - const changed_paths = [...new Set([...old_paths, ...selected_instance_paths])] + const changed_paths = [ + ...new Set([...old_paths, ...selected_instance_paths]) + ] changed_paths.forEach(p => render_nodes_needed.add(p)) break } @@ -196,19 +209,32 @@ async function graph_explorer(opts) { if (Array.isArray(value)) { confirmed_instance_paths = value } else { - console.warn('confirmed_selected is not an array, defaulting to empty.', value) + console.warn( + 'confirmed_selected is not an array, defaulting to empty.', + value + ) confirmed_instance_paths = [] } - const changed_paths = [...new Set([...old_paths, ...confirmed_instance_paths])] + const changed_paths = [ + ...new Set([...old_paths, ...confirmed_instance_paths]) + ] changed_paths.forEach(p => render_nodes_needed.add(p)) break } case path.endsWith('instance_states.json'): - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - instance_states = value - needs_render = true - } else console.warn('instance_states is not a valid object, ignoring.', value) - break + if ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) + ) { + instance_states = value + needs_render = true + } else + console.warn( + 'instance_states is not a valid object, ignoring.', + value + ) + break } } @@ -258,7 +284,7 @@ async function graph_explorer(opts) { } } - function inject_style({ data }) { + function inject_style ({ data }) { const sheet = new CSSStyleSheet() sheet.replaceSync(data[0]) shadow.adoptedStyleSheets = [sheet] @@ -281,13 +307,13 @@ async function graph_explorer(opts) { } } -/****************************************************************************** + /****************************************************************************** 3. VIEW AND RENDERING LOGIC - These functions build the `view` array and render the DOM. - `build_and_render_view` is the main orchestrator. - `build_view_recursive` creates the flat `view` array from the hierarchical data. ******************************************************************************/ - function build_and_render_view(focal_instance_path, hub_toggle = false) { + function build_and_render_view (focal_instance_path, hub_toggle = false) { if (Object.keys(all_entries).length === 0) { console.warn('No entries available to render.') return @@ -306,7 +332,7 @@ async function graph_explorer(opts) { base_path: '/', parent_instance_path: '', depth: 0, - is_last_sub : true, + is_last_sub: true, is_hub: false, parent_pipe_trail: [], instance_states, @@ -317,12 +343,16 @@ async function graph_explorer(opts) { let new_scroll_top = old_scroll_top if (focal_instance_path) { // If an action was focused on a specific node (like a toggle), try to keep it in the same position. - const old_toggled_node_index = old_view.findIndex(node => node.instance_path === focal_instance_path) - const new_toggled_node_index = view.findIndex(node => node.instance_path === focal_instance_path) + const old_toggled_node_index = old_view.findIndex( + node => node.instance_path === focal_instance_path + ) + const new_toggled_node_index = view.findIndex( + node => node.instance_path === focal_instance_path + ) if (old_toggled_node_index !== -1 && new_toggled_node_index !== -1) { const index_change = new_toggled_node_index - old_toggled_node_index - new_scroll_top = old_scroll_top + (index_change * node_height) + new_scroll_top = old_scroll_top + index_change * node_height } } else if (old_view.length > 0) { // Otherwise, try to keep the topmost visible node in the same position. @@ -330,14 +360,19 @@ async function graph_explorer(opts) { const scroll_offset = old_scroll_top % node_height const old_top_node = old_view[old_top_node_index] if (old_top_node) { - const new_top_node_index = view.findIndex(node => node.instance_path === old_top_node.instance_path) + const new_top_node_index = view.findIndex( + node => node.instance_path === old_top_node.instance_path + ) if (new_top_node_index !== -1) { - new_scroll_top = (new_top_node_index * node_height) + scroll_offset + new_scroll_top = new_top_node_index * node_height + scroll_offset } } } - const render_anchor_index = Math.max(0, Math.floor(new_scroll_top / node_height)) + const render_anchor_index = Math.max( + 0, + Math.floor(new_scroll_top / node_height) + ) start_index = Math.max(0, render_anchor_index - chunk_size) end_index = Math.min(view.length, render_anchor_index + chunk_size) @@ -353,7 +388,9 @@ async function graph_explorer(opts) { container.appendChild(bottom_sentinel) top_sentinel.style.height = `${start_index * node_height}px` - bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + bottom_sentinel.style.height = `${ + (view.length - end_index) * node_height + }px` observer.observe(top_sentinel) observer.observe(bottom_sentinel) @@ -375,7 +412,7 @@ async function graph_explorer(opts) { const container_height = container.clientHeight const content_height = view.length * node_height const max_scroll_top = content_height - container_height - + if (new_scroll_top > max_scroll_top) { spacer_initial_height = new_scroll_top - max_scroll_top spacer_initial_scroll_top = new_scroll_top @@ -396,7 +433,7 @@ async function graph_explorer(opts) { } // Traverses the hierarchical `all_entries` data and builds a flat `view` array for rendering. - function build_view_recursive({ + function build_view_recursive ({ base_path, parent_instance_path, parent_base_path = null, @@ -411,7 +448,7 @@ async function graph_explorer(opts) { const instance_path = `${parent_instance_path}|${base_path}` const entry = all_entries[base_path] if (!entry) return [] - + if (!instance_states[instance_path]) { instance_states[instance_path] = { expanded_subs: false, @@ -419,16 +456,17 @@ async function graph_explorer(opts) { } } const state = instance_states[instance_path] - const is_hub_on_top = (base_path === all_entries[parent_base_path]?.hubs?.[0]) || (base_path === '/') + const is_hub_on_top = + base_path === all_entries[parent_base_path]?.hubs?.[0] || + base_path === '/' // Calculate the pipe trail for drawing the tree lines. Quite complex logic here. const children_pipe_trail = [...parent_pipe_trail] let last_pipe = null if (depth > 0) { - if (is_hub) { last_pipe = [...parent_pipe_trail] - if (is_last_sub) { + if (is_last_sub) { children_pipe_trail.pop() children_pipe_trail.push(true) last_pipe.pop() @@ -462,7 +500,7 @@ async function graph_explorer(opts) { parent_instance_path: instance_path, parent_base_path: base_path, depth: depth + 1, - is_last_sub : i === arr.length - 1, + is_last_sub: i === arr.length - 1, is_hub: true, is_first_hub: is_hub ? is_hub_on_top : false, parent_pipe_trail: children_pipe_trail, @@ -479,7 +517,10 @@ async function graph_explorer(opts) { depth, is_last_sub, is_hub, - pipe_trail: ((is_hub && is_last_sub) || (is_hub && is_hub_on_top)) ? last_pipe : parent_pipe_trail, + pipe_trail: + (is_hub && is_last_sub) || (is_hub && is_hub_on_top) + ? last_pipe + : parent_pipe_trail, is_hub_on_top }) @@ -502,17 +543,30 @@ async function graph_explorer(opts) { } return current_view } - -/****************************************************************************** + + /****************************************************************************** 4. NODE CREATION AND EVENT HANDLING - `create_node` generates the DOM element for a single node. - It sets up event handlers for user interactions like selecting or toggling. ******************************************************************************/ - - function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top, is_search_match, is_direct_match, is_in_original_view }) { + + function create_node ({ + base_path, + instance_path, + depth, + is_last_sub, + is_hub, + pipe_trail, + is_hub_on_top, + is_search_match, + is_direct_match, + is_in_original_view + }) { const entry = all_entries[base_path] if (!entry) { - console.error(`Entry not found for path: ${base_path}. Cannot create node.`) + console.error( + `Entry not found for path: ${base_path}. Cannot create node.` + ) const err_el = document.createElement('div') err_el.className = 'node error' err_el.textContent = `Error: Missing entry for ${base_path}` @@ -522,7 +576,9 @@ async function graph_explorer(opts) { const states = mode === 'search' ? search_state_instances : instance_states let state = states[instance_path] if (!state) { - console.warn(`State not found for instance: ${instance_path}. Using default.`) + console.warn( + `State not found for instance: ${instance_path}. Using default.` + ) state = { expanded_subs: false, expanded_hubs: false } states[instance_path] = state } @@ -541,12 +597,14 @@ async function graph_explorer(opts) { } } - if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') - if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') + if (selected_instance_paths.includes(instance_path)) + el.classList.add('selected') + if (confirmed_instance_paths.includes(instance_path)) + el.classList.add('confirmed') const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 - + if (depth) { el.style.paddingLeft = '17.5px' } @@ -556,7 +614,8 @@ async function graph_explorer(opts) { if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' - const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const prefix_class = + has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' el.innerHTML = `
🪄
/🌐` const wand_el = el.querySelector('.wand') @@ -574,16 +633,31 @@ async function graph_explorer(opts) { } const name_el = el.querySelector('.name') - if (name_el) name_el.onclick = (ev) => select_node(ev, instance_path, base_path) + if (name_el) + name_el.onclick = ev => select_node(ev, instance_path, base_path) return el } - const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) - const pipe_html = pipe_trail.map(should_pipe => ``).join('') - - const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' - const icon_class = (has_hubs && base_path !== '/') && mode !== 'search' ? 'icon clickable' : 'icon' + const prefix_class_name = get_prefix({ + is_last_sub, + has_subs, + state, + is_hub, + is_hub_on_top + }) + const pipe_html = pipe_trail + .map( + should_pipe => `` + ) + .join('') + + const prefix_class = + has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const icon_class = + has_hubs && base_path !== '/' && mode !== 'search' + ? 'icon clickable' + : 'icon' el.innerHTML = ` ${pipe_html} @@ -615,15 +689,22 @@ async function graph_explorer(opts) { } const name_el = el.querySelector('.name') - if (name_el) name_el.onclick = (ev) => select_node(ev, instance_path, base_path) - - if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { + if (name_el) + name_el.onclick = ev => select_node(ev, instance_path, base_path) + + if ( + selected_instance_paths.includes(instance_path) || + confirmed_instance_paths.includes(instance_path) + ) { const checkbox_div = document.createElement('div') checkbox_div.className = 'confirm-wrapper' const is_confirmed = confirmed_instance_paths.includes(instance_path) - checkbox_div.innerHTML = `` + checkbox_div.innerHTML = `` const checkbox_input = checkbox_div.querySelector('input') - if (checkbox_input) checkbox_input.onchange = (ev) => handle_confirm(ev, instance_path) + if (checkbox_input) + checkbox_input.onchange = ev => handle_confirm(ev, instance_path) el.appendChild(checkbox_div) } @@ -634,7 +715,9 @@ async function graph_explorer(opts) { function re_render_node (instance_path) { const node_data = view.find(n => n.instance_path === instance_path) if (node_data) { - const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + const old_node_el = shadow.querySelector( + `[data-instance_path="${CSS.escape(instance_path)}"]` + ) if (old_node_el) { const new_node_el = create_node(node_data) old_node_el.replaceWith(new_node_el) @@ -643,7 +726,7 @@ async function graph_explorer(opts) { } // `get_prefix` determines which box-drawing character to use for the node's prefix. It gives the name of a specific CSS class. - function get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) { + function get_prefix ({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) { if (!state) { console.error('get_prefix called with invalid state.') return 'middle-line' @@ -664,17 +747,19 @@ async function graph_explorer(opts) { } else if (is_last_sub) { if (expanded_subs && expanded_hubs) return 'bottom-cross' if (expanded_subs) return 'bottom-tee-down' - if (expanded_hubs) return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' + if (expanded_hubs) + return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' return has_subs ? 'bottom-line' : 'bottom-light-line' } else { if (expanded_subs && expanded_hubs) return 'middle-cross' if (expanded_subs) return 'middle-tee-down' - if (expanded_hubs) return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' + if (expanded_hubs) + return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' return has_subs ? 'middle-line' : 'middle-light-line' } } - -/****************************************************************************** + + /****************************************************************************** 5. MENUBAR AND SEARCH ******************************************************************************/ function render_menubar () { @@ -735,7 +820,7 @@ async function graph_explorer(opts) { base_path: '/', parent_instance_path: '', depth: 0, - is_last_sub : true, + is_last_sub: true, is_hub: false, parent_pipe_trail: [], instance_states, @@ -747,7 +832,7 @@ async function graph_explorer(opts) { base_path: '/', parent_instance_path: '', depth: 0, - is_last_sub : true, + is_last_sub: true, is_hub: false, parent_pipe_trail: [], instance_states: search_state_instances, // Use a temporary state for search @@ -757,7 +842,7 @@ async function graph_explorer(opts) { render_search_results(search_view, query) } - function build_search_view_recursive({ + function build_search_view_recursive ({ query, base_path, parent_instance_path, @@ -773,27 +858,30 @@ async function graph_explorer(opts) { if (!entry) return [] const instance_path = `${parent_instance_path}|${base_path}` - const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) + const is_direct_match = + entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) let sub_results = [] if (Array.isArray(entry.subs)) { const children_pipe_trail = [...parent_pipe_trail] if (depth > 0) children_pipe_trail.push(!is_last_sub) - sub_results = entry.subs.map((sub_path, i, arr) => { - return build_search_view_recursive({ - query, - base_path: sub_path, - parent_instance_path: instance_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: false, - parent_pipe_trail: children_pipe_trail, - instance_states, - all_entries, - original_view + sub_results = entry.subs + .map((sub_path, i, arr) => { + return build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries, + original_view + }) }) - }).flat() + .flat() } const has_matching_descendant = sub_results.length > 0 @@ -807,7 +895,9 @@ async function graph_explorer(opts) { expanded_hubs: false } - const is_in_original_view = original_view.some(node => node.instance_path === instance_path) + const is_in_original_view = original_view.some( + node => node.instance_path === instance_path + ) const current_node_view = { base_path, @@ -844,21 +934,27 @@ async function graph_explorer(opts) { container.appendChild(fragment) } -/****************************************************************************** + /****************************************************************************** 6. VIEW MANIPULATION & USER ACTIONS - These functions handle user interactions like selecting, confirming, toggling, and resetting the graph. ******************************************************************************/ - function select_node(ev, instance_path) { + function select_node (ev, instance_path) { if (mode === 'search') { let current_path = instance_path // Traverse up the tree to expand all parents while (current_path) { - const parent_path = current_path.substring(0, current_path.lastIndexOf('|')) + const parent_path = current_path.substring( + 0, + current_path.lastIndexOf('|') + ) if (!parent_path) break // Stop if there's no parent left if (!instance_states[parent_path]) { - instance_states[parent_path] = { expanded_subs: false, expanded_hubs: false } + instance_states[parent_path] = { + expanded_subs: false, + expanded_hubs: false + } } instance_states[parent_path].expanded_subs = true current_path = parent_path @@ -882,7 +978,7 @@ async function graph_explorer(opts) { } } - function handle_confirm(ev, instance_path) { + function handle_confirm (ev, instance_path) { if (!ev.target) return console.warn('Checkbox event target is missing.') const is_checked = ev.target.checked const new_selected_paths = [...selected_instance_paths] @@ -892,11 +988,11 @@ async function graph_explorer(opts) { const idx = new_selected_paths.indexOf(instance_path) if (idx > -1) new_selected_paths.splice(idx, 1) if (!new_confirmed_paths.includes(instance_path)) { - new_confirmed_paths.push(instance_path) + new_confirmed_paths.push(instance_path) } } else { if (!new_selected_paths.includes(instance_path)) { - new_selected_paths.push(instance_path) + new_selected_paths.push(instance_path) } const idx = new_confirmed_paths.indexOf(instance_path) if (idx > -1) new_confirmed_paths.splice(idx, 1) @@ -905,10 +1001,15 @@ async function graph_explorer(opts) { update_runtime_state('confirmed_selected', new_confirmed_paths) } - function toggle_subs(instance_path) { + function toggle_subs (instance_path) { if (!instance_states[instance_path]) { - console.warn(`Toggling subs for non-existent state: ${instance_path}. Creating default state.`) - instance_states[instance_path] = { expanded_subs: false, expanded_hubs: false } + console.warn( + `Toggling subs for non-existent state: ${instance_path}. Creating default state.` + ) + instance_states[instance_path] = { + expanded_subs: false, + expanded_hubs: false + } } const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs @@ -918,10 +1019,15 @@ async function graph_explorer(opts) { update_runtime_state('instance_states', instance_states) } - function toggle_hubs(instance_path) { + function toggle_hubs (instance_path) { if (!instance_states[instance_path]) { - console.warn(`Toggling hubs for non-existent state: ${instance_path}. Creating default state.`) - instance_states[instance_path] = { expanded_subs: false, expanded_hubs: false } + console.warn( + `Toggling hubs for non-existent state: ${instance_path}. Creating default state.` + ) + instance_states[instance_path] = { + expanded_subs: false, + expanded_hubs: false + } } const state = instance_states[instance_path] state.expanded_hubs ? hub_num-- : hub_num++ @@ -931,12 +1037,15 @@ async function graph_explorer(opts) { update_runtime_state('instance_states', instance_states) } - function reset() { + function reset () { const root_path = '/' const root_instance_path = '|/' const new_instance_states = {} if (all_entries[root_path]) { - new_instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + new_instance_states[root_instance_path] = { + expanded_subs: true, + expanded_hubs: false + } } update_runtime_state('vertical_scroll_value', 0) update_runtime_state('horizontal_scroll_value', 0) @@ -945,12 +1054,12 @@ async function graph_explorer(opts) { update_runtime_state('instance_states', new_instance_states) } -/****************************************************************************** + /****************************************************************************** 7. VIRTUAL SCROLLING - These functions implement virtual scrolling to handle large graphs efficiently using an IntersectionObserver. ******************************************************************************/ - function onscroll() { + function onscroll () { if (scroll_update_pending) return scroll_update_pending = true requestAnimationFrame(() => { @@ -979,12 +1088,15 @@ async function graph_explorer(opts) { }) } - async function fill_viewport_downwards() { + async function fill_viewport_downwards () { if (is_rendering || end_index >= view.length) return is_rendering = true const container_rect = container.getBoundingClientRect() let sentinel_rect = bottom_sentinel.getBoundingClientRect() - while (end_index < view.length && sentinel_rect.top < container_rect.bottom + 500) { + while ( + end_index < view.length && + sentinel_rect.top < container_rect.bottom + 500 + ) { render_next_chunk() await new Promise(resolve => requestAnimationFrame(resolve)) sentinel_rect = bottom_sentinel.getBoundingClientRect() @@ -992,7 +1104,7 @@ async function graph_explorer(opts) { is_rendering = false } - async function fill_viewport_upwards() { + async function fill_viewport_upwards () { if (is_rendering || start_index <= 0) return is_rendering = true const container_rect = container.getBoundingClientRect() @@ -1005,7 +1117,7 @@ async function graph_explorer(opts) { is_rendering = false } - function handle_sentinel_intersection(entries) { + function handle_sentinel_intersection (entries) { entries.forEach(entry => { if (entry.isIntersecting) { if (entry.target === top_sentinel) fill_viewport_upwards() @@ -1014,22 +1126,26 @@ async function graph_explorer(opts) { }) } - function render_next_chunk() { + function render_next_chunk () { if (end_index >= view.length) return const fragment = document.createDocumentFragment() const next_end = Math.min(view.length, end_index + chunk_size) - for (let i = end_index; i < next_end; i++) if (view[i]) fragment.appendChild(create_node(view[i])) + for (let i = end_index; i < next_end; i++) + if (view[i]) fragment.appendChild(create_node(view[i])) container.insertBefore(fragment, bottom_sentinel) end_index = next_end - bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + bottom_sentinel.style.height = `${ + (view.length - end_index) * node_height + }px` cleanup_dom(false) } - function render_prev_chunk() { + function render_prev_chunk () { if (start_index <= 0) return const fragment = document.createDocumentFragment() const prev_start = Math.max(0, start_index - chunk_size) - for (let i = prev_start; i < start_index; i++) if (view[i]) fragment.appendChild(create_node(view[i])) + for (let i = prev_start; i < start_index; i++) + if (view[i]) fragment.appendChild(create_node(view[i])) container.insertBefore(fragment, top_sentinel.nextSibling) start_index = prev_start top_sentinel.style.height = `${start_index * node_height}px` @@ -1037,7 +1153,7 @@ async function graph_explorer(opts) { } // Removes nodes from the DOM that are far outside the viewport. - function cleanup_dom(is_scrolling_up) { + function cleanup_dom (is_scrolling_up) { const rendered_count = end_index - start_index if (rendered_count <= max_rendered_nodes) return @@ -1051,7 +1167,9 @@ async function graph_explorer(opts) { } } end_index -= to_remove_count - bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + bottom_sentinel.style.height = `${ + (view.length - end_index) * node_height + }px` } else { // If scrolling down, remove nodes from the top. for (let i = 0; i < to_remove_count; i++) { @@ -1073,11 +1191,11 @@ async function graph_explorer(opts) { - It defines the default datasets (`entries`, `style`, `runtime`) and their initial values. ******************************************************************************/ -function fallback_module() { +function fallback_module () { return { api: fallback_instance } - function fallback_instance() { + function fallback_instance () { return { drive: { 'entries/': { @@ -1194,6 +1312,7 @@ function fallback_module() { } } } + }).call(this)}).call(this,"/lib/graph_explorer.js") },{"./STATE":1}],3:[function(require,module,exports){ const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' @@ -1207,14 +1326,16 @@ if (!has_save) { localStorage.clear() } -fetch(init_url, fetch_opts).then(res => res.text()).then(async source => { - const module = { exports: {} } - const f = new Function('module', 'require', source) - f(module, require) - const init = module.exports - await init(args, prefix) - require('./page') // or whatever is otherwise the main entry of our project -}) +fetch(init_url, fetch_opts) + .then(res => res.text()) + .then(async source => { + const module = { exports: {} } + const f = new Function('module', 'require', source) + f(module, require) + const init = module.exports + await init(args, prefix) + require('./page') // or whatever is otherwise the main entry of our project + }) },{"./page":4}],4:[function(require,module,exports){ (function (__filename,__dirname){(function (){ @@ -1229,11 +1350,13 @@ const app = require('..') const sheet = new CSSStyleSheet() config().then(() => boot({ sid: '' })) -async function config() { - const path = path => new URL(`../src/node_modules/${path}`, `file://${__dirname}`).href.slice(8) +async function config () { + const path = path => + new URL(`../src/node_modules/${path}`, `file://${__dirname}`).href.slice(8) const html = document.documentElement const meta = document.createElement('meta') - const font = 'https://fonts.googleapis.com/css?family=Nunito:300,400,700,900|Slackey&display=swap' + const font = + 'https://fonts.googleapis.com/css?family=Nunito:300,400,700,900|Slackey&display=swap' const loadFont = `` html.setAttribute('lang', 'en') meta.setAttribute('name', 'viewport') @@ -1247,7 +1370,7 @@ async function config() { /****************************************************************************** PAGE BOOT ******************************************************************************/ -async function boot(opts) { +async function boot (opts) { // ---------------------------------------- // ID + JSON STATE // ---------------------------------------- @@ -1268,35 +1391,38 @@ async function boot(opts) { // ---------------------------------------- // ELEMENTS // ---------------------------------------- - { // desktop + { + // desktop shadow.append(await app(subs[0])) } // ---------------------------------------- // INIT // ---------------------------------------- - async function onbatch(batch) { - for (const {type, paths} of batch) { - const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all( + paths.map(path => drive.get(path).then(file => file.raw)) + ) on[type] && on[type](data) } } } -async function inject(data) { +async function inject (data) { sheet.replaceSync(data.join('\n')) } function fallback_module () { return { _: { - '..': { - $: '', + '..': { + $: '', 0: '', mapping: { - 'style': 'style', - 'entries': 'entries', - 'runtime': 'runtime', - 'mode': 'mode' + style: 'style', + entries: 'entries', + runtime: 'runtime', + mode: 'mode' } } }, @@ -1306,5 +1432,6 @@ function fallback_module () { } } } + }).call(this)}).call(this,"/web/page.js","/web") },{"..":2,"../lib/STATE":1}]},{},[3]); diff --git a/lib/entries.json b/lib/entries.json index c9047f6..132978a 100644 --- a/lib/entries.json +++ b/lib/entries.json @@ -767,4 +767,4 @@ "/tasks/0:theme_widget/subs/3:text_editor/editor" ] } -} \ No newline at end of file +} diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index b87d4d5..3f93183 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -4,8 +4,8 @@ const { get } = statedb(fallback_module) module.exports = graph_explorer -async function graph_explorer(opts) { -/****************************************************************************** +async function graph_explorer (opts) { + /****************************************************************************** 1. COMPONENT INITIALIZATION - This sets up the initial state, variables, and the basic DOM structure. - It also initializes the IntersectionObserver for virtual scrolling and @@ -44,7 +44,7 @@ async function graph_explorer(opts) { const container = shadow.querySelector('.graph-container') document.body.style.margin = 0 - + let scroll_update_pending = false container.onscroll = onscroll @@ -56,7 +56,7 @@ async function graph_explorer(opts) { const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') - + const observer = new IntersectionObserver(handle_sentinel_intersection, { root: container, rootMargin: '500px 0px', @@ -74,12 +74,12 @@ async function graph_explorer(opts) { return el -/****************************************************************************** + /****************************************************************************** 2. STATE AND DATA HANDLING - These functions process incoming data from the STATE module's `sdb.watch`. - `onbatch` is the primary entry point. ******************************************************************************/ - async function onbatch(batch) { + async function onbatch (batch) { // Prevent feedback loops from scroll or toggle actions. if (drive_updated_by_scroll) { drive_updated_by_scroll = false @@ -96,32 +96,37 @@ async function graph_explorer(opts) { for (const { type, paths } of batch) { if (!paths || paths.length === 0) continue - const data = await Promise.all(paths.map(async (path) => { - try { - const file = await drive.get(path) - if (!file) return null - return file.raw - } catch (e) { - console.error(`Error getting file from drive: ${path}`, e) - return null - } - })) + const data = await Promise.all( + paths.map(async path => { + try { + const file = await drive.get(path) + if (!file) return null + return file.raw + } catch (e) { + console.error(`Error getting file from drive: ${path}`, e) + return null + } + }) + ) // Call the appropriate handler based on `type`. const func = on[type] func ? func({ data, paths }) : fail(data, type) } } - function fail (data, type) { throw new Error(`Invalid message type: ${type}`, { cause: { data, type } }) } + function fail (data, type) { + throw new Error(`Invalid message type: ${type}`, { cause: { data, type } }) + } - function on_entries({ data }) { + function on_entries ({ data }) { if (!data || data[0] === null || data[0] === undefined) { console.error('Entries data is missing or empty.') all_entries = {} return } try { - const parsed_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + const parsed_data = + typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] if (typeof parsed_data !== 'object' || parsed_data === null) { console.error('Parsed entries data is not a valid object.') all_entries = {} @@ -133,13 +138,16 @@ async function graph_explorer(opts) { all_entries = {} return } - + // After receiving entries, ensure the root node state is initialized and trigger the first render. const root_path = '/' if (all_entries[root_path]) { const root_instance_path = '|/' if (!instance_states[root_instance_path]) { - instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + instance_states[root_instance_path] = { + expanded_subs: true, + expanded_hubs: false + } } build_and_render_view() } else { @@ -156,7 +164,7 @@ async function graph_explorer(opts) { for (let i = 0; i < paths.length; i++) { const path = paths[i] if (data[i] === null) continue - + let value try { value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] @@ -180,10 +188,15 @@ async function graph_explorer(opts) { if (Array.isArray(value)) { selected_instance_paths = value } else { - console.warn('selected_instance_paths is not an array, defaulting to empty.', value) + console.warn( + 'selected_instance_paths is not an array, defaulting to empty.', + value + ) selected_instance_paths = [] } - const changed_paths = [...new Set([...old_paths, ...selected_instance_paths])] + const changed_paths = [ + ...new Set([...old_paths, ...selected_instance_paths]) + ] changed_paths.forEach(p => render_nodes_needed.add(p)) break } @@ -192,19 +205,32 @@ async function graph_explorer(opts) { if (Array.isArray(value)) { confirmed_instance_paths = value } else { - console.warn('confirmed_selected is not an array, defaulting to empty.', value) + console.warn( + 'confirmed_selected is not an array, defaulting to empty.', + value + ) confirmed_instance_paths = [] } - const changed_paths = [...new Set([...old_paths, ...confirmed_instance_paths])] + const changed_paths = [ + ...new Set([...old_paths, ...confirmed_instance_paths]) + ] changed_paths.forEach(p => render_nodes_needed.add(p)) break } case path.endsWith('instance_states.json'): - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - instance_states = value - needs_render = true - } else console.warn('instance_states is not a valid object, ignoring.', value) - break + if ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) + ) { + instance_states = value + needs_render = true + } else + console.warn( + 'instance_states is not a valid object, ignoring.', + value + ) + break } } @@ -254,7 +280,7 @@ async function graph_explorer(opts) { } } - function inject_style({ data }) { + function inject_style ({ data }) { const sheet = new CSSStyleSheet() sheet.replaceSync(data[0]) shadow.adoptedStyleSheets = [sheet] @@ -277,13 +303,13 @@ async function graph_explorer(opts) { } } -/****************************************************************************** + /****************************************************************************** 3. VIEW AND RENDERING LOGIC - These functions build the `view` array and render the DOM. - `build_and_render_view` is the main orchestrator. - `build_view_recursive` creates the flat `view` array from the hierarchical data. ******************************************************************************/ - function build_and_render_view(focal_instance_path, hub_toggle = false) { + function build_and_render_view (focal_instance_path, hub_toggle = false) { if (Object.keys(all_entries).length === 0) { console.warn('No entries available to render.') return @@ -302,7 +328,7 @@ async function graph_explorer(opts) { base_path: '/', parent_instance_path: '', depth: 0, - is_last_sub : true, + is_last_sub: true, is_hub: false, parent_pipe_trail: [], instance_states, @@ -313,12 +339,16 @@ async function graph_explorer(opts) { let new_scroll_top = old_scroll_top if (focal_instance_path) { // If an action was focused on a specific node (like a toggle), try to keep it in the same position. - const old_toggled_node_index = old_view.findIndex(node => node.instance_path === focal_instance_path) - const new_toggled_node_index = view.findIndex(node => node.instance_path === focal_instance_path) + const old_toggled_node_index = old_view.findIndex( + node => node.instance_path === focal_instance_path + ) + const new_toggled_node_index = view.findIndex( + node => node.instance_path === focal_instance_path + ) if (old_toggled_node_index !== -1 && new_toggled_node_index !== -1) { const index_change = new_toggled_node_index - old_toggled_node_index - new_scroll_top = old_scroll_top + (index_change * node_height) + new_scroll_top = old_scroll_top + index_change * node_height } } else if (old_view.length > 0) { // Otherwise, try to keep the topmost visible node in the same position. @@ -326,14 +356,19 @@ async function graph_explorer(opts) { const scroll_offset = old_scroll_top % node_height const old_top_node = old_view[old_top_node_index] if (old_top_node) { - const new_top_node_index = view.findIndex(node => node.instance_path === old_top_node.instance_path) + const new_top_node_index = view.findIndex( + node => node.instance_path === old_top_node.instance_path + ) if (new_top_node_index !== -1) { - new_scroll_top = (new_top_node_index * node_height) + scroll_offset + new_scroll_top = new_top_node_index * node_height + scroll_offset } } } - const render_anchor_index = Math.max(0, Math.floor(new_scroll_top / node_height)) + const render_anchor_index = Math.max( + 0, + Math.floor(new_scroll_top / node_height) + ) start_index = Math.max(0, render_anchor_index - chunk_size) end_index = Math.min(view.length, render_anchor_index + chunk_size) @@ -349,7 +384,9 @@ async function graph_explorer(opts) { container.appendChild(bottom_sentinel) top_sentinel.style.height = `${start_index * node_height}px` - bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + bottom_sentinel.style.height = `${ + (view.length - end_index) * node_height + }px` observer.observe(top_sentinel) observer.observe(bottom_sentinel) @@ -371,7 +408,7 @@ async function graph_explorer(opts) { const container_height = container.clientHeight const content_height = view.length * node_height const max_scroll_top = content_height - container_height - + if (new_scroll_top > max_scroll_top) { spacer_initial_height = new_scroll_top - max_scroll_top spacer_initial_scroll_top = new_scroll_top @@ -392,7 +429,7 @@ async function graph_explorer(opts) { } // Traverses the hierarchical `all_entries` data and builds a flat `view` array for rendering. - function build_view_recursive({ + function build_view_recursive ({ base_path, parent_instance_path, parent_base_path = null, @@ -407,7 +444,7 @@ async function graph_explorer(opts) { const instance_path = `${parent_instance_path}|${base_path}` const entry = all_entries[base_path] if (!entry) return [] - + if (!instance_states[instance_path]) { instance_states[instance_path] = { expanded_subs: false, @@ -415,16 +452,17 @@ async function graph_explorer(opts) { } } const state = instance_states[instance_path] - const is_hub_on_top = (base_path === all_entries[parent_base_path]?.hubs?.[0]) || (base_path === '/') + const is_hub_on_top = + base_path === all_entries[parent_base_path]?.hubs?.[0] || + base_path === '/' // Calculate the pipe trail for drawing the tree lines. Quite complex logic here. const children_pipe_trail = [...parent_pipe_trail] let last_pipe = null if (depth > 0) { - if (is_hub) { last_pipe = [...parent_pipe_trail] - if (is_last_sub) { + if (is_last_sub) { children_pipe_trail.pop() children_pipe_trail.push(true) last_pipe.pop() @@ -458,7 +496,7 @@ async function graph_explorer(opts) { parent_instance_path: instance_path, parent_base_path: base_path, depth: depth + 1, - is_last_sub : i === arr.length - 1, + is_last_sub: i === arr.length - 1, is_hub: true, is_first_hub: is_hub ? is_hub_on_top : false, parent_pipe_trail: children_pipe_trail, @@ -475,7 +513,10 @@ async function graph_explorer(opts) { depth, is_last_sub, is_hub, - pipe_trail: ((is_hub && is_last_sub) || (is_hub && is_hub_on_top)) ? last_pipe : parent_pipe_trail, + pipe_trail: + (is_hub && is_last_sub) || (is_hub && is_hub_on_top) + ? last_pipe + : parent_pipe_trail, is_hub_on_top }) @@ -498,17 +539,30 @@ async function graph_explorer(opts) { } return current_view } - -/****************************************************************************** + + /****************************************************************************** 4. NODE CREATION AND EVENT HANDLING - `create_node` generates the DOM element for a single node. - It sets up event handlers for user interactions like selecting or toggling. ******************************************************************************/ - - function create_node({ base_path, instance_path, depth, is_last_sub, is_hub, pipe_trail, is_hub_on_top, is_search_match, is_direct_match, is_in_original_view }) { + + function create_node ({ + base_path, + instance_path, + depth, + is_last_sub, + is_hub, + pipe_trail, + is_hub_on_top, + is_search_match, + is_direct_match, + is_in_original_view + }) { const entry = all_entries[base_path] if (!entry) { - console.error(`Entry not found for path: ${base_path}. Cannot create node.`) + console.error( + `Entry not found for path: ${base_path}. Cannot create node.` + ) const err_el = document.createElement('div') err_el.className = 'node error' err_el.textContent = `Error: Missing entry for ${base_path}` @@ -518,7 +572,9 @@ async function graph_explorer(opts) { const states = mode === 'search' ? search_state_instances : instance_states let state = states[instance_path] if (!state) { - console.warn(`State not found for instance: ${instance_path}. Using default.`) + console.warn( + `State not found for instance: ${instance_path}. Using default.` + ) state = { expanded_subs: false, expanded_hubs: false } states[instance_path] = state } @@ -537,12 +593,14 @@ async function graph_explorer(opts) { } } - if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') - if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') + if (selected_instance_paths.includes(instance_path)) + el.classList.add('selected') + if (confirmed_instance_paths.includes(instance_path)) + el.classList.add('confirmed') const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 - + if (depth) { el.style.paddingLeft = '17.5px' } @@ -552,7 +610,8 @@ async function graph_explorer(opts) { if (base_path === '/' && instance_path === '|/') { const { expanded_subs } = state const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' - const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const prefix_class = + has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' el.innerHTML = `
🪄
/🌐` const wand_el = el.querySelector('.wand') @@ -570,16 +629,31 @@ async function graph_explorer(opts) { } const name_el = el.querySelector('.name') - if (name_el) name_el.onclick = (ev) => select_node(ev, instance_path, base_path) + if (name_el) + name_el.onclick = ev => select_node(ev, instance_path, base_path) return el } - const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) - const pipe_html = pipe_trail.map(should_pipe => ``).join('') - - const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' - const icon_class = (has_hubs && base_path !== '/') && mode !== 'search' ? 'icon clickable' : 'icon' + const prefix_class_name = get_prefix({ + is_last_sub, + has_subs, + state, + is_hub, + is_hub_on_top + }) + const pipe_html = pipe_trail + .map( + should_pipe => `` + ) + .join('') + + const prefix_class = + has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const icon_class = + has_hubs && base_path !== '/' && mode !== 'search' + ? 'icon clickable' + : 'icon' el.innerHTML = ` ${pipe_html} @@ -611,15 +685,22 @@ async function graph_explorer(opts) { } const name_el = el.querySelector('.name') - if (name_el) name_el.onclick = (ev) => select_node(ev, instance_path, base_path) - - if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { + if (name_el) + name_el.onclick = ev => select_node(ev, instance_path, base_path) + + if ( + selected_instance_paths.includes(instance_path) || + confirmed_instance_paths.includes(instance_path) + ) { const checkbox_div = document.createElement('div') checkbox_div.className = 'confirm-wrapper' const is_confirmed = confirmed_instance_paths.includes(instance_path) - checkbox_div.innerHTML = `` + checkbox_div.innerHTML = `` const checkbox_input = checkbox_div.querySelector('input') - if (checkbox_input) checkbox_input.onchange = (ev) => handle_confirm(ev, instance_path) + if (checkbox_input) + checkbox_input.onchange = ev => handle_confirm(ev, instance_path) el.appendChild(checkbox_div) } @@ -630,7 +711,9 @@ async function graph_explorer(opts) { function re_render_node (instance_path) { const node_data = view.find(n => n.instance_path === instance_path) if (node_data) { - const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + const old_node_el = shadow.querySelector( + `[data-instance_path="${CSS.escape(instance_path)}"]` + ) if (old_node_el) { const new_node_el = create_node(node_data) old_node_el.replaceWith(new_node_el) @@ -639,7 +722,7 @@ async function graph_explorer(opts) { } // `get_prefix` determines which box-drawing character to use for the node's prefix. It gives the name of a specific CSS class. - function get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) { + function get_prefix ({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) { if (!state) { console.error('get_prefix called with invalid state.') return 'middle-line' @@ -660,17 +743,19 @@ async function graph_explorer(opts) { } else if (is_last_sub) { if (expanded_subs && expanded_hubs) return 'bottom-cross' if (expanded_subs) return 'bottom-tee-down' - if (expanded_hubs) return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' + if (expanded_hubs) + return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' return has_subs ? 'bottom-line' : 'bottom-light-line' } else { if (expanded_subs && expanded_hubs) return 'middle-cross' if (expanded_subs) return 'middle-tee-down' - if (expanded_hubs) return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' + if (expanded_hubs) + return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' return has_subs ? 'middle-line' : 'middle-light-line' } } - -/****************************************************************************** + + /****************************************************************************** 5. MENUBAR AND SEARCH ******************************************************************************/ function render_menubar () { @@ -731,7 +816,7 @@ async function graph_explorer(opts) { base_path: '/', parent_instance_path: '', depth: 0, - is_last_sub : true, + is_last_sub: true, is_hub: false, parent_pipe_trail: [], instance_states, @@ -743,7 +828,7 @@ async function graph_explorer(opts) { base_path: '/', parent_instance_path: '', depth: 0, - is_last_sub : true, + is_last_sub: true, is_hub: false, parent_pipe_trail: [], instance_states: search_state_instances, // Use a temporary state for search @@ -753,7 +838,7 @@ async function graph_explorer(opts) { render_search_results(search_view, query) } - function build_search_view_recursive({ + function build_search_view_recursive ({ query, base_path, parent_instance_path, @@ -769,27 +854,30 @@ async function graph_explorer(opts) { if (!entry) return [] const instance_path = `${parent_instance_path}|${base_path}` - const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) + const is_direct_match = + entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) let sub_results = [] if (Array.isArray(entry.subs)) { const children_pipe_trail = [...parent_pipe_trail] if (depth > 0) children_pipe_trail.push(!is_last_sub) - sub_results = entry.subs.map((sub_path, i, arr) => { - return build_search_view_recursive({ - query, - base_path: sub_path, - parent_instance_path: instance_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: false, - parent_pipe_trail: children_pipe_trail, - instance_states, - all_entries, - original_view + sub_results = entry.subs + .map((sub_path, i, arr) => { + return build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries, + original_view + }) }) - }).flat() + .flat() } const has_matching_descendant = sub_results.length > 0 @@ -803,7 +891,9 @@ async function graph_explorer(opts) { expanded_hubs: false } - const is_in_original_view = original_view.some(node => node.instance_path === instance_path) + const is_in_original_view = original_view.some( + node => node.instance_path === instance_path + ) const current_node_view = { base_path, @@ -840,21 +930,27 @@ async function graph_explorer(opts) { container.appendChild(fragment) } -/****************************************************************************** + /****************************************************************************** 6. VIEW MANIPULATION & USER ACTIONS - These functions handle user interactions like selecting, confirming, toggling, and resetting the graph. ******************************************************************************/ - function select_node(ev, instance_path) { + function select_node (ev, instance_path) { if (mode === 'search') { let current_path = instance_path // Traverse up the tree to expand all parents while (current_path) { - const parent_path = current_path.substring(0, current_path.lastIndexOf('|')) + const parent_path = current_path.substring( + 0, + current_path.lastIndexOf('|') + ) if (!parent_path) break // Stop if there's no parent left if (!instance_states[parent_path]) { - instance_states[parent_path] = { expanded_subs: false, expanded_hubs: false } + instance_states[parent_path] = { + expanded_subs: false, + expanded_hubs: false + } } instance_states[parent_path].expanded_subs = true current_path = parent_path @@ -878,7 +974,7 @@ async function graph_explorer(opts) { } } - function handle_confirm(ev, instance_path) { + function handle_confirm (ev, instance_path) { if (!ev.target) return console.warn('Checkbox event target is missing.') const is_checked = ev.target.checked const new_selected_paths = [...selected_instance_paths] @@ -888,11 +984,11 @@ async function graph_explorer(opts) { const idx = new_selected_paths.indexOf(instance_path) if (idx > -1) new_selected_paths.splice(idx, 1) if (!new_confirmed_paths.includes(instance_path)) { - new_confirmed_paths.push(instance_path) + new_confirmed_paths.push(instance_path) } } else { if (!new_selected_paths.includes(instance_path)) { - new_selected_paths.push(instance_path) + new_selected_paths.push(instance_path) } const idx = new_confirmed_paths.indexOf(instance_path) if (idx > -1) new_confirmed_paths.splice(idx, 1) @@ -901,10 +997,15 @@ async function graph_explorer(opts) { update_runtime_state('confirmed_selected', new_confirmed_paths) } - function toggle_subs(instance_path) { + function toggle_subs (instance_path) { if (!instance_states[instance_path]) { - console.warn(`Toggling subs for non-existent state: ${instance_path}. Creating default state.`) - instance_states[instance_path] = { expanded_subs: false, expanded_hubs: false } + console.warn( + `Toggling subs for non-existent state: ${instance_path}. Creating default state.` + ) + instance_states[instance_path] = { + expanded_subs: false, + expanded_hubs: false + } } const state = instance_states[instance_path] state.expanded_subs = !state.expanded_subs @@ -914,10 +1015,15 @@ async function graph_explorer(opts) { update_runtime_state('instance_states', instance_states) } - function toggle_hubs(instance_path) { + function toggle_hubs (instance_path) { if (!instance_states[instance_path]) { - console.warn(`Toggling hubs for non-existent state: ${instance_path}. Creating default state.`) - instance_states[instance_path] = { expanded_subs: false, expanded_hubs: false } + console.warn( + `Toggling hubs for non-existent state: ${instance_path}. Creating default state.` + ) + instance_states[instance_path] = { + expanded_subs: false, + expanded_hubs: false + } } const state = instance_states[instance_path] state.expanded_hubs ? hub_num-- : hub_num++ @@ -927,12 +1033,15 @@ async function graph_explorer(opts) { update_runtime_state('instance_states', instance_states) } - function reset() { + function reset () { const root_path = '/' const root_instance_path = '|/' const new_instance_states = {} if (all_entries[root_path]) { - new_instance_states[root_instance_path] = { expanded_subs: true, expanded_hubs: false } + new_instance_states[root_instance_path] = { + expanded_subs: true, + expanded_hubs: false + } } update_runtime_state('vertical_scroll_value', 0) update_runtime_state('horizontal_scroll_value', 0) @@ -941,12 +1050,12 @@ async function graph_explorer(opts) { update_runtime_state('instance_states', new_instance_states) } -/****************************************************************************** + /****************************************************************************** 7. VIRTUAL SCROLLING - These functions implement virtual scrolling to handle large graphs efficiently using an IntersectionObserver. ******************************************************************************/ - function onscroll() { + function onscroll () { if (scroll_update_pending) return scroll_update_pending = true requestAnimationFrame(() => { @@ -975,12 +1084,15 @@ async function graph_explorer(opts) { }) } - async function fill_viewport_downwards() { + async function fill_viewport_downwards () { if (is_rendering || end_index >= view.length) return is_rendering = true const container_rect = container.getBoundingClientRect() let sentinel_rect = bottom_sentinel.getBoundingClientRect() - while (end_index < view.length && sentinel_rect.top < container_rect.bottom + 500) { + while ( + end_index < view.length && + sentinel_rect.top < container_rect.bottom + 500 + ) { render_next_chunk() await new Promise(resolve => requestAnimationFrame(resolve)) sentinel_rect = bottom_sentinel.getBoundingClientRect() @@ -988,7 +1100,7 @@ async function graph_explorer(opts) { is_rendering = false } - async function fill_viewport_upwards() { + async function fill_viewport_upwards () { if (is_rendering || start_index <= 0) return is_rendering = true const container_rect = container.getBoundingClientRect() @@ -1001,7 +1113,7 @@ async function graph_explorer(opts) { is_rendering = false } - function handle_sentinel_intersection(entries) { + function handle_sentinel_intersection (entries) { entries.forEach(entry => { if (entry.isIntersecting) { if (entry.target === top_sentinel) fill_viewport_upwards() @@ -1010,22 +1122,26 @@ async function graph_explorer(opts) { }) } - function render_next_chunk() { + function render_next_chunk () { if (end_index >= view.length) return const fragment = document.createDocumentFragment() const next_end = Math.min(view.length, end_index + chunk_size) - for (let i = end_index; i < next_end; i++) if (view[i]) fragment.appendChild(create_node(view[i])) + for (let i = end_index; i < next_end; i++) + if (view[i]) fragment.appendChild(create_node(view[i])) container.insertBefore(fragment, bottom_sentinel) end_index = next_end - bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + bottom_sentinel.style.height = `${ + (view.length - end_index) * node_height + }px` cleanup_dom(false) } - function render_prev_chunk() { + function render_prev_chunk () { if (start_index <= 0) return const fragment = document.createDocumentFragment() const prev_start = Math.max(0, start_index - chunk_size) - for (let i = prev_start; i < start_index; i++) if (view[i]) fragment.appendChild(create_node(view[i])) + for (let i = prev_start; i < start_index; i++) + if (view[i]) fragment.appendChild(create_node(view[i])) container.insertBefore(fragment, top_sentinel.nextSibling) start_index = prev_start top_sentinel.style.height = `${start_index * node_height}px` @@ -1033,7 +1149,7 @@ async function graph_explorer(opts) { } // Removes nodes from the DOM that are far outside the viewport. - function cleanup_dom(is_scrolling_up) { + function cleanup_dom (is_scrolling_up) { const rendered_count = end_index - start_index if (rendered_count <= max_rendered_nodes) return @@ -1047,7 +1163,9 @@ async function graph_explorer(opts) { } } end_index -= to_remove_count - bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + bottom_sentinel.style.height = `${ + (view.length - end_index) * node_height + }px` } else { // If scrolling down, remove nodes from the top. for (let i = 0; i < to_remove_count; i++) { @@ -1069,11 +1187,11 @@ async function graph_explorer(opts) { - It defines the default datasets (`entries`, `style`, `runtime`) and their initial values. ******************************************************************************/ -function fallback_module() { +function fallback_module () { return { api: fallback_instance } - function fallback_instance() { + function fallback_instance () { return { drive: { 'entries/': { @@ -1189,4 +1307,4 @@ function fallback_module() { } } } -} \ No newline at end of file +} diff --git a/web/boot.js b/web/boot.js index 6749e09..03bccaa 100644 --- a/web/boot.js +++ b/web/boot.js @@ -9,11 +9,13 @@ if (!has_save) { localStorage.clear() } -fetch(init_url, fetch_opts).then(res => res.text()).then(async source => { - const module = { exports: {} } - const f = new Function('module', 'require', source) - f(module, require) - const init = module.exports - await init(args, prefix) - require('./page') // or whatever is otherwise the main entry of our project -}) +fetch(init_url, fetch_opts) + .then(res => res.text()) + .then(async source => { + const module = { exports: {} } + const f = new Function('module', 'require', source) + f(module, require) + const init = module.exports + await init(args, prefix) + require('./page') // or whatever is otherwise the main entry of our project + }) diff --git a/web/page.js b/web/page.js index 3ff3c77..b1645b4 100644 --- a/web/page.js +++ b/web/page.js @@ -9,11 +9,13 @@ const app = require('..') const sheet = new CSSStyleSheet() config().then(() => boot({ sid: '' })) -async function config() { - const path = path => new URL(`../src/node_modules/${path}`, `file://${__dirname}`).href.slice(8) +async function config () { + const path = path => + new URL(`../src/node_modules/${path}`, `file://${__dirname}`).href.slice(8) const html = document.documentElement const meta = document.createElement('meta') - const font = 'https://fonts.googleapis.com/css?family=Nunito:300,400,700,900|Slackey&display=swap' + const font = + 'https://fonts.googleapis.com/css?family=Nunito:300,400,700,900|Slackey&display=swap' const loadFont = `` html.setAttribute('lang', 'en') meta.setAttribute('name', 'viewport') @@ -27,7 +29,7 @@ async function config() { /****************************************************************************** PAGE BOOT ******************************************************************************/ -async function boot(opts) { +async function boot (opts) { // ---------------------------------------- // ID + JSON STATE // ---------------------------------------- @@ -48,35 +50,38 @@ async function boot(opts) { // ---------------------------------------- // ELEMENTS // ---------------------------------------- - { // desktop + { + // desktop shadow.append(await app(subs[0])) } // ---------------------------------------- // INIT // ---------------------------------------- - async function onbatch(batch) { - for (const {type, paths} of batch) { - const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all( + paths.map(path => drive.get(path).then(file => file.raw)) + ) on[type] && on[type](data) } } } -async function inject(data) { +async function inject (data) { sheet.replaceSync(data.join('\n')) } function fallback_module () { return { _: { - '..': { - $: '', + '..': { + $: '', 0: '', mapping: { - 'style': 'style', - 'entries': 'entries', - 'runtime': 'runtime', - 'mode': 'mode' + style: 'style', + entries: 'entries', + runtime: 'runtime', + mode: 'mode' } } }, @@ -85,4 +90,4 @@ function fallback_module () { 'lang/': {} } } -} \ No newline at end of file +} From 0c074ac2169324e317fed659510c3fba78dd7bb3 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 19 Aug 2025 16:41:24 +0500 Subject: [PATCH 045/130] simplyfied code according to feedback and replaced repeating code with functions --- lib/graph_explorer.js | 158 +++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 93 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 3f93183..1b26bc2 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -264,6 +264,15 @@ async function graph_explorer (opts) { if (typeof new_search_query === 'string') search_query = new_search_query if (new_previous_mode) previous_mode = new_previous_mode + + if (new_current_mode) { + const valid_modes = ['default', 'menubar', 'search'] + if (!valid_modes.includes(new_current_mode)) { + console.warn(`Invalid mode "${new_current_mode}" provided. Ignoring update.`) + return + } + } + if (new_current_mode === 'search' && !search_query) { search_state_instances = instance_states } @@ -303,6 +312,19 @@ async function graph_explorer (opts) { } } + function get_or_create_state (states, instance_path) { + if (!states[instance_path]) { + console.warn( + `State not found for instance: ${instance_path}. Creating default state.` + ) + states[instance_path] = { + expanded_subs: false, + expanded_hubs: false + } + } + return states[instance_path] + } + /****************************************************************************** 3. VIEW AND RENDERING LOGIC - These functions build the `view` array and render the DOM. @@ -570,14 +592,7 @@ async function graph_explorer (opts) { } const states = mode === 'search' ? search_state_instances : instance_states - let state = states[instance_path] - if (!state) { - console.warn( - `State not found for instance: ${instance_path}. Using default.` - ) - state = { expanded_subs: false, expanded_hubs: false } - states[instance_path] = state - } + const state = get_or_create_state(states, instance_path) const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` @@ -676,11 +691,7 @@ async function graph_explorer (opts) { if (has_subs) { const prefix_el = el.querySelector('.prefix') if (prefix_el) { - if (mode !== 'search') { - prefix_el.onclick = () => toggle_subs(instance_path) - } else { - prefix_el.onclick = null - } + prefix_el.onclick = mode === 'search' ? null : () => toggle_subs(instance_path) } } @@ -759,59 +770,52 @@ async function graph_explorer (opts) { 5. MENUBAR AND SEARCH ******************************************************************************/ function render_menubar () { - menubar.replaceChildren() // Clear existing menubar const search_button = document.createElement('button') search_button.textContent = 'Search' search_button.onclick = toggle_search_mode - menubar.appendChild(search_button) - - if (mode === 'search') { - const search_input = document.createElement('input') - search_input.type = 'text' - search_input.placeholder = 'Search entries...' - search_input.className = 'search-input' - search_input.oninput = on_search_input - search_input.value = search_query - menubar.appendChild(search_input) - requestAnimationFrame(() => search_input.focus()) + if (mode !== 'search') { + menubar.replaceChildren(search_button) + return } + + const search_input = Object.assign(document.createElement('input'), { + type: 'text', + placeholder: 'Search entries...', + className: 'search-input', + value: search_query + }) + search_input.oninput = on_search_input + + menubar.replaceChildren(search_button, search_input) + requestAnimationFrame(() => search_input.focus()) } function handle_mode_change () { - if (mode === 'default') { - menubar.style.display = 'none' - } else { - menubar.style.display = 'flex' - } + menubar.style.display = mode === 'default' ? 'none' : 'flex' build_and_render_view() } function toggle_search_mode () { - const new_mode = mode === 'search' ? previous_mode : 'search' if (mode === 'search') { search_query = '' drive_updated_by_search = true update_mode_state('search_query', '') } - update_mode_state('current_mode', new_mode) + update_mode_state('current_mode', mode === 'search' ? previous_mode : 'search') search_state_instances = instance_states } function on_search_input (event) { - const query = event.target.value.trim() - search_query = query + search_query = event.target.value.trim() drive_updated_by_search = true - update_mode_state('search_query', query) - if (query === '') search_state_instances = instance_states - perform_search(query) + update_mode_state('search_query', search_query) + if (search_query === '') search_state_instances = instance_states + perform_search(search_query) } function perform_search (query) { - if (!query) { - build_and_render_view() - return - } + if (!query) return build_and_render_view() const original_view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -882,9 +886,7 @@ async function graph_explorer (opts) { const has_matching_descendant = sub_results.length > 0 - if (!is_direct_match && !has_matching_descendant) { - return [] - } + if (!is_direct_match && !has_matching_descendant) return [] instance_states[instance_path] = { expanded_subs: has_matching_descendant, @@ -945,14 +947,7 @@ async function graph_explorer (opts) { current_path.lastIndexOf('|') ) if (!parent_path) break // Stop if there's no parent left - - if (!instance_states[parent_path]) { - instance_states[parent_path] = { - expanded_subs: false, - expanded_hubs: false - } - } - instance_states[parent_path].expanded_subs = true + get_or_create_state(instance_states, parent_path).expanded_subs = true current_path = parent_path } drive_updated_by_toggle = true @@ -961,14 +956,13 @@ async function graph_explorer (opts) { } if (ev.ctrlKey) { - const new_selected_paths = [...selected_instance_paths] - const index = new_selected_paths.indexOf(instance_path) - if (index > -1) { - new_selected_paths.splice(index, 1) + const new_selected = new Set(selected_instance_paths) + if (new_selected.has(instance_path)) { + new_selected.delete(instance_path) } else { - new_selected_paths.push(instance_path) + new_selected.add(instance_path) } - update_runtime_state('selected_instance_paths', new_selected_paths) + update_runtime_state('selected_instance_paths', [...new_selected]) } else { update_runtime_state('selected_instance_paths', [instance_path]) } @@ -977,37 +971,24 @@ async function graph_explorer (opts) { function handle_confirm (ev, instance_path) { if (!ev.target) return console.warn('Checkbox event target is missing.') const is_checked = ev.target.checked - const new_selected_paths = [...selected_instance_paths] - const new_confirmed_paths = [...confirmed_instance_paths] + + const new_selected = new Set(selected_instance_paths) + const new_confirmed = new Set(confirmed_instance_paths) if (is_checked) { - const idx = new_selected_paths.indexOf(instance_path) - if (idx > -1) new_selected_paths.splice(idx, 1) - if (!new_confirmed_paths.includes(instance_path)) { - new_confirmed_paths.push(instance_path) - } + new_selected.delete(instance_path) + new_confirmed.add(instance_path) } else { - if (!new_selected_paths.includes(instance_path)) { - new_selected_paths.push(instance_path) - } - const idx = new_confirmed_paths.indexOf(instance_path) - if (idx > -1) new_confirmed_paths.splice(idx, 1) + new_selected.add(instance_path) + new_confirmed.delete(instance_path) } - update_runtime_state('selected_instance_paths', new_selected_paths) - update_runtime_state('confirmed_selected', new_confirmed_paths) + + update_runtime_state('selected_instance_paths', [...new_selected]) + update_runtime_state('confirmed_selected', [...new_confirmed]) } function toggle_subs (instance_path) { - if (!instance_states[instance_path]) { - console.warn( - `Toggling subs for non-existent state: ${instance_path}. Creating default state.` - ) - instance_states[instance_path] = { - expanded_subs: false, - expanded_hubs: false - } - } - const state = instance_states[instance_path] + const state = get_or_create_state(instance_states, instance_path) state.expanded_subs = !state.expanded_subs build_and_render_view(instance_path) // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. @@ -1016,16 +997,7 @@ async function graph_explorer (opts) { } function toggle_hubs (instance_path) { - if (!instance_states[instance_path]) { - console.warn( - `Toggling hubs for non-existent state: ${instance_path}. Creating default state.` - ) - instance_states[instance_path] = { - expanded_subs: false, - expanded_hubs: false - } - } - const state = instance_states[instance_path] + const state = get_or_create_state(instance_states, instance_path) state.expanded_hubs ? hub_num-- : hub_num++ state.expanded_hubs = !state.expanded_hubs build_and_render_view(instance_path, true) @@ -1307,4 +1279,4 @@ function fallback_module () { } } } -} +} \ No newline at end of file From 55c0e73effc22cddba51edae93b5862ffa000076 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 19 Aug 2025 20:32:41 +0500 Subject: [PATCH 046/130] replaced more repeating code with functions and simplified some of the logic in if statements --- lib/graph_explorer.js | 849 +++++++++++++++--------------------------- 1 file changed, 302 insertions(+), 547 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 1b26bc2..3e497d8 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -81,32 +81,20 @@ async function graph_explorer (opts) { ******************************************************************************/ async function onbatch (batch) { // Prevent feedback loops from scroll or toggle actions. - if (drive_updated_by_scroll) { - drive_updated_by_scroll = false - return - } - if (drive_updated_by_toggle) { - drive_updated_by_toggle = false - return - } - if (drive_updated_by_search) { - drive_updated_by_search = false - return - } + if (check_and_reset_feedback_flags()) return for (const { type, paths } of batch) { - if (!paths || paths.length === 0) continue + if (!paths || !paths.length) continue const data = await Promise.all( - paths.map(async path => { - try { - const file = await drive.get(path) - if (!file) return null - return file.raw - } catch (e) { - console.error(`Error getting file from drive: ${path}`, e) - return null - } - }) + paths.map(path => + drive + .get(path) + .then(file => (file ? file.raw : null)) + .catch(e => { + console.error(`Error getting file from drive: ${path}`, e) + return null + }) + ) ) // Call the appropriate handler based on `type`. const func = on[type] @@ -119,25 +107,18 @@ async function graph_explorer (opts) { } function on_entries ({ data }) { - if (!data || data[0] === null || data[0] === undefined) { + if (!data || data[0] == null) { console.error('Entries data is missing or empty.') all_entries = {} return } - try { - const parsed_data = - typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] - if (typeof parsed_data !== 'object' || parsed_data === null) { - console.error('Parsed entries data is not a valid object.') - all_entries = {} - return - } - all_entries = parsed_data - } catch (e) { - console.error('Failed to parse entries data:', e) + const parsed_data = parse_json_data(data[0], 'entries.json') + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed entries data is not a valid object.') all_entries = {} return } + all_entries = parsed_data // After receiving entries, ensure the root node state is initialized and trigger the first render. const root_path = '/' @@ -161,17 +142,11 @@ async function graph_explorer (opts) { let needs_render = false const render_nodes_needed = new Set() - for (let i = 0; i < paths.length; i++) { - const path = paths[i] - if (data[i] === null) continue + paths.forEach((path, i) => { + if (data[i] === null) return + const value = parse_json_data(data[i], path) + if (value === null) return - let value - try { - value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] - } catch (e) { - console.error(`Failed to parse JSON for ${path}:`, e) - continue - } // Handle different runtime state updates based on the path i.e files switch (true) { case path.endsWith('node_height.json'): @@ -183,94 +158,64 @@ async function graph_explorer (opts) { case path.endsWith('horizontal_scroll_value.json'): if (typeof value === 'number') horizontal_scroll_value = value break - case path.endsWith('selected_instance_paths.json'): { - const old_paths = [...selected_instance_paths] - if (Array.isArray(value)) { - selected_instance_paths = value - } else { - console.warn( - 'selected_instance_paths is not an array, defaulting to empty.', - value - ) - selected_instance_paths = [] - } - const changed_paths = [ - ...new Set([...old_paths, ...selected_instance_paths]) - ] - changed_paths.forEach(p => render_nodes_needed.add(p)) + case path.endsWith('selected_instance_paths.json'): + selected_instance_paths = process_path_array_update({ + current_paths: selected_instance_paths, + value, + render_set: render_nodes_needed, + name: 'selected_instance_paths' + }) break - } - case path.endsWith('confirmed_selected.json'): { - const old_paths = [...confirmed_instance_paths] - if (Array.isArray(value)) { - confirmed_instance_paths = value - } else { - console.warn( - 'confirmed_selected is not an array, defaulting to empty.', - value - ) - confirmed_instance_paths = [] - } - const changed_paths = [ - ...new Set([...old_paths, ...confirmed_instance_paths]) - ] - changed_paths.forEach(p => render_nodes_needed.add(p)) + case path.endsWith('confirmed_selected.json'): + confirmed_instance_paths = process_path_array_update({ + current_paths: confirmed_instance_paths, + value, + render_set: render_nodes_needed, + name: 'confirmed_selected' + }) break - } case path.endsWith('instance_states.json'): - if ( - typeof value === 'object' && - value !== null && - !Array.isArray(value) - ) { + if (typeof value === 'object' && value && !Array.isArray(value)) { instance_states = value needs_render = true - } else + } else { console.warn( 'instance_states is not a valid object, ignoring.', value ) + } break } - } + }) - if (needs_render) { - build_and_render_view() - } else if (render_nodes_needed.size > 0) { + if (needs_render) build_and_render_view() + else if (render_nodes_needed.size > 0) { render_nodes_needed.forEach(re_render_node) } } function on_mode ({ data, paths }) { - let new_current_mode - let new_previous_mode - let new_search_query - - for (let i = 0; i < paths.length; i++) { - const path = paths[i] - const raw_data = data[i] - if (raw_data === null) continue - let value - try { - value = JSON.parse(raw_data) - } catch (e) { - console.error(`Failed to parse JSON for ${path}:`, e) - continue - } + let new_current_mode, new_previous_mode, new_search_query + + paths.forEach((path, i) => { + const value = parse_json_data(data[i], path) + if (value === null) return + if (path.endsWith('current_mode.json')) new_current_mode = value else if (path.endsWith('previous_mode.json')) new_previous_mode = value else if (path.endsWith('search_query.json')) new_search_query = value - } + }) if (typeof new_search_query === 'string') search_query = new_search_query if (new_previous_mode) previous_mode = new_previous_mode - if (new_current_mode) { - const valid_modes = ['default', 'menubar', 'search'] - if (!valid_modes.includes(new_current_mode)) { - console.warn(`Invalid mode "${new_current_mode}" provided. Ignoring update.`) - return - } + if ( + new_current_mode && + !['default', 'menubar', 'search'].includes(new_current_mode) + ) { + return void console.warn( + `Invalid mode "${new_current_mode}" provided. Ignoring update.` + ) } if (new_current_mode === 'search' && !search_query) { @@ -278,15 +223,11 @@ async function graph_explorer (opts) { } if (!new_current_mode || mode === new_current_mode) return - if (mode && new_current_mode === 'search') { - update_mode_state('previous_mode', mode) - } + if (mode && new_current_mode === 'search') update_drive_state({ dataset: 'mode', name: 'previous_mode', value: mode }) mode = new_current_mode render_menubar() handle_mode_change() - if (mode === 'search' && search_query) { - perform_search(search_query) - } + if (mode === 'search' && search_query) perform_search(search_query) } function inject_style ({ data }) { @@ -296,31 +237,17 @@ async function graph_explorer (opts) { } // Helper to persist component state to the drive. - async function update_runtime_state (name, value) { - try { - await drive.put(`runtime/${name}.json`, JSON.stringify(value)) - } catch (e) { - console.error(`Failed to update runtime state for ${name}:`, e) - } - } - - async function update_mode_state (name, value) { + async function update_drive_state ({ dataset, name, value }) { try { - await drive.put(`mode/${name}.json`, JSON.stringify(value)) + await drive.put(`${dataset}/${name}.json`, JSON.stringify(value)) } catch (e) { - console.error(`Failed to update mode state for ${name}:`, e) + console.error(`Failed to update ${dataset} state for ${name}:`, e) } } function get_or_create_state (states, instance_path) { if (!states[instance_path]) { - console.warn( - `State not found for instance: ${instance_path}. Creating default state.` - ) - states[instance_path] = { - expanded_subs: false, - expanded_hubs: false - } + states[instance_path] = { expanded_subs: false, expanded_hubs: false } } return states[instance_path] } @@ -332,18 +259,13 @@ async function graph_explorer (opts) { - `build_view_recursive` creates the flat `view` array from the hierarchical data. ******************************************************************************/ function build_and_render_view (focal_instance_path, hub_toggle = false) { - if (Object.keys(all_entries).length === 0) { - console.warn('No entries available to render.') - return - } + if (Object.keys(all_entries).length === 0) return void console.warn('No entries available to render.') + const old_view = [...view] const old_scroll_top = vertical_scroll_value const old_scroll_left = horizontal_scroll_value - let existing_spacer_height = 0 - if (spacer_element && spacer_element.parentNode) { - existing_spacer_height = parseFloat(spacer_element.style.height) || 0 - } + if (spacer_element && spacer_element.parentNode) existing_spacer_height = parseFloat(spacer_element.style.height) || 0 // Recursively build the new `view` array from the graph data. view = build_view_recursive({ @@ -357,58 +279,23 @@ async function graph_explorer (opts) { all_entries }) - // Calculate the new scroll position to maintain the user's viewport. - let new_scroll_top = old_scroll_top - if (focal_instance_path) { - // If an action was focused on a specific node (like a toggle), try to keep it in the same position. - const old_toggled_node_index = old_view.findIndex( - node => node.instance_path === focal_instance_path - ) - const new_toggled_node_index = view.findIndex( - node => node.instance_path === focal_instance_path - ) - - if (old_toggled_node_index !== -1 && new_toggled_node_index !== -1) { - const index_change = new_toggled_node_index - old_toggled_node_index - new_scroll_top = old_scroll_top + index_change * node_height - } - } else if (old_view.length > 0) { - // Otherwise, try to keep the topmost visible node in the same position. - const old_top_node_index = Math.floor(old_scroll_top / node_height) - const scroll_offset = old_scroll_top % node_height - const old_top_node = old_view[old_top_node_index] - if (old_top_node) { - const new_top_node_index = view.findIndex( - node => node.instance_path === old_top_node.instance_path - ) - if (new_top_node_index !== -1) { - new_scroll_top = new_top_node_index * node_height + scroll_offset - } - } - } - - const render_anchor_index = Math.max( - 0, - Math.floor(new_scroll_top / node_height) - ) + const new_scroll_top = calculate_new_scroll_top({ + old_scroll_top, + old_view, + focal_path: focal_instance_path + }) + const render_anchor_index = Math.max(0, Math.floor(new_scroll_top / node_height)) start_index = Math.max(0, render_anchor_index - chunk_size) end_index = Math.min(view.length, render_anchor_index + chunk_size) const fragment = document.createDocumentFragment() for (let i = start_index; i < end_index; i++) { if (view[i]) fragment.appendChild(create_node(view[i])) - else console.warn(`Missing node at index ${i} in view.`) } - container.replaceChildren() - container.appendChild(top_sentinel) - container.appendChild(fragment) - container.appendChild(bottom_sentinel) - + container.replaceChildren(top_sentinel, fragment, bottom_sentinel) top_sentinel.style.height = `${start_index * node_height}px` - bottom_sentinel.style.height = `${ - (view.length - end_index) * node_height - }px` + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` observer.observe(top_sentinel) observer.observe(bottom_sentinel) @@ -419,35 +306,13 @@ async function graph_explorer (opts) { vertical_scroll_value = container.scrollTop } - // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. - if (hub_toggle || hub_num > 0) { - spacer_element = document.createElement('div') - spacer_element.className = 'spacer' - container.appendChild(spacer_element) - - if (hub_toggle) { - requestAnimationFrame(() => { - const container_height = container.clientHeight - const content_height = view.length * node_height - const max_scroll_top = content_height - container_height - - if (new_scroll_top > max_scroll_top) { - spacer_initial_height = new_scroll_top - max_scroll_top - spacer_initial_scroll_top = new_scroll_top - spacer_element.style.height = `${spacer_initial_height}px` - } - set_scroll_and_sync() - }) - } else { - spacer_element.style.height = `${existing_spacer_height}px` - requestAnimationFrame(set_scroll_and_sync) - } - } else { - spacer_element = null - spacer_initial_height = 0 - spacer_initial_scroll_top = 0 - requestAnimationFrame(set_scroll_and_sync) - } + // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. + handle_spacer_element({ + hub_toggle, + existing_height: existing_spacer_height, + new_scroll_top, + sync_fn: set_scroll_and_sync + }) } // Traverses the hierarchical `all_entries` data and builds a flat `view` array for rendering. @@ -467,16 +332,9 @@ async function graph_explorer (opts) { const entry = all_entries[base_path] if (!entry) return [] - if (!instance_states[instance_path]) { - instance_states[instance_path] = { - expanded_subs: false, - expanded_hubs: false - } - } - const state = instance_states[instance_path] + const state = get_or_create_state(instance_states, instance_path) const is_hub_on_top = - base_path === all_entries[parent_base_path]?.hubs?.[0] || - base_path === '/' + base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' // Calculate the pipe trail for drawing the tree lines. Quite complex logic here. const children_pipe_trail = [...parent_pipe_trail] @@ -512,8 +370,8 @@ async function graph_explorer (opts) { // If hubs are expanded, recursively add them to the view first (they appear above the node). if (state.expanded_hubs && Array.isArray(entry.hubs)) { entry.hubs.forEach((hub_path, i, arr) => { - current_view = current_view.concat( - build_view_recursive({ + current_view.push( + ...build_view_recursive({ base_path: hub_path, parent_instance_path: instance_path, parent_base_path: base_path, @@ -545,8 +403,8 @@ async function graph_explorer (opts) { // If subs are expanded, recursively add them to the view (they appear below the node). if (state.expanded_subs && Array.isArray(entry.subs)) { entry.subs.forEach((sub_path, i, arr) => { - current_view = current_view.concat( - build_view_recursive({ + current_view.push( + ...build_view_recursive({ base_path: sub_path, parent_instance_path: instance_path, depth: depth + 1, @@ -582,9 +440,6 @@ async function graph_explorer (opts) { }) { const entry = all_entries[base_path] if (!entry) { - console.error( - `Entry not found for path: ${base_path}. Cannot create node.` - ) const err_el = document.createElement('div') err_el.className = 'node error' err_el.textContent = `Error: Missing entry for ${base_path}` @@ -593,126 +448,49 @@ async function graph_explorer (opts) { const states = mode === 'search' ? search_state_instances : instance_states const state = get_or_create_state(states, instance_path) - const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path if (is_search_match) { el.classList.add('search-result') - if (is_direct_match) { - el.classList.add('direct-match') - } - if (!is_in_original_view) { - el.classList.add('new-entry') - } + if (is_direct_match) el.classList.add('direct-match') + if (!is_in_original_view) el.classList.add('new-entry') } - if (selected_instance_paths.includes(instance_path)) - el.classList.add('selected') - if (confirmed_instance_paths.includes(instance_path)) - el.classList.add('confirmed') + if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') + if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 - if (depth) { - el.style.paddingLeft = '17.5px' - } + if (depth) el.style.paddingLeft = '17.5px' el.style.height = `${node_height}px` - // Handle the special case for the root node since its a bit different. - if (base_path === '/' && instance_path === '|/') { - const { expanded_subs } = state - const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' - const prefix_class = - has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' - el.innerHTML = `
🪄
/🌐` - - const wand_el = el.querySelector('.wand') - if (wand_el) wand_el.onclick = reset - - if (has_subs) { - const prefix_el = el.querySelector('.prefix') - if (prefix_el) { - if (mode !== 'search') { - prefix_el.onclick = () => toggle_subs(instance_path) - } else { - prefix_el.onclick = null - } - } - } - - const name_el = el.querySelector('.name') - if (name_el) - name_el.onclick = ev => select_node(ev, instance_path, base_path) - - return el - } - - const prefix_class_name = get_prefix({ - is_last_sub, - has_subs, - state, - is_hub, - is_hub_on_top - }) - const pipe_html = pipe_trail - .map( - should_pipe => `` - ) - .join('') + if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) - const prefix_class = - has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' - const icon_class = - has_hubs && base_path !== '/' && mode !== 'search' - ? 'icon clickable' - : 'icon' + const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) + const pipe_html = pipe_trail.map(p => ``).join('') + const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const icon_class = has_hubs && base_path !== '/' && mode !== 'search' ? 'icon clickable' : 'icon' el.innerHTML = ` - ${pipe_html} + ${pipe_html} ${entry.name || base_path} ` - if (has_hubs && base_path !== '/') { - const icon_el = el.querySelector('.icon') - if (icon_el) { - if (mode !== 'search') { - icon_el.onclick = () => toggle_hubs(instance_path) - } else { - icon_el.onclick = null - } - } - } + const icon_el = el.querySelector('.icon') + if (icon_el && has_hubs && base_path !== '/') icon_el.onclick = mode !== 'search' ? () => toggle_hubs(instance_path) : null - if (has_subs) { - const prefix_el = el.querySelector('.prefix') - if (prefix_el) { - prefix_el.onclick = mode === 'search' ? null : () => toggle_subs(instance_path) - } - } + const prefix_el = el.querySelector('.prefix') + if (prefix_el && has_subs) prefix_el.onclick = mode !== 'search' ? () => toggle_subs(instance_path) : null - const name_el = el.querySelector('.name') - if (name_el) - name_el.onclick = ev => select_node(ev, instance_path, base_path) + el.querySelector('.name').onclick = ev => select_node(ev, instance_path) - if ( - selected_instance_paths.includes(instance_path) || - confirmed_instance_paths.includes(instance_path) - ) { - const checkbox_div = document.createElement('div') - checkbox_div.className = 'confirm-wrapper' - const is_confirmed = confirmed_instance_paths.includes(instance_path) - checkbox_div.innerHTML = `` - const checkbox_input = checkbox_div.querySelector('input') - if (checkbox_input) - checkbox_input.onchange = ev => handle_confirm(ev, instance_path) - el.appendChild(checkbox_div) + if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { + el.appendChild(create_confirm_checkbox(instance_path)) } return el @@ -722,13 +500,8 @@ async function graph_explorer (opts) { function re_render_node (instance_path) { const node_data = view.find(n => n.instance_path === instance_path) if (node_data) { - const old_node_el = shadow.querySelector( - `[data-instance_path="${CSS.escape(instance_path)}"]` - ) - if (old_node_el) { - const new_node_el = create_node(node_data) - old_node_el.replaceWith(new_node_el) - } + const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (old_node_el) old_node_el.replaceWith(create_node(node_data)) } } @@ -762,7 +535,7 @@ async function graph_explorer (opts) { if (expanded_subs) return 'middle-tee-down' if (expanded_hubs) return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' - return has_subs ? 'middle-line' : 'middle-light-line' + return has_subs ? 'middle-line' : 'middle-light-line' } } @@ -770,22 +543,20 @@ async function graph_explorer (opts) { 5. MENUBAR AND SEARCH ******************************************************************************/ function render_menubar () { - const search_button = document.createElement('button') - search_button.textContent = 'Search' - search_button.onclick = toggle_search_mode + const search_button = Object.assign(document.createElement('button'), { + textContent: 'Search', + onclick: toggle_search_mode + }) - if (mode !== 'search') { - menubar.replaceChildren(search_button) - return - } + if (mode !== 'search') return void menubar.replaceChildren(search_button) const search_input = Object.assign(document.createElement('input'), { type: 'text', placeholder: 'Search entries...', className: 'search-input', - value: search_query + value: search_query, + oninput: on_search_input }) - search_input.oninput = on_search_input menubar.replaceChildren(search_button, search_input) requestAnimationFrame(() => search_input.focus()) @@ -800,16 +571,16 @@ async function graph_explorer (opts) { if (mode === 'search') { search_query = '' drive_updated_by_search = true - update_mode_state('search_query', '') + update_drive_state({ dataset: 'mode', name: 'search_query', value: '' }) } - update_mode_state('current_mode', mode === 'search' ? previous_mode : 'search') + update_drive_state({ dataset: 'mode', name: 'current_mode', value: mode === 'search' ? previous_mode : 'search' }) search_state_instances = instance_states } function on_search_input (event) { search_query = event.target.value.trim() drive_updated_by_search = true - update_mode_state('search_query', search_query) + update_drive_state({ dataset: 'mode', name: 'search_query', value: search_query }) if (search_query === '') search_state_instances = instance_states perform_search(search_query) } @@ -858,44 +629,31 @@ async function graph_explorer (opts) { if (!entry) return [] const instance_path = `${parent_instance_path}|${base_path}` - const is_direct_match = - entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) - - let sub_results = [] - if (Array.isArray(entry.subs)) { - const children_pipe_trail = [...parent_pipe_trail] - if (depth > 0) children_pipe_trail.push(!is_last_sub) - - sub_results = entry.subs - .map((sub_path, i, arr) => { - return build_search_view_recursive({ - query, - base_path: sub_path, - parent_instance_path: instance_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: false, - parent_pipe_trail: children_pipe_trail, - instance_states, - all_entries, - original_view - }) - }) - .flat() - } + const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) - const has_matching_descendant = sub_results.length > 0 + const children_pipe_trail = [...parent_pipe_trail] + if (depth > 0) children_pipe_trail.push(!is_last_sub) + + const sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => + build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries, + original_view + }) + ) + const has_matching_descendant = sub_results.length > 0 if (!is_direct_match && !has_matching_descendant) return [] - instance_states[instance_path] = { - expanded_subs: has_matching_descendant, - expanded_hubs: false - } - - const is_in_original_view = original_view.some( - node => node.instance_path === instance_path - ) + instance_states[instance_path] = { expanded_subs: has_matching_descendant, expanded_hubs: false } + const is_in_original_view = original_view.some(node => node.instance_path === instance_path) const current_node_view = { base_path, @@ -915,21 +673,15 @@ async function graph_explorer (opts) { function render_search_results (search_view, query) { view = search_view - container.replaceChildren() - if (search_view.length === 0) { const no_results_el = document.createElement('div') no_results_el.className = 'no-results' no_results_el.textContent = `No results for "${query}"` - container.appendChild(no_results_el) - return + return container.replaceChildren(no_results_el) } - const fragment = document.createDocumentFragment() - for (const node_data of search_view) { - fragment.appendChild(create_node(node_data)) - } - container.appendChild(fragment) + search_view.forEach(node_data => fragment.appendChild(create_node(node_data))) + container.replaceChildren(fragment) } /****************************************************************************** @@ -942,36 +694,28 @@ async function graph_explorer (opts) { let current_path = instance_path // Traverse up the tree to expand all parents while (current_path) { - const parent_path = current_path.substring( - 0, - current_path.lastIndexOf('|') - ) - if (!parent_path) break // Stop if there's no parent left + const parent_path = current_path.substring(0, current_path.lastIndexOf('|')) + if (!parent_path) break get_or_create_state(instance_states, parent_path).expanded_subs = true current_path = parent_path } drive_updated_by_toggle = true - update_runtime_state('instance_states', instance_states) - update_mode_state('current_mode', previous_mode) + update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) + update_drive_state({ dataset: 'mode', name: 'current_mode', value: previous_mode }) } + const new_selected = new Set(selected_instance_paths) if (ev.ctrlKey) { - const new_selected = new Set(selected_instance_paths) - if (new_selected.has(instance_path)) { - new_selected.delete(instance_path) - } else { - new_selected.add(instance_path) - } - update_runtime_state('selected_instance_paths', [...new_selected]) + new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) } else { - update_runtime_state('selected_instance_paths', [instance_path]) + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [instance_path] }) } } function handle_confirm (ev, instance_path) { - if (!ev.target) return console.warn('Checkbox event target is missing.') + if (!ev.target) return const is_checked = ev.target.checked - const new_selected = new Set(selected_instance_paths) const new_confirmed = new Set(confirmed_instance_paths) @@ -983,8 +727,8 @@ async function graph_explorer (opts) { new_confirmed.delete(instance_path) } - update_runtime_state('selected_instance_paths', [...new_selected]) - update_runtime_state('confirmed_selected', [...new_confirmed]) + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) + update_drive_state({ dataset: 'runtime', name: 'confirmed_selected', value: [...new_confirmed] }) } function toggle_subs (instance_path) { @@ -993,7 +737,7 @@ async function graph_explorer (opts) { build_and_render_view(instance_path) // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true - update_runtime_state('instance_states', instance_states) + update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) } function toggle_hubs (instance_path) { @@ -1002,24 +746,19 @@ async function graph_explorer (opts) { state.expanded_hubs = !state.expanded_hubs build_and_render_view(instance_path, true) drive_updated_by_toggle = true - update_runtime_state('instance_states', instance_states) + update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) } function reset () { - const root_path = '/' const root_instance_path = '|/' - const new_instance_states = {} - if (all_entries[root_path]) { - new_instance_states[root_instance_path] = { - expanded_subs: true, - expanded_hubs: false - } + const new_instance_states = { + [root_instance_path]: { expanded_subs: true, expanded_hubs: false } } - update_runtime_state('vertical_scroll_value', 0) - update_runtime_state('horizontal_scroll_value', 0) - update_runtime_state('selected_instance_paths', []) - update_runtime_state('confirmed_selected', []) - update_runtime_state('instance_states', new_instance_states) + update_drive_state({ dataset: 'runtime', name: 'vertical_scroll_value', value: 0 }) + update_drive_state({ dataset: 'runtime', name: 'horizontal_scroll_value', value: 0 }) + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [] }) + update_drive_state({ dataset: 'runtime', name: 'confirmed_selected', value: [] }) + update_drive_state({ dataset: 'runtime', name: 'instance_states', value: new_instance_states }) } /****************************************************************************** @@ -1032,26 +771,16 @@ async function graph_explorer (opts) { scroll_update_pending = true requestAnimationFrame(() => { const scroll_delta = vertical_scroll_value - container.scrollTop - // Handle removal of the scroll spacer. - if (spacer_element && scroll_delta > 0 && container.scrollTop == 0) { + if (spacer_element && scroll_delta > 0 && container.scrollTop === 0) { spacer_element.remove() spacer_element = null spacer_initial_height = 0 - spacer_initial_scroll_top = 0 hub_num = 0 } - if (vertical_scroll_value !== container.scrollTop) { - vertical_scroll_value = container.scrollTop - drive_updated_by_scroll = true // Set flag to prevent render loop. - update_runtime_state('vertical_scroll_value', vertical_scroll_value) - } - if (horizontal_scroll_value !== container.scrollLeft) { - horizontal_scroll_value = container.scrollLeft - drive_updated_by_scroll = true - update_runtime_state('horizontal_scroll_value', horizontal_scroll_value) - } + vertical_scroll_value = update_scroll_state({ current_value: vertical_scroll_value, new_value: container.scrollTop, name: 'vertical_scroll_value' }) + horizontal_scroll_value = update_scroll_state({ current_value: horizontal_scroll_value, new_value: container.scrollLeft, name: 'horizontal_scroll_value' }) scroll_update_pending = false }) } @@ -1061,10 +790,7 @@ async function graph_explorer (opts) { is_rendering = true const container_rect = container.getBoundingClientRect() let sentinel_rect = bottom_sentinel.getBoundingClientRect() - while ( - end_index < view.length && - sentinel_rect.top < container_rect.bottom + 500 - ) { + while (end_index < view.length && sentinel_rect.top < container_rect.bottom + 500) { render_next_chunk() await new Promise(resolve => requestAnimationFrame(resolve)) sentinel_rect = bottom_sentinel.getBoundingClientRect() @@ -1102,9 +828,7 @@ async function graph_explorer (opts) { if (view[i]) fragment.appendChild(create_node(view[i])) container.insertBefore(fragment, bottom_sentinel) end_index = next_end - bottom_sentinel.style.height = `${ - (view.length - end_index) * node_height - }px` + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` cleanup_dom(false) } @@ -1112,8 +836,9 @@ async function graph_explorer (opts) { if (start_index <= 0) return const fragment = document.createDocumentFragment() const prev_start = Math.max(0, start_index - chunk_size) - for (let i = prev_start; i < start_index; i++) + for (let i = prev_start; i < start_index; i++) { if (view[i]) fragment.appendChild(create_node(view[i])) + } container.insertBefore(fragment, top_sentinel.nextSibling) start_index = prev_start top_sentinel.style.height = `${start_index * node_height}px` @@ -1128,32 +853,151 @@ async function graph_explorer (opts) { const to_remove_count = rendered_count - max_rendered_nodes if (is_scrolling_up) { // If scrolling up, remove nodes from the bottom. - for (let i = 0; i < to_remove_count; i++) { - const temp = bottom_sentinel.previousElementSibling - if (temp && temp !== top_sentinel) { - temp.remove() - } - } + remove_dom_nodes({ count: to_remove_count, start_el: bottom_sentinel, next_prop: 'previousElementSibling', boundary_el: top_sentinel }) end_index -= to_remove_count - bottom_sentinel.style.height = `${ - (view.length - end_index) * node_height - }px` + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` } else { // If scrolling down, remove nodes from the top. - for (let i = 0; i < to_remove_count; i++) { - const temp = top_sentinel.nextElementSibling - if (temp && temp !== bottom_sentinel) { - temp.remove() - } - } + remove_dom_nodes({ count: to_remove_count, start_el: top_sentinel, next_prop: 'nextElementSibling', boundary_el: bottom_sentinel }) start_index += to_remove_count top_sentinel.style.height = `${start_index * node_height}px` } } + + /****************************************************************************** + 8. HELPER FUNCTIONS + ******************************************************************************/ + function check_and_reset_feedback_flags () { + if (drive_updated_by_scroll) { + drive_updated_by_scroll = false + return true + } + if (drive_updated_by_toggle) { + drive_updated_by_toggle = false + return true + } + if (drive_updated_by_search) { + drive_updated_by_search = false + return true + } + return false + } + + function parse_json_data (data, path) { + if (data === null) return null + try { + return typeof data === 'string' ? JSON.parse(data) : data + } catch (e) { + console.error(`Failed to parse JSON for ${path}:`, e) + return null + } + } + + function process_path_array_update ({ current_paths, value, render_set, name }) { + const old_paths = [...current_paths] + const new_paths = Array.isArray(value) + ? value + : (console.warn(`${name} is not an array, defaulting to empty.`, value), []) + ;[...new Set([...old_paths, ...new_paths])].forEach(p => render_set.add(p)) + return new_paths + } + + function calculate_new_scroll_top ({ old_scroll_top, old_view, focal_path }) { + // Calculate the new scroll position to maintain the user's viewport. + if (focal_path) { + // If an action was focused on a specific node (like a toggle), try to keep it in the same position. + const old_idx = old_view.findIndex(n => n.instance_path === focal_path) + const new_idx = view.findIndex(n => n.instance_path === focal_path) + if (old_idx !== -1 && new_idx !== -1) { + return old_scroll_top + (new_idx - old_idx) * node_height + } + } else if (old_view.length > 0) { + // Otherwise, try to keep the topmost visible node in the same position. + const old_top_idx = Math.floor(old_scroll_top / node_height) + const old_top_node = old_view[old_top_idx] + if (old_top_node) { + const new_top_idx = view.findIndex(n => n.instance_path === old_top_node.instance_path) + if (new_top_idx !== -1) { + return new_top_idx * node_height + (old_scroll_top % node_height) + } + } + } + return old_scroll_top + } + + function handle_spacer_element ({ hub_toggle, existing_height, new_scroll_top, sync_fn }) { + if (hub_toggle || hub_num > 0) { + spacer_element = document.createElement('div') + spacer_element.className = 'spacer' + container.appendChild(spacer_element) + + if (hub_toggle) { + requestAnimationFrame(() => { + const max_scroll = container.scrollHeight - container.clientHeight + if (new_scroll_top > max_scroll) { + spacer_element.style.height = `${new_scroll_top - max_scroll}px` + } + sync_fn() + }) + } else { + spacer_element.style.height = `${existing_height}px` + requestAnimationFrame(sync_fn) + } + } else { + spacer_element = null + spacer_initial_height = 0 + requestAnimationFrame(sync_fn) + } + } + + function create_root_node ({ state, has_subs, instance_path }) { + // Handle the special case for the root node since its a bit different. + const el = document.createElement('div') + el.className = 'node type-root' + el.dataset.instance_path = instance_path + const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const prefix_name = state.expanded_subs ? 'tee-down' : 'line-h' + el.innerHTML = `
🪄
/🌐` + + el.querySelector('.wand').onclick = reset + if (has_subs) { + const prefix_el = el.querySelector('.prefix') + if (prefix_el) prefix_el.onclick = mode !== 'search' ? () => toggle_subs(instance_path) : null + } + el.querySelector('.name').onclick = ev => select_node(ev, instance_path) + return el + } + + function create_confirm_checkbox (instance_path) { + const checkbox_div = document.createElement('div') + checkbox_div.className = 'confirm-wrapper' + const is_confirmed = confirmed_instance_paths.includes(instance_path) + checkbox_div.innerHTML = `` + const checkbox_input = checkbox_div.querySelector('input') + if (checkbox_input) checkbox_input.onchange = ev => handle_confirm(ev, instance_path) + return checkbox_div + } + + function update_scroll_state ({ current_value, new_value, name }) { + if (current_value !== new_value) { + drive_updated_by_scroll = true // Set flag to prevent render loop. + update_drive_state({ dataset: 'runtime', name, value: new_value }) + return new_value + } + return current_value + } + + function remove_dom_nodes ({ count, start_el, next_prop, boundary_el }) { + for (let i = 0; i < count; i++) { + const temp = start_el[next_prop] + if (temp && temp !== boundary_el) temp.remove() + else break + } + } } /****************************************************************************** - 8. FALLBACK CONFIGURATION + 9. FALLBACK CONFIGURATION - This provides the default data and API configuration for the component, following the pattern described in `instructions.md`. - It defines the default datasets (`entries`, `style`, `runtime`) and their @@ -1171,96 +1015,7 @@ function fallback_module () { }, 'style/': { 'theme.css': { - raw: ` - .graph-container, .node { - font-family: monospace; - } - .graph-container { - color: #abb2bf; - background-color: #282c34; - padding: 10px; - height: 500px; /* Or make it flexible */ - overflow: auto; - } - .node { - display: flex; - align-items: center; - white-space: nowrap; - cursor: default; - } - .node.error { - color: red; - } - .node.selected { - background-color: #776346; - } - .node.confirmed { - background-color: #774346; - } - .node.new-entry { - background-color: #87ceeb; /* sky blue */ - } - .menubar { - display: flex; - padding: 5px; - background-color: #21252b; - border-bottom: 1px solid #181a1f; - } - .search-input { - margin-left: auto; - background-color: #282c34; - color: #abb2bf; - border: 1px solid #181a1f; - } - .confirm-wrapper { - margin-left: auto; - padding-left: 10px; - } - .indent { - display: flex; - } - .pipe { - text-align: center; - } - .pipe::before { content: '┃'; } - .blank { - width: 8.5px; - text-align: center; - } - .clickable { - cursor: pointer; - } - .prefix, .icon { - margin-right: 2px; - } - .top-cross::before { content: '┏╋'; } - .top-tee-down::before { content: '┏┳'; } - .top-tee-up::before { content: '┏┻'; } - .top-line::before { content: '┏━'; } - .middle-cross::before { content: '┣╋'; } - .middle-tee-down::before { content: '┣┳'; } - .middle-tee-up::before { content: '┣┻'; } - .middle-line::before { content: '┣━'; } - .bottom-cross::before { content: '┗╋'; } - .bottom-tee-down::before { content: '┗┳'; } - .bottom-tee-up::before { content: '┗┻'; } - .bottom-line::before { content: '┗━'; } - .bottom-light-tee-up::before { content: '┖┸'; } - .bottom-light-line::before { content: '┖─'; } - .middle-light-tee-up::before { content: '┠┸'; } - .middle-light-line::before { content: '┠─'; } - .tee-down::before { content: '┳'; } - .line-h::before { content: '━'; } - .icon { display: inline-block; text-align: center; } - .name { flex-grow: 1; } - .node.type-root > .icon::before { content: '🌐'; } - .node.type-folder > .icon::before { content: '📁'; } - .node.type-html-file > .icon::before { content: '📄'; } - .node.type-js-file > .icon::before { content: '📜'; } - .node.type-css-file > .icon::before { content: '🎨'; } - .node.type-json-file > .icon::before { content: '📝'; } - .node.type-file > .icon::before { content: '📄'; } - ` + '$ref' : 'theme.css' } }, 'runtime/': { From 67692dfa0096d022b5dcdbb3fa752dcc915a50ed Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 19 Aug 2025 21:28:04 +0500 Subject: [PATCH 047/130] bold matching letters in search mode --- lib/graph_explorer.js | 40 +++++++++++++++++-- lib/theme.css | 90 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 lib/theme.css diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 3e497d8..16adb9f 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -436,7 +436,8 @@ async function graph_explorer (opts) { is_hub_on_top, is_search_match, is_direct_match, - is_in_original_view + is_in_original_view, + query }) { const entry = all_entries[base_path] if (!entry) { @@ -473,12 +474,16 @@ async function graph_explorer (opts) { const pipe_html = pipe_trail.map(p => ``).join('') const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' const icon_class = has_hubs && base_path !== '/' && mode !== 'search' ? 'icon clickable' : 'icon' + const entry_name = entry.name || base_path + const name_html = (is_direct_match && query) + ? get_highlighted_name(entry_name, query) + : entry_name el.innerHTML = ` ${pipe_html} - ${entry.name || base_path} + ${name_html} ` const icon_el = el.querySelector('.icon') @@ -680,7 +685,7 @@ async function graph_explorer (opts) { return container.replaceChildren(no_results_el) } const fragment = document.createDocumentFragment() - search_view.forEach(node_data => fragment.appendChild(create_node(node_data))) + search_view.forEach(node_data => fragment.appendChild(create_node({ ...node_data, query }))) container.replaceChildren(fragment) } @@ -867,6 +872,25 @@ async function graph_explorer (opts) { /****************************************************************************** 8. HELPER FUNCTIONS ******************************************************************************/ +function get_highlighted_name (name, query) { + // Creates a new regular expression. + // `escape_regex(query)` sanitizes the query string to treat special regex characters literally. + // `(...)` creates a capturing group for the escaped query. + // 'gi' flags: 'g' for global (all occurrences), 'i' for case-insensitive. + const regex = new RegExp(`(${escape_regex(query)})`, 'gi') + // Replaces all matches of the regex in 'name' with the matched text wrapped in tags. + // '$1' refers to the content of the first capturing group (the matched query). + return name.replace(regex, '$1') +} + +function escape_regex (string) { + // Escapes special regular expression characters in a string. + // It replaces characters like -, /, \, ^, $, *, +, ?, ., (, ), |, [, ], {, } + // with their escaped versions (e.g., '.' becomes '\.'). + // This prevents them from being interpreted as regex metacharacters. + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char +} + function check_and_reset_feedback_flags () { if (drive_updated_by_scroll) { drive_updated_by_scroll = false @@ -893,6 +917,16 @@ async function graph_explorer (opts) { } } + function parse_json_data (data, path) { + if (data === null) return null + try { + return typeof data === 'string' ? JSON.parse(data) : data + } catch (e) { + console.error(`Failed to parse JSON for ${path}:`, e) + return null + } + } + function process_path_array_update ({ current_paths, value, render_set, name }) { const old_paths = [...current_paths] const new_paths = Array.isArray(value) diff --git a/lib/theme.css b/lib/theme.css new file mode 100644 index 0000000..b42292b --- /dev/null +++ b/lib/theme.css @@ -0,0 +1,90 @@ + +.graph-container, .node { + font-family: monospace; +} +.graph-container { + color: #abb2bf; + background-color: #282c34; + padding: 10px; + height: 500px; /* Or make it flexible */ + overflow: auto; +} +.node { + display: flex; + align-items: center; + white-space: nowrap; + cursor: default; +} +.node.error { + color: red; +} +.node.selected { + background-color: #776346; +} +.node.confirmed { + background-color: #774346; +} +.node.new-entry { + background-color: #87ceeb; /* sky blue */ + color: #282c34; +} +.menubar { + display: flex; + padding: 5px; + background-color: #21252b; + border-bottom: 1px solid #181a1f; +} +.search-input { + margin-left: auto; + background-color: #282c34; + color: #abb2bf; + border: 1px solid #181a1f; +} +.confirm-wrapper { + margin-left: auto; + padding-left: 10px; +} +.indent { + display: flex; +} +.pipe { + text-align: center; +} +.pipe::before { content: '┃'; } +.blank { + width: 8.5px; + text-align: center; +} +.clickable { + cursor: pointer; +} +.prefix, .icon { + margin-right: 2px; +} +.top-cross::before { content: '┏╋'; } +.top-tee-down::before { content: '┏┳'; } +.top-tee-up::before { content: '┏┻'; } +.top-line::before { content: '┏━'; } +.middle-cross::before { content: '┣╋'; } +.middle-tee-down::before { content: '┣┳'; } +.middle-tee-up::before { content: '┣┻'; } +.middle-line::before { content: '┣━'; } +.bottom-cross::before { content: '┗╋'; } +.bottom-tee-down::before { content: '┗┳'; } +.bottom-tee-up::before { content: '┗┻'; } +.bottom-line::before { content: '┗━'; } +.bottom-light-tee-up::before { content: '┖┸'; } +.bottom-light-line::before { content: '┖─'; } +.middle-light-tee-up::before { content: '┠┸'; } +.middle-light-line::before { content: '┠─'; } +.tee-down::before { content: '┳'; } +.line-h::before { content: '━'; } +.icon { display: inline-block; text-align: center; } +.name { flex-grow: 1; } +.node.type-root > .icon::before { content: '🌐'; } +.node.type-folder > .icon::before { content: '📁'; } +.node.type-html-file > .icon::before { content: '📄'; } +.node.type-js-file > .icon::before { content: '📜'; } +.node.type-css-file > .icon::before { content: '🎨'; } +.node.type-json-file > .icon::before { content: '📝'; } +.node.type-file > .icon::before { content: '📄'; } \ No newline at end of file From 6e727e288e2f5157f78e7756fdc614cbdd0fc9f6 Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 20 Aug 2025 23:18:51 +0500 Subject: [PATCH 048/130] changed color and moved bar under explorer --- lib/graph_explorer.js | 2 +- lib/theme.css | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 16adb9f..561ada5 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -37,8 +37,8 @@ async function graph_explorer (opts) { el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = ` -
+ ` const menubar = shadow.querySelector('.menubar') const container = shadow.querySelector('.graph-container') diff --git a/lib/theme.css b/lib/theme.css index b42292b..51e507d 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -25,8 +25,10 @@ background-color: #774346; } .node.new-entry { - background-color: #87ceeb; /* sky blue */ - color: #282c34; + background-color: #87cfeb34; +} +.node.direct-match { + background-color: #0030669c; } .menubar { display: flex; From 71cb9904acc0252dbcfdeb2a5dee65e3841cef44 Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 20 Aug 2025 23:46:59 +0500 Subject: [PATCH 049/130] bundled --- bundle.js | 980 +++++++++++++++++++++--------------------------------- 1 file changed, 370 insertions(+), 610 deletions(-) diff --git a/bundle.js b/bundle.js index b4cb740..d3be2b2 100644 --- a/bundle.js +++ b/bundle.js @@ -41,8 +41,8 @@ async function graph_explorer (opts) { el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = ` -
+ ` const menubar = shadow.querySelector('.menubar') const container = shadow.querySelector('.graph-container') @@ -85,32 +85,20 @@ async function graph_explorer (opts) { ******************************************************************************/ async function onbatch (batch) { // Prevent feedback loops from scroll or toggle actions. - if (drive_updated_by_scroll) { - drive_updated_by_scroll = false - return - } - if (drive_updated_by_toggle) { - drive_updated_by_toggle = false - return - } - if (drive_updated_by_search) { - drive_updated_by_search = false - return - } + if (check_and_reset_feedback_flags()) return for (const { type, paths } of batch) { - if (!paths || paths.length === 0) continue + if (!paths || !paths.length) continue const data = await Promise.all( - paths.map(async path => { - try { - const file = await drive.get(path) - if (!file) return null - return file.raw - } catch (e) { - console.error(`Error getting file from drive: ${path}`, e) - return null - } - }) + paths.map(path => + drive + .get(path) + .then(file => (file ? file.raw : null)) + .catch(e => { + console.error(`Error getting file from drive: ${path}`, e) + return null + }) + ) ) // Call the appropriate handler based on `type`. const func = on[type] @@ -123,25 +111,18 @@ async function graph_explorer (opts) { } function on_entries ({ data }) { - if (!data || data[0] === null || data[0] === undefined) { + if (!data || data[0] == null) { console.error('Entries data is missing or empty.') all_entries = {} return } - try { - const parsed_data = - typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] - if (typeof parsed_data !== 'object' || parsed_data === null) { - console.error('Parsed entries data is not a valid object.') - all_entries = {} - return - } - all_entries = parsed_data - } catch (e) { - console.error('Failed to parse entries data:', e) + const parsed_data = parse_json_data(data[0], 'entries.json') + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed entries data is not a valid object.') all_entries = {} return } + all_entries = parsed_data // After receiving entries, ensure the root node state is initialized and trigger the first render. const root_path = '/' @@ -165,17 +146,11 @@ async function graph_explorer (opts) { let needs_render = false const render_nodes_needed = new Set() - for (let i = 0; i < paths.length; i++) { - const path = paths[i] - if (data[i] === null) continue + paths.forEach((path, i) => { + if (data[i] === null) return + const value = parse_json_data(data[i], path) + if (value === null) return - let value - try { - value = typeof data[i] === 'string' ? JSON.parse(data[i]) : data[i] - } catch (e) { - console.error(`Failed to parse JSON for ${path}:`, e) - continue - } // Handle different runtime state updates based on the path i.e files switch (true) { case path.endsWith('node_height.json'): @@ -187,101 +162,76 @@ async function graph_explorer (opts) { case path.endsWith('horizontal_scroll_value.json'): if (typeof value === 'number') horizontal_scroll_value = value break - case path.endsWith('selected_instance_paths.json'): { - const old_paths = [...selected_instance_paths] - if (Array.isArray(value)) { - selected_instance_paths = value - } else { - console.warn( - 'selected_instance_paths is not an array, defaulting to empty.', - value - ) - selected_instance_paths = [] - } - const changed_paths = [ - ...new Set([...old_paths, ...selected_instance_paths]) - ] - changed_paths.forEach(p => render_nodes_needed.add(p)) + case path.endsWith('selected_instance_paths.json'): + selected_instance_paths = process_path_array_update({ + current_paths: selected_instance_paths, + value, + render_set: render_nodes_needed, + name: 'selected_instance_paths' + }) break - } - case path.endsWith('confirmed_selected.json'): { - const old_paths = [...confirmed_instance_paths] - if (Array.isArray(value)) { - confirmed_instance_paths = value - } else { - console.warn( - 'confirmed_selected is not an array, defaulting to empty.', - value - ) - confirmed_instance_paths = [] - } - const changed_paths = [ - ...new Set([...old_paths, ...confirmed_instance_paths]) - ] - changed_paths.forEach(p => render_nodes_needed.add(p)) + case path.endsWith('confirmed_selected.json'): + confirmed_instance_paths = process_path_array_update({ + current_paths: confirmed_instance_paths, + value, + render_set: render_nodes_needed, + name: 'confirmed_selected' + }) break - } case path.endsWith('instance_states.json'): - if ( - typeof value === 'object' && - value !== null && - !Array.isArray(value) - ) { + if (typeof value === 'object' && value && !Array.isArray(value)) { instance_states = value needs_render = true - } else + } else { console.warn( 'instance_states is not a valid object, ignoring.', value ) + } break } - } + }) - if (needs_render) { - build_and_render_view() - } else if (render_nodes_needed.size > 0) { + if (needs_render) build_and_render_view() + else if (render_nodes_needed.size > 0) { render_nodes_needed.forEach(re_render_node) } } function on_mode ({ data, paths }) { - let new_current_mode - let new_previous_mode - let new_search_query - - for (let i = 0; i < paths.length; i++) { - const path = paths[i] - const raw_data = data[i] - if (raw_data === null) continue - let value - try { - value = JSON.parse(raw_data) - } catch (e) { - console.error(`Failed to parse JSON for ${path}:`, e) - continue - } + let new_current_mode, new_previous_mode, new_search_query + + paths.forEach((path, i) => { + const value = parse_json_data(data[i], path) + if (value === null) return + if (path.endsWith('current_mode.json')) new_current_mode = value else if (path.endsWith('previous_mode.json')) new_previous_mode = value else if (path.endsWith('search_query.json')) new_search_query = value - } + }) if (typeof new_search_query === 'string') search_query = new_search_query if (new_previous_mode) previous_mode = new_previous_mode + + if ( + new_current_mode && + !['default', 'menubar', 'search'].includes(new_current_mode) + ) { + return void console.warn( + `Invalid mode "${new_current_mode}" provided. Ignoring update.` + ) + } + if (new_current_mode === 'search' && !search_query) { search_state_instances = instance_states } if (!new_current_mode || mode === new_current_mode) return - if (mode && new_current_mode === 'search') { - update_mode_state('previous_mode', mode) - } + if (mode && new_current_mode === 'search') update_drive_state({ dataset: 'mode', name: 'previous_mode', value: mode }) mode = new_current_mode render_menubar() handle_mode_change() - if (mode === 'search' && search_query) { - perform_search(search_query) - } + if (mode === 'search' && search_query) perform_search(search_query) } function inject_style ({ data }) { @@ -291,20 +241,19 @@ async function graph_explorer (opts) { } // Helper to persist component state to the drive. - async function update_runtime_state (name, value) { + async function update_drive_state ({ dataset, name, value }) { try { - await drive.put(`runtime/${name}.json`, JSON.stringify(value)) + await drive.put(`${dataset}/${name}.json`, JSON.stringify(value)) } catch (e) { - console.error(`Failed to update runtime state for ${name}:`, e) + console.error(`Failed to update ${dataset} state for ${name}:`, e) } } - async function update_mode_state (name, value) { - try { - await drive.put(`mode/${name}.json`, JSON.stringify(value)) - } catch (e) { - console.error(`Failed to update mode state for ${name}:`, e) + function get_or_create_state (states, instance_path) { + if (!states[instance_path]) { + states[instance_path] = { expanded_subs: false, expanded_hubs: false } } + return states[instance_path] } /****************************************************************************** @@ -314,18 +263,13 @@ async function graph_explorer (opts) { - `build_view_recursive` creates the flat `view` array from the hierarchical data. ******************************************************************************/ function build_and_render_view (focal_instance_path, hub_toggle = false) { - if (Object.keys(all_entries).length === 0) { - console.warn('No entries available to render.') - return - } + if (Object.keys(all_entries).length === 0) return void console.warn('No entries available to render.') + const old_view = [...view] const old_scroll_top = vertical_scroll_value const old_scroll_left = horizontal_scroll_value - let existing_spacer_height = 0 - if (spacer_element && spacer_element.parentNode) { - existing_spacer_height = parseFloat(spacer_element.style.height) || 0 - } + if (spacer_element && spacer_element.parentNode) existing_spacer_height = parseFloat(spacer_element.style.height) || 0 // Recursively build the new `view` array from the graph data. view = build_view_recursive({ @@ -339,58 +283,23 @@ async function graph_explorer (opts) { all_entries }) - // Calculate the new scroll position to maintain the user's viewport. - let new_scroll_top = old_scroll_top - if (focal_instance_path) { - // If an action was focused on a specific node (like a toggle), try to keep it in the same position. - const old_toggled_node_index = old_view.findIndex( - node => node.instance_path === focal_instance_path - ) - const new_toggled_node_index = view.findIndex( - node => node.instance_path === focal_instance_path - ) - - if (old_toggled_node_index !== -1 && new_toggled_node_index !== -1) { - const index_change = new_toggled_node_index - old_toggled_node_index - new_scroll_top = old_scroll_top + index_change * node_height - } - } else if (old_view.length > 0) { - // Otherwise, try to keep the topmost visible node in the same position. - const old_top_node_index = Math.floor(old_scroll_top / node_height) - const scroll_offset = old_scroll_top % node_height - const old_top_node = old_view[old_top_node_index] - if (old_top_node) { - const new_top_node_index = view.findIndex( - node => node.instance_path === old_top_node.instance_path - ) - if (new_top_node_index !== -1) { - new_scroll_top = new_top_node_index * node_height + scroll_offset - } - } - } - - const render_anchor_index = Math.max( - 0, - Math.floor(new_scroll_top / node_height) - ) + const new_scroll_top = calculate_new_scroll_top({ + old_scroll_top, + old_view, + focal_path: focal_instance_path + }) + const render_anchor_index = Math.max(0, Math.floor(new_scroll_top / node_height)) start_index = Math.max(0, render_anchor_index - chunk_size) end_index = Math.min(view.length, render_anchor_index + chunk_size) const fragment = document.createDocumentFragment() for (let i = start_index; i < end_index; i++) { if (view[i]) fragment.appendChild(create_node(view[i])) - else console.warn(`Missing node at index ${i} in view.`) } - container.replaceChildren() - container.appendChild(top_sentinel) - container.appendChild(fragment) - container.appendChild(bottom_sentinel) - + container.replaceChildren(top_sentinel, fragment, bottom_sentinel) top_sentinel.style.height = `${start_index * node_height}px` - bottom_sentinel.style.height = `${ - (view.length - end_index) * node_height - }px` + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` observer.observe(top_sentinel) observer.observe(bottom_sentinel) @@ -401,35 +310,13 @@ async function graph_explorer (opts) { vertical_scroll_value = container.scrollTop } - // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. - if (hub_toggle || hub_num > 0) { - spacer_element = document.createElement('div') - spacer_element.className = 'spacer' - container.appendChild(spacer_element) - - if (hub_toggle) { - requestAnimationFrame(() => { - const container_height = container.clientHeight - const content_height = view.length * node_height - const max_scroll_top = content_height - container_height - - if (new_scroll_top > max_scroll_top) { - spacer_initial_height = new_scroll_top - max_scroll_top - spacer_initial_scroll_top = new_scroll_top - spacer_element.style.height = `${spacer_initial_height}px` - } - set_scroll_and_sync() - }) - } else { - spacer_element.style.height = `${existing_spacer_height}px` - requestAnimationFrame(set_scroll_and_sync) - } - } else { - spacer_element = null - spacer_initial_height = 0 - spacer_initial_scroll_top = 0 - requestAnimationFrame(set_scroll_and_sync) - } + // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. + handle_spacer_element({ + hub_toggle, + existing_height: existing_spacer_height, + new_scroll_top, + sync_fn: set_scroll_and_sync + }) } // Traverses the hierarchical `all_entries` data and builds a flat `view` array for rendering. @@ -449,16 +336,9 @@ async function graph_explorer (opts) { const entry = all_entries[base_path] if (!entry) return [] - if (!instance_states[instance_path]) { - instance_states[instance_path] = { - expanded_subs: false, - expanded_hubs: false - } - } - const state = instance_states[instance_path] + const state = get_or_create_state(instance_states, instance_path) const is_hub_on_top = - base_path === all_entries[parent_base_path]?.hubs?.[0] || - base_path === '/' + base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' // Calculate the pipe trail for drawing the tree lines. Quite complex logic here. const children_pipe_trail = [...parent_pipe_trail] @@ -494,8 +374,8 @@ async function graph_explorer (opts) { // If hubs are expanded, recursively add them to the view first (they appear above the node). if (state.expanded_hubs && Array.isArray(entry.hubs)) { entry.hubs.forEach((hub_path, i, arr) => { - current_view = current_view.concat( - build_view_recursive({ + current_view.push( + ...build_view_recursive({ base_path: hub_path, parent_instance_path: instance_path, parent_base_path: base_path, @@ -527,8 +407,8 @@ async function graph_explorer (opts) { // If subs are expanded, recursively add them to the view (they appear below the node). if (state.expanded_subs && Array.isArray(entry.subs)) { entry.subs.forEach((sub_path, i, arr) => { - current_view = current_view.concat( - build_view_recursive({ + current_view.push( + ...build_view_recursive({ base_path: sub_path, parent_instance_path: instance_path, depth: depth + 1, @@ -560,13 +440,11 @@ async function graph_explorer (opts) { is_hub_on_top, is_search_match, is_direct_match, - is_in_original_view + is_in_original_view, + query }) { const entry = all_entries[base_path] if (!entry) { - console.error( - `Entry not found for path: ${base_path}. Cannot create node.` - ) const err_el = document.createElement('div') err_el.className = 'node error' err_el.textContent = `Error: Missing entry for ${base_path}` @@ -574,138 +452,54 @@ async function graph_explorer (opts) { } const states = mode === 'search' ? search_state_instances : instance_states - let state = states[instance_path] - if (!state) { - console.warn( - `State not found for instance: ${instance_path}. Using default.` - ) - state = { expanded_subs: false, expanded_hubs: false } - states[instance_path] = state - } - + const state = get_or_create_state(states, instance_path) const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path if (is_search_match) { el.classList.add('search-result') - if (is_direct_match) { - el.classList.add('direct-match') - } - if (!is_in_original_view) { - el.classList.add('new-entry') - } + if (is_direct_match) el.classList.add('direct-match') + if (!is_in_original_view) el.classList.add('new-entry') } - if (selected_instance_paths.includes(instance_path)) - el.classList.add('selected') - if (confirmed_instance_paths.includes(instance_path)) - el.classList.add('confirmed') + if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') + if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 - if (depth) { - el.style.paddingLeft = '17.5px' - } + if (depth) el.style.paddingLeft = '17.5px' el.style.height = `${node_height}px` - // Handle the special case for the root node since its a bit different. - if (base_path === '/' && instance_path === '|/') { - const { expanded_subs } = state - const prefix_class_name = expanded_subs ? 'tee-down' : 'line-h' - const prefix_class = - has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' - el.innerHTML = `
🪄
/🌐` - - const wand_el = el.querySelector('.wand') - if (wand_el) wand_el.onclick = reset - - if (has_subs) { - const prefix_el = el.querySelector('.prefix') - if (prefix_el) { - if (mode !== 'search') { - prefix_el.onclick = () => toggle_subs(instance_path) - } else { - prefix_el.onclick = null - } - } - } - - const name_el = el.querySelector('.name') - if (name_el) - name_el.onclick = ev => select_node(ev, instance_path, base_path) - - return el - } - - const prefix_class_name = get_prefix({ - is_last_sub, - has_subs, - state, - is_hub, - is_hub_on_top - }) - const pipe_html = pipe_trail - .map( - should_pipe => `` - ) - .join('') + if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) - const prefix_class = - has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' - const icon_class = - has_hubs && base_path !== '/' && mode !== 'search' - ? 'icon clickable' - : 'icon' + const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) + const pipe_html = pipe_trail.map(p => ``).join('') + const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const icon_class = has_hubs && base_path !== '/' && mode !== 'search' ? 'icon clickable' : 'icon' + const entry_name = entry.name || base_path + const name_html = (is_direct_match && query) + ? get_highlighted_name(entry_name, query) + : entry_name el.innerHTML = ` - ${pipe_html} + ${pipe_html} - ${entry.name || base_path} + ${name_html} ` - if (has_hubs && base_path !== '/') { - const icon_el = el.querySelector('.icon') - if (icon_el) { - if (mode !== 'search') { - icon_el.onclick = () => toggle_hubs(instance_path) - } else { - icon_el.onclick = null - } - } - } + const icon_el = el.querySelector('.icon') + if (icon_el && has_hubs && base_path !== '/') icon_el.onclick = mode !== 'search' ? () => toggle_hubs(instance_path) : null - if (has_subs) { - const prefix_el = el.querySelector('.prefix') - if (prefix_el) { - if (mode !== 'search') { - prefix_el.onclick = () => toggle_subs(instance_path) - } else { - prefix_el.onclick = null - } - } - } + const prefix_el = el.querySelector('.prefix') + if (prefix_el && has_subs) prefix_el.onclick = mode !== 'search' ? () => toggle_subs(instance_path) : null - const name_el = el.querySelector('.name') - if (name_el) - name_el.onclick = ev => select_node(ev, instance_path, base_path) + el.querySelector('.name').onclick = ev => select_node(ev, instance_path) - if ( - selected_instance_paths.includes(instance_path) || - confirmed_instance_paths.includes(instance_path) - ) { - const checkbox_div = document.createElement('div') - checkbox_div.className = 'confirm-wrapper' - const is_confirmed = confirmed_instance_paths.includes(instance_path) - checkbox_div.innerHTML = `` - const checkbox_input = checkbox_div.querySelector('input') - if (checkbox_input) - checkbox_input.onchange = ev => handle_confirm(ev, instance_path) - el.appendChild(checkbox_div) + if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { + el.appendChild(create_confirm_checkbox(instance_path)) } return el @@ -715,13 +509,8 @@ async function graph_explorer (opts) { function re_render_node (instance_path) { const node_data = view.find(n => n.instance_path === instance_path) if (node_data) { - const old_node_el = shadow.querySelector( - `[data-instance_path="${CSS.escape(instance_path)}"]` - ) - if (old_node_el) { - const new_node_el = create_node(node_data) - old_node_el.replaceWith(new_node_el) - } + const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (old_node_el) old_node_el.replaceWith(create_node(node_data)) } } @@ -755,7 +544,7 @@ async function graph_explorer (opts) { if (expanded_subs) return 'middle-tee-down' if (expanded_hubs) return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' - return has_subs ? 'middle-line' : 'middle-light-line' + return has_subs ? 'middle-line' : 'middle-light-line' } } @@ -763,59 +552,50 @@ async function graph_explorer (opts) { 5. MENUBAR AND SEARCH ******************************************************************************/ function render_menubar () { - menubar.replaceChildren() // Clear existing menubar - const search_button = document.createElement('button') - search_button.textContent = 'Search' - search_button.onclick = toggle_search_mode + const search_button = Object.assign(document.createElement('button'), { + textContent: 'Search', + onclick: toggle_search_mode + }) - menubar.appendChild(search_button) + if (mode !== 'search') return void menubar.replaceChildren(search_button) - if (mode === 'search') { - const search_input = document.createElement('input') - search_input.type = 'text' - search_input.placeholder = 'Search entries...' - search_input.className = 'search-input' - search_input.oninput = on_search_input - search_input.value = search_query - menubar.appendChild(search_input) - requestAnimationFrame(() => search_input.focus()) - } + const search_input = Object.assign(document.createElement('input'), { + type: 'text', + placeholder: 'Search entries...', + className: 'search-input', + value: search_query, + oninput: on_search_input + }) + + menubar.replaceChildren(search_button, search_input) + requestAnimationFrame(() => search_input.focus()) } function handle_mode_change () { - if (mode === 'default') { - menubar.style.display = 'none' - } else { - menubar.style.display = 'flex' - } + menubar.style.display = mode === 'default' ? 'none' : 'flex' build_and_render_view() } function toggle_search_mode () { - const new_mode = mode === 'search' ? previous_mode : 'search' if (mode === 'search') { search_query = '' drive_updated_by_search = true - update_mode_state('search_query', '') + update_drive_state({ dataset: 'mode', name: 'search_query', value: '' }) } - update_mode_state('current_mode', new_mode) + update_drive_state({ dataset: 'mode', name: 'current_mode', value: mode === 'search' ? previous_mode : 'search' }) search_state_instances = instance_states } function on_search_input (event) { - const query = event.target.value.trim() - search_query = query + search_query = event.target.value.trim() drive_updated_by_search = true - update_mode_state('search_query', query) - if (query === '') search_state_instances = instance_states - perform_search(query) + update_drive_state({ dataset: 'mode', name: 'search_query', value: search_query }) + if (search_query === '') search_state_instances = instance_states + perform_search(search_query) } function perform_search (query) { - if (!query) { - build_and_render_view() - return - } + if (!query) return build_and_render_view() const original_view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -858,46 +638,31 @@ async function graph_explorer (opts) { if (!entry) return [] const instance_path = `${parent_instance_path}|${base_path}` - const is_direct_match = - entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) - - let sub_results = [] - if (Array.isArray(entry.subs)) { - const children_pipe_trail = [...parent_pipe_trail] - if (depth > 0) children_pipe_trail.push(!is_last_sub) - - sub_results = entry.subs - .map((sub_path, i, arr) => { - return build_search_view_recursive({ - query, - base_path: sub_path, - parent_instance_path: instance_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: false, - parent_pipe_trail: children_pipe_trail, - instance_states, - all_entries, - original_view - }) - }) - .flat() - } + const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) - const has_matching_descendant = sub_results.length > 0 - - if (!is_direct_match && !has_matching_descendant) { - return [] - } + const children_pipe_trail = [...parent_pipe_trail] + if (depth > 0) children_pipe_trail.push(!is_last_sub) + + const sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => + build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries, + original_view + }) + ) - instance_states[instance_path] = { - expanded_subs: has_matching_descendant, - expanded_hubs: false - } + const has_matching_descendant = sub_results.length > 0 + if (!is_direct_match && !has_matching_descendant) return [] - const is_in_original_view = original_view.some( - node => node.instance_path === instance_path - ) + instance_states[instance_path] = { expanded_subs: has_matching_descendant, expanded_hubs: false } + const is_in_original_view = original_view.some(node => node.instance_path === instance_path) const current_node_view = { base_path, @@ -917,21 +682,15 @@ async function graph_explorer (opts) { function render_search_results (search_view, query) { view = search_view - container.replaceChildren() - if (search_view.length === 0) { const no_results_el = document.createElement('div') no_results_el.className = 'no-results' no_results_el.textContent = `No results for "${query}"` - container.appendChild(no_results_el) - return + return container.replaceChildren(no_results_el) } - const fragment = document.createDocumentFragment() - for (const node_data of search_view) { - fragment.appendChild(create_node(node_data)) - } - container.appendChild(fragment) + search_view.forEach(node_data => fragment.appendChild(create_node({ ...node_data, query }))) + container.replaceChildren(fragment) } /****************************************************************************** @@ -944,114 +703,71 @@ async function graph_explorer (opts) { let current_path = instance_path // Traverse up the tree to expand all parents while (current_path) { - const parent_path = current_path.substring( - 0, - current_path.lastIndexOf('|') - ) - if (!parent_path) break // Stop if there's no parent left - - if (!instance_states[parent_path]) { - instance_states[parent_path] = { - expanded_subs: false, - expanded_hubs: false - } - } - instance_states[parent_path].expanded_subs = true + const parent_path = current_path.substring(0, current_path.lastIndexOf('|')) + if (!parent_path) break + get_or_create_state(instance_states, parent_path).expanded_subs = true current_path = parent_path } drive_updated_by_toggle = true - update_runtime_state('instance_states', instance_states) - update_mode_state('current_mode', previous_mode) + update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) + update_drive_state({ dataset: 'mode', name: 'current_mode', value: previous_mode }) } + const new_selected = new Set(selected_instance_paths) if (ev.ctrlKey) { - const new_selected_paths = [...selected_instance_paths] - const index = new_selected_paths.indexOf(instance_path) - if (index > -1) { - new_selected_paths.splice(index, 1) - } else { - new_selected_paths.push(instance_path) - } - update_runtime_state('selected_instance_paths', new_selected_paths) + new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) } else { - update_runtime_state('selected_instance_paths', [instance_path]) + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [instance_path] }) } } function handle_confirm (ev, instance_path) { - if (!ev.target) return console.warn('Checkbox event target is missing.') + if (!ev.target) return const is_checked = ev.target.checked - const new_selected_paths = [...selected_instance_paths] - const new_confirmed_paths = [...confirmed_instance_paths] + const new_selected = new Set(selected_instance_paths) + const new_confirmed = new Set(confirmed_instance_paths) if (is_checked) { - const idx = new_selected_paths.indexOf(instance_path) - if (idx > -1) new_selected_paths.splice(idx, 1) - if (!new_confirmed_paths.includes(instance_path)) { - new_confirmed_paths.push(instance_path) - } + new_selected.delete(instance_path) + new_confirmed.add(instance_path) } else { - if (!new_selected_paths.includes(instance_path)) { - new_selected_paths.push(instance_path) - } - const idx = new_confirmed_paths.indexOf(instance_path) - if (idx > -1) new_confirmed_paths.splice(idx, 1) + new_selected.add(instance_path) + new_confirmed.delete(instance_path) } - update_runtime_state('selected_instance_paths', new_selected_paths) - update_runtime_state('confirmed_selected', new_confirmed_paths) + + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) + update_drive_state({ dataset: 'runtime', name: 'confirmed_selected', value: [...new_confirmed] }) } function toggle_subs (instance_path) { - if (!instance_states[instance_path]) { - console.warn( - `Toggling subs for non-existent state: ${instance_path}. Creating default state.` - ) - instance_states[instance_path] = { - expanded_subs: false, - expanded_hubs: false - } - } - const state = instance_states[instance_path] + const state = get_or_create_state(instance_states, instance_path) state.expanded_subs = !state.expanded_subs build_and_render_view(instance_path) // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true - update_runtime_state('instance_states', instance_states) + update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) } function toggle_hubs (instance_path) { - if (!instance_states[instance_path]) { - console.warn( - `Toggling hubs for non-existent state: ${instance_path}. Creating default state.` - ) - instance_states[instance_path] = { - expanded_subs: false, - expanded_hubs: false - } - } - const state = instance_states[instance_path] + const state = get_or_create_state(instance_states, instance_path) state.expanded_hubs ? hub_num-- : hub_num++ state.expanded_hubs = !state.expanded_hubs build_and_render_view(instance_path, true) drive_updated_by_toggle = true - update_runtime_state('instance_states', instance_states) + update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) } function reset () { - const root_path = '/' const root_instance_path = '|/' - const new_instance_states = {} - if (all_entries[root_path]) { - new_instance_states[root_instance_path] = { - expanded_subs: true, - expanded_hubs: false - } + const new_instance_states = { + [root_instance_path]: { expanded_subs: true, expanded_hubs: false } } - update_runtime_state('vertical_scroll_value', 0) - update_runtime_state('horizontal_scroll_value', 0) - update_runtime_state('selected_instance_paths', []) - update_runtime_state('confirmed_selected', []) - update_runtime_state('instance_states', new_instance_states) + update_drive_state({ dataset: 'runtime', name: 'vertical_scroll_value', value: 0 }) + update_drive_state({ dataset: 'runtime', name: 'horizontal_scroll_value', value: 0 }) + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [] }) + update_drive_state({ dataset: 'runtime', name: 'confirmed_selected', value: [] }) + update_drive_state({ dataset: 'runtime', name: 'instance_states', value: new_instance_states }) } /****************************************************************************** @@ -1064,26 +780,16 @@ async function graph_explorer (opts) { scroll_update_pending = true requestAnimationFrame(() => { const scroll_delta = vertical_scroll_value - container.scrollTop - // Handle removal of the scroll spacer. - if (spacer_element && scroll_delta > 0 && container.scrollTop == 0) { + if (spacer_element && scroll_delta > 0 && container.scrollTop === 0) { spacer_element.remove() spacer_element = null spacer_initial_height = 0 - spacer_initial_scroll_top = 0 hub_num = 0 } - if (vertical_scroll_value !== container.scrollTop) { - vertical_scroll_value = container.scrollTop - drive_updated_by_scroll = true // Set flag to prevent render loop. - update_runtime_state('vertical_scroll_value', vertical_scroll_value) - } - if (horizontal_scroll_value !== container.scrollLeft) { - horizontal_scroll_value = container.scrollLeft - drive_updated_by_scroll = true - update_runtime_state('horizontal_scroll_value', horizontal_scroll_value) - } + vertical_scroll_value = update_scroll_state({ current_value: vertical_scroll_value, new_value: container.scrollTop, name: 'vertical_scroll_value' }) + horizontal_scroll_value = update_scroll_state({ current_value: horizontal_scroll_value, new_value: container.scrollLeft, name: 'horizontal_scroll_value' }) scroll_update_pending = false }) } @@ -1093,10 +799,7 @@ async function graph_explorer (opts) { is_rendering = true const container_rect = container.getBoundingClientRect() let sentinel_rect = bottom_sentinel.getBoundingClientRect() - while ( - end_index < view.length && - sentinel_rect.top < container_rect.bottom + 500 - ) { + while (end_index < view.length && sentinel_rect.top < container_rect.bottom + 500) { render_next_chunk() await new Promise(resolve => requestAnimationFrame(resolve)) sentinel_rect = bottom_sentinel.getBoundingClientRect() @@ -1134,9 +837,7 @@ async function graph_explorer (opts) { if (view[i]) fragment.appendChild(create_node(view[i])) container.insertBefore(fragment, bottom_sentinel) end_index = next_end - bottom_sentinel.style.height = `${ - (view.length - end_index) * node_height - }px` + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` cleanup_dom(false) } @@ -1144,8 +845,9 @@ async function graph_explorer (opts) { if (start_index <= 0) return const fragment = document.createDocumentFragment() const prev_start = Math.max(0, start_index - chunk_size) - for (let i = prev_start; i < start_index; i++) + for (let i = prev_start; i < start_index; i++) { if (view[i]) fragment.appendChild(create_node(view[i])) + } container.insertBefore(fragment, top_sentinel.nextSibling) start_index = prev_start top_sentinel.style.height = `${start_index * node_height}px` @@ -1160,32 +862,180 @@ async function graph_explorer (opts) { const to_remove_count = rendered_count - max_rendered_nodes if (is_scrolling_up) { // If scrolling up, remove nodes from the bottom. - for (let i = 0; i < to_remove_count; i++) { - const temp = bottom_sentinel.previousElementSibling - if (temp && temp !== top_sentinel) { - temp.remove() - } - } + remove_dom_nodes({ count: to_remove_count, start_el: bottom_sentinel, next_prop: 'previousElementSibling', boundary_el: top_sentinel }) end_index -= to_remove_count - bottom_sentinel.style.height = `${ - (view.length - end_index) * node_height - }px` + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` } else { // If scrolling down, remove nodes from the top. - for (let i = 0; i < to_remove_count; i++) { - const temp = top_sentinel.nextElementSibling - if (temp && temp !== bottom_sentinel) { - temp.remove() - } - } + remove_dom_nodes({ count: to_remove_count, start_el: top_sentinel, next_prop: 'nextElementSibling', boundary_el: bottom_sentinel }) start_index += to_remove_count top_sentinel.style.height = `${start_index * node_height}px` } } + + /****************************************************************************** + 8. HELPER FUNCTIONS + ******************************************************************************/ +function get_highlighted_name (name, query) { + // Creates a new regular expression. + // `escape_regex(query)` sanitizes the query string to treat special regex characters literally. + // `(...)` creates a capturing group for the escaped query. + // 'gi' flags: 'g' for global (all occurrences), 'i' for case-insensitive. + const regex = new RegExp(`(${escape_regex(query)})`, 'gi') + // Replaces all matches of the regex in 'name' with the matched text wrapped in tags. + // '$1' refers to the content of the first capturing group (the matched query). + return name.replace(regex, '$1') +} + +function escape_regex (string) { + // Escapes special regular expression characters in a string. + // It replaces characters like -, /, \, ^, $, *, +, ?, ., (, ), |, [, ], {, } + // with their escaped versions (e.g., '.' becomes '\.'). + // This prevents them from being interpreted as regex metacharacters. + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char +} + + function check_and_reset_feedback_flags () { + if (drive_updated_by_scroll) { + drive_updated_by_scroll = false + return true + } + if (drive_updated_by_toggle) { + drive_updated_by_toggle = false + return true + } + if (drive_updated_by_search) { + drive_updated_by_search = false + return true + } + return false + } + + function parse_json_data (data, path) { + if (data === null) return null + try { + return typeof data === 'string' ? JSON.parse(data) : data + } catch (e) { + console.error(`Failed to parse JSON for ${path}:`, e) + return null + } + } + + function parse_json_data (data, path) { + if (data === null) return null + try { + return typeof data === 'string' ? JSON.parse(data) : data + } catch (e) { + console.error(`Failed to parse JSON for ${path}:`, e) + return null + } + } + + function process_path_array_update ({ current_paths, value, render_set, name }) { + const old_paths = [...current_paths] + const new_paths = Array.isArray(value) + ? value + : (console.warn(`${name} is not an array, defaulting to empty.`, value), []) + ;[...new Set([...old_paths, ...new_paths])].forEach(p => render_set.add(p)) + return new_paths + } + + function calculate_new_scroll_top ({ old_scroll_top, old_view, focal_path }) { + // Calculate the new scroll position to maintain the user's viewport. + if (focal_path) { + // If an action was focused on a specific node (like a toggle), try to keep it in the same position. + const old_idx = old_view.findIndex(n => n.instance_path === focal_path) + const new_idx = view.findIndex(n => n.instance_path === focal_path) + if (old_idx !== -1 && new_idx !== -1) { + return old_scroll_top + (new_idx - old_idx) * node_height + } + } else if (old_view.length > 0) { + // Otherwise, try to keep the topmost visible node in the same position. + const old_top_idx = Math.floor(old_scroll_top / node_height) + const old_top_node = old_view[old_top_idx] + if (old_top_node) { + const new_top_idx = view.findIndex(n => n.instance_path === old_top_node.instance_path) + if (new_top_idx !== -1) { + return new_top_idx * node_height + (old_scroll_top % node_height) + } + } + } + return old_scroll_top + } + + function handle_spacer_element ({ hub_toggle, existing_height, new_scroll_top, sync_fn }) { + if (hub_toggle || hub_num > 0) { + spacer_element = document.createElement('div') + spacer_element.className = 'spacer' + container.appendChild(spacer_element) + + if (hub_toggle) { + requestAnimationFrame(() => { + const max_scroll = container.scrollHeight - container.clientHeight + if (new_scroll_top > max_scroll) { + spacer_element.style.height = `${new_scroll_top - max_scroll}px` + } + sync_fn() + }) + } else { + spacer_element.style.height = `${existing_height}px` + requestAnimationFrame(sync_fn) + } + } else { + spacer_element = null + spacer_initial_height = 0 + requestAnimationFrame(sync_fn) + } + } + + function create_root_node ({ state, has_subs, instance_path }) { + // Handle the special case for the root node since its a bit different. + const el = document.createElement('div') + el.className = 'node type-root' + el.dataset.instance_path = instance_path + const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const prefix_name = state.expanded_subs ? 'tee-down' : 'line-h' + el.innerHTML = `
🪄
/🌐` + + el.querySelector('.wand').onclick = reset + if (has_subs) { + const prefix_el = el.querySelector('.prefix') + if (prefix_el) prefix_el.onclick = mode !== 'search' ? () => toggle_subs(instance_path) : null + } + el.querySelector('.name').onclick = ev => select_node(ev, instance_path) + return el + } + + function create_confirm_checkbox (instance_path) { + const checkbox_div = document.createElement('div') + checkbox_div.className = 'confirm-wrapper' + const is_confirmed = confirmed_instance_paths.includes(instance_path) + checkbox_div.innerHTML = `` + const checkbox_input = checkbox_div.querySelector('input') + if (checkbox_input) checkbox_input.onchange = ev => handle_confirm(ev, instance_path) + return checkbox_div + } + + function update_scroll_state ({ current_value, new_value, name }) { + if (current_value !== new_value) { + drive_updated_by_scroll = true // Set flag to prevent render loop. + update_drive_state({ dataset: 'runtime', name, value: new_value }) + return new_value + } + return current_value + } + + function remove_dom_nodes ({ count, start_el, next_prop, boundary_el }) { + for (let i = 0; i < count; i++) { + const temp = start_el[next_prop] + if (temp && temp !== boundary_el) temp.remove() + else break + } + } } /****************************************************************************** - 8. FALLBACK CONFIGURATION + 9. FALLBACK CONFIGURATION - This provides the default data and API configuration for the component, following the pattern described in `instructions.md`. - It defines the default datasets (`entries`, `style`, `runtime`) and their @@ -1203,96 +1053,7 @@ function fallback_module () { }, 'style/': { 'theme.css': { - raw: ` - .graph-container, .node { - font-family: monospace; - } - .graph-container { - color: #abb2bf; - background-color: #282c34; - padding: 10px; - height: 500px; /* Or make it flexible */ - overflow: auto; - } - .node { - display: flex; - align-items: center; - white-space: nowrap; - cursor: default; - } - .node.error { - color: red; - } - .node.selected { - background-color: #776346; - } - .node.confirmed { - background-color: #774346; - } - .node.new-entry { - background-color: #87ceeb; /* sky blue */ - } - .menubar { - display: flex; - padding: 5px; - background-color: #21252b; - border-bottom: 1px solid #181a1f; - } - .search-input { - margin-left: auto; - background-color: #282c34; - color: #abb2bf; - border: 1px solid #181a1f; - } - .confirm-wrapper { - margin-left: auto; - padding-left: 10px; - } - .indent { - display: flex; - } - .pipe { - text-align: center; - } - .pipe::before { content: '┃'; } - .blank { - width: 8.5px; - text-align: center; - } - .clickable { - cursor: pointer; - } - .prefix, .icon { - margin-right: 2px; - } - .top-cross::before { content: '┏╋'; } - .top-tee-down::before { content: '┏┳'; } - .top-tee-up::before { content: '┏┻'; } - .top-line::before { content: '┏━'; } - .middle-cross::before { content: '┣╋'; } - .middle-tee-down::before { content: '┣┳'; } - .middle-tee-up::before { content: '┣┻'; } - .middle-line::before { content: '┣━'; } - .bottom-cross::before { content: '┗╋'; } - .bottom-tee-down::before { content: '┗┳'; } - .bottom-tee-up::before { content: '┗┻'; } - .bottom-line::before { content: '┗━'; } - .bottom-light-tee-up::before { content: '┖┸'; } - .bottom-light-line::before { content: '┖─'; } - .middle-light-tee-up::before { content: '┠┸'; } - .middle-light-line::before { content: '┠─'; } - .tee-down::before { content: '┳'; } - .line-h::before { content: '━'; } - .icon { display: inline-block; text-align: center; } - .name { flex-grow: 1; } - .node.type-root > .icon::before { content: '🌐'; } - .node.type-folder > .icon::before { content: '📁'; } - .node.type-html-file > .icon::before { content: '📄'; } - .node.type-js-file > .icon::before { content: '📜'; } - .node.type-css-file > .icon::before { content: '🎨'; } - .node.type-json-file > .icon::before { content: '📝'; } - .node.type-file > .icon::before { content: '📄'; } - ` + '$ref' : 'theme.css' } }, 'runtime/': { @@ -1312,7 +1073,6 @@ function fallback_module () { } } } - }).call(this)}).call(this,"/lib/graph_explorer.js") },{"./STATE":1}],3:[function(require,module,exports){ const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' From bdc94c91aa9ca411f724dcb46b808ad124593fc8 Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 21 Aug 2025 00:10:04 +0500 Subject: [PATCH 050/130] removed some of the anomlies --- bundle.js | 11 ----------- lib/graph_explorer.js | 11 ----------- 2 files changed, 22 deletions(-) diff --git a/bundle.js b/bundle.js index d3be2b2..96fde54 100644 --- a/bundle.js +++ b/bundle.js @@ -34,7 +34,6 @@ async function graph_explorer (opts) { let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. - let spacer_initial_height = 0 let hub_num = 0 // Counter for expanded hubs. const el = document.createElement('div') @@ -921,16 +920,6 @@ function escape_regex (string) { } } - function parse_json_data (data, path) { - if (data === null) return null - try { - return typeof data === 'string' ? JSON.parse(data) : data - } catch (e) { - console.error(`Failed to parse JSON for ${path}:`, e) - return null - } - } - function process_path_array_update ({ current_paths, value, render_set, name }) { const old_paths = [...current_paths] const new_paths = Array.isArray(value) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 561ada5..d144e09 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -30,7 +30,6 @@ async function graph_explorer (opts) { let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. - let spacer_initial_height = 0 let hub_num = 0 // Counter for expanded hubs. const el = document.createElement('div') @@ -917,16 +916,6 @@ function escape_regex (string) { } } - function parse_json_data (data, path) { - if (data === null) return null - try { - return typeof data === 'string' ? JSON.parse(data) : data - } catch (e) { - console.error(`Failed to parse JSON for ${path}:`, e) - return null - } - } - function process_path_array_update ({ current_paths, value, render_set, name }) { const old_paths = [...current_paths] const new_paths = Array.isArray(value) From 558ff96ad8e9c3a19f295d6d3bc0b091c849642a Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 21 Aug 2025 17:02:24 +0500 Subject: [PATCH 051/130] fixed the spacer element bug --- lib/graph_explorer.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index d144e09..7e4beba 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -30,6 +30,8 @@ async function graph_explorer (opts) { let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. + let spacer_initial_height = 0 + let spacer_initial_scroll_top = 0 let hub_num = 0 // Counter for expanded hubs. const el = document.createElement('div') @@ -780,6 +782,7 @@ async function graph_explorer (opts) { spacer_element.remove() spacer_element = null spacer_initial_height = 0 + spacer_initial_scroll_top = 0 hub_num = 0 } @@ -956,9 +959,14 @@ function escape_regex (string) { if (hub_toggle) { requestAnimationFrame(() => { - const max_scroll = container.scrollHeight - container.clientHeight - if (new_scroll_top > max_scroll) { - spacer_element.style.height = `${new_scroll_top - max_scroll}px` + const container_height = container.clientHeight + const content_height = view.length * node_height + const max_scroll_top = content_height - container_height + + if (new_scroll_top > max_scroll_top) { + spacer_initial_height = new_scroll_top - max_scroll_top + spacer_initial_scroll_top = new_scroll_top + spacer_element.style.height = `${spacer_initial_height}px` } sync_fn() }) @@ -969,6 +977,7 @@ function escape_regex (string) { } else { spacer_element = null spacer_initial_height = 0 + spacer_initial_scroll_top = 0 requestAnimationFrame(sync_fn) } } From f4525a98feaf7da6f63be9ec9064aaa9dd65f14d Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 23 Aug 2025 10:51:23 +0500 Subject: [PATCH 052/130] Added manipulation in search mode --- lib/graph_explorer.js | 144 +++++++++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 31 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 7e4beba..43a6574 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -21,6 +21,7 @@ async function graph_explorer (opts) { let all_entries = {} // Holds the entire graph structure from entries.json. let instance_states = {} // Holds expansion state {expanded_subs, expanded_hubs} for each node instance. let search_state_instances = {} + let search_entry_states = {} // Holds expansion state for search mode interactions separately let view = [] // A flat array representing the visible nodes in the graph. let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. let previous_mode = 'menubar' @@ -186,6 +187,17 @@ async function graph_explorer (opts) { ) } break + case path.endsWith('search_entry_states.json'): + if (typeof value === 'object' && value && !Array.isArray(value)) { + search_entry_states = value + if (mode === 'search') needs_render = true + } else { + console.warn( + 'search_entry_states is not a valid object, ignoring.', + value + ) + } + break } }) @@ -450,6 +462,9 @@ async function graph_explorer (opts) { const states = mode === 'search' ? search_state_instances : instance_states const state = get_or_create_state(states, instance_path) + // For search mode interactions (hubs/subs toggle), use search_entry_states + const interaction_states = mode === 'search' ? search_entry_states : instance_states + const interaction_state = get_or_create_state(interaction_states, instance_path) const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path @@ -473,8 +488,8 @@ async function graph_explorer (opts) { const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) const pipe_html = pipe_trail.map(p => ``).join('') - const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' - const icon_class = has_hubs && base_path !== '/' && mode !== 'search' ? 'icon clickable' : 'icon' + const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + const icon_class = has_hubs && base_path !== '/' ? 'icon clickable' : 'icon' const entry_name = entry.name || base_path const name_html = (is_direct_match && query) ? get_highlighted_name(entry_name, query) @@ -488,10 +503,18 @@ async function graph_explorer (opts) { ` const icon_el = el.querySelector('.icon') - if (icon_el && has_hubs && base_path !== '/') icon_el.onclick = mode !== 'search' ? () => toggle_hubs(instance_path) : null + if (icon_el && has_hubs && base_path !== '/') { + icon_el.onclick = mode === 'search' + ? () => toggle_search_hubs(instance_path) + : () => toggle_hubs(instance_path) + } const prefix_el = el.querySelector('.prefix') - if (prefix_el && has_subs) prefix_el.onclick = mode !== 'search' ? () => toggle_subs(instance_path) : null + if (prefix_el && has_subs) { + prefix_el.onclick = mode === 'search' + ? () => toggle_search_subs(instance_path) + : () => toggle_subs(instance_path) + } el.querySelector('.name').onclick = ev => select_node(ev, instance_path) @@ -629,7 +652,8 @@ async function graph_explorer (opts) { parent_pipe_trail, instance_states, all_entries, - original_view + original_view, + is_expanded_child = false }) { const entry = all_entries[base_path] if (!entry) return [] @@ -640,25 +664,76 @@ async function graph_explorer (opts) { const children_pipe_trail = [...parent_pipe_trail] if (depth > 0) children_pipe_trail.push(!is_last_sub) - const sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => + // Process hubs if they should be expanded + const search_state = search_entry_states[instance_path] + const should_expand_hubs = search_state ? search_state.expanded_hubs : false + const should_expand_subs = search_state ? search_state.expanded_subs : false + + const hub_results = should_expand_hubs ? (entry.hubs || []).flatMap((hub_path, i, arr) => build_search_view_recursive({ query, - base_path: sub_path, + base_path: hub_path, parent_instance_path: instance_path, depth: depth + 1, is_last_sub: i === arr.length - 1, - is_hub: false, + is_hub: true, parent_pipe_trail: children_pipe_trail, instance_states, all_entries, - original_view + original_view, + is_expanded_child: true // Mark as expanded child }) - ) + ) : [] + + // Handle subs: if manually expanded, show ALL children; otherwise, search through them + let sub_results = [] + if (should_expand_subs) { + // Show ALL subs when manually expanded + sub_results = (entry.subs || []).map((sub_path, i, arr) => { + const sub_result = build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries, + original_view, + is_expanded_child: true // Mark as expanded child + }) + return sub_result + }).flat() + } else if (!is_expanded_child) { + // Only search through subs if this node itself isn't an expanded child + sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => + build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries, + original_view, + is_expanded_child: false // Regular search + }) + ) + } const has_matching_descendant = sub_results.length > 0 - if (!is_direct_match && !has_matching_descendant) return [] - - instance_states[instance_path] = { expanded_subs: has_matching_descendant, expanded_hubs: false } + + // If this is an expanded child, always include it regardless of search match + if (!is_expanded_child && !is_direct_match && !has_matching_descendant) return [] + + // Set instance states for rendering + const final_expand_subs = search_state ? search_state.expanded_subs : has_matching_descendant + const final_expand_hubs = search_state ? search_state.expanded_hubs : false + + instance_states[instance_path] = { expanded_subs: final_expand_subs, expanded_hubs: final_expand_hubs } const is_in_original_view = original_view.some(node => node.instance_path === instance_path) const current_node_view = { @@ -674,7 +749,7 @@ async function graph_explorer (opts) { is_in_original_view } - return [current_node_view, ...sub_results] + return [...hub_results, current_node_view, ...sub_results] } function render_search_results (search_view, query) { @@ -696,20 +771,6 @@ async function graph_explorer (opts) { toggling, and resetting the graph. ******************************************************************************/ function select_node (ev, instance_path) { - if (mode === 'search') { - let current_path = instance_path - // Traverse up the tree to expand all parents - while (current_path) { - const parent_path = current_path.substring(0, current_path.lastIndexOf('|')) - if (!parent_path) break - get_or_create_state(instance_states, parent_path).expanded_subs = true - current_path = parent_path - } - drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) - update_drive_state({ dataset: 'mode', name: 'current_mode', value: previous_mode }) - } - const new_selected = new Set(selected_instance_paths) if (ev.ctrlKey) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) @@ -755,6 +816,22 @@ async function graph_explorer (opts) { update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) } + function toggle_search_subs (instance_path) { + const state = get_or_create_state(search_entry_states, instance_path) + state.expanded_subs = !state.expanded_subs + perform_search(search_query) // Re-render search results with new state + drive_updated_by_toggle = true + update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + } + + function toggle_search_hubs (instance_path) { + const state = get_or_create_state(search_entry_states, instance_path) + state.expanded_hubs = !state.expanded_hubs + perform_search(search_query) // Re-render search results with new state + drive_updated_by_toggle = true + update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + } + function reset () { const root_instance_path = '|/' const new_instance_states = { @@ -987,14 +1064,18 @@ function escape_regex (string) { const el = document.createElement('div') el.className = 'node type-root' el.dataset.instance_path = instance_path - const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const prefix_class = has_subs ? 'prefix clickable' : 'prefix' const prefix_name = state.expanded_subs ? 'tee-down' : 'line-h' el.innerHTML = `
🪄
/🌐` el.querySelector('.wand').onclick = reset if (has_subs) { const prefix_el = el.querySelector('.prefix') - if (prefix_el) prefix_el.onclick = mode !== 'search' ? () => toggle_subs(instance_path) : null + if (prefix_el) { + prefix_el.onclick = mode === 'search' + ? () => toggle_search_subs(instance_path) + : () => toggle_subs(instance_path) + } } el.querySelector('.name').onclick = ev => select_node(ev, instance_path) return el @@ -1056,7 +1137,8 @@ function fallback_module () { 'horizontal_scroll_value.json': { raw: '0' }, 'selected_instance_paths.json': { raw: '[]' }, 'confirmed_selected.json': { raw: '[]' }, - 'instance_states.json': { raw: '{}' } + 'instance_states.json': { raw: '{}' }, + 'search_entry_states.json': { raw: '{}' } }, 'mode/': { 'current_mode.json': { raw: '"menubar"' }, From d910986407e0ecb8f824bc69abefcecc9f08f640 Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 27 Aug 2025 21:24:41 +0500 Subject: [PATCH 053/130] Fixed Prefix bugs & Created pipe_calculate func --- lib/graph_explorer.js | 150 ++++++++++++++++++++++++++++-------------- 1 file changed, 99 insertions(+), 51 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 43a6574..7d9cd18 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -265,6 +265,59 @@ async function graph_explorer (opts) { return states[instance_path] } + // Extracted pipe logic for reuse in both default and search modes + function calculate_pipe_trail ({ + depth, + is_hub, + is_last_sub, + is_first_hub = false, + is_hub_on_top, + parent_pipe_trail, + parent_base_path, + base_path, + all_entries + }) { + const children_pipe_trail = [...parent_pipe_trail] + let last_pipe = null + const calculated_is_hub_on_top = base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' + const final_is_hub_on_top = is_hub_on_top !== undefined ? is_hub_on_top : calculated_is_hub_on_top + + if (depth > 0) { + if (is_hub) { + last_pipe = [...parent_pipe_trail] + if (is_last_sub) { + children_pipe_trail.pop() + children_pipe_trail.push(true) + last_pipe.pop() + last_pipe.push(true) + if (is_first_hub) { + last_pipe.pop() + last_pipe.push(false) + } + } + if (final_is_hub_on_top && !is_last_sub) { + last_pipe.pop() + last_pipe.push(true) + children_pipe_trail.pop() + children_pipe_trail.push(true) + } + if (is_first_hub) { + children_pipe_trail.pop() + children_pipe_trail.push(false) + } + } + children_pipe_trail.push(is_hub || !is_last_sub) + } + + const pipe_trail = (is_hub && is_last_sub) || (is_hub && final_is_hub_on_top) ? last_pipe : parent_pipe_trail + + return { + children_pipe_trail, + pipe_trail, + is_hub_on_top: final_is_hub_on_top + } + } + /****************************************************************************** 3. VIEW AND RENDERING LOGIC - These functions build the `view` array and render the DOM. @@ -346,38 +399,18 @@ async function graph_explorer (opts) { if (!entry) return [] const state = get_or_create_state(instance_states, instance_path) - const is_hub_on_top = - base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' - - // Calculate the pipe trail for drawing the tree lines. Quite complex logic here. - const children_pipe_trail = [...parent_pipe_trail] - let last_pipe = null - if (depth > 0) { - if (is_hub) { - last_pipe = [...parent_pipe_trail] - if (is_last_sub) { - children_pipe_trail.pop() - children_pipe_trail.push(true) - last_pipe.pop() - last_pipe.push(true) - if (is_first_hub) { - last_pipe.pop() - last_pipe.push(false) - } - } - if (is_hub_on_top && !is_last_sub) { - last_pipe.pop() - last_pipe.push(true) - children_pipe_trail.pop() - children_pipe_trail.push(true) - } - if (is_first_hub) { - children_pipe_trail.pop() - children_pipe_trail.push(false) - } - } - children_pipe_trail.push(is_hub || !is_last_sub) - } + + // Use extracted pipe logic + const { children_pipe_trail, pipe_trail, is_hub_on_top } = calculate_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + parent_pipe_trail, + parent_base_path, + base_path, + all_entries + }) let current_view = [] // If hubs are expanded, recursively add them to the view first (they appear above the node). @@ -406,10 +439,7 @@ async function graph_explorer (opts) { depth, is_last_sub, is_hub, - pipe_trail: - (is_hub && is_last_sub) || (is_hub && is_hub_on_top) - ? last_pipe - : parent_pipe_trail, + pipe_trail, is_hub_on_top }) @@ -462,9 +492,7 @@ async function graph_explorer (opts) { const states = mode === 'search' ? search_state_instances : instance_states const state = get_or_create_state(states, instance_path) - // For search mode interactions (hubs/subs toggle), use search_entry_states - const interaction_states = mode === 'search' ? search_entry_states : instance_states - const interaction_state = get_or_create_state(interaction_states, instance_path) + const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path @@ -631,11 +659,13 @@ async function graph_explorer (opts) { query, base_path: '/', parent_instance_path: '', + parent_base_path: null, depth: 0, is_last_sub: true, is_hub: false, + is_first_hub: false, parent_pipe_trail: [], - instance_states: search_state_instances, // Use a temporary state for search + instance_states: search_state_instances, all_entries, original_view }) @@ -646,9 +676,11 @@ async function graph_explorer (opts) { query, base_path, parent_instance_path, + parent_base_path = null, depth, is_last_sub, is_hub, + is_first_hub = false, parent_pipe_trail, instance_states, all_entries, @@ -661,29 +693,42 @@ async function graph_explorer (opts) { const instance_path = `${parent_instance_path}|${base_path}` const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) - const children_pipe_trail = [...parent_pipe_trail] - if (depth > 0) children_pipe_trail.push(!is_last_sub) + // Use extracted pipe logic for consistent rendering + const { children_pipe_trail, pipe_trail, is_hub_on_top } = calculate_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + parent_pipe_trail, + parent_base_path, + base_path, + all_entries + }) // Process hubs if they should be expanded const search_state = search_entry_states[instance_path] const should_expand_hubs = search_state ? search_state.expanded_hubs : false const should_expand_subs = search_state ? search_state.expanded_subs : false - const hub_results = should_expand_hubs ? (entry.hubs || []).flatMap((hub_path, i, arr) => - build_search_view_recursive({ + // Process hubs: if manually expanded, show ALL hubs regardless of search match + const hub_results = should_expand_hubs ? (entry.hubs || []).map((hub_path, i, arr) => { + const hub_result = build_search_view_recursive({ query, base_path: hub_path, parent_instance_path: instance_path, + parent_base_path: base_path, depth: depth + 1, is_last_sub: i === arr.length - 1, is_hub: true, + is_first_hub: i === 0, parent_pipe_trail: children_pipe_trail, instance_states, all_entries, original_view, - is_expanded_child: true // Mark as expanded child + is_expanded_child: true }) - ) : [] + return hub_result + }).flat() : [] // Handle subs: if manually expanded, show ALL children; otherwise, search through them let sub_results = [] @@ -694,14 +739,16 @@ async function graph_explorer (opts) { query, base_path: sub_path, parent_instance_path: instance_path, + parent_base_path: base_path, depth: depth + 1, is_last_sub: i === arr.length - 1, is_hub: false, + is_first_hub: false, parent_pipe_trail: children_pipe_trail, instance_states, all_entries, original_view, - is_expanded_child: true // Mark as expanded child + is_expanded_child: true }) return sub_result }).flat() @@ -712,14 +759,15 @@ async function graph_explorer (opts) { query, base_path: sub_path, parent_instance_path: instance_path, + parent_base_path: base_path, depth: depth + 1, is_last_sub: i === arr.length - 1, is_hub: false, + is_first_hub: false, parent_pipe_trail: children_pipe_trail, instance_states, all_entries, - original_view, - is_expanded_child: false // Regular search + original_view }) ) } @@ -742,8 +790,8 @@ async function graph_explorer (opts) { depth, is_last_sub, is_hub, - pipe_trail: parent_pipe_trail, - is_hub_on_top: false, + pipe_trail, + is_hub_on_top, is_search_match: true, is_direct_match, is_in_original_view From 26a23401f10ff19a9a1ee2f6a98464aa64b5afe5 Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 27 Aug 2025 21:54:50 +0500 Subject: [PATCH 054/130] split `calculate_prefix()` to improve performance and simplicity --- lib/graph_explorer.js | 106 +++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 32 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 7d9cd18..6326085 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -265,6 +265,39 @@ async function graph_explorer (opts) { return states[instance_path] } + function calculate_children_pipe_trail ({ + depth, + is_hub, + is_last_sub, + is_first_hub = false, + parent_pipe_trail, + parent_base_path, + base_path, + all_entries + }) { + const children_pipe_trail = [...parent_pipe_trail] + const is_hub_on_top = base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' + + if (depth > 0) { + if (is_hub) { + if (is_last_sub) { + children_pipe_trail.pop() + children_pipe_trail.push(true) + } + if (is_hub_on_top && !is_last_sub) { + children_pipe_trail.pop() + children_pipe_trail.push(true) + } + if (is_first_hub) { + children_pipe_trail.pop() + children_pipe_trail.push(false) + } + } + children_pipe_trail.push(is_hub || !is_last_sub) + } + return { children_pipe_trail, is_hub_on_top } + } + // Extracted pipe logic for reuse in both default and search modes function calculate_pipe_trail ({ depth, @@ -311,11 +344,7 @@ async function graph_explorer (opts) { const pipe_trail = (is_hub && is_last_sub) || (is_hub && final_is_hub_on_top) ? last_pipe : parent_pipe_trail - return { - children_pipe_trail, - pipe_trail, - is_hub_on_top: final_is_hub_on_top - } + return { pipe_trail, is_hub_on_top: final_is_hub_on_top } } /****************************************************************************** @@ -400,8 +429,7 @@ async function graph_explorer (opts) { const state = get_or_create_state(instance_states, instance_path) - // Use extracted pipe logic - const { children_pipe_trail, pipe_trail, is_hub_on_top } = calculate_pipe_trail({ + const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ depth, is_hub, is_last_sub, @@ -439,8 +467,9 @@ async function graph_explorer (opts) { depth, is_last_sub, is_hub, - pipe_trail, - is_hub_on_top + is_first_hub, + parent_pipe_trail, + parent_base_path }) // If subs are expanded, recursively add them to the view (they appear below the node). @@ -450,6 +479,7 @@ async function graph_explorer (opts) { ...build_view_recursive({ base_path: sub_path, parent_instance_path: instance_path, + parent_base_path: base_path, depth: depth + 1, is_last_sub: i === arr.length - 1, is_hub: false, @@ -475,8 +505,9 @@ async function graph_explorer (opts) { depth, is_last_sub, is_hub, - pipe_trail, - is_hub_on_top, + is_first_hub, + parent_pipe_trail, + parent_base_path, is_search_match, is_direct_match, is_in_original_view, @@ -493,6 +524,17 @@ async function graph_explorer (opts) { const states = mode === 'search' ? search_state_instances : instance_states const state = get_or_create_state(states, instance_path) + const { pipe_trail, is_hub_on_top } = calculate_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + parent_pipe_trail, + parent_base_path, + base_path, + all_entries + }) + const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path @@ -654,6 +696,7 @@ async function graph_explorer (opts) { instance_states, all_entries }) + const original_view_paths = original_view.map(n => n.instance_path) search_state_instances = {} const search_view = build_search_view_recursive({ query, @@ -667,7 +710,7 @@ async function graph_explorer (opts) { parent_pipe_trail: [], instance_states: search_state_instances, all_entries, - original_view + original_view_paths }) render_search_results(search_view, query) } @@ -684,7 +727,7 @@ async function graph_explorer (opts) { parent_pipe_trail, instance_states, all_entries, - original_view, + original_view_paths, is_expanded_child = false }) { const entry = all_entries[base_path] @@ -694,7 +737,7 @@ async function graph_explorer (opts) { const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) // Use extracted pipe logic for consistent rendering - const { children_pipe_trail, pipe_trail, is_hub_on_top } = calculate_pipe_trail({ + const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ depth, is_hub, is_last_sub, @@ -709,10 +752,10 @@ async function graph_explorer (opts) { const search_state = search_entry_states[instance_path] const should_expand_hubs = search_state ? search_state.expanded_hubs : false const should_expand_subs = search_state ? search_state.expanded_subs : false - + // Process hubs: if manually expanded, show ALL hubs regardless of search match - const hub_results = should_expand_hubs ? (entry.hubs || []).map((hub_path, i, arr) => { - const hub_result = build_search_view_recursive({ + const hub_results = (should_expand_hubs ? (entry.hubs || []) : []).flatMap((hub_path, i, arr) => { + return build_search_view_recursive({ query, base_path: hub_path, parent_instance_path: instance_path, @@ -724,18 +767,17 @@ async function graph_explorer (opts) { parent_pipe_trail: children_pipe_trail, instance_states, all_entries, - original_view, + original_view_paths, is_expanded_child: true }) - return hub_result - }).flat() : [] + }) // Handle subs: if manually expanded, show ALL children; otherwise, search through them let sub_results = [] if (should_expand_subs) { // Show ALL subs when manually expanded - sub_results = (entry.subs || []).map((sub_path, i, arr) => { - const sub_result = build_search_view_recursive({ + sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => { + return build_search_view_recursive({ query, base_path: sub_path, parent_instance_path: instance_path, @@ -747,11 +789,10 @@ async function graph_explorer (opts) { parent_pipe_trail: children_pipe_trail, instance_states, all_entries, - original_view, + original_view_paths, is_expanded_child: true }) - return sub_result - }).flat() + }) } else if (!is_expanded_child) { // Only search through subs if this node itself isn't an expanded child sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => @@ -767,22 +808,22 @@ async function graph_explorer (opts) { parent_pipe_trail: children_pipe_trail, instance_states, all_entries, - original_view + original_view_paths }) ) } const has_matching_descendant = sub_results.length > 0 - + // If this is an expanded child, always include it regardless of search match if (!is_expanded_child && !is_direct_match && !has_matching_descendant) return [] - + // Set instance states for rendering const final_expand_subs = search_state ? search_state.expanded_subs : has_matching_descendant const final_expand_hubs = search_state ? search_state.expanded_hubs : false - + instance_states[instance_path] = { expanded_subs: final_expand_subs, expanded_hubs: final_expand_hubs } - const is_in_original_view = original_view.some(node => node.instance_path === instance_path) + const is_in_original_view = original_view_paths.includes(instance_path) const current_node_view = { base_path, @@ -790,8 +831,9 @@ async function graph_explorer (opts) { depth, is_last_sub, is_hub, - pipe_trail, - is_hub_on_top, + is_first_hub, + parent_pipe_trail, + parent_base_path, is_search_match: true, is_direct_match, is_in_original_view From e060bbe58631386ae1814eec038ae659ef827660 Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 27 Aug 2025 22:53:29 +0500 Subject: [PATCH 055/130] Some more pipe related fixes --- lib/graph_explorer.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 6326085..3b65342 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -291,6 +291,10 @@ async function graph_explorer (opts) { if (is_first_hub) { children_pipe_trail.pop() children_pipe_trail.push(false) + if (mode === 'search') { + children_pipe_trail.pop() + children_pipe_trail.push(is_last_sub) + } } } children_pipe_trail.push(is_hub || !is_last_sub) @@ -310,7 +314,6 @@ async function graph_explorer (opts) { base_path, all_entries }) { - const children_pipe_trail = [...parent_pipe_trail] let last_pipe = null const calculated_is_hub_on_top = base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' const final_is_hub_on_top = is_hub_on_top !== undefined ? is_hub_on_top : calculated_is_hub_on_top @@ -319,8 +322,6 @@ async function graph_explorer (opts) { if (is_hub) { last_pipe = [...parent_pipe_trail] if (is_last_sub) { - children_pipe_trail.pop() - children_pipe_trail.push(true) last_pipe.pop() last_pipe.push(true) if (is_first_hub) { @@ -331,20 +332,13 @@ async function graph_explorer (opts) { if (final_is_hub_on_top && !is_last_sub) { last_pipe.pop() last_pipe.push(true) - children_pipe_trail.pop() - children_pipe_trail.push(true) - } - if (is_first_hub) { - children_pipe_trail.pop() - children_pipe_trail.push(false) } } - children_pipe_trail.push(is_hub || !is_last_sub) } const pipe_trail = (is_hub && is_last_sub) || (is_hub && final_is_hub_on_top) ? last_pipe : parent_pipe_trail - - return { pipe_trail, is_hub_on_top: final_is_hub_on_top } + const product = { pipe_trail, is_hub_on_top: final_is_hub_on_top } + return product } /****************************************************************************** From 3889ee47dce5b1d505d7b89715f590ea133dc1ca Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 27 Aug 2025 22:55:56 +0500 Subject: [PATCH 056/130] bundled --- bundle.js | 335 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 255 insertions(+), 80 deletions(-) diff --git a/bundle.js b/bundle.js index 96fde54..5f77ee7 100644 --- a/bundle.js +++ b/bundle.js @@ -25,6 +25,7 @@ async function graph_explorer (opts) { let all_entries = {} // Holds the entire graph structure from entries.json. let instance_states = {} // Holds expansion state {expanded_subs, expanded_hubs} for each node instance. let search_state_instances = {} + let search_entry_states = {} // Holds expansion state for search mode interactions separately let view = [] // A flat array representing the visible nodes in the graph. let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. let previous_mode = 'menubar' @@ -34,6 +35,8 @@ async function graph_explorer (opts) { let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. + let spacer_initial_height = 0 + let spacer_initial_scroll_top = 0 let hub_num = 0 // Counter for expanded hubs. const el = document.createElement('div') @@ -188,6 +191,17 @@ async function graph_explorer (opts) { ) } break + case path.endsWith('search_entry_states.json'): + if (typeof value === 'object' && value && !Array.isArray(value)) { + search_entry_states = value + if (mode === 'search') needs_render = true + } else { + console.warn( + 'search_entry_states is not a valid object, ignoring.', + value + ) + } + break } }) @@ -255,6 +269,82 @@ async function graph_explorer (opts) { return states[instance_path] } + function calculate_children_pipe_trail ({ + depth, + is_hub, + is_last_sub, + is_first_hub = false, + parent_pipe_trail, + parent_base_path, + base_path, + all_entries + }) { + const children_pipe_trail = [...parent_pipe_trail] + const is_hub_on_top = base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' + + if (depth > 0) { + if (is_hub) { + if (is_last_sub) { + children_pipe_trail.pop() + children_pipe_trail.push(true) + } + if (is_hub_on_top && !is_last_sub) { + children_pipe_trail.pop() + children_pipe_trail.push(true) + } + if (is_first_hub) { + children_pipe_trail.pop() + children_pipe_trail.push(false) + if (mode === 'search') { + children_pipe_trail.pop() + children_pipe_trail.push(is_last_sub) + } + } + } + children_pipe_trail.push(is_hub || !is_last_sub) + } + return { children_pipe_trail, is_hub_on_top } + } + + // Extracted pipe logic for reuse in both default and search modes + function calculate_pipe_trail ({ + depth, + is_hub, + is_last_sub, + is_first_hub = false, + is_hub_on_top, + parent_pipe_trail, + parent_base_path, + base_path, + all_entries + }) { + let last_pipe = null + const calculated_is_hub_on_top = base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' + const final_is_hub_on_top = is_hub_on_top !== undefined ? is_hub_on_top : calculated_is_hub_on_top + + if (depth > 0) { + if (is_hub) { + last_pipe = [...parent_pipe_trail] + if (is_last_sub) { + last_pipe.pop() + last_pipe.push(true) + if (is_first_hub) { + last_pipe.pop() + last_pipe.push(false) + } + } + if (final_is_hub_on_top && !is_last_sub) { + last_pipe.pop() + last_pipe.push(true) + } + } + } + + const pipe_trail = (is_hub && is_last_sub) || (is_hub && final_is_hub_on_top) ? last_pipe : parent_pipe_trail + const product = { pipe_trail, is_hub_on_top: final_is_hub_on_top } + return product + } + /****************************************************************************** 3. VIEW AND RENDERING LOGIC - These functions build the `view` array and render the DOM. @@ -336,38 +426,17 @@ async function graph_explorer (opts) { if (!entry) return [] const state = get_or_create_state(instance_states, instance_path) - const is_hub_on_top = - base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' - - // Calculate the pipe trail for drawing the tree lines. Quite complex logic here. - const children_pipe_trail = [...parent_pipe_trail] - let last_pipe = null - if (depth > 0) { - if (is_hub) { - last_pipe = [...parent_pipe_trail] - if (is_last_sub) { - children_pipe_trail.pop() - children_pipe_trail.push(true) - last_pipe.pop() - last_pipe.push(true) - if (is_first_hub) { - last_pipe.pop() - last_pipe.push(false) - } - } - if (is_hub_on_top && !is_last_sub) { - last_pipe.pop() - last_pipe.push(true) - children_pipe_trail.pop() - children_pipe_trail.push(true) - } - if (is_first_hub) { - children_pipe_trail.pop() - children_pipe_trail.push(false) - } - } - children_pipe_trail.push(is_hub || !is_last_sub) - } + + const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + parent_pipe_trail, + parent_base_path, + base_path, + all_entries + }) let current_view = [] // If hubs are expanded, recursively add them to the view first (they appear above the node). @@ -396,11 +465,9 @@ async function graph_explorer (opts) { depth, is_last_sub, is_hub, - pipe_trail: - (is_hub && is_last_sub) || (is_hub && is_hub_on_top) - ? last_pipe - : parent_pipe_trail, - is_hub_on_top + is_first_hub, + parent_pipe_trail, + parent_base_path }) // If subs are expanded, recursively add them to the view (they appear below the node). @@ -410,6 +477,7 @@ async function graph_explorer (opts) { ...build_view_recursive({ base_path: sub_path, parent_instance_path: instance_path, + parent_base_path: base_path, depth: depth + 1, is_last_sub: i === arr.length - 1, is_hub: false, @@ -435,8 +503,9 @@ async function graph_explorer (opts) { depth, is_last_sub, is_hub, - pipe_trail, - is_hub_on_top, + is_first_hub, + parent_pipe_trail, + parent_base_path, is_search_match, is_direct_match, is_in_original_view, @@ -452,6 +521,18 @@ async function graph_explorer (opts) { const states = mode === 'search' ? search_state_instances : instance_states const state = get_or_create_state(states, instance_path) + + const { pipe_trail, is_hub_on_top } = calculate_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + parent_pipe_trail, + parent_base_path, + base_path, + all_entries + }) + const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path @@ -475,8 +556,8 @@ async function graph_explorer (opts) { const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) const pipe_html = pipe_trail.map(p => ``).join('') - const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' - const icon_class = has_hubs && base_path !== '/' && mode !== 'search' ? 'icon clickable' : 'icon' + const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + const icon_class = has_hubs && base_path !== '/' ? 'icon clickable' : 'icon' const entry_name = entry.name || base_path const name_html = (is_direct_match && query) ? get_highlighted_name(entry_name, query) @@ -490,10 +571,18 @@ async function graph_explorer (opts) { ` const icon_el = el.querySelector('.icon') - if (icon_el && has_hubs && base_path !== '/') icon_el.onclick = mode !== 'search' ? () => toggle_hubs(instance_path) : null + if (icon_el && has_hubs && base_path !== '/') { + icon_el.onclick = mode === 'search' + ? () => toggle_search_hubs(instance_path) + : () => toggle_hubs(instance_path) + } const prefix_el = el.querySelector('.prefix') - if (prefix_el && has_subs) prefix_el.onclick = mode !== 'search' ? () => toggle_subs(instance_path) : null + if (prefix_el && has_subs) { + prefix_el.onclick = mode === 'search' + ? () => toggle_search_subs(instance_path) + : () => toggle_subs(instance_path) + } el.querySelector('.name').onclick = ev => select_node(ev, instance_path) @@ -605,18 +694,21 @@ async function graph_explorer (opts) { instance_states, all_entries }) + const original_view_paths = original_view.map(n => n.instance_path) search_state_instances = {} const search_view = build_search_view_recursive({ query, base_path: '/', parent_instance_path: '', + parent_base_path: null, depth: 0, is_last_sub: true, is_hub: false, + is_first_hub: false, parent_pipe_trail: [], - instance_states: search_state_instances, // Use a temporary state for search + instance_states: search_state_instances, all_entries, - original_view + original_view_paths }) render_search_results(search_view, query) } @@ -625,13 +717,16 @@ async function graph_explorer (opts) { query, base_path, parent_instance_path, + parent_base_path = null, depth, is_last_sub, is_hub, + is_first_hub = false, parent_pipe_trail, instance_states, all_entries, - original_view + original_view_paths, + is_expanded_child = false }) { const entry = all_entries[base_path] if (!entry) return [] @@ -639,29 +734,94 @@ async function graph_explorer (opts) { const instance_path = `${parent_instance_path}|${base_path}` const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) - const children_pipe_trail = [...parent_pipe_trail] - if (depth > 0) children_pipe_trail.push(!is_last_sub) + // Use extracted pipe logic for consistent rendering + const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + parent_pipe_trail, + parent_base_path, + base_path, + all_entries + }) + + // Process hubs if they should be expanded + const search_state = search_entry_states[instance_path] + const should_expand_hubs = search_state ? search_state.expanded_hubs : false + const should_expand_subs = search_state ? search_state.expanded_subs : false - const sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => - build_search_view_recursive({ + // Process hubs: if manually expanded, show ALL hubs regardless of search match + const hub_results = (should_expand_hubs ? (entry.hubs || []) : []).flatMap((hub_path, i, arr) => { + return build_search_view_recursive({ query, - base_path: sub_path, + base_path: hub_path, parent_instance_path: instance_path, + parent_base_path: base_path, depth: depth + 1, is_last_sub: i === arr.length - 1, - is_hub: false, + is_hub: true, + is_first_hub: i === 0, parent_pipe_trail: children_pipe_trail, instance_states, all_entries, - original_view + original_view_paths, + is_expanded_child: true + }) + }) + + // Handle subs: if manually expanded, show ALL children; otherwise, search through them + let sub_results = [] + if (should_expand_subs) { + // Show ALL subs when manually expanded + sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => { + return build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + is_first_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries, + original_view_paths, + is_expanded_child: true + }) }) - ) + } else if (!is_expanded_child) { + // Only search through subs if this node itself isn't an expanded child + sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => + build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === arr.length - 1, + is_hub: false, + is_first_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + all_entries, + original_view_paths + }) + ) + } const has_matching_descendant = sub_results.length > 0 - if (!is_direct_match && !has_matching_descendant) return [] - instance_states[instance_path] = { expanded_subs: has_matching_descendant, expanded_hubs: false } - const is_in_original_view = original_view.some(node => node.instance_path === instance_path) + // If this is an expanded child, always include it regardless of search match + if (!is_expanded_child && !is_direct_match && !has_matching_descendant) return [] + + // Set instance states for rendering + const final_expand_subs = search_state ? search_state.expanded_subs : has_matching_descendant + const final_expand_hubs = search_state ? search_state.expanded_hubs : false + + instance_states[instance_path] = { expanded_subs: final_expand_subs, expanded_hubs: final_expand_hubs } + const is_in_original_view = original_view_paths.includes(instance_path) const current_node_view = { base_path, @@ -669,14 +829,15 @@ async function graph_explorer (opts) { depth, is_last_sub, is_hub, - pipe_trail: parent_pipe_trail, - is_hub_on_top: false, + is_first_hub, + parent_pipe_trail, + parent_base_path, is_search_match: true, is_direct_match, is_in_original_view } - return [current_node_view, ...sub_results] + return [...hub_results, current_node_view, ...sub_results] } function render_search_results (search_view, query) { @@ -698,20 +859,6 @@ async function graph_explorer (opts) { toggling, and resetting the graph. ******************************************************************************/ function select_node (ev, instance_path) { - if (mode === 'search') { - let current_path = instance_path - // Traverse up the tree to expand all parents - while (current_path) { - const parent_path = current_path.substring(0, current_path.lastIndexOf('|')) - if (!parent_path) break - get_or_create_state(instance_states, parent_path).expanded_subs = true - current_path = parent_path - } - drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) - update_drive_state({ dataset: 'mode', name: 'current_mode', value: previous_mode }) - } - const new_selected = new Set(selected_instance_paths) if (ev.ctrlKey) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) @@ -757,6 +904,22 @@ async function graph_explorer (opts) { update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) } + function toggle_search_subs (instance_path) { + const state = get_or_create_state(search_entry_states, instance_path) + state.expanded_subs = !state.expanded_subs + perform_search(search_query) // Re-render search results with new state + drive_updated_by_toggle = true + update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + } + + function toggle_search_hubs (instance_path) { + const state = get_or_create_state(search_entry_states, instance_path) + state.expanded_hubs = !state.expanded_hubs + perform_search(search_query) // Re-render search results with new state + drive_updated_by_toggle = true + update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + } + function reset () { const root_instance_path = '|/' const new_instance_states = { @@ -784,6 +947,7 @@ async function graph_explorer (opts) { spacer_element.remove() spacer_element = null spacer_initial_height = 0 + spacer_initial_scroll_top = 0 hub_num = 0 } @@ -960,9 +1124,14 @@ function escape_regex (string) { if (hub_toggle) { requestAnimationFrame(() => { - const max_scroll = container.scrollHeight - container.clientHeight - if (new_scroll_top > max_scroll) { - spacer_element.style.height = `${new_scroll_top - max_scroll}px` + const container_height = container.clientHeight + const content_height = view.length * node_height + const max_scroll_top = content_height - container_height + + if (new_scroll_top > max_scroll_top) { + spacer_initial_height = new_scroll_top - max_scroll_top + spacer_initial_scroll_top = new_scroll_top + spacer_element.style.height = `${spacer_initial_height}px` } sync_fn() }) @@ -973,6 +1142,7 @@ function escape_regex (string) { } else { spacer_element = null spacer_initial_height = 0 + spacer_initial_scroll_top = 0 requestAnimationFrame(sync_fn) } } @@ -982,14 +1152,18 @@ function escape_regex (string) { const el = document.createElement('div') el.className = 'node type-root' el.dataset.instance_path = instance_path - const prefix_class = has_subs && mode !== 'search' ? 'prefix clickable' : 'prefix' + const prefix_class = has_subs ? 'prefix clickable' : 'prefix' const prefix_name = state.expanded_subs ? 'tee-down' : 'line-h' el.innerHTML = `
🪄
/🌐` el.querySelector('.wand').onclick = reset if (has_subs) { const prefix_el = el.querySelector('.prefix') - if (prefix_el) prefix_el.onclick = mode !== 'search' ? () => toggle_subs(instance_path) : null + if (prefix_el) { + prefix_el.onclick = mode === 'search' + ? () => toggle_search_subs(instance_path) + : () => toggle_subs(instance_path) + } } el.querySelector('.name').onclick = ev => select_node(ev, instance_path) return el @@ -1051,7 +1225,8 @@ function fallback_module () { 'horizontal_scroll_value.json': { raw: '0' }, 'selected_instance_paths.json': { raw: '[]' }, 'confirmed_selected.json': { raw: '[]' }, - 'instance_states.json': { raw: '{}' } + 'instance_states.json': { raw: '{}' }, + 'search_entry_states.json': { raw: '{}' } }, 'mode/': { 'current_mode.json': { raw: '"menubar"' }, From e2455ca8720f768f148a6e6fc758acba282013e5 Mon Sep 17 00:00:00 2001 From: ddroid Date: Thu, 28 Aug 2025 18:04:04 +0500 Subject: [PATCH 057/130] Fixed the pipes bug --- lib/graph_explorer.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 3b65342..70261b3 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -291,10 +291,6 @@ async function graph_explorer (opts) { if (is_first_hub) { children_pipe_trail.pop() children_pipe_trail.push(false) - if (mode === 'search') { - children_pipe_trail.pop() - children_pipe_trail.push(is_last_sub) - } } } children_pipe_trail.push(is_hub || !is_last_sub) @@ -757,7 +753,7 @@ async function graph_explorer (opts) { depth: depth + 1, is_last_sub: i === arr.length - 1, is_hub: true, - is_first_hub: i === 0, + is_first_hub: is_hub_on_top, parent_pipe_trail: children_pipe_trail, instance_states, all_entries, From f49ce0771a3ec43c9893b1968d8f7e85a0e3d54b Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 29 Aug 2025 20:34:57 +0500 Subject: [PATCH 058/130] Added Reset for Search mode & did some tweaks --- lib/graph_explorer.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 70261b3..cfe710b 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -913,6 +913,14 @@ async function graph_explorer (opts) { } function reset () { + // reset all of the manual expansions made + if (mode === 'search') { + search_entry_states = {} + drive_updated_by_toggle = true + update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + perform_search(search_query) + return + } const root_instance_path = '|/' const new_instance_states = { [root_instance_path]: { expanded_subs: true, expanded_hubs: false } @@ -1144,20 +1152,18 @@ function escape_regex (string) { const el = document.createElement('div') el.className = 'node type-root' el.dataset.instance_path = instance_path - const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + const prefix_class = has_subs || mode === 'search' ? 'prefix clickable' : 'prefix' const prefix_name = state.expanded_subs ? 'tee-down' : 'line-h' - el.innerHTML = `
🪄
/🌐` + el.innerHTML = `
🪄
/🌐` el.querySelector('.wand').onclick = reset if (has_subs) { const prefix_el = el.querySelector('.prefix') if (prefix_el) { - prefix_el.onclick = mode === 'search' - ? () => toggle_search_subs(instance_path) - : () => toggle_subs(instance_path) + prefix_el.onclick = mode === 'search' ? null : () => toggle_subs(instance_path) } } - el.querySelector('.name').onclick = ev => select_node(ev, instance_path) + el.querySelector('.name').onclick = ev => mode === 'search' ? null : select_node(ev, instance_path) return el } From 0a3249cc293dec55078743f8e59ff77241f3dbf4 Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 29 Aug 2025 20:37:18 +0500 Subject: [PATCH 059/130] Added Selection to search mode --- lib/graph_explorer.js | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index cfe710b..639e0fc 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -24,7 +24,7 @@ async function graph_explorer (opts) { let search_entry_states = {} // Holds expansion state for search mode interactions separately let view = [] // A flat array representing the visible nodes in the graph. let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. - let previous_mode = 'menubar' + let previous_mode let search_query = '' let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. @@ -576,7 +576,7 @@ async function graph_explorer (opts) { : () => toggle_subs(instance_path) } - el.querySelector('.name').onclick = ev => select_node(ev, instance_path) + el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { el.appendChild(create_confirm_checkbox(instance_path)) @@ -860,6 +860,36 @@ async function graph_explorer (opts) { } } + // Add the clicked entry and all its parents in the default tree + function search_expand_into_default (target_instance_path) { + if (!target_instance_path) return + const parts = target_instance_path.split('|').filter(Boolean) + if (parts.length === 0) return + + const root_state = get_or_create_state(instance_states, '|/') + root_state.expanded_subs = true + + // Walk from root to target, expanding the path relative to alredy expanded entries + for (let i = 0; i < parts.length - 1; i++) { + const parent_base = parts[i] + const child_base = parts[i + 1] + const parent_instance_path = parts.slice(0, i + 1).map(p => '|' + p).join('') + const parent_state = get_or_create_state(instance_states, parent_instance_path) + const parent_entry = all_entries[parent_base] + if (!parent_entry) continue + if (Array.isArray(parent_entry.subs) && parent_entry.subs.includes(child_base)) parent_state.expanded_subs = true + if (Array.isArray(parent_entry.hubs) && parent_entry.hubs.includes(child_base)) parent_state.expanded_hubs = true + } + + // Persist selection and expansion state + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [target_instance_path] }) + drive_updated_by_toggle = true + update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) + search_query = '' + update_drive_state({ dataset: 'mode', name: 'query', value: '' }) + update_drive_state({ dataset: 'mode', name: 'current_mode', value: previous_mode }) + } + function handle_confirm (ev, instance_path) { if (!ev.target) return const is_checked = ev.target.checked From 2df0367f80706d26afee6c170ad2c76bc8163d8e Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 29 Aug 2025 20:56:51 +0500 Subject: [PATCH 060/130] Added Multiselect --- lib/graph_explorer.js | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 639e0fc..2d7926c 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -29,6 +29,7 @@ async function graph_explorer (opts) { let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. + let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 @@ -208,7 +209,7 @@ async function graph_explorer (opts) { } function on_mode ({ data, paths }) { - let new_current_mode, new_previous_mode, new_search_query + let new_current_mode, new_previous_mode, new_search_query, new_multi_select_enabled paths.forEach((path, i) => { const value = parse_json_data(data[i], path) @@ -217,18 +218,22 @@ async function graph_explorer (opts) { if (path.endsWith('current_mode.json')) new_current_mode = value else if (path.endsWith('previous_mode.json')) new_previous_mode = value else if (path.endsWith('search_query.json')) new_search_query = value + else if (path.endsWith('multi_select_enabled.json')) new_multi_select_enabled = value }) if (typeof new_search_query === 'string') search_query = new_search_query if (new_previous_mode) previous_mode = new_previous_mode + if (typeof new_multi_select_enabled === 'boolean') { + multi_select_enabled = new_multi_select_enabled + render_menubar() // Re-render menubar to update button text + } if ( new_current_mode && !['default', 'menubar', 'search'].includes(new_current_mode) ) { - return void console.warn( - `Invalid mode "${new_current_mode}" provided. Ignoring update.` - ) + console.warn(`Invalid mode "${new_current_mode}" provided. Ignoring update.`) + return } if (new_current_mode === 'search' && !search_query) { @@ -344,7 +349,10 @@ async function graph_explorer (opts) { - `build_view_recursive` creates the flat `view` array from the hierarchical data. ******************************************************************************/ function build_and_render_view (focal_instance_path, hub_toggle = false) { - if (Object.keys(all_entries).length === 0) return void console.warn('No entries available to render.') + if (Object.keys(all_entries).length === 0) { + console.warn('No entries available to render.') + return + } const old_view = [...view] const old_scroll_top = vertical_scroll_value @@ -637,7 +645,11 @@ async function graph_explorer (opts) { onclick: toggle_search_mode }) - if (mode !== 'search') return void menubar.replaceChildren(search_button) + const multi_select_button = document.createElement('button') + multi_select_button.innerHTML = `Multi Select: ${multi_select_enabled ? 'true' : 'false'}` + multi_select_button.onclick = mode === 'search' ? null : toggle_multi_select + + if (mode !== 'search') return menubar.replaceChildren(search_button, multi_select_button) const search_input = Object.assign(document.createElement('input'), { type: 'text', @@ -647,7 +659,7 @@ async function graph_explorer (opts) { oninput: on_search_input }) - menubar.replaceChildren(search_button, search_input) + menubar.replaceChildren(search_button, multi_select_button, search_input) requestAnimationFrame(() => search_input.focus()) } @@ -666,6 +678,12 @@ async function graph_explorer (opts) { search_state_instances = instance_states } + function toggle_multi_select () { + multi_select_enabled = !multi_select_enabled + update_drive_state({ dataset: 'mode', name: 'multi_select_enabled', value: multi_select_enabled }) + render_menubar() // Re-render to update button text + } + function on_search_input (event) { search_query = event.target.value.trim() drive_updated_by_search = true @@ -852,7 +870,7 @@ async function graph_explorer (opts) { ******************************************************************************/ function select_node (ev, instance_path) { const new_selected = new Set(selected_instance_paths) - if (ev.ctrlKey) { + if (ev.ctrlKey || multi_select_enabled) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) } else { @@ -1259,7 +1277,8 @@ function fallback_module () { 'mode/': { 'current_mode.json': { raw: '"menubar"' }, 'previous_mode.json': { raw: '"menubar"' }, - 'search_query.json': { raw: '""' } + 'search_query.json': { raw: '""' }, + 'multi_select_enabled.json': { raw: 'false' } } } } From 5299c7a7cd35e4f3238ee89942d037bbc2989552 Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 29 Aug 2025 21:28:40 +0500 Subject: [PATCH 061/130] Add ui-tweaks for mobile devices --- lib/graph_explorer.js | 28 +++++++++++++++++++++++++--- lib/theme.css | 18 ++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 2d7926c..548cde1 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -56,6 +56,7 @@ async function graph_explorer (opts) { const chunk_size = 50 const max_rendered_nodes = chunk_size * 3 let node_height + let scale_factor = 1 // Scale factor for mobile devices const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') @@ -65,6 +66,9 @@ async function graph_explorer (opts) { rootMargin: '500px 0px', threshold: 0 }) + + calculate_mobile_scale() + window.onresize = calculate_mobile_scale // Define handlers for different data types from the drive, called by `onbatch`. const on = { entries: on_entries, @@ -343,10 +347,11 @@ async function graph_explorer (opts) { } /****************************************************************************** - 3. VIEW AND RENDERING LOGIC + 3. VIEW AND RENDERING LOGIC AND SCALING - These functions build the `view` array and render the DOM. - `build_and_render_view` is the main orchestrator. - `build_view_recursive` creates the flat `view` array from the hierarchical data. + - `calculate_mobile_scale` calculates the scale factor for mobile devices. ******************************************************************************/ function build_and_render_view (focal_instance_path, hub_toggle = false) { if (Object.keys(all_entries).length === 0) { @@ -491,6 +496,23 @@ async function graph_explorer (opts) { return current_view } + function calculate_mobile_scale() { + const screen_width = window.innerWidth + const screen_height = window.innerHeight + const is_mobile = screen_width < 768 || screen_height < 600 + + if (is_mobile) { + // Scale proportionally based on screen size + const width_scale = Math.max(1.3, Math.min(2, 768 / screen_width)) + const height_scale = Math.max(1.3, Math.min(2, 600 / screen_height)) + scale_factor = Math.max(width_scale, height_scale) + } else { + scale_factor = 1 + } + + // Initialize the CSS variable + shadow.host.style.setProperty('--scale-factor', scale_factor) + } /****************************************************************************** 4. NODE CREATION AND EVENT HANDLING - `create_node` generates the DOM element for a single node. @@ -549,8 +571,8 @@ async function graph_explorer (opts) { const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 - if (depth) el.style.paddingLeft = '17.5px' - el.style.height = `${node_height}px` + if (depth) el.style.paddingLeft = `${17.5 * scale_factor}px` + el.style.height = `${node_height * scale_factor}px` if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) diff --git a/lib/theme.css b/lib/theme.css index 51e507d..e732ac7 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -1,6 +1,7 @@ .graph-container, .node { font-family: monospace; + font-size: calc(14px * var(--scale-factor, 1)); /* Variable from the `graph_explorer.js` */ } .graph-container { color: #abb2bf; @@ -51,17 +52,19 @@ } .pipe { text-align: center; + font-size: calc(14px * var(--scale-factor, 1)); } .pipe::before { content: '┃'; } .blank { - width: 8.5px; + width: calc(8.5px * var(--scale-factor, 1)); text-align: center; } .clickable { cursor: pointer; } .prefix, .icon { - margin-right: 2px; + margin-right: calc(2px * var(--scale-factor, 1)); + font-size: calc(14px * var(--scale-factor, 1)); } .top-cross::before { content: '┏╋'; } .top-tee-down::before { content: '┏┳'; } @@ -81,8 +84,15 @@ .middle-light-line::before { content: '┠─'; } .tee-down::before { content: '┳'; } .line-h::before { content: '━'; } -.icon { display: inline-block; text-align: center; } -.name { flex-grow: 1; } +.icon { + display: inline-block; + text-align: center; + font-size: calc(16px * var(--scale-factor, 1)); +} +.name { + flex-grow: 1; + font-size: calc(14px * var(--scale-factor, 1)); +} .node.type-root > .icon::before { content: '🌐'; } .node.type-folder > .icon::before { content: '📁'; } .node.type-html-file > .icon::before { content: '📄'; } From 5f828b09d2302298f3aed2a552bc8fa377549ea4 Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 29 Aug 2025 22:47:19 +0500 Subject: [PATCH 062/130] Added Standardx --- lib/graph_explorer.js | 157 ++++++++++++++++++++---------------------- package.json | 21 +++++- web/page.js | 8 +-- 3 files changed, 96 insertions(+), 90 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 548cde1..f6e4bcd 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -33,7 +33,6 @@ async function graph_explorer (opts) { let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 - let spacer_initial_scroll_top = 0 let hub_num = 0 // Counter for expanded hubs. const el = document.createElement('div') @@ -66,7 +65,7 @@ async function graph_explorer (opts) { rootMargin: '500px 0px', threshold: 0 }) - + calculate_mobile_scale() window.onresize = calculate_mobile_scale // Define handlers for different data types from the drive, called by `onbatch`. @@ -156,53 +155,53 @@ async function graph_explorer (opts) { // Handle different runtime state updates based on the path i.e files switch (true) { - case path.endsWith('node_height.json'): - node_height = value - break - case path.endsWith('vertical_scroll_value.json'): - if (typeof value === 'number') vertical_scroll_value = value - break - case path.endsWith('horizontal_scroll_value.json'): - if (typeof value === 'number') horizontal_scroll_value = value - break - case path.endsWith('selected_instance_paths.json'): - selected_instance_paths = process_path_array_update({ - current_paths: selected_instance_paths, - value, - render_set: render_nodes_needed, - name: 'selected_instance_paths' - }) - break - case path.endsWith('confirmed_selected.json'): - confirmed_instance_paths = process_path_array_update({ - current_paths: confirmed_instance_paths, - value, - render_set: render_nodes_needed, - name: 'confirmed_selected' - }) - break - case path.endsWith('instance_states.json'): - if (typeof value === 'object' && value && !Array.isArray(value)) { - instance_states = value - needs_render = true - } else { - console.warn( - 'instance_states is not a valid object, ignoring.', - value - ) - } - break - case path.endsWith('search_entry_states.json'): - if (typeof value === 'object' && value && !Array.isArray(value)) { - search_entry_states = value - if (mode === 'search') needs_render = true - } else { - console.warn( - 'search_entry_states is not a valid object, ignoring.', - value - ) - } - break + case path.endsWith('node_height.json'): + node_height = value + break + case path.endsWith('vertical_scroll_value.json'): + if (typeof value === 'number') vertical_scroll_value = value + break + case path.endsWith('horizontal_scroll_value.json'): + if (typeof value === 'number') horizontal_scroll_value = value + break + case path.endsWith('selected_instance_paths.json'): + selected_instance_paths = process_path_array_update({ + current_paths: selected_instance_paths, + value, + render_set: render_nodes_needed, + name: 'selected_instance_paths' + }) + break + case path.endsWith('confirmed_selected.json'): + confirmed_instance_paths = process_path_array_update({ + current_paths: confirmed_instance_paths, + value, + render_set: render_nodes_needed, + name: 'confirmed_selected' + }) + break + case path.endsWith('instance_states.json'): + if (typeof value === 'object' && value && !Array.isArray(value)) { + instance_states = value + needs_render = true + } else { + console.warn( + 'instance_states is not a valid object, ignoring.', + value + ) + } + break + case path.endsWith('search_entry_states.json'): + if (typeof value === 'object' && value && !Array.isArray(value)) { + search_entry_states = value + if (mode === 'search') needs_render = true + } else { + console.warn( + 'search_entry_states is not a valid object, ignoring.', + value + ) + } + break } }) @@ -342,12 +341,12 @@ async function graph_explorer (opts) { } const pipe_trail = (is_hub && is_last_sub) || (is_hub && final_is_hub_on_top) ? last_pipe : parent_pipe_trail - const product = { pipe_trail, is_hub_on_top: final_is_hub_on_top } + const product = { pipe_trail, is_hub_on_top: final_is_hub_on_top } return product } /****************************************************************************** - 3. VIEW AND RENDERING LOGIC AND SCALING + 3. VIEW AND RENDERING LOGIC AND SCALING - These functions build the `view` array and render the DOM. - `build_and_render_view` is the main orchestrator. - `build_view_recursive` creates the flat `view` array from the hierarchical data. @@ -404,7 +403,7 @@ async function graph_explorer (opts) { vertical_scroll_value = container.scrollTop } - // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. + // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. handle_spacer_element({ hub_toggle, existing_height: existing_spacer_height, @@ -431,7 +430,7 @@ async function graph_explorer (opts) { if (!entry) return [] const state = get_or_create_state(instance_states, instance_path) - + const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ depth, is_hub, @@ -443,7 +442,7 @@ async function graph_explorer (opts) { all_entries }) - let current_view = [] + const current_view = [] // If hubs are expanded, recursively add them to the view first (they appear above the node). if (state.expanded_hubs && Array.isArray(entry.hubs)) { entry.hubs.forEach((hub_path, i, arr) => { @@ -496,11 +495,11 @@ async function graph_explorer (opts) { return current_view } - function calculate_mobile_scale() { + function calculate_mobile_scale () { const screen_width = window.innerWidth const screen_height = window.innerHeight const is_mobile = screen_width < 768 || screen_height < 600 - + if (is_mobile) { // Scale proportionally based on screen size const width_scale = Math.max(1.3, Math.min(2, 768 / screen_width)) @@ -509,7 +508,7 @@ async function graph_explorer (opts) { } else { scale_factor = 1 } - + // Initialize the CSS variable shadow.host.style.setProperty('--scale-factor', scale_factor) } @@ -594,15 +593,15 @@ async function graph_explorer (opts) { const icon_el = el.querySelector('.icon') if (icon_el && has_hubs && base_path !== '/') { - icon_el.onclick = mode === 'search' - ? () => toggle_search_hubs(instance_path) + icon_el.onclick = mode === 'search' + ? () => toggle_search_hubs(instance_path) : () => toggle_hubs(instance_path) } const prefix_el = el.querySelector('.prefix') if (prefix_el && has_subs) { - prefix_el.onclick = mode === 'search' - ? () => toggle_search_subs(instance_path) + prefix_el.onclick = mode === 'search' + ? () => toggle_search_subs(instance_path) : () => toggle_subs(instance_path) } @@ -646,15 +645,13 @@ async function graph_explorer (opts) { } else if (is_last_sub) { if (expanded_subs && expanded_hubs) return 'bottom-cross' if (expanded_subs) return 'bottom-tee-down' - if (expanded_hubs) - return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' + if (expanded_hubs) { return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' } return has_subs ? 'bottom-line' : 'bottom-light-line' } else { if (expanded_subs && expanded_hubs) return 'middle-cross' if (expanded_subs) return 'middle-tee-down' - if (expanded_hubs) - return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' - return has_subs ? 'middle-line' : 'middle-light-line' + if (expanded_hubs) { return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' } + return has_subs ? 'middle-line' : 'middle-light-line' } } @@ -1017,7 +1014,6 @@ async function graph_explorer (opts) { spacer_element.remove() spacer_element = null spacer_initial_height = 0 - spacer_initial_scroll_top = 0 hub_num = 0 } @@ -1066,8 +1062,7 @@ async function graph_explorer (opts) { if (end_index >= view.length) return const fragment = document.createDocumentFragment() const next_end = Math.min(view.length, end_index + chunk_size) - for (let i = end_index; i < next_end; i++) - if (view[i]) fragment.appendChild(create_node(view[i])) + for (let i = end_index; i < next_end; i++) { if (view[i]) fragment.appendChild(create_node(view[i])) } container.insertBefore(fragment, bottom_sentinel) end_index = next_end bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` @@ -1109,24 +1104,24 @@ async function graph_explorer (opts) { /****************************************************************************** 8. HELPER FUNCTIONS ******************************************************************************/ -function get_highlighted_name (name, query) { + function get_highlighted_name (name, query) { // Creates a new regular expression. // `escape_regex(query)` sanitizes the query string to treat special regex characters literally. // `(...)` creates a capturing group for the escaped query. // 'gi' flags: 'g' for global (all occurrences), 'i' for case-insensitive. - const regex = new RegExp(`(${escape_regex(query)})`, 'gi') - // Replaces all matches of the regex in 'name' with the matched text wrapped in tags. - // '$1' refers to the content of the first capturing group (the matched query). - return name.replace(regex, '$1') -} + const regex = new RegExp(`(${escape_regex(query)})`, 'gi') + // Replaces all matches of the regex in 'name' with the matched text wrapped in tags. + // '$1' refers to the content of the first capturing group (the matched query). + return name.replace(regex, '$1') + } -function escape_regex (string) { + function escape_regex (string) { // Escapes special regular expression characters in a string. // It replaces characters like -, /, \, ^, $, *, +, ?, ., (, ), |, [, ], {, } // with their escaped versions (e.g., '.' becomes '\.'). // This prevents them from being interpreted as regex metacharacters. - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char -} + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char + } function check_and_reset_feedback_flags () { if (drive_updated_by_scroll) { @@ -1200,7 +1195,6 @@ function escape_regex (string) { if (new_scroll_top > max_scroll_top) { spacer_initial_height = new_scroll_top - max_scroll_top - spacer_initial_scroll_top = new_scroll_top spacer_element.style.height = `${spacer_initial_height}px` } sync_fn() @@ -1212,7 +1206,6 @@ function escape_regex (string) { } else { spacer_element = null spacer_initial_height = 0 - spacer_initial_scroll_top = 0 requestAnimationFrame(sync_fn) } } @@ -1284,7 +1277,7 @@ function fallback_module () { }, 'style/': { 'theme.css': { - '$ref' : 'theme.css' + $ref: 'theme.css' } }, 'runtime/': { @@ -1305,4 +1298,4 @@ function fallback_module () { } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index d7f8a31..7d91990 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,28 @@ "start": "budo web/boot.js:bundle.js --open --live", "build": "browserify web/boot.js -o bundle.js", "start:act": "budo web/boot.js:bundle.js --dir ./ --live --open", - "build:act": "browserify web/boot.js > bundle.js" + "build:act": "browserify web/boot.js > bundle.js", + "lint": "standardx", + "lint:fix": "standardx --fix" }, "devDependencies": { "browserify": "^17.0.1", - "budo": "^11.8.4" + "budo": "^11.8.4", + "standardx": "^7.0.0" + }, + "eslintConfig": { + "rules": { + "camelcase": 0, + "indent": [ + "error", + 2 + ] + }, + "ignorePatterns": [ + "lib/STATE.js", + "bundle.js", + "node_modules/" + ] }, "repository": { "type": "git", diff --git a/web/page.js b/web/page.js index b1645b4..ef16bda 100644 --- a/web/page.js +++ b/web/page.js @@ -10,8 +10,6 @@ const sheet = new CSSStyleSheet() config().then(() => boot({ sid: '' })) async function config () { - const path = path => - new URL(`../src/node_modules/${path}`, `file://${__dirname}`).href.slice(8) const html = document.documentElement const meta = document.createElement('meta') const font = @@ -50,10 +48,8 @@ async function boot (opts) { // ---------------------------------------- // ELEMENTS // ---------------------------------------- - { - // desktop - shadow.append(await app(subs[0])) - } + // desktop + shadow.append(await app(subs[0])) // ---------------------------------------- // INIT // ---------------------------------------- From f5a4576b1c91b3c93ec06f7b43b39aafc7fc59f9 Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 29 Aug 2025 23:36:12 +0500 Subject: [PATCH 063/130] Implemented `on[type](params)` --- lib/graph_explorer.js | 207 ++++++++++++++++++++++++++++-------------- 1 file changed, 137 insertions(+), 70 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index f6e4bcd..744fd4b 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -145,6 +145,15 @@ async function graph_explorer (opts) { } function on_runtime ({ data, paths }) { + const on_runtime_paths = { + 'node_height.json': handle_node_height, + 'vertical_scroll_value.json': handle_vertical_scroll, + 'horizontal_scroll_value.json': handle_horizontal_scroll, + 'selected_instance_paths.json': handle_selected_paths, + 'confirmed_selected.json': handle_confirmed_paths, + 'instance_states.json': handle_instance_states, + 'search_entry_states.json': handle_search_entry_states + } let needs_render = false const render_nodes_needed = new Set() @@ -153,55 +162,12 @@ async function graph_explorer (opts) { const value = parse_json_data(data[i], path) if (value === null) return - // Handle different runtime state updates based on the path i.e files - switch (true) { - case path.endsWith('node_height.json'): - node_height = value - break - case path.endsWith('vertical_scroll_value.json'): - if (typeof value === 'number') vertical_scroll_value = value - break - case path.endsWith('horizontal_scroll_value.json'): - if (typeof value === 'number') horizontal_scroll_value = value - break - case path.endsWith('selected_instance_paths.json'): - selected_instance_paths = process_path_array_update({ - current_paths: selected_instance_paths, - value, - render_set: render_nodes_needed, - name: 'selected_instance_paths' - }) - break - case path.endsWith('confirmed_selected.json'): - confirmed_instance_paths = process_path_array_update({ - current_paths: confirmed_instance_paths, - value, - render_set: render_nodes_needed, - name: 'confirmed_selected' - }) - break - case path.endsWith('instance_states.json'): - if (typeof value === 'object' && value && !Array.isArray(value)) { - instance_states = value - needs_render = true - } else { - console.warn( - 'instance_states is not a valid object, ignoring.', - value - ) - } - break - case path.endsWith('search_entry_states.json'): - if (typeof value === 'object' && value && !Array.isArray(value)) { - search_entry_states = value - if (mode === 'search') needs_render = true - } else { - console.warn( - 'search_entry_states is not a valid object, ignoring.', - value - ) - } - break + // Extract filename from path and use handler if available + const filename = path.split('/').pop() + const handler = on_runtime_paths[filename] + if (handler) { + const result = handler({ value, render_nodes_needed }) + if (result?.needs_render) needs_render = true } }) @@ -209,19 +175,78 @@ async function graph_explorer (opts) { else if (render_nodes_needed.size > 0) { render_nodes_needed.forEach(re_render_node) } + + function handle_node_height ({ value }) { + node_height = value + } + + function handle_vertical_scroll ({ value }) { + if (typeof value === 'number') vertical_scroll_value = value + } + + function handle_horizontal_scroll ({ value }) { + if (typeof value === 'number') horizontal_scroll_value = value + } + + function handle_selected_paths ({ value, render_nodes_needed }) { + selected_instance_paths = process_path_array_update({ + current_paths: selected_instance_paths, + value, + render_set: render_nodes_needed, + name: 'selected_instance_paths' + }) + } + + function handle_confirmed_paths ({ value, render_nodes_needed }) { + confirmed_instance_paths = process_path_array_update({ + current_paths: confirmed_instance_paths, + value, + render_set: render_nodes_needed, + name: 'confirmed_selected' + }) + } + + function handle_instance_states ({ value }) { + if (typeof value === 'object' && value && !Array.isArray(value)) { + instance_states = value + return { needs_render: true } + } else { + console.warn('instance_states is not a valid object, ignoring.', value) + } + } + + function handle_search_entry_states ({ value }) { + if (typeof value === 'object' && value && !Array.isArray(value)) { + search_entry_states = value + if (mode === 'search') return { needs_render: true } + } else { + console.warn('search_entry_states is not a valid object, ignoring.', value) + } + } } function on_mode ({ data, paths }) { + const on_mode_paths = { + 'current_mode.json': handle_current_mode, + 'previous_mode.json': handle_previous_mode, + 'search_query.json': handle_search_query, + 'multi_select_enabled.json': handle_multi_select_enabled + } let new_current_mode, new_previous_mode, new_search_query, new_multi_select_enabled paths.forEach((path, i) => { const value = parse_json_data(data[i], path) if (value === null) return - if (path.endsWith('current_mode.json')) new_current_mode = value - else if (path.endsWith('previous_mode.json')) new_previous_mode = value - else if (path.endsWith('search_query.json')) new_search_query = value - else if (path.endsWith('multi_select_enabled.json')) new_multi_select_enabled = value + const filename = path.split('/').pop() + const handler = on_mode_paths[filename] + if (handler) { + const result = handler({ value }) + if (result?.current_mode !== undefined) new_current_mode = result.current_mode + if (result?.previous_mode !== undefined) new_previous_mode = result.previous_mode + if (result?.search_query !== undefined) new_search_query = result.search_query + if (result?.multi_select_enabled !== undefined) new_multi_select_enabled = result.multi_select_enabled + } }) if (typeof new_search_query === 'string') search_query = new_search_query @@ -249,6 +274,22 @@ async function graph_explorer (opts) { render_menubar() handle_mode_change() if (mode === 'search' && search_query) perform_search(search_query) + + function handle_current_mode ({ value }) { + return { current_mode: value } + } + + function handle_previous_mode ({ value }) { + return { previous_mode: value } + } + + function handle_search_query ({ value }) { + return { search_query: value } + } + + function handle_multi_select_enabled ({ value }) { + return { multi_select_enabled: value } + } } function inject_style ({ data }) { @@ -629,28 +670,54 @@ async function graph_explorer (opts) { console.error('get_prefix called with invalid state.') return 'middle-line' } - const { expanded_subs, expanded_hubs } = state - if (is_hub) { - if (is_hub_on_top) { - if (expanded_subs && expanded_hubs) return 'top-cross' - if (expanded_subs) return 'top-tee-down' - if (expanded_hubs) return 'top-tee-up' - return 'top-line' - } else { - if (expanded_subs && expanded_hubs) return 'middle-cross' - if (expanded_subs) return 'middle-tee-down' - if (expanded_hubs) return 'middle-tee-up' - return 'middle-line' - } - } else if (is_last_sub) { + + // Define handlers for different prefix types based on node position + const on_prefix_types = { + hub_on_top: get_hub_on_top_prefix, + hub_not_on_top: get_hub_not_on_top_prefix, + last_sub: get_last_sub_prefix, + middle_sub: get_middle_sub_prefix + } + // Determine the prefix type based on node position + let prefix_type + if (is_hub && is_hub_on_top) prefix_type = 'hub_on_top' + else if (is_hub && !is_hub_on_top) prefix_type = 'hub_not_on_top' + else if (is_last_sub) prefix_type = 'last_sub' + else prefix_type = 'middle_sub' + + const handler = on_prefix_types[prefix_type] + + return handler ? handler({ state, has_subs }) : 'middle-line' + + function get_hub_on_top_prefix ({ state }) { + const { expanded_subs, expanded_hubs } = state + if (expanded_subs && expanded_hubs) return 'top-cross' + if (expanded_subs) return 'top-tee-down' + if (expanded_hubs) return 'top-tee-up' + return 'top-line' + } + + function get_hub_not_on_top_prefix ({ state }) { + const { expanded_subs, expanded_hubs } = state + if (expanded_subs && expanded_hubs) return 'middle-cross' + if (expanded_subs) return 'middle-tee-down' + if (expanded_hubs) return 'middle-tee-up' + return 'middle-line' + } + + function get_last_sub_prefix ({ state, has_subs }) { + const { expanded_subs, expanded_hubs } = state if (expanded_subs && expanded_hubs) return 'bottom-cross' if (expanded_subs) return 'bottom-tee-down' - if (expanded_hubs) { return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' } + if (expanded_hubs) return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' return has_subs ? 'bottom-line' : 'bottom-light-line' - } else { + } + + function get_middle_sub_prefix ({ state, has_subs }) { + const { expanded_subs, expanded_hubs } = state if (expanded_subs && expanded_hubs) return 'middle-cross' if (expanded_subs) return 'middle-tee-down' - if (expanded_hubs) { return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' } + if (expanded_hubs) return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' return has_subs ? 'middle-line' : 'middle-light-line' } } From 506f936140b257f1ff610be38b26c15192230e37 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 30 Aug 2025 00:19:59 +0500 Subject: [PATCH 064/130] Jump to already opened hubs of entries --- lib/graph_explorer.js | 72 ++++++++++++++++++++++++++++++++++++++++--- lib/theme.css | 15 ++++++++- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 744fd4b..9e9b660 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -625,11 +625,24 @@ async function graph_explorer (opts) { ? get_highlighted_name(entry_name, query) : entry_name + // Check if hub is alrady expanded elsewhere + const existing_expanded_instance = has_hubs && !state.expanded_hubs ? find_expanded_hub_instance(base_path, instance_path) : null + // Don't show `jump` button if entry is a expnded hub + const is_this_an_expanded_hub = is_hub && view.some(node => { + const node_state = instance_states[node.instance_path] + if (!node_state || !node_state.expanded_hubs) return false + const node_entry = all_entries[node.base_path] + return node_entry && Array.isArray(node_entry.hubs) && node_entry.hubs.includes(base_path) + }) + const is_duplicate_hub = existing_expanded_instance !== null && is_this_an_expanded_hub + const navigate_button_html = is_duplicate_hub ? '^' : '' + el.innerHTML = ` ${pipe_html} ${name_html} + ${navigate_button_html} ` const icon_el = el.querySelector('.icon') @@ -639,6 +652,9 @@ async function graph_explorer (opts) { : () => toggle_hubs(instance_path) } + const navigate_el = el.querySelector('.navigate-to-hub') + if (navigate_el) navigate_el.onclick = () => scroll_to_and_highlight_hub(base_path) + const prefix_el = el.querySelector('.prefix') if (prefix_el && has_subs) { prefix_el.onclick = mode === 'search' @@ -648,9 +664,7 @@ async function graph_explorer (opts) { el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) - if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { - el.appendChild(create_confirm_checkbox(instance_path)) - } + if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) return el } @@ -1169,7 +1183,55 @@ async function graph_explorer (opts) { } /****************************************************************************** - 8. HELPER FUNCTIONS + 8. HUB DUPLICATION PREVENTION + ******************************************************************************/ + function find_expanded_hub_instance (target_base_path, exclude_instance_path) { + // Look through all nodes in the current view to find expanded hubs + for (const node of view) { + if (node.instance_path === exclude_instance_path) continue + + const state = instance_states[node.instance_path] + if (!state || !state.expanded_hubs) continue + + const entry = all_entries[node.base_path] + if (!entry || !Array.isArray(entry.hubs)) continue + + if (entry.hubs.includes(target_base_path)) return node.instance_path + } + return null + } + + function scroll_to_and_highlight_hub (target_base_path) { + // Find the hub entry with the same base_path in the current view + const hub_index = view.findIndex(n => n.base_path === target_base_path) + if (hub_index === -1) return + + // Calculate scroll position + const target_scroll_top = hub_index * node_height + container.scrollTop = target_scroll_top + + // Find and highlight the DOM element + const hub_instance_path = view[hub_index].instance_path + const hub_element = shadow.querySelector(`[data-instance_path="${CSS.escape(hub_instance_path)}"]`) + if (hub_element) { + hub_element.style.backgroundColor = 'pink' + hub_element.style.transition = 'background-color 0.3s ease' + // remove highlight after 2 seconds + setTimeout(() => { + hub_element.style.backgroundColor = '' + setTimeout(() => { + hub_element.style.transition = '' + }, 300) + }, 2000) + } + } + + function is_hub_already_expanded (base_path, exclude_instance_path) { + return find_expanded_hub_instance(base_path, exclude_instance_path) !== null + } + + /****************************************************************************** + 9. HELPER FUNCTIONS ******************************************************************************/ function get_highlighted_name (name, query) { // Creates a new regular expression. @@ -1326,7 +1388,7 @@ async function graph_explorer (opts) { } /****************************************************************************** - 9. FALLBACK CONFIGURATION + 10. FALLBACK CONFIGURATION - This provides the default data and API configuration for the component, following the pattern described in `instructions.md`. - It defines the default datasets (`entries`, `style`, `runtime`) and their diff --git a/lib/theme.css b/lib/theme.css index e732ac7..af0756a 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -99,4 +99,17 @@ .node.type-js-file > .icon::before { content: '📜'; } .node.type-css-file > .icon::before { content: '🎨'; } .node.type-json-file > .icon::before { content: '📝'; } -.node.type-file > .icon::before { content: '📄'; } \ No newline at end of file +.node.type-file > .icon::before { content: '📄'; } +.navigate-to-hub { + margin-left: calc(5px * var(--scale-factor, 1)); + padding: calc(2px * var(--scale-factor, 1)) calc(4px * var(--scale-factor, 1)); + background-color: #61afef; + color: #282c34; + border-radius: calc(3px * var(--scale-factor, 1)); + font-size: calc(12px * var(--scale-factor, 1)); + font-weight: bold; + transition: background-color 0.2s ease; +} +.navigate-to-hub:hover { + background-color: #528bcc; +} \ No newline at end of file From 7dc1d7421e62877887a3b8176c216d14fd1ec86e Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 30 Aug 2025 00:28:36 +0500 Subject: [PATCH 065/130] bundled --- bundle.js | 460 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 326 insertions(+), 134 deletions(-) diff --git a/bundle.js b/bundle.js index 5f77ee7..7090a37 100644 --- a/bundle.js +++ b/bundle.js @@ -28,15 +28,15 @@ async function graph_explorer (opts) { let search_entry_states = {} // Holds expansion state for search mode interactions separately let view = [] // A flat array representing the visible nodes in the graph. let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. - let previous_mode = 'menubar' + let previous_mode let search_query = '' let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. + let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 - let spacer_initial_scroll_top = 0 let hub_num = 0 // Counter for expanded hubs. const el = document.createElement('div') @@ -59,6 +59,7 @@ async function graph_explorer (opts) { const chunk_size = 50 const max_rendered_nodes = chunk_size * 3 let node_height + let scale_factor = 1 // Scale factor for mobile devices const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') @@ -68,6 +69,9 @@ async function graph_explorer (opts) { rootMargin: '500px 0px', threshold: 0 }) + + calculate_mobile_scale() + window.onresize = calculate_mobile_scale // Define handlers for different data types from the drive, called by `onbatch`. const on = { entries: on_entries, @@ -145,6 +149,15 @@ async function graph_explorer (opts) { } function on_runtime ({ data, paths }) { + const on_runtime_paths = { + 'node_height.json': handle_node_height, + 'vertical_scroll_value.json': handle_vertical_scroll, + 'horizontal_scroll_value.json': handle_horizontal_scroll, + 'selected_instance_paths.json': handle_selected_paths, + 'confirmed_selected.json': handle_confirmed_paths, + 'instance_states.json': handle_instance_states, + 'search_entry_states.json': handle_search_entry_states + } let needs_render = false const render_nodes_needed = new Set() @@ -153,55 +166,12 @@ async function graph_explorer (opts) { const value = parse_json_data(data[i], path) if (value === null) return - // Handle different runtime state updates based on the path i.e files - switch (true) { - case path.endsWith('node_height.json'): - node_height = value - break - case path.endsWith('vertical_scroll_value.json'): - if (typeof value === 'number') vertical_scroll_value = value - break - case path.endsWith('horizontal_scroll_value.json'): - if (typeof value === 'number') horizontal_scroll_value = value - break - case path.endsWith('selected_instance_paths.json'): - selected_instance_paths = process_path_array_update({ - current_paths: selected_instance_paths, - value, - render_set: render_nodes_needed, - name: 'selected_instance_paths' - }) - break - case path.endsWith('confirmed_selected.json'): - confirmed_instance_paths = process_path_array_update({ - current_paths: confirmed_instance_paths, - value, - render_set: render_nodes_needed, - name: 'confirmed_selected' - }) - break - case path.endsWith('instance_states.json'): - if (typeof value === 'object' && value && !Array.isArray(value)) { - instance_states = value - needs_render = true - } else { - console.warn( - 'instance_states is not a valid object, ignoring.', - value - ) - } - break - case path.endsWith('search_entry_states.json'): - if (typeof value === 'object' && value && !Array.isArray(value)) { - search_entry_states = value - if (mode === 'search') needs_render = true - } else { - console.warn( - 'search_entry_states is not a valid object, ignoring.', - value - ) - } - break + // Extract filename from path and use handler if available + const filename = path.split('/').pop() + const handler = on_runtime_paths[filename] + if (handler) { + const result = handler({ value, render_nodes_needed }) + if (result?.needs_render) needs_render = true } }) @@ -209,30 +179,93 @@ async function graph_explorer (opts) { else if (render_nodes_needed.size > 0) { render_nodes_needed.forEach(re_render_node) } + + function handle_node_height ({ value }) { + node_height = value + } + + function handle_vertical_scroll ({ value }) { + if (typeof value === 'number') vertical_scroll_value = value + } + + function handle_horizontal_scroll ({ value }) { + if (typeof value === 'number') horizontal_scroll_value = value + } + + function handle_selected_paths ({ value, render_nodes_needed }) { + selected_instance_paths = process_path_array_update({ + current_paths: selected_instance_paths, + value, + render_set: render_nodes_needed, + name: 'selected_instance_paths' + }) + } + + function handle_confirmed_paths ({ value, render_nodes_needed }) { + confirmed_instance_paths = process_path_array_update({ + current_paths: confirmed_instance_paths, + value, + render_set: render_nodes_needed, + name: 'confirmed_selected' + }) + } + + function handle_instance_states ({ value }) { + if (typeof value === 'object' && value && !Array.isArray(value)) { + instance_states = value + return { needs_render: true } + } else { + console.warn('instance_states is not a valid object, ignoring.', value) + } + } + + function handle_search_entry_states ({ value }) { + if (typeof value === 'object' && value && !Array.isArray(value)) { + search_entry_states = value + if (mode === 'search') return { needs_render: true } + } else { + console.warn('search_entry_states is not a valid object, ignoring.', value) + } + } } function on_mode ({ data, paths }) { - let new_current_mode, new_previous_mode, new_search_query + const on_mode_paths = { + 'current_mode.json': handle_current_mode, + 'previous_mode.json': handle_previous_mode, + 'search_query.json': handle_search_query, + 'multi_select_enabled.json': handle_multi_select_enabled + } + let new_current_mode, new_previous_mode, new_search_query, new_multi_select_enabled paths.forEach((path, i) => { const value = parse_json_data(data[i], path) if (value === null) return - if (path.endsWith('current_mode.json')) new_current_mode = value - else if (path.endsWith('previous_mode.json')) new_previous_mode = value - else if (path.endsWith('search_query.json')) new_search_query = value + const filename = path.split('/').pop() + const handler = on_mode_paths[filename] + if (handler) { + const result = handler({ value }) + if (result?.current_mode !== undefined) new_current_mode = result.current_mode + if (result?.previous_mode !== undefined) new_previous_mode = result.previous_mode + if (result?.search_query !== undefined) new_search_query = result.search_query + if (result?.multi_select_enabled !== undefined) new_multi_select_enabled = result.multi_select_enabled + } }) if (typeof new_search_query === 'string') search_query = new_search_query if (new_previous_mode) previous_mode = new_previous_mode + if (typeof new_multi_select_enabled === 'boolean') { + multi_select_enabled = new_multi_select_enabled + render_menubar() // Re-render menubar to update button text + } if ( new_current_mode && !['default', 'menubar', 'search'].includes(new_current_mode) ) { - return void console.warn( - `Invalid mode "${new_current_mode}" provided. Ignoring update.` - ) + console.warn(`Invalid mode "${new_current_mode}" provided. Ignoring update.`) + return } if (new_current_mode === 'search' && !search_query) { @@ -245,6 +278,22 @@ async function graph_explorer (opts) { render_menubar() handle_mode_change() if (mode === 'search' && search_query) perform_search(search_query) + + function handle_current_mode ({ value }) { + return { current_mode: value } + } + + function handle_previous_mode ({ value }) { + return { previous_mode: value } + } + + function handle_search_query ({ value }) { + return { search_query: value } + } + + function handle_multi_select_enabled ({ value }) { + return { multi_select_enabled: value } + } } function inject_style ({ data }) { @@ -295,10 +344,6 @@ async function graph_explorer (opts) { if (is_first_hub) { children_pipe_trail.pop() children_pipe_trail.push(false) - if (mode === 'search') { - children_pipe_trail.pop() - children_pipe_trail.push(is_last_sub) - } } } children_pipe_trail.push(is_hub || !is_last_sub) @@ -341,18 +386,22 @@ async function graph_explorer (opts) { } const pipe_trail = (is_hub && is_last_sub) || (is_hub && final_is_hub_on_top) ? last_pipe : parent_pipe_trail - const product = { pipe_trail, is_hub_on_top: final_is_hub_on_top } + const product = { pipe_trail, is_hub_on_top: final_is_hub_on_top } return product } /****************************************************************************** - 3. VIEW AND RENDERING LOGIC + 3. VIEW AND RENDERING LOGIC AND SCALING - These functions build the `view` array and render the DOM. - `build_and_render_view` is the main orchestrator. - `build_view_recursive` creates the flat `view` array from the hierarchical data. + - `calculate_mobile_scale` calculates the scale factor for mobile devices. ******************************************************************************/ function build_and_render_view (focal_instance_path, hub_toggle = false) { - if (Object.keys(all_entries).length === 0) return void console.warn('No entries available to render.') + if (Object.keys(all_entries).length === 0) { + console.warn('No entries available to render.') + return + } const old_view = [...view] const old_scroll_top = vertical_scroll_value @@ -399,7 +448,7 @@ async function graph_explorer (opts) { vertical_scroll_value = container.scrollTop } - // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. + // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. handle_spacer_element({ hub_toggle, existing_height: existing_spacer_height, @@ -426,7 +475,7 @@ async function graph_explorer (opts) { if (!entry) return [] const state = get_or_create_state(instance_states, instance_path) - + const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ depth, is_hub, @@ -438,7 +487,7 @@ async function graph_explorer (opts) { all_entries }) - let current_view = [] + const current_view = [] // If hubs are expanded, recursively add them to the view first (they appear above the node). if (state.expanded_hubs && Array.isArray(entry.hubs)) { entry.hubs.forEach((hub_path, i, arr) => { @@ -491,6 +540,23 @@ async function graph_explorer (opts) { return current_view } + function calculate_mobile_scale () { + const screen_width = window.innerWidth + const screen_height = window.innerHeight + const is_mobile = screen_width < 768 || screen_height < 600 + + if (is_mobile) { + // Scale proportionally based on screen size + const width_scale = Math.max(1.3, Math.min(2, 768 / screen_width)) + const height_scale = Math.max(1.3, Math.min(2, 600 / screen_height)) + scale_factor = Math.max(width_scale, height_scale) + } else { + scale_factor = 1 + } + + // Initialize the CSS variable + shadow.host.style.setProperty('--scale-factor', scale_factor) + } /****************************************************************************** 4. NODE CREATION AND EVENT HANDLING - `create_node` generates the DOM element for a single node. @@ -549,8 +615,8 @@ async function graph_explorer (opts) { const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 - if (depth) el.style.paddingLeft = '17.5px' - el.style.height = `${node_height}px` + if (depth) el.style.paddingLeft = `${17.5 * scale_factor}px` + el.style.height = `${node_height * scale_factor}px` if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) @@ -563,32 +629,46 @@ async function graph_explorer (opts) { ? get_highlighted_name(entry_name, query) : entry_name + // Check if hub is alrady expanded elsewhere + const existing_expanded_instance = has_hubs && !state.expanded_hubs ? find_expanded_hub_instance(base_path, instance_path) : null + // Don't show `jump` button if entry is a expnded hub + const is_this_an_expanded_hub = is_hub && view.some(node => { + const node_state = instance_states[node.instance_path] + if (!node_state || !node_state.expanded_hubs) return false + const node_entry = all_entries[node.base_path] + return node_entry && Array.isArray(node_entry.hubs) && node_entry.hubs.includes(base_path) + }) + const is_duplicate_hub = existing_expanded_instance !== null && is_this_an_expanded_hub + const navigate_button_html = is_duplicate_hub ? '^' : '' + el.innerHTML = ` ${pipe_html} ${name_html} + ${navigate_button_html} ` const icon_el = el.querySelector('.icon') if (icon_el && has_hubs && base_path !== '/') { - icon_el.onclick = mode === 'search' - ? () => toggle_search_hubs(instance_path) + icon_el.onclick = mode === 'search' + ? () => toggle_search_hubs(instance_path) : () => toggle_hubs(instance_path) } + const navigate_el = el.querySelector('.navigate-to-hub') + if (navigate_el) navigate_el.onclick = () => scroll_to_and_highlight_hub(base_path) + const prefix_el = el.querySelector('.prefix') if (prefix_el && has_subs) { - prefix_el.onclick = mode === 'search' - ? () => toggle_search_subs(instance_path) + prefix_el.onclick = mode === 'search' + ? () => toggle_search_subs(instance_path) : () => toggle_subs(instance_path) } - el.querySelector('.name').onclick = ev => select_node(ev, instance_path) + el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) - if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) { - el.appendChild(create_confirm_checkbox(instance_path)) - } + if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) return el } @@ -608,31 +688,55 @@ async function graph_explorer (opts) { console.error('get_prefix called with invalid state.') return 'middle-line' } - const { expanded_subs, expanded_hubs } = state - if (is_hub) { - if (is_hub_on_top) { - if (expanded_subs && expanded_hubs) return 'top-cross' - if (expanded_subs) return 'top-tee-down' - if (expanded_hubs) return 'top-tee-up' - return 'top-line' - } else { - if (expanded_subs && expanded_hubs) return 'middle-cross' - if (expanded_subs) return 'middle-tee-down' - if (expanded_hubs) return 'middle-tee-up' - return 'middle-line' - } - } else if (is_last_sub) { + + // Define handlers for different prefix types based on node position + const on_prefix_types = { + hub_on_top: get_hub_on_top_prefix, + hub_not_on_top: get_hub_not_on_top_prefix, + last_sub: get_last_sub_prefix, + middle_sub: get_middle_sub_prefix + } + // Determine the prefix type based on node position + let prefix_type + if (is_hub && is_hub_on_top) prefix_type = 'hub_on_top' + else if (is_hub && !is_hub_on_top) prefix_type = 'hub_not_on_top' + else if (is_last_sub) prefix_type = 'last_sub' + else prefix_type = 'middle_sub' + + const handler = on_prefix_types[prefix_type] + + return handler ? handler({ state, has_subs }) : 'middle-line' + + function get_hub_on_top_prefix ({ state }) { + const { expanded_subs, expanded_hubs } = state + if (expanded_subs && expanded_hubs) return 'top-cross' + if (expanded_subs) return 'top-tee-down' + if (expanded_hubs) return 'top-tee-up' + return 'top-line' + } + + function get_hub_not_on_top_prefix ({ state }) { + const { expanded_subs, expanded_hubs } = state + if (expanded_subs && expanded_hubs) return 'middle-cross' + if (expanded_subs) return 'middle-tee-down' + if (expanded_hubs) return 'middle-tee-up' + return 'middle-line' + } + + function get_last_sub_prefix ({ state, has_subs }) { + const { expanded_subs, expanded_hubs } = state if (expanded_subs && expanded_hubs) return 'bottom-cross' if (expanded_subs) return 'bottom-tee-down' - if (expanded_hubs) - return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' + if (expanded_hubs) return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' return has_subs ? 'bottom-line' : 'bottom-light-line' - } else { + } + + function get_middle_sub_prefix ({ state, has_subs }) { + const { expanded_subs, expanded_hubs } = state if (expanded_subs && expanded_hubs) return 'middle-cross' if (expanded_subs) return 'middle-tee-down' - if (expanded_hubs) - return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' - return has_subs ? 'middle-line' : 'middle-light-line' + if (expanded_hubs) return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' + return has_subs ? 'middle-line' : 'middle-light-line' } } @@ -645,7 +749,11 @@ async function graph_explorer (opts) { onclick: toggle_search_mode }) - if (mode !== 'search') return void menubar.replaceChildren(search_button) + const multi_select_button = document.createElement('button') + multi_select_button.innerHTML = `Multi Select: ${multi_select_enabled ? 'true' : 'false'}` + multi_select_button.onclick = mode === 'search' ? null : toggle_multi_select + + if (mode !== 'search') return menubar.replaceChildren(search_button, multi_select_button) const search_input = Object.assign(document.createElement('input'), { type: 'text', @@ -655,7 +763,7 @@ async function graph_explorer (opts) { oninput: on_search_input }) - menubar.replaceChildren(search_button, search_input) + menubar.replaceChildren(search_button, multi_select_button, search_input) requestAnimationFrame(() => search_input.focus()) } @@ -674,6 +782,12 @@ async function graph_explorer (opts) { search_state_instances = instance_states } + function toggle_multi_select () { + multi_select_enabled = !multi_select_enabled + update_drive_state({ dataset: 'mode', name: 'multi_select_enabled', value: multi_select_enabled }) + render_menubar() // Re-render to update button text + } + function on_search_input (event) { search_query = event.target.value.trim() drive_updated_by_search = true @@ -761,7 +875,7 @@ async function graph_explorer (opts) { depth: depth + 1, is_last_sub: i === arr.length - 1, is_hub: true, - is_first_hub: i === 0, + is_first_hub: is_hub_on_top, parent_pipe_trail: children_pipe_trail, instance_states, all_entries, @@ -860,7 +974,7 @@ async function graph_explorer (opts) { ******************************************************************************/ function select_node (ev, instance_path) { const new_selected = new Set(selected_instance_paths) - if (ev.ctrlKey) { + if (ev.ctrlKey || multi_select_enabled) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) } else { @@ -868,6 +982,36 @@ async function graph_explorer (opts) { } } + // Add the clicked entry and all its parents in the default tree + function search_expand_into_default (target_instance_path) { + if (!target_instance_path) return + const parts = target_instance_path.split('|').filter(Boolean) + if (parts.length === 0) return + + const root_state = get_or_create_state(instance_states, '|/') + root_state.expanded_subs = true + + // Walk from root to target, expanding the path relative to alredy expanded entries + for (let i = 0; i < parts.length - 1; i++) { + const parent_base = parts[i] + const child_base = parts[i + 1] + const parent_instance_path = parts.slice(0, i + 1).map(p => '|' + p).join('') + const parent_state = get_or_create_state(instance_states, parent_instance_path) + const parent_entry = all_entries[parent_base] + if (!parent_entry) continue + if (Array.isArray(parent_entry.subs) && parent_entry.subs.includes(child_base)) parent_state.expanded_subs = true + if (Array.isArray(parent_entry.hubs) && parent_entry.hubs.includes(child_base)) parent_state.expanded_hubs = true + } + + // Persist selection and expansion state + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [target_instance_path] }) + drive_updated_by_toggle = true + update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) + search_query = '' + update_drive_state({ dataset: 'mode', name: 'query', value: '' }) + update_drive_state({ dataset: 'mode', name: 'current_mode', value: previous_mode }) + } + function handle_confirm (ev, instance_path) { if (!ev.target) return const is_checked = ev.target.checked @@ -921,6 +1065,14 @@ async function graph_explorer (opts) { } function reset () { + // reset all of the manual expansions made + if (mode === 'search') { + search_entry_states = {} + drive_updated_by_toggle = true + update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + perform_search(search_query) + return + } const root_instance_path = '|/' const new_instance_states = { [root_instance_path]: { expanded_subs: true, expanded_hubs: false } @@ -947,7 +1099,6 @@ async function graph_explorer (opts) { spacer_element.remove() spacer_element = null spacer_initial_height = 0 - spacer_initial_scroll_top = 0 hub_num = 0 } @@ -996,8 +1147,7 @@ async function graph_explorer (opts) { if (end_index >= view.length) return const fragment = document.createDocumentFragment() const next_end = Math.min(view.length, end_index + chunk_size) - for (let i = end_index; i < next_end; i++) - if (view[i]) fragment.appendChild(create_node(view[i])) + for (let i = end_index; i < next_end; i++) { if (view[i]) fragment.appendChild(create_node(view[i])) } container.insertBefore(fragment, bottom_sentinel) end_index = next_end bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` @@ -1037,26 +1187,74 @@ async function graph_explorer (opts) { } /****************************************************************************** - 8. HELPER FUNCTIONS + 8. HUB DUPLICATION PREVENTION ******************************************************************************/ -function get_highlighted_name (name, query) { + function find_expanded_hub_instance (target_base_path, exclude_instance_path) { + // Look through all nodes in the current view to find expanded hubs + for (const node of view) { + if (node.instance_path === exclude_instance_path) continue + + const state = instance_states[node.instance_path] + if (!state || !state.expanded_hubs) continue + + const entry = all_entries[node.base_path] + if (!entry || !Array.isArray(entry.hubs)) continue + + if (entry.hubs.includes(target_base_path)) return node.instance_path + } + return null + } + + function scroll_to_and_highlight_hub (target_base_path) { + // Find the hub entry with the same base_path in the current view + const hub_index = view.findIndex(n => n.base_path === target_base_path) + if (hub_index === -1) return + + // Calculate scroll position + const target_scroll_top = hub_index * node_height + container.scrollTop = target_scroll_top + + // Find and highlight the DOM element + const hub_instance_path = view[hub_index].instance_path + const hub_element = shadow.querySelector(`[data-instance_path="${CSS.escape(hub_instance_path)}"]`) + if (hub_element) { + hub_element.style.backgroundColor = 'pink' + hub_element.style.transition = 'background-color 0.3s ease' + // remove highlight after 2 seconds + setTimeout(() => { + hub_element.style.backgroundColor = '' + setTimeout(() => { + hub_element.style.transition = '' + }, 300) + }, 2000) + } + } + + function is_hub_already_expanded (base_path, exclude_instance_path) { + return find_expanded_hub_instance(base_path, exclude_instance_path) !== null + } + + /****************************************************************************** + 9. HELPER FUNCTIONS + ******************************************************************************/ + function get_highlighted_name (name, query) { // Creates a new regular expression. // `escape_regex(query)` sanitizes the query string to treat special regex characters literally. // `(...)` creates a capturing group for the escaped query. // 'gi' flags: 'g' for global (all occurrences), 'i' for case-insensitive. - const regex = new RegExp(`(${escape_regex(query)})`, 'gi') - // Replaces all matches of the regex in 'name' with the matched text wrapped in tags. - // '$1' refers to the content of the first capturing group (the matched query). - return name.replace(regex, '$1') -} + const regex = new RegExp(`(${escape_regex(query)})`, 'gi') + // Replaces all matches of the regex in 'name' with the matched text wrapped in tags. + // '$1' refers to the content of the first capturing group (the matched query). + return name.replace(regex, '$1') + } -function escape_regex (string) { + function escape_regex (string) { // Escapes special regular expression characters in a string. // It replaces characters like -, /, \, ^, $, *, +, ?, ., (, ), |, [, ], {, } // with their escaped versions (e.g., '.' becomes '\.'). // This prevents them from being interpreted as regex metacharacters. - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char -} + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char + } function check_and_reset_feedback_flags () { if (drive_updated_by_scroll) { @@ -1130,7 +1328,6 @@ function escape_regex (string) { if (new_scroll_top > max_scroll_top) { spacer_initial_height = new_scroll_top - max_scroll_top - spacer_initial_scroll_top = new_scroll_top spacer_element.style.height = `${spacer_initial_height}px` } sync_fn() @@ -1142,7 +1339,6 @@ function escape_regex (string) { } else { spacer_element = null spacer_initial_height = 0 - spacer_initial_scroll_top = 0 requestAnimationFrame(sync_fn) } } @@ -1152,20 +1348,18 @@ function escape_regex (string) { const el = document.createElement('div') el.className = 'node type-root' el.dataset.instance_path = instance_path - const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + const prefix_class = has_subs || mode === 'search' ? 'prefix clickable' : 'prefix' const prefix_name = state.expanded_subs ? 'tee-down' : 'line-h' - el.innerHTML = `
🪄
/🌐` + el.innerHTML = `
🪄
/🌐` el.querySelector('.wand').onclick = reset if (has_subs) { const prefix_el = el.querySelector('.prefix') if (prefix_el) { - prefix_el.onclick = mode === 'search' - ? () => toggle_search_subs(instance_path) - : () => toggle_subs(instance_path) + prefix_el.onclick = mode === 'search' ? null : () => toggle_subs(instance_path) } } - el.querySelector('.name').onclick = ev => select_node(ev, instance_path) + el.querySelector('.name').onclick = ev => mode === 'search' ? null : select_node(ev, instance_path) return el } @@ -1198,7 +1392,7 @@ function escape_regex (string) { } /****************************************************************************** - 9. FALLBACK CONFIGURATION + 10. FALLBACK CONFIGURATION - This provides the default data and API configuration for the component, following the pattern described in `instructions.md`. - It defines the default datasets (`entries`, `style`, `runtime`) and their @@ -1216,7 +1410,7 @@ function fallback_module () { }, 'style/': { 'theme.css': { - '$ref' : 'theme.css' + $ref: 'theme.css' } }, 'runtime/': { @@ -1231,12 +1425,14 @@ function fallback_module () { 'mode/': { 'current_mode.json': { raw: '"menubar"' }, 'previous_mode.json': { raw: '"menubar"' }, - 'search_query.json': { raw: '""' } + 'search_query.json': { raw: '""' }, + 'multi_select_enabled.json': { raw: 'false' } } } } } } + }).call(this)}).call(this,"/lib/graph_explorer.js") },{"./STATE":1}],3:[function(require,module,exports){ const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' @@ -1262,7 +1458,7 @@ fetch(init_url, fetch_opts) }) },{"./page":4}],4:[function(require,module,exports){ -(function (__filename,__dirname){(function (){ +(function (__filename){(function (){ const STATE = require('../lib/STATE') const statedb = STATE(__filename) const { sdb } = statedb(fallback_module) @@ -1275,8 +1471,6 @@ const sheet = new CSSStyleSheet() config().then(() => boot({ sid: '' })) async function config () { - const path = path => - new URL(`../src/node_modules/${path}`, `file://${__dirname}`).href.slice(8) const html = document.documentElement const meta = document.createElement('meta') const font = @@ -1315,10 +1509,8 @@ async function boot (opts) { // ---------------------------------------- // ELEMENTS // ---------------------------------------- - { - // desktop - shadow.append(await app(subs[0])) - } + // desktop + shadow.append(await app(subs[0])) // ---------------------------------------- // INIT // ---------------------------------------- @@ -1357,5 +1549,5 @@ function fallback_module () { } } -}).call(this)}).call(this,"/web/page.js","/web") +}).call(this)}).call(this,"/web/page.js") },{"..":2,"../lib/STATE":1}]},{},[3]); From c4716b1cdda2379c1b3075eab0eff62071e6136b Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 31 Aug 2025 16:39:58 +0500 Subject: [PATCH 066/130] Increase the expand subs trigger to include prefix and pipes before entry prefix --- lib/graph_explorer.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 9e9b660..da0a48e 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -655,11 +655,17 @@ async function graph_explorer (opts) { const navigate_el = el.querySelector('.navigate-to-hub') if (navigate_el) navigate_el.onclick = () => scroll_to_and_highlight_hub(base_path) - const prefix_el = el.querySelector('.prefix') - if (prefix_el && has_subs) { - prefix_el.onclick = mode === 'search' + // Add click event to the whole first part (indent + prefix) for expanding/collapsing subs + if (has_subs) { + const indent_el = el.querySelector('.indent') + const prefix_el = el.querySelector('.prefix') + + const toggle_subs_handler = mode === 'search' ? () => toggle_search_subs(instance_path) : () => toggle_subs(instance_path) + + if (indent_el) indent_el.onclick = toggle_subs_handler + if (prefix_el) prefix_el.onclick = toggle_subs_handler } el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) From 0dae9018e4bd8bc8a18bf59bf4a26ff98708a4a7 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 31 Aug 2025 16:45:14 +0500 Subject: [PATCH 067/130] Add drive based bold styling --- lib/graph_explorer.js | 4 ++-- lib/theme.css | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index da0a48e..47882bf 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -1245,9 +1245,9 @@ async function graph_explorer (opts) { // `(...)` creates a capturing group for the escaped query. // 'gi' flags: 'g' for global (all occurrences), 'i' for case-insensitive. const regex = new RegExp(`(${escape_regex(query)})`, 'gi') - // Replaces all matches of the regex in 'name' with the matched text wrapped in tags. + // Replaces all matches of the regex in 'name' with the matched text wrapped in search-match class. // '$1' refers to the content of the first capturing group (the matched query). - return name.replace(regex, '$1') + return name.replace(regex, '$1') } function escape_regex (string) { diff --git a/lib/theme.css b/lib/theme.css index af0756a..5f15a9c 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -112,4 +112,8 @@ } .navigate-to-hub:hover { background-color: #528bcc; +} +.search-match { + font-weight: bold; + color: #e5c07b; } \ No newline at end of file From 246dd36a409cc50cab20c1a03f33a35cc8536ab0 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 31 Aug 2025 16:54:20 +0500 Subject: [PATCH 068/130] move the searchbar to a new div above menubar --- lib/graph_explorer.js | 17 +++++++++++++++-- lib/theme.css | 10 +++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 47882bf..5ef6a1f 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -40,8 +40,10 @@ async function graph_explorer (opts) { const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = `
+ ` + const searchbar = shadow.querySelector('.searchbar') const menubar = shadow.querySelector('.menubar') const container = shadow.querySelector('.graph-container') @@ -272,6 +274,7 @@ async function graph_explorer (opts) { if (mode && new_current_mode === 'search') update_drive_state({ dataset: 'mode', name: 'previous_mode', value: mode }) mode = new_current_mode render_menubar() + render_searchbar() handle_mode_change() if (mode === 'search' && search_query) perform_search(search_query) @@ -755,8 +758,17 @@ async function graph_explorer (opts) { multi_select_button.innerHTML = `Multi Select: ${multi_select_enabled ? 'true' : 'false'}` multi_select_button.onclick = mode === 'search' ? null : toggle_multi_select - if (mode !== 'search') return menubar.replaceChildren(search_button, multi_select_button) + menubar.replaceChildren(search_button, multi_select_button) + } + + function render_searchbar () { + if (mode !== 'search') { + searchbar.style.display = 'none' + searchbar.replaceChildren() + return + } + searchbar.style.display = 'flex' const search_input = Object.assign(document.createElement('input'), { type: 'text', placeholder: 'Search entries...', @@ -765,12 +777,13 @@ async function graph_explorer (opts) { oninput: on_search_input }) - menubar.replaceChildren(search_button, multi_select_button, search_input) + searchbar.replaceChildren(search_input) requestAnimationFrame(() => search_input.focus()) } function handle_mode_change () { menubar.style.display = mode === 'default' ? 'none' : 'flex' + render_searchbar() build_and_render_view() } diff --git a/lib/theme.css b/lib/theme.css index 5f15a9c..4bb7938 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -31,6 +31,12 @@ .node.direct-match { background-color: #0030669c; } +.searchbar { + display: none; + padding: 5px; + background-color: #21252b; + border-bottom: 1px solid #181a1f; +} .menubar { display: flex; padding: 5px; @@ -38,10 +44,12 @@ border-bottom: 1px solid #181a1f; } .search-input { - margin-left: auto; + width: 100%; background-color: #282c34; color: #abb2bf; border: 1px solid #181a1f; + padding: 5px; + border-radius: 3px; } .confirm-wrapper { margin-left: auto; From c2fa80c0d32695da62b68efb60a5e9aca3617efe Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 31 Aug 2025 17:55:58 +0500 Subject: [PATCH 069/130] Add ^ button to all the repeating entries and relocate position next to the icon --- lib/graph_explorer.js | 24 ++++++++++++------------ lib/theme.css | 1 + 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 5ef6a1f..8fb575c 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -628,24 +628,16 @@ async function graph_explorer (opts) { ? get_highlighted_name(entry_name, query) : entry_name - // Check if hub is alrady expanded elsewhere - const existing_expanded_instance = has_hubs && !state.expanded_hubs ? find_expanded_hub_instance(base_path, instance_path) : null - // Don't show `jump` button if entry is a expnded hub - const is_this_an_expanded_hub = is_hub && view.some(node => { - const node_state = instance_states[node.instance_path] - if (!node_state || !node_state.expanded_hubs) return false - const node_entry = all_entries[node.base_path] - return node_entry && Array.isArray(node_entry.hubs) && node_entry.hubs.includes(base_path) - }) - const is_duplicate_hub = existing_expanded_instance !== null && is_this_an_expanded_hub - const navigate_button_html = is_duplicate_hub ? '^' : '' + // Check if this entry appears elsewhere in the view (any duplicate) + const duplicate_instance = find_any_duplicate_entry(base_path, instance_path) + const navigate_button_html = duplicate_instance ? '^' : '' el.innerHTML = ` ${pipe_html} - ${name_html} ${navigate_button_html} + ${name_html} ` const icon_el = el.querySelector('.icon') @@ -1220,6 +1212,14 @@ async function graph_explorer (opts) { return null } + function find_any_duplicate_entry (target_base_path, exclude_instance_path) { + for (const node of view) { + if (node.instance_path === exclude_instance_path) continue + if (node.base_path === target_base_path) return node.instance_path + } + return null + } + function scroll_to_and_highlight_hub (target_base_path) { // Find the hub entry with the same base_path in the current view const hub_index = view.findIndex(n => n.base_path === target_base_path) diff --git a/lib/theme.css b/lib/theme.css index 4bb7938..923927e 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -110,6 +110,7 @@ .node.type-file > .icon::before { content: '📄'; } .navigate-to-hub { margin-left: calc(5px * var(--scale-factor, 1)); + margin-right: calc(5px * var(--scale-factor, 1)); padding: calc(2px * var(--scale-factor, 1)) calc(4px * var(--scale-factor, 1)); background-color: #61afef; color: #282c34; From 21d206e6b82763fcb3bf0f5f54c1dd8e0481443d Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 31 Aug 2025 20:39:03 +0500 Subject: [PATCH 070/130] Added Cycling through entries with jump to already opened --- lib/graph_explorer.js | 87 ++++++++++++++++++++++++------------------- lib/theme.css | 4 ++ 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 8fb575c..c1248fe 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -629,8 +629,9 @@ async function graph_explorer (opts) { : entry_name // Check if this entry appears elsewhere in the view (any duplicate) - const duplicate_instance = find_any_duplicate_entry(base_path, instance_path) - const navigate_button_html = duplicate_instance ? '^' : '' + collect_all_duplicate_entries() + const has_duplicate_entries = has_duplicates(base_path) + const navigate_button_html = has_duplicate_entries ? '^' : '' el.innerHTML = ` ${pipe_html} @@ -648,17 +649,19 @@ async function graph_explorer (opts) { } const navigate_el = el.querySelector('.navigate-to-hub') - if (navigate_el) navigate_el.onclick = () => scroll_to_and_highlight_hub(base_path) + if (navigate_el) { + navigate_el.onclick = () => cycle_to_next_duplicate(base_path, instance_path) + } // Add click event to the whole first part (indent + prefix) for expanding/collapsing subs if (has_subs) { const indent_el = el.querySelector('.indent') const prefix_el = el.querySelector('.prefix') - + const toggle_subs_handler = mode === 'search' ? () => toggle_search_subs(instance_path) : () => toggle_subs(instance_path) - + if (indent_el) indent_el.onclick = toggle_subs_handler if (prefix_el) prefix_el.onclick = toggle_subs_handler } @@ -1196,59 +1199,65 @@ async function graph_explorer (opts) { /****************************************************************************** 8. HUB DUPLICATION PREVENTION ******************************************************************************/ - function find_expanded_hub_instance (target_base_path, exclude_instance_path) { - // Look through all nodes in the current view to find expanded hubs + + function collect_all_duplicate_entries () { + duplicate_entries_map = {} + const base_path_counts = {} for (const node of view) { - if (node.instance_path === exclude_instance_path) continue + if (!base_path_counts[node.base_path]) { + base_path_counts[node.base_path] = [] + } + base_path_counts[node.base_path].push(node.instance_path) + } + + // Store only duplicates + for (const [base_path, instance_paths] of Object.entries(base_path_counts)) { + if (instance_paths.length > 1) { + duplicate_entries_map[base_path] = instance_paths + } + } + } - const state = instance_states[node.instance_path] - if (!state || !state.expanded_hubs) continue + function get_next_duplicate_instance (base_path, current_instance_path) { + const duplicates = duplicate_entries_map[base_path] + if (!duplicates || duplicates.length <= 1) return null - const entry = all_entries[node.base_path] - if (!entry || !Array.isArray(entry.hubs)) continue + const current_index = duplicates.indexOf(current_instance_path) + if (current_index === -1) return duplicates[0] - if (entry.hubs.includes(target_base_path)) return node.instance_path - } - return null + const next_index = (current_index + 1) % duplicates.length + return duplicates[next_index] } - function find_any_duplicate_entry (target_base_path, exclude_instance_path) { - for (const node of view) { - if (node.instance_path === exclude_instance_path) continue - if (node.base_path === target_base_path) return node.instance_path + function has_duplicates (base_path) { + return duplicate_entries_map[base_path] && duplicate_entries_map[base_path].length > 1 + } + + function cycle_to_next_duplicate (base_path, current_instance_path) { + const next_instance_path = get_next_duplicate_instance(base_path, current_instance_path) + if (next_instance_path) { + scroll_to_and_highlight_instance(next_instance_path) } - return null } - function scroll_to_and_highlight_hub (target_base_path) { - // Find the hub entry with the same base_path in the current view - const hub_index = view.findIndex(n => n.base_path === target_base_path) - if (hub_index === -1) return + function scroll_to_and_highlight_instance (target_instance_path) { + const target_index = view.findIndex(n => n.instance_path === target_instance_path) + if (target_index === -1) return // Calculate scroll position - const target_scroll_top = hub_index * node_height + const target_scroll_top = target_index * node_height container.scrollTop = target_scroll_top // Find and highlight the DOM element - const hub_instance_path = view[hub_index].instance_path - const hub_element = shadow.querySelector(`[data-instance_path="${CSS.escape(hub_instance_path)}"]`) - if (hub_element) { - hub_element.style.backgroundColor = 'pink' - hub_element.style.transition = 'background-color 0.3s ease' - // remove highlight after 2 seconds + const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(target_instance_path)}"]`) + if (target_element) { + target_element.classList.add('highlight-instance') setTimeout(() => { - hub_element.style.backgroundColor = '' - setTimeout(() => { - hub_element.style.transition = '' - }, 300) + target_element.classList.remove('highlight-instance') }, 2000) } } - function is_hub_already_expanded (base_path, exclude_instance_path) { - return find_expanded_hub_instance(base_path, exclude_instance_path) !== null - } - /****************************************************************************** 9. HELPER FUNCTIONS ******************************************************************************/ diff --git a/lib/theme.css b/lib/theme.css index 923927e..1785b9e 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -125,4 +125,8 @@ .search-match { font-weight: bold; color: #e5c07b; +} +.highlight-instance { + background-color: pink; + transition: background-color 0.3s ease; } \ No newline at end of file From cd3d2ed2f98a1b51028dd45a8dc61d3509d8743f Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 31 Aug 2025 22:12:41 +0500 Subject: [PATCH 071/130] Replace current implementation with container queries instead --- lib/graph_explorer.js | 26 +----- lib/theme.css | 208 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 182 insertions(+), 52 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index c1248fe..0bb1867 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -57,7 +57,6 @@ async function graph_explorer (opts) { const chunk_size = 50 const max_rendered_nodes = chunk_size * 3 let node_height - let scale_factor = 1 // Scale factor for mobile devices const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') @@ -68,8 +67,6 @@ async function graph_explorer (opts) { threshold: 0 }) - calculate_mobile_scale() - window.onresize = calculate_mobile_scale // Define handlers for different data types from the drive, called by `onbatch`. const on = { entries: on_entries, @@ -539,23 +536,6 @@ async function graph_explorer (opts) { return current_view } - function calculate_mobile_scale () { - const screen_width = window.innerWidth - const screen_height = window.innerHeight - const is_mobile = screen_width < 768 || screen_height < 600 - - if (is_mobile) { - // Scale proportionally based on screen size - const width_scale = Math.max(1.3, Math.min(2, 768 / screen_width)) - const height_scale = Math.max(1.3, Math.min(2, 600 / screen_height)) - scale_factor = Math.max(width_scale, height_scale) - } else { - scale_factor = 1 - } - - // Initialize the CSS variable - shadow.host.style.setProperty('--scale-factor', scale_factor) - } /****************************************************************************** 4. NODE CREATION AND EVENT HANDLING - `create_node` generates the DOM element for a single node. @@ -614,8 +594,10 @@ async function graph_explorer (opts) { const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 - if (depth) el.style.paddingLeft = `${17.5 * scale_factor}px` - el.style.height = `${node_height * scale_factor}px` + if (depth) { + el.classList.add('left-indent') + el.style.paddingLeft *= depth + } if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) diff --git a/lib/theme.css b/lib/theme.css index 1785b9e..a2a6137 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -1,7 +1,11 @@ +/* Container setup for responsive scaling */ +.graph-explorer-wrapper { + container-type: inline-size; + container-name: graph-explorer; +} .graph-container, .node { font-family: monospace; - font-size: calc(14px * var(--scale-factor, 1)); /* Variable from the `graph_explorer.js` */ } .graph-container { color: #abb2bf; @@ -31,12 +35,6 @@ .node.direct-match { background-color: #0030669c; } -.searchbar { - display: none; - padding: 5px; - background-color: #21252b; - border-bottom: 1px solid #181a1f; -} .menubar { display: flex; padding: 5px; @@ -44,12 +42,10 @@ border-bottom: 1px solid #181a1f; } .search-input { - width: 100%; + margin-left: auto; background-color: #282c34; color: #abb2bf; border: 1px solid #181a1f; - padding: 5px; - border-radius: 3px; } .confirm-wrapper { margin-left: auto; @@ -60,19 +56,17 @@ } .pipe { text-align: center; - font-size: calc(14px * var(--scale-factor, 1)); } .pipe::before { content: '┃'; } .blank { - width: calc(8.5px * var(--scale-factor, 1)); + width: 8.5px; text-align: center; } .clickable { cursor: pointer; } .prefix, .icon { - margin-right: calc(2px * var(--scale-factor, 1)); - font-size: calc(14px * var(--scale-factor, 1)); + margin-right: 2px; } .top-cross::before { content: '┏╋'; } .top-tee-down::before { content: '┏┳'; } @@ -92,30 +86,22 @@ .middle-light-line::before { content: '┠─'; } .tee-down::before { content: '┳'; } .line-h::before { content: '━'; } -.icon { - display: inline-block; - text-align: center; - font-size: calc(16px * var(--scale-factor, 1)); -} -.name { - flex-grow: 1; - font-size: calc(14px * var(--scale-factor, 1)); -} +.icon { display: inline-block; text-align: center; } +.name { flex-grow: 1; } .node.type-root > .icon::before { content: '🌐'; } .node.type-folder > .icon::before { content: '📁'; } .node.type-html-file > .icon::before { content: '📄'; } .node.type-js-file > .icon::before { content: '📜'; } .node.type-css-file > .icon::before { content: '🎨'; } .node.type-json-file > .icon::before { content: '📝'; } -.node.type-file > .icon::before { content: '📄'; } -.navigate-to-hub { - margin-left: calc(5px * var(--scale-factor, 1)); - margin-right: calc(5px * var(--scale-factor, 1)); - padding: calc(2px * var(--scale-factor, 1)) calc(4px * var(--scale-factor, 1)); +.node.type-file > .icon::before { content: '📄'; }.navigate-to-hub { + margin-left: 5px; + margin-right: 5px; + padding: 2px 4px; background-color: #61afef; color: #282c34; - border-radius: calc(3px * var(--scale-factor, 1)); - font-size: calc(12px * var(--scale-factor, 1)); + border-radius: 3px; + font-size: 12px; font-weight: bold; transition: background-color 0.2s ease; } @@ -129,4 +115,166 @@ .highlight-instance { background-color: pink; transition: background-color 0.3s ease; +} + +/* Base sizes for desktop (768px and above) */ +.left-indent { + padding-left: 17.5px !important; +} +.node { + font-size: 14px; + height: 16px; + padding-left: 0; +} + +.pipe, .blank { + width: 8.5px; + font-size: 14px; +} + +.prefix, .icon { + font-size: 14px; +} + +.navigate-to-hub { + font-size: 12px; + padding: 2px 4px; +} + +.graph-container { + padding: 10px; +} + +/* Medium screens (480px to 767px) - 1.3x scale */ +@media (max-width: 767px) { + .left-indent { + padding-left: 22.75px !important; + } + .node { + font-size: 18px !important; + height: 21px !important; + } + + .pipe, .blank { + width: 11px !important; + font-size: 18px !important; + } + + .prefix, .icon { + font-size: 18px !important; + margin-right: 3px !important; + } + + .navigate-to-hub { + font-size: 16px !important; + padding: 3px 5px !important; + margin-left: 7px !important; + margin-right: 7px !important; + } + + .graph-container { + padding: 13px !important; + } + + .confirm-wrapper { + padding-left: 13px !important; + } +} + +/* Small screens (320px to 479px) - 1.6x scale */ +@media (max-width: 479px) { + .left-indent { + padding-left: 28px !important; + } + .node { + font-size: 22px !important; + height: 26px !important; + } + + .pipe, .blank { + width: 14px !important; + font-size: 22px !important; + } + + .prefix, .icon { + font-size: 22px !important; + margin-right: 4px !important; + } + + .navigate-to-hub { + font-size: 19px !important; + padding: 4px 6px !important; + margin-left: 8px !important; + margin-right: 8px !important; + } + + .graph-container { + padding: 16px !important; + } + + .confirm-wrapper { + padding-left: 16px !important; + } +} + +/* Extra small screens (below 320px) - 2x scale */ +@media (max-width: 319px) { + .left-indent { + padding-left: 35px !important; + } + .node { + font-size: 28px !important; + height: 32px !important; + } + + .pipe, .blank { + width: 17px !important; + font-size: 28px !important; + } + + .prefix, .icon { + font-size: 28px !important; + margin-right: 4px !important; + } + + .navigate-to-hub { + font-size: 24px !important; + padding: 4px 8px !important; + margin-left: 10px !important; + margin-right: 10px !important; + } + + .graph-container { + padding: 20px !important; + } + + .confirm-wrapper { + padding-left: 20px !important; + } +} + +/* Height-based responsive adjustments for mobile experience */ +@media (max-height: 600px) { + .left-indent { + padding-left: 22.75px !important; + } + .node { + font-size: 20px !important; + height: 24px !important; + } + + .pipe, .blank { + width: 12px !important; + font-size: 20px !important; + } + + .prefix, .icon { + font-size: 20px !important; + margin-right: 3px !important; + } + + .navigate-to-hub { + font-size: 18px !important; + padding: 3px 6px !important; + } } \ No newline at end of file From 1bf182c1b8727448345e6238e896fad59712efa6 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 31 Aug 2025 22:25:10 +0500 Subject: [PATCH 072/130] Make the last clicked node look different --- lib/graph_explorer.js | 18 ++++++++++++++++-- lib/theme.css | 4 ++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 0bb1867..4abbde5 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -34,6 +34,7 @@ async function graph_explorer (opts) { let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 let hub_num = 0 // Counter for expanded hubs. + let last_clicked_node = null // Track the last clicked node instance path for highlighting. const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -151,7 +152,8 @@ async function graph_explorer (opts) { 'selected_instance_paths.json': handle_selected_paths, 'confirmed_selected.json': handle_confirmed_paths, 'instance_states.json': handle_instance_states, - 'search_entry_states.json': handle_search_entry_states + 'search_entry_states.json': handle_search_entry_states, + 'last_clicked_node.json': handle_last_clicked_node } let needs_render = false const render_nodes_needed = new Set() @@ -222,6 +224,13 @@ async function graph_explorer (opts) { console.warn('search_entry_states is not a valid object, ignoring.', value) } } + + function handle_last_clicked_node ({ value, render_nodes_needed }) { + const old_last_clicked = last_clicked_node + last_clicked_node = typeof value === 'string' ? value : null + if (old_last_clicked) render_nodes_needed.add(old_last_clicked) + if (last_clicked_node) render_nodes_needed.add(last_clicked_node) + } } function on_mode ({ data, paths }) { @@ -590,6 +599,7 @@ async function graph_explorer (opts) { if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') + if (last_clicked_node === instance_path) el.classList.add('last-clicked') const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 @@ -965,6 +975,9 @@ async function graph_explorer (opts) { toggling, and resetting the graph. ******************************************************************************/ function select_node (ev, instance_path) { + last_clicked_node = instance_path + update_drive_state({ dataset: 'runtime', name: 'last_clicked_node', value: instance_path }) + const new_selected = new Set(selected_instance_paths) if (ev.ctrlKey || multi_select_enabled) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) @@ -1426,7 +1439,8 @@ function fallback_module () { 'selected_instance_paths.json': { raw: '[]' }, 'confirmed_selected.json': { raw: '[]' }, 'instance_states.json': { raw: '{}' }, - 'search_entry_states.json': { raw: '{}' } + 'search_entry_states.json': { raw: '{}' }, + 'last_clicked_node.json': { raw: 'null' } }, 'mode/': { 'current_mode.json': { raw: '"menubar"' }, diff --git a/lib/theme.css b/lib/theme.css index a2a6137..39b62f2 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -29,6 +29,10 @@ .node.confirmed { background-color: #774346; } +.node.last-clicked { + background-color: #4a6741; + border-right: 5px solid #7cb342; +} .node.new-entry { background-color: #87cfeb34; } From bfd0154e7688824850cd630cd8df8aabf175985b Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 31 Aug 2025 22:41:10 +0500 Subject: [PATCH 073/130] Add Select Between for bulk toggle selecting --- lib/graph_explorer.js | 85 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 4abbde5..47a5fad 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -30,6 +30,8 @@ async function graph_explorer (opts) { let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key + let select_between_enabled = false // Flag to enable select between mode + let select_between_first_node = null // First node selected in select between mode let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 @@ -238,9 +240,10 @@ async function graph_explorer (opts) { 'current_mode.json': handle_current_mode, 'previous_mode.json': handle_previous_mode, 'search_query.json': handle_search_query, - 'multi_select_enabled.json': handle_multi_select_enabled + 'multi_select_enabled.json': handle_multi_select_enabled, + 'select_between_enabled.json': handle_select_between_enabled } - let new_current_mode, new_previous_mode, new_search_query, new_multi_select_enabled + let new_current_mode, new_previous_mode, new_search_query, new_multi_select_enabled, new_select_between_enabled paths.forEach((path, i) => { const value = parse_json_data(data[i], path) @@ -254,6 +257,7 @@ async function graph_explorer (opts) { if (result?.previous_mode !== undefined) new_previous_mode = result.previous_mode if (result?.search_query !== undefined) new_search_query = result.search_query if (result?.multi_select_enabled !== undefined) new_multi_select_enabled = result.multi_select_enabled + if (result?.select_between_enabled !== undefined) new_select_between_enabled = result.select_between_enabled } }) @@ -263,6 +267,11 @@ async function graph_explorer (opts) { multi_select_enabled = new_multi_select_enabled render_menubar() // Re-render menubar to update button text } + if (typeof new_select_between_enabled === 'boolean') { + select_between_enabled = new_select_between_enabled + if (!select_between_enabled) select_between_first_node = null + render_menubar() + } if ( new_current_mode && @@ -299,6 +308,10 @@ async function graph_explorer (opts) { function handle_multi_select_enabled ({ value }) { return { multi_select_enabled: value } } + + function handle_select_between_enabled ({ value }) { + return { select_between_enabled: value } + } } function inject_style ({ data }) { @@ -745,7 +758,11 @@ async function graph_explorer (opts) { multi_select_button.innerHTML = `Multi Select: ${multi_select_enabled ? 'true' : 'false'}` multi_select_button.onclick = mode === 'search' ? null : toggle_multi_select - menubar.replaceChildren(search_button, multi_select_button) + const select_between_button = document.createElement('button') + select_between_button.innerHTML = `Select Between: ${select_between_enabled ? 'true' : 'false'}` + select_between_button.onclick = mode === 'search' ? null : toggle_select_between + + menubar.replaceChildren(search_button, multi_select_button, select_between_button) } function render_searchbar () { @@ -786,10 +803,28 @@ async function graph_explorer (opts) { function toggle_multi_select () { multi_select_enabled = !multi_select_enabled + // Disable select between when enabling multi select + if (multi_select_enabled && select_between_enabled) { + select_between_enabled = false + select_between_first_node = null + update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: false }) + } update_drive_state({ dataset: 'mode', name: 'multi_select_enabled', value: multi_select_enabled }) render_menubar() // Re-render to update button text } + function toggle_select_between () { + select_between_enabled = !select_between_enabled + select_between_first_node = null // Reset first node selection + // Disable multi select when enabling select between + if (select_between_enabled && multi_select_enabled) { + multi_select_enabled = false + update_drive_state({ dataset: 'mode', name: 'multi_select_enabled', value: false }) + } + update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: select_between_enabled }) + render_menubar() // Re-render to update button text + } + function on_search_input (event) { search_query = event.target.value.trim() drive_updated_by_search = true @@ -978,8 +1013,19 @@ async function graph_explorer (opts) { last_clicked_node = instance_path update_drive_state({ dataset: 'runtime', name: 'last_clicked_node', value: instance_path }) + // Handle shift+click to enable select between mode temporarily + if (ev.shiftKey && !select_between_enabled) { + select_between_enabled = true + select_between_first_node = null + update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: true }) + render_menubar() + } + const new_selected = new Set(selected_instance_paths) - if (ev.ctrlKey || multi_select_enabled) { + + if (select_between_enabled) { + handle_select_between(instance_path, new_selected) + } else if (ev.ctrlKey || multi_select_enabled) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) } else { @@ -987,6 +1033,34 @@ async function graph_explorer (opts) { } } + function handle_select_between (instance_path, new_selected) { + if (!select_between_first_node) { + select_between_first_node = instance_path + } else { + const first_index = view.findIndex(n => n.instance_path === select_between_first_node) + const second_index = view.findIndex(n => n.instance_path === instance_path) + + if (first_index !== -1 && second_index !== -1) { + const start_index = Math.min(first_index, second_index) + const end_index = Math.max(first_index, second_index) + + // Toggle selection for all nodes in the range + for (let i = start_index; i <= end_index; i++) { + const node_instance_path = view[i].instance_path + new_selected.has(node_instance_path) ? new_selected.delete(node_instance_path) : new_selected.add(node_instance_path) + } + + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) + } + + // Reset select between mode after second click + select_between_enabled = false + select_between_first_node = null + update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: false }) + render_menubar() + } + } + // Add the clicked entry and all its parents in the default tree function search_expand_into_default (target_instance_path) { if (!target_instance_path) return @@ -1446,7 +1520,8 @@ function fallback_module () { 'current_mode.json': { raw: '"menubar"' }, 'previous_mode.json': { raw: '"menubar"' }, 'search_query.json': { raw: '""' }, - 'multi_select_enabled.json': { raw: 'false' } + 'multi_select_enabled.json': { raw: 'false' }, + 'select_between_enabled.json': { raw: 'false' } } } } From 35d313a25bcf6900452a975cf0c832ad484da570 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 31 Aug 2025 22:44:41 +0500 Subject: [PATCH 074/130] lint & bundled --- bundle.js | 251 +++++++++++++++++++++++++++++------------- lib/graph_explorer.js | 16 +-- 2 files changed, 183 insertions(+), 84 deletions(-) diff --git a/bundle.js b/bundle.js index 7090a37..baacff1 100644 --- a/bundle.js +++ b/bundle.js @@ -34,18 +34,23 @@ async function graph_explorer (opts) { let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key + let select_between_enabled = false // Flag to enable select between mode + let select_between_first_node = null // First node selected in select between mode let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 let hub_num = 0 // Counter for expanded hubs. + let last_clicked_node = null // Track the last clicked node instance path for highlighting. const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) shadow.innerHTML = `
+ ` + const searchbar = shadow.querySelector('.searchbar') const menubar = shadow.querySelector('.menubar') const container = shadow.querySelector('.graph-container') @@ -59,7 +64,6 @@ async function graph_explorer (opts) { const chunk_size = 50 const max_rendered_nodes = chunk_size * 3 let node_height - let scale_factor = 1 // Scale factor for mobile devices const top_sentinel = document.createElement('div') const bottom_sentinel = document.createElement('div') @@ -70,8 +74,6 @@ async function graph_explorer (opts) { threshold: 0 }) - calculate_mobile_scale() - window.onresize = calculate_mobile_scale // Define handlers for different data types from the drive, called by `onbatch`. const on = { entries: on_entries, @@ -156,7 +158,8 @@ async function graph_explorer (opts) { 'selected_instance_paths.json': handle_selected_paths, 'confirmed_selected.json': handle_confirmed_paths, 'instance_states.json': handle_instance_states, - 'search_entry_states.json': handle_search_entry_states + 'search_entry_states.json': handle_search_entry_states, + 'last_clicked_node.json': handle_last_clicked_node } let needs_render = false const render_nodes_needed = new Set() @@ -227,6 +230,13 @@ async function graph_explorer (opts) { console.warn('search_entry_states is not a valid object, ignoring.', value) } } + + function handle_last_clicked_node ({ value, render_nodes_needed }) { + const old_last_clicked = last_clicked_node + last_clicked_node = typeof value === 'string' ? value : null + if (old_last_clicked) render_nodes_needed.add(old_last_clicked) + if (last_clicked_node) render_nodes_needed.add(last_clicked_node) + } } function on_mode ({ data, paths }) { @@ -234,9 +244,10 @@ async function graph_explorer (opts) { 'current_mode.json': handle_current_mode, 'previous_mode.json': handle_previous_mode, 'search_query.json': handle_search_query, - 'multi_select_enabled.json': handle_multi_select_enabled + 'multi_select_enabled.json': handle_multi_select_enabled, + 'select_between_enabled.json': handle_select_between_enabled } - let new_current_mode, new_previous_mode, new_search_query, new_multi_select_enabled + let new_current_mode, new_previous_mode, new_search_query, new_multi_select_enabled, new_select_between_enabled paths.forEach((path, i) => { const value = parse_json_data(data[i], path) @@ -250,6 +261,7 @@ async function graph_explorer (opts) { if (result?.previous_mode !== undefined) new_previous_mode = result.previous_mode if (result?.search_query !== undefined) new_search_query = result.search_query if (result?.multi_select_enabled !== undefined) new_multi_select_enabled = result.multi_select_enabled + if (result?.select_between_enabled !== undefined) new_select_between_enabled = result.select_between_enabled } }) @@ -259,6 +271,11 @@ async function graph_explorer (opts) { multi_select_enabled = new_multi_select_enabled render_menubar() // Re-render menubar to update button text } + if (typeof new_select_between_enabled === 'boolean') { + select_between_enabled = new_select_between_enabled + if (!select_between_enabled) select_between_first_node = null + render_menubar() + } if ( new_current_mode && @@ -276,6 +293,7 @@ async function graph_explorer (opts) { if (mode && new_current_mode === 'search') update_drive_state({ dataset: 'mode', name: 'previous_mode', value: mode }) mode = new_current_mode render_menubar() + render_searchbar() handle_mode_change() if (mode === 'search' && search_query) perform_search(search_query) @@ -294,6 +312,10 @@ async function graph_explorer (opts) { function handle_multi_select_enabled ({ value }) { return { multi_select_enabled: value } } + + function handle_select_between_enabled ({ value }) { + return { select_between_enabled: value } + } } function inject_style ({ data }) { @@ -540,23 +562,6 @@ async function graph_explorer (opts) { return current_view } - function calculate_mobile_scale () { - const screen_width = window.innerWidth - const screen_height = window.innerHeight - const is_mobile = screen_width < 768 || screen_height < 600 - - if (is_mobile) { - // Scale proportionally based on screen size - const width_scale = Math.max(1.3, Math.min(2, 768 / screen_width)) - const height_scale = Math.max(1.3, Math.min(2, 600 / screen_height)) - scale_factor = Math.max(width_scale, height_scale) - } else { - scale_factor = 1 - } - - // Initialize the CSS variable - shadow.host.style.setProperty('--scale-factor', scale_factor) - } /****************************************************************************** 4. NODE CREATION AND EVENT HANDLING - `create_node` generates the DOM element for a single node. @@ -611,12 +616,15 @@ async function graph_explorer (opts) { if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') + if (last_clicked_node === instance_path) el.classList.add('last-clicked') const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 - if (depth) el.style.paddingLeft = `${17.5 * scale_factor}px` - el.style.height = `${node_height * scale_factor}px` + if (depth) { + el.classList.add('left-indent') + el.style.paddingLeft *= depth + } if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) @@ -629,24 +637,17 @@ async function graph_explorer (opts) { ? get_highlighted_name(entry_name, query) : entry_name - // Check if hub is alrady expanded elsewhere - const existing_expanded_instance = has_hubs && !state.expanded_hubs ? find_expanded_hub_instance(base_path, instance_path) : null - // Don't show `jump` button if entry is a expnded hub - const is_this_an_expanded_hub = is_hub && view.some(node => { - const node_state = instance_states[node.instance_path] - if (!node_state || !node_state.expanded_hubs) return false - const node_entry = all_entries[node.base_path] - return node_entry && Array.isArray(node_entry.hubs) && node_entry.hubs.includes(base_path) - }) - const is_duplicate_hub = existing_expanded_instance !== null && is_this_an_expanded_hub - const navigate_button_html = is_duplicate_hub ? '^' : '' + // Check if this entry appears elsewhere in the view (any duplicate) + collect_all_duplicate_entries() + const has_duplicate_entries = has_duplicates(base_path) + const navigate_button_html = has_duplicate_entries ? '^' : '' el.innerHTML = ` ${pipe_html} - ${name_html} ${navigate_button_html} + ${name_html} ` const icon_el = el.querySelector('.icon') @@ -657,13 +658,21 @@ async function graph_explorer (opts) { } const navigate_el = el.querySelector('.navigate-to-hub') - if (navigate_el) navigate_el.onclick = () => scroll_to_and_highlight_hub(base_path) + if (navigate_el) { + navigate_el.onclick = () => cycle_to_next_duplicate(base_path, instance_path) + } + + // Add click event to the whole first part (indent + prefix) for expanding/collapsing subs + if (has_subs) { + const indent_el = el.querySelector('.indent') + const prefix_el = el.querySelector('.prefix') - const prefix_el = el.querySelector('.prefix') - if (prefix_el && has_subs) { - prefix_el.onclick = mode === 'search' + const toggle_subs_handler = mode === 'search' ? () => toggle_search_subs(instance_path) : () => toggle_subs(instance_path) + + if (indent_el) indent_el.onclick = toggle_subs_handler + if (prefix_el) prefix_el.onclick = toggle_subs_handler } el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) @@ -753,8 +762,21 @@ async function graph_explorer (opts) { multi_select_button.innerHTML = `Multi Select: ${multi_select_enabled ? 'true' : 'false'}` multi_select_button.onclick = mode === 'search' ? null : toggle_multi_select - if (mode !== 'search') return menubar.replaceChildren(search_button, multi_select_button) + const select_between_button = document.createElement('button') + select_between_button.innerHTML = `Select Between: ${select_between_enabled ? 'true' : 'false'}` + select_between_button.onclick = mode === 'search' ? null : toggle_select_between + + menubar.replaceChildren(search_button, multi_select_button, select_between_button) + } + + function render_searchbar () { + if (mode !== 'search') { + searchbar.style.display = 'none' + searchbar.replaceChildren() + return + } + searchbar.style.display = 'flex' const search_input = Object.assign(document.createElement('input'), { type: 'text', placeholder: 'Search entries...', @@ -763,12 +785,13 @@ async function graph_explorer (opts) { oninput: on_search_input }) - menubar.replaceChildren(search_button, multi_select_button, search_input) + searchbar.replaceChildren(search_input) requestAnimationFrame(() => search_input.focus()) } function handle_mode_change () { menubar.style.display = mode === 'default' ? 'none' : 'flex' + render_searchbar() build_and_render_view() } @@ -784,10 +807,28 @@ async function graph_explorer (opts) { function toggle_multi_select () { multi_select_enabled = !multi_select_enabled + // Disable select between when enabling multi select + if (multi_select_enabled && select_between_enabled) { + select_between_enabled = false + select_between_first_node = null + update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: false }) + } update_drive_state({ dataset: 'mode', name: 'multi_select_enabled', value: multi_select_enabled }) render_menubar() // Re-render to update button text } + function toggle_select_between () { + select_between_enabled = !select_between_enabled + select_between_first_node = null // Reset first node selection + // Disable multi select when enabling select between + if (select_between_enabled && multi_select_enabled) { + multi_select_enabled = false + update_drive_state({ dataset: 'mode', name: 'multi_select_enabled', value: false }) + } + update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: select_between_enabled }) + render_menubar() // Re-render to update button text + } + function on_search_input (event) { search_query = event.target.value.trim() drive_updated_by_search = true @@ -973,8 +1014,22 @@ async function graph_explorer (opts) { toggling, and resetting the graph. ******************************************************************************/ function select_node (ev, instance_path) { + last_clicked_node = instance_path + update_drive_state({ dataset: 'runtime', name: 'last_clicked_node', value: instance_path }) + + // Handle shift+click to enable select between mode temporarily + if (ev.shiftKey && !select_between_enabled) { + select_between_enabled = true + select_between_first_node = null + update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: true }) + render_menubar() + } + const new_selected = new Set(selected_instance_paths) - if (ev.ctrlKey || multi_select_enabled) { + + if (select_between_enabled) { + handle_select_between(instance_path, new_selected) + } else if (ev.ctrlKey || multi_select_enabled) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) } else { @@ -982,6 +1037,34 @@ async function graph_explorer (opts) { } } + function handle_select_between (instance_path, new_selected) { + if (!select_between_first_node) { + select_between_first_node = instance_path + } else { + const first_index = view.findIndex(n => n.instance_path === select_between_first_node) + const second_index = view.findIndex(n => n.instance_path === instance_path) + + if (first_index !== -1 && second_index !== -1) { + const start_index = Math.min(first_index, second_index) + const end_index = Math.max(first_index, second_index) + + // Toggle selection for all nodes in the range + for (let i = start_index; i <= end_index; i++) { + const node_instance_path = view[i].instance_path + new_selected.has(node_instance_path) ? new_selected.delete(node_instance_path) : new_selected.add(node_instance_path) + } + + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) + } + + // Reset select between mode after second click + select_between_enabled = false + select_between_first_node = null + update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: false }) + render_menubar() + } + } + // Add the clicked entry and all its parents in the default tree function search_expand_into_default (target_instance_path) { if (!target_instance_path) return @@ -1189,51 +1272,65 @@ async function graph_explorer (opts) { /****************************************************************************** 8. HUB DUPLICATION PREVENTION ******************************************************************************/ - function find_expanded_hub_instance (target_base_path, exclude_instance_path) { - // Look through all nodes in the current view to find expanded hubs + + function collect_all_duplicate_entries () { + duplicate_entries_map = {} + const base_path_counts = {} for (const node of view) { - if (node.instance_path === exclude_instance_path) continue + if (!base_path_counts[node.base_path]) { + base_path_counts[node.base_path] = [] + } + base_path_counts[node.base_path].push(node.instance_path) + } + + // Store only duplicates + for (const [base_path, instance_paths] of Object.entries(base_path_counts)) { + if (instance_paths.length > 1) { + duplicate_entries_map[base_path] = instance_paths + } + } + } + + function get_next_duplicate_instance (base_path, current_instance_path) { + const duplicates = duplicate_entries_map[base_path] + if (!duplicates || duplicates.length <= 1) return null - const state = instance_states[node.instance_path] - if (!state || !state.expanded_hubs) continue + const current_index = duplicates.indexOf(current_instance_path) + if (current_index === -1) return duplicates[0] - const entry = all_entries[node.base_path] - if (!entry || !Array.isArray(entry.hubs)) continue + const next_index = (current_index + 1) % duplicates.length + return duplicates[next_index] + } - if (entry.hubs.includes(target_base_path)) return node.instance_path + function has_duplicates (base_path) { + return duplicate_entries_map[base_path] && duplicate_entries_map[base_path].length > 1 + } + + function cycle_to_next_duplicate (base_path, current_instance_path) { + const next_instance_path = get_next_duplicate_instance(base_path, current_instance_path) + if (next_instance_path) { + scroll_to_and_highlight_instance(next_instance_path) } - return null } - function scroll_to_and_highlight_hub (target_base_path) { - // Find the hub entry with the same base_path in the current view - const hub_index = view.findIndex(n => n.base_path === target_base_path) - if (hub_index === -1) return + function scroll_to_and_highlight_instance (target_instance_path) { + const target_index = view.findIndex(n => n.instance_path === target_instance_path) + if (target_index === -1) return // Calculate scroll position - const target_scroll_top = hub_index * node_height + const target_scroll_top = target_index * node_height container.scrollTop = target_scroll_top // Find and highlight the DOM element - const hub_instance_path = view[hub_index].instance_path - const hub_element = shadow.querySelector(`[data-instance_path="${CSS.escape(hub_instance_path)}"]`) - if (hub_element) { - hub_element.style.backgroundColor = 'pink' - hub_element.style.transition = 'background-color 0.3s ease' - // remove highlight after 2 seconds + const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(target_instance_path)}"]`) + if (target_element) { + target_element.classList.add('highlight-instance') setTimeout(() => { - hub_element.style.backgroundColor = '' - setTimeout(() => { - hub_element.style.transition = '' - }, 300) + target_element.classList.remove('highlight-instance') }, 2000) } } - function is_hub_already_expanded (base_path, exclude_instance_path) { - return find_expanded_hub_instance(base_path, exclude_instance_path) !== null - } - /****************************************************************************** 9. HELPER FUNCTIONS ******************************************************************************/ @@ -1243,9 +1340,9 @@ async function graph_explorer (opts) { // `(...)` creates a capturing group for the escaped query. // 'gi' flags: 'g' for global (all occurrences), 'i' for case-insensitive. const regex = new RegExp(`(${escape_regex(query)})`, 'gi') - // Replaces all matches of the regex in 'name' with the matched text wrapped in tags. + // Replaces all matches of the regex in 'name' with the matched text wrapped in search-match class. // '$1' refers to the content of the first capturing group (the matched query). - return name.replace(regex, '$1') + return name.replace(regex, '$1') } function escape_regex (string) { @@ -1420,13 +1517,15 @@ function fallback_module () { 'selected_instance_paths.json': { raw: '[]' }, 'confirmed_selected.json': { raw: '[]' }, 'instance_states.json': { raw: '{}' }, - 'search_entry_states.json': { raw: '{}' } + 'search_entry_states.json': { raw: '{}' }, + 'last_clicked_node.json': { raw: 'null' } }, 'mode/': { 'current_mode.json': { raw: '"menubar"' }, 'previous_mode.json': { raw: '"menubar"' }, 'search_query.json': { raw: '""' }, - 'multi_select_enabled.json': { raw: 'false' } + 'multi_select_enabled.json': { raw: 'false' }, + 'select_between_enabled.json': { raw: 'false' } } } } diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 47a5fad..8ff886c 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -617,7 +617,7 @@ async function graph_explorer (opts) { const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 - if (depth) { + if (depth) { el.classList.add('left-indent') el.style.paddingLeft *= depth } @@ -1012,7 +1012,7 @@ async function graph_explorer (opts) { function select_node (ev, instance_path) { last_clicked_node = instance_path update_drive_state({ dataset: 'runtime', name: 'last_clicked_node', value: instance_path }) - + // Handle shift+click to enable select between mode temporarily if (ev.shiftKey && !select_between_enabled) { select_between_enabled = true @@ -1020,9 +1020,9 @@ async function graph_explorer (opts) { update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: true }) render_menubar() } - + const new_selected = new Set(selected_instance_paths) - + if (select_between_enabled) { handle_select_between(instance_path, new_selected) } else if (ev.ctrlKey || multi_select_enabled) { @@ -1039,20 +1039,20 @@ async function graph_explorer (opts) { } else { const first_index = view.findIndex(n => n.instance_path === select_between_first_node) const second_index = view.findIndex(n => n.instance_path === instance_path) - + if (first_index !== -1 && second_index !== -1) { const start_index = Math.min(first_index, second_index) const end_index = Math.max(first_index, second_index) - + // Toggle selection for all nodes in the range for (let i = start_index; i <= end_index; i++) { const node_instance_path = view[i].instance_path new_selected.has(node_instance_path) ? new_selected.delete(node_instance_path) : new_selected.add(node_instance_path) } - + update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) } - + // Reset select between mode after second click select_between_enabled = false select_between_first_node = null From 73dbe0a763bf2496bbd5c2977a688ff2ac9047af Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 14 Sep 2025 17:18:19 +0500 Subject: [PATCH 075/130] Fixed the mapping & admin errors --- lib/graph_explorer.js | 3 ++- package.json | 4 ++++ web/page.js | 11 +++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 8ff886c..9117f04 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -32,6 +32,7 @@ async function graph_explorer (opts) { let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key let select_between_enabled = false // Flag to enable select between mode let select_between_first_node = null // First node selected in select between mode + let duplicate_entries_map = {} let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 @@ -1346,7 +1347,7 @@ async function graph_explorer (opts) { // It replaces characters like -, /, \, ^, $, *, +, ?, ., (, ), |, [, ], {, } // with their escaped versions (e.g., '.' becomes '\.'). // This prevents them from being interpreted as regex metacharacters. - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char + return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char } function check_and_reset_feedback_flags () { diff --git a/package.json b/package.json index 7d91990..7c3048c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,10 @@ "standardx": "^7.0.0" }, "eslintConfig": { + "env": { + "browser": true, + "es2021": true + }, "rules": { "camelcase": 0, "indent": [ diff --git a/web/page.js b/web/page.js index ef16bda..9a17672 100644 --- a/web/page.js +++ b/web/page.js @@ -1,5 +1,9 @@ const STATE = require('../lib/STATE') const statedb = STATE(__filename) +const admin_api = statedb.admin() +admin_api.on(event => { + console.log(event) +}) const { sdb } = statedb(fallback_module) /****************************************************************************** @@ -74,7 +78,7 @@ function fallback_module () { $: '', 0: '', mapping: { - style: 'style', + style: 'theme', entries: 'entries', runtime: 'runtime', mode: 'mode' @@ -83,7 +87,10 @@ function fallback_module () { }, drive: { 'theme/': { 'style.css': { raw: "body { font-family: 'system-ui'; }" } }, - 'lang/': {} + 'lang/': {}, + 'entries/': {}, + 'runtime/': {}, + 'mode/': {} } } } From e73bd4d68eb9811cfdcf15acf707b621399750a0 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 14 Sep 2025 18:48:35 +0500 Subject: [PATCH 076/130] Added color for matching entries and changed logic for appearance of ^ button --- lib/entries.json | 2 +- lib/graph_explorer.js | 69 ++++++++++++++++++++++++++++--------------- lib/theme.css | 4 +++ 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/lib/entries.json b/lib/entries.json index 132978a..26ea45a 100644 --- a/lib/entries.json +++ b/lib/entries.json @@ -15,7 +15,7 @@ "/pins": { "name": "pins", "type": "folder", - "subs": [], + "subs": ["/data/themes"], "hubs": [ "/" ] diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 9117f04..bb4a5d1 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -637,43 +637,46 @@ async function graph_explorer (opts) { // Check if this entry appears elsewhere in the view (any duplicate) collect_all_duplicate_entries() const has_duplicate_entries = has_duplicates(base_path) - const navigate_button_html = has_duplicate_entries ? '^' : '' + + // coloring class for duplicates + if (has_duplicate_entries) { + el.classList.add('matching-entry') + } el.innerHTML = ` ${pipe_html} - ${navigate_button_html} - ${name_html} + ${name_html} ` - const icon_el = el.querySelector('.icon') - if (icon_el && has_hubs && base_path !== '/') { - icon_el.onclick = mode === 'search' - ? () => toggle_search_hubs(instance_path) - : () => toggle_hubs(instance_path) - } + // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate + if (has_duplicate_entries) { + el.onclick = () => add_jump_button_to_matching_entry(el, base_path, instance_path) + } else { + const icon_el = el.querySelector('.icon') + if (icon_el && has_hubs && base_path !== '/') { + icon_el.onclick = mode === 'search' + ? () => toggle_search_hubs(instance_path) + : () => toggle_hubs(instance_path) + } - const navigate_el = el.querySelector('.navigate-to-hub') - if (navigate_el) { - navigate_el.onclick = () => cycle_to_next_duplicate(base_path, instance_path) - } + // Add click event to the whole first part (indent + prefix) for expanding/collapsing subs + if (has_subs) { + const indent_el = el.querySelector('.indent') + const prefix_el = el.querySelector('.prefix') - // Add click event to the whole first part (indent + prefix) for expanding/collapsing subs - if (has_subs) { - const indent_el = el.querySelector('.indent') - const prefix_el = el.querySelector('.prefix') + const toggle_subs_handler = mode === 'search' + ? () => toggle_search_subs(instance_path) + : () => toggle_subs(instance_path) - const toggle_subs_handler = mode === 'search' - ? () => toggle_search_subs(instance_path) - : () => toggle_subs(instance_path) + if (indent_el) indent_el.onclick = toggle_subs_handler + if (prefix_el) prefix_el.onclick = toggle_subs_handler + } - if (indent_el) indent_el.onclick = toggle_subs_handler - if (prefix_el) prefix_el.onclick = toggle_subs_handler + el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) } - el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) - if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) return el @@ -1310,6 +1313,24 @@ async function graph_explorer (opts) { } } + function add_jump_button_to_matching_entry (el, base_path, instance_path) { + // Check if jump button already exists + if (el.querySelector('.navigate-to-hub')) return + + const navigate_button = document.createElement('span') + navigate_button.className = 'navigate-to-hub clickable' + navigate_button.textContent = '^' + navigate_button.onclick = (event) => { + event.stopPropagation() // Prevent triggering the whole entry click again + cycle_to_next_duplicate(base_path, instance_path) + } + + const name_el = el.querySelector('.name') + if (name_el) { + el.insertBefore(navigate_button, name_el) + } + } + function scroll_to_and_highlight_instance (target_instance_path) { const target_index = view.findIndex(n => n.instance_path === target_instance_path) if (target_index === -1) return diff --git a/lib/theme.css b/lib/theme.css index 39b62f2..b78c655 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -120,6 +120,10 @@ background-color: pink; transition: background-color 0.3s ease; } +.matching-entry .name { + color: #e06c75 !important; + font-weight: bold; +} /* Base sizes for desktop (768px and above) */ .left-indent { From d5295f963623a7c745ac9b856b4c2f4fb32c2dca Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 14 Sep 2025 19:28:00 +0500 Subject: [PATCH 077/130] change the position of ^ button to wand column --- lib/graph_explorer.js | 18 ++++++++++++++---- lib/theme.css | 8 +++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index bb4a5d1..4345496 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -1317,6 +1317,15 @@ async function graph_explorer (opts) { // Check if jump button already exists if (el.querySelector('.navigate-to-hub')) return + // Get current left padding value to match the width + const computedStyle = window.getComputedStyle(el) + const leftPadding = computedStyle.paddingLeft + + // Create a div to replace the left padding + const indent_button_div = document.createElement('div') + indent_button_div.className = 'indent-btn-container' + indent_button_div.style.width = leftPadding + const navigate_button = document.createElement('span') navigate_button.className = 'navigate-to-hub clickable' navigate_button.textContent = '^' @@ -1325,10 +1334,11 @@ async function graph_explorer (opts) { cycle_to_next_duplicate(base_path, instance_path) } - const name_el = el.querySelector('.name') - if (name_el) { - el.insertBefore(navigate_button, name_el) - } + indent_button_div.appendChild(navigate_button) + + // Remove left padding + el.classList.remove('left-indent') + el.insertBefore(indent_button_div, el.firstChild) } function scroll_to_and_highlight_instance (target_instance_path) { diff --git a/lib/theme.css b/lib/theme.css index b78c655..19d1e94 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -58,6 +58,11 @@ .indent { display: flex; } +.indent-btn-container { + display: flex; + align-items: center; + justify-content: center; +} .pipe { text-align: center; } @@ -98,7 +103,8 @@ .node.type-js-file > .icon::before { content: '📜'; } .node.type-css-file > .icon::before { content: '🎨'; } .node.type-json-file > .icon::before { content: '📝'; } -.node.type-file > .icon::before { content: '📄'; }.navigate-to-hub { +.node.type-file > .icon::before { content: '📄'; } +.navigate-to-hub { margin-left: 5px; margin-right: 5px; padding: 2px 4px; From d37c2a08436ad62d4c403eeacb64da28d9d06073 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 14 Sep 2025 20:02:30 +0500 Subject: [PATCH 078/130] disable prevent duplicate in search mode & bar styling --- lib/graph_explorer.js | 26 +++++++++++++++----------- lib/theme.css | 3 ++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 4345496..72794f9 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -635,12 +635,15 @@ async function graph_explorer (opts) { : entry_name // Check if this entry appears elsewhere in the view (any duplicate) - collect_all_duplicate_entries() - const has_duplicate_entries = has_duplicates(base_path) - - // coloring class for duplicates - if (has_duplicate_entries) { - el.classList.add('matching-entry') + let has_duplicate_entries = false + if (mode !== 'search') { // disabled in search mode + collect_all_duplicate_entries() + has_duplicate_entries = has_duplicates(base_path) + + // coloring class for duplicates + if (has_duplicate_entries) { + el.classList.add('matching-entry') + } } el.innerHTML = ` @@ -651,7 +654,7 @@ async function graph_explorer (opts) { ` // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate - if (has_duplicate_entries) { + if (has_duplicate_entries && mode !== 'search') { el.onclick = () => add_jump_button_to_matching_entry(el, base_path, instance_path) } else { const icon_el = el.querySelector('.icon') @@ -776,14 +779,15 @@ async function graph_explorer (opts) { return } - searchbar.style.display = 'flex' - const search_input = Object.assign(document.createElement('input'), { + const search_opts = { type: 'text', placeholder: 'Search entries...', className: 'search-input', value: search_query, oninput: on_search_input - }) + } + searchbar.style.display = 'flex' + const search_input = Object.assign(document.createElement('input'), search_opts) searchbar.replaceChildren(search_input) requestAnimationFrame(() => search_input.focus()) @@ -1270,7 +1274,7 @@ async function graph_explorer (opts) { } /****************************************************************************** - 8. HUB DUPLICATION PREVENTION + 8. ENTRY DUPLICATION PREVENTION ******************************************************************************/ function collect_all_duplicate_entries () { diff --git a/lib/theme.css b/lib/theme.css index 19d1e94..7c4fd5a 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -46,10 +46,11 @@ border-bottom: 1px solid #181a1f; } .search-input { - margin-left: auto; background-color: #282c34; color: #abb2bf; border: 1px solid #181a1f; + display: flex; + width: 100%; } .confirm-wrapper { margin-left: auto; From 5f318232a78996e6a63ea72aa74c8fa1ae84afbd Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 14 Sep 2025 20:32:10 +0500 Subject: [PATCH 079/130] Added flag for hubs --- lib/graph_explorer.js | 40 ++++++++++++++++++++++++++++++++++++---- web/page.js | 8 +++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 72794f9..56f3676 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -26,6 +26,7 @@ async function graph_explorer (opts) { let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. let previous_mode let search_query = '' + let hubs_flag = 'default' // Flag for hubs behavior: 'default' (prevent duplication), 'true' (no duplication prevention), 'false' (disable hubs) let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. @@ -76,7 +77,8 @@ async function graph_explorer (opts) { entries: on_entries, style: inject_style, runtime: on_runtime, - mode: on_mode + mode: on_mode, + flags: on_flags } // Start watching for state changes. This is the main trigger for all updates. await sdb.watch(onbatch) @@ -315,6 +317,33 @@ async function graph_explorer (opts) { } } + function on_flags ({ data, paths }) { + const on_flags_paths = { + 'hubs.json': handle_hubs_flag + } + + paths.forEach((path, i) => { + const value = parse_json_data(data[i], path) + if (value === null) return + + const filename = path.split('/').pop() + const handler = on_flags_paths[filename] + if (handler) { + const result = handler(value) + if (result && result.needs_render) build_and_render_view() + } + }) + + function handle_hubs_flag (value) { + if (typeof value === 'string' && ['default', 'true', 'false'].includes(value)) { + hubs_flag = value + return { needs_render: true } + } else { + console.warn('hubs flag must be one of: "default", "true", "false", ignoring.', value) + } + } + } + function inject_style ({ data }) { const sheet = new CSSStyleSheet() sheet.replaceSync(data[0]) @@ -615,7 +644,7 @@ async function graph_explorer (opts) { if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') if (last_clicked_node === instance_path) el.classList.add('last-clicked') - const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 + const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 if (depth) { @@ -636,7 +665,7 @@ async function graph_explorer (opts) { // Check if this entry appears elsewhere in the view (any duplicate) let has_duplicate_entries = false - if (mode !== 'search') { // disabled in search mode + if (mode !== 'search' && hubs_flag !== 'true') { // disabled in search mode and when hubs_flag is 'true' collect_all_duplicate_entries() has_duplicate_entries = has_duplicates(base_path) @@ -654,7 +683,7 @@ async function graph_explorer (opts) { ` // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate - if (has_duplicate_entries && mode !== 'search') { + if (has_duplicate_entries && mode !== 'search' && hubs_flag !== 'true') { el.onclick = () => add_jump_button_to_matching_entry(el, base_path, instance_path) } else { const icon_el = el.querySelector('.icon') @@ -1558,6 +1587,9 @@ function fallback_module () { 'search_query.json': { raw: '""' }, 'multi_select_enabled.json': { raw: 'false' }, 'select_between_enabled.json': { raw: 'false' } + }, + 'flags/': { + 'hubs.json': { raw: '"default"' } } } } diff --git a/web/page.js b/web/page.js index 9a17672..215e283 100644 --- a/web/page.js +++ b/web/page.js @@ -2,7 +2,7 @@ const STATE = require('../lib/STATE') const statedb = STATE(__filename) const admin_api = statedb.admin() admin_api.on(event => { - console.log(event) + // console.log(event) }) const { sdb } = statedb(fallback_module) @@ -81,7 +81,8 @@ function fallback_module () { style: 'theme', entries: 'entries', runtime: 'runtime', - mode: 'mode' + mode: 'mode', + flags: 'flags' } } }, @@ -90,7 +91,8 @@ function fallback_module () { 'lang/': {}, 'entries/': {}, 'runtime/': {}, - 'mode/': {} + 'mode/': {}, + 'flags/': {} } } } From e41fb79360efb85e032efa572eca626ba8117a10 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 14 Sep 2025 20:57:31 +0500 Subject: [PATCH 080/130] use {type, message} inside update_drive_state --- lib/graph_explorer.js | 67 ++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 56f3676..d7f9952 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -289,7 +289,7 @@ async function graph_explorer (opts) { } if (!new_current_mode || mode === new_current_mode) return - if (mode && new_current_mode === 'search') update_drive_state({ dataset: 'mode', name: 'previous_mode', value: mode }) + if (mode && new_current_mode === 'search') update_drive_state({ type: 'mode/previous_mode', message: mode }) mode = new_current_mode render_menubar() render_searchbar() @@ -351,10 +351,11 @@ async function graph_explorer (opts) { } // Helper to persist component state to the drive. - async function update_drive_state ({ dataset, name, value }) { + async function update_drive_state ({ type, message }) { try { - await drive.put(`${dataset}/${name}.json`, JSON.stringify(value)) + await drive.put(`${type}.json`, JSON.stringify(message)) } catch (e) { + const [dataset, name] = type.split('/') console.error(`Failed to update ${dataset} state for ${name}:`, e) } } @@ -832,9 +833,9 @@ async function graph_explorer (opts) { if (mode === 'search') { search_query = '' drive_updated_by_search = true - update_drive_state({ dataset: 'mode', name: 'search_query', value: '' }) + update_drive_state({ type: 'mode/search_query', message: '' }) } - update_drive_state({ dataset: 'mode', name: 'current_mode', value: mode === 'search' ? previous_mode : 'search' }) + update_drive_state({ type: 'mode/current_mode', message: mode === 'search' ? previous_mode : 'search' }) search_state_instances = instance_states } @@ -844,9 +845,9 @@ async function graph_explorer (opts) { if (multi_select_enabled && select_between_enabled) { select_between_enabled = false select_between_first_node = null - update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: false }) + update_drive_state({ type: 'mode/select_between_enabled', message: false }) } - update_drive_state({ dataset: 'mode', name: 'multi_select_enabled', value: multi_select_enabled }) + update_drive_state({ type: 'mode/multi_select_enabled', message: multi_select_enabled }) render_menubar() // Re-render to update button text } @@ -856,16 +857,16 @@ async function graph_explorer (opts) { // Disable multi select when enabling select between if (select_between_enabled && multi_select_enabled) { multi_select_enabled = false - update_drive_state({ dataset: 'mode', name: 'multi_select_enabled', value: false }) + update_drive_state({ type: 'mode/multi_select_enabled', message: false }) } - update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: select_between_enabled }) + update_drive_state({ type: 'mode/select_between_enabled', message: select_between_enabled }) render_menubar() // Re-render to update button text } function on_search_input (event) { search_query = event.target.value.trim() drive_updated_by_search = true - update_drive_state({ dataset: 'mode', name: 'search_query', value: search_query }) + update_drive_state({ type: 'mode/search_query', message: search_query }) if (search_query === '') search_state_instances = instance_states perform_search(search_query) } @@ -1048,13 +1049,13 @@ async function graph_explorer (opts) { ******************************************************************************/ function select_node (ev, instance_path) { last_clicked_node = instance_path - update_drive_state({ dataset: 'runtime', name: 'last_clicked_node', value: instance_path }) + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) // Handle shift+click to enable select between mode temporarily if (ev.shiftKey && !select_between_enabled) { select_between_enabled = true select_between_first_node = null - update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: true }) + update_drive_state({ type: 'mode/select_between_enabled', message: true }) render_menubar() } @@ -1064,9 +1065,9 @@ async function graph_explorer (opts) { handle_select_between(instance_path, new_selected) } else if (ev.ctrlKey || multi_select_enabled) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) } else { - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [instance_path] }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [instance_path] }) } } @@ -1087,13 +1088,13 @@ async function graph_explorer (opts) { new_selected.has(node_instance_path) ? new_selected.delete(node_instance_path) : new_selected.add(node_instance_path) } - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) } // Reset select between mode after second click select_between_enabled = false select_between_first_node = null - update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: false }) + update_drive_state({ type: 'mode/select_between_enabled', message: false }) render_menubar() } } @@ -1120,12 +1121,12 @@ async function graph_explorer (opts) { } // Persist selection and expansion state - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [target_instance_path] }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [target_instance_path] }) drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) search_query = '' - update_drive_state({ dataset: 'mode', name: 'query', value: '' }) - update_drive_state({ dataset: 'mode', name: 'current_mode', value: previous_mode }) + update_drive_state({ type: 'mode/query', message: '' }) + update_drive_state({ type: 'mode/current_mode', message: previous_mode }) } function handle_confirm (ev, instance_path) { @@ -1142,8 +1143,8 @@ async function graph_explorer (opts) { new_confirmed.delete(instance_path) } - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) - update_drive_state({ dataset: 'runtime', name: 'confirmed_selected', value: [...new_confirmed] }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) + update_drive_state({ type: 'runtime/confirmed_selected', message: [...new_confirmed] }) } function toggle_subs (instance_path) { @@ -1152,7 +1153,7 @@ async function graph_explorer (opts) { build_and_render_view(instance_path) // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) } function toggle_hubs (instance_path) { @@ -1161,7 +1162,7 @@ async function graph_explorer (opts) { state.expanded_hubs = !state.expanded_hubs build_and_render_view(instance_path, true) drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) } function toggle_search_subs (instance_path) { @@ -1169,7 +1170,7 @@ async function graph_explorer (opts) { state.expanded_subs = !state.expanded_subs perform_search(search_query) // Re-render search results with new state drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) } function toggle_search_hubs (instance_path) { @@ -1177,7 +1178,7 @@ async function graph_explorer (opts) { state.expanded_hubs = !state.expanded_hubs perform_search(search_query) // Re-render search results with new state drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) } function reset () { @@ -1185,7 +1186,7 @@ async function graph_explorer (opts) { if (mode === 'search') { search_entry_states = {} drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) perform_search(search_query) return } @@ -1193,11 +1194,11 @@ async function graph_explorer (opts) { const new_instance_states = { [root_instance_path]: { expanded_subs: true, expanded_hubs: false } } - update_drive_state({ dataset: 'runtime', name: 'vertical_scroll_value', value: 0 }) - update_drive_state({ dataset: 'runtime', name: 'horizontal_scroll_value', value: 0 }) - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [] }) - update_drive_state({ dataset: 'runtime', name: 'confirmed_selected', value: [] }) - update_drive_state({ dataset: 'runtime', name: 'instance_states', value: new_instance_states }) + update_drive_state({ type: 'runtime/vertical_scroll_value', message: 0 }) + update_drive_state({ type: 'runtime/horizontal_scroll_value', message: 0 }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [] }) + update_drive_state({ type: 'runtime/confirmed_selected', message: [] }) + update_drive_state({ type: 'runtime/instance_states', message: new_instance_states }) } /****************************************************************************** @@ -1534,7 +1535,7 @@ async function graph_explorer (opts) { function update_scroll_state ({ current_value, new_value, name }) { if (current_value !== new_value) { drive_updated_by_scroll = true // Set flag to prevent render loop. - update_drive_state({ dataset: 'runtime', name, value: new_value }) + update_drive_state({ type: `runtime/${name}`, message: new_value }) return new_value } return current_value From 4e687b11e764bade911ecee721c00f439a025ea7 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 14 Sep 2025 22:52:43 +0500 Subject: [PATCH 081/130] bundled --- bundle.js | 216 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 147 insertions(+), 69 deletions(-) diff --git a/bundle.js b/bundle.js index baacff1..52cc6d2 100644 --- a/bundle.js +++ b/bundle.js @@ -30,12 +30,14 @@ async function graph_explorer (opts) { let mode // Current mode of the graph explorer, can be set to 'default', 'menubar' or 'search'. Its value should be set by the `mode` file in the drive. let previous_mode let search_query = '' + let hubs_flag = 'default' // Flag for hubs behavior: 'default' (prevent duplication), 'true' (no duplication prevention), 'false' (disable hubs) let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key let select_between_enabled = false // Flag to enable select between mode let select_between_first_node = null // First node selected in select between mode + let duplicate_entries_map = {} let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 @@ -79,7 +81,8 @@ async function graph_explorer (opts) { entries: on_entries, style: inject_style, runtime: on_runtime, - mode: on_mode + mode: on_mode, + flags: on_flags } // Start watching for state changes. This is the main trigger for all updates. await sdb.watch(onbatch) @@ -290,7 +293,7 @@ async function graph_explorer (opts) { } if (!new_current_mode || mode === new_current_mode) return - if (mode && new_current_mode === 'search') update_drive_state({ dataset: 'mode', name: 'previous_mode', value: mode }) + if (mode && new_current_mode === 'search') update_drive_state({ type: 'mode/previous_mode', message: mode }) mode = new_current_mode render_menubar() render_searchbar() @@ -318,6 +321,33 @@ async function graph_explorer (opts) { } } + function on_flags ({ data, paths }) { + const on_flags_paths = { + 'hubs.json': handle_hubs_flag + } + + paths.forEach((path, i) => { + const value = parse_json_data(data[i], path) + if (value === null) return + + const filename = path.split('/').pop() + const handler = on_flags_paths[filename] + if (handler) { + const result = handler(value) + if (result && result.needs_render) build_and_render_view() + } + }) + + function handle_hubs_flag (value) { + if (typeof value === 'string' && ['default', 'true', 'false'].includes(value)) { + hubs_flag = value + return { needs_render: true } + } else { + console.warn('hubs flag must be one of: "default", "true", "false", ignoring.', value) + } + } + } + function inject_style ({ data }) { const sheet = new CSSStyleSheet() sheet.replaceSync(data[0]) @@ -325,10 +355,11 @@ async function graph_explorer (opts) { } // Helper to persist component state to the drive. - async function update_drive_state ({ dataset, name, value }) { + async function update_drive_state ({ type, message }) { try { - await drive.put(`${dataset}/${name}.json`, JSON.stringify(value)) + await drive.put(`${type}.json`, JSON.stringify(message)) } catch (e) { + const [dataset, name] = type.split('/') console.error(`Failed to update ${dataset} state for ${name}:`, e) } } @@ -618,7 +649,7 @@ async function graph_explorer (opts) { if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') if (last_clicked_node === instance_path) el.classList.add('last-clicked') - const has_hubs = Array.isArray(entry.hubs) && entry.hubs.length > 0 + const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 if (depth) { @@ -638,45 +669,51 @@ async function graph_explorer (opts) { : entry_name // Check if this entry appears elsewhere in the view (any duplicate) - collect_all_duplicate_entries() - const has_duplicate_entries = has_duplicates(base_path) - const navigate_button_html = has_duplicate_entries ? '^' : '' + let has_duplicate_entries = false + if (mode !== 'search' && hubs_flag !== 'true') { // disabled in search mode and when hubs_flag is 'true' + collect_all_duplicate_entries() + has_duplicate_entries = has_duplicates(base_path) + + // coloring class for duplicates + if (has_duplicate_entries) { + el.classList.add('matching-entry') + } + } el.innerHTML = ` ${pipe_html} - ${navigate_button_html} - ${name_html} + ${name_html} ` - const icon_el = el.querySelector('.icon') - if (icon_el && has_hubs && base_path !== '/') { - icon_el.onclick = mode === 'search' - ? () => toggle_search_hubs(instance_path) - : () => toggle_hubs(instance_path) - } + // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate + if (has_duplicate_entries && mode !== 'search' && hubs_flag !== 'true') { + el.onclick = () => add_jump_button_to_matching_entry(el, base_path, instance_path) + } else { + const icon_el = el.querySelector('.icon') + if (icon_el && has_hubs && base_path !== '/') { + icon_el.onclick = mode === 'search' + ? () => toggle_search_hubs(instance_path) + : () => toggle_hubs(instance_path) + } - const navigate_el = el.querySelector('.navigate-to-hub') - if (navigate_el) { - navigate_el.onclick = () => cycle_to_next_duplicate(base_path, instance_path) - } + // Add click event to the whole first part (indent + prefix) for expanding/collapsing subs + if (has_subs) { + const indent_el = el.querySelector('.indent') + const prefix_el = el.querySelector('.prefix') - // Add click event to the whole first part (indent + prefix) for expanding/collapsing subs - if (has_subs) { - const indent_el = el.querySelector('.indent') - const prefix_el = el.querySelector('.prefix') + const toggle_subs_handler = mode === 'search' + ? () => toggle_search_subs(instance_path) + : () => toggle_subs(instance_path) - const toggle_subs_handler = mode === 'search' - ? () => toggle_search_subs(instance_path) - : () => toggle_subs(instance_path) + if (indent_el) indent_el.onclick = toggle_subs_handler + if (prefix_el) prefix_el.onclick = toggle_subs_handler + } - if (indent_el) indent_el.onclick = toggle_subs_handler - if (prefix_el) prefix_el.onclick = toggle_subs_handler + el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) } - el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) - if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) return el @@ -776,14 +813,15 @@ async function graph_explorer (opts) { return } - searchbar.style.display = 'flex' - const search_input = Object.assign(document.createElement('input'), { + const search_opts = { type: 'text', placeholder: 'Search entries...', className: 'search-input', value: search_query, oninput: on_search_input - }) + } + searchbar.style.display = 'flex' + const search_input = Object.assign(document.createElement('input'), search_opts) searchbar.replaceChildren(search_input) requestAnimationFrame(() => search_input.focus()) @@ -799,9 +837,9 @@ async function graph_explorer (opts) { if (mode === 'search') { search_query = '' drive_updated_by_search = true - update_drive_state({ dataset: 'mode', name: 'search_query', value: '' }) + update_drive_state({ type: 'mode/search_query', message: '' }) } - update_drive_state({ dataset: 'mode', name: 'current_mode', value: mode === 'search' ? previous_mode : 'search' }) + update_drive_state({ type: 'mode/current_mode', message: mode === 'search' ? previous_mode : 'search' }) search_state_instances = instance_states } @@ -811,9 +849,9 @@ async function graph_explorer (opts) { if (multi_select_enabled && select_between_enabled) { select_between_enabled = false select_between_first_node = null - update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: false }) + update_drive_state({ type: 'mode/select_between_enabled', message: false }) } - update_drive_state({ dataset: 'mode', name: 'multi_select_enabled', value: multi_select_enabled }) + update_drive_state({ type: 'mode/multi_select_enabled', message: multi_select_enabled }) render_menubar() // Re-render to update button text } @@ -823,16 +861,16 @@ async function graph_explorer (opts) { // Disable multi select when enabling select between if (select_between_enabled && multi_select_enabled) { multi_select_enabled = false - update_drive_state({ dataset: 'mode', name: 'multi_select_enabled', value: false }) + update_drive_state({ type: 'mode/multi_select_enabled', message: false }) } - update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: select_between_enabled }) + update_drive_state({ type: 'mode/select_between_enabled', message: select_between_enabled }) render_menubar() // Re-render to update button text } function on_search_input (event) { search_query = event.target.value.trim() drive_updated_by_search = true - update_drive_state({ dataset: 'mode', name: 'search_query', value: search_query }) + update_drive_state({ type: 'mode/search_query', message: search_query }) if (search_query === '') search_state_instances = instance_states perform_search(search_query) } @@ -1015,13 +1053,13 @@ async function graph_explorer (opts) { ******************************************************************************/ function select_node (ev, instance_path) { last_clicked_node = instance_path - update_drive_state({ dataset: 'runtime', name: 'last_clicked_node', value: instance_path }) + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) // Handle shift+click to enable select between mode temporarily if (ev.shiftKey && !select_between_enabled) { select_between_enabled = true select_between_first_node = null - update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: true }) + update_drive_state({ type: 'mode/select_between_enabled', message: true }) render_menubar() } @@ -1031,9 +1069,9 @@ async function graph_explorer (opts) { handle_select_between(instance_path, new_selected) } else if (ev.ctrlKey || multi_select_enabled) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) } else { - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [instance_path] }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [instance_path] }) } } @@ -1054,13 +1092,13 @@ async function graph_explorer (opts) { new_selected.has(node_instance_path) ? new_selected.delete(node_instance_path) : new_selected.add(node_instance_path) } - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) } // Reset select between mode after second click select_between_enabled = false select_between_first_node = null - update_drive_state({ dataset: 'mode', name: 'select_between_enabled', value: false }) + update_drive_state({ type: 'mode/select_between_enabled', message: false }) render_menubar() } } @@ -1087,12 +1125,12 @@ async function graph_explorer (opts) { } // Persist selection and expansion state - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [target_instance_path] }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [target_instance_path] }) drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) search_query = '' - update_drive_state({ dataset: 'mode', name: 'query', value: '' }) - update_drive_state({ dataset: 'mode', name: 'current_mode', value: previous_mode }) + update_drive_state({ type: 'mode/query', message: '' }) + update_drive_state({ type: 'mode/current_mode', message: previous_mode }) } function handle_confirm (ev, instance_path) { @@ -1109,8 +1147,8 @@ async function graph_explorer (opts) { new_confirmed.delete(instance_path) } - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [...new_selected] }) - update_drive_state({ dataset: 'runtime', name: 'confirmed_selected', value: [...new_confirmed] }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) + update_drive_state({ type: 'runtime/confirmed_selected', message: [...new_confirmed] }) } function toggle_subs (instance_path) { @@ -1119,7 +1157,7 @@ async function graph_explorer (opts) { build_and_render_view(instance_path) // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) } function toggle_hubs (instance_path) { @@ -1128,7 +1166,7 @@ async function graph_explorer (opts) { state.expanded_hubs = !state.expanded_hubs build_and_render_view(instance_path, true) drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'instance_states', value: instance_states }) + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) } function toggle_search_subs (instance_path) { @@ -1136,7 +1174,7 @@ async function graph_explorer (opts) { state.expanded_subs = !state.expanded_subs perform_search(search_query) // Re-render search results with new state drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) } function toggle_search_hubs (instance_path) { @@ -1144,7 +1182,7 @@ async function graph_explorer (opts) { state.expanded_hubs = !state.expanded_hubs perform_search(search_query) // Re-render search results with new state drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) } function reset () { @@ -1152,7 +1190,7 @@ async function graph_explorer (opts) { if (mode === 'search') { search_entry_states = {} drive_updated_by_toggle = true - update_drive_state({ dataset: 'runtime', name: 'search_entry_states', value: search_entry_states }) + update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) perform_search(search_query) return } @@ -1160,11 +1198,11 @@ async function graph_explorer (opts) { const new_instance_states = { [root_instance_path]: { expanded_subs: true, expanded_hubs: false } } - update_drive_state({ dataset: 'runtime', name: 'vertical_scroll_value', value: 0 }) - update_drive_state({ dataset: 'runtime', name: 'horizontal_scroll_value', value: 0 }) - update_drive_state({ dataset: 'runtime', name: 'selected_instance_paths', value: [] }) - update_drive_state({ dataset: 'runtime', name: 'confirmed_selected', value: [] }) - update_drive_state({ dataset: 'runtime', name: 'instance_states', value: new_instance_states }) + update_drive_state({ type: 'runtime/vertical_scroll_value', message: 0 }) + update_drive_state({ type: 'runtime/horizontal_scroll_value', message: 0 }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [] }) + update_drive_state({ type: 'runtime/confirmed_selected', message: [] }) + update_drive_state({ type: 'runtime/instance_states', message: new_instance_states }) } /****************************************************************************** @@ -1270,7 +1308,7 @@ async function graph_explorer (opts) { } /****************************************************************************** - 8. HUB DUPLICATION PREVENTION + 8. ENTRY DUPLICATION PREVENTION ******************************************************************************/ function collect_all_duplicate_entries () { @@ -1313,6 +1351,34 @@ async function graph_explorer (opts) { } } + function add_jump_button_to_matching_entry (el, base_path, instance_path) { + // Check if jump button already exists + if (el.querySelector('.navigate-to-hub')) return + + // Get current left padding value to match the width + const computedStyle = window.getComputedStyle(el) + const leftPadding = computedStyle.paddingLeft + + // Create a div to replace the left padding + const indent_button_div = document.createElement('div') + indent_button_div.className = 'indent-btn-container' + indent_button_div.style.width = leftPadding + + const navigate_button = document.createElement('span') + navigate_button.className = 'navigate-to-hub clickable' + navigate_button.textContent = '^' + navigate_button.onclick = (event) => { + event.stopPropagation() // Prevent triggering the whole entry click again + cycle_to_next_duplicate(base_path, instance_path) + } + + indent_button_div.appendChild(navigate_button) + + // Remove left padding + el.classList.remove('left-indent') + el.insertBefore(indent_button_div, el.firstChild) + } + function scroll_to_and_highlight_instance (target_instance_path) { const target_index = view.findIndex(n => n.instance_path === target_instance_path) if (target_index === -1) return @@ -1350,7 +1416,7 @@ async function graph_explorer (opts) { // It replaces characters like -, /, \, ^, $, *, +, ?, ., (, ), |, [, ], {, } // with their escaped versions (e.g., '.' becomes '\.'). // This prevents them from being interpreted as regex metacharacters. - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char + return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char } function check_and_reset_feedback_flags () { @@ -1473,7 +1539,7 @@ async function graph_explorer (opts) { function update_scroll_state ({ current_value, new_value, name }) { if (current_value !== new_value) { drive_updated_by_scroll = true // Set flag to prevent render loop. - update_drive_state({ dataset: 'runtime', name, value: new_value }) + update_drive_state({ type: `runtime/${name}`, message: new_value }) return new_value } return current_value @@ -1526,6 +1592,9 @@ function fallback_module () { 'search_query.json': { raw: '""' }, 'multi_select_enabled.json': { raw: 'false' }, 'select_between_enabled.json': { raw: 'false' } + }, + 'flags/': { + 'hubs.json': { raw: '"default"' } } } } @@ -1560,6 +1629,10 @@ fetch(init_url, fetch_opts) (function (__filename){(function (){ const STATE = require('../lib/STATE') const statedb = STATE(__filename) +const admin_api = statedb.admin() +admin_api.on(event => { + // console.log(event) +}) const { sdb } = statedb(fallback_module) /****************************************************************************** @@ -1634,16 +1707,21 @@ function fallback_module () { $: '', 0: '', mapping: { - style: 'style', + style: 'theme', entries: 'entries', runtime: 'runtime', - mode: 'mode' + mode: 'mode', + flags: 'flags' } } }, drive: { 'theme/': { 'style.css': { raw: "body { font-family: 'system-ui'; }" } }, - 'lang/': {} + 'lang/': {}, + 'entries/': {}, + 'runtime/': {}, + 'mode/': {}, + 'flags/': {} } } } From c3a43abf412c2ce1c5a6373723228a230cdbdaaf Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 20 Sep 2025 13:39:50 +0500 Subject: [PATCH 082/130] Removed `query` in js & !important in CSS --- lib/graph_explorer.js | 7 -- lib/theme.css | 234 ++++++++++++++++++++++-------------------- package.json | 3 +- 3 files changed, 123 insertions(+), 121 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index d7f9952..7fe9498 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -650,7 +650,6 @@ async function graph_explorer (opts) { if (depth) { el.classList.add('left-indent') - el.style.paddingLeft *= depth } if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) @@ -1351,14 +1350,8 @@ async function graph_explorer (opts) { // Check if jump button already exists if (el.querySelector('.navigate-to-hub')) return - // Get current left padding value to match the width - const computedStyle = window.getComputedStyle(el) - const leftPadding = computedStyle.paddingLeft - - // Create a div to replace the left padding const indent_button_div = document.createElement('div') indent_button_div.className = 'indent-btn-container' - indent_button_div.style.width = leftPadding const navigate_button = document.createElement('span') navigate_button.className = 'navigate-to-hub clickable' diff --git a/lib/theme.css b/lib/theme.css index 7c4fd5a..9566970 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -63,6 +63,7 @@ display: flex; align-items: center; justify-content: center; + width: 17.5px; } .pipe { text-align: center; @@ -128,18 +129,17 @@ transition: background-color 0.3s ease; } .matching-entry .name { - color: #e06c75 !important; + color: #e06c75; font-weight: bold; } /* Base sizes for desktop (768px and above) */ -.left-indent { - padding-left: 17.5px !important; -} .node { font-size: 14px; height: 16px; - padding-left: 0; +} +.left-indent { + padding-left: 17.5px; } .pipe, .blank { @@ -163,133 +163,143 @@ /* Medium screens (480px to 767px) - 1.3x scale */ @media (max-width: 767px) { .left-indent { - padding-left: 22.75px !important; + padding-left: 22.75px; } - .node { - font-size: 18px !important; - height: 21px !important; - } - - .pipe, .blank { - width: 11px !important; - font-size: 18px !important; - } - - .prefix, .icon { - font-size: 18px !important; - margin-right: 3px !important; - } - - .navigate-to-hub { - font-size: 16px !important; - padding: 3px 5px !important; - margin-left: 7px !important; - margin-right: 7px !important; - } - - .graph-container { - padding: 13px !important; - } - - .confirm-wrapper { - padding-left: 13px !important; - } -} - -/* Small screens (320px to 479px) - 1.6x scale */ -@media (max-width: 479px) { - .left-indent { - padding-left: 28px !important; + .indent-btn-container { + width: 22.75px; } .node { - font-size: 22px !important; - height: 26px !important; + font-size: 18px; + height: 21px; } .pipe, .blank { - width: 14px !important; - font-size: 22px !important; + width: 11px; + font-size: 18px; } .prefix, .icon { - font-size: 22px !important; - margin-right: 4px !important; + font-size: 18px; + margin-right: 3px; } .navigate-to-hub { - font-size: 19px !important; - padding: 4px 6px !important; - margin-left: 8px !important; - margin-right: 8px !important; + font-size: 16px; + padding: 3px 5px; + margin-left: 7px; + margin-right: 7px; } .graph-container { - padding: 16px !important; + padding: 13px; } .confirm-wrapper { - padding-left: 16px !important; + padding-left: 13px; } } - -/* Extra small screens (below 320px) - 2x scale */ -@media (max-width: 319px) { - .left-indent { - padding-left: 35px !important; + /* Height-based responsive adjustments for mobile experience */ + @media (max-height: 600px) { + .left-indent { + padding-left: 25px; + } + .indent-btn-container { + width: 25px; + } + .node { + font-size: 20px; + height: 24px; + } + + .pipe, .blank { + width: 12px; + font-size: 20px; + } + + .prefix, .icon { + font-size: 20px; + margin-right: 3px; + } + + .navigate-to-hub { + font-size: 18px; + padding: 3px 6px; + } } - .node { - font-size: 28px !important; - height: 32px !important; + /* Small screens (320px to 479px) - 1.6x scale */ + @media (max-width: 479px) { + .left-indent { + padding-left: 28px; + } + .indent-btn-container { + width: 28px; + } + .node { + font-size: 22px; + height: 26px; + } + + .pipe, .blank { + width: 14px; + font-size: 22px; + } + + .prefix, .icon { + font-size: 22px; + margin-right: 4px; + } + + .navigate-to-hub { + font-size: 19px; + padding: 4px 6px; + margin-left: 8px; + margin-right: 8px; + } + + .graph-container { + padding: 16px; + } + + .confirm-wrapper { + padding-left: 16px; + } } - - .pipe, .blank { - width: 17px !important; - font-size: 28px !important; - } - - .prefix, .icon { - font-size: 28px !important; - margin-right: 4px !important; - } - - .navigate-to-hub { - font-size: 24px !important; - padding: 4px 8px !important; - margin-left: 10px !important; - margin-right: 10px !important; - } - - .graph-container { - padding: 20px !important; - } - - .confirm-wrapper { - padding-left: 20px !important; - } -} -/* Height-based responsive adjustments for mobile experience */ -@media (max-height: 600px) { - .left-indent { - padding-left: 22.75px !important; - } - .node { - font-size: 20px !important; - height: 24px !important; - } - - .pipe, .blank { - width: 12px !important; - font-size: 20px !important; - } - - .prefix, .icon { - font-size: 20px !important; - margin-right: 3px !important; - } - - .navigate-to-hub { - font-size: 18px !important; - padding: 3px 6px !important; - } -} \ No newline at end of file + /* Extra small screens (below 320px) - 2x scale */ + @media (max-width: 319px) { + .left-indent { + padding-left: 35px; + } + .indent-btn-container { + width: 35px; + } + .node { + font-size: 28px; + height: 32px; + } + + .pipe, .blank { + width: 17px; + font-size: 28px; + } + + .prefix, .icon { + font-size: 28px; + margin-right: 4px; + } + + .navigate-to-hub { + font-size: 24px; + padding: 4px 8px; + margin-left: 10px; + margin-right: 10px; + } + + .graph-container { + padding: 20px; + } + + .confirm-wrapper { + padding-left: 20px; + } + } diff --git a/package.json b/package.json index 7c3048c..8ec33a8 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,7 @@ }, "eslintConfig": { "env": { - "browser": true, - "es2021": true + "browser": true }, "rules": { "camelcase": 0, From 0e4335dc8c2bac5464392c6df79d009481b0dc8e Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 20 Sep 2025 14:27:59 +0500 Subject: [PATCH 083/130] Move the Jump Button to next match on clicking --- lib/graph_explorer.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 7fe9498..05e4e11 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -1342,7 +1342,29 @@ async function graph_explorer (opts) { function cycle_to_next_duplicate (base_path, current_instance_path) { const next_instance_path = get_next_duplicate_instance(base_path, current_instance_path) if (next_instance_path) { + remove_jump_button_from_entry(current_instance_path) scroll_to_and_highlight_instance(next_instance_path) + + // Add jump button to the target entry + const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(next_instance_path)}"]`) + if (target_element) { + add_jump_button_to_matching_entry(target_element, base_path, next_instance_path) + } + } + } + + function remove_jump_button_from_entry (instance_path) { + const current_element = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (current_element) { + const button_container = current_element.querySelector('.indent-btn-container') + if (button_container) { + button_container.remove() + // Restore left-indent class + const node_data = view.find(n => n.instance_path === instance_path) + if (node_data && node_data.depth > 0) { + current_element.classList.add('left-indent') + } + } } } From 160334ab3de9f697412bac85e2d63f620a6f5f4e Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 20 Sep 2025 15:22:29 +0500 Subject: [PATCH 084/130] maintain the same vertical position on screen during jump --- lib/graph_explorer.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 05e4e11..383a0dd 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -1343,7 +1343,7 @@ async function graph_explorer (opts) { const next_instance_path = get_next_duplicate_instance(base_path, current_instance_path) if (next_instance_path) { remove_jump_button_from_entry(current_instance_path) - scroll_to_and_highlight_instance(next_instance_path) + scroll_to_and_highlight_instance(next_instance_path, current_instance_path) // Add jump button to the target entry const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(next_instance_path)}"]`) @@ -1390,12 +1390,23 @@ async function graph_explorer (opts) { el.insertBefore(indent_button_div, el.firstChild) } - function scroll_to_and_highlight_instance (target_instance_path) { + function scroll_to_and_highlight_instance (target_instance_path, source_instance_path = null) { const target_index = view.findIndex(n => n.instance_path === target_instance_path) if (target_index === -1) return // Calculate scroll position - const target_scroll_top = target_index * node_height + let target_scroll_top = target_index * node_height + + if (source_instance_path) { + const source_index = view.findIndex(n => n.instance_path === source_instance_path) + if (source_index !== -1) { + const source_scroll_top = source_index * node_height + const current_scroll_top = container.scrollTop + const source_visible_offset = source_scroll_top - current_scroll_top + target_scroll_top = target_scroll_top - source_visible_offset + } + } + container.scrollTop = target_scroll_top // Find and highlight the DOM element From 4d4333d856c081c89bf3677193322039cb8be790 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 20 Sep 2025 19:22:10 +0500 Subject: [PATCH 085/130] Finished the functionality for Last Clicked --- lib/graph_explorer.js | 88 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 383a0dd..594ff14 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -30,6 +30,7 @@ async function graph_explorer (opts) { let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. + let drive_updated_by_match = false // Flag to prevent `onbatch` from re-rendering on matching entry updates. let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key let select_between_enabled = false // Flag to enable select between mode let select_between_first_node = null // First node selected in select between mode @@ -492,6 +493,7 @@ async function graph_explorer (opts) { observer.observe(bottom_sentinel) const set_scroll_and_sync = () => { + drive_updated_by_scroll = true container.scrollTop = new_scroll_top container.scrollLeft = old_scroll_left vertical_scroll_value = container.scrollTop @@ -684,7 +686,17 @@ async function graph_explorer (opts) { // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate if (has_duplicate_entries && mode !== 'search' && hubs_flag !== 'true') { - el.onclick = () => add_jump_button_to_matching_entry(el, base_path, instance_path) + el.onclick = () => { + // Manually update last clicked + last_clicked_node = instance_path + drive_updated_by_match = true + + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + // Manually update DOM + update_last_clicked_styling(instance_path) + add_jump_button_to_matching_entry(el, base_path, instance_path) + } } else { const icon_el = el.querySelector('.icon') if (icon_el && has_hubs && base_path !== '/') { @@ -1134,6 +1146,9 @@ async function graph_explorer (opts) { const new_selected = new Set(selected_instance_paths) const new_confirmed = new Set(confirmed_instance_paths) + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + if (is_checked) { new_selected.delete(instance_path) new_confirmed.add(instance_path) @@ -1149,6 +1164,10 @@ async function graph_explorer (opts) { function toggle_subs (instance_path) { const state = get_or_create_state(instance_states, instance_path) state.expanded_subs = !state.expanded_subs + + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + build_and_render_view(instance_path) // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true @@ -1159,6 +1178,11 @@ async function graph_explorer (opts) { const state = get_or_create_state(instance_states, instance_path) state.expanded_hubs ? hub_num-- : hub_num++ state.expanded_hubs = !state.expanded_hubs + + last_clicked_node = instance_path + drive_updated_by_scroll = true // Prevent onbatch interference with hub spacer + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + build_and_render_view(instance_path, true) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) @@ -1167,21 +1191,30 @@ async function graph_explorer (opts) { function toggle_search_subs (instance_path) { const state = get_or_create_state(search_entry_states, instance_path) state.expanded_subs = !state.expanded_subs - perform_search(search_query) // Re-render search results with new state - drive_updated_by_toggle = true + + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + perform_search(search_query) + drive_updated_by_search = true update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) } function toggle_search_hubs (instance_path) { const state = get_or_create_state(search_entry_states, instance_path) state.expanded_hubs = !state.expanded_hubs - perform_search(search_query) // Re-render search results with new state - drive_updated_by_toggle = true + + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + perform_search(search_query) + drive_updated_by_search = true update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) } function reset () { // reset all of the manual expansions made + instance_states = {} if (mode === 'search') { search_entry_states = {} drive_updated_by_toggle = true @@ -1343,12 +1376,37 @@ async function graph_explorer (opts) { const next_instance_path = get_next_duplicate_instance(base_path, current_instance_path) if (next_instance_path) { remove_jump_button_from_entry(current_instance_path) + + // First, handle the scroll and DOM updates without drive state changes scroll_to_and_highlight_instance(next_instance_path, current_instance_path) - // Add jump button to the target entry - const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(next_instance_path)}"]`) - if (target_element) { - add_jump_button_to_matching_entry(target_element, base_path, next_instance_path) + // Manually update DOM styling + update_last_clicked_styling(next_instance_path) + last_clicked_node = next_instance_path + drive_updated_by_scroll = true // Prevent onbatch from interfering with scroll + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: next_instance_path }) + + // Add jump button to the target entry (with a small delay to ensure DOM is ready) + setTimeout(() => { + const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(next_instance_path)}"]`) + if (target_element) { + add_jump_button_to_matching_entry(target_element, base_path, next_instance_path) + } + }, 10) + } + } + + function update_last_clicked_styling (new_instance_path) { + // Remove last-clicked class from all elements + const all_nodes = shadow.querySelectorAll('.node.last-clicked') + all_nodes.forEach(node => node.classList.remove('last-clicked')) + + // Add last-clicked class to the new element + if (new_instance_path) { + const new_element = shadow.querySelector(`[data-instance_path="${CSS.escape(new_instance_path)}"]`) + if (new_element) { + new_element.classList.add('last-clicked') } } } @@ -1380,6 +1438,14 @@ async function graph_explorer (opts) { navigate_button.textContent = '^' navigate_button.onclick = (event) => { event.stopPropagation() // Prevent triggering the whole entry click again + // Manually update last clicked node for jump button + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + // Manually update DOM classes for last-clicked styling + update_last_clicked_styling(instance_path) + cycle_to_next_duplicate(base_path, instance_path) } @@ -1454,6 +1520,10 @@ async function graph_explorer (opts) { drive_updated_by_search = false return true } + if (drive_updated_by_match) { + drive_updated_by_match = false + return true + } return false } From c7459c9bbb613f36f79b8906feb00b7898913544 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 20 Sep 2025 20:19:02 +0500 Subject: [PATCH 086/130] Replace wand with jump button --- lib/graph_explorer.js | 54 +++++++++++++++++++++++++++++++++++-------- lib/theme.css | 23 ++++++++++++------ 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 594ff14..19ab043 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -40,6 +40,7 @@ async function graph_explorer (opts) { let spacer_initial_height = 0 let hub_num = 0 // Counter for expanded hubs. let last_clicked_node = null // Track the last clicked node instance path for highlighting. + let root_wand_state = null // Store original root wand state when replaced with jump button const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -1414,11 +1415,25 @@ async function graph_explorer (opts) { function remove_jump_button_from_entry (instance_path) { const current_element = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) if (current_element) { + // restore the wand icon + const node_data = view.find(n => n.instance_path === instance_path) + if (node_data && node_data.base_path === '/' && instance_path === '|/') { + const wand_el = current_element.querySelector('.wand.navigate-to-hub') + if (wand_el && root_wand_state) { + wand_el.textContent = root_wand_state.content + wand_el.className = root_wand_state.className + wand_el.onclick = root_wand_state.onclick + + root_wand_state = null + } + return + } + + // Regular behavior for non-root nodes const button_container = current_element.querySelector('.indent-btn-container') if (button_container) { button_container.remove() // Restore left-indent class - const node_data = view.find(n => n.instance_path === instance_path) if (node_data && node_data.depth > 0) { current_element.classList.add('left-indent') } @@ -1430,6 +1445,34 @@ async function graph_explorer (opts) { // Check if jump button already exists if (el.querySelector('.navigate-to-hub')) return + // replace the wand icon temporarily + if (base_path === '/' && instance_path === '|/') { + const wand_el = el.querySelector('.wand') + if (wand_el) { + // Store original wand state in JavaScript variable + root_wand_state = { + content: wand_el.textContent, + className: wand_el.className, + onclick: wand_el.onclick + } + + // Replace with jump button + wand_el.textContent = '^' + wand_el.className = 'wand navigate-to-hub clickable' + wand_el.onclick = (event) => { + event.stopPropagation() + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + update_last_clicked_styling(instance_path) + + cycle_to_next_duplicate(base_path, instance_path) + } + } + return + } + const indent_button_div = document.createElement('div') indent_button_div.className = 'indent-btn-container' @@ -1474,15 +1517,6 @@ async function graph_explorer (opts) { } container.scrollTop = target_scroll_top - - // Find and highlight the DOM element - const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(target_instance_path)}"]`) - if (target_element) { - target_element.classList.add('highlight-instance') - setTimeout(() => { - target_element.classList.remove('highlight-instance') - }, 2000) - } } /****************************************************************************** diff --git a/lib/theme.css b/lib/theme.css index 9566970..ef181ff 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -117,6 +117,10 @@ font-weight: bold; transition: background-color 0.2s ease; } +.wand.navigate-to-hub { + margin-left: 1px; + margin-right: 1px; +} .navigate-to-hub:hover { background-color: #528bcc; } @@ -124,10 +128,6 @@ font-weight: bold; color: #e5c07b; } -.highlight-instance { - background-color: pink; - transition: background-color 0.3s ease; -} .matching-entry .name { color: #e06c75; font-weight: bold; @@ -189,7 +189,10 @@ margin-left: 7px; margin-right: 7px; } - + .wand.navigate-to-hub { + margin-left: 1.5px; + margin-right: 1.5px; + } .graph-container { padding: 13px; } @@ -255,7 +258,10 @@ margin-left: 8px; margin-right: 8px; } - + .wand.navigate-to-hub { + margin-left: 1.66px; + margin-right: 1.66px; + } .graph-container { padding: 16px; } @@ -294,7 +300,10 @@ margin-left: 10px; margin-right: 10px; } - + .wand.navigate-to-hub { + margin-left: 2px; + margin-right: 2px; + } .graph-container { padding: 20px; } From ad1a3c6fffac683b736e884df787a71e41c0759b Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 20 Sep 2025 20:32:51 +0500 Subject: [PATCH 087/130] First Instance of duplicates is intractive --- lib/graph_explorer.js | 50 ++++++++++++++++++++++++++++++++----------- lib/theme.css | 4 ++++ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 19ab043..36d0947 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -668,13 +668,19 @@ async function graph_explorer (opts) { // Check if this entry appears elsewhere in the view (any duplicate) let has_duplicate_entries = false + let is_first_occurrence = false if (mode !== 'search' && hubs_flag !== 'true') { // disabled in search mode and when hubs_flag is 'true' collect_all_duplicate_entries() has_duplicate_entries = has_duplicates(base_path) // coloring class for duplicates if (has_duplicate_entries) { - el.classList.add('matching-entry') + is_first_occurrence = is_first_duplicate(base_path, instance_path) + if (is_first_occurrence) { + el.classList.add('first-matching-entry') + } else { + el.classList.add('matching-entry') + } } } @@ -682,11 +688,11 @@ async function graph_explorer (opts) { ${pipe_html} - ${name_html} + ${name_html} ` // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate - if (has_duplicate_entries && mode !== 'search' && hubs_flag !== 'true') { + if (has_duplicate_entries && !is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { el.onclick = () => { // Manually update last clicked last_clicked_node = instance_path @@ -719,7 +725,19 @@ async function graph_explorer (opts) { if (prefix_el) prefix_el.onclick = toggle_subs_handler } - el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) + // Special handling for first duplicate entry - it should have normal select behavior but also show jump button + const name_el = el.querySelector('.name') + if (has_duplicate_entries && is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { + name_el.onclick = ev => { + select_node(ev, instance_path) + // Also add jump button functionality for first occurrence + setTimeout(() => { + add_jump_button_to_matching_entry(el, base_path, instance_path) + }, 10) + } + } else { + name_el.onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) + } } if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) @@ -1350,27 +1368,35 @@ async function graph_explorer (opts) { base_path_counts[node.base_path].push(node.instance_path) } - // Store only duplicates + // Store only duplicates with first occurrence info for (const [base_path, instance_paths] of Object.entries(base_path_counts)) { if (instance_paths.length > 1) { - duplicate_entries_map[base_path] = instance_paths + duplicate_entries_map[base_path] = { + instances: instance_paths, + first_instance: instance_paths[0] // First occurrence in view order + } } } } function get_next_duplicate_instance (base_path, current_instance_path) { const duplicates = duplicate_entries_map[base_path] - if (!duplicates || duplicates.length <= 1) return null + if (!duplicates || duplicates.instances.length <= 1) return null - const current_index = duplicates.indexOf(current_instance_path) - if (current_index === -1) return duplicates[0] + const current_index = duplicates.instances.indexOf(current_instance_path) + if (current_index === -1) return duplicates.instances[0] - const next_index = (current_index + 1) % duplicates.length - return duplicates[next_index] + const next_index = (current_index + 1) % duplicates.instances.length + return duplicates.instances[next_index] } function has_duplicates (base_path) { - return duplicate_entries_map[base_path] && duplicate_entries_map[base_path].length > 1 + return duplicate_entries_map[base_path] && duplicate_entries_map[base_path].instances.length > 1 + } + + function is_first_duplicate (base_path, instance_path) { + const duplicates = duplicate_entries_map[base_path] + return duplicates && duplicates.first_instance === instance_path } function cycle_to_next_duplicate (base_path, current_instance_path) { diff --git a/lib/theme.css b/lib/theme.css index ef181ff..be81c14 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -132,6 +132,10 @@ color: #e06c75; font-weight: bold; } +.first-matching-entry .name { + color: #b48ead; + font-weight: bold; +} /* Base sizes for desktop (768px and above) */ .node { From 5ebdefccb112934bd36934a01b3a43ddb39bbf62 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 20 Sep 2025 20:45:06 +0500 Subject: [PATCH 088/130] bundled --- bundle.js | 226 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 191 insertions(+), 35 deletions(-) diff --git a/bundle.js b/bundle.js index 52cc6d2..8ee0a00 100644 --- a/bundle.js +++ b/bundle.js @@ -34,6 +34,7 @@ async function graph_explorer (opts) { let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. + let drive_updated_by_match = false // Flag to prevent `onbatch` from re-rendering on matching entry updates. let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key let select_between_enabled = false // Flag to enable select between mode let select_between_first_node = null // First node selected in select between mode @@ -43,6 +44,7 @@ async function graph_explorer (opts) { let spacer_initial_height = 0 let hub_num = 0 // Counter for expanded hubs. let last_clicked_node = null // Track the last clicked node instance path for highlighting. + let root_wand_state = null // Store original root wand state when replaced with jump button const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -496,6 +498,7 @@ async function graph_explorer (opts) { observer.observe(bottom_sentinel) const set_scroll_and_sync = () => { + drive_updated_by_scroll = true container.scrollTop = new_scroll_top container.scrollLeft = old_scroll_left vertical_scroll_value = container.scrollTop @@ -654,7 +657,6 @@ async function graph_explorer (opts) { if (depth) { el.classList.add('left-indent') - el.style.paddingLeft *= depth } if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) @@ -670,13 +672,19 @@ async function graph_explorer (opts) { // Check if this entry appears elsewhere in the view (any duplicate) let has_duplicate_entries = false + let is_first_occurrence = false if (mode !== 'search' && hubs_flag !== 'true') { // disabled in search mode and when hubs_flag is 'true' collect_all_duplicate_entries() has_duplicate_entries = has_duplicates(base_path) // coloring class for duplicates if (has_duplicate_entries) { - el.classList.add('matching-entry') + is_first_occurrence = is_first_duplicate(base_path, instance_path) + if (is_first_occurrence) { + el.classList.add('first-matching-entry') + } else { + el.classList.add('matching-entry') + } } } @@ -684,12 +692,22 @@ async function graph_explorer (opts) { ${pipe_html} - ${name_html} + ${name_html} ` // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate - if (has_duplicate_entries && mode !== 'search' && hubs_flag !== 'true') { - el.onclick = () => add_jump_button_to_matching_entry(el, base_path, instance_path) + if (has_duplicate_entries && !is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { + el.onclick = () => { + // Manually update last clicked + last_clicked_node = instance_path + drive_updated_by_match = true + + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + // Manually update DOM + update_last_clicked_styling(instance_path) + add_jump_button_to_matching_entry(el, base_path, instance_path) + } } else { const icon_el = el.querySelector('.icon') if (icon_el && has_hubs && base_path !== '/') { @@ -711,7 +729,19 @@ async function graph_explorer (opts) { if (prefix_el) prefix_el.onclick = toggle_subs_handler } - el.querySelector('.name').onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) + // Special handling for first duplicate entry - it should have normal select behavior but also show jump button + const name_el = el.querySelector('.name') + if (has_duplicate_entries && is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { + name_el.onclick = ev => { + select_node(ev, instance_path) + // Also add jump button functionality for first occurrence + setTimeout(() => { + add_jump_button_to_matching_entry(el, base_path, instance_path) + }, 10) + } + } else { + name_el.onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) + } } if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) @@ -1139,6 +1169,9 @@ async function graph_explorer (opts) { const new_selected = new Set(selected_instance_paths) const new_confirmed = new Set(confirmed_instance_paths) + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + if (is_checked) { new_selected.delete(instance_path) new_confirmed.add(instance_path) @@ -1154,6 +1187,10 @@ async function graph_explorer (opts) { function toggle_subs (instance_path) { const state = get_or_create_state(instance_states, instance_path) state.expanded_subs = !state.expanded_subs + + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + build_and_render_view(instance_path) // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true @@ -1164,6 +1201,11 @@ async function graph_explorer (opts) { const state = get_or_create_state(instance_states, instance_path) state.expanded_hubs ? hub_num-- : hub_num++ state.expanded_hubs = !state.expanded_hubs + + last_clicked_node = instance_path + drive_updated_by_scroll = true // Prevent onbatch interference with hub spacer + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + build_and_render_view(instance_path, true) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) @@ -1172,21 +1214,30 @@ async function graph_explorer (opts) { function toggle_search_subs (instance_path) { const state = get_or_create_state(search_entry_states, instance_path) state.expanded_subs = !state.expanded_subs - perform_search(search_query) // Re-render search results with new state - drive_updated_by_toggle = true + + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + perform_search(search_query) + drive_updated_by_search = true update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) } function toggle_search_hubs (instance_path) { const state = get_or_create_state(search_entry_states, instance_path) state.expanded_hubs = !state.expanded_hubs - perform_search(search_query) // Re-render search results with new state - drive_updated_by_toggle = true + + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + perform_search(search_query) + drive_updated_by_search = true update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) } function reset () { // reset all of the manual expansions made + instance_states = {} if (mode === 'search') { search_entry_states = {} drive_updated_by_toggle = true @@ -1321,33 +1372,102 @@ async function graph_explorer (opts) { base_path_counts[node.base_path].push(node.instance_path) } - // Store only duplicates + // Store only duplicates with first occurrence info for (const [base_path, instance_paths] of Object.entries(base_path_counts)) { if (instance_paths.length > 1) { - duplicate_entries_map[base_path] = instance_paths + duplicate_entries_map[base_path] = { + instances: instance_paths, + first_instance: instance_paths[0] // First occurrence in view order + } } } } function get_next_duplicate_instance (base_path, current_instance_path) { const duplicates = duplicate_entries_map[base_path] - if (!duplicates || duplicates.length <= 1) return null + if (!duplicates || duplicates.instances.length <= 1) return null - const current_index = duplicates.indexOf(current_instance_path) - if (current_index === -1) return duplicates[0] + const current_index = duplicates.instances.indexOf(current_instance_path) + if (current_index === -1) return duplicates.instances[0] - const next_index = (current_index + 1) % duplicates.length - return duplicates[next_index] + const next_index = (current_index + 1) % duplicates.instances.length + return duplicates.instances[next_index] } function has_duplicates (base_path) { - return duplicate_entries_map[base_path] && duplicate_entries_map[base_path].length > 1 + return duplicate_entries_map[base_path] && duplicate_entries_map[base_path].instances.length > 1 + } + + function is_first_duplicate (base_path, instance_path) { + const duplicates = duplicate_entries_map[base_path] + return duplicates && duplicates.first_instance === instance_path } function cycle_to_next_duplicate (base_path, current_instance_path) { const next_instance_path = get_next_duplicate_instance(base_path, current_instance_path) if (next_instance_path) { - scroll_to_and_highlight_instance(next_instance_path) + remove_jump_button_from_entry(current_instance_path) + + // First, handle the scroll and DOM updates without drive state changes + scroll_to_and_highlight_instance(next_instance_path, current_instance_path) + + // Manually update DOM styling + update_last_clicked_styling(next_instance_path) + last_clicked_node = next_instance_path + drive_updated_by_scroll = true // Prevent onbatch from interfering with scroll + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: next_instance_path }) + + // Add jump button to the target entry (with a small delay to ensure DOM is ready) + setTimeout(() => { + const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(next_instance_path)}"]`) + if (target_element) { + add_jump_button_to_matching_entry(target_element, base_path, next_instance_path) + } + }, 10) + } + } + + function update_last_clicked_styling (new_instance_path) { + // Remove last-clicked class from all elements + const all_nodes = shadow.querySelectorAll('.node.last-clicked') + all_nodes.forEach(node => node.classList.remove('last-clicked')) + + // Add last-clicked class to the new element + if (new_instance_path) { + const new_element = shadow.querySelector(`[data-instance_path="${CSS.escape(new_instance_path)}"]`) + if (new_element) { + new_element.classList.add('last-clicked') + } + } + } + + function remove_jump_button_from_entry (instance_path) { + const current_element = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (current_element) { + // restore the wand icon + const node_data = view.find(n => n.instance_path === instance_path) + if (node_data && node_data.base_path === '/' && instance_path === '|/') { + const wand_el = current_element.querySelector('.wand.navigate-to-hub') + if (wand_el && root_wand_state) { + wand_el.textContent = root_wand_state.content + wand_el.className = root_wand_state.className + wand_el.onclick = root_wand_state.onclick + + root_wand_state = null + } + return + } + + // Regular behavior for non-root nodes + const button_container = current_element.querySelector('.indent-btn-container') + if (button_container) { + button_container.remove() + // Restore left-indent class + if (node_data && node_data.depth > 0) { + current_element.classList.add('left-indent') + } + } } } @@ -1355,20 +1475,50 @@ async function graph_explorer (opts) { // Check if jump button already exists if (el.querySelector('.navigate-to-hub')) return - // Get current left padding value to match the width - const computedStyle = window.getComputedStyle(el) - const leftPadding = computedStyle.paddingLeft + // replace the wand icon temporarily + if (base_path === '/' && instance_path === '|/') { + const wand_el = el.querySelector('.wand') + if (wand_el) { + // Store original wand state in JavaScript variable + root_wand_state = { + content: wand_el.textContent, + className: wand_el.className, + onclick: wand_el.onclick + } + + // Replace with jump button + wand_el.textContent = '^' + wand_el.className = 'wand navigate-to-hub clickable' + wand_el.onclick = (event) => { + event.stopPropagation() + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + update_last_clicked_styling(instance_path) + + cycle_to_next_duplicate(base_path, instance_path) + } + } + return + } - // Create a div to replace the left padding const indent_button_div = document.createElement('div') indent_button_div.className = 'indent-btn-container' - indent_button_div.style.width = leftPadding const navigate_button = document.createElement('span') navigate_button.className = 'navigate-to-hub clickable' navigate_button.textContent = '^' navigate_button.onclick = (event) => { event.stopPropagation() // Prevent triggering the whole entry click again + // Manually update last clicked node for jump button + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + // Manually update DOM classes for last-clicked styling + update_last_clicked_styling(instance_path) + cycle_to_next_duplicate(base_path, instance_path) } @@ -1379,22 +1529,24 @@ async function graph_explorer (opts) { el.insertBefore(indent_button_div, el.firstChild) } - function scroll_to_and_highlight_instance (target_instance_path) { + function scroll_to_and_highlight_instance (target_instance_path, source_instance_path = null) { const target_index = view.findIndex(n => n.instance_path === target_instance_path) if (target_index === -1) return // Calculate scroll position - const target_scroll_top = target_index * node_height - container.scrollTop = target_scroll_top - - // Find and highlight the DOM element - const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(target_instance_path)}"]`) - if (target_element) { - target_element.classList.add('highlight-instance') - setTimeout(() => { - target_element.classList.remove('highlight-instance') - }, 2000) + let target_scroll_top = target_index * node_height + + if (source_instance_path) { + const source_index = view.findIndex(n => n.instance_path === source_instance_path) + if (source_index !== -1) { + const source_scroll_top = source_index * node_height + const current_scroll_top = container.scrollTop + const source_visible_offset = source_scroll_top - current_scroll_top + target_scroll_top = target_scroll_top - source_visible_offset + } } + + container.scrollTop = target_scroll_top } /****************************************************************************** @@ -1432,6 +1584,10 @@ async function graph_explorer (opts) { drive_updated_by_search = false return true } + if (drive_updated_by_match) { + drive_updated_by_match = false + return true + } return false } From f1637b8e928217ca53599fbb68e3aa963a2b89e5 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 21 Sep 2025 17:47:04 +0500 Subject: [PATCH 089/130] applied container queries --- lib/theme.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/theme.css b/lib/theme.css index be81c14..53add38 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -1,7 +1,7 @@ /* Container setup for responsive scaling */ -.graph-explorer-wrapper { +.graph-container { container-type: inline-size; - container-name: graph-explorer; + container-name: graph-container; } .graph-container, .node { @@ -165,7 +165,7 @@ } /* Medium screens (480px to 767px) - 1.3x scale */ -@media (max-width: 767px) { +@container graph-container (max-width: 767px) { .left-indent { padding-left: 22.75px; } @@ -206,7 +206,7 @@ } } /* Height-based responsive adjustments for mobile experience */ - @media (max-height: 600px) { + @container graph-container (max-height: 600px) { .left-indent { padding-left: 25px; } @@ -234,7 +234,7 @@ } } /* Small screens (320px to 479px) - 1.6x scale */ - @media (max-width: 479px) { + @container graph-container (max-width: 479px) { .left-indent { padding-left: 28px; } @@ -276,7 +276,7 @@ } /* Extra small screens (below 320px) - 2x scale */ - @media (max-width: 319px) { + @container graph-container (max-width: 319px) { .left-indent { padding-left: 35px; } From d1f4ef92d75ca5645012f67b2297cb25bbdb7622 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 21 Sep 2025 17:57:18 +0500 Subject: [PATCH 090/130] Replaced Anonymous Functions --- lib/graph_explorer.js | 70 +++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 36d0947..c82bc4d 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -493,13 +493,6 @@ async function graph_explorer (opts) { observer.observe(top_sentinel) observer.observe(bottom_sentinel) - const set_scroll_and_sync = () => { - drive_updated_by_scroll = true - container.scrollTop = new_scroll_top - container.scrollLeft = old_scroll_left - vertical_scroll_value = container.scrollTop - } - // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. handle_spacer_element({ hub_toggle, @@ -507,6 +500,13 @@ async function graph_explorer (opts) { new_scroll_top, sync_fn: set_scroll_and_sync }) + + function set_scroll_and_sync () { + drive_updated_by_scroll = true + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + vertical_scroll_value = container.scrollTop + } } // Traverses the hierarchical `all_entries` data and builds a flat `view` array for rendering. @@ -693,17 +693,7 @@ async function graph_explorer (opts) { // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate if (has_duplicate_entries && !is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { - el.onclick = () => { - // Manually update last clicked - last_clicked_node = instance_path - drive_updated_by_match = true - - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) - - // Manually update DOM - update_last_clicked_styling(instance_path) - add_jump_button_to_matching_entry(el, base_path, instance_path) - } + el.onclick = jump_out_to_next_duplicate } else { const icon_el = el.querySelector('.icon') if (icon_el && has_hubs && base_path !== '/') { @@ -743,6 +733,17 @@ async function graph_explorer (opts) { if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) return el + function jump_out_to_next_duplicate () { + // Manually update last clicked + last_clicked_node = instance_path + drive_updated_by_match = true + + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + // Manually update DOM + update_last_clicked_styling(instance_path) + add_jump_button_to_matching_entry(el, base_path, instance_path) + } } // `re_render_node` updates a single node in the DOM, used when only its selection state changes. @@ -1260,7 +1261,8 @@ async function graph_explorer (opts) { function onscroll () { if (scroll_update_pending) return scroll_update_pending = true - requestAnimationFrame(() => { + requestAnimationFrame(scroll_frames) + function scroll_frames () { const scroll_delta = vertical_scroll_value - container.scrollTop // Handle removal of the scroll spacer. if (spacer_element && scroll_delta > 0 && container.scrollTop === 0) { @@ -1273,7 +1275,7 @@ async function graph_explorer (opts) { vertical_scroll_value = update_scroll_state({ current_value: vertical_scroll_value, new_value: container.scrollTop, name: 'vertical_scroll_value' }) horizontal_scroll_value = update_scroll_state({ current_value: horizontal_scroll_value, new_value: container.scrollLeft, name: 'horizontal_scroll_value' }) scroll_update_pending = false - }) + } } async function fill_viewport_downwards () { @@ -1415,12 +1417,13 @@ async function graph_explorer (opts) { update_drive_state({ type: 'runtime/last_clicked_node', message: next_instance_path }) // Add jump button to the target entry (with a small delay to ensure DOM is ready) - setTimeout(() => { + setTimeout(jump_out, 10) + function jump_out () { const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(next_instance_path)}"]`) if (target_element) { add_jump_button_to_matching_entry(target_element, base_path, next_instance_path) } - }, 10) + } } } @@ -1636,17 +1639,7 @@ async function graph_explorer (opts) { container.appendChild(spacer_element) if (hub_toggle) { - requestAnimationFrame(() => { - const container_height = container.clientHeight - const content_height = view.length * node_height - const max_scroll_top = content_height - container_height - - if (new_scroll_top > max_scroll_top) { - spacer_initial_height = new_scroll_top - max_scroll_top - spacer_element.style.height = `${spacer_initial_height}px` - } - sync_fn() - }) + requestAnimationFrame(spacer_frames) } else { spacer_element.style.height = `${existing_height}px` requestAnimationFrame(sync_fn) @@ -1656,6 +1649,17 @@ async function graph_explorer (opts) { spacer_initial_height = 0 requestAnimationFrame(sync_fn) } + function spacer_frames () { + const container_height = container.clientHeight + const content_height = view.length * node_height + const max_scroll_top = content_height - container_height + + if (new_scroll_top > max_scroll_top) { + spacer_initial_height = new_scroll_top - max_scroll_top + spacer_element.style.height = `${spacer_initial_height}px` + } + sync_fn() + } } function create_root_node ({ state, has_subs, instance_path }) { From e5c84122d025b7c59df86bddc201fc38b0ee3369 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 21 Sep 2025 20:04:43 +0500 Subject: [PATCH 091/130] Added true Intraction based order for duplicate_map --- lib/graph_explorer.js | 123 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 10 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index c82bc4d..9f126de 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -35,6 +35,7 @@ async function graph_explorer (opts) { let select_between_enabled = false // Flag to enable select between mode let select_between_first_node = null // First node selected in select between mode let duplicate_entries_map = {} + let view_order_tracking = {} // Tracks instance paths by base path in real time as they are added into the view through toggle expand/collapse actions. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 @@ -143,6 +144,17 @@ async function graph_explorer (opts) { expanded_hubs: false } } + // Initialize root tracking when entries are first loaded + if (Object.keys(view_order_tracking).length === 0) { + add_instance_to_view_tracking(root_path, root_instance_path) + // Add initially expanded subs if any + const root_entry = all_entries[root_path] + if (root_entry && Array.isArray(root_entry.subs)) { + root_entry.subs.forEach(sub_path => { + add_instances_recursively(sub_path, root_instance_path, instance_states, all_entries) + }) + } + } build_and_render_view() } else { console.warn('Root path "/" not found in entries. Clearing view.') @@ -1183,8 +1195,24 @@ async function graph_explorer (opts) { function toggle_subs (instance_path) { const state = get_or_create_state(instance_states, instance_path) + const was_expanded = state.expanded_subs state.expanded_subs = !state.expanded_subs + // Update view order tracking for the toggled subs + const base_path = instance_path.split('|').pop() + const entry = all_entries[base_path] + if (entry && Array.isArray(entry.subs)) { + entry.subs.forEach(sub_path => { + if (was_expanded) { + // Collapsing so + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + } else { + // Expanding so + add_instances_recursively(sub_path, instance_path, instance_states, all_entries) + } + }) + } + last_clicked_node = instance_path update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) @@ -1196,9 +1224,25 @@ async function graph_explorer (opts) { function toggle_hubs (instance_path) { const state = get_or_create_state(instance_states, instance_path) + const was_expanded = state.expanded_hubs state.expanded_hubs ? hub_num-- : hub_num++ state.expanded_hubs = !state.expanded_hubs + // Update view order tracking for the toggled hubs + const base_path = instance_path.split('|').pop() + const entry = all_entries[base_path] + if (entry && Array.isArray(entry.hubs)) { + entry.hubs.forEach(hub_path => { + if (was_expanded) { + // Collapsing so + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + } else { + // Expanding so + add_instances_recursively(hub_path, instance_path, instance_states, all_entries) + } + }) + } + last_clicked_node = instance_path drive_updated_by_scroll = true // Prevent onbatch interference with hub spacer update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) @@ -1235,6 +1279,7 @@ async function graph_explorer (opts) { function reset () { // reset all of the manual expansions made instance_states = {} + view_order_tracking = {} // Clear view order tracking on reset if (mode === 'search') { search_entry_states = {} drive_updated_by_toggle = true @@ -1362,16 +1407,8 @@ async function graph_explorer (opts) { function collect_all_duplicate_entries () { duplicate_entries_map = {} - const base_path_counts = {} - for (const node of view) { - if (!base_path_counts[node.base_path]) { - base_path_counts[node.base_path] = [] - } - base_path_counts[node.base_path].push(node.instance_path) - } - - // Store only duplicates with first occurrence info - for (const [base_path, instance_paths] of Object.entries(base_path_counts)) { + // Use view_order_tracking for duplicate detection + for (const [base_path, instance_paths] of Object.entries(view_order_tracking)) { if (instance_paths.length > 1) { duplicate_entries_map[base_path] = { instances: instance_paths, @@ -1381,6 +1418,72 @@ async function graph_explorer (opts) { } } + function add_instance_to_view_tracking (base_path, instance_path) { + if (!view_order_tracking[base_path]) view_order_tracking[base_path] = [] + if (!view_order_tracking[base_path].includes(instance_path)) view_order_tracking[base_path].push(instance_path) + } + + function remove_instance_from_view_tracking (base_path, instance_path) { + if (view_order_tracking[base_path]) { + const index = view_order_tracking[base_path].indexOf(instance_path) + if (index !== -1) { + view_order_tracking[base_path].splice(index, 1) + // Clean up empty arrays + if (view_order_tracking[base_path].length === 0) { + delete view_order_tracking[base_path] + } + } + } + } + + // Recursively add instances to tracking when expanding + function add_instances_recursively (base_path, parent_instance_path, instance_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + entry.hubs.forEach(hub_path => { + add_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + entry.subs.forEach(sub_path => { + add_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } + + // Add the instance itself + add_instance_to_view_tracking(base_path, instance_path) + } + + // Recursively remove instances from tracking when collapsing + function remove_instances_recursively (base_path, parent_instance_path, instance_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + entry.hubs.forEach(hub_path => { + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + entry.subs.forEach(sub_path => { + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } + + // Remove the instance itself + remove_instance_from_view_tracking(base_path, instance_path) + } + function get_next_duplicate_instance (base_path, current_instance_path) { const duplicates = duplicate_entries_map[base_path] if (!duplicates || duplicates.instances.length <= 1) return null From aaccad18ba64dbd38d7c69558adde7ccdd87c88c Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 21 Sep 2025 21:36:16 +0500 Subject: [PATCH 092/130] Add view_order_tracking to drive --- lib/graph_explorer.js | 83 ++++++++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 9f126de..63f6bc6 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -31,6 +31,8 @@ async function graph_explorer (opts) { let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let drive_updated_by_match = false // Flag to prevent `onbatch` from re-rendering on matching entry updates. + let drive_updated_by_tracking = false // Flag to prevent `onbatch` from re-rendering on view order tracking updates. + let is_loading_from_drive = false // Flag to prevent saving to drive during initial load let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key let select_between_enabled = false // Flag to enable select between mode let select_between_first_node = null // First node selected in select between mode @@ -100,15 +102,15 @@ async function graph_explorer (opts) { for (const { type, paths } of batch) { if (!paths || !paths.length) continue const data = await Promise.all( - paths.map(path => - drive + paths.map(path => { + return drive .get(path) .then(file => (file ? file.raw : null)) .catch(e => { console.error(`Error getting file from drive: ${path}`, e) return null }) - ) + }) ) // Call the appropriate handler based on `type`. const func = on[type] @@ -144,17 +146,7 @@ async function graph_explorer (opts) { expanded_hubs: false } } - // Initialize root tracking when entries are first loaded - if (Object.keys(view_order_tracking).length === 0) { - add_instance_to_view_tracking(root_path, root_instance_path) - // Add initially expanded subs if any - const root_entry = all_entries[root_path] - if (root_entry && Array.isArray(root_entry.subs)) { - root_entry.subs.forEach(sub_path => { - add_instances_recursively(sub_path, root_instance_path, instance_states, all_entries) - }) - } - } + // trcking will be initialized later if drive data is empty build_and_render_view() } else { console.warn('Root path "/" not found in entries. Clearing view.') @@ -172,7 +164,8 @@ async function graph_explorer (opts) { 'confirmed_selected.json': handle_confirmed_paths, 'instance_states.json': handle_instance_states, 'search_entry_states.json': handle_search_entry_states, - 'last_clicked_node.json': handle_last_clicked_node + 'last_clicked_node.json': handle_last_clicked_node, + 'view_order_tracking.json': handle_view_order_tracking } let needs_render = false const render_nodes_needed = new Set() @@ -250,6 +243,20 @@ async function graph_explorer (opts) { if (old_last_clicked) render_nodes_needed.add(old_last_clicked) if (last_clicked_node) render_nodes_needed.add(last_clicked_node) } + + function handle_view_order_tracking ({ value }) { + if (typeof value === 'object' && value && !Array.isArray(value)) { + is_loading_from_drive = true + view_order_tracking = value + is_loading_from_drive = false + if (Object.keys(view_order_tracking).length === 0) { + initialize_tracking_from_current_state() + } + return { needs_render: true } + } else { + console.warn('view_order_tracking is not a valid object, ignoring.', value) + } + } } function on_mode ({ data, paths }) { @@ -484,6 +491,9 @@ async function graph_explorer (opts) { all_entries }) + // Recalculate duplicates after view is built + collect_all_duplicate_entries() + const new_scroll_top = calculate_new_scroll_top({ old_scroll_top, old_view, @@ -649,7 +659,6 @@ async function graph_explorer (opts) { const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path - if (is_search_match) { el.classList.add('search-result') if (is_direct_match) el.classList.add('direct-match') @@ -668,7 +677,6 @@ async function graph_explorer (opts) { } if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) - const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) const pipe_html = pipe_trail.map(p => ``).join('') const prefix_class = has_subs ? 'prefix clickable' : 'prefix' @@ -682,7 +690,6 @@ async function graph_explorer (opts) { let has_duplicate_entries = false let is_first_occurrence = false if (mode !== 'search' && hubs_flag !== 'true') { // disabled in search mode and when hubs_flag is 'true' - collect_all_duplicate_entries() has_duplicate_entries = has_duplicates(base_path) // coloring class for duplicates @@ -1280,6 +1287,8 @@ async function graph_explorer (opts) { // reset all of the manual expansions made instance_states = {} view_order_tracking = {} // Clear view order tracking on reset + drive_updated_by_tracking = true + update_drive_state({ type: 'runtime/view_order_tracking', message: view_order_tracking }) if (mode === 'search') { search_entry_states = {} drive_updated_by_toggle = true @@ -1418,9 +1427,32 @@ async function graph_explorer (opts) { } } + function initialize_tracking_from_current_state () { + const root_path = '/' + const root_instance_path = '|/' + if (all_entries[root_path]) { + add_instance_to_view_tracking(root_path, root_instance_path) + // Add initially expanded subs if any + const root_entry = all_entries[root_path] + if (root_entry && Array.isArray(root_entry.subs)) { + root_entry.subs.forEach(sub_path => { + add_instances_recursively(sub_path, root_instance_path, instance_states, all_entries) + }) + } + } + } + function add_instance_to_view_tracking (base_path, instance_path) { if (!view_order_tracking[base_path]) view_order_tracking[base_path] = [] - if (!view_order_tracking[base_path].includes(instance_path)) view_order_tracking[base_path].push(instance_path) + if (!view_order_tracking[base_path].includes(instance_path)) { + view_order_tracking[base_path].push(instance_path) + + // Only save to drive if not currently loading from drive + if (!is_loading_from_drive) { + drive_updated_by_tracking = true + update_drive_state({ type: 'runtime/view_order_tracking', message: view_order_tracking }) + } + } } function remove_instance_from_view_tracking (base_path, instance_path) { @@ -1432,6 +1464,12 @@ async function graph_explorer (opts) { if (view_order_tracking[base_path].length === 0) { delete view_order_tracking[base_path] } + + // Only save to drive if not currently loading from drive + if (!is_loading_from_drive) { + drive_updated_by_tracking = true + update_drive_state({ type: 'runtime/view_order_tracking', message: view_order_tracking }) + } } } } @@ -1690,6 +1728,10 @@ async function graph_explorer (opts) { drive_updated_by_match = false return true } + if (drive_updated_by_tracking) { + drive_updated_by_tracking = false + return true + } return false } @@ -1843,7 +1885,8 @@ function fallback_module () { 'confirmed_selected.json': { raw: '[]' }, 'instance_states.json': { raw: '{}' }, 'search_entry_states.json': { raw: '{}' }, - 'last_clicked_node.json': { raw: 'null' } + 'last_clicked_node.json': { raw: 'null' }, + 'view_order_tracking.json': { raw: '{}' } }, 'mode/': { 'current_mode.json': { raw: '"menubar"' }, From 6a8c1c7592cdd26bb828b66f55487329c0765ef6 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 21 Sep 2025 21:45:43 +0500 Subject: [PATCH 093/130] bundled --- bundle.js | 252 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 201 insertions(+), 51 deletions(-) diff --git a/bundle.js b/bundle.js index 8ee0a00..6d7374f 100644 --- a/bundle.js +++ b/bundle.js @@ -35,10 +35,13 @@ async function graph_explorer (opts) { let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. let drive_updated_by_match = false // Flag to prevent `onbatch` from re-rendering on matching entry updates. + let drive_updated_by_tracking = false // Flag to prevent `onbatch` from re-rendering on view order tracking updates. + let is_loading_from_drive = false // Flag to prevent saving to drive during initial load let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key let select_between_enabled = false // Flag to enable select between mode let select_between_first_node = null // First node selected in select between mode let duplicate_entries_map = {} + let view_order_tracking = {} // Tracks instance paths by base path in real time as they are added into the view through toggle expand/collapse actions. let is_rendering = false // Flag to prevent concurrent rendering operations in virtual scrolling. let spacer_element = null // DOM element used to manage scroll position when hubs are toggled. let spacer_initial_height = 0 @@ -103,15 +106,15 @@ async function graph_explorer (opts) { for (const { type, paths } of batch) { if (!paths || !paths.length) continue const data = await Promise.all( - paths.map(path => - drive + paths.map(path => { + return drive .get(path) .then(file => (file ? file.raw : null)) .catch(e => { console.error(`Error getting file from drive: ${path}`, e) return null }) - ) + }) ) // Call the appropriate handler based on `type`. const func = on[type] @@ -147,6 +150,7 @@ async function graph_explorer (opts) { expanded_hubs: false } } + // trcking will be initialized later if drive data is empty build_and_render_view() } else { console.warn('Root path "/" not found in entries. Clearing view.') @@ -164,7 +168,8 @@ async function graph_explorer (opts) { 'confirmed_selected.json': handle_confirmed_paths, 'instance_states.json': handle_instance_states, 'search_entry_states.json': handle_search_entry_states, - 'last_clicked_node.json': handle_last_clicked_node + 'last_clicked_node.json': handle_last_clicked_node, + 'view_order_tracking.json': handle_view_order_tracking } let needs_render = false const render_nodes_needed = new Set() @@ -242,6 +247,20 @@ async function graph_explorer (opts) { if (old_last_clicked) render_nodes_needed.add(old_last_clicked) if (last_clicked_node) render_nodes_needed.add(last_clicked_node) } + + function handle_view_order_tracking ({ value }) { + if (typeof value === 'object' && value && !Array.isArray(value)) { + is_loading_from_drive = true + view_order_tracking = value + is_loading_from_drive = false + if (Object.keys(view_order_tracking).length === 0) { + initialize_tracking_from_current_state() + } + return { needs_render: true } + } else { + console.warn('view_order_tracking is not a valid object, ignoring.', value) + } + } } function on_mode ({ data, paths }) { @@ -476,6 +495,9 @@ async function graph_explorer (opts) { all_entries }) + // Recalculate duplicates after view is built + collect_all_duplicate_entries() + const new_scroll_top = calculate_new_scroll_top({ old_scroll_top, old_view, @@ -497,13 +519,6 @@ async function graph_explorer (opts) { observer.observe(top_sentinel) observer.observe(bottom_sentinel) - const set_scroll_and_sync = () => { - drive_updated_by_scroll = true - container.scrollTop = new_scroll_top - container.scrollLeft = old_scroll_left - vertical_scroll_value = container.scrollTop - } - // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. handle_spacer_element({ hub_toggle, @@ -511,6 +526,13 @@ async function graph_explorer (opts) { new_scroll_top, sync_fn: set_scroll_and_sync }) + + function set_scroll_and_sync () { + drive_updated_by_scroll = true + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + vertical_scroll_value = container.scrollTop + } } // Traverses the hierarchical `all_entries` data and builds a flat `view` array for rendering. @@ -641,7 +663,6 @@ async function graph_explorer (opts) { const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path - if (is_search_match) { el.classList.add('search-result') if (is_direct_match) el.classList.add('direct-match') @@ -660,7 +681,6 @@ async function graph_explorer (opts) { } if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) - const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) const pipe_html = pipe_trail.map(p => ``).join('') const prefix_class = has_subs ? 'prefix clickable' : 'prefix' @@ -674,7 +694,6 @@ async function graph_explorer (opts) { let has_duplicate_entries = false let is_first_occurrence = false if (mode !== 'search' && hubs_flag !== 'true') { // disabled in search mode and when hubs_flag is 'true' - collect_all_duplicate_entries() has_duplicate_entries = has_duplicates(base_path) // coloring class for duplicates @@ -697,17 +716,7 @@ async function graph_explorer (opts) { // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate if (has_duplicate_entries && !is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { - el.onclick = () => { - // Manually update last clicked - last_clicked_node = instance_path - drive_updated_by_match = true - - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) - - // Manually update DOM - update_last_clicked_styling(instance_path) - add_jump_button_to_matching_entry(el, base_path, instance_path) - } + el.onclick = jump_out_to_next_duplicate } else { const icon_el = el.querySelector('.icon') if (icon_el && has_hubs && base_path !== '/') { @@ -747,6 +756,17 @@ async function graph_explorer (opts) { if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) return el + function jump_out_to_next_duplicate () { + // Manually update last clicked + last_clicked_node = instance_path + drive_updated_by_match = true + + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + // Manually update DOM + update_last_clicked_styling(instance_path) + add_jump_button_to_matching_entry(el, base_path, instance_path) + } } // `re_render_node` updates a single node in the DOM, used when only its selection state changes. @@ -1186,8 +1206,24 @@ async function graph_explorer (opts) { function toggle_subs (instance_path) { const state = get_or_create_state(instance_states, instance_path) + const was_expanded = state.expanded_subs state.expanded_subs = !state.expanded_subs + // Update view order tracking for the toggled subs + const base_path = instance_path.split('|').pop() + const entry = all_entries[base_path] + if (entry && Array.isArray(entry.subs)) { + entry.subs.forEach(sub_path => { + if (was_expanded) { + // Collapsing so + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + } else { + // Expanding so + add_instances_recursively(sub_path, instance_path, instance_states, all_entries) + } + }) + } + last_clicked_node = instance_path update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) @@ -1199,9 +1235,25 @@ async function graph_explorer (opts) { function toggle_hubs (instance_path) { const state = get_or_create_state(instance_states, instance_path) + const was_expanded = state.expanded_hubs state.expanded_hubs ? hub_num-- : hub_num++ state.expanded_hubs = !state.expanded_hubs + // Update view order tracking for the toggled hubs + const base_path = instance_path.split('|').pop() + const entry = all_entries[base_path] + if (entry && Array.isArray(entry.hubs)) { + entry.hubs.forEach(hub_path => { + if (was_expanded) { + // Collapsing so + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + } else { + // Expanding so + add_instances_recursively(hub_path, instance_path, instance_states, all_entries) + } + }) + } + last_clicked_node = instance_path drive_updated_by_scroll = true // Prevent onbatch interference with hub spacer update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) @@ -1238,6 +1290,9 @@ async function graph_explorer (opts) { function reset () { // reset all of the manual expansions made instance_states = {} + view_order_tracking = {} // Clear view order tracking on reset + drive_updated_by_tracking = true + update_drive_state({ type: 'runtime/view_order_tracking', message: view_order_tracking }) if (mode === 'search') { search_entry_states = {} drive_updated_by_toggle = true @@ -1264,7 +1319,8 @@ async function graph_explorer (opts) { function onscroll () { if (scroll_update_pending) return scroll_update_pending = true - requestAnimationFrame(() => { + requestAnimationFrame(scroll_frames) + function scroll_frames () { const scroll_delta = vertical_scroll_value - container.scrollTop // Handle removal of the scroll spacer. if (spacer_element && scroll_delta > 0 && container.scrollTop === 0) { @@ -1277,7 +1333,7 @@ async function graph_explorer (opts) { vertical_scroll_value = update_scroll_state({ current_value: vertical_scroll_value, new_value: container.scrollTop, name: 'vertical_scroll_value' }) horizontal_scroll_value = update_scroll_state({ current_value: horizontal_scroll_value, new_value: container.scrollLeft, name: 'horizontal_scroll_value' }) scroll_update_pending = false - }) + } } async function fill_viewport_downwards () { @@ -1364,16 +1420,8 @@ async function graph_explorer (opts) { function collect_all_duplicate_entries () { duplicate_entries_map = {} - const base_path_counts = {} - for (const node of view) { - if (!base_path_counts[node.base_path]) { - base_path_counts[node.base_path] = [] - } - base_path_counts[node.base_path].push(node.instance_path) - } - - // Store only duplicates with first occurrence info - for (const [base_path, instance_paths] of Object.entries(base_path_counts)) { + // Use view_order_tracking for duplicate detection + for (const [base_path, instance_paths] of Object.entries(view_order_tracking)) { if (instance_paths.length > 1) { duplicate_entries_map[base_path] = { instances: instance_paths, @@ -1383,6 +1431,101 @@ async function graph_explorer (opts) { } } + function initialize_tracking_from_current_state () { + const root_path = '/' + const root_instance_path = '|/' + if (all_entries[root_path]) { + add_instance_to_view_tracking(root_path, root_instance_path) + // Add initially expanded subs if any + const root_entry = all_entries[root_path] + if (root_entry && Array.isArray(root_entry.subs)) { + root_entry.subs.forEach(sub_path => { + add_instances_recursively(sub_path, root_instance_path, instance_states, all_entries) + }) + } + } + } + + function add_instance_to_view_tracking (base_path, instance_path) { + if (!view_order_tracking[base_path]) view_order_tracking[base_path] = [] + if (!view_order_tracking[base_path].includes(instance_path)) { + view_order_tracking[base_path].push(instance_path) + + // Only save to drive if not currently loading from drive + if (!is_loading_from_drive) { + drive_updated_by_tracking = true + update_drive_state({ type: 'runtime/view_order_tracking', message: view_order_tracking }) + } + } + } + + function remove_instance_from_view_tracking (base_path, instance_path) { + if (view_order_tracking[base_path]) { + const index = view_order_tracking[base_path].indexOf(instance_path) + if (index !== -1) { + view_order_tracking[base_path].splice(index, 1) + // Clean up empty arrays + if (view_order_tracking[base_path].length === 0) { + delete view_order_tracking[base_path] + } + + // Only save to drive if not currently loading from drive + if (!is_loading_from_drive) { + drive_updated_by_tracking = true + update_drive_state({ type: 'runtime/view_order_tracking', message: view_order_tracking }) + } + } + } + } + + // Recursively add instances to tracking when expanding + function add_instances_recursively (base_path, parent_instance_path, instance_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + entry.hubs.forEach(hub_path => { + add_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + entry.subs.forEach(sub_path => { + add_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } + + // Add the instance itself + add_instance_to_view_tracking(base_path, instance_path) + } + + // Recursively remove instances from tracking when collapsing + function remove_instances_recursively (base_path, parent_instance_path, instance_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + entry.hubs.forEach(hub_path => { + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + entry.subs.forEach(sub_path => { + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } + + // Remove the instance itself + remove_instance_from_view_tracking(base_path, instance_path) + } + function get_next_duplicate_instance (base_path, current_instance_path) { const duplicates = duplicate_entries_map[base_path] if (!duplicates || duplicates.instances.length <= 1) return null @@ -1419,12 +1562,13 @@ async function graph_explorer (opts) { update_drive_state({ type: 'runtime/last_clicked_node', message: next_instance_path }) // Add jump button to the target entry (with a small delay to ensure DOM is ready) - setTimeout(() => { + setTimeout(jump_out, 10) + function jump_out () { const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(next_instance_path)}"]`) if (target_element) { add_jump_button_to_matching_entry(target_element, base_path, next_instance_path) } - }, 10) + } } } @@ -1588,6 +1732,10 @@ async function graph_explorer (opts) { drive_updated_by_match = false return true } + if (drive_updated_by_tracking) { + drive_updated_by_tracking = false + return true + } return false } @@ -1640,17 +1788,7 @@ async function graph_explorer (opts) { container.appendChild(spacer_element) if (hub_toggle) { - requestAnimationFrame(() => { - const container_height = container.clientHeight - const content_height = view.length * node_height - const max_scroll_top = content_height - container_height - - if (new_scroll_top > max_scroll_top) { - spacer_initial_height = new_scroll_top - max_scroll_top - spacer_element.style.height = `${spacer_initial_height}px` - } - sync_fn() - }) + requestAnimationFrame(spacer_frames) } else { spacer_element.style.height = `${existing_height}px` requestAnimationFrame(sync_fn) @@ -1660,6 +1798,17 @@ async function graph_explorer (opts) { spacer_initial_height = 0 requestAnimationFrame(sync_fn) } + function spacer_frames () { + const container_height = container.clientHeight + const content_height = view.length * node_height + const max_scroll_top = content_height - container_height + + if (new_scroll_top > max_scroll_top) { + spacer_initial_height = new_scroll_top - max_scroll_top + spacer_element.style.height = `${spacer_initial_height}px` + } + sync_fn() + } } function create_root_node ({ state, has_subs, instance_path }) { @@ -1740,7 +1889,8 @@ function fallback_module () { 'confirmed_selected.json': { raw: '[]' }, 'instance_states.json': { raw: '{}' }, 'search_entry_states.json': { raw: '{}' }, - 'last_clicked_node.json': { raw: 'null' } + 'last_clicked_node.json': { raw: 'null' }, + 'view_order_tracking.json': { raw: '{}' } }, 'mode/': { 'current_mode.json': { raw: '"menubar"' }, From e4448926d8b91ea3be35c927f09008ee676a14da Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 23 Sep 2025 10:01:23 +0500 Subject: [PATCH 094/130] Fixed Last-Clicked inside search mode --- lib/graph_explorer.js | 334 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 311 insertions(+), 23 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 63f6bc6..44e479c 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -30,6 +30,7 @@ async function graph_explorer (opts) { let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. + let ignore_drive_updated_by_scroll = false // Prevent scroll flag. let drive_updated_by_match = false // Flag to prevent `onbatch` from re-rendering on matching entry updates. let drive_updated_by_tracking = false // Flag to prevent `onbatch` from re-rendering on view order tracking updates. let is_loading_from_drive = false // Flag to prevent saving to drive during initial load @@ -96,8 +97,24 @@ async function graph_explorer (opts) { - `onbatch` is the primary entry point. ******************************************************************************/ async function onbatch (batch) { + console.log('[SEARCH DEBUG] onbatch caled:', { + mode, + search_query, + last_clicked_node, + feedback_flags: { + scroll: drive_updated_by_scroll, + toggle: drive_updated_by_toggle, + search: drive_updated_by_search, + match: drive_updated_by_match, + tracking: drive_updated_by_tracking + } + }) + // Prevent feedback loops from scroll or toggle actions. - if (check_and_reset_feedback_flags()) return + if (check_and_reset_feedback_flags()) { + console.log('[SEARCH DEBUG] onbatch prevented by feedback flags') + return + } for (const { type, paths } of batch) { if (!paths || !paths.length) continue @@ -146,8 +163,14 @@ async function graph_explorer (opts) { expanded_hubs: false } } - // trcking will be initialized later if drive data is empty - build_and_render_view() + // don't rebuild view if we're in search mode with active query + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_entries: skipping build_and_render_view in Search Mode with query:', search_query) + perform_search(search_query) + } else { + // tracking will be initialized later if drive data is empty + build_and_render_view() + } } else { console.warn('Root path "/" not found in entries. Clearing view.') view = [] @@ -184,8 +207,14 @@ async function graph_explorer (opts) { } }) - if (needs_render) build_and_render_view() - else if (render_nodes_needed.size > 0) { + if (needs_render) { + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_runtime: Skipping build_and_render_view in search mode with query:', search_query) + perform_search(search_query) + } else { + build_and_render_view() + } + } else if (render_nodes_needed.size > 0) { render_nodes_needed.forEach(re_render_node) } @@ -351,7 +380,14 @@ async function graph_explorer (opts) { const handler = on_flags_paths[filename] if (handler) { const result = handler(value) - if (result && result.needs_render) build_and_render_view() + if (result && result.needs_render) { + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_flags: Skipping build_and_render_view in search mode with query:', search_query) + perform_search(search_query) + } else { + build_and_render_view() + } + } } }) @@ -468,6 +504,25 @@ async function graph_explorer (opts) { - `calculate_mobile_scale` calculates the scale factor for mobile devices. ******************************************************************************/ function build_and_render_view (focal_instance_path, hub_toggle = false) { + console.log('[SEARCH DEBUG] build_and_render_view called:', { + focal_instance_path, + hub_toggle, + current_mode: mode, + search_query, + last_clicked_node, + stack_trace: new Error().stack.split('\n').slice(1, 4).map(line => line.trim()) + }) + + // This fuction should'nt be called in search mode for search + if (mode === 'search' && search_query && !hub_toggle) { + console.error('[SEARCH DEBUG] build_and_render_view called inappropriately in search mode!', { + mode, + search_query, + focal_instance_path, + stack_trace: new Error().stack.split('\n').slice(1, 6).map(line => line.trim()) + }) + } + if (Object.keys(all_entries).length === 0) { console.warn('No entries available to render.') return @@ -745,7 +800,7 @@ async function graph_explorer (opts) { }, 10) } } else { - name_el.onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) + name_el.onclick = ev => mode === 'search' ? handle_search_name_click(ev, instance_path) : select_node(ev, instance_path) } } @@ -880,12 +935,14 @@ async function graph_explorer (opts) { } function toggle_search_mode () { + const target_mode = mode === 'search' ? previous_mode : 'search' + console.log('[SEARCH DEBUG] Switching mode from', mode, 'to', target_mode) if (mode === 'search') { search_query = '' - drive_updated_by_search = true update_drive_state({ type: 'mode/search_query', message: '' }) } - update_drive_state({ type: 'mode/current_mode', message: mode === 'search' ? previous_mode : 'search' }) + ignore_drive_updated_by_scroll = true + update_drive_state({ type: 'mode/current_mode', message: target_mode }) search_state_instances = instance_states } @@ -922,7 +979,28 @@ async function graph_explorer (opts) { } function perform_search (query) { - if (!query) return build_and_render_view() + console.log('[SEARCH DEBUG] perform_search called:', { + query, + current_mode: mode, + search_query_var: search_query, + has_search_entry_states: Object.keys(search_entry_states).length > 0, + last_clicked_node + }) + + // Check if we are actualy in search mode + if (mode !== 'search') { + console.error('[SEARCH DEBUG] perform_search called but not in search mode!', { + current_mode: mode, + query + }) + return build_and_render_view() + } + + if (!query) { + console.log('[SEARCH DEBUG] No query provided, building default view') + return build_and_render_view() + } + const original_view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -949,6 +1027,8 @@ async function graph_explorer (opts) { all_entries, original_view_paths }) + + console.log('[SEARCH DEBUG] Search view built:', search_view.length) render_search_results(search_view, query) } @@ -1151,31 +1231,72 @@ async function graph_explorer (opts) { // Add the clicked entry and all its parents in the default tree function search_expand_into_default (target_instance_path) { - if (!target_instance_path) return + console.log('[SEARCH DEBUG] search_expand_into_default called:', { + target_instance_path, + current_mode: mode, + search_query, + previous_mode, + current_search_entry_states: Object.keys(search_entry_states).length, + current_instance_states: Object.keys(instance_states).length + }) + + if (!target_instance_path) { + console.warn('[SEARCH DEBUG] No target_instance_path provided') + return + } + const parts = target_instance_path.split('|').filter(Boolean) - if (parts.length === 0) return + if (parts.length === 0) { + console.warn('[SEARCH DEBUG] No valid parts found in instance path:', target_instance_path) + return + } + + console.log('[SEARCH DEBUG] Parsed instance path parts:', parts) + + console.log('[SEARCH DEBUG] About to call handle_search_node_click before mode transition') + handle_search_node_click(target_instance_path) + console.log('[SEARCH DEBUG] Setting up default mode expansion states') const root_state = get_or_create_state(instance_states, '|/') root_state.expanded_subs = true - // Walk from root to target, expanding the path relative to alredy expanded entries + // Walk from root to target, expanding the path relative to already expanded entries for (let i = 0; i < parts.length - 1; i++) { const parent_base = parts[i] const child_base = parts[i + 1] const parent_instance_path = parts.slice(0, i + 1).map(p => '|' + p).join('') const parent_state = get_or_create_state(instance_states, parent_instance_path) const parent_entry = all_entries[parent_base] + + console.log('[SEARCH DEBUG] Processing parent-child relationship:', { + parent_base, + child_base, + parent_instance_path, + has_parent_entry: !!parent_entry + }) + if (!parent_entry) continue - if (Array.isArray(parent_entry.subs) && parent_entry.subs.includes(child_base)) parent_state.expanded_subs = true - if (Array.isArray(parent_entry.hubs) && parent_entry.hubs.includes(child_base)) parent_state.expanded_hubs = true + if (Array.isArray(parent_entry.subs) && parent_entry.subs.includes(child_base)) { + parent_state.expanded_subs = true + console.log('[SEARCH DEBUG] Expanded subs for:', parent_instance_path) + } + if (Array.isArray(parent_entry.hubs) && parent_entry.hubs.includes(child_base)) { + parent_state.expanded_hubs = true + console.log('[SEARCH DEBUG] Expanded hubs for:', parent_instance_path) + } } + console.log('[SEARCH DEBUG] Current mode before switch:', mode) + console.log('[SEARCH DEBUG] Target previous_mode:', previous_mode) + // Persist selection and expansion state update_drive_state({ type: 'runtime/selected_instance_paths', message: [target_instance_path] }) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) search_query = '' update_drive_state({ type: 'mode/query', message: '' }) + + console.log('[SEARCH DEBUG] About to switch from search mode to:', previous_mode) update_drive_state({ type: 'mode/current_mode', message: previous_mode }) } @@ -1185,8 +1306,13 @@ async function graph_explorer (opts) { const new_selected = new Set(selected_instance_paths) const new_confirmed = new Set(confirmed_instance_paths) - last_clicked_node = instance_path - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + // use specific logic for mode + if (mode === 'search') { + handle_search_node_click(instance_path) + } else { + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + } if (is_checked) { new_selected.delete(instance_path) @@ -1260,11 +1386,24 @@ async function graph_explorer (opts) { } function toggle_search_subs (instance_path) { + console.log('[SEARCH DEBUG] toggle_search_subs called:', { + instance_path, + mode, + search_query, + current_state: search_entry_states[instance_path]?.expanded_subs || false + }) + const state = get_or_create_state(search_entry_states, instance_path) + const old_expanded = state.expanded_subs state.expanded_subs = !state.expanded_subs - last_clicked_node = instance_path - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + console.log('[SEARCH DEBUG] Toggled subs state:', { + instance_path, + old_expanded, + new_expanded: state.expanded_subs + }) + + handle_search_node_click(instance_path) perform_search(search_query) drive_updated_by_search = true @@ -1272,15 +1411,163 @@ async function graph_explorer (opts) { } function toggle_search_hubs (instance_path) { + console.log('[SEARCH DEBUG] toggle_search_hubs called:', { + instance_path, + mode, + search_query, + current_state: search_entry_states[instance_path]?.expanded_hubs || false + }) + const state = get_or_create_state(search_entry_states, instance_path) + const old_expanded = state.expanded_hubs state.expanded_hubs = !state.expanded_hubs - last_clicked_node = instance_path - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + console.log('[SEARCH DEBUG] Toggled hubs state:', { + instance_path, + old_expanded, + new_expanded: state.expanded_hubs + }) + handle_search_node_click(instance_path) + + console.log('[SEARCH DEBUG] About to perform_search after toggle_search_hubs') perform_search(search_query) drive_updated_by_search = true update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) + console.log('[SEARCH DEBUG] toggle_search_hubs completed') + } + + function handle_search_node_click (instance_path) { + console.log('[SEARCH DEBUG] handle_search_node_click called:', { + instance_path, + current_mode: mode, + search_query, + previous_last_clicked: last_clicked_node + }) + + if (mode !== 'search') { + console.warn('[SEARCH DEBUG] handle_search_node_click called but not in search mode!', { + current_mode: mode, + instance_path + }) + return + } + + // we need to handle last_clicked_node differently + const old_last_clicked = last_clicked_node + last_clicked_node = instance_path + + console.log('[SEARCH DEBUG] Updating last_clicked_node:', { + old_value: old_last_clicked, + new_value: last_clicked_node, + mode, + search_query + }) + + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + // Update visual styling for search mode nodes + update_search_last_clicked_styling(instance_path) + } + + function update_search_last_clicked_styling (target_instance_path) { + console.log('[SEARCH DEBUG] update_search_last_clicked_styling called:', { + target_instance_path, + mode, + search_query + }) + + // Remove `last-clicked` class from all search result nodes + const search_nodes = container.querySelectorAll('.node.search-result') + console.log('[SEARCH DEBUG] Found search result nodes:', search_nodes.length) + search_nodes.forEach(node => { + const was_last_clicked = node.classList.contains('last-clicked') + node.classList.remove('last-clicked') + if (was_last_clicked) { + console.log('[SEARCH DEBUG] Removed last-clicked from:', node.dataset.instance_path) + } + }) + + // Add last-clicked class to the target node if it exists in search results + const target_node = container.querySelector(`[data-instance_path="${target_instance_path}"].search-result`) + if (target_node) { + target_node.classList.add('last-clicked') + console.log('[SEARCH DEBUG] Added last-clicked to target node:', target_instance_path) + } else { + console.warn('[SEARCH DEBUG] Target node not found in search results:', { + target_instance_path, + available_search_nodes: Array.from(search_nodes).map(n => n.dataset.instance_path) + }) + } + } + + function handle_search_name_click (ev, instance_path) { + console.log('[SEARCH DEBUG] handle_search_name_click called:', { + instance_path, + mode, + search_query, + ctrlKey: ev.ctrlKey, + metaKey: ev.metaKey, + shiftKey: ev.shiftKey, + multi_select_enabled, + current_selected: selected_instance_paths.length + }) + + if (mode !== 'search') { + console.error('[SEARCH DEBUG] handle_search_name_click called but not in search mode!', { + current_mode: mode, + instance_path + }) + return + } + + handle_search_node_click(instance_path) + + if (ev.ctrlKey || ev.metaKey || multi_select_enabled) { + search_select_node(ev, instance_path) + } else if (ev.shiftKey) { + search_select_node(ev, instance_path) + } else { + // Regular click + search_expand_into_default(instance_path) + } + } + + function search_select_node (ev, instance_path) { + console.log('[SEARCH DEBUG] search_select_node called:', { + instance_path, + mode, + search_query, + shiftKey: ev.shiftKey, + ctrlKey: ev.ctrlKey, + metaKey: ev.metaKey, + multi_select_enabled, + select_between_enabled, + current_selected: selected_instance_paths + }) + + if (ev.shiftKey && !select_between_enabled) { + select_between_enabled = true + select_between_first_node = instance_path + update_drive_state({ type: 'mode/select_between_enabled', message: true }) + return + } + + if (multi_select_enabled || ev.ctrlKey || ev.metaKey) { + const new_selected = new Set(selected_instance_paths) + if (new_selected.has(instance_path)) { + console.log('[SEARCH DEBUG] Deselecting node:', instance_path) + new_selected.delete(instance_path) + } else { + console.log('[SEARCH DEBUG] Selecting node:', instance_path) + new_selected.add(instance_path) + } + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) + } else { + update_drive_state({ type: 'runtime/selected_instance_paths', message: [instance_path] }) + } + + console.log('[SEARCH DEBUG] search_select_node completed, new selection:', selected_instance_paths) } function reset () { @@ -1712,10 +1999,10 @@ async function graph_explorer (opts) { } function check_and_reset_feedback_flags () { - if (drive_updated_by_scroll) { + if (drive_updated_by_scroll && !ignore_drive_updated_by_scroll) { drive_updated_by_scroll = false return true - } + } else ignore_drive_updated_by_scroll = false if (drive_updated_by_toggle) { drive_updated_by_toggle = false return true @@ -1732,6 +2019,7 @@ async function graph_explorer (opts) { drive_updated_by_tracking = false return true } + console.log('[SEARCH DEBUG] No feedback flags set, allowing onbatch') return false } From 8f220cc25c620fd4b8f2cb81e4820e34750d5bdf Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 23 Sep 2025 21:55:02 +0500 Subject: [PATCH 095/130] temp fix for prefix in search mode --- lib/graph_explorer.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 44e479c..9b669b6 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -45,6 +45,7 @@ async function graph_explorer (opts) { let hub_num = 0 // Counter for expanded hubs. let last_clicked_node = null // Track the last clicked node instance path for highlighting. let root_wand_state = null // Store original root wand state when replaced with jump button + const manipulated_inside_search = {} const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -421,6 +422,10 @@ async function graph_explorer (opts) { if (!states[instance_path]) { states[instance_path] = { expanded_subs: false, expanded_hubs: false } } + if (states[instance_path].expanded_subs === null) { + states[instance_path].expanded_subs = true + } + return states[instance_path] } @@ -697,7 +702,17 @@ async function graph_explorer (opts) { return err_el } - const states = mode === 'search' ? search_state_instances : instance_states + let states + if (mode === 'search') { + if (manipulated_inside_search[instance_path]) { + search_entry_states[instance_path] = manipulated_inside_search[instance_path] + states = search_entry_states + } else { + states = search_state_instances + } + } else { + states = instance_states + } const state = get_or_create_state(states, instance_path) const { pipe_trail, is_hub_on_top } = calculate_pipe_trail({ @@ -1396,7 +1411,9 @@ async function graph_explorer (opts) { const state = get_or_create_state(search_entry_states, instance_path) const old_expanded = state.expanded_subs state.expanded_subs = !state.expanded_subs - + const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs ? null : true + const has_matching_parents = manipulated_inside_search[instance_path] ? search_entry_states[instance_path]?.expanded_hubs : search_state_instances[instance_path]?.expanded_hubs + manipulated_inside_search[instance_path] = { expanded_hubs: has_matching_parents, expanded_subs: has_matching_descendant } console.log('[SEARCH DEBUG] Toggled subs state:', { instance_path, old_expanded, @@ -1421,7 +1438,8 @@ async function graph_explorer (opts) { const state = get_or_create_state(search_entry_states, instance_path) const old_expanded = state.expanded_hubs state.expanded_hubs = !state.expanded_hubs - + const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs + manipulated_inside_search[instance_path] = { expanded_hubs: state.expanded_hubs, expanded_subs: has_matching_descendant } console.log('[SEARCH DEBUG] Toggled hubs state:', { instance_path, old_expanded, From 60bdb4ff7fb0dd57383969b72f61d55c26a049d1 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 23 Sep 2025 22:16:18 +0500 Subject: [PATCH 096/130] bundled --- bundle.js | 354 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 330 insertions(+), 24 deletions(-) diff --git a/bundle.js b/bundle.js index 6d7374f..5c361c5 100644 --- a/bundle.js +++ b/bundle.js @@ -34,6 +34,7 @@ async function graph_explorer (opts) { let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. + let ignore_drive_updated_by_scroll = false // Prevent scroll flag. let drive_updated_by_match = false // Flag to prevent `onbatch` from re-rendering on matching entry updates. let drive_updated_by_tracking = false // Flag to prevent `onbatch` from re-rendering on view order tracking updates. let is_loading_from_drive = false // Flag to prevent saving to drive during initial load @@ -48,6 +49,7 @@ async function graph_explorer (opts) { let hub_num = 0 // Counter for expanded hubs. let last_clicked_node = null // Track the last clicked node instance path for highlighting. let root_wand_state = null // Store original root wand state when replaced with jump button + const manipulated_inside_search = {} const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -100,8 +102,24 @@ async function graph_explorer (opts) { - `onbatch` is the primary entry point. ******************************************************************************/ async function onbatch (batch) { + console.log('[SEARCH DEBUG] onbatch caled:', { + mode, + search_query, + last_clicked_node, + feedback_flags: { + scroll: drive_updated_by_scroll, + toggle: drive_updated_by_toggle, + search: drive_updated_by_search, + match: drive_updated_by_match, + tracking: drive_updated_by_tracking + } + }) + // Prevent feedback loops from scroll or toggle actions. - if (check_and_reset_feedback_flags()) return + if (check_and_reset_feedback_flags()) { + console.log('[SEARCH DEBUG] onbatch prevented by feedback flags') + return + } for (const { type, paths } of batch) { if (!paths || !paths.length) continue @@ -150,8 +168,14 @@ async function graph_explorer (opts) { expanded_hubs: false } } - // trcking will be initialized later if drive data is empty - build_and_render_view() + // don't rebuild view if we're in search mode with active query + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_entries: skipping build_and_render_view in Search Mode with query:', search_query) + perform_search(search_query) + } else { + // tracking will be initialized later if drive data is empty + build_and_render_view() + } } else { console.warn('Root path "/" not found in entries. Clearing view.') view = [] @@ -188,8 +212,14 @@ async function graph_explorer (opts) { } }) - if (needs_render) build_and_render_view() - else if (render_nodes_needed.size > 0) { + if (needs_render) { + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_runtime: Skipping build_and_render_view in search mode with query:', search_query) + perform_search(search_query) + } else { + build_and_render_view() + } + } else if (render_nodes_needed.size > 0) { render_nodes_needed.forEach(re_render_node) } @@ -355,7 +385,14 @@ async function graph_explorer (opts) { const handler = on_flags_paths[filename] if (handler) { const result = handler(value) - if (result && result.needs_render) build_and_render_view() + if (result && result.needs_render) { + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_flags: Skipping build_and_render_view in search mode with query:', search_query) + perform_search(search_query) + } else { + build_and_render_view() + } + } } }) @@ -389,6 +426,10 @@ async function graph_explorer (opts) { if (!states[instance_path]) { states[instance_path] = { expanded_subs: false, expanded_hubs: false } } + if (states[instance_path].expanded_subs === null) { + states[instance_path].expanded_subs = true + } + return states[instance_path] } @@ -472,6 +513,25 @@ async function graph_explorer (opts) { - `calculate_mobile_scale` calculates the scale factor for mobile devices. ******************************************************************************/ function build_and_render_view (focal_instance_path, hub_toggle = false) { + console.log('[SEARCH DEBUG] build_and_render_view called:', { + focal_instance_path, + hub_toggle, + current_mode: mode, + search_query, + last_clicked_node, + stack_trace: new Error().stack.split('\n').slice(1, 4).map(line => line.trim()) + }) + + // This fuction should'nt be called in search mode for search + if (mode === 'search' && search_query && !hub_toggle) { + console.error('[SEARCH DEBUG] build_and_render_view called inappropriately in search mode!', { + mode, + search_query, + focal_instance_path, + stack_trace: new Error().stack.split('\n').slice(1, 6).map(line => line.trim()) + }) + } + if (Object.keys(all_entries).length === 0) { console.warn('No entries available to render.') return @@ -646,7 +706,17 @@ async function graph_explorer (opts) { return err_el } - const states = mode === 'search' ? search_state_instances : instance_states + let states + if (mode === 'search') { + if (manipulated_inside_search[instance_path]) { + search_entry_states[instance_path] = manipulated_inside_search[instance_path] + states = search_entry_states + } else { + states = search_state_instances + } + } else { + states = instance_states + } const state = get_or_create_state(states, instance_path) const { pipe_trail, is_hub_on_top } = calculate_pipe_trail({ @@ -749,7 +819,7 @@ async function graph_explorer (opts) { }, 10) } } else { - name_el.onclick = ev => mode === 'search' ? search_expand_into_default(instance_path) : select_node(ev, instance_path) + name_el.onclick = ev => mode === 'search' ? handle_search_name_click(ev, instance_path) : select_node(ev, instance_path) } } @@ -884,12 +954,14 @@ async function graph_explorer (opts) { } function toggle_search_mode () { + const target_mode = mode === 'search' ? previous_mode : 'search' + console.log('[SEARCH DEBUG] Switching mode from', mode, 'to', target_mode) if (mode === 'search') { search_query = '' - drive_updated_by_search = true update_drive_state({ type: 'mode/search_query', message: '' }) } - update_drive_state({ type: 'mode/current_mode', message: mode === 'search' ? previous_mode : 'search' }) + ignore_drive_updated_by_scroll = true + update_drive_state({ type: 'mode/current_mode', message: target_mode }) search_state_instances = instance_states } @@ -926,7 +998,28 @@ async function graph_explorer (opts) { } function perform_search (query) { - if (!query) return build_and_render_view() + console.log('[SEARCH DEBUG] perform_search called:', { + query, + current_mode: mode, + search_query_var: search_query, + has_search_entry_states: Object.keys(search_entry_states).length > 0, + last_clicked_node + }) + + // Check if we are actualy in search mode + if (mode !== 'search') { + console.error('[SEARCH DEBUG] perform_search called but not in search mode!', { + current_mode: mode, + query + }) + return build_and_render_view() + } + + if (!query) { + console.log('[SEARCH DEBUG] No query provided, building default view') + return build_and_render_view() + } + const original_view = build_view_recursive({ base_path: '/', parent_instance_path: '', @@ -953,6 +1046,8 @@ async function graph_explorer (opts) { all_entries, original_view_paths }) + + console.log('[SEARCH DEBUG] Search view built:', search_view.length) render_search_results(search_view, query) } @@ -1155,31 +1250,72 @@ async function graph_explorer (opts) { // Add the clicked entry and all its parents in the default tree function search_expand_into_default (target_instance_path) { - if (!target_instance_path) return + console.log('[SEARCH DEBUG] search_expand_into_default called:', { + target_instance_path, + current_mode: mode, + search_query, + previous_mode, + current_search_entry_states: Object.keys(search_entry_states).length, + current_instance_states: Object.keys(instance_states).length + }) + + if (!target_instance_path) { + console.warn('[SEARCH DEBUG] No target_instance_path provided') + return + } + const parts = target_instance_path.split('|').filter(Boolean) - if (parts.length === 0) return + if (parts.length === 0) { + console.warn('[SEARCH DEBUG] No valid parts found in instance path:', target_instance_path) + return + } + console.log('[SEARCH DEBUG] Parsed instance path parts:', parts) + + console.log('[SEARCH DEBUG] About to call handle_search_node_click before mode transition') + handle_search_node_click(target_instance_path) + + console.log('[SEARCH DEBUG] Setting up default mode expansion states') const root_state = get_or_create_state(instance_states, '|/') root_state.expanded_subs = true - // Walk from root to target, expanding the path relative to alredy expanded entries + // Walk from root to target, expanding the path relative to already expanded entries for (let i = 0; i < parts.length - 1; i++) { const parent_base = parts[i] const child_base = parts[i + 1] const parent_instance_path = parts.slice(0, i + 1).map(p => '|' + p).join('') const parent_state = get_or_create_state(instance_states, parent_instance_path) const parent_entry = all_entries[parent_base] + + console.log('[SEARCH DEBUG] Processing parent-child relationship:', { + parent_base, + child_base, + parent_instance_path, + has_parent_entry: !!parent_entry + }) + if (!parent_entry) continue - if (Array.isArray(parent_entry.subs) && parent_entry.subs.includes(child_base)) parent_state.expanded_subs = true - if (Array.isArray(parent_entry.hubs) && parent_entry.hubs.includes(child_base)) parent_state.expanded_hubs = true + if (Array.isArray(parent_entry.subs) && parent_entry.subs.includes(child_base)) { + parent_state.expanded_subs = true + console.log('[SEARCH DEBUG] Expanded subs for:', parent_instance_path) + } + if (Array.isArray(parent_entry.hubs) && parent_entry.hubs.includes(child_base)) { + parent_state.expanded_hubs = true + console.log('[SEARCH DEBUG] Expanded hubs for:', parent_instance_path) + } } + console.log('[SEARCH DEBUG] Current mode before switch:', mode) + console.log('[SEARCH DEBUG] Target previous_mode:', previous_mode) + // Persist selection and expansion state update_drive_state({ type: 'runtime/selected_instance_paths', message: [target_instance_path] }) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) search_query = '' update_drive_state({ type: 'mode/query', message: '' }) + + console.log('[SEARCH DEBUG] About to switch from search mode to:', previous_mode) update_drive_state({ type: 'mode/current_mode', message: previous_mode }) } @@ -1189,8 +1325,13 @@ async function graph_explorer (opts) { const new_selected = new Set(selected_instance_paths) const new_confirmed = new Set(confirmed_instance_paths) - last_clicked_node = instance_path - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + // use specific logic for mode + if (mode === 'search') { + handle_search_node_click(instance_path) + } else { + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + } if (is_checked) { new_selected.delete(instance_path) @@ -1264,11 +1405,26 @@ async function graph_explorer (opts) { } function toggle_search_subs (instance_path) { + console.log('[SEARCH DEBUG] toggle_search_subs called:', { + instance_path, + mode, + search_query, + current_state: search_entry_states[instance_path]?.expanded_subs || false + }) + const state = get_or_create_state(search_entry_states, instance_path) + const old_expanded = state.expanded_subs state.expanded_subs = !state.expanded_subs + const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs ? null : true + const has_matching_parents = manipulated_inside_search[instance_path] ? search_entry_states[instance_path]?.expanded_hubs : search_state_instances[instance_path]?.expanded_hubs + manipulated_inside_search[instance_path] = { expanded_hubs: has_matching_parents, expanded_subs: has_matching_descendant } + console.log('[SEARCH DEBUG] Toggled subs state:', { + instance_path, + old_expanded, + new_expanded: state.expanded_subs + }) - last_clicked_node = instance_path - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + handle_search_node_click(instance_path) perform_search(search_query) drive_updated_by_search = true @@ -1276,15 +1432,164 @@ async function graph_explorer (opts) { } function toggle_search_hubs (instance_path) { + console.log('[SEARCH DEBUG] toggle_search_hubs called:', { + instance_path, + mode, + search_query, + current_state: search_entry_states[instance_path]?.expanded_hubs || false + }) + const state = get_or_create_state(search_entry_states, instance_path) + const old_expanded = state.expanded_hubs state.expanded_hubs = !state.expanded_hubs + const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs + manipulated_inside_search[instance_path] = { expanded_hubs: state.expanded_hubs, expanded_subs: has_matching_descendant } + console.log('[SEARCH DEBUG] Toggled hubs state:', { + instance_path, + old_expanded, + new_expanded: state.expanded_hubs + }) - last_clicked_node = instance_path - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + handle_search_node_click(instance_path) + console.log('[SEARCH DEBUG] About to perform_search after toggle_search_hubs') perform_search(search_query) drive_updated_by_search = true update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) + console.log('[SEARCH DEBUG] toggle_search_hubs completed') + } + + function handle_search_node_click (instance_path) { + console.log('[SEARCH DEBUG] handle_search_node_click called:', { + instance_path, + current_mode: mode, + search_query, + previous_last_clicked: last_clicked_node + }) + + if (mode !== 'search') { + console.warn('[SEARCH DEBUG] handle_search_node_click called but not in search mode!', { + current_mode: mode, + instance_path + }) + return + } + + // we need to handle last_clicked_node differently + const old_last_clicked = last_clicked_node + last_clicked_node = instance_path + + console.log('[SEARCH DEBUG] Updating last_clicked_node:', { + old_value: old_last_clicked, + new_value: last_clicked_node, + mode, + search_query + }) + + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + // Update visual styling for search mode nodes + update_search_last_clicked_styling(instance_path) + } + + function update_search_last_clicked_styling (target_instance_path) { + console.log('[SEARCH DEBUG] update_search_last_clicked_styling called:', { + target_instance_path, + mode, + search_query + }) + + // Remove `last-clicked` class from all search result nodes + const search_nodes = container.querySelectorAll('.node.search-result') + console.log('[SEARCH DEBUG] Found search result nodes:', search_nodes.length) + search_nodes.forEach(node => { + const was_last_clicked = node.classList.contains('last-clicked') + node.classList.remove('last-clicked') + if (was_last_clicked) { + console.log('[SEARCH DEBUG] Removed last-clicked from:', node.dataset.instance_path) + } + }) + + // Add last-clicked class to the target node if it exists in search results + const target_node = container.querySelector(`[data-instance_path="${target_instance_path}"].search-result`) + if (target_node) { + target_node.classList.add('last-clicked') + console.log('[SEARCH DEBUG] Added last-clicked to target node:', target_instance_path) + } else { + console.warn('[SEARCH DEBUG] Target node not found in search results:', { + target_instance_path, + available_search_nodes: Array.from(search_nodes).map(n => n.dataset.instance_path) + }) + } + } + + function handle_search_name_click (ev, instance_path) { + console.log('[SEARCH DEBUG] handle_search_name_click called:', { + instance_path, + mode, + search_query, + ctrlKey: ev.ctrlKey, + metaKey: ev.metaKey, + shiftKey: ev.shiftKey, + multi_select_enabled, + current_selected: selected_instance_paths.length + }) + + if (mode !== 'search') { + console.error('[SEARCH DEBUG] handle_search_name_click called but not in search mode!', { + current_mode: mode, + instance_path + }) + return + } + + handle_search_node_click(instance_path) + + if (ev.ctrlKey || ev.metaKey || multi_select_enabled) { + search_select_node(ev, instance_path) + } else if (ev.shiftKey) { + search_select_node(ev, instance_path) + } else { + // Regular click + search_expand_into_default(instance_path) + } + } + + function search_select_node (ev, instance_path) { + console.log('[SEARCH DEBUG] search_select_node called:', { + instance_path, + mode, + search_query, + shiftKey: ev.shiftKey, + ctrlKey: ev.ctrlKey, + metaKey: ev.metaKey, + multi_select_enabled, + select_between_enabled, + current_selected: selected_instance_paths + }) + + if (ev.shiftKey && !select_between_enabled) { + select_between_enabled = true + select_between_first_node = instance_path + update_drive_state({ type: 'mode/select_between_enabled', message: true }) + return + } + + if (multi_select_enabled || ev.ctrlKey || ev.metaKey) { + const new_selected = new Set(selected_instance_paths) + if (new_selected.has(instance_path)) { + console.log('[SEARCH DEBUG] Deselecting node:', instance_path) + new_selected.delete(instance_path) + } else { + console.log('[SEARCH DEBUG] Selecting node:', instance_path) + new_selected.add(instance_path) + } + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) + } else { + update_drive_state({ type: 'runtime/selected_instance_paths', message: [instance_path] }) + } + + console.log('[SEARCH DEBUG] search_select_node completed, new selection:', selected_instance_paths) } function reset () { @@ -1716,10 +2021,10 @@ async function graph_explorer (opts) { } function check_and_reset_feedback_flags () { - if (drive_updated_by_scroll) { + if (drive_updated_by_scroll && !ignore_drive_updated_by_scroll) { drive_updated_by_scroll = false return true - } + } else ignore_drive_updated_by_scroll = false if (drive_updated_by_toggle) { drive_updated_by_toggle = false return true @@ -1736,6 +2041,7 @@ async function graph_explorer (opts) { drive_updated_by_tracking = false return true } + console.log('[SEARCH DEBUG] No feedback flags set, allowing onbatch') return false } From 06a181c1567ef7feeacf71fd36979e8ee7330b14 Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 26 Sep 2025 10:42:18 +0500 Subject: [PATCH 097/130] Fixed multi-select in search mode --- lib/graph_explorer.js | 57 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 9b669b6..fce6ab0 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -953,6 +953,13 @@ async function graph_explorer (opts) { const target_mode = mode === 'search' ? previous_mode : 'search' console.log('[SEARCH DEBUG] Switching mode from', mode, 'to', target_mode) if (mode === 'search') { + // when switching from search to previous mode, expand selected entries + if (selected_instance_paths.length > 0) { + console.log('[SEARCH DEBUG] Expanding selected entries in default mode:', selected_instance_paths) + expand_selected_entries_in_default(selected_instance_paths) + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + } search_query = '' update_drive_state({ type: 'mode/search_query', message: '' }) } @@ -1245,7 +1252,7 @@ async function graph_explorer (opts) { } // Add the clicked entry and all its parents in the default tree - function search_expand_into_default (target_instance_path) { + function expand_entry_path_in_default (target_instance_path) { console.log('[SEARCH DEBUG] search_expand_into_default called:', { target_instance_path, current_mode: mode, @@ -1268,10 +1275,6 @@ async function graph_explorer (opts) { console.log('[SEARCH DEBUG] Parsed instance path parts:', parts) - console.log('[SEARCH DEBUG] About to call handle_search_node_click before mode transition') - handle_search_node_click(target_instance_path) - - console.log('[SEARCH DEBUG] Setting up default mode expansion states') const root_state = get_or_create_state(instance_states, '|/') root_state.expanded_subs = true @@ -1300,6 +1303,38 @@ async function graph_explorer (opts) { console.log('[SEARCH DEBUG] Expanded hubs for:', parent_instance_path) } } + } + + // expand multiple selected entry in the default tree + function expand_selected_entries_in_default (selected_paths) { + console.log('[SEARCH DEBUG] expand_selected_entries_in_default called:', { + selected_paths, + current_mode: mode, + search_query, + previous_mode + }) + + if (!Array.isArray(selected_paths) || selected_paths.length === 0) { + console.warn('[SEARCH DEBUG] No valid selected paths provided') + return + } + + // expand foreach selected path + selected_paths.forEach(path => { + expand_entry_path_in_default(path) + }) + + console.log('[SEARCH DEBUG] All selected entries expanded in default mode') + } + + // Add the clicked entry and all its parents in the default tree + function search_expand_into_default (target_instance_path) { + if (!target_instance_path) { + return + } + + handle_search_node_click(target_instance_path) + expand_entry_path_in_default(target_instance_path) console.log('[SEARCH DEBUG] Current mode before switch:', mode) console.log('[SEARCH DEBUG] Target previous_mode:', previous_mode) @@ -1309,7 +1344,7 @@ async function graph_explorer (opts) { drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) search_query = '' - update_drive_state({ type: 'mode/query', message: '' }) + update_drive_state({ type: 'mode/search_query', message: '' }) console.log('[SEARCH DEBUG] About to switch from search mode to:', previous_mode) update_drive_state({ type: 'mode/current_mode', message: previous_mode }) @@ -1580,12 +1615,14 @@ async function graph_explorer (opts) { console.log('[SEARCH DEBUG] Selecting node:', instance_path) new_selected.add(instance_path) } - update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) + const new_selection_array = [...new_selected] + update_drive_state({ type: 'runtime/selected_instance_paths', message: new_selection_array }) + console.log('[SEARCH DEBUG] search_select_node completed, new selection:', new_selection_array) } else { - update_drive_state({ type: 'runtime/selected_instance_paths', message: [instance_path] }) + const new_selection_array = [instance_path] + update_drive_state({ type: 'runtime/selected_instance_paths', message: new_selection_array }) + console.log('[SEARCH DEBUG] search_select_node completed, new selection:', new_selection_array) } - - console.log('[SEARCH DEBUG] search_select_node completed, new selection:', selected_instance_paths) } function reset () { From 3a537faa5408a214d27e984562d3b613ce477ada Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 26 Sep 2025 18:19:35 +0500 Subject: [PATCH 098/130] Fixed Select Between in search mode --- lib/graph_explorer.js | 77 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index fce6ab0..351c2a4 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -913,11 +913,11 @@ async function graph_explorer (opts) { const multi_select_button = document.createElement('button') multi_select_button.innerHTML = `Multi Select: ${multi_select_enabled ? 'true' : 'false'}` - multi_select_button.onclick = mode === 'search' ? null : toggle_multi_select + multi_select_button.onclick = toggle_multi_select const select_between_button = document.createElement('button') select_between_button.innerHTML = `Select Between: ${select_between_enabled ? 'true' : 'false'}` - select_between_button.onclick = mode === 'search' ? null : toggle_select_between + select_between_button.onclick = toggle_select_between menubar.replaceChildren(search_button, multi_select_button, select_between_button) } @@ -953,13 +953,20 @@ async function graph_explorer (opts) { const target_mode = mode === 'search' ? previous_mode : 'search' console.log('[SEARCH DEBUG] Switching mode from', mode, 'to', target_mode) if (mode === 'search') { - // when switching from search to previous mode, expand selected entries + // When switching from search to default mode, expand selected entries if (selected_instance_paths.length > 0) { console.log('[SEARCH DEBUG] Expanding selected entries in default mode:', selected_instance_paths) expand_selected_entries_in_default(selected_instance_paths) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) } + // Reset select-between mode when leaving search mode + if (select_between_enabled) { + select_between_enabled = false + select_between_first_node = null + update_drive_state({ type: 'mode/select_between_enabled', message: false }) + console.log('[SEARCH DEBUG] Reset select-between mode when leaving search') + } search_query = '' update_drive_state({ type: 'mode/search_query', message: '' }) } @@ -1580,6 +1587,9 @@ async function graph_explorer (opts) { search_select_node(ev, instance_path) } else if (ev.shiftKey) { search_select_node(ev, instance_path) + } else if (select_between_enabled) { + // Handle select-between mode when button is enabled + search_select_node(ev, instance_path) } else { // Regular click search_expand_into_default(instance_path) @@ -1596,18 +1606,55 @@ async function graph_explorer (opts) { metaKey: ev.metaKey, multi_select_enabled, select_between_enabled, + select_between_first_node, current_selected: selected_instance_paths }) - if (ev.shiftKey && !select_between_enabled) { + const new_selected = new Set(selected_instance_paths) + + if (select_between_enabled) { + if (!select_between_first_node) { + select_between_first_node = instance_path + console.log('[SEARCH DEBUG] Set first node for select between:', instance_path) + } else { + console.log('[SEARCH DEBUG] Completing select between range:', { + first: select_between_first_node, + second: instance_path + }) + const first_index = view.findIndex(n => n.instance_path === select_between_first_node) + const second_index = view.findIndex(n => n.instance_path === instance_path) + + if (first_index !== -1 && second_index !== -1) { + const start_index = Math.min(first_index, second_index) + const end_index = Math.max(first_index, second_index) + + // Toggle selection for all nodes in between + for (let i = start_index; i <= end_index; i++) { + const node_instance_path = view[i].instance_path + if (new_selected.has(node_instance_path)) { + new_selected.delete(node_instance_path) + } else { + new_selected.add(node_instance_path) + } + } + } + + // Reset select between mode after completing the selection + select_between_enabled = false + select_between_first_node = null + update_drive_state({ type: 'mode/select_between_enabled', message: false }) + render_menubar() + console.log('[SEARCH DEBUG] Reset select between mode') + } + } else if (ev.shiftKey) { + // Enable select between mode on shift click select_between_enabled = true select_between_first_node = instance_path update_drive_state({ type: 'mode/select_between_enabled', message: true }) + render_menubar() + console.log('[SEARCH DEBUG] Enabled select between mode with first node:', instance_path) return - } - - if (multi_select_enabled || ev.ctrlKey || ev.metaKey) { - const new_selected = new Set(selected_instance_paths) + } else if (multi_select_enabled || ev.ctrlKey || ev.metaKey) { if (new_selected.has(instance_path)) { console.log('[SEARCH DEBUG] Deselecting node:', instance_path) new_selected.delete(instance_path) @@ -1615,14 +1662,16 @@ async function graph_explorer (opts) { console.log('[SEARCH DEBUG] Selecting node:', instance_path) new_selected.add(instance_path) } - const new_selection_array = [...new_selected] - update_drive_state({ type: 'runtime/selected_instance_paths', message: new_selection_array }) - console.log('[SEARCH DEBUG] search_select_node completed, new selection:', new_selection_array) } else { - const new_selection_array = [instance_path] - update_drive_state({ type: 'runtime/selected_instance_paths', message: new_selection_array }) - console.log('[SEARCH DEBUG] search_select_node completed, new selection:', new_selection_array) + // Single selection mode + new_selected.clear() + new_selected.add(instance_path) + console.log('[SEARCH DEBUG] Single selecting node:', instance_path) } + + const new_selection_array = [...new_selected] + update_drive_state({ type: 'runtime/selected_instance_paths', message: new_selection_array }) + console.log('[SEARCH DEBUG] search_select_node completed, new selection:', new_selection_array) } function reset () { From f5655e6e5c98b691918d5029f5c15ef92fb1a580 Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 26 Sep 2025 22:22:10 +0500 Subject: [PATCH 099/130] Added contrast to last-clicked in search mode --- lib/graph_explorer.js | 15 +++++++++------ lib/theme.css | 4 ++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 351c2a4..e9a2d66 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -737,7 +737,9 @@ async function graph_explorer (opts) { if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') - if (last_clicked_node === instance_path) el.classList.add('last-clicked') + if (last_clicked_node === instance_path) { + mode === 'search' ? el.classList.add('search-last-clicked') : el.classList.add('last-clicked') + } const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 @@ -1542,7 +1544,7 @@ async function graph_explorer (opts) { console.log('[SEARCH DEBUG] Found search result nodes:', search_nodes.length) search_nodes.forEach(node => { const was_last_clicked = node.classList.contains('last-clicked') - node.classList.remove('last-clicked') + mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') if (was_last_clicked) { console.log('[SEARCH DEBUG] Removed last-clicked from:', node.dataset.instance_path) } @@ -1551,7 +1553,7 @@ async function graph_explorer (opts) { // Add last-clicked class to the target node if it exists in search results const target_node = container.querySelector(`[data-instance_path="${target_instance_path}"].search-result`) if (target_node) { - target_node.classList.add('last-clicked') + mode === 'search' ? target_node.classList.add('search-last-clicked') : target_node.classList.add('last-clicked') console.log('[SEARCH DEBUG] Added last-clicked to target node:', target_instance_path) } else { console.warn('[SEARCH DEBUG] Target node not found in search results:', { @@ -1962,13 +1964,14 @@ async function graph_explorer (opts) { function update_last_clicked_styling (new_instance_path) { // Remove last-clicked class from all elements const all_nodes = shadow.querySelectorAll('.node.last-clicked') - all_nodes.forEach(node => node.classList.remove('last-clicked')) - + all_nodes.forEach(node => { + mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') + }) // Add last-clicked class to the new element if (new_instance_path) { const new_element = shadow.querySelector(`[data-instance_path="${CSS.escape(new_instance_path)}"]`) if (new_element) { - new_element.classList.add('last-clicked') + mode === 'search' ? new_element.classList.add('search-last-clicked') : new_element.classList.add('last-clicked') } } } diff --git a/lib/theme.css b/lib/theme.css index 53add38..3b0a9a2 100644 --- a/lib/theme.css +++ b/lib/theme.css @@ -136,6 +136,10 @@ color: #b48ead; font-weight: bold; } +.node.search-last-clicked { + color: greenyellow; + border-right: 5px solid #7cb342; +} /* Base sizes for desktop (768px and above) */ .node { From 4cfca7889af7982a944f45d824bca64bb64a3278 Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 26 Sep 2025 23:05:30 +0500 Subject: [PATCH 100/130] Defined all the anonymous functions for foreach loops --- lib/graph_explorer.js | 243 +++++++++++++++++++++--------------------- 1 file changed, 121 insertions(+), 122 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index e9a2d66..ca38a9c 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -120,20 +120,22 @@ async function graph_explorer (opts) { for (const { type, paths } of batch) { if (!paths || !paths.length) continue const data = await Promise.all( - paths.map(path => { - return drive - .get(path) - .then(file => (file ? file.raw : null)) - .catch(e => { - console.error(`Error getting file from drive: ${path}`, e) - return null - }) - }) + paths.map(path => batch_get(path)) ) // Call the appropriate handler based on `type`. const func = on[type] func ? func({ data, paths }) : fail(data, type) } + + function batch_get (path) { + return drive + .get(path) + .then(file => (file ? file.raw : null)) + .catch(e => { + console.error(`Error getting file from drive: ${path}`, e) + return null + }) + } } function fail (data, type) { @@ -194,19 +196,7 @@ async function graph_explorer (opts) { let needs_render = false const render_nodes_needed = new Set() - paths.forEach((path, i) => { - if (data[i] === null) return - const value = parse_json_data(data[i], path) - if (value === null) return - - // Extract filename from path and use handler if available - const filename = path.split('/').pop() - const handler = on_runtime_paths[filename] - if (handler) { - const result = handler({ value, render_nodes_needed }) - if (result?.needs_render) needs_render = true - } - }) + paths.forEach((path, i) => runtime_handler(path, data[i])) if (needs_render) { if (mode === 'search' && search_query) { @@ -219,6 +209,20 @@ async function graph_explorer (opts) { render_nodes_needed.forEach(re_render_node) } + function runtime_handler (path, data) { + if (data === null) return + const value = parse_json_data(data, path) + if (value === null) return + + // Extract filename from path and use handler if available + const filename = path.split('/').pop() + const handler = on_runtime_paths[filename] + if (handler) { + const result = handler({ value, render_nodes_needed }) + if (result?.needs_render) needs_render = true + } + } + function handle_node_height ({ value }) { node_height = value } @@ -299,21 +303,7 @@ async function graph_explorer (opts) { } let new_current_mode, new_previous_mode, new_search_query, new_multi_select_enabled, new_select_between_enabled - paths.forEach((path, i) => { - const value = parse_json_data(data[i], path) - if (value === null) return - - const filename = path.split('/').pop() - const handler = on_mode_paths[filename] - if (handler) { - const result = handler({ value }) - if (result?.current_mode !== undefined) new_current_mode = result.current_mode - if (result?.previous_mode !== undefined) new_previous_mode = result.previous_mode - if (result?.search_query !== undefined) new_search_query = result.search_query - if (result?.multi_select_enabled !== undefined) new_multi_select_enabled = result.multi_select_enabled - if (result?.select_between_enabled !== undefined) new_select_between_enabled = result.select_between_enabled - } - }) + paths.forEach((path, i) => mode_handler(path, data[i])) if (typeof new_search_query === 'string') search_query = new_search_query if (new_previous_mode) previous_mode = new_previous_mode @@ -347,6 +337,21 @@ async function graph_explorer (opts) { handle_mode_change() if (mode === 'search' && search_query) perform_search(search_query) + function mode_handler (path, data) { + const value = parse_json_data(data, path) + if (value === null) return + + const filename = path.split('/').pop() + const handler = on_mode_paths[filename] + if (handler) { + const result = handler({ value }) + if (result?.current_mode !== undefined) new_current_mode = result.current_mode + if (result?.previous_mode !== undefined) new_previous_mode = result.previous_mode + if (result?.search_query !== undefined) new_search_query = result.search_query + if (result?.multi_select_enabled !== undefined) new_multi_select_enabled = result.multi_select_enabled + if (result?.select_between_enabled !== undefined) new_select_between_enabled = result.select_between_enabled + } + } function handle_current_mode ({ value }) { return { current_mode: value } } @@ -373,8 +378,10 @@ async function graph_explorer (opts) { 'hubs.json': handle_hubs_flag } - paths.forEach((path, i) => { - const value = parse_json_data(data[i], path) + paths.forEach((path, i) => flags_handler(path, data[i])) + + function flags_handler (path, data) { + const value = parse_json_data(data, path) if (value === null) return const filename = path.split('/').pop() @@ -390,7 +397,7 @@ async function graph_explorer (opts) { } } } - }) + } function handle_hubs_flag (value) { if (typeof value === 'string' && ['default', 'true', 'false'].includes(value)) { @@ -809,13 +816,7 @@ async function graph_explorer (opts) { // Special handling for first duplicate entry - it should have normal select behavior but also show jump button const name_el = el.querySelector('.name') if (has_duplicate_entries && is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { - name_el.onclick = ev => { - select_node(ev, instance_path) - // Also add jump button functionality for first occurrence - setTimeout(() => { - add_jump_button_to_matching_entry(el, base_path, instance_path) - }, 10) - } + name_el.onclick = ev => jump_and_select_matching_entry(ev, instance_path) } else { name_el.onclick = ev => mode === 'search' ? handle_search_name_click(ev, instance_path) : select_node(ev, instance_path) } @@ -824,6 +825,13 @@ async function graph_explorer (opts) { if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) return el + function jump_and_select_matching_entry (ev, instance_path) { + select_node(ev, instance_path) + // Also add jump button functionality for first occurrence + setTimeout(() => { + add_jump_button_to_matching_entry(el, base_path, instance_path) + }, 10) + } function jump_out_to_next_duplicate () { // Manually update last clicked last_clicked_node = instance_path @@ -1329,9 +1337,7 @@ async function graph_explorer (opts) { } // expand foreach selected path - selected_paths.forEach(path => { - expand_entry_path_in_default(path) - }) + selected_paths.forEach(path => expand_entry_path_in_default(path)) console.log('[SEARCH DEBUG] All selected entries expanded in default mode') } @@ -1393,17 +1399,7 @@ async function graph_explorer (opts) { // Update view order tracking for the toggled subs const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => { - if (was_expanded) { - // Collapsing so - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - } else { - // Expanding so - add_instances_recursively(sub_path, instance_path, instance_states, all_entries) - } - }) - } + if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => add_or_remove_subs_instance_recursively(sub_path, instance_path, instance_states, all_entries)) last_clicked_node = instance_path update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) @@ -1412,6 +1408,16 @@ async function graph_explorer (opts) { // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + + function add_or_remove_subs_instance_recursively (sub_path, instance_path, instance_states, all_entries) { + if (was_expanded) { + // Collapsing so + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + } else { + // Expanding so + add_instances_recursively(sub_path, instance_path, instance_states, all_entries) + } + } } function toggle_hubs (instance_path) { @@ -1423,17 +1429,7 @@ async function graph_explorer (opts) { // Update view order tracking for the toggled hubs const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => { - if (was_expanded) { - // Collapsing so - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - } else { - // Expanding so - add_instances_recursively(hub_path, instance_path, instance_states, all_entries) - } - }) - } + if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => add_or_remove_hubs_instance_recursively(hub_path, instance_path, instance_states, all_entries)) last_clicked_node = instance_path drive_updated_by_scroll = true // Prevent onbatch interference with hub spacer @@ -1442,6 +1438,16 @@ async function graph_explorer (opts) { build_and_render_view(instance_path, true) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + + function add_or_remove_hubs_instance_recursively (hub_path, instance_path, instance_states, all_entries) { + if (was_expanded) { + // Collapsing so + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + } else { + // Expanding so + add_instances_recursively(hub_path, instance_path, instance_states, all_entries) + } + } } function toggle_search_subs (instance_path) { @@ -1542,13 +1548,7 @@ async function graph_explorer (opts) { // Remove `last-clicked` class from all search result nodes const search_nodes = container.querySelectorAll('.node.search-result') console.log('[SEARCH DEBUG] Found search result nodes:', search_nodes.length) - search_nodes.forEach(node => { - const was_last_clicked = node.classList.contains('last-clicked') - mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') - if (was_last_clicked) { - console.log('[SEARCH DEBUG] Removed last-clicked from:', node.dataset.instance_path) - } - }) + search_nodes.forEach(node => remove_last_clicked_styling(node)) // Add last-clicked class to the target node if it exists in search results const target_node = container.querySelector(`[data-instance_path="${target_instance_path}"].search-result`) @@ -1561,6 +1561,14 @@ async function graph_explorer (opts) { available_search_nodes: Array.from(search_nodes).map(n => n.dataset.instance_path) }) } + + function remove_last_clicked_styling (node) { + const was_last_clicked = node.classList.contains('last-clicked') + mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') + if (was_last_clicked) { + console.log('[SEARCH DEBUG] Removed last-clicked from:', node.dataset.instance_path) + } + } } function handle_search_name_click (ev, instance_path) { @@ -1752,12 +1760,14 @@ async function graph_explorer (opts) { } function handle_sentinel_intersection (entries) { - entries.forEach(entry => { - if (entry.isIntersecting) { - if (entry.target === top_sentinel) fill_viewport_upwards() - else if (entry.target === bottom_sentinel) fill_viewport_downwards() - } - }) + entries.forEach(entry => fill_downwards_or_upwards(entry)) + } + + function fill_downwards_or_upwards (entry) { + if (entry.isIntersecting) { + if (entry.target === top_sentinel) fill_viewport_upwards() + else if (entry.target === bottom_sentinel) fill_viewport_downwards() + } } function render_next_chunk () { @@ -1828,9 +1838,7 @@ async function graph_explorer (opts) { // Add initially expanded subs if any const root_entry = all_entries[root_path] if (root_entry && Array.isArray(root_entry.subs)) { - root_entry.subs.forEach(sub_path => { - add_instances_recursively(sub_path, root_instance_path, instance_states, all_entries) - }) + root_entry.subs.forEach(sub_path => add_instances_recursively(sub_path, root_instance_path, instance_states, all_entries)) } } } @@ -1876,15 +1884,11 @@ async function graph_explorer (opts) { const state = get_or_create_state(instance_states, instance_path) if (state.expanded_hubs && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => { - add_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) + entry.hubs.forEach(hub_path => add_instances_recursively(hub_path, instance_path, instance_states, all_entries)) } if (state.expanded_subs && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => { - add_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) + entry.subs.forEach(sub_path => add_instances_recursively(sub_path, instance_path, instance_states, all_entries)) } // Add the instance itself @@ -1899,17 +1903,8 @@ async function graph_explorer (opts) { const state = get_or_create_state(instance_states, instance_path) - if (state.expanded_hubs && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => { - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) - } - - if (state.expanded_subs && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => { - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) - } + if (state.expanded_hubs && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => remove_instances_recursively(hub_path, instance_path, instance_states, all_entries)) + if (state.expanded_subs && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => remove_instances_recursively(sub_path, instance_path, instance_states, all_entries)) // Remove the instance itself remove_instance_from_view_tracking(base_path, instance_path) @@ -2023,18 +2018,20 @@ async function graph_explorer (opts) { // Replace with jump button wand_el.textContent = '^' wand_el.className = 'wand navigate-to-hub clickable' - wand_el.onclick = (event) => { - event.stopPropagation() - last_clicked_node = instance_path - drive_updated_by_match = true - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + wand_el.onclick = (ev) => handle_jump_button_click(ev, instance_path) + } + return - update_last_clicked_styling(instance_path) + function handle_jump_button_click (ev, instance_path) { + ev.stopPropagation() + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) - cycle_to_next_duplicate(base_path, instance_path) - } + update_last_clicked_styling(instance_path) + + cycle_to_next_duplicate(base_path, instance_path) } - return } const indent_button_div = document.createElement('div') @@ -2043,8 +2040,16 @@ async function graph_explorer (opts) { const navigate_button = document.createElement('span') navigate_button.className = 'navigate-to-hub clickable' navigate_button.textContent = '^' - navigate_button.onclick = (event) => { - event.stopPropagation() // Prevent triggering the whole entry click again + navigate_button.onclick = (ev) => handle_navigate_button_click(ev, instance_path) + + indent_button_div.appendChild(navigate_button) + + // Remove left padding + el.classList.remove('left-indent') + el.insertBefore(indent_button_div, el.firstChild) + + function handle_navigate_button_click (ev, instance_path) { + ev.stopPropagation() // Prevent triggering the whole entry click again // Manually update last clicked node for jump button last_clicked_node = instance_path drive_updated_by_match = true @@ -2055,12 +2060,6 @@ async function graph_explorer (opts) { cycle_to_next_duplicate(base_path, instance_path) } - - indent_button_div.appendChild(navigate_button) - - // Remove left padding - el.classList.remove('left-indent') - el.insertBefore(indent_button_div, el.firstChild) } function scroll_to_and_highlight_instance (target_instance_path, source_instance_path = null) { From 6d482d6af03f8efc69606e777ba858a3a6440744 Mon Sep 17 00:00:00 2001 From: ddroid Date: Fri, 26 Sep 2025 23:34:02 +0500 Subject: [PATCH 101/130] bundled --- bundle.js | 368 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 228 insertions(+), 140 deletions(-) diff --git a/bundle.js b/bundle.js index 5c361c5..189005a 100644 --- a/bundle.js +++ b/bundle.js @@ -124,20 +124,22 @@ async function graph_explorer (opts) { for (const { type, paths } of batch) { if (!paths || !paths.length) continue const data = await Promise.all( - paths.map(path => { - return drive - .get(path) - .then(file => (file ? file.raw : null)) - .catch(e => { - console.error(`Error getting file from drive: ${path}`, e) - return null - }) - }) + paths.map(path => batch_get(path)) ) // Call the appropriate handler based on `type`. const func = on[type] func ? func({ data, paths }) : fail(data, type) } + + function batch_get (path) { + return drive + .get(path) + .then(file => (file ? file.raw : null)) + .catch(e => { + console.error(`Error getting file from drive: ${path}`, e) + return null + }) + } } function fail (data, type) { @@ -198,19 +200,7 @@ async function graph_explorer (opts) { let needs_render = false const render_nodes_needed = new Set() - paths.forEach((path, i) => { - if (data[i] === null) return - const value = parse_json_data(data[i], path) - if (value === null) return - - // Extract filename from path and use handler if available - const filename = path.split('/').pop() - const handler = on_runtime_paths[filename] - if (handler) { - const result = handler({ value, render_nodes_needed }) - if (result?.needs_render) needs_render = true - } - }) + paths.forEach((path, i) => runtime_handler(path, data[i])) if (needs_render) { if (mode === 'search' && search_query) { @@ -223,6 +213,20 @@ async function graph_explorer (opts) { render_nodes_needed.forEach(re_render_node) } + function runtime_handler (path, data) { + if (data === null) return + const value = parse_json_data(data, path) + if (value === null) return + + // Extract filename from path and use handler if available + const filename = path.split('/').pop() + const handler = on_runtime_paths[filename] + if (handler) { + const result = handler({ value, render_nodes_needed }) + if (result?.needs_render) needs_render = true + } + } + function handle_node_height ({ value }) { node_height = value } @@ -303,21 +307,7 @@ async function graph_explorer (opts) { } let new_current_mode, new_previous_mode, new_search_query, new_multi_select_enabled, new_select_between_enabled - paths.forEach((path, i) => { - const value = parse_json_data(data[i], path) - if (value === null) return - - const filename = path.split('/').pop() - const handler = on_mode_paths[filename] - if (handler) { - const result = handler({ value }) - if (result?.current_mode !== undefined) new_current_mode = result.current_mode - if (result?.previous_mode !== undefined) new_previous_mode = result.previous_mode - if (result?.search_query !== undefined) new_search_query = result.search_query - if (result?.multi_select_enabled !== undefined) new_multi_select_enabled = result.multi_select_enabled - if (result?.select_between_enabled !== undefined) new_select_between_enabled = result.select_between_enabled - } - }) + paths.forEach((path, i) => mode_handler(path, data[i])) if (typeof new_search_query === 'string') search_query = new_search_query if (new_previous_mode) previous_mode = new_previous_mode @@ -351,6 +341,21 @@ async function graph_explorer (opts) { handle_mode_change() if (mode === 'search' && search_query) perform_search(search_query) + function mode_handler (path, data) { + const value = parse_json_data(data, path) + if (value === null) return + + const filename = path.split('/').pop() + const handler = on_mode_paths[filename] + if (handler) { + const result = handler({ value }) + if (result?.current_mode !== undefined) new_current_mode = result.current_mode + if (result?.previous_mode !== undefined) new_previous_mode = result.previous_mode + if (result?.search_query !== undefined) new_search_query = result.search_query + if (result?.multi_select_enabled !== undefined) new_multi_select_enabled = result.multi_select_enabled + if (result?.select_between_enabled !== undefined) new_select_between_enabled = result.select_between_enabled + } + } function handle_current_mode ({ value }) { return { current_mode: value } } @@ -377,8 +382,10 @@ async function graph_explorer (opts) { 'hubs.json': handle_hubs_flag } - paths.forEach((path, i) => { - const value = parse_json_data(data[i], path) + paths.forEach((path, i) => flags_handler(path, data[i])) + + function flags_handler (path, data) { + const value = parse_json_data(data, path) if (value === null) return const filename = path.split('/').pop() @@ -394,7 +401,7 @@ async function graph_explorer (opts) { } } } - }) + } function handle_hubs_flag (value) { if (typeof value === 'string' && ['default', 'true', 'false'].includes(value)) { @@ -741,7 +748,9 @@ async function graph_explorer (opts) { if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') - if (last_clicked_node === instance_path) el.classList.add('last-clicked') + if (last_clicked_node === instance_path) { + mode === 'search' ? el.classList.add('search-last-clicked') : el.classList.add('last-clicked') + } const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry.hubs) && entry.hubs.length > 0 const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 @@ -811,13 +820,7 @@ async function graph_explorer (opts) { // Special handling for first duplicate entry - it should have normal select behavior but also show jump button const name_el = el.querySelector('.name') if (has_duplicate_entries && is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { - name_el.onclick = ev => { - select_node(ev, instance_path) - // Also add jump button functionality for first occurrence - setTimeout(() => { - add_jump_button_to_matching_entry(el, base_path, instance_path) - }, 10) - } + name_el.onclick = ev => jump_and_select_matching_entry(ev, instance_path) } else { name_el.onclick = ev => mode === 'search' ? handle_search_name_click(ev, instance_path) : select_node(ev, instance_path) } @@ -826,6 +829,13 @@ async function graph_explorer (opts) { if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) return el + function jump_and_select_matching_entry (ev, instance_path) { + select_node(ev, instance_path) + // Also add jump button functionality for first occurrence + setTimeout(() => { + add_jump_button_to_matching_entry(el, base_path, instance_path) + }, 10) + } function jump_out_to_next_duplicate () { // Manually update last clicked last_clicked_node = instance_path @@ -917,11 +927,11 @@ async function graph_explorer (opts) { const multi_select_button = document.createElement('button') multi_select_button.innerHTML = `Multi Select: ${multi_select_enabled ? 'true' : 'false'}` - multi_select_button.onclick = mode === 'search' ? null : toggle_multi_select + multi_select_button.onclick = toggle_multi_select const select_between_button = document.createElement('button') select_between_button.innerHTML = `Select Between: ${select_between_enabled ? 'true' : 'false'}` - select_between_button.onclick = mode === 'search' ? null : toggle_select_between + select_between_button.onclick = toggle_select_between menubar.replaceChildren(search_button, multi_select_button, select_between_button) } @@ -957,6 +967,20 @@ async function graph_explorer (opts) { const target_mode = mode === 'search' ? previous_mode : 'search' console.log('[SEARCH DEBUG] Switching mode from', mode, 'to', target_mode) if (mode === 'search') { + // When switching from search to default mode, expand selected entries + if (selected_instance_paths.length > 0) { + console.log('[SEARCH DEBUG] Expanding selected entries in default mode:', selected_instance_paths) + expand_selected_entries_in_default(selected_instance_paths) + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + } + // Reset select-between mode when leaving search mode + if (select_between_enabled) { + select_between_enabled = false + select_between_first_node = null + update_drive_state({ type: 'mode/select_between_enabled', message: false }) + console.log('[SEARCH DEBUG] Reset select-between mode when leaving search') + } search_query = '' update_drive_state({ type: 'mode/search_query', message: '' }) } @@ -1249,7 +1273,7 @@ async function graph_explorer (opts) { } // Add the clicked entry and all its parents in the default tree - function search_expand_into_default (target_instance_path) { + function expand_entry_path_in_default (target_instance_path) { console.log('[SEARCH DEBUG] search_expand_into_default called:', { target_instance_path, current_mode: mode, @@ -1272,10 +1296,6 @@ async function graph_explorer (opts) { console.log('[SEARCH DEBUG] Parsed instance path parts:', parts) - console.log('[SEARCH DEBUG] About to call handle_search_node_click before mode transition') - handle_search_node_click(target_instance_path) - - console.log('[SEARCH DEBUG] Setting up default mode expansion states') const root_state = get_or_create_state(instance_states, '|/') root_state.expanded_subs = true @@ -1304,6 +1324,36 @@ async function graph_explorer (opts) { console.log('[SEARCH DEBUG] Expanded hubs for:', parent_instance_path) } } + } + + // expand multiple selected entry in the default tree + function expand_selected_entries_in_default (selected_paths) { + console.log('[SEARCH DEBUG] expand_selected_entries_in_default called:', { + selected_paths, + current_mode: mode, + search_query, + previous_mode + }) + + if (!Array.isArray(selected_paths) || selected_paths.length === 0) { + console.warn('[SEARCH DEBUG] No valid selected paths provided') + return + } + + // expand foreach selected path + selected_paths.forEach(path => expand_entry_path_in_default(path)) + + console.log('[SEARCH DEBUG] All selected entries expanded in default mode') + } + + // Add the clicked entry and all its parents in the default tree + function search_expand_into_default (target_instance_path) { + if (!target_instance_path) { + return + } + + handle_search_node_click(target_instance_path) + expand_entry_path_in_default(target_instance_path) console.log('[SEARCH DEBUG] Current mode before switch:', mode) console.log('[SEARCH DEBUG] Target previous_mode:', previous_mode) @@ -1313,7 +1363,7 @@ async function graph_explorer (opts) { drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) search_query = '' - update_drive_state({ type: 'mode/query', message: '' }) + update_drive_state({ type: 'mode/search_query', message: '' }) console.log('[SEARCH DEBUG] About to switch from search mode to:', previous_mode) update_drive_state({ type: 'mode/current_mode', message: previous_mode }) @@ -1353,17 +1403,7 @@ async function graph_explorer (opts) { // Update view order tracking for the toggled subs const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => { - if (was_expanded) { - // Collapsing so - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - } else { - // Expanding so - add_instances_recursively(sub_path, instance_path, instance_states, all_entries) - } - }) - } + if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => add_or_remove_subs_instance_recursively(sub_path, instance_path, instance_states, all_entries)) last_clicked_node = instance_path update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) @@ -1372,6 +1412,16 @@ async function graph_explorer (opts) { // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + + function add_or_remove_subs_instance_recursively (sub_path, instance_path, instance_states, all_entries) { + if (was_expanded) { + // Collapsing so + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + } else { + // Expanding so + add_instances_recursively(sub_path, instance_path, instance_states, all_entries) + } + } } function toggle_hubs (instance_path) { @@ -1383,17 +1433,7 @@ async function graph_explorer (opts) { // Update view order tracking for the toggled hubs const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => { - if (was_expanded) { - // Collapsing so - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - } else { - // Expanding so - add_instances_recursively(hub_path, instance_path, instance_states, all_entries) - } - }) - } + if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => add_or_remove_hubs_instance_recursively(hub_path, instance_path, instance_states, all_entries)) last_clicked_node = instance_path drive_updated_by_scroll = true // Prevent onbatch interference with hub spacer @@ -1402,6 +1442,16 @@ async function graph_explorer (opts) { build_and_render_view(instance_path, true) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + + function add_or_remove_hubs_instance_recursively (hub_path, instance_path, instance_states, all_entries) { + if (was_expanded) { + // Collapsing so + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + } else { + // Expanding so + add_instances_recursively(hub_path, instance_path, instance_states, all_entries) + } + } } function toggle_search_subs (instance_path) { @@ -1502,18 +1552,12 @@ async function graph_explorer (opts) { // Remove `last-clicked` class from all search result nodes const search_nodes = container.querySelectorAll('.node.search-result') console.log('[SEARCH DEBUG] Found search result nodes:', search_nodes.length) - search_nodes.forEach(node => { - const was_last_clicked = node.classList.contains('last-clicked') - node.classList.remove('last-clicked') - if (was_last_clicked) { - console.log('[SEARCH DEBUG] Removed last-clicked from:', node.dataset.instance_path) - } - }) + search_nodes.forEach(node => remove_last_clicked_styling(node)) // Add last-clicked class to the target node if it exists in search results const target_node = container.querySelector(`[data-instance_path="${target_instance_path}"].search-result`) if (target_node) { - target_node.classList.add('last-clicked') + mode === 'search' ? target_node.classList.add('search-last-clicked') : target_node.classList.add('last-clicked') console.log('[SEARCH DEBUG] Added last-clicked to target node:', target_instance_path) } else { console.warn('[SEARCH DEBUG] Target node not found in search results:', { @@ -1521,6 +1565,14 @@ async function graph_explorer (opts) { available_search_nodes: Array.from(search_nodes).map(n => n.dataset.instance_path) }) } + + function remove_last_clicked_styling (node) { + const was_last_clicked = node.classList.contains('last-clicked') + mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') + if (was_last_clicked) { + console.log('[SEARCH DEBUG] Removed last-clicked from:', node.dataset.instance_path) + } + } } function handle_search_name_click (ev, instance_path) { @@ -1549,6 +1601,9 @@ async function graph_explorer (opts) { search_select_node(ev, instance_path) } else if (ev.shiftKey) { search_select_node(ev, instance_path) + } else if (select_between_enabled) { + // Handle select-between mode when button is enabled + search_select_node(ev, instance_path) } else { // Regular click search_expand_into_default(instance_path) @@ -1565,18 +1620,55 @@ async function graph_explorer (opts) { metaKey: ev.metaKey, multi_select_enabled, select_between_enabled, + select_between_first_node, current_selected: selected_instance_paths }) - if (ev.shiftKey && !select_between_enabled) { + const new_selected = new Set(selected_instance_paths) + + if (select_between_enabled) { + if (!select_between_first_node) { + select_between_first_node = instance_path + console.log('[SEARCH DEBUG] Set first node for select between:', instance_path) + } else { + console.log('[SEARCH DEBUG] Completing select between range:', { + first: select_between_first_node, + second: instance_path + }) + const first_index = view.findIndex(n => n.instance_path === select_between_first_node) + const second_index = view.findIndex(n => n.instance_path === instance_path) + + if (first_index !== -1 && second_index !== -1) { + const start_index = Math.min(first_index, second_index) + const end_index = Math.max(first_index, second_index) + + // Toggle selection for all nodes in between + for (let i = start_index; i <= end_index; i++) { + const node_instance_path = view[i].instance_path + if (new_selected.has(node_instance_path)) { + new_selected.delete(node_instance_path) + } else { + new_selected.add(node_instance_path) + } + } + } + + // Reset select between mode after completing the selection + select_between_enabled = false + select_between_first_node = null + update_drive_state({ type: 'mode/select_between_enabled', message: false }) + render_menubar() + console.log('[SEARCH DEBUG] Reset select between mode') + } + } else if (ev.shiftKey) { + // Enable select between mode on shift click select_between_enabled = true select_between_first_node = instance_path update_drive_state({ type: 'mode/select_between_enabled', message: true }) + render_menubar() + console.log('[SEARCH DEBUG] Enabled select between mode with first node:', instance_path) return - } - - if (multi_select_enabled || ev.ctrlKey || ev.metaKey) { - const new_selected = new Set(selected_instance_paths) + } else if (multi_select_enabled || ev.ctrlKey || ev.metaKey) { if (new_selected.has(instance_path)) { console.log('[SEARCH DEBUG] Deselecting node:', instance_path) new_selected.delete(instance_path) @@ -1584,12 +1676,16 @@ async function graph_explorer (opts) { console.log('[SEARCH DEBUG] Selecting node:', instance_path) new_selected.add(instance_path) } - update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) } else { - update_drive_state({ type: 'runtime/selected_instance_paths', message: [instance_path] }) + // Single selection mode + new_selected.clear() + new_selected.add(instance_path) + console.log('[SEARCH DEBUG] Single selecting node:', instance_path) } - console.log('[SEARCH DEBUG] search_select_node completed, new selection:', selected_instance_paths) + const new_selection_array = [...new_selected] + update_drive_state({ type: 'runtime/selected_instance_paths', message: new_selection_array }) + console.log('[SEARCH DEBUG] search_select_node completed, new selection:', new_selection_array) } function reset () { @@ -1668,12 +1764,14 @@ async function graph_explorer (opts) { } function handle_sentinel_intersection (entries) { - entries.forEach(entry => { - if (entry.isIntersecting) { - if (entry.target === top_sentinel) fill_viewport_upwards() - else if (entry.target === bottom_sentinel) fill_viewport_downwards() - } - }) + entries.forEach(entry => fill_downwards_or_upwards(entry)) + } + + function fill_downwards_or_upwards (entry) { + if (entry.isIntersecting) { + if (entry.target === top_sentinel) fill_viewport_upwards() + else if (entry.target === bottom_sentinel) fill_viewport_downwards() + } } function render_next_chunk () { @@ -1744,9 +1842,7 @@ async function graph_explorer (opts) { // Add initially expanded subs if any const root_entry = all_entries[root_path] if (root_entry && Array.isArray(root_entry.subs)) { - root_entry.subs.forEach(sub_path => { - add_instances_recursively(sub_path, root_instance_path, instance_states, all_entries) - }) + root_entry.subs.forEach(sub_path => add_instances_recursively(sub_path, root_instance_path, instance_states, all_entries)) } } } @@ -1792,15 +1888,11 @@ async function graph_explorer (opts) { const state = get_or_create_state(instance_states, instance_path) if (state.expanded_hubs && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => { - add_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) + entry.hubs.forEach(hub_path => add_instances_recursively(hub_path, instance_path, instance_states, all_entries)) } if (state.expanded_subs && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => { - add_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) + entry.subs.forEach(sub_path => add_instances_recursively(sub_path, instance_path, instance_states, all_entries)) } // Add the instance itself @@ -1815,17 +1907,8 @@ async function graph_explorer (opts) { const state = get_or_create_state(instance_states, instance_path) - if (state.expanded_hubs && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => { - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) - } - - if (state.expanded_subs && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => { - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) - } + if (state.expanded_hubs && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => remove_instances_recursively(hub_path, instance_path, instance_states, all_entries)) + if (state.expanded_subs && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => remove_instances_recursively(sub_path, instance_path, instance_states, all_entries)) // Remove the instance itself remove_instance_from_view_tracking(base_path, instance_path) @@ -1880,13 +1963,14 @@ async function graph_explorer (opts) { function update_last_clicked_styling (new_instance_path) { // Remove last-clicked class from all elements const all_nodes = shadow.querySelectorAll('.node.last-clicked') - all_nodes.forEach(node => node.classList.remove('last-clicked')) - + all_nodes.forEach(node => { + mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') + }) // Add last-clicked class to the new element if (new_instance_path) { const new_element = shadow.querySelector(`[data-instance_path="${CSS.escape(new_instance_path)}"]`) if (new_element) { - new_element.classList.add('last-clicked') + mode === 'search' ? new_element.classList.add('search-last-clicked') : new_element.classList.add('last-clicked') } } } @@ -1938,18 +2022,20 @@ async function graph_explorer (opts) { // Replace with jump button wand_el.textContent = '^' wand_el.className = 'wand navigate-to-hub clickable' - wand_el.onclick = (event) => { - event.stopPropagation() - last_clicked_node = instance_path - drive_updated_by_match = true - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + wand_el.onclick = (ev) => handle_jump_button_click(ev, instance_path) + } + return - update_last_clicked_styling(instance_path) + function handle_jump_button_click (ev, instance_path) { + ev.stopPropagation() + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) - cycle_to_next_duplicate(base_path, instance_path) - } + update_last_clicked_styling(instance_path) + + cycle_to_next_duplicate(base_path, instance_path) } - return } const indent_button_div = document.createElement('div') @@ -1958,8 +2044,16 @@ async function graph_explorer (opts) { const navigate_button = document.createElement('span') navigate_button.className = 'navigate-to-hub clickable' navigate_button.textContent = '^' - navigate_button.onclick = (event) => { - event.stopPropagation() // Prevent triggering the whole entry click again + navigate_button.onclick = (ev) => handle_navigate_button_click(ev, instance_path) + + indent_button_div.appendChild(navigate_button) + + // Remove left padding + el.classList.remove('left-indent') + el.insertBefore(indent_button_div, el.firstChild) + + function handle_navigate_button_click (ev, instance_path) { + ev.stopPropagation() // Prevent triggering the whole entry click again // Manually update last clicked node for jump button last_clicked_node = instance_path drive_updated_by_match = true @@ -1970,12 +2064,6 @@ async function graph_explorer (opts) { cycle_to_next_duplicate(base_path, instance_path) } - - indent_button_div.appendChild(navigate_button) - - // Remove left padding - el.classList.remove('left-indent') - el.insertBefore(indent_button_div, el.firstChild) } function scroll_to_and_highlight_instance (target_instance_path, source_instance_path = null) { From 9fcdcbe92c4e18ef8e05882d04959b9ba74786a0 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 27 Sep 2025 20:25:42 +0500 Subject: [PATCH 102/130] Added flag to disable selected entries --- lib/graph_explorer.js | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index ca38a9c..732d49f 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -27,9 +27,11 @@ async function graph_explorer (opts) { let previous_mode let search_query = '' let hubs_flag = 'default' // Flag for hubs behavior: 'default' (prevent duplication), 'true' (no duplication prevention), 'false' (disable hubs) + let selection_flag = 'default' // Flag for selection behavior: 'default' (enable selection), 'false' (disable selection) let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. + let drive_updated_by_last_clicked = false // Flag to prevent `onbatch` from re-rendering on last clicked node updates. let ignore_drive_updated_by_scroll = false // Prevent scroll flag. let drive_updated_by_match = false // Flag to prevent `onbatch` from re-rendering on matching entry updates. let drive_updated_by_tracking = false // Flag to prevent `onbatch` from re-rendering on view order tracking updates. @@ -375,7 +377,8 @@ async function graph_explorer (opts) { function on_flags ({ data, paths }) { const on_flags_paths = { - 'hubs.json': handle_hubs_flag + 'hubs.json': handle_hubs_flag, + 'selection.json': handle_selection_flag } paths.forEach((path, i) => flags_handler(path, data[i])) @@ -407,6 +410,12 @@ async function graph_explorer (opts) { console.warn('hubs flag must be one of: "default", "true", "false", ignoring.', value) } } + + function handle_selection_flag (value) { + console.log("11111122222223333333333", value) + selection_flag = value + return { needs_render: true } + } } function inject_style ({ data }) { @@ -815,10 +824,21 @@ async function graph_explorer (opts) { // Special handling for first duplicate entry - it should have normal select behavior but also show jump button const name_el = el.querySelector('.name') - if (has_duplicate_entries && is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { - name_el.onclick = ev => jump_and_select_matching_entry(ev, instance_path) + if (selection_flag !== false) { + if (has_duplicate_entries && is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { + name_el.onclick = ev => jump_and_select_matching_entry(ev, instance_path) + } else { + name_el.onclick = ev => mode === 'search' ? handle_search_name_click(ev, instance_path) : select_node(ev, instance_path) + } } else { - name_el.onclick = ev => mode === 'search' ? handle_search_name_click(ev, instance_path) : select_node(ev, instance_path) + name_el.onclick = () => handle_last_clicked_node(instance_path) + } + + function handle_last_clicked_node (instance_path) { + last_clicked_node = instance_path + drive_updated_by_last_clicked = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + update_last_clicked_styling(instance_path) } } @@ -1958,7 +1978,8 @@ async function graph_explorer (opts) { function update_last_clicked_styling (new_instance_path) { // Remove last-clicked class from all elements - const all_nodes = shadow.querySelectorAll('.node.last-clicked') + const all_nodes = mode === 'search' ? shadow.querySelectorAll('.node.search-last-clicked') : shadow.querySelectorAll('.node.last-clicked') + console.log('Removing last-clicked class from all nodes', all_nodes) all_nodes.forEach(node => { mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') }) @@ -2125,6 +2146,10 @@ async function graph_explorer (opts) { drive_updated_by_tracking = false return true } + if (drive_updated_by_last_clicked) { + drive_updated_by_last_clicked = false + return true + } console.log('[SEARCH DEBUG] No feedback flags set, allowing onbatch') return false } @@ -2290,7 +2315,8 @@ function fallback_module () { 'select_between_enabled.json': { raw: 'false' } }, 'flags/': { - 'hubs.json': { raw: '"default"' } + 'hubs.json': { raw: '"default"' }, + 'selection.json': { raw: 'false' } } } } From 8e294cb098ff107fba733cbf9320ff907e288bdb Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 27 Sep 2025 20:43:01 +0500 Subject: [PATCH 103/130] Fixed toggle subs/hubs glitch when there is no query --- lib/graph_explorer.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 732d49f..2e673db 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -412,7 +412,6 @@ async function graph_explorer (opts) { } function handle_selection_flag (value) { - console.log("11111122222223333333333", value) selection_flag = value return { needs_render: true } } @@ -804,7 +803,7 @@ async function graph_explorer (opts) { } else { const icon_el = el.querySelector('.icon') if (icon_el && has_hubs && base_path !== '/') { - icon_el.onclick = mode === 'search' + icon_el.onclick = (mode === 'search' && search_query) ? () => toggle_search_hubs(instance_path) : () => toggle_hubs(instance_path) } @@ -814,7 +813,7 @@ async function graph_explorer (opts) { const indent_el = el.querySelector('.indent') const prefix_el = el.querySelector('.prefix') - const toggle_subs_handler = mode === 'search' + const toggle_subs_handler = (mode === 'search' && search_query) ? () => toggle_search_subs(instance_path) : () => toggle_subs(instance_path) @@ -2231,18 +2230,18 @@ async function graph_explorer (opts) { const el = document.createElement('div') el.className = 'node type-root' el.dataset.instance_path = instance_path - const prefix_class = has_subs || mode === 'search' ? 'prefix clickable' : 'prefix' + const prefix_class = has_subs || (mode === 'search' && search_query) ? 'prefix clickable' : 'prefix' const prefix_name = state.expanded_subs ? 'tee-down' : 'line-h' - el.innerHTML = `
🪄
/🌐` + el.innerHTML = `
🪄
/🌐` el.querySelector('.wand').onclick = reset if (has_subs) { const prefix_el = el.querySelector('.prefix') if (prefix_el) { - prefix_el.onclick = mode === 'search' ? null : () => toggle_subs(instance_path) + prefix_el.onclick = (mode === 'search' && search_query) ? null : () => toggle_subs(instance_path) } } - el.querySelector('.name').onclick = ev => mode === 'search' ? null : select_node(ev, instance_path) + el.querySelector('.name').onclick = ev => (mode === 'search' && search_query) ? null : select_node(ev, instance_path) return el } @@ -2316,7 +2315,7 @@ function fallback_module () { }, 'flags/': { 'hubs.json': { raw: '"default"' }, - 'selection.json': { raw: 'false' } + 'selection.json': { raw: 'true' } } } } From 4b4d84bb3bdddaf6e2e3d805431c749ea9f6b6eb Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 27 Sep 2025 21:59:49 +0500 Subject: [PATCH 104/130] Added flag to either collapse entries subs & hubs on parent level or recursively --- lib/graph_explorer.js | 225 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 215 insertions(+), 10 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 2e673db..b574477 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -28,6 +28,7 @@ async function graph_explorer (opts) { let search_query = '' let hubs_flag = 'default' // Flag for hubs behavior: 'default' (prevent duplication), 'true' (no duplication prevention), 'false' (disable hubs) let selection_flag = 'default' // Flag for selection behavior: 'default' (enable selection), 'false' (disable selection) + let recursive_collapse_flag = false // Flag for recursive collapse: true (recursive), false (parent level only) let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. @@ -378,7 +379,8 @@ async function graph_explorer (opts) { function on_flags ({ data, paths }) { const on_flags_paths = { 'hubs.json': handle_hubs_flag, - 'selection.json': handle_selection_flag + 'selection.json': handle_selection_flag, + 'recursive_collapse.json': handle_recursive_collapse_flag } paths.forEach((path, i) => flags_handler(path, data[i])) @@ -415,6 +417,11 @@ async function graph_explorer (opts) { selection_flag = value return { needs_render: true } } + + function handle_recursive_collapse_flag (value) { + recursive_collapse_flag = value + return { needs_render: false } + } } function inject_style ({ data }) { @@ -1418,7 +1425,19 @@ async function graph_explorer (opts) { // Update view order tracking for the toggled subs const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => add_or_remove_subs_instance_recursively(sub_path, instance_path, instance_states, all_entries)) + + if (entry && Array.isArray(entry.subs)) { + if (was_expanded && recursive_collapse_flag === true) { + // collapse all sub descendants + entry.subs.forEach(sub_path => { + collapse_subs_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } else { + // only toggle direct subs + entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, all_entries)) + } + } last_clicked_node = instance_path update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) @@ -1428,7 +1447,7 @@ async function graph_explorer (opts) { drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) - function add_or_remove_subs_instance_recursively (sub_path, instance_path, instance_states, all_entries) { + function toggle_subs_instance (sub_path, instance_path, instance_states, all_entries) { if (was_expanded) { // Collapsing so remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) @@ -1448,7 +1467,19 @@ async function graph_explorer (opts) { // Update view order tracking for the toggled hubs const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => add_or_remove_hubs_instance_recursively(hub_path, instance_path, instance_states, all_entries)) + + if (entry && Array.isArray(entry.hubs)) { + if (was_expanded && recursive_collapse_flag === true) { + // collapse all hub descendants + entry.hubs.forEach(hub_path => { + collapse_hubs_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } else { + // only toggle direct hubs + entry.hubs.forEach(hub_path => toggle_hubs_instance(hub_path, instance_path, instance_states, all_entries)) + } + } last_clicked_node = instance_path drive_updated_by_scroll = true // Prevent onbatch interference with hub spacer @@ -1458,7 +1489,7 @@ async function graph_explorer (opts) { drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) - function add_or_remove_hubs_instance_recursively (hub_path, instance_path, instance_states, all_entries) { + function toggle_hubs_instance (hub_path, instance_path, instance_states, all_entries) { if (was_expanded) { // Collapsing so remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) @@ -1474,19 +1505,32 @@ async function graph_explorer (opts) { instance_path, mode, search_query, - current_state: search_entry_states[instance_path]?.expanded_subs || false + current_state: search_entry_states[instance_path]?.expanded_subs || false, + recursive_collapse_flag }) const state = get_or_create_state(search_entry_states, instance_path) const old_expanded = state.expanded_subs state.expanded_subs = !state.expanded_subs + + if (old_expanded && recursive_collapse_flag === true) { + const base_path = instance_path.split('|').pop() + const entry = all_entries[base_path] + if (entry && Array.isArray(entry.subs)) { + entry.subs.forEach(sub_path => { + collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, all_entries) + }) + } + } + const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs ? null : true const has_matching_parents = manipulated_inside_search[instance_path] ? search_entry_states[instance_path]?.expanded_hubs : search_state_instances[instance_path]?.expanded_hubs manipulated_inside_search[instance_path] = { expanded_hubs: has_matching_parents, expanded_subs: has_matching_descendant } console.log('[SEARCH DEBUG] Toggled subs state:', { instance_path, old_expanded, - new_expanded: state.expanded_subs + new_expanded: state.expanded_subs, + recursive_state: old_expanded && recursive_collapse_flag === true }) handle_search_node_click(instance_path) @@ -1501,18 +1545,31 @@ async function graph_explorer (opts) { instance_path, mode, search_query, - current_state: search_entry_states[instance_path]?.expanded_hubs || false + current_state: search_entry_states[instance_path]?.expanded_hubs || false, + recursive_collapse_flag }) const state = get_or_create_state(search_entry_states, instance_path) const old_expanded = state.expanded_hubs state.expanded_hubs = !state.expanded_hubs + + if (old_expanded && recursive_collapse_flag === true) { + const base_path = instance_path.split('|').pop() + const entry = all_entries[base_path] + if (entry && Array.isArray(entry.hubs)) { + entry.hubs.forEach(hub_path => { + collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, all_entries) + }) + } + } + const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs manipulated_inside_search[instance_path] = { expanded_hubs: state.expanded_hubs, expanded_subs: has_matching_descendant } console.log('[SEARCH DEBUG] Toggled hubs state:', { instance_path, old_expanded, - new_expanded: state.expanded_hubs + new_expanded: state.expanded_hubs, + recursive_state: old_expanded && recursive_collapse_flag === true }) handle_search_node_click(instance_path) @@ -1929,6 +1986,153 @@ async function graph_explorer (opts) { remove_instance_from_view_tracking(base_path, instance_path) } + // Recursively hubs all subs in default mode + function collapse_subs_recursively (base_path, parent_instance_path, instance_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + hub_num = Math.max(0, hub_num - 1) // Decrement hub counter + entry.hubs.forEach(hub_path => { + collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } + } + + // Recursively hubs all hubs in default mode + function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + hub_num = Math.max(0, hub_num - 1) + entry.hubs.forEach(hub_path => { + collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } + } + + // Recursively collapse in default mode + function collapse_all_recursively (base_path, parent_instance_path, instance_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + hub_num = Math.max(0, hub_num - 1) + entry.hubs.forEach(hub_path => { + collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } + } + + // Recursively subs all hubs in search mode + function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(search_entry_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) + }) + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + entry.hubs.forEach(hub_path => { + collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) + }) + } + } + + // Recursively hubs all hubs in search mode + function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(search_entry_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + entry.hubs.forEach(hub_path => { + collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) + }) + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) + }) + } + } + + // Recursively collapse in search mode + function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(search_entry_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) + }) + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + entry.hubs.forEach(hub_path => { + collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) + }) + } + } + function get_next_duplicate_instance (base_path, current_instance_path) { const duplicates = duplicate_entries_map[base_path] if (!duplicates || duplicates.instances.length <= 1) return null @@ -2315,7 +2519,8 @@ function fallback_module () { }, 'flags/': { 'hubs.json': { raw: '"default"' }, - 'selection.json': { raw: 'true' } + 'selection.json': { raw: 'true' }, + 'recursive_collapse.json': { raw: 'true' } } } } From a808d8f5670605b14c6613c15a7b9d92304da98b Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 27 Sep 2025 23:59:11 +0500 Subject: [PATCH 105/130] Added Duplicate detection inside search mode --- lib/graph_explorer.js | 74 +++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index b574477..b891dbe 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -783,7 +783,7 @@ async function graph_explorer (opts) { // Check if this entry appears elsewhere in the view (any duplicate) let has_duplicate_entries = false let is_first_occurrence = false - if (mode !== 'search' && hubs_flag !== 'true') { // disabled in search mode and when hubs_flag is 'true' + if (hubs_flag !== 'true') { has_duplicate_entries = has_duplicates(base_path) // coloring class for duplicates @@ -805,7 +805,7 @@ async function graph_explorer (opts) { ` // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate - if (has_duplicate_entries && !is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { + if (has_duplicate_entries && !is_first_occurrence && hubs_flag !== 'true') { el.onclick = jump_out_to_next_duplicate } else { const icon_el = el.querySelector('.icon') @@ -831,7 +831,7 @@ async function graph_explorer (opts) { // Special handling for first duplicate entry - it should have normal select behavior but also show jump button const name_el = el.querySelector('.name') if (selection_flag !== false) { - if (has_duplicate_entries && is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { + if (has_duplicate_entries && is_first_occurrence && hubs_flag !== 'true') { name_el.onclick = ev => jump_and_select_matching_entry(ev, instance_path) } else { name_el.onclick = ev => mode === 'search' ? handle_search_name_click(ev, instance_path) : select_node(ev, instance_path) @@ -852,7 +852,11 @@ async function graph_explorer (opts) { return el function jump_and_select_matching_entry (ev, instance_path) { - select_node(ev, instance_path) + if (mode === 'search') { + handle_search_name_click(ev, instance_path) + } else { + select_node(ev, instance_path) + } // Also add jump button functionality for first occurrence setTimeout(() => { add_jump_button_to_matching_entry(el, base_path, instance_path) @@ -860,13 +864,14 @@ async function graph_explorer (opts) { } function jump_out_to_next_duplicate () { // Manually update last clicked - last_clicked_node = instance_path - drive_updated_by_match = true - - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) - - // Manually update DOM - update_last_clicked_styling(instance_path) + if (mode === 'search') { + handle_search_node_click(instance_path) + } else { + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + update_last_clicked_styling(instance_path) + } add_jump_button_to_matching_entry(el, base_path, instance_path) } } @@ -1078,6 +1083,7 @@ async function graph_explorer (opts) { }) const original_view_paths = original_view.map(n => n.instance_path) search_state_instances = {} + const search_tracking = {} const search_view = build_search_view_recursive({ query, base_path: '/', @@ -1090,7 +1096,9 @@ async function graph_explorer (opts) { parent_pipe_trail: [], instance_states: search_state_instances, all_entries, - original_view_paths + original_view_paths, + is_expanded_child: false, + search_tracking }) console.log('[SEARCH DEBUG] Search view built:', search_view.length) @@ -1110,7 +1118,8 @@ async function graph_explorer (opts) { instance_states, all_entries, original_view_paths, - is_expanded_child = false + is_expanded_child = false, + search_tracking = {} }) { const entry = all_entries[base_path] if (!entry) return [] @@ -1118,6 +1127,11 @@ async function graph_explorer (opts) { const instance_path = `${parent_instance_path}|${base_path}` const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) + // track instance for duplicate detection + if (!search_tracking[base_path]) search_tracking[base_path] = [] + const is_first_occurrence_in_search = !search_tracking[base_path].length + search_tracking[base_path].push(instance_path) + // Use extracted pipe logic for consistent rendering const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ depth, @@ -1150,7 +1164,8 @@ async function graph_explorer (opts) { instance_states, all_entries, original_view_paths, - is_expanded_child: true + is_expanded_child: true, + search_tracking }) }) @@ -1172,11 +1187,12 @@ async function graph_explorer (opts) { instance_states, all_entries, original_view_paths, - is_expanded_child: true + is_expanded_child: true, + search_tracking }) }) - } else if (!is_expanded_child) { - // Only search through subs if this node itself isn't an expanded child + } else if (!is_expanded_child && is_first_occurrence_in_search) { + // Only search through subs for the first occurrence of this base_path sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => build_search_view_recursive({ query, @@ -1190,7 +1206,9 @@ async function graph_explorer (opts) { parent_pipe_trail: children_pipe_trail, instance_states, all_entries, - original_view_paths + original_view_paths, + is_expanded_child: false, + search_tracking }) ) } @@ -1198,10 +1216,11 @@ async function graph_explorer (opts) { const has_matching_descendant = sub_results.length > 0 // If this is an expanded child, always include it regardless of search match + // only include if it's the first occurrence OR if a dirct match if (!is_expanded_child && !is_direct_match && !has_matching_descendant) return [] + if (!is_expanded_child && !is_first_occurrence_in_search && !is_direct_match) return [] - // Set instance states for rendering - const final_expand_subs = search_state ? search_state.expanded_subs : has_matching_descendant + const final_expand_subs = search_state ? search_state.expanded_subs : (has_matching_descendant && is_first_occurrence_in_search) const final_expand_hubs = search_state ? search_state.expanded_hubs : false instance_states[instance_path] = { expanded_subs: final_expand_subs, expanded_hubs: final_expand_hubs } @@ -1232,9 +1251,24 @@ async function graph_explorer (opts) { no_results_el.textContent = `No results for "${query}"` return container.replaceChildren(no_results_el) } + + // temporary tracking map for search results to detect duplicates + const search_tracking = {} + search_view.forEach(node => { + const { base_path, instance_path } = node + if (!search_tracking[base_path]) search_tracking[base_path] = [] + search_tracking[base_path].push(instance_path) + }) + + const original_tracking = view_order_tracking + view_order_tracking = search_tracking + collect_all_duplicate_entries() + const fragment = document.createDocumentFragment() search_view.forEach(node_data => fragment.appendChild(create_node({ ...node_data, query }))) container.replaceChildren(fragment) + + view_order_tracking = original_tracking } /****************************************************************************** From 51a0cab00c030c343f2f273704d6e259f7554165 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 27 Sep 2025 23:59:33 +0500 Subject: [PATCH 106/130] bundled --- bundle.js | 342 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 303 insertions(+), 39 deletions(-) diff --git a/bundle.js b/bundle.js index 189005a..8c1ea96 100644 --- a/bundle.js +++ b/bundle.js @@ -31,9 +31,12 @@ async function graph_explorer (opts) { let previous_mode let search_query = '' let hubs_flag = 'default' // Flag for hubs behavior: 'default' (prevent duplication), 'true' (no duplication prevention), 'false' (disable hubs) + let selection_flag = 'default' // Flag for selection behavior: 'default' (enable selection), 'false' (disable selection) + let recursive_collapse_flag = false // Flag for recursive collapse: true (recursive), false (parent level only) let drive_updated_by_scroll = false // Flag to prevent `onbatch` from re-rendering on scroll updates. let drive_updated_by_toggle = false // Flag to prevent `onbatch` from re-rendering on toggle updates. let drive_updated_by_search = false // Flag to prevent `onbatch` from re-rendering on search updates. + let drive_updated_by_last_clicked = false // Flag to prevent `onbatch` from re-rendering on last clicked node updates. let ignore_drive_updated_by_scroll = false // Prevent scroll flag. let drive_updated_by_match = false // Flag to prevent `onbatch` from re-rendering on matching entry updates. let drive_updated_by_tracking = false // Flag to prevent `onbatch` from re-rendering on view order tracking updates. @@ -379,7 +382,9 @@ async function graph_explorer (opts) { function on_flags ({ data, paths }) { const on_flags_paths = { - 'hubs.json': handle_hubs_flag + 'hubs.json': handle_hubs_flag, + 'selection.json': handle_selection_flag, + 'recursive_collapse.json': handle_recursive_collapse_flag } paths.forEach((path, i) => flags_handler(path, data[i])) @@ -411,6 +416,16 @@ async function graph_explorer (opts) { console.warn('hubs flag must be one of: "default", "true", "false", ignoring.', value) } } + + function handle_selection_flag (value) { + selection_flag = value + return { needs_render: true } + } + + function handle_recursive_collapse_flag (value) { + recursive_collapse_flag = value + return { needs_render: false } + } } function inject_style ({ data }) { @@ -772,7 +787,7 @@ async function graph_explorer (opts) { // Check if this entry appears elsewhere in the view (any duplicate) let has_duplicate_entries = false let is_first_occurrence = false - if (mode !== 'search' && hubs_flag !== 'true') { // disabled in search mode and when hubs_flag is 'true' + if (hubs_flag !== 'true') { has_duplicate_entries = has_duplicates(base_path) // coloring class for duplicates @@ -794,12 +809,12 @@ async function graph_explorer (opts) { ` // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate - if (has_duplicate_entries && !is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { + if (has_duplicate_entries && !is_first_occurrence && hubs_flag !== 'true') { el.onclick = jump_out_to_next_duplicate } else { const icon_el = el.querySelector('.icon') if (icon_el && has_hubs && base_path !== '/') { - icon_el.onclick = mode === 'search' + icon_el.onclick = (mode === 'search' && search_query) ? () => toggle_search_hubs(instance_path) : () => toggle_hubs(instance_path) } @@ -809,7 +824,7 @@ async function graph_explorer (opts) { const indent_el = el.querySelector('.indent') const prefix_el = el.querySelector('.prefix') - const toggle_subs_handler = mode === 'search' + const toggle_subs_handler = (mode === 'search' && search_query) ? () => toggle_search_subs(instance_path) : () => toggle_subs(instance_path) @@ -819,10 +834,21 @@ async function graph_explorer (opts) { // Special handling for first duplicate entry - it should have normal select behavior but also show jump button const name_el = el.querySelector('.name') - if (has_duplicate_entries && is_first_occurrence && mode !== 'search' && hubs_flag !== 'true') { - name_el.onclick = ev => jump_and_select_matching_entry(ev, instance_path) + if (selection_flag !== false) { + if (has_duplicate_entries && is_first_occurrence && hubs_flag !== 'true') { + name_el.onclick = ev => jump_and_select_matching_entry(ev, instance_path) + } else { + name_el.onclick = ev => mode === 'search' ? handle_search_name_click(ev, instance_path) : select_node(ev, instance_path) + } } else { - name_el.onclick = ev => mode === 'search' ? handle_search_name_click(ev, instance_path) : select_node(ev, instance_path) + name_el.onclick = () => handle_last_clicked_node(instance_path) + } + + function handle_last_clicked_node (instance_path) { + last_clicked_node = instance_path + drive_updated_by_last_clicked = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + update_last_clicked_styling(instance_path) } } @@ -830,7 +856,11 @@ async function graph_explorer (opts) { return el function jump_and_select_matching_entry (ev, instance_path) { - select_node(ev, instance_path) + if (mode === 'search') { + handle_search_name_click(ev, instance_path) + } else { + select_node(ev, instance_path) + } // Also add jump button functionality for first occurrence setTimeout(() => { add_jump_button_to_matching_entry(el, base_path, instance_path) @@ -838,13 +868,14 @@ async function graph_explorer (opts) { } function jump_out_to_next_duplicate () { // Manually update last clicked - last_clicked_node = instance_path - drive_updated_by_match = true - - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) - - // Manually update DOM - update_last_clicked_styling(instance_path) + if (mode === 'search') { + handle_search_node_click(instance_path) + } else { + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + update_last_clicked_styling(instance_path) + } add_jump_button_to_matching_entry(el, base_path, instance_path) } } @@ -1056,6 +1087,7 @@ async function graph_explorer (opts) { }) const original_view_paths = original_view.map(n => n.instance_path) search_state_instances = {} + const search_tracking = {} const search_view = build_search_view_recursive({ query, base_path: '/', @@ -1068,7 +1100,9 @@ async function graph_explorer (opts) { parent_pipe_trail: [], instance_states: search_state_instances, all_entries, - original_view_paths + original_view_paths, + is_expanded_child: false, + search_tracking }) console.log('[SEARCH DEBUG] Search view built:', search_view.length) @@ -1088,7 +1122,8 @@ async function graph_explorer (opts) { instance_states, all_entries, original_view_paths, - is_expanded_child = false + is_expanded_child = false, + search_tracking = {} }) { const entry = all_entries[base_path] if (!entry) return [] @@ -1096,6 +1131,11 @@ async function graph_explorer (opts) { const instance_path = `${parent_instance_path}|${base_path}` const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) + // track instance for duplicate detection + if (!search_tracking[base_path]) search_tracking[base_path] = [] + const is_first_occurrence_in_search = !search_tracking[base_path].length + search_tracking[base_path].push(instance_path) + // Use extracted pipe logic for consistent rendering const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ depth, @@ -1128,7 +1168,8 @@ async function graph_explorer (opts) { instance_states, all_entries, original_view_paths, - is_expanded_child: true + is_expanded_child: true, + search_tracking }) }) @@ -1150,11 +1191,12 @@ async function graph_explorer (opts) { instance_states, all_entries, original_view_paths, - is_expanded_child: true + is_expanded_child: true, + search_tracking }) }) - } else if (!is_expanded_child) { - // Only search through subs if this node itself isn't an expanded child + } else if (!is_expanded_child && is_first_occurrence_in_search) { + // Only search through subs for the first occurrence of this base_path sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => build_search_view_recursive({ query, @@ -1168,7 +1210,9 @@ async function graph_explorer (opts) { parent_pipe_trail: children_pipe_trail, instance_states, all_entries, - original_view_paths + original_view_paths, + is_expanded_child: false, + search_tracking }) ) } @@ -1176,10 +1220,11 @@ async function graph_explorer (opts) { const has_matching_descendant = sub_results.length > 0 // If this is an expanded child, always include it regardless of search match + // only include if it's the first occurrence OR if a dirct match if (!is_expanded_child && !is_direct_match && !has_matching_descendant) return [] + if (!is_expanded_child && !is_first_occurrence_in_search && !is_direct_match) return [] - // Set instance states for rendering - const final_expand_subs = search_state ? search_state.expanded_subs : has_matching_descendant + const final_expand_subs = search_state ? search_state.expanded_subs : (has_matching_descendant && is_first_occurrence_in_search) const final_expand_hubs = search_state ? search_state.expanded_hubs : false instance_states[instance_path] = { expanded_subs: final_expand_subs, expanded_hubs: final_expand_hubs } @@ -1210,9 +1255,24 @@ async function graph_explorer (opts) { no_results_el.textContent = `No results for "${query}"` return container.replaceChildren(no_results_el) } + + // temporary tracking map for search results to detect duplicates + const search_tracking = {} + search_view.forEach(node => { + const { base_path, instance_path } = node + if (!search_tracking[base_path]) search_tracking[base_path] = [] + search_tracking[base_path].push(instance_path) + }) + + const original_tracking = view_order_tracking + view_order_tracking = search_tracking + collect_all_duplicate_entries() + const fragment = document.createDocumentFragment() search_view.forEach(node_data => fragment.appendChild(create_node({ ...node_data, query }))) container.replaceChildren(fragment) + + view_order_tracking = original_tracking } /****************************************************************************** @@ -1403,7 +1463,19 @@ async function graph_explorer (opts) { // Update view order tracking for the toggled subs const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => add_or_remove_subs_instance_recursively(sub_path, instance_path, instance_states, all_entries)) + + if (entry && Array.isArray(entry.subs)) { + if (was_expanded && recursive_collapse_flag === true) { + // collapse all sub descendants + entry.subs.forEach(sub_path => { + collapse_subs_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } else { + // only toggle direct subs + entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, all_entries)) + } + } last_clicked_node = instance_path update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) @@ -1413,7 +1485,7 @@ async function graph_explorer (opts) { drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) - function add_or_remove_subs_instance_recursively (sub_path, instance_path, instance_states, all_entries) { + function toggle_subs_instance (sub_path, instance_path, instance_states, all_entries) { if (was_expanded) { // Collapsing so remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) @@ -1433,7 +1505,19 @@ async function graph_explorer (opts) { // Update view order tracking for the toggled hubs const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => add_or_remove_hubs_instance_recursively(hub_path, instance_path, instance_states, all_entries)) + + if (entry && Array.isArray(entry.hubs)) { + if (was_expanded && recursive_collapse_flag === true) { + // collapse all hub descendants + entry.hubs.forEach(hub_path => { + collapse_hubs_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } else { + // only toggle direct hubs + entry.hubs.forEach(hub_path => toggle_hubs_instance(hub_path, instance_path, instance_states, all_entries)) + } + } last_clicked_node = instance_path drive_updated_by_scroll = true // Prevent onbatch interference with hub spacer @@ -1443,7 +1527,7 @@ async function graph_explorer (opts) { drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) - function add_or_remove_hubs_instance_recursively (hub_path, instance_path, instance_states, all_entries) { + function toggle_hubs_instance (hub_path, instance_path, instance_states, all_entries) { if (was_expanded) { // Collapsing so remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) @@ -1459,19 +1543,32 @@ async function graph_explorer (opts) { instance_path, mode, search_query, - current_state: search_entry_states[instance_path]?.expanded_subs || false + current_state: search_entry_states[instance_path]?.expanded_subs || false, + recursive_collapse_flag }) const state = get_or_create_state(search_entry_states, instance_path) const old_expanded = state.expanded_subs state.expanded_subs = !state.expanded_subs + + if (old_expanded && recursive_collapse_flag === true) { + const base_path = instance_path.split('|').pop() + const entry = all_entries[base_path] + if (entry && Array.isArray(entry.subs)) { + entry.subs.forEach(sub_path => { + collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, all_entries) + }) + } + } + const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs ? null : true const has_matching_parents = manipulated_inside_search[instance_path] ? search_entry_states[instance_path]?.expanded_hubs : search_state_instances[instance_path]?.expanded_hubs manipulated_inside_search[instance_path] = { expanded_hubs: has_matching_parents, expanded_subs: has_matching_descendant } console.log('[SEARCH DEBUG] Toggled subs state:', { instance_path, old_expanded, - new_expanded: state.expanded_subs + new_expanded: state.expanded_subs, + recursive_state: old_expanded && recursive_collapse_flag === true }) handle_search_node_click(instance_path) @@ -1486,18 +1583,31 @@ async function graph_explorer (opts) { instance_path, mode, search_query, - current_state: search_entry_states[instance_path]?.expanded_hubs || false + current_state: search_entry_states[instance_path]?.expanded_hubs || false, + recursive_collapse_flag }) const state = get_or_create_state(search_entry_states, instance_path) const old_expanded = state.expanded_hubs state.expanded_hubs = !state.expanded_hubs + + if (old_expanded && recursive_collapse_flag === true) { + const base_path = instance_path.split('|').pop() + const entry = all_entries[base_path] + if (entry && Array.isArray(entry.hubs)) { + entry.hubs.forEach(hub_path => { + collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, all_entries) + }) + } + } + const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs manipulated_inside_search[instance_path] = { expanded_hubs: state.expanded_hubs, expanded_subs: has_matching_descendant } console.log('[SEARCH DEBUG] Toggled hubs state:', { instance_path, old_expanded, - new_expanded: state.expanded_hubs + new_expanded: state.expanded_hubs, + recursive_state: old_expanded && recursive_collapse_flag === true }) handle_search_node_click(instance_path) @@ -1914,6 +2024,153 @@ async function graph_explorer (opts) { remove_instance_from_view_tracking(base_path, instance_path) } + // Recursively hubs all subs in default mode + function collapse_subs_recursively (base_path, parent_instance_path, instance_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + hub_num = Math.max(0, hub_num - 1) // Decrement hub counter + entry.hubs.forEach(hub_path => { + collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } + } + + // Recursively hubs all hubs in default mode + function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + hub_num = Math.max(0, hub_num - 1) + entry.hubs.forEach(hub_path => { + collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } + } + + // Recursively collapse in default mode + function collapse_all_recursively (base_path, parent_instance_path, instance_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + }) + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + hub_num = Math.max(0, hub_num - 1) + entry.hubs.forEach(hub_path => { + collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + }) + } + } + + // Recursively subs all hubs in search mode + function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(search_entry_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) + }) + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + entry.hubs.forEach(hub_path => { + collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) + }) + } + } + + // Recursively hubs all hubs in search mode + function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(search_entry_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + entry.hubs.forEach(hub_path => { + collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) + }) + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) + }) + } + } + + // Recursively collapse in search mode + function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = all_entries[base_path] + if (!entry) return + + const state = get_or_create_state(search_entry_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + entry.subs.forEach(sub_path => { + collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) + }) + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + entry.hubs.forEach(hub_path => { + collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) + }) + } + } + function get_next_duplicate_instance (base_path, current_instance_path) { const duplicates = duplicate_entries_map[base_path] if (!duplicates || duplicates.instances.length <= 1) return null @@ -1962,7 +2219,8 @@ async function graph_explorer (opts) { function update_last_clicked_styling (new_instance_path) { // Remove last-clicked class from all elements - const all_nodes = shadow.querySelectorAll('.node.last-clicked') + const all_nodes = mode === 'search' ? shadow.querySelectorAll('.node.search-last-clicked') : shadow.querySelectorAll('.node.last-clicked') + console.log('Removing last-clicked class from all nodes', all_nodes) all_nodes.forEach(node => { mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') }) @@ -2129,6 +2387,10 @@ async function graph_explorer (opts) { drive_updated_by_tracking = false return true } + if (drive_updated_by_last_clicked) { + drive_updated_by_last_clicked = false + return true + } console.log('[SEARCH DEBUG] No feedback flags set, allowing onbatch') return false } @@ -2210,18 +2472,18 @@ async function graph_explorer (opts) { const el = document.createElement('div') el.className = 'node type-root' el.dataset.instance_path = instance_path - const prefix_class = has_subs || mode === 'search' ? 'prefix clickable' : 'prefix' + const prefix_class = has_subs || (mode === 'search' && search_query) ? 'prefix clickable' : 'prefix' const prefix_name = state.expanded_subs ? 'tee-down' : 'line-h' - el.innerHTML = `
🪄
/🌐` + el.innerHTML = `
🪄
/🌐` el.querySelector('.wand').onclick = reset if (has_subs) { const prefix_el = el.querySelector('.prefix') if (prefix_el) { - prefix_el.onclick = mode === 'search' ? null : () => toggle_subs(instance_path) + prefix_el.onclick = (mode === 'search' && search_query) ? null : () => toggle_subs(instance_path) } } - el.querySelector('.name').onclick = ev => mode === 'search' ? null : select_node(ev, instance_path) + el.querySelector('.name').onclick = ev => (mode === 'search' && search_query) ? null : select_node(ev, instance_path) return el } @@ -2294,7 +2556,9 @@ function fallback_module () { 'select_between_enabled.json': { raw: 'false' } }, 'flags/': { - 'hubs.json': { raw: '"default"' } + 'hubs.json': { raw: '"default"' }, + 'selection.json': { raw: 'true' }, + 'recursive_collapse.json': { raw: 'true' } } } } From e205d86539309a4ee804d1b81063b9dbdb13b84c Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 28 Sep 2025 20:59:05 +0500 Subject: [PATCH 107/130] fixed the jump bug in search mode --- lib/graph_explorer.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index b891dbe..d5f435c 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -863,15 +863,10 @@ async function graph_explorer (opts) { }, 10) } function jump_out_to_next_duplicate () { - // Manually update last clicked - if (mode === 'search') { - handle_search_node_click(instance_path) - } else { - last_clicked_node = instance_path - drive_updated_by_match = true - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) - update_last_clicked_styling(instance_path) - } + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + update_last_clicked_styling(instance_path) add_jump_button_to_matching_entry(el, base_path, instance_path) } } From 23634411ab6c208a694eed058b1117ba45a31ff6 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 28 Sep 2025 21:27:21 +0500 Subject: [PATCH 108/130] Added all flags to menubar --- lib/graph_explorer.js | 46 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index d5f435c..f8771d1 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -942,20 +942,31 @@ async function graph_explorer (opts) { 5. MENUBAR AND SEARCH ******************************************************************************/ function render_menubar () { - const search_button = Object.assign(document.createElement('button'), { - textContent: 'Search', - onclick: toggle_search_mode - }) + const search_button = document.createElement('button') + search_button.textContent = 'Search' + search_button.onclick = toggle_search_mode const multi_select_button = document.createElement('button') - multi_select_button.innerHTML = `Multi Select: ${multi_select_enabled ? 'true' : 'false'}` + multi_select_button.textContent = `Multi Select: ${multi_select_enabled}` multi_select_button.onclick = toggle_multi_select const select_between_button = document.createElement('button') - select_between_button.innerHTML = `Select Between: ${select_between_enabled ? 'true' : 'false'}` + select_between_button.textContent = `Select Between: ${select_between_enabled}` select_between_button.onclick = toggle_select_between - menubar.replaceChildren(search_button, multi_select_button, select_between_button) + const hubs_button = document.createElement('button') + hubs_button.textContent = `Hubs: ${hubs_flag}` + hubs_button.onclick = toggle_hubs_flag + + const selection_button = document.createElement('button') + selection_button.textContent = `Selection: ${selection_flag}` + selection_button.onclick = toggle_selection_flag + + const recursive_collapse_button = document.createElement('button') + recursive_collapse_button.textContent = `Recursive Collapse: ${recursive_collapse_flag}` + recursive_collapse_button.onclick = toggle_recursive_collapse_flag + + menubar.replaceChildren(search_button, multi_select_button, select_between_button, hubs_button, selection_button, recursive_collapse_button) } function render_searchbar () { @@ -1035,6 +1046,27 @@ async function graph_explorer (opts) { render_menubar() // Re-render to update button text } + function toggle_hubs_flag () { + const values = ['default', 'true', 'false'] + const current_index = values.indexOf(hubs_flag) + const next_index = (current_index + 1) % values.length + hubs_flag = values[next_index] + update_drive_state({ type: 'flags/hubs', message: hubs_flag }) + render_menubar() + } + + function toggle_selection_flag () { + selection_flag = !selection_flag + update_drive_state({ type: 'flags/selection', message: selection_flag }) + render_menubar() + } + + function toggle_recursive_collapse_flag () { + recursive_collapse_flag = !recursive_collapse_flag + update_drive_state({ type: 'flags/recursive_collapse', message: recursive_collapse_flag }) + render_menubar() + } + function on_search_input (event) { search_query = event.target.value.trim() drive_updated_by_search = true From f31b28a212acab38b9460e0e0f617f885cb68a3f Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 28 Sep 2025 23:06:07 +0500 Subject: [PATCH 109/130] Added Basic Keyboard Navigation for Graph Explorer --- lib/graph_explorer.js | 174 +++++++++++++++++++++++++++++++++++++++++- web/page.js | 6 +- 2 files changed, 177 insertions(+), 3 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index f8771d1..cf0b270 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -49,6 +49,7 @@ async function graph_explorer (opts) { let last_clicked_node = null // Track the last clicked node instance path for highlighting. let root_wand_state = null // Store original root wand state when replaced with jump button const manipulated_inside_search = {} + let keybinds = {} // Store keyboard navigation bindings const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -88,11 +89,14 @@ async function graph_explorer (opts) { style: inject_style, runtime: on_runtime, mode: on_mode, - flags: on_flags + flags: on_flags, + keybinds: on_keybinds } // Start watching for state changes. This is the main trigger for all updates. await sdb.watch(onbatch) + document.onkeydown = handle_keyboard_navigation + return el /****************************************************************************** @@ -430,6 +434,19 @@ async function graph_explorer (opts) { shadow.adoptedStyleSheets = [sheet] } + function on_keybinds ({ data }) { + if (!data || data[0] == null) { + console.error('Keybinds data is missing or empty.') + return + } + const parsed_data = parse_json_data(data[0]) + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed keybinds data is not a valid object.') + return + } + keybinds = parsed_data + } + // Helper to persist component state to the drive. async function update_drive_state ({ type, message }) { try { @@ -2536,6 +2553,149 @@ async function graph_explorer (opts) { else break } } + + /****************************************************************************** + 9. KEYBOARD NAVIGATION + - Handles keyboard-based navigation for the graph explorer + - Navigate up/down around last_clicked node + ******************************************************************************/ + function handle_keyboard_navigation (event) { + // Don't handle keyboard events if focus is on input elements + if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { + return + } + + let key_combination = '' + if (event.ctrlKey) key_combination += 'Control+' + if (event.altKey) key_combination += 'Alt+' + if (event.shiftKey) key_combination += 'Shift+' + key_combination += event.key + + const action = keybinds[key_combination] || keybinds[event.key] + if (!action) return + + // Prevent default behavior for handled keys + event.preventDefault() + + // Execute the appropriate action + switch (action) { + case 'navigate_up': + navigate_to_adjacent_node(-1) + break + case 'navigate_down': + navigate_to_adjacent_node(1) + break + case 'toggle_subs': + toggle_subs_for_current_node() + break + case 'toggle_hubs': + toggle_hubs_for_current_node() + break + case 'multiselect': + multiselect_current_node() + break + case 'select_between': + select_between_current_node() + break + } + } + + function navigate_to_adjacent_node (direction) { + if (view.length === 0) return + if (!last_clicked_node) last_clicked_node = view[0].instance_path + const current_index = view.findIndex(node => node.instance_path === last_clicked_node) + if (current_index === -1) return + + const new_index = current_index + direction + if (new_index < 0 || new_index >= view.length) return + + const new_node = view[new_index] + last_clicked_node = new_node.instance_path + drive_updated_by_last_clicked = true + update_drive_state({ type: 'runtime/last_clicked_node', message: last_clicked_node }) + + // Update visual styling + if (mode === 'search' && search_query) { + update_search_last_clicked_styling(last_clicked_node) + } else { + update_last_clicked_styling(last_clicked_node) + } + + scroll_to_node(new_node.instance_path) + } + + function toggle_subs_for_current_node () { + if (!last_clicked_node) return + + if (mode === 'search' && search_query) { + toggle_search_subs(last_clicked_node) + } else { + toggle_subs(last_clicked_node) + } + } + + function toggle_hubs_for_current_node () { + if (!last_clicked_node) return + + if (mode === 'search' && search_query) { + toggle_search_hubs(last_clicked_node) + } else { + toggle_hubs(last_clicked_node) + } + } + + function multiselect_current_node () { + if (!last_clicked_node || selection_flag === false) return + + // IMPORTANT FIX!!!!! : synthetic event object for compatibility with existing functions + const synthetic_event = { ctrlKey: true, metaKey: false, shiftKey: false } + + if (mode === 'search' && search_query) { + search_select_node(synthetic_event, last_clicked_node) + } else { + select_node(synthetic_event, last_clicked_node) + } + } + + function select_between_current_node () { + if (!last_clicked_node || selection_flag === false) return + + if (!select_between_enabled) { + // Enable select between mode and set first node + select_between_enabled = true + select_between_first_node = last_clicked_node + update_drive_state({ type: 'mode/select_between_enabled', message: true }) + render_menubar() + } else { + // Complete the select between operation + const synthetic_event = { ctrlKey: false, metaKey: false, shiftKey: true } + + if (mode === 'search' && search_query) { + search_select_node(synthetic_event, last_clicked_node) + } else { + select_node(synthetic_event, last_clicked_node) + } + } + } + + function scroll_to_node (instance_path) { + const node_index = view.findIndex(node => node.instance_path === instance_path) + if (node_index === -1 || !node_height) return + + const target_scroll_top = node_index * node_height + const container_height = container.clientHeight + const current_scroll_top = container.scrollTop + + // Only scroll if the node is not fully visible + if (target_scroll_top < current_scroll_top || target_scroll_top + node_height > current_scroll_top + container_height) { + const centered_scroll_top = target_scroll_top - (container_height / 2) + (node_height / 2) + container.scrollTop = Math.max(0, centered_scroll_top) + + vertical_scroll_value = container.scrollTop + drive_updated_by_scroll = true + update_drive_state({ type: 'runtime/vertical_scroll_value', message: vertical_scroll_value }) + } + } } /****************************************************************************** @@ -2582,6 +2742,18 @@ function fallback_module () { 'hubs.json': { raw: '"default"' }, 'selection.json': { raw: 'true' }, 'recursive_collapse.json': { raw: 'true' } + }, + 'keybinds/': { + 'navigation.json': { + raw: JSON.stringify({ + ArrowUp: 'navigate_up', + ArrowDown: 'navigate_down', + 'Control+ArrowDown': 'toggle_subs', + 'Control+ArrowUp': 'toggle_hubs', + 'Alt+s': 'multiselect', + 'Alt+b': 'select_between' + }) + } } } } diff --git a/web/page.js b/web/page.js index 215e283..4460b6d 100644 --- a/web/page.js +++ b/web/page.js @@ -82,7 +82,8 @@ function fallback_module () { entries: 'entries', runtime: 'runtime', mode: 'mode', - flags: 'flags' + flags: 'flags', + keybinds: 'keybinds' } } }, @@ -92,7 +93,8 @@ function fallback_module () { 'entries/': {}, 'runtime/': {}, 'mode/': {}, - 'flags/': {} + 'flags/': {}, + 'keybinds/': {} } } } From 32b4f62b9a8d03483ff2a1496e3025fd5ec37845 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sun, 28 Sep 2025 23:20:36 +0500 Subject: [PATCH 110/130] Bundled --- bundle.js | 239 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 220 insertions(+), 19 deletions(-) diff --git a/bundle.js b/bundle.js index 8c1ea96..a849904 100644 --- a/bundle.js +++ b/bundle.js @@ -53,6 +53,7 @@ async function graph_explorer (opts) { let last_clicked_node = null // Track the last clicked node instance path for highlighting. let root_wand_state = null // Store original root wand state when replaced with jump button const manipulated_inside_search = {} + let keybinds = {} // Store keyboard navigation bindings const el = document.createElement('div') el.className = 'graph-explorer-wrapper' @@ -92,11 +93,14 @@ async function graph_explorer (opts) { style: inject_style, runtime: on_runtime, mode: on_mode, - flags: on_flags + flags: on_flags, + keybinds: on_keybinds } // Start watching for state changes. This is the main trigger for all updates. await sdb.watch(onbatch) + document.onkeydown = handle_keyboard_navigation + return el /****************************************************************************** @@ -434,6 +438,19 @@ async function graph_explorer (opts) { shadow.adoptedStyleSheets = [sheet] } + function on_keybinds ({ data }) { + if (!data || data[0] == null) { + console.error('Keybinds data is missing or empty.') + return + } + const parsed_data = parse_json_data(data[0]) + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed keybinds data is not a valid object.') + return + } + keybinds = parsed_data + } + // Helper to persist component state to the drive. async function update_drive_state ({ type, message }) { try { @@ -867,15 +884,10 @@ async function graph_explorer (opts) { }, 10) } function jump_out_to_next_duplicate () { - // Manually update last clicked - if (mode === 'search') { - handle_search_node_click(instance_path) - } else { - last_clicked_node = instance_path - drive_updated_by_match = true - update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) - update_last_clicked_styling(instance_path) - } + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + update_last_clicked_styling(instance_path) add_jump_button_to_matching_entry(el, base_path, instance_path) } } @@ -951,20 +963,31 @@ async function graph_explorer (opts) { 5. MENUBAR AND SEARCH ******************************************************************************/ function render_menubar () { - const search_button = Object.assign(document.createElement('button'), { - textContent: 'Search', - onclick: toggle_search_mode - }) + const search_button = document.createElement('button') + search_button.textContent = 'Search' + search_button.onclick = toggle_search_mode const multi_select_button = document.createElement('button') - multi_select_button.innerHTML = `Multi Select: ${multi_select_enabled ? 'true' : 'false'}` + multi_select_button.textContent = `Multi Select: ${multi_select_enabled}` multi_select_button.onclick = toggle_multi_select const select_between_button = document.createElement('button') - select_between_button.innerHTML = `Select Between: ${select_between_enabled ? 'true' : 'false'}` + select_between_button.textContent = `Select Between: ${select_between_enabled}` select_between_button.onclick = toggle_select_between - menubar.replaceChildren(search_button, multi_select_button, select_between_button) + const hubs_button = document.createElement('button') + hubs_button.textContent = `Hubs: ${hubs_flag}` + hubs_button.onclick = toggle_hubs_flag + + const selection_button = document.createElement('button') + selection_button.textContent = `Selection: ${selection_flag}` + selection_button.onclick = toggle_selection_flag + + const recursive_collapse_button = document.createElement('button') + recursive_collapse_button.textContent = `Recursive Collapse: ${recursive_collapse_flag}` + recursive_collapse_button.onclick = toggle_recursive_collapse_flag + + menubar.replaceChildren(search_button, multi_select_button, select_between_button, hubs_button, selection_button, recursive_collapse_button) } function render_searchbar () { @@ -1044,6 +1067,27 @@ async function graph_explorer (opts) { render_menubar() // Re-render to update button text } + function toggle_hubs_flag () { + const values = ['default', 'true', 'false'] + const current_index = values.indexOf(hubs_flag) + const next_index = (current_index + 1) % values.length + hubs_flag = values[next_index] + update_drive_state({ type: 'flags/hubs', message: hubs_flag }) + render_menubar() + } + + function toggle_selection_flag () { + selection_flag = !selection_flag + update_drive_state({ type: 'flags/selection', message: selection_flag }) + render_menubar() + } + + function toggle_recursive_collapse_flag () { + recursive_collapse_flag = !recursive_collapse_flag + update_drive_state({ type: 'flags/recursive_collapse', message: recursive_collapse_flag }) + render_menubar() + } + function on_search_input (event) { search_query = event.target.value.trim() drive_updated_by_search = true @@ -2513,6 +2557,149 @@ async function graph_explorer (opts) { else break } } + + /****************************************************************************** + 9. KEYBOARD NAVIGATION + - Handles keyboard-based navigation for the graph explorer + - Navigate up/down around last_clicked node + ******************************************************************************/ + function handle_keyboard_navigation (event) { + // Don't handle keyboard events if focus is on input elements + if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { + return + } + + let key_combination = '' + if (event.ctrlKey) key_combination += 'Control+' + if (event.altKey) key_combination += 'Alt+' + if (event.shiftKey) key_combination += 'Shift+' + key_combination += event.key + + const action = keybinds[key_combination] || keybinds[event.key] + if (!action) return + + // Prevent default behavior for handled keys + event.preventDefault() + + // Execute the appropriate action + switch (action) { + case 'navigate_up': + navigate_to_adjacent_node(-1) + break + case 'navigate_down': + navigate_to_adjacent_node(1) + break + case 'toggle_subs': + toggle_subs_for_current_node() + break + case 'toggle_hubs': + toggle_hubs_for_current_node() + break + case 'multiselect': + multiselect_current_node() + break + case 'select_between': + select_between_current_node() + break + } + } + + function navigate_to_adjacent_node (direction) { + if (view.length === 0) return + if (!last_clicked_node) last_clicked_node = view[0].instance_path + const current_index = view.findIndex(node => node.instance_path === last_clicked_node) + if (current_index === -1) return + + const new_index = current_index + direction + if (new_index < 0 || new_index >= view.length) return + + const new_node = view[new_index] + last_clicked_node = new_node.instance_path + drive_updated_by_last_clicked = true + update_drive_state({ type: 'runtime/last_clicked_node', message: last_clicked_node }) + + // Update visual styling + if (mode === 'search' && search_query) { + update_search_last_clicked_styling(last_clicked_node) + } else { + update_last_clicked_styling(last_clicked_node) + } + + scroll_to_node(new_node.instance_path) + } + + function toggle_subs_for_current_node () { + if (!last_clicked_node) return + + if (mode === 'search' && search_query) { + toggle_search_subs(last_clicked_node) + } else { + toggle_subs(last_clicked_node) + } + } + + function toggle_hubs_for_current_node () { + if (!last_clicked_node) return + + if (mode === 'search' && search_query) { + toggle_search_hubs(last_clicked_node) + } else { + toggle_hubs(last_clicked_node) + } + } + + function multiselect_current_node () { + if (!last_clicked_node || selection_flag === false) return + + // IMPORTANT FIX!!!!! : synthetic event object for compatibility with existing functions + const synthetic_event = { ctrlKey: true, metaKey: false, shiftKey: false } + + if (mode === 'search' && search_query) { + search_select_node(synthetic_event, last_clicked_node) + } else { + select_node(synthetic_event, last_clicked_node) + } + } + + function select_between_current_node () { + if (!last_clicked_node || selection_flag === false) return + + if (!select_between_enabled) { + // Enable select between mode and set first node + select_between_enabled = true + select_between_first_node = last_clicked_node + update_drive_state({ type: 'mode/select_between_enabled', message: true }) + render_menubar() + } else { + // Complete the select between operation + const synthetic_event = { ctrlKey: false, metaKey: false, shiftKey: true } + + if (mode === 'search' && search_query) { + search_select_node(synthetic_event, last_clicked_node) + } else { + select_node(synthetic_event, last_clicked_node) + } + } + } + + function scroll_to_node (instance_path) { + const node_index = view.findIndex(node => node.instance_path === instance_path) + if (node_index === -1 || !node_height) return + + const target_scroll_top = node_index * node_height + const container_height = container.clientHeight + const current_scroll_top = container.scrollTop + + // Only scroll if the node is not fully visible + if (target_scroll_top < current_scroll_top || target_scroll_top + node_height > current_scroll_top + container_height) { + const centered_scroll_top = target_scroll_top - (container_height / 2) + (node_height / 2) + container.scrollTop = Math.max(0, centered_scroll_top) + + vertical_scroll_value = container.scrollTop + drive_updated_by_scroll = true + update_drive_state({ type: 'runtime/vertical_scroll_value', message: vertical_scroll_value }) + } + } } /****************************************************************************** @@ -2559,6 +2746,18 @@ function fallback_module () { 'hubs.json': { raw: '"default"' }, 'selection.json': { raw: 'true' }, 'recursive_collapse.json': { raw: 'true' } + }, + 'keybinds/': { + 'navigation.json': { + raw: JSON.stringify({ + ArrowUp: 'navigate_up', + ArrowDown: 'navigate_down', + 'Control+ArrowDown': 'toggle_subs', + 'Control+ArrowUp': 'toggle_hubs', + 'Alt+s': 'multiselect', + 'Alt+b': 'select_between' + }) + } } } } @@ -2675,7 +2874,8 @@ function fallback_module () { entries: 'entries', runtime: 'runtime', mode: 'mode', - flags: 'flags' + flags: 'flags', + keybinds: 'keybinds' } } }, @@ -2685,7 +2885,8 @@ function fallback_module () { 'entries/': {}, 'runtime/': {}, 'mode/': {}, - 'flags/': {} + 'flags/': {}, + 'keybinds/': {} } } } From 2e55887da178bb19c449106e22967337c064717e Mon Sep 17 00:00:00 2001 From: ddroid Date: Mon, 29 Sep 2025 23:55:40 +0500 Subject: [PATCH 111/130] Updated according to feedback --- lib/graph_explorer.js | 210 +++++++++++++++++++++--------------------- 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index cf0b270..c722744 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -875,9 +875,7 @@ async function graph_explorer (opts) { select_node(ev, instance_path) } // Also add jump button functionality for first occurrence - setTimeout(() => { - add_jump_button_to_matching_entry(el, base_path, instance_path) - }, 10) + setTimeout(() => add_jump_button_to_matching_entry(el, base_path, instance_path), 10) } function jump_out_to_next_duplicate () { last_clicked_node = instance_path @@ -1298,11 +1296,7 @@ async function graph_explorer (opts) { // temporary tracking map for search results to detect duplicates const search_tracking = {} - search_view.forEach(node => { - const { base_path, instance_path } = node - if (!search_tracking[base_path]) search_tracking[base_path] = [] - search_tracking[base_path].push(instance_path) - }) + search_view.forEach(node => set_search_tracking(node)) const original_tracking = view_order_tracking view_order_tracking = search_tracking @@ -1313,6 +1307,12 @@ async function graph_explorer (opts) { container.replaceChildren(fragment) view_order_tracking = original_tracking + + function set_search_tracking (node) { + const { base_path, instance_path } = node + if (!search_tracking[base_path]) search_tracking[base_path] = [] + search_tracking[base_path].push(instance_path) + } } /****************************************************************************** @@ -1505,16 +1505,8 @@ async function graph_explorer (opts) { const entry = all_entries[base_path] if (entry && Array.isArray(entry.subs)) { - if (was_expanded && recursive_collapse_flag === true) { - // collapse all sub descendants - entry.subs.forEach(sub_path => { - collapse_subs_recursively(sub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) - } else { - // only toggle direct subs - entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, all_entries)) - } + if (was_expanded && recursive_collapse_flag === true) entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) + else entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, all_entries)) } last_clicked_node = instance_path @@ -1534,6 +1526,11 @@ async function graph_explorer (opts) { add_instances_recursively(sub_path, instance_path, instance_states, all_entries) } } + + function collapse_and_remove_instance (sub_path, instance_path, instance_states, all_entries) { + collapse_subs_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + } } function toggle_hubs (instance_path) { @@ -1549,14 +1546,16 @@ async function graph_explorer (opts) { if (entry && Array.isArray(entry.hubs)) { if (was_expanded && recursive_collapse_flag === true) { // collapse all hub descendants - entry.hubs.forEach(hub_path => { - collapse_hubs_recursively(hub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) } else { // only toggle direct hubs entry.hubs.forEach(hub_path => toggle_hubs_instance(hub_path, instance_path, instance_states, all_entries)) } + + function collapse_and_remove_instance (hub_path, instance_path, instance_states, all_entries) { + collapse_hubs_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + } } last_clicked_node = instance_path @@ -1594,11 +1593,7 @@ async function graph_explorer (opts) { if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => { - collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, all_entries) - }) - } + if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, all_entries)) } const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs ? null : true @@ -1634,11 +1629,7 @@ async function graph_explorer (opts) { if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => { - collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, all_entries) - }) - } + if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, all_entries)) } const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs @@ -2074,19 +2065,17 @@ async function graph_explorer (opts) { if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) // Decrement hub counter - entry.hubs.forEach(hub_path => { - collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) + } + function collapse_and_remove_instance (base_path, instance_path, instance_states, all_entries) { + collapse_subs_recursively(base_path, instance_path, instance_states, all_entries) + remove_instances_recursively(base_path, instance_path, instance_states, all_entries) } } @@ -2101,18 +2090,16 @@ async function graph_explorer (opts) { if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => { - collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) + } + function collapse_and_remove_instance (base_path, instance_path, instance_states, all_entries) { + collapse_all_recursively(base_path, instance_path, instance_states, all_entries) + remove_instances_recursively(base_path, instance_path, instance_states, all_entries) } } @@ -2126,19 +2113,18 @@ async function graph_explorer (opts) { if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, all_entries)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => { - collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, all_entries)) + } + + function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, all_entries) { + collapse_all_recursively(base_path, instance_path, instance_states, all_entries) + remove_instances_recursively(base_path, instance_path, instance_states, all_entries) } } @@ -2152,16 +2138,12 @@ async function graph_explorer (opts) { if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => { - collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) } } @@ -2175,16 +2157,12 @@ async function graph_explorer (opts) { if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => { - collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) } } @@ -2198,16 +2176,12 @@ async function graph_explorer (opts) { if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => { - collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) } } @@ -2261,9 +2235,7 @@ async function graph_explorer (opts) { // Remove last-clicked class from all elements const all_nodes = mode === 'search' ? shadow.querySelectorAll('.node.search-last-clicked') : shadow.querySelectorAll('.node.last-clicked') console.log('Removing last-clicked class from all nodes', all_nodes) - all_nodes.forEach(node => { - mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') - }) + all_nodes.forEach(node => (mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked'))) // Add last-clicked class to the new element if (new_instance_path) { const new_element = shadow.querySelector(`[data-instance_path="${CSS.escape(new_instance_path)}"]`) @@ -2564,7 +2536,17 @@ async function graph_explorer (opts) { if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { return } + const on_bind = { + navigate_up_current_node, + navigate_down_current_node, + toggle_subs_for_current_node, + toggle_hubs_for_current_node, + multiselect_current_node, + select_between_current_node, + toggle_search_mode, + jump_to_next_duplicate + } let key_combination = '' if (event.ctrlKey) key_combination += 'Control+' if (event.altKey) key_combination += 'Alt+' @@ -2576,30 +2558,17 @@ async function graph_explorer (opts) { // Prevent default behavior for handled keys event.preventDefault() - + const base_path = last_clicked_node.split('|').pop() + const current_instance_path = last_clicked_node // Execute the appropriate action - switch (action) { - case 'navigate_up': - navigate_to_adjacent_node(-1) - break - case 'navigate_down': - navigate_to_adjacent_node(1) - break - case 'toggle_subs': - toggle_subs_for_current_node() - break - case 'toggle_hubs': - toggle_hubs_for_current_node() - break - case 'multiselect': - multiselect_current_node() - break - case 'select_between': - select_between_current_node() - break - } + on_bind[action]({ base_path, current_instance_path }) + } + function navigate_up_current_node () { + navigate_to_adjacent_node(-1) + } + function navigate_down_current_node () { + navigate_to_adjacent_node(1) } - function navigate_to_adjacent_node (direction) { if (view.length === 0) return if (!last_clicked_node) last_clicked_node = view[0].instance_path @@ -2627,6 +2596,17 @@ async function graph_explorer (opts) { function toggle_subs_for_current_node () { if (!last_clicked_node) return + const base_path = last_clicked_node.split('|').pop() + const entry = all_entries[base_path] + const has_subs = Array.isArray(entry?.subs) && entry.subs.length > 0 + if (!has_subs) return + + if (hubs_flag === 'default') { + const has_duplicate_entries = has_duplicates(base_path) + const is_first_occurrence = is_first_duplicate(base_path, last_clicked_node) + if (has_duplicate_entries && !is_first_occurrence) return + } + if (mode === 'search' && search_query) { toggle_search_subs(last_clicked_node) } else { @@ -2637,6 +2617,18 @@ async function graph_explorer (opts) { function toggle_hubs_for_current_node () { if (!last_clicked_node) return + const base_path = last_clicked_node.split('|').pop() + const entry = all_entries[base_path] + const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry?.hubs) && entry.hubs.length > 0 + if (!has_hubs || base_path === '/') return + + if (hubs_flag === 'default') { + const has_duplicate_entries = has_duplicates(base_path) + const is_first_occurrence = is_first_duplicate(base_path, last_clicked_node) + + if (has_duplicate_entries && !is_first_occurrence) return + } + if (mode === 'search' && search_query) { toggle_search_hubs(last_clicked_node) } else { @@ -2696,6 +2688,12 @@ async function graph_explorer (opts) { update_drive_state({ type: 'runtime/vertical_scroll_value', message: vertical_scroll_value }) } } + + function jump_to_next_duplicate ({ base_path, current_instance_path }) { + if (hubs_flag === 'default') { + cycle_to_next_duplicate(base_path, current_instance_path) + } + } } /****************************************************************************** @@ -2746,12 +2744,14 @@ function fallback_module () { 'keybinds/': { 'navigation.json': { raw: JSON.stringify({ - ArrowUp: 'navigate_up', - ArrowDown: 'navigate_down', - 'Control+ArrowDown': 'toggle_subs', - 'Control+ArrowUp': 'toggle_hubs', - 'Alt+s': 'multiselect', - 'Alt+b': 'select_between' + ArrowUp: 'navigate_up_current_node', + ArrowDown: 'navigate_down_current_node', + 'Control+ArrowDown': 'toggle_subs_for_current_node', + 'Control+ArrowUp': 'toggle_hubs_for_current_node', + 'Alt+s': 'multiselect_current_node', + 'Alt+b': 'select_between_current_node', + 'Control+m': 'toggle_search_mode', + 'Alt+j': 'jump_to_next_duplicate' }) } } From 5405c9a2bb24d74453ae4e6abc41573ef80aa40c Mon Sep 17 00:00:00 2001 From: ddroid Date: Mon, 29 Sep 2025 23:58:04 +0500 Subject: [PATCH 112/130] Bundled --- bundle.js | 210 +++++++++++++++++++++++++++--------------------------- 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/bundle.js b/bundle.js index a849904..a83cd79 100644 --- a/bundle.js +++ b/bundle.js @@ -879,9 +879,7 @@ async function graph_explorer (opts) { select_node(ev, instance_path) } // Also add jump button functionality for first occurrence - setTimeout(() => { - add_jump_button_to_matching_entry(el, base_path, instance_path) - }, 10) + setTimeout(() => add_jump_button_to_matching_entry(el, base_path, instance_path), 10) } function jump_out_to_next_duplicate () { last_clicked_node = instance_path @@ -1302,11 +1300,7 @@ async function graph_explorer (opts) { // temporary tracking map for search results to detect duplicates const search_tracking = {} - search_view.forEach(node => { - const { base_path, instance_path } = node - if (!search_tracking[base_path]) search_tracking[base_path] = [] - search_tracking[base_path].push(instance_path) - }) + search_view.forEach(node => set_search_tracking(node)) const original_tracking = view_order_tracking view_order_tracking = search_tracking @@ -1317,6 +1311,12 @@ async function graph_explorer (opts) { container.replaceChildren(fragment) view_order_tracking = original_tracking + + function set_search_tracking (node) { + const { base_path, instance_path } = node + if (!search_tracking[base_path]) search_tracking[base_path] = [] + search_tracking[base_path].push(instance_path) + } } /****************************************************************************** @@ -1509,16 +1509,8 @@ async function graph_explorer (opts) { const entry = all_entries[base_path] if (entry && Array.isArray(entry.subs)) { - if (was_expanded && recursive_collapse_flag === true) { - // collapse all sub descendants - entry.subs.forEach(sub_path => { - collapse_subs_recursively(sub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) - } else { - // only toggle direct subs - entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, all_entries)) - } + if (was_expanded && recursive_collapse_flag === true) entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) + else entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, all_entries)) } last_clicked_node = instance_path @@ -1538,6 +1530,11 @@ async function graph_explorer (opts) { add_instances_recursively(sub_path, instance_path, instance_states, all_entries) } } + + function collapse_and_remove_instance (sub_path, instance_path, instance_states, all_entries) { + collapse_subs_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + } } function toggle_hubs (instance_path) { @@ -1553,14 +1550,16 @@ async function graph_explorer (opts) { if (entry && Array.isArray(entry.hubs)) { if (was_expanded && recursive_collapse_flag === true) { // collapse all hub descendants - entry.hubs.forEach(hub_path => { - collapse_hubs_recursively(hub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) } else { // only toggle direct hubs entry.hubs.forEach(hub_path => toggle_hubs_instance(hub_path, instance_path, instance_states, all_entries)) } + + function collapse_and_remove_instance (hub_path, instance_path, instance_states, all_entries) { + collapse_hubs_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + } } last_clicked_node = instance_path @@ -1598,11 +1597,7 @@ async function graph_explorer (opts) { if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => { - collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, all_entries) - }) - } + if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, all_entries)) } const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs ? null : true @@ -1638,11 +1633,7 @@ async function graph_explorer (opts) { if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() const entry = all_entries[base_path] - if (entry && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => { - collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, all_entries) - }) - } + if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, all_entries)) } const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs @@ -2078,19 +2069,17 @@ async function graph_explorer (opts) { if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) // Decrement hub counter - entry.hubs.forEach(hub_path => { - collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) + } + function collapse_and_remove_instance (base_path, instance_path, instance_states, all_entries) { + collapse_subs_recursively(base_path, instance_path, instance_states, all_entries) + remove_instances_recursively(base_path, instance_path, instance_states, all_entries) } } @@ -2105,18 +2094,16 @@ async function graph_explorer (opts) { if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => { - collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) + } + function collapse_and_remove_instance (base_path, instance_path, instance_states, all_entries) { + collapse_all_recursively(base_path, instance_path, instance_states, all_entries) + remove_instances_recursively(base_path, instance_path, instance_states, all_entries) } } @@ -2130,19 +2117,18 @@ async function graph_explorer (opts) { if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_all_recursively(sub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, all_entries)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => { - collapse_all_recursively(hub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, all_entries)) + } + + function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, all_entries) { + collapse_all_recursively(base_path, instance_path, instance_states, all_entries) + remove_instances_recursively(base_path, instance_path, instance_states, all_entries) } } @@ -2156,16 +2142,12 @@ async function graph_explorer (opts) { if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => { - collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) } } @@ -2179,16 +2161,12 @@ async function graph_explorer (opts) { if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => { - collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) } } @@ -2202,16 +2180,12 @@ async function graph_explorer (opts) { if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => { - collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries) - }) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => { - collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries) - }) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) } } @@ -2265,9 +2239,7 @@ async function graph_explorer (opts) { // Remove last-clicked class from all elements const all_nodes = mode === 'search' ? shadow.querySelectorAll('.node.search-last-clicked') : shadow.querySelectorAll('.node.last-clicked') console.log('Removing last-clicked class from all nodes', all_nodes) - all_nodes.forEach(node => { - mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') - }) + all_nodes.forEach(node => (mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked'))) // Add last-clicked class to the new element if (new_instance_path) { const new_element = shadow.querySelector(`[data-instance_path="${CSS.escape(new_instance_path)}"]`) @@ -2568,7 +2540,17 @@ async function graph_explorer (opts) { if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { return } + const on_bind = { + navigate_up_current_node, + navigate_down_current_node, + toggle_subs_for_current_node, + toggle_hubs_for_current_node, + multiselect_current_node, + select_between_current_node, + toggle_search_mode, + jump_to_next_duplicate + } let key_combination = '' if (event.ctrlKey) key_combination += 'Control+' if (event.altKey) key_combination += 'Alt+' @@ -2580,30 +2562,17 @@ async function graph_explorer (opts) { // Prevent default behavior for handled keys event.preventDefault() - + const base_path = last_clicked_node.split('|').pop() + const current_instance_path = last_clicked_node // Execute the appropriate action - switch (action) { - case 'navigate_up': - navigate_to_adjacent_node(-1) - break - case 'navigate_down': - navigate_to_adjacent_node(1) - break - case 'toggle_subs': - toggle_subs_for_current_node() - break - case 'toggle_hubs': - toggle_hubs_for_current_node() - break - case 'multiselect': - multiselect_current_node() - break - case 'select_between': - select_between_current_node() - break - } + on_bind[action]({ base_path, current_instance_path }) + } + function navigate_up_current_node () { + navigate_to_adjacent_node(-1) + } + function navigate_down_current_node () { + navigate_to_adjacent_node(1) } - function navigate_to_adjacent_node (direction) { if (view.length === 0) return if (!last_clicked_node) last_clicked_node = view[0].instance_path @@ -2631,6 +2600,17 @@ async function graph_explorer (opts) { function toggle_subs_for_current_node () { if (!last_clicked_node) return + const base_path = last_clicked_node.split('|').pop() + const entry = all_entries[base_path] + const has_subs = Array.isArray(entry?.subs) && entry.subs.length > 0 + if (!has_subs) return + + if (hubs_flag === 'default') { + const has_duplicate_entries = has_duplicates(base_path) + const is_first_occurrence = is_first_duplicate(base_path, last_clicked_node) + if (has_duplicate_entries && !is_first_occurrence) return + } + if (mode === 'search' && search_query) { toggle_search_subs(last_clicked_node) } else { @@ -2641,6 +2621,18 @@ async function graph_explorer (opts) { function toggle_hubs_for_current_node () { if (!last_clicked_node) return + const base_path = last_clicked_node.split('|').pop() + const entry = all_entries[base_path] + const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry?.hubs) && entry.hubs.length > 0 + if (!has_hubs || base_path === '/') return + + if (hubs_flag === 'default') { + const has_duplicate_entries = has_duplicates(base_path) + const is_first_occurrence = is_first_duplicate(base_path, last_clicked_node) + + if (has_duplicate_entries && !is_first_occurrence) return + } + if (mode === 'search' && search_query) { toggle_search_hubs(last_clicked_node) } else { @@ -2700,6 +2692,12 @@ async function graph_explorer (opts) { update_drive_state({ type: 'runtime/vertical_scroll_value', message: vertical_scroll_value }) } } + + function jump_to_next_duplicate ({ base_path, current_instance_path }) { + if (hubs_flag === 'default') { + cycle_to_next_duplicate(base_path, current_instance_path) + } + } } /****************************************************************************** @@ -2750,12 +2748,14 @@ function fallback_module () { 'keybinds/': { 'navigation.json': { raw: JSON.stringify({ - ArrowUp: 'navigate_up', - ArrowDown: 'navigate_down', - 'Control+ArrowDown': 'toggle_subs', - 'Control+ArrowUp': 'toggle_hubs', - 'Alt+s': 'multiselect', - 'Alt+b': 'select_between' + ArrowUp: 'navigate_up_current_node', + ArrowDown: 'navigate_down_current_node', + 'Control+ArrowDown': 'toggle_subs_for_current_node', + 'Control+ArrowUp': 'toggle_hubs_for_current_node', + 'Alt+s': 'multiselect_current_node', + 'Alt+b': 'select_between_current_node', + 'Control+m': 'toggle_search_mode', + 'Alt+j': 'jump_to_next_duplicate' }) } } From 902951189bd4d368ae589eaf9ff5e3b65bf4abad Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 30 Sep 2025 17:23:50 +0500 Subject: [PATCH 113/130] Added Base Protocol and Messages --- README.md | 12 +++- lib/graph_explorer.js | 143 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4a5ad56..81ba74b 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,19 @@ Require the `graph_explorer` function and call it with a configuration object. I ```javascript const graph_explorer = require('./graph_explorer.js') -// Provide `opts` and `Protocol` as parameters -const graphElement = await graph_explorer(opts, protocol); +// Provide `opts` and optional `protocol` as parameters +const graph = await graph_explorer(opts, protocol) // Append the element to your application's body or another container -document.body.appendChild(graphElement); +document.body.appendChild(graph) ``` +### Protocol System + +The graph explorer supports bidirectional message-based communication through an optional protocol parameter. This allows parent modules to: +- Control the graph explorer programmatically (change modes, select nodes, expand/collapse, etc.) +- Receive notifications about user interactions and state changes + ## Drive The component expects to receive data through datasets in drive. It responds to two types of messages: `entries` and `style`. diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index c722744..efb41d3 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -4,7 +4,7 @@ const { get } = statedb(fallback_module) module.exports = graph_explorer -async function graph_explorer (opts) { +async function graph_explorer (opts, protocol) { /****************************************************************************** 1. COMPONENT INITIALIZATION - This sets up the initial state, variables, and the basic DOM structure. @@ -51,6 +51,12 @@ async function graph_explorer (opts) { const manipulated_inside_search = {} let keybinds = {} // Store keyboard navigation bindings + // Protocol system for message-based communication + let send = null + if (protocol) { + send = protocol(msg => onmessage(msg)) + } + const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) @@ -99,6 +105,134 @@ async function graph_explorer (opts) { return el + /****************************************************************************** + PROTOCOL MESSAGE HANDLING + - Handles incoming messages and sends outgoing messages. + - @TODO: define the messages we wanna to send inorder to receive some info. +******************************************************************************/ + function onmessage ({ type, data }) { + const on_message_types = { + set_mode: handle_set_mode, + set_search_query: handle_set_search_query, + select_nodes: handle_select_nodes, + expand_node: handle_expand_node, + collapse_node: handle_collapse_node, + toggle_node: handle_toggle_node, + get_selected: handle_get_selected, + get_confirmed: handle_get_confirmed, + clear_selection: handle_clear_selection, + set_flag: handle_set_flag, + scroll_to_node: handle_scroll_to_node + } + + const handler = on_message_types[type] + if (handler) handler(data) + else console.warn(`[graph_explorer-protocol] Unknown message type: ${type}`, data) + + function handle_set_mode (data) { + const { mode: new_mode } = data + if (new_mode && ['default', 'menubar', 'search'].includes(new_mode)) { + update_drive_state({ type: 'mode/current_mode', message: new_mode }) + send_message({ type: 'mode_changed', data: { mode: new_mode } }) + } + } + + function handle_set_search_query (data) { + const { query } = data + if (typeof query === 'string') { + search_query = query + drive_updated_by_search = true + update_drive_state({ type: 'mode/search_query', message: query }) + if (mode === 'search') perform_search(query) + send_message({ type: 'search_query_changed', data: { query } }) + } + } + + function handle_select_nodes (data) { + const { instance_paths } = data + if (Array.isArray(instance_paths)) { + update_drive_state({ type: 'runtime/selected_instance_paths', message: instance_paths }) + send_message({ type: 'selection_changed', data: { selected: instance_paths } }) + } + } + + function handle_expand_node (data) { + const { instance_path, expand_subs = true, expand_hubs = false } = data + if (instance_path && instance_states[instance_path]) { + instance_states[instance_path].expanded_subs = expand_subs + instance_states[instance_path].expanded_hubs = expand_hubs + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'node_expanded', data: { instance_path, expand_subs, expand_hubs } }) + } + } + + function handle_collapse_node (data) { + const { instance_path } = data + if (instance_path && instance_states[instance_path]) { + instance_states[instance_path].expanded_subs = false + instance_states[instance_path].expanded_hubs = false + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'node_collapsed', data: { instance_path } }) + } + } + + function handle_toggle_node (data) { + const { instance_path, toggle_type = 'subs' } = data + if (instance_path && instance_states[instance_path]) { + if (toggle_type === 'subs') { + toggle_subs(instance_path) + } else if (toggle_type === 'hubs') { + toggle_hubs(instance_path) + } + send_message({ type: 'node_toggled', data: { instance_path, toggle_type } }) + } + } + + function handle_get_selected (data) { + send_message({ type: 'selected_nodes', data: { selected: selected_instance_paths } }) + } + + function handle_get_confirmed (data) { + send_message({ type: 'confirmed_nodes', data: { confirmed: confirmed_instance_paths } }) + } + + function handle_clear_selection (data) { + update_drive_state({ type: 'runtime/selected_instance_paths', message: [] }) + update_drive_state({ type: 'runtime/confirmed_selected', message: [] }) + send_message({ type: 'selection_cleared', data: {} }) + } + + function handle_set_flag (data) { + const { flag_type, value } = data + if (flag_type === 'hubs' && ['default', 'true', 'false'].includes(value)) { + update_drive_state({ type: 'flags/hubs', message: value }) + } else if (flag_type === 'selection') { + update_drive_state({ type: 'flags/selection', message: value }) + } else if (flag_type === 'recursive_collapse') { + update_drive_state({ type: 'flags/recursive_collapse', message: value }) + } + send_message({ type: 'flag_changed', data: { flag_type, value } }) + } + + function handle_scroll_to_node (data) { + const { instance_path } = data + const node_index = view.findIndex(n => n.instance_path === instance_path) + if (node_index !== -1) { + const scroll_position = node_index * node_height + container.scrollTop = scroll_position + send_message({ type: 'scrolled_to_node', data: { instance_path, scroll_position } }) + } + } + } + + function send_message ({ type, data }) { + if (send) { + send({ type, data }) + } + } + /****************************************************************************** 2. STATE AND DATA HANDLING - These functions process incoming data from the STATE module's `sdb.watch`. @@ -1014,6 +1148,7 @@ async function graph_explorer (opts) { function toggle_search_mode () { const target_mode = mode === 'search' ? previous_mode : 'search' console.log('[SEARCH DEBUG] Switching mode from', mode, 'to', target_mode) + send_message({ type: 'mode_toggling', data: { from: mode, to: target_mode } }) if (mode === 'search') { // When switching from search to default mode, expand selected entries if (selected_instance_paths.length > 0) { @@ -1035,6 +1170,7 @@ async function graph_explorer (opts) { ignore_drive_updated_by_scroll = true update_drive_state({ type: 'mode/current_mode', message: target_mode }) search_state_instances = instance_states + send_message({ type: 'mode_changed', data: { mode: target_mode } }) } function toggle_multi_select () { @@ -1323,6 +1459,7 @@ async function graph_explorer (opts) { function select_node (ev, instance_path) { last_clicked_node = instance_path update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + send_message({ type: 'node_clicked', data: { instance_path } }) // Handle shift+click to enable select between mode temporarily if (ev.shiftKey && !select_between_enabled) { @@ -1339,8 +1476,10 @@ async function graph_explorer (opts) { } else if (ev.ctrlKey || multi_select_enabled) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) + send_message({ type: 'selection_changed', data: { selected: [...new_selected] } }) } else { update_drive_state({ type: 'runtime/selected_instance_paths', message: [instance_path] }) + send_message({ type: 'selection_changed', data: { selected: [instance_path] } }) } } @@ -1516,6 +1655,7 @@ async function graph_explorer (opts) { // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'subs_toggled', data: { instance_path, expanded: state.expanded_subs } }) function toggle_subs_instance (sub_path, instance_path, instance_states, all_entries) { if (was_expanded) { @@ -1565,6 +1705,7 @@ async function graph_explorer (opts) { build_and_render_view(instance_path, true) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'hubs_toggled', data: { instance_path, expanded: state.expanded_hubs } }) function toggle_hubs_instance (hub_path, instance_path, instance_states, all_entries) { if (was_expanded) { From f975200ec04527be6d305d0ec46ad60212f30236 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 30 Sep 2025 17:42:10 +0500 Subject: [PATCH 114/130] Added Docs for Protocol --- PROTOCOL.md | 285 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 287 insertions(+) create mode 100644 PROTOCOL.md diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..771e9f1 --- /dev/null +++ b/PROTOCOL.md @@ -0,0 +1,285 @@ +# Graph Explorer Protocol System + +The `graph_explorer` module implements a bidirectional message-based communication protocol that allows parent modules to control the graph explorer and receive notifications after the requested message was processed. + +## Usage + +When initializing the graph explorer, pass a protocol function as the second parameter: + +```javascript +const _ = {} // Store the send function to communicate with graph_explorer +const graph_explorer = require('./lib/graph_explorer.js') + +const element = await graph_explorer(opts, protocol) + +function protocol (send) { + // Store the send function to communicate with graph_explorer + _.graph_send = send + + // Return a message handler function + return onmessage + + function onmessage ({ type, data }) { + // Handle messages from graph_explorer + switch (type) { + case 'node_clicked': + console.log('Node clicked:', data.instance_path) + break + case 'selection_changed': + console.log('Selection changed:', data.selected) + break + // ... handle other message types + } + } +} +``` + +## Incoming Messages (Parent → Graph Explorer) + +These messages can be sent to the graph explorer to control its behavior: + +### `set_mode` +Change the current display mode. + +**Data:** +- `mode` (String): One of `'default'`, `'menubar'`, or `'search'` + +**Example:** +```javascript +graph_send({ type: 'set_mode', data: { mode: 'search' }}) +``` + +### `set_search_query` +Set the search query (automatically switches to search mode if not already). + +**Data:** +- `query` (String): The search query string + +**Example:** +```javascript +graph_send({ type: 'set_search_query', data: { query: 'my search' }}) +``` + +### `select_nodes` +Programmatically select specific nodes. + +**Data:** +- `instance_paths` (Array): Array of instance paths to select - More about Instance paths will be defined at the end of this file + +**Example:** +```javascript +graph_send({ type: 'select_nodes', data: { instance_paths: ['|/', '|/src'] }}) +``` + +### `expand_node` +Expand a specific node's children and/or hubs. + +**Data:** +- `instance_path` (String): The instance path of the node to expand +- `expand_subs` (Boolean, optional): Whether to expand children (default: true) +- `expand_hubs` (Boolean, optional): Whether to expand hubs (default: false) + +**Example:** +```javascript +graph_send({ type: 'expand_node', data: { instance_path: '|/', expand_subs: true, expand_hubs: true }}) +``` + +### `collapse_node` +Collapse a specific node's children and hubs. + +**Data:** +- `instance_path` (String): The instance path of the node to collapse + +**Example:** +```javascript +graph_send({ type: 'collapse_node', data: { instance_path: '|/src' }}) +``` + +### `toggle_node` +Toggle expansion state of a node. + +**Data:** +- `instance_path` (String): The instance path of the node to toggle +- `toggle_type` (String, optional): Either `'subs'` or `'hubs'` (default: `'subs'`) + +**Example:** +```javascript +graph_send({ type: 'toggle_node', data: { instance_path: '|/src', toggle_type: 'subs' }}) +``` + +### `get_selected` +Request the current selection state. + +**Data:** None (empty object) + +**Response:** Triggers a `selected_nodes` message + +**Example:** +```javascript +graph_send({ type: 'get_selected', data: {}}) +``` + +### `get_confirmed` +Request the current confirmed selection state. + +**Data:** None (empty object) + +**Response:** Triggers a `confirmed_nodes` message + +**Example:** +```javascript +graph_send({ type: 'get_confirmed', data: {}}) +``` + +### `clear_selection` +Clear all selected and confirmed nodes. + +**Data:** None (empty object) + +**Example:** +```javascript +graph_send({ type: 'clear_selection', data: {}}) +``` + +### `set_flag` +Set a configuration flag. + +**Data:** +- `flag_type` (String): One of `'hubs'`, `'selection'`, or `'recursive_collapse'` +- `value` (String|Boolean): The flag value + - For `'hubs'`: `'default'`, `'true'`, or `'false'` + - For `'selection'`: Boolean + - For `'recursive_collapse'`: Boolean + +**Example:** +```javascript +graph_send({ type: 'set_flag', data: { flag_type: 'hubs', value: 'true' }}) +``` + +### `scroll_to_node` +Scroll to a specific node in the view. + +**Data:** +- `instance_path` (String): The instance path of the node to scroll to + +**Example:** +```javascript +graph_send({ type: 'scroll_to_node', data: { instance_path: '|/src/index.js' }}) +``` + +## Outgoing Messages (Graph Explorer → Parent) + +These messages are sent by the graph explorer to notify the parent module of events: + +### `node_clicked` +Fired when a node is clicked. + +**Data:** +- `instance_path` (String): The instance path of the clicked node + +### `selection_changed` +Fired when the selection state changes. + +**Data:** +- `selected` (Array): Array of currently selected instance paths + +### `subs_toggled` +Fired when a node's children are expanded or collapsed. + +**Data:** +- `instance_path` (String): The instance path of the toggled node +- `expanded` (Boolean): Whether the children are now expanded + +### `hubs_toggled` +Fired when a node's hubs are expanded or collapsed. + +**Data:** +- `instance_path` (String): The instance path of the toggled node +- `expanded` (Boolean): Whether the hubs are now expanded + +### `mode_toggling` +Fired when the mode is about to change. + +**Data:** +- `from` (String): The current mode +- `to` (String): The target mode + +### `mode_changed` +Fired when the mode has changed. + +**Data:** +- `mode` (String): The new mode + +### `search_query_changed` +Fired when the search query changes. + +**Data:** +- `query` (String): The new search query + +### `node_expanded` +Fired in response to an `expand_node` command. + +**Data:** +- `instance_path` (String): The expanded node's instance path +- `expand_subs` (Boolean): Whether children were expanded +- `expand_hubs` (Boolean): Whether hubs were expanded + +### `node_collapsed` +Fired in response to a `collapse_node` command. + +**Data:** +- `instance_path` (String): The collapsed node's instance path + +### `node_toggled` +Fired in response to a `toggle_node` command. + +**Data:** +- `instance_path` (String): The toggled node's instance path +- `toggle_type` (String): Either `'subs'` or `'hubs'` + +### `selected_nodes` +Fired in response to a `get_selected` command. + +**Data:** +- `selected` (Array): Array of currently selected instance paths + +### `confirmed_nodes` +Fired in response to a `get_confirmed` command. + +**Data:** +- `confirmed` (Array): Array of currently confirmed instance paths + +### `selection_cleared` +Fired in response to a `clear_selection` command. + +**Data:** Empty object + +### `flag_changed` +Fired in response to a `set_flag` command. + +**Data:** +- `flag_type` (String): The flag that was changed +- `value` (String|Boolean): The new flag value + +### `scrolled_to_node` +Fired in response to a `scroll_to_node` command. + +**Data:** +- `instance_path` (String): The node that was scrolled to +- `scroll_position` (Number): The scroll position in pixels + +## Instance Paths + +Instance paths uniquely identify a node in the graph, including its position in the hierarchy. They follow the format: + +``` +|/path/to/node +``` + +For example: +- Root: `|/` +- First-level child: `|/src` +- Nested child: `|/src|/src/index.js` + +The pipe character (`|`) separates hierarchy levels, allowing the same base path to appear multiple times in different contexts (e.g., when a node is referenced as both a child and a hub). + diff --git a/README.md b/README.md index 81ba74b..afa1ced 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ The graph explorer supports bidirectional message-based communication through an - Control the graph explorer programmatically (change modes, select nodes, expand/collapse, etc.) - Receive notifications about user interactions and state changes +For complete protocol documentation, see [PROTOCOL.md](./PROTOCOL.md). + ## Drive The component expects to receive data through datasets in drive. It responds to two types of messages: `entries` and `style`. From 905c64abd817fefa2b267b2350495d9aff500eb7 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 30 Sep 2025 23:33:17 +0500 Subject: [PATCH 115/130] Replaced all_entries with DB --- lib/graph_explorer.js | 200 +++++++++++++++++++++--------------------- lib/graphdb.js | 43 +++++++++ 2 files changed, 143 insertions(+), 100 deletions(-) create mode 100644 lib/graphdb.js diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index efb41d3..09113ae 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -1,4 +1,5 @@ const STATE = require('./STATE') +const graphdb = require('./graphdb') const statedb = STATE(__filename) const { get } = statedb(fallback_module) @@ -18,7 +19,7 @@ async function graph_explorer (opts, protocol) { let horizontal_scroll_value = 0 let selected_instance_paths = [] let confirmed_instance_paths = [] - let all_entries = {} // Holds the entire graph structure from entries.json. + let db = null // Database for entries let instance_states = {} // Holds expansion state {expanded_subs, expanded_hubs} for each node instance. let search_state_instances = {} let search_entry_states = {} // Holds expansion state for search mode interactions separately @@ -286,20 +287,20 @@ async function graph_explorer (opts, protocol) { function on_entries ({ data }) { if (!data || data[0] == null) { console.error('Entries data is missing or empty.') - all_entries = {} + db = graphdb({}) return } const parsed_data = parse_json_data(data[0], 'entries.json') if (typeof parsed_data !== 'object' || !parsed_data) { console.error('Parsed entries data is not a valid object.') - all_entries = {} + db = graphdb({}) return } - all_entries = parsed_data + db = graphdb(parsed_data) // After receiving entries, ensure the root node state is initialized and trigger the first render. const root_path = '/' - if (all_entries[root_path]) { + if (db.has(root_path)) { const root_instance_path = '|/' if (!instance_states[root_instance_path]) { instance_states[root_instance_path] = { @@ -610,10 +611,11 @@ async function graph_explorer (opts, protocol) { parent_pipe_trail, parent_base_path, base_path, - all_entries + db }) { const children_pipe_trail = [...parent_pipe_trail] - const is_hub_on_top = base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' + const parent_entry = db.get(parent_base_path) + const is_hub_on_top = base_path === parent_entry?.hubs?.[0] || base_path === '/' if (depth > 0) { if (is_hub) { @@ -645,10 +647,11 @@ async function graph_explorer (opts, protocol) { parent_pipe_trail, parent_base_path, base_path, - all_entries + db }) { let last_pipe = null - const calculated_is_hub_on_top = base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' + const parent_entry = db.get(parent_base_path) + const calculated_is_hub_on_top = base_path === parent_entry?.hubs?.[0] || base_path === '/' const final_is_hub_on_top = is_hub_on_top !== undefined ? is_hub_on_top : calculated_is_hub_on_top if (depth > 0) { @@ -701,7 +704,7 @@ async function graph_explorer (opts, protocol) { }) } - if (Object.keys(all_entries).length === 0) { + if (!db || db.isEmpty()) { console.warn('No entries available to render.') return } @@ -721,7 +724,7 @@ async function graph_explorer (opts, protocol) { is_hub: false, parent_pipe_trail: [], instance_states, - all_entries + db }) // Recalculate duplicates after view is built @@ -764,7 +767,7 @@ async function graph_explorer (opts, protocol) { } } - // Traverses the hierarchical `all_entries` data and builds a flat `view` array for rendering. + // Traverses the hierarchical entries data and builds a flat `view` array for rendering. function build_view_recursive ({ base_path, parent_instance_path, @@ -775,10 +778,10 @@ async function graph_explorer (opts, protocol) { is_first_hub = false, parent_pipe_trail, instance_states, - all_entries + db }) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return [] const state = get_or_create_state(instance_states, instance_path) @@ -791,7 +794,7 @@ async function graph_explorer (opts, protocol) { parent_pipe_trail, parent_base_path, base_path, - all_entries + db }) const current_view = [] @@ -809,7 +812,7 @@ async function graph_explorer (opts, protocol) { is_first_hub: is_hub ? is_hub_on_top : false, parent_pipe_trail: children_pipe_trail, instance_states, - all_entries + db }) ) }) @@ -839,7 +842,7 @@ async function graph_explorer (opts, protocol) { is_hub: false, parent_pipe_trail: children_pipe_trail, instance_states, - all_entries + db }) ) }) @@ -867,7 +870,7 @@ async function graph_explorer (opts, protocol) { is_in_original_view, query }) { - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) { const err_el = document.createElement('div') err_el.className = 'node error' @@ -896,7 +899,7 @@ async function graph_explorer (opts, protocol) { parent_pipe_trail, parent_base_path, base_path, - all_entries + db }) const el = document.createElement('div') @@ -1257,28 +1260,25 @@ async function graph_explorer (opts, protocol) { is_hub: false, parent_pipe_trail: [], instance_states, - all_entries + db }) const original_view_paths = original_view.map(n => n.instance_path) search_state_instances = {} const search_tracking = {} - const search_view = build_search_view_recursive({ - query, + view = build_search_view_recursive({ base_path: '/', parent_instance_path: '', - parent_base_path: null, depth: 0, is_last_sub: true, is_hub: false, is_first_hub: false, parent_pipe_trail: [], instance_states: search_state_instances, - all_entries, + db, original_view_paths, is_expanded_child: false, search_tracking }) - console.log('[SEARCH DEBUG] Search view built:', search_view.length) render_search_results(search_view, query) } @@ -1294,12 +1294,12 @@ async function graph_explorer (opts, protocol) { is_first_hub = false, parent_pipe_trail, instance_states, - all_entries, + db, original_view_paths, is_expanded_child = false, search_tracking = {} }) { - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return [] const instance_path = `${parent_instance_path}|${base_path}` @@ -1319,7 +1319,7 @@ async function graph_explorer (opts, protocol) { parent_pipe_trail, parent_base_path, base_path, - all_entries + db }) // Process hubs if they should be expanded @@ -1340,7 +1340,7 @@ async function graph_explorer (opts, protocol) { is_first_hub: is_hub_on_top, parent_pipe_trail: children_pipe_trail, instance_states, - all_entries, + db, original_view_paths, is_expanded_child: true, search_tracking @@ -1363,7 +1363,7 @@ async function graph_explorer (opts, protocol) { is_first_hub: false, parent_pipe_trail: children_pipe_trail, instance_states, - all_entries, + db, original_view_paths, is_expanded_child: true, search_tracking @@ -1383,7 +1383,7 @@ async function graph_explorer (opts, protocol) { is_first_hub: false, parent_pipe_trail: children_pipe_trail, instance_states, - all_entries, + db, original_view_paths, is_expanded_child: false, search_tracking @@ -1544,7 +1544,7 @@ async function graph_explorer (opts, protocol) { const child_base = parts[i + 1] const parent_instance_path = parts.slice(0, i + 1).map(p => '|' + p).join('') const parent_state = get_or_create_state(instance_states, parent_instance_path) - const parent_entry = all_entries[parent_base] + const parent_entry = db.get(parent_base) console.log('[SEARCH DEBUG] Processing parent-child relationship:', { parent_base, @@ -1641,11 +1641,11 @@ async function graph_explorer (opts, protocol) { // Update view order tracking for the toggled subs const base_path = instance_path.split('|').pop() - const entry = all_entries[base_path] + const entry = db.get(base_path) if (entry && Array.isArray(entry.subs)) { - if (was_expanded && recursive_collapse_flag === true) entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) - else entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, all_entries)) + if (was_expanded && recursive_collapse_flag === true) entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) + else entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, db)) } last_clicked_node = instance_path @@ -1657,19 +1657,19 @@ async function graph_explorer (opts, protocol) { update_drive_state({ type: 'runtime/instance_states', message: instance_states }) send_message({ type: 'subs_toggled', data: { instance_path, expanded: state.expanded_subs } }) - function toggle_subs_instance (sub_path, instance_path, instance_states, all_entries) { + function toggle_subs_instance (sub_path, instance_path, instance_states, db) { if (was_expanded) { // Collapsing so - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, db) } else { // Expanding so - add_instances_recursively(sub_path, instance_path, instance_states, all_entries) + add_instances_recursively(sub_path, instance_path, instance_states, db) } } - function collapse_and_remove_instance (sub_path, instance_path, instance_states, all_entries) { - collapse_subs_recursively(sub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + function collapse_and_remove_instance (sub_path, instance_path, instance_states, db) { + collapse_subs_recursively(sub_path, instance_path, instance_states, db) + remove_instances_recursively(sub_path, instance_path, instance_states, db) } } @@ -1681,20 +1681,20 @@ async function graph_explorer (opts, protocol) { // Update view order tracking for the toggled hubs const base_path = instance_path.split('|').pop() - const entry = all_entries[base_path] + const entry = db.get(base_path) if (entry && Array.isArray(entry.hubs)) { if (was_expanded && recursive_collapse_flag === true) { // collapse all hub descendants - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) } else { // only toggle direct hubs - entry.hubs.forEach(hub_path => toggle_hubs_instance(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => toggle_hubs_instance(hub_path, instance_path, instance_states, db)) } - function collapse_and_remove_instance (hub_path, instance_path, instance_states, all_entries) { - collapse_hubs_recursively(hub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + function collapse_and_remove_instance (hub_path, instance_path, instance_states, db) { + collapse_hubs_recursively(hub_path, instance_path, instance_states, db) + remove_instances_recursively(hub_path, instance_path, instance_states, db) } } @@ -1707,13 +1707,13 @@ async function graph_explorer (opts, protocol) { update_drive_state({ type: 'runtime/instance_states', message: instance_states }) send_message({ type: 'hubs_toggled', data: { instance_path, expanded: state.expanded_hubs } }) - function toggle_hubs_instance (hub_path, instance_path, instance_states, all_entries) { + function toggle_hubs_instance (hub_path, instance_path, instance_states, db) { if (was_expanded) { // Collapsing so - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, db) } else { // Expanding so - add_instances_recursively(hub_path, instance_path, instance_states, all_entries) + add_instances_recursively(hub_path, instance_path, instance_states, db) } } } @@ -1733,8 +1733,8 @@ async function graph_explorer (opts, protocol) { if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() - const entry = all_entries[base_path] - if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, all_entries)) + const entry = db.get(base_path) + if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, db)) } const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs ? null : true @@ -1769,8 +1769,8 @@ async function graph_explorer (opts, protocol) { if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() - const entry = all_entries[base_path] - if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, all_entries)) + const entry = db.get(base_path) + if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, db)) } const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs @@ -2119,12 +2119,12 @@ async function graph_explorer (opts, protocol) { function initialize_tracking_from_current_state () { const root_path = '/' const root_instance_path = '|/' - if (all_entries[root_path]) { + if (db.has(root_path)) { add_instance_to_view_tracking(root_path, root_instance_path) // Add initially expanded subs if any - const root_entry = all_entries[root_path] + const root_entry = db.get(root_path) if (root_entry && Array.isArray(root_entry.subs)) { - root_entry.subs.forEach(sub_path => add_instances_recursively(sub_path, root_instance_path, instance_states, all_entries)) + root_entry.subs.forEach(sub_path => add_instances_recursively(sub_path, root_instance_path, instance_states, db)) } } } @@ -2162,19 +2162,19 @@ async function graph_explorer (opts, protocol) { } // Recursively add instances to tracking when expanding - function add_instances_recursively (base_path, parent_instance_path, instance_states, all_entries) { + function add_instances_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_hubs && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => add_instances_recursively(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => add_instances_recursively(hub_path, instance_path, instance_states, db)) } if (state.expanded_subs && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => add_instances_recursively(sub_path, instance_path, instance_states, all_entries)) + entry.subs.forEach(sub_path => add_instances_recursively(sub_path, instance_path, instance_states, db)) } // Add the instance itself @@ -2182,48 +2182,48 @@ async function graph_explorer (opts, protocol) { } // Recursively remove instances from tracking when collapsing - function remove_instances_recursively (base_path, parent_instance_path, instance_states, all_entries) { + function remove_instances_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) - if (state.expanded_hubs && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => remove_instances_recursively(hub_path, instance_path, instance_states, all_entries)) - if (state.expanded_subs && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => remove_instances_recursively(sub_path, instance_path, instance_states, all_entries)) + if (state.expanded_hubs && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => remove_instances_recursively(hub_path, instance_path, instance_states, db)) + if (state.expanded_subs && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => remove_instances_recursively(sub_path, instance_path, instance_states, db)) // Remove the instance itself remove_instance_from_view_tracking(base_path, instance_path) } // Recursively hubs all subs in default mode - function collapse_subs_recursively (base_path, parent_instance_path, instance_states, all_entries) { + function collapse_subs_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) + entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) // Decrement hub counter - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) } - function collapse_and_remove_instance (base_path, instance_path, instance_states, all_entries) { - collapse_subs_recursively(base_path, instance_path, instance_states, all_entries) - remove_instances_recursively(base_path, instance_path, instance_states, all_entries) + function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { + collapse_subs_recursively(base_path, instance_path, instance_states, db) + remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively hubs all hubs in default mode - function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, all_entries) { + function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) @@ -2231,98 +2231,98 @@ async function graph_explorer (opts, protocol) { if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) + entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) } - function collapse_and_remove_instance (base_path, instance_path, instance_states, all_entries) { - collapse_all_recursively(base_path, instance_path, instance_states, all_entries) - remove_instances_recursively(base_path, instance_path, instance_states, all_entries) + function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { + collapse_all_recursively(base_path, instance_path, instance_states, db) + remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively collapse in default mode - function collapse_all_recursively (base_path, parent_instance_path, instance_states, all_entries) { + function collapse_all_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, all_entries)) + entry.subs.forEach(sub_path => collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, db)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, db)) } - function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, all_entries) { - collapse_all_recursively(base_path, instance_path, instance_states, all_entries) - remove_instances_recursively(base_path, instance_path, instance_states, all_entries) + function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, db) { + collapse_all_recursively(base_path, instance_path, instance_states, db) + remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively subs all hubs in search mode - function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) } } // Recursively hubs all hubs in search mode - function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) } } // Recursively collapse in search mode - function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) } } @@ -2738,7 +2738,7 @@ async function graph_explorer (opts, protocol) { if (!last_clicked_node) return const base_path = last_clicked_node.split('|').pop() - const entry = all_entries[base_path] + const entry = db.get(base_path) const has_subs = Array.isArray(entry?.subs) && entry.subs.length > 0 if (!has_subs) return @@ -2759,7 +2759,7 @@ async function graph_explorer (opts, protocol) { if (!last_clicked_node) return const base_path = last_clicked_node.split('|').pop() - const entry = all_entries[base_path] + const entry = db.get(base_path) const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry?.hubs) && entry.hubs.length > 0 if (!has_hubs || base_path === '/') return diff --git a/lib/graphdb.js b/lib/graphdb.js new file mode 100644 index 0000000..fda4e7e --- /dev/null +++ b/lib/graphdb.js @@ -0,0 +1,43 @@ +module.exports = graphdb + +function graphdb (entries) { + // Validate entries + if (!entries || typeof entries !== 'object') { + console.warn('[graphdb] Invalid entries provided, using empty object') + entries = {} + } + + const api = { + get, + has, + keys, + isEmpty, + root, + raw + } + + return api + + function get (path) { + return entries[path] || null + } + + function has (path) { + return path in entries + } + function keys () { + return Object.keys(entries) + } + + function isEmpty () { + return Object.keys(entries).length === 0 + } + + function root () { + return entries['/'] || null + } + + function raw () { + return entries + } +} From a1e6ba2e661bdf1538d52bf7259126bde58fed34 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 30 Sep 2025 23:33:54 +0500 Subject: [PATCH 116/130] Add Jump button to last clicked if duplicate --- lib/graph_explorer.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 09113ae..f6f5c43 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -2730,7 +2730,13 @@ async function graph_explorer (opts, protocol) { } else { update_last_clicked_styling(last_clicked_node) } - + const base_path = last_clicked_node.split('|').pop() + const has_duplicate_entries = has_duplicates(base_path) + const is_first_occurrence = is_first_duplicate(base_path, last_clicked_node) + if (has_duplicate_entries && !is_first_occurrence) { + const el = shadow.querySelector(`[data-instance_path="${CSS.escape(last_clicked_node)}"]`) + add_jump_button_to_matching_entry(el, base_path, last_clicked_node) + } scroll_to_node(new_node.instance_path) } From fc38ba544d5d0eff06ada1743e759fd054329dbe Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 30 Sep 2025 23:34:08 +0500 Subject: [PATCH 117/130] Updated Docs --- README.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/README.md b/README.md index afa1ced..96ec5e4 100644 --- a/README.md +++ b/README.md @@ -124,3 +124,92 @@ The `style` message provides a string of CSS content that will be injected direc The component maintains a complete `view` array representing the flattened, visible graph structure. It uses an `IntersectionObserver` with two sentinel elements at the top and bottom of the scrollable container. When a sentinel becomes visible, the component dynamically renders the next or previous "chunk" of nodes and removes nodes that have scrolled far out of view. This ensures that the number of DOM elements remains small and constant, providing excellent performance regardless of the total number of nodes in the graph. + +## Modes + +The graph explorer supports three distinct modes that change how users interact with the component: + +### default +The standard navigation mode where users can: +- Click to expand/collapse nodes +- Navigate the graph +- Select individual nodes + +### menubar +An enhanced mode with a visible menubar providing gui based quick access to: +- Mode switching button +- Flag toggles +- Multi-select control +- Select-between control + +### search +A specialized mode for finding and filtering nodes: +- Displays a search input bar +- Filters the view to show only matching nodes +- Supports multi-select and select-between operations on search results + +**Mode State Management:** +- Current mode is stored in `drive` at `mode/current_mode.json` +- Previous mode is tracked in `mode/previous_mode.json` (used when exiting search) +- Search query is persisted in `mode/search_query.json` + +## Flags + +Flags control behaviors of the graph explorer. They are stored in the `flags/` dataset: + +### hubs (`flags/hubs.json`) +Controls the display of hub connections (non-hierarchical relationships): +- `"default"` - Hubs are collapsed by default +- `"true"` - All hubs are expanded +- `"false"` - All hubs are hidden + +Toggle through values using the menubar button in menubar mode. + +### selection (`flags/selection.json`) +Enables or disables node selection functionality: +- `true` - Users can select nodes (default) +- `false` - Selection is disabled + +### recursive_collapse (`flags/recursive_collapse.json`) +Controls collapse behavior for hierarchical nodes: +- `true` - Collapsing a node also collapses all its descendants (default) +- `false` - Only the clicked node is collapsed + +## Keybinds + +The graph explorer supports keyboard navigation and actions. Keybinds are defined in `keybinds/navigation.json` and can be customized through the drive system. + +### Default Keybinds + +| Key Combination | Action | Description | +|----------------|--------|-------------| +| `ArrowUp` | Navigate Up | Move focus to the previous visible node | +| `ArrowDown` | Navigate Down | Move focus to the next visible node | +| `Control+ArrowDown` | Toggle Subs | Expand/collapse child nodes (subs) of the current node | +| `Control+ArrowUp` | Toggle Hubs | Expand/collapse hub connections of the current node | +| `Alt+s` | Multi-select | Add/remove the current node to/from the selection | +| `Alt+b` | Select Between | Select all nodes between the last clicked and current node | +| `Control+m` | Toggle Search | Switch between current mode and search mode | +| `Alt+j` | Jump to Next Duplicate | Navigate to the next occurrence of a duplicate node | + +**Customizing Keybinds:** + +Keybinds can be customized by updating the `keybinds/navigation.json` file in the drive with a JSON object mapping key combinations to action names: + +```javascript +{ + "ArrowUp": "navigate_up_current_node", + "ArrowDown": "navigate_down_current_node", + "Control+ArrowDown": "toggle_subs_for_current_node", + "Control+ArrowUp": "toggle_hubs_for_current_node", + "Alt+s": "multiselect_current_node", + "Alt+b": "select_between_current_node", + "Control+m": "toggle_search_mode", + "Alt+j": "jump_to_next_duplicate" +} +``` + +**Key Combination Format:** +- Modifier keys: `Control+`, `Alt+`, `Shift+` +- Can combine multiple modifiers: `Control+Shift+Key` +- Key names follow standard JavaScript `event.key` values From cd881594ec97403f3ccf84db6782850680b836e5 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 30 Sep 2025 23:49:07 +0500 Subject: [PATCH 118/130] Bundled --- bundle.js | 402 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 297 insertions(+), 105 deletions(-) diff --git a/bundle.js b/bundle.js index a83cd79..cfd11eb 100644 --- a/bundle.js +++ b/bundle.js @@ -3,12 +3,13 @@ },{}],2:[function(require,module,exports){ (function (__filename){(function (){ const STATE = require('./STATE') +const graphdb = require('./graphdb') const statedb = STATE(__filename) const { get } = statedb(fallback_module) module.exports = graph_explorer -async function graph_explorer (opts) { +async function graph_explorer (opts, protocol) { /****************************************************************************** 1. COMPONENT INITIALIZATION - This sets up the initial state, variables, and the basic DOM structure. @@ -22,7 +23,7 @@ async function graph_explorer (opts) { let horizontal_scroll_value = 0 let selected_instance_paths = [] let confirmed_instance_paths = [] - let all_entries = {} // Holds the entire graph structure from entries.json. + let db = null // Database for entries let instance_states = {} // Holds expansion state {expanded_subs, expanded_hubs} for each node instance. let search_state_instances = {} let search_entry_states = {} // Holds expansion state for search mode interactions separately @@ -55,6 +56,12 @@ async function graph_explorer (opts) { const manipulated_inside_search = {} let keybinds = {} // Store keyboard navigation bindings + // Protocol system for message-based communication + let send = null + if (protocol) { + send = protocol(msg => onmessage(msg)) + } + const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) @@ -103,6 +110,134 @@ async function graph_explorer (opts) { return el + /****************************************************************************** + PROTOCOL MESSAGE HANDLING + - Handles incoming messages and sends outgoing messages. + - @TODO: define the messages we wanna to send inorder to receive some info. +******************************************************************************/ + function onmessage ({ type, data }) { + const on_message_types = { + set_mode: handle_set_mode, + set_search_query: handle_set_search_query, + select_nodes: handle_select_nodes, + expand_node: handle_expand_node, + collapse_node: handle_collapse_node, + toggle_node: handle_toggle_node, + get_selected: handle_get_selected, + get_confirmed: handle_get_confirmed, + clear_selection: handle_clear_selection, + set_flag: handle_set_flag, + scroll_to_node: handle_scroll_to_node + } + + const handler = on_message_types[type] + if (handler) handler(data) + else console.warn(`[graph_explorer-protocol] Unknown message type: ${type}`, data) + + function handle_set_mode (data) { + const { mode: new_mode } = data + if (new_mode && ['default', 'menubar', 'search'].includes(new_mode)) { + update_drive_state({ type: 'mode/current_mode', message: new_mode }) + send_message({ type: 'mode_changed', data: { mode: new_mode } }) + } + } + + function handle_set_search_query (data) { + const { query } = data + if (typeof query === 'string') { + search_query = query + drive_updated_by_search = true + update_drive_state({ type: 'mode/search_query', message: query }) + if (mode === 'search') perform_search(query) + send_message({ type: 'search_query_changed', data: { query } }) + } + } + + function handle_select_nodes (data) { + const { instance_paths } = data + if (Array.isArray(instance_paths)) { + update_drive_state({ type: 'runtime/selected_instance_paths', message: instance_paths }) + send_message({ type: 'selection_changed', data: { selected: instance_paths } }) + } + } + + function handle_expand_node (data) { + const { instance_path, expand_subs = true, expand_hubs = false } = data + if (instance_path && instance_states[instance_path]) { + instance_states[instance_path].expanded_subs = expand_subs + instance_states[instance_path].expanded_hubs = expand_hubs + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'node_expanded', data: { instance_path, expand_subs, expand_hubs } }) + } + } + + function handle_collapse_node (data) { + const { instance_path } = data + if (instance_path && instance_states[instance_path]) { + instance_states[instance_path].expanded_subs = false + instance_states[instance_path].expanded_hubs = false + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'node_collapsed', data: { instance_path } }) + } + } + + function handle_toggle_node (data) { + const { instance_path, toggle_type = 'subs' } = data + if (instance_path && instance_states[instance_path]) { + if (toggle_type === 'subs') { + toggle_subs(instance_path) + } else if (toggle_type === 'hubs') { + toggle_hubs(instance_path) + } + send_message({ type: 'node_toggled', data: { instance_path, toggle_type } }) + } + } + + function handle_get_selected (data) { + send_message({ type: 'selected_nodes', data: { selected: selected_instance_paths } }) + } + + function handle_get_confirmed (data) { + send_message({ type: 'confirmed_nodes', data: { confirmed: confirmed_instance_paths } }) + } + + function handle_clear_selection (data) { + update_drive_state({ type: 'runtime/selected_instance_paths', message: [] }) + update_drive_state({ type: 'runtime/confirmed_selected', message: [] }) + send_message({ type: 'selection_cleared', data: {} }) + } + + function handle_set_flag (data) { + const { flag_type, value } = data + if (flag_type === 'hubs' && ['default', 'true', 'false'].includes(value)) { + update_drive_state({ type: 'flags/hubs', message: value }) + } else if (flag_type === 'selection') { + update_drive_state({ type: 'flags/selection', message: value }) + } else if (flag_type === 'recursive_collapse') { + update_drive_state({ type: 'flags/recursive_collapse', message: value }) + } + send_message({ type: 'flag_changed', data: { flag_type, value } }) + } + + function handle_scroll_to_node (data) { + const { instance_path } = data + const node_index = view.findIndex(n => n.instance_path === instance_path) + if (node_index !== -1) { + const scroll_position = node_index * node_height + container.scrollTop = scroll_position + send_message({ type: 'scrolled_to_node', data: { instance_path, scroll_position } }) + } + } + } + + function send_message ({ type, data }) { + if (send) { + send({ type, data }) + } + } + /****************************************************************************** 2. STATE AND DATA HANDLING - These functions process incoming data from the STATE module's `sdb.watch`. @@ -156,20 +291,20 @@ async function graph_explorer (opts) { function on_entries ({ data }) { if (!data || data[0] == null) { console.error('Entries data is missing or empty.') - all_entries = {} + db = graphdb({}) return } const parsed_data = parse_json_data(data[0], 'entries.json') if (typeof parsed_data !== 'object' || !parsed_data) { console.error('Parsed entries data is not a valid object.') - all_entries = {} + db = graphdb({}) return } - all_entries = parsed_data + db = graphdb(parsed_data) // After receiving entries, ensure the root node state is initialized and trigger the first render. const root_path = '/' - if (all_entries[root_path]) { + if (db.has(root_path)) { const root_instance_path = '|/' if (!instance_states[root_instance_path]) { instance_states[root_instance_path] = { @@ -480,10 +615,11 @@ async function graph_explorer (opts) { parent_pipe_trail, parent_base_path, base_path, - all_entries + db }) { const children_pipe_trail = [...parent_pipe_trail] - const is_hub_on_top = base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' + const parent_entry = db.get(parent_base_path) + const is_hub_on_top = base_path === parent_entry?.hubs?.[0] || base_path === '/' if (depth > 0) { if (is_hub) { @@ -515,10 +651,11 @@ async function graph_explorer (opts) { parent_pipe_trail, parent_base_path, base_path, - all_entries + db }) { let last_pipe = null - const calculated_is_hub_on_top = base_path === all_entries[parent_base_path]?.hubs?.[0] || base_path === '/' + const parent_entry = db.get(parent_base_path) + const calculated_is_hub_on_top = base_path === parent_entry?.hubs?.[0] || base_path === '/' const final_is_hub_on_top = is_hub_on_top !== undefined ? is_hub_on_top : calculated_is_hub_on_top if (depth > 0) { @@ -571,7 +708,7 @@ async function graph_explorer (opts) { }) } - if (Object.keys(all_entries).length === 0) { + if (!db || db.isEmpty()) { console.warn('No entries available to render.') return } @@ -591,7 +728,7 @@ async function graph_explorer (opts) { is_hub: false, parent_pipe_trail: [], instance_states, - all_entries + db }) // Recalculate duplicates after view is built @@ -634,7 +771,7 @@ async function graph_explorer (opts) { } } - // Traverses the hierarchical `all_entries` data and builds a flat `view` array for rendering. + // Traverses the hierarchical entries data and builds a flat `view` array for rendering. function build_view_recursive ({ base_path, parent_instance_path, @@ -645,10 +782,10 @@ async function graph_explorer (opts) { is_first_hub = false, parent_pipe_trail, instance_states, - all_entries + db }) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return [] const state = get_or_create_state(instance_states, instance_path) @@ -661,7 +798,7 @@ async function graph_explorer (opts) { parent_pipe_trail, parent_base_path, base_path, - all_entries + db }) const current_view = [] @@ -679,7 +816,7 @@ async function graph_explorer (opts) { is_first_hub: is_hub ? is_hub_on_top : false, parent_pipe_trail: children_pipe_trail, instance_states, - all_entries + db }) ) }) @@ -709,7 +846,7 @@ async function graph_explorer (opts) { is_hub: false, parent_pipe_trail: children_pipe_trail, instance_states, - all_entries + db }) ) }) @@ -737,7 +874,7 @@ async function graph_explorer (opts) { is_in_original_view, query }) { - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) { const err_el = document.createElement('div') err_el.className = 'node error' @@ -766,7 +903,7 @@ async function graph_explorer (opts) { parent_pipe_trail, parent_base_path, base_path, - all_entries + db }) const el = document.createElement('div') @@ -1018,6 +1155,7 @@ async function graph_explorer (opts) { function toggle_search_mode () { const target_mode = mode === 'search' ? previous_mode : 'search' console.log('[SEARCH DEBUG] Switching mode from', mode, 'to', target_mode) + send_message({ type: 'mode_toggling', data: { from: mode, to: target_mode } }) if (mode === 'search') { // When switching from search to default mode, expand selected entries if (selected_instance_paths.length > 0) { @@ -1039,6 +1177,7 @@ async function graph_explorer (opts) { ignore_drive_updated_by_scroll = true update_drive_state({ type: 'mode/current_mode', message: target_mode }) search_state_instances = instance_states + send_message({ type: 'mode_changed', data: { mode: target_mode } }) } function toggle_multi_select () { @@ -1125,28 +1264,25 @@ async function graph_explorer (opts) { is_hub: false, parent_pipe_trail: [], instance_states, - all_entries + db }) const original_view_paths = original_view.map(n => n.instance_path) search_state_instances = {} const search_tracking = {} - const search_view = build_search_view_recursive({ - query, + view = build_search_view_recursive({ base_path: '/', parent_instance_path: '', - parent_base_path: null, depth: 0, is_last_sub: true, is_hub: false, is_first_hub: false, parent_pipe_trail: [], instance_states: search_state_instances, - all_entries, + db, original_view_paths, is_expanded_child: false, search_tracking }) - console.log('[SEARCH DEBUG] Search view built:', search_view.length) render_search_results(search_view, query) } @@ -1162,12 +1298,12 @@ async function graph_explorer (opts) { is_first_hub = false, parent_pipe_trail, instance_states, - all_entries, + db, original_view_paths, is_expanded_child = false, search_tracking = {} }) { - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return [] const instance_path = `${parent_instance_path}|${base_path}` @@ -1187,7 +1323,7 @@ async function graph_explorer (opts) { parent_pipe_trail, parent_base_path, base_path, - all_entries + db }) // Process hubs if they should be expanded @@ -1208,7 +1344,7 @@ async function graph_explorer (opts) { is_first_hub: is_hub_on_top, parent_pipe_trail: children_pipe_trail, instance_states, - all_entries, + db, original_view_paths, is_expanded_child: true, search_tracking @@ -1231,7 +1367,7 @@ async function graph_explorer (opts) { is_first_hub: false, parent_pipe_trail: children_pipe_trail, instance_states, - all_entries, + db, original_view_paths, is_expanded_child: true, search_tracking @@ -1251,7 +1387,7 @@ async function graph_explorer (opts) { is_first_hub: false, parent_pipe_trail: children_pipe_trail, instance_states, - all_entries, + db, original_view_paths, is_expanded_child: false, search_tracking @@ -1327,6 +1463,7 @@ async function graph_explorer (opts) { function select_node (ev, instance_path) { last_clicked_node = instance_path update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + send_message({ type: 'node_clicked', data: { instance_path } }) // Handle shift+click to enable select between mode temporarily if (ev.shiftKey && !select_between_enabled) { @@ -1343,8 +1480,10 @@ async function graph_explorer (opts) { } else if (ev.ctrlKey || multi_select_enabled) { new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) + send_message({ type: 'selection_changed', data: { selected: [...new_selected] } }) } else { update_drive_state({ type: 'runtime/selected_instance_paths', message: [instance_path] }) + send_message({ type: 'selection_changed', data: { selected: [instance_path] } }) } } @@ -1409,7 +1548,7 @@ async function graph_explorer (opts) { const child_base = parts[i + 1] const parent_instance_path = parts.slice(0, i + 1).map(p => '|' + p).join('') const parent_state = get_or_create_state(instance_states, parent_instance_path) - const parent_entry = all_entries[parent_base] + const parent_entry = db.get(parent_base) console.log('[SEARCH DEBUG] Processing parent-child relationship:', { parent_base, @@ -1506,11 +1645,11 @@ async function graph_explorer (opts) { // Update view order tracking for the toggled subs const base_path = instance_path.split('|').pop() - const entry = all_entries[base_path] + const entry = db.get(base_path) if (entry && Array.isArray(entry.subs)) { - if (was_expanded && recursive_collapse_flag === true) entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) - else entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, all_entries)) + if (was_expanded && recursive_collapse_flag === true) entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) + else entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, db)) } last_clicked_node = instance_path @@ -1520,20 +1659,21 @@ async function graph_explorer (opts) { // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'subs_toggled', data: { instance_path, expanded: state.expanded_subs } }) - function toggle_subs_instance (sub_path, instance_path, instance_states, all_entries) { + function toggle_subs_instance (sub_path, instance_path, instance_states, db) { if (was_expanded) { // Collapsing so - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(sub_path, instance_path, instance_states, db) } else { // Expanding so - add_instances_recursively(sub_path, instance_path, instance_states, all_entries) + add_instances_recursively(sub_path, instance_path, instance_states, db) } } - function collapse_and_remove_instance (sub_path, instance_path, instance_states, all_entries) { - collapse_subs_recursively(sub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(sub_path, instance_path, instance_states, all_entries) + function collapse_and_remove_instance (sub_path, instance_path, instance_states, db) { + collapse_subs_recursively(sub_path, instance_path, instance_states, db) + remove_instances_recursively(sub_path, instance_path, instance_states, db) } } @@ -1545,20 +1685,20 @@ async function graph_explorer (opts) { // Update view order tracking for the toggled hubs const base_path = instance_path.split('|').pop() - const entry = all_entries[base_path] + const entry = db.get(base_path) if (entry && Array.isArray(entry.hubs)) { if (was_expanded && recursive_collapse_flag === true) { // collapse all hub descendants - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) } else { // only toggle direct hubs - entry.hubs.forEach(hub_path => toggle_hubs_instance(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => toggle_hubs_instance(hub_path, instance_path, instance_states, db)) } - function collapse_and_remove_instance (hub_path, instance_path, instance_states, all_entries) { - collapse_hubs_recursively(hub_path, instance_path, instance_states, all_entries) - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + function collapse_and_remove_instance (hub_path, instance_path, instance_states, db) { + collapse_hubs_recursively(hub_path, instance_path, instance_states, db) + remove_instances_recursively(hub_path, instance_path, instance_states, db) } } @@ -1569,14 +1709,15 @@ async function graph_explorer (opts) { build_and_render_view(instance_path, true) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'hubs_toggled', data: { instance_path, expanded: state.expanded_hubs } }) - function toggle_hubs_instance (hub_path, instance_path, instance_states, all_entries) { + function toggle_hubs_instance (hub_path, instance_path, instance_states, db) { if (was_expanded) { // Collapsing so - remove_instances_recursively(hub_path, instance_path, instance_states, all_entries) + remove_instances_recursively(hub_path, instance_path, instance_states, db) } else { // Expanding so - add_instances_recursively(hub_path, instance_path, instance_states, all_entries) + add_instances_recursively(hub_path, instance_path, instance_states, db) } } } @@ -1596,8 +1737,8 @@ async function graph_explorer (opts) { if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() - const entry = all_entries[base_path] - if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, all_entries)) + const entry = db.get(base_path) + if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, db)) } const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs ? null : true @@ -1632,8 +1773,8 @@ async function graph_explorer (opts) { if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() - const entry = all_entries[base_path] - if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, all_entries)) + const entry = db.get(base_path) + if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, db)) } const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs @@ -1982,12 +2123,12 @@ async function graph_explorer (opts) { function initialize_tracking_from_current_state () { const root_path = '/' const root_instance_path = '|/' - if (all_entries[root_path]) { + if (db.has(root_path)) { add_instance_to_view_tracking(root_path, root_instance_path) // Add initially expanded subs if any - const root_entry = all_entries[root_path] + const root_entry = db.get(root_path) if (root_entry && Array.isArray(root_entry.subs)) { - root_entry.subs.forEach(sub_path => add_instances_recursively(sub_path, root_instance_path, instance_states, all_entries)) + root_entry.subs.forEach(sub_path => add_instances_recursively(sub_path, root_instance_path, instance_states, db)) } } } @@ -2025,19 +2166,19 @@ async function graph_explorer (opts) { } // Recursively add instances to tracking when expanding - function add_instances_recursively (base_path, parent_instance_path, instance_states, all_entries) { + function add_instances_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_hubs && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => add_instances_recursively(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => add_instances_recursively(hub_path, instance_path, instance_states, db)) } if (state.expanded_subs && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => add_instances_recursively(sub_path, instance_path, instance_states, all_entries)) + entry.subs.forEach(sub_path => add_instances_recursively(sub_path, instance_path, instance_states, db)) } // Add the instance itself @@ -2045,48 +2186,48 @@ async function graph_explorer (opts) { } // Recursively remove instances from tracking when collapsing - function remove_instances_recursively (base_path, parent_instance_path, instance_states, all_entries) { + function remove_instances_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) - if (state.expanded_hubs && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => remove_instances_recursively(hub_path, instance_path, instance_states, all_entries)) - if (state.expanded_subs && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => remove_instances_recursively(sub_path, instance_path, instance_states, all_entries)) + if (state.expanded_hubs && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => remove_instances_recursively(hub_path, instance_path, instance_states, db)) + if (state.expanded_subs && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => remove_instances_recursively(sub_path, instance_path, instance_states, db)) // Remove the instance itself remove_instance_from_view_tracking(base_path, instance_path) } // Recursively hubs all subs in default mode - function collapse_subs_recursively (base_path, parent_instance_path, instance_states, all_entries) { + function collapse_subs_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) + entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) // Decrement hub counter - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) } - function collapse_and_remove_instance (base_path, instance_path, instance_states, all_entries) { - collapse_subs_recursively(base_path, instance_path, instance_states, all_entries) - remove_instances_recursively(base_path, instance_path, instance_states, all_entries) + function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { + collapse_subs_recursively(base_path, instance_path, instance_states, db) + remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively hubs all hubs in default mode - function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, all_entries) { + function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) @@ -2094,98 +2235,98 @@ async function graph_explorer (opts) { if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, all_entries)) + entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) } - function collapse_and_remove_instance (base_path, instance_path, instance_states, all_entries) { - collapse_all_recursively(base_path, instance_path, instance_states, all_entries) - remove_instances_recursively(base_path, instance_path, instance_states, all_entries) + function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { + collapse_all_recursively(base_path, instance_path, instance_states, db) + remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively collapse in default mode - function collapse_all_recursively (base_path, parent_instance_path, instance_states, all_entries) { + function collapse_all_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, all_entries)) + entry.subs.forEach(sub_path => collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, db)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, db)) } - function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, all_entries) { - collapse_all_recursively(base_path, instance_path, instance_states, all_entries) - remove_instances_recursively(base_path, instance_path, instance_states, all_entries) + function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, db) { + collapse_all_recursively(base_path, instance_path, instance_states, db) + remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively subs all hubs in search mode - function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) } } // Recursively hubs all hubs in search mode - function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) } } // Recursively collapse in search mode - function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, all_entries) { + function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = all_entries[base_path] + const entry = db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, all_entries)) + entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, all_entries)) + entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) } } @@ -2593,7 +2734,13 @@ async function graph_explorer (opts) { } else { update_last_clicked_styling(last_clicked_node) } - + const base_path = last_clicked_node.split('|').pop() + const has_duplicate_entries = has_duplicates(base_path) + const is_first_occurrence = is_first_duplicate(base_path, last_clicked_node) + if (has_duplicate_entries && !is_first_occurrence) { + const el = shadow.querySelector(`[data-instance_path="${CSS.escape(last_clicked_node)}"]`) + add_jump_button_to_matching_entry(el, base_path, last_clicked_node) + } scroll_to_node(new_node.instance_path) } @@ -2601,7 +2748,7 @@ async function graph_explorer (opts) { if (!last_clicked_node) return const base_path = last_clicked_node.split('|').pop() - const entry = all_entries[base_path] + const entry = db.get(base_path) const has_subs = Array.isArray(entry?.subs) && entry.subs.length > 0 if (!has_subs) return @@ -2622,7 +2769,7 @@ async function graph_explorer (opts) { if (!last_clicked_node) return const base_path = last_clicked_node.split('|').pop() - const entry = all_entries[base_path] + const entry = db.get(base_path) const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry?.hubs) && entry.hubs.length > 0 if (!has_hubs || base_path === '/') return @@ -2765,7 +2912,52 @@ function fallback_module () { } }).call(this)}).call(this,"/lib/graph_explorer.js") -},{"./STATE":1}],3:[function(require,module,exports){ +},{"./STATE":1,"./graphdb":3}],3:[function(require,module,exports){ +module.exports = graphdb + +function graphdb (entries) { + // Validate entries + if (!entries || typeof entries !== 'object') { + console.warn('[graphdb] Invalid entries provided, using empty object') + entries = {} + } + + const api = { + get, + has, + keys, + isEmpty, + root, + raw + } + + return api + + function get (path) { + return entries[path] || null + } + + function has (path) { + return path in entries + } + function keys () { + return Object.keys(entries) + } + + function isEmpty () { + return Object.keys(entries).length === 0 + } + + function root () { + return entries['/'] || null + } + + function raw () { + return entries + } +} + +},{}],4:[function(require,module,exports){ const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' const init_url = location.hash === '#dev' ? 'web/init.js' : prefix + 'src/node_modules/init.js' const args = arguments @@ -2788,7 +2980,7 @@ fetch(init_url, fetch_opts) require('./page') // or whatever is otherwise the main entry of our project }) -},{"./page":4}],4:[function(require,module,exports){ +},{"./page":5}],5:[function(require,module,exports){ (function (__filename){(function (){ const STATE = require('../lib/STATE') const statedb = STATE(__filename) @@ -2892,4 +3084,4 @@ function fallback_module () { } }).call(this)}).call(this,"/web/page.js") -},{"..":2,"../lib/STATE":1}]},{},[3]); +},{"..":2,"../lib/STATE":1}]},{},[4]); From 176ff3a172389baab3e7f59d4c0449295c77ca59 Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 30 Sep 2025 23:54:39 +0500 Subject: [PATCH 119/130] a mistake --- lib/graph_explorer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index f6f5c43..bc7edb4 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -1265,7 +1265,8 @@ async function graph_explorer (opts, protocol) { const original_view_paths = original_view.map(n => n.instance_path) search_state_instances = {} const search_tracking = {} - view = build_search_view_recursive({ + const search_view = build_search_view_recursive({ + query, base_path: '/', parent_instance_path: '', depth: 0, From 5a768275666d732b98720779d19b37157494ca4e Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 30 Sep 2025 23:54:54 +0500 Subject: [PATCH 120/130] bundled --- bundle.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundle.js b/bundle.js index cfd11eb..3b919a5 100644 --- a/bundle.js +++ b/bundle.js @@ -1269,7 +1269,8 @@ async function graph_explorer (opts, protocol) { const original_view_paths = original_view.map(n => n.instance_path) search_state_instances = {} const search_tracking = {} - view = build_search_view_recursive({ + const search_view = build_search_view_recursive({ + query, base_path: '/', parent_instance_path: '', depth: 0, From fa4b818a50f3384dd5cac887748d169f65ba970d Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 11 Oct 2025 18:15:44 +0500 Subject: [PATCH 121/130] Added Undo Feature --- lib/graph_explorer.js | 114 +++++++++++++++++++++++++++++++++++++++++- web/page.js | 6 ++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index bc7edb4..bebe2a4 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -37,6 +37,7 @@ async function graph_explorer (opts, protocol) { let ignore_drive_updated_by_scroll = false // Prevent scroll flag. let drive_updated_by_match = false // Flag to prevent `onbatch` from re-rendering on matching entry updates. let drive_updated_by_tracking = false // Flag to prevent `onbatch` from re-rendering on view order tracking updates. + let drive_updated_by_undo = false // Flag to prevent onbatch from re-rendering on undo updates let is_loading_from_drive = false // Flag to prevent saving to drive during initial load let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key let select_between_enabled = false // Flag to enable select between mode @@ -51,6 +52,7 @@ async function graph_explorer (opts, protocol) { let root_wand_state = null // Store original root wand state when replaced with jump button const manipulated_inside_search = {} let keybinds = {} // Store keyboard navigation bindings + let undo_stack = [] // Stack to track drive state changes for undo functionality // Protocol system for message-based communication let send = null @@ -97,7 +99,8 @@ async function graph_explorer (opts, protocol) { runtime: on_runtime, mode: on_mode, flags: on_flags, - keybinds: on_keybinds + keybinds: on_keybinds, + undo: on_undo } // Start watching for state changes. This is the main trigger for all updates. await sdb.watch(onbatch) @@ -582,14 +585,66 @@ async function graph_explorer (opts, protocol) { keybinds = parsed_data } + function on_undo ({ data }) { + if (!data || data[0] == null) { + console.error('Undo stack data is missing or empty.') + return + } + const parsed_data = parse_json_data(data[0]) + if (!Array.isArray(parsed_data)) { + console.error('Parsed undo stack data is not a valid array.') + return + } + undo_stack = parsed_data + } + // Helper to persist component state to the drive. async function update_drive_state ({ type, message }) { + // Save current state to undo stack before updating (except for some) + const should_track = ( + !drive_updated_by_undo && + !type.includes('scroll') && + !type.includes('last_clicked') && + !type.includes('view_order_tracking') && + !type.includes('select_between') && + type !== 'undo/stack' + ) + if (should_track) { + await save_to_undo_stack(type) + } + try { await drive.put(`${type}.json`, JSON.stringify(message)) } catch (e) { const [dataset, name] = type.split('/') console.error(`Failed to update ${dataset} state for ${name}:`, e) } + if (should_track) { + render_menubar() + } + } + + async function save_to_undo_stack (type) { + try { + const current_file = await drive.get(`${type}.json`) + if (current_file && current_file.raw) { + const snapshot = { + type, + value: current_file.raw, + timestamp: Date.now() + } + + // Add to stack (limit to 50 items to prevent memory issues) + undo_stack.push(snapshot) + if (undo_stack.length > 50) { + undo_stack.shift() + } + drive_updated_by_undo = true + await drive.put('undo/stack.json', JSON.stringify(undo_stack)) + } + } catch (e) { + console.error('Failed to save to undo stack:', e) + } } function get_or_create_state (states, instance_path) { @@ -1098,6 +1153,11 @@ async function graph_explorer (opts, protocol) { search_button.textContent = 'Search' search_button.onclick = toggle_search_mode + const undo_button = document.createElement('button') + undo_button.textContent = `Undo (${undo_stack.length})` + undo_button.onclick = () => undo(1) + undo_button.disabled = undo_stack.length === 0 + const multi_select_button = document.createElement('button') multi_select_button.textContent = `Multi Select: ${multi_select_enabled}` multi_select_button.onclick = toggle_multi_select @@ -1118,7 +1178,7 @@ async function graph_explorer (opts, protocol) { recursive_collapse_button.textContent = `Recursive Collapse: ${recursive_collapse_flag}` recursive_collapse_button.onclick = toggle_recursive_collapse_flag - menubar.replaceChildren(search_button, multi_select_button, select_between_button, hubs_button, selection_button, recursive_collapse_button) + menubar.replaceChildren(search_button, undo_button, multi_select_button, select_between_button, hubs_button, selection_button, recursive_collapse_button) } function render_searchbar () { @@ -2545,6 +2605,10 @@ async function graph_explorer (opts, protocol) { drive_updated_by_last_clicked = false return true } + if (drive_updated_by_undo) { + drive_updated_by_undo = false + return true + } console.log('[SEARCH DEBUG] No feedback flags set, allowing onbatch') return false } @@ -2842,6 +2906,49 @@ async function graph_explorer (opts, protocol) { cycle_to_next_duplicate(base_path, current_instance_path) } } + + /****************************************************************************** + 10. UNDO FUNCTIONALITY + - Implements undo functionality to revert drive state changes + ******************************************************************************/ + async function undo (steps = 1) { + if (undo_stack.length === 0) { + console.warn('No actions to undo') + return + } + + const actions_to_undo = Math.min(steps, undo_stack.length) + console.log(`Undoing ${actions_to_undo} action(s)`) + + // Pop the specified number of actions from the stack + const snapshots_to_restore = [] + for (let i = 0; i < actions_to_undo; i++) { + const snapshot = undo_stack.pop() + if (snapshot) snapshots_to_restore.push(snapshot) + } + + // Restore the last snapshot's state + if (snapshots_to_restore.length > 0) { + const snapshot = snapshots_to_restore[snapshots_to_restore.length - 1] + + try { + // Restore the state WITHOUT setting drive_updated_by_undo flag + // This allows onbatch to process the change and update the UI + await drive.put(`${snapshot.type}.json`, snapshot.value) + + // Update the undo stack in drive (with flag to prevent tracking this update) + // drive_updated_by_undo = true + await drive.put('undo/stack.json', JSON.stringify(undo_stack)) + + console.log(`Undo completed: restored ${snapshot.type} to previous state`) + + // Re-render menubar to update undo button count + render_menubar() + } catch (e) { + console.error('Failed to undo action:', e) + } + } + } } /****************************************************************************** @@ -2902,6 +3009,9 @@ function fallback_module () { 'Alt+j': 'jump_to_next_duplicate' }) } + }, + 'undo/': { + 'stack.json': { raw: '[]' } } } } diff --git a/web/page.js b/web/page.js index 4460b6d..beefb8e 100644 --- a/web/page.js +++ b/web/page.js @@ -83,7 +83,8 @@ function fallback_module () { runtime: 'runtime', mode: 'mode', flags: 'flags', - keybinds: 'keybinds' + keybinds: 'keybinds', + undo: 'undo' } } }, @@ -94,7 +95,8 @@ function fallback_module () { 'runtime/': {}, 'mode/': {}, 'flags/': {}, - 'keybinds/': {} + 'keybinds/': {}, + 'undo/': {} } } } From e1d637871a1545a81c8abb4da9d0005ab7182829 Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 11 Oct 2025 18:15:58 +0500 Subject: [PATCH 122/130] small tweaks --- lib/graph_explorer.js | 40 ++++++++++++++++++++-------------------- lib/graphdb.js | 12 ++++++------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index bebe2a4..e191fde 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -7,11 +7,11 @@ module.exports = graph_explorer async function graph_explorer (opts, protocol) { /****************************************************************************** - 1. COMPONENT INITIALIZATION + COMPONENT INITIALIZATION - This sets up the initial state, variables, and the basic DOM structure. - It also initializes the IntersectionObserver for virtual scrolling and sets up the watcher for state changes. -******************************************************************************/ + ******************************************************************************/ const { sdb } = await get(opts.sid) const { drive } = sdb @@ -110,10 +110,10 @@ async function graph_explorer (opts, protocol) { return el /****************************************************************************** - PROTOCOL MESSAGE HANDLING +ESSAGE HANDLING - Handles incoming messages and sends outgoing messages. - @TODO: define the messages we wanna to send inorder to receive some info. -******************************************************************************/ + ******************************************************************************/ function onmessage ({ type, data }) { const on_message_types = { set_mode: handle_set_mode, @@ -238,10 +238,10 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 2. STATE AND DATA HANDLING + STATE AND DATA HANDLING - These functions process incoming data from the STATE module's `sdb.watch`. - `onbatch` is the primary entry point. -******************************************************************************/ + ******************************************************************************/ async function onbatch (batch) { console.log('[SEARCH DEBUG] onbatch caled:', { mode, @@ -733,12 +733,12 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 3. VIEW AND RENDERING LOGIC AND SCALING + VIEW AND RENDERING LOGIC AND SCALING - These functions build the `view` array and render the DOM. - `build_and_render_view` is the main orchestrator. - `build_view_recursive` creates the flat `view` array from the hierarchical data. - `calculate_mobile_scale` calculates the scale factor for mobile devices. -******************************************************************************/ + ******************************************************************************/ function build_and_render_view (focal_instance_path, hub_toggle = false) { console.log('[SEARCH DEBUG] build_and_render_view called:', { focal_instance_path, @@ -909,7 +909,7 @@ async function graph_explorer (opts, protocol) { 4. NODE CREATION AND EVENT HANDLING - `create_node` generates the DOM element for a single node. - It sets up event handlers for user interactions like selecting or toggling. -******************************************************************************/ + ******************************************************************************/ function create_node ({ base_path, @@ -1146,8 +1146,8 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 5. MENUBAR AND SEARCH -******************************************************************************/ + MENUBAR AND SEARCH + ******************************************************************************/ function render_menubar () { const search_button = document.createElement('button') search_button.textContent = 'Search' @@ -1513,7 +1513,7 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 6. VIEW MANIPULATION & USER ACTIONS + VIEW MANIPULATION & USER ACTIONS - These functions handle user interactions like selecting, confirming, toggling, and resetting the graph. ******************************************************************************/ @@ -2056,10 +2056,10 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 7. VIRTUAL SCROLLING + VIRTUAL SCROLLING - These functions implement virtual scrolling to handle large graphs efficiently using an IntersectionObserver. -******************************************************************************/ + ******************************************************************************/ function onscroll () { if (scroll_update_pending) return scroll_update_pending = true @@ -2161,7 +2161,7 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 8. ENTRY DUPLICATION PREVENTION + ENTRY DUPLICATION PREVENTION ******************************************************************************/ function collect_all_duplicate_entries () { @@ -2559,7 +2559,7 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 9. HELPER FUNCTIONS + HELPER FUNCTIONS ******************************************************************************/ function get_highlighted_name (name, query) { // Creates a new regular expression. @@ -2733,7 +2733,7 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 9. KEYBOARD NAVIGATION + KEYBOARD NAVIGATION - Handles keyboard-based navigation for the graph explorer - Navigate up/down around last_clicked node ******************************************************************************/ @@ -2908,7 +2908,7 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 10. UNDO FUNCTIONALITY + UNDO FUNCTIONALITY - Implements undo functionality to revert drive state changes ******************************************************************************/ async function undo (steps = 1) { @@ -2952,12 +2952,12 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 10. FALLBACK CONFIGURATION + FALLBACK CONFIGURATION - This provides the default data and API configuration for the component, following the pattern described in `instructions.md`. - It defines the default datasets (`entries`, `style`, `runtime`) and their initial values. -******************************************************************************/ + ******************************************************************************/ function fallback_module () { return { api: fallback_instance diff --git a/lib/graphdb.js b/lib/graphdb.js index fda4e7e..5e9a37f 100644 --- a/lib/graphdb.js +++ b/lib/graphdb.js @@ -18,26 +18,26 @@ function graphdb (entries) { return api - function get (path) { + function get (path) { return entries[path] || null } - function has (path) { + function has (path) { return path in entries } - function keys () { + function keys () { return Object.keys(entries) } - function isEmpty () { + function isEmpty () { return Object.keys(entries).length === 0 } - function root () { + function root () { return entries['/'] || null } - function raw () { + function raw () { return entries } } From e9ab98733ced2969761008b932399cbf9e7fed9a Mon Sep 17 00:00:00 2001 From: ddroid Date: Mon, 20 Oct 2025 23:15:40 +0500 Subject: [PATCH 123/130] bundled --- bundle.js | 170 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 141 insertions(+), 29 deletions(-) diff --git a/bundle.js b/bundle.js index 3b919a5..5555acf 100644 --- a/bundle.js +++ b/bundle.js @@ -11,11 +11,11 @@ module.exports = graph_explorer async function graph_explorer (opts, protocol) { /****************************************************************************** - 1. COMPONENT INITIALIZATION + COMPONENT INITIALIZATION - This sets up the initial state, variables, and the basic DOM structure. - It also initializes the IntersectionObserver for virtual scrolling and sets up the watcher for state changes. -******************************************************************************/ + ******************************************************************************/ const { sdb } = await get(opts.sid) const { drive } = sdb @@ -41,6 +41,7 @@ async function graph_explorer (opts, protocol) { let ignore_drive_updated_by_scroll = false // Prevent scroll flag. let drive_updated_by_match = false // Flag to prevent `onbatch` from re-rendering on matching entry updates. let drive_updated_by_tracking = false // Flag to prevent `onbatch` from re-rendering on view order tracking updates. + let drive_updated_by_undo = false // Flag to prevent onbatch from re-rendering on undo updates let is_loading_from_drive = false // Flag to prevent saving to drive during initial load let multi_select_enabled = false // Flag to enable multi-select mode without ctrl key let select_between_enabled = false // Flag to enable select between mode @@ -55,6 +56,7 @@ async function graph_explorer (opts, protocol) { let root_wand_state = null // Store original root wand state when replaced with jump button const manipulated_inside_search = {} let keybinds = {} // Store keyboard navigation bindings + let undo_stack = [] // Stack to track drive state changes for undo functionality // Protocol system for message-based communication let send = null @@ -101,7 +103,8 @@ async function graph_explorer (opts, protocol) { runtime: on_runtime, mode: on_mode, flags: on_flags, - keybinds: on_keybinds + keybinds: on_keybinds, + undo: on_undo } // Start watching for state changes. This is the main trigger for all updates. await sdb.watch(onbatch) @@ -111,10 +114,10 @@ async function graph_explorer (opts, protocol) { return el /****************************************************************************** - PROTOCOL MESSAGE HANDLING +ESSAGE HANDLING - Handles incoming messages and sends outgoing messages. - @TODO: define the messages we wanna to send inorder to receive some info. -******************************************************************************/ + ******************************************************************************/ function onmessage ({ type, data }) { const on_message_types = { set_mode: handle_set_mode, @@ -239,10 +242,10 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 2. STATE AND DATA HANDLING + STATE AND DATA HANDLING - These functions process incoming data from the STATE module's `sdb.watch`. - `onbatch` is the primary entry point. -******************************************************************************/ + ******************************************************************************/ async function onbatch (batch) { console.log('[SEARCH DEBUG] onbatch caled:', { mode, @@ -586,14 +589,66 @@ async function graph_explorer (opts, protocol) { keybinds = parsed_data } + function on_undo ({ data }) { + if (!data || data[0] == null) { + console.error('Undo stack data is missing or empty.') + return + } + const parsed_data = parse_json_data(data[0]) + if (!Array.isArray(parsed_data)) { + console.error('Parsed undo stack data is not a valid array.') + return + } + undo_stack = parsed_data + } + // Helper to persist component state to the drive. async function update_drive_state ({ type, message }) { + // Save current state to undo stack before updating (except for some) + const should_track = ( + !drive_updated_by_undo && + !type.includes('scroll') && + !type.includes('last_clicked') && + !type.includes('view_order_tracking') && + !type.includes('select_between') && + type !== 'undo/stack' + ) + if (should_track) { + await save_to_undo_stack(type) + } + try { await drive.put(`${type}.json`, JSON.stringify(message)) } catch (e) { const [dataset, name] = type.split('/') console.error(`Failed to update ${dataset} state for ${name}:`, e) } + if (should_track) { + render_menubar() + } + } + + async function save_to_undo_stack (type) { + try { + const current_file = await drive.get(`${type}.json`) + if (current_file && current_file.raw) { + const snapshot = { + type, + value: current_file.raw, + timestamp: Date.now() + } + + // Add to stack (limit to 50 items to prevent memory issues) + undo_stack.push(snapshot) + if (undo_stack.length > 50) { + undo_stack.shift() + } + drive_updated_by_undo = true + await drive.put('undo/stack.json', JSON.stringify(undo_stack)) + } + } catch (e) { + console.error('Failed to save to undo stack:', e) + } } function get_or_create_state (states, instance_path) { @@ -682,12 +737,12 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 3. VIEW AND RENDERING LOGIC AND SCALING + VIEW AND RENDERING LOGIC AND SCALING - These functions build the `view` array and render the DOM. - `build_and_render_view` is the main orchestrator. - `build_view_recursive` creates the flat `view` array from the hierarchical data. - `calculate_mobile_scale` calculates the scale factor for mobile devices. -******************************************************************************/ + ******************************************************************************/ function build_and_render_view (focal_instance_path, hub_toggle = false) { console.log('[SEARCH DEBUG] build_and_render_view called:', { focal_instance_path, @@ -858,7 +913,7 @@ async function graph_explorer (opts, protocol) { 4. NODE CREATION AND EVENT HANDLING - `create_node` generates the DOM element for a single node. - It sets up event handlers for user interactions like selecting or toggling. -******************************************************************************/ + ******************************************************************************/ function create_node ({ base_path, @@ -1095,13 +1150,18 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 5. MENUBAR AND SEARCH -******************************************************************************/ + MENUBAR AND SEARCH + ******************************************************************************/ function render_menubar () { const search_button = document.createElement('button') search_button.textContent = 'Search' search_button.onclick = toggle_search_mode + const undo_button = document.createElement('button') + undo_button.textContent = `Undo (${undo_stack.length})` + undo_button.onclick = () => undo(1) + undo_button.disabled = undo_stack.length === 0 + const multi_select_button = document.createElement('button') multi_select_button.textContent = `Multi Select: ${multi_select_enabled}` multi_select_button.onclick = toggle_multi_select @@ -1122,7 +1182,7 @@ async function graph_explorer (opts, protocol) { recursive_collapse_button.textContent = `Recursive Collapse: ${recursive_collapse_flag}` recursive_collapse_button.onclick = toggle_recursive_collapse_flag - menubar.replaceChildren(search_button, multi_select_button, select_between_button, hubs_button, selection_button, recursive_collapse_button) + menubar.replaceChildren(search_button, undo_button, multi_select_button, select_between_button, hubs_button, selection_button, recursive_collapse_button) } function render_searchbar () { @@ -1457,7 +1517,7 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 6. VIEW MANIPULATION & USER ACTIONS + VIEW MANIPULATION & USER ACTIONS - These functions handle user interactions like selecting, confirming, toggling, and resetting the graph. ******************************************************************************/ @@ -2000,10 +2060,10 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 7. VIRTUAL SCROLLING + VIRTUAL SCROLLING - These functions implement virtual scrolling to handle large graphs efficiently using an IntersectionObserver. -******************************************************************************/ + ******************************************************************************/ function onscroll () { if (scroll_update_pending) return scroll_update_pending = true @@ -2105,7 +2165,7 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 8. ENTRY DUPLICATION PREVENTION + ENTRY DUPLICATION PREVENTION ******************************************************************************/ function collect_all_duplicate_entries () { @@ -2503,7 +2563,7 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 9. HELPER FUNCTIONS + HELPER FUNCTIONS ******************************************************************************/ function get_highlighted_name (name, query) { // Creates a new regular expression. @@ -2549,6 +2609,10 @@ async function graph_explorer (opts, protocol) { drive_updated_by_last_clicked = false return true } + if (drive_updated_by_undo) { + drive_updated_by_undo = false + return true + } console.log('[SEARCH DEBUG] No feedback flags set, allowing onbatch') return false } @@ -2673,7 +2737,7 @@ async function graph_explorer (opts, protocol) { } /****************************************************************************** - 9. KEYBOARD NAVIGATION + KEYBOARD NAVIGATION - Handles keyboard-based navigation for the graph explorer - Navigate up/down around last_clicked node ******************************************************************************/ @@ -2846,15 +2910,58 @@ async function graph_explorer (opts, protocol) { cycle_to_next_duplicate(base_path, current_instance_path) } } + + /****************************************************************************** + UNDO FUNCTIONALITY + - Implements undo functionality to revert drive state changes + ******************************************************************************/ + async function undo (steps = 1) { + if (undo_stack.length === 0) { + console.warn('No actions to undo') + return + } + + const actions_to_undo = Math.min(steps, undo_stack.length) + console.log(`Undoing ${actions_to_undo} action(s)`) + + // Pop the specified number of actions from the stack + const snapshots_to_restore = [] + for (let i = 0; i < actions_to_undo; i++) { + const snapshot = undo_stack.pop() + if (snapshot) snapshots_to_restore.push(snapshot) + } + + // Restore the last snapshot's state + if (snapshots_to_restore.length > 0) { + const snapshot = snapshots_to_restore[snapshots_to_restore.length - 1] + + try { + // Restore the state WITHOUT setting drive_updated_by_undo flag + // This allows onbatch to process the change and update the UI + await drive.put(`${snapshot.type}.json`, snapshot.value) + + // Update the undo stack in drive (with flag to prevent tracking this update) + // drive_updated_by_undo = true + await drive.put('undo/stack.json', JSON.stringify(undo_stack)) + + console.log(`Undo completed: restored ${snapshot.type} to previous state`) + + // Re-render menubar to update undo button count + render_menubar() + } catch (e) { + console.error('Failed to undo action:', e) + } + } + } } /****************************************************************************** - 10. FALLBACK CONFIGURATION + FALLBACK CONFIGURATION - This provides the default data and API configuration for the component, following the pattern described in `instructions.md`. - It defines the default datasets (`entries`, `style`, `runtime`) and their initial values. -******************************************************************************/ + ******************************************************************************/ function fallback_module () { return { api: fallback_instance @@ -2906,6 +3013,9 @@ function fallback_module () { 'Alt+j': 'jump_to_next_duplicate' }) } + }, + 'undo/': { + 'stack.json': { raw: '[]' } } } } @@ -2934,26 +3044,26 @@ function graphdb (entries) { return api - function get (path) { + function get (path) { return entries[path] || null } - function has (path) { + function has (path) { return path in entries } - function keys () { + function keys () { return Object.keys(entries) } - function isEmpty () { + function isEmpty () { return Object.keys(entries).length === 0 } - function root () { + function root () { return entries['/'] || null } - function raw () { + function raw () { return entries } } @@ -3068,7 +3178,8 @@ function fallback_module () { runtime: 'runtime', mode: 'mode', flags: 'flags', - keybinds: 'keybinds' + keybinds: 'keybinds', + undo: 'undo' } } }, @@ -3079,7 +3190,8 @@ function fallback_module () { 'runtime/': {}, 'mode/': {}, 'flags/': {}, - 'keybinds/': {} + 'keybinds/': {}, + 'undo/': {} } } } From c970abfa569c4eaeb8ffd35fea4478c809441fbe Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 28 Oct 2025 17:58:14 +0500 Subject: [PATCH 124/130] Added graphdb via protocol --- .gitignore | 3 +- lib/graph_explorer.js | 635 ++++++++++++++++++++++---------------- lib/graphdb.js | 4 +- {lib => web}/entries.json | 0 web/page.js | 90 +++++- 5 files changed, 457 insertions(+), 275 deletions(-) rename {lib => web}/entries.json (100%) diff --git a/.gitignore b/.gitignore index 8bb47fc..de0279b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /node_modules/* /package-lock.json /npm-debug.log -/package-lock.json \ No newline at end of file +/package-lock.json +/.idea \ No newline at end of file diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index e191fde..06011a7 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -1,5 +1,4 @@ const STATE = require('./STATE') -const graphdb = require('./graphdb') const statedb = STATE(__filename) const { get } = statedb(fallback_module) @@ -60,6 +59,9 @@ async function graph_explorer (opts, protocol) { send = protocol(msg => onmessage(msg)) } + // Create db object that communicates via protocol messages + db = create_db() + const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) @@ -94,7 +96,6 @@ async function graph_explorer (opts, protocol) { // Define handlers for different data types from the drive, called by `onbatch`. const on = { - entries: on_entries, style: inject_style, runtime: on_runtime, mode: on_mode, @@ -126,13 +127,20 @@ ESSAGE HANDLING get_confirmed: handle_get_confirmed, clear_selection: handle_clear_selection, set_flag: handle_set_flag, - scroll_to_node: handle_scroll_to_node + scroll_to_node: handle_scroll_to_node, + db_response: handle_db_response, + db_initialized: handle_db_initialized } const handler = on_message_types[type] if (handler) handler(data) else console.warn(`[graph_explorer-protocol] Unknown message type: ${type}`, data) + function handle_db_response (data) { + const { id, result } = data + db.handle_response(id, result) + } + function handle_set_mode (data) { const { mode: new_mode } = data if (new_mode && ['default', 'menubar', 'search'].includes(new_mode)) { @@ -182,13 +190,13 @@ ESSAGE HANDLING } } - function handle_toggle_node (data) { + async function handle_toggle_node (data) { const { instance_path, toggle_type = 'subs' } = data if (instance_path && instance_states[instance_path]) { if (toggle_type === 'subs') { - toggle_subs(instance_path) + await toggle_subs(instance_path) } else if (toggle_type === 'hubs') { - toggle_hubs(instance_path) + await toggle_hubs(instance_path) } send_message({ type: 'node_toggled', data: { instance_path, toggle_type } }) } @@ -230,13 +238,74 @@ ESSAGE HANDLING } } } - + async function handle_db_initialized (data) { + // Page.js, trigger initial render + // After receiving entries, ensure the root node state is initialized and trigger the first render. + const root_path = '/' + if (db.has(root_path)) { + const root_instance_path = '|/' + if (!instance_states[root_instance_path]) { + instance_states[root_instance_path] = { + expanded_subs: true, + expanded_hubs: false + } + } + // don't rebuild view if we're in search mode with active query + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_entries: skipping build_and_render_view in Search Mode with query:', search_query) + perform_search(search_query) + } else { + // tracking will be initialized later if drive data is empty + build_and_render_view() + } + } else { + console.warn('Root path "/" not found in entries. Clearing view.') + view = [] + if (container) container.replaceChildren() + } + } function send_message ({ type, data }) { if (send) { send({ type, data }) } } + function create_db () { + // Pending requests map to store [resolve || reject] callbacks + const pending_requests = new Map() + let request_id = 0 + + return { + // All operations are async via protocol messages + get: (path) => send_db_request('db_get', { path }), + has: (path) => send_db_request('db_has', { path }), + is_empty: () => send_db_request('db_is_empty', {}), + root: () => send_db_request('db_root', {}), + keys: () => send_db_request('db_keys', {}), + raw: () => send_db_request('db_raw', {}), + // Handle responses from page.js + handle_response: (id, result) => { + const pending = pending_requests.get(id) + if (pending) { + pending.resolve(result) + pending_requests.delete(id) + } + } + } + + function send_db_request (operation, params) { + return new Promise((resolve, reject) => { + // Generate a unique ID + const id = request_id++ + pending_requests.set(id, { resolve, reject }) + send_message({ + type: 'db_request', + data: { id, operation, params } + }) + }) + } + } + /****************************************************************************** STATE AND DATA HANDLING - These functions process incoming data from the STATE module's `sdb.watch`. @@ -269,7 +338,7 @@ ESSAGE HANDLING ) // Call the appropriate handler based on `type`. const func = on[type] - func ? func({ data, paths }) : fail(data, type) + func ? await func({ data, paths }) : fail(data, type) } function batch_get (path) { @@ -287,46 +356,7 @@ ESSAGE HANDLING throw new Error(`Invalid message type: ${type}`, { cause: { data, type } }) } - function on_entries ({ data }) { - if (!data || data[0] == null) { - console.error('Entries data is missing or empty.') - db = graphdb({}) - return - } - const parsed_data = parse_json_data(data[0], 'entries.json') - if (typeof parsed_data !== 'object' || !parsed_data) { - console.error('Parsed entries data is not a valid object.') - db = graphdb({}) - return - } - db = graphdb(parsed_data) - - // After receiving entries, ensure the root node state is initialized and trigger the first render. - const root_path = '/' - if (db.has(root_path)) { - const root_instance_path = '|/' - if (!instance_states[root_instance_path]) { - instance_states[root_instance_path] = { - expanded_subs: true, - expanded_hubs: false - } - } - // don't rebuild view if we're in search mode with active query - if (mode === 'search' && search_query) { - console.log('[SEARCH DEBUG] on_entries: skipping build_and_render_view in Search Mode with query:', search_query) - perform_search(search_query) - } else { - // tracking will be initialized later if drive data is empty - build_and_render_view() - } - } else { - console.warn('Root path "/" not found in entries. Clearing view.') - view = [] - if (container) container.replaceChildren() - } - } - - function on_runtime ({ data, paths }) { + async function on_runtime ({ data, paths }) { const on_runtime_paths = { 'node_height.json': handle_node_height, 'vertical_scroll_value.json': handle_vertical_scroll, @@ -346,9 +376,9 @@ ESSAGE HANDLING if (needs_render) { if (mode === 'search' && search_query) { console.log('[SEARCH DEBUG] on_runtime: Skipping build_and_render_view in search mode with query:', search_query) - perform_search(search_query) + await perform_search(search_query) } else { - build_and_render_view() + await build_and_render_view() } } else if (render_nodes_needed.size > 0) { render_nodes_needed.forEach(re_render_node) @@ -438,7 +468,7 @@ ESSAGE HANDLING } } - function on_mode ({ data, paths }) { + async function on_mode ({ data, paths }) { const on_mode_paths = { 'current_mode.json': handle_current_mode, 'previous_mode.json': handle_previous_mode, @@ -479,8 +509,8 @@ ESSAGE HANDLING mode = new_current_mode render_menubar() render_searchbar() - handle_mode_change() - if (mode === 'search' && search_query) perform_search(search_query) + await handle_mode_change() + if (mode === 'search' && search_query) await perform_search(search_query) function mode_handler (path, data) { const value = parse_json_data(data, path) @@ -658,7 +688,7 @@ ESSAGE HANDLING return states[instance_path] } - function calculate_children_pipe_trail ({ + async function calculate_children_pipe_trail ({ depth, is_hub, is_last_sub, @@ -669,7 +699,7 @@ ESSAGE HANDLING db }) { const children_pipe_trail = [...parent_pipe_trail] - const parent_entry = db.get(parent_base_path) + const parent_entry = await db.get(parent_base_path) const is_hub_on_top = base_path === parent_entry?.hubs?.[0] || base_path === '/' if (depth > 0) { @@ -693,7 +723,7 @@ ESSAGE HANDLING } // Extracted pipe logic for reuse in both default and search modes - function calculate_pipe_trail ({ + async function calculate_pipe_trail ({ depth, is_hub, is_last_sub, @@ -705,7 +735,7 @@ ESSAGE HANDLING db }) { let last_pipe = null - const parent_entry = db.get(parent_base_path) + const parent_entry = await db.get(parent_base_path) const calculated_is_hub_on_top = base_path === parent_entry?.hubs?.[0] || base_path === '/' const final_is_hub_on_top = is_hub_on_top !== undefined ? is_hub_on_top : calculated_is_hub_on_top @@ -739,7 +769,7 @@ ESSAGE HANDLING - `build_view_recursive` creates the flat `view` array from the hierarchical data. - `calculate_mobile_scale` calculates the scale factor for mobile devices. ******************************************************************************/ - function build_and_render_view (focal_instance_path, hub_toggle = false) { + async function build_and_render_view (focal_instance_path, hub_toggle = false) { console.log('[SEARCH DEBUG] build_and_render_view called:', { focal_instance_path, hub_toggle, @@ -759,7 +789,8 @@ ESSAGE HANDLING }) } - if (!db || db.isEmpty()) { + const is_empty = await db.is_empty() + if (!db || is_empty) { console.warn('No entries available to render.') return } @@ -771,7 +802,7 @@ ESSAGE HANDLING if (spacer_element && spacer_element.parentNode) existing_spacer_height = parseFloat(spacer_element.style.height) || 0 // Recursively build the new `view` array from the graph data. - view = build_view_recursive({ + view = await build_view_recursive({ base_path: '/', parent_instance_path: '', depth: 0, @@ -823,7 +854,7 @@ ESSAGE HANDLING } // Traverses the hierarchical entries data and builds a flat `view` array for rendering. - function build_view_recursive ({ + async function build_view_recursive ({ base_path, parent_instance_path, parent_base_path = null, @@ -836,12 +867,12 @@ ESSAGE HANDLING db }) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return [] const state = get_or_create_state(instance_states, instance_path) - const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ + const { children_pipe_trail, is_hub_on_top } = await calculate_children_pipe_trail({ depth, is_hub, is_last_sub, @@ -855,24 +886,37 @@ ESSAGE HANDLING const current_view = [] // If hubs are expanded, recursively add them to the view first (they appear above the node). if (state.expanded_hubs && Array.isArray(entry.hubs)) { - entry.hubs.forEach((hub_path, i, arr) => { - current_view.push( - ...build_view_recursive({ - base_path: hub_path, - parent_instance_path: instance_path, - parent_base_path: base_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: true, - is_first_hub: is_hub ? is_hub_on_top : false, - parent_pipe_trail: children_pipe_trail, - instance_states, - db - }) - ) - }) + for (let i = 0; i < entry.hubs.length; i++) { + const hub_path = entry.hubs[i] + const hub_view = await build_view_recursive({ + base_path: hub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.hubs.length - 1, + is_hub: true, + is_first_hub: is_hub ? is_hub_on_top : false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db + }) + current_view.push(...hub_view) + } } + // Calculate pipe_trail for this node + const { pipe_trail, is_hub_on_top: calculated_is_hub_on_top } = await calculate_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + is_hub_on_top, + parent_pipe_trail, + parent_base_path, + base_path, + db + }) + current_view.push({ base_path, instance_path, @@ -881,26 +925,29 @@ ESSAGE HANDLING is_hub, is_first_hub, parent_pipe_trail, - parent_base_path + parent_base_path, + entry, // Include entry data in view to avoid async lookups during rendering + pipe_trail, // Pre-calculated pipe trail + is_hub_on_top: calculated_is_hub_on_top // Pre-calculated hub position }) // If subs are expanded, recursively add them to the view (they appear below the node). if (state.expanded_subs && Array.isArray(entry.subs)) { - entry.subs.forEach((sub_path, i, arr) => { - current_view.push( - ...build_view_recursive({ - base_path: sub_path, - parent_instance_path: instance_path, - parent_base_path: base_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: false, - parent_pipe_trail: children_pipe_trail, - instance_states, - db - }) - ) - }) + for (let i = 0; i < entry.subs.length; i++) { + const sub_path = entry.subs[i] + const sub_view = await build_view_recursive({ + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.subs.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db + }) + current_view.push(...sub_view) + } } return current_view } @@ -917,15 +964,14 @@ ESSAGE HANDLING depth, is_last_sub, is_hub, - is_first_hub, - parent_pipe_trail, - parent_base_path, is_search_match, is_direct_match, is_in_original_view, - query + query, + entry, // Entry data is now passed from view + pipe_trail, // Pre-calculated pipe trail + is_hub_on_top // Pre-calculated hub position }) { - const entry = db.get(base_path) if (!entry) { const err_el = document.createElement('div') err_el.className = 'node error' @@ -946,17 +992,6 @@ ESSAGE HANDLING } const state = get_or_create_state(states, instance_path) - const { pipe_trail, is_hub_on_top } = calculate_pipe_trail({ - depth, - is_hub, - is_last_sub, - is_first_hub, - parent_pipe_trail, - parent_base_path, - base_path, - db - }) - const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path @@ -981,6 +1016,7 @@ ESSAGE HANDLING if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) + // Use pre-calculated pipe_trail const pipe_html = pipe_trail.map(p => ``).join('') const prefix_class = has_subs ? 'prefix clickable' : 'prefix' const icon_class = has_hubs && base_path !== '/' ? 'icon clickable' : 'icon' @@ -1202,13 +1238,13 @@ ESSAGE HANDLING requestAnimationFrame(() => search_input.focus()) } - function handle_mode_change () { + async function handle_mode_change () { menubar.style.display = mode === 'default' ? 'none' : 'flex' render_searchbar() - build_and_render_view() + await build_and_render_view() } - function toggle_search_mode () { + async function toggle_search_mode () { const target_mode = mode === 'search' ? previous_mode : 'search' console.log('[SEARCH DEBUG] Switching mode from', mode, 'to', target_mode) send_message({ type: 'mode_toggling', data: { from: mode, to: target_mode } }) @@ -1216,7 +1252,7 @@ ESSAGE HANDLING // When switching from search to default mode, expand selected entries if (selected_instance_paths.length > 0) { console.log('[SEARCH DEBUG] Expanding selected entries in default mode:', selected_instance_paths) - expand_selected_entries_in_default(selected_instance_paths) + await expand_selected_entries_in_default(selected_instance_paths) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) } @@ -1289,7 +1325,7 @@ ESSAGE HANDLING perform_search(search_query) } - function perform_search (query) { + async function perform_search (query) { console.log('[SEARCH DEBUG] perform_search called:', { query, current_mode: mode, @@ -1297,22 +1333,12 @@ ESSAGE HANDLING has_search_entry_states: Object.keys(search_entry_states).length > 0, last_clicked_node }) - - // Check if we are actualy in search mode - if (mode !== 'search') { - console.error('[SEARCH DEBUG] perform_search called but not in search mode!', { - current_mode: mode, - query - }) - return build_and_render_view() - } - if (!query) { console.log('[SEARCH DEBUG] No query provided, building default view') return build_and_render_view() } - const original_view = build_view_recursive({ + const original_view = await build_view_recursive({ base_path: '/', parent_instance_path: '', depth: 0, @@ -1325,7 +1351,7 @@ ESSAGE HANDLING const original_view_paths = original_view.map(n => n.instance_path) search_state_instances = {} const search_tracking = {} - const search_view = build_search_view_recursive({ + const search_view = await build_search_view_recursive({ query, base_path: '/', parent_instance_path: '', @@ -1344,7 +1370,7 @@ ESSAGE HANDLING render_search_results(search_view, query) } - function build_search_view_recursive ({ + async function build_search_view_recursive ({ query, base_path, parent_instance_path, @@ -1360,7 +1386,7 @@ ESSAGE HANDLING is_expanded_child = false, search_tracking = {} }) { - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return [] const instance_path = `${parent_instance_path}|${base_path}` @@ -1372,7 +1398,7 @@ ESSAGE HANDLING search_tracking[base_path].push(instance_path) // Use extracted pipe logic for consistent rendering - const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ + const { children_pipe_trail, is_hub_on_top } = await calculate_children_pipe_trail({ depth, is_hub, is_last_sub, @@ -1389,39 +1415,19 @@ ESSAGE HANDLING const should_expand_subs = search_state ? search_state.expanded_subs : false // Process hubs: if manually expanded, show ALL hubs regardless of search match - const hub_results = (should_expand_hubs ? (entry.hubs || []) : []).flatMap((hub_path, i, arr) => { - return build_search_view_recursive({ - query, - base_path: hub_path, - parent_instance_path: instance_path, - parent_base_path: base_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: true, - is_first_hub: is_hub_on_top, - parent_pipe_trail: children_pipe_trail, - instance_states, - db, - original_view_paths, - is_expanded_child: true, - search_tracking - }) - }) - - // Handle subs: if manually expanded, show ALL children; otherwise, search through them - let sub_results = [] - if (should_expand_subs) { - // Show ALL subs when manually expanded - sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => { - return build_search_view_recursive({ + const hub_results = [] + if (should_expand_hubs && entry.hubs) { + for (let i = 0; i < entry.hubs.length; i++) { + const hub_path = entry.hubs[i] + const hub_view = await build_search_view_recursive({ query, - base_path: sub_path, + base_path: hub_path, parent_instance_path: instance_path, parent_base_path: base_path, depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: false, - is_first_hub: false, + is_last_sub: i === entry.hubs.length - 1, + is_hub: true, + is_first_hub: is_hub_on_top, parent_pipe_trail: children_pipe_trail, instance_states, db, @@ -1429,27 +1435,60 @@ ESSAGE HANDLING is_expanded_child: true, search_tracking }) - }) + hub_results.push(...hub_view) + } + } + + // Handle subs: if manually expanded, show ALL children; otherwise, search through them + const sub_results = [] + if (should_expand_subs) { + // Show ALL subs when manually expanded + if (entry.subs) { + for (let i = 0; i < entry.subs.length; i++) { + const sub_path = entry.subs[i] + const sub_view = await build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.subs.length - 1, + is_hub: false, + is_first_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db, + original_view_paths, + is_expanded_child: true, + search_tracking + }) + sub_results.push(...sub_view) + } + } } else if (!is_expanded_child && is_first_occurrence_in_search) { // Only search through subs for the first occurrence of this base_path - sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => - build_search_view_recursive({ - query, - base_path: sub_path, - parent_instance_path: instance_path, - parent_base_path: base_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: false, - is_first_hub: false, - parent_pipe_trail: children_pipe_trail, - instance_states, - db, - original_view_paths, - is_expanded_child: false, - search_tracking - }) - ) + if (entry.subs) { + for (let i = 0; i < entry.subs.length; i++) { + const sub_path = entry.subs[i] + const sub_view = await build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.subs.length - 1, + is_hub: false, + is_first_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db, + original_view_paths, + is_expanded_child: false, + search_tracking + }) + sub_results.push(...sub_view) + } + } } const has_matching_descendant = sub_results.length > 0 @@ -1465,6 +1504,19 @@ ESSAGE HANDLING instance_states[instance_path] = { expanded_subs: final_expand_subs, expanded_hubs: final_expand_hubs } const is_in_original_view = original_view_paths.includes(instance_path) + // Calculate pipe_trail for this search node + const { pipe_trail, is_hub_on_top: calculated_is_hub_on_top } = await calculate_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + is_hub_on_top, + parent_pipe_trail, + parent_base_path, + base_path, + db + }) + const current_node_view = { base_path, instance_path, @@ -1476,7 +1528,10 @@ ESSAGE HANDLING parent_base_path, is_search_match: true, is_direct_match, - is_in_original_view + is_in_original_view, + entry, // Include entry data + pipe_trail, // Pre-calculated pipe trail + is_hub_on_top: calculated_is_hub_on_top // Pre-calculated hub position } return [...hub_results, current_node_view, ...sub_results] @@ -1573,7 +1628,7 @@ ESSAGE HANDLING } // Add the clicked entry and all its parents in the default tree - function expand_entry_path_in_default (target_instance_path) { + async function expand_entry_path_in_default (target_instance_path) { console.log('[SEARCH DEBUG] search_expand_into_default called:', { target_instance_path, current_mode: mode, @@ -1605,7 +1660,7 @@ ESSAGE HANDLING const child_base = parts[i + 1] const parent_instance_path = parts.slice(0, i + 1).map(p => '|' + p).join('') const parent_state = get_or_create_state(instance_states, parent_instance_path) - const parent_entry = db.get(parent_base) + const parent_entry = await db.get(parent_base) console.log('[SEARCH DEBUG] Processing parent-child relationship:', { parent_base, @@ -1627,7 +1682,7 @@ ESSAGE HANDLING } // expand multiple selected entry in the default tree - function expand_selected_entries_in_default (selected_paths) { + async function expand_selected_entries_in_default (selected_paths) { console.log('[SEARCH DEBUG] expand_selected_entries_in_default called:', { selected_paths, current_mode: mode, @@ -1641,19 +1696,21 @@ ESSAGE HANDLING } // expand foreach selected path - selected_paths.forEach(path => expand_entry_path_in_default(path)) + for (const path of selected_paths) { + await expand_entry_path_in_default(path) + } console.log('[SEARCH DEBUG] All selected entries expanded in default mode') } // Add the clicked entry and all its parents in the default tree - function search_expand_into_default (target_instance_path) { + async function search_expand_into_default (target_instance_path) { if (!target_instance_path) { return } handle_search_node_click(target_instance_path) - expand_entry_path_in_default(target_instance_path) + await expand_entry_path_in_default(target_instance_path) console.log('[SEARCH DEBUG] Current mode before switch:', mode) console.log('[SEARCH DEBUG] Target previous_mode:', previous_mode) @@ -1695,18 +1752,25 @@ ESSAGE HANDLING update_drive_state({ type: 'runtime/confirmed_selected', message: [...new_confirmed] }) } - function toggle_subs (instance_path) { + async function toggle_subs (instance_path) { const state = get_or_create_state(instance_states, instance_path) const was_expanded = state.expanded_subs state.expanded_subs = !state.expanded_subs // Update view order tracking for the toggled subs const base_path = instance_path.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) if (entry && Array.isArray(entry.subs)) { - if (was_expanded && recursive_collapse_flag === true) entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) - else entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, db)) + if (was_expanded && recursive_collapse_flag === true) { + for (const sub_path of entry.subs) { + await collapse_and_remove_instance(sub_path, instance_path, instance_states, db) + } + } else { + for (const sub_path of entry.subs) { + await toggle_subs_instance(sub_path, instance_path, instance_states, db) + } + } } last_clicked_node = instance_path @@ -1718,23 +1782,23 @@ ESSAGE HANDLING update_drive_state({ type: 'runtime/instance_states', message: instance_states }) send_message({ type: 'subs_toggled', data: { instance_path, expanded: state.expanded_subs } }) - function toggle_subs_instance (sub_path, instance_path, instance_states, db) { + async function toggle_subs_instance (sub_path, instance_path, instance_states, db) { if (was_expanded) { // Collapsing so - remove_instances_recursively(sub_path, instance_path, instance_states, db) + await remove_instances_recursively(sub_path, instance_path, instance_states, db) } else { // Expanding so - add_instances_recursively(sub_path, instance_path, instance_states, db) + await add_instances_recursively(sub_path, instance_path, instance_states, db) } } - function collapse_and_remove_instance (sub_path, instance_path, instance_states, db) { - collapse_subs_recursively(sub_path, instance_path, instance_states, db) - remove_instances_recursively(sub_path, instance_path, instance_states, db) + async function collapse_and_remove_instance (sub_path, instance_path, instance_states, db) { + await collapse_subs_recursively(sub_path, instance_path, instance_states, db) + await remove_instances_recursively(sub_path, instance_path, instance_states, db) } } - function toggle_hubs (instance_path) { + async function toggle_hubs (instance_path) { const state = get_or_create_state(instance_states, instance_path) const was_expanded = state.expanded_hubs state.expanded_hubs ? hub_num-- : hub_num++ @@ -1742,20 +1806,24 @@ ESSAGE HANDLING // Update view order tracking for the toggled hubs const base_path = instance_path.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) if (entry && Array.isArray(entry.hubs)) { if (was_expanded && recursive_collapse_flag === true) { // collapse all hub descendants - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance(hub_path, instance_path, instance_states, db) + } } else { // only toggle direct hubs - entry.hubs.forEach(hub_path => toggle_hubs_instance(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await toggle_hubs_instance(hub_path, instance_path, instance_states, db) + } } - function collapse_and_remove_instance (hub_path, instance_path, instance_states, db) { - collapse_hubs_recursively(hub_path, instance_path, instance_states, db) - remove_instances_recursively(hub_path, instance_path, instance_states, db) + async function collapse_and_remove_instance (hub_path, instance_path, instance_states, db) { + await collapse_hubs_recursively(hub_path, instance_path, instance_states, db) + await remove_instances_recursively(hub_path, instance_path, instance_states, db) } } @@ -1768,18 +1836,18 @@ ESSAGE HANDLING update_drive_state({ type: 'runtime/instance_states', message: instance_states }) send_message({ type: 'hubs_toggled', data: { instance_path, expanded: state.expanded_hubs } }) - function toggle_hubs_instance (hub_path, instance_path, instance_states, db) { + async function toggle_hubs_instance (hub_path, instance_path, instance_states, db) { if (was_expanded) { // Collapsing so - remove_instances_recursively(hub_path, instance_path, instance_states, db) + await remove_instances_recursively(hub_path, instance_path, instance_states, db) } else { // Expanding so - add_instances_recursively(hub_path, instance_path, instance_states, db) + await add_instances_recursively(hub_path, instance_path, instance_states, db) } } } - function toggle_search_subs (instance_path) { + async function toggle_search_subs (instance_path) { console.log('[SEARCH DEBUG] toggle_search_subs called:', { instance_path, mode, @@ -1794,7 +1862,7 @@ ESSAGE HANDLING if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, db)) } @@ -1815,7 +1883,7 @@ ESSAGE HANDLING update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) } - function toggle_search_hubs (instance_path) { + async function toggle_search_hubs (instance_path) { console.log('[SEARCH DEBUG] toggle_search_hubs called:', { instance_path, mode, @@ -1830,7 +1898,7 @@ ESSAGE HANDLING if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, db)) } @@ -2177,15 +2245,17 @@ ESSAGE HANDLING } } - function initialize_tracking_from_current_state () { + async function initialize_tracking_from_current_state () { const root_path = '/' const root_instance_path = '|/' - if (db.has(root_path)) { + if (await db.has(root_path)) { add_instance_to_view_tracking(root_path, root_instance_path) // Add initially expanded subs if any - const root_entry = db.get(root_path) + const root_entry = await db.get(root_path) if (root_entry && Array.isArray(root_entry.subs)) { - root_entry.subs.forEach(sub_path => add_instances_recursively(sub_path, root_instance_path, instance_states, db)) + for (const sub_path of root_entry.subs) { + await add_instances_recursively(sub_path, root_instance_path, instance_states, db) + } } } } @@ -2223,19 +2293,23 @@ ESSAGE HANDLING } // Recursively add instances to tracking when expanding - function add_instances_recursively (base_path, parent_instance_path, instance_states, db) { + async function add_instances_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_hubs && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => add_instances_recursively(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await add_instances_recursively(hub_path, instance_path, instance_states, db) + } } if (state.expanded_subs && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => add_instances_recursively(sub_path, instance_path, instance_states, db)) + for (const sub_path of entry.subs) { + await add_instances_recursively(sub_path, instance_path, instance_states, db) + } } // Add the instance itself @@ -2243,48 +2317,60 @@ ESSAGE HANDLING } // Recursively remove instances from tracking when collapsing - function remove_instances_recursively (base_path, parent_instance_path, instance_states, db) { + async function remove_instances_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) - if (state.expanded_hubs && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => remove_instances_recursively(hub_path, instance_path, instance_states, db)) - if (state.expanded_subs && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => remove_instances_recursively(sub_path, instance_path, instance_states, db)) + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + for (const hub_path of entry.hubs) { + await remove_instances_recursively(hub_path, instance_path, instance_states, db) + } + } + if (state.expanded_subs && Array.isArray(entry.subs)) { + for (const sub_path of entry.subs) { + await remove_instances_recursively(sub_path, instance_path, instance_states, db) + } + } // Remove the instance itself remove_instance_from_view_tracking(base_path, instance_path) } // Recursively hubs all subs in default mode - function collapse_subs_recursively (base_path, parent_instance_path, instance_states, db) { + async function collapse_subs_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) + for (const sub_path of entry.subs) { + await collapse_and_remove_instance(sub_path, instance_path, instance_states, db) + } } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) // Decrement hub counter - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance(hub_path, instance_path, instance_states, db) + } } - function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { - collapse_subs_recursively(base_path, instance_path, instance_states, db) - remove_instances_recursively(base_path, instance_path, instance_states, db) + async function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { + await collapse_subs_recursively(base_path, instance_path, instance_states, db) + await remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively hubs all hubs in default mode - function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, db) { + async function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) @@ -2292,98 +2378,118 @@ ESSAGE HANDLING if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance(hub_path, instance_path, instance_states, db) + } } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) + for (const sub_path of entry.subs) { + await collapse_and_remove_instance(sub_path, instance_path, instance_states, db) + } } - function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { - collapse_all_recursively(base_path, instance_path, instance_states, db) - remove_instances_recursively(base_path, instance_path, instance_states, db) + async function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { + await collapse_all_recursively(base_path, instance_path, instance_states, db) + await remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively collapse in default mode - function collapse_all_recursively (base_path, parent_instance_path, instance_states, db) { + async function collapse_all_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, db)) + for (const sub_path of entry.subs) { + await collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, db) + } } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, db) + } } - function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, db) { - collapse_all_recursively(base_path, instance_path, instance_states, db) - remove_instances_recursively(base_path, instance_path, instance_states, db) + async function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, db) { + await collapse_all_recursively(base_path, instance_path, instance_states, db) + await remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively subs all hubs in search mode - function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, db) { + async function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) + for (const sub_path of entry.subs) { + await collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db) + } } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) + for (const hub_path of entry.hubs) { + await collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db) + } } } // Recursively hubs all hubs in search mode - function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, db) { + async function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) + for (const hub_path of entry.hubs) { + await collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db) + } } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) + for (const sub_path of entry.subs) { + await collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db) + } } } // Recursively collapse in search mode - function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, db) { + async function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) + for (const sub_path of entry.subs) { + await collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db) + } } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) + for (const hub_path of entry.hubs) { + await collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db) + } } } @@ -2805,11 +2911,11 @@ ESSAGE HANDLING scroll_to_node(new_node.instance_path) } - function toggle_subs_for_current_node () { + async function toggle_subs_for_current_node () { if (!last_clicked_node) return const base_path = last_clicked_node.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) const has_subs = Array.isArray(entry?.subs) && entry.subs.length > 0 if (!has_subs) return @@ -2820,17 +2926,17 @@ ESSAGE HANDLING } if (mode === 'search' && search_query) { - toggle_search_subs(last_clicked_node) + await toggle_search_subs(last_clicked_node) } else { - toggle_subs(last_clicked_node) + await toggle_subs(last_clicked_node) } } - function toggle_hubs_for_current_node () { + async function toggle_hubs_for_current_node () { if (!last_clicked_node) return const base_path = last_clicked_node.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry?.hubs) && entry.hubs.length > 0 if (!has_hubs || base_path === '/') return @@ -2842,9 +2948,9 @@ ESSAGE HANDLING } if (mode === 'search' && search_query) { - toggle_search_hubs(last_clicked_node) + await toggle_search_hubs(last_clicked_node) } else { - toggle_hubs(last_clicked_node) + await toggle_hubs(last_clicked_node) } } @@ -2965,9 +3071,6 @@ function fallback_module () { function fallback_instance () { return { drive: { - 'entries/': { - 'entries.json': { $ref: 'entries.json' } - }, 'style/': { 'theme.css': { $ref: 'theme.css' diff --git a/lib/graphdb.js b/lib/graphdb.js index 5e9a37f..2d84f78 100644 --- a/lib/graphdb.js +++ b/lib/graphdb.js @@ -11,7 +11,7 @@ function graphdb (entries) { get, has, keys, - isEmpty, + is_empty, root, raw } @@ -29,7 +29,7 @@ function graphdb (entries) { return Object.keys(entries) } - function isEmpty () { + function is_empty () { return Object.keys(entries).length === 0 } diff --git a/lib/entries.json b/web/entries.json similarity index 100% rename from lib/entries.json rename to web/entries.json diff --git a/web/page.js b/web/page.js index beefb8e..f8b49a7 100644 --- a/web/page.js +++ b/web/page.js @@ -1,4 +1,5 @@ const STATE = require('../lib/STATE') +const graphdb = require('../lib/graphdb') const statedb = STATE(__filename) const admin_api = statedb.admin() admin_api.on(event => { @@ -36,11 +37,18 @@ async function boot (opts) { // ID + JSON STATE // ---------------------------------------- const on = { - theme: inject + theme: inject, + entries: on_entries } const { drive } = sdb - const subs = await sdb.watch(onbatch, on) + // Database instance for Graph Explorer + let db = null + // Send function for Graph Explorer protocol + let send_to_graph_explorer = null + + const subs = await sdb.watch(onbatch) + console.log(subs) // ---------------------------------------- // TEMPLATE @@ -53,12 +61,82 @@ async function boot (opts) { // ELEMENTS // ---------------------------------------- // desktop - shadow.append(await app(subs[0])) + shadow.append(await app(subs[0], graph_explorer_protocol)) // ---------------------------------------- // INIT // ---------------------------------------- + function graph_explorer_protocol (send) { + send_to_graph_explorer = send + return on_graph_explorer_message + + function on_graph_explorer_message ({ type, data }) { + if (type === 'db_init') { + db = graphdb(data) + // Send back confirmation with the entries + send({ type: 'db_initialized', data: { entries: data } }) + } else if (type === 'db_request') { + handle_db_request(data, send) + } + } + + function handle_db_request (data, send) { + const { id, operation, params } = data + let result + + if (!db) { + console.error('[page.js] Database not initialized yet') + send({ type: 'db_response', data: { id, result: null } }) + return + } + + if (operation === 'db_get') { + result = db.get(params.path) + } else if (operation === 'db_has') { + result = db.has(params.path) + } else if (operation === 'db_is_empty') { + result = db.is_empty() + } else if (operation === 'db_root') { + result = db.root() + } else if (operation === 'db_keys') { + result = db.keys() + } else if (operation === 'db_raw') { + result = db.raw() + } else { + console.warn('[page.js] Unknown db operation:', operation) + result = null + } + + send({ type: 'db_response', data: { id, result } }) + } + } + + function on_entries (data) { + if (!data || data[0] == null) { + console.error('Entries data is missing or empty.') + db = graphdb({}) + if (send_to_graph_explorer) { + send_to_graph_explorer({ type: 'db_initialized', data: { entries: {} } }) + } + return + } + const parsed_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed entries data is not a valid object.') + db = graphdb({}) + if (send_to_graph_explorer) { + send_to_graph_explorer({ type: 'db_initialized', data: { entries: {} } }) + } + return + } + db = graphdb(parsed_data) + if (send_to_graph_explorer) { + send_to_graph_explorer({ type: 'db_initialized', data: { entries: parsed_data } }) + } + } + async function onbatch (batch) { + console.log(batch) for (const { type, paths } of batch) { const data = await Promise.all( paths.map(path => drive.get(path).then(file => file.raw)) @@ -79,19 +157,19 @@ function fallback_module () { 0: '', mapping: { style: 'theme', - entries: 'entries', runtime: 'runtime', mode: 'mode', flags: 'flags', keybinds: 'keybinds', undo: 'undo' } - } + }, + '../lib/graphdb': 0 }, drive: { 'theme/': { 'style.css': { raw: "body { font-family: 'system-ui'; }" } }, + 'entries/': { 'entries.json': { $ref: 'entries.json' } }, 'lang/': {}, - 'entries/': {}, 'runtime/': {}, 'mode/': {}, 'flags/': {}, From 1492433753a6ee234a1295bd157a4e3f976c3d8f Mon Sep 17 00:00:00 2001 From: ddroid Date: Tue, 28 Oct 2025 20:12:55 +0500 Subject: [PATCH 125/130] Bundled --- bundle.js | 777 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 495 insertions(+), 282 deletions(-) diff --git a/bundle.js b/bundle.js index 5555acf..17d0f59 100644 --- a/bundle.js +++ b/bundle.js @@ -3,7 +3,6 @@ },{}],2:[function(require,module,exports){ (function (__filename){(function (){ const STATE = require('./STATE') -const graphdb = require('./graphdb') const statedb = STATE(__filename) const { get } = statedb(fallback_module) @@ -60,10 +59,14 @@ async function graph_explorer (opts, protocol) { // Protocol system for message-based communication let send = null + let graph_explorer_mid = 0 // Message ID counter for graph_explorer.js -> page.js messages if (protocol) { send = protocol(msg => onmessage(msg)) } + // Create db object that communicates via protocol messages + db = create_db() + const el = document.createElement('div') el.className = 'graph-explorer-wrapper' const shadow = el.attachShadow({ mode: 'closed' }) @@ -98,7 +101,6 @@ async function graph_explorer (opts, protocol) { // Define handlers for different data types from the drive, called by `onbatch`. const on = { - entries: on_entries, style: inject_style, runtime: on_runtime, mode: on_mode, @@ -114,11 +116,12 @@ async function graph_explorer (opts, protocol) { return el /****************************************************************************** -ESSAGE HANDLING + ESSAGE HANDLING - Handles incoming messages and sends outgoing messages. - - @TODO: define the messages we wanna to send inorder to receive some info. + - Messages follow standardized format: { head: [by, to, mid], refs, type, data } ******************************************************************************/ - function onmessage ({ type, data }) { + function onmessage (msg) { + const { type, data } = msg const on_message_types = { set_mode: handle_set_mode, set_search_query: handle_set_search_query, @@ -130,12 +133,18 @@ ESSAGE HANDLING get_confirmed: handle_get_confirmed, clear_selection: handle_clear_selection, set_flag: handle_set_flag, - scroll_to_node: handle_scroll_to_node + scroll_to_node: handle_scroll_to_node, + db_response: handle_db_response, + db_initialized: handle_db_initialized } const handler = on_message_types[type] if (handler) handler(data) - else console.warn(`[graph_explorer-protocol] Unknown message type: ${type}`, data) + else console.warn(`[graph_explorer-protocol] Unknown message type: ${type}`, msg) + + function handle_db_response () { + db.handle_response(msg) + } function handle_set_mode (data) { const { mode: new_mode } = data @@ -186,13 +195,13 @@ ESSAGE HANDLING } } - function handle_toggle_node (data) { + async function handle_toggle_node (data) { const { instance_path, toggle_type = 'subs' } = data if (instance_path && instance_states[instance_path]) { if (toggle_type === 'subs') { - toggle_subs(instance_path) + await toggle_subs(instance_path) } else if (toggle_type === 'hubs') { - toggle_hubs(instance_path) + await toggle_hubs(instance_path) } send_message({ type: 'node_toggled', data: { instance_path, toggle_type } }) } @@ -234,10 +243,80 @@ ESSAGE HANDLING } } } - - function send_message ({ type, data }) { + async function handle_db_initialized (data) { + // Page.js, trigger initial render + // After receiving entries, ensure the root node state is initialized and trigger the first render. + const root_path = '/' + if (await db.has(root_path)) { + const root_instance_path = '|/' + if (!instance_states[root_instance_path]) { + instance_states[root_instance_path] = { + expanded_subs: true, + expanded_hubs: false + } + } + // don't rebuild view if we're in search mode with active query + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_entries: skipping build_and_render_view in Search Mode with query:', search_query) + perform_search(search_query) + } else { + // tracking will be initialized later if drive data is empty + build_and_render_view() + } + } else { + console.warn('Root path "/" not found in entries. Clearing view.') + view = [] + if (container) container.replaceChildren() + } + } + function send_message (msg) { if (send) { - send({ type, data }) + send(msg) + } + } + + function create_db () { + // Pending requests map: key is message head [by, to, mid], value is {resolve, reject} + const pending_requests = new Map() + + return { + // All operations are async via protocol messages + get: (path) => send_db_request('db_get', { path }), + has: (path) => send_db_request('db_has', { path }), + is_empty: () => send_db_request('db_is_empty', {}), + root: () => send_db_request('db_root', {}), + keys: () => send_db_request('db_keys', {}), + raw: () => send_db_request('db_raw', {}), + // Handle responses from page.js + handle_response: (msg) => { + if (!msg.refs || !msg.refs.cause) { + console.warn('[graph_explorer] Response missing refs.cause:', msg) + return + } + const request_head_key = JSON.stringify(msg.refs.cause) + const pending = pending_requests.get(request_head_key) + if (pending) { + pending.resolve(msg.data.result) + pending_requests.delete(request_head_key) + } else { + console.warn('[graph_explorer] No pending request for response:', msg.refs.cause) + } + } + } + + function send_db_request (operation, params) { + return new Promise((resolve, reject) => { + const head = ['graph_explorer', 'page_js', graph_explorer_mid++] + const head_key = JSON.stringify(head) + pending_requests.set(head_key, { resolve, reject }) + + send_message({ + head, + refs: null, // New request has no references + type: operation, + data: params + }) + }) } } @@ -273,7 +352,7 @@ ESSAGE HANDLING ) // Call the appropriate handler based on `type`. const func = on[type] - func ? func({ data, paths }) : fail(data, type) + func ? await func({ data, paths }) : fail(data, type) } function batch_get (path) { @@ -291,46 +370,7 @@ ESSAGE HANDLING throw new Error(`Invalid message type: ${type}`, { cause: { data, type } }) } - function on_entries ({ data }) { - if (!data || data[0] == null) { - console.error('Entries data is missing or empty.') - db = graphdb({}) - return - } - const parsed_data = parse_json_data(data[0], 'entries.json') - if (typeof parsed_data !== 'object' || !parsed_data) { - console.error('Parsed entries data is not a valid object.') - db = graphdb({}) - return - } - db = graphdb(parsed_data) - - // After receiving entries, ensure the root node state is initialized and trigger the first render. - const root_path = '/' - if (db.has(root_path)) { - const root_instance_path = '|/' - if (!instance_states[root_instance_path]) { - instance_states[root_instance_path] = { - expanded_subs: true, - expanded_hubs: false - } - } - // don't rebuild view if we're in search mode with active query - if (mode === 'search' && search_query) { - console.log('[SEARCH DEBUG] on_entries: skipping build_and_render_view in Search Mode with query:', search_query) - perform_search(search_query) - } else { - // tracking will be initialized later if drive data is empty - build_and_render_view() - } - } else { - console.warn('Root path "/" not found in entries. Clearing view.') - view = [] - if (container) container.replaceChildren() - } - } - - function on_runtime ({ data, paths }) { + async function on_runtime ({ data, paths }) { const on_runtime_paths = { 'node_height.json': handle_node_height, 'vertical_scroll_value.json': handle_vertical_scroll, @@ -350,9 +390,9 @@ ESSAGE HANDLING if (needs_render) { if (mode === 'search' && search_query) { console.log('[SEARCH DEBUG] on_runtime: Skipping build_and_render_view in search mode with query:', search_query) - perform_search(search_query) + await perform_search(search_query) } else { - build_and_render_view() + await build_and_render_view() } } else if (render_nodes_needed.size > 0) { render_nodes_needed.forEach(re_render_node) @@ -442,7 +482,7 @@ ESSAGE HANDLING } } - function on_mode ({ data, paths }) { + async function on_mode ({ data, paths }) { const on_mode_paths = { 'current_mode.json': handle_current_mode, 'previous_mode.json': handle_previous_mode, @@ -483,8 +523,8 @@ ESSAGE HANDLING mode = new_current_mode render_menubar() render_searchbar() - handle_mode_change() - if (mode === 'search' && search_query) perform_search(search_query) + await handle_mode_change() + if (mode === 'search' && search_query) await perform_search(search_query) function mode_handler (path, data) { const value = parse_json_data(data, path) @@ -662,7 +702,7 @@ ESSAGE HANDLING return states[instance_path] } - function calculate_children_pipe_trail ({ + async function calculate_children_pipe_trail ({ depth, is_hub, is_last_sub, @@ -673,7 +713,7 @@ ESSAGE HANDLING db }) { const children_pipe_trail = [...parent_pipe_trail] - const parent_entry = db.get(parent_base_path) + const parent_entry = await db.get(parent_base_path) const is_hub_on_top = base_path === parent_entry?.hubs?.[0] || base_path === '/' if (depth > 0) { @@ -697,7 +737,7 @@ ESSAGE HANDLING } // Extracted pipe logic for reuse in both default and search modes - function calculate_pipe_trail ({ + async function calculate_pipe_trail ({ depth, is_hub, is_last_sub, @@ -709,7 +749,7 @@ ESSAGE HANDLING db }) { let last_pipe = null - const parent_entry = db.get(parent_base_path) + const parent_entry = await db.get(parent_base_path) const calculated_is_hub_on_top = base_path === parent_entry?.hubs?.[0] || base_path === '/' const final_is_hub_on_top = is_hub_on_top !== undefined ? is_hub_on_top : calculated_is_hub_on_top @@ -743,7 +783,7 @@ ESSAGE HANDLING - `build_view_recursive` creates the flat `view` array from the hierarchical data. - `calculate_mobile_scale` calculates the scale factor for mobile devices. ******************************************************************************/ - function build_and_render_view (focal_instance_path, hub_toggle = false) { + async function build_and_render_view (focal_instance_path, hub_toggle = false) { console.log('[SEARCH DEBUG] build_and_render_view called:', { focal_instance_path, hub_toggle, @@ -763,7 +803,8 @@ ESSAGE HANDLING }) } - if (!db || db.isEmpty()) { + const is_empty = await db.is_empty() + if (!db || is_empty) { console.warn('No entries available to render.') return } @@ -775,7 +816,7 @@ ESSAGE HANDLING if (spacer_element && spacer_element.parentNode) existing_spacer_height = parseFloat(spacer_element.style.height) || 0 // Recursively build the new `view` array from the graph data. - view = build_view_recursive({ + view = await build_view_recursive({ base_path: '/', parent_instance_path: '', depth: 0, @@ -827,7 +868,7 @@ ESSAGE HANDLING } // Traverses the hierarchical entries data and builds a flat `view` array for rendering. - function build_view_recursive ({ + async function build_view_recursive ({ base_path, parent_instance_path, parent_base_path = null, @@ -840,12 +881,12 @@ ESSAGE HANDLING db }) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return [] const state = get_or_create_state(instance_states, instance_path) - const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ + const { children_pipe_trail, is_hub_on_top } = await calculate_children_pipe_trail({ depth, is_hub, is_last_sub, @@ -859,24 +900,37 @@ ESSAGE HANDLING const current_view = [] // If hubs are expanded, recursively add them to the view first (they appear above the node). if (state.expanded_hubs && Array.isArray(entry.hubs)) { - entry.hubs.forEach((hub_path, i, arr) => { - current_view.push( - ...build_view_recursive({ - base_path: hub_path, - parent_instance_path: instance_path, - parent_base_path: base_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: true, - is_first_hub: is_hub ? is_hub_on_top : false, - parent_pipe_trail: children_pipe_trail, - instance_states, - db - }) - ) - }) + for (let i = 0; i < entry.hubs.length; i++) { + const hub_path = entry.hubs[i] + const hub_view = await build_view_recursive({ + base_path: hub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.hubs.length - 1, + is_hub: true, + is_first_hub: is_hub ? is_hub_on_top : false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db + }) + current_view.push(...hub_view) + } } + // Calculate pipe_trail for this node + const { pipe_trail, is_hub_on_top: calculated_is_hub_on_top } = await calculate_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + is_hub_on_top, + parent_pipe_trail, + parent_base_path, + base_path, + db + }) + current_view.push({ base_path, instance_path, @@ -885,26 +939,29 @@ ESSAGE HANDLING is_hub, is_first_hub, parent_pipe_trail, - parent_base_path + parent_base_path, + entry, // Include entry data in view to avoid async lookups during rendering + pipe_trail, // Pre-calculated pipe trail + is_hub_on_top: calculated_is_hub_on_top // Pre-calculated hub position }) // If subs are expanded, recursively add them to the view (they appear below the node). if (state.expanded_subs && Array.isArray(entry.subs)) { - entry.subs.forEach((sub_path, i, arr) => { - current_view.push( - ...build_view_recursive({ - base_path: sub_path, - parent_instance_path: instance_path, - parent_base_path: base_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: false, - parent_pipe_trail: children_pipe_trail, - instance_states, - db - }) - ) - }) + for (let i = 0; i < entry.subs.length; i++) { + const sub_path = entry.subs[i] + const sub_view = await build_view_recursive({ + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.subs.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db + }) + current_view.push(...sub_view) + } } return current_view } @@ -921,15 +978,14 @@ ESSAGE HANDLING depth, is_last_sub, is_hub, - is_first_hub, - parent_pipe_trail, - parent_base_path, is_search_match, is_direct_match, is_in_original_view, - query + query, + entry, // Entry data is now passed from view + pipe_trail, // Pre-calculated pipe trail + is_hub_on_top // Pre-calculated hub position }) { - const entry = db.get(base_path) if (!entry) { const err_el = document.createElement('div') err_el.className = 'node error' @@ -950,17 +1006,6 @@ ESSAGE HANDLING } const state = get_or_create_state(states, instance_path) - const { pipe_trail, is_hub_on_top } = calculate_pipe_trail({ - depth, - is_hub, - is_last_sub, - is_first_hub, - parent_pipe_trail, - parent_base_path, - base_path, - db - }) - const el = document.createElement('div') el.className = `node type-${entry.type || 'unknown'}` el.dataset.instance_path = instance_path @@ -985,6 +1030,7 @@ ESSAGE HANDLING if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) + // Use pre-calculated pipe_trail const pipe_html = pipe_trail.map(p => ``).join('') const prefix_class = has_subs ? 'prefix clickable' : 'prefix' const icon_class = has_hubs && base_path !== '/' ? 'icon clickable' : 'icon' @@ -1206,13 +1252,13 @@ ESSAGE HANDLING requestAnimationFrame(() => search_input.focus()) } - function handle_mode_change () { + async function handle_mode_change () { menubar.style.display = mode === 'default' ? 'none' : 'flex' render_searchbar() - build_and_render_view() + await build_and_render_view() } - function toggle_search_mode () { + async function toggle_search_mode () { const target_mode = mode === 'search' ? previous_mode : 'search' console.log('[SEARCH DEBUG] Switching mode from', mode, 'to', target_mode) send_message({ type: 'mode_toggling', data: { from: mode, to: target_mode } }) @@ -1220,7 +1266,7 @@ ESSAGE HANDLING // When switching from search to default mode, expand selected entries if (selected_instance_paths.length > 0) { console.log('[SEARCH DEBUG] Expanding selected entries in default mode:', selected_instance_paths) - expand_selected_entries_in_default(selected_instance_paths) + await expand_selected_entries_in_default(selected_instance_paths) drive_updated_by_toggle = true update_drive_state({ type: 'runtime/instance_states', message: instance_states }) } @@ -1293,7 +1339,7 @@ ESSAGE HANDLING perform_search(search_query) } - function perform_search (query) { + async function perform_search (query) { console.log('[SEARCH DEBUG] perform_search called:', { query, current_mode: mode, @@ -1301,22 +1347,12 @@ ESSAGE HANDLING has_search_entry_states: Object.keys(search_entry_states).length > 0, last_clicked_node }) - - // Check if we are actualy in search mode - if (mode !== 'search') { - console.error('[SEARCH DEBUG] perform_search called but not in search mode!', { - current_mode: mode, - query - }) - return build_and_render_view() - } - if (!query) { console.log('[SEARCH DEBUG] No query provided, building default view') return build_and_render_view() } - const original_view = build_view_recursive({ + const original_view = await build_view_recursive({ base_path: '/', parent_instance_path: '', depth: 0, @@ -1329,7 +1365,7 @@ ESSAGE HANDLING const original_view_paths = original_view.map(n => n.instance_path) search_state_instances = {} const search_tracking = {} - const search_view = build_search_view_recursive({ + const search_view = await build_search_view_recursive({ query, base_path: '/', parent_instance_path: '', @@ -1348,7 +1384,7 @@ ESSAGE HANDLING render_search_results(search_view, query) } - function build_search_view_recursive ({ + async function build_search_view_recursive ({ query, base_path, parent_instance_path, @@ -1364,7 +1400,7 @@ ESSAGE HANDLING is_expanded_child = false, search_tracking = {} }) { - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return [] const instance_path = `${parent_instance_path}|${base_path}` @@ -1376,7 +1412,7 @@ ESSAGE HANDLING search_tracking[base_path].push(instance_path) // Use extracted pipe logic for consistent rendering - const { children_pipe_trail, is_hub_on_top } = calculate_children_pipe_trail({ + const { children_pipe_trail, is_hub_on_top } = await calculate_children_pipe_trail({ depth, is_hub, is_last_sub, @@ -1393,39 +1429,19 @@ ESSAGE HANDLING const should_expand_subs = search_state ? search_state.expanded_subs : false // Process hubs: if manually expanded, show ALL hubs regardless of search match - const hub_results = (should_expand_hubs ? (entry.hubs || []) : []).flatMap((hub_path, i, arr) => { - return build_search_view_recursive({ - query, - base_path: hub_path, - parent_instance_path: instance_path, - parent_base_path: base_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: true, - is_first_hub: is_hub_on_top, - parent_pipe_trail: children_pipe_trail, - instance_states, - db, - original_view_paths, - is_expanded_child: true, - search_tracking - }) - }) - - // Handle subs: if manually expanded, show ALL children; otherwise, search through them - let sub_results = [] - if (should_expand_subs) { - // Show ALL subs when manually expanded - sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => { - return build_search_view_recursive({ + const hub_results = [] + if (should_expand_hubs && entry.hubs) { + for (let i = 0; i < entry.hubs.length; i++) { + const hub_path = entry.hubs[i] + const hub_view = await build_search_view_recursive({ query, - base_path: sub_path, + base_path: hub_path, parent_instance_path: instance_path, parent_base_path: base_path, depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: false, - is_first_hub: false, + is_last_sub: i === entry.hubs.length - 1, + is_hub: true, + is_first_hub: is_hub_on_top, parent_pipe_trail: children_pipe_trail, instance_states, db, @@ -1433,27 +1449,60 @@ ESSAGE HANDLING is_expanded_child: true, search_tracking }) - }) + hub_results.push(...hub_view) + } + } + + // Handle subs: if manually expanded, show ALL children; otherwise, search through them + const sub_results = [] + if (should_expand_subs) { + // Show ALL subs when manually expanded + if (entry.subs) { + for (let i = 0; i < entry.subs.length; i++) { + const sub_path = entry.subs[i] + const sub_view = await build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.subs.length - 1, + is_hub: false, + is_first_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db, + original_view_paths, + is_expanded_child: true, + search_tracking + }) + sub_results.push(...sub_view) + } + } } else if (!is_expanded_child && is_first_occurrence_in_search) { // Only search through subs for the first occurrence of this base_path - sub_results = (entry.subs || []).flatMap((sub_path, i, arr) => - build_search_view_recursive({ - query, - base_path: sub_path, - parent_instance_path: instance_path, - parent_base_path: base_path, - depth: depth + 1, - is_last_sub: i === arr.length - 1, - is_hub: false, - is_first_hub: false, - parent_pipe_trail: children_pipe_trail, - instance_states, - db, - original_view_paths, - is_expanded_child: false, - search_tracking - }) - ) + if (entry.subs) { + for (let i = 0; i < entry.subs.length; i++) { + const sub_path = entry.subs[i] + const sub_view = await build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.subs.length - 1, + is_hub: false, + is_first_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db, + original_view_paths, + is_expanded_child: false, + search_tracking + }) + sub_results.push(...sub_view) + } + } } const has_matching_descendant = sub_results.length > 0 @@ -1469,6 +1518,19 @@ ESSAGE HANDLING instance_states[instance_path] = { expanded_subs: final_expand_subs, expanded_hubs: final_expand_hubs } const is_in_original_view = original_view_paths.includes(instance_path) + // Calculate pipe_trail for this search node + const { pipe_trail, is_hub_on_top: calculated_is_hub_on_top } = await calculate_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + is_hub_on_top, + parent_pipe_trail, + parent_base_path, + base_path, + db + }) + const current_node_view = { base_path, instance_path, @@ -1480,7 +1542,10 @@ ESSAGE HANDLING parent_base_path, is_search_match: true, is_direct_match, - is_in_original_view + is_in_original_view, + entry, // Include entry data + pipe_trail, // Pre-calculated pipe trail + is_hub_on_top: calculated_is_hub_on_top // Pre-calculated hub position } return [...hub_results, current_node_view, ...sub_results] @@ -1577,7 +1642,7 @@ ESSAGE HANDLING } // Add the clicked entry and all its parents in the default tree - function expand_entry_path_in_default (target_instance_path) { + async function expand_entry_path_in_default (target_instance_path) { console.log('[SEARCH DEBUG] search_expand_into_default called:', { target_instance_path, current_mode: mode, @@ -1609,7 +1674,7 @@ ESSAGE HANDLING const child_base = parts[i + 1] const parent_instance_path = parts.slice(0, i + 1).map(p => '|' + p).join('') const parent_state = get_or_create_state(instance_states, parent_instance_path) - const parent_entry = db.get(parent_base) + const parent_entry = await db.get(parent_base) console.log('[SEARCH DEBUG] Processing parent-child relationship:', { parent_base, @@ -1631,7 +1696,7 @@ ESSAGE HANDLING } // expand multiple selected entry in the default tree - function expand_selected_entries_in_default (selected_paths) { + async function expand_selected_entries_in_default (selected_paths) { console.log('[SEARCH DEBUG] expand_selected_entries_in_default called:', { selected_paths, current_mode: mode, @@ -1645,19 +1710,21 @@ ESSAGE HANDLING } // expand foreach selected path - selected_paths.forEach(path => expand_entry_path_in_default(path)) + for (const path of selected_paths) { + await expand_entry_path_in_default(path) + } console.log('[SEARCH DEBUG] All selected entries expanded in default mode') } // Add the clicked entry and all its parents in the default tree - function search_expand_into_default (target_instance_path) { + async function search_expand_into_default (target_instance_path) { if (!target_instance_path) { return } handle_search_node_click(target_instance_path) - expand_entry_path_in_default(target_instance_path) + await expand_entry_path_in_default(target_instance_path) console.log('[SEARCH DEBUG] Current mode before switch:', mode) console.log('[SEARCH DEBUG] Target previous_mode:', previous_mode) @@ -1699,18 +1766,25 @@ ESSAGE HANDLING update_drive_state({ type: 'runtime/confirmed_selected', message: [...new_confirmed] }) } - function toggle_subs (instance_path) { + async function toggle_subs (instance_path) { const state = get_or_create_state(instance_states, instance_path) const was_expanded = state.expanded_subs state.expanded_subs = !state.expanded_subs // Update view order tracking for the toggled subs const base_path = instance_path.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) if (entry && Array.isArray(entry.subs)) { - if (was_expanded && recursive_collapse_flag === true) entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) - else entry.subs.forEach(sub_path => toggle_subs_instance(sub_path, instance_path, instance_states, db)) + if (was_expanded && recursive_collapse_flag === true) { + for (const sub_path of entry.subs) { + await collapse_and_remove_instance(sub_path, instance_path, instance_states, db) + } + } else { + for (const sub_path of entry.subs) { + await toggle_subs_instance(sub_path, instance_path, instance_states, db) + } + } } last_clicked_node = instance_path @@ -1722,23 +1796,23 @@ ESSAGE HANDLING update_drive_state({ type: 'runtime/instance_states', message: instance_states }) send_message({ type: 'subs_toggled', data: { instance_path, expanded: state.expanded_subs } }) - function toggle_subs_instance (sub_path, instance_path, instance_states, db) { + async function toggle_subs_instance (sub_path, instance_path, instance_states, db) { if (was_expanded) { // Collapsing so - remove_instances_recursively(sub_path, instance_path, instance_states, db) + await remove_instances_recursively(sub_path, instance_path, instance_states, db) } else { // Expanding so - add_instances_recursively(sub_path, instance_path, instance_states, db) + await add_instances_recursively(sub_path, instance_path, instance_states, db) } } - function collapse_and_remove_instance (sub_path, instance_path, instance_states, db) { - collapse_subs_recursively(sub_path, instance_path, instance_states, db) - remove_instances_recursively(sub_path, instance_path, instance_states, db) + async function collapse_and_remove_instance (sub_path, instance_path, instance_states, db) { + await collapse_subs_recursively(sub_path, instance_path, instance_states, db) + await remove_instances_recursively(sub_path, instance_path, instance_states, db) } } - function toggle_hubs (instance_path) { + async function toggle_hubs (instance_path) { const state = get_or_create_state(instance_states, instance_path) const was_expanded = state.expanded_hubs state.expanded_hubs ? hub_num-- : hub_num++ @@ -1746,20 +1820,24 @@ ESSAGE HANDLING // Update view order tracking for the toggled hubs const base_path = instance_path.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) if (entry && Array.isArray(entry.hubs)) { if (was_expanded && recursive_collapse_flag === true) { // collapse all hub descendants - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance(hub_path, instance_path, instance_states, db) + } } else { // only toggle direct hubs - entry.hubs.forEach(hub_path => toggle_hubs_instance(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await toggle_hubs_instance(hub_path, instance_path, instance_states, db) + } } - function collapse_and_remove_instance (hub_path, instance_path, instance_states, db) { - collapse_hubs_recursively(hub_path, instance_path, instance_states, db) - remove_instances_recursively(hub_path, instance_path, instance_states, db) + async function collapse_and_remove_instance (hub_path, instance_path, instance_states, db) { + await collapse_hubs_recursively(hub_path, instance_path, instance_states, db) + await remove_instances_recursively(hub_path, instance_path, instance_states, db) } } @@ -1772,18 +1850,18 @@ ESSAGE HANDLING update_drive_state({ type: 'runtime/instance_states', message: instance_states }) send_message({ type: 'hubs_toggled', data: { instance_path, expanded: state.expanded_hubs } }) - function toggle_hubs_instance (hub_path, instance_path, instance_states, db) { + async function toggle_hubs_instance (hub_path, instance_path, instance_states, db) { if (was_expanded) { // Collapsing so - remove_instances_recursively(hub_path, instance_path, instance_states, db) + await remove_instances_recursively(hub_path, instance_path, instance_states, db) } else { // Expanding so - add_instances_recursively(hub_path, instance_path, instance_states, db) + await add_instances_recursively(hub_path, instance_path, instance_states, db) } } } - function toggle_search_subs (instance_path) { + async function toggle_search_subs (instance_path) { console.log('[SEARCH DEBUG] toggle_search_subs called:', { instance_path, mode, @@ -1798,7 +1876,7 @@ ESSAGE HANDLING if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, db)) } @@ -1819,7 +1897,7 @@ ESSAGE HANDLING update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) } - function toggle_search_hubs (instance_path) { + async function toggle_search_hubs (instance_path) { console.log('[SEARCH DEBUG] toggle_search_hubs called:', { instance_path, mode, @@ -1834,7 +1912,7 @@ ESSAGE HANDLING if (old_expanded && recursive_collapse_flag === true) { const base_path = instance_path.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, db)) } @@ -2181,15 +2259,17 @@ ESSAGE HANDLING } } - function initialize_tracking_from_current_state () { + async function initialize_tracking_from_current_state () { const root_path = '/' const root_instance_path = '|/' - if (db.has(root_path)) { + if (await db.has(root_path)) { add_instance_to_view_tracking(root_path, root_instance_path) // Add initially expanded subs if any - const root_entry = db.get(root_path) + const root_entry = await db.get(root_path) if (root_entry && Array.isArray(root_entry.subs)) { - root_entry.subs.forEach(sub_path => add_instances_recursively(sub_path, root_instance_path, instance_states, db)) + for (const sub_path of root_entry.subs) { + await add_instances_recursively(sub_path, root_instance_path, instance_states, db) + } } } } @@ -2227,19 +2307,23 @@ ESSAGE HANDLING } // Recursively add instances to tracking when expanding - function add_instances_recursively (base_path, parent_instance_path, instance_states, db) { + async function add_instances_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_hubs && Array.isArray(entry.hubs)) { - entry.hubs.forEach(hub_path => add_instances_recursively(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await add_instances_recursively(hub_path, instance_path, instance_states, db) + } } if (state.expanded_subs && Array.isArray(entry.subs)) { - entry.subs.forEach(sub_path => add_instances_recursively(sub_path, instance_path, instance_states, db)) + for (const sub_path of entry.subs) { + await add_instances_recursively(sub_path, instance_path, instance_states, db) + } } // Add the instance itself @@ -2247,48 +2331,60 @@ ESSAGE HANDLING } // Recursively remove instances from tracking when collapsing - function remove_instances_recursively (base_path, parent_instance_path, instance_states, db) { + async function remove_instances_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) - if (state.expanded_hubs && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => remove_instances_recursively(hub_path, instance_path, instance_states, db)) - if (state.expanded_subs && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => remove_instances_recursively(sub_path, instance_path, instance_states, db)) + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + for (const hub_path of entry.hubs) { + await remove_instances_recursively(hub_path, instance_path, instance_states, db) + } + } + if (state.expanded_subs && Array.isArray(entry.subs)) { + for (const sub_path of entry.subs) { + await remove_instances_recursively(sub_path, instance_path, instance_states, db) + } + } // Remove the instance itself remove_instance_from_view_tracking(base_path, instance_path) } // Recursively hubs all subs in default mode - function collapse_subs_recursively (base_path, parent_instance_path, instance_states, db) { + async function collapse_subs_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) + for (const sub_path of entry.subs) { + await collapse_and_remove_instance(sub_path, instance_path, instance_states, db) + } } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) // Decrement hub counter - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance(hub_path, instance_path, instance_states, db) + } } - function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { - collapse_subs_recursively(base_path, instance_path, instance_states, db) - remove_instances_recursively(base_path, instance_path, instance_states, db) + async function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { + await collapse_subs_recursively(base_path, instance_path, instance_states, db) + await remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively hubs all hubs in default mode - function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, db) { + async function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) @@ -2296,98 +2392,118 @@ ESSAGE HANDLING if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => collapse_and_remove_instance(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance(hub_path, instance_path, instance_states, db) + } } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance(sub_path, instance_path, instance_states, db)) + for (const sub_path of entry.subs) { + await collapse_and_remove_instance(sub_path, instance_path, instance_states, db) + } } - function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { - collapse_all_recursively(base_path, instance_path, instance_states, db) - remove_instances_recursively(base_path, instance_path, instance_states, db) + async function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { + await collapse_all_recursively(base_path, instance_path, instance_states, db) + await remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively collapse in default mode - function collapse_all_recursively (base_path, parent_instance_path, instance_states, db) { + async function collapse_all_recursively (base_path, parent_instance_path, instance_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(instance_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, db)) + for (const sub_path of entry.subs) { + await collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, db) + } } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false hub_num = Math.max(0, hub_num - 1) - entry.hubs.forEach(hub_path => collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, db)) + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, db) + } } - function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, db) { - collapse_all_recursively(base_path, instance_path, instance_states, db) - remove_instances_recursively(base_path, instance_path, instance_states, db) + async function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, db) { + await collapse_all_recursively(base_path, instance_path, instance_states, db) + await remove_instances_recursively(base_path, instance_path, instance_states, db) } } // Recursively subs all hubs in search mode - function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, db) { + async function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) + for (const sub_path of entry.subs) { + await collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db) + } } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) + for (const hub_path of entry.hubs) { + await collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db) + } } } // Recursively hubs all hubs in search mode - function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, db) { + async function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) + for (const hub_path of entry.hubs) { + await collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db) + } } if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) + for (const sub_path of entry.subs) { + await collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db) + } } } // Recursively collapse in search mode - function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, db) { + async function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, db) { const instance_path = `${parent_instance_path}|${base_path}` - const entry = db.get(base_path) + const entry = await db.get(base_path) if (!entry) return const state = get_or_create_state(search_entry_states, instance_path) if (state.expanded_subs && Array.isArray(entry.subs)) { state.expanded_subs = false - entry.subs.forEach(sub_path => collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db)) + for (const sub_path of entry.subs) { + await collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db) + } } if (state.expanded_hubs && Array.isArray(entry.hubs)) { state.expanded_hubs = false - entry.hubs.forEach(hub_path => collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db)) + for (const hub_path of entry.hubs) { + await collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db) + } } } @@ -2809,11 +2925,11 @@ ESSAGE HANDLING scroll_to_node(new_node.instance_path) } - function toggle_subs_for_current_node () { + async function toggle_subs_for_current_node () { if (!last_clicked_node) return const base_path = last_clicked_node.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) const has_subs = Array.isArray(entry?.subs) && entry.subs.length > 0 if (!has_subs) return @@ -2824,17 +2940,17 @@ ESSAGE HANDLING } if (mode === 'search' && search_query) { - toggle_search_subs(last_clicked_node) + await toggle_search_subs(last_clicked_node) } else { - toggle_subs(last_clicked_node) + await toggle_subs(last_clicked_node) } } - function toggle_hubs_for_current_node () { + async function toggle_hubs_for_current_node () { if (!last_clicked_node) return const base_path = last_clicked_node.split('|').pop() - const entry = db.get(base_path) + const entry = await db.get(base_path) const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry?.hubs) && entry.hubs.length > 0 if (!has_hubs || base_path === '/') return @@ -2846,9 +2962,9 @@ ESSAGE HANDLING } if (mode === 'search' && search_query) { - toggle_search_hubs(last_clicked_node) + await toggle_search_hubs(last_clicked_node) } else { - toggle_hubs(last_clicked_node) + await toggle_hubs(last_clicked_node) } } @@ -2969,9 +3085,6 @@ function fallback_module () { function fallback_instance () { return { drive: { - 'entries/': { - 'entries.json': { $ref: 'entries.json' } - }, 'style/': { 'theme.css': { $ref: 'theme.css' @@ -3023,7 +3136,7 @@ function fallback_module () { } }).call(this)}).call(this,"/lib/graph_explorer.js") -},{"./STATE":1,"./graphdb":3}],3:[function(require,module,exports){ +},{"./STATE":1}],3:[function(require,module,exports){ module.exports = graphdb function graphdb (entries) { @@ -3037,7 +3150,7 @@ function graphdb (entries) { get, has, keys, - isEmpty, + is_empty, root, raw } @@ -3055,7 +3168,7 @@ function graphdb (entries) { return Object.keys(entries) } - function isEmpty () { + function is_empty () { return Object.keys(entries).length === 0 } @@ -3094,6 +3207,7 @@ fetch(init_url, fetch_opts) },{"./page":5}],5:[function(require,module,exports){ (function (__filename){(function (){ const STATE = require('../lib/STATE') +const graphdb = require('../lib/graphdb') const statedb = STATE(__filename) const admin_api = statedb.admin() admin_api.on(event => { @@ -3131,11 +3245,24 @@ async function boot (opts) { // ID + JSON STATE // ---------------------------------------- const on = { - theme: inject + theme: inject, + entries: on_entries } const { drive } = sdb - const subs = await sdb.watch(onbatch, on) + // Database instance for Graph Explorer + let db = null + // Send function for Graph Explorer protocol + let send_to_graph_explorer = null + // Message ID counter for page_js -> graph_explorer messages + let page_js_mid = 0 + + // Permissions structure (placeholder) + // Example: perms = { graph_explorer: { deny_list: ['db_raw'] } } + // const perms = {} + + const subs = await sdb.watch(onbatch) + console.log(subs) // ---------------------------------------- // TEMPLATE @@ -3148,12 +3275,98 @@ async function boot (opts) { // ELEMENTS // ---------------------------------------- // desktop - shadow.append(await app(subs[0])) + shadow.append(await app(subs[0], graph_explorer_protocol)) // ---------------------------------------- // INIT // ---------------------------------------- + function graph_explorer_protocol (send) { + send_to_graph_explorer = send + return on_graph_explorer_message + + function on_graph_explorer_message (msg) { + const { type } = msg + + if (type.startsWith('db_')) { + handle_db_request(msg, send) + } + } + + function handle_db_request (request_msg, send) { + const { head: request_head, type: operation, data: params } = request_msg + let result + + if (!db) { + console.error('[page.js] Database not initialized yet') + send_response(request_head, null) + return + } + + // TODO: Check permissions here + // if (perms.graph_explorer?.deny_list?.includes(operation)) { + // console.warn('[page.js] Operation denied by permissions:', operation) + // send_response(request_head, null) + // return + // } + + if (operation === 'db_get') { + result = db.get(params.path) + } else if (operation === 'db_has') { + result = db.has(params.path) + } else if (operation === 'db_is_empty') { + result = db.is_empty() + } else if (operation === 'db_root') { + result = db.root() + } else if (operation === 'db_keys') { + result = db.keys() + } else if (operation === 'db_raw') { + result = db.raw() + } else { + console.warn('[page.js] Unknown db operation:', operation) + result = null + } + + send_response(request_head, result) + + function send_response (request_head, result) { + // Create standardized response message + const response_head = ['page_js', 'graph_explorer', page_js_mid++] + send({ + head: response_head, + refs: { cause: request_head }, // Reference the original request + type: 'db_response', + data: { result } + }) + } + } + } + + function on_entries (data) { + if (!data || data[0] == null) { + console.error('Entries data is missing or empty.') + db = graphdb({}) + if (send_to_graph_explorer) { + send_to_graph_explorer({ type: 'db_initialized', data: { entries: {} } }) + } + return + } + const parsed_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed entries data is not a valid object.') + db = graphdb({}) + if (send_to_graph_explorer) { + send_to_graph_explorer({ type: 'db_initialized', data: { entries: {} } }) + } + return + } + db = graphdb(parsed_data) + if (send_to_graph_explorer) { + send_to_graph_explorer({ type: 'db_initialized', data: { entries: parsed_data } }) + } + } + async function onbatch (batch) { + console.log(batch) for (const { type, paths } of batch) { const data = await Promise.all( paths.map(path => drive.get(path).then(file => file.raw)) @@ -3174,19 +3387,19 @@ function fallback_module () { 0: '', mapping: { style: 'theme', - entries: 'entries', runtime: 'runtime', mode: 'mode', flags: 'flags', keybinds: 'keybinds', undo: 'undo' } - } + }, + '../lib/graphdb': 0 }, drive: { 'theme/': { 'style.css': { raw: "body { font-family: 'system-ui'; }" } }, + 'entries/': { 'entries.json': { $ref: 'entries.json' } }, 'lang/': {}, - 'entries/': {}, 'runtime/': {}, 'mode/': {}, 'flags/': {}, @@ -3197,4 +3410,4 @@ function fallback_module () { } }).call(this)}).call(this,"/web/page.js") -},{"..":2,"../lib/STATE":1}]},{},[4]); +},{"..":2,"../lib/STATE":1,"../lib/graphdb":3}]},{},[4]); From 6226f83b91a7338fe97834bcb68fe41cbf1c76a6 Mon Sep 17 00:00:00 2001 From: ddroid Date: Wed, 29 Oct 2025 22:29:15 +0500 Subject: [PATCH 126/130] Change message structure to standard --- lib/graph_explorer.js | 52 ++++++++++++++++++++++++++----------------- web/page.js | 44 +++++++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index 06011a7..eb92236 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -55,6 +55,7 @@ async function graph_explorer (opts, protocol) { // Protocol system for message-based communication let send = null + let graph_explorer_mid = 0 // Message ID counter for graph_explorer.js -> page.js messages if (protocol) { send = protocol(msg => onmessage(msg)) } @@ -111,11 +112,12 @@ async function graph_explorer (opts, protocol) { return el /****************************************************************************** -ESSAGE HANDLING + ESSAGE HANDLING - Handles incoming messages and sends outgoing messages. - - @TODO: define the messages we wanna to send inorder to receive some info. + - Messages follow standardized format: { head: [by, to, mid], refs, type, data } ******************************************************************************/ - function onmessage ({ type, data }) { + function onmessage (msg) { + const { type, data } = msg const on_message_types = { set_mode: handle_set_mode, set_search_query: handle_set_search_query, @@ -134,11 +136,10 @@ ESSAGE HANDLING const handler = on_message_types[type] if (handler) handler(data) - else console.warn(`[graph_explorer-protocol] Unknown message type: ${type}`, data) + else console.warn(`[graph_explorer-protocol] Unknown message type: ${type}`, msg) - function handle_db_response (data) { - const { id, result } = data - db.handle_response(id, result) + function handle_db_response () { + db.handle_response(msg) } function handle_set_mode (data) { @@ -242,7 +243,7 @@ ESSAGE HANDLING // Page.js, trigger initial render // After receiving entries, ensure the root node state is initialized and trigger the first render. const root_path = '/' - if (db.has(root_path)) { + if (await db.has(root_path)) { const root_instance_path = '|/' if (!instance_states[root_instance_path]) { instance_states[root_instance_path] = { @@ -264,16 +265,15 @@ ESSAGE HANDLING if (container) container.replaceChildren() } } - function send_message ({ type, data }) { + function send_message (msg) { if (send) { - send({ type, data }) + send(msg) } } function create_db () { - // Pending requests map to store [resolve || reject] callbacks + // Pending requests map: key is message head [by, to, mid], value is {resolve, reject} const pending_requests = new Map() - let request_id = 0 return { // All operations are async via protocol messages @@ -284,23 +284,33 @@ ESSAGE HANDLING keys: () => send_db_request('db_keys', {}), raw: () => send_db_request('db_raw', {}), // Handle responses from page.js - handle_response: (id, result) => { - const pending = pending_requests.get(id) + handle_response: (msg) => { + if (!msg.refs || !msg.refs.cause) { + console.warn('[graph_explorer] Response missing refs.cause:', msg) + return + } + const request_head_key = JSON.stringify(msg.refs.cause) + const pending = pending_requests.get(request_head_key) if (pending) { - pending.resolve(result) - pending_requests.delete(id) + pending.resolve(msg.data.result) + pending_requests.delete(request_head_key) + } else { + console.warn('[graph_explorer] No pending request for response:', msg.refs.cause) } } } function send_db_request (operation, params) { return new Promise((resolve, reject) => { - // Generate a unique ID - const id = request_id++ - pending_requests.set(id, { resolve, reject }) + const head = ['graph_explorer', 'page_js', graph_explorer_mid++] + const head_key = JSON.stringify(head) + pending_requests.set(head_key, { resolve, reject }) + send_message({ - type: 'db_request', - data: { id, operation, params } + head, + refs: null, // New request has no references + type: operation, + data: params }) }) } diff --git a/web/page.js b/web/page.js index f8b49a7..36ae086 100644 --- a/web/page.js +++ b/web/page.js @@ -46,6 +46,12 @@ async function boot (opts) { let db = null // Send function for Graph Explorer protocol let send_to_graph_explorer = null + // Message ID counter for page_js -> graph_explorer messages + let page_js_mid = 0 + + // Permissions structure (placeholder) + // Example: perms = { graph_explorer: { deny_list: ['db_raw'] } } + // const perms = {} const subs = await sdb.watch(onbatch) console.log(subs) @@ -70,26 +76,31 @@ async function boot (opts) { send_to_graph_explorer = send return on_graph_explorer_message - function on_graph_explorer_message ({ type, data }) { - if (type === 'db_init') { - db = graphdb(data) - // Send back confirmation with the entries - send({ type: 'db_initialized', data: { entries: data } }) - } else if (type === 'db_request') { - handle_db_request(data, send) + function on_graph_explorer_message (msg) { + const { type } = msg + + if (type.startsWith('db_')) { + handle_db_request(msg, send) } } - function handle_db_request (data, send) { - const { id, operation, params } = data + function handle_db_request (request_msg, send) { + const { head: request_head, type: operation, data: params } = request_msg let result if (!db) { console.error('[page.js] Database not initialized yet') - send({ type: 'db_response', data: { id, result: null } }) + send_response(request_head, null) return } + // TODO: Check permissions here + // if (perms.graph_explorer?.deny_list?.includes(operation)) { + // console.warn('[page.js] Operation denied by permissions:', operation) + // send_response(request_head, null) + // return + // } + if (operation === 'db_get') { result = db.get(params.path) } else if (operation === 'db_has') { @@ -107,7 +118,18 @@ async function boot (opts) { result = null } - send({ type: 'db_response', data: { id, result } }) + send_response(request_head, result) + + function send_response (request_head, result) { + // Create standardized response message + const response_head = ['page_js', 'graph_explorer', page_js_mid++] + send({ + head: response_head, + refs: { cause: request_head }, // Reference the original request + type: 'db_response', + data: { result } + }) + } } } From 29131b91d839ed96718a657f4c1930fa339e34d2 Mon Sep 17 00:00:00 2001 From: serapath Date: Sun, 23 Nov 2025 19:33:13 -0800 Subject: [PATCH 127/130] upgrade datashell --- index.html | 9 ++------- index.js | 5 +++++ lib/STATE.js | 0 lib/graph_explorer.js | 2 +- package.json | 6 ++---- web/boot.js | 21 --------------------- web/page.js | 9 ++++----- 7 files changed, 14 insertions(+), 38 deletions(-) create mode 100644 index.js delete mode 100644 lib/STATE.js delete mode 100644 web/boot.js diff --git a/index.html b/index.html index 1f24e14..ce6e643 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,5 @@ - - - - - - - + + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..75eb21c --- /dev/null +++ b/index.js @@ -0,0 +1,5 @@ +const env = { version: 'latest' } +const arg = { x: 321, y: 543 } +const url = 'https://playproject.io/datashell/shim.js' +const src = `${url}?${new URLSearchParams(env)}#${new URLSearchParams(arg)}` +this.open ? document.body.append(Object.assign(document.createElement('script'), { src })) : importScripts(src) \ No newline at end of file diff --git a/lib/STATE.js b/lib/STATE.js deleted file mode 100644 index e69de29..0000000 diff --git a/lib/graph_explorer.js b/lib/graph_explorer.js index eb92236..3f9602d 100644 --- a/lib/graph_explorer.js +++ b/lib/graph_explorer.js @@ -1,4 +1,4 @@ -const STATE = require('./STATE') +const STATE = require('STATE') const statedb = STATE(__filename) const { get } = statedb(fallback_module) diff --git a/package.json b/package.json index 8ec33a8..07848bf 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,8 @@ "lib": "lib" }, "scripts": { - "start": "budo web/boot.js:bundle.js --open --live", - "build": "browserify web/boot.js -o bundle.js", - "start:act": "budo web/boot.js:bundle.js --dir ./ --live --open", - "build:act": "browserify web/boot.js > bundle.js", + "start": "budo web/page.js:bundle.js --dir . --live --open -- -i STATE", + "build": "browserify web/page.js -i STATE -o bundle.js", "lint": "standardx", "lint:fix": "standardx --fix" }, diff --git a/web/boot.js b/web/boot.js deleted file mode 100644 index 03bccaa..0000000 --- a/web/boot.js +++ /dev/null @@ -1,21 +0,0 @@ -const prefix = 'https://raw.githubusercontent.com/alyhxn/playproject/main/' -const init_url = location.hash === '#dev' ? 'web/init.js' : prefix + 'src/node_modules/init.js' -const args = arguments - -const has_save = location.hash.includes('#save') -const fetch_opts = has_save ? {} : { cache: 'no-store' } - -if (!has_save) { - localStorage.clear() -} - -fetch(init_url, fetch_opts) - .then(res => res.text()) - .then(async source => { - const module = { exports: {} } - const f = new Function('module', 'require', source) - f(module, require) - const init = module.exports - await init(args, prefix) - require('./page') // or whatever is otherwise the main entry of our project - }) diff --git a/web/page.js b/web/page.js index 36ae086..08742fd 100644 --- a/web/page.js +++ b/web/page.js @@ -1,5 +1,4 @@ -const STATE = require('../lib/STATE') -const graphdb = require('../lib/graphdb') +const STATE = require('STATE') const statedb = STATE(__filename) const admin_api = statedb.admin() admin_api.on(event => { @@ -7,12 +6,13 @@ admin_api.on(event => { }) const { sdb } = statedb(fallback_module) +const graphdb = require('../lib/graphdb') /****************************************************************************** PAGE ******************************************************************************/ const app = require('..') const sheet = new CSSStyleSheet() -config().then(() => boot({ sid: '' })) +config().then(boot) async function config () { const html = document.documentElement @@ -32,7 +32,7 @@ async function config () { /****************************************************************************** PAGE BOOT ******************************************************************************/ -async function boot (opts) { +async function boot () { // ---------------------------------------- // ID + JSON STATE // ---------------------------------------- @@ -52,7 +52,6 @@ async function boot (opts) { // Permissions structure (placeholder) // Example: perms = { graph_explorer: { deny_list: ['db_raw'] } } // const perms = {} - const subs = await sdb.watch(onbatch) console.log(subs) From 1120f7f6544926ac038327be262161404f2b4724 Mon Sep 17 00:00:00 2001 From: serapath Date: Sun, 23 Nov 2025 19:33:29 -0800 Subject: [PATCH 128/130] update bundle --- bundle.js | 42 +++++++++--------------------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/bundle.js b/bundle.js index 17d0f59..a65801b 100644 --- a/bundle.js +++ b/bundle.js @@ -1,8 +1,6 @@ (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i res.text()) - .then(async source => { - const module = { exports: {} } - const f = new Function('module', 'require', source) - f(module, require) - const init = module.exports - await init(args, prefix) - require('./page') // or whatever is otherwise the main entry of our project - }) - -},{"./page":5}],5:[function(require,module,exports){ +},{}],4:[function(require,module,exports){ (function (__filename){(function (){ -const STATE = require('../lib/STATE') -const graphdb = require('../lib/graphdb') +const STATE = require('STATE') const statedb = STATE(__filename) const admin_api = statedb.admin() admin_api.on(event => { @@ -3215,12 +3191,13 @@ admin_api.on(event => { }) const { sdb } = statedb(fallback_module) +const graphdb = require('../lib/graphdb') /****************************************************************************** PAGE ******************************************************************************/ const app = require('..') const sheet = new CSSStyleSheet() -config().then(() => boot({ sid: '' })) +config().then(boot) async function config () { const html = document.documentElement @@ -3240,7 +3217,7 @@ async function config () { /****************************************************************************** PAGE BOOT ******************************************************************************/ -async function boot (opts) { +async function boot () { // ---------------------------------------- // ID + JSON STATE // ---------------------------------------- @@ -3260,7 +3237,6 @@ async function boot (opts) { // Permissions structure (placeholder) // Example: perms = { graph_explorer: { deny_list: ['db_raw'] } } // const perms = {} - const subs = await sdb.watch(onbatch) console.log(subs) @@ -3410,4 +3386,4 @@ function fallback_module () { } }).call(this)}).call(this,"/web/page.js") -},{"..":2,"../lib/STATE":1,"../lib/graphdb":3}]},{},[4]); +},{"..":1,"../lib/graphdb":2,"STATE":3}]},{},[4]); From c66519ba49fa61e358b7f369e16672378d80a2eb Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 29 Nov 2025 03:08:56 +0500 Subject: [PATCH 129/130] Updated Protocol Docs --- PROTOCOL.md | 104 ++++++++++++++++++++++++++++++++++++++++++++-------- README.md | 56 ++++++++++++++++++++++------ 2 files changed, 134 insertions(+), 26 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index 771e9f1..de10d12 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -1,6 +1,6 @@ # Graph Explorer Protocol System -The `graph_explorer` module implements a bidirectional message-based communication protocol that allows parent modules to control the graph explorer and receive notifications after the requested message was processed. +The `graph_explorer` module implements a standard bidirectional message-based communication protocol that allows parent modules to control the graph explorer and receive notifications after the requested message was processed. ## Usage @@ -8,7 +8,7 @@ When initializing the graph explorer, pass a protocol function as the second par ```javascript const _ = {} // Store the send function to communicate with graph_explorer -const graph_explorer = require('./lib/graph_explorer.js') +const graph_explorer = require('graph-explorer') const element = await graph_explorer(opts, protocol) @@ -19,7 +19,8 @@ function protocol (send) { // Return a message handler function return onmessage - function onmessage ({ type, data }) { + function onmessage (msg) { + const { head, refs, type, data } = msg // Handle messages from graph_explorer switch (type) { case 'node_clicked': @@ -34,6 +35,24 @@ function protocol (send) { } ``` +## Message Structure + +All messages follow the standard protocol format: + +```javascript +{ + head: [sender_id, receiver_id, message_id], + refs: { cause: parent_message_head }, + type: "message_type", + data: { ... } +} +``` + +- `head`: `[from, to, id]` - Unique message identifier +- `refs`: reference to cause (empty `{}` for user events) +- `type`: Message type string +- `data`: Message payload + ## Incoming Messages (Parent → Graph Explorer) These messages can be sent to the graph explorer to control its behavior: @@ -46,7 +65,12 @@ Change the current display mode. **Example:** ```javascript -graph_send({ type: 'set_mode', data: { mode: 'search' }}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'set_mode', + data: { mode: 'search' } +}) ``` ### `set_search_query` @@ -57,7 +81,12 @@ Set the search query (automatically switches to search mode if not already). **Example:** ```javascript -graph_send({ type: 'set_search_query', data: { query: 'my search' }}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'set_search_query', + data: { query: 'my search' } +}) ``` ### `select_nodes` @@ -68,7 +97,12 @@ Programmatically select specific nodes. **Example:** ```javascript -graph_send({ type: 'select_nodes', data: { instance_paths: ['|/', '|/src'] }}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'select_nodes', + data: { instance_paths: ['|/', '|/src'] } +}) ``` ### `expand_node` @@ -81,7 +115,12 @@ Expand a specific node's children and/or hubs. **Example:** ```javascript -graph_send({ type: 'expand_node', data: { instance_path: '|/', expand_subs: true, expand_hubs: true }}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'expand_node', + data: { instance_path: '|/', expand_subs: true, expand_hubs: true } +}) ``` ### `collapse_node` @@ -92,7 +131,12 @@ Collapse a specific node's children and hubs. **Example:** ```javascript -graph_send({ type: 'collapse_node', data: { instance_path: '|/src' }}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'collapse_node', + data: { instance_path: '|/src' } +}) ``` ### `toggle_node` @@ -104,7 +148,12 @@ Toggle expansion state of a node. **Example:** ```javascript -graph_send({ type: 'toggle_node', data: { instance_path: '|/src', toggle_type: 'subs' }}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'toggle_node', + data: { instance_path: '|/src', toggle_type: 'subs' } +}) ``` ### `get_selected` @@ -116,7 +165,12 @@ Request the current selection state. **Example:** ```javascript -graph_send({ type: 'get_selected', data: {}}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'get_selected', + data: {} +}) ``` ### `get_confirmed` @@ -128,7 +182,12 @@ Request the current confirmed selection state. **Example:** ```javascript -graph_send({ type: 'get_confirmed', data: {}}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'get_confirmed', + data: {} +}) ``` ### `clear_selection` @@ -138,7 +197,12 @@ Clear all selected and confirmed nodes. **Example:** ```javascript -graph_send({ type: 'clear_selection', data: {}}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'clear_selection', + data: {} +}) ``` ### `set_flag` @@ -153,7 +217,12 @@ Set a configuration flag. **Example:** ```javascript -graph_send({ type: 'set_flag', data: { flag_type: 'hubs', value: 'true' }}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'set_flag', + data: { flag_type: 'hubs', value: 'true' } +}) ``` ### `scroll_to_node` @@ -164,12 +233,17 @@ Scroll to a specific node in the view. **Example:** ```javascript -graph_send({ type: 'scroll_to_node', data: { instance_path: '|/src/index.js' }}) +graph_send({ + head: [by, to, mid++], + refs: {}, + type: 'scroll_to_node', + data: { instance_path: '|/src/index.js' } +}) ``` ## Outgoing Messages (Graph Explorer → Parent) -These messages are sent by the graph explorer to notify the parent module of events: +These messages are sent by the graph explorer to notify the parent module of events. They follow the same standard protocol format: ### `node_clicked` Fired when a node is clicked. diff --git a/README.md b/README.md index 96ec5e4..e73ec6d 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,15 @@ A lightweight, high-performance frontend component for rendering and exploring i - **Virtual Scrolling:** Renders only the visible nodes, ensuring smooth scrolling and interaction even with very large graphs. - **Interactive Exploration:** Allows users to expand and collapse both hierarchical children (`subs`) and related connections (`hubs`). -- **Dynamic Data Loading:** Listens for data updates and re-renders the view accordingly. -## Usage +- **Standard Protocol:** Implements the standard bidirectional message-based communication protocol for seamless integration. +- **Drive-based Data Flow:** Uses the drive system for efficient data management and real-time updates. -Require the `graph_explorer` function and call it with a configuration object. It returns a DOM element that can be appended to the page. +## Quick Start + +The graph explorer requires data to be supplied through a drive system and communicates via the standard protocol: ```javascript -const graph_explorer = require('./graph_explorer.js') +const graph_explorer = require('graph-explorer') // Provide `opts` and optional `protocol` as parameters const graph = await graph_explorer(opts, protocol) @@ -21,21 +23,53 @@ const graph = await graph_explorer(opts, protocol) document.body.appendChild(graph) ``` -### Protocol System -The graph explorer supports bidirectional message-based communication through an optional protocol parameter. This allows parent modules to: +## Protocol System + +The graph explorer implements the **standard bidirectional message-based communication protocol** that allows parent modules to: - Control the graph explorer programmatically (change modes, select nodes, expand/collapse, etc.) - Receive notifications about user interactions and state changes +All messages follow the standard format: +```javascript +{ + head: [sender_id, receiver_id, message_id], + refs: { cause: parent_message_head }, + type: "message_type", + data: { ... } +} +``` + For complete protocol documentation, see [PROTOCOL.md](./PROTOCOL.md). -## Drive +## Data Flow -The component expects to receive data through datasets in drive. It responds to two types of messages: `entries` and `style`. +The graph explorer uses a drive-based data system for efficient data management: + +### Required Drive Datasets + +1. **`entries/entries.json`** - Core graph data (see format below) +2. **`theme/style.css`** - CSS styles for the component +3. **`mode/`** - Current mode and search state +4. **`flags/`** - Configuration flags +5. **`keybinds/`** - Keyboard navigation bindings + +### Data Integration Pattern + +The recommended approach is to use the `graph_explorer_wrapper` which handles: +- Drive data watching and processing +- Protocol communication setup +- Database initialization +- Message routing between parent and graph explorer + +```javascript +const graph_explorer_wrapper = require('graph_explorer_wrapper') +const graph = await graph_explorer_wrapper(opts, protocol) +``` ### 1. `entries` -The `entries` message provides the core graph data. It should be an object where each key is a unique path identifier for a node, and the value is an object describing that node's properties. +The `entries` dataset provides the core graph data. It should be stored in `entries/entries.json` as an object where each key is a unique path identifier for a node, and the value is an object describing that node's properties. **Example `entries` Object:** @@ -83,9 +117,9 @@ The `entries` message provides the core graph data. It should be an object where - `subs` (Array): An array of paths to child nodes. An empty array indicates no children. - `hubs` (Array): An array of paths to related, non-hierarchical nodes. -### 2. `style` +### 2. `theme` -The `style` message provides a string of CSS content that will be injected directly into the component's Shadow DOM. This allows for full control over the visual appearance of the graph, nodes, icons, and tree lines. +The `theme` dataset provides CSS styles and should be stored in `theme/style.css`. The styles are injected directly into the component's Shadow DOM for full visual control. **Example `style` Data:** From 75f42772ea0d43f2b3d019ab761e1c43b6adcc5d Mon Sep 17 00:00:00 2001 From: ddroid Date: Sat, 29 Nov 2025 03:28:25 +0500 Subject: [PATCH 130/130] Added Usage docs and commented wrapper code --- README.md | 10 +- PROTOCOL.md => guide/PROTOCOL.md | 0 guide/USAGE.md | 328 ++++++++++++++++++++++ guide/commented_graph_explorer_wrapper.js | 318 +++++++++++++++++++++ 4 files changed, 651 insertions(+), 5 deletions(-) rename PROTOCOL.md => guide/PROTOCOL.md (100%) create mode 100644 guide/USAGE.md create mode 100644 guide/commented_graph_explorer_wrapper.js diff --git a/README.md b/README.md index e73ec6d..1a54733 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ const graph = await graph_explorer(opts, protocol) // Append the element to your application's body or another container document.body.appendChild(graph) ``` - +For detailed usage instructions, see [USAGE.md](./guide/USAGE.md). ## Protocol System @@ -40,7 +40,7 @@ All messages follow the standard format: } ``` -For complete protocol documentation, see [PROTOCOL.md](./PROTOCOL.md). +For complete protocol documentation, see [PROTOCOL.md](./guide/PROTOCOL.md). ## Data Flow @@ -56,15 +56,15 @@ The graph explorer uses a drive-based data system for efficient data management: ### Data Integration Pattern -The recommended approach is to use the `graph_explorer_wrapper` which handles: +The recommended approach is to use the `graph_explorer` which handles: - Drive data watching and processing - Protocol communication setup - Database initialization - Message routing between parent and graph explorer ```javascript -const graph_explorer_wrapper = require('graph_explorer_wrapper') -const graph = await graph_explorer_wrapper(opts, protocol) +const graph_explorer = require('graph-explorer') +const graph = await graph_explorer(opts, protocol) ``` ### 1. `entries` diff --git a/PROTOCOL.md b/guide/PROTOCOL.md similarity index 100% rename from PROTOCOL.md rename to guide/PROTOCOL.md diff --git a/guide/USAGE.md b/guide/USAGE.md new file mode 100644 index 0000000..c4c92e1 --- /dev/null +++ b/guide/USAGE.md @@ -0,0 +1,328 @@ +# Graph Explorer Usage Guide + +This guide explains how to integrate the graph explorer component using the simple file-based approach with the standard protocol. + +## Quick Setup Pattern + +1. **Create `entries.json`** in your component directory +2. **Copy `graphdb.js`** to your component directory +3. **Create `entries` dataset** with `$ref` to your entries.json +4. **Implement protocol** using the standard pattern +5. **Pass drive to graph-explorer** via protocol + +## Step 1: Create Graph Data File + +Create `entries.json` in the same directory as your component: + +```json +{ + "/": { + "name": "Root Directory", + "type": "root", + "subs": ["/src", "/assets", "/README.md"], + "hubs": ["/LICENSE"] + }, + "/src": { + "name": "src", + "type": "folder", + "subs": ["/src/index.js", "/src/styles.css"] + }, + "/src/index.js": { + "name": "index.js", + "type": "js-file" + }, + "/README.md": { + "name": "README.md", + "type": "file" + } +} +``` + +## Step 2: Add GraphDB Module +It can be a custom one but the simplest one is as below. +Copy `graphdb.js` to your component directory: + +```javascript +// graphdb.js +module.exports = graphdb + +function graphdb (entries) { + if (!entries || typeof entries !== 'object') { + console.warn('[graphdb] Invalid entries provided, using empty object') + entries = {} + } + + const api = { + get, + has, + keys, + is_empty, + root, + raw + } + + return api + + function get (path) { + return entries[path] || null + } + + function has (path) { + return path in entries + } + + function keys () { + return Object.keys(entries) + } + + function is_empty () { + return Object.keys(entries).length === 0 + } + + function root () { + return entries['/'] || null + } + + function raw () { + return entries + } +} +``` + +## Step 3: Create Component with Drive Dataset + +```javascript +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const graph_explorer = require('graph-explorer') +const graphdb = require('./graphdb') + +module.exports = my_component_with_graph + +async function my_component_with_graph (opts, protocol) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const ids = opts.ids + if (!ids || !ids.up) { + throw new Error(`Component ${__filename} requires ids.up to be provided`) + } + + const by = id + let db = null + let send_to_graph_explorer = null + let mid = 0 + + const on = { + theme: inject, + entries: on_entries + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const subs = await sdb.watch(onbatch) + const explorer_el = await graph_explorer(subs[0], graph_explorer_protocol) + shadow.append(explorer_el) + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + on[type] && on[type](data) + } + } + + function inject (data) { + sheet.replaceSync(data.join('\n')) + } + + function on_entries (data) { + if (!data || !data[0]) { + console.error('Entries data is missing or empty.') + db = graphdb({}) + notify_db_initialized({}) + return + } + + let parsed_data + try { + parsed_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + } catch (e) { + console.error('Failed to parse entries data:', e) + parsed_data = {} + } + + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed entries data is not a valid object.') + parsed_data = {} + } + + db = graphdb(parsed_data) + notify_db_initialized(parsed_data) + } + + function notify_db_initialized (entries) { + if (send_to_graph_explorer) { + const head = [by, 'graph_explorer', mid++] + send_to_graph_explorer({ + head, + type: 'db_initialized', + data: { entries } + }) + } + } + + function graph_explorer_protocol (send) { + send_to_graph_explorer = send + return on_graph_explorer_message + + function on_graph_explorer_message (msg) { + const { type } = msg + if (type.startsWith('db_')) { + handle_db_request(msg, send) + } + } + + function handle_db_request (request_msg, send) { + const { head: request_head, type: operation, data: params } = request_msg + let result + + if (!db) { + console.error('[my_component] Database not initialized yet') + send_response(request_head, null) + return + } + + if (operation === 'db_get') { + result = db.get(params.path) + } else if (operation === 'db_has') { + result = db.has(params.path) + } else if (operation === 'db_is_empty') { + result = db.is_empty() + } else if (operation === 'db_root') { + result = db.root() + } else if (operation === 'db_keys') { + result = db.keys() + } else if (operation === 'db_raw') { + result = db.raw() + } else { + console.warn('[my_component] Unknown db operation:', operation) + result = null + } + + send_response(request_head, result) + + function send_response (request_head, result) { + const response_head = [by, 'graph_explorer', mid++] + send({ + head: response_head, + refs: { cause: request_head }, + type: 'db_response', + data: { result } + }) + } + } + } +} + +function fallback_module () { + return { + _: { + 'graph-explorer': { $: '' }, + './graphdb': { $: '' } + }, + api: fallback_instance + } + + function fallback_instance () { + return { + _: { + 'graph-explorer': { + $: '', + 0: '', + mapping: { + style: 'theme', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo' + } + }, + './graphdb': { + $: '' + } + }, + drive: { + 'theme/': { + 'style.css': { + raw: ` + :host { + display: block; + height: 100%; + width: 100%; + } + .graph-container { + color: #abb2bf; + background-color: #282c34; + padding: 10px; + height: 100vh; + overflow: auto; + } + .node { + display: flex; + align-items: center; + white-space: nowrap; + cursor: default; + height: 22px; + } + .clickable { + cursor: pointer; + } + .node.type-folder > .icon::before { content: '�'; } + .node.type-js-file > .icon::before { content: '📜'; } + .node.type-file > .icon::before { content: '📄'; } + ` + } + }, + 'entries/': { + 'entries.json': { + $ref: 'entries.json' + } + }, + 'runtime/': {}, + 'mode/': {}, + 'flags/': {}, + 'keybinds/': {}, + 'undo/': {} + } + } + } +} +``` + +## Step 4: Use Your Component + +## Key Points + +1. `entries.json`: Store in same directory as your component +2. `graphdb.js`: Copy the simple module to your directory +3. `$ref`: Use `$ref: 'entries.json'` to link your file +4. `Protocol`: Follow the standard pattern for communication +5. `Drive`: Pass entries to graphdb, then drive to graph_explorer + +## File Structure + +``` +my-component/ +├── my_component_with_graph.js +├── entries.json +└── graphdb.js +``` + +This approach keeps everything simple and local to your component while using the standard protocol for communication. diff --git a/guide/commented_graph_explorer_wrapper.js b/guide/commented_graph_explorer_wrapper.js new file mode 100644 index 0000000..fa059bd --- /dev/null +++ b/guide/commented_graph_explorer_wrapper.js @@ -0,0 +1,318 @@ +// ============================================================================= +// GRAPH EXPLORER WRAPPER - COMMENTED EXAMPLE +// ============================================================================= +// This file demonstrates how to create a wrapper component that integrates +// the graph-explorer with the drive system and standard protocol. you don't need a wrapper but it often gets complex with the addition of overrides especially when you want to customize the behavior. +// ============================================================================= + +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const graph_explorer = require('graph-explorer') +const graphdb = require('./graphdb') + +module.exports = graph_explorer_wrapper + +// ============================================================================= +// MAIN COMPONENT FUNCTION +// ============================================================================= +async function graph_explorer_wrapper (opts, protocol) { + // ------------------------------------------------------------------------- + // 1. COMPONENT INITIALIZATION + // ------------------------------------------------------------------------- + + // Get component instance and state database from STATE system + const { id, sdb } = await get(opts.sid) + const { drive } = sdb // Drive system for data management + + // Validate required parent ID for protocol communication + const ids = opts.ids + if (!ids || !ids.up) { + throw new Error(`Component ${__filename} requires ids.up to be provided`) + } + + // Set up protocol identifiers + const by = id // Our component ID (sender) + // const to = ids.up // Parent component ID (receiver) - not used directly + + // ------------------------------------------------------------------------- + // 2. INTERNAL STATE MANAGEMENT + // ------------------------------------------------------------------------- + + let db = null // Graph database instance (will be initialized from entries data) + + // Protocol communication variables + let send_to_graph_explorer = null // Function to send messages TO graph explorer + let mid = 0 // Message ID counter for outgoing messages + + // ------------------------------------------------------------------------- + // 3. DATA HANDLERS + // ------------------------------------------------------------------------- + + // Map of data types to handler functions + const on = { + theme: inject, // Handle CSS theme updates + entries: on_entries // Handle graph data updates + } + + // ------------------------------------------------------------------------- + // 4. DOM ELEMENT SETUP + // ------------------------------------------------------------------------- + + // Create main component container + const el = document.createElement('div') + + // Create Shadow DOM for style isolation + const shadow = el.attachShadow({ mode: 'closed' }) + + // Set up CSS stylesheet for the Shadow DOM + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + // ------------------------------------------------------------------------- + // 5. DRIVE DATA WATCHING + // ------------------------------------------------------------------------- + + // Start watching drive data changes + // This will call onbatch() whenever drive data is updated + const subs = await sdb.watch(onbatch) + + // Initialize the actual graph explorer component + // Pass drive subscriptions and our protocol handler + const explorer_el = await graph_explorer(subs[0], graph_explorer_protocol) + shadow.append(explorer_el) + + // Return the main element to the parent + return el + + // ------------------------------------------------------------------------- + // 6. DRIVE DATA PROCESSING + // ------------------------------------------------------------------------- + + // Called when drive data changes (batch updates) + async function onbatch (batch) { + // Process each change in the batch + for (const { type, paths } of batch) { + // Get the actual data for all changed paths + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + + // Call the appropriate handler for this data type + on[type] && on[type](data) + } + } + + // ------------------------------------------------------------------------- + // 7. THEME/STYLE HANDLING + // ------------------------------------------------------------------------- + + // Inject CSS styles into the Shadow DOM + function inject (data) { + // data is an array of CSS strings, join them and apply to stylesheet + sheet.replaceSync(data.join('\n')) + } + + // ------------------------------------------------------------------------- + // 8. GRAPH DATA HANDLING + // ------------------------------------------------------------------------- + + // Handle entries data updates (core graph structure) + function on_entries (data) { + // Validate incoming data + if (!data || !data[0]) { + console.error('Entries data is missing or empty.') + db = graphdb({}) // Create empty database + notify_db_initialized({}) + return + } + + let parsed_data + try { + // Parse JSON data if it's a string, otherwise use as-is + parsed_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + } catch (e) { + console.error('Failed to parse entries data:', e) + parsed_data = {} + } + + // Ensure we have a valid object + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed entries data is not a valid object.') + parsed_data = {} + } + + // Create graph database from parsed data + db = graphdb(parsed_data) + + // Notify graph explorer that database is ready + notify_db_initialized(parsed_data) + } + + // ------------------------------------------------------------------------- + // 9. DATABASE NOTIFICATION + // ------------------------------------------------------------------------- + + // Send message to graph explorer when database is initialized/updated + function notify_db_initialized (entries) { + if (send_to_graph_explorer) { + // Create standard protocol message + const head = [by, 'graph_explorer', mid++] + send_to_graph_explorer({ + head, + type: 'db_initialized', + data: { entries } + }) + } + } + + // ------------------------------------------------------------------------- + // 10. PROTOCOL IMPLEMENTATION + // ------------------------------------------------------------------------- + + // Standard protocol handler function + // Called by graph_explorer to establish communication + function graph_explorer_protocol (send) { + // Store the send function provided by graph explorer + // This allows us to send messages TO the graph explorer + send_to_graph_explorer = send + + // Return our message handler function + // This will be called when graph explorer sends messages TO US + return on_graph_explorer_message + + // ------------------------------------------------------------------------- + // 11. MESSAGE HANDLING + // ------------------------------------------------------------------------- + + // Handle incoming messages from graph explorer + function on_graph_explorer_message (msg) { + const { type } = msg + + // Route database-related messages to the database handler + if (type.startsWith('db_')) { + handle_db_request(msg, send) + } + } + + // ------------------------------------------------------------------------- + // 12. DATABASE REQUEST HANDLING + // ------------------------------------------------------------------------- + + // Handle database operation requests from graph explorer + function handle_db_request (request_msg, send) { + const { head: request_head, type: operation, data: params } = request_msg + let result + + // Ensure database is initialized + if (!db) { + console.error('[graph_explorer_wrapper] Database not initialized yet') + send_response(request_head, null) + return + } + + // Execute the requested database operation + if (operation === 'db_get') { + result = db.get(params.path) + } else if (operation === 'db_has') { + result = db.has(params.path) + } else if (operation === 'db_is_empty') { + result = db.is_empty() + } else if (operation === 'db_root') { + result = db.root() + } else if (operation === 'db_keys') { + result = db.keys() + } else if (operation === 'db_raw') { + result = db.raw() + } else { + console.warn('[graph_explorer_wrapper] Unknown db operation:', operation) + result = null + } + + // Send the response back to graph explorer + send_response(request_head, result) + + // ------------------------------------------------------------------- + // 13. RESPONSE SENDING + // ------------------------------------------------------------------- + + function send_response (request_head, result) { + // Create standardized response message following the protocol + const response_head = [by, 'graph_explorer', mid++] + send({ + head: response_head, + refs: { cause: request_head }, // Reference original request for causality + type: 'db_response', + data: { result } + }) + } + } + } +} + +// ============================================================================= +// 14. FALLBACK MODULE DEFINITION +// ============================================================================= +// This provides default structure and data when the component is used in +// isolation or for development/testing purposes. + +function fallback_module () { + return { + // Module dependencies mapping + _: { + 'graph-explorer': { $: '' }, + './graphdb': { $: '' } + }, + // Instance factory function + api: fallback_instance + } + + function fallback_instance () { + return { + // Instance dependencies mapping + _: { + 'graph-explorer': { + $: '', + 0: '', + // Drive dataset mappings for graph explorer + mapping: { + style: 'theme', // CSS styles -> theme dataset + runtime: 'runtime', // Runtime state -> runtime dataset + mode: 'mode', // Mode settings -> mode dataset + flags: 'flags', // Configuration flags -> flags dataset + keybinds: 'keybinds', // Keyboard bindings -> keybinds dataset + undo: 'undo' // Undo history -> undo dataset + } + }, + './graphdb': { + $: '' + } + }, + // Default drive structure with sample data + drive: { + // Theme dataset with default styles + 'theme/': { + 'style.css': { + raw: ` + :host { + display: block; + height: 100%; + width: 100%; + } + ` + } + }, + // Entries dataset for graph data + 'entries/': { + 'entries.json': { + $ref: 'entries.json' // Reference to external entries file + } + }, + // Other required datasets (empty by default) + 'runtime/': {}, + 'mode/': {}, + 'flags/': {}, + 'keybinds/': {}, + 'undo/': {} + } + } + } +}