Browse Source

20.6.0 release

David Benson 3 years ago
parent
commit
55f57b13f6
100 changed files with 8001 additions and 7551 deletions
  1. 2 2
      .github/stale.yml
  2. 21 0
      ChangeLog
  3. 1 3
      README.md
  4. 1 1
      VERSION
  5. 32 85
      src/main/java/com/mxgraph/online/AbsAuthServlet.java
  6. 63 0
      src/main/java/com/mxgraph/online/AtlasAuth.java
  7. 29 0
      src/main/java/com/mxgraph/online/AuthComm.java
  8. 87 0
      src/main/java/com/mxgraph/online/AuthServletComm.java
  9. 65 0
      src/main/java/com/mxgraph/online/DropboxAuth.java
  10. 13 59
      src/main/java/com/mxgraph/online/DropboxAuthServlet.java
  11. 76 0
      src/main/java/com/mxgraph/online/GitHubAuth.java
  12. 13 70
      src/main/java/com/mxgraph/online/GitHubAuthServlet.java
  13. 74 0
      src/main/java/com/mxgraph/online/GitlabAuth.java
  14. 13 68
      src/main/java/com/mxgraph/online/GitlabAuthServlet.java
  15. 186 0
      src/main/java/com/mxgraph/online/GoogleAuth.java
  16. 8 175
      src/main/java/com/mxgraph/online/GoogleAuthServlet.java
  17. 73 0
      src/main/java/com/mxgraph/online/MSGraphAuth.java
  18. 11 61
      src/main/java/com/mxgraph/online/MSGraphAuthServlet.java
  19. 28 8
      src/main/webapp/electron.js
  20. 1727 1726
      src/main/webapp/js/app.min.js
  21. 3 40
      src/main/webapp/js/diagramly/App.js
  22. 2 2
      src/main/webapp/js/diagramly/Devel.js
  23. 15 4
      src/main/webapp/js/diagramly/Dialogs.js
  24. 2 2
      src/main/webapp/js/diagramly/DropboxClient.js
  25. 10 3
      src/main/webapp/js/diagramly/Editor.js
  26. 47 118
      src/main/webapp/js/diagramly/EditorUi.js
  27. 40 20
      src/main/webapp/js/diagramly/Menus.js
  28. 43 36
      src/main/webapp/js/diagramly/sidebar/Sidebar.js
  29. 54 0
      src/main/webapp/js/export.js
  30. 17 40
      src/main/webapp/js/grapheditor/EditorUi.js
  31. 10 7
      src/main/webapp/js/grapheditor/Format.js
  32. 156 89
      src/main/webapp/js/grapheditor/Graph.js
  33. 2 6
      src/main/webapp/js/grapheditor/Menus.js
  34. 128 37
      src/main/webapp/js/grapheditor/Sidebar.js
  35. 1384 1383
      src/main/webapp/js/integrate.min.js
  36. 162 162
      src/main/webapp/js/stencils.min.js
  37. 1637 1637
      src/main/webapp/js/viewer-static.min.js
  38. 1637 1637
      src/main/webapp/js/viewer.min.js
  39. 26 25
      src/main/webapp/mxgraph/mxClient.js
  40. 1 0
      src/main/webapp/resources/dia.txt
  41. 1 0
      src/main/webapp/resources/dia_am.txt
  42. 1 0
      src/main/webapp/resources/dia_ar.txt
  43. 1 0
      src/main/webapp/resources/dia_bg.txt
  44. 1 0
      src/main/webapp/resources/dia_bn.txt
  45. 1 0
      src/main/webapp/resources/dia_bs.txt
  46. 1 0
      src/main/webapp/resources/dia_ca.txt
  47. 1 0
      src/main/webapp/resources/dia_cs.txt
  48. 1 0
      src/main/webapp/resources/dia_da.txt
  49. 1 0
      src/main/webapp/resources/dia_de.txt
  50. 1 0
      src/main/webapp/resources/dia_el.txt
  51. 1 0
      src/main/webapp/resources/dia_eo.txt
  52. 1 0
      src/main/webapp/resources/dia_es.txt
  53. 1 0
      src/main/webapp/resources/dia_et.txt
  54. 1 0
      src/main/webapp/resources/dia_eu.txt
  55. 1 0
      src/main/webapp/resources/dia_fa.txt
  56. 1 0
      src/main/webapp/resources/dia_fi.txt
  57. 1 0
      src/main/webapp/resources/dia_fil.txt
  58. 25 24
      src/main/webapp/resources/dia_fr.txt
  59. 1 0
      src/main/webapp/resources/dia_gl.txt
  60. 1 0
      src/main/webapp/resources/dia_gu.txt
  61. 1 0
      src/main/webapp/resources/dia_he.txt
  62. 1 0
      src/main/webapp/resources/dia_hi.txt
  63. 1 0
      src/main/webapp/resources/dia_hr.txt
  64. 1 0
      src/main/webapp/resources/dia_hu.txt
  65. 1 0
      src/main/webapp/resources/dia_i18n.txt
  66. 1 0
      src/main/webapp/resources/dia_id.txt
  67. 1 0
      src/main/webapp/resources/dia_it.txt
  68. 20 19
      src/main/webapp/resources/dia_ja.txt
  69. 1 0
      src/main/webapp/resources/dia_kn.txt
  70. 1 0
      src/main/webapp/resources/dia_ko.txt
  71. 1 0
      src/main/webapp/resources/dia_lt.txt
  72. 1 0
      src/main/webapp/resources/dia_lv.txt
  73. 1 0
      src/main/webapp/resources/dia_ml.txt
  74. 1 0
      src/main/webapp/resources/dia_mr.txt
  75. 1 0
      src/main/webapp/resources/dia_ms.txt
  76. 1 0
      src/main/webapp/resources/dia_my.txt
  77. 1 0
      src/main/webapp/resources/dia_nl.txt
  78. 1 0
      src/main/webapp/resources/dia_no.txt
  79. 1 0
      src/main/webapp/resources/dia_pl.txt
  80. 1 0
      src/main/webapp/resources/dia_pt-br.txt
  81. 1 0
      src/main/webapp/resources/dia_pt.txt
  82. 1 0
      src/main/webapp/resources/dia_ro.txt
  83. 1 0
      src/main/webapp/resources/dia_ru.txt
  84. 1 0
      src/main/webapp/resources/dia_si.txt
  85. 1 0
      src/main/webapp/resources/dia_sk.txt
  86. 1 0
      src/main/webapp/resources/dia_sl.txt
  87. 1 0
      src/main/webapp/resources/dia_sr.txt
  88. 1 0
      src/main/webapp/resources/dia_sv.txt
  89. 1 0
      src/main/webapp/resources/dia_sw.txt
  90. 1 0
      src/main/webapp/resources/dia_ta.txt
  91. 1 0
      src/main/webapp/resources/dia_te.txt
  92. 1 0
      src/main/webapp/resources/dia_th.txt
  93. 1 0
      src/main/webapp/resources/dia_tr.txt
  94. 1 0
      src/main/webapp/resources/dia_uk.txt
  95. 1 0
      src/main/webapp/resources/dia_vi.txt
  96. 1 0
      src/main/webapp/resources/dia_zh-tw.txt
  97. 1 0
      src/main/webapp/resources/dia_zh.txt
  98. 1 1
      src/main/webapp/service-worker.js
  99. 1 1
      src/main/webapp/service-worker.js.map
  100. 0 0
      src/main/webapp/styles/dark.css

+ 2 - 2
.github/stale.yml

@@ -1,7 +1,7 @@
 # Number of days of inactivity before an issue becomes stale
-daysUntilStale: 180
+daysUntilStale: 365
 # Number of days of inactivity before a stale issue is closed
-daysUntilClose: 7
+daysUntilClose: 20
 # Issues with these labels will never be considered stale
 exemptLabels:
   - pinned

+ 21 - 0
ChangeLog

@@ -1,3 +1,24 @@
+22-NOV-2022: 20.6.0
+
+- Adds search, scratchpad in simple UI shapes dialog
+- Removes tooltip for removed click event handler
+- Supports reading uncompressed library entries [DS-929]
+- Adds AWS lambda authenication replication [DS-930]
+- Fixes ignored locked table and row styles [3174]
+- Adds lock/unlock option in arrange tab
+- Fixes mouse selection bugs on iOS [2758,3055,3056]
+- Handles change of search query via clipboard event
+- [desktop] Added CLI csv export [202]
+- [desktop] Added layers selection support to CLI export [143]
+- Blocks functions from being overwritten with values [DID-6750]
+- Improves visibility of more shapes item in sidebar [DS-931]
+- Checks url parameters before using them [CSP-1035]
+- Removes potential ReDoS code that is not used [CSP-1036]
+- Escapes url parameters used in output [CSP-1037]
+- Fixes link to app folder in Dropbox [2843]
+- Fixes restoring from mxGraphModel files [DID-6788]
+- Fixes hover shape picker ignores container [3199]
+
 06-NOV-2022: 20.5.3
 
 - Uses static Graph.sanitizeHtml

+ 1 - 3
README.md

@@ -1,8 +1,6 @@
-[![Build Status](https://travis-ci.com/jgraph/drawio.svg?branch=master)](https://travis-ci.com/jgraph/drawio)
-
 About
 -----
-draw.io, this project, is a configurable diagramming/whiteboarding visualization application. draw.io is owned and developed by JGraph Ltd, a UK based software company.
+draw.io, this project, is a configurable diagramming/whiteboarding visualization application. draw.io is jointly owned and developed by JGraph Ltd and draw.io AG.
 
 As well as running this project, we run a production-grade deployment of the diagramming interface at https://app.diagrams.net.
 

+ 1 - 1
VERSION

@@ -1 +1 @@
-20.5.3
+20.6.0

+ 32 - 85
src/main/java/com/mxgraph/online/AbsAuthServlet.java

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2006-2019, JGraph Ltd
+ * Copyright (c) 2006-2022, JGraph Ltd
  */
 package com.mxgraph.online;
 
@@ -22,21 +22,17 @@ import java.util.logging.Logger;
 
 import javax.cache.Cache;
 import javax.cache.CacheException;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import com.google.appengine.api.memcache.MemcacheServiceException;
 import com.google.gson.Gson;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 
 @SuppressWarnings("serial")
-abstract public class AbsAuthServlet extends HttpServlet
+abstract public class AbsAuth extends HttpServlet implements AuthComm
 {
-	private static final Logger log = Logger.getLogger(AbsAuthServlet.class.getName());
+	private static final Logger log = Logger.getLogger(AbsAuth.class.getName());
 	private static final boolean DEBUG = false;
 	protected static final String SEPARATOR = "/:::/";
 	public static final int X_WWW_FORM_URLENCODED = 1;
@@ -145,7 +141,7 @@ abstract public class AbsAuthServlet extends HttpServlet
 				tokenCache.put(key, val);
 				done = true;
 			}
-			catch(MemcacheServiceException e)
+			catch(Exception e)
 			{
 				//delay in re-trial is above
 				done = false;
@@ -153,47 +149,16 @@ abstract public class AbsAuthServlet extends HttpServlet
 		}
 		while(!done && trials < 3);
 	}
-	
-	protected String getCookieValue(String name, HttpServletRequest request)
-	{
-		String val = null;
-		
-		Cookie[] cookies = request.getCookies();
-		
-		if (cookies != null)
-		{
-			for (Cookie cookie : cookies)
-			{
-				if (name.equals(cookie.getName()))
-				{
-					val = cookie.getValue();
-					break;
-				}
-			}
-		}
-		
-		return val;
-	}
-	
-	protected void addCookie(String name, String val, int age, HttpServletResponse response)
-	{
-		response.addHeader("Set-Cookie", name + "=" + val + "; Max-Age=" + age + ";path=" + cookiePath + "; Secure; HttpOnly; SameSite=none");
-	}
-	
-	protected void deleteCookie(String name, HttpServletResponse response)
-	{
-		response.addHeader("Set-Cookie", name + "= ;path=" + cookiePath + "; expires=Thu, 01 Jan 1970 00:00:00 UTC; Secure; HttpOnly; SameSite=none");
-	}
-	
+
 	//To support multiple tokens in one cookie
-	protected String getTokenFromCookieVal(String tokenCookieVal, HttpServletRequest request)
+	protected String getTokenFromCookieVal(String tokenCookieVal, Object request)
 	{
 		return tokenCookieVal;
 	}
 	
-	protected void logout(String tokenCookieName, String tokenCookieVal, HttpServletRequest request, HttpServletResponse response)
+	protected void logout(String tokenCookieName, String tokenCookieVal, Object request, Object response)
 	{
-		deleteCookie(tokenCookieName, response);
+		deleteCookie(tokenCookieName, cookiePath, response);
 	}
 	
 	//https://stackoverflow.com/questions/4390800/determine-if-a-string-is-absolute-url-or-relative-url-in-java
@@ -215,38 +180,31 @@ abstract public class AbsAuthServlet extends HttpServlet
 		}
 	}
 
-	/**
-	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
-	 */
-	protected void doGet(HttpServletRequest request,
-			HttpServletResponse response) throws ServletException, IOException
+	protected void doGetAbst(Object request, Object response) throws IOException
 	{
-		String stateOnly = request.getParameter("getState");
+		String stateOnly = getParameter("getState", request);
 		
 		if ("1".equals(stateOnly))
 		{
 			String state = new BigInteger(256, random).toString(32);
 			String key = new BigInteger(256, random).toString(32);
 			putCacheValue(key, state);
-			response.setStatus(HttpServletResponse.SC_OK);
+			setStatus(HttpServletResponse.SC_OK, response);
 			//Chrome blocks this cookie when draw.io is running in an iframe. The cookie is added to parent frame. TODO FIXME
-			addCookie(STATE_COOKIE, key, STATE_COOKIE_AGE, response); //10 min to finish auth
-			response.setHeader("Content-Type", "text/plain");
-			OutputStream out = response.getOutputStream();
-			out.write(state.getBytes());
-			out.flush();
-			out.close();
+			addCookie(STATE_COOKIE, key, STATE_COOKIE_AGE, cookiePath, response); //10 min to finish auth
+			setHeader("Content-Type", "text/plain", response);
+			setBody(state, response);
 			return;
 		}
 		
-		String code = request.getParameter("code");
-		String error = request.getParameter("error");
+		String code = getParameter("code", request);
+		String error = getParameter("error", request);
 		HashMap<String, String> stateVars = new HashMap<>();
 		String secret = null, client = null, redirectUri = null, domain = null, stateToken = null, cookieToken = null, version = null, successRedirect = null;
 		
 		try
 		{
-			String state = request.getParameter("state");
+			String state = getParameter("state", request);
 			
 			try 
 			{
@@ -281,7 +239,7 @@ abstract public class AbsAuthServlet extends HttpServlet
 					cookieToken = (String) tokenCache.get(cacheKey);
 					//Delete cookie & cache after being used since it is a single use
 					tokenCache.remove(cacheKey);
-					deleteCookie(STATE_COOKIE, response);
+					deleteCookie(STATE_COOKIE, cookiePath, response);
 				}
 			}
 			catch(Exception e)
@@ -291,14 +249,14 @@ abstract public class AbsAuthServlet extends HttpServlet
 			}
 
 			Config CONFIG = getConfig();
-			redirectUri = CONFIG.getRedirectUrl(domain != null? domain : request.getServerName());
+			redirectUri = CONFIG.getRedirectUrl(domain != null? domain : getServerName(request));
 			
 			secret = CONFIG.getClientSecret(client);
 			
 			String tokenCookie = TOKEN_COOKIE + client; //Such that we support multiple client ids
 			
 			//TODO This code should be removed when new code is propagated
-			String refreshToken = request.getParameter("refresh_token"), tokenCookieVal = null;
+			String refreshToken = getParameter("refresh_token", request), tokenCookieVal = null;
 			
 			if (refreshToken == null)
 			{
@@ -307,7 +265,7 @@ abstract public class AbsAuthServlet extends HttpServlet
 			}
 			
 			//Logout (delete refresh token)
-			String logoutParam = request.getParameter("doLogout");
+			String logoutParam = getParameter("doLogout", request);
 			
 			if ("1".equals(logoutParam))
 			{
@@ -315,36 +273,29 @@ abstract public class AbsAuthServlet extends HttpServlet
 			}
 			else if (error != null)
 			{
-				response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+				setStatus(HttpServletResponse.SC_UNAUTHORIZED, response);
 				
-				OutputStream out = response.getOutputStream();
-	
-				PrintWriter writer = new PrintWriter(out);
-
 				// Writes JavaScript code
-				writer.println(processAuthError(error));
-	
-				writer.flush();
-				writer.close();
+				setBody(processAuthError(error), response);
 			}
 			else if ((code == null && refreshToken == null) || client == null || redirectUri == null || secret == null)
 			{
-				response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+				setStatus(HttpServletResponse.SC_BAD_REQUEST, response);
 			}
 			//Non GAE runtimes are excluded from state check. TODO Change GAE stub to return null from CacheFactory
 			else if (IS_GAE && (stateToken == null || !stateToken.equals(cookieToken)))
 			{
-				response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+				setStatus(HttpServletResponse.SC_UNAUTHORIZED, response);
 			}
 			else
 			{
 				Response authResp = contactOAuthServer(CONFIG.AUTH_SERVICE_URL, code, refreshToken, secret, client, redirectUri, successRedirect != null, 1);
 				
-				response.setStatus(authResp.status);
+				setStatus(authResp.status, response);
 				
 				if (authResp.refreshToken != null)
 				{
-					addCookie(tokenCookie, getRefreshTokenCookie(authResp.refreshToken, tokenCookieVal, authResp.accessToken), TOKEN_COOKIE_AGE, response);
+					addCookie(tokenCookie, getRefreshTokenCookie(authResp.refreshToken, tokenCookieVal, authResp.accessToken), TOKEN_COOKIE_AGE, cookiePath, response);
 				}
 				
 				if (authResp.content != null)
@@ -352,23 +303,19 @@ abstract public class AbsAuthServlet extends HttpServlet
 					if (successRedirect != null)
 					{
 						//successRedirect is validated above
-						response.sendRedirect(successRedirect + "#" + Utils.encodeURIComponent(authResp.content, "UTF-8"));
+						sendRedirect(successRedirect + "#" + Utils.encodeURIComponent(authResp.content, "UTF-8"), response);
 					}
 					else
 					{
-						OutputStream out = response.getOutputStream();
-						PrintWriter writer = new PrintWriter(out);
-						writer.println(authResp.content);
-						writer.flush();
-						writer.close();
+						setBody(authResp.content, response);
 					}
 				}
 			}
 		}
 		catch (Exception e) 
 		{
-			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-			log.log(Level.SEVERE, "AUTH-SERVLET: [" + request.getRemoteAddr()+ "] ERROR: " + e.getMessage());
+			setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response);
+			log.log(Level.SEVERE, "AUTH-SERVLET: [" + getRemoteAddr(request)+ "] ERROR: " + e.getMessage());
 		}
 	}
 
@@ -377,7 +324,7 @@ abstract public class AbsAuthServlet extends HttpServlet
 		return refreshToken;
 	}
 
