/** * Copyright (c) 2006-2017, JGraph Holdings Ltd * Copyright (c) 2006-2017, draw.io AG */ DrawioFile = function(ui, data) { mxEventSource.call(this); this.ui = ui; this.setData(data || ''); this.initialData = this.getData(); this.created = new Date().getTime(); // Creates the stats object this.stats = { opened: 0, /* number of calls to open */ merged: 0, /* number of calls to merge */ fileMerged: 0, /* number of calls to mergeFile */ fileReloaded: 0, /* number of calls to mergeFile */ conflicts: 0, /* number of write conflicts when saving a file */ timeouts: 0, /* number of time we have given up to retry after a write conflict */ saved: 0, /* number of calls to fileSaved */ closed: 0, /* number of calls to close */ destroyed: 0, /* number of calls to close */ joined: 0, /* number of join messages received */ checksumErrors: 0, /* number of checksum errors */ bytesSent: 0, /* number of bytes send in messages */ bytesReceived: 0, /* number of bytes received in messages */ msgSent: 0, /* number of messages sent */ msgReceived: 0, /* number of messages received */ cacheHits: 0, /* number of times the cache returned patches */ cacheMiss: 0, /* number of times we have missed a cache entry */ cacheFail: 0 /* number of times we have failed to read the cache */ }; }; /** * Global switch for realtime collaboration type to use sync URL parameter * with the following possible values: * * - none: overwrite * - manual: manual sync * - auto: automatic sync */ DrawioFile.SYNC = urlParams['sync'] || 'auto'; /** * Specifies if last write wins should be used for values and styles. */ DrawioFile.LAST_WRITE_WINS = true; /** * Specifies if export is restricted. */ DrawioFile.RESTRICT_EXPORT = false; // Extends mxEventSource mxUtils.extend(DrawioFile, mxEventSource); /** * Specifies the resource key for all changes saved status message. */ DrawioFile.prototype.allChangesSavedKey = 'allChangesSaved'; /** * Specifies the resource key for saving spinner. */ DrawioFile.prototype.savingSpinnerKey = 'saving'; /** * Specifies the resource key for saving status message. */ DrawioFile.prototype.savingStatusKey = 'saving'; /** * Specifies the delay between the last change and the autosave. */ DrawioFile.prototype.autosaveDelay = 1500; /** * Specifies the maximum delay before an autosave is forced even if the graph * is being changed. */ DrawioFile.prototype.maxAutosaveDelay = 30000; /** * Specifies the delay for loading the file after an optimistic sync message. * This should be the delay for the file to be saved minus the delay for the * sync message to travel. */ DrawioFile.prototype.optimisticSyncDelay = 300; /** * Contains the thread for the next autosave. */ DrawioFile.prototype.autosaveThread = null; /** * Stores the time stamp for the last autosave. */ DrawioFile.prototype.lastAutosave = null; /** * Stores the time stamp for the last autosave. */ DrawioFile.prototype.lastSaved = null; /** * Stores the time stamp for the last autosave. */ DrawioFile.prototype.lastChanged = null; /** * Stores the time stamp when the file was opened. */ DrawioFile.prototype.opened = null; /** * Stores the modified state. */ DrawioFile.prototype.modified = false; /** * Stores a shadow of the modified state. */ DrawioFile.prototype.shadowModified = false; /** * Holds a copy of the current file data. */ DrawioFile.prototype.data = null; /** * Holds a copy of the parsed last saved file data. */ DrawioFile.prototype.shadowPages = null; /** * Specifies if the graph change listener is enabled. Default is true. */ DrawioFile.prototype.changeListenerEnabled = true; /** * Sets the delay for autosave in milliseconds. Default is 1500. */ DrawioFile.prototype.lastAutosaveRevision = null; /** * Sets the delay between revisions when using autosave. Default is 300000 * ie 5 mins. Set this to 0 to create a revision on every autosave. */ DrawioFile.prototype.maxAutosaveRevisionDelay = 300000; /** * Specifies if notify events should be ignored. */ DrawioFile.prototype.inConflictState = false; /** * Specifies if notify events should be ignored. */ DrawioFile.prototype.invalidChecksum = false; /** * Specifies if stats should be sent. */ DrawioFile.prototype.ageStart = null; /** * Specifies if notify events should be ignored. */ DrawioFile.prototype.getSize = function() { return (this.data != null) ? this.data.length : 0; }; /** * Returns the shadow pages. If they do not exist they are created. */ DrawioFile.prototype.getShadowPages = function() { if (this.shadowPages == null) { this.shadowPages = this.ui.getPagesForXml(this.initialData); } return this.shadowPages; }; /** * Sets the shadow pages. */ DrawioFile.prototype.setShadowPages = function(pages) { this.shadowPages = pages; }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.synchronizeFile = function(success, error) { if (this.savingFile) { if (error != null) { error({message: mxResources.get('busy')}); } } else { var acceptResponse = true; var timeoutThread = window.setTimeout(mxUtils.bind(this, function() { acceptResponse = false; if (error != null) { error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout'), retry: mxUtils.bind(this, function() { this.synchronizeFile(success, error); })}); } }), this.ui.timeout); var errorWrapper = mxUtils.bind(this, function(e) { if (acceptResponse) { window.clearTimeout(timeoutThread); if (error != null) { error(e); } } }); var abort = mxUtils.bind(this, function() { return !acceptResponse; }); if (this.sync != null) { this.sync.fileChanged(mxUtils.bind(this, function(patched) { if (acceptResponse) { window.clearTimeout(timeoutThread); this.sync.cleanup(success, error, patched); } }), errorWrapper, abort); } else { this.updateFile(mxUtils.bind(this, function() { if (acceptResponse) { window.clearTimeout(timeoutThread); if (success != null) { success(); } } }), errorWrapper, abort); } } }; /** * Adds the listener for automatically saving the diagram for local changes. * Immediate is passed through to scheduleCleanup. */ DrawioFile.prototype.updateFile = function(success, error, abort, shadow, immediate) { if (abort == null || !abort()) { EditorUi.debug('DrawioFile.updateFile', [this], 'immediate', immediate, 'invalidChecksum', this.invalidChecksum); if (this.ui.getCurrentFile() != this || this.invalidChecksum) { if (error != null) { error(); } } else { this.getLatestVersion(mxUtils.bind(this, function(latestFile) { try { if (abort == null || !abort()) { EditorUi.debug('DrawioFile.updateFile', [this], 'invalidChecksum', this.invalidChecksum, 'latestFile', [latestFile]); if (this.ui.getCurrentFile() != this || this.invalidChecksum) { if (error != null) { error(); } } else { if (latestFile != null) { this.mergeFile(latestFile, success, error, shadow, immediate); } else { this.reloadFile(success, error); } } } } catch (e) { if (error != null) { error(e); } } }), error); } } }; /** * Adds the listener for automatically saving the diagram for local changes. * Immediate is passed through to scheduleCleanup. */ DrawioFile.prototype.mergeFile = function(file, success, error, diffShadow, immediate) { var reportError = true; try { // Loads new document as shadow document var pages = file.getShadowPages(); if (pages != null && pages.length > 0) { // Patches the current document var shadow = this.getShadowPages(); var patches = [this.ui.diffPages((diffShadow != null) ? diffShadow : shadow, pages)]; var ignored = this.ignorePatches(patches); if (!ignored) { try { this.stats.fileMerged++; if (this.sync != null) { this.sync.sendLocalChanges(); } // Computes local changes var changes = (!this.isModified()) ? null : this.ui.diffPages(shadow, (this.isRealtime()) ? this.ownPages : this.ui.pages); // Patching previous shadow to verify checksum var patchedDetails = {}; var currentDetails = {}; var patched = this.ui.patchPages(this.ui.clonePages(shadow), patches[0]); var checksum = this.ui.getHashValueForPages(patched, patchedDetails); var current = this.ui.getHashValueForPages(pages, currentDetails); EditorUi.debug('File.mergeFile', [this], 'file', [file], 'ui', this.ui.pages, 'shadow', shadow, 'pages', pages, 'patched', patched, 'patches', patches, 'changes', changes, 'checksum', checksum, 'current', current, 'valid', checksum == current, 'from', this.getCurrentRevisionId(), 'to', file.getCurrentRevisionId(), 'modified', this.isModified(), 'immediate', immediate); if (checksum != null && checksum != current) { this.checksumError(error, patches, null, null, 'mergeFile', checksum, current, file.getCurrentRevisionId()); // Abnormal termination return; } else { this.setShadowPages(pages); // Patches the realtime document if (this.sync != null) { var pending = this.sync.patchRealtime( patches, (DrawioFile.LAST_WRITE_WINS) ? changes : null, null, immediate); if (pending != null && !mxUtils.isEmptyObject(pending)) { patches.push(pending); } } // Patches the current document this.patch(patches, (DrawioFile.LAST_WRITE_WINS) ? changes : null); } this.invalidChecksum = false; this.inConflictState = false; this.setDescriptor(file.getDescriptor()); this.descriptorChanged(); if (success != null) { success(); } } catch (e) { this.inConflictState = true; this.invalidChecksum = true; this.descriptorChanged(); if (error != null) { error(e); } try { if (reportError) { var user = this.getCurrentUser(); var uid = (user != null) ? user.id : 'unknown'; EditorUi.logError('Error in mergeFile', null, this.getMode() + '.' + this.getId(), uid, e); } } catch (e2) { // ignore } } } else { this.invalidChecksum = false; this.inConflictState = false; this.setDescriptor(file.getDescriptor()); this.descriptorChanged(); EditorUi.debug('File.mergeFile', [this], 'file', [file], 'ignored', ignored); if (success != null) { success(); } } } else { reportError = false; throw new Error(mxResources.get('notADiagramFile')); } } catch (e) { if (error != null) { error(e); } } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.getAnonymizedXmlForPages = function(pages) { var enc = new mxCodec(mxUtils.createXmlDocument()); var file = enc.document.createElement('mxfile'); if (pages != null) { for (var i = 0; i < pages.length; i++) { var temp = enc.encode(new mxGraphModel(pages[i].root)); if (urlParams['dev'] != '1') { temp = this.ui.anonymizeNode(temp, true); } temp.setAttribute('id', pages[i].getId()); if (pages[i].viewState) { this.ui.editor.graph.saveViewState(pages[i].viewState, temp, true); } file.appendChild(temp); } } return mxUtils.getPrettyXml(file); }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.compressReportData = function(data, limit, max) { limit = (limit != null) ? limit : 10000; if (max != null && data != null && data.length > max) { data = data.substring(0, max) + '[...]'; } else if (data != null && data.length > limit) { data = Graph.compress(data) + '\n'; } return data; }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.checksumError = function(fn, patches, details, etag, functionName, checksum, current, rev) { this.stats.checksumErrors++; this.inConflictState = true; this.invalidChecksum = true; this.descriptorChanged(); if (this.sync != null) { this.sync.updateOnlineState(); } if (fn != null) { fn(); } try { var user = this.getCurrentUser(); var uid = (user != null) ? user.id : 'unknown'; var id = (this.getId() != '') ? this.getId() : ('(' + this.ui.hashValue(this.getTitle()) + ')'); var bytes = JSON.stringify(patches).length; var limit = 1000; var data = null; if (patches != null && bytes < limit) { var newPatches = []; for (var i = 0; i < patches.length; i++) { newPatches.push(this.ui.anonymizePatch(patches[i])); } data = JSON.stringify(newPatches); if (data != null && data.length < limit) { data = Graph.compress(data); } else { data = null; } } this.getLatestVersion(mxUtils.bind(this, function(latestFile) { // Logs checksum error for file try { var type = (data != null) ? 'report' : 'error'; var latest = this.ui.getHashValueForPages(latestFile.getShadowPages()); var latestVersion = 'unknown'; var latestAgent = 'unknown'; var latestType = 'unknown'; try { var node = (latestFile.initialData != null && latestFile.initialData.length > 0) ? mxUtils.parseXml(latestFile.initialData).documentElement : null; if (node != null) { if (node.getAttribute('version') != null) { latestVersion = node.getAttribute('version'); } if (node.getAttribute('agent') != null) { latestAgent = node.getAttribute('agent'); } if (node.getAttribute('type') != null) { latestType = node.getAttribute('type'); } } } catch (e) { // ignore } EditorUi.logError('Checksum ' + type + ' in ' + functionName, null, this.getMode() + '.' + id, 'user_' + uid + ((this.sync != null) ? '-client_' + this.sync.clientId : '-nosync') + '-bytes_' + bytes + '-patches_' + patches.length + ((data != null) ? ('-json_' + data) : '') + '-size_' + this.getSize() + ((checksum != null) ? ('-expected_' + checksum) : '') + ((current != null) ? ('-current_' + current) : '') + ((rev != null) ? ('-rev_' + this.ui.hashValue(rev)) : '') + ((latest != null) ? ('-latest_' + latest) : '') + '-latestRev_' + this.ui.hashValue( latestFile.getCurrentRevisionId()) + ('-latestVersion_' + latestVersion) + ('-latestAgent_' + latestAgent) + ('-latestType_' + latestType)); EditorUi.logEvent({category: 'CHECKSUM-ERROR-SYNC-FILE-' + id, action: functionName, label: 'user_' + uid + ((this.sync != null) ? '-client_' + this.sync.clientId : '-nosync') + '-bytes_' + bytes + '-patches_' + patches.length + '-size_' + this.getSize()}); } catch (e) { // ignore } }), function() {}); } catch (e) { // ignore } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.sendErrorReport = function(title, details, error, max) { try { var shadow = this.compressReportData( this.getAnonymizedXmlForPages( this.getShadowPages()), 25000); var data = this.compressReportData( this.getAnonymizedXmlForPages( this.ui.pages), 25000); var user = this.getCurrentUser(); var uid = (user != null) ? this.ui.hashValue(user.id) : 'unknown'; var cid = (this.sync != null) ? '-client_' + this.sync.clientId : '-nosync'; var filename = this.getTitle(); var dot = filename.lastIndexOf('.'); var ext = 'xml'; if (dot > 0) { ext = filename.substring(dot); } var stack = (error != null) ? error.stack : new Error().stack; EditorUi.sendReport(title + ' ' + new Date().toISOString() + ':' + '\n\nAppVersion=' + navigator.appVersion + '\nFile=' + this.ui.hashValue(this.getId()) + ' (' + this.getMode() + ')' + ((this.isModified()) ? ' modified' : '') + '\nSize/Type=' + this.getSize() + ' (' + ext + ')' + '\nUser=' + uid + cid + '\nPrefix=' + this.ui.editor.graph.model.prefix + '\nSync=' + DrawioFile.SYNC + ((this.sync != null) ? (((this.sync.enabled) ? ' enabled' : '') + ((this.sync.isConnected()) ? ' connected' : '')) : '') + '\nPlugins=' + ((mxSettings.settings != null) ? mxSettings.getPlugins() : 'null') + '\n\nStats:\n' + JSON.stringify(this.stats, null, 2) + ((details != null) ? ('\n\n' + details) : '') + ((error != null) ? ('\n\nError: ' + error.message) : '') + '\n\nStack:\n' + stack + '\n\nShadow:\n' + shadow + '\n\nData:\n' + data, max); } catch (e) { // ignore } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.reloadFile = function(success, error) { try { this.stats.fileReloaded++; // Saves current view state and changes since last save var changes = this.ui.diffPages(this.getShadowPages(), (this.isRealtime()) ? this.ownPages : this.ui.pages) var graph = this.ui.editor.graph; var selection = graph.getSelectionCells(); var viewState = graph.getViewState(); var page = this.ui.currentPage; this.getLatestVersion(mxUtils.bind(this, function(latestFile) { EditorUi.debug('DrawioFile.reloadFile', [this], 'modified', this.isModified(), 'rev', this.getCurrentRevisionId(), 'changes', [changes], 'viewState', [viewState], 'latestFile', latestFile); if (latestFile != null) { this.ui.replaceFileData(latestFile.getData(), [changes]); this.ui.restoreViewState(page, viewState, selection); this.setDescriptor(latestFile.getDescriptor()); this.descriptorChanged(); if (success != null) { success(); } } else { // Loads file from scratch with new spinner and resets undo history // This should never be called as latestFile should not be null this.ui.spinner.stop(); this.ui.loadFile(this.getHash(), true, null, mxUtils.bind(this, function() { if (this.ui.fileLoadedError == null) { // Carry-over stats var file = this.ui.getCurrentFile(); if (file != null) { file.stats = this.stats; file.patch([changes]); file.fileChanged(); } this.ui.restoreViewState(page, viewState, selection); if (success != null) { success(); } } else if (error != null) { error(this.ui.fileLoadedError); } }), true); } }), error); } catch (e) { if (error != null) { error(e); } } }; /** * Shows a conflict dialog to the user. */ DrawioFile.prototype.copyFile = function(success, error) { this.ui.editor.editAsNew(this.ui.getFileData(true), this.ui.getCopyFilename(this)); }; /** * Returns true if the patches in the given array are empty. */ DrawioFile.prototype.ignorePatches = function(patches) { var ignore = true; if (patches != null) { for (var i = 0; i < patches.length && ignore; i++) { ignore = ignore && mxUtils.isEmptyObject(patches[i]); } } return ignore; }; /** * Applies the given patches to the file. If sendChanges is true the snapshot in * the sync client is not updated so a diff can be computed and propagated. */ DrawioFile.prototype.patch = function(patches, resolver, undoable, sendChanges) { if (patches != null) { // Saves state of undo history var undoMgr = this.ui.editor.undoManager; var history = undoMgr.history.slice(); var nextAdd = undoMgr.indexOfNextAdd; // Hides graph during updates var graph = this.ui.editor.graph; graph.container.style.visibility = 'hidden'; // Ignores change events var prev = this.changeListenerEnabled; this.changeListenerEnabled = undoable; // Folding and math change require special handling var fold = graph.foldingEnabled; var math = graph.mathEnabled; // Updates text editor if cell changes during validation var redraw = graph.cellRenderer.redraw; graph.cellRenderer.redraw = function(state) { if (state.view.graph.isEditing(state.cell)) { state.view.graph.scrollCellToVisible(state.cell); state.view.graph.cellEditor.resize(); } redraw.apply(this, arguments); }; graph.model.beginUpdate(); try { if (undoable) { var oldPages = this.ui.pages.slice(); var currentPage = this.ui.currentPage; var pages = this.ui.applyPatches(this.ui.pages, patches, true, resolver, this.isModified()); for (var i = 0; i < pages.length; i++) { var index = mxUtils.indexOf(this.ui.pages, pages[i]); if (index < 0) { this.ui.insertPage(pages[i], Math.min( i, this.ui.pages.length)); } else { this.ui.movePage(index, i); } } for (var i = 0; i < oldPages.length; i++) { if (mxUtils.indexOf(pages, oldPages[i]) < 0) { this.ui.removePage(oldPages[i]); } } // Reselects the current page if (mxUtils.indexOf(this.ui.pages, currentPage) >= 0) { this.ui.selectPage(currentPage, true); } } else { this.ui.pages = this.ui.applyPatches(this.ui.pages, patches, true, resolver, this.isModified()); } // Always needs at least one page if (this.ui.pages.length == 0) { this.ui.pages.push(this.ui.createPage()); } // Checks if current page was removed if (mxUtils.indexOf(this.ui.pages, this.ui.currentPage) < 0) { this.ui.selectPage(this.ui.pages[0], true); } // Checks if default parent was replaced graph.checkDefaultParent(); } finally { // Changes visibility before action states are updated via model event graph.container.style.visibility = ''; graph.model.endUpdate(); // Restores previous state graph.cellRenderer.redraw = redraw; this.changeListenerEnabled = prev; // Restores history state if (!undoable) { undoMgr.history = history; undoMgr.indexOfNextAdd = nextAdd; undoMgr.fireEvent(new mxEventObject(mxEvent.CLEAR)); } if (this.ui.currentPage == null || this.ui.currentPage.needsUpdate) { // Updates the graph and background if (math != graph.mathEnabled) { this.ui.editor.updateGraphComponents(); graph.refresh(); } else { if (fold != graph.foldingEnabled) { graph.view.revalidate(); } else { graph.view.validate(); } graph.sizeDidChange(); } } // Updates snapshot for finding local changes in sync if (this.sync != null && this.isRealtime() && !sendChanges) { this.sync.snapshot = this.ui.clonePages(this.ui.pages); } this.ui.editor.fireEvent(new mxEventObject('pagesPatched', 'patches', patches)); } EditorUi.debug('DrawioFile.patch', [this], 'patches', patches, 'resolver', resolver, 'undoable', undoable); } return patches; }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.save = function(revision, success, error, unloading, overwrite, manual) { try { EditorUi.debug('DrawioFile.save', [this], 'revision', revision, 'unloading', unloading, 'overwrite', overwrite, 'manual', manual, 'saving', this.savingFile, 'editable', this.isEditable(), 'invalidChecksum', this.invalidChecksum); if (!this.isEditable()) { if (error != null) { error({message: mxResources.get('readOnly')}); } else { throw new Error(mxResources.get('readOnly')); } } else if (!overwrite && this.invalidChecksum) { if (error != null) { error({message: mxResources.get('checksum')}); } else { throw new Error(mxResources.get('checksum')); } } else { this.updateFileData(); this.clearAutosave(); if (success != null) { success(); } } } catch (e) { if (error != null) { error(e); } else { throw e; } } }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.createData = function() { var actualPages = this.ui.pages; if (this.isRealtime()) { // Uses ownPages for getting file data below this.ui.pages = this.ownPages; // Updates view state in own current page if (this.ui.currentPage != null) { var ownPage = this.ui.getPageById( this.ui.currentPage.getId(), this.ownPages); if (ownPage != null) { ownPage.viewState = this.ui.editor.graph.getViewState(); ownPage.needsUpdate = true; } } } var result = this.ui.getFileData(null, null, null, null, null, null, null, null, this, !this.isCompressed()); this.ui.pages = actualPages; return result; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.updateFileData = function() { // Sends pending local changes and updates own pages if (this.sync != null) { this.sync.sendLocalChanges(); } this.setData(this.createData()); if (this.sync != null) { this.sync.fileDataUpdated(); } }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.isCompressedStorage = function() { return Editor.defaultCompressed; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.isCompressed = function() { var compressed = (this.ui.fileNode != null) ? this.ui.fileNode.getAttribute('compressed') : null; if (compressed != null) { return compressed != 'false'; } else { return this.isCompressedStorage() && Editor.compressXml; } }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.setLocked = function(locked) { this.ui.fileNode.setAttribute('locked', (locked) ? 'true' : 'false'); this.ui.fireEvent(new mxEventObject('lockedChanged')); this.fileChanged(); if (locked) { this.ui.editor.graph.clearSelection(); } }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.isLocked = function() { var locked = (this.ui.fileNode != null) ? this.ui.fileNode.getAttribute('locked') : null; if (locked != null) { return locked == 'true'; } return false; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.saveAs = function(filename, success, error) { }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.saveFile = function(title, revision, success, error) { }; /** * Returns true if copy, export and print are not allowed for this file. */ DrawioFile.prototype.getFileUrl = function() { return null; }; /** * Returns true if copy, export and print are not allowed for this file. */ DrawioFile.prototype.getFolderUrl = function(fn) { return null; }; /** * Returns true if copy, export and print are not allowed for this file. */ DrawioFile.prototype.getPublicUrl = function(fn) { fn(null); }; /** * Returns true if copy, export and print are not allowed for this file. */ DrawioFile.prototype.isRestricted = function() { return DrawioFile.RESTRICT_EXPORT; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.isModified = function() { return this.modified; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.getShadowModified = function() { return this.shadowModified; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.setShadowModified = function(value) { this.shadowModified = value; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.setModified = function(value) { this.modified = value; this.shadowModified = value; }; /** * Specifies if the autosave checkbox should be shown in the document * properties dialog. Default is false. */ DrawioFile.prototype.isAutosaveOptional = function() { return false; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.isAutosave = function() { return !this.inConflictState && this.ui.editor.autosave; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.isRenamable = function() { return false; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.rename = function(title, success, error) { }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.isMovable = function() { return false; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.isTrashed = function() { return false; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.move = function(folderId, success, error) { }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.share = function() { if (this.ui.drive != null) { this.ui.confirm(mxResources.get('saveItToGoogleDriveToCollaborate', [this.getTitle()]), mxUtils.bind(this, function() { this.ui.pickFolder(App.MODE_GOOGLE, mxUtils.bind(this, function(folderId) { var graph = this.ui.editor.graph; var selection = graph.getSelectionCells(); var viewState = graph.getViewState(); var page = this.ui.currentPage; this.ui.createFile(this.getTitle(), this.ui.getFileData(null, null, null, null, null, null, null, null, this), null, App.MODE_GOOGLE, null, true, folderId, null, null, mxUtils.bind(this, function() { this.ui.restoreViewState(page, viewState, selection); this.ui.actions.get('share').funct(); })); })); }), null, mxResources.get('saveToGoogleDrive', null, 'Save to Google Drive'), mxResources.get('cancel')); } else { this.ui.alert(mxResources.get('sharingAvailable'), null, 380); } }; /** * Returns the hash of the file which consists of a prefix for the storage * type and the ID of the file. */ DrawioFile.prototype.getHash = function() { return ''; }; /** * Returns the ID of the file. */ DrawioFile.prototype.getId = function() { return ''; }; /** * Returns true if the file is editable. */ DrawioFile.prototype.isEditable = function() { return !this.ui.editor.isChromelessView() || this.ui.editor.editable; }; /** * Returns the location as a new object. * @type mx.Point */ DrawioFile.prototype.getUi = function() { return this.ui; }; /** * Returns the current title of the file. */ DrawioFile.prototype.getTitle = function() { return ''; }; /** * Sets the current data of the file. */ DrawioFile.prototype.setData = function(data) { this.data = data; EditorUi.debug('DrawioFile.setData', [this], 'data', [data]); }; /** * Returns the current data of the file. */ DrawioFile.prototype.getData = function() { return this.data; }; /** * Removes external fonts. */ DrawioFile.prototype.removeExtFonts = function(elems) { for (var i = 0; elems != null && i < elems.length; i++) { var e = elems[i]; if (e.id != null && e.id.indexOf('extFont_') == 0) { e.parentNode.removeChild(e); } } }; /** * Opens this file in the editor. */ DrawioFile.prototype.open = function() { this.stats.opened++; var data = this.getData(); if (data != null) { this.ui.setFileData(data); // Updates shadow in case any page IDs have been updated // only if the file has not been modified and reopened if (!this.isModified()) { this.setShadowPages(this.ui.clonePages(this.ui.pages)); } } this.installListeners(); if (this.isSyncSupported()) { this.startSync(); } EditorUi.debug('DrawioFile.open', [this]); }; /** * Returns true if polling should be used to update the file. */ DrawioFile.prototype.isPolling = function() { return false; }; /** * Returns the polling interval. */ DrawioFile.prototype.getPollingInterval = function() { return 10000; }; /** * Hook for subclassers. */ DrawioFile.prototype.isSyncSupported = function() { return false; }; /** * Returns true if the realtime model was initialized. */ DrawioFile.prototype.isRealtime = function() { return this.ownPages != null && this.ui.pages != null; }; /** * Returns true if the file supportes realtime collaboration. */ DrawioFile.prototype.isRealtimeSupported = function() { return false; }; /** * Returns true if realtime collaboration is enabled for this file. */ DrawioFile.prototype.isRealtimeEnabled = function() { return Editor.enableRealtime && urlParams['fast-sync'] != '0'; }; /** * Returns true if all changes should be sent out immediately. */ DrawioFile.prototype.setRealtimeEnabled = function() { // do nothing }; /** * Returns true if realtime can be enabled and disabled for this file. */ DrawioFile.prototype.isRealtimeOptional = function() { return false; }; /** * Returns the ready state of realtime collaboration websocket. * * 0 - CONNECTING * 1 - OPEN * 2 - CLOSING * 3 - CLOSED */ DrawioFile.prototype.getRealtimeState = function() { return (this.sync != null && this.sync.p2pCollab != null) ? this.sync.p2pCollab.getState() : 3 /* CLOSED */; }; /** * Returns true if all changes should be sent out immediately. */ DrawioFile.prototype.getRealtimeError = function() { return (this.sync != null && this.sync.p2pCollab != null) ? this.sync.p2pCollab.getLastError() : null; }; /** * Returns true if the notification to update should be sent * together with the save request. */ DrawioFile.prototype.isOptimisticSync = function() { return false; }; /** * Hook for subclassers. */ DrawioFile.prototype.isRevisionHistorySupported = function() { return false; }; /** * Hook for subclassers. */ DrawioFile.prototype.getRevisions = function(success, error) { success(null); }; /** * Hook for subclassers to get the latest descriptor of this file * and return it in the success handler. */ DrawioFile.prototype.loadDescriptor = function(success, error) { success(null); }; /** * Hook for subclassers to get the latest etag of this file * and return it in the success handler. */ DrawioFile.prototype.loadPatchDescriptor = function(success, error) { this.loadDescriptor(mxUtils.bind(this, function(desc) { success(desc); }), error); }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.patchDescriptor = function(desc, patch) { this.setDescriptorEtag(desc, this.getDescriptorEtag(patch)); this.descriptorChanged(); }; /** * Creates a starts the synchronization. */ DrawioFile.prototype.startSync = function() { if (((DrawioFile.SYNC == 'auto' || DrawioFile.SYNC == 'fast') && urlParams['stealth'] != '1') && (urlParams['rt'] == '1' || !this.ui.editor.chromeless || this.ui.editor.editable)) { if (this.sync == null) { this.sync = new DrawioFileSync(this); } this.addListener('realtimeStateChanged', mxUtils.bind(this, function() { this.ui.fireEvent(new mxEventObject('realtimeStateChanged')); })); this.sync.start(); } }; /** * Hook for subclassers to check if an error is a conflict. */ DrawioFile.prototype.isConflict = function() { return false; }; /** * Gets the channel ID for sync messages. */ DrawioFile.prototype.getChannelId = function() { // Slash, space and plus replaced with underscore return Graph.compress(this.getHash()).replace(/[\/ +]/g, '_'); }; /** * Gets the channel ID from the given descriptor. */ DrawioFile.prototype.getChannelKey = function(desc) { return null; }; /** * Returns the current etag. */ DrawioFile.prototype.getCurrentUser = function() { return null; }; /** * Hook for subclassers to get the latest version of this file * and return it in the success handler. */ DrawioFile.prototype.getLatestVersion = function(success, error) { success(null); }; /** * Hook for subclassers to get the latest version ID of this file * and return it in the success handler. */ DrawioFile.prototype.getLatestVersionId = function(success, error) { success(-1); }; /** * Returns the last modified date of this file. */ DrawioFile.prototype.getLastModifiedDate = function() { return new Date(); }; /** * Sets the current revision ID. */ DrawioFile.prototype.setCurrentRevisionId = function(id) { this.setDescriptorRevisionId(this.getDescriptor(), id); }; /** * Returns the current revision ID. */ DrawioFile.prototype.getCurrentRevisionId = function() { return this.getDescriptorRevisionId(this.getDescriptor()); }; /** * Sets the current etag. */ DrawioFile.prototype.setCurrentEtag = function(etag) { this.setDescriptorEtag(this.getDescriptor(), etag); }; /** * Returns the current etag. */ DrawioFile.prototype.getCurrentEtag = function() { return this.getDescriptorEtag(this.getDescriptor()); }; /** * Returns the descriptor from this file. */ DrawioFile.prototype.getDescriptor = function() { return null; }; /** * Sets the descriptor for this file. */ DrawioFile.prototype.setDescriptor = function() { }; /** * Updates the revision ID on the given descriptor. */ DrawioFile.prototype.setDescriptorRevisionId = function(desc, id) { this.setDescriptorEtag(desc, id); }; /** * Returns the revision ID from the given descriptor. */ DrawioFile.prototype.getDescriptorRevisionId = function(desc) { return this.getDescriptorEtag(desc); }; /** * Updates the etag on the given descriptor. */ DrawioFile.prototype.setDescriptorEtag = function(desc, etag) { }; /** * Returns the etag from the given descriptor. */ DrawioFile.prototype.getDescriptorEtag = function(desc) { return null; }; /** * Returns the secret from the given descriptor. This must be stored * in a custom property and generated by the saving client so that a * token can be obtained from the cache for writing the patch after * saving the file. If this cannot be saved in a custom property then * null must be returned so that no deltas are used for updating the * file (the file is reloaded every time instead). This is needed to * make sure nobody with read-only permissions can write a patch to * the cache before the saving client wrote the patch and inject * data into the file via other clients merging that data. */ DrawioFile.prototype.getDescriptorSecret = function(desc) { return null; }; /** * Returns the checksum from the given descriptor. This must be stored * in a custom property and generated by the saving client so that * the current state of the editor can be compared with the state. */ DrawioFile.prototype.getDescriptorChecksum = function(desc) { return null; }; /** * Installs the change listener. */ DrawioFile.prototype.installListeners = function() { if (this.changeListener == null) { this.changeListener = mxUtils.bind(this, function(sender, eventObject) { var edit = (eventObject != null) ? eventObject.getProperty('edit') : null; if (this.changeListenerEnabled && this.isEditable() && (edit == null || !edit.ignoreEdit)) { this.fileChanged(); } }); this.ui.editor.graph.model.addListener(mxEvent.CHANGE, this.changeListener); // Some options trigger autosave this.ui.editor.graph.addListener('gridSizeChanged', this.changeListener); this.ui.editor.graph.addListener('shadowVisibleChanged', this.changeListener); this.ui.addListener('pageFormatChanged', this.changeListener); this.ui.addListener('pageScaleChanged', this.changeListener); this.ui.addListener('backgroundColorChanged', this.changeListener); this.ui.addListener('backgroundImageChanged', this.changeListener); this.ui.addListener('foldingEnabledChanged', this.changeListener); this.ui.addListener('mathEnabledChanged', this.changeListener); this.ui.addListener('gridEnabledChanged', this.changeListener); this.ui.addListener('guidesEnabledChanged', this.changeListener); this.ui.addListener('tooltipsEnabledChanged', this.changeListener); this.ui.addListener('pageViewChanged', this.changeListener); this.ui.addListener('connectionPointsChanged', this.changeListener); this.ui.addListener('connectionArrowsChanged', this.changeListener); } }; /** * Returns the location as a new object. * @type mx.Point */ DrawioFile.prototype.addAllSavedStatus = function(status) { if (this.ui.statusContainer != null && this.ui.getCurrentFile() == this) { status = (status != null) ? status : mxUtils.htmlEntities(mxResources.get(this.allChangesSavedKey)); var rev = (this.isRevisionHistorySupported() && status != mxUtils.htmlEntities( mxResources.get(this.savingStatusKey)) + '...') ? 'data-action="revisionHistory" ' : ''; this.ui.editor.setStatus('
' + status + (this.isLocked() ? ' ' : '') + '
'); } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.saveDraft = function(data) { try { if (this.draftId == null) { if (this.usedDraftId != null) { this.draftId = this.usedDraftId; } else { this.draftId = Editor.guid(); } } var draft = {type: 'draft', created: this.created, modified: new Date().getTime(), data: (data != null) ? data : this.ui.getFileData(), title: this.getTitle(), fileObject: this.fileObject, aliveCheck: this.ui.draftAliveCheck}; this.ui.setDatabaseItem('.draft_' + this.draftId, JSON.stringify(draft)); EditorUi.debug('DrawioFile.saveDraft', [this], 'draftId', this.draftId, [draft]); } catch (e) { // Removes any stored draft this.removeDraft(); } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.removeDraft = function() { try { if (this.draftId != null) { EditorUi.debug('DrawioFile.removeDraft', [this], 'draftId', this.draftId); this.ui.removeDatabaseItem('.draft_' + this.draftId); this.usedDraftId = this.draftId; this.draftId = null; } } catch (e) { // ignore } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.addUnsavedStatus = function(err) { if (!this.inConflictState && this.ui.statusContainer != null && this.ui.getCurrentFile() == this) { if (err instanceof Error && err.message != null && err.message != '') { this.ui.updateStatus(mxUtils.bind(this, function() { var status = mxUtils.htmlEntities(mxResources.get('unsavedChanges')); this.ui.editor.setStatus('
' + status + ' (' + mxUtils.htmlEntities(err.message) + ')
'); })); } else { this.ui.updateStatus(mxUtils.bind(this, function() { var msg = this.getErrorMessage(err); if (msg == null && this.lastSaved != null) { var str = this.ui.timeSince(new Date(this.lastSaved)); // Only show if more than a minute ago if (str != null) { msg = mxResources.get('lastSaved', [str]); } } if (msg != null && msg.length > 60) { msg = msg.substring(0, 60) + '...'; } var status = mxUtils.htmlEntities(mxResources.get('unsavedChangesClickHereToSave')) + ((msg != null && msg != '') ? ' (' + mxUtils.htmlEntities(msg) + ')' : ''); var action = 'data-action="' + ((this.ui.mode == null || !this.isEditable()) ? 'saveAs' : 'save') + '"'; this.ui.editor.setStatus('
' + status + '
'); })); if (EditorUi.enableDrafts && (this.getMode() == null || EditorUi.isElectronApp)) { this.lastDraftSave = this.lastDraftSave || Date.now(); if (this.saveDraftThread != null) { window.clearTimeout(this.saveDraftThread); this.saveDraftThread = null; // Max delay without saving is double the delay for autosave or 30 sec if (Date.now() - this.lastDraftSave > Math.max(2 * EditorUi.draftSaveDelay, 30000)) { this.lastDraftSave = Date.now(); this.saveDraft(); } } this.saveDraftThread = window.setTimeout(mxUtils.bind(this, function() { this.lastDraftSave = Date.now(); this.saveDraftThread = null; this.saveDraft(); }), EditorUi.draftSaveDelay || 0); } } } }; /** * Halts all timers and shows a conflict status message. The optional error * handler is invoked first. */ DrawioFile.prototype.addConflictStatus = function(message, fn) { this.ui.updateStatus(mxUtils.bind(this, function() { if (this.invalidChecksum && message == null) { message = mxResources.get('checksum'); } this.setConflictStatus(mxUtils.htmlEntities(mxResources.get('fileChangedSync')) + ((message != null && message != '') ? ' (' + mxUtils.htmlEntities(message) + ')' : ''), fn); })); this.ui.spinner.stop(); this.clearAutosave(); }; /** * Halts all timers and shows a conflict status message. The optional error * handler is invoked first. */ DrawioFile.prototype.setConflictStatus = function(message, fn) { this.ui.editor.setStatus('
' + message + '
', fn); }; /** * Shows a conflict dialog to the user. */ DrawioFile.prototype.showRefreshDialog = function(success, error, message) { if (message == null) { message = mxResources.get('checksum'); } if (this.ui.editor.isChromelessView() && !this.ui.editor.editable) { this.ui.alert(mxResources.get('fileChangedSync'), mxUtils.bind(this, function() { this.reloadFile(success, error); })); } else { // Allows for escape key to be pressed while dialog is showing this.addConflictStatus(message, mxUtils.bind(this, function() { this.showRefreshDialog(success, error); })); this.ui.showError(mxResources.get('warning') + ' (' + message + ')', mxResources.get('fileChangedSyncDialog'), mxResources.get('makeCopy'), mxUtils.bind(this, function() { this.copyFile(mxUtils.bind(this, function() { if (success != null) { success(); } this.ui.alert(mxResources.get('copyCreated')); }), error); }), null, mxResources.get('merge'), mxUtils.bind(this, function() { if (this.ui.spinner.spin(document.body, mxResources.get('updatingDocument'))) { this.reloadFile(mxUtils.bind(this, function() { this.ui.spinner.stop(); if (success != null) { success(); } }), mxUtils.bind(this, function() { this.ui.spinner.stop(); if (error != null) { error(); } })); } }), mxResources.get('cancel'), mxUtils.bind(this, function() { this.ui.hideDialog(); }), 380, 130); } }; /** * Shows a dialog with no synchronize option. */ DrawioFile.prototype.showCopyDialog = function(success, error, overwrite) { this.inConflictState = false; this.invalidChecksum = false; this.addUnsavedStatus(); this.ui.showError(mxResources.get('externalChanges'), mxResources.get('fileChangedOverwriteDialog'), mxResources.get('makeCopy'), mxUtils.bind(this, function() { this.copyFile(success, error); }), null, mxResources.get('overwrite'), overwrite, mxResources.get('cancel'), mxUtils.bind(this, function() { this.ui.hideDialog(); }), 380, 150); }; /** * Shows a conflict dialog to the user. */ DrawioFile.prototype.showConflictDialog = function(overwrite, synchronize) { this.ui.showError(mxResources.get('externalChanges'), mxResources.get('fileChangedSyncDialog'), mxResources.get('overwrite'), overwrite, null, mxResources.get('merge'), synchronize, mxResources.get('cancel'), mxUtils.bind(this, function() { this.ui.hideDialog(); this.handleFileError(null, false); }), 380, 130); }; /** * Checks if the client is authorized and calls the next step. */ DrawioFile.prototype.redirectToNewApp = function(error, details) { this.ui.spinner.stop(); if (!this.redirectDialogShowing) { this.redirectDialogShowing = true; var url = window.location.protocol + '//' + window.location.host + '/' + this.ui.getSearch( ['create', 'title', 'mode', 'url', 'drive', 'splash', 'state']) + '#' + this.getHash(); var msg = mxResources.get('redirectToNewApp'); if (details != null) { msg += ' (' + details + ')'; } var redirect = mxUtils.bind(this, function() { var fn = mxUtils.bind(this, function() { this.redirectDialogShowing = false; if (window.location.href == url) { window.location.reload(); } else { window.location.href = url; } }); if (error == null && this.isModified()) { this.ui.confirm(mxResources.get('allChangesLost'), mxUtils.bind(this, function() { this.redirectDialogShowing = false; }), fn, mxResources.get('cancel'), mxResources.get('discardChanges')); } else { fn(); } }); if (error != null) { if (this.isModified()) { this.ui.confirm(msg, mxUtils.bind(this, function() { this.redirectDialogShowing = false; error(); }), redirect, mxResources.get('cancel'), mxResources.get('discardChanges')); } else { this.ui.confirm(msg, redirect, mxUtils.bind(this, function() { this.redirectDialogShowing = false; error(); })); } } else { this.ui.alert(mxResources.get('redirectToNewApp'), redirect); } } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.handleFileSuccess = function(saved) { this.ui.spinner.stop(); if (this.ui.getCurrentFile() == this) { EditorUi.debug('DrawioFile.handleFileSuccess', [this], 'saved', saved, 'modified', this.isModified(), 'remoteFileChanged', (this.sync == null) ? 'n/a' : this.sync.remoteFileChanged); if (this.isModified()) { this.fileChanged(); } else if (saved) { if (this.isTrashed()) { this.ui.updateStatus(mxUtils.bind(this, function() { this.addAllSavedStatus(mxUtils.htmlEntities( mxResources.get(this.allChangesSavedKey)) + ' (' + mxUtils.htmlEntities(mxResources.get('fileMovedToTrash')) + ')'); })); } else { this.ui.updateStatus(mxUtils.bind(this, function() { this.addAllSavedStatus(); })); } if (this.sync != null) { this.sync.resetUpdateStatusThread(); if (this.sync.remoteFileChanged) { this.sync.remoteFileChanged = false; this.sync.fileChangedNotify(); } } } else { this.ui.clearStatus(); } } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.handleFileError = function(err, manual) { this.ui.spinner.stop(); if (this.ui.getCurrentFile() == this) { if (this.inConflictState) { this.handleConflictError(err, manual); } else { if (this.isModified()) { this.addUnsavedStatus(err); } if (manual) { this.ui.handleError(err, (err != null) ? mxResources.get('errorSavingFile') : null); } else if (!this.isModified()) { this.ui.updateStatus(mxUtils.bind(this, function() { var msg = this.getErrorMessage(err); if (msg != null && msg.length > 60) { msg = msg.substring(0, 60) + '...'; } this.ui.editor.setStatus('
' + mxUtils.htmlEntities(mxResources.get('error')) + ((msg != null) ? ' (' + mxUtils.htmlEntities(msg) + ')' : '') + '
'); })); } } } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.handleConflictError = function(err, manual) { var success = mxUtils.bind(this, function() { this.handleFileSuccess(true); }); var error = mxUtils.bind(this, function(err2) { this.handleFileError(err2, true); }); var overwrite = mxUtils.bind(this, function() { if (this.ui.spinner.spin(document.body, mxResources.get(this.savingSpinnerKey))) { this.ui.clearStatus(); var isRepoFile = (this.constructor == GitHubFile) || (this.constructor == GitLabFile); this.save(true, success, error, null, true, (isRepoFile && err != null) ? err.commitMessage : null); } }); var synchronize = mxUtils.bind(this, function() { if (this.ui.spinner.spin(document.body, mxResources.get('updatingDocument'))) { this.synchronizeFile(mxUtils.bind(this, function() { this.ui.spinner.stop(); if (this.ui.spinner.spin(document.body, mxResources.get(this.savingSpinnerKey))) { var isRepoFile = (this.constructor == GitHubFile) || (this.constructor == GitLabFile); this.save(true, success, error, null, null, (isRepoFile && err != null) ? err.commitMessage : null); } }), error); } }) if (DrawioFile.SYNC == 'none') { this.showCopyDialog(success, error, overwrite); } else if (this.invalidChecksum && manual) { this.showRefreshDialog(success, error, this.getErrorMessage(err)); } else if (manual) { this.showConflictDialog(overwrite, synchronize); } else { this.addConflictStatus(this.getErrorMessage(err), mxUtils.bind(this, function() { this.ui.editor.setStatus(mxUtils.htmlEntities( mxResources.get('updatingDocument'))); this.synchronizeFile(success, error); })); } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.getErrorMessage = function(err) { var msg = (err != null) ? ((err.error != null) ? err.error.message : err.message) : null; if (msg == null && err != null && err.code == App.ERROR_TIMEOUT) { msg = mxResources.get('timeout'); } // XHR blocked by CORS or response has no CORS headers else if (msg == '0') { msg = mxResources.get('noResponse'); } return msg; }; /** * Returns true if the oldest unsaved change is older than . */ DrawioFile.prototype.isOverdue = function() { return this.ageStart != null && (Date.now() - this.ageStart.getTime()) >= this.ui.warnInterval; }; /** * Invoked when the compression has changed. */ DrawioFile.prototype.compressionChanged = function(compressed) { // Changes the internal compressed data in the pages to the current state var pages = (this.ownPages != null) ? this.ownPages : this.ui.pages; if (pages != null) { for (var i = 0; i < pages.length; i++) { var pageNode = pages[i].node; if (pageNode != null && (this.ui.currentPage == null || this.ui.currentPage.getId() != pages[i].getId())) { var models = pageNode.getElementsByTagName('mxGraphModel'); var modelNode = (models.length > 0) ? models[0] : null; var xml = Graph.decompress(mxUtils.getNodeValue(pageNode)); if (compressed) { if (xml.length == 0 && modelNode != null) { EditorUi.removeChildNodes(pageNode); mxUtils.setTextContent(pageNode, Graph.compressNode(modelNode)); EditorUi.debug('DrawioFile.compressionChanged', [this], 'Page ' + i + ' compressed'); } } else { if (xml.length > 0 && modelNode == null) { EditorUi.removeChildNodes(pageNode); pageNode.appendChild(mxUtils.parseXml(xml).documentElement); EditorUi.debug('DrawioFile.compressionChanged', [this], 'Page ' + i + ' decompressed'); } } } } } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.fileReplaced = function(patches) { this.invalidChecksum = false; this.inConflictState = false; if (patches != null) { this.patch(patches); } if (this.sync != null) { this.sync.initRealtime(); } EditorUi.debug('DrawioFile.fileReplaced', [this], 'patches', patches); }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.fileChanged = function(sync) { sync = (sync != null) ? sync : true; this.lastChanged = new Date(); this.setModified(true); EditorUi.debug('DrawioFile.fileChanged', [this], 'autosaveDelay', this.autosaveDelay, 'autosave', this.isAutosave(), 'saving', this.savingFile); if (this.isAutosave()) { if (this.savingStatusKey != null) { this.ui.updateStatus(mxUtils.bind(this, function() { this.addAllSavedStatus(mxUtils.htmlEntities( mxResources.get(this.savingStatusKey)) + '...'); })); } this.ui.scheduleSanityCheck(); if (this.ageStart == null) { this.ageStart = new Date(); } this.autosave(this.autosaveDelay, this.maxAutosaveDelay, mxUtils.bind(this, function(resp) { this.ui.stopSanityCheck(); // Does not update status if another autosave was scheduled if (this.autosaveThread == null) { this.handleFileSuccess(true); this.ageStart = null; } else if (this.isModified()) { this.ui.scheduleSanityCheck(); this.ageStart = this.lastChanged; } }), mxUtils.bind(this, function(err) { this.handleFileError(err); })); } else { this.ageStart = null; if ((!this.isAutosaveOptional() || !this.ui.editor.autosave) && !this.inConflictState) { this.addUnsavedStatus(); } } if (this.sync != null && sync) { this.sync.localFileChanged(); } }; /** * Creates a secret and token pair for writing a patch to the cache. */ DrawioFile.prototype.createSecret = function(success) { var secret = Editor.guid(32); if (Editor.enableRealtimeCache && this.sync != null && !this.isOptimisticSync()) { this.sync.createToken(secret, mxUtils.bind(this, function(token) { EditorUi.debug('DrawioFile.createSecret', [this], 'secret', secret, 'token', token); success(secret, token); }), mxUtils.bind(this, function() { success(secret); })); } else { success(secret); } }; /** * Invokes sync and updates shadow document. */ DrawioFile.prototype.fileSaving = function() { if (this.sync != null) { this.sync.fileSaving(); } }; /** * Invokes sync and updates shadow document. */ DrawioFile.prototype.fileSaved = function(savedData, lastDesc, success, error, token, pages, checksum) { this.lastSaved = new Date(); this.ageStart = null; this.stats.saved++; try { this.inConflictState = false; this.invalidChecksum = false; pages = (pages != null) ? pages : this.ui.getPagesForXml(savedData); try { if (this.sync == null || this.isOptimisticSync()) { this.setShadowPages(pages); if (this.sync != null) { this.sync.lastModified = this.getLastModifiedDate(); this.sync.resetUpdateStatusThread(); if (this.isRealtime()) { this.sync.scheduleCleanup(); } } if (success != null) { success(); } } else { this.sync.fileSaved(pages, lastDesc, success, error, token, checksum); } } catch (e) { this.inConflictState = true; this.invalidChecksum = true; this.descriptorChanged(); if (error != null) { error(e); } try { var user = this.getCurrentUser(); var uid = (user != null) ? user.id : 'unknown'; EditorUi.logError('Error in fileSaved', null, this.getMode() + '.' + this.getId(), uid, e); } catch (e2) { // ignore } } EditorUi.debug('DrawioFile.fileSaved', [this], 'savedData', [savedData], 'desc', [lastDesc], 'inConflictState', this.inConflictState, 'invalidChecksum', this.invalidChecksum); } catch (e) { this.descriptorChanged(); if (error != null) { error(e); } } }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.autosave = function(delay, maxDelay, success, error) { if (this.lastAutosave == null) { this.lastAutosave = Date.now(); } var tmp = (Date.now() - this.lastAutosave < maxDelay) ? delay : 0; this.clearAutosave(); // Starts new timer or executes immediately if not unsaved for maxDelay var thread = window.setTimeout(mxUtils.bind(this, function() { try { this.lastAutosave = null; if (this.autosaveThread == thread) { this.autosaveThread = null; } EditorUi.debug('DrawioFile.autosave', [this], 'thread', thread, 'modified', this.isModified(), 'now', this.isAutosaveNow(), 'saving', this.savingFile); // Workaround for duplicate save if UI is blocking // after save while pending autosave triggers if (this.isModified() && this.isAutosaveNow()) { var rev = this.isAutosaveRevision(); if (rev) { this.lastAutosaveRevision = new Date().getTime(); } this.save(rev, mxUtils.bind(this, function(resp) { this.autosaveCompleted(); if (success != null) { success(resp); } }), mxUtils.bind(this, function(resp) { if (error != null) { error(resp); } })); } else { if (!this.isModified()) { this.ui.clearStatus(); } if (success != null) { success(null); } } } catch (e) { if (error != null) { error(e); } } }), tmp); this.autosaveThread = thread; EditorUi.debug('DrawioFile.autosave', [this], 'thread', thread, 'delay', delay, 'maxDelay', maxDelay, 'actualDelay', tmp, 'lastAutosave', this.lastAutosave, 'saving', this.savingFile); }; /** * Returns true if an autosave is required at the time of execution. * This implementation returns true. */ DrawioFile.prototype.isAutosaveNow = function() { return true; }; /** * Hooks for subclassers after the autosave has completed. */ DrawioFile.prototype.autosaveCompleted = function() { }; /** * Adds the listener for automatically saving the diagram for local changes. */ DrawioFile.prototype.clearAutosave = function() { if (this.autosaveThread != null) { window.clearTimeout(this.autosaveThread); this.autosaveThread = null; } }; /** * Returns the location as a new object. * @type mx.Point */ DrawioFile.prototype.isAutosaveRevision = function() { var now = new Date().getTime(); return (this.lastAutosaveRevision == null) || (now - this.lastAutosaveRevision) > this.maxAutosaveRevisionDelay; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.descriptorChanged = function() { this.fireEvent(new mxEventObject('descriptorChanged')); }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ DrawioFile.prototype.contentChanged = function() { this.fireEvent(new mxEventObject('contentChanged')); }; /** * Returns the location as a new object. */ DrawioFile.prototype.close = function(unloading) { try { if (this.isAutosave() && this.isModified()) { this.updateFileData(); this.save(this.isAutosaveRevision(), null, null, unloading); } } catch (e) { // ignore } this.stats.closed++; this.destroy(); }; /** * Returns the location as a new object. */ DrawioFile.prototype.hasSameExtension = function(title, newTitle) { if (title != null && newTitle != null) { var dot = title.lastIndexOf('.'); var ext = (dot > 0) ? title.substring(dot) : ''; dot = newTitle.lastIndexOf('.'); return ext === ((dot > 0) ? newTitle.substring(dot) : ''); } return title == newTitle; }; /** * Removes the change listener. */ DrawioFile.prototype.removeListeners = function() { if (this.changeListener != null) { this.ui.editor.graph.model.removeListener(this.changeListener); this.ui.editor.graph.removeListener(this.changeListener); this.ui.removeListener(this.changeListener); this.changeListener = null; } }; /** * Stops any pending autosaves and removes all listeners. */ DrawioFile.prototype.destroy = function() { this.clearAutosave(); this.removeListeners(); this.stats.destroyed++; if (this.sync != null) { this.sync.destroy(); this.sync = null; } }; /** * Are comments supported */ DrawioFile.prototype.commentsSupported = function() { return false; //The default is false and files that support it must explicitly state that }; /** * Show refresh button? */ DrawioFile.prototype.commentsRefreshNeeded = function() { return true; }; /** * Show save button? */ DrawioFile.prototype.commentsSaveNeeded = function() { return false; }; /** * Get comments of the file */ DrawioFile.prototype.getComments = function(success, error) { success([]); //placeholder }; /** * Add a comment to the file */ DrawioFile.prototype.addComment = function(comment, success, error) { success(Date.now()); //placeholder }; /** * Can add a reply to a reply */ DrawioFile.prototype.canReplyToReplies = function() { return true; }; /** * Can add comments (The permission to comment to this file) */ DrawioFile.prototype.canComment = function() { return true; }; /** * Get a new comment object */ DrawioFile.prototype.newComment = function(content, user) { return new DrawioComment(this, null, content, Date.now(), Date.now(), false, user); };