drawio/etc/sheetsaddon/Code.gs
2019-04-15 05:06:28 +02:00

462 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Draw.io Diagrams Sheets add-on v1.0
* Copyright (c) 2019, JGraph Ltd
*/
var EXPORT_URL = "https://exp.draw.io/ImageExport4/export";
var DRAW_URL = "https://www.draw.io/";
/**
* Creates a menu entry in the Google Docs UI when the document is opened.
*/
function onOpen()
{
SpreadsheetApp.getUi().createAddonMenu()
.addItem("Insert Diagrams...", "insertDiagrams")
.addSeparator()
.addItem("Update Current Sheet", "updateCurrent")
.addItem("Update All Sheets", "updateAll")
.addSeparator()
.addItem("New Diagram...", "newDiagram")
.addToUi();
//We cannot get selected image yet! So we cannot do update/edit selected
}
/**
* Runs when the add-on is installed.
*/
function onInstall()
{
onOpen();
}
/**
* Gets the user's OAuth 2.0 access token so that it can be passed to Picker.
* This technique keeps Picker from needing to show its own authorization
* dialog, but is only possible if the OAuth scope that Picker needs is
* available in Apps Script. In this case, the function includes an unused call
* to a DriveApp method to ensure that Apps Script requests access to all files
* in the user's Drive.
*
* @return {string} The user's OAuth 2.0 access token.
*/
function getOAuthToken() {
DriveApp.getRootFolder();
return ScriptApp.getOAuthToken();
}
/**
* Shows a picker and lets the user select multiple diagrams to insert.
*/
function insertDiagrams()
{
var html = HtmlService.createHtmlOutputFromFile('Picker.html')
.setWidth(620).setHeight(440)
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
SpreadsheetApp.getUi().showModalDialog(html, 'Select draw.io Diagrams:');
}
/**
* Inserts an image for the given diagram.
*/
function pickerHandler(items)
{
if (items != null && items.length > 0)
{
var errors = [];
var sheet = SpreadsheetApp.getActiveSheet();
var curCell = sheet.getSelection().getCurrentCell();
var col = 1, row = 1;
if (curCell != null)
{
col = curCell.getColumn();
row = curCell.getRow();
}
var step = 1;
for (var i = 0; i < items.length; i++)
{
try
{
var ins = insertDiagram(items[i].id, items[i].page, col, row);
if (ins != null)
{
col = col + step;
row = row + step;
}
else
{
errors.push("- " + items[i].name + ": Unknown error");
}
}
catch (e)
{
errors.push("- " + items[i].name + ": " + e.message);
}
}
// Shows message only in case of errors
if (errors.length > 0)
{
var msg = "";
if (errors.length > 0)
{
msg += errors.length + " insert" + ((errors.length > 1) ? "s" : "") + " failed:\n";
}
msg += errors.join("\n");
SpreadsheetApp.getUi().alert(msg);
}
//Trying to refresh but it doesn't work!
SpreadsheetApp.flush();
}
}
/**
* Inserts the given diagram at the given position.
*/
function insertDiagram(id, page, col, row)
{
var result = fetchImage(id, page, 'auto'); //We should set scale to 1 as with 'auto' we cannot scale it back (see below)
var blob = result[0];
var w = result[1];
var h = result[2];
var s = result[3];
var img = null;
if (blob != null)
{
img = SpreadsheetApp.getActiveSheet().insertImage(blob, col, row);
//Setting width/height force the image to disappear, and a refresh is needed!
img.setWidth( w / s );
img.setHeight( h / s );
var link = createLink(id, page, result[4], 'auto');
img.setAltTextDescription(link);
}
else
{
throw new Error("Invalid image " + id);
}
return img;
}
/**
* Get draw.io diagrams images in a given sheet
*/
function getSheetDrawioDiagrams(sheet)
{
var allImages = sheet.getImages();
var drawioImages = [];
for (var i = 0; i < allImages.length; i++)
{
if (isValidLink(allImages[i].getAltTextDescription()))
{
drawioImages.push(allImages[i]);
}
}
return drawioImages;
}
/**
* Updates the current sheet diagrams in-place.
*/
function updateCurrent()
{
var drawioImages = getSheetDrawioDiagrams(SpreadsheetApp.getActiveSheet());
if (drawioImages.length > 0)
{
updateElements(drawioImages);
}
else
{
SpreadsheetApp.getUi().alert("No diagrams found in current sheet");
}
}
/**
* Updates all diagrams in the document.
*/
function updateAll()
{
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var drawioImages = [];
for (var i = 0; i < sheets.length; i++)
{
var imgs = getSheetDrawioDiagrams(sheets[i]);
drawioImages.push.apply(drawioImages, imgs);
}
updateElements(drawioImages);
}
/**
* Updates all given diagrams.
*/
function updateElements(elts)
{
if (elts != null)
{
var updated = 0;
var errors = [];
for (var i = 0; i < elts.length; i++)
{
try
{
var upd = updateElement(elts[i]);
if (upd != null)
{
updated++;
}
else
{
errors.push("- Unknown Error");
}
}
catch (e)
{
errors.push("- " + e.message);
}
}
// Shows status in case of errors or multiple updates
if (errors.length > 0 || updated > 0)
{
var msg = "";
if (updated > 0)
{
msg += updated + " diagram" + ((updated > 1) ? "s" : "") + " updated\n";
}
if (errors.length > 0)
{
msg += errors.length + " update" + ((errors.length > 1) ? "s" : "") + " failed:\n";
}
msg += errors.join("\n");
SpreadsheetApp.getUi().alert(msg);
}
}
}
/**
* Returns true if the given URL points to draw.io
*/
function createLink(id, page, pageId, scale)
{
var params = [];
if (pageId != null)
{
params.push('page-id=' + pageId);
}
else if (page != null && page != "0")
{
params.push('page=' + page);
}
params.push('scale=' + (scale || '1'));
return DRAW_URL + ((params.length > 0) ? "?" + params.join("&") : "") + "#G" + id;
}
/**
* Returns true if the given URL points to draw.io
*/
function isValidLink(url)
{
return url != null && (url.substring(0, DRAW_URL.length) == DRAW_URL || url.substring(0, 22) == "https://drive.draw.io/");
}
/**
* Returns the diagram ID for the given URL.
*/
function getDiagramId(url)
{
return url.substring(url.lastIndexOf("#G") + 2);
}
/**
* Returns the diagram ID for the given URL.
*/
function getUrlParams(url)
{
var result = {};
var idx = url.indexOf("?");
if (idx > 0)
{
var idx2 = url.indexOf("#", idx + 1);
if (idx2 < 0)
{
idx2 = url.length;
}
if (idx2 > idx)
{
var search = url.substring(idx + 1, idx2);
var tokens = search.split("&");
for (var i = 0; i < tokens.length; i++)
{
var idx3 = tokens[i].indexOf('=');
if (idx3 > 0)
{
result[tokens[i].substring(0, idx3)] = tokens[i].substring(idx3 + 1);
}
}
}
}
return result;
}
/**
* Updates the diagram in the given inline image and returns the new inline image.
*/
function updateElement(elt)
{
var result = null;
var url = elt.getAltTextDescription();
var id = getDiagramId(url);
var params = getUrlParams(url);
if (id != null && id.length > 0)
{
result = updateDiagram(id, params["page"], params["scale"] || 1, elt, params["page-id"]);
}
else
{
// commenting this out as well - invalid link might indicate image is not coming from draw.io
// throw new Error("Invalid link " + url);
}
return result;
}
/**
* Updates the diagram in the given inline image and returns the new inline image.
*/
function updateDiagram(id, page, scale, elt, pageId)
{
var img = null;
var result = fetchImage(id, page, scale, pageId);
var isOK = false;
if (result != null)
{
var blob = result[0];
var w = result[1];
var h = result[2];
var s = result[3];
if (blob != null)
{
isOK = true;
// replace image with the same link
var img = elt.replace(blob);
//TODO needs refresh after setting image dimensions
img.setWidth( w / s );
img.setHeight( h / s );
var link = createLink(id, page, result[4], scale);
img.setAltTextDescription(link);
}
}
if (!isOK)
{
throw new Error("Invalid image " + id);
}
return img;
}
/**
* Fetches the diagram for the given document ID.
*/
function fetchImage(id, page, scale, pageId)
{
var file = DriveApp.getFileById(id);
if (file != null && file.getSize() > 0)
{
var data = {
"format": "png",
"scale": scale || "1",
"xml": encodeURIComponent(file.getBlob().getDataAsString())
};
if (pageId != null)
{
data.pageId = pageId;
}
else
{
data.from = page || "0";
}
var response = UrlFetchApp.fetch(EXPORT_URL,
{
"method": "post",
"payload": data
});
var headers = response.getHeaders();
return [response.getBlob(), headers["content-ex-width"] || 0, headers["content-ex-height"] || 0, headers["content-scale"] || 1, headers["content-page-id"]];
}
else
{
// Returns an empty, transparent 1x1 PNG image as a placeholder
return [Utilities.newBlob(Utilities.base64Decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNg+M9QDwADgQF/e5IkGQAAAABJRU5ErkJggg=="), "image/png")];
}
}
/**
* Creates a new diagram.
*/
function newDiagram()
{
openUrl('https://www.draw.io/?mode=google');
}
/**
* Open a URL in a new tab.
*/
function openUrl(url)
{
var html = HtmlService.createHtmlOutput('<html><script>'
+'window.close = function(){window.setTimeout(function(){google.script.host.close()},9)};'
+'var a = document.createElement("a"); a.href="'+url+'"; a.target="_blank";'
+'if(document.createEvent){'
+' var event=document.createEvent("MouseEvents");'
+' if(navigator.userAgent.toLowerCase().indexOf("firefox")>-1){window.document.body.append(a)}'
+' event.initEvent("click",true,true); a.dispatchEvent(event);'
+'}else{ a.click() }'
+'close();'
+'</script>'
// Offer URL as clickable link in case above code fails.
+'<body style="word-break:break-word;font-family:sans-serif;">Failed to open automatically. <a href="'+url+'" target="_blank" onclick="window.close()">Click here to proceed</a>.</body>'
+'<script>google.script.host.setHeight(40);google.script.host.setWidth(410)</script>'
+'</html>')
.setWidth(90).setHeight(1);
SpreadsheetApp.getUi().showModalDialog(html, "Opening...");
}