Annotation of loncom/html/adm/LC_math_editor/src/ui.js, revision 1.4
1.1 damieng 1: /*
2:
3: Copyright (C) 2014 Michigan State University Board of Trustees
4:
5: The JavaScript code in this page is free software: you can
6: redistribute it and/or modify it under the terms of the GNU
7: General Public License (GNU GPL) as published by the Free Software
8: Foundation, either version 3 of the License, or (at your option)
9: any later version. The code is distributed WITHOUT ANY WARRANTY;
10: without even the implied warranty of MERCHANTABILITY or FITNESS
11: FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
12:
13: As additional permission under GNU GPL version 3 section 7, you
14: may distribute non-source (e.g., minimized or compacted) forms of
15: that code without the copy of the GNU GPL normally required by
16: section 4, provided you include this license notice and a URL
17: through which recipients can access the Corresponding Source.
18:
19: */
20:
21: "use strict";
22:
23: var handleChange = function(math_object) {
24: // math_object has 3 fields: ta, output_node, oldtxt
25: // we need to pass this object instead of the values because oldtxt will change
26: var ta, output_node, txt, parser, output, root, test1, test2;
27: ta = math_object.ta;
28: output_node = math_object.output_node;
29: txt = ta.value;
30:
31: // automatically add brackets to something like "1;2;3", for LON-CAPA:
32: // NOTE: this is ugly and sometimes adds brackets to error messages
33: test1 = '';
34: test2 = txt;
35: while (test2 != test1) {
36: test1 = test2;
37: test2 = test1.replace(/\[[^\[\]]*\]/g, '');
38: }
39: if (test2.split("[").length == test2.split("]").length) {
40: test1 = '';
41: while (test2 != test1) {
42: test1 = test2;
43: test2 = test1.replace(/\([^\(\)]*\)/g, '');
44: }
45: if (test2.split("(").length == test2.split(")").length) {
1.2 damieng 46: test1 = '';
47: while (test2 != test1) {
48: test1 = test2;
49: test2 = test1.replace(/\{[^\{\}]*\}/g, '');
50: }
51: if (test2.split("{").length == test2.split("}").length) {
52: if (test2.indexOf(Definitions.ARG_SEPARATOR) != -1) {
53: txt = '['+txt+']';
54: }
1.1 damieng 55: }
56: }
57: }
58:
59: if (txt != math_object.oldtxt) {
60: math_object.oldtxt = txt;
61: while (output_node.firstChild != null)
62: output_node.removeChild(output_node.firstChild);
63: output_node.removeAttribute("title");
64: if (txt != "") {
65: parser = math_object.parser;
66: try {
67: root = parser.parse(txt);
68: if (root != null) {
69: var math = document.createElement("math");
70: math.setAttribute("display", "block");
71: math.appendChild(root.toMathML());
72: output_node.appendChild(math);
73: MathJax.Hub.Queue(["Typeset", MathJax.Hub, output_node]);
74: }
75: } catch (e) {
76: output = "error: " + e;
77: output_node.setAttribute("title", output);
78: if (e instanceof ParseException) {
79: output_node.appendChild(document.createTextNode(txt.substring(0, e.from)));
80: var span = document.createElement('span');
81: span.appendChild(document.createTextNode(txt.substring(e.from, e.to + 1)));
1.3 damieng 82: span.style.border = 'solid 1px red';// this used to be CSS "math-error", but using CSS caused too many problems
83: span.style.minWidth = '1px';
1.1 damieng 84: output_node.appendChild(span);
85: if (e.to < txt.length - 1) {
86: output_node.appendChild(document.createTextNode(txt.substring(e.to + 1)));
87: }
88: } else {
89: var tn = document.createTextNode(output);
90: output_node.appendChild(tn);
91: }
92: }
93: }
94: }
95: }
96:
1.2 damieng 97: var math_objects = [];
1.1 damieng 98:
99: /*
100: Looks for elements with the "math" class, and
101: adds a preview div afterward which is updated automatically.
1.2 damieng 102: Can be called again after math fields have been added, removed, or when options have changed.
1.1 damieng 103: */
104: var initEditors = function() {
1.2 damieng 105: // to hide the MathJax messages (note: this could be done elsewhere)
106: MathJax.Hub.Config({
107: messageStyle: "none"
108: });
1.1 damieng 109: var math_inputs = document.getElementsByClassName('math');
1.2 damieng 110: // first remove the nodes and objects for the inputs that are gone
111: for (var i=0; i<math_objects.length; i++) {
112: var ta = math_objects[i].ta;
113: var found = false;
114: for (var j=0; j<math_inputs.length; j++) {
115: if (math_inputs[j] == ta) {
116: found = true;
117: break;
118: }
119: }
120: if (!found) {
121: var output_node = math_objects[i].output_node;
122: if (output_node.parentNode) {
123: output_node.parentNode.removeChild(output_node);
124: }
125: math_objects.splice(i, 1);
126: i--;
127: }
128: }
129: // then create or update nodes and objects for the new inputs
1.1 damieng 130: for (var i=0; i<math_inputs.length; i++) {
131: var ta = math_inputs[i];
132: if (ta.nodeName == "TEXTAREA" || ta.nodeName == "INPUT") {
1.2 damieng 133: var ind_math = -1;
134: for (var j=0; j<math_objects.length; j++) {
135: if (math_objects[j].ta == ta) {
136: ind_math = j;
137: break;
138: }
139: }
1.1 damieng 140: var implicit_operators = (ta.getAttribute("data-implicit_operators") === "true");
141: var unit_mode = (ta.getAttribute("data-unit_mode") === "true");
142: var constants = ta.getAttribute("data-constants");
143: if (constants)
144: constants = constants.split(/[\s,]+/);
1.2 damieng 145: var output_node;
146: if (ind_math == -1) {
147: output_node = document.createElement("span");
148: output_node.style.display = "none";
149: output_node.style.position = "absolute";
150: output_node.style.backgroundColor = "rgba(255,255,224,0.9)";
151: output_node.style.color = "black";
152: output_node.style.border = "1px solid #A0A0A0";
153: output_node.style.padding = "5px";
154: output_node.style.zIndex = "1";
155: var getCSSAbsolutePosition = function getCSSAbsolutePosition(el) {
156: var x = 0;
157: var y = 0;
158: while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) {
1.4 ! damieng 159: var scrollLeft;
! 160: if (el.nodeName == 'INPUT')
! 161: scrollLeft = 0;
! 162: else
! 163: scrollLeft = el.scrollLeft;
! 164: x += el.offsetLeft - scrollLeft;
1.2 damieng 165: y += el.offsetTop - el.scrollTop;
166: el = el.offsetParent;
167: if (el) {
168: var style = window.getComputedStyle(el);
169: if (style.position == 'absolute' || style.position == 'relative')
170: break;
171: }
172: }
173: return {top: y, left: x};
174: }
175: var place = function(ta, output_node) {
176: // position the output_node below or on top of ta
177: var ta_rect = ta.getBoundingClientRect();
178: var root = document.documentElement;
179: var docTop = (window.pageYOffset || root.scrollTop) - (root.clientTop || 0);
180: var docLeft = (window.pageXOffset || root.scrollLeft) - (root.clientLeft || 0);
181: var ta_pos = getCSSAbsolutePosition(ta);
182: output_node.style.left = ta_pos.left + "px";
183: if (window.innerHeight > ta_rect.bottom + output_node.offsetHeight)
184: output_node.style.top = (ta_pos.top + ta.offsetHeight) + "px";
185: else
186: output_node.style.top = (ta_pos.top - output_node.offsetHeight) + "px";
187: }
188: if (ta.nextSibling)
189: ta.parentNode.insertBefore(output_node, ta.nextSibling);
190: else
191: ta.parentNode.appendChild(output_node);
192: var hide_node = function(an_output_node) {
193: // returns a function that will hide the node on any event
194: // (we can't use the node directly because it changes in the loop)
195: return function(e) {
196: an_output_node.style.display = "none";
197: };
198: }
1.3 damieng 199: var hide_node_if_no_error = function(an_output_node) {
200: return function(e) {
201: if (!an_output_node.hasAttribute('title'))
202: an_output_node.style.display = "none";
203: };
204: }
1.2 damieng 205: var focus = function(a_ta, an_output_node) {
206: return function(e) {
207: if (a_ta.value != '') {
208: an_output_node.style.display = "block";
209: place(a_ta, an_output_node);
210: }
211: };
1.1 damieng 212: };
1.2 damieng 213: ta.addEventListener("blur", hide_node(output_node), false);
214: ta.addEventListener("focus", focus(ta, output_node), false);
1.3 damieng 215: output_node.addEventListener("mouseenter", hide_node_if_no_error(output_node), false);
1.2 damieng 216: ind_math = math_objects.length;
217: var oldtxt = "";
218: math_objects[ind_math] = {
219: "ta": ta,
220: "output_node": output_node,
221: "oldtxt": oldtxt,
222: "parser": new Parser(implicit_operators, unit_mode, constants)
223: };
224: var changeObjectN = function(n) {
225: return function(e) {
226: var obj = math_objects[n];
227: handleChange(obj);
228: if (document.activeElement == obj.ta) {
229: if (obj.ta.value != '') {
230: obj.output_node.style.display = "block";
231: MathJax.Hub.Queue(function () {
232: // position the element only when MathJax is done, because the output_node height might change
233: place(obj.ta, obj.output_node);
234: });
235: } else {
236: obj.output_node.style.display = "none";
237: }
238: }
239: };
240: };
241: var startChange = changeObjectN(ind_math);
242: if (ta.value != oldtxt)
243: startChange(); // process non-empty fields even though they are not visible yet
244: ta.addEventListener('change', startChange, false);
245: ta.addEventListener('keyup', startChange, false);
246: } else {
247: // only create a new parser and update the result if the options have changed
248: var same_constants;
249: var parser = math_objects[ind_math].parser;
250: if (!constants && parser.constants.length == 0) {
251: same_constants = true;
252: } else {
253: if (constants) {
254: same_constants = parser.constants.length == constants.length;
255: if (same_constants) {
256: for (var j=0; j<constants.length; j++) {
257: if (parser.constants[j] != constants[j]) {
258: same_constants = false;
259: break;
260: }
261: }
262: }
263: } else {
264: same_constants = false;
265: }
266: }
267: if (parser.implicit_operators != implicit_operators || parser.unit_mode != unit_mode || !same_constants) {
268: math_objects[ind_math].parser = new Parser(implicit_operators, unit_mode, constants);
269: if (ta.value != '') {
270: math_objects[ind_math].oldtxt = '';
271: handleChange(math_objects[ind_math]);
272: }
273: }
274: }
1.1 damieng 275: }
276: }
277: }
278:
1.2 damieng 279: /**
280: * Updates display for <span class="math"> and <div class="math"> (LON-CAPA ln and dlm tags)
281: */
282: var updateMathSpanAndDiv = function() {
283: var nl = document.getElementsByClassName('math');
284: // convert to an array because the nodelist would change as we are removing nodes from the document
285: var math_nodes = [];
286: for (var i = 0, ref = math_nodes.length = nl.length; i < ref; i++) {
287: math_nodes[i] = nl[i];
288: }
289: for (var i=0; i<math_nodes.length; i++) {
290: var el = math_nodes[i];
291: if (el.nodeName == "SPAN" || el.nodeName == "DIV") {
292: if (el.firstChild == null || el.firstChild.nodeType != 3)
293: continue;
294: var bspan = (el.nodeName == "SPAN");
295: var txt = el.firstChild.nodeValue;
296: var implicit_operators = (el.getAttribute("data-implicit_operators") === "true");
297: var unit_mode = (el.getAttribute("data-unit_mode") === "true");
298: var constants = el.getAttribute("data-constants");
299: if (constants)
300: constants = constants.split(/[\s,]+/);
301: var parser = new Parser(implicit_operators, unit_mode, constants);
302: try {
303: var root = parser.parse(txt);
304: if (root != null) {
305: var math = document.createElement("math");
306: math.setAttribute("display", bspan ? "inline" : "block");
307: math.appendChild(root.toMathML(['#000000']));
308: // at this point it would be nice to replace el by math, but MathJax does not
309: // always typeset math elements when given directly, so we need to typeset the parent...
310: el.classList.remove('math');
311: el.removeChild(el.firstChild);
312: el.appendChild(math);
313: MathJax.Hub.Queue(["Typeset", MathJax.Hub, el]);
314: }
315: } catch (e) {
316: el.firstChild.nodeValue = "[syntax error in math:" + e + "]";
317: el.classList.remove('math');
318: }
319: }
320: }
321: }
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>