# HG changeset patch # User ymh # Date 1400401872 -7200 # Node ID 7de2652f7ee8afc516694b330983cd318dcc2f14 # Parent ded85569cb981a11852ad6bea05fdac6b9fb5a86 SaveAs project client side diff -r ded85569cb98 -r 7de2652f7ee8 client/css/renkan.css --- 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; } diff -r ded85569cb98 -r 7de2652f7ee8 client/gruntfile.js --- 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:" } } } diff -r ded85569cb98 -r 7de2652f7ee8 client/js/defaults.js --- 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 - + /* */ - + }; diff -r ded85569cb98 -r 7de2652f7ee8 client/js/main-renderer.js --- 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' } }); diff -r ded85569cb98 -r 7de2652f7ee8 client/js/renderer/scene.js --- 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 @@ '
<%-translate("Add Node")%>
<% } %>' + '<% if (options.show_addedge_button) { %>
' + '
<%-translate("Add Edge")%>
<% } %>' + + '<% if (options.show_export_button) { %>
<%-translate("Download Project")%>
<% } %>' + '<% if (options.show_save_button) { %>
<% } %>' + '<% if (options.show_open_button) { %>
<%-translate("Open Project")%>
<% } %>' + '<% if (options.show_bookmarklet) { %>
' + @@ -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"); diff -r ded85569cb98 -r 7de2652f7ee8 client/lib/FileSaver.js --- /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; + }); +} diff -r ded85569cb98 -r 7de2652f7ee8 server/src/main/webapp/static/js/config.js --- 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',