editor.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. (function($, undefined) {
  2. // http://stackoverflow.com/questions/1891444/how-can-i-get-cursor-position-in-a-textarea
  3. $.fn.getCursorPosition = function() {
  4. var el = $(this).get(0);
  5. var pos = 0;
  6. if ('selectionStart' in el) {
  7. pos = el.selectionStart;
  8. } else if ('selection' in document) {
  9. el.focus();
  10. var Sel = document.selection.createRange();
  11. var SelLength = document.selection.createRange().text.length;
  12. Sel.moveStart('character', -el.value.length);
  13. pos = Sel.text.length - SelLength;
  14. }
  15. return pos;
  16. };
  17. var QUEUE = MathJax.Hub.queue; // shorthand for the queue
  18. var math = null; // the element jax for the math output
  19. var trigger_update = true;
  20. var input_textarea = $('#math-input');
  21. var pretty_print = $('#pretty-print');
  22. // Set the requested query as input value if a query string is given.
  23. if (location.search.substr(0, 3) == '?q=')
  24. input_textarea.val(decodeURIComponent(location.search.substr(3)));
  25. input_textarea.bind('change keyup click', function() {
  26. trigger_update = true;
  27. });
  28. input_textarea.closest('div').click(function() {
  29. input_textarea.focus();
  30. });
  31. var STATUS_FAILURE = 0,
  32. STATUS_NOPROGRESS = 1,
  33. STATUS_SUCCESS = 2,
  34. STATUS_ERROR = 3;
  35. var status_icons = ['thumbs-down', 'thumbs-up', 'thumbs-up', 'remove'];
  36. var status_labels = ['important', 'warning', 'success', 'important'];
  37. var status_titles = ['Incorrect', 'No progress', 'Correct', 'Error'];
  38. var status_messages = [
  39. 'This step is incorrect.',
  40. 'This step leads to the correct answer, but not in a lower number of '
  41. + 'steps than the previous step.',
  42. 'This step is correct.',
  43. 'An error occurred while validating this step.'
  44. ];
  45. function set_status(elem, stat) {
  46. elem = $(elem);
  47. elem.find('.label').remove();
  48. if (stat !== undefined) {
  49. var label = $('<span class="label label-'
  50. + status_labels[stat] + '"/>');
  51. label.append('<i class="icon-white icon-'
  52. + status_icons[stat] + '"/>');
  53. //label.tooltip({placement: 'left', title: status_messages[stat]});
  54. label.popover({
  55. placement: 'left',
  56. trigger: 'hover',
  57. title: status_titles[stat],
  58. content: status_messages[stat]
  59. });
  60. elem.append(label);
  61. }
  62. }
  63. function get_current_line() {
  64. var input = input_textarea.val(),
  65. caret = input_textarea.getCursorPosition(),
  66. lines = 0;
  67. for (var i = 0; i < caret; i++) {
  68. if (input.charAt(i) == '\n')
  69. lines++;
  70. }
  71. return lines;
  72. }
  73. // Get the element jax when MathJax has produced it.
  74. QUEUE.Push(function() {
  75. // The onchange event handler that typesets the math entered
  76. // by the user. Hide the box, then typeset, then show it again
  77. // so we don't see a flash as the math is cleared and replaced.
  78. var update_math = function(tex) {
  79. var parts = tex.split('\n');
  80. var math_lines = pretty_print.find('div.box script');
  81. // Stretch textarea size with number of input lines
  82. input_textarea.attr('rows', parts.length);
  83. // Select all mathjax instances which are inside a div.box element
  84. var mathjax_instances = [];
  85. var all_instances = MathJax.Hub.getAllJax('pretty-print');
  86. for (var i = 0; i < all_instances.length; i++) {
  87. var elem = all_instances[i];
  88. if ($('#' + elem.inputID).parent().hasClass('box'))
  89. mathjax_instances.push(elem);
  90. }
  91. var real_lines = 0,
  92. updated_line = -1,
  93. current_line = get_current_line();
  94. for (var p = 0; p < parts.length; p++) {
  95. if (!parts[p])
  96. continue;
  97. // Check if we want to update an existing line or append the
  98. // line.
  99. if (real_lines < math_lines.length) {
  100. var elem = mathjax_instances[real_lines];
  101. // Update the line when the input is modified.
  102. if (elem.originalText != parts[p]) {
  103. updated_line = real_lines;
  104. QUEUE.Push(['Text', elem, parts[p]]);
  105. }
  106. elem = $(math_lines[real_lines]).parent();
  107. if (updated_line > -1) {
  108. // Remove the out-of-date status information. This will
  109. // be done from now on for all remaining lines, whether
  110. // they are up-to-date or not.
  111. set_status(elem);
  112. }
  113. } else {
  114. var line = '`' + parts[p] + '`',
  115. elem = $('<div class="box"/>').text(line);
  116. pretty_print.append(elem);
  117. QUEUE.Push(['Typeset', MathJax.Hub, elem[0]]);
  118. }
  119. // Highlight current line.
  120. $(elem).toggleClass('current-line', p == current_line);
  121. real_lines++;
  122. }
  123. QUEUE.Push(function() {
  124. // Remove out-dated mathematical lines.
  125. for (var p = real_lines; p < math_lines.length; p++)
  126. $(math_lines[p].parentNode).remove();
  127. // Remove old hints, given that at least one line is updated.
  128. // Iterate over the DOM nodes until the updated line is found,
  129. // and remove all following hint nodes. Note that if there is
  130. // no line updated, all hints not directly following the last
  131. // line are removed.
  132. var elems = pretty_print.find('div');
  133. if(updated_line == -1)
  134. updated_line = real_lines;
  135. for(var i = 0, lines = 0, hints = 0; i < elems.length; i++) {
  136. var elem = $(elems[i]);
  137. if (lines > updated_line || hints >= updated_line) {
  138. if (elem.hasClass('hint'))
  139. elem.remove();
  140. } else if (elem.hasClass('hint'))
  141. hints++;
  142. else if (elem.hasClass('box'))
  143. lines++;
  144. }
  145. });
  146. }
  147. window.update_math = function() {
  148. if (trigger_update) {
  149. trigger_update = false;
  150. // Preprocess input to fix TRS-MathJax incompatibilities.
  151. var input = input_textarea.val();
  152. // Make sure that xx is not displayed as a cross.
  153. input = input.replace(/xx/g, 'x x');
  154. update_math(input);
  155. }
  156. };
  157. window.update_math();
  158. setInterval(window.update_math, 100);
  159. });
  160. var error = $('#error'),
  161. loader = $('#loader');
  162. error.find('.close').click(function() {
  163. error.hide();
  164. });
  165. function report_error(e) {
  166. error.show().find('.text').text(e.error);
  167. if (console && console.log)
  168. console.log('error:', e);
  169. loader.hide();
  170. };
  171. function clear_error() {
  172. error.hide();
  173. };
  174. var pending_request = false;
  175. function show_loader() {
  176. pending_request = true;
  177. loader.css('visibility', 'visible');
  178. };
  179. function hide_loader() {
  180. pending_request = false;
  181. loader.css('visibility', 'hidden');
  182. clear_error();
  183. };
  184. function append_hint(hint) {
  185. pretty_print.find('div').last().filter('.hint').remove();
  186. var elem = $('<div class="hint"/>');
  187. elem.text(hint);
  188. elem.append('<div class="icon icon-info-sign"/>');
  189. pretty_print.append(elem);
  190. QUEUE.Push(['Typeset', MathJax.Hub, elem[0]]);
  191. };
  192. function append_input(input) {
  193. input_textarea.val(input_textarea.val() + '\n' + input);
  194. };
  195. $('#btn-clear').click(function() {
  196. input_textarea.val('');
  197. pretty_print.find('.box,.hint').remove();
  198. trigger_update = true;
  199. clear_error();
  200. hide_loader();
  201. });
  202. function bind_request(btn, url, handler, condition) {
  203. $('#btn-' + btn).click(function() {
  204. var input = input_textarea.val();
  205. if (pending_request || !$.trim(input).length
  206. || (condition && !condition())) {
  207. return;
  208. }
  209. show_loader();
  210. // TODO: disable input box and enable it when this ajax request is done
  211. // (on failure and success).
  212. $.post(url, {data: input}, function(response) {
  213. if (!response)
  214. return;
  215. if ('error' in response)
  216. return report_error(response);
  217. handler(response);
  218. hide_loader();
  219. }, 'json').error(report_error);
  220. });
  221. }
  222. // No need to show a hint if there is already one at the end of the
  223. // calculation
  224. function no_hint_displayed() {
  225. return !pretty_print.children(':last').hasClass('hint');
  226. }
  227. bind_request('hint', '/hint', function(response) {
  228. append_hint(response.hint);
  229. input_textarea.focus();
  230. }, no_hint_displayed);
  231. bind_request('step', '/step', function(response) {
  232. if ('step' in response) {
  233. append_input(response.step);
  234. trigger_update = true;
  235. }
  236. if('hint' in response && no_hint_displayed())
  237. append_hint(response.hint);
  238. input_textarea.focus();
  239. });
  240. bind_request('validate', '/validate', function(response) {
  241. var math_lines = pretty_print.find('div.box');
  242. var i = 0;
  243. // Remove the status indicator from all remaining lines.
  244. for(; i < math_lines.length; i++)
  245. set_status(math_lines[i]);
  246. i = 0;
  247. // Check if the first line has a correct syntax, since there is
  248. // nothing to validate here.
  249. if (i < math_lines.length && i <= response.validated) {
  250. set_status(math_lines[i], STATUS_SUCCESS);
  251. i++;
  252. }
  253. // Mark every line as {wrong,no-progress,correct,error}.
  254. for (; i < math_lines.length && i <= response.validated; i++)
  255. set_status(math_lines[i], response.status[i - 1]);
  256. if (i < math_lines.length) {
  257. // Mark the current line as wrong.
  258. set_status(math_lines[i], STATUS_FAILURE);
  259. }
  260. });
  261. bind_request('answer', '/answer', function(response) {
  262. if ('steps' in response) {
  263. for(i = 0; i < response.steps.length; i++) {
  264. cur = response.steps[i];
  265. if ('step' in cur)
  266. append_input(cur.step);
  267. if('hint' in cur)
  268. append_hint(cur.hint);
  269. trigger_update = true;
  270. window.update_math();
  271. }
  272. }
  273. if('hint' in response)
  274. append_hint(response.hint);
  275. input_textarea.focus();
  276. });
  277. })(jQuery);