Merge pull request #623 from mathquill/docs.readthedocs

Refactor documentation from README into Read the Docs
diff --git a/Makefile b/Makefile
index dcb2497..8acc2a6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,23 @@
 #
+# -*- Prerequisites -*-
+#
+
+# the fact that 'I am Node.js' is unquoted here looks wrong to me but it
+# CAN'T be quoted, I tried. Apparently in GNU Makefiles, in the paren+comma
+# syntax for conditionals, quotes are literal; and because the $(shell...)
+# call has parentheses and single and double quotes, the quoted syntaxes
+# don't work (I tried), we HAVE to use the paren+comma syntax
+ifneq ($(shell node -e 'console.log("I am Node.js")'), I am Node.js)
+  ifeq ($(shell nodejs -e 'console.log("I am Node.js")' 2>/dev/null), I am Node.js)
+    $(error You have /usr/bin/nodejs but no /usr/bin/node, please 'sudo apt-get install nodejs-legacy' (see http://stackoverflow.com/a/21171188/362030 ))
+  endif
+
+  $(error Please install Node.js: https://nodejs.org/ )
+endif
+
+
+
+#
 # -*- Configuration -*-
 #
 
diff --git a/package.json b/package.json
index e9889de..9c8fb28 100644
--- a/package.json
+++ b/package.json
@@ -8,10 +8,10 @@
     "url": "https://github.com/mathquill/mathquill.git"
   },
   "dependencies": {
-    "pjs": "3.x"
+    "pjs": ">=3.1.0 <5.0.0"
   },
   "devDependencies": {
-    "mocha": "*",
+    "mocha": ">=2.4.1",
     "uglify-js": "2.x",
     "less": ">=1.5.1"
   }
diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js
index 414edaa..1538c51 100644
--- a/src/commands/math/commands.js
+++ b/src/commands/math/commands.js
@@ -138,7 +138,7 @@
 var SupSub = P(MathCommand, function(_, super_) {
   _.ctrlSeq = '_{...}^{...}';
   _.createLeftOf = function(cursor) {
-    if (!cursor[L] && cursor.options.supSubsRequireOperand) return;
+    if (!this.replacedFragment && !cursor[L] && cursor.options.supSubsRequireOperand) return;
     return super_.createLeftOf.apply(this, arguments);
   };
   _.contactWeld = function(cursor) {
@@ -181,7 +181,6 @@
         break;
       }
     }
-    this.respace();
   };
   Options.p.charsThatBreakOutOfSupSub = '';
   _.finalizeTree = function() {
@@ -230,10 +229,6 @@
     }
     return latex('_', this.sub) + latex('^', this.sup);
   };
-  _.respace = _.siblingCreated = _.siblingDeleted = function(opts, dir) {
-    if (dir === R) return; // ignore if sibling only changed on the right
-    this.jQ.toggleClass('mq-limit', this[L].ctrlSeq === '\\int ');
-  };
   _.addBlock = function(block) {
     if (this.supsub === 'sub') {
       this.sup = this.upInto = this.sub.upOutOf = block;
@@ -382,7 +377,23 @@
 
 LatexCmds['∫'] =
 LatexCmds['int'] =
-LatexCmds.integral = bind(Symbol,'\\int ','<big>&int;</big>');
+LatexCmds.integral = P(SummationNotation, function(_, super_) {
+  _.init = function() {
+    var htmlTemplate =
+      '<span class="mq-int mq-non-leaf">'
+    +   '<big>&int;</big>'
+    +   '<span class="mq-supsub mq-non-leaf">'
+    +     '<span class="mq-sup"><span class="mq-sup-inner">&1</span></span>'
+    +     '<span class="mq-sub">&0</span>'
+    +     '<span style="display:inline-block;width:0">&#8203</span>'
+    +   '</span>'
+    + '</span>'
+    ;
+    Symbol.prototype.init.call(this, '\\int ', htmlTemplate);
+  };
+  // FIXME: refactor rather than overriding
+  _.createLeftOf = MathCommand.p.createLeftOf;
+});
 
 var Fraction =
 LatexCmds.frac =
diff --git a/src/commands/text.js b/src/commands/text.js
index d4352bd..59614ee 100644
--- a/src/commands/text.js
+++ b/src/commands/text.js
@@ -296,7 +296,6 @@
   };
 });
 
-CharCmds.$ =
 LatexCmds.text =
 LatexCmds.textnormal =
 LatexCmds.textrm =
diff --git a/src/css/math.less b/src/css/math.less
index fd828b0..aae409d 100644
--- a/src/css/math.less
+++ b/src/css/math.less
@@ -90,6 +90,28 @@
     font-size: 200%;
   }
 
