gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
387 lines (356 loc) • 19.1 kB
text/typescript
/*
* Copyright (C) 1998-2017 by Northwoods Software Corporation
* All Rights Reserved.
*
* Go Local Storage
*/
import * as go from "../../release/go";
import * as gcs from "./GoCloudStorage";
import {Promise} from "es6-promise";
/**
* <p>Class for saving / loading <a href="http://gojs.net/latest/api/symbols/Diagram.html">Diagram</a> <a href="http://gojs.net/latest/api/symbols/Model.html">models</a>
* to / from Local Storage. GoLocalStorage is the only {@link GoCloudStorage} subclass than can be used in a local page; that is, one not served by a web server.</p>
* <p><b>Note</b>: that this class will not work with browsers that do not have Local Storage support (like some old versions of Internet Explorer).</p>
* @category Storage
*/
export class GoLocalStorage extends gcs.GoCloudStorage {
private _localStorage: Storage;
/**
* The number of files to display in {@link ui} before loading more
*/
private static _MIN_FILES_IN_UI = 100;
/**
* @constructor
* @param {go.Diagram|go.Diagram[]} managedDiagrams An array of GoJS <a href="http://gojs.net/latest/api/symbols/Diagram.html">Diagrams</a> whose model(s) will be saved to
* / loaded from Local Storage. Can also be a single Diagram.
* @param {string} defaultModel String representation of the default model data for new diagrams. If this is null, default new
* diagrams will be empty. Usually a value given by calling <a href="https://gojs.net/latest/api/symbols/Model.html#toJson">.toJson()</a> on a GoJS Diagram's Model.
* @param {string} iconsRelativeDirectory The directory path relative to the page in which this instance of GoLocalStorage exists, in which
* the storage service brand icons can be found. The default value is "../goCloudStorageIcons/".
*/
constructor(managedDiagrams: go.Diagram|go.Diagram[], defaultModel?: string, iconsRelativeDirectory?: string) {
super(managedDiagrams, defaultModel);
this._localStorage = window.localStorage;
this.ui.id = "goLocalStorageCustomFilepicker";
this._serviceName = "Local Storage";
}
/**
* Get the browser window's <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage">Local Storage</a> property.
* @function.
* @return {Storage}
*/
get localStorage(): Storage { return this._localStorage }
/**
* Check if Local Storage is supported by the current browser.
* @param {boolean} refreshToken This parameter can be ignored. It exists only to maintain GoCloudStorage system structure
* @return {Promise<boolean>} Returns a Promise that resolves with a boolean (true if local storage is supported, false if not)
*/
public authorize(refreshToken: boolean = false) {
const storage = this;
return new Promise(function(resolve: Function, reject: Function){
try {
storage.localStorage.setItem('item', 'item');
storage.localStorage.removeItem('item');
resolve(true);
} catch (e) {
// local storage not supported
resolve(false);
}
});
}
/**
* Show the custom Go Local Storage filepicker {@link ui}.
* @param {string} action Clarify what action is being done after file selection. Must be one of the following:
* <ul>
* <li>New</li>
* <li>Open</li>
* <li>Save</li>
* <li>Delete</li>
* </ul>
* @param {number} numAdditionalFiles Optional: Number of files to show in UI, in addition to a static numerical property (that can only be
* modified by changing source code). This prevents long wait times while the UI loads if there are a large number of diagram files stored in Local Storage.
* @return {Promise<any>} Returns a Promise that resolves (in {@link save}, {@link load}, or {@link remove} with an {@link DiagramFile}
* representing the saved/loaded/deleted file
*/
public showUI(action: string, numAdditionalFiles?: number) {
const storage = this;
const ui = storage.ui;
const spacestring = "qwe45qw34"; // used as a placeholder for spaces in IDs
if (!numAdditionalFiles) numAdditionalFiles = 0;
let maxFilesToShow: number = GoLocalStorage._MIN_FILES_IN_UI + numAdditionalFiles;
ui.innerHTML = "<img class='icons' src='" + storage.iconsRelativeDirectory + "localStorage.png'></img>";
let title: string = action + " Diagram File";
ui.innerHTML += "<strong>" + title + "</strong><hr></hr>";
document.getElementsByTagName('body')[0].appendChild(ui);
ui.style.visibility = 'visible';
const filesDiv = document.createElement('div');
filesDiv.id = 'fileOptions';
// filter out non-diagram files in local storage (only until max allowed files is reached)
let savedDiagrams: Object[] = [];
let numFilesToCheck: number = GoLocalStorage._MIN_FILES_IN_UI + numAdditionalFiles;
let numFilesChecked: number = 0;
let hasCheckedAllFiles: boolean = false;
if (storage.localStorage.length !== 0) {
for (let key in storage.localStorage) {
if (savedDiagrams.length < maxFilesToShow) {
numFilesChecked++;
let fileContent: string = storage.localStorage.getItem(key);
if (fileContent && fileContent.indexOf("GraphLinksModel" || "TreeModel") !== -1) {
let file: Object = { key: key, model: fileContent };
savedDiagrams.push(file);
}
if (numFilesChecked === storage.localStorage.length) hasCheckedAllFiles = true;
}
}
} else hasCheckedAllFiles = true;
if (savedDiagrams.length !== 0) {
// list diagram files in local storage as selectable files (as many as MIN_FILES_IN_UI + additionalFiles param)
for (let i = 0; i < savedDiagrams.length; i++) {
let kvp: Object = savedDiagrams[i];
let file: string = kvp['key'];
let fileId: string = file.replace(/ /g, spacestring);
if (action !== 'Save') {
filesDiv.innerHTML +=
"<div class='fileOption'>" +
"<input id=" + fileId + " type='radio' name='localStorageFile' />" +
"<label id =" + fileId + "-label" + " for='" + fileId + "'>" + file + "</label>" +
"</div>";
} else {
filesDiv.innerHTML +=
"<div class='fileOption'>" +
"<label id =" + fileId + "-label" + " for='" + fileId + "'>" + file + "</label>" +
"</div>";
}
}
}
// If there may be more diagram files to show, say so and provide user with option to try loading more in the UI
if (!hasCheckedAllFiles) {
let num: number = numAdditionalFiles + 50;
filesDiv.innerHTML += "<p>There may be more diagram files not shown. <a id='localStorageLoadMoreFiles'>Click here</a> to try loading more.</p>"
document.getElementById("localStorageLoadMoreFiles").onclick = function () {
storage.showUI(action, num);
}
}
ui.appendChild(filesDiv);
// italicize currently open file, if a file is currently open
if (storage.currentDiagramFile.id) {
let string: string = storage.currentDiagramFile.id.replace(/ /g, spacestring);
let el: HTMLElement = document.getElementById(string + '-label');
if (el) el.style.fontStyle = "italic";
}
// user input div (only for save)
if (action === 'Save') {
const userInputDiv = document.createElement('div');
userInputDiv.id = 'userInputDiv';
userInputDiv.innerHTML += '<span>Save Diagram As </span><input id="userInput" placeholder="Enter filename"></input>';
ui.appendChild(userInputDiv);
}
const submitDiv = document.createElement('div');
submitDiv.id = "submitDiv";
const actionButton = document.createElement('button');
actionButton.textContent = action;
actionButton.id = 'actionButton';
actionButton.onclick = function () {
storage.processUIResult(action);
}
submitDiv.appendChild(actionButton);
ui.appendChild(submitDiv);
const cancelDiv = document.createElement('div');
const cancelButton = document.createElement('button');
cancelButton.id = 'cancelButton';
cancelButton.textContent = 'Cancel';
cancelButton.onclick = function () {
storage.hideUI(true);
}
cancelDiv.appendChild(cancelButton);
ui.appendChild(cancelDiv);
return storage._deferredPromise['promise']; // will not resolve until action (save, load, delete) completes
}
/**
* @private
* @hidden
* Process the result of pressing the action button on the custom GoLocalStorage filepicker {@link ui}.
* @param {string} action The action being done. Acceptable values:
* <ul>
* <li>Save</li>
* <li>Load</li>
* <li>Delete</li>
* </ul>
*/
public processUIResult(action: string) {
const storage = this;
// Helper: Return key of the file selected from the custom localstorage filepicker menu
function getSelectedFile() {
const radios = document.getElementsByName('localStorageFile');
let selectedFile: string = null;
for (let i = 0; i < radios.length; i++) {
if ((<HTMLInputElement>radios[i]).checked) {
selectedFile = radios[i].id.replace(/qwe45qw34/g, ' ');
}
}
if (selectedFile) return selectedFile;
else return null;
}
let file: string = getSelectedFile();
switch (action) {
case 'Save': {
var name = (<HTMLInputElement>document.getElementById('userInput')).value;
if (name) {
name += '.diagram';
storage.save(name);
} else {
// handle bad name
}
break;
}
case 'Load': {
storage.load(file);
break;
}
case 'Delete': {
storage.remove(file);
break;
}
}
storage.hideUI();
}
/**
* <p>Get information about a diagram file saved to local storage. This data includes:</p>
* <ul>
* <li>content: The content of the saved file (a string respresentation of a GoJS Diagram Model)</li>
* <li>id: The key of the file in local storage</li>
* <li>name: Same as id value</li>
* <li>path: Same as id value</li>
* <ul>
* <p><b>Note:</b> Id, name, and path are all provided (despite being the same). They are requisite for creating valid {@link DiagramFile}s. </p>
* @param {string} path A valid key corresponding to a saved diagram file in Local Storage
* @return {Promise<any>} Returns a Promise that resolves with information about a diagram file saved to local storage
*/
public getFile(path: string) {
if (path.indexOf('.diagram') === -1) path += '.diagram';
return new Promise(function (resolve, reject) {
let fileContent: string = (!!window.localStorage.getItem(path)) ? window.localStorage.getItem(path) : null;
let file: Object = { name: path, content: fileContent, path: path, id: path };
resolve(file);
});
}
/**
* Check whether a file exists in LocalStorage at a given path.
* @param {string} path A valid key corresponding to a saved diagram file in Local Storage
* @return {Promise<any>} Returns a Promise that resolves with a boolean stating whether a file exists in LocalStorage at a given path
*/
public checkFileExists(path: string) {
if (path.indexOf('.diagram') === -1) path += '.diagram';
return new Promise(function (resolve, reject) {
let fileExists: boolean = !!(window.localStorage.getItem(path));
resolve(fileExists);
});
}
/**
* Save the current {@link managedDiagrams}'s model data to Local Storage using the custom filepicker {@link ui}.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file
*/
public saveWithUI() {
const storage = this;
return new Promise(function (resolve: Function, reject: Function) {
resolve(storage.showUI('Save'));
});
}
/**
* Save {@link managedDiagrams}' model data to Local Storage. If path is supplied save to that path. If no path is supplied but {@link currentDiagramFile} has non-null,
* valid properties, update saved diagram file content at the key in Local Storage corresponding to currentDiagramFile.path with current managedDiagrams' model data.
* If no path is supplied and currentDiagramFile is null or has null properties, this calls {@link saveWithUI}.
* @param {string} path A string to save diagram model data to (becomes the key for the file in Local Storage)
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the saved file
*/
public save(path?: string) {
const storage = this;
return new Promise(function (resolve: Function, reject: Function) {
if (path) {
if (path.indexOf('.diagram') === -1) path += '.diagram';
let item: string = storage.makeSaveFile();
storage.localStorage.setItem(path, item);
let savedFile: gcs.DiagramFile = { name: path, id: path, path: path };
storage.currentDiagramFile = savedFile;
resolve(savedFile); // used if saveDiagramAs was called without UI
// if saveDiagramAs has been called in processUIResult, need to resolve / reset the Deferred Promise instance variable
storage._deferredPromise['promise'].resolve(savedFile);
storage._deferredPromise['promise'] = storage.makeDeferredPromise();
} else if (storage.currentDiagramFile.path) {
let saveName: string = storage.currentDiagramFile['path'];
let savedFile: gcs.DiagramFile = { name: saveName, path: saveName, id: saveName };
let item: string = storage.makeSaveFile();
storage.localStorage.setItem(saveName, item);
resolve(saveName);
} else {
resolve(storage.saveWithUI());
}
});
}
/**
* Get the contents of a given file; load to {@link managedDiagrams} model. Use the custom filepicker {@link ui}.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the loaded file
*/
public loadWithUI() {
const storage = this;
return new Promise(function (resolve: Function, reject: Function) {
resolve(storage.showUI('Load'));
}).catch(function(e: any){
throw Error(e);
});
}
/**
* Get the contents of a given file; load to {@link managedDiagrams} model.
* @param {string} path A valid localstorage key to load diagram model data from
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the loaded file
*/
public load(path: string) {
const storage = this;
return new Promise(function (resolve: Function, reject: Function) {
if (path) {
let fileContents: string = storage.localStorage.getItem(path);
if (fileContents) {
storage.loadFromFileContents(fileContents);
let loadedFile: gcs.DiagramFile = { name: path, id: path, path: path };
storage.currentDiagramFile = loadedFile;
resolve(loadedFile); // used if loadDiagram was called without UI
// if loadDiagram has been called in processUIResult, need to resolve / reset the Deferred Promise instance variable
storage._deferredPromise.promise.resolve(loadedFile);
storage._deferredPromise.promise = storage.makeDeferredPromise();
} else throw Error('Cannot load file from local storage with path ' + path);
} else throw Error('Cannot load file from local storage with path ' + path);
}).catch(function(e: any) {
//console.error(e);
throw Error(e);
});
}
/**
* Delete a diagram from Local Storage using the custom filepicker menu {@link ui}.
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file
*/
public removeWithUI() {
const storage = this;
return new Promise(function (resolve: Function, reject: Function) {
resolve(storage.showUI('Delete'));
});
}
/**
* Delete a given diagram from Local Storage.
* @param {string} path A valid localstorage key to delete diagram model data from
* @return {Promise<any>} Returns a Promise that resolves with a {@link DiagramFile} representing the deleted file
*/
public remove(path: string) {
const storage = this;
return new Promise(function (resolve: Function, reject: Function) {
if (path) {
let deletedFile: gcs.DiagramFile = { name: path, path: path, id: path };
if (storage.currentDiagramFile && path === storage.currentDiagramFile['name']) storage.currentDiagramFile = {name: null, path: null, id: null};
storage.localStorage.removeItem(path); // remove file from local storage
resolve(deletedFile); // used if deleteDiagram was called without UI
// if deleteDiagram has been called in processUIResult, need to resolve / reset the Deferred Promise instance variable
storage._deferredPromise['promise'].resolve(deletedFile);
storage._deferredPromise['promise'] = storage.makeDeferredPromise();
} else throw Error('Cannot delete file from local storage with path ' + path);
});
}
}