File:  [LON-CAPA] / loncom / html / res / adm / pages / reactionresponse / reaction_preview.js
Revision 1.2: download - view: text, annotated - select for diffs
Thu Jan 5 15:17:18 2017 UTC (7 years, 11 months ago) by damieng
Branches: MAIN
CVS tags: version_2_12_X, version_2_11_X, version_2_11_5_msu, version_2_11_5, version_2_11_4_uiuc, version_2_11_4_msu, version_2_11_4, HEAD
reaction preview: also fixing the syntax when the return key is used

/*

Copyright Michigan State University Board of Trustees

This file is part of the LearningOnline Network with CAPA (LON-CAPA).

LON-CAPA is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

LON-CAPA is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with LON-CAPA; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

/home/httpd/html/adm/gpl.txt

http://www.lon-capa.org/

*/

'use strict';

// initialize namespaces if necessary
var LC = LC || {};
LC.HW = LC.HW || {};


/**
 * This is implementing the reaction response real-time preview.
 * Some of the code comes from the old reaction editor at
 * loncom/html/res/adm/pages/reactionresponse/reaction_editor.html
 * 
 * @constructor
 * 
 * @param {string} field_id - the input field id
 */
LC.HW.ReactionPreview = function(field_id) {
    /** @type {string} */
    this.field_id = field_id;
    /** @type {number} */
    this.level = null;
    /** @type {Array.<string>} */
    this.reactants = null;
    /** @type {Array.<string>} */
    this.products = null;
    /** @type {string} */
    this.old_string = null;
}

/**
 * Parses the reaction string
 * @param {string} s - the user input
 */
LC.HW.ReactionPreview.prototype.parse_reaction = function(s) {
    var reaction_array = s.split(/\s*->\s*/);
    this.reactants = new Array(0);
    this.products = new Array(0);

    if (reaction_array.length > 0) 
        this.reactants = reaction_array[0].split(/\s+\+\s*/);
    if (reaction_array.length > 1) 
        this.products = reaction_array[1].split(/\s+\+\s*/);
}

/**
 * Converts the reaction string to the CAPA syntax.
 * @param {string} s - the user input
 * @returns {string}
 */
LC.HW.ReactionPreview.prototype.to_capa = function(s) {
    var reaction = '';
    var i;
    
    this.parse_reaction(s);
    
    for (i = 0; i < this.reactants.length; i++) 
        this.reactants[i] = this.capa_component(this.reactants[i]);
    for (i = 0; i < this.products.length; i++) 
        this.products[i] = this.capa_component(this.products[i]);
    
    // this.reactants.sort();
    // this.products.sort();
    
    for (i = 0; i < this.reactants.length-1; i++) {
        reaction += this.reactants[i];
        reaction += ' + ';
    }
    if (i < this.reactants.length)
        reaction += this.reactants[i];
    if (this.products.length > 0) {
        reaction += ' -> ';
        for (i = 0; i < this.products.length-1; i++) {
            reaction += this.products[i];
            reaction += ' + ';
        }
        if (i < this.products.length)
            reaction += this.products[i];
    }
    
    return reaction;
}

/**
 * Converts a component string to the CAPA syntax.
 * @param {string} s - the component string
 */
LC.HW.ReactionPreview.prototype.capa_component = function(s) {
    var reactant = '';
    var i = 0;
    this.level = 0;
    
    for ( ; s.substring(i, i+1) == ' '; i++)
        ;
    for ( ; LC.HW.ReactionPreview.isDigit(s.substring(i, i+1)); i++)
        reactant += s.substring(i, i+1);
    for ( ; i < s.length; i++)
        reactant += this.capa_char(s.substring(i, i+1));
    
    return reactant;
}

/**
 * Processes a character for the CAPA syntax.
 * @param {string} chr - a character
 * @returns {string}
 */
LC.HW.ReactionPreview.prototype.capa_char = function(chr) {
    if (this.level == 0) { // baseline
        if (chr == '^') 
            this.level = 1;
        if (chr == ' ')
            return '';
        return chr;
    }
    if (this.level == 1) { // superscript
        if (LC.HW.ReactionPreview.isDigit(chr))
            return chr;
        this.level = 0;
        return chr;
    }
}

/**
 * Converts a reaction string to HTML for preview.
 * @param {string} string - the user input
 * @returns {string}
 */
LC.HW.ReactionPreview.prototype.to_html = function(string) {
    var reaction = '';
    var i;

    this.parse_reaction(string);
    for (i = 0; i < this.reactants.length-1; i++) {
        reaction += this.html_component(this.reactants[i]);
        reaction += ' + ';
    }
    reaction += this.html_component(this.reactants[i]);

    if (this.products.length > 0) {
        reaction += ' &rarr; ';
        for (i = 0; i < this.products.length-1; i++) {
            reaction += this.html_component(this.products[i]);
            reaction += ' + ';
        }
        reaction += this.html_component(this.products[i]);
    }

    return reaction;
}

/**
 * Converts a component to HTML.
 * @param {string} string - the user string for the component
 * @returns {string}
 */
LC.HW.ReactionPreview.prototype.html_component = function(string) {
    var reactant = '';
    var i = 0;
    this.level = -1;

    var hydrate = string.split('.');
    if (hydrate.length > 1) 
        return this.html_component(hydrate[0]) + '&middot;' + this.html_component(hydrate[1]);
        
    for ( ; i < string.length; i++)
        reactant += this.html_char(string.substring(i, i+1));

    return reactant;
}

/**
 * Converts a character to HTML, updating the level.
 * @param {string} string - the user input character
 * @returns {string}
 */