+  .mq-int {
+    > big {
+      display: inline-block;
+      .transform(scaleX(.7));
+      vertical-align: -.16em;
+    }
+
+    > .mq-supsub {
+      font-size: 80%;
+      vertical-align: -1.1em;
+      padding-right: .2em;
+
+      > .mq-sup > .mq-sup-inner {
+        vertical-align: 1.3em;
+      }
+
+      > .mq-sub {
+        margin-left: -.35em;
+      }
+    }
+  }
+
   .mq-roman {
     font-style: normal;
   }
@@ -132,10 +154,6 @@
     text-align: left;
     font-size: 90%;
     vertical-align: -.5em;
-    &.mq-limit {
-      font-size: 80%;
-      vertical-align: -.4em;
-    }
 
     &.mq-sup-only {
       vertical-align: .5em;
@@ -154,9 +172,6 @@
       display: block;
       float: left;
     }
-    &.mq-limit .mq-sub {
-      margin-left: -.25em;
-    }
 
     .mq-binary-operator {
       padding: 0 .1em;
diff --git a/src/publicapi.js b/src/publicapi.js
index 022dfda..2ccdeb2 100644
--- a/src/publicapi.js
+++ b/src/publicapi.js
@@ -213,6 +213,19 @@
       var cmd = Embed().setOptions(options);
       cmd.createLeftOf(this.__controller.cursor);
     };
+    _.clickAt = function(clientX, clientY, target) {
+      target = target || document.elementFromPoint(clientX, clientY);
+
+      var ctrlr = this.__controller, root = ctrlr.root;
+      if (!jQuery.contains(root.jQ[0], target)) target = root.jQ[0];
+      ctrlr.seek($(target), clientX + pageXOffset, clientY + pageYOffset);
+      if (ctrlr.blurred) this.focus();
+      return this;
+    };
+    _.ignoreNextMousedown = function(fn) {
+      this.__controller.cursor.options.ignoreNextMousedown = fn;
+      return this;
+    };
   });
   MQ.EditableField = function() { throw "wtf don't call me, I'm 'abstract'"; };
   MQ.EditableField.prototype = APIClasses.EditableField.prototype;
