/** * Copyright (c) 2006-2017, JGraph Ltd * Copyright (c) 2006-2017, Gaudenz Alder */ /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.DIFF_INSERT = 'i'; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.DIFF_REMOVE = 'r'; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.DIFF_UPDATE = 'u'; /** * Shared codec. */ EditorUi.prototype.codec = new mxCodec(); /** * Contains all view state properties that should not be ignored in diff sync. */ EditorUi.prototype.viewStateProperties = {background: true, backgroundImage: true, shadowVisible: true, foldingEnabled: true, pageScale: true, mathEnabled: true, pageFormat: true, extFonts: true}; /** * Contains all known cell properties that should be ignored for a generic cell diff. */ EditorUi.prototype.cellProperties = {id: true, value: true, xmlValue: true, vertex: true, edge: true, visible: true, collapsed: true, connectable: true, parent: true, children: true, previous: true, source: true, target: true, edges: true, geometry: true, style: true, mxObjectId: true, mxTransient: true}; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.patchPages = function(pages, diff, markPages, resolver, updateEdgeParents) { var resolverLookup = {}; var newPages = []; var inserted = {}; var removed = {}; var lookup = {}; var moved = {}; if (resolver != null && resolver[EditorUi.DIFF_UPDATE] != null) { for (var id in resolver[EditorUi.DIFF_UPDATE]) { resolverLookup[id] = resolver[EditorUi.DIFF_UPDATE][id]; } } if (diff[EditorUi.DIFF_REMOVE] != null) { for (var i = 0; i < diff[EditorUi.DIFF_REMOVE].length; i++) { removed[diff[EditorUi.DIFF_REMOVE][i]] = true; } } if (diff[EditorUi.DIFF_INSERT] != null) { for (var i = 0; i < diff[EditorUi.DIFF_INSERT].length; i++) { inserted[diff[EditorUi.DIFF_INSERT][i].previous] = diff[EditorUi.DIFF_INSERT][i]; } } if (diff[EditorUi.DIFF_UPDATE] != null) { for (var id in diff[EditorUi.DIFF_UPDATE]) { var pageDiff = diff[EditorUi.DIFF_UPDATE][id]; if (pageDiff.previous != null) { moved[pageDiff.previous] = id; } } } // Restores existing order and creates lookup if (pages != null) { var prev = ''; for (var i = 0; i < pages.length; i++) { var pageId = pages[i].getId(); lookup[pageId] = pages[i]; if (moved[prev] == null && !removed[pageId] && (diff[EditorUi.DIFF_UPDATE] == null || diff[EditorUi.DIFF_UPDATE][pageId] == null || diff[EditorUi.DIFF_UPDATE][pageId].previous == null)) { moved[prev] = pageId; } prev = pageId; } } // FIXME: Workaround for possible duplicate pages var added = {}; var addPage = mxUtils.bind(this, function(page) { var id = (page != null) ? page.getId() : ''; if (page != null && !added[id]) { added[id] = true; newPages.push(page); var pageDiff = (diff[EditorUi.DIFF_UPDATE] != null) ? diff[EditorUi.DIFF_UPDATE][id] : null; if (pageDiff != null) { this.updatePageRoot(page); if (pageDiff.name != null) { page.setName(pageDiff.name); } if (pageDiff.view != null) { this.patchViewState(page, pageDiff.view); } if (pageDiff.cells != null) { this.patchPage(page, pageDiff.cells, resolverLookup[page.getId()], updateEdgeParents); } if (markPages && (pageDiff.cells != null || pageDiff.view != null)) { page.needsUpdate = true; } } } var mov = moved[id]; if (mov != null) { delete moved[id]; addPage(lookup[mov]); } var ins = inserted[id]; if (ins != null) { delete inserted[id]; insertPage(ins); } }); var insertPage = mxUtils.bind(this, function(ins) { var diagram = mxUtils.parseXml(ins.data).documentElement; var newPage = new DiagramPage(diagram); this.updatePageRoot(newPage); var page = lookup[newPage.getId()]; if (page == null) { addPage(newPage); } else { // Updates root if page already in UI page.root = newPage.root; if (this.currentPage == page) { this.editor.graph.model.setRoot(page.root); } else if (markPages) { page.needsUpdate = true; } } }); addPage(); // Handles orphaned moved pages for (var id in moved) { addPage(lookup[moved[id]]); delete moved[id]; } // Handles orphaned inserted pages for (var id in inserted) { insertPage(inserted[id]); delete inserted[id]; } return newPages; }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.patchViewState = function(page, diff) { if (page.viewState != null && diff != null) { if (page == this.currentPage) { page.viewState = this.editor.graph.getViewState(); } for (var key in diff) { try { page.viewState[key] = JSON.parse(diff[key]); } catch(e) {} //Ignore TODO Is this correct, we encountered an undefined value for a key (extFonts) } if (page == this.currentPage) { this.editor.graph.setViewState(page.viewState, true); } } }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.createParentLookup = function(model, diff) { var parentLookup = {}; function getLookup(id) { var result = parentLookup[id]; if (result == null) { result = {inserted: [], moved: {}}; parentLookup[id] = result; } return result; }; if (diff[EditorUi.DIFF_INSERT] != null) { for (var i = 0; i < diff[EditorUi.DIFF_INSERT].length; i++) { var temp = diff[EditorUi.DIFF_INSERT][i]; var par = (temp.parent != null) ? temp.parent : ''; var prev = (temp.previous != null) ? temp.previous : ''; getLookup(par).inserted[prev] = temp; } } if (diff[EditorUi.DIFF_UPDATE] != null) { for (var id in diff[EditorUi.DIFF_UPDATE]) { var temp = diff[EditorUi.DIFF_UPDATE][id]; if (temp.previous != null) { var par = temp.parent; if (par == null) { var cell = model.getCell(id); if (cell != null) { var parent = model.getParent(cell); if (parent != null) { par = parent.getId(); } } } if (par != null) { getLookup(par).moved[temp.previous] = id; } } } } return parentLookup; }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.patchPage = function(page, diff, resolver, updateEdgeParents) { var model = (page == this.currentPage) ? this.editor.graph.model : new mxGraphModel(page.root); var parentLookup = this.createParentLookup(model, diff); model.beginUpdate(); try { // Disables or delays update of edge parents to after patch var prev = model.updateEdgeParent; var dict = new mxDictionary(); var pendingUpdates = []; model.updateEdgeParent = function(edge, root) { if (!dict.get(edge) && updateEdgeParents) { dict.put(edge, true); pendingUpdates.push(edge); } }; // Handles new root cells var temp = parentLookup['']; var cellDiff = (temp != null && temp.inserted != null) ? temp.inserted[''] : null; var root = null; if (cellDiff != null) { root = this.getCellForJson(cellDiff); } // Handles cells becoming root (very unlikely but possible) if (root == null) { var id = (temp != null && temp.moved != null) ? temp.moved[''] : null; if (id != null) { root = model.getCell(id); } } if (root != null) { model.setRoot(root); page.root = root; } // Inserts and updates previous and parent (hierarchy update) this.patchCellRecursive(page, model, model.root, parentLookup, diff); // Removes cells after parents have been updated above if (diff[EditorUi.DIFF_REMOVE] != null) { for (var i = 0; i < diff[EditorUi.DIFF_REMOVE].length; i++) { var cell = model.getCell(diff[EditorUi.DIFF_REMOVE][i]); if (cell != null) { model.remove(cell); } } } // Updates cell states and terminals if (diff[EditorUi.DIFF_UPDATE] != null) { var res = (resolver != null && resolver.cells != null) ? resolver.cells[EditorUi.DIFF_UPDATE] : null; for (var id in diff[EditorUi.DIFF_UPDATE]) { this.patchCell(model, model.getCell(id), diff[EditorUi.DIFF_UPDATE][id], (res != null) ? res[id] : null); } } // Updates terminals for inserted cells if (diff[EditorUi.DIFF_INSERT] != null) { for (var i = 0; i < diff[EditorUi.DIFF_INSERT].length; i++) { var cellDiff = diff[EditorUi.DIFF_INSERT][i]; var cell = model.getCell(cellDiff.id); if (cell != null) { model.setTerminal(cell, model.getCell(cellDiff.source), true); model.setTerminal(cell, model.getCell(cellDiff.target), false); } } } // Delayed update of edge parents model.updateEdgeParent = prev; if (updateEdgeParents && pendingUpdates.length > 0) { for (var i = 0; i < pendingUpdates.length; i++) { if (model.contains(pendingUpdates[i])) { model.updateEdgeParent(pendingUpdates[i]); } } } } finally { model.endUpdate(); } }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.patchCellRecursive = function(page, model, cell, parentLookup, diff) { if (cell != null) { var temp = parentLookup[cell.getId()]; var inserted = (temp != null && temp.inserted != null) ? temp.inserted : {}; var moved = (temp != null && temp.moved != null) ? temp.moved : {}; var index = 0; // Restores existing order var childCount = model.getChildCount(cell); var prev = ''; for (var i = 0; i < childCount; i++) { var cellId = model.getChildAt(cell, i).getId(); if (moved[prev] == null && (diff[EditorUi.DIFF_UPDATE] == null || diff[EditorUi.DIFF_UPDATE][cellId] == null || (diff[EditorUi.DIFF_UPDATE][cellId].previous == null && diff[EditorUi.DIFF_UPDATE][cellId].parent == null))) { moved[prev] = cellId; } prev = cellId; } var addCell = mxUtils.bind(this, function(child, insert) { var id = (child != null) ? child.getId() : ''; // Ignores the insert if the cell is already in the model if (child != null && insert) { var ex = model.getCell(id); if (ex != null && ex != child) { child = null; } } if (child != null) { if (model.getChildAt(cell, index) != child) { model.add(cell, child, index); } this.patchCellRecursive(page, model, child, parentLookup, diff); index++; } return id; }); // Uses stack to avoid recursion for children var children = [null]; while (children.length > 0) { var entry = children.shift(); var child = (entry != null) ? entry.child : null; var insert = (entry != null) ? entry.insert : false; var id = addCell(child, insert); // Move and insert are mutually exclusive per predecessor // since an insert changes the predecessor of existing cells // and is therefore ignored in the loop above where the order // for existing cells is added to the moved object var mov = moved[id]; if (mov != null) { delete moved[id]; children.push({child: model.getCell(mov)}); } var ins = inserted[id]; if (ins != null) { delete inserted[id]; children.push({child: this.getCellForJson(ins), insert: true}); } // Orphaned moves and inserts are operations where the previous cell vanished // in the local model so their position in the child array cannot be determined. // In this case those cells are appended. Dependencies between orphans are // maintained because for-in loops enumerate the IDs in order of insertion. if (children.length == 0) { // Handles orphaned moved pages for (var id in moved) { children.push({child: model.getCell(moved[id])}); delete moved[id]; } // Handles orphaned inserted pages for (var id in inserted) { children.push({child: this.getCellForJson(inserted[id]), insert: true}); delete inserted[id]; } } } } }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.patchCell = function(model, cell, diff, resolve) { if (cell != null && diff != null) { // Last write wins for value except if label is empty if (resolve == null || (resolve.xmlValue == null && (resolve.value == null || resolve.value == ''))) { if ('value' in diff) { model.setValue(cell, diff.value); } else if (diff.xmlValue != null) { model.setValue(cell, mxUtils.parseXml(diff.xmlValue).documentElement); } } // Last write wins for style if ((resolve == null || resolve.style == null) && diff.style != null) { model.setStyle(cell, diff.style); } if (diff.visible != null) { model.setVisible(cell, diff.visible == 1); } if (diff.collapsed != null) { model.setCollapsed(cell, diff.collapsed == 1); } if (diff.vertex != null) { // Changes vertex state in-place cell.vertex = diff.vertex == 1; } if (diff.edge != null) { // Changes edge state in-place cell.edge = diff.edge == 1; } if (diff.connectable != null) { // Changes connectable state in-place cell.connectable = diff.connectable == 1; } if (diff.geometry != null) { model.setGeometry(cell, this.codec.decode(mxUtils.parseXml( diff.geometry).documentElement)); } if (diff.source != null) { model.setTerminal(cell, model.getCell(diff.source), true); } if (diff.target != null) { model.setTerminal(cell, model.getCell(diff.target), false); } for (var key in diff) { if (!this.cellProperties[key]) { cell[key] = diff[key]; } } } }; /** * Gets a file node that is comparable with a remote file node * so that using isEqualNode returns true if the files can be * considered equal. */ EditorUi.prototype.getPagesForNode = function(node, nodeName) { var tmp = this.editor.extractGraphModel(node, true, true); if (tmp != null) { node = tmp; } var diagrams = node.getElementsByTagName(nodeName || 'diagram'); var pages = []; if (diagrams.length > 0) { for (var i = 0; i < diagrams.length; i++) { var page = new DiagramPage(diagrams[i]); this.updatePageRoot(page, true); pages.push(page); } } else if (node.nodeName == 'mxGraphModel') { var graph = this.editor.graph; var page = new DiagramPage(node.ownerDocument.createElement('diagram')); page.setName(mxResources.get('pageWithNumber', [1])); mxUtils.setTextContent(page.node, Graph.compressNode(node, true)); pages.push(page); } return pages; }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.diffPages = function(oldPages, newPages) { var graph = this.editor.graph; var inserted = []; var removed = []; var result = {}; var lookup = {}; var diff = {}; var prev = null; for (var i = 0; i < newPages.length; i++) { lookup[newPages[i].getId()] = {page: newPages[i], prev: prev}; prev = newPages[i]; } prev = null; for (var i = 0; i < oldPages.length; i++) { var id = oldPages[i].getId(); var newPage = lookup[id]; if (newPage == null) { removed.push(id); } else { var temp = this.diffPage(oldPages[i], newPage.page); var pageDiff = {}; if (Object.keys(temp).length > 0) { pageDiff.cells = temp; } var view = this.diffViewState(oldPages[i], newPage.page); if (Object.keys(view).length > 0) { pageDiff.view = view; } if (((newPage.prev != null) ? prev == null : prev != null) || (prev != null && newPage.prev != null && prev.getId() != newPage.prev.getId())) { pageDiff.previous = (newPage.prev != null) ? newPage.prev.getId() : ''; } // FIXME: Check why names can be null in newer files // ignore in hash and do not diff null names for now if (newPage.page.getName() != null && oldPages[i].getName() != newPage.page.getName()) { pageDiff.name = newPage.page.getName(); } if (Object.keys(pageDiff).length > 0) { diff[id] = pageDiff; } } delete lookup[oldPages[i].getId()]; prev = oldPages[i]; } for (var id in lookup) { var newPage = lookup[id]; inserted.push({data: mxUtils.getXml(newPage.page.node), previous: (newPage.prev != null) ? newPage.prev.getId() : ''}); } if (Object.keys(diff).length > 0) { result[EditorUi.DIFF_UPDATE] = diff; } if (removed.length > 0) { result[EditorUi.DIFF_REMOVE] = removed; } if (inserted.length > 0) { result[EditorUi.DIFF_INSERT] = inserted; } return result; }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.createCellLookup = function(cell, prev, lookup) { lookup = (lookup != null) ? lookup : {}; lookup[cell.getId()] = {cell: cell, prev: prev}; var childCount = cell.getChildCount(); prev = null; for (var i = 0; i < childCount; i++) { var child = cell.getChildAt(i); this.createCellLookup(child, prev, lookup); prev = child; } return lookup; }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.diffCellRecursive = function(cell, prev, lookup, diff, removed) { diff = (diff != null) ? diff : {}; var newCell = lookup[cell.getId()]; delete lookup[cell.getId()]; if (newCell == null) { removed.push(cell.getId()); } else { var temp = this.diffCell(cell, newCell.cell); if (temp.parent != null || (((newCell.prev != null) ? prev == null : prev != null) || (prev != null && newCell.prev != null && prev.getId() != newCell.prev.getId()))) { temp.previous = (newCell.prev != null) ? newCell.prev.getId() : ''; } if (Object.keys(temp).length > 0) { diff[cell.getId()] = temp; } } var childCount = cell.getChildCount(); prev = null; for (var i = 0; i < childCount; i++) { var child = cell.getChildAt(i); this.diffCellRecursive(child, prev, lookup, diff, removed); prev = child; } return diff; }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.diffPage = function(oldPage, newPage) { var inserted = []; var removed = []; var result = {}; this.updatePageRoot(oldPage); this.updatePageRoot(newPage); var lookup = this.createCellLookup(newPage.root); var diff = this.diffCellRecursive(oldPage.root, null, lookup, diff, removed); for (var id in lookup) { var newCell = lookup[id]; inserted.push(this.getJsonForCell(newCell.cell, newCell.prev)); } if (Object.keys(diff).length > 0) { result[EditorUi.DIFF_UPDATE] = diff; } if (removed.length > 0) { result[EditorUi.DIFF_REMOVE] = removed; } if (inserted.length > 0) { result[EditorUi.DIFF_INSERT] = inserted; } return result; }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.diffViewState = function(oldPage, newPage) { var source = oldPage.viewState; var target = newPage.viewState; var result = {}; if (newPage == this.currentPage) { target = this.editor.graph.getViewState(); } if (source != null && target != null) { for (var key in this.viewStateProperties) { // LATER: Check if normalization is needed for // object attribute order to compare JSON var old = JSON.stringify(source[key]); var now = JSON.stringify(target[key]); if (old != now) { result[key] = now; } } } return result; }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.getCellForJson = function(json) { var geometry = (json.geometry != null) ? this.codec.decode( mxUtils.parseXml(json.geometry).documentElement) : null; var value = json.value; if (json.xmlValue != null) { value = mxUtils.parseXml(json.xmlValue).documentElement; } var cell = new mxCell(value, geometry, json.style); cell.connectable = json.connectable != 0; cell.collapsed = json.collapsed == 1; cell.visible = json.visible != 0; cell.vertex = json.vertex == 1; cell.edge = json.edge == 1; cell.id = json.id; for (var key in json) { if (!this.cellProperties[key]) { cell[key] = json[key]; } } return cell; }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.getJsonForCell = function(cell, previous) { var result = {id: cell.getId()}; if (cell.vertex) { result.vertex = 1; } if (cell.edge) { result.edge = 1; } if (!cell.connectable) { result.connectable = 0; } if (cell.parent != null) { result.parent = cell.parent.getId(); } if (previous != null) { result.previous = previous.getId(); } if (cell.source != null) { result.source = cell.source.getId(); } if (cell.target != null) { result.target = cell.target.getId(); } if (cell.style != null) { result.style = cell.style; } if (cell.geometry != null) { result.geometry = mxUtils.getXml(this.codec.encode(cell.geometry)); } if (cell.collapsed) { result.collapsed = 1; } if (!cell.visible) { result.visible = 0; } if (cell.value != null) { if (typeof cell.value === 'object' && typeof cell.value.nodeType === 'number' && typeof cell.value.nodeName === 'string' && typeof cell.value.getAttribute === 'function') { result.xmlValue = mxUtils.getXml(cell.value); } else { result.value = cell.value; } } for (var key in cell) { if (!this.cellProperties[key] && typeof cell[key] !== 'function') { result[key] = cell[key]; } } return result; }; /** * Removes all labels, user objects and styles from the given node in-place. */ EditorUi.prototype.diffCell = function(oldCell, newCell) { var diff = {}; if (oldCell.vertex != newCell.vertex) { diff.vertex = (newCell.vertex) ? 1 : 0; } if (oldCell.edge != newCell.edge) { diff.edge = (newCell.edge) ? 1 : 0; } if (oldCell.connectable != newCell.connectable) { diff.connectable = (newCell.connectable) ? 1 : 0; } if (((oldCell.parent != null) ? newCell.parent == null : newCell.parent != null) || (oldCell.parent != null && newCell.parent != null && oldCell.parent.getId() != newCell.parent.getId())) { diff.parent = (newCell.parent != null) ? newCell.parent.getId() : ''; } if (((oldCell.source != null) ? newCell.source == null : newCell.source != null) || (oldCell.source != null && newCell.source != null && oldCell.source.getId() != newCell.source.getId())) { diff.source = (newCell.source != null) ? newCell.source.getId() : ''; } if (((oldCell.target != null) ? newCell.target == null : newCell.target != null) || (oldCell.target != null && newCell.target != null && oldCell.target.getId() != newCell.target.getId())) { diff.target = (newCell.target != null) ? newCell.target.getId() : ''; } function isNode(value) { return value != null && typeof value === 'object' && typeof value.nodeType === 'number' && typeof value.nodeName === 'string' && typeof value.getAttribute === 'function'; }; if (isNode(oldCell.value) && isNode(newCell.value)) { if (!oldCell.value.isEqualNode(newCell.value)) { diff.xmlValue = mxUtils.getXml(newCell.value); } } else if (oldCell.value != newCell.value) { if (isNode(newCell.value)) { diff.xmlValue = mxUtils.getXml(newCell.value); } else { diff.value = (newCell.value != null) ? newCell.value : null; } } if (oldCell.style != newCell.style) { // LATER: Split into keys and do fine-grained diff diff.style = newCell.style; } if (oldCell.visible != newCell.visible) { diff.visible = (newCell.visible) ? 1 : 0; } if (oldCell.collapsed != newCell.collapsed) { diff.collapsed = (newCell.collapsed) ? 1 : 0; } // FIXME: Proto only needed because source.geometry has no constructor (wrong type?) if (!this.isObjectEqual(oldCell.geometry, newCell.geometry, new mxGeometry())) { var node = this.codec.encode(newCell.geometry); if (node != null) { diff.geometry = mxUtils.getXml(node); } } // Compares all keys from oldCell to newCell and uses null in the diff // to force the attribute to be removed in the receiving client for (var key in oldCell) { if (!this.cellProperties[key] && typeof oldCell[key] !== 'function' && typeof newCell[key] !== 'function' && oldCell[key] != newCell[key]) { diff[key] = (newCell[key] === undefined) ? null : newCell[key]; } } // Compares the remaining keys in newCell with oldCell for (var key in newCell) { if (!(key in oldCell) && !this.cellProperties[key] && typeof oldCell[key] !== 'function' && typeof newCell[key] !== 'function' && oldCell[key] != newCell[key]) { diff[key] = (newCell[key] === undefined) ? null : newCell[key]; } } return diff; }; /** * */ EditorUi.prototype.isObjectEqual = function(source, target, proto) { if (source == null && target == null) { return true; } else if ((source != null) ? target == null : target != null) { return false; } else { var replacer = function(key, value) { return (proto == null || proto[key] != value) ? ((value === true) ? 1 : value) : undefined; }; //console.log('eq', JSON.stringify(source, replacer), JSON.stringify(target, replacer)); return JSON.stringify(source, replacer) == JSON.stringify(target, replacer); } };