I experiment with a custom observer that allows to edit cells and currently I ac…cess the cell body in a very hackish way, see below.
=> What is the recommended way to access the cell definition in a custom observer?
I suggest that you pass the cell definition (dependencies and function body) to the observer instead of only passing the result value:
`fulfilled(value, name, definition)`
instead of
`fulfilled(value, name)`
Screenshot:
![image](https://user-images.githubusercontent.com/12675339/137472043-70d53d67-308d-4641-8c45-f3552d51d92d.png)
Draft for custom Editor, based on default Inspector:
```
import {Inspector} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js";
export const FORBIDDEN = {};
export default class Editor extends Inspector {
constructor(element, runtime){
super(element);
this._runtime = runtime;
}
//overrides the the default fulfilled method of the inspector
//so that editable input fields are shown instead of spans
fulfilled(value, name) {
const {_node} = this;
if (!isnode(value) || (value.parentNode && value.parentNode !== _node)) {
let expand = _node.firstChild
&& _node.firstChild.classList
&& _node.firstChild.classList.contains("observablehq--expanded");
value = this.edit(value, false, expand, name);
value.classList.add("observablehq--inspect");
}
_node.classList.remove("observablehq--running", "observablehq--error");
if (_node.firstChild !== value) {
if (_node.firstChild) {
while (_node.lastChild !== _node.firstChild) _node.removeChild(_node.lastChild);
_node.replaceChild(value, _node.firstChild);
} else {
_node.appendChild(value);
}
}
this.dispatch(_node, "update");
}
edit(value, shallow, expand, name) {
let type = typeof value;
switch (type) {
case "boolean":
case "undefined": { value += ""; break; }
case "number": { value = value === 0 && 1 / value < 0 ? "-0" : value + ""; break; }
case "bigint": { value = value + "n"; break; }
case "symbol": { value = this.formatSymbol(value, name); break; }
case "function": { return this.editFunction(value, name); }
case "string": { return this.formatString(value, shallow, expand, name); }
default: {
if (value === null) { type = null, value = "null"; break; }
if (value instanceof Date) { type = "date", value = this.formatDate(value, name); break; }
if (value === FORBIDDEN) { type = "forbidden", value = "[forbidden]"; break; }
switch (toString.call(value)) {
case "[object RegExp]": { type = "regexp", value = this.formatRegExp(value, name); break; }
case "[object Error]": // https://github.com/lodash/lodash/blob/master/isError.js#L26
case "[object DOMException]": { type = "error", value = this.formatError(value, name); break; }
default: return (expand ? this.editExpanded : this.editCollapsed)(value, shallow, name);
}
break;
}
}
const editor = document.createElement("div");
editor.className = `custom-editor observablehq--${type}`;
const output = editor.appendChild(document.createElement("span"));
output.className = `observablehq--${type}`;
output.textContent = value;
const definition = editor.appendChild(document.createElement("div"));
if (name) definition.appendChild(this.nameLabel(name));
let firstModule = this._runtime._modules.values().next().value;
const input = definition.appendChild(document.createElement("input"));
input.className = `observablehq--${type}`;
input.value = this.cellBody(name);
input.oninput = (event) => this.inputChanged(name, firstModule, event);
return editor;
}
inputChanged(name, module, event){
var cellBody = event.currentTarget.value;
console.log("cell input changed " + name + ': ' + cellBody);
//module.redefine(name, cellBody);
//TODO
}
cellBody(name){
let runtime = this._runtime;
let firstDefine = this._runtime._modules.keys().next().value;
let cellBodies = this.extractCellBodies(firstDefine, name);
let cellBody = cellBodies[name];
return cellBody;
}
extractCellBodies(define, name){
let cellBodies = {};
let variableMock = {
define: (varName, inputs, definition) => {
let extractedDefinition = definition?definition:inputs;
cellBodies[varName] = this.extractFunctionBody(extractedDefinition);
}
};
let moduleMock = { variable: () => variableMock};
let runtimeMock = { module: ()=> moduleMock};
let observerMock = (undefined)=>{};
define(runtimeMock, observerMock);
return cellBodies;
}
extractFunctionBody(definition){
let functionString = definition.toString();
let startIndex = functionString.indexOf('{return(\n') +9;
let endIndex = functionString.length - 3;
let body = functionString.substring(startIndex, endIndex);
return body;
}
nameLabel(name) {
const n = document.createElement("span");
n.className = "observablehq--cellname";
n.textContent = `${name} = `;
return n;
}
editFunction(f, name){
const span = document.createElement("span");
span.textContent = name;
return span;
//TODO
//Origional source:
//https://github.com/observablehq/inspector/blob/main/src/formatString.js
}
editExpanded(object, _, name){
const span = document.createElement("span");
span.textContent = name;
return span;
//TODO
//Original source:
//https://github.com/observablehq/inspector/blob/main/src/expanded.js
}
editCollapsed(object, shallow, name){
const span = document.createElement("span");
span.textContent = name;
return span;
//TODO
//Original source:
//https://github.com/observablehq/inspector/blob/main/src/collapsed.js
}
formatString(string, shallow, expanded, name){
const span = document.createElement("span");
span.textContent = string;
return span;
//TODO
//Origional source:
//https://github.com/observablehq/inspector/blob/main/src/inspectFunction.js
}
formatSymbol(symbol) {
return Symbol.prototype.toString.call(symbol);
}
formatDate(date) {
return "mocked-date-string" + date;
//TODO
//Origional source:
//https://github.com/observablehq/inspector/blob/main/src/formatDate.js
}
formatRegExp(value) {
return RegExp.prototype.toString.call(value);
}
formatError(value) {
return value.stack || Error.prototype.toString.call(value);
}
dispatch(node, type, detail) {
detail = detail || {};
var document = node.ownerDocument, event = document.defaultView.CustomEvent;
if (typeof event === "function") {
event = new event(type, {detail: detail});
} else {
event = document.createEvent("Event");
event.initEvent(type, false, false);
event.detail = detail;
}
node.dispatchEvent(event);
}
}
// Returns true if the given value is something that should be added to the DOM
// by the inspector, rather than being inspected. This deliberately excludes
// DocumentFragment since appending a fragment “dissolves” (mutates) the
// fragment, and we wish for the inspector to not have side-effects. Also,
// HTMLElement.prototype is an instanceof Element, but not an element!
function isnode(value) {
return (value instanceof Element || value instanceof Text)
&& (value instanceof value.constructor);
}
Editor.into = function(container, runtime) {
if (typeof container === "string") {
container = document.querySelector(container);
if (container == null) throw new Error("container not found");
}
return function() {
let element = container.appendChild(document.createElement("div"));
return new Editor(element, runtime);
};
};
```
Usage example:
```
<!DOCTYPE html>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@observablehq/inspector@3/dist/inspector.css">
<body>
<!-- Also see
https://github.com/observablehq/runtime
https://github.com/observablehq/runtime/issues/322
-->
<script type="module">
import {Runtime} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js";
import Editor from "./editor.js";
//import define from "https://api.observablehq.com/@tmcw/hello-world.js?v=3";
import define from "./797362a5c2538221@23.js"
const runtime = new Runtime();
const editor = Editor.into(document.body, runtime);
const module = runtime.module(define, editor);
</script>
```
Exported observablehq notebook:
```
export default function define(runtime, observer) {
const main = runtime.module();
main.variable(observer()).define(["md"], function(md){return(
md`# Number demo`
)});
main.variable(observer("z")).define("z", ["b"], function(b){return(
b+3
)});
main.variable(observer("b")).define("b", function(){return(
1
)});
main.variable(observer("a")).define("a", ["b"], function(b){return(
b+2
)});
main.variable(observer("c")).define("c", ["z"], function(z){return(
z-3
)});
main.variable(observer("v")).define("v", ["c","z"], function(c,z){return(
c+z
)});
return main;
}
```