diff --git a/src/services/mouse.js b/src/services/mouse.js
index c060461..b2b543a 100644
--- a/src/services/mouse.js
+++ b/src/services/mouse.js
@@ -3,6 +3,7 @@
  *******************************************************/
 
 Controller.open(function(_) {
+  Options.p.ignoreNextMousedown = noop;
   _.delegateMouseEvents = function() {
     var ultimateRootjQ = this.root.jQ;
     //drag-to-select event handling
@@ -12,6 +13,12 @@
       var ctrlr = root.controller, cursor = ctrlr.cursor, blink = cursor.blink;
       var textareaSpan = ctrlr.textareaSpan, textarea = ctrlr.textarea;
 
+      e.preventDefault(); // doesn't work in IE≤8, but it's a one-line fix:
+      e.target.unselectable = true; // http://jsbin.com/yagekiji/1
+
+      if (cursor.options.ignoreNextMousedown(e)) return;
+      else cursor.options.ignoreNextMousedown = noop;
+
       var target;
       function mousemove(e) { target = $(e.target); }
       function docmousemove(e) {
@@ -42,8 +49,6 @@
         if (!ctrlr.editable) rootjQ.prepend(textareaSpan);
         textarea.focus();
       }
-      e.preventDefault(); // doesn't work in IE≤8, but it's a one-line fix:
-      e.target.unselectable = true; // http://jsbin.com/yagekiji/1
 
       cursor.blink = noop;
       ctrlr.seek($(e.target), e.pageX, e.pageY).cursor.startSelection();
diff --git a/test/unit/focusBlur.test.js b/test/unit/focusBlur.test.js
index 1bd3af9..27180ae 100644
--- a/test/unit/focusBlur.test.js
+++ b/test/unit/focusBlur.test.js
@@ -1,6 +1,6 @@
 suite('focusBlur', function() {
   function assertHasFocus(mq, name, invert) {
-    assert.ok(!!invert ^ $(mq.el()).find('textarea').is(':focus'), name + (invert ? ' does not have focus' : ' has focus'));
+    assert.ok(!!invert ^ ($(mq.el()).find('textarea')[0] === document.activeElement), name + (invert ? ' does not have focus' : ' has focus'));
   }
 
   suite('handlers can shift focus away', function() {
diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js
index a2c62c3..55645b6 100644
--- a/test/unit/publicapi.test.js
+++ b/test/unit/publicapi.test.js
@@ -744,6 +744,20 @@
 
       $(mq.el()).remove();
     });
+    test('integral still has empty limits', function() {
+      var mq = MQ.MathField($('<span>').appendTo('#mock')[0], {
+        sumStartsWithNEquals: true
+      });
+      assert.equal(mq.latex(), '');
+
+      mq.cmd('\\int');
+      assert.equal(mq.latex(), '\\int_{ }^{ }');
+
+      mq.cmd('0');
+      assert.equal(mq.latex(), '\\int_0^{ }', 'cursor in the from block');
+
+      $(mq.el()).remove();
+    });
   });
 
   suite('substituteTextarea', function() {
@@ -762,6 +776,58 @@
     });
   });
 
+  suite('clickAt', function() {
+    test('inserts at coordinates', function() {
+      // Insert filler so that the page is taller than the window so this test is deterministic
+      // Test that we use clientY instead of pageY
+      var windowHeight = $(window).height();
+      var filler = $('<div>').height(windowHeight);
+      filler.insertBefore('#mock');
+
+      var mq = MQ.MathField($('<span>').appendTo('#mock')[0]);
+      mq.typedText("mmmm/mmmm");
+      mq.el().scrollIntoView();
+
+      var box = mq.el().getBoundingClientRect();
+      var clientX = box.left + 30;
+      var clientY = box.top + 30;
+      var target = document.elementFromPoint(clientX, clientY);
+
+      assert.equal(document.activeElement, document.body);
+      mq.clickAt(clientX, clientY, target).write('x');
+      assert.equal(document.activeElement, $(mq.el()).find('textarea')[0]);
+
+      assert.equal(mq.latex(), "\\frac{mmmm}{mmxmm}");
+
+      filler.remove();
+      $(mq.el()).remove();
+    });
+    test('target is optional', function() {
+      // Insert filler so that the page is taller than the window so this test is deterministic
+      // Test that we use clientY instead of pageY
+      var windowHeight = $(window).height();
+      var filler = $('<div>').height(windowHeight);
+      filler.insertBefore('#mock');
+
+      var mq = MQ.MathField($('<span>').appendTo('#mock')[0]);
+      mq.typedText("mmmm/mmmm");
+      mq.el().scrollIntoView();
+
+      var box = mq.el().getBoundingClientRect();
+      var clientX = box.left + 30;
+      var clientY = box.top + 30;
+
+      assert.equal(document.activeElement, document.body);
+      mq.clickAt(clientX, clientY).write('x');
+      assert.equal(document.activeElement, $(mq.el()).find('textarea')[0]);
+
+      assert.equal(mq.latex(), "\\frac{mmmm}{mmxmm}");
+
+      filler.remove();
+      $(mq.el()).remove();
+    });
+  });
+
   suite('dropEmbedded', function() {
     test('inserts into empty', function() {
       var mq = MQ.MathField($('<span>').appendTo('#mock')[0]);
@@ -792,7 +858,7 @@
 
       mq.el().scrollIntoView();
 
-      mq.dropEmbedded(mqx + 30, mqy + 40, {
+      mq.dropEmbedded(mqx + 30, mqy + 30, {
         htmlString: '<span class="embedded-html"></span>',
         text: function () { return "embedded text" },
         latex: function () { return "embedded latex" }
diff --git a/test/unit/saneKeyboardEvents.test.js b/test/unit/saneKeyboardEvents.test.js
index 9247803..158d425 100644
--- a/test/unit/saneKeyboardEvents.test.js
+++ b/test/unit/saneKeyboardEvents.test.js
@@ -146,42 +146,42 @@
       assert.ok(document.activeElement !== el[0], 'textarea remains blurred');
     });
 
-    if (!document.hasFocus()) {
-      test('blur in keystroke handler: DOCUMENT NEEDS FOCUS, SEE CONSOLE ');
-      console.warn(
-        'The test "blur in keystroke handler" needs the document to have ' +
-        'focus. Only when the document has focus does .select() on an ' +
-        'element also focus it, which is part of the problematic behavior ' +
-        'we are testing robustness against. (Specifically, erroneously ' +
-        'calling .select() in a timeout after the textarea has blurred, ' +
-        '"stealing back" focus.)\n' +
-        'Normally, the page being open and focused is enough to have focus, ' +
-        'but with the Developer Tools open, it depends on whether you last ' +
-        'clicked on something in the Developer Tools or on the page itself. ' +
-        'Click the page, or close the Developer Tools, and Refresh.'
-      );
-    }
-    else {
-      test('blur in keystroke handler', function(done) {
-        var shim = saneKeyboardEvents(el, {
-          keystroke: function(key) {
-            assert.equal(key, 'Left');
-            el[0].blur();
-          }
-        });
+    test('blur in keystroke handler', function(done) {
+      if (!document.hasFocus()) {
+        console.warn(
+          'The test "blur in keystroke handler" needs the document to have ' +
+          'focus. Only when the document has focus does .select() on an ' +
+          'element also focus it, which is part of the problematic behavior ' +
+          'we are testing robustness against. (Specifically, erroneously ' +
+          'calling .select() in a timeout after the textarea has blurred, ' +
+          '"stealing back" focus.)\n' +
+          'Normally, the page being open and focused is enough to have focus, ' +
+          'but with the Developer Tools open, it depends on whether you last ' +
+          'clicked on something in the Developer Tools or on the page itself. ' +
+          'Click the page, or close the Developer Tools, and Refresh.'
+        );
+        el.remove(); // LOL next line skips teardown https://git.io/vaUWq
+        this.skip();
+      }
 
-        shim.select('foobar');
-        assert.ok(document.activeElement === el[0], 'textarea focused');
-
-        el.trigger(Event('keydown', { which: 37 }));
-        assert.ok(document.activeElement !== el[0], 'textarea blurred');
-
-        setTimeout(function() {
-          assert.ok(document.activeElement !== el[0], 'textarea remains blurred');
-          done();
-        });
+      var shim = saneKeyboardEvents(el, {
+        keystroke: function(key) {
+          assert.equal(key, 'Left');
+          el[0].blur();
+        }
       });
-    }
+
+      shim.select('foobar');
+      assert.ok(document.activeElement === el[0], 'textarea focused');
+
+      el.trigger(Event('keydown', { which: 37 }));
+      assert.ok(document.activeElement !== el[0], 'textarea blurred');
+
+      setTimeout(function() {
+        assert.ok(document.activeElement !== el[0], 'textarea remains blurred');
+        done();
+      });
+    });
 
     suite('selected text after keypress or paste doesn\'t get mistaken' +
          ' for inputted text', function() {
diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js
index 25628ef..c3c89f1 100644
--- a/test/unit/typing.test.js
+++ b/test/unit/typing.test.js
@@ -56,6 +56,11 @@
       mq.typedText('\\asdf+');
       assertLatex('\\text{asdf}+');
     });
+
+    test('dollar sign', function() {
+      mq.typedText('$');
+      assertLatex('\\$');
+    });
   });
 
   suite('auto-expanding parens', function() {
@@ -1034,6 +1039,9 @@
       assert.equal(mq.typedText('^').latex(), 'x^{^{ }}');
       assert.equal(mq.typedText('2').latex(), 'x^{^2}');
       assert.equal(mq.typedText('n').latex(), 'x^{^{2n}}');
+      mq.latex('');
+      assert.equal(mq.typedText('2').latex(), '2');
+      assert.equal(mq.keystroke('Shift-Left').typedText('^').latex(), '^2');
 
       mq.latex('');
       MQ.config({ supSubsRequireOperand: true });
@@ -1052,6 +1060,9 @@
       assert.equal(mq.typedText('^').latex(), 'x^{ }');
       assert.equal(mq.typedText('2').latex(), 'x^2');
       assert.equal(mq.typedText('n').latex(), 'x^{2n}');
+      mq.latex('');
+      assert.equal(mq.typedText('2').latex(), '2');
+      assert.equal(mq.keystroke('Shift-Left').typedText('^').latex(), '^2');
     });
   });
 });
diff --git a/test/visual.html b/test/visual.html
index 1c6f44f..f066a7b 100644
--- a/test/visual.html
+++ b/test/visual.html
@@ -24,6 +24,9 @@
 td {

   width: 33%;

 }

+#static-latex-rendering-table td {

+  width: 50%;

+}

 #show-textareas-button {

   float: right;

 }

@@ -77,14 +80,14 @@
   <td><span class="mathquill-static-math">\sqrt{\MathQuillMathField{x^2+y^2}}</span>

 </table>

 

-<p>Clicks/mousedown to drag should work anywhere in the blue box: <div class="math-container" style="border: solid 1px lightblue; height: 5em; width: 15em; line-height: 5em; text-align: center"><span class="mathquill-math-field">x_{very\ long\ thing}^2 + a_0 = 0</span></div>

+<p>Touch taps/clicks/mousedown to drag should work anywhere in the blue box: <div class="math-container" style="border: solid 1px lightblue; height: 5em; width: 15em; line-height: 5em; text-align: center; -webkit-tap-highlight-color: rgba(0,0,0,0)"><span class="mathquill-math-field">x_{very\ long\ thing}^2 + a_0 = 0</span></div>

 

 <h3>Redrawing</h3>

 <p>

-  <span id="reflowing-test">\sqrt{_0^1}</span>

+  <span id="reflowing-test">\sqrt{}</span>

   should look the same as

   <span class="mathquill-static-math">

-    \sqrt{\pi\sqrt\sqrt\frac12\int_0^1}

+    \sqrt{\pi\sqrt\sqrt\frac12}

   </span>

 </p>

 <script type="text/javascript">

@@ -96,7 +99,7 @@
     var textarea = $('#reflowing-test textarea');

     // paste some stuff that needs resizing

     textarea.trigger('paste');

-    textarea.val('\\pi\\sqrt{\\sqrt{\\frac12}}\\int');

+    textarea.val('\\pi\\sqrt{\\sqrt{\\frac12}}');

     setTimeout(function() { if (count !== 1) throw 'reflow not called'; });

   });

 </script>

