ScreenShare = (function() { function encode(cells) { const codec = new mxCodec(); const encoded = codec.encode(cells); return mxUtils.getXml(encoded); }; return class { decode(xmlString) { const parsedXml = mxUtils.parseXml(xmlString).documentElement; const codec = new mxCodec(); codec.lookup = id => this.graph.model.cells[id]; return codec.decode(parsedXml); } constructor(client, peers, graph, undoManager, confirm, alert) { this.graph = graph; this.undoManager = undoManager; this.sharingWith = null; this.confirm = confirm; this.alert = alert; const otherPeerEndScreenshare = peer => { if (this.sharingWith === peer) { this.alert(`Peer ${shortUUID(peer)} left. You are alone again.`); this.sharingWith = null; } } peers.on('leave', otherPeerEndScreenshare); const share = (what, data) => { if (this.sharingWith) { this.p2p.send(this.sharingWith, what, data, err => { if (err) console.log("ignoring err:", err) }); } } let listenerEnabled = true; this.undoManager.addListener(null, (source, eventObj) => { if (listenerEnabled) { if (eventObj.properties.edit) { const {changes, redone, undone, significant} = eventObj.properties.edit; share("undoEvent", { encodedChanges: changes.map(c => encode(c)), redone, undone, significant, }); } } }); this.graph.selectionModel.addListener(mxEvent.CHANGE, (source, eventObj) => { if (listenerEnabled) { const {added, removed} = eventObj.properties; share("selectionEvent", { addedIds: removed ? removed.map(cell => cell.id) : [], removedIds: added ? added.map(cell => cell.id) : [], }); } }); // Locking const locked = {}; // map cell id => mxCellHighlight const lockCell = cell => { const highlight = locked[cell.id]; if (!highlight) { const highlight = new mxCellHighlight(this.graph, "#7700ff", 6); highlight.highlight(this.graph.view.getState(cell)); locked[cell.id] = highlight; } }; const unlockCell = cell => { const highlight = locked[cell.id]; if (highlight) { highlight.destroy(); delete locked[cell.id] } } // Locking part #1: Intercepting mxGraph.fireMouseEvent const oldFireMouseEvent = this.graph.fireMouseEvent; this.graph.fireMouseEvent = function(evtName, me, sender) { if (me.state && locked[me.state.cell.id]) { // clicked shape is locked return; } oldFireMouseEvent.apply(this, arguments); } // Locking part #2: Ignore double clicks on locked cells const oldDblClick = this.graph.dblClick; this.graph.dblClick = function(evt, cell) { if (cell && locked[cell.id]) { // clicked shape is locked return; } oldDblClick.apply(this, arguments); } // Locking part #3: Protect locked 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 => !locked[cell.id]); } } mxSelectionChange.prototype = oldMxSelectionChange.prototype; // mxGraphHandler overrides to get previews of moving shapes // These overrides wrap the original implementations and additionally send messages to the "screensharee". // The screensharee uses this messages to draw previews at his side. // Begin of move const oldStart = this.graph.graphHandler.start; this.graph.graphHandler.start = function(cell, x, y, cells) { oldStart.apply(this, arguments); // cells that will be moved on our side cells = this.graph.graphHandler.getCells(cell); share("graphHandlerStart", { cellIds: cells.map(cell => cell.id), x, y }); }; // Redraw operation, caused by mouseMove event, during move const oldUpdateLivePreview = this.graph.graphHandler.updateLivePreview; this.graph.graphHandler.updateLivePreview = function(dx, dy) { oldUpdateLivePreview.apply(this, arguments); share("graphHandlerUpdateLivePreview", {dx, dy}); } // End of move const oldReset = this.graph.graphHandler.reset; this.graph.graphHandler.reset = function() { oldReset.apply(this, arguments); share("graphHandlerReset", null); // no data }; //// VERTEX HANDLER OVERRIDES - a broken attempt at previewing resizing shapes .... // const oldVertexStart = mxVertexHandler.prototype.start; // mxVertexHandler.prototype.start = function(x, y, index) { // console.log("begin resize", this, x, y , index); // oldVertexStart.apply(this, arguments); // shareFunctionCall("vertexHandlerStart", { // cellId: this.state.cell.id, // x, y, // index, // number (0-7) of resize handle pressed // }); // } // const oldVertexReset = mxVertexHandler.prototype.reset; // mxVertexHandler.prototype.reset = function() { // console.log("reset resize"); // oldVertexReset.apply(this, arguments); // shareFunctionCall("vertexHandlerReset", { // cellId: this.state.cell.id, // }); // } // const oldVertexUpdateLivePreview = mxVertexHandler.prototype.updateLivePreview; // mxVertexHandler.prototype.updateLivePreview = function(me) { // console.log("update resize preview", me); // oldVertexUpdateLivePreview.apply(this, arguments); // shareFunctionCall("vertexHandlerUpdateLivePreview", { // cellId: this.state.cell.id, // bounds: { // x: this.bounds.x, // y: this.bounds.y, // width: this.bounds.width, // height: this.bounds.height, // }, // }); // } // Handler for incoming requests from other peers this.p2p = new PeerToPeer(client, { // Handlers for received mxGraphHandler messages that we sent above. // mxGraphHandler (moving cells) "graphHandlerStart": (from, {cellIds, x, y}, reply) => { if (this.sharingWith === from) { // the mxGraphHandler will determine the cells to move based on the current selection // a hack within a hack - we temporarily override 'getCells': const oldGetCells = this.graph.graphHandler.getCells; this.graph.graphHandler.getCells = function(initialCell) { return cellIds.map(id => this.graph.model.cells[id]); } oldStart.apply(this.graph.graphHandler, [ null, // 'cells' - this argument isn't important since we overrided getCells x, y, null, ]); this.graph.graphHandler.checkPreview(); // force some stuff to happen this.graph.graphHandler.getCells = oldGetCells; // restore override } reply(); }, "graphHandlerUpdateLivePreview": (from, {dx,dy}, reply) => { if (this.sharingWith === from) { oldUpdateLivePreview.apply(this.graph.graphHandler, [dx, dy]); } reply(); }, "graphHandlerReset": (from, _, reply) => { if (this.sharingWith === from) { oldReset.apply(this.graph.graphHandler, []); } reply(); }, "undoEvent": (from, {encodedChanges, undone, redone, significant}, reply) => { if (this.sharingWith === from) { try { listenerEnabled = false; // Undoable Edit happened at other peer const changes = encodedChanges.map(encoded => { const change = this.decode(encoded); change.model = this.graph.model; return change }); if (undone) { this.undoManager.undo(); } else if (redone) { this.undoManager.redo(); } else { // Probably not necessary to wrap in update-transaction, but won't do harm: this.graph.model.beginUpdate(); changes.forEach(change => this.graph.model.execute(change)); this.graph.model.endUpdate(); } } finally { listenerEnabled = true; reply(); // acknowledge } } }, "selectionEvent": (from, {addedIds, removedIds}, reply) => { if (this.sharingWith === from) { try { listenerEnabled = false; // Selection changed at other peer - lock selected cells const removed = removedIds.map(id => this.graph.model.cells[id]); const added = addedIds.map(id => this.graph.model.cells[id]); removed.forEach(unlockCell); added.forEach(lockCell); } finally { listenerEnabled = true; reply(); // acknowledge } } }, // Received Screen Share request "init_screenshare": (from, {graphSerialized, selectedCellIds}, reply) => { const yes = () => { const doc = mxUtils.parseXml(graphSerialized); const codec = new mxCodec(doc); codec.decode(doc.documentElement, this.graph.model); selectedCellIds.forEach(id => lockCell(this.graph.model.cells[id])); this.sharingWith = from; reply(); // acknowledge this.alert("You are now screen sharing with " + shortUUID(from)); }; const no = () => { reply("denied") }; this.confirm(`Peer ${shortUUID(from)} wants to screen share.
Your diagram will be erased and replaced by his/hers.

Accept?`, yes, no); }, "end_screenshare": (from, data, reply) => { reply(); otherPeerEndScreenshare(from); } }); } initshare(peer) { const doIt = () => { const graphSerialized = encode(this.graph.model); const selectedCellIds = this.graph.getSelectionCells().map(cell => cell.id); this.p2p.send(peer, "init_screenshare", { graphSerialized, selectedCellIds, }, (err, data) => { if (err) { if (err === "denied") { this.alert(`Peer ${peer} denied your sharing request :(`); } else { this.alert("Error sending screenshare request: " + err); } } else { this.alert("Accepted: You are now screen sharing with " + shortUUID(peer)); this.sharingWith = peer; } }); this.alert("Request sent. Awaiting response.") } if (this.sharingWith && this.sharingWith !== peer) { // first, end earlier screenshare const yes = () => { this.p2p.send(this.sharingWith, "end_screenshare", null, (err, data) => { // don't care about response doIt(); }) }; const no = () => { // do nothing } this.confirm(`To screenshare with peer ${shortUUID(peer)}, you first have to end your screenshare with peer ${shortUUID(this.sharingWith)}.

OK?`, yes, no); } else { doIt(); } } } })();