gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
592 lines (505 loc) • 20.1 kB
JavaScript
/*
* Functionality in this file pertains to how GoTimeline imports data from outside sources (Google Drive, Local Drive)
* This includes auth flows as well as the actual formatting and importing of data
*/
replaceNodeData = true;
// local disk csv file data import stuff
function readSingleFile(e) {
var file = e.target.files[0];
if (!file) {
return;
}
var reader = new FileReader();
reader.onload = function(e) {
var contents = e.target.result;
getImportDataType(file, categories);
};
reader.readAsText(file);
}
// Google Drive data import stuff
var CLIENT_ID = "16225373139-n24vtg7konuetna3ofbmfcaj2infhgmg.apps.googleusercontent.com";
var API_KEY = "AIzaSyCeMqYjdrskXIgtv35kyet8ji2m3_ilydY";
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"];
var SCOPES = 'https://www.googleapis.com/auth/drive';
gPicker = null;
gOAuthToken = null;
/**
* On load, called to load the auth2 library and API client library.
*/
function authorizeGoogleUserForDataImport() {
function auth() {
gapi.auth.authorize({
'client_id': CLIENT_ID,
'scope': SCOPES,
'immediate': false
},function (authResult) {
if (window['google']) gPicker = window['google']['picker']; // used for importing google drive sheets for data
gOAuthToken = authResult.access_token
if (authResult && !authResult.error) {
createGoogleSheetPicker(getImportDataType);
}
});
}
gapi.load('client:auth', auth);
gapi.load('picker', {});
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
});
}
/*
* Ask user if what type(s) of Nodes the data file to import is for
* @param f file to import
* @param categories -- an Array of possible Node template categories to choose from
*/
function getImportDataType (f, categories) {
// remove any pre-existing data ordering div
var dom = document.getElementById("ge-data-type-div");
if (dom) {
document.body.removeChild(dom);
}
var dataTypeMenu = document.createElement("div");
dataTypeMenu.id = "ge-data-type-div";
dataTypeMenu.className = "ge-menu";
var titleDiv = document.createElement("div");
titleDiv.id = "ge-data-type-title-div";
var title = document.createElement("h4");
var fname = (f.docs == undefined) ? f.name : f.docs[0].name;
title.innerText = fname;
title.id = "ge-data-type-title";
titleDiv.appendChild(title);
var desc = document.createElement("i");
desc.innerHTML = "What Node type is this data for?";
titleDiv.appendChild(desc);
dataTypeMenu.appendChild(titleDiv);
for (var i = 0; i < categories.length; i++) {
var cat = categories[i];
var b = document.createElement("button");
b.className = "ge-button";
b.innerText = cat + " Nodes";
b.data = cat;
if (cat === "") b.innerText = "Default";
b.onclick = function () {
buildDataOrdering(f, this.data);
document.body.removeChild(dataTypeMenu);
}
dataTypeMenu.appendChild(b);
}
var cb = document.createElement("button");
cb.innerText = "Cancel";
cb.className = "ge-button";
cb.onclick = function () {
document.body.removeChild(dataTypeMenu);
}
document.body.appendChild(dataTypeMenu);
}
/*
* get info from user on how their data is ordered in the file they want to import data from
* @param f The file used for data import
* @param cat The category of Node this data will be used to create
*/
function buildDataOrdering(f, cat) {
// this is a JavaScript object that holds data props supported by this Node category
var dataProps = nodeGenerationMap.get(cat).dataProps;
// reset dataOrdering -- this is used so we know what data corresponds with what column when we import
// kvp's are <col><data property name>
var dataOrdering = new go.Map();
// is "f" some data passed from the Google Picker API?
if (f.docs != undefined) {
f = f.docs[0];
}
if (f.name == undefined) return;
// remove any pre-existing data ordering div
var dom = document.getElementById("ge-data-ordering-div");
if (dom) {
document.body.removeChild(dom);
}
var dataOrderingMenu = document.createElement("div");
dataOrderingMenu.id = "ge-data-ordering-div";
dataOrderingMenu.className = "ge-menu";
var titleDiv = document.createElement("div");
titleDiv.id = "ge-data-ordering-title-div";
var title = document.createElement("h4");
title.innerText = f.name;
title.id = "ge-data-ordering-title";
titleDiv.appendChild(title);
var desc = document.createElement("i");
desc.innerHTML = "Tell us how your data is organized, and we'll do the rest. <hr/>";
titleDiv.appendChild(desc);
dataOrderingMenu.appendChild(titleDiv);
var datasDiv = document.createElement("div");
datasDiv.id = "ge-datas-div";
// <select> elements
var i = 1;
for (let index in dataProps) {
var div = document.createElement("div");
div.className = "ge-data-option-div";
var p2 = document.createElement("span");
p2.innerText = "What data is in column " + i + "?";
div.appendChild(p2);
var s = document.createElement("select");
s.id = i-1;
for (let it in dataProps) {
var option = document.createElement("option");
option.value = dataProps[it].name;
option.innerText = dataProps[it].name;
s.appendChild(option);
}
s.value = dataProps[i-1].name;
dataOrdering.set(i-1, s.value);
// update fileDataOrdering map if <select> element changes
s.onchange = function () {
dataOrdering.set(this.id,this.value)
}
div.appendChild(s);
datasDiv.appendChild(div);
i++;
}
dataOrderingMenu.appendChild(datasDiv);
var checkboxDiv = document.createElement("div");
checkboxDiv.id = "ge-replace-event-data-checkbox-div";
// checkbox to handle whether or not to replace all model data with imported data
var checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = replaceNodeData;
checkbox.className = "ge-checkbox";
checkbox.id = "ge-replace-event-data-checkbox";
checkbox.onchange = function () {
replaceNodeData = this.checked;
}
var cbLabel = document.createElement("label");
cbLabel.id = "ge-replace-event-data-checkbox-label";
cbLabel.for = checkbox.id;
cbLabel.innerText = "Replace existing node data?";
checkboxDiv.appendChild(checkbox);
checkboxDiv.appendChild(cbLabel);
dataOrderingMenu.appendChild(checkboxDiv);
// load button - try to load the file
// this will fail if the user has not given a unique data type for each column
var lb = document.createElement("button");
lb.innerText = "Load Data";
lb.className = "ge-button";
lb.onclick = function () {
var fileInput = document.getElementById('file-input');
fileInput.value = "";
var selectValues = [];
var divs = dataOrderingMenu.getElementsByTagName("div");
for (var j = 0; j < divs.length; j++) {
var div = divs[j];
if (div.className != "ge-data-option-div") continue;
var selectVal = div.getElementsByTagName("select")[0].value;
if (selectValues.indexOf(selectVal) != -1 ) {
storageManager.showMessage("Each column must hold different data");
return;
} else {
selectValues.push(selectVal);
}
}
// if we got to here, all select values are unique
document.body.removeChild(dataOrderingMenu);
// it's a google drive file
if (f.parentId != undefined) {
importGoogleDriveData(f, cat, dataOrdering);
}
// it's a local file on disk
else {
var reader = new FileReader();
reader.onload = function(e) {
var contents = e.target.result;
importDataFromContents(f, contents, cat, dataOrdering);
};
reader.readAsText(f);
}
} // end onclick for load button
// choose a different file button
var fb = document.createElement("button");
fb.innerText = "Choose different file";
fb.className = "ge-button";
fb.onclick = function () {
var fileInput = document.getElementById('file-input');
fileInput.value = "";
document.body.removeChild(dataOrderingMenu);
document.getElementById('file-input').click();
}
// cancel button
var cb = document.createElement("button");
cb.innerText = "Cancel";
cb.className = "ge-button";
cb.onclick = function () {
var fileInput = document.getElementById('file-input');
fileInput.value = "";
document.body.removeChild(dataOrderingMenu);
}
dataOrderingMenu.appendChild(lb);
dataOrderingMenu.appendChild(fb);
dataOrderingMenu.appendChild(cb);
document.body.appendChild(dataOrderingMenu);
}
// Import data from Google Drive and construct a diagram with it
function importGoogleDriveData(f, cat, dataOrdering) {
gapi.client.request({
'path': '/drive/v3/files/' + f.id + "/export",
'params': { 'mimeType': 'text/csv' },
'method': 'GET',
callback: function (p1,req) {
var json = JSON.parse(req);
var data = json.gapiRequest.data.body;
importDataFromContents(f, data, cat, dataOrdering);
} // end callback
});
}
// generic -- loads csv data into diagram data
function importDataFromContents(f, fileContents, cat, dataOrdering) {
var nodeDataArray = [];
var nodeGenerationObj = nodeGenerationMap.get(cat);
// load in all events or ranges in the spreadsheet
var lines = fileContents.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line === "") break;
var tokens = line.split(",");
// delete blank tokens
for (var j = 0; j < tokens.length; j++) {
if (tokens[j] == "") {
tokens.splice(j,1);
j--;
}
}
// sweep through tokens for data based on dataOrdering
var nodeData = nodeGenerationObj.defaultNodeData;
for (var k = 0; k < dataOrdering.size; k++) {
var prop = dataOrdering.get(k); // data of type dataType is in column k
var dataPropType = null;
for (let m in nodeGenerationObj.dataProps) {
if (nodeGenerationObj.dataProps[m].name === prop) {
dataPropType = nodeGenerationObj.dataProps[m].type;
}
}
nodeData[prop] = tokens[k];
if (dataPropType === "boolean") {
var str = tokens[k].replace(/\s/g, '');
if (str === "true") nodeData[prop] = true;
if (str === "false") nodeData[prop] = false;
else nodeData[prop] = true;
}
if (dataPropType === "date") {
nodeData[prop] = makeLocalDate(tokens[k]);
}
}
var nd = {};
for (let p in nodeData) {
nd[p] = nodeData[p];
}
nd["category"] = cat;
nodeDataArray.push(nd);
nodeData = null;
} // end line iteration
if (replaceNodeData) {
myDiagram.model.nodeDataArray = [];
myDiagram.model.linkDataArray = [];
}
for (let i in nodeDataArray) {
var nd = nodeDataArray[i];
var preds = nodeGenerationObj.predicates !== undefined ? nodeGenerationObj.predicates : null;
var postCreateFunction = nodeGenerationObj.postCreateFunction !== undefined ? nodeGenerationObj.postCreateFunction : null;
addNodeToDiagram(nd, preds, postCreateFunction);
}
}
// @param cb callback function -- what to do with the response from the Picker -- the picked file
function createGoogleSheetPicker (cb) {
// (appId is just the first number of clientId before '-')
let appId = "16225373139"; // GoCloudStorage app Id
var view = new gPicker.View(gPicker.ViewId.DOCS);
view.setMimeTypes("application/vnd.google-apps.spreadsheet");
var picker = new gPicker.PickerBuilder()
.enableFeature(gPicker.Feature.NAV_HIDDEN)
.enableFeature(gPicker.Feature.MULTISELECT_ENABLED)
.setAppId(appId)
.setOrigin(window.location.protocol + '//' + window.location.host)
.setOAuthToken(gOAuthToken)
.addView(view)
.setDeveloperKey("AIzaSyDBj43lBLpYMMVKw4aN_pvuRg7_XMVGf18") // same picker api key as GoCloudStorage Google App
.setCallback(function (args) {
// only ask if range or event node data if a file is picked
if (args.action === "picked") {
cb(args, categories);
}
})
.build();
picker.setVisible(true);
}
// CREATE Nodes DYNAMICALLY
/*
* Create a Node of a given category using a UI
* @param category -- optional?
* @param dataProps -- an array of the data properties the node may have
* Must be an Array of JavaScript objects. Each object must have a "name" and "type" property
* @param defaultNodeData optional -- the default data for a Node of this category
* @param predicates optional -- an Array of functions that act as predicates to determine whether this Node can be added
* If any of these predicates returns false, the Node will not be created.
* You must format your predicate functions to take a JavaScript object as a parameter
* that represents the proposed data for the Node about to be created
* @param postCreateFunction optional -- performed after the Node is created.
* Perhaps you need to run some clean up or other checks -- if so, do them here. This function may take the newly created node as a parameter
*/
function createNode(category) {
//if (!defaultNodeData) defaultNodeData = {};
if (!category) category = "";
var nodeGenerationObj = nodeGenerationMap.get(category);
if (!nodeGenerationObj) throw new Error("nodeGenerationMap not defined at key " + category);
var dataProps = nodeGenerationObj.dataProps;
var defaultNodeData = nodeGenerationObj.defaultNodeData === undefined ? {} : nodeGenerationObj.defaultNodeData;
var predicates = nodeGenerationObj.predicates === undefined ? null : nodeGenerationObj.predicates;
var postCreateFunction = nodeGenerationObj.postCreateFunction === undefined ? null : nodeGenerationObj.postCreateFunction;
// remove any preexisting create window
var d = document.getElementById("ge-build-node-div");
if (d !== null && d !== undefined) {
document.body.removeChild(d);
}
var buildNodeMenu = document.createElement("div");
buildNodeMenu.id = "ge-build-node-div";
buildNodeMenu.className = "ge-menu";
var titleDiv = document.createElement("div");
titleDiv.className = "ge-handle";
titleDiv.id = "ge-build-node-title-div";
var title = document.createElement("h4");
title.innerText = "Build " + category + "Node";
title.id = "ge-build-node-title";
titleDiv.appendChild(title);
buildNodeMenu.appendChild(titleDiv);
var desc = document.createElement("i");
desc.innerHTML = "Tell us all about this " + category + " Node.";
buildNodeMenu.appendChild(desc);
var dataPropertiesDiv = document.createElement("div");
dataPropertiesDiv.className = "ge-scrollable";
function addDataPropertyDiv(index) {
var propertyDiv = document.createElement("div");
propertyDiv.className = "ge-build-node-property-div";
var dataPropName = dataProps[index].name;
var dataPropType = dataProps[index].type;
var dataPropDefaultVal = dataProps[index].value;
if (dataPropDefaultVal == undefined) dataPropDefaultVal = "";
// label for input
var label = document.createElement("option");
label.for = "ge-create-node-data-"+index;
label.innerText = dataPropName;
var input = document.createElement("input");
input.data = dataPropType; // this should change if the <select> tag associated with this changes
input.id = "ge-create-node-data-"+index;
switch (dataPropType) {
case "date":
input.type = "date"; break;
case "boolean":
input.type = "checkbox"; break;
case "color":
input.type = "color"; break;
}
if (input.type === "checkbox") {
if (dataPropDefaultVal !== true && dataPropDefaultVal !== false) input.checked = true;
else input.checked = dataPropDefaultVal;
}
input.value = dataPropDefaultVal;
input.onkeyup = function(e){
if (e.keyCode == 13) { // enter key
var crb = document.getElementById("ge-build-node-button");
crb.click();
}
};
propertyDiv.appendChild(input);
propertyDiv.appendChild(label);
dataPropertiesDiv.appendChild(propertyDiv);
}
for (let i in dataProps) {
addDataPropertyDiv(i);
}
buildNodeMenu.appendChild(dataPropertiesDiv);
// create button
var crb = document.createElement("button");
crb.id = "ge-build-node-button";
crb.innerText = "Create Node";
crb.className = "ge-button";
crb.onclick = function () {
// build node data
var dataPropertiesDivs = dataPropertiesDiv.getElementsByTagName("div");
function makeNodeDataCopy(nodeData) {
var copy = {};
for (let prop in nodeData) {
copy[prop] = nodeData[prop];
}
return copy;
}
var nodeData = makeNodeDataCopy(defaultNodeData);
nodeData["category"] = category;
// iterate over each data property div, building kvp for a node's data
for (var i = 0; i < dataPropertiesDivs.length; i++) {
var div = dataPropertiesDivs[i];
var input = div.getElementsByTagName("input")[0];
var key = dataProps[i].name;
var val = input.value;
if (input.type === "checkbox") val = input.checked;
if (input.type === "date") val = makeLocalDate(val);
nodeData[key] = val;
}
addNodeToDiagram(nodeData, predicates, postCreateFunction);
document.body.removeChild(buildNodeMenu);
}
// cancel button
var cb = document.createElement("button");
cb.innerText = "Cancel";
cb.className = "ge-button";
cb.onclick = function () {
document.body.removeChild(buildNodeMenu);
}
buildNodeMenu.appendChild(crb);
buildNodeMenu.appendChild(cb);
document.body.appendChild(buildNodeMenu);
refreshDraggableWindows();
}
/*
* Constructs a node from some node data and adds it to the diagram, as long as all predicates return true
* Called by createNode, as well as during the import process
* @param nodeData
* @param predicates
* @param postCreateFunction
*/
function addNodeToDiagram(nodeData, predicates, postCreateFunction) {
// if any of the supplied predicates returns false, do not create this Node
var doNotCreate = false;
if (predicates) {
for (let i in predicates) {
var pred = predicates[i];
if (!pred(nodeData)) {
doNotCreate = true;
}
}
}
if (!doNotCreate) {
myDiagram.model.addNodeData(nodeData);
var node = myDiagram.findNodeForData(nodeData);
myDiagram.select(node);
if (postCreateFunction) {
postCreateFunction(node);
}
}
}
// end data interaction / node generation
// return a Date with the correct timezone offset. accepts a formatted string.
// Dunno if this is the best place for this but its very important for anything that uses Dates, like GoTimeline
function makeLocalDate(dateStr) {
d = new Date(dateStr);
d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
return d;
}