@@ -183,7 +186,7 @@
 </table>

 

 <h3>Static LaTeX rendering (<code>.mathquill-static-math</code>) tests</h3>

-<table>

+<table id="static-latex-rendering-table">

 <tr><td><span class="mathquill-static-math">^{\frac{as}{ }df}</span><td><span>^{\frac{as}{ }df}</span>

 <tr><td><span class="mathquill-static-math">e^{i\pi}+1=0</span><td><span>e^{i\pi}+1=0</span>

 <tr><td><span class="mathquill-static-math">\sqrt[n]{1}</span><td><span>\sqrt[n]{1}</span>

@@ -208,6 +211,8 @@
 <tr><td><span class="mathquill-static-math">1+\sum_0^n+\sum_{i=0123}^n+\sum_0^{wordiness}</span><td><span>1+\sum_0^n+\sum_{i=0123}^n+\sum_0^{wordiness}</span>^M

 <tr><td><span class="mathquill-static-math">x\ \ \ +\ \ \ y</span><td><span>x\ \ \ +\ \ \ y</span>^M

 <tr><td><span class="mathquill-static-math">\sum _{n=0}^3\cos x</span><td><span>\sum _{n=0}^3\cos x</span>^M

+<tr><td><span class="mathquill-static-math">\int _{\phi =0}^{2\pi }\int _{\theta =0}^{\pi }\int _{r=0}^{\infty }f(r,\theta ,\phi )r^2\sin \theta drd\theta d\phi </span><td><span>\int _{\phi =0}^{2\pi }\int _{\theta =0}^{\pi }\int _{r=0}^{\infty }f(r,\theta ,\phi )r^2\sin \theta drd\theta d\phi </span>

