Merge pull request #692 from desmosinc/feature.substituteKeyboardEvents
`substituteKeyboardEvents` config option
diff --git a/src/publicapi.js b/src/publicapi.js
index 2ccdeb2..78ce1b4 100644
--- a/src/publicapi.js
+++ b/src/publicapi.js
@@ -77,6 +77,7 @@
MQ.L = L;
MQ.R = R;
+ MQ.saneKeyboardEvents = saneKeyboardEvents;
function config(currentOptions, newOptions) {
if (newOptions && newOptions.handlers) {
diff --git a/src/services/saneKeyboardEvents.util.js b/src/services/saneKeyboardEvents.util.js
index cdb9686..e6bfb4f 100644
--- a/src/services/saneKeyboardEvents.util.js
+++ b/src/services/saneKeyboardEvents.util.js
@@ -95,8 +95,8 @@
var textarea = jQuery(el);
var target = jQuery(handlers.container || textarea);
- // checkTextareaFor() is called after keypress or paste events to
- // say "Hey, I think something was just typed" or "pasted" (resp.),
+ // checkTextareaFor() is called after key or clipboard events to
+ // say "Hey, I think something was just typed" or "pasted" etc,
// so that at all subsequent opportune times (next event or timeout),
// will check for expected typed or pasted text.
// Need to check repeatedly because #135: in Safari 5.1 (at least),
@@ -109,6 +109,13 @@
clearTimeout(timeoutId);
timeoutId = setTimeout(checker);
}
+ function checkTextareaOnce(checker) {
+ checkTextareaFor(function(e) {
+ checkTextarea = noop;
+ clearTimeout(timeoutId);
+ checker(e);
+ });
+ }
target.bind('keydown keypress input keyup focusout paste', function(e) { checkTextarea(e); });
@@ -148,12 +155,12 @@
keydown = e;
keypress = null;
- if (shouldBeSelected) checkTextareaFor(function(e) {
+ if (shouldBeSelected) checkTextareaOnce(function(e) {
if (!(e && e.type === 'focusout') && textarea[0].select) {
- textarea[0].select(); // re-select textarea in case it's an unrecognized
+ // re-select textarea in case it's an unrecognized key that clears
+ // the selection, then never again, 'cos next thing might be blur
+ textarea[0].select();
}
- checkTextarea = noop; // key that clears the selection, then never
- clearTimeout(timeoutId); // again, 'cos next thing might be blur
});
handleKey();
@@ -229,6 +236,8 @@
keydown: onKeydown,
keypress: onKeypress,
focusout: onBlur,
+ cut: function() { checkTextareaOnce(function() { handlers.cut(); }); },
+ copy: function() { checkTextareaOnce(function() { handlers.copy(); }); },
paste: onPaste
});
diff --git a/src/services/textarea.js b/src/services/textarea.js
index dd2d2de..eaed10c 100644
--- a/src/services/textarea.js
+++ b/src/services/textarea.js
@@ -18,7 +18,6 @@
var ctrlr = this;
ctrlr.cursor.selectionChanged = function() { ctrlr.selectionChanged(); };
- ctrlr.container.bind('copy', function() { ctrlr.setTextareaSelection(); });
};
_.selectionChanged = function() {
var ctrlr = this;
@@ -52,6 +51,7 @@
this.container.prepend('<span class="mq-selectable">$'+ctrlr.exportLatex()+'$</span>');
ctrlr.blurred = true;
textarea.bind('cut paste', false)
+ .bind('copy', function() { ctrlr.setTextareaSelection(); })
.focus(function() { ctrlr.blurred = false; }).blur(function() {
if (cursor.selection) cursor.selection.clear();
setTimeout(detach); //detaching during blur explodes in WebKit
@@ -66,23 +66,13 @@
if (text) textarea.select();
};
};
+ Options.p.substituteKeyboardEvents = saneKeyboardEvents;
_.editablesTextareaEvents = function() {
- var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor,
- textarea = ctrlr.textarea, textareaSpan = ctrlr.textareaSpan;
+ var ctrlr = this, textarea = ctrlr.textarea, textareaSpan = ctrlr.textareaSpan;
- var keyboardEventsShim = saneKeyboardEvents(textarea, this);
+ var keyboardEventsShim = this.options.substituteKeyboardEvents(textarea, this);
this.selectFn = function(text) { keyboardEventsShim.select(text); };
-
- this.container.prepend(textareaSpan)
- .on('cut', function(e) {
- if (cursor.selection) {
- setTimeout(function() {
- ctrlr.notify('edit'); // deletes selection if present
- cursor.parent.bubble('reflow');
- });
- }
- });
-
+ this.container.prepend(textareaSpan);
this.focusBlurEvents();
};
_.typedText = function(ch) {
@@ -91,6 +81,18 @@
cursor.parent.write(cursor, ch);
this.scrollHoriz();
};
+ _.cut = function() {
+ var ctrlr = this, cursor = ctrlr.cursor;
+ if (cursor.selection) {
+ setTimeout(function() {
+ ctrlr.notify('edit'); // deletes selection if present
+ cursor.parent.bubble('reflow');
+ });
+ }
+ };
+ _.copy = function() {
+ this.setTextareaSelection();
+ };
_.paste = function(text) {
// TODO: document `statelessClipboard` config option in README, after
// making it work like it should, that is, in both text and math mode
diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js
index 4791491..b15c5de 100644
--- a/test/unit/publicapi.test.js
+++ b/test/unit/publicapi.test.js
@@ -793,6 +793,51 @@
});
});
+ suite('substituteKeyboardEvents', function() {
+ test('can intercept key events', function() {
+ var mq = MQ.MathField($('<span>').appendTo('#mock')[0], {
+ substituteKeyboardEvents: function(textarea, handlers) {
+ return MQ.saneKeyboardEvents(textarea, jQuery.extend({}, handlers, {
+ keystroke: function(_key, evt) {
+ key = _key;
+ return handlers.keystroke.apply(handlers, arguments);
+ }
+ }));
+ }
+ });
+ var key;
+
+ $(mq.el()).find('textarea').trigger({ type: 'keydown', which: '37' });
+ assert.equal(key, 'Left');
+
+ $(mq.el()).remove();
+ });
+ test('cut is async', function() {
+ var mq = MQ.MathField($('<span>').appendTo('#mock')[0], {
+ substituteKeyboardEvents: function(textarea, handlers) {
+ return MQ.saneKeyboardEvents(textarea, jQuery.extend({}, handlers, {
+ cut: function() {
+ count += 1;
+ return handlers.cut.apply(handlers, arguments);
+ }
+ }));
+ }
+ });
+ var count = 0;
+
+ $(mq.el()).find('textarea').trigger('cut');
+ assert.equal(count, 0);
+
+ $(mq.el()).find('textarea').trigger('input');
+ assert.equal(count, 1);
+
+ $(mq.el()).find('textarea').trigger('keyup');
+ assert.equal(count, 1);
+
+ $(mq.el()).remove();
+ });
+ });
+
suite('clickAt', function() {
test('inserts at coordinates', function() {
// Insert filler so that the page is taller than the window so this test is deterministic
diff --git a/test/unit/saneKeyboardEvents.test.js b/test/unit/saneKeyboardEvents.test.js
index 158d425..fe59265 100644
--- a/test/unit/saneKeyboardEvents.test.js
+++ b/test/unit/saneKeyboardEvents.test.js
@@ -410,4 +410,17 @@
el.trigger('input');
});
});
+
+ suite('copy', function() {
+ test('only runs handler once even if handler synchronously selects', function() {
+ // ...which MathQuill does and resulted in a stack overflow: https://git.io/vosm0
+ var shim = saneKeyboardEvents(el, {
+ copy: function() {
+ shim.select();
+ }
+ });
+
+ el.trigger('copy');
+ });
+ });
});
diff --git a/test/visual.html b/test/visual.html
index a5918b5..91a9874 100644
--- a/test/visual.html
+++ b/test/visual.html
@@ -276,6 +276,12 @@
<p>In Safari on iOS, this should be focusable but not bring up the on-screen keyboard; to test, try focusing anything else and confirm this blurs: <span id="no-kbd-math"></span> (confirmed working on iOS 6.1.3)</p>
+<h3>substituteKeyboardEvents</h3>
+
+<p>Should be able to prevent cut, typing, and pasting in this field: <span id="disable-typing">1+2+3</span></p>
+
+<p>Should wrap anything you type in '<>': <span id="wrap-typing">1+2+3</span></p>
+
</div>
<script type="text/javascript">
window.onerror = function(err) {
@@ -446,6 +452,29 @@
return $('<span tabindex=0 style="display:inline-block;width:1px;height:1px" />')[0];
}
});
+
+MQ.MathField($('#disable-typing')[0], {
+ substituteKeyboardEvents: function(textarea, handlers) {
+ return MQ.saneKeyboardEvents(textarea, $.extend({}, handlers, {
+ cut: $.noop,
+ paste: $.noop,
+ keystroke: $.noop,
+ typedText: $.noop
+ }));
+ }
+});
+
+MQ.MathField($('#wrap-typing')[0], {
+ substituteKeyboardEvents: function(textarea, handlers) {
+ return MQ.saneKeyboardEvents(textarea, $.extend({}, handlers, {
+ typedText: function (text) {
+ handlers.typedText('<');
+ handlers.typedText(text);
+ handlers.typedText('>');
+ }
+ }));
+ }
+});
</script>
</body>
</html>