SaveAs project client side
authorymh <ymh.work@gmail.com>
Sun, 18 May 2014 10:31:12 +0200
changeset 297 7de2652f7ee8
parent 296 ded85569cb98
child 298 2f35c2ae7de8
SaveAs project client side
client/css/renkan.css
client/gruntfile.js
client/js/defaults.js
client/js/main-renderer.js
client/js/renderer/scene.js
client/lib/FileSaver.js
server/src/main/webapp/static/js/config.js
--- a/client/css/renkan.css	Fri May 16 17:36:21 2014 +0200
+++ b/client/css/renkan.css	Sun May 18 10:31:12 2014 +0200
@@ -178,6 +178,22 @@
     background-position: -172px 0;
 }
 
+.Rk-Export-Button {
+    width: 30px; background-position: -274px 0;
+}
+
+.Rk-Export-Button.disabled {
+    opacity: .5; cursor: default;
+}
+
+.Rk-Export-Button:hover {
+    background-position: -274px -35px;
+}
+
+.Rk-Export-Button.disabled:hover {
+    opacity: 1; background-position: -274px 0;
+}
+
 .Rk-Bookmarklet-Button {
     width: 30px; background-position: -138px 0;
 }
--- a/client/gruntfile.js	Fri May 16 17:36:21 2014 +0200
+++ b/client/gruntfile.js	Sun May 18 10:31:12 2014 +0200
@@ -34,7 +34,8 @@
           paths: {
             requtils: "require-utils",
             jquery: "empty:",
-            underscore: "empty:"
+            underscore: "empty:",
+            filesaver: "empty:"
           }
         }
       }
--- a/client/js/defaults.js	Fri May 16 17:36:21 2014 +0200
+++ b/client/js/defaults.js	Sun May 18 10:31:12 2014 +0200
@@ -1,5 +1,5 @@
 Rkns.defaults = {
-    
+
     language: (navigator.language || navigator.userLanguage || "en"),
         /* GUI Language */
     container: "renkan",
@@ -38,13 +38,14 @@
     autoscale_padding: 50,
     default_view: false,
 	/* Allows to load default view (zoom+offset) at start on read_only mode, instead of autoScale. default_view has to be an integer 0,1,2... */
-    
+
     /* TOP BAR BUTTONS */
     show_search_field: true,
     show_user_list: true,
     user_name_editable: true,
     user_color_editable: true,
     show_save_button: true,
+    show_export_button: true,
     show_open_button: false,
     show_addnode_button: true,
     show_addedge_button: true,
@@ -52,9 +53,9 @@
     show_fullscreen_button: true,
     home_button_url: false,
     home_button_title: "Home",
-    
+
     /* MINI-MAP OPTIONS */
-    
+
     show_minimap: true,
         /* Show a small map at the bottom right */
     minimap_width: 160,
@@ -64,15 +65,15 @@
     minimap_border_color: "#cccccc",
     minimap_highlight_color: "#ffff00",
     minimap_highlight_weight: 5,
-    
+
     /* EDGE/NODE COMMON OPTIONS */
-       
+
     buttons_background: "#202020",
     buttons_label_color: "#c000c0",
     buttons_label_font_size: 9,
-    
+
     /* NODE DISPLAY OPTIONS */
-    
+
     show_node_circles: true,
         /* Show circles for nodes */
     clip_node_images: true,
@@ -91,9 +92,9 @@
         /* Maximum displayed text length */
     label_untitled_nodes: "(untitled)",
         /* Label to display on untitled nodes */
-    
+
     /* EDGE DISPLAY OPTIONS */
-    
+
     edge_stroke_width: 2,
     selected_edge_stroke_width: 4,
     edge_label_distance: 0,
@@ -102,9 +103,9 @@
     edge_arrow_width: 12,
     edge_gap_in_bundles: 12,
     label_untitled_edges: "",
-    
+
     /* CONTEXTUAL DISPLAY (TOOLTIP OR EDITOR) OPTIONS */
-   
+
     tooltip_width: 275,
     tooltip_padding: 10,
     tooltip_margin: 15,
@@ -114,9 +115,9 @@
     tooltip_bottom_color: "#d0d0d0",
     tooltip_border_color: "#808080",
     tooltip_border_width: 1,
-    
+
     /* NODE EDITOR OPTIONS */
-    
+
     show_node_editor_uri: true,
     show_node_editor_description: true,
     show_node_editor_size: true,
@@ -124,30 +125,30 @@
     show_node_editor_image: true,
     show_node_editor_creator: true,
     uploaded_image_max_kb: 500,
-    
+
     /* NODE TOOLTIP OPTIONS */
-    
+
     show_node_tooltip_uri: true,
     show_node_tooltip_description: true,
     show_node_tooltip_color: true,
     show_node_tooltip_image: true,
     show_node_tooltip_creator: true,
-    
+
     /* EDGE EDITOR OPTIONS */
-    
+
     show_edge_editor_uri: true,
     show_edge_editor_color: true,
     show_edge_editor_direction: true,
     show_edge_editor_nodes: true,
     show_edge_editor_creator: true,
-    
+
     /* EDGE TOOLTIP OPTIONS */
-    
+
     show_edge_tooltip_uri: true,
     show_edge_tooltip_color: true,
     show_edge_tooltip_nodes: true,
     show_edge_tooltip_creator: true
-    
+
     /* */
-    
+
 };
