Merge branch 'dev' into 'main'
Basic graph editing See merge request dao-governance-framework/science-publishing-dao!9
This commit is contained in:
commit
ae5ab09e16
|
@ -1,4 +1,4 @@
|
||||||
import { WDAG } from '../supporting/wdag.js';
|
import { WeightedDirectedGraph } from '../supporting/wdg.js';
|
||||||
import { Action } from '../display/action.js';
|
import { Action } from '../display/action.js';
|
||||||
import { Actor } from '../display/actor.js';
|
import { Actor } from '../display/actor.js';
|
||||||
import { ReputationHolder } from '../reputation/reputation-holder.js';
|
import { ReputationHolder } from '../reputation/reputation-holder.js';
|
||||||
|
@ -51,7 +51,7 @@ export class Forum extends ReputationHolder {
|
||||||
super(name, scene);
|
super(name, scene);
|
||||||
this.dao = dao;
|
this.dao = dao;
|
||||||
this.id = this.reputationPublicKey;
|
this.id = this.reputationPublicKey;
|
||||||
this.graph = new WDAG(scene);
|
this.graph = new WeightedDirectedGraph(scene);
|
||||||
this.actions = {
|
this.actions = {
|
||||||
propagate: new Action('propagate', scene),
|
propagate: new Action('propagate', scene),
|
||||||
confirm: new Action('confirm', scene),
|
confirm: new Action('confirm', scene),
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { Box } from './box.js';
|
||||||
import { Form } from './form.js';
|
import { Form } from './form.js';
|
||||||
|
|
||||||
export class Remark extends Box {
|
export class Remark extends Box {
|
||||||
constructor(doc, text, opts) {
|
constructor(doc, text, opts = {}) {
|
||||||
super('Remark', doc.el, opts);
|
super('Remark', opts.parentEl ?? doc.el, opts);
|
||||||
this.setInnerHTML(text);
|
this.setInnerHTML(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,10 @@ export class Remark extends Box {
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class Document extends Box {
|
export class Document extends Box {
|
||||||
form() {
|
elements = [];
|
||||||
return this.addElement(new Form(this));
|
|
||||||
|
form(opts) {
|
||||||
|
return this.addElement(new Form(this, opts));
|
||||||
}
|
}
|
||||||
|
|
||||||
remark(text, opts) {
|
remark(text, opts) {
|
||||||
|
@ -25,13 +27,17 @@ export class Document extends Box {
|
||||||
}
|
}
|
||||||
|
|
||||||
addElement(element) {
|
addElement(element) {
|
||||||
this.elements = this.elements ?? [];
|
|
||||||
this.elements.push(element);
|
this.elements.push(element);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.el.innerHTML = '';
|
||||||
|
this.elements = [];
|
||||||
|
}
|
||||||
|
|
||||||
get lastElement() {
|
get lastElement() {
|
||||||
if (!this.elements?.length) return null;
|
if (!this.elements.length) return null;
|
||||||
return this.elements[this.elements.length - 1];
|
return this.elements[this.elements.length - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,11 @@ import { MermaidDiagram } from './mermaid.js';
|
||||||
export class Flowchart extends MermaidDiagram {
|
export class Flowchart extends MermaidDiagram {
|
||||||
constructor(box, logBox, direction = 'BT') {
|
constructor(box, logBox, direction = 'BT') {
|
||||||
super(box, logBox);
|
super(box, logBox);
|
||||||
|
this.direction = direction;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
this.log(`graph ${direction}`, false);
|
init() {
|
||||||
|
this.log(`graph ${this.direction}`, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,27 +4,43 @@ import { Box } from './box.js';
|
||||||
const updateValuesOnEventTypes = ['keyup', 'mouseup'];
|
const updateValuesOnEventTypes = ['keyup', 'mouseup'];
|
||||||
|
|
||||||
export class FormElement extends Box {
|
export class FormElement extends Box {
|
||||||
constructor(name, parentEl, opts) {
|
constructor(name, form, opts) {
|
||||||
|
const parentEl = opts.parentEl ?? form.el;
|
||||||
super(name, parentEl, opts);
|
super(name, parentEl, opts);
|
||||||
|
this.form = form;
|
||||||
this.id = opts.id ?? name;
|
this.id = opts.id ?? name;
|
||||||
|
this.includeInOutput = opts.includeInOutput ?? true;
|
||||||
const { cb } = opts;
|
const { cb } = opts;
|
||||||
if (cb) {
|
if (cb) {
|
||||||
updateValuesOnEventTypes.forEach((eventType) => this.el.addEventListener(eventType, () => {
|
updateValuesOnEventTypes.forEach((eventType) => this.el.addEventListener(eventType, () => {
|
||||||
cb(this);
|
cb(this, { initializing: false });
|
||||||
}));
|
}));
|
||||||
cb(this);
|
cb(this, { initializing: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Button extends FormElement { }
|
export class Button extends FormElement {
|
||||||
|
constructor(name, form, opts) {
|
||||||
|
super(name, form, opts);
|
||||||
|
this.button = document.createElement('button');
|
||||||
|
this.button.setAttribute('type', 'button');
|
||||||
|
this.button.innerHTML = name;
|
||||||
|
this.button.disabled = !!opts.disabled;
|
||||||
|
this.el.appendChild(this.button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class TextField extends FormElement {
|
export class TextField extends FormElement {
|
||||||
constructor(name, parentEl, opts) {
|
constructor(name, form, opts) {
|
||||||
super(name, parentEl, opts);
|
super(name, form, opts);
|
||||||
this.label = document.createElement('label');
|
this.label = document.createElement('label');
|
||||||
this.label.innerHTML = name;
|
this.labelDiv = document.createElement('div');
|
||||||
|
this.label.appendChild(this.labelDiv);
|
||||||
|
this.labelDiv.innerHTML = name;
|
||||||
this.input = document.createElement('input');
|
this.input = document.createElement('input');
|
||||||
|
this.input.disabled = !!opts.disabled;
|
||||||
|
this.input.defaultValue = opts.defaultValue || '';
|
||||||
this.label.appendChild(this.input);
|
this.label.appendChild(this.input);
|
||||||
this.el.appendChild(this.label);
|
this.el.appendChild(this.label);
|
||||||
}
|
}
|
||||||
|
@ -36,25 +52,83 @@ export class TextField extends FormElement {
|
||||||
|
|
||||||
export class TextArea extends FormElement { }
|
export class TextArea extends FormElement { }
|
||||||
|
|
||||||
export class Form {
|
export class SubFormArray extends FormElement {
|
||||||
|
constructor(name, form, opts) {
|
||||||
|
super(name, form, opts);
|
||||||
|
this.subForms = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.subForms.map((subForm) => subForm.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(subForm) {
|
||||||
|
const idx = this.subForms.findIndex((s) => s === subForm);
|
||||||
|
this.subForms.splice(idx, 1);
|
||||||
|
subForm.el.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubForm extends FormElement {
|
||||||
|
constructor(name, form, opts) {
|
||||||
|
const parentEl = opts.subFormArray ? opts.subFormArray.el : form.el;
|
||||||
|
const subForm = form.document.form({ name, parentEl }).lastElement;
|
||||||
|
super(name, form, { ...opts, parentEl });
|
||||||
|
this.subForm = subForm;
|
||||||
|
if (opts.subFormArray) {
|
||||||
|
opts.subFormArray.subForms.push(this);
|
||||||
|
this.includeInOutput = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.subForm.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Form extends Box {
|
||||||
constructor(document, opts = {}) {
|
constructor(document, opts = {}) {
|
||||||
|
super(opts.name, opts.parentEl || document.el, opts);
|
||||||
this.document = document;
|
this.document = document;
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.id = opts.id ?? `form_${randomID()}`;
|
this.id = opts.id ?? `form_${randomID()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
button(opts) {
|
button(opts) {
|
||||||
this.items.push(new Button(opts.name, this.document.el, opts));
|
this.items.push(new Button(opts.name, this, opts));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
textField(opts) {
|
textField(opts) {
|
||||||
this.items.push(new TextField(opts.name, this.document.el, opts));
|
this.items.push(new TextField(opts.name, this, opts));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
textArea(opts) {
|
textArea(opts) {
|
||||||
this.items.push(new TextArea(opts.name, this.document.el, opts));
|
this.items.push(new TextArea(opts.name, this, opts));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subForm(opts) {
|
||||||
|
this.items.push(new SubForm(opts.name, this, opts));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
subFormArray(opts) {
|
||||||
|
this.items.push(new SubFormArray(opts.name, this, opts));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastItem() {
|
||||||
|
return this.items[this.items.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.items.reduce((result, { id, value, includeInOutput }) => {
|
||||||
|
if (includeInOutput && value !== undefined) {
|
||||||
|
result[id] = value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ export class MermaidDiagram {
|
||||||
activationBkgColor: '#1d3f49',
|
activationBkgColor: '#1d3f49',
|
||||||
activationBorderColor: '#569595',
|
activationBorderColor: '#569595',
|
||||||
},
|
},
|
||||||
|
securityLevel: 'loose', // 'loose' so that we can use click events
|
||||||
|
// logLevel: 'debug',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,15 +53,24 @@ export class MermaidDiagram {
|
||||||
return debounce(async () => {
|
return debounce(async () => {
|
||||||
const text = this.getText();
|
const text = this.getText();
|
||||||
try {
|
try {
|
||||||
const graph = await mermaid.mermaidAPI.render(
|
await mermaid.mermaidAPI.render(
|
||||||
this.element.getId(),
|
this.element.getId(),
|
||||||
text,
|
text,
|
||||||
|
(svgCode, bindFunctions) => {
|
||||||
|
this.renderBox.setInnerHTML(svgCode);
|
||||||
|
if (bindFunctions) {
|
||||||
|
bindFunctions(this.renderBox.el);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
this.renderBox.setInnerHTML(graph);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`render text:\n${text}`);
|
console.error(`render text:\n${text}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.logBoxPre.textContent = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ export class Scene {
|
||||||
this.actors = new Set();
|
this.actors = new Set();
|
||||||
this.dateStart = new Date();
|
this.dateStart = new Date();
|
||||||
this.flowcharts = new Map();
|
this.flowcharts = new Map();
|
||||||
|
this.documents = [];
|
||||||
|
|
||||||
MermaidDiagram.initializeAPI();
|
MermaidDiagram.initializeAPI();
|
||||||
|
|
||||||
|
@ -60,8 +61,7 @@ export class Scene {
|
||||||
const container = this.middleSection.addBox(name).flex();
|
const container = this.middleSection.addBox(name).flex();
|
||||||
const box = container.addBox('Flowchart').addClass('padded');
|
const box = container.addBox('Flowchart').addClass('padded');
|
||||||
const logBox = container.addBox('Flowchart text').addClass('dim');
|
const logBox = container.addBox('Flowchart text').addClass('dim');
|
||||||
const flowchart = new MermaidDiagram(box, logBox);
|
const flowchart = new Flowchart(box, logBox, direction);
|
||||||
flowchart.log(`graph ${direction}`, false);
|
|
||||||
this.flowcharts.set(id, flowchart);
|
this.flowcharts.set(id, flowchart);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,10 @@ export class Scene {
|
||||||
return this.documents[this.documents.length - 1];
|
return this.documents[this.documents.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDocument(name) {
|
||||||
|
return this.documents.find((doc) => doc.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
registerActor(actor) {
|
registerActor(actor) {
|
||||||
this.actors.add(actor);
|
this.actors.add(actor);
|
||||||
if (actor.options.announce) {
|
if (actor.options.announce) {
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
export class Edge {
|
||||||
|
constructor(graph, type, from, to, weight, data, options = {}) {
|
||||||
|
this.graph = graph;
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
this.type = type;
|
||||||
|
this.weight = weight;
|
||||||
|
this.data = data;
|
||||||
|
this.options = options;
|
||||||
|
this.installedClickCallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.installedClickCallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getKey({
|
||||||
|
from, to, type,
|
||||||
|
}) {
|
||||||
|
return ['edge', from.id, to.id, type].join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCombinedKey({ from, to }) {
|
||||||
|
return ['edge', from.id, to.id].join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
getComorphicEdges() {
|
||||||
|
return this.graph.getEdges(null, this.from, this.to);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHtml() {
|
||||||
|
let html = '<table>';
|
||||||
|
for (const { type, weight } of this.getComorphicEdges()) {
|
||||||
|
html += `<tr><td>${type}</td><td>${weight}</td></tr>`;
|
||||||
|
}
|
||||||
|
html += '</table>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFlowchartNode() {
|
||||||
|
return `${Edge.getCombinedKey(this)}(${this.getHtml()})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayEdgeNode() {
|
||||||
|
if (this.options.hide) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.graph.flowchart?.log(this.getFlowchartNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
displayEdge() {
|
||||||
|
if (this.options.hide) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`);
|
||||||
|
this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`);
|
||||||
|
if (this.graph.editable && !this.installedClickCallback) {
|
||||||
|
this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} "Edit Edge"`);
|
||||||
|
this.installedClickCallback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static prepareEditorDocument(graph, doc, from, to) {
|
||||||
|
doc.clear();
|
||||||
|
const form = doc.form({ name: 'editorForm' }).lastElement;
|
||||||
|
doc.remark('<h3>Edit Edge</h3>', { parentEl: form.el });
|
||||||
|
form
|
||||||
|
.textField({
|
||||||
|
id: 'from', name: 'from', defaultValue: from, disabled: true,
|
||||||
|
})
|
||||||
|
.textField({
|
||||||
|
id: 'to', name: 'to', defaultValue: to, disabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem;
|
||||||
|
const addEdgeForm = (edge) => {
|
||||||
|
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
|
||||||
|
doc.remark('<br/>', { parentEl: subForm.el });
|
||||||
|
subForm.textField({ id: 'type', name: 'type', defaultValue: edge.type });
|
||||||
|
subForm.textField({ id: 'weight', name: 'weight', defaultValue: edge.weight });
|
||||||
|
subForm.button({
|
||||||
|
id: 'remove',
|
||||||
|
name: 'Remove Edge Type',
|
||||||
|
cb: (_, { initializing }) => {
|
||||||
|
if (initializing) return;
|
||||||
|
subFormArray.remove(subForm);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const edge of graph.getEdges(null, from, to)) {
|
||||||
|
addEdgeForm(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.button({
|
||||||
|
id: 'add',
|
||||||
|
name: 'Add Edge Type',
|
||||||
|
cb: (_, { initializing }) => {
|
||||||
|
if (initializing) return;
|
||||||
|
addEdgeForm(new Edge(graph, null, graph.getVertex(from), graph.getVertex(to)));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
form.button({
|
||||||
|
id: 'save',
|
||||||
|
name: 'Save',
|
||||||
|
cb: ({ form: { value } }, { initializing }) => {
|
||||||
|
if (initializing) return;
|
||||||
|
// Handle additions and updates
|
||||||
|
for (const { type, weight } of value.edges) {
|
||||||
|
graph.setEdgeWeight(type, from, to, weight);
|
||||||
|
}
|
||||||
|
// Handle removals
|
||||||
|
for (const edge of graph.getEdges(null, from, to)) {
|
||||||
|
if (!value.edges.find(({ type }) => type === edge.type)) {
|
||||||
|
graph.deleteEdge(edge.type, from, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graph.redraw();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
export class Vertex {
|
||||||
|
constructor(graph, type, id, data, options = {}) {
|
||||||
|
this.graph = graph;
|
||||||
|
this.type = type;
|
||||||
|
this._id = id;
|
||||||
|
this.data = data;
|
||||||
|
this.options = options;
|
||||||
|
this.edges = {
|
||||||
|
from: [],
|
||||||
|
to: [],
|
||||||
|
};
|
||||||
|
this.installedClickCallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.installedClickCallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set id(newId) {
|
||||||
|
this._id = newId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdges(type, away) {
|
||||||
|
return this.edges[away ? 'from' : 'to'].filter(
|
||||||
|
(edge) => edge.type === type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisplayLabel(label) {
|
||||||
|
this.label = label;
|
||||||
|
this.displayVertex();
|
||||||
|
}
|
||||||
|
|
||||||
|
displayVertex() {
|
||||||
|
if (this.options.hide) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.graph.flowchart?.log(`${this.id}[${this.label}]`);
|
||||||
|
if (this.graph.editable && !this.installedClickCallback) {
|
||||||
|
this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex"`);
|
||||||
|
this.installedClickCallback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static prepareEditorDocument(graph, doc, vertexId) {
|
||||||
|
doc.clear();
|
||||||
|
const vertex = graph.getVertex(vertexId);
|
||||||
|
if (!vertex) {
|
||||||
|
throw new Error(`Could not find WDG Vertex ${vertexId}`);
|
||||||
|
}
|
||||||
|
doc.remark('<h3>Edit Vertex</h3>');
|
||||||
|
const form = doc.form().lastElement;
|
||||||
|
form
|
||||||
|
.textField({
|
||||||
|
id: 'id', name: 'id', defaultValue: vertex.id, disabled: true,
|
||||||
|
})
|
||||||
|
.textField({ id: 'type', name: 'type', defaultValue: vertex.type })
|
||||||
|
.textField({ id: 'label', name: 'label', defaultValue: vertex.label })
|
||||||
|
|
||||||
|
.button({
|
||||||
|
id: 'save',
|
||||||
|
name: 'Save',
|
||||||
|
cb: ({ form: { value } }, { initializing }) => {
|
||||||
|
if (initializing) return;
|
||||||
|
if (value.id && value.id !== vertex.id) {
|
||||||
|
// TODO: When an ID changes we really need to wipe out and redraw!
|
||||||
|
// But we don't yet have a systematic approach for doing that.
|
||||||
|
// Everything is getting rendered as needed. Lacking abstraction.
|
||||||
|
// HMM we're not actually that far! Just wipe everything out and draw each vertex and edge :)
|
||||||
|
// for (const vertex of )
|
||||||
|
// for (const edge of [...vertex.edges.to, ...vertex.edges.from]) {
|
||||||
|
// edge.displayEdge();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
Object.assign(vertex, value);
|
||||||
|
vertex.displayVertex();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,175 +0,0 @@
|
||||||
const getEdgeKey = ({ from, to }) => btoa([from.id, to.id]).replaceAll(/[^A-Za-z0-9]+/g, '');
|
|
||||||
|
|
||||||
export class Vertex {
|
|
||||||
constructor(graph, type, id, data, options = {}) {
|
|
||||||
this.graph = graph;
|
|
||||||
this.type = type;
|
|
||||||
this.id = id;
|
|
||||||
this.data = data;
|
|
||||||
this.options = options;
|
|
||||||
this.edges = {
|
|
||||||
from: [],
|
|
||||||
to: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdges(type, away) {
|
|
||||||
return this.edges[away ? 'from' : 'to'].filter(
|
|
||||||
(edge) => edge.type === type,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDisplayLabel(label) {
|
|
||||||
if (this.options.hide) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.graph.flowchart?.log(`${this.id}[${label}]`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Edge {
|
|
||||||
constructor(graph, type, from, to, weight, data, options = {}) {
|
|
||||||
this.graph = graph;
|
|
||||||
this.from = from;
|
|
||||||
this.to = to;
|
|
||||||
this.type = type;
|
|
||||||
this.weight = weight;
|
|
||||||
this.data = data;
|
|
||||||
this.options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHtml() {
|
|
||||||
let html = '<table>';
|
|
||||||
for (const { type, weight } of this.graph.getEdges(null, this.from, this.to)) {
|
|
||||||
html += `<tr><td>${type}</td><td>${weight}</td></tr>`;
|
|
||||||
}
|
|
||||||
html += '</table>';
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFlowchartNode() {
|
|
||||||
return `${getEdgeKey(this)}(${this.getHtml()})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
displayEdgeNode() {
|
|
||||||
if (this.options.hide) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.graph.flowchart?.log(this.getFlowchartNode());
|
|
||||||
}
|
|
||||||
|
|
||||||
displayEdge() {
|
|
||||||
if (this.options.hide) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`);
|
|
||||||
this.graph.flowchart?.log(`class ${getEdgeKey(this)} edge`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WDAG {
|
|
||||||
constructor(scene) {
|
|
||||||
this.scene = scene;
|
|
||||||
this.vertices = new Map();
|
|
||||||
this.edgeTypes = new Map();
|
|
||||||
this.nextVertexId = 0;
|
|
||||||
this.flowchart = scene?.flowchart;
|
|
||||||
}
|
|
||||||
|
|
||||||
withFlowchart() {
|
|
||||||
this.scene?.withAdditionalFlowchart();
|
|
||||||
this.flowchart = this.scene?.lastFlowchart();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
addVertex(type, id, data, label, options) {
|
|
||||||
// Support simple case of auto-incremented numeric ids
|
|
||||||
if (typeof id === 'object') {
|
|
||||||
data = id;
|
|
||||||
id = this.nextVertexId++;
|
|
||||||
}
|
|
||||||
if (this.vertices.has(id)) {
|
|
||||||
throw new Error(`Vertex already exists with id: ${id}`);
|
|
||||||
}
|
|
||||||
const vertex = new Vertex(this, type, id, data, options);
|
|
||||||
this.vertices.set(id, vertex);
|
|
||||||
vertex.setDisplayLabel(label ?? id);
|
|
||||||
return vertex;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVertex(id) {
|
|
||||||
return this.vertices.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getVertexData(id) {
|
|
||||||
return this.getVertex(id)?.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVerticesData() {
|
|
||||||
return Array.from(this.vertices.values()).map(({ data }) => data);
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdge(type, from, to) {
|
|
||||||
from = from instanceof Vertex ? from : this.getVertex(from);
|
|
||||||
to = to instanceof Vertex ? to : this.getVertex(to);
|
|
||||||
if (!from || !to) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const edges = this.edgeTypes.get(type);
|
|
||||||
const edgeKey = getEdgeKey({ from, to });
|
|
||||||
return edges?.get(edgeKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdgeWeight(type, from, to) {
|
|
||||||
return this.getEdge(type, from, to)?.weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEdgeWeight(type, from, to, weight, data, options) {
|
|
||||||
from = from instanceof Vertex ? from : this.getVertex(from);
|
|
||||||
to = to instanceof Vertex ? to : this.getVertex(to);
|
|
||||||
const edge = new Edge(this, type, from, to, weight, data, options);
|
|
||||||
let edges = this.edgeTypes.get(type);
|
|
||||||
if (!edges) {
|
|
||||||
edges = new Map();
|
|
||||||
this.edgeTypes.set(type, edges);
|
|
||||||
}
|
|
||||||
const edgeKey = getEdgeKey(edge);
|
|
||||||
edges.set(edgeKey, edge);
|
|
||||||
edge.displayEdgeNode();
|
|
||||||
return edge;
|
|
||||||
}
|
|
||||||
|
|
||||||
addEdge(type, from, to, weight, data, options) {
|
|
||||||
from = from instanceof Vertex ? from : this.getVertex(from);
|
|
||||||
to = to instanceof Vertex ? to : this.getVertex(to);
|
|
||||||
if (this.getEdge(type, from, to)) {
|
|
||||||
throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
|
|
||||||
}
|
|
||||||
const edge = this.setEdgeWeight(type, from, to, weight, data, options);
|
|
||||||
from.edges.from.push(edge);
|
|
||||||
to.edges.to.push(edge);
|
|
||||||
edge.displayEdge();
|
|
||||||
return edge;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdges(type, from, to) {
|
|
||||||
from = from instanceof Vertex ? from : this.getVertex(from);
|
|
||||||
to = to instanceof Vertex ? to : this.getVertex(to);
|
|
||||||
const edgeTypes = type ? [type] : Array.from(this.edgeTypes.keys());
|
|
||||||
return edgeTypes.flatMap((edgeType) => {
|
|
||||||
const edges = this.edgeTypes.get(edgeType);
|
|
||||||
return Array.from(edges?.values() || []).filter((edge) => {
|
|
||||||
const matchFrom = from === null || from === undefined || from === edge.from;
|
|
||||||
const matchTo = to === null || to === undefined || to === edge.to;
|
|
||||||
return matchFrom && matchTo;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
countVertices(type) {
|
|
||||||
if (!type) {
|
|
||||||
return this.vertices.size;
|
|
||||||
}
|
|
||||||
return Array.from(this.vertices.values()).filter((vertex) => vertex.type === type).length;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
import { Vertex } from './vertex.js';
|
||||||
|
import { Edge } from './edge.js';
|
||||||
|
|
||||||
|
const graphs = [];
|
||||||
|
|
||||||
|
const makeWDGHandler = (graphIndex) => (vertexId) => {
|
||||||
|
const graph = graphs[graphIndex];
|
||||||
|
// We want a document for editing this node, which may be a vertex or an edge
|
||||||
|
const editorDoc = graph.scene.getDocument('editorDocument')
|
||||||
|
?? graph.scene.withDocument('editorDocument').lastDocument;
|
||||||
|
if (vertexId.startsWith('edge:')) {
|
||||||
|
const [, from, to] = vertexId.split(':');
|
||||||
|
Edge.prepareEditorDocument(graph, editorDoc, from, to);
|
||||||
|
} else {
|
||||||
|
Vertex.prepareEditorDocument(graph, editorDoc, vertexId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WeightedDirectedGraph {
|
||||||
|
constructor(scene, options = {}) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.vertices = new Map();
|
||||||
|
this.edgeTypes = new Map();
|
||||||
|
this.nextVertexId = 0;
|
||||||
|
this.flowchart = scene?.flowchart;
|
||||||
|
this.editable = options.editable;
|
||||||
|
this.index = graphs.length;
|
||||||
|
graphs.push(this);
|
||||||
|
// Mermaid supports a click callback, but we can't customize arguments; we just get the vertex ID.
|
||||||
|
// In order to provide the appropriate graph context for each callback, we create a separate callback
|
||||||
|
// function for each graph.
|
||||||
|
window[`WDGHandler${this.index}`] = makeWDGHandler(this.index);
|
||||||
|
this.history = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getHistory() {
|
||||||
|
// record operations that modify the graph
|
||||||
|
return this.history;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
vertices: Array.from(this.vertices.values()),
|
||||||
|
edgeTypes: Array.from(this.edgeTypes.keys()),
|
||||||
|
edges: Array.from(this.edgeTypes.values()).flatMap((edges) => Array.from(edges.values())),
|
||||||
|
history: this.getHistory(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw() {
|
||||||
|
// Call .reset() on all vertices and edges
|
||||||
|
for (const vertex of this.vertices.values()) {
|
||||||
|
vertex.reset();
|
||||||
|
}
|
||||||
|
for (const edges of this.edgeTypes.values()) {
|
||||||
|
for (const edge of edges.values()) {
|
||||||
|
edge.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the target div
|
||||||
|
this.flowchart?.reset();
|
||||||
|
this.flowchart?.init();
|
||||||
|
|
||||||
|
// Draw all vertices and edges
|
||||||
|
for (const vertex of this.vertices.values()) {
|
||||||
|
vertex.displayVertex();
|
||||||
|
}
|
||||||
|
// Let's flatmap and dedupe by [from, to] since each edge
|
||||||
|
// renders all comorphic edges as well.
|
||||||
|
const edgesFrom = new Map(); // edgeMap[from][to] = edge
|
||||||
|
for (const edges of this.edgeTypes.values()) {
|
||||||
|
for (const edge of edges.values()) {
|
||||||
|
const edgesTo = edgesFrom.get(edge.from) || new Map();
|
||||||
|
edgesTo.set(edge.to, edge);
|
||||||
|
edgesFrom.set(edge.from, edgesTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edgesTo of edgesFrom.values()) {
|
||||||
|
for (const edge of edgesTo.values()) {
|
||||||
|
edge.displayEdge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rerender
|
||||||
|
this.flowchart?.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
withFlowchart() {
|
||||||
|
this.scene?.withAdditionalFlowchart();
|
||||||
|
this.flowchart = this.scene?.lastFlowchart();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVertex(type, id, data, label, options) {
|
||||||
|
// Supports simple case of auto-incremented numeric ids
|
||||||
|
if (typeof id === 'object') {
|
||||||
|
data = id;
|
||||||
|
id = this.nextVertexId++;
|
||||||
|
}
|
||||||
|
id = (typeof id === 'number') ? id.toString(10) : id;
|
||||||
|
if (this.vertices.has(id)) {
|
||||||
|
throw new Error(`Vertex already exists with id: ${id}`);
|
||||||
|
}
|
||||||
|
const vertex = new Vertex(this, type, id, data, options);
|
||||||
|
this.vertices.set(id, vertex);
|
||||||
|
vertex.setDisplayLabel(label ?? id);
|
||||||
|
return vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVertex(id) {
|
||||||
|
id = (typeof id === 'number') ? id.toString(10) : id;
|
||||||
|
return this.vertices.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVertexData(id) {
|
||||||
|
return this.getVertex(id)?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVerticesData() {
|
||||||
|
return Array.from(this.vertices.values()).map(({ data }) => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdge(type, from, to) {
|
||||||
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
if (!from || !to) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const edges = this.edgeTypes.get(type);
|
||||||
|
const edgeKey = Edge.getKey({ from, to, type });
|
||||||
|
return edges?.get(edgeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteEdge(type, from, to) {
|
||||||
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
const edges = this.edgeTypes.get(type);
|
||||||
|
const edgeKey = Edge.getKey({ type, from, to });
|
||||||
|
if (!edges) return;
|
||||||
|
const edge = edges.get(edgeKey);
|
||||||
|
if (!edge) return;
|
||||||
|
to.edges.from.forEach((x, i) => (x === edge) && to.edges.from.splice(i, 1));
|
||||||
|
from.edges.to.forEach((x, i) => (x === edge) && from.edges.to.splice(i, 1));
|
||||||
|
edges.delete(edgeKey);
|
||||||
|
if (edges.size === 0) {
|
||||||
|
this.edgeTypes.delete(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdgeWeight(type, from, to) {
|
||||||
|
return this.getEdge(type, from, to)?.weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEdgeWeight(type, from, to, weight, data, options) {
|
||||||
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
const edge = new Edge(this, type, from, to, weight, data, options);
|
||||||
|
let edges = this.edgeTypes.get(type);
|
||||||
|
if (!edges) {
|
||||||
|
edges = new Map();
|
||||||
|
this.edgeTypes.set(type, edges);
|
||||||
|
}
|
||||||
|
const edgeKey = Edge.getKey(edge);
|
||||||
|
edges.set(edgeKey, edge);
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdge(type, from, to, weight, data, options) {
|
||||||
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
const existingEdges = this.getEdges(type, from, to);
|
||||||
|
if (this.getEdge(type, from, to)) {
|
||||||
|
throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
|
||||||
|
}
|
||||||
|
const edge = this.setEdgeWeight(type, from, to, weight, data, options);
|
||||||
|
from.edges.from.push(edge);
|
||||||
|
to.edges.to.push(edge);
|
||||||
|
if (existingEdges.length) {
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
} else {
|
||||||
|
edge.displayEdge();
|
||||||
|
}
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdges(type, from, to) {
|
||||||
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
const edgeTypes = type ? [type] : Array.from(this.edgeTypes.keys());
|
||||||
|
return edgeTypes.flatMap((edgeType) => {
|
||||||
|
const edges = this.edgeTypes.get(edgeType);
|
||||||
|
return Array.from(edges?.values() || []).filter((edge) => {
|
||||||
|
const matchFrom = from === null || from === undefined || from === edge.from;
|
||||||
|
const matchTo = to === null || to === undefined || to === edge.to;
|
||||||
|
return matchFrom && matchTo;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
countVertices(type) {
|
||||||
|
if (!type) {
|
||||||
|
return this.vertices.size;
|
||||||
|
}
|
||||||
|
return Array.from(this.vertices.values()).filter((vertex) => vertex.type === type).length;
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,3 +79,7 @@ label {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
}
|
}
|
||||||
|
label > div {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
|
@ -55,11 +55,12 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="./tests/basic.test.html">Basic Sequencing</a></li>
|
<li><a href="./tests/basic.test.html">Basic Sequencing</a></li>
|
||||||
<li><a href="./tests/basic2.test.html">Basic Sequencing 2</a></li>
|
<li><a href="./tests/basic2.test.html">Basic Sequencing 2</a></li>
|
||||||
<li><a href="./tests/wdag.test.html">WDAG</a></li>
|
<li><a href="./tests/wdg.test.html">Weighted Directed Graph</a></li>
|
||||||
<li><a href="./tests/debounce.test.html">Debounce</a></li>
|
<li><a href="./tests/debounce.test.html">Debounce</a></li>
|
||||||
<li><a href="./tests/flowchart.test.html">Flowchart</a></li>
|
<li><a href="./tests/flowchart.test.html">Flowchart</a></li>
|
||||||
<li><a href="./tests/mocha.test.html">Mocha</a></li>
|
<li><a href="./tests/mocha.test.html">Mocha</a></li>
|
||||||
<li><a href="./tests/input.test.html">Input</a></li>
|
<li><a href="./tests/input.test.html">Input</a></li>
|
||||||
|
<li><a href="./tests/document.test.html">Document</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<h4><a href="./tests/all.test.html">All</a></h4>
|
<h4><a href="./tests/all.test.html">All</a></h4>
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<script type="module" src="./scripts/mocha.test.js"></script>
|
<script type="module" src="./scripts/mocha.test.js"></script>
|
||||||
<script type="module" src="./scripts/validation-pool.test.js"></script>
|
<script type="module" src="./scripts/validation-pool.test.js"></script>
|
||||||
<script type="module" src="./scripts/vm.test.js"></script>
|
<script type="module" src="./scripts/vm.test.js"></script>
|
||||||
<script type="module" src="./scripts/wdag.test.js"></script>
|
<script type="module" src="./scripts/wdg.test.js"></script>
|
||||||
<script type="module" src="./scripts/forum/forum1.test.js"></script>
|
<script type="module" src="./scripts/forum/forum1.test.js"></script>
|
||||||
<script type="module" src="./scripts/forum/forum2.test.js"></script>
|
<script type="module" src="./scripts/forum/forum2.test.js"></script>
|
||||||
<script type="module" src="./scripts/forum/forum3.test.js"></script>
|
<script type="module" src="./scripts/forum/forum3.test.js"></script>
|
||||||
|
@ -38,6 +38,7 @@
|
||||||
<script type="module" src="./scripts/forum/forum10.test.js"></script>
|
<script type="module" src="./scripts/forum/forum10.test.js"></script>
|
||||||
<script type="module" src="./scripts/forum/forum11.test.js"></script>
|
<script type="module" src="./scripts/forum/forum11.test.js"></script>
|
||||||
<script type="module" src="./scripts/input.test.js"></script>
|
<script type="module" src="./scripts/input.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/document.test.js"></script>
|
||||||
<script defer class="mocha-init">
|
<script defer class="mocha-init">
|
||||||
mocha.setup({
|
mocha.setup({
|
||||||
ui: 'bdd',
|
ui: 'bdd',
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Document</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
|
||||||
|
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2><a href="../">DGF Tests</a></h2>
|
||||||
|
<div id="mocha"></div>
|
||||||
|
<div id="scene"></div>
|
||||||
|
</body>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/radash/10.7.0/radash.js"
|
||||||
|
integrity="sha512-S207zKWG3iqXqe6msO7/Mr8X3DzzF4u8meFlokHjGtBPTGUhgzVo0lpcqEy0GoiMUdcoct+H+SqzoLsxXbynzg=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script src="https://unpkg.com/mocha/mocha.js"></script>
|
||||||
|
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||||
|
<script type="module" src="./scripts/document.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
window.should = chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Box } from '../../classes/display/box.js';
|
||||||
|
// import { Document } from '../../classes/display/document.js';
|
||||||
|
import { Scene } from '../../classes/display/scene.js';
|
||||||
|
import { mochaRun } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('scene');
|
||||||
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
|
const scene = window.scene = new Scene('Document test', rootBox);
|
||||||
|
|
||||||
|
scene.withDocument();
|
||||||
|
|
||||||
|
describe('Document', () => {
|
||||||
|
describe('remark', () => {
|
||||||
|
it('can exist', () => {
|
||||||
|
const docFunction = (doc) => doc.remark('Hello');
|
||||||
|
scene.withDocument('Document', docFunction);
|
||||||
|
});
|
||||||
|
it.skip('can include handlebars expressions', () => { });
|
||||||
|
it.skip('updates rendered output when input changes', () => { });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mochaRun();
|
|
@ -9,26 +9,55 @@ const scene = window.scene = new Scene('Input test', rootBox);
|
||||||
|
|
||||||
scene.withDocument();
|
scene.withDocument();
|
||||||
|
|
||||||
describe('Document', () => {
|
describe('Document > Form > TextField', () => {
|
||||||
it('Exists', () => {
|
before(() => {
|
||||||
scene.withDocument('Document', (doc) => doc.remark('Hello'));
|
scene.withDocument('Document 1', (d) => d.form());
|
||||||
});
|
});
|
||||||
|
it('can accept input and call value update callback', () => {
|
||||||
|
const doc = scene.lastDocument;
|
||||||
|
const form = doc.lastElement;
|
||||||
|
/**
|
||||||
|
* Handler callback for form element value updates.
|
||||||
|
* In this case we use a collection of DisplayValues as a straightforward way to render the form element values.
|
||||||
|
*/
|
||||||
|
const dvMap = new Map();
|
||||||
|
const updateFieldValueDisplay = ({ id, name, value }) => {
|
||||||
|
const dv = dvMap.get(id) ?? scene.addDisplayValue(name);
|
||||||
|
dvMap.set(id, dv);
|
||||||
|
dv.set(value);
|
||||||
|
};
|
||||||
|
|
||||||
describe('Input', () => {
|
form.textField({ id: 'input1', name: 'Input 1', cb: updateFieldValueDisplay });
|
||||||
it('Accepts input', () => {
|
doc.remark('Hmm...!');
|
||||||
scene.withDocument('Document', (doc) => doc.form());
|
});
|
||||||
const doc = scene.lastDocument;
|
// it('can exist within a graph', () => {
|
||||||
const form1 = doc.lastElement;
|
// scene.withAdditionalFlowchart({ id: 'flowchart', name: 'Graph' });
|
||||||
const dvMap = new Map();
|
// const graph = scene.lastFlowchart();
|
||||||
const updateFieldValueDisplay = ({ name, value }) => {
|
// });
|
||||||
const dv = dvMap.get(name) ?? scene.addDisplayValue(name);
|
});
|
||||||
dvMap.set(name, dv);
|
|
||||||
dv.set(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
form1.textField({ id: 'input1', name: 'Input 1', cb: updateFieldValueDisplay });
|
describe('Document > Form > Button', () => {
|
||||||
doc.remark('Hmm...!');
|
before(() => {
|
||||||
});
|
scene.withDocument('Document 2', (d) => d.form());
|
||||||
|
});
|
||||||
|
it('calls a callback when clicked', () => {
|
||||||
|
const doc = scene.lastDocument;
|
||||||
|
const form = doc.lastElement;
|
||||||
|
const dvMap = new Map();
|
||||||
|
let clicks = 0;
|
||||||
|
const handleClick = ({ id, name }, { initializing }) => {
|
||||||
|
const dv = dvMap.get(id) ?? scene.addDisplayValue(name);
|
||||||
|
dvMap.set(id, dv);
|
||||||
|
if (!initializing) {
|
||||||
|
clicks++;
|
||||||
|
dv.set(`clicked ${clicks} time${clicks !== 1 ? 's' : ''}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
doc.remark('<br/>');
|
||||||
|
doc.remark('Button:');
|
||||||
|
form.button({ id: 'button1', name: 'Button 1', cb: handleClick });
|
||||||
|
doc.remark('Yeah?');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { Box } from '../../classes/display/box.js';
|
import { Box } from '../../classes/display/box.js';
|
||||||
import { Scene } from '../../classes/display/scene.js';
|
import { Scene } from '../../classes/display/scene.js';
|
||||||
import { WDAG } from '../../classes/supporting/wdag.js';
|
import { WeightedDirectedGraph } from '../../classes/supporting/wdg.js';
|
||||||
import { mochaRun } from '../../util/helpers.js';
|
import { mochaRun } from '../../util/helpers.js';
|
||||||
|
|
||||||
const rootElement = document.getElementById('scene');
|
const rootElement = document.getElementById('scene');
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
window.scene = new Scene('WDAG test', rootBox);
|
window.scene = new Scene('WDG test', rootBox);
|
||||||
|
|
||||||
describe('Query the graph', function tests() {
|
describe('Weighted Directed Acyclic Graph', function tests() {
|
||||||
this.timeout(0);
|
this.timeout(0);
|
||||||
|
|
||||||
let graph;
|
let graph;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
graph = (window.graph = new WDAG()).withFlowchart();
|
graph = (window.graph = new WeightedDirectedGraph(window.scene)).withFlowchart();
|
||||||
|
|
||||||
graph.addVertex('v1', {});
|
graph.addVertex('v1', {});
|
||||||
graph.addVertex('v1', {});
|
graph.addVertex('v1', {});
|
||||||
|
@ -34,17 +34,35 @@ describe('Query the graph', function tests() {
|
||||||
|
|
||||||
it('can query for all e1 edges from a particular vertex', () => {
|
it('can query for all e1 edges from a particular vertex', () => {
|
||||||
const edges = graph.getEdges('e1', 2);
|
const edges = graph.getEdges('e1', 2);
|
||||||
edges.map(({ from, to, weight }) => [from.id, to.id, weight]).should.have.deep.members([[2, 1, 0.5]]);
|
edges.map(({ from, to, weight }) => [from.id, to.id, weight]).should.have.deep.members([[
|
||||||
|
'2', '1', 0.5,
|
||||||
|
]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can query for all e1 edges to a particular vertex', () => {
|
it('can query for all e1 edges to a particular vertex', () => {
|
||||||
const edges = graph.getEdges('e1', null, 1);
|
const edges = graph.getEdges('e1', null, 1);
|
||||||
edges.map(({ from, to, weight }) => [from.id, to.id, weight]).should.have.deep.members([
|
edges.map(({ from, to, weight }) => [from.id, to.id, weight]).should.have.deep.members([
|
||||||
[0, 1, 1],
|
['0', '1', 1],
|
||||||
[2, 1, 0.5],
|
['2', '1', 0.5],
|
||||||
[3, 1, 0.25],
|
['3', '1', 0.25],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('editable', () => {
|
||||||
|
let graph;
|
||||||
|
|
||||||
|
it('should be editable', () => {
|
||||||
|
graph = (window.graph2 = new WeightedDirectedGraph(window.scene, { editable: true })).withFlowchart();
|
||||||
|
|
||||||
|
graph.addVertex('v1', {});
|
||||||
|
graph.addVertex('v2', {});
|
||||||
|
graph.addVertex('v3', {});
|
||||||
|
|
||||||
|
graph.addEdge('e1', 2, 1, 1);
|
||||||
|
graph.addEdge('e2', 1, 0, 0.5);
|
||||||
|
graph.addEdge('e3', 2, 0, 0.25);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
mochaRun();
|
mochaRun();
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>WDAG test</title>
|
<title>Weighted Directed Graph test</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
|
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
|
||||||
<link type="text/css" rel="stylesheet" href="../index.css" />
|
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||||
|
@ -24,4 +24,4 @@
|
||||||
});
|
});
|
||||||
chai.should();
|
chai.should();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="./scripts/wdag.test.js"></script>
|
<script type="module" src="./scripts/wdg.test.js"></script>
|
Loading…
Reference in New Issue