<commit_before>var selected_cancer_type = 'tcga_gbm';

var SELECTED_CANCER_TYPE_OLD = '';
var SELECTED_CANCER_TYPE_NEW = '';

// todo: put all the jquery selectors in one place
$(document).ready(function() {
    $('#select_cancer_type').change(function() {
        SELECTED_CANCER_TYPE_NEW = $('#select_cancer_type').val();
    });
});

var Gistic = function(gistics) {
    // store the DataTable object once it has been created
    Gistic.dt = '';

    Gistic.gene_list_el = $('#gene_list');
    Gistic.dialog_el = $('#gistic_dialog');

    var sort_by_cytoband = function(x,y) {
        // sorts two cytobands,
        // where a cytoband in an array of strings, e.g. [12, p, 11, 5] ~ 12p11.5

        if (parseInt(x[1]) - parseInt(y[1]) !== 0) {
            return parseInt(x[1]) - parseInt(y[1]);
        } else if (x[2] === 'p' && y[2] === 'q') {
            return -1;
        } else if (x[2] === 'q' && y[2] === 'p') {
            return 1;
        } else if (x[2] === y[2]) {
            return parseInt(x[3]) - parseInt(y[3]);
        } else {
            console.log('error: cytoband sorting logic fell through');
        }
    };

    $.extend( $.fn.dataTableExt.oSort, {
        // bind the cytoband sorting function
        "cytoband-asc": sort_by_cytoband,

        "cytoband-desc": function(x,y) {
            return -1 * sort_by_cytoband(x,y);
        }
    } );

    var self = {
        getDt: function() {return dt;},

        drawTable : function(table_el, genes, options) {
            // draws a DataTable in the specific DOM element, table_el
            // with the specified DataTable options

            var aaData = gistics;

            var aoColumnDefs = [
                {"sTitle": "
Amp
" +
                    "
Del
",
                    "sWidth": '5px',
                    "aTargets": [0],
                    "mDataProp": function(source, type, val) {
                        if (type === 'display') {
                            if (source.ampdel) {     // true means amplified
                                // mark amps/dels as reds and blues
                                return "

";
                            } else {
                                return "

";
                            }
                        }
                        return source.ampdel;
                    }
            },
            {"sTitle": "Chr", "aTargets":[1],
                "mDataProp": function(source, type, val) {
                    if (type === 'display') {
                        return source.chromosome;
                    }
                    return source.chromosome;
                }
            },
            {"sTitle": "Cytoband", "aTargets":[2], "sType": "cytoband",
                "mDataProp": function(source, type, val) {
                    var cyto = source.cytoband;
                    if (type === 'display') {
                        return cyto;
                    }
                    else if (type === 'sort') {
                        // eg. 17p12.1
                        var regexp = "([0-9]{1,2})" +   // match the chr
                            "([pq])" +                  // match the arm
                            "([0-9]{1,2})" +            // match the first coordinate
                            "(?:\.?)" +                 // noncapturing, optional,
                            // match the decimal point
                            "([0-9]{0,2})";            // optional, match the 2nd coordinate
                        regexp = "^" + regexp + "$";
                        regexp = new RegExp(regexp);

                        return cyto.match(regexp);
                    }
                    return source;
                }
            },
            {"sTitle": "#", "aTargets":[3], "sType": "numeric", "sClass": 'gistic_center_col',
                "mDataProp": function(source, type, val) {
                    return source.nonSangerGenes.length + source.sangerGenes.length;
                }
            },
            {"sTitle": "Genes",
                "aTargets":[4], "sType": "numeric", "sClass": "gistic_gene_cell",
                "mDataProp": function(source, type, val) {
                    var all_genes = source.sangerGenes.concat(source.nonSangerGenes);

                    if (type === 'display') {

                        var all_genes = $.map(all_genes, function(g) {
                            // bind ioGeneSet to each gene
                            // highlight ones that are already in the gene list

                            var highlight = '';

                            if (genes.indexOf(g) !== -1) {
                                highlight = ' gistic_selected_gene';
                            }

                            return "" + g + "";
                        });

                        if (all_genes.length > 5) {

                            all_genes = all_genes.slice(0,5)    // visible genes
                            .concat(" +" +
                                    (all_genes.length - 5)  + " more")
                            .concat(" less")

                            .concat("
") // hidden genes div
                            .concat(all_genes.slice(5))
                            .concat("
")
                        }

                        return all_genes.join(" ");
                    }
                    else if (type === 'sort') {
                        return all_genes.length;
                    }
                    return all_genes;
                }
            },
            {"sTitle": "Q Value", "aTargets":[5], "sType": "numeric", "sClass": "gistic_center_col",
                "mDataProp": function(source, type, val) {
                    var rounded = parseFloat(source.qval) .toExponential(1);   // round Q-Values
                    if (type === 'display') {
                        return rounded;
                    }
                    return rounded;
                }
            }
            ];

            options.aaSorting = [[ 5, "asc" ]];     // sort Q-Value column on load
            options.oLanguage = {'sSearch': 'Filter by Gene:'};
            options.aaData = aaData;
            options.aoColumnDefs = aoColumnDefs;

            Gistic.dt = table_el.dataTable(options);

            // everytime you draw
            // update the selected_genes
            Gistic.selected_genes = $.map($('.gistic_selected_gene'),
                function(i, val) {
                return $(val).html();
            });

            // paint regions red and blue
            //$('.gistic_amp').parent().css('background-color', 'red');
            //$('.gistic_del').parent().css('background-color', 'blue');

            // center cols
            $('.gistic_center_col').css('text-align', 'center');

            // todo: maybe we'll want this someday
            // bind double clicking
            //Gistic.dt.fnGetNodes().forEach(function(i) {
            //    $(i).find('.gistic_gene_cell').
            //        select(Gistic.UI.select_all_genes);
            //});

            // put in the help box
            $('#gistic_table_filter').parent().
                prepend('' +
                        'Click on a gene to select it');

            $('#gistic_close').show();
            return;
        },

        getGistics: function() {
            return gistics;
        }
    };

    return self;
};

Gistic.UI = ( function() {
    // dump of all sorts of UI functions
    // the closure is to keep the private GISTIC variable

    var GISTIC = {};

    return {
        open_dialog : function() {

            Gistic.table_el = $('#gistic_table');

            var options = { "sScrollY": "350px",
                "bPaginate": false,
                "bJQueryUI": true,
                "bDestroy": true};

            $('#gistic_msg_box').hide();
            $('#gistic_cancel').hide();

            $('#gistic_loading').show();
            $('#gistic_dialog').dialog('open');

            var genes = GeneSet($('#gene_list').val()).getAllGenes();

            // hide the Gistic button when there is no gistic data
            // if ajax hasn't already been done...then do an ajax call
            if (SELECTED_CANCER_TYPE_OLD !== SELECTED_CANCER_TYPE_NEW) {

                SELECTED_CANCER_TYPE_OLD = SELECTED_CANCER_TYPE_NEW;

                $.ajax({
                    url: 'Gistic.json',
                    data: {'selected_cancer_type': SELECTED_CANCER_TYPE_NEW},
                    dataType: 'json',
                    success: function(data) {
                        $('#gistic_loading').hide();

                        GISTIC = Gistic(data);

                        GISTIC.drawTable(Gistic.table_el, genes, options);
                    }
                });
            } else {
                $('#gistic_loading').hide();
                GISTIC.drawTable(Gistic.table_el, genes, options);
            }

            // redraw table
            //Gistic.dt.fnDraw();
        },

        expandGisticGenes : function(el) {
            // shows/hides additional genes in the genes column
            // currently initializing to hiding all non-Sanger genes

            el = $(el).parents()[0];

            // grab all the elements
            var more = $(el).children('#gistic_more');
            var less = $(el).children('#gistic_less');
            var hidden = $(el).children('#gistic_hidden');

            // and toggle them
            $(more).toggle();
            $(less).toggle();
            $(hidden).slideToggle('slow');
        },

        ioGeneSet : function(el) {
            $(el).toggleClass('gistic_selected_gene');
        },

        updateGenes: function() {
            var geneSet = GeneSet(Gistic.gene_list_el.val());

            var currently_selected = $.map($('.gistic_selected_gene'),
                function(i, val) { return $(val).html(); });

            var remove_genes = $.grep(Gistic.selected_genes, function(i) {
                // genes that are not selected but are in the geneset
                return $.inArray(Gistic.selected_genes[i], currently_selected) === -1;
            });

            var new_genes = $.grep(currently_selected, function(i) {
                // genes that are selected and not in the gene set
                return $.inArray(currently_selected[i], geneSet.getAllGenes()) === -1;
            });

            $.each(remove_genes, function(i,val) {
                // remove remove_genes from geneset
                geneSet.filterOut(val);
            });

            // append new_genes
            var out = geneSet.toString() + '\n' + $.makeArray(new_genes).join(" ");
            out = out.trim();

            // push to gene set
            Gistic.gene_list_el.val(out);
        },

        select_all_genes: function(el) {
            // todo: maybe we'll want this someday
            var max = 50;       // max no of genes users are allowed to select
            var selection = $(this).find('.gistic_gene');

            if (selection.length > 50) {
                // show error message
                $('#gistic_msg_box').show();
                return;
            } else {
                $('#gistic_msg_box').hide();
            }

            if (selection.length > 5) {
                // expand the genes if there are genes to be expanded
                $(this).find('#gistic_more').click();
            }
            selection.toggleClass('gistic_selected_gene');
        }
    };
})();
<commit_msg>renders in IE8. now to get the select genes button to work<commit_after>var selected_cancer_type = 'tcga_gbm';

var SELECTED_CANCER_TYPE_OLD = '';
var SELECTED_CANCER_TYPE_NEW = '';

// todo: put all the jquery selectors in one place
$(document).ready(function() {
    $('#select_cancer_type').change(function() {
        SELECTED_CANCER_TYPE_NEW = $('#select_cancer_type').val();
    });
});

var Gistic = function(gistics) {
    // store the DataTable object once it has been created
    Gistic.dt = '';

    Gistic.gene_list_el = $('#gene_list');
    Gistic.dialog_el = $('#gistic_dialog');

    var sort_by_cytoband = function(x,y) {
        // sorts two cytobands,
        // where a cytoband in an array of strings, e.g. [12, p, 11, 5] ~ 12p11.5

        if (parseInt(x[1]) - parseInt(y[1]) !== 0) {
            return parseInt(x[1]) - parseInt(y[1]);
        } else if (x[2] === 'p' && y[2] === 'q') {
            return -1;
        } else if (x[2] === 'q' && y[2] === 'p') {
            return 1;
        } else if (x[2] === y[2]) {
            return parseInt(x[3]) - parseInt(y[3]);
        } else {
            console.log('error: cytoband sorting logic fell through');
        }
    };

    $.extend( $.fn.dataTableExt.oSort, {
        // bind the cytoband sorting function
        "cytoband-asc": sort_by_cytoband,

        "cytoband-desc": function(x,y) {
            return -1 * sort_by_cytoband(x,y);
        }
    } );

    var self = {
        getDt: function() {return dt;},

        drawTable : function(table_el, genes, options) {
            // draws a DataTable in the specific DOM element, table_el
            // with the specified DataTable options

            var aaData = gistics;

            var aoColumnDefs = [
                {"sTitle": "
Amp
" +
                    "
Del
",
                    "sWidth": '5px',
                    "aTargets": [0],
                    "mDataProp": function(source, type, val) {
                        if (type === 'display') {
                            if (source.ampdel) {     // true means amplified
                                // mark amps/dels as reds and blues
                                return "

";
                            } else {
                                return "

";
                            }
                        }
                        return source.ampdel;
                    }
            },
            {"sTitle": "Chr", "aTargets":[1],
                "mDataProp": function(source, type, val) {
                    if (type === 'display') {
                        return source.chromosome;
                    }
                    return source.chromosome;
                }
            },
            {"sTitle": "Cytoband", "aTargets":[2], "sType": "cytoband",
                "mDataProp": function(source, type, val) {
                    var cyto = source.cytoband;
                    if (type === 'display') {
                        return cyto;
                    }
                    else if (type === 'sort') {
                        // eg. 17p12.1
                        var regexp = "([0-9]{1,2})" +   // match the chr
                            "([pq])" +                  // match the arm
                            "([0-9]{1,2})" +            // match the first coordinate
                            "(?:\.?)" +                 // noncapturing, optional,
                            // match the decimal point
                            "([0-9]{0,2})";            // optional, match the 2nd coordinate
                        regexp = "^" + regexp + "$";
                        regexp = new RegExp(regexp);

                        return cyto.match(regexp);
                    }
                    return source;
                }
            },
            {"sTitle": "#", "aTargets":[3], "sType": "numeric", "sClass": 'gistic_center_col',
                "mDataProp": function(source, type, val) {
                    return source.nonSangerGenes.length + source.sangerGenes.length;
                }
            },
            {"sTitle": "Genes",
                "aTargets":[4], "sType": "numeric", "sClass": "gistic_gene_cell",
                "mDataProp": function(source, type, val) {
                    var all_genes = source.sangerGenes.concat(source.nonSangerGenes);

                    if (type === 'display') {

                        var all_genes = $.map(all_genes, function(g) {
                            // bind ioGeneSet to each gene
                            // highlight ones that are already in the gene list

                            var highlight = '';

                            if ($.inArray(g, genes) !== -1) {
                                highlight = ' gistic_selected_gene';
                            }

                            return "" + g + "";
                        });

                        if (all_genes.length > 5) {

                            all_genes = all_genes.slice(0,5)    // visible genes
                            .concat(" +" +
                                    (all_genes.length - 5)  + " more")
                            .concat(" less")

                            .concat("
") // hidden genes div
                            .concat(all_genes.slice(5))
                            .concat("
")
                        }

                        return all_genes.join(" ");
                    }
                    else if (type === 'sort') {
                        return all_genes.length;
                    }
                    return all_genes;
                }
            },
            {"sTitle": "Q Value", "aTargets":[5], "sType": "numeric", "sClass": "gistic_center_col",
                "mDataProp": function(source, type, val) {
                    var rounded = parseFloat(source.qval) .toExponential(1);   // round Q-Values
                    if (type === 'display') {
                        return rounded;
                    }
                    return rounded;
                }
            }
            ];

            options.aaSorting = [[ 5, "asc" ]];     // sort Q-Value column on load
            options.oLanguage = {'sSearch': 'Filter by Gene:'};
            options.aaData = aaData;
            options.aoColumnDefs = aoColumnDefs;

            Gistic.dt = table_el.dataTable(options);

            // everytime you draw
            // update the selected_genes
            Gistic.selected_genes = $.map($('.gistic_selected_gene'),
                function(i, val) {
                return $(val).html();
            });

            // paint regions red and blue
            //$('.gistic_amp').parent().css('background-color', 'red');
            //$('.gistic_del').parent().css('background-color', 'blue');

            // center cols
            $('.gistic_center_col').css('text-align', 'center');

            // todo: maybe we'll want this someday
            // bind double clicking
            //Gistic.dt.fnGetNodes().forEach(function(i) {
            //    $(i).find('.gistic_gene_cell').
            //        select(Gistic.UI.select_all_genes);
            //});

            // put in the help box
            $('#gistic_table_filter').parent().
                prepend('' +
                        'Click on a gene to select it');

            $('#gistic_close').show();
            return;
        },

        getGistics: function() {
            return gistics;
        }
    };

    return self;
};

Gistic.UI = ( function() {
    // dump of all sorts of UI functions
    // the closure is to keep the private GISTIC variable

    var GISTIC = {};

    return {
        open_dialog : function() {

            Gistic.table_el = $('#gistic_table');

            var options = { "sScrollY": "350px",
                "bPaginate": false,
                "bJQueryUI": true,
                "bDestroy": true};

            $('#gistic_msg_box').hide();
            $('#gistic_cancel').hide();

            $('#gistic_loading').show();
            $('#gistic_dialog').dialog('open');

            var genes = GeneSet($('#gene_list').val()).getAllGenes();

            // hide the Gistic button when there is no gistic data
            // if ajax hasn't already been done...then do an ajax call
            if (SELECTED_CANCER_TYPE_OLD !== SELECTED_CANCER_TYPE_NEW) {

                SELECTED_CANCER_TYPE_OLD = SELECTED_CANCER_TYPE_NEW;

                $.ajax({
                    url: 'Gistic.json',
                    data: {'selected_cancer_type': SELECTED_CANCER_TYPE_NEW},
                    dataType: 'json',
                    success: function(data) {
                        $('#gistic_loading').hide();

                        GISTIC = Gistic(data);

                        GISTIC.drawTable(Gistic.table_el, genes, options);
                    }
                });
            } else {
                $('#gistic_loading').hide();
                GISTIC.drawTable(Gistic.table_el, genes, options);
            }

            // redraw table
            //Gistic.dt.fnDraw();
        },

        expandGisticGenes : function(el) {
            // shows/hides additional genes in the genes column
            // currently initializing to hiding all non-Sanger genes

            el = $(el).parents()[0];

            // grab all the elements
            var more = $(el).children('#gistic_more');
            var less = $(el).children('#gistic_less');
            var hidden = $(el).children('#gistic_hidden');

            // and toggle them
            $(more).toggle();
            $(less).toggle();
            $(hidden).slideToggle('slow');
        },

        ioGeneSet : function(el) {
            $(el).toggleClass('gistic_selected_gene');
        },

        updateGenes: function() {
            var geneSet = GeneSet(Gistic.gene_list_el.val());

            var currently_selected = $.map($('.gistic_selected_gene'),
                function(i, val) { return $(val).html(); });

            var remove_genes = $.grep(Gistic.selected_genes, function(i) {
                // genes that are not selected but are in the geneset
                return $.inArray(Gistic.selected_genes[i], currently_selected) === -1;
            });

            var new_genes = $.grep(currently_selected, function(i) {
                // genes that are selected and not in the gene set
                return $.inArray(currently_selected[i], geneSet.getAllGenes()) === -1;
            });

            $.each(remove_genes, function(i,val) {
                // remove remove_genes from geneset
                geneSet.filterOut(val);
            });

            // append new_genes
            var out = geneSet.toString() + '\n' + $.makeArray(new_genes).join(" ");
            out = out.trim();

            // push to gene set
            Gistic.gene_list_el.val(out);
        },

        select_all_genes: function(el) {
            // todo: maybe we'll want this someday
            var max = 50;       // max no of genes users are allowed to select
            var selection = $(this).find('.gistic_gene');

            if (selection.length > 50) {
                // show error message
                $('#gistic_msg_box').show();
                return;
            } else {
                $('#gistic_msg_box').hide();
            }

            if (selection.length > 5) {
                // expand the genes if there are genes to be expanded
                $(this).find('#gistic_more').click();
            }
            selection.toggleClass('gistic_selected_gene');
        }
    };
})();
<|endoftext|>


<commit_before>/**
 * @include GeoExt/widgets/MapPanel.js
 */

/**
 *  @example
 *
 *      var popup = new GeoExt.Popup({
 *          title: "My Popup",
 *          location: feature,
 *          width: 200,
 *          html: "
Popup content
",
 *          collapsible: true
 *      });
 */

/**
 * @class GeoExt.window.Popup
 */
Ext.define('GeoExt.window.Popup', {
    extend: 'Ext.window.Window',
    alias: 'widget.gx_popup',
    alternateClassName : 'GeoExt.Popup',

    /**
     * @property {Boolean} insideViewport
     * @private
     * Whether the popup is currently inside the map viewport.
     */
    insideViewport: null,

    /**
     * @property {Boolean} animCollapse
     * @private
     * Animate the transition when the panel is collapsed.
     * Default is ``false``.  Collapsing animation is not supported yet for
     * popups.
     */
    animCollapse: false,

    /**
     * @property {Boolean} draggable
     * @private
     * Enable dragging of this Panel.  Defaults to ``false``
     * because the popup defaults to being anchored, and anchored popups
     * should not be draggable.
     */
    draggable: false,

    /**
     * @property {Boolean} shadow
     * @private
     * Give the popup window a shadow.  Defaults to ``false``
     * because shadows are not supported yet for popups (the shadow does
     * not look good with the anchor).
     */
    shadow: false,

    /** api: config[unpinnable]
     *  ``Boolean`` The popup should have a "unpin" tool that unanchors it from
     *  its location.  Default is ``true``.
     */

    /**
     * @cfg {Boolean} unpinnable
     * The popup should have a "unpin" tool that unanchors it from
     * its location.  Default is ``true``.
     */
    unpinnable: true,

    /**
     * @cfg {OpenLayers.Map / GeoExt.MapPanel} map
     * The map this popup will be anchored to (only required if ``anchored``
     * is set to true and the map cannot be derived from the ``location``'s
     * layer.
     */
    map: null,

    config: {

        /**
         * @cfg {Boolean} anchored
         * The popup begins anchored to its location. Default is
         *  ``true``.
         */
        anchored: true,

        /**
         * @cfg {Boolean} panIn
         * The popup should pan the map so that the popup is
         * fully in view when it is rendered.  Default is ``true``.
         */
        panIn: true,

        /**
         * @cfg {OpenLayers.Feature.Vector / OpenLayers.LonLat / OpenLayers.Pixel} location
         * A location for this popup's anchor.
         */
        location: null,

        /**
         * Some Ext.Window defaults need to be overriden here
         * because some Ext.Window behavior is not currently supported.
         */

        /**
         * @cfg {String} popupCls
         * CSS class name for the popup DOM elements. Default is
         *  "gx-popup".
         */
        popupCls: "gx-popup",

        /**
         * @cfg {String} ancCls
         * CSS class name for the popup's anchor.
         */
        ancCls: null,

        /**
         * @cfg {String} anchorPosition
         * Controls the anchor position for the popup. If set to
         *  ``auto``, the anchor will be positioned on the top or the bottom of
         *  the window, minimizing map movement. Supported values are ``bottom-left``,
         *  ``bottom-right``, ``top-left``, ``top-right`` or ``auto``.
         *  Defaults to ``auto``.
         */
        anchorPosition: "auto"
    },

    /**
     *
     */
    initComponent: function() {
        if(this.map instanceof GeoExt.MapPanel) {
            this.map = this.map.map;
        }
        if(!this.map && this.getLocation() instanceof OpenLayers.Feature.Vector &&
                                                        this.getLocation().layer) {
            this.map = this.getLocation().layer.map;
        }
        if (this.getLocation() instanceof OpenLayers.Feature.Vector) {
            this.setLocation(this.getLocation().geometry);
        }
        if (this.getLocation() instanceof OpenLayers.Geometry) {
            if (typeof this.getLocation().getCentroid == "function") {
                this.setLocation(this.getLocation().getCentroid());
            }
            this.setLocation(this.getLocation().getBounds().getCenterLonLat());
        } else if (this.getLocation() instanceof OpenLayers.Pixel) {
            this.setLocation(this.map.getLonLatFromViewPortPx(this.getLocation()));
        } else {
            this.setAnchored(false);
        }

        var mapExtent =  this.map.getExtent();
        if (mapExtent && this.getLocation()) {
            this.insideViewport = mapExtent.containsLonLat(this.getLocation());
        }

        if(this.getAnchored()) {
            this.addAnchorEvents();
        }

        this.baseCls = this.getPopupCls() + " " + this.baseCls;
        this.elements += ',anc';

        this.callParent(arguments);
    },

    /**
     * @private
     * @param {Object} ct
     * @param {Object} position
     * The "onRender" listener of this component.
     * Executes when the popup is rendered and creates the anchor div
     */
    onRender: function(ct, position) {
        this.callParent(arguments);
        this.setAncCls(this.getPopupCls() + "-anc");

        //create anchor dom element.
        //this.createElement("anc", this.el.dom);
        var dh = Ext.core.DomHelper; // create shorthand alias
        // specification the anchor div
        var spec = {
            tag: 'div',
            cls: this.getAncCls()
        };

        var ancDiv = dh.append(
            this.el.dom, // the context element 'my-div' can either be the id or the actual node
            spec      // the specification object
        );
        this.anc = Ext.get(ancDiv);
    },

    /**
     * @private
     * Initializes the tools on the popup.  In particular
     * it adds the 'unpin' tool if the popup is unpinnable.
     */
    initTools : function() {
        if(this.unpinnable) {
            if (!this.tools) {
                this.tools = [];
            }
            this.tools.push({
                type:'unpin',
                handler: Ext.bind(this.unanchorPopup, this, [])
            });
        }
        this.callParent(arguments);
    },

    /**
     * @private
     * Override.
     */
    show: function() {
        this.callParent(arguments);
        if(this.getAnchored()) {
            this.position();
            if(this.getPanIn() && !this._mapMove) {
                this.panIntoView();
            }
        }
    },

    /**
     * @private
     * Override.
     */
    maximize: function() {
        if(!this.maximized && this.anc) {
            this.unanchorPopup();
        }
        this.callParent(arguments);
    },

    /**
     * @param {Integer} w the width to apply.
     * @param {Integer} h the height to apply.
     * Sets the size of the popup, taking into account the size of the anchor.
     */
    setSize: function(w, h) {
        if(this.anc) {
            var ancSize = this.anc.getSize();

            if(typeof w == 'object') {
                h = w.height - ancSize.height;
                w = w.width;
            } else if(!isNaN(h)){
                h = h - ancSize.height;
            }
        }
        this.callParent([w,h]);
    },

    /**
     * @private
     * Positions the popup relative to its location
     */
    position: function() {
        if(this._mapMove === true) {
            this.insideViewport = this.map.getExtent().containsLonLat(this.getLocation());
            if(this.insideViewport !== this.isVisible()) {
                this.setVisible(this.insideViewport);
            }
        }

        if(this.isVisible()) {
            var locationPx = this.map.getPixelFromLonLat(this.getLocation()),
                mapBox = Ext.fly(this.map.div).getBox(true),
                top = locationPx.y + mapBox.y,
                left = locationPx.x + mapBox.x,
                elSize = this.el.getSize(),
                ancSize = this.anc.getSize(),
                ancPos = this.getAnchorPosition();

            if (ancPos.indexOf("right") > -1 || locationPx.x > mapBox.width / 2) {
                // right
                this.anc.addCls("right");
                var ancRight = this.el.getX(true) + elSize.width -
                               this.anc.getX(true) - ancSize.width;
                left -= elSize.width - ancRight - ancSize.width / 2;
            } else {
                // left
                this.anc.removeCls("right");
                var ancLeft = this.anc.getLeft(true);
                left -= ancLeft + ancSize.width / 2;
            }

            if (ancPos.indexOf("bottom") > -1 || locationPx.y > mapBox.height / 2) {
                // bottom
                this.anc.removeCls("top");
                // position the anchor
                var popupHeight = this.getHeight();
                if (isNaN(popupHeight) === false) {
                    this.anc.setTop((popupHeight-1) + "px");
                }

                top -= elSize.height + ancSize.height;

            } else {
                // top
                this.anc.addCls("top");
                // remove eventually set top property (bottom-case)
                this.anc.setTop("");
                top += ancSize.height; // ok
            }

            this.setPosition(left, top);
        }
    },

    /**
     * @private
     * Unanchors a popup from its location. This removes the popup from its
     * MapPanel and adds it to the page body.
     */
    unanchorPopup: function() {
        this.removeAnchorEvents();

        //make the window draggable
        this.draggable = true;
        this.header.addCls("x-window-header-draggable");
        var ddConfig = Ext.applyIf({
            el: this.el,
            delegate: '#' + Ext.escapeId(this.header.id)
        }, this.draggable);
        this.dd = new Ext.util.ComponentDragger(this, ddConfig);

        //remove anchor
        this.anc.remove();
        this.anc = null;

        //hide unpin tool
        this.tools.unpin.hide();
    },

    /**
     * @private
     * Pans the MapPanel's map so that an anchored popup can come entirely
     * into view, with padding specified as per normal OpenLayers.Map popup
     * padding.
     */
    panIntoView: function() {
        var mapBox = Ext.fly(this.map.div).getBox(true);

        //assumed viewport takes up whole body element of map panel
        var popupPos =  this.getPosition(true);
        popupPos[0] -= mapBox.x;
        popupPos[1] -= mapBox.y;

        var panelSize = [mapBox.width, mapBox.height]; // [X,Y]

        var popupSize = this.getSize();

        var newPos = [popupPos[0], popupPos[1]];

        //For now, using native OpenLayers popup padding.  This may not be ideal.
        var padding = this.map.paddingForPopups;

        // X
        if(popupPos[0] < padding.left) {
            newPos[0] = padding.left;
        } else if(popupPos[0] + popupSize.width > panelSize[0] - padding.right) {
            newPos[0] = panelSize[0] - padding.right - popupSize.width;
        }

        // Y
        if(popupPos[1] < padding.top) {
            newPos[1] = padding.top;
        } else if(popupPos[1] + popupSize.height > panelSize[1] - padding.bottom) {
            newPos[1] = panelSize[1] - padding.bottom - popupSize.height;
        }

        var dx = popupPos[0] - newPos[0];
        var dy = popupPos[1] - newPos[1];

        this.map.pan(dx, dy);
    },

    /**
     * @private
     */
    onMapMove: function() {
        if (!(this.hidden && this.insideViewport)){
            this._mapMove = true;
            this.position();
            delete this._mapMove;
        }
    },

    /**
     * @private
     */
    addAnchorEvents: function() {
        this.map.events.on({
            "move" : this.onMapMove,
            scope : this
        });

        this.on({
            "resize": this.position,
            "collapse": this.position,
            "expand": this.position,
            scope: this
        });
    },

    /**
     * @private
     */
    removeAnchorEvents: function() {
        //stop position with location
        this.map.events.un({
            "move" : this.onMapMove,
            scope : this
        });

        this.un("resize", this.position, this);
        this.un("collapse", this.position, this);
        this.un("expand", this.position, this);
    },

    /**
     * @private
     * Cleanup events before destroying the popup.
     */
    beforeDestroy: function() {
        if(this.getAnchored()) {
            this.removeAnchorEvents();
        }
        this.callParent(arguments);
    }

});
<commit_msg>Improved API-docs for the popup class.<commit_after>/**
 * @include GeoExt/panel/Map.js
 */

/**
 * @class GeoExt.window.Popup
 *
 * Popups are a specialized Window that supports anchoring
 * to a particular location in a {@link GeoExt.panel.Map MapPanel}.
 * When a popup is anchored to a {@link #location}, that means that
 * the popup will visibly point to the location on the map,
 * and move accordingly when the map is panned or zoomed.
 *
 *     @example
 *     var popup = Ext.create('GeoExt.window.Popup', {
 *         title: "My Popup",
 *         location: feature,
 *         width: 200,
 *         html: "
Popup content
",
 *         collapsible: true
 *     });
 *
 * Or create it via xtype declaration:
 *
 *     @example
 *     var popup = {
 *         xtype: 'gx_popup',
 *         title: "My Popup",
 *         location: feature,
 *         width: 200,
 *         html: "
Popup content
",
 *         collapsible: true
 *     }
 */
Ext.define('GeoExt.window.Popup', {
    extend: 'Ext.window.Window',
    alias: 'widget.gx_popup',
    alternateClassName : 'GeoExt.Popup',

    /**
     * @property {Boolean} insideViewport
     * @private
     * Whether the popup is currently inside the map viewport.
     */
    insideViewport: null,

    /**
     * @property {Boolean} animCollapse
     * @private
     * Animate the transition when the panel is collapsed.
     * Collapsing animation is not supported yet for
     * popups.
     */
    animCollapse: false,

    /**
     * @property {Boolean} draggable
     * @private
     * Enable dragging of this Panel.
     * because the popup defaults to being anchored, and anchored popups
     * should not be draggable.
     */
    draggable: false,

    /**
     * @property {Boolean} shadow
     * @private
     * Give the popup window a shadow.
     * Because shadows are not supported yet for popups (the shadow does
     * not look good with the anchor).
     */
    shadow: false,

    /**
     * @cfg {Boolean} unpinnable
     * The popup should have a "unpin" tool that unanchors it from
     * its location.
     */
    unpinnable: true,

    /**
     * @cfg {GeoExt.panel.Map/OpenLayers.Map} map
     * The map this popup will be anchored to (only required if `anchored`
     * is set to true and the map cannot be derived from the `location`'s
     * layer).
     */
    map: null,

    config: {

        /**
         * @cfg {Boolean} anchored
         * The popup begins anchored to its location.
         */
        anchored: true,

        /**
         * @cfg {Boolean} panIn
         * The popup should pan the map so that the popup is
         * fully in view when it is rendered.
         */
        panIn: true,

        /**
         * @cfg {OpenLayers.Feature.Vector/OpenLayers.LonLat/OpenLayers.Pixel} location
         * A location for this popup's anchor.
         */
        location: null,

        /*
         * Some Ext.Window defaults need to be overriden here
         * because some Ext.Window behavior is not currently supported.
         */

        /**
         * @cfg {String} popupCls
         * CSS class name for the popup DOM elements.
         */
        popupCls: "gx-popup",

        /**
         * @cfg {String} ancCls
         * CSS class name for the popup's anchor.
         */
        ancCls: null,

        /**
         * @cfg {String} anchorPosition
         * Controls the anchor position for the popup. If set to
         *  `auto`, the anchor will be positioned on the top or the bottom of
         *  the window, minimizing map movement. Supported values are `bottom-left`,
         *  `bottom-right`, `top-left`, `top-right` or `auto`.
         */
        anchorPosition: "auto"
    },

    initComponent: function() {
        if(this.map instanceof GeoExt.MapPanel) {
            this.map = this.map.map;
        }
        if(!this.map && this.getLocation() instanceof OpenLayers.Feature.Vector &&
                                                        this.getLocation().layer) {
            this.map = this.getLocation().layer.map;
        }
        if (this.getLocation() instanceof OpenLayers.Feature.Vector) {
            this.setLocation(this.getLocation().geometry);
        }
        if (this.getLocation() instanceof OpenLayers.Geometry) {
            if (typeof this.getLocation().getCentroid == "function") {
                this.setLocation(this.getLocation().getCentroid());
            }
            this.setLocation(this.getLocation().getBounds().getCenterLonLat());
        } else if (this.getLocation() instanceof OpenLayers.Pixel) {
            this.setLocation(this.map.getLonLatFromViewPortPx(this.getLocation()));
        } else {
            this.setAnchored(false);
        }

        var mapExtent =  this.map.getExtent();
        if (mapExtent && this.getLocation()) {
            this.insideViewport = mapExtent.containsLonLat(this.getLocation());
        }

        if(this.getAnchored()) {
            this.addAnchorEvents();
        }

        this.baseCls = this.getPopupCls() + " " + this.baseCls;
        this.elements += ',anc';

        this.callParent(arguments);
    },

    /**
     * @private
     * @param {Object} ct
     * @param {Object} position
     * The "onRender" listener of this component.
     * Executes when the popup is rendered and creates the anchor div
     */
    onRender: function(ct, position) {
        this.callParent(arguments);
        this.setAncCls(this.getPopupCls() + "-anc");

        //create anchor dom element.
        //this.createElement("anc", this.el.dom);
        var dh = Ext.core.DomHelper; // create shorthand alias
        // specification the anchor div
        var spec = {
            tag: 'div',
            cls: this.getAncCls()
        };

        var ancDiv = dh.append(
            this.el.dom, // the context element 'my-div' can either be the id or the actual node
            spec      // the specification object
        );
        this.anc = Ext.get(ancDiv);
    },

    /**
     * @private
     * Initializes the tools on the popup.  In particular
     * it adds the 'unpin' tool if the popup is unpinnable.
     */
    initTools : function() {
        if(this.unpinnable) {
            if (!this.tools) {
                this.tools = [];
            }
            this.tools.push({
                type:'unpin',
                handler: Ext.bind(this.unanchorPopup, this, [])
            });
        }
        this.callParent(arguments);
    },

    /**
     * @private
     * Override.
     */
    show: function() {
        this.callParent(arguments);
        if(this.getAnchored()) {
            this.position();
            if(this.getPanIn() && !this._mapMove) {
                this.panIntoView();
            }
        }
    },

    /**
     * @private
     * Override.
     */
    maximize: function() {
        if(!this.maximized && this.anc) {
            this.unanchorPopup();
        }
        this.callParent(arguments);
    },

    /**
     * @param {Integer} w the width to apply.
     * @param {Integer} h the height to apply.
     *
     * Sets the size of the popup, taking into account the size of the anchor.
     */
    setSize: function(w, h) {
        if(this.anc) {
            var ancSize = this.anc.getSize();

            if(typeof w == 'object') {
                h = w.height - ancSize.height;
                w = w.width;
            } else if(!isNaN(h)){
                h = h - ancSize.height;
            }
        }
        this.callParent([w,h]);
    },

    /**
     * @private
     * Positions the popup relative to its location
     */
    position: function() {
        if(this._mapMove === true) {
            this.insideViewport = this.map.getExtent().containsLonLat(this.getLocation());
            if(this.insideViewport !== this.isVisible()) {
                this.setVisible(this.insideViewport);
            }
        }

        if(this.isVisible()) {
            var locationPx = this.map.getPixelFromLonLat(this.getLocation()),
                mapBox = Ext.fly(this.map.div).getBox(true),
                top = locationPx.y + mapBox.y,
                left = locationPx.x + mapBox.x,
                elSize = this.el.getSize(),
                ancSize = this.anc.getSize(),
                ancPos = this.getAnchorPosition();

            if (ancPos.indexOf("right") > -1 || locationPx.x > mapBox.width / 2) {
                // right
                this.anc.addCls("right");
                var ancRight = this.el.getX(true) + elSize.width -
                               this.anc.getX(true) - ancSize.width;
                left -= elSize.width - ancRight - ancSize.width / 2;
            } else {
                // left
                this.anc.removeCls("right");
                var ancLeft = this.anc.getLeft(true);
                left -= ancLeft + ancSize.width / 2;
            }

            if (ancPos.indexOf("bottom") > -1 || locationPx.y > mapBox.height / 2) {
                // bottom
                this.anc.removeCls("top");
                // position the anchor
                var popupHeight = this.getHeight();
                if (isNaN(popupHeight) === false) {
                    this.anc.setTop((popupHeight-1) + "px");
                }

                top -= elSize.height + ancSize.height;

            } else {
                // top
                this.anc.addCls("top");
                // remove eventually set top property (bottom-case)
                this.anc.setTop("");
                top += ancSize.height; // ok
            }

            this.setPosition(left, top);
        }
    },

    /**
     * @private
     * Unanchors a popup from its location. This removes the popup from its
     * MapPanel and adds it to the page body.
     */
    unanchorPopup: function() {
        this.removeAnchorEvents();

        //make the window draggable
        this.draggable = true;
        this.header.addCls("x-window-header-draggable");
        var ddConfig = Ext.applyIf({
            el: this.el,
            delegate: '#' + Ext.escapeId(this.header.id)
        }, this.draggable);
        this.dd = new Ext.util.ComponentDragger(this, ddConfig);

        //remove anchor
        this.anc.remove();
        this.anc = null;

        //hide unpin tool
        this.tools.unpin.hide();
    },

    /**
     * @private
     * Pans the MapPanel's map so that an anchored popup can come entirely
     * into view, with padding specified as per normal OpenLayers.Map popup
     * padding.
     */
    panIntoView: function() {
        var mapBox = Ext.fly(this.map.div).getBox(true);

        //assumed viewport takes up whole body element of map panel
        var popupPos =  this.getPosition(true);
        popupPos[0] -= mapBox.x;
        popupPos[1] -= mapBox.y;

        var panelSize = [mapBox.width, mapBox.height]; // [X,Y]

        var popupSize = this.getSize();

        var newPos = [popupPos[0], popupPos[1]];

        //For now, using native OpenLayers popup padding.  This may not be ideal.
        var padding = this.map.paddingForPopups;

        // X
        if(popupPos[0] < padding.left) {
            newPos[0] = padding.left;
        } else if(popupPos[0] + popupSize.width > panelSize[0] - padding.right) {
            newPos[0] = panelSize[0] - padding.right - popupSize.width;
        }

        // Y
        if(popupPos[1] < padding.top) {
            newPos[1] = padding.top;
        } else if(popupPos[1] + popupSize.height > panelSize[1] - padding.bottom) {
            newPos[1] = panelSize[1] - padding.bottom - popupSize.height;
        }

        var dx = popupPos[0] - newPos[0];
        var dy = popupPos[1] - newPos[1];

        this.map.pan(dx, dy);
    },

    /**
     * @private
     */
    onMapMove: function() {
        if (!(this.hidden && this.insideViewport)){
            this._mapMove = true;
            this.position();
            delete this._mapMove;
        }
    },

    /**
     * @private
     */
    addAnchorEvents: function() {
        this.map.events.on({
            "move" : this.onMapMove,
            scope : this
        });

        this.on({
            "resize": this.position,
            "collapse": this.position,
            "expand": this.position,
            scope: this
        });
    },

    /**
     * @private
     */
    removeAnchorEvents: function() {
        //stop position with location
        this.map.events.un({
            "move" : this.onMapMove,
            scope : this
        });

        this.un("resize", this.position, this);
        this.un("collapse", this.position, this);
        this.un("expand", this.position, this);
    },

    /**
     * @private
     * Cleanup events before destroying the popup.
     */
    beforeDestroy: function() {
        if(this.getAnchored()) {
            this.removeAnchorEvents();
        }
        this.callParent(arguments);
    }

});
<|endoftext|>


<commit_before>/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

module.exports = angular.module('trafficOps.utils', [])
    .service('formUtils', require('./FormUtils'))
    .service('locationUtils', require('./LocationUtils'))
    .service('serverUtils', require('./serverUtils'))
    .service('stringUtils', require('./StringUtils'));
<commit_msg>fixes serverutils typo<commit_after>/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

module.exports = angular.module('trafficOps.utils', [])
    .service('formUtils', require('./FormUtils'))
    .service('locationUtils', require('./LocationUtils'))
    .service('serverUtils', require('./ServerUtils'))
    .service('stringUtils', require('./StringUtils'));
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2021 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas 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 Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import {AlertManagerContext} from '@canvas/alerts/react/AlertManager'
import {ApolloProvider} from 'react-apollo'
import DiscussionTopicManager from '../DiscussionTopicManager'
import {fireEvent, render, waitFor} from '@testing-library/react'
import {handlers} from '../../graphql/mswHandlers'
import {mswClient} from '../../../../shared/msw/mswClient'
import {mswServer} from '../../../../shared/msw/mswServer'
import React from 'react'

jest.mock('@canvas/rce/RichContentEditor')
jest.mock('../utils', () => ({
  ...jest.requireActual('../utils'),
  responsiveQuerySizes: () => ({desktop: {maxWidth: '1024px'}})
}))

describe('DiscussionsIsolatedView', () => {
  const server = mswServer(handlers)
  const setOnFailure = jest.fn()
  const setOnSuccess = jest.fn()

  beforeAll(() => {
    // eslint-disable-next-line no-undef
    fetchMock.dontMock()
    server.listen()

    window.ENV = {
      discussion_topic_id: '1',
      course_id: '1',
      isolated_view: true
    }

    window.matchMedia = jest.fn().mockImplementation(() => {
      return {
        matches: true,
        media: '',
        onchange: null,
        addListener: jest.fn(),
        removeListener: jest.fn()
      }
    })
  })

  beforeEach(() => {
    mswClient.cache.reset()
  })

  afterEach(() => {
    server.resetHandlers()
    setOnFailure.mockClear()
    setOnSuccess.mockClear()
  })

  afterAll(() => {
    server.close()
    // eslint-disable-next-line no-undef
    fetchMock.enableMocks()
  })

  const setup = () => {
    return render(
      <ApolloProvider client={mswClient}>
        <AlertManagerContext.Provider value={{setOnFailure, setOnSuccess}}>
          <DiscussionTopicManager discussionTopicId="1" />
        </AlertManagerContext.Provider>
      </ApolloProvider>
    )
  }

  it('should be able to post a reply to an entry', async () => {
    const {findByText, findByTestId, queryByTestId} = setup()

    const replyButton = await findByTestId('threading-toolbar-reply')
    fireEvent.click(replyButton)

    expect(findByText('Thread')).toBeTruthy()

    const doReplyButton = await findByTestId('DiscussionEdit-submit')
    fireEvent.click(doReplyButton)

    await waitFor(() => expect(queryByTestId('DiscussionEdit-container')).not.toBeInTheDocument())

    await waitFor(() =>
      expect(setOnSuccess).toHaveBeenCalledWith('The discussion entry was successfully created.')
    )
  })

  it('should be able to edit a root entry', async () => {
    const {findByText, findByTestId, findAllByTestId} = setup()

    const expandButton = await findByTestId('expand-button')
    fireEvent.click(expandButton)

    const actionsButtons = await findAllByTestId('thread-actions-menu')
    fireEvent.click(actionsButtons[0]) // Root Entry kebab

    const editButton = await findByText('Edit')
    fireEvent.click(editButton)

    const saveButton = await findByText('Save')
    fireEvent.click(saveButton)

    await waitFor(() =>
      expect(setOnSuccess).toHaveBeenCalledWith('The reply was successfully updated.')
    )
  })

  it('should open isolated view when go to reply button is clicked', async () => {
    const container = setup()
    await waitFor(() => expect(container.queryByTestId('isolated-view-container')).toBeNull())
    fireEvent.change(await container.findByTestId('search-filter'), {
      target: {value: 'a'}
    })
    const goToReply = await container.findByTestId('go-to-reply')
    fireEvent.click(goToReply)

    await waitFor(() => expect(container.queryByTestId('isolated-view-container')).not.toBeNull())
  })

  it('should show reply button in isolated view when search term is present', async () => {
    const container = setup()
    fireEvent.change(await container.findByTestId('search-filter'), {
      target: {value: 'a'}
    })
    const goToReply = await container.findByTestId('go-to-reply')
    fireEvent.click(goToReply)
    await waitFor(() => expect(container.queryByTestId('threading-toolbar-reply')).toBeNull())
  })

  it('go to topic button should clear search term', async () => {
    const container = setup()
    fireEvent.change(await container.findByTestId('search-filter'), {
      target: {value: 'a'}
    })
    const goToReply = await container.findByTestId('go-to-reply')
    fireEvent.click(goToReply)

    const isolatedKabab = await container.findAllByTestId('thread-actions-menu')
    fireEvent.click(isolatedKabab[0])

    await waitFor(() => {
      expect(container.queryByTestId('discussion-topic-container')).toBeNull()
    })
    const goToTopic = await container.findByText('Go To Topic')
    fireEvent.click(goToTopic)

    expect(await container.findByTestId('discussion-topic-container')).toBeTruthy()
  })

  it('should clear input when button is pressed', async () => {
    const container = setup()
    let searchInput = container.findByTestId('search-filter')

    fireEvent.change(await container.findByTestId('search-filter'), {
      target: {value: 'A new Search'}
    })
    let clearSearchButton = container.queryByTestId('clear-search-button')
    searchInput = container.getByLabelText('Search entries or author')
    expect(searchInput.value).toBe('A new Search')
    expect(clearSearchButton).toBeInTheDocument()

    fireEvent.click(clearSearchButton)
    clearSearchButton = container.queryByTestId('clear-search-button')
    searchInput = container.getByLabelText('Search entries or author')
    expect(searchInput.value).toBe('')
    expect(clearSearchButton).toBeNull()
  })
})
<commit_msg>Revert "spec: unskips discussionisolatedview tests"<commit_after>/*
 * Copyright (C) 2021 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas 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 Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import {AlertManagerContext} from '@canvas/alerts/react/AlertManager'
import {ApolloProvider} from 'react-apollo'
import DiscussionTopicManager from '../DiscussionTopicManager'
import {fireEvent, render, waitFor} from '@testing-library/react'
import {handlers} from '../../graphql/mswHandlers'
import {mswClient} from '../../../../shared/msw/mswClient'
import {mswServer} from '../../../../shared/msw/mswServer'
import React from 'react'

jest.mock('@canvas/rce/RichContentEditor')
jest.mock('../utils', () => ({
  ...jest.requireActual('../utils'),
  responsiveQuerySizes: () => ({desktop: {maxWidth: '1024px'}})
}))

describe('DiscussionsIsolatedView', () => {
  const server = mswServer(handlers)
  const setOnFailure = jest.fn()
  const setOnSuccess = jest.fn()

  beforeAll(() => {
    // eslint-disable-next-line no-undef
    fetchMock.dontMock()
    server.listen()

    window.ENV = {
      discussion_topic_id: '1',
      course_id: '1',
      isolated_view: true
    }

    window.matchMedia = jest.fn().mockImplementation(() => {
      return {
        matches: true,
        media: '',
        onchange: null,
        addListener: jest.fn(),
        removeListener: jest.fn()
      }
    })
  })

  beforeEach(() => {
    mswClient.cache.reset()
  })

  afterEach(() => {
    server.resetHandlers()
    setOnFailure.mockClear()
    setOnSuccess.mockClear()
  })

  afterAll(() => {
    server.close()
    // eslint-disable-next-line no-undef
    fetchMock.enableMocks()
  })

  const setup = () => {
    return render(
      <ApolloProvider client={mswClient}>
        <AlertManagerContext.Provider value={{setOnFailure, setOnSuccess}}>
          <DiscussionTopicManager discussionTopicId="1" />
        </AlertManagerContext.Provider>
      </ApolloProvider>
    )
  }

  it.skip('should be able to post a reply to an entry', async () => {
    const {findByText, findByTestId, queryByTestId} = setup()

    const replyButton = await findByTestId('threading-toolbar-reply')
    fireEvent.click(replyButton)

    expect(findByText('Thread')).toBeTruthy()

    const doReplyButton = await findByTestId('DiscussionEdit-submit')
    fireEvent.click(doReplyButton)

    await waitFor(() => expect(queryByTestId('DiscussionEdit-container')).not.toBeInTheDocument())

    await waitFor(() =>
      expect(setOnSuccess).toHaveBeenCalledWith('The discussion entry was successfully created.')
    )
  })

  it('should be able to edit a root entry', async () => {
    const {findByText, findByTestId, findAllByTestId} = setup()

    const expandButton = await findByTestId('expand-button')
    fireEvent.click(expandButton)

    const actionsButtons = await findAllByTestId('thread-actions-menu')
    fireEvent.click(actionsButtons[0]) // Root Entry kebab

    const editButton = await findByText('Edit')
    fireEvent.click(editButton)

    const saveButton = await findByText('Save')
    fireEvent.click(saveButton)

    await waitFor(() =>
      expect(setOnSuccess).toHaveBeenCalledWith('The reply was successfully updated.')
    )
  })

  it('should open isolated view when go to reply button is clicked', async () => {
    const container = setup()
    await waitFor(() => expect(container.queryByTestId('isolated-view-container')).toBeNull())
    fireEvent.change(await container.findByTestId('search-filter'), {
      target: {value: 'a'}
    })
    const goToReply = await container.findByTestId('go-to-reply')
    fireEvent.click(goToReply)

    await waitFor(() => expect(container.queryByTestId('isolated-view-container')).not.toBeNull())
  })

  it('should show reply button in isolated view when search term is present', async () => {
    const container = setup()
    fireEvent.change(await container.findByTestId('search-filter'), {
      target: {value: 'a'}
    })
    const goToReply = await container.findByTestId('go-to-reply')
    fireEvent.click(goToReply)
    await waitFor(() => expect(container.queryByTestId('threading-toolbar-reply')).toBeNull())
  })

  // This isn't a broken test. This functionality is really not working.
  it.skip('go to topic button should clear search term', async () => {
    const container = setup()
    fireEvent.change(await container.findByTestId('search-filter'), {
      target: {value: 'a'}
    })
    const goToReply = await container.findByTestId('go-to-reply')
    fireEvent.click(goToReply)

    const isolatedKabab = await container.findAllByTestId('thread-actions-menu')
    fireEvent.click(isolatedKabab[0])

    await waitFor(() => {
      expect(container.queryByTestId('discussion-topic-container')).toBeNull()
    })
    const goToTopic = await container.findByText('Go To Topic')
    fireEvent.click(goToTopic)

    expect(await container.findByTestId('discussion-topic-container')).toBeTruthy()
  })

  it('should clear input when button is pressed', async () => {
    const container = setup()
    let searchInput = container.findByTestId('search-filter')

    fireEvent.change(await container.findByTestId('search-filter'), {
      target: {value: 'A new Search'}
    })
    let clearSearchButton = container.queryByTestId('clear-search-button')
    searchInput = container.getByLabelText('Search entries or author')
    expect(searchInput.value).toBe('A new Search')
    expect(clearSearchButton).toBeInTheDocument()

    fireEvent.click(clearSearchButton)
    clearSearchButton = container.queryByTestId('clear-search-button')
    searchInput = container.getByLabelText('Search entries or author')
    expect(searchInput.value).toBe('')
    expect(clearSearchButton).toBeNull()
  })
})
<|endoftext|>


<commit_before>// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

let map;
/** Editable marker that displays when a user clicks in the map */
let editMarker;
let userLocation;
let reports;
let markers = [];

/** Creates a map and adds it to the page. */
function createMap(){
    map = new google.maps.Map(document.getElementById('map'), {
        center: { lat: 37.422, lng: -122.084 },
        zoom:15});

    /** When the user clicks on the map, show a marker with a form the user can edit. */
    map.addListener('click', (event) => {
        createMarkerForEdit(event.latLng.lat(), event.latLng.lng());
    });

    var controlDiv = document.getElementById('floating-panel');
    map.controls[google.maps.ControlPosition.TOP_RIGHT].push(controlDiv);
    fetchMarkers();
}

/** Fetches markers from the backend and adds them to the map. */
function fetchMarkers(){
    fetch('/markers').then(response => response.json()).then((markers) => {
        markers.forEach((marker) => {
            createMarkerForDisplay(marker.lat, marker.lng, marker.crimeType, marker.date, marker.time, marker.description);
        });
    });
}

/** Creates a marker that shows a read-only info window when clicked. */
function createMarkerForDisplay(lat, lng, crimeType, date, time, address, description){
    const marker = new google.maps.Marker({position: {lat: lat, lng: lng, map: map}});

    var infoWindow = new google.maps.InfoWindow({content: crimeType, date, time, address, description});

    marker.addListener('click', () => {
        infoWindow.open(map, marker);
    });
}

/** Sends a marker to the backend for saving. */
function postMarker(lat, lng, type, date, time, address, description){
    const params = new URLSearchParams();

    params.append('lat', lat);
    params.append('lng', lng);
    params.append('crimeType', type);
    params.append('date', date);
    params.append('time', time);
    params.append('address', address);
    params.append('description', description);

    fetch('/markers', {method: 'POST', body: params})
    .catch((error) => {
        console.error(error);
    });
}

/** Creates a marker that shows a textbox the user can edit. */
function createMarkerForEdit(lat, lng){
    /** If we are already showing an editable marker, then remove it. */
    if(editMarker){
        editMarker.setMap(null);
    }

    document.getElementById('reportsForm').style.display = "block";
    editMarker = new google.maps.Marker({position: {lat: lat, lng: lng}, map: map});

    let infoWindow = new google.maps.InfoWindow({content: buildInfoWindow(lat, lng)});

    /** When the user closes the editable info window, remove the marker. */
    google.maps.event.addListener(infoWindow, 'closeclick', () => {
        editMarker.setMap(null);
    });

    infoWindow.open(map, editMarker);
}

/** Builds and returns HTML elements that show an editable textbox and submit button. */
function buildInfoWindow(lat, lng){
    const button = document.createElement('button');
    button.appendChild(document.createTextNode('Submit'));

    let divContainer = document.createElement('div');
    divContainer.appendChild(document.getElementById('reportsForm'));
    divContainer.appendChild(button);

    button.onclick = () => {
        postMarker(lat, lng, getRadioValueCrimes(), document.getElementById('date').value, document.getElementById('time').value, document.getElementById('address').value, document.getElementById('description').value);
        createMarkerForDisplay(lat, lng, getRadioValueCrimes(), document.getElementById('date').value, document.getElementById('time').value, document.getElementById('address').value, document.getElementById('description').value);
        editMarker.setMap(null);
    }

    return divContainer;
}

/** Looks for the value checked in the type of crime report's section. */
function getRadioValueCrimes(){
    if(document.getElementById('homicide').checked) {
        return document.getElementById('homicide').value;
    }else if(document.getElementById('sexualAssault').checked) {
        return document.getElementById('sexualAssault').value;
    }else if(document.getElementById('robbery').checked) {
        return document.getElementById('robbery').value;
    }else if(document.getElementById('harrassment').checked) {
        return document.getElementById('harrassment').value;
    }else if(document.getElementById('kidnapping').checked) {
        return document.getElementById('kidnapping').value;
    }else if(document.getElementById('drugs').checked) {
        return document.getElementById('drugs').value;
    }else{
        return document.getElementById('other').value;
    }
}

/**
Puts a marker on the user's location
 */
function getUserLocation() {
    var infoWindow = new google.maps.InfoWindow;
    // Try HTML5 geolocation.
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(function(position) {
        userLocation = {
            lat: position.coords.latitude,
            lng: position.coords.longitude
        };
        var marker = new google.maps.Marker({
            position: userLocation,
            map: map,
            title: 'User location'
        });
        marker.setMap(map);
        map.setCenter(userLocation);
        hardcodedMarkers();
        initHeatMap();
        //addMarkers();
        },
        function() {
            handleLocationError(true, infoWindow, map.getCenter());
        });
    } else {
          // Browser doesn't support Geolocation
        handleLocationError(false, infoWindow, map.getCenter());
    }
}

function handleLocationError(browserHasGeolocation, infoWindow, pos) {
    infoWindow.setPosition(pos);
    infoWindow.setContent(browserHasGeolocation ?
        'Error: The Geolocation service failed.' :
        'Error: Your browser doesn\'t support geolocation.');
    infoWindow.open(map);
}

function hardcodedMarkers(){
    reports = [
        ['Robbery',31.62931,-106.392942],
        ['Murder',31.627014,-106.396744],
        ['Harassment',31.632739,-106.396744],
        ['Robbery',31.634478,-106.400389]
    ];

    var infowindow = new google.maps.InfoWindow();
    var marker, i;
    for (i = 0; i < reports.length; i++) {
      marker = new google.maps.Marker({
        position: new google.maps.LatLng(reports[i][1], reports[i][2]),
        map: map
      });
      markers.push(marker);

      google.maps.event.addListener(marker, 'click', (function(marker, i) {
        return function() {
          infowindow.setContent(reports[i][0]);
          infowindow.open(map, marker);
        }
      })(marker, i));
    }
}
/**
Show/Hide Heatmap
 */
var heatmap;

function initHeatMap() {
    heatmap = new google.maps.visualization.HeatmapLayer({
    data: getPoints(),
    map: map
    });
}

function toggleHeatmap() {
    heatmap.setMap(heatmap.getMap() ? null : map);
}

function getPoints() {
    return [
          new google.maps.LatLng(reports[0][1], reports[0][2]),
          new google.maps.LatLng(reports[1][1], reports[1][2]),
          new google.maps.LatLng(reports[2][1], reports[2][2]),
          new google.maps.LatLng(reports[3][1], reports[3][2])
    ];
}

var reportsSideBar = document.getElementById("sideBar");

var filters = document.getElementById("filters");

var span = document.getElementsByClassName("close")[0];

var apply = document.getElementById("apply");

// When the user clicks the button, open the modal
filters.onclick = function() {
  reportsSideBar.style.display = "block";
}

// When the user clicks on <span> (x), close the modal
span.onclick = function() {
  reportsSideBar.style.display = "none";
}

apply.onclick = function(){
  reportsSideBar.style.display = "none";
}

/**
Hide and show markers of reports
 */
function toggleMarkers(map){
    if(map == map){
        // Shows any markers currently in the array.
        setMapOnAll(map);
    }else if(map == null){
        // Removes the markers from the map, but keeps them in the array.
        setMapOnAll(null);
    }
}

// Sets the map on all markers in the array.
function setMapOnAll(map) {
  for (var i = 0; i < markers.length; i++) {
    markers[i].setMap(map);
  }
}
<commit_msg>Fix bug on create map<commit_after>// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

let map;
/** Editable marker that displays when a user clicks in the map */
let editMarker;
let userLocation;
let reports;
let markers = [];

/** Creates a map and adds it to the page. */
function createMap(){
    map = new google.maps.Map(document.getElementById('map'), {
        center: userLocation,
        zoom:15});

    /** When the user clicks on the map, show a marker with a form the user can edit. */
    map.addListener('click', (event) => {
        createMarkerForEdit(event.latLng.lat(), event.latLng.lng());
    });

    var controlDiv = document.getElementById('floating-panel');
    map.controls[google.maps.ControlPosition.TOP_RIGHT].push(controlDiv);
    fetchMarkers();
}

/** Fetches markers from the backend and adds them to the map. */
function fetchMarkers(){
    fetch('/markers').then(response => response.json()).then((markers) => {
        markers.forEach((marker) => {
            createMarkerForDisplay(marker.lat, marker.lng, marker.crimeType, marker.date, marker.time, marker.description);
        });
    });
}

/** Creates a marker that shows a read-only info window when clicked. */
function createMarkerForDisplay(lat, lng, crimeType, date, time, address, description){
    const marker = new google.maps.Marker({position: {lat: lat, lng: lng, map: map}});

    var infoWindow = new google.maps.InfoWindow({content: crimeType, date, time, address, description});

    marker.addListener('click', () => {
        infoWindow.open(map, marker);
    });
}

/** Sends a marker to the backend for saving. */
function postMarker(lat, lng, type, date, time, address, description){
    const params = new URLSearchParams();

    params.append('lat', lat);
    params.append('lng', lng);
    params.append('crimeType', type);
    params.append('date', date);
    params.append('time', time);
    params.append('address', address);
    params.append('description', description);

    fetch('/markers', {method: 'POST', body: params})
    .catch((error) => {
        console.error(error);
    });
}

/** Creates a marker that shows a textbox the user can edit. */
function createMarkerForEdit(lat, lng){
    /** If we are already showing an editable marker, then remove it. */
    if(editMarker){
        editMarker.setMap(null);
    }

    document.getElementById('reportsForm').style.display = "block";
    editMarker = new google.maps.Marker({position: {lat: lat, lng: lng}, map: map});

    let infoWindow = new google.maps.InfoWindow({content: buildInfoWindow(lat, lng)});

    /** When the user closes the editable info window, remove the marker. */
    google.maps.event.addListener(infoWindow, 'closeclick', () => {
        editMarker.setMap(null);
    });

    infoWindow.open(map, editMarker);
}

/** Builds and returns HTML elements that show an editable textbox and submit button. */
function buildInfoWindow(lat, lng){
    const button = document.createElement('button');
    button.appendChild(document.createTextNode('Submit'));

    let divContainer = document.createElement('div');
    divContainer.appendChild(document.getElementById('reportsForm'));
    divContainer.appendChild(button);

    button.onclick = () => {
        postMarker(lat, lng, getRadioValueCrimes(), document.getElementById('date').value, document.getElementById('time').value, document.getElementById('address').value, document.getElementById('description').value);
        createMarkerForDisplay(lat, lng, getRadioValueCrimes(), document.getElementById('date').value, document.getElementById('time').value, document.getElementById('address').value, document.getElementById('description').value);
        editMarker.setMap(null);
    }

    return divContainer;
}

/** Looks for the value checked in the type of crime report's section. */
function getRadioValueCrimes(){
    if(document.getElementById('homicide').checked) {
        return document.getElementById('homicide').value;
    }else if(document.getElementById('sexualAssault').checked) {
        return document.getElementById('sexualAssault').value;
    }else if(document.getElementById('robbery').checked) {
        return document.getElementById('robbery').value;
    }else if(document.getElementById('harrassment').checked) {
        return document.getElementById('harrassment').value;
    }else if(document.getElementById('kidnapping').checked) {
        return document.getElementById('kidnapping').value;
    }else if(document.getElementById('drugs').checked) {
        return document.getElementById('drugs').value;
    }else{
        return document.getElementById('other').value;
    }
}

/**
Puts a marker on the user's location
 */
function getUserLocation() {
    var infoWindow = new google.maps.InfoWindow;
    // Try HTML5 geolocation.
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(function(position) {
        userLocation = {
            lat: position.coords.latitude,
            lng: position.coords.longitude
        };
        var marker = new google.maps.Marker({
            position: userLocation,
            map: map,
            title: 'User location'
        });
        marker.setMap(map);
        map.setCenter(userLocation);
        hardcodedMarkers();
        initHeatMap();
        },
        function() {
            handleLocationError(true, infoWindow, map.getCenter());
        });
    } else {
          // Browser doesn't support Geolocation
        handleLocationError(false, infoWindow, map.getCenter());
    }
}

function handleLocationError(browserHasGeolocation, infoWindow, pos) {
    infoWindow.setPosition(pos);
    infoWindow.setContent(browserHasGeolocation ?
        'Error: The Geolocation service failed.' :
        'Error: Your browser doesn\'t support geolocation.');
    infoWindow.open(map);
}

function hardcodedMarkers(){
    reports = [
        ['Robbery',31.62931,-106.392942],
        ['Murder',31.627014,-106.396744],
        ['Harassment',31.632739,-106.396744],
        ['Robbery',31.634478,-106.400389]
        ['new marker']
    ];

    var infowindow = new google.maps.InfoWindow();
    var marker, i;
    for (i = 0; i < reports.length; i++) {
      marker = new google.maps.Marker({
        position: new google.maps.LatLng(reports[i][1], reports[i][2]),
        map: map
      });
      markers.push(marker);

      google.maps.event.addListener(marker, 'click', (function(marker, i) {
        return function() {
          infowindow.setContent(reports[i][0]);
          infowindow.open(map, marker);
        }
      })(marker, i));
    }
}
/**
Show/Hide Heatmap
 */
var heatmap;

function initHeatMap() {
    heatmap = new google.maps.visualization.HeatmapLayer({
    data: getPoints(),
    map: map
    });
}

function toggleHeatmap() {
    heatmap.setMap(heatmap.getMap() ? null : map);
}

function getPoints() {
    return [
          new google.maps.LatLng(reports[0][1], reports[0][2]),
          new google.maps.LatLng(reports[1][1], reports[1][2]),
          new google.maps.LatLng(reports[2][1], reports[2][2]),
          new google.maps.LatLng(reports[3][1], reports[3][2])
    ];
}

var reportsSideBar = document.getElementById("sideBar");

var filters = document.getElementById("filters");

var span = document.getElementsByClassName("close")[0];

var apply = document.getElementById("apply");

// When the user clicks the button, open the modal
filters.onclick = function() {
  reportsSideBar.style.display = "block";
}

// When the user clicks on <span> (x), close the modal
span.onclick = function() {
  reportsSideBar.style.display = "none";
}

apply.onclick = function(){
  reportsSideBar.style.display = "none";
}

/**
Hide and show markers of reports
 */
function toggleMarkers(map){
    if(map == map){
        // Shows any markers currently in the array.
        setMapOnAll(map);
    }else if(map == null){
        // Removes the markers from the map, but keeps them in the array.
        setMapOnAll(null);
    }
}

// Sets the map on all markers in the array.
function setMapOnAll(map) {
  for (var i = 0; i < markers.length; i++) {
    markers[i].setMap(map);
  }
}
<|endoftext|>


<commit_before>import { FS } from 'grind-support'
import './Template'

export class Compiler {
	engine = null
	directives = { }
	sectionStack = [ ]

	constructor(engine) {
		this.engine = engine
	}

	async compile(template) {
		let contents = (await FS.readFile(template)).toString().trim()
		let extendsLayout = null

		const expressions = [ ]
		let match = null

		// Loops through and finds all directives
		while((match = contents.match(/@([a-z0-9_]+)(\s*\()?/))) {
			if(match.index > 0) {
				expressions.push({
					type: 'string',
					contents: contents.substring(0, match.index)
				})

				contents = contents.substring(match.index)
			}

			let args = null

			if(match[2]) {
				let openCount = -1
				let index = 0
				let startIndex = 0
				let lastIndex = 0

				while(openCount !== 0 && (index = Template.nextIndexOfParenthesis(contents, index)) >= 0) {
					const parenthesis = contents.substring(index, index + 1)

					if(parenthesis === ')') {
						openCount--
					} else if(openCount === -1) {
						startIndex = index
						openCount = 1
					} else {
						openCount++
					}

					lastIndex = index
					index++
				}

				args = contents.substring(startIndex + 1, lastIndex)
				contents = contents.substring(lastIndex + 1)
			} else {
				contents = contents.substring(match[0].length)
			}

			if(match[1] === 'extends') {
				extendsLayout = args
				continue
			}

			const result = this.compileDirective(match[1], args)

			if(!result.isNil) {
				expressions.push({
					type: 'code',
					contents: result
				})
			}
		}

		contents = contents.trim()

		if(contents.length > 0) {
			expressions.push({
				type: 'string',
				contents: contents
			})
		}

		let code = ''

		for(const { type, contents } of expressions) {
			if(type === 'code') {
				code += `${contents}\n`
			} else {
				code += `output += \`${Template.sanitizeHtml(contents)}\`;\n`
			}
		}

		code = `template = function(_, _sections = { }) {\nlet output = '';\n${Template.contextualize(code)}\n`

		if(extendsLayout !== null) {
			code += `return _.$compiler._extends(${extendsLayout}, _, _sections);\n}`
		} else {
			code += `return output;\n}`
		}

		return eval(code)
	}

	_extends(template, context, sections) {
		return this.compile(this.engine.resolve(template)).then(template => template(context, sections))
	}

	compileDirective(name, args) {
		if(name === 'directive') {
			// Avoid infinite loop
			return null
		}

		if(typeof this.directives[name] === 'function') {
			return this.directives[name](args)
		}

		return this[`compile${name[0].toUpperCase()}${name.substring(1)}`](args)
	}

	compileIf(condition) {
		return `if(${condition}) {`
	}

	compileElseif(condition) {
		return `} else if(${condition}) {`
	}

	compileElse() {
		return '} else {'
	}

	compileEndif() {
		return this.compileEnd()
	}

	compileFor(args) {
		return `for(${args}) {`
	}

	compileEndfor() {
		return this.compileEnd()
	}

	compileWhile(condition) {
		return `while(${condition}) {`
	}

	compileEndwhile() {
		return this.compileEnd()
	}

	compileEnd() {
		return '}'
	}

	compileSection(args) {
		if(args.indexOf(',') === -1) {
			this.sectionStack.push(args)
			return this._compileSection(args, `function() {\nlet output = '';`)
		}

		args = args.split(/,/)

		if(args.length !== 2) {
			throw new Error('Invalid section block')
		}

		return this._compileSection(args[0], `function() { return ${args[1]}; })`)
	}

	_compileSection(name, code) {
		return `(_sections[${name}] = (_sections[${name}] || [ ])).unshift(${code}\n`
	}

	/**
	 * Ends the current section and returns output
	 * @return {string} Output from the section
	 */
	compileEndsection() {
		this.sectionStack.pop()
		return 'return output;\n})'
	}

	/**
	 * Ends the current section and yields it for display
	 * @return {string} Output from the section
	 */
	compileShow() {
		const section = this.sectionStack[this.sectionStack.length - 1]
		return `${this.compileEndsection()};\n${this.compileYield(section)}`
	}

	/**
	 * Compiles the yield directive to output a section
	 *
	 * @param  {string} section Name of the section to yield
	 * @return {string}         Code to render the section
	 */
	compileYield(section) {
		return `output += _sections[${section}].length > 0 ? (_sections[${section}].pop())() : ''`
	}

	/**
	 * Renders content from the section section
	 * @return {string} Code to render the super section
	 */
	compileSuper() {
		// Due to how sections work, we can cheat by just calling yeild
		// which will pop off the next chunk of content in this section
		// and render it within ours
		return this.compileYield(this.sectionStack[this.sectionStack.length - 1])
	}

	/**
	 * Alias of compileSuper for compatibility with Blade
	 * @return {string} Code to render the super section
	 */
	compileParent() {
		return this.compileSuper()
	}

}
<commit_msg>Improved directive handling<commit_after>import { FS } from 'grind-support'
import './Template'

export class Compiler {
	engine = null
	directives = { }
	sectionStack = [ ]

	constructor(engine) {
		this.engine = engine
	}

	async compile(template) {
		let contents = (await FS.readFile(template)).toString().trim()
		let extendsLayout = null

		const expressions = [ ]
		let match = null

		// Loops through and finds all directives
		while((match = contents.match(/@([a-zA-Z0-9_]+)(\s*\()?/))) {
			if(match.index > 0) {
				expressions.push({
					type: 'string',
					contents: contents.substring(0, match.index)
				})

				contents = contents.substring(match.index)
			}

			let args = null

			if(match[2]) {
				let openCount = -1
				let index = 0
				let startIndex = 0
				let lastIndex = 0

				while(openCount !== 0 && (index = Template.nextIndexOfParenthesis(contents, index)) >= 0) {
					const parenthesis = contents.substring(index, index + 1)

					if(parenthesis === ')') {
						openCount--
					} else if(openCount === -1) {
						startIndex = index
						openCount = 1
					} else {
						openCount++
					}

					lastIndex = index
					index++
				}

				args = contents.substring(startIndex + 1, lastIndex)
				contents = contents.substring(lastIndex + 1)
			} else {
				contents = contents.substring(match[0].length)
			}

			if(match[1] === 'extends') {
				extendsLayout = args
				continue
			}

			const result = this.compileDirective(match[1].toLowerCase(), args)

			if(!result.isNil) {
				expressions.push({
					type: 'code',
					contents: result
				})
			}
		}

		contents = contents.trim()

		if(contents.length > 0) {
			expressions.push({
				type: 'string',
				contents: contents
			})
		}

		let code = ''

		for(const { type, contents } of expressions) {
			if(type === 'code') {
				code += `${contents}\n`
			} else {
				code += `output += \`${Template.sanitizeHtml(contents)}\`;\n`
			}
		}

		code = `template = function(_, _sections = { }) {\nlet output = '';\n${Template.contextualize(code)}\n`

		if(extendsLayout !== null) {
			code += `return _.$compiler._extends(${extendsLayout}, _, _sections);\n}`
		} else {
			code += `return output;\n}`
		}

		return eval(code)
	}

	_extends(template, context, sections) {
		return this.compile(this.engine.resolve(template)).then(template => template(context, sections))
	}

	compileDirective(name, args) {
		if(name === 'directive') {
			// Avoid infinite loop
			return null
		}

		if(typeof this.directives[name] === 'function') {
			return this.directives[name](args)
		}

		const method = `compile${name[0].toUpperCase()}${name.substring(1)}`

		if(typeof this[method] !== 'function') {
			throw new Error(`@${name} is not a valid Stone directive.`)
		}

		return this[method](args)
	}

	compileIf(condition) {
		return `if(${condition}) {`
	}

	compileElseif(condition) {
		return `} else if(${condition}) {`
	}

	compileElse() {
		return '} else {'
	}

	compileEndif() {
		return this.compileEnd()
	}

	compileFor(args) {
		return `for(${args}) {`
	}

	compileEndfor() {
		return this.compileEnd()
	}

	compileWhile(condition) {
		return `while(${condition}) {`
	}

	compileEndwhile() {
		return this.compileEnd()
	}

	compileEnd() {
		return '}'
	}

	compileSection(args) {
		if(args.indexOf(',') === -1) {
			this.sectionStack.push(args)
			return this._compileSection(args, `function() {\nlet output = '';`)
		}

		args = args.split(/,/)

		if(args.length !== 2) {
			throw new Error('Invalid section block')
		}

		return this._compileSection(args[0], `function() { return ${args[1]}; })`)
	}

	_compileSection(name, code) {
		return `(_sections[${name}] = (_sections[${name}] || [ ])).unshift(${code}\n`
	}

	/**
	 * Ends the current section and returns output
	 * @return {string} Output from the section
	 */
	compileEndsection() {
		this.sectionStack.pop()
		return 'return output;\n})'
	}

	/**
	 * Ends the current section and yields it for display
	 * @return {string} Output from the section
	 */
	compileShow() {
		const section = this.sectionStack[this.sectionStack.length - 1]
		return `${this.compileEndsection()};\n${this.compileYield(section)}`
	}

	/**
	 * Compiles the yield directive to output a section
	 *
	 * @param  {string} section Name of the section to yield
	 * @return {string}         Code to render the section
	 */
	compileYield(section) {
		return `output += _sections[${section}].length > 0 ? (_sections[${section}].pop())() : ''`
	}

	/**
	 * Renders content from the section section
	 * @return {string} Code to render the super section
	 */
	compileSuper() {
		// Due to how sections work, we can cheat by just calling yeild
		// which will pop off the next chunk of content in this section
		// and render it within ours
		return this.compileYield(this.sectionStack[this.sectionStack.length - 1])
	}

	/**
	 * Alias of compileSuper for compatibility with Blade
	 * @return {string} Code to render the super section
	 */
	compileParent() {
		return this.compileSuper()
	}

}
<|endoftext|>