--- a/client/js/main-renderer.js	Fri May 16 17:36:21 2014 +0200
+++ b/client/js/main-renderer.js	Sun May 18 10:31:12 2014 +0200
@@ -5,6 +5,7 @@
         paths: {
             'jquery':'../lib/jquery.min',
             'underscore':'../lib/underscore-min',
+            'filesaver' :'../lib/FileSaver',
             'requtils':'require-utils'
         }
     });
--- a/client/js/renderer/scene.js	Fri May 16 17:36:21 2014 +0200
+++ b/client/js/renderer/scene.js	Sun May 18 10:31:12 2014 +0200
@@ -1,5 +1,5 @@
 
-define(['jquery', 'underscore', 'requtils', 'renderer/miniframe'], function ($, _, requtils, MiniFrame) {
+define(['jquery', 'underscore', 'filesaver', 'requtils', 'renderer/miniframe'], function ($, _, filesaver, requtils, MiniFrame) {
     'use strict';
 
     var Utils = requtils.getUtils();
@@ -255,6 +255,7 @@
         bindClick(".Rk-AddEdge-Button", "addEdgeBtn");
         bindClick(".Rk-Save-Button", "save");
         bindClick(".Rk-Open-Button", "open");
+        bindClick(".Rk-Export-Button", "exportProject");
         this.$.find(".Rk-Bookmarklet-Button")
           /*jshint scripturl:true */
           .attr("href","javascript:" + Utils._BOOKMARKLET_CODE(_renkan))
@@ -455,6 +456,7 @@
                 '<div class="Rk-TopBar-Tooltip-Contents"><%-translate("Add Node")%></div></div></div><% } %>' +
                 '<% if (options.show_addedge_button) { %><div class="Rk-TopBar-Separator"></div><div class="Rk-TopBar-Button Rk-AddEdge-Button"><div class="Rk-TopBar-Tooltip">' +
                 '<div class="Rk-TopBar-Tooltip-Contents"><%-translate("Add Edge")%></div></div></div><% } %>' +
+                '<% if (options.show_export_button) { %><div class="Rk-TopBar-Separator"></div><div class="Rk-TopBar-Button Rk-Export-Button"><div class="Rk-TopBar-Tooltip"><div class="Rk-TopBar-Tooltip-Contents"><%-translate("Download Project")%></div></div></div><% } %>' +
                 '<% if (options.show_save_button) { %><div class="Rk-TopBar-Separator"></div><div class="Rk-TopBar-Button Rk-Save-Button"><div class="Rk-TopBar-Tooltip"><div class="Rk-TopBar-Tooltip-Contents"> </div></div></div><% } %>' +
                 '<% if (options.show_open_button) { %><div class="Rk-TopBar-Separator"></div><div class="Rk-TopBar-Button Rk-Open-Button"><div class="Rk-TopBar-Tooltip"><div class="Rk-TopBar-Tooltip-Contents"><%-translate("Open Project")%></div></div></div><% } %>' +
                 '<% if (options.show_bookmarklet) { %><div class="Rk-TopBar-Separator"></div><a class="Rk-TopBar-Button Rk-Bookmarklet-Button" href="#"><div class="Rk-TopBar-Tooltip"><div class="Rk-TopBar-Tooltip-Contents">' +
@@ -1176,6 +1178,33 @@
             }
             return false;
         },
+        exportProject: function() {
+          var projectJSON = this.renkan.project.toJSON(),
+              downloadLink = document.createElement("a"),
+              projectId = projectJSON.id,
+              fileNameToSaveAs = projectId + ".json";
+
+          // clean ids
+          delete projectJSON.id;
+          _.each(projectJSON.nodes, function(e,i,l) {
+            delete e._id;
+            delete e.id;
+          });
+          _.each(projectJSON.edges, function(e,i,l) {
+            delete e._id;
+            delete e.id;
+          });
+          _.each(projectJSON.views, function(e,i,l) {
+            delete e._id;
+            delete e.id;
+          });
+          projectJSON.users = [];
+
+          var projectJSONStr = JSON.stringify(projectJSON, null, 2);
+          var blob = new Blob([projectJSONStr], {type: "application/json;charset=utf-8"});
+          filesaver(blob,fileNameToSaveAs);
+
+        },
         foldBins: function() {
             var foldBinsButton = this.$.find(".Rk-Fold-Bins"),
             bins = this.renkan.$.find(".Rk-Bins");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/lib/FileSaver.js	Sun May 18 10:31:12 2014 +0200
@@ -0,0 +1,253 @@
+/*! FileSaver.js
+ *  A saveAs() FileSaver implementation.
+ *  2014-01-24
+ *
+ *  By Eli Grey, http://eligrey.com
+ *  License: X11/MIT
+ *    See LICENSE.md
+ */
+
+/*global self */
+/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
+
+var saveAs = saveAs
+  // IE 10+ (native saveAs)
+  || (typeof navigator !== "undefined" &&
+      navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
+  // Everyone else
+  || (function(view) {
+	"use strict";
+	// IE <10 is explicitly unsupported
+	if (typeof navigator !== "undefined" &&
+	    /MSIE [1-9]\./.test(navigator.userAgent)) {
+		return;
+	}
+	var
+		  doc = view.document
+		  // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet
+		, get_URL = function() {
+			return view.URL || view.webkitURL || view;
+		}
+		, URL = view.URL || view.webkitURL || view
+		, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
+		, can_use_save_link = !view.externalHost && "download" in save_link
+		, click = function(node) {
+			var event = doc.createEvent("MouseEvents");
+			event.initMouseEvent(
+				"click", true, false, view, 0, 0, 0, 0, 0
+				, false, false, false, false, 0, null
+			);
+			node.dispatchEvent(event);
+		}
+		, webkit_req_fs = view.webkitRequestFileSystem
+		, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
+		, throw_outside = function(ex) {
+			(view.setImmediate || view.setTimeout)(function() {
+				throw ex;
+			}, 0);
+		}
+		, force_saveable_type = "application/octet-stream"
+		, fs_min_size = 0
+		, deletion_queue = []
+		, process_deletion_queue = function() {
+			var i = deletion_queue.length;
+			while (i--) {
+				var file = deletion_queue[i];
+				if (typeof file === "string") { // file is an object URL
+					URL.revokeObjectURL(file);
+				} else { // file is a File
+					file.remove();
+				}
+			}
+			deletion_queue.length = 0; // clear queue
+		}
+		, dispatch = function(filesaver, event_types, event) {
+			event_types = [].concat(event_types);
+			var i = event_types.length;
+			while (i--) {
+				var listener = filesaver["on" + event_types[i]];
+				if (typeof listener === "function") {
+					try {
+						listener.call(filesaver, event || filesaver);
+					} catch (ex) {
+						throw_outside(ex);
+					}
+				}
+			}
+		}
+		, FileSaver = function(blob, name) {
+			// First try a.download, then web filesystem, then object URLs
+			var
+				  filesaver = this
+				, type = blob.type
+				, blob_changed = false
+				, object_url
+				, target_view
+				, get_object_url = function() {
+					var object_url = get_URL().createObjectURL(blob);
+					deletion_queue.push(object_url);
+					return object_url;
+				}
+				, dispatch_all = function() {
+					dispatch(filesaver, "writestart progress write writeend".split(" "));
+				}
+				// on any filesys errors revert to saving with object URLs
+				, fs_error = function() {
+					// don't create more object URLs than needed
+					if (blob_changed || !object_url) {
+						object_url = get_object_url(blob);
+					}
+					if (target_view) {
+						target_view.location.href = object_url;
+					} else {
+						window.open(object_url, "_blank");
+					}
+					filesaver.readyState = filesaver.DONE;
+					dispatch_all();
+				}
+				, abortable = function(func) {
+					return function() {
+						if (filesaver.readyState !== filesaver.DONE) {
+							return func.apply(this, arguments);
+						}
+					};
+				}
+				, create_if_not_found = {create: true, exclusive: false}
+				, slice
+			;
+			filesaver.readyState = filesaver.INIT;
+			if (!name) {
+				name = "download";
+			}
+			if (can_use_save_link) {
+				object_url = get_object_url(blob);
+				// FF for Android has a nasty garbage collection mechanism
+				// that turns all objects that are not pure javascript into 'deadObject'
+				// this means `doc` and `save_link` are unusable and need to be recreated
+				// `view` is usable though:
+				doc = view.document;
+				save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a");
+				save_link.href = object_url;
+				save_link.download = name;
+				var event = doc.createEvent("MouseEvents");
+				event.initMouseEvent(
+					"click", true, false, view, 0, 0, 0, 0, 0
+					, false, false, false, false, 0, null
+				);
+				save_link.dispatchEvent(event);
+				filesaver.readyState = filesaver.DONE;
+				dispatch_all();
+				return;
+			}
+			// Object and web filesystem URLs have a problem saving in Google Chrome when
+			// viewed in a tab, so I force save with application/octet-stream
+			// http://code.google.com/p/chromium/issues/detail?id=91158
+			if (view.chrome && type && type !== force_saveable_type) {
+				slice = blob.slice || blob.webkitSlice;
+				blob = slice.call(blob, 0, blob.size, force_saveable_type);
+				blob_changed = true;
+			}
+			// Since I can't be sure that the guessed media type will trigger a download
+			// in WebKit, I append .download to the filename.
+			// https://bugs.webkit.org/show_bug.cgi?id=65440
+			if (webkit_req_fs && name !== "download") {
+				name += ".download";
+			}
+			if (type === force_saveable_type || webkit_req_fs) {
+				target_view = view;
+			}
+			if (!req_fs) {
+				fs_error();
+				return;
+			}
+			fs_min_size += blob.size;
+			req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
+				fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
+					var save = function() {
+						dir.getFile(name, create_if_not_found, abortable(function(file) {
+							file.createWriter(abortable(function(writer) {
+								writer.onwriteend = function(event) {
+									target_view.location.href = file.toURL();
+									deletion_queue.push(file);
+									filesaver.readyState = filesaver.DONE;
+									dispatch(filesaver, "writeend", event);
+								};
+								writer.onerror = function() {
+									var error = writer.error;
+									if (error.code !== error.ABORT_ERR) {
+										fs_error();
+									}
+								};
+								"writestart progress write abort".split(" ").forEach(function(event) {
+									writer["on" + event] = filesaver["on" + event];
+								});
+								writer.write(blob);
+								filesaver.abort = function() {
+									writer.abort();
+									filesaver.readyState = filesaver.DONE;
+								};
+								filesaver.readyState = filesaver.WRITING;
+							}), fs_error);
+						}), fs_error);
+					};
+					dir.getFile(name, {create: false}, abortable(function(file) {
+						// delete file if it already exists
+						file.remove();
+						save();
+					}), abortable(function(ex) {
+						if (ex.code === ex.NOT_FOUND_ERR) {
+							save();
+						} else {
+							fs_error();
+						}
+					}));
+				}), fs_error);
+			}), fs_error);
+		}
+		, FS_proto = FileSaver.prototype
+		, saveAs = function(blob, name) {
+			return new FileSaver(blob, name);
+		}
+	;
+	FS_proto.abort = function() {
+		var filesaver = this;
+		filesaver.readyState = filesaver.DONE;
+		dispatch(filesaver, "abort");
+	};
+	FS_proto.readyState = FS_proto.INIT = 0;
+	FS_proto.WRITING = 1;
+	FS_proto.DONE = 2;
+
+	FS_proto.error =
+	FS_proto.onwritestart =
+	FS_proto.onprogress =
+	FS_proto.onwrite =
+	FS_proto.onabort =
+	FS_proto.onerror =
+	FS_proto.onwriteend =
+		null;
+
+	view.addEventListener("unload", process_deletion_queue, false);
+	saveAs.unload = function() {
+		process_deletion_queue();
+		view.removeEventListener("unload", process_deletion_queue, false);
+	};
+	return saveAs;
+}(
+	   typeof self !== "undefined" && self
+	|| typeof window !== "undefined" && window
+	|| this.content
+));
+// `self` is undefined in Firefox for Android content script context
+// while `this` is nsIContentFrameMessageManager
+// with an attribute `content` that corresponds to the window
+
+if (typeof module !== "undefined" && module !== null) {
+  module.exports = saveAs;
+} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) {
+  define([], function() {
+    return saveAs;
+  });
+}
--- a/server/src/main/webapp/static/js/config.js	Fri May 16 17:36:21 2014 +0200
+++ b/server/src/main/webapp/static/js/config.js	Sun May 18 10:31:12 2014 +0200
@@ -15,6 +15,7 @@
 	   rcolor: 'static/lib/rcolor',
 	   underscore: 'static/lib/underscore-min',
 	   jquery: 'static/lib/jquery.min',
+	   filesaver: 'static/lib/FileSaver',
 	},
 	packages:[{
 		name: 'dojo',