diff --git a/bower.json b/bower.json index 057ae8f1..4aef8807 100644 --- a/bower.json +++ b/bower.json @@ -24,6 +24,7 @@ "bootstrap": "^3.3.7", "selectize": "^0.12.2", "eventEmitter": "^5.1.0", - "lz-string": "^1.4.4" + "lz-string": "^1.4.4", + "clipboard": "^1.5.12" } } diff --git a/static/clippy.svg b/static/clippy.svg new file mode 100644 index 00000000..e1b17035 --- /dev/null +++ b/static/clippy.svg @@ -0,0 +1,3 @@ + diff --git a/static/explorer.css b/static/explorer.css index 31c315de..537339d3 100644 --- a/static/explorer.css +++ b/static/explorer.css @@ -176,7 +176,17 @@ span.icon { .status.warning { color: #d7d800; } + .navbar { margin-bottom: 2px; border-radius: 0; -} \ No newline at end of file +} + +/* Override the cursor that Bootstrap defines for readonly inputs, as it looks + * very odd on IE (and 'text' makes it clearer that it's copyable everywhere). + */ +.permalink { + cursor: text; + display: inline; + width: 100%; +} diff --git a/static/index.html b/static/index.html index b091ff84..4160dc01 100644 --- a/static/index.html +++ b/static/index.html @@ -15,15 +15,15 @@
+ @@ -130,7 +130,23 @@ -// Type your code here, or load an example. +++ ++ + + + ++// Type your code here, or load an example. int square(int num) { return num * num; }diff --git a/static/main.js b/static/main.js index 839b8d74..2be111c7 100644 --- a/static/main.js +++ b/static/main.js @@ -33,7 +33,8 @@ require.config({ sifter: 'ext/sifter/sifter.min', microplugin: 'ext/microplugin/src/microplugin', events: 'ext/eventEmitter/EventEmitter', - lzstring: 'ext/lz-string/libs/lz-string' + lzstring: 'ext/lz-string/libs/lz-string', + clipboard: 'ext/clipboard/dist/clipboard' }, packages: [{ name: "codemirror", @@ -56,6 +57,7 @@ define(function (require) { var compiler = require('compiler'); var editor = require('editor'); var url = require('url'); + var clipboard = require('clipboard'); var Hub = require('hub'); analytics.initialise(); @@ -71,8 +73,14 @@ define(function (require) { }; var root = $("#root"); var config = url.deserialiseState(window.location.hash.substr(1)); + if (config) { + // replace anything in the default config with that from the hash + config = _.extend(defaultConfig, config); + } $(window).bind('hashchange', function () { - window.location.reload(); // punt on hash events and just reload the page + // punt on hash events and just reload the page if there's a hash + if (window.location.hash.substr(1)) + window.location.reload(); }); if (!config) { @@ -81,6 +89,7 @@ define(function (require) { config = savedState !== null ? JSON.parse(savedState) : defaultConfig; } + console.log(config); var layout = new GoldenLayout(config, root); layout.on('stateChanged', function () { var state = JSON.stringify(layout.toConfig()); @@ -97,4 +106,42 @@ define(function (require) { $(window).resize(sizeRoot); sizeRoot(); + + new clipboard('.btn.clippy'); + + // TODO: promises? + function permalink() { + var config = layout.toConfig(); + return window.location.href.split('#')[0] + '#' + url.serialiseState(config); + } + + function popover() { + var elem = $(".urls.template").clone(); + _.defer(function () { + $(".permalink:visible").val(permalink()); + }); + return elem.html(); + } + + var getLink = $("#get-link").popover({ + container: 'body', + content: popover, + html: true, + placement: 'bottom', + trigger: 'manual' + }).click(function () { + getLink.popover('show'); + }); + + $(document).on('keyup.editable', function (e) { + if (e.which === 27) { + getLink.popover("hide"); + } + }); + + $(document).on('click.editable', function (e) { + var target = $(e.target); + if (!target.is(getLink) && target.closest('.popover').length === 0) + getLink.popover("hide"); + }); }); \ No newline at end of file diff --git a/static/url.js b/static/url.js index bdadbf88..596306b0 100644 --- a/static/url.js +++ b/static/url.js @@ -25,6 +25,7 @@ define(function (require) { "use strict"; + var GoldenLayout = require('goldenlayout'); var rison = require('rison'); var $ = require('jquery'); var editor = require('editor'); @@ -62,8 +63,9 @@ define(function (require) { /* falls through */ case 3: state = convertOldState(state); - break; + break; // no fall through case 4: + state = GoldenLayout.unminifyConfig(state); break; default: return false; @@ -71,13 +73,25 @@ define(function (require) { return state; } + function risonify(obj) { + return rison.quote(rison.encode_object(obj)); + } + + function unrisonify(text) { + return rison.decode_object(decodeURIComponent(text.replace(/\+/g, '%20'))); + } + function deserialiseState(stateText) { var state; try { - state = rison.decode_object(decodeURIComponent(stateText.replace(/\+/g, '%20'))); + state = unrisonify(stateText); + if (state && state.z) { + state = unrisonify(lzstring.decompressFromBase64(state.z)); + } } catch (ignored) { } + if (!state) { try { state = $.parseJSON(decodeURIComponent(stateText)); @@ -88,10 +102,16 @@ define(function (require) { } function serialiseState(stateText) { - // convert arrays to objects - // filter out anything barring content from non-leaves - // filter out anything barring type, componentName, componentState from leaves - // compress the whole thing if savings made? + var ctx = GoldenLayout.minifyConfig({content: stateText.content}); + ctx.version = 4; + var uncompressed = risonify(ctx); + var compressed = risonify({z: lzstring.compressToBase64(uncompressed)}); + var MinimalSavings = 0.20; // at least this ratio smaller + if (compressed.length < uncompressed.length * (1.0 - MinimalSavings)) { + return compressed; + } else { + return uncompressed; + } } return {