|
|
@@ -1343,7 +1343,7 @@ Draw.loadPlugin(async function(ui) {
|
|
|
const buttonShare = menu.addItem("Share this diagram",
|
|
|
null, // image
|
|
|
() => {
|
|
|
- const ops = history.getOpsSequence();
|
|
|
+ const ops = historyWrapper.history.getOpsSequence();
|
|
|
const serialized = ops.map(op => op.serialize());
|
|
|
controller.addInput("new_share", "in", [serialized], controller.wallclockToSimtime());
|
|
|
}, // callback
|
|
|
@@ -1463,10 +1463,6 @@ Draw.loadPlugin(async function(ui) {
|
|
|
|
|
|
const uiState = new UIState();
|
|
|
|
|
|
- const context = new Context(async id => {
|
|
|
- throw new Error("Fetch is forbidden. ", id)
|
|
|
- });
|
|
|
-
|
|
|
const codec = new mxCodec();
|
|
|
const xmlSerializer = new XMLSerializer();
|
|
|
const xmlParser = new DOMParser();
|
|
|
@@ -1520,12 +1516,8 @@ Draw.loadPlugin(async function(ui) {
|
|
|
model.setStyle(cell, style);
|
|
|
},
|
|
|
parent: () => {
|
|
|
- const {parentId} = value;
|
|
|
- if (parentId === null) {
|
|
|
- // Creates a mxChildChange object
|
|
|
- model.remove(cell);
|
|
|
- }
|
|
|
- else {
|
|
|
+ if (value) {
|
|
|
+ const {parentId} = value;
|
|
|
let parent = getCell(parentId);
|
|
|
if (parent === undefined) {
|
|
|
console.log("creating non-existing parent");
|
|
|
@@ -1534,6 +1526,9 @@ Draw.loadPlugin(async function(ui) {
|
|
|
}
|
|
|
// Creates a mxChildChange object
|
|
|
model.add(parent, cell, null);
|
|
|
+ } else {
|
|
|
+ // Creates a mxChildChange object
|
|
|
+ model.remove(cell);
|
|
|
}
|
|
|
},
|
|
|
value: () => {
|
|
|
@@ -1607,39 +1602,80 @@ Draw.loadPlugin(async function(ui) {
|
|
|
return a.id > b.id;
|
|
|
};
|
|
|
|
|
|
- let history, mergePromise;
|
|
|
- function resetHistory() {
|
|
|
- history = new History(context, setState, resolve);
|
|
|
- mergePromise = Promise.resolve();
|
|
|
- }
|
|
|
- resetHistory();
|
|
|
+ // Fired when a local change happens
|
|
|
+ model.addListener(mxEvent.NOTIFY, function(sender, event) {
|
|
|
+ if (historyWrapper.transactionDepth === 0) {
|
|
|
+ console.log("NOTIFY:", event.properties.edit.changes);
|
|
|
+
|
|
|
+ const delta = {};
|
|
|
+ for (const change of event.properties.edit.changes) {
|
|
|
+ changeToDelta(change, delta);
|
|
|
+ }
|
|
|
+ if (Object.keys(delta).length > 0) {
|
|
|
+ const op = historyWrapper.history.new(delta,
|
|
|
+ false // do NOT update mxGraphModel with the change (the change was already executed)
|
|
|
+ );
|
|
|
+ const serializedOp = op.serialize();
|
|
|
+ console.log("NEW LOCAL OP:", serializedOp);
|
|
|
+ controller.addInput("new_edit", "in", [serializedOp], controller.wallclockToSimtime());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- function queuedMerge(serializedOp) {
|
|
|
- mergePromise = mergePromise.then(() => {
|
|
|
- return history.context.receiveOperation(serializedOp).then(op => {
|
|
|
- try {
|
|
|
- listenForEdits++;
|
|
|
+ class HistoryWrapper {
|
|
|
+ constructor(graph, context) {
|
|
|
+ this.graph = graph;
|
|
|
+ this.context = context;
|
|
|
+ this.transactionDepth = 0;
|
|
|
+ }
|
|
|
|
|
|
- model.beginUpdate();
|
|
|
- history.autoMerge(op);
|
|
|
- model.endUpdate();
|
|
|
- }
|
|
|
- catch (e) {
|
|
|
- e.op = op;
|
|
|
- throw e;
|
|
|
- }
|
|
|
- finally {
|
|
|
- listenForEdits--;
|
|
|
- }
|
|
|
- console.log("Merged ", op.id);
|
|
|
+ newEmptyDiagram() {
|
|
|
+ console.log("newEmptyDiagram")
|
|
|
+ this.history = new History(this.context, setState, resolve);
|
|
|
+ this.graph.model.clear(); // the resetting of the diagram will be recorded into the (blank) history
|
|
|
+ this.mergePromise = Promise.resolve();
|
|
|
+ }
|
|
|
+
|
|
|
+ // returns Promise
|
|
|
+ loadExistingDiagram(ops) {
|
|
|
+ console.log("loadExistingDiagram")
|
|
|
+ this.graph.setEnabled(false);
|
|
|
+ this.transactionDepth++; // prevent us from re-recording any changes
|
|
|
+ this.graph.model.clear();
|
|
|
+ this.history = new History(this.context, setState, resolve);
|
|
|
+ this.mergePromise = Promise.resolve();
|
|
|
+ for (const op of ops) {
|
|
|
+ this.queuedMerge(op);
|
|
|
+ }
|
|
|
+ return this.mergePromise.then(() => {
|
|
|
+ this.graph.setEnabled(true);
|
|
|
+ this.transactionDepth--;
|
|
|
});
|
|
|
- });
|
|
|
- }
|
|
|
- function leaveOnError() {
|
|
|
- mergePromise.catch(err => {
|
|
|
- console.error("Unexpected error merging operation", err.op.id, err);
|
|
|
- controller.addInput("leave", "in", [], controller.wallclockToSimtime());
|
|
|
- });
|
|
|
+ }
|
|
|
+
|
|
|
+ // returns Promise
|
|
|
+ queuedMerge(serializedOp) {
|
|
|
+ this.mergePromise = this.mergePromise.then(() => {
|
|
|
+ return this.context.receiveOperation(serializedOp).then(op => {
|
|
|
+ console.log("Merging ", op.id);
|
|
|
+ try {
|
|
|
+ this.transactionDepth++;
|
|
|
+
|
|
|
+ this.graph.model.beginUpdate();
|
|
|
+ this.history.autoMerge(op);
|
|
|
+ this.graph.model.endUpdate();
|
|
|
+ }
|
|
|
+ catch (e) {
|
|
|
+ e.op = op;
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ finally {
|
|
|
+ this.transactionDepth--;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ return this.mergePromise;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function getMyName() {
|
|
|
@@ -1651,23 +1687,17 @@ Draw.loadPlugin(async function(ui) {
|
|
|
const selectionCounter = new Map();
|
|
|
const userSelection = new Map();
|
|
|
|
|
|
+ // const handleMergeError = err => {
|
|
|
+ // console.error("Unexpected error merging:", err);
|
|
|
+ // controller.addInput("leave", "in", [], controller.wallclockToSimtime());
|
|
|
+ // }
|
|
|
+
|
|
|
controller.addMyOwnOutputListener({
|
|
|
'add': event => {
|
|
|
// console.log("output event:", event);
|
|
|
if (event.name === "ack_join") {
|
|
|
const [ops] = event.parameters;
|
|
|
- // console.log("Outevent ack_join", ops.length, "ops..");
|
|
|
- try {
|
|
|
- listenForEdits++;
|
|
|
- model.clear();
|
|
|
- resetHistory();
|
|
|
- for (const op of ops) {
|
|
|
- queuedMerge(op);
|
|
|
- }
|
|
|
- leaveOnError();
|
|
|
- } finally {
|
|
|
- listenForEdits--;
|
|
|
- }
|
|
|
+ historyWrapper.loadExistingDiagram(ops);
|
|
|
}
|
|
|
else if (event.name === "ack_new_share") {
|
|
|
// nothing needs to happen
|
|
|
@@ -1677,8 +1707,7 @@ Draw.loadPlugin(async function(ui) {
|
|
|
const [sessionId, msg] = event.parameters;
|
|
|
if (msg.type === "pub_edit") {
|
|
|
const {op} = msg;
|
|
|
- queuedMerge(op);
|
|
|
- leaveOnError();
|
|
|
+ historyWrapper.queuedMerge(op);
|
|
|
}
|
|
|
else if (msg.type === "update_cursor") {
|
|
|
const {userId, name, x, y} = msg;
|
|
|
@@ -1837,9 +1866,7 @@ Draw.loadPlugin(async function(ui) {
|
|
|
parentId: change.parent.id,
|
|
|
isVertex: change.parent.isVertex(),
|
|
|
isEdge: change.parent.isEdge(),
|
|
|
- } : {
|
|
|
- parentId: null,
|
|
|
- };
|
|
|
+ } : null;
|
|
|
} else {
|
|
|
// no previous parent -> cell was created:
|
|
|
console.log("no previous parent");
|
|
|
@@ -1880,49 +1907,37 @@ Draw.loadPlugin(async function(ui) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Fired when a local change happens
|
|
|
- let listenForEdits = 0;
|
|
|
- model.addListener(mxEvent.NOTIFY, function(sender, event) {
|
|
|
- if (listenForEdits === 0) {
|
|
|
- console.log("NOTIFY:", event.properties.edit.changes);
|
|
|
-
|
|
|
- const delta = {};
|
|
|
- for (const change of event.properties.edit.changes) {
|
|
|
- changeToDelta(change, delta);
|
|
|
- }
|
|
|
- if (Object.keys(delta).length > 0) {
|
|
|
- const op = history.new(delta,
|
|
|
- false // do NOT update mxGraphModel with the change (the change was already executed)
|
|
|
- );
|
|
|
- const serializedOp = op.serialize();
|
|
|
- console.log("OP:", serializedOp);
|
|
|
- controller.addInput("new_edit", "in", [serializedOp], controller.wallclockToSimtime());
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
|
|
|
// UI stuff
|
|
|
uiState.install(ui);
|
|
|
|
|
|
- document.addEventListener('keydown', e => {
|
|
|
- // console.log(e);
|
|
|
- if (e.code === 'KeyC') {
|
|
|
- console.log("KeyC pressed: Clear model and reset history");
|
|
|
- resetHistory();
|
|
|
- model.clear();
|
|
|
- }
|
|
|
|
|
|
- if (e.code === 'KeyH') {
|
|
|
- const seq = history.getOpsSequence();
|
|
|
- console.log(seq.map(op => op.serialize()));
|
|
|
- }
|
|
|
+ const context = new Context(async id => {
|
|
|
+ throw new Error("Fetch is forbidden. ", id)
|
|
|
+ });
|
|
|
+ const historyWrapper = new HistoryWrapper(graph, context);
|
|
|
+ historyWrapper.newEmptyDiagram();
|
|
|
|
|
|
- // if (e.code === 'KeyU') {
|
|
|
- // const u = window.prompt("Choose a user name:", myName);
|
|
|
- // if (u) {
|
|
|
- // myName = u;
|
|
|
- // }
|
|
|
- // }
|
|
|
+
|
|
|
+ // For debugging
|
|
|
+ document.addEventListener('keydown', e => {
|
|
|
+ // console.log(e);
|
|
|
+ // if (e.code === 'KeyC') {
|
|
|
+ // console.log("KeyC pressed: Clear model and reset history");
|
|
|
+ // historyWrapper.reset();
|
|
|
+ // }
|
|
|
+
|
|
|
+ // if (e.code === 'KeyH') {
|
|
|
+ // const seq = historyWrapper.history.getOpsSequence();
|
|
|
+ // console.log(seq.map(op => op.serialize()));
|
|
|
+ // }
|
|
|
+
|
|
|
+ // if (e.code === 'KeyU') {
|
|
|
+ // const u = window.prompt("Choose a user name:", myName);
|
|
|
+ // if (u) {
|
|
|
+ // myName = u;
|
|
|
+ // }
|
|
|
+ // }
|
|
|
});
|
|
|
});
|
|
|
},{"../../../../../lib/versioning/DragHandler.js":16,"../../../../../lib/versioning/GhostOverlays.js":17,"../../../../../lib/versioning/History.js":18,"../../../../../lib/versioning/UserColors.js":19}]},{},[20]);
|