From c64e48946effa7890bc8db659feda63b92cb248e Mon Sep 17 00:00:00 2001 From: Jeff Avallone Date: Wed, 17 Dec 2014 19:24:33 -0500 Subject: [PATCH] Adding support for canceling a parse/render --- spec/regexper_spec.js | 73 +++++++++++++++++++-- src/js/parser/javascript.js | 6 ++ src/js/parser/javascript/node.js | 108 +++++++++++++++++-------------- src/js/regexper.js | 13 ++++ 4 files changed, 147 insertions(+), 53 deletions(-) diff --git a/spec/regexper_spec.js b/spec/regexper_spec.js index 34835db..42629a7 100644 --- a/spec/regexper_spec.js +++ b/spec/regexper_spec.js @@ -96,6 +96,41 @@ describe('regexper.js', function() { }); + describe('#documentKeypressListener', function() { + + beforeEach(function() { + this.event = document.createEvent('Event'); + spyOn(parser, 'cancel'); + }); + + describe('when the keyCode is not 27 (Escape)', function() { + + beforeEach(function() { + this.event.keyCode = 42; + }); + + it('does not cancel the parser', function() { + this.regexper.documentKeypressListener(this.event); + expect(parser.cancel).not.toHaveBeenCalled(); + }); + + }); + + describe('when the keyCode is 27 (Escape)', function() { + + beforeEach(function() { + this.event.keyCode = 27; + }); + + it('cancels the parser', function() { + this.regexper.documentKeypressListener(this.event); + expect(parser.cancel).toHaveBeenCalled(); + }); + + }); + + }); + describe('#submitListener', function() { beforeEach(function() { @@ -168,9 +203,16 @@ describe('regexper.js', function() { describe('#bindListeners', function() { + beforeEach(function() { + spyOn(this.regexper, 'keypressListener'); + spyOn(this.regexper, 'submitListener'); + spyOn(this.regexper, 'updatePercentage'); + spyOn(this.regexper, 'documentKeypressListener'); + spyOn(this.regexper, 'hashchangeListener'); + }); + it('binds #keypressListener to keypress on the text field', function() { spyOn(this.regexper.field, 'addEventListener'); - spyOn(this.regexper, 'keypressListener'); this.regexper.bindListeners(); expect(this.regexper.field.addEventListener).toHaveBeenCalledWith('keypress', jasmine.any(Function)); @@ -180,7 +222,6 @@ describe('regexper.js', function() { it('binds #submitListener to submit on the form', function() { spyOn(this.regexper.form, 'addEventListener'); - spyOn(this.regexper, 'submitListener'); this.regexper.bindListeners(); expect(this.regexper.form.addEventListener).toHaveBeenCalledWith('submit', jasmine.any(Function)); @@ -190,17 +231,24 @@ describe('regexper.js', function() { it('binds #updatePercentage to updateStatus on the root', function() { spyOn(this.regexper.root, 'addEventListener'); - spyOn(this.regexper, 'updatePercentage'); this.regexper.bindListeners(); expect(this.regexper.root.addEventListener).toHaveBeenCalledWith('updateStatus', jasmine.any(Function)); - this.regexper.root.addEventListener.calls.mostRecent().args[1](); + this.regexper.root.addEventListener.calls.first().args[1](); expect(this.regexper.updatePercentage).toHaveBeenCalled(); }); + it('binds #documentKeypressListener to keyup on the root', function() { + spyOn(this.regexper.root, 'addEventListener'); + this.regexper.bindListeners(); + expect(this.regexper.root.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function)); + + this.regexper.root.addEventListener.calls.mostRecent().args[1](); + expect(this.regexper.documentKeypressListener).toHaveBeenCalled(); + }); + it('binds #hashchangeListener to hashchange on the window', function() { spyOn(window, 'addEventListener'); - spyOn(this.regexper, 'hashchangeListener'); this.regexper.bindListeners(); expect(window.addEventListener).toHaveBeenCalledWith('hashchange', jasmine.any(Function)); @@ -262,6 +310,21 @@ describe('regexper.js', function() { }); + describe('when the parser is cancelled', function() { + + beforeEach(function(done) { + spyOn(this.regexper, 'updateLinks'); + this.regexper.showExpression('example expression'); + this.renderPromise.reject('Render cancelled'); + setTimeout(done, 100); + }); + + it('clears the state', function() { + expect(this.regexper.state).toEqual(''); + }); + + }); + }); }); diff --git a/src/js/parser/javascript.js b/src/js/parser/javascript.js index 2713f8b..f7e1f9a 100644 --- a/src/js/parser/javascript.js +++ b/src/js/parser/javascript.js @@ -41,8 +41,14 @@ parser.Parser.RepeatSpec = { module: RepeatSpec }; parser.parse = (parse => { return function() { Subexp.resetCounter(); + Node.reset(); + return parse.apply(this, arguments); }; })(parser.parse); +parser.cancel = () => { + Node.cancelRender = true; +}; + export default parser; diff --git a/src/js/parser/javascript/node.js b/src/js/parser/javascript/node.js index b7a0892..dadd8b1 100644 --- a/src/js/parser/javascript/node.js +++ b/src/js/parser/javascript/node.js @@ -1,9 +1,6 @@ import _ from 'lodash'; import Q from 'q'; -var renderCounter = 0, - maxCounter = 0; - export default class Node { constructor(textValue, offset, elements, properties) { this.textValue = textValue; @@ -66,57 +63,69 @@ export default class Node { return this.container.transform(matrix); } - renderLabel(text) { + deferredStep() { var deferred = Q.defer(), - group = this.container.group() + result = arguments; + + setTimeout(() => { + if (Node.cancelRender) { + deferred.reject('Render cancelled'); + } else { + deferred.resolve.apply(this, result); + } + }, 1); + + return deferred.promise; + } + + renderLabel(text) { + var group = this.container.group() .addClass('label'), rect = group.rect(), text = group.text(0, 0, _.flatten([text])); - setTimeout(deferred.resolve.bind(deferred, group)); - deferred.promise.then(() => { - var box = text.getBBox(), - margin = 5; + return this.deferredStep(group) + .then(group => { + var box = text.getBBox(), + margin = 5; - text.transform(Snap.matrix() - .translate(margin, box.height / 2 + 2 * margin)); + text.transform(Snap.matrix() + .translate(margin, box.height / 2 + 2 * margin)); - rect.attr({ - width: box.width + 2 * margin, - height: box.height + 2 * margin + rect.attr({ + width: box.width + 2 * margin, + height: box.height + 2 * margin + }); + + return group; }); - }); - - return deferred.promise; } startRender() { - renderCounter++; + Node.renderCounter++; } doneRender() { - var evt, deferred = Q.defer(); + var evt; - if (maxCounter === 0) { - maxCounter = renderCounter; + if (Node.maxCounter === 0) { + Node.maxCounter = Node.renderCounter; } - renderCounter--; + Node.renderCounter--; evt = document.createEvent('Event'); evt.initEvent('updateStatus', true, true); evt.detail = { - percentage: (maxCounter - renderCounter) / maxCounter + percentage: (Node.maxCounter - Node.renderCounter) / Node.maxCounter }; document.body.dispatchEvent(evt); - if (renderCounter === 0) { - maxCounter = 0; + if (Node.renderCounter === 0) { + Node.maxCounter = 0; } - setTimeout(deferred.resolve.bind(deferred), 1); - - return deferred.promise; + return this.deferredStep(); } render(container) { @@ -191,8 +200,7 @@ export default class Node { } renderLabeledBox(label, content, options) { - var deferred = Q.defer(), - label = this.container.text() + var label = this.container.text() .addClass([this.type, 'label'].join('-')) .attr({ text: label @@ -211,26 +219,30 @@ export default class Node { this.container.prepend(label); this.container.prepend(box); - setTimeout(deferred.resolve); - deferred.promise.then(() => { - var labelBox = label.getBBox(), - contentBox = content.getBBox(); + return this.deferredStep() + .then(() => { + var labelBox = label.getBBox(), + contentBox = content.getBBox(); - label.transform(Snap.matrix() - .translate(0, labelBox.height)); + label.transform(Snap.matrix() + .translate(0, labelBox.height)); - box - .transform(Snap.matrix() - .translate(0, labelBox.height)) - .attr({ - width: Math.max(contentBox.width + options.padding * 2, labelBox.width), - height: contentBox.height + options.padding * 2 - }); + box + .transform(Snap.matrix() + .translate(0, labelBox.height)) + .attr({ + width: Math.max(contentBox.width + options.padding * 2, labelBox.width), + height: contentBox.height + options.padding * 2 + }); - content.transform(Snap.matrix() - .translate(box.getBBox().cx - contentBox.cx, labelBox.height + options.padding)); - }); - - return deferred.promise; + content.transform(Snap.matrix() + .translate(box.getBBox().cx - contentBox.cx, labelBox.height + options.padding)); + }); } }; + +Node.reset = () => { + Node.renderCounter = 0; + Node.maxCounter = 0; + Node.cancelRender = false; +}; diff --git a/src/js/regexper.js b/src/js/regexper.js index a6d98ba..70a7a1f 100644 --- a/src/js/regexper.js +++ b/src/js/regexper.js @@ -32,6 +32,12 @@ export default class Regexper { } } + documentKeypressListener(event) { + if (event.keyCode === 27) { + parser.cancel(); + } + } + submitListener(event) { event.returnValue = false; if (event.preventDefault) { @@ -62,6 +68,7 @@ export default class Regexper { this.field.addEventListener('keypress', this.keypressListener.bind(this)); this.form.addEventListener('submit', this.submitListener.bind(this)); this.root.addEventListener('updateStatus', this.updatePercentage.bind(this)); + this.root.addEventListener('keyup', this.documentKeypressListener.bind(this)); window.addEventListener('hashchange', this.hashchangeListener.bind(this)); } @@ -92,6 +99,12 @@ export default class Regexper { .then(() => { this.state = 'has-results'; this.updateLinks(); + }, (message) => { + if (message === 'Render cancelled') { + this.state = ''; + } else { + throw message; + } }) .done(); }