From 69cfa789611bdb3e17152c1e81fc4a5a8bfcf863 Mon Sep 17 00:00:00 2001 From: Chris Broadfoot Date: Tue, 7 Feb 2017 10:29:28 -0800 Subject: [PATCH] static: add optional save shortcut support to playground If enabled (via the "enableShortcuts" option), Ctrl+S/Cmd+S now saves/shares the playground snippet and initiates a download. Credit to Alexander Kucherenko (see golang.org/cl/35950). Updates golang/go#15378. Change-Id: I8a2d733c25e4c95787fbe1f5ac00fa1befbb4693 Reviewed-on: https://go-review.googlesource.com/36486 Reviewed-by: Andrew Gerrand --- godoc/static/playground.js | 112 ++++++++++++++++++++++++------------- godoc/static/static.go | 112 ++++++++++++++++++++++++------------- 2 files changed, 144 insertions(+), 80 deletions(-) diff --git a/godoc/static/playground.js b/godoc/static/playground.js index 93dea15c9..66d0dc44e 100644 --- a/godoc/static/playground.js +++ b/godoc/static/playground.js @@ -169,8 +169,9 @@ function PlaygroundOutput(el) { cl = write.Kind; var m = write.Body; - if (write.Kind == 'end') + if (write.Kind == 'end') { m = '\nProgram exited' + (m?(': '+m):'.'); + } if (m.indexOf('IMAGE:') === 0) { // TODO(adg): buffer all writes before creating image @@ -235,11 +236,12 @@ function PlaygroundOutput(el) { // toysEl - toys select element (optional) // enableHistory - enable using HTML5 history API (optional) // transport - playground transport to use (default is HTTPTransport) + // enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false) function playground(opts) { var code = $(opts.codeEl); var transport = opts['transport'] || new HTTPTransport(); var running; - + // autoindent helpers. function insertTabs(n) { // find the selection start and end @@ -273,8 +275,26 @@ function PlaygroundOutput(el) { insertTabs(tabs); }, 1); } - + + // NOTE(cbro): e is a jQuery event, not a DOM event. + function handleSaveShortcut(e) { + if (e.isDefaultPrevented()) return false; + if (!e.metaKey && !e.ctrlKey) return false; + if (e.key != "S" && e.key != "s") return false; + + e.preventDefault(); + + // Share and save + share(function(url) { + window.location.href = url + ".go?download=true"; + }); + + return true; + } + function keyHandler(e) { + if (opts.enableShortcuts && handleSaveShortcut(e)) return; + if (e.keyCode == 9 && !e.ctrlKey) { // tab (but not ctrl-tab) insertTabs(1); e.preventDefault(); @@ -297,7 +317,7 @@ function PlaygroundOutput(el) { code.unbind('keydown').bind('keydown', keyHandler); var outdiv = $(opts.outputEl).empty(); var output = $('
').appendTo(outdiv);
-  
+
     function body() {
       return $(opts.codeEl).val();
     }