+<tr><td><span class="mathquill-static-math">\int_0^{\frac{\frac{1}{2}}{3}} \int_0^{\frac{1}{\frac{2}{3}}} \int_0^{\frac{1}{\frac{2}{\frac{3}{\frac{4}{5}}}}}</span><td><span>\int_0^{\frac{\frac{1}{2}}{3}} \int_0^{\frac{1}{\frac{2}{3}}} \int_0^{\frac{1}{\frac{2}{\frac{3}{\frac{4}{5}}}}}</span>

 <tr><td colspan=2><span id="sixes"></span>

 <script>

 $(function() {

@@ -297,7 +302,25 @@
 // test selecting from outside the mathquill editable

 var $mq = $('.math-container .mathquill-math-field');

 $('.math-container').mousedown(function(e) {

-  if (!jQuery.contains($mq[0], e.target)) $mq.triggerHandler(e);

+  if (e.target === $mq[0] || $.contains($mq[0], e.target)) return;

+  $mq.triggerHandler(e);

+})

+// test API for "fast touch taps" #622 & #403

+.on('touchstart', function() {

+  var moved = false;

+  $(this).on('touchmove.tmp', function() { moved = true; })

+  .on('touchend.tmp', function(e) {

+    $(this).off('.tmp');

+    MathQuill($mq[0]).ignoreNextMousedown(function() {

+      return Date.now() < e.timeStamp + 1000;

+    });

+    if (moved) return; // note that this happens after .ignoreNextMousedown()

+      // because even if the touch gesture doesn't 'count' as a tap to us,

+      // we still want to suppress the legacy mouse events, else we'd react

+      // fast to some taps and slow to others, that'd be weird

+    var touch = e.originalEvent.changedTouches[0];

+    MathQuill($mq[0]).clickAt(touch.clientX, touch.clientY, touch.target);

+  });

 });

 

 // Selection Tests