diff --git a/examples/common/examples.js b/examples/common/examples.js index 557b9f2059..89629cb74b 100644 --- a/examples/common/examples.js +++ b/examples/common/examples.js @@ -1,39 +1,4 @@ -var exampleUtils = { - /* Decode query components into a dictionary of values. - * - * @returns {object}: the query parameters as a dictionary. - */ - getQuery: function () { - var query = document.location.search.replace(/(^\?)/, '').split( - '&').map(function (n) { - n = n.split('='); - if (n[0]) { - this[decodeURIComponent(n[0].replace(/\+/g, '%20'))] = decodeURIComponent(n[1].replace(/\+/g, '%20')); - } - return this; - }.bind({}))[0]; - return query; - }, - - /* Encode a dictionary of parameters to the query string, setting the window - * location and history. This will also remove undefined values from the - * set properites of params. - * - * @param {object} params: the query parameters as a dictionary. - */ - setQuery: function (params) { - $.each(params, function (key, value) { - if (value === undefined) { - delete params[key]; - } - }); - var newurl = window.location.protocol + '//' + window.location.host + - window.location.pathname + '?' + $.param(params); - window.history.replaceState(params, '', newurl); - } -}; - -window.utils = exampleUtils; +window.utils = require('./utils'); /* Add a function to take a screenshot. Show the screenshot so that a user can * click on it to save it or right-click to copy it. */ diff --git a/examples/common/utils.js b/examples/common/utils.js new file mode 100644 index 0000000000..d73dd521cf --- /dev/null +++ b/examples/common/utils.js @@ -0,0 +1,36 @@ +var exampleUtils = { + /* Decode query components into a dictionary of values. + * + * @returns {object}: the query parameters as a dictionary. + */ + getQuery: function () { + var query = document.location.search.replace(/(^\?)/, '').split( + '&').map(function (n) { + n = n.split('='); + if (n[0]) { + this[decodeURIComponent(n[0].replace(/\+/g, '%20'))] = decodeURIComponent(n[1].replace(/\+/g, '%20')); + } + return this; + }.bind({}))[0]; + return query; + }, + + /* Encode a dictionary of parameters to the query string, setting the window + * location and history. This will also remove undefined values from the + * set properites of params. + * + * @param {object} params: the query parameters as a dictionary. + */ + setQuery: function (params) { + $.each(params, function (key, value) { + if (value === undefined) { + delete params[key]; + } + }); + var newurl = window.location.protocol + '//' + window.location.host + + window.location.pathname + '?' + $.param(params); + window.history.replaceState(params, '', newurl); + } +}; + +module.exports = exampleUtils; diff --git a/package.json b/package.json index 78af71a650..e655390d17 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "mousetrap": "^1.6.0", "nib": "^1.1.2", "node-resemble": "^2.0.1", + "pako": "^1.0.6", "phantomjs-prebuilt": "^2.1.5", "proj4": "2.3.16", "pug": "2.0.0-rc.2", diff --git a/tutorials/basic/index.pug b/tutorials/basic/index.pug index 026c39c06d..0c03716266 100644 --- a/tutorials/basic/index.pug +++ b/tutorials/basic/index.pug @@ -30,6 +30,7 @@ block mainTutorial height: 100%; padding: 0; margin: 0; + overflow: hidden; } :markdown-it diff --git a/tutorials/common/tutorials.css b/tutorials/common/tutorials.css index b238b064e4..043bea14eb 100644 --- a/tutorials/common/tutorials.css +++ b/tutorials/common/tutorials.css @@ -12,6 +12,7 @@ html, body { width: 100%; height: 100%; overflow: hidden; + display: flex; } .navbar #maincontent { height: calc(100% - 60px); @@ -28,9 +29,12 @@ html, body { overflow-y: auto; padding: 0.5em; background: ivory; + resize: horizontal; + min-width: 10em; } #workframe { overflow: hidden; + flex-grow: 100; } .codeblock .codeblock_entry { width: calc(100% - 6em); diff --git a/tutorials/common/tutorials.js b/tutorials/common/tutorials.js index 243f58a5de..3767b5afde 100644 --- a/tutorials/common/tutorials.js +++ b/tutorials/common/tutorials.js @@ -1,4 +1,6 @@ var $ = require('jquery'); +var pako = require('pako'); +var utils = require('../../examples/common/utils'); /* Track the last selector used for generating a code block and also a timer so * we can avoid rendering the same codeblock if it is actively being edited. */ @@ -26,6 +28,7 @@ var processBlockInfo = { ' height: 100%;\n' + ' padding: 0;\n' + ' margin: 0;\n' + + ' overflow: hidden;\n' + '}' }; @@ -156,6 +159,7 @@ function process_block(selector) { window.tutorial = targetelem[0].contentWindow; window.tutorials = window.tutorials || {}; window.tutorials[target] = targetelem[0].contentWindow; + elem.trigger('geojs-tutorial-run'); } } @@ -187,13 +191,101 @@ function process_block_debounce(selector, debounce) { }, 2000); } +/** + * Run any default code blocks, then start listening for changes in code + * blocks. + */ +function run_tutorial() { + /* If any of the codeblocks is marked 'default', run them. Do this in a + * timeout so that other start up scripts can run */ + $('.codeblock[initial="true"]').each(function (idx, elem) { + run_block(elem); + }); + /* Whenever a code block changes, run it with its parents */ + $('.codeblock textarea').bind('input propertychange', function (evt) { + run_block(evt.target, true, true); + }); + $('.codeblock .CodeMirror').each(function () { + $(this)[0].CodeMirror.on('change', function (elem) { + run_block(elem.getWrapperElement(), true, true); + }); + }); + /* Bind run and reset buttons */ + $('.codeblock_reset').click(function (evt) { + var elem = $('textarea', $(evt.target).closest('.codeblock')); + elem.val(elem.attr('defaultvalue')); + $('.CodeMirror', $(evt.target).closest('.codeblock'))[0].CodeMirror.setValue(elem.attr('defaultvalue')); + run_block(evt.target, true); + }); + $('.codeblock_run').click(function (evt) { + run_block(evt.target, undefined, undefined, evt.shiftKey); + }); +} + +/** + * Get query parameters for each step and update them as we start. Monitor the + * code blocks and update the url when they change if appropriate. + * + * @param {boolean} alwaysKeep If `true`, update the url even if the `keep` url + * parameter is not specified. + */ +function start_keeper(alwaysKeep) { + var query = utils.getQuery(), + keep = query.keep || (alwaysKeep === true); + + $('.codeblock').each(function () { + var block = $(this), + key = 'src' + (block.attr('step') !== '1' ? block.attr('step') : ''); + if (query[key]) { + try { + /* Strip out white space and pluses, then convert ., -, and _ to /, +, + * =. By removing whitespace, the url is more robust against email + * handling. The others keep things short. */ + var src = atob(query[key].replace(/(\s|\+)/g, '').replace(/\./g, '/').replace(/-/g, '+').replace(/_/g, '=')); + src = pako.inflate(src, {to: 'string', raw: true}); + if ($('.CodeMirror', block).length) { + $('.CodeMirror', block)[0].CodeMirror.setValue(src); + } else { + $('textarea', block).val(src); + } + } catch (err) { } + } + }); + if (keep) { + $(document).on('geojs-tutorial-run', '.codeblock', function () { + var newQuery = {}; + if (query.keep && alwaysKeep !== true) { + newQuery.keep = query.keep; + } + $('.codeblock').each(function () { + var block = $(this), + defaultSrc = $('textarea', block).attr('defaultvalue'), + key = 'src' + (block.attr('step') !== '1' ? block.attr('step') : ''); + + var src = $('.CodeMirror', block).length ? $('.CodeMirror', block)[0].CodeMirror.getValue() : $('textarea', block).val().trim(); + if (src !== defaultSrc) { + var comp = btoa(pako.deflate(src, {to: 'string', level: 9, raw: true})); + /* instead of using regular base64, convert /, +, and = to ., -, and _ + * so that they don't need to be escaped on the url. This reduces the + * average length of the url by 6 percent. */ + comp = comp.replace(/\//g, '.').replace(/\+/g, '-').replace(/=/g, '_'); + newQuery[key] = comp; + } + }); + utils.setQuery(newQuery); + }); + } +} + /** * Process code blocks to remove unwanted white space and store default values, * set up event handling, and run any initial code blocks. * * @param {boolean} useCodeMirror If explicitly false, don't use CodeMirror. + * @param {boolean} alwaysKeep If `true`, update the url even if the `keep` url + * parameter is not specified. */ -function start_tutorial(useCodeMirror) { +function start_tutorial(useCodeMirror, alwaysKeep) { /* clean up whitespace and store a default value for each code block */ $('.codeblock').each(function () { var elem = $('textarea', this), @@ -225,29 +317,8 @@ function start_tutorial(useCodeMirror) { } /* Check if iframe srcdoc support is present */ processBlockInfo.srcdocSupport = !!('srcdoc' in document.createElement('iframe')); - /* If any of the codeblocks is marked 'default', run them */ - $('.codeblock[initial="true"]').each(function (idx, elem) { - run_block(elem); - }); - /* Whenever a code block changes, run it with its parents */ - $('.codeblock textarea').bind('input propertychange', function (evt) { - run_block(evt.target, true, true); - }); - $('.codeblock .CodeMirror').each(function () { - $(this)[0].CodeMirror.on('change', function (elem) { - run_block(elem.getWrapperElement(), true, true); - }); - }); - /* Bind run and reset buttons */ - $('.codeblock_reset').click(function (evt) { - var elem = $('textarea', $(evt.target).closest('.codeblock')); - elem.val(elem.attr('defaultvalue')); - $('.CodeMirror', $(evt.target).closest('.codeblock'))[0].CodeMirror.setValue(elem.attr('defaultvalue')); - run_block(evt.target, true); - }); - $('.codeblock_run').click(function (evt) { - run_block(evt.target, undefined, undefined, evt.shiftKey); - }); + start_keeper(alwaysKeep); + run_tutorial(); } module.exports = start_tutorial; diff --git a/tutorials/editor/editor.js b/tutorials/editor/editor.js new file mode 100644 index 0000000000..4ac38243df --- /dev/null +++ b/tutorials/editor/editor.js @@ -0,0 +1,7 @@ +/* global start_tutorial */ + +$(function () { + /* start_tutorial has to be called explicitly when we are ready since we need + * to ask it to always keep url changes. */ + start_tutorial(undefined, true); +}); diff --git a/tutorials/editor/index.pug b/tutorials/editor/index.pug new file mode 100644 index 0000000000..4c8e73c3df --- /dev/null +++ b/tutorials/editor/index.pug @@ -0,0 +1,21 @@ +extends ../common/index.pug + +block mainTutorial + :markdown-it + # Editor + Any changes made will be stored in the URL whenever the code is run. This can be sent as a link, bookmarked, or otherwise shared. + + You can interact with the code through the javascript console by accessing the top-level variables in the `tutorial` global parameter. + + +codeblock('javascript', 1, undefined, true). + var map = geo.map({ + node: "#map", + center: {x: 4.90, y: 52.37}, + zoom: 14 + }); + var layer = map.createLayer('osm'); + +codeblock_test('map has one osm layer from openstreetmap', [ + 'map.layers().length === 1', + 'map.layers()[0] instanceof geo.osmLayer', + 'layer.url().match(/openstreetmap/)' + ]) diff --git a/tutorials/editor/thumb.jpg b/tutorials/editor/thumb.jpg new file mode 100644 index 0000000000..2281869765 Binary files /dev/null and b/tutorials/editor/thumb.jpg differ diff --git a/tutorials/editor/tutorial.json b/tutorials/editor/tutorial.json new file mode 100644 index 0000000000..641276455a --- /dev/null +++ b/tutorials/editor/tutorial.json @@ -0,0 +1,9 @@ +{ + "title": "Editor", + "hideNavbar": true, + "level": 10, + "tutorialJs": ["editor.js"], + "about": { + "text": "Edit and save work in URL." + } +}