|
@@ -838,55 +838,169 @@ function version(uuid) {
|
|
|
var _default = version;
|
|
|
exports.default = _default;
|
|
|
},{"./validate.js":14}],16:[function(require,module,exports){
|
|
|
+class DragHandler {
|
|
|
|
|
|
-class DisabledCells {
|
|
|
- constructor() {
|
|
|
- this.disabledCells = new Set();
|
|
|
- }
|
|
|
-
|
|
|
- add(cell) {
|
|
|
- this.disabledCells.add(cell);
|
|
|
+ constructor(controller) {
|
|
|
+ this.controller = controller;
|
|
|
+ this.online = false;
|
|
|
}
|
|
|
|
|
|
- delete(cell) {
|
|
|
- this.disabledCells.delete(cell);
|
|
|
+ setOnline(online) {
|
|
|
+ this.online = online;
|
|
|
}
|
|
|
|
|
|
install(graph) {
|
|
|
+ const oldStart = graph.graphHandler.start;
|
|
|
+ const oldUpdate = graph.graphHandler.updateLivePreview;
|
|
|
+ const oldReset = graph.graphHandler.reset;
|
|
|
+ const oldMoveCells = graph.graphHandler.moveCells;
|
|
|
const self = this;
|
|
|
- // part #1: Intercepting mxGraph.fireMouseEvent
|
|
|
- const oldFireMouseEvent = graph.fireMouseEvent;
|
|
|
- graph.fireMouseEvent = function(evtName, me, sender) {
|
|
|
- if (me.state && self.disabledCells.has(me.state.cell.id)) {
|
|
|
- // clicked shape is disabled
|
|
|
- return;
|
|
|
- }
|
|
|
- oldFireMouseEvent.apply(this, arguments);
|
|
|
- }
|
|
|
|
|
|
- // part #2: Ignore double clicks on disabled cells
|
|
|
- const oldDblClick = graph.dblClick;
|
|
|
- graph.dblClick = function(evt, cell) {
|
|
|
- if (cell && self.disabledCells.has(cell.id)) {
|
|
|
- // clicked shape is disabled
|
|
|
- return;
|
|
|
+ graph.graphHandler.start = function(cell, x, y) {
|
|
|
+ // start dragging (locally)
|
|
|
+ oldStart.apply(this, arguments);
|
|
|
+
|
|
|
+ console.log("drag start");
|
|
|
+
|
|
|
+ if (self.online) {
|
|
|
+ const cells = graph.graphHandler.getCells(cell);
|
|
|
+ const cellIds = cells.map(c => c.id);
|
|
|
+
|
|
|
+ let acquiredCallback, denyCallback;
|
|
|
+ const acquiredPromise = new Promise((resolve, reject) => {
|
|
|
+ acquiredCallback = resolve;
|
|
|
+ denyCallback = reject;
|
|
|
+ }).then(
|
|
|
+ () => {console.log("acquired")},
|
|
|
+ e => {console.log("denied"); throw e},
|
|
|
+ );
|
|
|
+
|
|
|
+ console.log("request lock");
|
|
|
+ self.controller.addInput("request_lock", "in", [cellIds, acquiredCallback, denyCallback], self.controller.wallclockToSimtime());
|
|
|
+
|
|
|
+ let resolveDragEnd;
|
|
|
+ const dragPromise = new Promise(resolve => {
|
|
|
+ resolveDragEnd = resolve;
|
|
|
+ });
|
|
|
+
|
|
|
+ const resetOnce = {
|
|
|
+ reset: () => {
|
|
|
+ graph.graphHandler.reset();
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ // let's pretend we acquire the lock
|
|
|
+
|
|
|
+ let moveCellsArgs;
|
|
|
+ // In the graph handler, moveCells is called when the mouse is released (end of drag).
|
|
|
+ graph.graphHandler.moveCells = function(cells, dx, dy, clone, target, event) {
|
|
|
+ moveCellsArgs = [cells, dx, dy, clone, target, event];
|
|
|
+ };
|
|
|
+
|
|
|
+ // When drag has completed AND a lock was acquired, release the lock.
|
|
|
+ Promise.all([dragPromise, acquiredPromise])
|
|
|
+ .then(() => {
|
|
|
+ if (moveCellsArgs !== undefined) {
|
|
|
+ oldMoveCells.apply(graph.graphHandler, moveCellsArgs);
|
|
|
+ }
|
|
|
+ console.log("release lock");
|
|
|
+ self.controller.addInput("release_lock", "in", [cellIds], self.controller.wallclockToSimtime());
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ console.log("could not acquire lock");
|
|
|
+ resetOnce.reset();
|
|
|
+ });
|
|
|
+
|
|
|
+ let dX = 0;
|
|
|
+ let dY = 0;
|
|
|
+ graph.graphHandler.updateLivePreview = function(dx, dy) {
|
|
|
+ oldUpdate.apply(this, arguments);
|
|
|
+ dX = dx;
|
|
|
+ dY = dy;
|
|
|
+ const msg = {
|
|
|
+ type: "update_drag",
|
|
|
+ cells,
|
|
|
+ dx, dy,
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ graph.graphHandler.reset = function() {
|
|
|
+ console.log("drag end");
|
|
|
+ oldReset.apply(this, arguments);
|
|
|
+ resetOnce.reset = () => {};
|
|
|
+ resolveDragEnd();
|
|
|
+ console.log(dX, dY);
|
|
|
+ };
|
|
|
}
|
|
|
- oldDblClick.apply(this, arguments);
|
|
|
- }
|
|
|
- // part #3: Protect disabled cells from ever being selected
|
|
|
- const oldMxSelectionChange = mxSelectionChange; // override constructor :)
|
|
|
- mxSelectionChange = function(selectionModel, added, removed) {
|
|
|
- oldMxSelectionChange.apply(this, arguments);
|
|
|
- if (this.added) {
|
|
|
- this.added = this.added.filter(cell => !self.disabledCells.has(cell.id));
|
|
|
+ else {
|
|
|
+ // offline
|
|
|
+ graph.graphHandler.updateLivePreview = oldUpdate;
|
|
|
+ graph.graphHandler.moveCells = oldMoveCells;
|
|
|
+ graph.graphHandler.reset = oldReset;
|
|
|
}
|
|
|
+
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ // document.addEventListener('keydown', e => {
|
|
|
+ // if (e.code === 'ShiftLeft') {
|
|
|
+ // graph.graphHandler.reset();
|
|
|
+ // }
|
|
|
+ // });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+module.exports = { DragHandler };
|
|
|
+},{}],17:[function(require,module,exports){
|
|
|
+const { UserColors } = require("./UserColors.js");
|
|
|
+
|
|
|
+class GhostOverlays {
|
|
|
+ constructor(graph, userColors) {
|
|
|
+ // this.userNames = userNames;
|
|
|
+ this.userColors = userColors;
|
|
|
+ this.canvas = graph.view.canvas;
|
|
|
+ this.svg = this.canvas.parentElement;
|
|
|
+
|
|
|
+ this.map = new Map();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update or create ghost cursor. Idempotent.
|
|
|
+ put(userId, name, x, y) {
|
|
|
+ let state = this.map.get(userId);
|
|
|
+ if (state === undefined) {
|
|
|
+ const g = document.createElementNS("http://www.w3.org/2000/svg", 'g');
|
|
|
+ const image = document.createElementNS("http://www.w3.org/2000/svg", 'image');
|
|
|
+ const text = document.createElementNS("http://www.w3.org/2000/svg", 'text');
|
|
|
+ text.style.fontSize = "10px";
|
|
|
+ text.style.fill = this.userColors.getColor(userId);
|
|
|
+ text.setAttribute('y', 30);
|
|
|
+ const textNode = document.createTextNode("");
|
|
|
+ text.appendChild(textNode);
|
|
|
+ image.setAttribute('href', "/lib/versioning/resources/cursor.svg");
|
|
|
+ image.setAttribute('width', 11.6);
|
|
|
+ image.setAttribute('height', 18.2);
|
|
|
+ g.appendChild(image);
|
|
|
+ g.appendChild(text);
|
|
|
+ this.canvas.appendChild(g);
|
|
|
+ const transform = this.svg.createSVGTransform();
|
|
|
+ g.transform.baseVal.appendItem(transform);
|
|
|
+ state = {transform, textNode, timeout: null};
|
|
|
+ this.map.set(userId, state);
|
|
|
}
|
|
|
- mxSelectionChange.prototype = oldMxSelectionChange.prototype;
|
|
|
+ state.transform.setTranslate(
|
|
|
+ (x + graph.view.translate.x) * graph.view.scale,
|
|
|
+ (y + graph.view.translate.y) * graph.view.scale,
|
|
|
+ );
|
|
|
+ state.textNode.data = name;
|
|
|
+ if (state.timeout) {
|
|
|
+ clearTimeout(state.timeout);
|
|
|
+ }
|
|
|
+ state.timeout = setTimeout(() => state.transform.setTranslate(-50, -50), 5000);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-module.exports = { DisabledCells };
|
|
|
-},{}],17:[function(require,module,exports){
|
|
|
+module.exports = { GhostOverlays };
|
|
|
+},{"./UserColors.js":19}],18:[function(require,module,exports){
|
|
|
"use strict";
|
|
|
|
|
|
const { v4: uuidv4 } = require("uuid");
|
|
@@ -1138,16 +1252,45 @@ class History {
|
|
|
|
|
|
module.exports = { Context, History, uuidv4 };
|
|
|
|
|
|
-},{"uuid":1}],18:[function(require,module,exports){
|
|
|
+},{"uuid":1}],19:[function(require,module,exports){
|
|
|
+class UserColors {
|
|
|
+ constructor() {
|
|
|
+ this.colors = ["#F6511D", "#FFB400", "#00A6ED", "#7FB800", "#0D2C54"];
|
|
|
+ this.i = 0;
|
|
|
+
|
|
|
+ this.map = new Map();
|
|
|
+ }
|
|
|
+
|
|
|
+ getColor(userId) {
|
|
|
+ let color = this.map.get(userId);
|
|
|
+ if (color === undefined) {
|
|
|
+ color = this.colors[this.i++];
|
|
|
+ this.map.set(userId, color);
|
|
|
+ }
|
|
|
+ return color;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = { UserColors };
|
|
|
+},{}],20:[function(require,module,exports){
|
|
|
// Build this plugin with 'browserify':
|
|
|
// browserify versioning.js > versioning.browser.js
|
|
|
|
|
|
Draw.loadPlugin(async function(ui) {
|
|
|
|
|
|
+ const {Context, History, uuidv4} = require("../../../../../lib/versioning/History.js");
|
|
|
+ const {UserColors} = require("../../../../../lib/versioning/UserColors.js")
|
|
|
+ const {DragHandler} = require("../../../../../lib/versioning/DragHandler.js");
|
|
|
+ const {GhostOverlays} = require("../../../../../lib/versioning/GhostOverlays.js");
|
|
|
+ await loadScript("../../../../../sccd/docs/runtimes/javascript/statecharts_core.js");
|
|
|
+ await loadScript("../../../../../lib/versioning/client.js");
|
|
|
+
|
|
|
+ const myId = uuidv4();
|
|
|
+
|
|
|
const names = ["Billy", "James", "Robert", "John", "Mary", "Patricia", "Jennifer", "Barbara"];
|
|
|
- let me = window.localStorage.getItem('drawio-username');
|
|
|
- if (!me) {
|
|
|
- me = names[Math.floor(Math.random()*names.length)];
|
|
|
+ let myName = window.localStorage.getItem('drawio-username');
|
|
|
+ if (!myName) {
|
|
|
+ myName = names[Math.floor(Math.random()*names.length)];
|
|
|
}
|
|
|
|
|
|
const graph = ui.editor.graph;
|
|
@@ -1158,10 +1301,7 @@ Draw.loadPlugin(async function(ui) {
|
|
|
window.graph = graph;
|
|
|
window.model = model;
|
|
|
|
|
|
- await loadScript("../../../../../sccd/docs/runtimes/javascript/statecharts_core.js");
|
|
|
- await loadScript("../../../../../lib/versioning/client.js");
|
|
|
-
|
|
|
- const {DisabledCells} = require("../../../../../lib/versioning/DisabledCells.js")
|
|
|
+ const userColors = new UserColors();
|
|
|
|
|
|
class EnabledState {
|
|
|
constructor() {
|
|
@@ -1191,7 +1331,7 @@ Draw.loadPlugin(async function(ui) {
|
|
|
() => {
|
|
|
const ops = history.getOpsSequence();
|
|
|
const serialized = ops.map(op => op.serialize());
|
|
|
- controller.addInput("new_share", "in", [serialized]);
|
|
|
+ controller.addInput("new_share", "in", [serialized], controller.wallclockToSimtime());
|
|
|
}, // callback
|
|
|
menu,
|
|
|
null, // ?
|
|
@@ -1201,7 +1341,7 @@ Draw.loadPlugin(async function(ui) {
|
|
|
() => {
|
|
|
const sessionId = window.prompt("Enter session ID:");
|
|
|
if (sessionId) {
|
|
|
- controller.addInput("join", "in", [sessionId]);
|
|
|
+ controller.addInput("join", "in", [sessionId], controller.wallclockToSimtime());
|
|
|
}
|
|
|
}, // callback
|
|
|
menu,
|
|
@@ -1210,7 +1350,7 @@ Draw.loadPlugin(async function(ui) {
|
|
|
const buttonLeave = menu.addItem("Leave session",
|
|
|
null, // image
|
|
|
() => {
|
|
|
- controller.addInput("leave", "in", []);
|
|
|
+ controller.addInput("leave", "in", [], controller.wallclockToSimtime());
|
|
|
}, // callback
|
|
|
menu,
|
|
|
null, // ?
|
|
@@ -1250,6 +1390,7 @@ Draw.loadPlugin(async function(ui) {
|
|
|
|
|
|
setOfflineDisconnected() {
|
|
|
graph.setEnabled(true);
|
|
|
+ dragHandler.setOnline(false);
|
|
|
this.statusTextNode.textContent = "Offline (no server connection)";
|
|
|
|
|
|
this.shareEnabled.disable();
|
|
@@ -1259,6 +1400,7 @@ Draw.loadPlugin(async function(ui) {
|
|
|
|
|
|
setOfflineConnected() {
|
|
|
graph.setEnabled(true);
|
|
|
+ dragHandler.setOnline(false);
|
|
|
this.statusTextNode.textContent = "Offline";
|
|
|
|
|
|
this.shareEnabled.enable();
|
|
@@ -1286,11 +1428,13 @@ Draw.loadPlugin(async function(ui) {
|
|
|
|
|
|
setOnline(sessionId) {
|
|
|
graph.setEnabled(true);
|
|
|
+ dragHandler.setOnline(true);
|
|
|
this.statusTextNode.textContent = "Online";
|
|
|
|
|
|
this.shareEnabled.disable();
|
|
|
this.joinEnabled.disable();
|
|
|
this.leaveEnabled.enable();
|
|
|
+
|
|
|
}
|
|
|
|
|
|
setReconnecting() {
|
|
@@ -1305,8 +1449,6 @@ Draw.loadPlugin(async function(ui) {
|
|
|
|
|
|
const uiState = new UIState();
|
|
|
|
|
|
- const {Context, History, uuidv4} = require("../../../../../lib/versioning/History.js");
|
|
|
-
|
|
|
const context = new Context(async id => {
|
|
|
throw new Error("Fetch is forbidden. ", id)
|
|
|
});
|
|
@@ -1334,107 +1476,97 @@ Draw.loadPlugin(async function(ui) {
|
|
|
|
|
|
const setState = (key, value) => {
|
|
|
// console.log("setState", key, value);
|
|
|
+ if (key === "root") {
|
|
|
+ // the only global 'key'
|
|
|
|
|
|
-
|
|
|
- try {
|
|
|
- // temporarily disable listener:
|
|
|
- // listenForEdits = false;
|
|
|
-
|
|
|
- if (key === "root") {
|
|
|
- // the only global 'key'
|
|
|
-
|
|
|
- const rootId = value;
|
|
|
- const root = getCell(rootId) || createCell(rootId, false, false);
|
|
|
- model.setRoot(root);
|
|
|
- }
|
|
|
- else {
|
|
|
- // key is concatenation: <cellId> + '_' + <attribute>
|
|
|
- const splitPoint = key.lastIndexOf('_');
|
|
|
- const attribute = key.substring(splitPoint+1);
|
|
|
- const cellId = key.substring(0, splitPoint-2);
|
|
|
- const isVertex = key[splitPoint-1] === 'V';
|
|
|
- const isEdge = key[splitPoint-1] === 'E';
|
|
|
-
|
|
|
- const cell = getCell(cellId) || createCell(cellId, isVertex, isEdge);
|
|
|
-
|
|
|
- const attrHandlers = {
|
|
|
- geometry: () => {
|
|
|
- new mxGeometryChange(model, )
|
|
|
- const serializedGeometry = value;
|
|
|
- const parsed = xmlParser.parseFromString(serializedGeometry, 'text/xml');
|
|
|
- const geometry = codec.decode(parsed.documentElement);
|
|
|
- model.setGeometry(cell, geometry);
|
|
|
- },
|
|
|
- style: () => {
|
|
|
- const style = value;
|
|
|
- model.setStyle(cell, style);
|
|
|
- },
|
|
|
- parent: () => {
|
|
|
- const cellId = value;
|
|
|
- const parent = model.cells[cellId];
|
|
|
- // The following appears to create a mxChildChange object, indicating that it is this the correct way to set the parent...
|
|
|
- if (parent) {
|
|
|
- model.add(parent, cell, null);
|
|
|
- } else {
|
|
|
- model.remove(cell);
|
|
|
- }
|
|
|
- },
|
|
|
- value: () => {
|
|
|
- let v;
|
|
|
- if (typeof value === 'object') {
|
|
|
- const {xmlEncoded} = value;
|
|
|
- v = xmlParser.parseFromString(xmlEncoded, 'text/xml');
|
|
|
- } else {
|
|
|
- v = value;
|
|
|
- }
|
|
|
- model.setValue(cell, v);
|
|
|
- },
|
|
|
- source: () => {
|
|
|
- const sourceId = value;
|
|
|
- if (sourceId === null) {
|
|
|
- model.setTerminal(cell, null, true);
|
|
|
- } else {
|
|
|
- const source = getCell(sourceId);
|
|
|
- if (source === undefined) {
|
|
|
- throw new Error("NO SUCH CELL:", cellId);
|
|
|
- }
|
|
|
- model.setTerminal(cell, source, true);
|
|
|
+ const rootId = value;
|
|
|
+ const root = getCell(rootId) || createCell(rootId, false, false);
|
|
|
+ model.setRoot(root);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ // key is concatenation: <cellId> + '_' + <attribute>
|
|
|
+ const splitPoint = key.lastIndexOf('_');
|
|
|
+ const attribute = key.substring(splitPoint+1);
|
|
|
+ const cellId = key.substring(0, splitPoint-2);
|
|
|
+ const isVertex = key[splitPoint-1] === 'V';
|
|
|
+ const isEdge = key[splitPoint-1] === 'E';
|
|
|
+
|
|
|
+ const cell = getCell(cellId) || createCell(cellId, isVertex, isEdge);
|
|
|
+
|
|
|
+ const attrHandlers = {
|
|
|
+ geometry: () => {
|
|
|
+ new mxGeometryChange(model, )
|
|
|
+ const serializedGeometry = value;
|
|
|
+ const parsed = xmlParser.parseFromString(serializedGeometry, 'text/xml');
|
|
|
+ const geometry = codec.decode(parsed.documentElement);
|
|
|
+ model.setGeometry(cell, geometry);
|
|
|
+ },
|
|
|
+ style: () => {
|
|
|
+ const style = value;
|
|
|
+ model.setStyle(cell, style);
|
|
|
+ },
|
|
|
+ parent: () => {
|
|
|
+ const cellId = value;
|
|
|
+ const parent = model.cells[cellId];
|
|
|
+ // The following appears to create a mxChildChange object, indicating that it is this the correct way to set the parent...
|
|
|
+ if (parent) {
|
|
|
+ model.add(parent, cell, null);
|
|
|
+ } else {
|
|
|
+ model.remove(cell);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ value: () => {
|
|
|
+ let v;
|
|
|
+ if (typeof value === 'object') {
|
|
|
+ const {xmlEncoded} = value;
|
|
|
+ v = xmlParser.parseFromString(xmlEncoded, 'text/xml');
|
|
|
+ } else {
|
|
|
+ v = value;
|
|
|
+ }
|
|
|
+ model.setValue(cell, v);
|
|
|
+ },
|
|
|
+ source: () => {
|
|
|
+ const sourceId = value;
|
|
|
+ if (sourceId === null) {
|
|
|
+ model.setTerminal(cell, null, true);
|
|
|
+ } else {
|
|
|
+ const source = getCell(sourceId);
|
|
|
+ if (source === undefined) {
|
|
|
+ throw new Error("NO SUCH CELL:", cellId);
|
|
|
}
|
|
|
- },
|
|
|
- target: () => {
|
|
|
- const targetId = value;
|
|
|
- if (targetId === null) {
|
|
|
- model.setTerminal(cell, null, false);
|
|
|
- } else {
|
|
|
- const target = getCell(targetId);
|
|
|
- if (target === undefined) {
|
|
|
- throw new Error("NO SUCH CELL:", cellId);
|
|
|
- }
|
|
|
- model.setTerminal(cell, target, false);
|
|
|
+ model.setTerminal(cell, source, true);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ target: () => {
|
|
|
+ const targetId = value;
|
|
|
+ if (targetId === null) {
|
|
|
+ model.setTerminal(cell, null, false);
|
|
|
+ } else {
|
|
|
+ const target = getCell(targetId);
|
|
|
+ if (target === undefined) {
|
|
|
+ throw new Error("NO SUCH CELL:", cellId);
|
|
|
}
|
|
|
- },
|
|
|
- collapsed: () => {
|
|
|
- const collapsed = value;
|
|
|
- model.setCollapsed(cell, collapsed);
|
|
|
- },
|
|
|
- visible: () => {
|
|
|
- const visible = value;
|
|
|
- model.setVisible(cell, visible);
|
|
|
- },
|
|
|
- vertex: () => {
|
|
|
- const vertex = value;
|
|
|
- cell.setVertex(vertex);
|
|
|
- },
|
|
|
- edge: () => {
|
|
|
- const edge = value;
|
|
|
- cell.setEdge(edge);
|
|
|
- },
|
|
|
- };
|
|
|
- attrHandlers[attribute]();
|
|
|
- }
|
|
|
- } finally {
|
|
|
- // re-enable:
|
|
|
- // listenForEdits = true;
|
|
|
+ model.setTerminal(cell, target, false);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ collapsed: () => {
|
|
|
+ const collapsed = value;
|
|
|
+ model.setCollapsed(cell, collapsed);
|
|
|
+ },
|
|
|
+ visible: () => {
|
|
|
+ const visible = value;
|
|
|
+ model.setVisible(cell, visible);
|
|
|
+ },
|
|
|
+ vertex: () => {
|
|
|
+ const vertex = value;
|
|
|
+ cell.setVertex(vertex);
|
|
|
+ },
|
|
|
+ edge: () => {
|
|
|
+ const edge = value;
|
|
|
+ cell.setEdge(edge);
|
|
|
+ },
|
|
|
+ };
|
|
|
+ attrHandlers[attribute]();
|
|
|
}
|
|
|
};
|
|
|
|
|
@@ -1471,11 +1603,18 @@ Draw.loadPlugin(async function(ui) {
|
|
|
function leaveOnError() {
|
|
|
mergePromise.catch(err => {
|
|
|
console.log("Unexpected error merging", err);
|
|
|
- controller.addInput("leave", "in", []);
|
|
|
+ controller.addInput("leave", "in", [], controller.wallclockToSimtime());
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- const controller = new client.Controller(uiState, new JsEventLoop());
|
|
|
+ function getMyName() {
|
|
|
+ return myName;
|
|
|
+ }
|
|
|
+
|
|
|
+ const controller = new client.Controller(uiState, myId, getMyName, new JsEventLoop());
|
|
|
+
|
|
|
+ const selectionCounter = new Map();
|
|
|
+ const userSelection = new Map();
|
|
|
|
|
|
controller.addMyOwnOutputListener({
|
|
|
'add': event => {
|
|
@@ -1499,101 +1638,82 @@ Draw.loadPlugin(async function(ui) {
|
|
|
// nothing needs to happen
|
|
|
const [sessionId] = event.parameters;
|
|
|
}
|
|
|
- else if (event.name === "received_op") {
|
|
|
- const [op] = event.parameters;
|
|
|
- queuedMerge(op);
|
|
|
- leaveOnError();
|
|
|
- }
|
|
|
- else if (event.name === "left") {
|
|
|
-
|
|
|
- }
|
|
|
- else if (event.name === "update_cursor") {
|
|
|
- const [msg] = event.parameters;
|
|
|
- ghostOverlays.put(msg.userId, msg.x, msg.y);
|
|
|
- // const [msg] = event.parameters;
|
|
|
- // let obj = ghostOverlays.get(msg.userId);
|
|
|
- // if (!obj) {
|
|
|
- // obj = { x: msg.x, y: msg.y, cell: null };
|
|
|
- // ghostOverlays.set(msg.userId, obj);
|
|
|
- // }
|
|
|
- // ghostOverlays.set(msg.userId, obj);
|
|
|
- // updateGhostCursors();
|
|
|
+ else if (event.name === "broadcast") {
|
|
|
+ const [sessionId, msg] = event.parameters;
|
|
|
+ if (msg.type === "pub_edit") {
|
|
|
+ const {op} = msg;
|
|
|
+ queuedMerge(op);
|
|
|
+ leaveOnError();
|
|
|
+ }
|
|
|
+ else if (msg.type === "update_cursor") {
|
|
|
+ const {userId, name, x, y} = msg;
|
|
|
+ ghostOverlays.put(userId, name, x, y);
|
|
|
+ }
|
|
|
+ else if (msg.type === "update_selection") {
|
|
|
+ const {userId, selectedIds} = msg;
|
|
|
+ const oldHighlights = userSelection.get(userId) || [];
|
|
|
+ for (const highlight of oldHighlights) {
|
|
|
+ highlight.destroy();
|
|
|
+ }
|
|
|
+ const color = userColors.getColor(userId);
|
|
|
+ const newHighlights = selectedIds.map(cellId => {
|
|
|
+ const cell = graph.model.cells[cellId];
|
|
|
+ const highlight = new mxCellHighlight(graph, color, 6);
|
|
|
+ highlight.highlight(graph.view.getState(cell));
|
|
|
+ return highlight;
|
|
|
+ });
|
|
|
+ userSelection.set(userId, newHighlights);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
- const disabledCells = new DisabledCells();
|
|
|
- disabledCells.install(graph);
|
|
|
-
|
|
|
- class GhostOverlays {
|
|
|
- constructor(ui) {
|
|
|
- this.map = new Map();
|
|
|
- this.canvas = ui.editor.graph.view.canvas;
|
|
|
- this.svg = this.canvas.parentElement;
|
|
|
- }
|
|
|
-
|
|
|
- // Update or create ghost cursor. Idempotent.
|
|
|
- put(userId, x, y) {
|
|
|
- let state = this.map.get(userId);
|
|
|
- if (state === undefined) {
|
|
|
- const g = document.createElementNS("http://www.w3.org/2000/svg", 'g');
|
|
|
- const image = document.createElementNS("http://www.w3.org/2000/svg", 'image');
|
|
|
- const text = document.createElementNS("http://www.w3.org/2000/svg", 'text');
|
|
|
- text.style.fontSize = "10px";
|
|
|
- text.setAttribute('y', 30);
|
|
|
- const textNode = document.createTextNode(userId);
|
|
|
- text.appendChild(textNode);
|
|
|
- image.setAttribute('href', "/lib/versioning/resources/cursor.svg");
|
|
|
- image.setAttribute('width', 11.6);
|
|
|
- image.setAttribute('height', 18.2);
|
|
|
- g.appendChild(image);
|
|
|
- g.appendChild(text);
|
|
|
- this.canvas.appendChild(g);
|
|
|
- const transform = this.svg.createSVGTransform();
|
|
|
- g.transform.baseVal.appendItem(transform);
|
|
|
- state = {transform, timeout: null};
|
|
|
- this.map.set(userId, state);
|
|
|
- }
|
|
|
- state.transform.setTranslate(
|
|
|
- (x + graph.view.translate.x) * graph.view.scale,
|
|
|
- (y + graph.view.translate.y) * graph.view.scale,
|
|
|
- );
|
|
|
- if (state.timeout) {
|
|
|
- clearTimeout(state.timeout);
|
|
|
- }
|
|
|
- state.timeout = setTimeout(() => state.transform.setTranslate(-50, -50), 5000);
|
|
|
- }
|
|
|
- }
|
|
|
+ const dragHandler = new DragHandler(controller);
|
|
|
+ dragHandler.install(graph);
|
|
|
|
|
|
- const ghostOverlays = new GhostOverlays(ui);
|
|
|
+ const ghostOverlays = new GhostOverlays(graph, userColors);
|
|
|
|
|
|
graph.addMouseListener({
|
|
|
mouseDown(graph, event) {
|
|
|
-
|
|
|
},
|
|
|
mouseMove(graph, event) {
|
|
|
- const msg = {
|
|
|
- type: "update_cursor",
|
|
|
- userId: me,
|
|
|
- x: event.graphX / graph.view.scale - graph.view.translate.x,
|
|
|
- y: event.graphY / graph.view.scale - graph.view.translate.y,
|
|
|
- }
|
|
|
-
|
|
|
- controller.addInput("update_cursor", "in", [msg]);
|
|
|
+ const x = event.graphX / graph.view.scale - graph.view.translate.x;
|
|
|
+ const y = event.graphY / graph.view.scale - graph.view.translate.y;
|
|
|
+ controller.addInput("broadcast_cursor", "in", [x,y], controller.wallclockToSimtime());
|
|
|
},
|
|
|
mouseUp(graph, event) {
|
|
|
-
|
|
|
},
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
+ graph.selectionModel.addListener(mxEvent.CHANGE, (source, eventObj) => {
|
|
|
+ console.log("selectionModel event CHANGE");
|
|
|
+ // const addedIds = (eventObj.properties.removed || []).map(cell => cell.id);
|
|
|
+ // const removedIds = (eventObj.properties.added || []).map(cell => cell.id);
|
|
|
+ const selectedIds = graph.getSelectionCells().map(cell => cell.id);
|
|
|
+ controller.addInput(
|
|
|
+ "broadcast_selection",
|
|
|
+ "in",
|
|
|
+ [selectedIds],
|
|
|
+ controller.wallclockToSimtime(),
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ const oldMxSelectionChange = mxSelectionChange; // override constructor :)
|
|
|
+ mxSelectionChange = function(selectionModel, added, removed) {
|
|
|
+ console.log("mxSelectionChange constructor");
|
|
|
+ oldMxSelectionChange.apply(this, arguments);
|
|
|
+ }
|
|
|
+ mxSelectionChange.prototype = oldMxSelectionChange.prototype;
|
|
|
+
|
|
|
+
|
|
|
{
|
|
|
// Upon loading the page, if a 'sessionId' URL parameter is given, automatically join the given session once the connection with the server is established.
|
|
|
const searchParams = new URLSearchParams(window.location.search);
|
|
|
const sessionId = searchParams.get('sessionId');
|
|
|
if (sessionId) {
|
|
|
- controller.addInput("init_join", "in", [sessionId]);
|
|
|
+ controller.addInput("init_join", "in", [sessionId], 0);
|
|
|
} else {
|
|
|
- controller.addInput("init_offline", "in", []);
|
|
|
+ controller.addInput("init_offline", "in", [], 0);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -1605,10 +1725,9 @@ Draw.loadPlugin(async function(ui) {
|
|
|
} else {
|
|
|
throw new Error("unexpected protocol: " + window.location.protocol);
|
|
|
}
|
|
|
- websocketAddress += "//" + window.location.host + "/websocket";
|
|
|
-
|
|
|
- controller.addInput("connect", "in", [websocketAddress]);
|
|
|
+ websocketAddress += "//" + window.location.host + "/websocket?me=" + myId;
|
|
|
|
|
|
+ controller.addInput("connect", "in", [websocketAddress], 0);
|
|
|
|
|
|
controller.start();
|
|
|
|
|
@@ -1730,7 +1849,7 @@ Draw.loadPlugin(async function(ui) {
|
|
|
);
|
|
|
const serializedOp = op.serialize();
|
|
|
console.log("OP:", serializedOp);
|
|
|
- controller.addInput("new_edit", "in", [serializedOp]);
|
|
|
+ controller.addInput("new_edit", "in", [serializedOp], controller.wallclockToSimtime());
|
|
|
}
|
|
|
}
|
|
|
});
|
|
@@ -1752,11 +1871,11 @@ Draw.loadPlugin(async function(ui) {
|
|
|
}
|
|
|
|
|
|
if (e.code === 'KeyU') {
|
|
|
- const u = window.prompt("Choose a user name:", me);
|
|
|
+ const u = window.prompt("Choose a user name:", myName);
|
|
|
if (u) {
|
|
|
- me = u;
|
|
|
+ myName = u;
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
});
|
|
|
-},{"../../../../../lib/versioning/DisabledCells.js":16,"../../../../../lib/versioning/History.js":17}]},{},[18]);
|
|
|
+},{"../../../../../lib/versioning/DragHandler.js":16,"../../../../../lib/versioning/GhostOverlays.js":17,"../../../../../lib/versioning/History.js":18,"../../../../../lib/versioning/UserColors.js":19}]},{},[20]);
|