@@ -307,7 +327,7 @@ function PlaygroundOutput(el) {
     function origin(href) {
       return (""+href).split("/").slice(0, 3).join("/");
     }
-  
+
     var pushedEmpty = (window.location.pathname == "/");
     function inputChanged() {
       if (pushedEmpty) {
@@ -350,7 +370,7 @@ function PlaygroundOutput(el) {
 
     function fmt() {
       loading();
-      var data = {"body": body()}; 
+      var data = {"body": body()};
       if ($(opts.fmtImportEl).is(":checked")) {
         data["imports"] = "true";
       }
@@ -369,48 +389,60 @@ function PlaygroundOutput(el) {
       });
     }
 
+    var shareURL; // jQuery element to show the shared URL.
+    var sharing = false; // true if there is a pending request.
+    var shareCallbacks = [];
+    function share(opt_callback) {
+      if (opt_callback) shareCallbacks.push(opt_callback);
+
+      if (sharing) return;
+      sharing = true;
+
+      var sharingData = body();
+      $.ajax("/share", {
+        processData: false,
+        data: sharingData,
+        type: "POST",
+        complete: function(xhr) {
+          sharing = false;
+          if (xhr.status != 200) {
+            alert("Server error; try again.");
+            return;
+          }
+          if (opts.shareRedirect) {
+            window.location = opts.shareRedirect + xhr.responseText;
+          }
+          var path = "/p/" + xhr.responseText;
+          var url = origin(window.location) + path;
+
+          for (var i = 0; i < shareCallbacks.length; i++) {
+            shareCallbacks[i](url);
+          }
+          shareCallbacks = [];
+
+          if (shareURL) {
+            shareURL.show().val(url).focus().select();
+
+            if (rewriteHistory) {
+              var historyData = {"code": sharingData};
+              window.history.pushState(historyData, "", path);
+              pushedEmpty = false;
+            }
+          }
+        }
+      });
+    }
+
     $(opts.runEl).click(run);
     $(opts.fmtEl).click(fmt);
 
     if (opts.shareEl !== null && (opts.shareURLEl !== null || opts.shareRedirect !== null)) {
-      var shareURL;
       if (opts.shareURLEl) {
         shareURL = $(opts.shareURLEl).hide();
       }
-      var sharing = false;
-      $(opts.shareEl).click(function() {
-        if (sharing) return;
-        sharing = true;
-        var sharingData = body();
-        $.ajax("/share", {
-          processData: false,
-          data: sharingData,
-          type: "POST",
-          complete: function(xhr) {
-            sharing = false;
-            if (xhr.status != 200) {
-              alert("Server error; try again.");
-              return;
-            }
-            if (opts.shareRedirect) {
-              window.location = opts.shareRedirect + xhr.responseText;
-            }
-            if (shareURL) {
-              var path = "/p/" + xhr.responseText;
-              var url = origin(window.location) + path;
-              shareURL.show().val(url).focus().select();
-  
-              if (rewriteHistory) {
-                var historyData = {"code": sharingData};
-                window.history.pushState(historyData, "", path);
-                pushedEmpty = false;
-              }
-            }
-          }
-        });
-      });
+      $(opts.shareEl).click(share);
     }
-  
+
     if (opts.toysEl !== null) {
       $(opts.toysEl).bind('change', function() {
         var toy = $(this).val();
diff --git a/godoc/static/static.go b/godoc/static/static.go
index 2d478afc0..6478a9af1 100644
--- a/godoc/static/static.go
+++ b/godoc/static/static.go
@@ -2379,8 +2379,9 @@ function PlaygroundOutput(el) {
 			cl = write.Kind;
 
 		var m = write.Body;
-		if (write.Kind == 'end') 
+		if (write.Kind == 'end') {
 			m = '\nProgram exited' + (m?(': '+m):'.');
+		}
 
 		if (m.indexOf('IMAGE:') === 0) {
 			// TODO(adg): buffer all writes before creating image
@@ -2445,11 +2446,12 @@ function PlaygroundOutput(el) {
   //  toysEl - toys select element (optional)
   //  enableHistory - enable using HTML5 history API (optional)
   //  transport - playground transport to use (default is HTTPTransport)
+  //  enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false)
   function playground(opts) {
     var code = $(opts.codeEl);
     var transport = opts['transport'] || new HTTPTransport();
     var running;
-  
+
     // autoindent helpers.
     function insertTabs(n) {
       // find the selection start and end
@@ -2483,8 +2485,26 @@ function PlaygroundOutput(el) {
         insertTabs(tabs);
       }, 1);
     }
-  
+
+    // NOTE(cbro): e is a jQuery event, not a DOM event.
+    function handleSaveShortcut(e) {
+      if (e.isDefaultPrevented()) return false;
+      if (!e.metaKey && !e.ctrlKey) return false;
+      if (e.key != "S" && e.key != "s") return false;
+
+      e.preventDefault();
+
+      // Share and save
+      share(function(url) {
+        window.location.href = url + ".go?download=true";
+      });
+
+      return true;
+    }
+
     function keyHandler(e) {
+      if (opts.enableShortcuts && handleSaveShortcut(e)) return;
+
       if (e.keyCode == 9 && !e.ctrlKey) { // tab (but not ctrl-tab)
         insertTabs(1);
         e.preventDefault();
@@ -2507,7 +2527,7 @@ function PlaygroundOutput(el) {
     code.unbind('keydown').bind('keydown', keyHandler);
     var outdiv = $(opts.outputEl).empty();
     var output = $('
').appendTo(outdiv);
-  
+
     function body() {
       return $(opts.codeEl).val();
     }
@@ -2517,7 +2537,7 @@ function PlaygroundOutput(el) {
     function origin(href) {
       return (""+href).split("/").slice(0, 3).join("/");
     }
-  
+
     var pushedEmpty = (window.location.pathname == "/");
     function inputChanged() {
       if (pushedEmpty) {
@@ -2560,7 +2580,7 @@ function PlaygroundOutput(el) {
 
     function fmt() {
       loading();
-      var data = {"body": body()}; 
+      var data = {"body": body()};
       if ($(opts.fmtImportEl).is(":checked")) {
         data["imports"] = "true";
       }
@@ -2579,48 +2599,60 @@ function PlaygroundOutput(el) {
       });
     }
 
+    var shareURL; // jQuery element to show the shared URL.
+    var sharing = false; // true if there is a pending request.
+    var shareCallbacks = [];
+    function share(opt_callback) {
+      if (opt_callback) shareCallbacks.push(opt_callback);
+
+      if (sharing) return;
+      sharing = true;
+
+      var sharingData = body();
+      $.ajax("/share", {
+        processData: false,
+        data: sharingData,
+        type: "POST",
+        complete: function(xhr) {
+          sharing = false;
+          if (xhr.status != 200) {
+            alert("Server error; try again.");
+            return;
+          }
+          if (opts.shareRedirect) {
+            window.location = opts.shareRedirect + xhr.responseText;
+          }
+          var path = "/p/" + xhr.responseText;
+          var url = origin(window.location) + path;
+
+          for (var i = 0; i < shareCallbacks.length; i++) {
+            shareCallbacks[i](url);
+          }
+          shareCallbacks = [];
+
+          if (shareURL) {
+            shareURL.show().val(url).focus().select();
+
+            if (rewriteHistory) {
+              var historyData = {"code": sharingData};
+              window.history.pushState(historyData, "", path);
+              pushedEmpty = false;
+            }
+          }
+        }
+      });
+    }
+
     $(opts.runEl).click(run);
     $(opts.fmtEl).click(fmt);
 
     if (opts.shareEl !== null && (opts.shareURLEl !== null || opts.shareRedirect !== null)) {
-      var shareURL;
       if (opts.shareURLEl) {
         shareURL = $(opts.shareURLEl).hide();
       }
-      var sharing = false;
-      $(opts.shareEl).click(function() {
-        if (sharing) return;
-        sharing = true;
-        var sharingData = body();
-        $.ajax("/share", {
-          processData: false,
-          data: sharingData,
-          type: "POST",
-          complete: function(xhr) {
-            sharing = false;
-            if (xhr.status != 200) {
-              alert("Server error; try again.");
-              return;
-            }
-            if (opts.shareRedirect) {
-              window.location = opts.shareRedirect + xhr.responseText;
-            }
-            if (shareURL) {
-              var path = "/p/" + xhr.responseText;
-              var url = origin(window.location) + path;
-              shareURL.show().val(url).focus().select();
-  
-              if (rewriteHistory) {
-                var historyData = {"code": sharingData};
-                window.history.pushState(historyData, "", path);
-                pushedEmpty = false;
-              }
-            }
-          }
-        });
-      });
+      $(opts.shareEl).click(share);
     }
-  
+
     if (opts.toysEl !== null) {
       $(opts.toysEl).bind('change', function() {
         var toy = $(this).val();