diff --git a/docs/examples/plot/index.js b/docs/examples/plot/index.js new file mode 100644 index 0000000..8cba914 --- /dev/null +++ b/docs/examples/plot/index.js @@ -0,0 +1,16 @@ +import _ from 'https://unpkg.com/lodash-es' + +const url = 'https://api.github.com/search/repositories?q=stars:%3E1&sort=stars' +const resp = await fetch(url) +const repos = await resp.json() +const langs = _(repos.items) + .map(r => r.language) + .filter(l => l != null) + .countBy() + .toPairs() + .map(([language, count]) => ({language, count})) + +import {barY} from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm"; + +barY(langs, {x: "language", y: "count", sort: {x: "y", reverse: true}, fill: 'purple'}) + .plot() diff --git a/index.html b/index.html index afa07df..eaf81ba 100644 --- a/index.html +++ b/index.html @@ -266,16 +266,23 @@ /* value_explorer */ - .embed_value_explorer_container { + .embed_value_explorer_container.is_not_dom_el { height: 0px; } + .embed_value_explorer_container.is_dom_el { + padding: 1em; + } + .embed_value_explorer_wrapper { - margin-left: 1em; /* preserve wrapper from getting clicks for code line left to it */ pointer-events: none; } + .embed_value_explorer_container.is_not_dom_el .embed_value_explorer_wrapper { + margin-left: 1em; + } + .embed_value_explorer_content { pointer-events: initial; white-space: pre; diff --git a/src/editor/editor.js b/src/editor/editor.js index 5b93143..a2a8bd1 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -169,10 +169,33 @@ export class Editor { } unembed_value_explorer() { - if(this.widget != null) { - this.ace_editor.getSession().widgetManager.removeLineWidget(this.widget) - this.widget = null + if(this.widget == null) { + return } + + const session = this.ace_editor.getSession() + const widget_bottom = this.widget.el.getBoundingClientRect().bottom + session.widgetManager.removeLineWidget(this.widget) + + if(this.widget.is_dom_el) { + /* + if cursor moves below widget, then ace editor first adjusts scroll, + and then widget gets remove, so scroll jerks. We have to set scroll + back + */ + // distance travelled by cursor + const distance = session.selection.getCursor().row - this.widget.row + if(distance > 0) { + const line_height = this.ace_editor.renderer.lineHeight + const scroll = widget_bottom - this.editor_container.getBoundingClientRect().bottom + if(scroll > 0) { + const scrollTop = session.getScrollTop() + session.setScrollTop(session.getScrollTop() - scroll - line_height*distance) + } + } + } + + this.widget = null } update_value_explorer_margin() { @@ -203,10 +226,6 @@ export class Editor { this.unembed_value_explorer() const session = this.ace_editor.getSession() - const pos = session.doc.indexToPosition(index) - const row = pos.row - - const line_height = this.ace_editor.renderer.lineHeight let content const container = el('div', {'class': 'embed_value_explorer_container'}, @@ -214,7 +233,6 @@ export class Editor { content = el('div', { // Ace editor cannot render widget before the first line. So we // render in on the next line and apply translate - 'style': `transform: translate(0px, -${line_height}px)`, 'class': 'embed_value_explorer_content', tabindex: 0 }) @@ -242,37 +260,49 @@ export class Editor { } }) - if(ok) { - const exp = new ValueExplorer({ - container: content, - event_target: container, - on_escape: escape, - scroll_to_element: t => { - if(initial_scroll_top == null) { - initial_scroll_top = session.getScrollTop() - } - let scroll - const out_of_bottom = t.getBoundingClientRect().bottom - this.editor_container.getBoundingClientRect().bottom - if(out_of_bottom > 0) { - session.setScrollTop(session.getScrollTop() + out_of_bottom) - } - const out_of_top = this.editor_container.getBoundingClientRect().top - t.getBoundingClientRect().top - if(out_of_top > 0) { - session.setScrollTop(session.getScrollTop() - out_of_top) - } - }, - }) + let is_dom_el - exp.render(value) + if(ok) { + if(value instanceof globalThis.app_window.Element && !value.isConnected) { + is_dom_el = true + content.appendChild(value) + } else { + is_dom_el = false + const exp = new ValueExplorer({ + container: content, + event_target: container, + on_escape: escape, + scroll_to_element: t => { + if(initial_scroll_top == null) { + initial_scroll_top = session.getScrollTop() + } + let scroll + const out_of_bottom = t.getBoundingClientRect().bottom - this.editor_container.getBoundingClientRect().bottom + if(out_of_bottom > 0) { + session.setScrollTop(session.getScrollTop() + out_of_bottom) + } + const out_of_top = this.editor_container.getBoundingClientRect().top - t.getBoundingClientRect().top + if(out_of_top > 0) { + session.setScrollTop(session.getScrollTop() - out_of_top) + } + }, + }) + + exp.render(value) + } } else { + is_dom_el = false content.appendChild(el('span', 'eval_error', stringify_for_header(error))) } const widget = this.widget = { - row, + row: is_dom_el + ? session.doc.indexToPosition(index + length).row + : session.doc.indexToPosition(index).row, fixedWidth: true, el: container, content, + is_dom_el, } @@ -282,13 +312,22 @@ export class Editor { session.widgetManager.attach(this.ace_editor); } - // update_value_explorer_margin relies on getLastVisibleRow which can be - // incorrect because it may be executed right after set_cursor_position - // which is async in ace_editor. Use setTimeout - setTimeout(() => { - this.update_value_explorer_margin() + if(is_dom_el) { + container.classList.add('is_dom_el') session.widgetManager.addLineWidget(widget) - }, 0) + } else { + container.classList.add('is_not_dom_el') + const line_height = this.ace_editor.renderer.lineHeight + content.style.transform = `translate(0px, -${line_height}px)` + // update_value_explorer_margin relies on getLastVisibleRow which can be + // incorrect because it may be executed right after set_cursor_position + // which is async in ace_editor. Use setTimeout + setTimeout(() => { + this.update_value_explorer_margin() + session.widgetManager.addLineWidget(widget) + }, 0) + } + } focus_value_explorer(return_to) { diff --git a/src/examples.js b/src/examples.js index 6b54e99..9c0a37d 100644 --- a/src/examples.js +++ b/src/examples.js @@ -27,6 +27,10 @@ export const examples = [ path: 'ethers', entrypoint: 'ethers/block_by_timestamp.js', }, + { + path: 'plot', + entrypoint: 'plot/index.js', + }, ].map(e => ({...e, entrypoint: e.entrypoint ?? e.path})) const files_list = examples