Annotation of loncom/html/res/adm/pages/reactionresponse/reaction_preview.js, revision 1.2
1.1 damieng 1: /*
2:
3: Copyright Michigan State University Board of Trustees
4:
5: This file is part of the LearningOnline Network with CAPA (LON-CAPA).
6:
7: LON-CAPA is free software; you can redistribute it and/or modify
8: it under the terms of the GNU General Public License as published by
9: the Free Software Foundation; either version 2 of the License, or
10: (at your option) any later version.
11:
12: LON-CAPA is distributed in the hope that it will be useful,
13: but WITHOUT ANY WARRANTY; without even the implied warranty of
14: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15: GNU General Public License for more details.
16:
17: You should have received a copy of the GNU General Public License
18: along with LON-CAPA; if not, write to the Free Software
19: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20:
21: /home/httpd/html/adm/gpl.txt
22:
23: http://www.lon-capa.org/
24:
25: */
26:
27: 'use strict';
28:
29: // initialize namespaces if necessary
30: var LC = LC || {};
31: LC.HW = LC.HW || {};
32:
33:
34: /**
35: * This is implementing the reaction response real-time preview.
36: * Some of the code comes from the old reaction editor at
37: * loncom/html/res/adm/pages/reactionresponse/reaction_editor.html
38: *
39: * @constructor
40: *
41: * @param {string} field_id - the input field id
42: */
43: LC.HW.ReactionPreview = function(field_id) {
44: /** @type {string} */
45: this.field_id = field_id;
46: /** @type {number} */
47: this.level = null;
48: /** @type {Array.<string>} */
49: this.reactants = null;
50: /** @type {Array.<string>} */
51: this.products = null;
52: /** @type {string} */
53: this.old_string = null;
54: }
55:
56: /**
57: * Parses the reaction string
58: * @param {string} s - the user input
59: */
60: LC.HW.ReactionPreview.prototype.parse_reaction = function(s) {
61: var reaction_array = s.split(/\s*->\s*/);
62: this.reactants = new Array(0);
63: this.products = new Array(0);
64:
65: if (reaction_array.length > 0)
66: this.reactants = reaction_array[0].split(/\s+\+\s*/);
67: if (reaction_array.length > 1)
68: this.products = reaction_array[1].split(/\s+\+\s*/);
69: }
70:
71: /**
72: * Converts the reaction string to the CAPA syntax.
73: * @param {string} s - the user input
74: * @returns {string}
75: */
76: LC.HW.ReactionPreview.prototype.to_capa = function(s) {
77: var reaction = '';
78: var i;
79:
80: this.parse_reaction(s);
81:
82: for (i = 0; i < this.reactants.length; i++)
83: this.reactants[i] = this.capa_component(this.reactants[i]);
84: for (i = 0; i < this.products.length; i++)
85: this.products[i] = this.capa_component(this.products[i]);
86:
87: // this.reactants.sort();
88: // this.products.sort();
89:
90: for (i = 0; i < this.reactants.length-1; i++) {
91: reaction += this.reactants[i];
92: reaction += ' + ';
93: }
94: if (i < this.reactants.length)
95: reaction += this.reactants[i];
96: if (this.products.length > 0) {
97: reaction += ' -> ';
98: for (i = 0; i < this.products.length-1; i++) {
99: reaction += this.products[i];
100: reaction += ' + ';
101: }
102: if (i < this.products.length)
103: reaction += this.products[i];
104: }
105:
106: return reaction;
107: }
108:
109: /**
110: * Converts a component string to the CAPA syntax.
111: * @param {string} s - the component string
112: */
113: LC.HW.ReactionPreview.prototype.capa_component = function(s) {
114: var reactant = '';
115: var i = 0;
116: this.level = 0;
117:
118: for ( ; s.substring(i, i+1) == ' '; i++)
119: ;
120: for ( ; LC.HW.ReactionPreview.isDigit(s.substring(i, i+1)); i++)
121: reactant += s.substring(i, i+1);
122: for ( ; i < s.length; i++)
123: reactant += this.capa_char(s.substring(i, i+1));
124:
125: return reactant;
126: }
127:
128: /**
129: * Processes a character for the CAPA syntax.
130: * @param {string} chr - a character
131: * @returns {string}
132: */
133: LC.HW.ReactionPreview.prototype.capa_char = function(chr) {
134: if (this.level == 0) { // baseline
135: if (chr == '^')
136: this.level = 1;
137: if (chr == ' ')
138: return '';
139: return chr;
140: }
141: if (this.level == 1) { // superscript
142: if (LC.HW.ReactionPreview.isDigit(chr))
143: return chr;
144: this.level = 0;
145: return chr;
146: }
147: }
148:
149: /**
150: * Converts a reaction string to HTML for preview.
151: * @param {string} string - the user input
152: * @returns {string}
153: */
154: LC.HW.ReactionPreview.prototype.to_html = function(string) {
155: var reaction = '';
156: var i;
157:
158: this.parse_reaction(string);
159: for (i = 0; i < this.reactants.length-1; i++) {
160: reaction += this.html_component(this.reactants[i]);
161: reaction += ' + ';
162: }
163: reaction += this.html_component(this.reactants[i]);
164:
165: if (this.products.length > 0) {
166: reaction += ' → ';
167: for (i = 0; i < this.products.length-1; i++) {
168: reaction += this.html_component(this.products[i]);
169: reaction += ' + ';
170: }
171: reaction += this.html_component(this.products[i]);
172: }
173:
174: return reaction;
175: }
176:
177: /**
178: * Converts a component to HTML.
179: * @param {string} string - the user string for the component
180: * @returns {string}
181: */
182: LC.HW.ReactionPreview.prototype.html_component = function(string) {
183: var reactant = '';
184: var i = 0;
185: this.level = -1;
186:
187: var hydrate = string.split('.');
188: if (hydrate.length > 1)
189: return this.html_component(hydrate[0]) + '·' + this.html_component(hydrate[1]);
190:
191: for ( ; i < string.length; i++)
192: reactant += this.html_char(string.substring(i, i+1));
193:
194: return reactant;
195: }
196:
197: /**
198: * Converts a character to HTML, updating the level.
199: * @param {string} string - the user input character
200: * @returns {string}
201: */
202: LC.HW.ReactionPreview.prototype.html_char = function(chr) {
203: if (this.level == -1) { // stoichiometric coefficient
204: if (LC.HW.ReactionPreview.isDigit(chr) || chr == '/')
205: return chr;
206: if (chr == ' ')
207: return '';
208: else
209: this.level = 0;
210: }
211: if (this.level == 0) { // baseline
212: if (LC.HW.ReactionPreview.isDigit(chr))
213: return chr.sub();
214: if (chr == '^') {
215: this.level = 1;
216: return '';
217: }
218: if (chr == '+') // baseline or superscript?
219: return '?';
220: if (chr == ' ')
221: return '';
222: if (chr == '(' || chr == '|') // coefficient
223: this.level = -1;
224: return chr;
225: }
226: if (this.level == 1) { // superscript
227: if (LC.HW.ReactionPreview.isDigit(chr))
228: return chr.sup();
229: if (chr == '+') {
230: this.level = 0;
231: return chr.sup();
232: }
233: if (chr == '-') {
234: this.level = 0;
235: return '<sup>−</sup>';
236: }
237: if (chr == ' ') {
238: this.level = 0;
239: return '';
240: }
241: this.level = 0;
242: return chr;
243: }
244: }
245:
246: /**
247: * Starts listening to input changes to display the preview.
248: */
249: LC.HW.ReactionPreview.prototype.start_preview = function() {
250: var ta = document.getElementById(this.field_id);
251: // The old reaction editor was suggesting using a readonly textline.
252: // To let users edit the field directly with problems using readonly=yes,
253: // the input's readOnly has to be set to false.
254: ta.readOnly = false;
255: var output_node = document.createElement('span');
256: output_node.id = this.field_id + '_preview';
257: output_node.style.display = 'none';
258: output_node.style.position = 'absolute';
259: output_node.style.backgroundColor = 'rgba(255,255,224,0.9)';
260: output_node.style.color = 'black';
261: output_node.style.border = '1px solid #A0A0A0';
262: output_node.style.padding = '5px';
263: output_node.style.zIndex = '1';
264: if (ta.nextSibling)
265: ta.parentNode.insertBefore(output_node, ta.nextSibling);
266: else
267: ta.parentNode.appendChild(output_node);
268: var hide_node = function(e) {
269: output_node.style.display = 'none';
270: }
271: output_node.addEventListener('mouseenter', hide_node, false);
272: var that = this;
273: var blur = function(e) {
274: output_node.style.display = 'none';
275: ta.value = that.to_capa(ta.value);
276: };
277: ta.addEventListener('blur', blur, false);
1.2 ! damieng 278: var keydown = function(e) {
! 279: if (e.keyCode == 13) // ENTER
! 280: ta.value = that.to_capa(ta.value);
! 281: };
! 282: ta.addEventListener('keydown', keydown, false);
1.1 damieng 283: var focus = function(e) {
284: if (ta.value != '') {
285: if (ta.value != this.old_string) {
286: that.handleChange();
287: } else {
288: output_node.style.display = 'block';
289: LC.HW.ReactionPreview.place(ta, output_node);
290: }
291: }
292: };
293: ta.addEventListener('focus', focus, false);
294: this.old_string = '';
295: var startChange = function(e) {
296: that.handleChange();
297: };
298: ta.addEventListener('change', startChange, false);
299: ta.addEventListener('keyup', startChange, false);
300: }
301:
302: /**
303: * Handle a change in the user input
304: */
305: LC.HW.ReactionPreview.prototype.handleChange = function() {
306: var ta = document.getElementById(this.field_id);
307: if (document.activeElement == ta) {
308: var output_node = document.getElementById(this.field_id + '_preview');
309: var s = ta.value;
310: this.old_string = s;
311: if (s != '') {
312: output_node.innerHTML = this.to_html(s);
313: output_node.style.display = 'block';
314: LC.HW.ReactionPreview.place(ta, output_node);
315: } else {
316: output_node.style.display = 'none';
317: }
318: }
319: }
320:
321: /**
322: * Returns an element absolute position.
323: * @static
324: * @param {Element} el
325: * @returns {Object} - keys: top, left
326: */
327: LC.HW.ReactionPreview.getCSSAbsolutePosition = function(el) {
328: var x = 0;
329: var y = 0;
330: while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) {
331: var scrollLeft;
332: if (el.nodeName == 'INPUT')
333: scrollLeft = 0;
334: else
335: scrollLeft = el.scrollLeft;
336: x += el.offsetLeft - scrollLeft;
337: y += el.offsetTop - el.scrollTop;
338: el = el.offsetParent;
339: if (el) {
340: var style = window.getComputedStyle(el);
341: if (style.position == 'absolute' || style.position == 'relative')
342: break;
343: }
344: }
345: return {top: y, left: x};
346: }
347:
348: /**
349: * Positions the output_node below or on top of the input field
350: * @static
351: * @param {Element} ta - the input field
352: * @param {Element} output_node - the preview node
353: */
354: LC.HW.ReactionPreview.place = function(ta, output_node) {
355: var ta_rect = ta.getBoundingClientRect();
356: var root = document.documentElement;
357: var docTop = (window.pageYOffset || root.scrollTop) - (root.clientTop || 0);
358: var docLeft = (window.pageXOffset || root.scrollLeft) - (root.clientLeft || 0);
359: var ta_pos = LC.HW.ReactionPreview.getCSSAbsolutePosition(ta);
360: output_node.style.left = ta_pos.left + 'px';
361: if (window.innerHeight > ta_rect.bottom + output_node.offsetHeight)
362: output_node.style.top = (ta_pos.top + ta.offsetHeight) + 'px';
363: else
364: output_node.style.top = (ta_pos.top - output_node.offsetHeight) + 'px';
365: }
366:
367: /**
368: * Returns true if the given character is a digit.
369: * @static
370: * @param {string} chr - a character
371: * @returns {boolean}
372: */
373: LC.HW.ReactionPreview.isDigit = function(chr) {
374: return (chr >= '0' && chr <= '9');
375: }
376:
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>