LC.HW.ReactionPreview.prototype.html_char = function(chr) {
    if (this.level == -1) { // stoichiometric coefficient
        if (LC.HW.ReactionPreview.isDigit(chr) || chr == '/')
            return chr;
        if (chr == ' ')
            return '';
        else
            this.level = 0;
    }
    if (this.level == 0) { // baseline
        if (LC.HW.ReactionPreview.isDigit(chr))
            return chr.sub();
        if (chr == '^') {
            this.level = 1;
            return '';
        }
        if (chr == '+') // baseline or superscript?
            return '?';
        if (chr == ' ')
            return '';
        if (chr == '(' || chr == '|') // coefficient
            this.level = -1;
        return chr;
    }
    if (this.level == 1) { // superscript
        if (LC.HW.ReactionPreview.isDigit(chr))
            return chr.sup();
        if (chr == '+') {
            this.level = 0;
            return chr.sup();
        }
        if (chr == '-') {
            this.level = 0;
            return '<sup>&minus;</sup>';
        } 
        if (chr == ' ') {
            this.level = 0;
            return '';
        }
        this.level = 0;
        return chr;
    }
}

/**
 * Starts listening to input changes to display the preview.
 */
LC.HW.ReactionPreview.prototype.start_preview = function() {
    var ta = document.getElementById(this.field_id);
    // The old reaction editor was suggesting using a readonly textline.
    // To let users edit the field directly with problems using readonly=yes,
    // the input's readOnly has to be set to false.
    ta.readOnly = false;
    var output_node = document.createElement('span');
    output_node.id = this.field_id + '_preview';
    output_node.style.display = 'none';
    output_node.style.position = 'absolute';
    output_node.style.backgroundColor = 'rgba(255,255,224,0.9)';
    output_node.style.color = 'black';
    output_node.style.border = '1px solid #A0A0A0';
    output_node.style.padding = '5px';
    output_node.style.zIndex = '1';
    if (ta.nextSibling)
        ta.parentNode.insertBefore(output_node, ta.nextSibling);
    else
        ta.parentNode.appendChild(output_node);
    var hide_node = function(e) {
        output_node.style.display = 'none';
    }
    output_node.addEventListener('mouseenter', hide_node, false);
    var that = this;
    var blur = function(e) {
        output_node.style.display = 'none';
        ta.value = that.to_capa(ta.value);
    };
    ta.addEventListener('blur', blur, false);
    var keydown = function(e) {
        if (e.keyCode == 13) // ENTER
            ta.value = that.to_capa(ta.value);
    };
    ta.addEventListener('keydown', keydown, false);
    var focus = function(e) {
        if (ta.value != '') {
            if (ta.value != this.old_string) {
                that.handleChange();
            } else {
                output_node.style.display = 'block';
                LC.HW.ReactionPreview.place(ta, output_node);
            }
        }
    };
    ta.addEventListener('focus', focus, false);
    this.old_string = '';
    var startChange = function(e) {
        that.handleChange();
    };
    ta.addEventListener('change', startChange, false);
    ta.addEventListener('keyup', startChange, false);
}

/**
 * Handle a change in the user input
 */
LC.HW.ReactionPreview.prototype.handleChange = function() {
    var ta = document.getElementById(this.field_id);
    if (document.activeElement == ta) {
        var output_node = document.getElementById(this.field_id + '_preview');
        var s = ta.value;
        this.old_string = s;
        if (s != '') {
            output_node.innerHTML = this.to_html(s);
            output_node.style.display = 'block';
            LC.HW.ReactionPreview.place(ta, output_node);
        } else {
            output_node.style.display = 'none';
        }
    }
}

/**
 * Returns an element absolute position.
 * @static
 * @param {Element} el
 * @returns {Object} - keys: top, left
 */
LC.HW.ReactionPreview.getCSSAbsolutePosition = function(el) {
    var x = 0;
    var y = 0;
    while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) {
        var scrollLeft;
        if (el.nodeName == 'INPUT')
            scrollLeft = 0;
        else
            scrollLeft = el.scrollLeft;
        x += el.offsetLeft - scrollLeft;
        y += el.offsetTop - el.scrollTop;
        el = el.offsetParent;
        if (el) {
            var style = window.getComputedStyle(el);
            if (style.position == 'absolute' || style.position == 'relative')
                break;
        }
    }
    return {top: y, left: x};
}

/**
 * Positions the output_node below or on top of the input field
 * @static
 * @param {Element} ta - the input field
 * @param {Element} output_node - the preview node
 */
LC.HW.ReactionPreview.place = function(ta, output_node) {
    var ta_rect = ta.getBoundingClientRect();
    var root = document.documentElement;
    var docTop = (window.pageYOffset || root.scrollTop)  - (root.clientTop || 0);
    var docLeft = (window.pageXOffset || root.scrollLeft) - (root.clientLeft || 0);
    var ta_pos = LC.HW.ReactionPreview.getCSSAbsolutePosition(ta);
    output_node.style.left = ta_pos.left + 'px';
    if (window.innerHeight > ta_rect.bottom + output_node.offsetHeight)
        output_node.style.top = (ta_pos.top + ta.offsetHeight) + 'px';
    else
        output_node.style.top = (ta_pos.top - output_node.offsetHeight) + 'px';
}

/**
 * Returns true if the given character is a digit.
 * @static
 * @param {string} chr - a character
 * @returns {boolean}
 */
LC.HW.ReactionPreview.isDigit = function(chr) {
    return (chr >= '0' && chr <= '9');
}


FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>