diff --git a/src/js/regexper.js b/src/js/regexper.js index 26e4ff3..209620d 100644 --- a/src/js/regexper.js +++ b/src/js/regexper.js @@ -1,3 +1,6 @@ +// The Regexper class manages the top-level behavior for the entire +// application. This includes event handlers for all user interactions. + import util from './util.js'; import Parser from './parser/javascript.js'; import _ from 'lodash'; @@ -17,7 +20,9 @@ export default class Regexper { this.svgContainer = root.querySelector('#regexp-render'); } + // Event handler for key presses in the regular expression form field. keypressListener(event) { + // Pressing Shift-Enter displays the expression. if (event.shiftKey && event.keyCode === 13) { event.returnValue = false; if (event.preventDefault) { @@ -28,12 +33,16 @@ export default class Regexper { } } + // Event handler for key presses while focused anywhere in the application. documentKeypressListener(event) { + // Pressing escape will cancel a currently running render. if (event.keyCode === 27 && this.running) { this.running.cancel(); } } + // Event handler for submission of the regular expression. Changes the URL + // hash which leads to the expression being rendered. submitListener(event) { event.returnValue = false; if (event.preventDefault) { @@ -44,13 +53,14 @@ export default class Regexper { this._setHash(this.field.value); } catch(e) { - // Most likely failed to set the URL has (probably because the expression - // is too long). Turn off the permalink and just show the expression + // Failed to set the URL hash (probably because the expression is too + // long). Turn off display of the permalink and just show the expression. this.permalinkEnabled = false; this.showExpression(this.field.value); } } + // Event handler for URL hash changes. Starts rendering of the expression. hashchangeListener() { var expr = this._getHash(); @@ -64,6 +74,7 @@ export default class Regexper { } } + // Binds all event listeners. bindListeners() { this.field.addEventListener('keypress', this.keypressListener.bind(this)); this.form.addEventListener('submit', this.submitListener.bind(this)); @@ -71,10 +82,15 @@ export default class Regexper { window.addEventListener('hashchange', this.hashchangeListener.bind(this)); } + // Set the URL hash. This method exists to facilitate automated testing + // (since changing the URL can throw off most JavaScript testing tools). _setHash(hash) { location.hash = encodeURIComponent(hash); } + // Retrieve the current URL hash. This method is also mostly for supporting + // automated testing, but also does some basic error handling for malformed + // URLs. _getHash() { try { return decodeURIComponent(location.hash.slice(1)); @@ -84,6 +100,11 @@ export default class Regexper { } } + // Currently state of the application. Useful values are: + // - `''` - State of the application when the page initially loads + // - `'is-loading'` - Displays the loading indicator + // - `'has-error'` - Displays the error message + // - `'has-results'` - Displays rendered results set state(state) { this.root.className = state; } @@ -92,6 +113,9 @@ export default class Regexper { return this.root.className; } + // Start the rendering of a regular expression. + // + // - __expression__ - Regular expression to display. showExpression(expression) { this.field.value = expression; this.state = ''; @@ -101,25 +125,32 @@ export default class Regexper { } } + // Creates a blob URL for linking to a rendered regular expression image. + // + // - __content__ - SVG image markup. buildBlobURL(content) { - var blob = new Blob([content], { type: 'image/svg+xml' }); - window.blob = blob; // Blob object has to stick around for IE - return URL.createObjectURL(blob); + // Blob object has to stick around for IE, so the instance is stored on the + // `window` object. + window.blob = new Blob([content], { type: 'image/svg+xml' }); + return URL.createObjectURL(window.blob); } + // Update the URLs of the 'download' and 'permalink' links. updateLinks() { var classes = _.without(this.links.className.split(' '), ['hide-download', 'hide-permalink']); + // Create the 'download' image URL. try { this.download.parentNode.style.display = null; this.download.href = this.buildBlobURL(this.svgContainer.querySelector('.svg').innerHTML); } catch(e) { - // Blobs or URLs created from them don't work here. - // Giving up on the download link + // Blobs or URLs created from a blob URL don't work in the current + // browser. Giving up on the download link. classes.push('hide-download'); } + // Create the 'permalink' URL. if (this.permalinkEnabled) { this.permalink.parentNode.style.display = null; this.permalink.href = location.toString(); @@ -130,20 +161,28 @@ export default class Regexper { this.links.className = classes.join(' '); } + // Display any warnings that were generated while rendering a regular expression. + // + // - __warnings__ - Array of warning messages to display. displayWarnings(warnings) { this.warnings.innerHTML = _.map(warnings, warning => { return `
  • ${warning}
  • `; }).join(''); } + // Render regular expression + // + // - __expression__ - Regular expression to render renderRegexp(expression) { var parseError = false, startTime, endTime; + // When a render is already in progress, cancel it and try rendering again + // after a short delay (canceling a render is not instantaneous). if (this.running) { this.running.cancel(); - util.wait(10).then(() => { + return util.wait(10).then(() => { return this.renderRegexp(expression); }); } @@ -155,7 +194,9 @@ export default class Regexper { this.running = new Parser(this.svgContainer); return this.running + // Parse the expression. .parse(expression) + // Display any error messages from the parser and abort the render. .catch(message => { this.state = 'has-error'; this.error.innerHTML = ''; @@ -165,9 +206,14 @@ export default class Regexper { throw message; }) + // When parsing is successful, render the parsed expression. .then(parser => { return parser.render(); }) + // Once rendering is complete: + // - Update links + // - Display any warnings + // - Track the completion of the render and how long it took .then(() => { this.state = 'has-results'; this.updateLinks(); @@ -177,6 +223,9 @@ export default class Regexper { endTime = new Date().getTime(); window._gaq.push(['_trackTiming', 'visualization', 'total time', endTime - startTime]); }) + // Handle any errors that happened during the rendering pipeline. + // Swallows parse errors and render cancellations. Any other exceptions + // are allowed to continue on to be tracked by the global error handler. .catch(message => { if (message === 'Render cancelled') { window._gaq.push(['_trackEvent', 'visualization', 'cancelled']); @@ -187,6 +236,8 @@ export default class Regexper { throw message; } }) + // Finally, mark rendering as complete (and pass along any exceptions + // that were thrown). .then( () => { this.running = false;