Files
architype/architype.js

1384 lines
31 KiB
JavaScript
Raw Normal View History

2019-05-25 04:16:20 +00:00
'use strict';
function randStr32() {
let num = Math.floor(Math.random() * Math.pow(2, 32));
return num.toString(16).padStart(8, '0');
}
function randStr64() {
return randStr32() + randStr32();
}
2019-06-21 20:26:41 +00:00
class Architype {
constructor(container) {
this.container_ = container;
this.container_.classList.add('architype');
// TODO: make theme selectable
2019-07-01 20:27:51 +00:00
this.container_.classList.add('dark');
2019-06-21 20:26:41 +00:00
2019-06-25 19:43:32 +00:00
this.container_.addEventListener('keydown', (e) => { this.onKeyDown(e); });
2019-07-01 20:57:22 +00:00
addEventListener('resize', (e) => { this.onResize(e); });
2019-06-25 19:43:32 +00:00
2019-06-21 20:26:41 +00:00
let editorElem = document.createElement('ul');
this.container_.appendChild(editorElem);
this.editor_ = new Editor(editorElem);
2019-06-22 06:36:08 +00:00
this.targets_ = document.createElement('datalist');
this.targets_.id = 'arch-targets';
this.container_.appendChild(this.targets_);
this.grid_ = document.createElement('div');
this.grid_.classList.add('grid');
this.container_.appendChild(this.grid_);
2019-07-01 21:18:26 +00:00
this.unserialize(JSON.parse(localStorage.getItem('currentState')));
this.observeStructure_ = new MutationObserver(e => { this.onStructureChange(e); });
this.observeStructure_.observe(editorElem, {
2019-06-22 06:36:08 +00:00
attributes: true,
attributeFilter: ['data-struct-change'],
2019-06-22 06:36:08 +00:00
childList: true,
subtree: true,
});
2019-06-25 17:48:40 +00:00
this.observeContent_ = new MutationObserver(e => { this.onContentChange(e); });
this.observeContent_.observe(editorElem, {
attributes: true,
attributeFilter: ['data-arch-value'],
subtree: true,
});
2019-07-01 21:18:26 +00:00
this.onStructureChange();
2019-06-25 17:48:40 +00:00
}
serialize() {
return {
version: 1,
editor: this.editor_.serialize(),
};
}
unserialize(ser) {
if (!ser) {
return;
}
switch (ser.version) {
case 1:
this.editor_.unserialize(ser.editor);
break;
default:
console.log('unrecognized localStorage.currentState version', ser);
break;
}
2019-06-22 06:36:08 +00:00
}
onStructureChange(e) {
2019-06-22 06:51:07 +00:00
this.graph_ = this.buildGraph();
this.onContentChange(e);
}
onContentChange(e) {
localStorage.setItem('currentState', JSON.stringify(this.serialize()));
2019-06-28 17:12:44 +00:00
this.buildGrid(this.graph_);
this.updateTargets(this.graph_);
2019-07-01 20:53:20 +00:00
this.fixSizes(this.graph_.nodes);
}
2019-06-25 19:43:32 +00:00
onKeyDown(e) {
switch (e.key) {
case 'z':
this.exportGraphviz();
break;
}
}
2019-07-01 20:53:20 +00:00
onResize(e) {
this.fixSizes(this.graph_.nodes);
}
2019-06-25 19:43:32 +00:00
exportGraphviz() {
let lines = [
'digraph G {',
'\trankdir = "LR";',
];
for (let type of ['nodes', 'links', 'groups']) {
for (let obj of this.graph_[type]) {
for (let line of obj.exportGraphviz()) {
lines.push('\t' + line);
}
}
}
lines.push('}');
2019-06-25 19:43:32 +00:00
navigator.clipboard.writeText(lines.join('\n'));
}
2019-06-28 17:12:44 +00:00
updateTargets(graph) {
// Lots of effort to avoid churning the datalist
let curTargets = new Map();
for (let option of this.targets_.options) {
curTargets.set(option.value, option);
}
2019-06-28 17:12:44 +00:00
for (let [label, entries] of graph.nodesByLabel.entries()) {
if (curTargets.has(label)) {
continue;
}
if (entries.length == 1 &&
document.activeElement.parentElement.xArchObj &&
document.activeElement.parentElement.xArchObj == entries[0]) {
// Skip an element currently being edited
continue;
}
let option = document.createElement('option');
option.value = label;
this.targets_.appendChild(option);
}
for (let [label, option] of curTargets.entries()) {
2019-06-28 17:12:44 +00:00
if (graph.nodesByLabel.has(label)) {
continue;
}
option.remove();
}
2019-06-22 06:36:08 +00:00
}
2019-06-22 06:51:07 +00:00
buildGraph() {
let graph = {
2019-06-26 16:50:03 +00:00
nodesByLabel: new Map(),
nodesByPageRank: new Map(),
2019-06-30 02:17:02 +00:00
nodesByPos: new Map(),
2019-07-01 05:10:34 +00:00
nodesBySubgraph: new Map(),
2019-06-22 06:36:08 +00:00
groups: [],
links: [],
nodes: [],
2019-06-22 06:36:08 +00:00
};
// Order here is important, as each step carefully builds on data
// constructed by the previous
2019-06-22 06:51:07 +00:00
this.buildGraphInt(graph, this.editor_.getEntries());
2019-06-23 05:48:46 +00:00
this.trimSoftNodes(graph);
this.processLinks(graph);
this.processGroups(graph);
this.manifestNodes(graph);
this.setPageRank(graph);
this.bucketByPageRank(graph);
2019-07-01 05:10:34 +00:00
this.bucketBySubgraph(graph);
2019-06-26 04:44:23 +00:00
this.setInitialPositions(graph);
this.setAffinity(graph);
while (this.iterate(graph));
this.fixOrigin(graph);
2019-06-22 06:51:07 +00:00
return graph;
2019-06-22 06:36:08 +00:00
}
2019-06-22 06:51:07 +00:00
buildGraphInt(graph, entries) {
2019-06-22 06:36:08 +00:00
for (let entry of entries) {
if (entry instanceof Node) {
2019-06-22 06:51:07 +00:00
this.buildGraphNode(graph, entry);
2019-06-22 06:36:08 +00:00
} else if (entry instanceof Group) {
2019-06-22 06:51:07 +00:00
this.buildGraphGroup(graph, entry);
2019-06-22 06:46:27 +00:00
} else if (entry instanceof Link) {
2019-06-22 06:51:07 +00:00
this.buildGraphLink(graph, entry);
2019-06-22 06:36:08 +00:00
}
}
}
2019-06-22 06:51:07 +00:00
buildGraphNode(graph, node) {
node.clear();
2019-06-26 18:27:52 +00:00
if (node.getLabel() == '') {
return;
}
let targets = graph.nodesByLabel.get(node.getLabel());
if (!targets) {
targets = [];
graph.nodesByLabel.set(node.getLabel(), targets);
2019-06-26 16:50:03 +00:00
}
2019-06-26 18:27:52 +00:00
targets.push(node);
2019-06-22 06:36:08 +00:00
}
2019-06-22 06:51:07 +00:00
buildGraphGroup(graph, group) {
2019-06-22 06:36:08 +00:00
group.clear();
2019-06-22 06:51:07 +00:00
graph.groups.push(group);
this.buildGraphInt(graph, group.getNodes());
2019-06-22 06:46:27 +00:00
}
2019-06-22 06:51:07 +00:00
buildGraphLink(graph, link) {
2019-06-22 06:46:27 +00:00
link.clear();
graph.links.push(link);
2019-06-22 06:51:07 +00:00
this.buildGraphInt(graph, [link.getFrom(), link.getTo()]);
2019-06-21 20:26:41 +00:00
}
2019-06-23 05:48:46 +00:00
trimSoftNodes(graph) {
2019-06-26 16:50:03 +00:00
for (let entries of graph.nodesByLabel.values()) {
2019-06-23 05:48:46 +00:00
for (let i = entries.length - 1; i >= 0 && entries.length > 1; --i) {
if (entries[i] instanceof Node && entries[i].isSoft()) {
entries.splice(i, 1);
}
}
}
}
processLinks(graph) {
for (let link of graph.links) {
// Re-resolve each from/to reference by label, so we skip soft nodes and
// handle multiple objects with the same label
2019-06-26 16:50:03 +00:00
link.from = graph.nodesByLabel.get(link.getFrom().getLabel()) || [];
link.to = graph.nodesByLabel.get(link.getTo().getLabel()) || [];
for (let from of link.from) {
for (let to of link.to) {
from.links.push(to);
}
}
}
}
processGroups(graph) {
for (let group of graph.groups) {
group.nodes = [];
for (let member of group.getNodes()) {
let nodes = graph.nodesByLabel.get(member.getLabel()) || [];
for (let node of nodes) {
group.nodes.push(node);
node.groups.push(group);
}
}
}
}
manifestNodes(graph) {
2019-06-26 16:50:03 +00:00
for (let entries of graph.nodesByLabel.values()) {
for (let entry of entries) {
if (entry instanceof Node) {
graph.nodes.push(entry);
}
}
}
}
setPageRank(graph) {
for (let link of graph.links) {
for (let to of link.to) {
this.setPageRankInt(to, new Set());
}
}
2019-07-01 21:18:26 +00:00
for (let group of graph.groups) {
// All members of a group get the rank of the maximum member, so the
// initial positions will put them all near each other
let maxRank = 0;
for (let member of group.nodes) {
maxRank = Math.max(maxRank, member.pageRank);
}
for (let member of group.nodes) {
member.pageRank = maxRank;
}
}
}
setPageRankInt(node, visited) {
if (visited.has(node)) {
// Loop detection
return;
}
++node.pageRank;
visited.add(node);
for (let out of node.links) {
this.setPageRankInt(out, visited);
}
visited.delete(node);
}
bucketByPageRank(graph) {
for (let node of graph.nodes) {
let bucket = graph.nodesByPageRank.get(node.pageRank);
if (!bucket) {
bucket = [];
graph.nodesByPageRank.set(node.pageRank, bucket);
}
bucket.push(node);
}
let cmp = (a, b) => {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
};
for (let bucket of graph.nodesByPageRank.values()) {
bucket.sort(cmp);
}
}
2019-06-26 04:44:23 +00:00
2019-07-01 05:10:34 +00:00
bucketBySubgraph(graph) {
let nodes = new Set();
let ranks = Array.from(graph.nodesByPageRank.keys());
ranks.sort((a, b) => a - b);
for (let rank of ranks) {
for (let node of graph.nodesByPageRank.get(rank)) {
nodes.add(node);
}
}
for (let subgraph = 0; nodes.size; ++subgraph) {
let node = nodes.values().next().value;
let subgraphArr = [];
graph.nodesBySubgraph.set(subgraph, subgraphArr);
this.recurseSubgraph(subgraph, subgraphArr, node, nodes);
}
}
recurseSubgraph(subgraph, subgraphArr, node, nodes) {
if (node.subgraph !== null) {
return;
}
node.subgraph = subgraph;
subgraphArr.push(node);
nodes.delete(node);
for (let to of node.links) {
this.recurseSubgraph(subgraph, subgraphArr, to, nodes);
}
for (let group of node.groups) {
for (let member of group.nodes) {
this.recurseSubgraph(subgraph, subgraphArr, member, nodes);
}
}
}
2019-06-26 04:44:23 +00:00
setInitialPositions(graph) {
const SPACING = 4;
let maxRankNodes = 0;
for (let nodes of graph.nodesByPageRank.values()) {
maxRankNodes = Math.max(maxRankNodes, nodes.length);
}
2019-06-26 04:44:23 +00:00
let ranks = Array.from(graph.nodesByPageRank.keys());
ranks.sort((a, b) => a - b);
for (let r = 0; r < ranks.length; ++r) {
let nodes = graph.nodesByPageRank.get(ranks[r]);
for (let n = 0; n < nodes.length; ++n) {
2019-06-30 02:17:02 +00:00
let node = nodes[n];
node.pos = [
r * SPACING,
Math.floor((nodes.length / 2) * SPACING) + (n * SPACING) +
(node.subgraph * SPACING * maxRankNodes),
2019-06-30 02:17:02 +00:00
];
graph.nodesByPos.set(node.pos.toString(), node);
2019-06-26 04:44:23 +00:00
}
}
}
2019-06-26 16:44:12 +00:00
fixOrigin(graph) {
let min = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
let max = [Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER];
for (let node of graph.nodes) {
for (let i of [0, 1]) {
min[i] = Math.min(min[i], node.pos[i]);
max[i] = Math.max(max[i], node.pos[i]);
}
}
// Offset is negative minimum, e.g min -1 means +1 to all values
for (let node of graph.nodes) {
for (let i of [0, 1]) {
node.pos[i] -= min[i];
}
}
graph.size = [
max[0] - min[0] + 1,
max[1] - min[1] + 1,
];
}
setAffinity(graph) {
const INF = 999999;
for (let node of graph.nodes) {
2019-06-30 02:17:02 +00:00
for (let other of graph.nodes) {
// Weak affinity full mesh
// Keep unassociated subgroups together
this.addAffinity(node, other, d => d);
if (node.subgraph != other.subgraph) {
this.addAffinity(node, other, d => d < 1.5 ? -INF : 0);
}
2019-06-30 02:17:02 +00:00
}
for (let to of node.links) {
// Stronger affinity for links
// Prefer to move toward the target instance
this.addAffinity(node, to, d => d < 1.5 ? -INF : d * 11);
this.addAffinity(to, node, d => d < 1.5 ? -INF : d * 9);
}
for (let group of node.groups) {
for (let member of group.nodes) {
// Even stronger affinity for groups
// Other nodes will reference this one and take care of the full
2019-06-30 02:17:02 +00:00
// group mesh
this.addAffinity(node, member, d => d * 100);
2019-07-01 00:45:40 +00:00
}
let members = new Set(group.nodes);
2019-07-01 00:45:40 +00:00
for (let other of graph.nodes) {
if (members.has(other)) {
continue;
}
// Nodes not in this group run away
this.addAffinity(other, node, d => d < 1.5 ? -INF : 0);
}
}
}
}
addAffinity(node, other, func) {
2019-06-30 02:17:02 +00:00
if (node == other) {
return;
}
node.affinity.push({
node: other,
distanceToWeight: func,
});
2019-06-30 02:17:02 +00:00
}
2019-06-28 17:12:44 +00:00
buildGrid(graph) {
2019-06-26 18:27:52 +00:00
this.grid_.innerHTML = '';
this.grid_.style.gridTemplateColumns =
2019-06-28 17:12:44 +00:00
'repeat(' + graph.size[0] + ',1fr)';
this.grid_.style.gridTemplateRows =
2019-06-28 17:12:44 +00:00
'repeat(' + graph.size[1] +
',minmax(0, calc((100vw - var(--editor-width)) / ' +
2019-06-28 17:12:44 +00:00
graph.size[0] + ')))';
2019-06-26 18:27:52 +00:00
2019-06-28 17:12:44 +00:00
this.drawGridNodes(graph);
}
2019-06-28 17:12:44 +00:00
iterate(graph) {
2019-06-30 02:17:02 +00:00
this.sortByMostTension(graph.nodes);
let newPos = null;
let newTension = this.getTotalTension(graph.nodes);
2019-06-28 17:12:44 +00:00
for (let node of graph.nodes) {
let origPos = node.pos;
let offsets = new Map();
let addOffset = (x, y) => {
if (!offsets.has([x, y].toString())) {
offsets.set([x, y].toString(), [x, y]);
}
};
for (let dir of [-1, 0, 1]) {
addOffset(Math.sign(node.vec[0]), dir);
addOffset(dir, Math.sign(node.vec[1]));
}
for (let offset of offsets.values()) {
2019-06-30 02:17:02 +00:00
let testPos = [node.pos[0] + offset[0], node.pos[1] + offset[1]];
if (graph.nodesByPos.has(testPos.toString())) {
continue;
}
node.pos = testPos;
let testTension = this.getTotalTension(graph.nodes);
node.pos = origPos;
2019-06-30 02:17:02 +00:00
if (testTension < newTension) {
newPos = testPos;
newTension = testTension;
}
}
2019-06-30 02:17:02 +00:00
if (newPos) {
graph.nodesByPos.delete(node.pos.toString());
node.pos = newPos;
graph.nodesByPos.set(node.pos.toString(), node);
return true;
}
}
return false;
}
getTotalTension(nodes) {
let total = 0;
for (let node of nodes) {
this.setTension(node);
total += node.tension;
}
return total;
}
2019-06-30 02:17:02 +00:00
sortByMostTension(nodes) {
for (let node of nodes) {
this.setTension(node);
2019-06-30 02:17:02 +00:00
}
nodes.sort((a, b) => b.tension - a.tension);
}
setTension(node) {
node.vec = [0, 0];
node.tension = 0;
for (let aff of node.affinity) {
let vec = [], vecsum = 0;
for (let i of [0, 1]) {
vec[i] = aff.node.pos[i] - node.pos[i];
vecsum += Math.abs(vec[i]);
};
let distance = Math.sqrt(Math.pow(vec[0], 2) + Math.pow(vec[1], 2));
let weight = aff.distanceToWeight(distance);
2019-06-30 02:17:02 +00:00
for (let i of [0, 1]) {
node.vec[i] += (weight * vec[i]) / vecsum;
2019-06-30 02:17:02 +00:00
}
node.tension += Math.abs(weight);
2019-06-30 02:17:02 +00:00
}
}
2019-06-28 17:12:44 +00:00
drawGridNodes(graph) {
for (let node of graph.nodes) {
2019-06-26 18:27:52 +00:00
node.gridElem = document.createElement('div');
node.gridElem.classList.add('gridNode');
this.grid_.appendChild(node.gridElem);
node.gridElem.innerText = node.getLabel();
node.gridElem.style.gridColumn = node.pos[0] + 1;
node.gridElem.style.gridRow = node.pos[1] + 1;
}
}
2019-07-01 20:53:20 +00:00
fixSizes(nodes) {
for (let node of nodes) {
let elem = node.gridElem;
2019-07-01 20:57:22 +00:00
elem.style.fontSize = null;
2019-07-01 21:35:22 +00:00
for (let size = 20;
2019-07-01 20:53:20 +00:00
size && (elem.scrollWidth > elem.clientWidth ||
elem.scrollHeight > elem.clientHeight);
--size) {
elem.style.fontSize = size + 'px';
}
}
}
2019-06-21 20:26:41 +00:00
}
class ListenUtils {
constructor() {
this.listeners_ = [];
}
listen(source, type, callback) {
source.addEventListener(type, callback);
this.listeners_.push([source, type, callback]);
}
clearListeners() {
for (let [source, type, callback] of this.listeners_) {
source.removeEventListener(type, callback);
}
this.listeners_ = [];
}
}
2019-06-20 20:52:58 +00:00
class List {
2019-05-25 04:16:20 +00:00
constructor(container) {
this.container_ = container;
this.minEntries_ = 0;
this.maxEntries_ = Number.MAX_SAFE_INTEGER;
2019-06-21 00:08:18 +00:00
}
setMinEntries(min) {
this.minEntries_ = min;
2019-05-25 04:16:20 +00:00
}
setMaxEntries(max) {
this.maxEntries_ = max;
}
getEntries() {
let ret = [];
for (let elem of this.container_.children) {
ret.push(elem.xArchObj);
}
return ret;
}
mayAdd() {
return this.container_.children.length < this.maxEntries_;
}
2019-05-25 04:16:20 +00:00
getSelected() {
let iter = document.activeElement;
while (iter) {
if (iter.parentElement == this.container_) {
return iter;
}
iter = iter.parentElement;
}
return null;
2019-05-25 04:16:20 +00:00
}
deleteSelected() {
if (this.container_.children.length <= this.minEntries_) {
2019-06-21 00:08:18 +00:00
return;
}
2019-05-25 04:16:20 +00:00
let sel = this.getSelected();
if (sel) {
sel.xArchObj.remove();
}
2019-05-25 04:16:20 +00:00
}
deleteSelectedAndAfter() {
let sel = this.getSelected();
if (sel) {
while (this.container_.lastElementChild != sel &&
this.container_.children.length > this.minEntries_) {
this.container_.lastElementChild.xArchObj.remove();
}
this.deleteSelected();
2019-05-25 04:16:20 +00:00
}
}
selectNext() {
let sel = this.getSelected() || this.container_.lastElementChild;
if (sel) {
this.select(sel.nextElementSibling ||
this.container_.firstElementChild);
}
2019-05-25 04:16:20 +00:00
}
selectPrev() {
let sel = this.getSelected() || this.container_.firstElementChild;
if (sel) {
this.select(sel.previousElementSibling ||
this.container_.lastElementChild);
}
2019-05-25 04:16:20 +00:00
}
selectPrevPage() {
let targetTop = this.container_.scrollTop - this.container_.clientHeight;
let sel = this.getSelected() || this.container_.lastElementSibling;
if (sel) {
while (sel.previousElementSibling &&
this.container_.scrollTop > targetTop) {
sel = sel.previousElementSibling;
this.select(sel);
}
2019-05-25 04:16:20 +00:00
}
}
selectNextPage() {
let targetTop = this.container_.scrollTop + this.container_.clientHeight;
let sel = this.getSelected() || this.container_.firstElementSibling;
if (sel) {
while (sel.nextElementSibling && this.container_.scrollTop < targetTop) {
sel = sel.nextElementSibling;
this.select(sel);
}
2019-05-25 04:16:20 +00:00
}
}
selectFirst() {
this.select(this.container_.firstElementChild);
}
selectLast() {
this.select(this.container_.lastElementChild);
}
select(elem) {
if (!elem) {
return;
}
elem.focus();
2019-05-25 00:03:19 +00:00
}
2019-05-25 04:16:20 +00:00
onKeyDown(e) {
switch (e.key) {
2019-06-20 23:54:14 +00:00
case 'Escape':
case 'ArrowLeft':
2019-07-01 20:29:34 +00:00
case 'h':
2019-06-20 23:54:14 +00:00
if (this.container_.parentElement.xArchObj) {
this.container_.parentElement.focus();
}
break;
case 'd':
this.deleteSelected();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
e.preventDefault();
2019-06-20 23:54:14 +00:00
break;
2019-05-26 02:53:55 +00:00
case 'D':
this.deleteSelectedAndAfter();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
e.preventDefault();
2019-06-20 23:54:14 +00:00
break;
2019-05-25 04:16:20 +00:00
case 'j':
case 'ArrowDown':
this.selectNext();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
e.preventDefault();
2019-06-20 23:54:14 +00:00
break;
2019-05-25 04:16:20 +00:00
case 'k':
case 'ArrowUp':
this.selectPrev();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
e.preventDefault();
2019-06-20 23:54:14 +00:00
break;
2019-05-25 04:16:20 +00:00
case 'PageUp':
this.selectPrevPage();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
e.preventDefault();
2019-06-20 23:54:14 +00:00
break;
2019-05-25 04:16:20 +00:00
case 'PageDown':
this.selectNextPage();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
e.preventDefault();
2019-06-20 23:54:14 +00:00
break;
2019-05-25 04:16:20 +00:00
case 'Home':
this.selectFirst();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
e.preventDefault();
2019-06-20 23:54:14 +00:00
break;
2019-05-25 04:16:20 +00:00
case 'End':
this.selectLast();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
e.preventDefault();
2019-06-20 23:54:14 +00:00
break;
2019-06-20 21:26:31 +00:00
}
}
}
class Editor extends List {
static NODE = 1;
static GROUP = 2;
static LINK = 3;
constructor(container, allowedTypes) {
2019-06-20 21:26:31 +00:00
super(container);
this.allowedTypes_ = new Set(allowedTypes ||
[Editor.NODE, Editor.GROUP, Editor.LINK]);
this.container_.classList.add('editor');
// Needs to accept focus to receive keydown, but shouldn't be in the normal
// tab flow.
this.container_.tabIndex = 99999;
2019-06-20 21:26:31 +00:00
this.container_.addEventListener('keydown', e => { this.onKeyDown(e); });
this.container_.focus();
2019-06-20 21:26:31 +00:00
}
2019-06-25 17:48:40 +00:00
clear() {
this.container_.innerHTML = '';
}
serialize() {
// Doesn't have a type, only used as part of other objects
let ret = [];
for (let entry of this.getEntries()) {
ret.push(entry.serialize());
}
return ret;
}
unserialize(ser) {
for (let entry of ser) {
this.container_.appendChild(EditorEntryBase.unserialize(entry));
}
}
isAllowed(type) {
return this.mayAdd() && this.allowedTypes_.has(type);
2019-06-20 21:26:31 +00:00
}
addNodeAfter() {
if (this.isAllowed(Editor.NODE)) {
Node.addAfter(this.container_, this.getSelected());
2019-06-20 20:52:58 +00:00
}
2019-06-20 21:26:31 +00:00
}
2019-06-20 20:52:58 +00:00
addNodeBefore() {
if (this.isAllowed(Editor.NODE)) {
Node.addBefore(this.container_, this.getSelected());
}
2019-06-20 21:26:31 +00:00
}
addLinkAfter() {
if (this.isAllowed(Editor.LINK)) {
Link.addAfter(this.container_, this.getSelected());
}
2019-06-20 20:52:58 +00:00
}
addLinkBefore() {
if (this.isAllowed(Editor.LINK)) {
Link.addBefore(this.container_, this.getSelected());
}
2019-06-20 20:52:58 +00:00
}
addGroupAfter() {
if (this.isAllowed(Editor.GROUP)) {
Group.addAfter(this.container_, this.getSelected());
}
2019-06-20 20:52:58 +00:00
}
addGroupBefore() {
if (this.isAllowed(Editor.GROUP)) {
Group.addBefore(this.container_, this.getSelected());
}
2019-06-20 20:52:58 +00:00
}
onKeyDown(e) {
switch (e.key) {
case 'g':
this.addGroupAfter();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
2019-06-20 20:52:58 +00:00
e.preventDefault();
return;
case 'G':
this.addGroupBefore();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
2019-06-20 20:52:58 +00:00
e.preventDefault();
return;
2019-07-01 20:29:34 +00:00
case 'i':
this.addLinkAfter();
e.stopPropagation();
e.preventDefault();
return;
2019-07-01 20:29:34 +00:00
case 'I':
this.addLinkBefore();
e.stopPropagation();
e.preventDefault();
return;
2019-06-20 20:52:58 +00:00
case 'n':
this.addNodeAfter();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
2019-06-20 20:52:58 +00:00
e.preventDefault();
return;
case 'N':
this.addNodeBefore();
2019-06-20 21:26:31 +00:00
e.stopPropagation();
2019-06-20 20:52:58 +00:00
e.preventDefault();
return;
2019-05-25 04:16:20 +00:00
}
2019-06-20 20:52:58 +00:00
super.onKeyDown(e);
2019-05-25 04:16:20 +00:00
}
}
class EditorEntryBase extends ListenUtils {
2019-05-25 04:16:20 +00:00
constructor() {
super();
this.id = randStr64();
2019-05-25 04:16:20 +00:00
this.elem_ = document.createElement('li');
this.elem_.tabIndex = 0;
this.listen(this.elem_, 'focus', () => this.onElemFocus());
this.listen(this.elem_, 'keydown', (e) => this.onKeyDown(e));
2019-05-25 04:16:20 +00:00
this.elem_.xArchObj = this;
}
remove() {
if (document.activeElement == this.elem_ ||
document.activeElement == document.body) {
if (this.elem_.nextElementSibling) {
this.elem_.nextElementSibling.focus();
} else if (this.elem_.previousElementSibling) {
this.elem_.previousElementSibling.focus();
2019-06-23 05:49:57 +00:00
} else if (this.elem_.parentElement) {
this.elem_.parentElement.focus();
}
}
2019-05-25 04:33:31 +00:00
this.elem_.remove();
this.clearListeners();
this.elem_.xArchObj = null;
2019-05-25 04:33:31 +00:00
}
2019-06-23 06:00:20 +00:00
clear() {
}
wantFocus() {
return false;
}
onElemFocus() {
this.elem_.scrollIntoView({block: 'nearest'});
2019-05-25 04:33:31 +00:00
}
onKeyDown(e) {
}
afterDomAdd() {
2019-05-27 03:29:43 +00:00
}
2019-05-25 04:16:20 +00:00
static addBefore(container, elem) {
let entry = new this();
2019-06-25 17:48:40 +00:00
container.insertBefore(entry.getElement(), elem);
entry.afterDomAdd();
2019-05-25 04:16:20 +00:00
}
static addAfter(container, elem) {
let entry = new this();
2019-06-25 17:48:40 +00:00
container.insertBefore(entry.getElement(), elem ? elem.nextSibling : null);
entry.afterDomAdd();
2019-06-25 17:48:40 +00:00
}
static unserialize(ser) {
switch (ser.type) {
case 'group':
return Group.unserialize(ser);
case 'link':
return Link.unserialize(ser);
case 'node':
return Node.unserialize(ser);
}
2019-05-25 04:16:20 +00:00
}
}
class Node extends EditorEntryBase {
2019-05-26 02:53:55 +00:00
constructor() {
super();
2019-07-01 20:27:51 +00:00
this.elem_.innerText = 'Node:';
this.elem_.classList.add('node');
2019-05-26 02:53:55 +00:00
this.input_ = document.createElement('input');
this.input_.type = 'text';
this.input_.placeholder = 'node name';
this.input_.setAttribute('list', 'arch-targets');
this.listen(this.input_, 'keydown', (e) => this.onInputKeyDown(e));
2019-06-22 06:36:08 +00:00
this.listen(this.input_, 'input', (e) => this.onInput());
this.listen(this.input_, 'blur', (e) => this.onInput());
2019-05-26 02:53:55 +00:00
this.elem_.appendChild(this.input_);
}
2019-05-26 02:53:55 +00:00
afterDomAdd() {
this.input_.focus();
}
2019-05-26 02:53:55 +00:00
2019-06-25 17:48:40 +00:00
serialize() {
return {
type: 'node',
label: this.getLabel(),
};
}
2019-06-25 19:43:32 +00:00
exportGraphviz() {
if (this.getLabel() == '') {
return [];
}
return ['"' + this.id + '" [label="' + this.getLabel() + '"];'];
2019-06-25 19:43:32 +00:00
}
2019-06-22 06:36:08 +00:00
clear() {
2019-06-23 06:00:20 +00:00
super.clear();
this.links = [];
this.groups = [];
this.affinity = [];
this.pageRank = 0;
2019-07-01 05:10:34 +00:00
this.subgraph = null;
2019-06-22 06:36:08 +00:00
}
2019-06-22 06:46:27 +00:00
getLabel() {
return this.input_.value;
}
2019-06-25 17:48:40 +00:00
setLabel(label) {
this.input_.value = label;
this.onInput();
}
getElement() {
return this.elem_;
}
wantFocus() {
return this.getLabel() == '';
}
2019-06-23 05:48:46 +00:00
isSoft() {
// Nested nodes are presumed to be references to other nodes if they exist
let iter = this.elem_.parentElement;
for (let iter = this.elem_.parentElement; iter; iter = iter.parentElement) {
if (iter.xArchObj) {
return true;
}
}
return false;
}
2019-06-22 06:36:08 +00:00
onInput() {
if (!this.input_.getAttribute('data-arch-value') ||
this.input_.value == '') {
this.input_.setAttribute('data-struct-change', 'x');
}
2019-06-22 06:36:08 +00:00
this.input_.setAttribute('data-arch-value', this.input_.value);
}
onInputKeyDown(e) {
switch (e.key) {
case 'Enter':
2019-06-25 17:57:46 +00:00
e.stopPropagation();
e.preventDefault();
if (this.elem_.nextElementSibling &&
this.elem_.nextElementSibling.xArchObj &&
this.elem_.nextElementSibling.xArchObj.wantFocus()) {
2019-06-25 17:57:46 +00:00
this.elem_.nextElementSibling.xArchObj.startEdit();
} else {
this.stopEdit();
}
break;
case 'Escape':
e.stopPropagation();
e.preventDefault();
this.stopEdit();
break;
case 'ArrowUp':
case 'ArrowDown':
case 'PageUp':
case 'PageDown':
this.stopEdit();
break;
default:
e.stopPropagation();
break;
}
}
onKeyDown(e) {
super.onKeyDown(e);
switch (e.key) {
case 'Enter':
this.startEdit();
e.stopPropagation();
e.preventDefault();
break;
}
2019-05-26 02:53:55 +00:00
}
startEdit() {
this.input_.focus();
}
stopEdit() {
2019-06-25 17:57:46 +00:00
this.elem_.focus();
2019-05-26 02:53:55 +00:00
}
2019-06-25 17:48:40 +00:00
static unserialize(ser) {
let node = new Node();
node.setLabel(ser.label);
return node.getElement();
}
}
class Group extends EditorEntryBase {
constructor() {
super();
2019-07-01 20:27:51 +00:00
this.elem_.innerText = 'Group:';
this.elem_.classList.add('group');
this.input_ = document.createElement('input');
this.input_.type = 'text';
this.input_.placeholder = 'group name';
this.listen(this.input_, 'keydown', (e) => this.onInputKeyDown(e));
2019-06-22 06:36:08 +00:00
this.listen(this.input_, 'input', (e) => this.onInput());
this.listen(this.input_, 'blur', (e) => this.onInput());
this.elem_.appendChild(this.input_);
2019-06-20 21:26:31 +00:00
let nodeList = document.createElement('div');
this.nodes_ = new Editor(nodeList, [Editor.NODE]);
this.nodes_.setMinEntries(1);
2019-06-20 21:26:31 +00:00
this.nodes_.addNodeAfter();
this.elem_.appendChild(nodeList);
2019-05-27 03:29:43 +00:00
}
afterDomAdd() {
this.input_.focus();
2019-05-26 02:53:55 +00:00
}
2019-06-25 17:48:40 +00:00
serialize() {
return {
type: 'group',
label: this.getLabel(),
members: this.nodes_.serialize(),
};
}
2019-06-25 19:43:32 +00:00
exportGraphviz() {
let lines = [
'subgraph "cluster_' + this.id + '" {',
];
if (this.getLabel() != '') {
lines.push('\tlabel = "' + this.getLabel() + '";');
}
for (let obj of this.nodes) {
for (let line of obj.exportGraphviz()) {
lines.push('\t' + line);
}
}
lines.push('}');
return lines;
2019-06-25 19:43:32 +00:00
}
2019-06-22 06:36:08 +00:00
clear() {
2019-06-23 06:00:20 +00:00
super.clear();
2019-06-22 06:36:08 +00:00
}
2019-06-22 06:46:27 +00:00
getNodes() {
return this.nodes_.getEntries();
}
getLabel() {
return this.input_.value;
}
2019-06-25 17:48:40 +00:00
setLabel(label) {
this.input_.value = label;
this.onInput();
}
getElement() {
return this.elem_;
}
onInputKeyDown(e) {
switch (e.key) {
case 'Enter':
e.stopPropagation();
e.preventDefault();
this.stopEdit();
{
let nodes = this.nodes_.getEntries();
2019-06-22 06:36:08 +00:00
if (nodes.length == 1 && nodes[0].getLabel() == '') {
nodes[0].startEdit();
}
}
break;
2019-06-20 23:36:43 +00:00
case 'Escape':
e.stopPropagation();
e.preventDefault();
this.stopEdit();
break;
case 'ArrowUp':
case 'ArrowDown':
case 'PageUp':
case 'PageDown':
this.stopEdit();
break;
default:
e.stopPropagation();
break;
}
}
onKeyDown(e) {
super.onKeyDown(e);
switch (e.key) {
case 'Enter':
this.startEdit();
e.stopPropagation();
e.preventDefault();
break;
2019-06-20 23:54:14 +00:00
case 'ArrowRight':
2019-07-01 20:29:34 +00:00
case 'l':
2019-06-20 23:54:14 +00:00
this.nodes_.selectNext();
break;
}
}
2019-06-22 06:36:08 +00:00
onInput() {
this.input_.setAttribute('data-arch-value', this.input_.value);
}
startEdit() {
this.input_.focus();
}
stopEdit() {
this.elem_.focus();
}
2019-06-25 17:48:40 +00:00
static unserialize(ser) {
let group = new Group();
group.setLabel(ser.label);
group.nodes_.clear();
group.nodes_.unserialize(ser.members);
return group.getElement();
}
}
class Link extends EditorEntryBase {
constructor() {
super();
2019-07-01 20:27:51 +00:00
this.elem_.innerText = 'Link:';
this.elem_.classList.add('link');
this.input_ = document.createElement('input');
this.input_.type = 'text';
this.input_.placeholder = 'label';
this.listen(this.input_, 'keydown', (e) => this.onInputKeyDown(e));
2019-06-22 06:46:27 +00:00
this.listen(this.input_, 'input', (e) => this.onInput());
this.listen(this.input_, 'blur', (e) => this.onInput());
this.elem_.appendChild(this.input_);
let nodeList = document.createElement('div');
this.nodes_ = new Editor(nodeList, [Editor.NODE]);
this.nodes_.setMinEntries(2);
this.nodes_.setMaxEntries(2);
this.nodes_.addNodeAfter();
this.nodes_.addNodeAfter();
this.elem_.appendChild(nodeList);
}
afterDomAdd() {
this.input_.focus();
}
2019-06-25 17:48:40 +00:00
serialize() {
return {
type: 'link',
label: this.getLabel(),
from: this.getFrom().serialize(),
to: this.getTo().serialize(),
};
}
2019-06-25 19:43:32 +00:00
exportGraphviz() {
if (this.getFrom().getLabel() == '' || this.getTo().getLabel() == '') {
return [];
}
let label = '';
if (this.getLabel() != '') {
label = ' [label="' + this.getLabel() + '"]';
2019-06-25 19:43:32 +00:00
}
let ret = [];
2019-06-26 19:39:32 +00:00
for (let from of this.from) {
for (let to of this.to) {
ret.push('"' + from.id + '" -> "' + to.id + '"' + label + ';');
}
}
return ret;
2019-06-25 19:43:32 +00:00
};
2019-06-22 06:46:27 +00:00
clear() {
2019-06-23 06:00:20 +00:00
super.clear();
2019-06-22 06:46:27 +00:00
}
getFrom() {
return this.nodes_.getEntries()[0];
}
getTo() {
return this.nodes_.getEntries()[1];
}
getLabel() {
return this.input_.value;
}
2019-06-25 17:48:40 +00:00
setLabel(label) {
this.input_.value = label;
this.onInput();
}
getElement() {
return this.elem_;
}
2019-06-22 06:46:27 +00:00
onInput() {
this.input_.setAttribute('data-arch-value', this.input_.value);
}
onInputKeyDown(e) {
switch (e.key) {
case 'Enter':
e.stopPropagation();
e.preventDefault();
this.stopEdit();
{
let nodes = this.nodes_.getEntries();
2019-06-22 06:36:08 +00:00
if (nodes[0].getLabel() == '') {
nodes[0].startEdit();
2019-06-22 06:36:08 +00:00
} else if (nodes[1].getLabel() == '') {
nodes[1].startEdit();
}
}
break;
case 'Escape':
e.stopPropagation();
e.preventDefault();
this.stopEdit();
break;
case 'ArrowUp':
case 'ArrowDown':
case 'PageUp':
case 'PageDown':
this.stopEdit();
break;
default:
e.stopPropagation();
break;
}
2019-05-26 02:53:55 +00:00
}
onKeyDown(e) {
super.onKeyDown(e);
switch (e.key) {
case 'Enter':
this.startEdit();
e.stopPropagation();
e.preventDefault();
break;
case 'ArrowRight':
2019-07-01 20:29:34 +00:00
case 'l':
this.nodes_.selectNext();
break;
}
}
startEdit() {
this.input_.focus();
}
stopEdit() {
this.elem_.focus();
}
2019-06-25 17:48:40 +00:00
static unserialize(ser) {
let link = new Link();
link.setLabel(ser.label);
link.nodes_.clear();
link.nodes_.unserialize([ser.from, ser.to]);
return link.getElement();
}
2019-05-26 02:53:55 +00:00
}
2019-06-21 20:26:41 +00:00
new Architype(document.getElementById('architype'));