-	protected  int getExpiresIn(JsonObject json)
+	protected int getExpiresIn(JsonObject json)
 	{
 		try
 		{

+ 63 - 0
src/main/java/com/mxgraph/online/AtlasAuth.java

@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2006-2019, JGraph Ltd
+ */
+package com.mxgraph.online;
+
+import java.io.IOException;
+
+@SuppressWarnings("serial")
+abstract public class AtlasAuth extends AbsAuth
+{
+	public static String CLIENT_SECRET_FILE_PATH = "atlas_client_secret";
+	public static String CLIENT_ID_FILE_PATH = "atlas_client_id";
+	
+	private static Config CONFIG = null;
+	
+	protected Config getConfig()
+	{
+		if (CONFIG == null)
+		{
+			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
+					clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
+		
+			CONFIG = new Config(clientIds, clientSerets);
+			CONFIG.REDIRECT_PATH = "/atlas";
+			CONFIG.AUTH_SERVICE_URL = "https://auth.atlassian.com/oauth/token";
+		}
+		
+		return CONFIG;
+	}
+
+	public AtlasAuth() 
+	{
+		super();
+		postType = JSON;
+		cookiePath = "/atlas";
+	}
+	
+	protected String processAuthResponse(String authRes, boolean jsonResponse)
+	{
+		StringBuffer res = new StringBuffer();
+		
+		//Call the opener callback function directly with the given json
+		if (!jsonResponse)
+		{
+			res.append("<!DOCTYPE html><html><head><script>");
+			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
+		}
+		
+		res.append(authRes);
+
+		if (!jsonResponse)
+		{
+			res.append(";");					
+			res.append("if (window.opener != null && window.opener.onAtlasCallback != null)"); 
+			res.append("{");
+			res.append("	window.opener.onAtlasCallback(authInfo, window);");
+			res.append("}");
+			res.append("})();</script></head><body></body></html>");
+		}
+
+		return res.toString();
+	}
+}

+ 29 - 0
src/main/java/com/mxgraph/online/AuthComm.java

@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2006-2022, JGraph Ltd
+ */
+package com.mxgraph.online;
+
+import java.io.IOException;
+
+public interface AuthComm
+{
+    String getCookieValue(String name, Object request_p);
+	
+	void addCookie(String name, String val, int age, String cookiePath, Object response_p);
+	
+    void deleteCookie(String name, String cookiePath, Object response_p);
+
+	String getParameter(String name, Object request);
+
+	String getServerName(Object request);
+
+	String getRemoteAddr(Object request);
+
+	void setBody(String body, Object response) throws IOException;
+	
+	void setStatus(int status, Object response);
+
+	void setHeader(String name, String value, Object response);
+
+	void sendRedirect(String url, Object response) throws IOException;
+}

+ 87 - 0
src/main/java/com/mxgraph/online/AuthServletComm.java

@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) 2006-2022, JGraph Ltd
+ */
+package com.mxgraph.online;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public interface AuthServletComm extends AuthComm 
+{
+    default String getCookieValue(String name, Object request_p)
+	{
+		HttpServletRequest request = (HttpServletRequest) request_p;
+		String val = null;
+		
+		Cookie[] cookies = request.getCookies();
+		
+		if (cookies != null)
+		{
+			for (Cookie cookie : cookies)
+			{
+				if (name.equals(cookie.getName()))
+				{
+					val = cookie.getValue();
+					break;
+				}
+			}
+		}
+		
+		return val;
+	}
+	
+	default void addCookie(String name, String val, int age, String cookiePath, Object response_p)
+	{
+		HttpServletResponse response = (HttpServletResponse) response_p;
+		response.addHeader("Set-Cookie", name + "=" + val + "; Max-Age=" + age + ";path=" + cookiePath + "; Secure; HttpOnly; SameSite=none");
+	}
+	
+	default void deleteCookie(String name, String cookiePath, Object response_p)
+	{
+		HttpServletResponse response = (HttpServletResponse) response_p;
+		response.addHeader("Set-Cookie", name + "= ;path=" + cookiePath + "; expires=Thu, 01 Jan 1970 00:00:00 UTC; Secure; HttpOnly; SameSite=none");
+	}
+	
+	default String getParameter(String name, Object request)
+	{
+		return ((HttpServletRequest) request).getParameter(name);
+	}
+
+	default String getServerName(Object request)
+	{
+		return ((HttpServletRequest) request).getServerName();
+	}
+
+	default String getRemoteAddr(Object request)
+	{
+		return ((HttpServletRequest) request).getRemoteAddr();
+	}
+
+	default void setBody(String body, Object response) throws IOException
+	{
+		OutputStream out = ((HttpServletResponse) response).getOutputStream();
+		out.write(body.getBytes());
+		out.flush();
+		out.close();
+	}
+	
+	default void setStatus(int status, Object response)
+	{
+		((HttpServletResponse) response).setStatus(status);
+	}
+
+	default void setHeader(String name, String value, Object response)
+	{
+		((HttpServletResponse) response).setHeader(name, value);
+	}
+
+	default void sendRedirect(String url, Object response) throws IOException
+	{
+		((HttpServletResponse) response).sendRedirect(url);
+	}
+}

+ 65 - 0
src/main/java/com/mxgraph/online/DropboxAuth.java

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2006-2019, JGraph Ltd
+ */
+package com.mxgraph.online;
+
+import java.io.IOException;
+
+@SuppressWarnings("serial")
+abstract public class DropboxAuth extends AbsAuth
+{
+	public static String CLIENT_SECRET_FILE_PATH = "dropbox_client_secret";
+	public static String CLIENT_ID_FILE_PATH = "dropbox_client_id";
+	
+	private static Config CONFIG = null;
+	
+	protected Config getConfig()
+	{
+		if (CONFIG == null)
+		{
+			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
+					clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
+			
+			CONFIG = new Config(clientIds, clientSerets);
+			CONFIG.AUTH_SERVICE_URL = "https://api.dropboxapi.com/oauth2/token";
+			CONFIG.REDIRECT_PATH = "/dropbox";
+		}
+		
+		return CONFIG;
+	}
+
+	public DropboxAuth()
+	{
+		super();
+		cookiePath = "/dropbox";
+		withRedirectUrlInRefresh = false;
+	}
+	
+	protected String processAuthResponse(String authRes, boolean jsonResponse)
+	{
+		StringBuffer res = new StringBuffer();
+		
+		if (!jsonResponse)
+		{
+			res.append("<!DOCTYPE html><html><head><script type=\"text/javascript\">");
+			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
+		}
+		
+		res.append(authRes);
+		
+		if (!jsonResponse)
+		{
+			res.append(";");
+			res.append("if (window.opener != null && window.opener.onDropboxCallback != null)"); 
+			res.append("{");
+			res.append("	window.opener.onDropboxCallback(authInfo, window);");
+			res.append("} else {");
+			res.append("	onDropboxCallback(authInfo);");
+			res.append("}");
+			res.append("})();</script>");
+			res.append("</head><body></body></html>");
+		}
+
+		return res.toString();
+	}
+}

+ 13 - 59
src/main/java/com/mxgraph/online/DropboxAuthServlet.java

@@ -1,65 +1,19 @@
-/**
- * Copyright (c) 2006-2019, JGraph Ltd
- */
 package com.mxgraph.online;
 
 import java.io.IOException;
 
-@SuppressWarnings("serial")
-public class DropboxAuthServlet extends AbsAuthServlet
-{
-	public static String CLIENT_SECRET_FILE_PATH = "dropbox_client_secret";
-	public static String CLIENT_ID_FILE_PATH = "dropbox_client_id";
-	
-	private static Config CONFIG = null;
-	
-	protected Config getConfig()
-	{
-		if (CONFIG == null)
-		{
-			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
-					clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
-			
-			CONFIG = new Config(clientIds, clientSerets);
-			CONFIG.AUTH_SERVICE_URL = "https://api.dropboxapi.com/oauth2/token";
-			CONFIG.REDIRECT_PATH = "/dropbox";
-		}
-		
-		return CONFIG;
-	}
-
-	public DropboxAuthServlet()
-	{
-		super();
-		cookiePath = "/dropbox";
-		withRedirectUrlInRefresh = false;
-	}
-	
-	protected String processAuthResponse(String authRes, boolean jsonResponse)
-	{
-		StringBuffer res = new StringBuffer();
-		
-		if (!jsonResponse)
-		{
-			res.append("<!DOCTYPE html><html><head><script type=\"text/javascript\">");
-			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
-		}
-		
-		res.append(authRes);
-		
-		if (!jsonResponse)
-		{
-			res.append(";");
-			res.append("if (window.opener != null && window.opener.onDropboxCallback != null)"); 
-			res.append("{");
-			res.append("	window.opener.onDropboxCallback(authInfo, window);");
-			res.append("} else {");
-			res.append("	onDropboxCallback(authInfo);");
-			res.append("}");
-			res.append("})();</script>");
-			res.append("</head><body></body></html>");
-		}
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
-		return res.toString();
-	}
+public class DropboxAuthServlet extends DropboxAuth implements AuthServletComm
+{
+    /**
+     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
+     */
+    public void doGet(HttpServletRequest request,
+            HttpServletResponse response) throws ServletException, IOException
+    {
+        super.doGetAbst(request, response);
+    }
 }

+ 76 - 0
src/main/java/com/mxgraph/online/GitHubAuth.java

@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2006-2019, JGraph Ltd
+ */
+package com.mxgraph.online;
+
+import java.io.IOException;
+
+@SuppressWarnings("serial")
+abstract public class GitHubAuth extends AbsAuth
+{
+	public static String CLIENT_SECRET_FILE_PATH = "github_client_secret";
+	public static String CLIENT_ID_FILE_PATH = "github_client_id";
+	public static String AUTH_SERVICE_URL_FILE_PATH = "github_auth_url";
+	
+	private static Config CONFIG = null;
+	
+	protected Config getConfig()
+	{
+		if (CONFIG == null)
+		{
+			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
+					clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
+			
+			CONFIG = new Config(clientIds, clientSerets);
+
+			try
+			{
+				CONFIG.AUTH_SERVICE_URL = SecretFacade.getSecret(AUTH_SERVICE_URL_FILE_PATH, getServletContext());
+			}
+			catch (Exception e)
+			{
+				CONFIG.AUTH_SERVICE_URL = "https://github.com/login/oauth/access_token";
+			}
+			
+			CONFIG.REDIRECT_PATH = "/github2";
+		}
+		
+		return CONFIG;
+	}
+
+	public GitHubAuth()
+	{
+		super();
+		cookiePath = "/github2";
+		withRedirectUrl = false;
+		withAcceptJsonHeader = true;
+	}
+	
+	protected String processAuthResponse(String authRes, boolean jsonResponse)
+	{
+		StringBuffer res = new StringBuffer();
+		
+		if (!jsonResponse)
+		{
+			res.append("<!DOCTYPE html><html><head><script type=\"text/javascript\">");
+			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
+		}
+		
+		res.append(authRes);
+		
+		if (!jsonResponse)
+		{
+			res.append(";");
+			res.append("if (window.opener != null && window.opener.onGitHubCallback != null)"); 
+			res.append("{");
+			res.append("	window.opener.onGitHubCallback(authInfo, window);");
+			res.append("} else {");
+			res.append("	onGitHubCallback(authInfo);");
+			res.append("}");
+			res.append("})();</script>");
+			res.append("</head><body></body></html>");
+		}
+
+		return res.toString();
+	}
+}

+ 13 - 70
src/main/java/com/mxgraph/online/GitHubAuthServlet.java

@@ -1,76 +1,19 @@
-/**
- * Copyright (c) 2006-2019, JGraph Ltd
- */
 package com.mxgraph.online;
 
 import java.io.IOException;
 
-@SuppressWarnings("serial")
-public class GitHubAuthServlet extends AbsAuthServlet
-{
-	public static String CLIENT_SECRET_FILE_PATH = "github_client_secret";
-	public static String CLIENT_ID_FILE_PATH = "github_client_id";
-	public static String AUTH_SERVICE_URL_FILE_PATH = "github_auth_url";
-	
-	private static Config CONFIG = null;
-	
-	protected Config getConfig()
-	{
-		if (CONFIG == null)
-		{
-			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
-					clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
-			
-			CONFIG = new Config(clientIds, clientSerets);
-
-			try
-			{
-				CONFIG.AUTH_SERVICE_URL = SecretFacade.getSecret(AUTH_SERVICE_URL_FILE_PATH, getServletContext());
-			}
-			catch (Exception e)
-			{
-				CONFIG.AUTH_SERVICE_URL = "https://github.com/login/oauth/access_token";
-			}
-			
-			CONFIG.REDIRECT_PATH = "/github2";
-		}
-		
-		return CONFIG;
-	}
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
-	public GitHubAuthServlet()
-	{
-		super();
-		cookiePath = "/github2";
-		withRedirectUrl = false;
-		withAcceptJsonHeader = true;
-	}
-	
-	protected String processAuthResponse(String authRes, boolean jsonResponse)
-	{
-		StringBuffer res = new StringBuffer();
-		
-		if (!jsonResponse)
-		{
-			res.append("<!DOCTYPE html><html><head><script type=\"text/javascript\">");
-			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
-		}
-		
-		res.append(authRes);
-		
-		if (!jsonResponse)
-		{
-			res.append(";");
-			res.append("if (window.opener != null && window.opener.onGitHubCallback != null)"); 
-			res.append("{");
-			res.append("	window.opener.onGitHubCallback(authInfo, window);");
-			res.append("} else {");
-			res.append("	onGitHubCallback(authInfo);");
-			res.append("}");
-			res.append("})();</script>");
-			res.append("</head><body></body></html>");
-		}
-
-		return res.toString();
-	}
+public class GitHubAuthServlet extends GitHubAuth implements AuthServletComm
+{
+    /**
+     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
+     */
+    public void doGet(HttpServletRequest request,
+            HttpServletResponse response) throws ServletException, IOException
+    {
+        super.doGetAbst(request, response);
+    }
 }

+ 74 - 0
src/main/java/com/mxgraph/online/GitlabAuth.java

@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2006-2019, JGraph Ltd
+ */
+package com.mxgraph.online;
+
+import java.io.IOException;
+
+@SuppressWarnings("serial")
+abstract public class GitlabAuth extends AbsAuth
+{
+	public static String CLIENT_SECRET_FILE_PATH = "gitlab_client_secret";
+	public static String CLIENT_ID_FILE_PATH = "gitlab_client_id";
+	public static String AUTH_SERVICE_URL_FILE_PATH = "gitlab_auth_url";
+	
+	private static Config CONFIG = null;
+	
+	protected Config getConfig()
+	{
+		if (CONFIG == null)
+		{
+			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
+					clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
+			
+			CONFIG = new Config(clientIds, clientSerets);
+
+			try
+			{
+				CONFIG.AUTH_SERVICE_URL = SecretFacade.getSecret(AUTH_SERVICE_URL_FILE_PATH, getServletContext());
+			}
+			catch (Exception e)
+			{
+				CONFIG.AUTH_SERVICE_URL = "https://gitlab.com/oauth/token";
+			}
+			
+			CONFIG.REDIRECT_PATH = "/gitlab";
+		}
+		
+		return CONFIG;
+	}
+
+	public GitlabAuth()
+	{
+		super();
+		cookiePath = "/gitlab";
+	}
+	
+	protected String processAuthResponse(String authRes, boolean jsonResponse)
+	{
+		StringBuffer res = new StringBuffer();
+		
+		if (!jsonResponse)
+		{
+			res.append("<!DOCTYPE html><html><head><script type=\"text/javascript\">");
+			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
+		}
+		
+		res.append(authRes);
+		
+		if (!jsonResponse)
+		{
+			res.append(";");
+			res.append("if (window.opener != null && window.opener.onGitLabCallback != null)"); 
+			res.append("{");
+			res.append("	window.opener.onGitLabCallback(authInfo, window);");
+			res.append("} else {");
+			res.append("	onGitLabCallback(authInfo);");
+			res.append("}");
+			res.append("})();</script>");
+			res.append("</head><body></body></html>");
+		}
+
+		return res.toString();
+	}
+}

+ 13 - 68
src/main/java/com/mxgraph/online/GitlabAuthServlet.java

@@ -1,74 +1,19 @@
-/**
- * Copyright (c) 2006-2019, JGraph Ltd
- */
 package com.mxgraph.online;
 
 import java.io.IOException;
 
-@SuppressWarnings("serial")
-public class GitlabAuthServlet extends AbsAuthServlet
-{
-	public static String CLIENT_SECRET_FILE_PATH = "gitlab_client_secret";
-	public static String CLIENT_ID_FILE_PATH = "gitlab_client_id";
-	public static String AUTH_SERVICE_URL_FILE_PATH = "gitlab_auth_url";
-	
-	private static Config CONFIG = null;
-	
-	protected Config getConfig()
-	{
-		if (CONFIG == null)
-		{
-			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
-					clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
-			
-			CONFIG = new Config(clientIds, clientSerets);
-
-			try
-			{
-				CONFIG.AUTH_SERVICE_URL = SecretFacade.getSecret(AUTH_SERVICE_URL_FILE_PATH, getServletContext());
-			}
-			catch (Exception e)
-			{
-				CONFIG.AUTH_SERVICE_URL = "https://gitlab.com/oauth/token";
-			}
-			
-			CONFIG.REDIRECT_PATH = "/gitlab";
-		}
-		
-		return CONFIG;
-	}
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
-	public GitlabAuthServlet()
-	{
-		super();
-		cookiePath = "/gitlab";
-	}
-	
-	protected String processAuthResponse(String authRes, boolean jsonResponse)
-	{
-		StringBuffer res = new StringBuffer();
-		
-		if (!jsonResponse)
-		{
-			res.append("<!DOCTYPE html><html><head><script type=\"text/javascript\">");
-			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
-		}
-		
-		res.append(authRes);
-		
-		if (!jsonResponse)
-		{
-			res.append(";");
-			res.append("if (window.opener != null && window.opener.onGitLabCallback != null)"); 
-			res.append("{");
-			res.append("	window.opener.onGitLabCallback(authInfo, window);");
-			res.append("} else {");
-			res.append("	onGitLabCallback(authInfo);");
-			res.append("}");
-			res.append("})();</script>");
-			res.append("</head><body></body></html>");
-		}
-
-		return res.toString();
-	}
+public class GitlabAuthServlet extends GitlabAuth implements AuthServletComm
+{
+    /**
+     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
+     */
+    public void doGet(HttpServletRequest request,
+            HttpServletResponse response) throws ServletException, IOException
+    {
+        super.doGetAbst(request, response);
+    }
 }

+ 186 - 0
src/main/java/com/mxgraph/online/GoogleAuth.java

@@ -0,0 +1,186 @@
+/**
+ * Copyright (c) 2006-2022, JGraph Ltd
+ */
+package com.mxgraph.online;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+
+@SuppressWarnings("serial")
+abstract public class GoogleAuth extends AbsAuth
+{
+	public static String CLIENT_SECRET_FILE_PATH = "google_client_secret";
+	public static String CLIENT_ID_FILE_PATH = "google_client_id";
+	
+	private static Config CONFIG = null;
+	
+	protected Config getConfig()
+	{
+		if (CONFIG == null)
+		{
+			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
+				clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
+			
+			CONFIG = new Config(clientIds, clientSerets);
+			CONFIG.REDIRECT_PATH = "/google";
+			CONFIG.AUTH_SERVICE_URL = "https://www.googleapis.com/oauth2/v4/token";
+		}
+		
+		return CONFIG;
+	}
+
+	public GoogleAuth()
+	{
+		super();
+		cookiePath = "/google";
+	}
+	
+	protected String getTokenFromCookieVal(String tokenCookieVal, Object request)
+	{
+		String userId = getParameter("userId", request);
+		
+		if (tokenCookieVal != null && userId != null)
+		{
+			String[] tokens = tokenCookieVal.split(SEPARATOR);
+			
+			for (int i = 0; i < tokens.length; i++)
+			{
+				if (tokens[i].startsWith(userId + ":"))
+				{
+					return tokens[i].substring(userId.length() + 1);
+				}
+			}
+		}
+
+		return tokenCookieVal;
+	}
+	
+	protected String getRefreshTokenCookie(String refreshToken, String tokenCookieVal, String accessToken) 
+	{
+		HttpURLConnection con = null;
+		String userId = null;
+		
+		try
+		{
+			URL obj = new URL("https://www.googleapis.com/oauth2/v2/userinfo?alt=json");
+			con = (HttpURLConnection) obj.openConnection();
+			con.setRequestProperty("Authorization", "Bearer " + accessToken);
+			con.setRequestProperty("User-Agent", "draw.io");
+			int status = con.getResponseCode();
+			
+			if (status >= 200 && status <= 299)
+			{
+				BufferedReader in = new BufferedReader(
+						new InputStreamReader(con.getInputStream()));
+				String inputLine;
+				StringBuffer strRes = new StringBuffer();
+				
+				while ((inputLine = in.readLine()) != null)
+				{
+					strRes.append(inputLine);
+				}
+				in.close();
+				
+				userId = new Gson().fromJson(strRes.toString(), JsonElement.class).getAsJsonObject().get("id").getAsString();
+			}
+		}
+		catch(Exception e)
+		{
+			e.printStackTrace();
+		}
+		
+		if (userId != null)
+		{
+			ArrayList<String> tokens = new ArrayList<>();
+			tokens.add(userId + ":" + refreshToken);
+			
+			if (tokenCookieVal != null)
+			{
+				String[] curTokens = tokenCookieVal.split(SEPARATOR);
+				
+				for (int i = 0; i < curTokens.length; i++)
+				{
+					if (!curTokens[i].startsWith(userId + ":"))
+					{
+						tokens.add(curTokens[i]);
+					}
+				}
+			}
+			
+			return String.join(SEPARATOR, tokens);
+		}
+		
+		return tokenCookieVal; //If we couldn't get the userId, we just return existing tokens such that we don't corrupt them
+	}
+	
+	protected void logout(String tokenCookieName, String tokenCookieVal, Object request, Object response)
+	{
+		String userId = getParameter("userId", request);
+		
+		if (tokenCookieVal != null && userId != null)
+		{
+			ArrayList<String> tokens = new ArrayList<>();
+			String[] curTokens = tokenCookieVal.split(SEPARATOR);
+			
+			for (int i = 0; i < curTokens.length; i++)
+			{
+				if (!curTokens[i].startsWith(userId + ":"))
+				{
+					tokens.add(curTokens[i]);
+				}
+			}
+			
+			if (tokens.size() > 0)
+            {
+                addCookie(tokenCookieName, String.join(SEPARATOR, tokens), TOKEN_COOKIE_AGE, cookiePath, response);
+            }
+            else
+            {
+                deleteCookie(tokenCookieName, cookiePath, response);
+            }
+		}
+	}
+	
+	protected String processAuthResponse(String authRes, boolean jsonResponse)
+	{
+		StringBuffer res = new StringBuffer();
+		
+		//In Office Add-in, we don't have access to opened window to attach a function to it, 
+		//	also with the redirect (since we had to open google auth in the same window) we lost Office Messaging.
+		//	This is due to using Google own file picker instead of creating our own picker 
+		//	(as we did with OneDrive since its picker only support popup windows which is not supported in Office)
+		//	This is why we load driveLoader.js which define onGDriveCallback and redirects automatically to the page including the picker
+		//	For other scenarios, we use another function name (onGoogleDriveCallback)
+		if (!jsonResponse)
+		{
+			res.append("<!DOCTYPE html><html><head>");
+			res.append("<script src=\"/connect/office365/js/driveLoader.js\" type=\"text/javascript\"></script>");
+			res.append("<script type=\"text/javascript\">");
+			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
+		}
+		
+		res.append(authRes);
+		
+		if (!jsonResponse)
+		{
+			res.append(";");
+			res.append("if (window.opener != null && window.opener.onGoogleDriveCallback != null)"); 
+			res.append("{");
+			res.append("	window.opener.onGoogleDriveCallback(authInfo, window);");
+			res.append("} else {");
+			res.append("	onGDriveCallback(authInfo);");
+			res.append("}");
+			res.append("})();</script>");
+			res.append("</head><body></body></html>");
+		}
+
+		return res.toString();
+	}
+}

+ 8 - 175
src/main/java/com/mxgraph/online/GoogleAuthServlet.java

@@ -3,187 +3,20 @@
  */
 package com.mxgraph.online;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.ArrayList;
 
+import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import com.google.gson.Gson;
-import com.google.gson.JsonElement;
-
-@SuppressWarnings("serial")
-public class GoogleAuthServlet extends AbsAuthServlet
+public class GoogleAuthServlet extends GoogleAuth implements AuthServletComm
 {
-	public static String CLIENT_SECRET_FILE_PATH = "google_client_secret";
-	public static String CLIENT_ID_FILE_PATH = "google_client_id";
-	
-	private static Config CONFIG = null;
-	
-	protected Config getConfig()
-	{
-		if (CONFIG == null)
-		{
-			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
-				clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
-			
-			CONFIG = new Config(clientIds, clientSerets);
-			CONFIG.REDIRECT_PATH = "/google";
-			CONFIG.AUTH_SERVICE_URL = "https://www.googleapis.com/oauth2/v4/token";
-		}
-		
-		return CONFIG;
-	}
-
-	public GoogleAuthServlet()
-	{
-		super();
-		cookiePath = "/google";
-	}
-	
-	protected String getTokenFromCookieVal(String tokenCookieVal, HttpServletRequest request)
-	{
-		String userId = request.getParameter("userId");
-		
-		if (tokenCookieVal != null && userId != null)
-		{
-			String[] tokens = tokenCookieVal.split(SEPARATOR);
-			
-			for (int i = 0; i < tokens.length; i++)
-			{
-				if (tokens[i].startsWith(userId + ":"))
-				{
-					return tokens[i].substring(userId.length() + 1);
-				}
-			}
-		}
-
-		return tokenCookieVal;
-	}
-	
-	protected String getRefreshTokenCookie(String refreshToken, String tokenCookieVal, String accessToken) 
-	{
-		HttpURLConnection con = null;
-		String userId = null;
-		
-		try
-		{
-			URL obj = new URL("https://www.googleapis.com/oauth2/v2/userinfo?alt=json");
-			con = (HttpURLConnection) obj.openConnection();
-			con.setRequestProperty("Authorization", "Bearer " + accessToken);
-			con.setRequestProperty("User-Agent", "draw.io");
-			int status = con.getResponseCode();
-			
-			if (status >= 200 && status <= 299)
-			{
-				BufferedReader in = new BufferedReader(
-						new InputStreamReader(con.getInputStream()));
-				String inputLine;
-				StringBuffer strRes = new StringBuffer();
-				
-				while ((inputLine = in.readLine()) != null)
-				{
-					strRes.append(inputLine);
-				}
-				in.close();
-				
-				userId = new Gson().fromJson(strRes.toString(), JsonElement.class).getAsJsonObject().get("id").getAsString();
-			}
-		}
-		catch(Exception e)
-		{
-			e.printStackTrace();
-		}
-		
-		if (userId != null)
-		{
-			ArrayList<String> tokens = new ArrayList<>();
-			tokens.add(userId + ":" + refreshToken);
-			
-			if (tokenCookieVal != null)
-			{
-				String[] curTokens = tokenCookieVal.split(SEPARATOR);
-				
-				for (int i = 0; i < curTokens.length; i++)
-				{
-					if (!curTokens[i].startsWith(userId + ":"))
-					{
-						tokens.add(curTokens[i]);
-					}
-				}
-			}
-			
-			return String.join(SEPARATOR, tokens);
-		}
-		
-		return tokenCookieVal; //If we couldn't get the userId, we just return existing tokens such that we don't corrupt them
-	}
-	
-	protected void logout(String tokenCookieName, String tokenCookieVal, HttpServletRequest request, HttpServletResponse response)
+	/**
+	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
+	 */
+	public void doGet(HttpServletRequest request,
+			HttpServletResponse response) throws ServletException, IOException
 	{
-		String userId = request.getParameter("userId");
-		
-		if (tokenCookieVal != null && userId != null)
-		{
-			ArrayList<String> tokens = new ArrayList<>();
-			String[] curTokens = tokenCookieVal.split(SEPARATOR);
-			
-			for (int i = 0; i < curTokens.length; i++)
-			{
-				if (!curTokens[i].startsWith(userId + ":"))
-				{
-					tokens.add(curTokens[i]);
-				}
-			}
-			
-			if (tokens.size() > 0)
-            {
-                addCookie(tokenCookieName, String.join(SEPARATOR, tokens), TOKEN_COOKIE_AGE, response);
-            }
-            else
-            {
-                deleteCookie(tokenCookieName, response);
-            }
-		}
-	}
-	
-	protected String processAuthResponse(String authRes, boolean jsonResponse)
-	{
-		StringBuffer res = new StringBuffer();
-		
-		//In Office Add-in, we don't have access to opened window to attach a function to it, 
-		//	also with the redirect (since we had to open google auth in the same window) we lost Office Messaging.
-		//	This is due to using Google own file picker instead of creating our own picker 
-		//	(as we did with OneDrive since its picker only support popup windows which is not supported in Office)
-		//	This is why we load driveLoader.js which define onGDriveCallback and redirects automatically to the page including the picker
-		//	For other scenarios, we use another function name (onGoogleDriveCallback)
-		if (!jsonResponse)
-		{
-			res.append("<!DOCTYPE html><html><head>");
-			res.append("<script src=\"/connect/office365/js/driveLoader.js\" type=\"text/javascript\"></script>");
-			res.append("<script type=\"text/javascript\">");
-			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
-		}
-		
-		res.append(authRes);
-		
-		if (!jsonResponse)
-		{
-			res.append(";");
-			res.append("if (window.opener != null && window.opener.onGoogleDriveCallback != null)"); 
-			res.append("{");
-			res.append("	window.opener.onGoogleDriveCallback(authInfo, window);");
-			res.append("} else {");
-			res.append("	onGDriveCallback(authInfo);");
-			res.append("}");
-			res.append("})();</script>");
-			res.append("</head><body></body></html>");
-		}
-
-		return res.toString();
+		super.doGetAbst(request, response);
 	}
 }

+ 73 - 0
src/main/java/com/mxgraph/online/MSGraphAuth.java

@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2006-2019, JGraph Ltd
+ */
+package com.mxgraph.online;
+
+import java.io.IOException;
+
+@SuppressWarnings("serial")
+abstract public class MSGraphAuth extends AbsAuth
+{
+	public static String CLIENT_SECRET_FILE_PATH = "msgraph_client_secret";
+	public static String CLIENT_ID_FILE_PATH = "msgraph_client_id";
+	
+	private static Config CONFIG = null;
+	
+	protected Config getConfig()
+	{
+		if (CONFIG == null)
+		{
+			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
+					clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
+			
+			CONFIG = new Config(clientIds, clientSerets);
+			CONFIG.REDIRECT_PATH = "/microsoft";
+			CONFIG.AUTH_SERVICE_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
+		}
+		
+		return CONFIG;
+	}	
+
+	public MSGraphAuth() 
+	{
+		super();
+		cookiePath = "/microsoft";
+	}
+	
+	protected String processAuthResponse(String authRes, boolean jsonResponse)
+	{
+		StringBuffer res = new StringBuffer();
+		
+		//Call the opener callback function directly with the given json
+		if (!jsonResponse)
+		{
+			res.append("<!DOCTYPE html><html><head><script>");
+			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
+		}
+		
+		res.append(authRes);
+
+		if (!jsonResponse)
+		{
+			res.append(";");					
+			res.append("if (window.opener != null && window.opener.onOneDriveCallback != null)"); 
+			res.append("{");
+			res.append("	window.opener.onOneDriveCallback(authInfo, window);");
+			res.append("} else {");
+			res.append("	var head = document.getElementsByTagName('head')[0];");
+			res.append("	var script = document.createElement('script');");
+			res.append("	script.onload = function() ");
+			res.append("	{");
+			res.append("		var authInfoStr = JSON.stringify(authInfo);");
+			res.append("		localStorage.setItem('.oneDriveAuthInfo', '{}');"); //setting this storage item means we have a refresh token
+			res.append("		Office.onReady(function () { Office.context.ui.messageParent(authInfoStr, { targetOrigin: '*' });});"); //TODO Use specific domain (more secure)
+			res.append("	};");
+			res.append("	script.src = 'https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js';");
+			res.append("	head.appendChild(script);");
+			res.append("}");
+			res.append("})();</script></head><body><div>Automatic login interrupted. Please close and select OneDrive again.</div></body></html>");
+		}
+
+		return res.toString();
+	}
+}

+ 11 - 61
src/main/java/com/mxgraph/online/MSGraphAuthServlet.java

@@ -5,69 +5,19 @@ package com.mxgraph.online;
 
 import java.io.IOException;
 
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
 @SuppressWarnings("serial")
-public class MSGraphAuthServlet extends AbsAuthServlet
+public class MSGraphAuthServlet extends MSGraphAuth implements AuthServletComm
 {
-	public static String CLIENT_SECRET_FILE_PATH = "msgraph_client_secret";
-	public static String CLIENT_ID_FILE_PATH = "msgraph_client_id";
-	
-	private static Config CONFIG = null;
-	
-	protected Config getConfig()
-	{
-		if (CONFIG == null)
-		{
-			String clientSerets = SecretFacade.getSecret(CLIENT_SECRET_FILE_PATH, getServletContext()), 
-					clientIds = SecretFacade.getSecret(CLIENT_ID_FILE_PATH, getServletContext());
-			
-			CONFIG = new Config(clientIds, clientSerets);
-			CONFIG.REDIRECT_PATH = "/microsoft";
-			CONFIG.AUTH_SERVICE_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
-		}
-		
-		return CONFIG;
-	}	
-
-	public MSGraphAuthServlet() 
+	/**
+	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
+	 */
+	public void doGet(HttpServletRequest request,
+			HttpServletResponse response) throws ServletException, IOException
 	{
-		super();
-		cookiePath = "/microsoft";
-	}
-	
-	protected String processAuthResponse(String authRes, boolean jsonResponse)
-	{
-		StringBuffer res = new StringBuffer();
-		
-		//Call the opener callback function directly with the given json
-		if (!jsonResponse)
-		{
-			res.append("<!DOCTYPE html><html><head><script>");
-			res.append("(function() { var authInfo = ");  //The following is a json containing access_token
-		}
-		
-		res.append(authRes);
-
-		if (!jsonResponse)
-		{
-			res.append(";");					
-			res.append("if (window.opener != null && window.opener.onOneDriveCallback != null)"); 
-			res.append("{");
-			res.append("	window.opener.onOneDriveCallback(authInfo, window);");
-			res.append("} else {");
-			res.append("	var head = document.getElementsByTagName('head')[0];");
-			res.append("	var script = document.createElement('script');");
-			res.append("	script.onload = function() ");
-			res.append("	{");
-			res.append("		var authInfoStr = JSON.stringify(authInfo);");
-			res.append("		localStorage.setItem('.oneDriveAuthInfo', '{}');"); //setting this storage item means we have a refresh token
-			res.append("		Office.onReady(function () { Office.context.ui.messageParent(authInfoStr, { targetOrigin: '*' });});"); //TODO Use specific domain (more secure)
-			res.append("	};");
-			res.append("	script.src = 'https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js';");
-			res.append("	head.appendChild(script);");
-			res.append("}");
-			res.append("})();</script></head><body><div>Automatic login interrupted. Please close and select OneDrive again.</div></body></html>");
-		}
-
-		return res.toString();
+		super.doGetAbst(request, response);
 	}
 }

+ 28 - 8
src/main/webapp/electron.js

@@ -319,6 +319,8 @@ app.on('ready', e =>
 				'export all pages (for PDF format only)')
 			.option('-p, --page-index <pageIndex>',
 				'selects a specific page, if not specified and the format is an image, the first page is selected', parseInt)
+			.option('-l, --layers <comma separated layer indexes>',
+				'selects which layers to export (applies to all pages), if not specified, all layers are selected')
 			.option('-g, --page-range <from>..<to>',
 				'selects a page range (for PDF format only)', argsRange)
 			.option('-u, --uncompressed',
@@ -350,6 +352,11 @@ app.on('ready', e =>
     	
     	windowsRegistry.push(dummyWin);
     	
+		/*ipcMain.on('log', function(event, msg)
+		{
+			console.log(msg);
+		});*/
+	
     	try
     	{
 	    	//Prepare arguments and confirm it's valid
@@ -418,6 +425,11 @@ app.on('ready', e =>
 				uncompressed: options.uncompressed
 			};
 
+			if (options.layers)
+			{
+				expArgs.extras = JSON.stringify({layers: options.layers.split(',')});
+			}
+
 			var paths = program.args;
 			
 			// If a file is passed 
@@ -475,14 +487,9 @@ app.on('ready', e =>
 						{
 							var ext = path.extname(curFile);
 							
-							expArgs.xml = fs.readFileSync(curFile, ext === '.png' || ext === '.vsdx' ? null : 'utf-8');
+							let fileContent = fs.readFileSync(curFile, ext === '.png' || ext === '.vsdx' ? null : 'utf-8');
 							
-							if (ext === '.png')
-							{
-								expArgs.xml = Buffer.from(expArgs.xml).toString('base64');
-								startExport();
-							}
-							else if (ext === '.vsdx')
+							if (ext === '.vsdx')
 							{
 								dummyWin.loadURL(`file://${__dirname}/vsdxImporter.html`);
 								
@@ -490,7 +497,7 @@ app.on('ready', e =>
 
 								contents.on('did-finish-load', function()
 							    {
-									contents.send('import', expArgs.xml);
+									contents.send('import', fileContent);
 
 									ipcMain.once('import-success', function(evt, xml)
 						    	    {
@@ -507,6 +514,19 @@ app.on('ready', e =>
 							}
 							else
 							{
+								if (ext === '.csv')
+								{
+									expArgs.csv = fileContent;
+								}
+								else if (ext === '.png')
+								{
+									expArgs.xml = Buffer.from(fileContent).toString('base64');
+								}
+								else
+								{
+									expArgs.xml = fileContent;
+								}
+
 								startExport();
 							}
 							

File diff suppressed because it is too large
+ 1727 - 1726
src/main/webapp/js/app.min.js


+ 3 - 40
src/main/webapp/js/diagramly/App.js

@@ -1626,12 +1626,7 @@ App.prototype.init = function()
 		this.formatContainer.style.visibility = 'hidden';
 		this.hsplit.style.display = 'none';
 		this.sidebarContainer.style.display = 'none';
-
-		if (this.sidebarFooterContainer != null)
-		{
-			this.sidebarFooterContainer.style.display = 'none';
-		}
-
+		
 		// Sets the initial mode
 		if (urlParams['local'] == '1')
 		{
@@ -2670,7 +2665,8 @@ App.prototype.appIconClicked = function(evt)
 		{
 			if (file != null && file.stat != null && file.stat.path_display != null)
 			{
-				var url = 'https://www.dropbox.com/home/Apps/drawio' + file.stat.path_display;
+				
+				var url = 'https://www.dropbox.com/home/Apps' + this.dropbox.appPath + file.stat.path_display;
 				
 				if (!mxEvent.isShiftDown(evt))
 				{
@@ -3442,7 +3438,6 @@ App.prototype.start = function()
 					// Removes open URL parameter. Hash is also updated in Init to load client.
 					if (urlParams['open'] != null && window.history && window.history.replaceState)
 					{
-						
 						window.history.replaceState(null, null, window.location.pathname +
 							this.getSearch(['open', 'sketch']));
 						window.location.hash = urlParams['open'];
@@ -6971,38 +6966,6 @@ App.prototype.updateHeader = function()
 			mxEvent.consume(evt);
 		}));
 		
-		if (!Editor.enableSimpleTheme && Editor.currentTheme != 'atlas' && urlParams['embed'] != '1')
-		{
-			this.darkModeElement = this.toggleFormatElement.cloneNode(true);
-			this.darkModeElement.setAttribute('title', mxResources.get('theme'));
-			this.darkModeElement.style.right = right + 'px';
-			this.toolbarContainer.appendChild(this.darkModeElement);
-			right += 20;
-
-			var updateDarkModeElement = mxUtils.bind(this, function()
-			{
-				this.darkModeElement.style.backgroundImage = 'url(\'' +
-					((Editor.isDarkMode()) ? Editor.lightModeImage :
-					Editor.darkModeImage) + '\')';
-			});
-
-			this.addListener('darkModeChanged', updateDarkModeElement);
-			updateDarkModeElement();
-			
-			// Prevents focus
-			mxEvent.addListener(this.darkModeElement, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
-				mxUtils.bind(this, function(evt)
-			{
-				evt.preventDefault();
-			}));
-			
-			mxEvent.addListener(this.darkModeElement, 'click', mxUtils.bind(this, function(evt)
-			{
-				this.actions.get('toggleDarkMode').funct();
-				mxEvent.consume(evt);
-			}));
-		}
-		
 		// Some style changes in Atlas theme
 		if (Editor.currentTheme == 'atlas')
 		{

+ 2 - 2
src/main/webapp/js/diagramly/Devel.js

@@ -32,8 +32,8 @@ if (!mxIsElectron && location.protocol !== 'http:')
 			'; ';
 
 		var styleHashes = '\'sha256-pVoUz0B9cDvBP/6KP+5uOMqPh1c14hF0KFqSELqeyNQ=\' ' + // index.html
-			'\'sha256-ndJH9UpsZbDCTSHCvE9p8Q5lixP7f3EyTKTI9GZ6vJM=\' ' + // Minimal.js/Light
-			'\'sha256-rj5KFv1/gbRGAFQysCKmAk5Z81G5rB2NsoSWc9m3Yzg=\' ' + // Minimal.js/Dark
+			'\'sha256-FXrwb6dUnotyIZ0a3h9mB7VA8PjhDqvFb14H6otvyeQ=\' ' + // Minimal.js/Light
+			'\'sha256-JZl2kcwr5gO3OTS2Sc0A/1G9NEligkzmy7Fm80eCe6g=\' ' + // Minimal.js/Dark
 			'\'sha256-7kY8ozVqKLIIBwZ24dhdmZkM26PsOlZmEi72RhmZKoM=\' ' + // mxTooltipHandler.js
 			'\'sha256-kuk5TvxZ/Kwuobo4g6uasb1xRQwr1+nfa1A3YGePO7U=\' ' + // MathJax
 			'\'sha256-ByOXYIXIkfNC3flUR/HoxR4Ak0pjOEF1q8XmtuIa6po=\' ' + // purify.min.js

+ 15 - 4
src/main/webapp/js/diagramly/Dialogs.js

@@ -8460,7 +8460,8 @@ var MoreShapesDialog = function(editorUi, expanded, entries)
 			}
 
 			// Redirects scratchpad and search entries
-			if (urlParams['sketch'] == '1' && editorUi.isSettingsEnabled())
+			if ((Editor.currentTheme == 'sketch' || Editor.currentTheme == 'simple') &&
+				editorUi.isSettingsEnabled())
 			{
 				var idx = mxUtils.indexOf(libs, '.scratchpad');
 
@@ -9542,6 +9543,16 @@ var LibraryDialog = function(editorUi, name, library, initialImages, file, mode)
 
 	header.appendChild(nameInput);
 
+	if (Editor.enableUncompressedLibraries)
+	{
+		nameInput.style.width = '420px';
+		var compressedInput = document.createElement('input');
+		compressedInput.setAttribute('type', 'checkbox');
+		compressedInput.style.marginRight = '10px';
+		header.appendChild(compressedInput);
+		mxUtils.write(header, mxResources.get('compressed'));
+	}
+
 	var div = document.createElement('div');
 	div.style.borderWidth = '1px 0px 1px 0px';
 	div.style.borderColor = '#d3d3d3';
@@ -9681,7 +9692,8 @@ var LibraryDialog = function(editorUi, name, library, initialImages, file, mode)
 					}
 					else if (img != null)
 					{
-						var cells = editorUi.stringToCells(Graph.decompress(img.xml));
+						var cells = editorUi.stringToCells((img.xml.charAt(0) == '<') ?
+							img.xml : Graph.decompress(img.xml));
 						
 						if (cells.length > 0)
 						{
@@ -10064,7 +10076,6 @@ var LibraryDialog = function(editorUi, name, library, initialImages, file, mode)
 				}
 				else
 				{
-
 					editorUi.spinner.stop();
 					editorUi.showError(mxResources.get('error'), mxResources.get('notInOffline'));
 				}
@@ -10132,7 +10143,7 @@ var LibraryDialog = function(editorUi, name, library, initialImages, file, mode)
 	var btns = document.createElement('div');
 	btns.style.textAlign = 'right';
 	btns.style.marginTop = '20px';
-	
+
 	var cancelBtn = mxUtils.button(mxResources.get('cancel'), function()
 	{
 		editorUi.hideDialog(true);

+ 2 - 2
src/main/webapp/js/diagramly/DropboxClient.js

@@ -995,9 +995,9 @@ DropboxClient.prototype.createFile = function(file, success, error)
 				}
 				
 				this.insertFile(file.name, data, mxUtils.bind(this, function(newFile)
-			    	{
+			    {
 					success(file.name, newFile);
-			    	}), error);
+			    }), error);
 			}), mxUtils.bind(this, function()
 			{
 	    			this.ui.spinner.stop();

+ 10 - 3
src/main/webapp/js/diagramly/Editor.js

@@ -131,6 +131,11 @@
 	 */
 	Editor.enableCustomLibraries = true;
 	
+	/**
+	 * Not yet implemented. Reading uncompressed supported.
+	 */
+	Editor.enableUncompressedLibraries = false;
+	
 	/**
 	 * Specifies if custom properties should be enabled.
 	 */
@@ -4545,8 +4550,9 @@
 		{
 			var sstate = this.editorUi.getSelectionState();
 
-			if (this.defaultColorSchemes != null && sstate.style.shape != 'image' &&
-				!sstate.containsLabel && sstate.cells.length > 0)
+			if (this.defaultColorSchemes != null && this.defaultColorSchemes.length > 0 &&
+				sstate.style.shape != 'image' && !sstate.containsLabel &&
+				sstate.cells.length > 0)
 			{
 				this.container.appendChild(this.addStyles(this.createPanel()));
 			}
@@ -5426,7 +5432,8 @@
 
 			if (this.format.currentScheme == null)
 			{
-				setScheme(Editor.isDarkMode() ? 1 : (urlParams['sketch'] == '1' ? 5 : 0));
+				setScheme(Math.min(dots.length - 1, Editor.isDarkMode()
+					? 1 : (urlParams['sketch'] == '1' ? 5 : 0)));
 			}
 			else
 			{

+ 47 - 118
src/main/webapp/js/diagramly/EditorUi.js

@@ -316,11 +316,6 @@
 	 */
 	EditorUi.prototype.timeout = Editor.prototype.timeout;
 
-	/**
-	 * Allows for two buttons in the sidebar footer.
-	 */
-	EditorUi.prototype.sidebarFooterHeight = 38;
-
 	/**
 	 * Specifies the default custom shape style.
 	 */
@@ -993,8 +988,7 @@
 				var oldPages = (this.pages != null) ? this.pages.slice() : null;
 				var nodes = node.getElementsByTagName('diagram');
 
-				if (urlParams['pages'] != '0' || nodes.length > 1 ||
-					(nodes.length == 1 && nodes[0].hasAttribute('name')))
+				if (nodes.length > 1 || (nodes.length == 1 && nodes[0].hasAttribute('name')))
 				{
 					this.fileNode = node;
 					this.pages = (this.pages != null) ? this.pages : [];
@@ -1016,7 +1010,7 @@
 				else
 				{
 					// Creates tabbed file structure if enforced by URL
-					if (urlParams['pages'] != '0' && this.fileNode == null)
+					if (this.fileNode == null)
 					{
 						this.fileNode = node.ownerDocument.createElement('mxfile');
 						this.currentPage = new DiagramPage(node.ownerDocument.createElement('diagram'));
@@ -1031,9 +1025,11 @@
 					if (this.currentPage != null)
 					{
 						this.currentPage.root = this.editor.graph.model.root;
+						graph.model.execute(new ChangePage(this, this.currentPage, this.currentPage, 0));
 					}
 				}
 				
+				// Removes old pages
 				if (oldPages != null)
 				{
 					for (var i = 0; i < oldPages.length; i++)
@@ -1820,8 +1816,7 @@
 			{
 				var nodes = node.getElementsByTagName('diagram');
 	
-				if (urlParams['pages'] != '0' || nodes.length > 1 ||
-					(nodes.length == 1 && nodes[0].hasAttribute('name')))
+				if (nodes.length > 1 || (nodes.length == 1 && nodes[0].hasAttribute('name')))
 				{
 					var selectedPage = null;
 					this.fileNode = node;
@@ -1860,7 +1855,7 @@
 			}
 			
 			// Creates tabbed file structure if enforced by URL
-			if (urlParams['pages'] != '0' && this.fileNode == null && node != null)
+			if (this.fileNode == null && node != null)
 			{
 				this.fileNode = node.ownerDocument.createElement('mxfile');
 				this.currentPage = new DiagramPage(node.ownerDocument.createElement('diagram'));
@@ -3146,7 +3141,7 @@
 	 */
 	EditorUi.prototype.repositionLibrary = function(nextChild) 
 	{
-	    var c = this.sidebar.container;
+	    var c = this.sidebar.getEntryContainer();
 	    
 	    if (nextChild == null)
 	    {
@@ -3767,7 +3762,8 @@
 			}
 			else if (img.xml != null)
 			{
-				var cells = this.stringToCells(Graph.decompress(img.xml));
+				var cells = this.stringToCells((img.xml.charAt(0) == '<') ?
+					img.xml : Graph.decompress(img.xml));
 				
 				if (cells.length > 0)
 				{
@@ -3897,54 +3893,6 @@
 		return format;
 	};
 	
-	/**
-	 * Hook for sidebar footer container.
-	 */
-	EditorUi.prototype.createSidebarFooterContainer = function()
-	{
-		if (urlParams['embedInline'] != '1')
-		{
-			var div =  this.createDiv('geSidebarContainer geSidebarFooter');
-			div.style.position = 'absolute';
-			div.style.overflow = 'hidden';
-			
-			var elt2 = document.createElement('a');
-			elt2.className = 'geTitle';
-			elt2.style.color = '#DF6C0C';
-			elt2.style.fontWeight = 'bold';
-			elt2.style.height = '100%';
-			elt2.style.paddingTop = '9px';
-			elt2.innerHTML = '<span>+</span>';
-			
-			var span = elt2.getElementsByTagName('span')[0];
-			span.style.fontSize = '18px';
-			span.style.marginRight = '5px';
-
-			mxUtils.write(elt2, mxResources.get('moreShapes') + '...');
-
-			// Prevents focus
-			mxEvent.addListener(elt2, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
-				mxUtils.bind(this, function(evt)
-			{
-				evt.preventDefault();
-			}));
-			
-			mxEvent.addListener(elt2, 'click', mxUtils.bind(this, function(evt)
-			{
-				this.actions.get('shapes').funct();
-				mxEvent.consume(evt);
-			}));
-			
-			div.appendChild(elt2);
-			
-			return div;
-		}
-		else
-		{
-			return null;
-		}
-	};
-	
 	/**
 	 * Translates this point by the given vector.
 	 * 
@@ -9742,8 +9690,21 @@
 					}
 			
 					this.addMenuItems(menu, ['-', 'cut', 'copy', 'copyAsImage',
-						'duplicate', 'lockUnlock'], null, evt);
-					
+						'duplicate', '-'], null, evt);
+
+					if (!this.isShowCellEditItems())
+					{
+						var item = this.addMenuItem(menu, 'delete');
+
+						if (item != null && item.firstChild != null &&
+							item.firstChild.nextSibling != null)
+						{
+							item.firstChild.nextSibling.style.color = 'red';
+						}
+					}
+
+					this.addMenuItems(menu, ['lockUnlock'], null, evt);
+
 					// Shows crop option for images
 					if (!this.isShowCellEditItems() && graph.getSelectionCount() == 1 &&
 						graph.isCellEditable(cell) && graph.getModel().isVertex(cell))
@@ -10322,6 +10283,11 @@
 		this.addListener('sketchModeChanged', themeChangeListener);
 		this.addListener('currentThemeChanged', mxUtils.bind(this, function()
 		{
+			if (this.sidebar != null)
+			{
+				this.sidebar.updateEntries();
+			}
+
 			this.updateButtonContainer();
 			this.refresh();
 		}));
@@ -10912,11 +10878,6 @@
 				{
 					if (isSimple(curr) && this.isDefaultTheme(value))
 					{
-						if (this.sidebarFooterContainer != null)
-						{
-							this.sidebarFooterContainer.style.display = 'block';
-						}
-						
 						this.menubarContainer.style.display = 'block';
 						this.toolbarContainer.style.display = 'block';
 						this.tabContainer.style.display = 'block';
@@ -10927,11 +10888,6 @@
 					}
 					else if (this.isDefaultTheme(curr) && isSimple(value))
 					{
-						if (this.sidebarFooterContainer != null)
-						{
-							this.sidebarFooterContainer.style.display = 'none';
-						}
-						
 						this.menubarContainer.style.display = 'none';
 						this.toolbarContainer.style.display = 'none';
 						this.tabContainer.style.display = 'none';
@@ -11114,7 +11070,7 @@
 			this.createShapesWindow();
 			this.sidebarContainer.style.left = '0px';
 			this.sidebarContainer.style.top = '0px';
-			this.sidebarContainer.style.bottom = '63px';
+			this.sidebarContainer.style.bottom = '32px';
 			this.sidebarContainer.style.width = '100%';
 		}
 
@@ -12088,30 +12044,13 @@
 	 */
 	EditorUi.prototype.createShapesPanel = function(container)
 	{
-		var div = document.createElement('div');
-		div.style.cssText = 'position:absolute;left:0;right:0;border-top:1px solid lightgray;' +
-			'height:24px;bottom:31px;text-align:center;cursor:pointer;padding:6px 0 0 0;';
-		div.className = 'geTitle';
-		var span = document.createElement('span');
-		span.style.fontSize = '18px';
-		span.style.marginRight = '5px';
-		span.innerHTML = '+';
-		div.appendChild(span);
-		mxUtils.write(div, mxResources.get('moreShapes'));
-		container.appendChild(div);
-		
-		mxEvent.addListener(div, 'click', mxUtils.bind(this, function()
-		{
-			this.actions.get('shapes').funct();
-		}));
-
 		var addMenu = mxUtils.bind(this, function(id, label)
 		{
 			var elt = this.createMenu(id, null, 'geTitle');
 
-			elt.style.cssText = 'position:absolute;border-top:1px solid lightgray;' +
-				'width:50%;height:24px;bottom:0px;text-align:center;cursor:pointer;' +
-				'padding:6px 0 0 0;cusor:pointer;';
+			elt.style.cssText = 'position:absolute;border-width:1px;cusor:pointer;border-style:none;' +
+				'height:24px;bottom:0px;text-align:center;padding:6px 6px 0 6px;border-top-style:solid;' +
+				'width:50%;height:32px;box-sizing:border-box;';
 			container.appendChild(elt);
 
 			return elt;
@@ -12120,11 +12059,11 @@
 		if (Editor.enableCustomLibraries && (urlParams['embed'] != '1' || urlParams['libraries'] == '1'))
 		{
 			// Defined in native apps together with openLibrary
-			if (this.actions.get('newLibrary') != null)
+			if (true)//this.actions.get('newLibrary') != null)
 			{
 				var div = document.createElement('div');
-				div.style.cssText = 'position:absolute;left:0px;width:50%;border-top:1px solid lightgray;' +
-					'height:30px;bottom:0px;text-align:center;cursor:pointer;padding:0px;';
+				div.style.cssText = 'position:absolute;left:0px;width:50%;border-width:1px;border-style:none;height:30px;' +
+					'bottom:0px;text-align:center;cursor:pointer;padding:0px;border-bottom:none;border-top-style:solid;';
 				div.className = 'geTitle';
 				var span = document.createElement('span');
 				span.style.cssText = 'position:relative;top:6px;';
@@ -12132,33 +12071,25 @@
 				div.appendChild(span);
 				container.appendChild(div);
 				
-				mxEvent.addListener(div, 'click', this.actions.get('newLibrary').funct);
+				// mxEvent.addListener(div, 'click', this.actions.get('newLibrary').funct);
 				
 				var div = div.cloneNode(false);
 				div.style.left = '50%';
-				div.style.borderLeft = '1px solid lightgray';
+				div.style.borderLeftStyle = 'solid';
 				var span = span.cloneNode(false);
 				mxUtils.write(span, mxResources.get('openLibrary'));
 				div.appendChild(span);
 				container.appendChild(div);
 				
-				mxEvent.addListener(div, 'click', this.actions.get('openLibrary').funct);
+				// mxEvent.addListener(div, 'click', this.actions.get('openLibrary').funct);
 			}
 			else
 			{
 				var elt = addMenu('newLibrary', mxResources.get('newLibrary'));
-				elt.style.boxSizing = 'border-box';
-				elt.style.paddingRight = '6px';
-				elt.style.paddingLeft = '6px';
-				elt.style.height = '32px';
 				elt.style.left = '0';
 				
 				var elt = addMenu('openLibraryFrom', mxResources.get('openLibraryFrom'));
-				elt.style.borderLeft = '1px solid lightgray';
-				elt.style.boxSizing = 'border-box';
-				elt.style.paddingRight = '6px';
-				elt.style.paddingLeft = '6px';
-				elt.style.height = '32px';
+				elt.style.borderLeftStyle = 'solid';
 				elt.style.left = '50%';
 			}
 		}
@@ -12256,7 +12187,7 @@
 			'html body .geStatus { overflow: hidden; text-overflow: ellipsis; }' +
 			'html body .geStatus > *:not([class]) { vertical-align:top; }' +
 			'html > body > div > a.geItem { background-color: #ffffff; color: #707070; border-top: 1px solid lightgray; border-left: 1px solid lightgray; }' +
-			'html body .geMenubarContainer { border-bottom:1px solid lightgray;background-color:#ffffff; }' +
+			'html body .geMenubarContainer { border-bottom:1px solid lightgray;background-color:#ffffff;display:inline-flex;align-items;center; }' +
 			'html body .mxWindow button.geBtn { font-size:12px !important; margin-left: 0; }' +
 			'html body .geSidebarContainer *:not(svg *) { font-size:9pt; }' +
 			'html body table.mxWindow td.mxWindowPane div.mxWindowPane *:not(svg *) { font-size:9pt; }' +
@@ -12328,9 +12259,10 @@
 	/**
 	 * Returns the current state of the dark mode.
 	 */
-	EditorUi.prototype.isAutoDarkMode = function()
+	EditorUi.prototype.isAutoDarkMode = function(ignoreUrl)
 	{
-		return urlParams['dark'] == 'auto' || (this.isSettingsEnabled() &&
+		return (!ignoreUrl && urlParams['dark'] == 'auto') ||
+			(this.isSettingsEnabled() &&
 			mxSettings.settings.darkMode == 'auto');
 	};
 	
@@ -14007,11 +13939,6 @@
 			Editor.currentTheme != 'sketch' && Editor.currentTheme != 'min')
 			? '' : 'none';
 		this.editor.graph.setEnabled(enabled);
-
-		if (this.sidebarFooterContainer != null)
-		{
-			this.sidebarFooterContainer.style.display = (enabled) ? '' : 'none';
-		}
 		
 		if (this.tabContainer != null)
 		{
@@ -16361,7 +16288,9 @@
 		this.actions.get('zoomOut').setEnabled(active);
 		this.actions.get('smartFit').setEnabled(active);
 		this.actions.get('resetView').setEnabled(active);
-		this.actions.get('toggleDarkMode').setEnabled(Editor.currentTheme != 'atlas');
+		this.actions.get('darkMode').setEnabled(Editor.currentTheme != 'atlas');
+		this.actions.get('lightMode').setEnabled(Editor.currentTheme != 'atlas');
+		this.actions.get('autoMode').setEnabled(Editor.currentTheme != 'atlas');
 		
 		// Updates undo history states
 		this.actions.get('undo').setEnabled(this.canUndo() && editable);

+ 40 - 20
src/main/webapp/js/diagramly/Menus.js

@@ -306,13 +306,29 @@
 				document.fullscreenElement != null;
 		});
 
-        var toggleDarkModeAction = editorUi.actions.put('toggleDarkMode', new Action(mxResources.get('dark'), function(e)
+        var lightModeAction = editorUi.actions.put('lightMode', new Action(mxResources.get('light'), function(e)
         {
-			editorUi.setAndPersistDarkMode(!Editor.isDarkMode());
+			editorUi.setAndPersistDarkMode(false);
         }));
 
-		toggleDarkModeAction.setToggleAction(true);
-		toggleDarkModeAction.setSelectedCallback(function() { return Editor.isDarkMode(); });
+		lightModeAction.setToggleAction(true);
+		lightModeAction.setSelectedCallback(function() { return !editorUi.isAutoDarkMode(true) && !Editor.isDarkMode(); });
+		
+        var darkModeAction = editorUi.actions.put('darkMode', new Action(mxResources.get('dark'), function(e)
+        {
+			editorUi.setAndPersistDarkMode(true);
+        }));
+
+		darkModeAction.setToggleAction(true);
+		darkModeAction.setSelectedCallback(function() { return !editorUi.isAutoDarkMode(true) && Editor.isDarkMode(); });
+		
+        var autoModeAction = editorUi.actions.put('autoMode', new Action(mxResources.get('automatic'), function(e)
+        {
+			editorUi.setAndPersistDarkMode('auto');
+        }));
+
+		autoModeAction.setToggleAction(true);
+		autoModeAction.setSelectedCallback(function() { return editorUi.isAutoDarkMode(true); });
 		
         var toggleSimpleModeAction = editorUi.actions.put('toggleSimpleMode', new Action(mxResources.get('simple'), function(e)
         {
@@ -1253,7 +1269,7 @@
 					editorUi.getServiceName() != 'atlassian' &&
 					urlParams['embed'] != '1')
 				{
-					var themeMenu = this.get('appearance');
+					var themeMenu = this.get('uiSwitches');
 					
 					if (themeMenu != null)
 					{
@@ -2897,33 +2913,26 @@
 			}
 		}))).isEnabled = isGraphEnabled;
 
-		this.put('appearance', new Menu(mxUtils.bind(this, function(menu, parent)
+		this.put('uiSwitches', new Menu(mxUtils.bind(this, function(menu, parent)
 		{
+			this.addMenuItems(menu, ['toggleSimpleMode'], parent);
+
 			if (Editor.isDarkMode() || (!mxClient.IS_IE && !mxClient.IS_IE11))
 			{
-				this.addMenuItems(menu, ['toggleDarkMode'], parent);
+				this.addMenuItems(menu, ['-', 'lightMode', 'darkMode', 'autoMode'], parent);
 			}
 
-			this.addMenuItems(menu, ['toggleSimpleMode'], parent);
+		})));
 
-			if (urlParams['test-prefs'] == '1')
-			{
-				this.addMenuItems(menu, ['-', 'preferences'], parent);
-			}
+		this.put('appearance', new Menu(mxUtils.bind(this, function(menu, parent)
+		{
+			this.addMenuItems(menu, ['lightMode', 'darkMode', 'autoMode'], parent);
 		})));
 
 		this.put('theme', new Menu(mxUtils.bind(this, function(menu, parent)
 		{
 			var theme = (urlParams['sketch'] == '1') ? 'sketch' : mxSettings.getUi();
 			
-			if (urlParams['embedInline'] != '1' && Editor.isDarkMode() ||
-				(!mxClient.IS_IE && !mxClient.IS_IE11))
-			{
-				this.addMenuItems(menu, ['toggleDarkMode'], parent);
-			}
-			
-			menu.addSeparator(parent);
-			
 			var item = menu.addItem(mxResources.get('automatic'), null, function()
 			{
 				editorUi.setCurrentTheme('');
@@ -4079,6 +4088,12 @@
 				if (urlParams['embed'] != '1' && urlParams['extAuth'] != '1' && editorUi.mode != App.MODE_ATLAS)
 				{
 					editorUi.menus.addSubmenu('theme', menu, parent);
+					
+					if (urlParams['embedInline'] != '1' && Editor.isDarkMode() ||
+						(!mxClient.IS_IE && !mxClient.IS_IE11))
+					{
+						editorUi.menus.addSubmenu('appearance', menu, parent);
+					}
 				}
 				
 				editorUi.menus.addSubmenu('units', menu, parent);
@@ -4150,6 +4165,11 @@
 				if (urlParams['embed'] != '1')
 				{
 					this.addSubmenu('theme', menu, parent);
+
+					if (Editor.isDarkMode() || (!mxClient.IS_IE && !mxClient.IS_IE11))
+					{
+						editorUi.menus.addSubmenu('appearance', menu, parent);
+					}
 				}
 
 				menu.addSeparator(parent);

+ 43 - 36
src/main/webapp/js/diagramly/sidebar/Sidebar.js

@@ -492,22 +492,45 @@
 			}
 		}
 	};
-
+	
 	/**
 	 * Overrides the sidebar init.
 	 */
-	Sidebar.prototype.init = function()
-	{
-		// Defines all entries for the sidebar. This is used in the MoreShapes dialog. Create screenshots using the savesidebar URL parameter and
-		// http://www.alderg.com/merge.html for creating a vertical stack of PNG images if multiple sidebars are part of an entry.
+	 Sidebar.prototype.init = function()
+	 {
+		this.updateEntries();
+
+		// Uses search.xml index file instead (faster load times)
+		this.addStencilsToIndex = false;
+		
+		// Contains additional tags for shapes
+		this.shapetags = {};
 
+		// Adds tags from compressed text file for improved searches
+		if (this.tagIndex != null)
+		{
+			this.addTagIndex(Graph.decompress(this.tagIndex));
+			this.tagIndex = null;	
+		}
+		
+		this.initPalettes();
+	 }
+	 
+	/**
+	 * Defines all entries for the sidebar. This is used in the MoreShapes dialog. Create screenshots using the savesidebar URL parameter and
+	 * http://www.alderg.com/merge.html for creating a vertical stack of PNG images if multiple sidebars are part of an entry.
+	 */
+	Sidebar.prototype.updateEntries = function()
+	{
 		var stdEntries = [{title: mxResources.get('general'), id: 'general', image: IMAGE_PATH + '/sidebar-general.png'},
 			{title: mxResources.get('basic'), id: 'basic', image: IMAGE_PATH + '/sidebar-basic.png'},
 			{title: mxResources.get('arrows'), id: 'arrows2', image: IMAGE_PATH + '/sidebar-arrows2.png'},
 			{title: mxResources.get('clipart'), id: 'clipart', image: IMAGE_PATH + '/sidebar-clipart.png'},
 			{title: mxResources.get('flowchart'), id: 'flowchart', image: IMAGE_PATH + '/sidebar-flowchart.png'}];
 		
-		if (urlParams['sketch'] == '1')
+		if (Editor.currentTheme == 'sketch' ||
+			Editor.currentTheme == 'simple' ||
+			Editor.currentTheme == 'min')
 		{
 			stdEntries = [{title: mxResources.get('searchShapes'), id: 'search'},
 				{title: mxResources.get('scratchpad'), id: '.scratchpad'}].
@@ -570,22 +593,8 @@
 								{title: 'Web Icons', id: 'webicons', image: IMAGE_PATH + '/sidebar-webIcons.png'},
 								{title: mxResources.get('signs'), id: 'signs', image: IMAGE_PATH + '/sidebar-signs.png'}]}];
 
-		// Uses search.xml index file instead (faster load times)
-		this.addStencilsToIndex = false;
-		
-		// Contains additional tags for shapes
-		this.shapetags = {};
+	};
 
-		// Adds tags from compressed text file for improved searches
-		if (this.tagIndex != null)
-		{
-			this.addTagIndex(Graph.decompress(this.tagIndex));
-			this.tagIndex = null;	
-		}
-		
-		this.initPalettes();
-	}
-	
 	/**
 	 * Overridden to add image export via servlet
 	 */
@@ -1429,28 +1438,26 @@
 		{
 			if (mxUtils.isAncestorNode(this.editorUi.sketchPickerMenuElt, elt))
 			{
-				var off = mxUtils.getOffset(this.editorUi.sketchPickerMenuElt);
-				
-				off.x += this.editorUi.sketchPickerMenuElt.offsetWidth + 4;
-				off.y += elt.offsetTop - bounds.height / 2 + 16;
+				var off = mxUtils.getOffset(elt);
 
-				return off;
+				off.x = elt.parentNode.offsetLeft + elt.parentNode.offsetWidth + 2;
+				off.y += (elt.offsetHeight - bounds.height) / 2;
+				
+				return new mxPoint(Math.max(0, off.x), Math.max(0, off.y));
 			}
-			else
+			else if (this.editorUi.sidebarWindow != null)
 			{
-				var result = sidebarGetTooltipOffset.apply(this, arguments);
 				var off = mxUtils.getOffset(this.editorUi.sidebarWindow.window.div);
+
+				off.x += this.editorUi.sidebarWindow.window.div.offsetWidth + 2;
+				off.y += elt.offsetTop - elt.offsetParent.scrollTop +
+					(elt.offsetHeight - bounds.height) / 2;
 				
-				result.x += off.x - 16;
-				result.y += off.y;
-				
-				return result;
+				return new mxPoint(Math.max(0, off.x), Math.max(0, off.y));
 			}
 		}
-		else
-		{
-			return sidebarGetTooltipOffset.apply(this, arguments);
-		}
+		
+		return sidebarGetTooltipOffset.apply(this, arguments);
 	};
     
 	/**

+ 54 - 0
src/main/webapp/js/export.js

@@ -8,6 +8,60 @@ Editor.initMath((remoteMath? 'https://app.diagrams.net/' : '') + 'math/es5/start
 
 function render(data)
 {
+	if (data.csv != null)
+	{
+		// CSV loads orgChart asynchronously and needs mxscript
+		window.mxscript = function (src, onLoad, id)
+		{
+			var s = document.createElement('script');
+			s.setAttribute('type', 'text/javascript');
+			s.setAttribute('defer', 'true');
+			s.setAttribute('src', src);
+
+			if (id != null)
+			{
+				s.setAttribute('id', id);
+			}
+			
+			if (onLoad != null)
+			{
+				var r = false;
+			
+				s.onload = s.onreadystatechange = function()
+				{
+					if (!r && (!this.readyState || this.readyState == 'complete'))
+					{
+						r = true;
+						onLoad();
+					}
+				};
+			}
+			
+			var t = document.getElementsByTagName('script')[0];
+			
+			if (t != null)
+			{
+				t.parentNode.insertBefore(s, t);
+			}
+		};
+
+		//Adjust some functions such that it can be instanciated without UI
+		EditorUi.prototype.createUi = function(){};
+		EditorUi.prototype.addTrees = function(){};
+		EditorUi.prototype.updateActionStates = function(){};
+		EditorUi.prototype.onBeforeUnload = function(){}; //Prevent unload message
+		var editorUi = new EditorUi();
+		
+		editorUi.importCsv(data.csv, function()
+		{
+			data.xml = mxUtils.getXml(editorUi.editor.getGraphXml());
+			delete data.csv;
+			render(data);
+		});
+
+		return;
+	}
+
 	var autoScale = false;
 	
 	if (data.scale == 'auto')

+ 17 - 40
src/main/webapp/js/grapheditor/EditorUi.js

@@ -373,6 +373,20 @@ EditorUi = function(editor, container, lightbox)
 				// Mouse down is needed for drag and drop
 				this.tabContainer.onselectstart = textEditing;
 			}
+
+			if (mxClient.IS_IOS)
+			{
+				mxUtils.setPrefixedStyle(this.menubarContainer.style, 'userSelect', 'none');
+				mxUtils.setPrefixedStyle(this.diagramContainer.style, 'userSelect', 'none');
+				mxUtils.setPrefixedStyle(this.sidebarContainer.style, 'userSelect', 'none');
+				mxUtils.setPrefixedStyle(this.formatContainer.style, 'userSelect', 'none');
+				mxUtils.setPrefixedStyle(this.footerContainer.style, 'userSelect', 'none');
+
+				if (this.tabContainer != null)
+				{
+					mxUtils.setPrefixedStyle(this.tabContainer.style, 'userSelect', 'none');
+				}
+			}
 		}
 		
 		// And uses built-in context menu while editing
@@ -1127,11 +1141,6 @@ EditorUi.prototype.toolbarHeight = 38;
  */
 EditorUi.prototype.footerHeight = 28;
 
-/**
- * Specifies the height of the optional sidebarFooterContainer. Default is 34.
- */
-EditorUi.prototype.sidebarFooterHeight = 34;
-
 /**
  * Specifies the position of the horizontal split bar. Default is 240 or 118 for
  * screen widths <= 640px.
@@ -1635,7 +1644,7 @@ EditorUi.prototype.installShapePicker = function()
 				{
 					if (cell != null)
 					{
-						graph.connectVertex(temp, dir, graph.defaultEdgeLength, mouseEvent, true, true, function(x, y, execute)
+						graph.connectVertex(temp, dir, graph.defaultEdgeLength, mouseEvent, true, false, function(x, y, execute)
 						{
 							execute(cell);
 								
@@ -4387,17 +4396,6 @@ EditorUi.prototype.refresh = function(sizeDidChange)
 		tmp += 1;
 	}
 	
-	var sidebarFooterHeight = 0;
-	
-	if (this.sidebarFooterContainer != null)
-	{
-		var bottom = this.footerHeight + off;
-		sidebarFooterHeight = Math.max(0, Math.min(h - tmp - bottom, this.sidebarFooterHeight));
-		this.sidebarFooterContainer.style.width = effHsplitPosition + 'px';
-		this.sidebarFooterContainer.style.height = sidebarFooterHeight + 'px';
-		this.sidebarFooterContainer.style.bottom = bottom + 'px';
-	}
-	
 	var fw = (this.format != null) ? this.formatWidth : 0;
 	this.sidebarContainer.style.top = tmp + 'px';
 	this.sidebarContainer.style.width = effHsplitPosition + 'px';
@@ -4432,7 +4430,7 @@ EditorUi.prototype.refresh = function(sizeDidChange)
 		th = this.tabContainer.clientHeight;
 	}
 	
-	this.sidebarContainer.style.bottom = (this.footerHeight + sidebarFooterHeight + off) + 'px';
+	this.sidebarContainer.style.bottom = (this.footerHeight + off) + 'px';
 	this.formatContainer.style.bottom = (this.footerHeight + off) + 'px';
 
 	this.diagramContainer.style.left =  (contLeft + diagContOffset.x) + 'px';
@@ -4466,7 +4464,6 @@ EditorUi.prototype.createDivs = function()
 	this.diagramContainer = this.createDiv('geDiagramContainer');
 	this.footerContainer = this.createDiv('geFooterContainer');
 	this.hsplit = this.createDiv('geHsplit');
-	this.hsplit.setAttribute('title', mxResources.get('collapseExpand'));
 
 	// Sets static style for containers
 	this.menubarContainer.style.top = '0px';
@@ -4483,12 +4480,6 @@ EditorUi.prototype.createDivs = function()
 	this.footerContainer.style.bottom = '0px';
 	this.footerContainer.style.zIndex = mxPopupMenu.prototype.zIndex - 3;
 	this.hsplit.style.width = this.splitSize + 'px';
-	this.sidebarFooterContainer = this.createSidebarFooterContainer();
-	
-	if (this.sidebarFooterContainer != null)
-	{
-		this.sidebarFooterContainer.style.left = '0px';
-	}
 	
 	if (!this.editor.chromeless)
 	{
@@ -4517,14 +4508,6 @@ EditorUi.prototype.createSidebarContainer = function()
 	return div;
 };
 
-/**
- * Hook for sidebar footer container. This implementation returns null.
- */
-EditorUi.prototype.createSidebarFooterContainer = function()
-{
-	return null;
-};
-
 /**
  * Creates the required containers.
  */
@@ -4581,11 +4564,6 @@ EditorUi.prototype.createUi = function()
 		this.container.appendChild(this.footerContainer);
 	}
 
-	if (this.sidebar != null && this.sidebarFooterContainer != null)
-	{
-		this.container.appendChild(this.sidebarFooterContainer);		
-	}
-
 	this.container.appendChild(this.diagramContainer);
 
 	if (this.container != null && this.tabContainer != null)
@@ -6165,8 +6143,7 @@ EditorUi.prototype.destroy = function()
 	
 	var c = [this.menubarContainer, this.toolbarContainer, this.sidebarContainer,
 	         this.formatContainer, this.diagramContainer, this.footerContainer,
-	         this.chromelessToolbar, this.hsplit, this.sidebarFooterContainer,
-	         this.layersDialog];
+	         this.chromelessToolbar, this.hsplit, this.layersDialog];
 	
 	for (var i = 0; i < c.length; i++)
 	{

+ 10 - 7
src/main/webapp/js/grapheditor/Format.js

@@ -402,10 +402,10 @@ Format.prototype.immediateRefresh = function()
 		arrangePanel.style.display = 'none';
 		this.panels.push(new ArrangePanel(this, ui, arrangePanel));
 		this.container.appendChild(arrangePanel);
-		
+
 		if (ss.cells.length > 0)
 		{
-			addClickHandler(label2, textPanel, idx++);
+			addClickHandler(label2, textPanel, idx + 1);
 		}
 		else
 		{
@@ -1535,9 +1535,11 @@ ArrangePanel.prototype.init = function()
 		}
 
 		this.container.appendChild(this.addTable(this.createPanel()));
-		this.container.appendChild(this.addGroupOps(this.createPanel()));
 	}
 	
+	// Allows to lock/unload button to be added
+	this.container.appendChild(this.addGroupOps(this.createPanel()));
+
 	if (ss.containsLabel)
 	{
 		// Adds functions from hidden style format panel
@@ -1799,7 +1801,7 @@ ArrangePanel.prototype.addGroupOps = function(div)
 		count += this.addActions(div, ['group', 'ungroup', 'copySize', 'pasteSize']) +
 			this.addActions(div, ['removeFromGroup']);
 	}
-	
+
 	var copyBtn = null;
 
 	if (ss.cells.length == 1 && ss.cells[0].value != null && !isNaN(ss.cells[0].value.nodeType))
@@ -1874,7 +1876,9 @@ ArrangePanel.prototype.addGroupOps = function(div)
 			' Shift+Click to Clear Anchor Points');
 		count++;
 	}
-	
+
+	count += this.addActions(div, ['lockUnlock']);
+
 	if (count == 0)
 	{
 		div.style.display = 'none';
@@ -5785,7 +5789,6 @@ DiagramStylePanel.prototype.init = function()
 {
 	var ui = this.editorUi;
 	var editor = ui.editor;
-	var graph = editor.graph;
 
 	this.darkModeChangedListener = mxUtils.bind(this, function()
 	{
@@ -6284,7 +6287,7 @@ DiagramStylePanel.prototype.addView = function(div)
 		if (pageCount < 15)
 		{
 			var left = document.createElement('div');
-			left.style.className = 'geAdaptiveAsset';
+			left.className = 'geAdaptiveAsset';
 			left.style.position = 'absolute';
 			left.style.left = '0px';
 			left.style.top = '0px';

+ 156 - 89
src/main/webapp/js/grapheditor/Graph.js

@@ -362,7 +362,8 @@ Graph = function(container, model, renderHint, stylesheet, themes, standalone)
 
 							// Ignores clicks inside cell to avoid delayed selection on
 							// merged cells when clicking on invisible part of dividers
-			    			if (this.isTableCell(state.cell) && !this.isCellSelected(state.cell) &&
+			    			if (this.isTableCell(state.cell) && this.isCellMovable(state.cell) &&
+								!this.isCellSelected(state.cell) &&
 								(!mxUtils.contains(state, me.getGraphX() - t1, me.getGraphY() - t1) ||
 								!mxUtils.contains(state, me.getGraphX() - t1, me.getGraphY() + t1) ||
 								!mxUtils.contains(state, me.getGraphX() + t1, me.getGraphY() + t1) ||
@@ -409,7 +410,7 @@ Graph = function(container, model, renderHint, stylesheet, themes, standalone)
 				    		while (!me.isConsumed() && current != null && (this.isTableCell(current.cell) ||
 				    			this.isTableRow(current.cell) || this.isTable(current.cell)))
 				    		{
-					    		if (this.isSwimlane(current.cell))
+					    		if (this.isSwimlane(current.cell) && this.isCellMovable(current.cell))
 					    		{
 					    			var offset = this.getActualStartSize(current.cell);
 					    			
@@ -422,7 +423,7 @@ Graph = function(container, model, renderHint, stylesheet, themes, standalone)
 		    							this.selectCellForEvent(current.cell, me.getEvent());
 						    			handler = this.selectionCellsHandler.getHandler(current.cell);
 			
-						    			if (handler != null)
+						    			if (handler != null && handler.customHandles != null)
 						    			{
 						    				// Swimlane start size handle is last custom handle
 						    				var handle = mxEvent.CUSTOM_HANDLE - handler.customHandles.length + 1;
@@ -656,7 +657,7 @@ Graph = function(container, model, renderHint, stylesheet, themes, standalone)
 				    			var box = new mxRectangle(me.getGraphX(), me.getGraphY());
 			    				box.grow(tol);
 	
-					    		if (this.isTableCell(state.cell))
+					    		if (this.isTableCell(state.cell) && this.isCellMovable(state.cell))
 					    		{
 				    				var row = this.model.getParent(state.cell);
 			    					var table = this.model.getParent(row);
@@ -684,7 +685,7 @@ Graph = function(container, model, renderHint, stylesheet, themes, standalone)
 					    		while (cursor == null && current != null && (this.isTableCell(current.cell) ||
 					    			this.isTableRow(current.cell) || this.isTable(current.cell)))
 					    		{
-						    		if (this.isSwimlane(current.cell))
+						    		if (this.isSwimlane(current.cell) && this.isCellMovable(current.cell))
 						    		{
 						    			var offset = this.getActualStartSize(current.cell);
 						    			var s = this.view.scale;
@@ -1143,7 +1144,7 @@ Graph = function(container, model, renderHint, stylesheet, themes, standalone)
 			return graphHandlerShouldRemoveCellsFromParent.apply(this, arguments);
 		};
 
-		// Unlocks all cells
+		// Returns true if the given cell is locked
 		this.isCellLocked = function(cell)
 		{
 			while (cell != null)
@@ -8208,6 +8209,46 @@ if (typeof mxVertexHandler !== 'undefined')
 				shape == 'table' || shape == 'tableRow';
 		};
 		
+		/**
+		 * Overridden to check table cells and rows.
+		 */
+		var graphIsCellEditable = Graph.prototype.isCellEditable;
+		Graph.prototype.isCellEditable = function(cell)
+		{
+			if (cell == null || !graphIsCellEditable.apply(this, arguments))
+			{
+				return false;
+			}
+			else if (this.isTableCell(cell) || this.isTableRow(cell))
+			{
+				return this.isCellEditable(this.model.getParent(cell));
+			}
+			else
+			{
+				return true;
+			}
+		};
+		
+		/**
+		 * Overridden to check table cells and rows.
+		 */
+		var graphIsCellMovable = Graph.prototype.isCellMovable;
+		Graph.prototype.isCellMovable = function(cell)
+		{
+			if (cell == null || !graphIsCellMovable.apply(this, arguments))
+			{
+				return false;
+			}
+			else if (this.isTableCell(cell) || this.isTableRow(cell))
+			{
+				return this.isCellMovable(this.model.getParent(cell));
+			}
+			else
+			{
+				return true;
+			}
+		};
+		
 		/**
 		 * Overridden to add expand style.
 		 */
@@ -12696,7 +12737,7 @@ if (typeof mxVertexHandler !== 'undefined')
 
 			var handles = vertexHandlerCreateCustomHandles.apply(this, arguments);
 			
-			if (this.graph.isTable(this.state.cell))
+			if (this.graph.isTable(this.state.cell) && this.graph.isCellMovable(this.state.cell))
 			{
 				var self = this;
 				var graph = this.graph;
@@ -12741,84 +12782,89 @@ if (typeof mxVertexHandler !== 'undefined')
 						(mxUtils.bind(this, function(index)
 						{
 							var rowState = rows[index];
-							var nextRow = (index < rows.length - 1) ? rows[index + 1] : null;
-							var ngeo = (nextRow != null) ? graph.getCellGeometry(nextRow.cell) : null;
-							var ng = (ngeo != null && ngeo.alternateBounds != null) ? ngeo.alternateBounds : ngeo;
-							
-							var shape = (rowLines[index] != null) ?
-								new TableLineShape(rowLines[index], mxConstants.NONE, 1) :
-								new mxLine(new mxRectangle(), mxConstants.NONE, 1, false);
-							shape.isDashed = sel.isDashed;
-							shape.svgStrokeTolerance++;
+							var handle = null;
 
-							var handle = new mxHandle(rowState, 'row-resize', null, shape);
-							handle.tableHandle = true;
-							var dy = 0;
-	
-							handle.shape.node.parentNode.insertBefore(handle.shape.node,
-								handle.shape.node.parentNode.firstChild);
-							
-							handle.redraw = function()
+							if (graph.isCellMovable(rowState.cell))
 							{
-								if (this.shape != null)
+								var nextRow = (index < rows.length - 1) ? rows[index + 1] : null;
+								var ngeo = (nextRow != null) ? graph.getCellGeometry(nextRow.cell) : null;
+								var ng = (ngeo != null && ngeo.alternateBounds != null) ? ngeo.alternateBounds : ngeo;
+								
+								var shape = (rowLines[index] != null) ?
+									new TableLineShape(rowLines[index], mxConstants.NONE, 1) :
+									new mxLine(new mxRectangle(), mxConstants.NONE, 1, false);
+								shape.isDashed = sel.isDashed;
+								shape.svgStrokeTolerance++;
+
+								handle = new mxHandle(rowState, 'row-resize', null, shape);
+								handle.tableHandle = true;
+								var dy = 0;
+		
+								handle.shape.node.parentNode.insertBefore(handle.shape.node,
+									handle.shape.node.parentNode.firstChild);
+								
+								handle.redraw = function()
 								{
-									this.shape.stroke = (dy == 0) ? mxConstants.NONE : sel.stroke;
-
-									if (this.shape.constructor == TableLineShape)
+									if (this.shape != null)
 									{
-										this.shape.line = moveLine(rowLines[index], 0, dy);
-										this.shape.updateBoundsFromLine();
-									}
-									else
-									{
-										var start = graph.getActualStartSize(tableState.cell, true);
-										this.shape.bounds.height = 1;
-										this.shape.bounds.y = this.state.y + this.state.height + dy * s;
-										this.shape.bounds.x = tableState.x + ((index == rows.length - 1) ?
-											0 : start.x * s);
-										this.shape.bounds.width = tableState.width - ((index == rows.length - 1) ?
-											0 : (start.width + start.x) + s);
-									}
+										this.shape.stroke = (dy == 0) ? mxConstants.NONE : sel.stroke;
 
-									this.shape.redraw();
-								}
-							};
-							
-							var shiftPressed = false;
-							
-							handle.setPosition = function(bounds, pt, me)
-							{
-								dy = Math.max(Graph.minTableRowHeight - bounds.height,
-									pt.y - bounds.y - bounds.height);
-								shiftPressed = mxEvent.isShiftDown(me.getEvent());
+										if (this.shape.constructor == TableLineShape)
+										{
+											this.shape.line = moveLine(rowLines[index], 0, dy);
+											this.shape.updateBoundsFromLine();
+										}
+										else
+										{
+											var start = graph.getActualStartSize(tableState.cell, true);
+											this.shape.bounds.height = 1;
+											this.shape.bounds.y = this.state.y + this.state.height + dy * s;
+											this.shape.bounds.x = tableState.x + ((index == rows.length - 1) ?
+												0 : start.x * s);
+											this.shape.bounds.width = tableState.width - ((index == rows.length - 1) ?
+												0 : (start.width + start.x) + s);
+										}
 
-								if (ng != null && shiftPressed)
-								{
-									dy = Math.min(dy, ng.height - Graph.minTableRowHeight);
-								}
-							};
-							
-							handle.execute = function(me)
-							{
-								if (dy != 0)
+										this.shape.redraw();
+									}
+								};
+								
+								var shiftPressed = false;
+								
+								handle.setPosition = function(bounds, pt, me)
 								{
-									graph.setTableRowHeight(this.state.cell,
-										dy, !shiftPressed);
-								}
-								else if (!self.blockDelayedSelection)
+									dy = Math.max(Graph.minTableRowHeight - bounds.height,
+										pt.y - bounds.y - bounds.height);
+									shiftPressed = mxEvent.isShiftDown(me.getEvent());
+
+									if (ng != null && shiftPressed)
+									{
+										dy = Math.min(dy, ng.height - Graph.minTableRowHeight);
+									}
+								};
+								
+								handle.execute = function(me)
 								{
-									var temp = graph.getCellAt(me.getGraphX(),
-										me.getGraphY()) || tableState.cell; 
-									graph.graphHandler.selectCellForEvent(temp, me);
-								}
+									if (dy != 0)
+									{
+										graph.setTableRowHeight(this.state.cell,
+											dy, !shiftPressed);
+									}
+									else if (!self.blockDelayedSelection)
+									{
+										var temp = graph.getCellAt(me.getGraphX(),
+											me.getGraphY()) || tableState.cell; 
+										graph.graphHandler.selectCellForEvent(temp, me);
+									}
+									
+									dy = 0;
+								};
 								
-								dy = 0;
-							};
-							
-							handle.reset = function()
-							{
-								dy = 0;
-							};
+								handle.reset = function()
+								{
+									dy = 0;
+								};
+							}
 							
 							handles.push(handle);
 						}))(i);
@@ -12950,7 +12996,10 @@ if (typeof mxVertexHandler !== 'undefined')
 			{
 				for (var i = 0; i < this.moveHandles.length; i++)
 				{
-					this.moveHandles[i].style.visibility = (visible) ? '' : 'hidden';
+					if (this.moveHandles[i] != null)
+					{
+						this.moveHandles[i].style.visibility = (visible) ? '' : 'hidden';
+					}
 				}
 			}
 			
@@ -12976,7 +13025,10 @@ if (typeof mxVertexHandler !== 'undefined')
 			{
 				for (var i = 0; i < this.moveHandles.length; i++)
 				{
-					this.moveHandles[i].parentNode.removeChild(this.moveHandles[i]);
+					if (this.moveHandles[i] != null)
+					{
+						this.moveHandles[i].parentNode.removeChild(this.moveHandles[i]);
+					}
 				}
 				
 				this.moveHandles = null;
@@ -12989,7 +13041,8 @@ if (typeof mxVertexHandler !== 'undefined')
 			{
 				(mxUtils.bind(this, function(rowState)
 				{
-					if (rowState != null && model.isVertex(rowState.cell))
+					if (rowState != null && model.isVertex(rowState.cell) &&
+						graph.isCellMovable(rowState.cell))
 					{
 						// Adds handle to move row
 						// LATER: Move to overlay pane to hide during zoom but keep padding
@@ -13037,6 +13090,10 @@ if (typeof mxVertexHandler !== 'undefined')
 						this.graph.container.appendChild(moveHandle);
 	
 					}
+					else
+					{
+						this.moveHandles.push(null);
+					}
 				}))(this.graph.view.getState(model.getChildAt(this.state.cell, i)));
 			}
 		};
@@ -13050,13 +13107,17 @@ if (typeof mxVertexHandler !== 'undefined')
 			{
 				for (var i = 0; i < this.customHandles.length; i++)
 				{
-					this.customHandles[i].destroy();
+					if (this.customHandles[i] != null)
+					{
+						this.customHandles[i].destroy();
+					}
 				}
 				
 				this.customHandles = this.createCustomHandles();
 			}
-			
-			if (this.graph.isTable(this.state.cell))
+
+			if (this.graph.isTable(this.state.cell) &&
+				this.graph.isCellMovable(this.state.cell))
 			{
 				this.refreshMoveHandles();
 			}
@@ -13088,7 +13149,8 @@ if (typeof mxVertexHandler !== 'undefined')
 				{
 					for (var i = 0; i < this.customHandles.length; i++)
 					{
-						if (this.customHandles[i].shape != null &&
+						if (this.customHandles[i] != null &&
+							this.customHandles[i].shape != null &&
 							this.customHandles[i].shape.bounds != null)
 						{
 							var b = this.customHandles[i].shape.bounds;
@@ -13880,13 +13942,15 @@ if (typeof mxVertexHandler !== 'undefined')
 			{
 				this.rotationShape.node.setAttribute('title', mxResources.get('rotateTooltip'));
 			}
-			
-			if (this.graph.isTable(this.state.cell))
+
+			if (this.graph.isTable(this.state.cell) &&
+				this.graph.isCellMovable(this.state.cell))
 			{
 				this.refreshMoveHandles();
 			}
 			// Draws corner rectangles for single selected table cells and rows
 			else if (this.graph.getSelectionCount() == 1 &&
+				this.graph.isCellMovable(this.state.cell) &&
 				(this.graph.isTableCell(this.state.cell) ||
 				this.graph.isTableRow(this.state.cell)))
 			{
@@ -14117,10 +14181,13 @@ if (typeof mxVertexHandler !== 'undefined')
 			{
 				for (var i = 0; i < this.moveHandles.length; i++)
 				{
-					this.moveHandles[i].style.left = (this.moveHandles[i].rowState.x +
-						this.moveHandles[i].rowState.width - 5) + 'px';
-					this.moveHandles[i].style.top = (this.moveHandles[i].rowState.y +
-						this.moveHandles[i].rowState.height / 2 - 6) + 'px';
+					if (this.moveHandles[i] != null)
+					{
+						this.moveHandles[i].style.left = (this.moveHandles[i].rowState.x +
+							this.moveHandles[i].rowState.width - 5) + 'px';
+						this.moveHandles[i].style.top = (this.moveHandles[i].rowState.y +
+							this.moveHandles[i].rowState.height / 2 - 6) + 'px';
+					}
 				}
 			}
 			

+ 2 - 6
src/main/webapp/js/grapheditor/Menus.js

@@ -1507,12 +1507,8 @@ Menus.prototype.addPopupMenuEditItems = function(menu, cell, evt)
 	}
 	else
 	{
-		if (this.isShowCellEditItems())
-		{
-			this.addMenuItems(menu, ['delete', '-', ], null, evt);
-		}
-
-		this.addMenuItems(menu, ['cut', 'copy', 'duplicate', 'lockUnlock'], null, evt);
+		this.addMenuItems(menu, ['cut', 'copy', 'duplicate',
+			'-', 'delete', 'lockUnlock'], null, evt);
 	}
 };
 

+ 128 - 37
src/main/webapp/js/grapheditor/Sidebar.js

@@ -18,6 +18,22 @@ function Sidebar(editorUi, container)
 	this.graph.container.style.visibility = 'hidden';
 	this.graph.foldingEnabled = false;
 
+	// Wrapper for entries and footer
+	this.container.style.overflow = 'visible';
+	this.wrapper = document.createElement('div');
+	this.wrapper.style.position = 'relative';
+	this.wrapper.style.overflowX = 'hidden';
+	this.wrapper.style.overflowY = 'auto';
+	this.wrapper.style.left = '0px';
+	this.wrapper.style.top = '0px';
+	this.wrapper.style.right = '0px';
+	this.wrapper.style.boxSizing = 'border-box';
+	this.wrapper.style.maxHeight = 'calc(100% - ' + this.moreShapesHeight + 'px)';
+	this.container.appendChild(this.wrapper);
+
+	var title = this.createMoreShapes();
+	this.container.appendChild(title);
+
 	document.body.appendChild(this.graph.container);
 	
 	this.pointerUpHandler = mxUtils.bind(this, function()
@@ -194,6 +210,11 @@ Sidebar.prototype.thumbPadding = (document.documentMode >= 5) ? 2 : 3;
  */
 Sidebar.prototype.thumbBorder = 2;
 
+/**
+ * Allows for two buttons in the sidebar footer.
+ */
+Sidebar.prototype.moreShapesHeight = 52;
+
 /*
  * Experimental smaller sidebar entries
  */
@@ -261,11 +282,27 @@ Sidebar.prototype.refresh = function()
 {
 	var graph = this.editorUi.editor.graph;
 	this.graph.stylesheet.styles = mxUtils.clone(graph.stylesheet.styles);
-	this.container.innerText = '';
+	this.wrapper.innerText = '';
 	this.palettes = new Object();
 	this.init();
 };
 
+/**
+ * Adds the general palette to the sidebar.
+ */
+Sidebar.prototype.getEntryContainer = function()
+{
+	return this.wrapper;
+};
+
+/**
+ * Adds the general palette to the sidebar.
+ */
+Sidebar.prototype.appendChild = function(child)
+{
+	this.wrapper.appendChild(child);
+};
+
 /**
  * Adds all palettes to the sidebar.
  */
@@ -274,12 +311,59 @@ Sidebar.prototype.getTooltipOffset = function(elt, bounds)
 	var b = document.body;
 	var d = document.documentElement;
 	var bottom = Math.max(b.clientHeight || 0, d.clientHeight);
-	var width = bounds.width + 2 * this.tooltipBorder + 4;
 	var height = bounds.height + 2 * this.tooltipBorder;
 	
-	return new mxPoint(this.container.offsetWidth + this.editorUi.splitSize + 10 + this.editorUi.container.offsetLeft,
+	return new mxPoint(this.container.offsetWidth + this.editorUi.splitSize + 4 + this.editorUi.container.offsetLeft,
 		Math.min(bottom - height - 20 /*status bar*/, Math.max(0, (this.editorUi.container.offsetTop +
-			this.container.offsetTop + elt.offsetTop - this.container.scrollTop - height / 2 + 16))));
+			this.container.offsetTop + elt.offsetTop - this.wrapper.scrollTop - height / 2 + 16))));
+};
+
+/**
+ * Adds all palettes to the sidebar.
+ */
+Sidebar.prototype.createMoreShapes = function()
+{
+	var div =  this.editorUi.createDiv('geSidebarContainer geSidebarFooter');
+	div.style.position = 'absolute';
+	div.style.overflow = 'hidden';
+	div.style.display = 'inline-flex';
+	div.style.alignItems = 'center';
+	div.style.justifyContent = 'center';
+	div.style.width = '100%';
+	div.style.marginTop = '-1px';
+	div.style.height = this.moreShapesHeight+ 'px';
+	
+	var title = document.createElement('button');
+	title.className = 'geBtn gePrimaryBtn';
+	title.style.display = 'inline-flex';
+	title.style.alignItems = 'center';
+	title.style.whiteSpace = 'nowrap';
+	title.style.padding = '8px';
+	title.style.margin = '0px';
+	title.innerHTML = '<span>+</span>';
+	
+	var span = title.getElementsByTagName('span')[0];
+	span.style.fontSize = '18px';
+	span.style.marginRight = '5px';
+
+	mxUtils.write(title, mxResources.get('moreShapes'));
+
+	// Prevents focus
+	mxEvent.addListener(title, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
+		mxUtils.bind(this, function(evt)
+	{
+		evt.preventDefault();
+	}));
+	
+	mxEvent.addListener(title, 'click', mxUtils.bind(this, function(evt)
+	{
+		this.editorUi.actions.get('shapes').funct();
+		mxEvent.consume(evt);
+	}));
+	
+	div.appendChild(title);
+	
+	return div;
 };
 
 /**
@@ -814,7 +898,7 @@ Sidebar.prototype.addSearchPalette = function(expand)
 {
 	var elt = document.createElement('div');
 	elt.style.visibility = 'hidden';
-	this.container.appendChild(elt);
+	this.appendChild(elt);
 		
 	var div = document.createElement('div');
 	div.className = 'geSidebar';
@@ -1066,42 +1150,49 @@ Sidebar.prototype.addSearchPalette = function(expand)
 			mxEvent.consume(evt);
 		}
 	}));
-	
-	mxEvent.addListener(input, 'keyup', mxUtils.bind(this, function(evt)
+
+	var searchChanged = mxUtils.bind(this, function()
 	{
-		if (input.value == '')
-		{
-			cross.setAttribute('src', Sidebar.prototype.searchImage);
-			cross.setAttribute('title', mxResources.get('search'));
-		}
-		else
-		{
-			cross.setAttribute('src', Dialog.prototype.closeImage);
-			cross.setAttribute('title', mxResources.get('reset'));
-		}
-		
-		if (input.value == '')
-		{
-			complete = true;
-			button.style.display = 'none';
-		}
-		else if (input.value != searchTerm)
+		window.setTimeout(mxUtils.bind(this, function()
 		{
-			button.style.display = 'none';
-			complete = false;
-		}
-		else if (!active)
-		{
-			if (complete)
+			if (input.value == '')
 			{
-				button.style.display = 'none';
+				cross.setAttribute('src', Sidebar.prototype.searchImage);
+				cross.setAttribute('title', mxResources.get('search'));
 			}
 			else
 			{
-				button.style.display = '';
+				cross.setAttribute('src', Dialog.prototype.closeImage);
+				cross.setAttribute('title', mxResources.get('reset'));
 			}
-		}
-	}));
+			
+			if (input.value == '')
+			{
+				complete = true;
+				button.style.display = 'none';
+			}
+			else if (input.value != searchTerm)
+			{
+				button.style.display = 'none';
+				complete = false;
+			}
+			else if (!active)
+			{
+				if (complete)
+				{
+					button.style.display = 'none';
+				}
+				else
+				{
+					button.style.display = '';
+				}
+			}
+		}), 0);
+	});
+	
+	mxEvent.addListener(input, 'keyup', searchChanged);
+	mxEvent.addListener(input, 'paste', searchChanged);
+	mxEvent.addListener(input, 'cut', searchChanged);
 
     // Workaround for blocked text selection in Editor
     mxEvent.addListener(input, 'mousedown', function(evt)
@@ -1127,7 +1218,7 @@ Sidebar.prototype.addSearchPalette = function(expand)
 
 	var outer = document.createElement('div');
     outer.appendChild(div);
-    this.container.appendChild(outer);
+    this.appendChild(outer);
 	
     // Keeps references to the DOM nodes
 	this.palettes['search'] = [elt, outer];
@@ -3643,7 +3734,7 @@ Sidebar.prototype.addPaletteFunctions = function(id, title, expanded, fns)
 Sidebar.prototype.addPalette = function(id, title, expanded, onInit)
 {
 	var elt = this.createTitle(title);
-	this.container.appendChild(elt);
+	this.appendChild(elt);
 	
 	var div = document.createElement('div');
 	div.className = 'geSidebar';
@@ -3668,7 +3759,7 @@ Sidebar.prototype.addPalette = function(id, title, expanded, onInit)
 	
 	var outer = document.createElement('div');
     outer.appendChild(div);
-    this.container.appendChild(outer);
+    this.appendChild(outer);
     
     // Keeps references to the DOM nodes
     if (id != null)

File diff suppressed because it is too large
+ 1384 - 1383
src/main/webapp/js/integrate.min.js


File diff suppressed because it is too large
+ 162 - 162
src/main/webapp/js/stencils.min.js


File diff suppressed because it is too large
+ 1637 - 1637
src/main/webapp/js/viewer-static.min.js


File diff suppressed because it is too large
+ 1637 - 1637
src/main/webapp/js/viewer.min.js


File diff suppressed because it is too large
+ 26 - 25
src/main/webapp/mxgraph/mxClient.js


+ 1 - 0
src/main/webapp/resources/dia.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_am.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_ar.txt

@@ -15,6 +15,7 @@ addLayer=‫إضافة طبقة‬
 addProperty=‫إضافة خاصية‬
 address=‫عنوان‬
 addToExistingDrawing=‫أضف إلى الرسم الحالي‬
+addToScratchpad=Add to Scratchpad
 addWaypoint=‫أضف نقطة على المسار‬
 adjustTo=‫ضبط الي‬
 advanced=‫متقدم‬

+ 1 - 0
src/main/webapp/resources/dia_bg.txt

@@ -15,6 +15,7 @@ addLayer=Добавяне на слой
 addProperty=Добавяне на свойство
 address=Адрес
 addToExistingDrawing=Добавяне към съществуващ чертеж
+addToScratchpad=Add to Scratchpad
 addWaypoint=Добавяне на ориентир
 adjustTo=Нагласи до
 advanced=Разширени

+ 1 - 0
src/main/webapp/resources/dia_bn.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_bs.txt

@@ -15,6 +15,7 @@ addLayer=Dodaj sloj
 addProperty=Dodaj karakteristiku
 address=Adresa
 addToExistingDrawing=Dodaj na postojeći crtež
+addToScratchpad=Add to Scratchpad
 addWaypoint=Dodaj međutačku
 adjustTo=Podesi prema
 advanced=Napredno

+ 1 - 0
src/main/webapp/resources/dia_ca.txt

@@ -15,6 +15,7 @@ addLayer=Afegeix una capa
 addProperty=Afegeix una propietat
 address=Adreça
 addToExistingDrawing=Afegeix al dibuix actual
+addToScratchpad=Add to Scratchpad
 addWaypoint=Afegeix una coordenada
 adjustTo=Ajustar a
 advanced=Avançat

+ 1 - 0
src/main/webapp/resources/dia_cs.txt

@@ -15,6 +15,7 @@ addLayer=Přidat vrstvu
 addProperty=Přidat vlastnost
 address=Adresa
 addToExistingDrawing=Přidat do existujícího výkresu
+addToScratchpad=Add to Scratchpad
 addWaypoint=Přidat průchozí bod
 adjustTo=Přizpůsobit
 advanced=Pokročilé

+ 1 - 0
src/main/webapp/resources/dia_da.txt

@@ -15,6 +15,7 @@ addLayer=Tilføj lag
 addProperty=Tilføj egenskab
 address=Adresse
 addToExistingDrawing=Føj til eksisterende tegning
+addToScratchpad=Add to Scratchpad
 addWaypoint=Tilføj støttepunkt
 adjustTo=Juster til
 advanced=Avanceret

+ 1 - 0
src/main/webapp/resources/dia_de.txt

@@ -15,6 +15,7 @@ addLayer=Ebene hinzufügen
 addProperty=Eigenschaft einfügen
 address=Adresse
 addToExistingDrawing=In vorhandene Zeichnung einfügen
+addToScratchpad=Zum Notizblock hinzufügen
 addWaypoint=Wegpunkt einfügen
 adjustTo=Verkleinern/Vergrößern
 advanced=Erweitert

+ 1 - 0
src/main/webapp/resources/dia_el.txt

@@ -15,6 +15,7 @@ addLayer=Προσθήκη επιπέδου
 addProperty=Προσθήκη ιδιότητας
 address=Διεύθυνση
 addToExistingDrawing=Προσθήκη σε υπάρχον σχεδιάγραμμα
+addToScratchpad=Add to Scratchpad
 addWaypoint=Προσθήκη σημείου αναφοράς
 adjustTo=Προσαρμογή σε
 advanced=Προχωρημένο

+ 1 - 0
src/main/webapp/resources/dia_eo.txt

@@ -15,6 +15,7 @@ addLayer=Aldoni tavolon
 addProperty=Aldoni econ
 address=Adreso
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_es.txt

@@ -15,6 +15,7 @@ addLayer=Agregar capa
 addProperty=Agregar propiedad
 address=Dirección
 addToExistingDrawing=Agregar al dibujo existente
+addToScratchpad=Add to Scratchpad
 addWaypoint=Agregar punto de referencia
 adjustTo=Ajustar a
 advanced=Avanzado

+ 1 - 0
src/main/webapp/resources/dia_et.txt

@@ -15,6 +15,7 @@ addLayer=Lisa kiht
 addProperty=Lisa väärtus
 address=Aadress
 addToExistingDrawing=Lisa olemasolevale joonisele
+addToScratchpad=Add to Scratchpad
 addWaypoint=Lisa teepunkt
 adjustTo=Kohalda
 advanced=Täpsemalt

+ 1 - 0
src/main/webapp/resources/dia_eu.txt

@@ -15,6 +15,7 @@ addLayer=Gehitu geruza
 addProperty=Gehitu propietatea
 address=Helbidea
 addToExistingDrawing=Gehitu diagramari
+addToScratchpad=Add to Scratchpad
 addWaypoint=Gehitu koordenatua
 adjustTo=Lerrokapena
 advanced=Aurreratua

+ 1 - 0
src/main/webapp/resources/dia_fa.txt

@@ -15,6 +15,7 @@ addLayer=‫افزودن لایه‬
 addProperty=‫افزودن ویژگی‬
 address=‫آدرس‬
 addToExistingDrawing=‫افزودن به طراحی موجود‬
+addToScratchpad=Add to Scratchpad
 addWaypoint=‫افزودن نقطه مسیر‬
 adjustTo=‫تنظیم به‬
 advanced=‫پیشرفته‬

+ 1 - 0
src/main/webapp/resources/dia_fi.txt

@@ -15,6 +15,7 @@ addLayer=Lisää taso
 addProperty=Lisää ominaisuus
 address=Osoite
 addToExistingDrawing=Lisää olemassa olevaan piirustukseen
+addToScratchpad=Add to Scratchpad
 addWaypoint=Lisää reittipiste
 adjustTo=Sovita
 advanced=Edistynyt

+ 1 - 0
src/main/webapp/resources/dia_fil.txt

@@ -15,6 +15,7 @@ addLayer=Magdagdag ng patong
 addProperty=Magdagdag ng katangian
 address=Address
 addToExistingDrawing=Magdagdag sa kasalukuyang Guhit
+addToScratchpad=Add to Scratchpad
 addWaypoint=Magdagdag ng Waypoint
 adjustTo=Isaayos sa
 advanced=Mas mahusay

+ 25 - 24
src/main/webapp/resources/dia_fr.txt

@@ -3,7 +3,7 @@
 about=À propos de
 aboutDrawio=À propos de draw.io
 accessDenied=Accès refusé
-accounts=Accounts
+accounts=Comptes
 action=Action
 actualSize=Taille réelle
 add=Ajouter
@@ -15,6 +15,7 @@ addLayer=Ajouter une couche
 addProperty=Ajouter une propriété
 address=Adresse
 addToExistingDrawing=Ajouter au diagramme existant
+addToScratchpad=Add to Scratchpad
 addWaypoint=Ajouter un repère
 adjustTo=Ajuster à
 advanced=Avancé
@@ -61,7 +62,7 @@ background=Arrière-plan
 backgroundColor=Couleur d'arrière-plan
 backgroundImage=Image d'arrière-plan
 basic=Basique
-beta=beta
+beta=bêta
 blankDrawing=Diagramme vierge
 blankDiagram=Diagramme vierge
 block=Bloc
@@ -820,8 +821,8 @@ property=Propriétés
 value=Valeur
 showMore=Afficher plus
 showLess=Afficher moins
-myDiagrams=My Diagrams
-allDiagrams=All Diagrams
+myDiagrams=Mes diagrammes
+allDiagrams=Tous les diagrammes
 recentlyUsed=Utilisé récemment
 listView=Vue liste
 gridView=Vue table
@@ -856,7 +857,7 @@ files=Fichiers
 shared=Partagés
 sharepoint=Sharepoint
 officeManualUpdateInst=Instructions: Copy draw.io diagram from the document. Then, in the box below, right-click and select "Paste" from the context menu.
-officeClickToEdit=Click icon to start editing:
+officeClickToEdit=Cliquer sur l'icône pour commencer l'édition :
 pasteDiagram=Paste draw.io diagram here
 connectOD=Connecter à OneDrive
 selectChildren=Sélectionner les enfants
@@ -869,7 +870,7 @@ reopen=Ré-ouvrir
 showResolved=Show Resolved
 reply=Répondre
 objectNotFound=Objet non trouvé
-reOpened=Re-opened
+reOpened=Réouvert
 markedAsResolved=Marqué comme résolu
 noCommentsFound=Aucun commentaires trouvés
 comments=Commentaires
@@ -941,7 +942,7 @@ startExp=Commencer l'exportation
 refreshDrawIndex=Refresh draw.io Diagrams Index
 reindexInst1=Click the "Start Indexing" button to refresh draw.io diagrams index.
 reindexInst2=Please note that the indexing procedure will take some time and the browser window must remain open until the indexing is completed.
-startIndexing=Start Indexing
+startIndexing=Commencer l'indexation
 confAPageFoundFetch=Page "{1}" found. Fetching
 confAAllDiagDone=All {1} diagrams processed. Process finished.
 confAStartedProcessing=Started processing page "{1}"
@@ -990,9 +991,9 @@ hiResPreview=Aperçu en haute résolution
 officeNotLoggedGD=You are not logged in to Google Drive. Please open draw.io task pane and login first.
 officePopupInfo=Please complete the process in the pop-up window.
 pickODFile=Pick OneDrive File
-createODFile=Create OneDrive File
+createODFile=Créer un fichier OneDrive
 pickGDriveFile=Pick Google Drive File
-createGDriveFile=Create Google Drive File
+createGDriveFile=Créer un fichier Google Drive
 pickDeviceFile=Pick Device File
 vsdNoConfig="vsdurl" is not configured
 ruler=Ruler
@@ -1078,7 +1079,7 @@ noAnchorsFound=Aucun ancrage trouvé
 attachment=Pièce jointe
 curDiagram=Current Diagram
 recentDiags=Recent Diagrams
-csvImport=CSV Import
+csvImport=Importation de CSV
 chooseFile=Choisir un fichier...
 choose=Choisir
 gdriveFname=Nom de fichier Google Drive
@@ -1089,14 +1090,14 @@ thumbnail=Miniature
 prevInDraw=Aperçu dans draw.io
 onedriveFname=Nom du fichier OneDrive
 diagFname=Diagram filename
-diagUrl=Diagram URL
-showDiag=Show Diagram
-diagPreview=Diagram Preview
+diagUrl=URL du diagramme
+showDiag=Afficher le diagramme
+diagPreview=Aperçu du diagramme
 csvFileUrl=CSV File URL
 generate=Générer
 selectDiag2Insert=Please select a diagram to insert it.
 errShowingDiag=Unexpected error. Cannot show diagram
-noRecentDiags=No recent diagrams found
+noRecentDiags=Aucun diagramme récent n'a été trouvé
 fetchingRecentFailed=Failed to fetch recent diagrams
 useSrch2FindDiags=Use the search box to find draw.io diagrams
 cantReadChckPerms=Cannot read the specified diagram. Please check you have read permission on that file.
@@ -1129,7 +1130,7 @@ plsSelectSingleFile=Please select a single file only
 attCorrupt=Attachment file "{1}" is corrupted
 loadAttFailed=Échec du chargement de la pièce jointe "{1}"
 embedDrawDiag=Embed draw.io Diagram
-addDiagram=Add Diagram
+addDiagram=Ajouter un diagramme
 embedDiagram=Embed Diagram
 editOwningPg=Edit owning page
 deepIndexing=Deep Indexing (Index diagrams that aren't used in any page also)
@@ -1206,7 +1207,7 @@ confAIgnoreCollectErr=Ignore collecting current pages errors
 drafts=Brouillons
 draftSaveInt=Draft save interval [sec] (0 to disable)
 pluginsDisabled=Plugins externes désactivés.
-extExpNotConfigured=External image service is not configured
+extExpNotConfigured=Le service d'image externe n'est pas configuré
 pathFilename=Chemin/Nom de fichier
 confAHugeInstances=Instances très grandes
 confAHugeInstancesDesc=If this instance includes 100,000+ pages, it is faster to request the current instance pages list from Atlassian. Please contact our support for more details.
@@ -1215,21 +1216,21 @@ chooseDrawioPsgesFile=Choose pages with draw.io diagrams csv file
 private=Privé
 diagramTooLarge=The diagram is too large, please reduce its size and try again.
 selectAdminUsers=Select Admin Users
-xyzTeam={1} Team
+xyzTeam=Équipe {1}
 addTeamTitle=Adding a new draw.io Team
 addTeamInst1=To create a new draw.io Team, you need to create a new Atlassian group with "drawio-" postfix (e.g, a group named "drawio-marketing").
 addTeamInst2=Then, configure which team member can edit/add configuration, templates, and libraries from this page.
 drawioTeams=draw.io Teams
-members=Members
-adminEditors=Admins/Editors
+members=Membres
+adminEditors=Administrateurs/éditeurs
 allowAll=Allow all
-noTeams=No teams found
-errorLoadingTeams=Error Loading Teams
-noTeamMembers=No team members found
-errLoadTMembers=Error loading team members
+noTeams=Aucune équipe trouvée
+errorLoadingTeams=Erreur de chargement des équipes
+noTeamMembers=Aucun membre de l'équipe n'a été trouvé
+errLoadTMembers=Erreur de chargement des membres de l'équipe
 errCreateTeamPage=Error creating team "{1}" page in "draw.io Configuration" space, please check you have the required permissions.
 gotoConfigPage=Please create the space from draw.io "Configuration" page.
-noAdminsSelected=No admins/editors selected
+noAdminsSelected=Aucun administrateur/éditeur sélectionné
 errCreateConfigFile=Error creating "configuration.json" file, please check you have the required permissions.
 errSetPageRestr=Error setting page restrictions
 notAdmin4Team=You are not an admin for this team

+ 1 - 0
src/main/webapp/resources/dia_gl.txt

@@ -15,6 +15,7 @@ addLayer=Engadir capa
 addProperty=Engadir propiedade
 address=Enderezo
 addToExistingDrawing=Engadir ao deseño existente
+addToScratchpad=Add to Scratchpad
 addWaypoint=Engadir punto de paso
 adjustTo=Axustar a
 advanced=Avanzado

+ 1 - 0
src/main/webapp/resources/dia_gu.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_he.txt

@@ -15,6 +15,7 @@ addLayer=‫הוסף שכבה‬
 addProperty=‫הוסף מאפיין‬
 address=‫כתובת‬
 addToExistingDrawing=‫הוסף לסקיצה קיימת‬
+addToScratchpad=Add to Scratchpad
 addWaypoint=‫הוסף נקודת ציון‬
 adjustTo=‫התאם ל‬
 advanced=‫מתקדם‬

+ 1 - 0
src/main/webapp/resources/dia_hi.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_hr.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_hu.txt

@@ -15,6 +15,7 @@ addLayer=Réteget hozzáad
 addProperty=Tulajdonság hozzáadás
 address=Cím
 addToExistingDrawing=Add hozzá a meglévő rajzhoz
+addToScratchpad=Add to Scratchpad
 addWaypoint=Útpont hozzáadás
 adjustTo=Módosít
 advanced=Haladó

+ 1 - 0
src/main/webapp/resources/dia_i18n.txt

@@ -15,6 +15,7 @@ addLayer=addLayer
 addProperty=addProperty
 address=address
 addToExistingDrawing=addToExistingDrawing
+addToScratchpad=addToScratchpad
 addWaypoint=addWaypoint
 adjustTo=adjustTo
 advanced=advanced

+ 1 - 0
src/main/webapp/resources/dia_id.txt

@@ -15,6 +15,7 @@ addLayer=Tambahkan Lapisan
 addProperty=Tambahkan Properti
 address=Alamat
 addToExistingDrawing=Tambahkan ke Gambar yang Ada
+addToScratchpad=Add to Scratchpad
 addWaypoint=Tambahkan Waypoint
 adjustTo=Atur ke
 advanced=Lanjutan

+ 1 - 0
src/main/webapp/resources/dia_it.txt

@@ -15,6 +15,7 @@ addLayer=Aggiungi livello
 addProperty=Aggiungi proprietà
 address=Indirizzo
 addToExistingDrawing=Aggiungi al disegno esistente
+addToScratchpad=Add to Scratchpad
 addWaypoint=Aggiungi punto di passaggio
 adjustTo=Adatta a
 advanced=Avanzate

+ 20 - 19
src/main/webapp/resources/dia_ja.txt

@@ -15,6 +15,7 @@ addLayer=レイヤーを追加する
 addProperty=属性を追加する
 address=アドレス
 addToExistingDrawing=既存のファイルに追加する
+addToScratchpad=Add to Scratchpad
 addWaypoint=途中点を追加する
 adjustTo=拡大縮小率
 advanced=高度な設定
@@ -77,7 +78,7 @@ bottomAlign=下端揃え
 bottomLeft=左下端
 bottomRight=右下端
 bpmn=BPMN
-bringForward=Bring Forward
+bringForward=前面へ移動
 browser=ブラウザ
 bulletedList=箇条書き
 business=ビジネス
@@ -400,7 +401,7 @@ images=イメージ
 imagePreviewError=このイメージは、プレビューのために読み込めませんでした。URL を確認してください。
 imageTooBig=画像が大きすぎます
 imgur=Imgur
-import=次をインポート:
+import=インポート
 importFrom=次の場所からインポート
 includeCopyOfMyDiagram=ダイアグラムのコピーを含める
 increaseIndent=インデント追加
@@ -669,7 +670,7 @@ saving=保存中
 scratchpad=スクラッチパッド
 scrollbars=スクロールバー
 search=検索
-searchShapes=検索図形
+searchShapes=図形を検索
 selectAll=すべて選択
 selectionOnly=選択範囲のみ
 selectCard=Select Card
@@ -680,7 +681,7 @@ selectFont=フォントを選択する
 selectNone=選択を解除
 selectTemplate=Select Template
 selectVertices=頂点を選択
-sendBackward=Send Backward
+sendBackward=背面へ移動
 sendMessage=送信する
 sendYourFeedback=フィードバックを送信
 serviceUnavailableOrBlocked=サービスはご利用になれません。あるいはブロックされています。
@@ -743,8 +744,8 @@ theme=テーマ
 timeout=時間切れ
 title=タイトル
 to=から
-toBack=背面に移動
-toFront=前面に移動
+toBack=最背面へ移動
+toFront=最前面へ移動
 tooLargeUseDownload=大きすぎます。代わりにダウンロードを利用してください。
 toolbar=Toolbar
 tooltips=ツールチップ
@@ -995,11 +996,11 @@ pickGDriveFile=Pick Google Drive File
 createGDriveFile=Create Google Drive File
 pickDeviceFile=Pick Device File
 vsdNoConfig="vsdurl" is not configured
-ruler=Ruler
-units=Units
-points=Points
-inches=Inches
-millimeters=Millimeters
+ruler=ルーラー
+units=単位
+points=ポイント
+inches=インチ
+millimeters=ミリメートル
 confEditDraftDelOrExt=This diagram is in a draft page, is deleted from the page, or is edited externally. It will be saved as a new attachment version and may not be reflected in the page.
 confDiagEditedExt=Diagram is edited in another session. It will be saved as a new attachment version but the page will show other session's modifications.
 macroNotFound=Macro Not Found
@@ -1046,7 +1047,7 @@ smart=Smart
 parentChildSpacing=Parent Child Spacing
 siblingSpacing=Sibling Spacing
 confNoPermErr=Sorry, you don't have enough permissions to view this embedded diagram from page {1}
-copyAsImage=Copy as Image
+copyAsImage=画像としてコピー
 lucidImport=Lucidchart Import
 lucidImportInst1=Click the "Start Import" button to import all Lucidchart diagrams.
 installFirst=Please install {1} first
@@ -1175,21 +1176,21 @@ confDraftTooBigErr=Draft size is too large. Pease check "Attachment Maximum Size
 owner=Owner
 repository=Repository
 branch=Branch
-meters=Meters
+meters=メートル
 teamsNoEditingMsg=Editor functionality is only available in Desktop environment (in MS Teams App or a web browser)
 contactOwner=Contact Owner
 viewerOnlyMsg=You cannot edit the diagrams in the mobile platform, please use the desktop client or a web browser.
-website=Website
-check4Updates=Check for updates
+website=ウェブサイト
+check4Updates=アップデートを確認
 attWriteFailedRetry={1}: Attachment write failed, trying again in {2} seconds...
 confPartialPageList=We couldn't fetch all pages due to an error in Confluence. Continuing using {1} pages only.
-spellCheck=Spell checker
+spellCheck=スペルチェック
 noChange=No Change
 lblToSvg=Convert labels to SVG
 txtSettings=Text Settings
 LinksLost=Links will be lost
 arcSize=Arc Size
-editConnectionPoints=Edit Connection Points
+editConnectionPoints=頂点を編集
 notInOffline=Not supported while offline
 notInDesktop=Not supported in Desktop App
 confConfigSpaceArchived=draw.io Configuration space (DRAWIOCONFIG) is archived. Please restore it first.
@@ -1200,8 +1201,8 @@ confAFileCleaned=Cleaning diagram draft "{1}" done
 confAFileCleanFailed=Cleaning diagram draft "{1}" failed
 confACleanOnly=Clean Diagram Drafts Only
 brush=Brush
-openDevTools=Open Developer Tools
-autoBkp=Automatic Backup
+openDevTools=開発者ツールを開く
+autoBkp=自動バックアップ
 confAIgnoreCollectErr=Ignore collecting current pages errors
 drafts=Drafts
 draftSaveInt=Draft save interval [sec] (0 to disable)

+ 1 - 0
src/main/webapp/resources/dia_kn.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_ko.txt

@@ -15,6 +15,7 @@ addLayer=레이어 추가
 addProperty=속성 추가
 address=주소
 addToExistingDrawing=기존 그림에 추가
+addToScratchpad=Add to Scratchpad
 addWaypoint=중간점 추가
 adjustTo=맞춤 비율
 advanced=고급

+ 1 - 0
src/main/webapp/resources/dia_lt.txt

@@ -15,6 +15,7 @@ addLayer=Pridėti sluoksnį
 addProperty=Pridėti savybę
 address=Adresas
 addToExistingDrawing=Pridėti prie esamo piešinio
+addToScratchpad=Add to Scratchpad
 addWaypoint=Pridėti atskaitos tašką
 adjustTo=Pritaikyti
 advanced=Išplėstinis

+ 1 - 0
src/main/webapp/resources/dia_lv.txt

@@ -15,6 +15,7 @@ addLayer=Pievienot slāni
 addProperty=Pievienot īpašību
 address=Adrese
 addToExistingDrawing=Pievienot esošajam zīmējumam
+addToScratchpad=Add to Scratchpad
 addWaypoint=Pievienot ceļa punktu
 adjustTo=Piesaistīt pie
 advanced=Papildus

+ 1 - 0
src/main/webapp/resources/dia_ml.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_mr.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_ms.txt

@@ -15,6 +15,7 @@ addLayer=Tambah Lapisan
 addProperty=Tambah Sifat
 address=Alamat
 addToExistingDrawing=Tambah ke Lukisan yang Sedia Ada
+addToScratchpad=Add to Scratchpad
 addWaypoint=Tambah Titik Arah
 adjustTo=Ubah kepada
 advanced=Lanjutan

+ 1 - 0
src/main/webapp/resources/dia_my.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_nl.txt

@@ -15,6 +15,7 @@ addLayer=Laag toevoegen
 addProperty=Eigenschap toevoegen
 address=Adres
 addToExistingDrawing=Aan bestaande tekening toevoegen
+addToScratchpad=Add to Scratchpad
 addWaypoint=Tussenpunt toevoegen
 adjustTo=Aanpassen aan
 advanced=Geavanceerd

+ 1 - 0
src/main/webapp/resources/dia_no.txt

@@ -15,6 +15,7 @@ addLayer=Legg til lag
 addProperty=Legg til egenskap
 address=Adresse
 addToExistingDrawing=Legg til i eksisterende tegning
+addToScratchpad=Add to Scratchpad
 addWaypoint=Legg til støttepunkt
 adjustTo=Juster til
 advanced=Avansert

+ 1 - 0
src/main/webapp/resources/dia_pl.txt

@@ -15,6 +15,7 @@ addLayer=Dodaj warstwę
 addProperty=Dodaj atrybut
 address=Adres
 addToExistingDrawing=Dodaj do istniejącego rysunku
+addToScratchpad=Add to Scratchpad
 addWaypoint=Dodaj punkt trasy
 adjustTo=Dostosuj do
 advanced=Zaawansowane

+ 1 - 0
src/main/webapp/resources/dia_pt-br.txt

@@ -15,6 +15,7 @@ addLayer=Adicionar camada
 addProperty=Adicionar propriedade
 address=Endereço
 addToExistingDrawing=Adicionar ao desenho existente
+addToScratchpad=Add to Scratchpad
 addWaypoint=Adicionar ponto intermediário
 adjustTo=Ajustar para
 advanced=Avançado

+ 1 - 0
src/main/webapp/resources/dia_pt.txt

@@ -15,6 +15,7 @@ addLayer=Adicionar camada
 addProperty=Adicionar propriedade
 address=Endereço
 addToExistingDrawing=Adicionar ao desenho existente
+addToScratchpad=Add to Scratchpad
 addWaypoint=Adicionar ponto de notificação
 adjustTo=Ajustar para
 advanced=Avançado

+ 1 - 0
src/main/webapp/resources/dia_ro.txt

@@ -15,6 +15,7 @@ addLayer=Adaugă layer
 addProperty=Adaugă proprietate
 address=Adresă
 addToExistingDrawing=Adaugă la desenul existent
+addToScratchpad=Add to Scratchpad
 addWaypoint=Adaugă reper
 adjustTo=Ajustează la
 advanced=Avansat

+ 1 - 0
src/main/webapp/resources/dia_ru.txt

@@ -15,6 +15,7 @@ addLayer=Добавить слой
 addProperty=Добавить свойство
 address=Адрес
 addToExistingDrawing=Добавить к существующей диаграмме
+addToScratchpad=Add to Scratchpad
 addWaypoint=Добавить опорную точку
 adjustTo=Привязать к
 advanced=Расширенные

+ 1 - 0
src/main/webapp/resources/dia_si.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_sk.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_sl.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_sr.txt

@@ -15,6 +15,7 @@ addLayer=Dodaj sloj
 addProperty=Dodaj osobinu
 address=Adresa
 addToExistingDrawing=Dodaj na postojeći crtež
+addToScratchpad=Add to Scratchpad
 addWaypoint=Dodaj međutačku
 adjustTo=Podesi prema
 advanced=Napredan

+ 1 - 0
src/main/webapp/resources/dia_sv.txt

@@ -15,6 +15,7 @@ addLayer=Lägg till lager
 addProperty=Lägg till egenskap
 address=Adress
 addToExistingDrawing=Lägg till på befintlig ritning
+addToScratchpad=Add to Scratchpad
 addWaypoint=Lägg till brytpunkt
 adjustTo=Justera till
 advanced=Avancerat

+ 1 - 0
src/main/webapp/resources/dia_sw.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_ta.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_te.txt

@@ -15,6 +15,7 @@ addLayer=Add Layer
 addProperty=Add Property
 address=Address
 addToExistingDrawing=Add to Existing Drawing
+addToScratchpad=Add to Scratchpad
 addWaypoint=Add Waypoint
 adjustTo=Adjust to
 advanced=Advanced

+ 1 - 0
src/main/webapp/resources/dia_th.txt

@@ -15,6 +15,7 @@ addLayer=เพิ่มชั้น
 addProperty=เพิ่มคุณสมบัติ
 address=ที่อยู่
 addToExistingDrawing=เพิ่มเข้าสู่การวาดปัจจุบัน
+addToScratchpad=Add to Scratchpad
 addWaypoint=เพิ่มการวางตำแหน่ง
 adjustTo=แก้ไขไป
 advanced=การตั้งค่าขั้นสูง

+ 1 - 0
src/main/webapp/resources/dia_tr.txt

@@ -15,6 +15,7 @@ addLayer=Katman ekle
 addProperty=Özellik ekle
 address=Adres
 addToExistingDrawing=Mevcut çizime ekle
+addToScratchpad=Add to Scratchpad
 addWaypoint=Ara nokta ekle
 adjustTo=Ayarla
 advanced=Gelişmiş

+ 1 - 0
src/main/webapp/resources/dia_uk.txt

@@ -15,6 +15,7 @@ addLayer=Додати шар
 addProperty=Додати властивість
 address=Адреса
 addToExistingDrawing=Додати до наявного креслення
+addToScratchpad=Add to Scratchpad
 addWaypoint=Додати точку шляху
 adjustTo=Виправити до
 advanced=Розширені

+ 1 - 0
src/main/webapp/resources/dia_vi.txt

@@ -15,6 +15,7 @@ addLayer=Thêm lớp
 addProperty=Thêm thuộc tính
 address=Địa chỉ
 addToExistingDrawing=Thêm vào bản vẽ có sẵn
+addToScratchpad=Add to Scratchpad
 addWaypoint=Thêm điểm tham chiếu
 adjustTo=Điều chỉnh đến
 advanced=Nâng cao

+ 1 - 0
src/main/webapp/resources/dia_zh-tw.txt

@@ -15,6 +15,7 @@ addLayer=新增圖層
 addProperty=新增屬性
 address=地址
 addToExistingDrawing=新增至現有圖紙
+addToScratchpad=Add to Scratchpad
 addWaypoint=新增航點
 adjustTo=調至
 advanced=進階

+ 1 - 0
src/main/webapp/resources/dia_zh.txt

@@ -15,6 +15,7 @@ addLayer=添加图层
 addProperty=添加属性
 address=地址
 addToExistingDrawing=添加至当前绘图
+addToScratchpad=Add to Scratchpad
 addWaypoint=添加航点
 adjustTo=调整到
 advanced=高级

File diff suppressed because it is too large
+ 1 - 1
src/main/webapp/service-worker.js


File diff suppressed because it is too large
+ 1 - 1
src/main/webapp/service-worker.js.map


+ 0 - 0
src/main/webapp/styles/dark.css


Some files were not shown because too many files changed in this diff