<commit_before>/*jslint node: true */
/*jshint laxbreak: true */
/*jshint laxcomma: true */
"use strict";

var d3 = require("d3");
var _ = require("underscore");
var ViewerHelper = require("./ViewerHelper");
var LegendDialog = require("./VariantLegendDialog");
var VariantFilterDialog = require("./VariantFilterDialog");
var Evidence = require('./Evidence');
var Constants = require("./Constants");

//'G', 'A', 'V', 'L', 'I' aliphatic. 'S', 'T' hydroxyl. 'C', 'M' sulfur-containing. 'D', 'N', 'E', 'Q' acidic.
// 'R', 'K', 'H' basic. 'F', 'Y', 'W' aromatic. 'P' imino. '*' stop gained or lost.
var aaList = ['G', 'A', 'V', 'L', 'I', 'S', 'T', 'C', 'M', 'D', 'N', 'E', 'Q', 'R', 'K', 'H', 'F', 'Y', 'W', 'P', 'd', '*'];

var getPredictionColorScore = function(siftScore, siftPrediction, polyphenScore, polyphenPrediction) {
    var sift = false,
        polyphen = false;
    if ((polyphenPrediction !== undefined) && (polyphenPrediction !== 'unknown')) {
        polyphen = polyphenScore !== undefined ? true : false;
    }
    if (siftPrediction !== undefined) {
        sift = siftScore !== undefined ? true : false;
    }
    if (sift && polyphen) {
        return (siftScore + (1 - polyphenScore)) / 2;
    } else if (sift && !polyphen) {
        return siftScore;
    } else if (!sift && polyphen) {
        return 1 - polyphenScore;
    } else if (polyphenPrediction === 'unknown') {
        return 1;
    } else {
        return undefined;
    }
};

var getVariantsFillColor = function(fv, d, extDatum, externalPrediction, predictionScore) {
    if (fv.overwritePredictions === true) {
        if (externalPrediction !== undefined) {
            d.siftInUse = false;
            d.polyphenInUse = false;
            extDatum.siftInUse = true;
            extDatum.polyphenInUse = true;
            return LegendDialog.getPredictionColor(externalPrediction);
        } else if (predictionScore !== undefined) {
            return LegendDialog.getPredictionColor(predictionScore);
        }
    } else {
        if (predictionScore !== undefined) {
            return LegendDialog.getPredictionColor(predictionScore);
        } else if (externalPrediction !== undefined) {
            d.siftInUse = false;
            d.polyphenInUse = false;
            extDatum.siftInUse = true;
            extDatum.polyphenInUse = true;
            return LegendDialog.getPredictionColor(externalPrediction);
        }
    }

    if (d.externalData) {
        if (extDatum.consequence) {
            var pos = Constants.getConsequenceTypes().indexOf(extDatum.consequence);
            return pos !== -1 ? LegendDialog.consequenceColors[pos % LegendDialog.consequenceColors.length] : 'black';
        } else {
            return 'black';
        }
    } else {
        return LegendDialog.othersColor;
    }
};

var variantsFill = function(d, fv) {
    if ((d.alternativeSequence === '*') || (d.begin > fv.maxPos)) {
        return LegendDialog.othersColor;
    } else if ((d.sourceType === Evidence.variantSourceType.uniprot) ||
        (d.sourceType === Evidence.variantSourceType.mixed)) {
        if (Evidence.existAssociation(d.association)) {
            return LegendDialog.UPDiseaseColor;
        } else {
            return LegendDialog.UPNonDiseaseColor;
        }
    } else {
        var externalPrediction, extDatum = {};
        if (d.externalData) {
            var keys = _.keys(d.externalData);
            extDatum = d.externalData[keys[0]];
            externalPrediction = getPredictionColorScore(extDatum.siftScore, extDatum.siftPrediction,
                extDatum.polyphenScore, extDatum.polyphenPrediction);
            extDatum.siftInUse = false;
            extDatum.polyphenInUse = false;
        }
        var predictionScore = getPredictionColorScore(d.siftScore, d.siftPrediction, d.polyphenScore,
            d.polyphenPrediction);

        return getVariantsFillColor(fv, d, extDatum, externalPrediction, predictionScore);
    }
};

var drawVariants = function(variantViewer, bars, frequency, fv, container, catTitle) {
    var variantCircle = bars.selectAll('circle')
        .data(function(d) {
            return d.variants;
        });

    var newCircles = variantCircle.enter().append('circle')
        .attr('r', function(d) {
            return frequency(0);
        });

    variantCircle
        .attr('class', function(d) {
            if (d === fv.selectedFeature) {
                return 'up_pftv_variant up_pftv_activeFeature';
            } else {
                return 'up_pftv_variant';
            }
        })
        .attr('cx', function(d) {
            return variantViewer.xScale(Math.min(d.begin, fv.sequence.length));
        })
        .attr('cy', function(d) {
            return variantViewer.yScale(d.alternativeSequence.charAt(0));
        })
        .attr('name', function(d) {
            var mutation = d.alternativeSequence === '*' ? 'STOP' :
                d.alternativeSequence;
            d.internalId = 'var_' + d.wildType + d.begin + mutation;
            return d.internalId;
        })
        .attr('fill', function(d) {
            return variantsFill(d, fv);
        })
        .attr('stroke', function(d) {
            if (d.externalData) {
                var keys = _.keys(d.externalData);
                var extDatum = d.externalData[keys[0]];
                if (extDatum.consequence) {
                    var pos = Constants.getConsequenceTypes().indexOf(extDatum.consequence);
                    return pos !== -1 ? LegendDialog.consequenceColors[pos % LegendDialog.consequenceColors.length] : 'black';
                } else {
                    return 'black';
                }
            } else {
                return 'none';
            }
        });

    ViewerHelper.addEventsClassAndTitle(catTitle, newCircles, fv, container);
    variantCircle.exit().remove();
};

var createDataSeries = function(fv, variantViewer, svg, features, series) {
    var mainChart = svg.append('g')
        .attr('transform', 'translate(0,' + variantViewer.margin.top + ')');

    var chartArea = mainChart.append('g')
        .attr('clip-path', 'url(#plotAreaClip)');

    mainChart.append('clipPath')
        .attr('id', 'plotAreaClip')
        .append('rect')
        .attr({ width: (variantViewer.width - 20), height: variantViewer.height })
        .attr('transform', 'translate(10, -10)');

    var dataSeries = chartArea
        .datum(features)
        .call(series);

    var yAxis = d3.svg.axis()
        .scale(variantViewer.yScale)
        .tickSize(-variantViewer.width)
        .orient('left');

    var yAxis2 = d3.svg.axis()
        .scale(variantViewer.yScale)
        .orient('right');

    mainChart.append('g')
        .attr('transform', 'translate(12 ,0)')
        .attr('class', 'variation-y axis')
        .call(yAxis);

    mainChart.append('g')
        .attr('transform', 'translate(' + (variantViewer.width - 18) + ', 0)')
        .attr('class', 'variation-y axis')
        .call(yAxis2);

    fv.globalContainer.selectAll('g.variation-y g.tick').attr('class', function(d) {
        return 'tick up_pftv_aa_' + (d === '*' ? 'loss' : d === 'del' ? 'deletion' : d);
    });

    return dataSeries;
};

var VariantViewer = function(catTitle, features, container, fv, variantHeight, titleContainer) {
    var variantViewer = this;
    variantViewer.height = variantHeight;
    variantViewer.width = fv.width;
    variantViewer.showManual = true;
    variantViewer.showAutomatic = true;
    variantViewer.xScale = fv.xScale;
    variantViewer.margin = { top: 20, bottom: 10 };
    variantViewer.features = features;

    variantViewer.filter = new VariantFilterDialog(fv, titleContainer, variantViewer);

    variantViewer.yScale = d3.scale.ordinal()
        .domain(aaList)
        .rangePoints([0, variantViewer.height - variantViewer.margin.top - variantViewer.margin.bottom]);

    var variationPlot = function() {
        var xScale = d3.scale.ordinal(),
            yScale = d3.scale.linear();

        var frequency = d3.scale.pow()
            .exponent(0.001)
            .domain([0, 1])
            .range([5, 10]);

        var variationPlot = function(selection) {
            var series, bars;

            selection.each(function(data) {
                // Generate chart
                series = d3.select(this);

                var withVariants = _.filter(data, function(elem) {
                    return elem.variants.length !== 0;
                });

                bars = series.selectAll('.up_pftv_var-series')
                    .data(withVariants, function(d) {
                        return d.pos;
                    });

                bars.enter()
                    .append('g')
                    .transition()
                    .duration(250)
                    .attr('class', 'up_pftv_var-series');

                drawVariants(variantViewer, bars, frequency, fv, container, catTitle);
                bars.exit().transition().duration(250).remove();
            });
        };

        variationPlot.xScale = function(value) {
            if (!arguments.length) {
                return xScale;
            }
            xScale = value;
            return variationPlot;
        };

        variationPlot.yScale = function(value) {
            if (!arguments.length) {
                return yScale;
            }
            yScale = value;
            return variationPlot;
        };

        return variationPlot;
    };

    var svg = ViewerHelper.createSVG(container, variantViewer.width, variantViewer.height, fv, 'up_pftv_variants-svg');

    // Data series
    var series = variationPlot()
        .xScale(variantViewer.xScale)
        .yScale(variantViewer.yScale);

    var dataSeries = createDataSeries(fv, variantViewer, svg, features, series);

    this.update = function() {
        dataSeries.call(series);
        if (fv.selectedFeature) {
            ViewerHelper.updateHighlight(fv);
        } else if (fv.highlight) {
            ViewerHelper.updateHighlight(fv);
        }
    };

    this.updateData = function(data) {
        dataSeries.datum(data);
        this.update();
    };

    this.reset = function() {
        this.filter.reset();
        this.updateData(this.features);
    };

    return this;
};

module.exports = VariantViewer;<commit_msg>Use deletion rather than d as suffix for variant matrix lines (so D and d do not conflict).<commit_after>/*jslint node: true */
/*jshint laxbreak: true */
/*jshint laxcomma: true */
"use strict";

var d3 = require("d3");
var _ = require("underscore");
var ViewerHelper = require("./ViewerHelper");
var LegendDialog = require("./VariantLegendDialog");
var VariantFilterDialog = require("./VariantFilterDialog");
var Evidence = require('./Evidence');
var Constants = require("./Constants");

//'G', 'A', 'V', 'L', 'I' aliphatic. 'S', 'T' hydroxyl. 'C', 'M' sulfur-containing. 'D', 'N', 'E', 'Q' acidic.
// 'R', 'K', 'H' basic. 'F', 'Y', 'W' aromatic. 'P' imino. '*' stop gained or lost.
var aaList = ['G', 'A', 'V', 'L', 'I', 'S', 'T', 'C', 'M', 'D', 'N', 'E', 'Q', 'R', 'K', 'H', 'F', 'Y', 'W', 'P', 'd', '*'];

var getPredictionColorScore = function(siftScore, siftPrediction, polyphenScore, polyphenPrediction) {
    var sift = false,
        polyphen = false;
    if ((polyphenPrediction !== undefined) && (polyphenPrediction !== 'unknown')) {
        polyphen = polyphenScore !== undefined ? true : false;
    }
    if (siftPrediction !== undefined) {
        sift = siftScore !== undefined ? true : false;
    }
    if (sift && polyphen) {
        return (siftScore + (1 - polyphenScore)) / 2;
    } else if (sift && !polyphen) {
        return siftScore;
    } else if (!sift && polyphen) {
        return 1 - polyphenScore;
    } else if (polyphenPrediction === 'unknown') {
        return 1;
    } else {
        return undefined;
    }
};

var getVariantsFillColor = function(fv, d, extDatum, externalPrediction, predictionScore) {
    if (fv.overwritePredictions === true) {
        if (externalPrediction !== undefined) {
            d.siftInUse = false;
            d.polyphenInUse = false;
            extDatum.siftInUse = true;
            extDatum.polyphenInUse = true;
            return LegendDialog.getPredictionColor(externalPrediction);
        } else if (predictionScore !== undefined) {
            return LegendDialog.getPredictionColor(predictionScore);
        }
    } else {
        if (predictionScore !== undefined) {
            return LegendDialog.getPredictionColor(predictionScore);
        } else if (externalPrediction !== undefined) {
            d.siftInUse = false;
            d.polyphenInUse = false;
            extDatum.siftInUse = true;
            extDatum.polyphenInUse = true;
            return LegendDialog.getPredictionColor(externalPrediction);
        }
    }

    if (d.externalData) {
        if (extDatum.consequence) {
            var pos = Constants.getConsequenceTypes().indexOf(extDatum.consequence);
            return pos !== -1 ? LegendDialog.consequenceColors[pos % LegendDialog.consequenceColors.length] : 'black';
        } else {
            return 'black';
        }
    } else {
        return LegendDialog.othersColor;
    }
};

var variantsFill = function(d, fv) {
    if ((d.alternativeSequence === '*') || (d.begin > fv.maxPos)) {
        return LegendDialog.othersColor;
    } else if ((d.sourceType === Evidence.variantSourceType.uniprot) ||
        (d.sourceType === Evidence.variantSourceType.mixed)) {
        if (Evidence.existAssociation(d.association)) {
            return LegendDialog.UPDiseaseColor;
        } else {
            return LegendDialog.UPNonDiseaseColor;
        }
    } else {
        var externalPrediction, extDatum = {};
        if (d.externalData) {
            var keys = _.keys(d.externalData);
            extDatum = d.externalData[keys[0]];
            externalPrediction = getPredictionColorScore(extDatum.siftScore, extDatum.siftPrediction,
                extDatum.polyphenScore, extDatum.polyphenPrediction);
            extDatum.siftInUse = false;
            extDatum.polyphenInUse = false;
        }
        var predictionScore = getPredictionColorScore(d.siftScore, d.siftPrediction, d.polyphenScore,
            d.polyphenPrediction);

        return getVariantsFillColor(fv, d, extDatum, externalPrediction, predictionScore);
    }
};

var drawVariants = function(variantViewer, bars, frequency, fv, container, catTitle) {
    var variantCircle = bars.selectAll('circle')
        .data(function(d) {
            return d.variants;
        });

    var newCircles = variantCircle.enter().append('circle')
        .attr('r', function(d) {
            return frequency(0);
        });

    variantCircle
        .attr('class', function(d) {
            if (d === fv.selectedFeature) {
                return 'up_pftv_variant up_pftv_activeFeature';
            } else {
                return 'up_pftv_variant';
            }
        })
        .attr('cx', function(d) {
            return variantViewer.xScale(Math.min(d.begin, fv.sequence.length));
        })
        .attr('cy', function(d) {
            return variantViewer.yScale(d.alternativeSequence.charAt(0));
        })
        .attr('name', function(d) {
            var mutation = d.alternativeSequence === '*' ? 'STOP' :
                d.alternativeSequence;
            d.internalId = 'var_' + d.wildType + d.begin + mutation;
            return d.internalId;
        })
        .attr('fill', function(d) {
            return variantsFill(d, fv);
        })
        .attr('stroke', function(d) {
            if (d.externalData) {
                var keys = _.keys(d.externalData);
                var extDatum = d.externalData[keys[0]];
                if (extDatum.consequence) {
                    var pos = Constants.getConsequenceTypes().indexOf(extDatum.consequence);
                    return pos !== -1 ? LegendDialog.consequenceColors[pos % LegendDialog.consequenceColors.length] : 'black';
                } else {
                    return 'black';
                }
            } else {
                return 'none';
            }
        });

    ViewerHelper.addEventsClassAndTitle(catTitle, newCircles, fv, container);
    variantCircle.exit().remove();
};

var createDataSeries = function(fv, variantViewer, svg, features, series) {
    var mainChart = svg.append('g')
        .attr('transform', 'translate(0,' + variantViewer.margin.top + ')');

    var chartArea = mainChart.append('g')
        .attr('clip-path', 'url(#plotAreaClip)');

    mainChart.append('clipPath')
        .attr('id', 'plotAreaClip')
        .append('rect')
        .attr({ width: (variantViewer.width - 20), height: variantViewer.height })
        .attr('transform', 'translate(10, -10)');

    var dataSeries = chartArea
        .datum(features)
        .call(series);

    var yAxis = d3.svg.axis()
        .scale(variantViewer.yScale)
        .tickSize(-variantViewer.width)
        .orient('left');

    var yAxis2 = d3.svg.axis()
        .scale(variantViewer.yScale)
        .orient('right');

    mainChart.append('g')
        .attr('transform', 'translate(12 ,0)')
        .attr('class', 'variation-y axis')
        .call(yAxis);

    mainChart.append('g')
        .attr('transform', 'translate(' + (variantViewer.width - 18) + ', 0)')
        .attr('class', 'variation-y axis')
        .call(yAxis2);

    fv.globalContainer.selectAll('g.variation-y g.tick').attr('class', function(d) {
        return 'tick up_pftv_aa_' + (d === '*' ? 'loss' : (d === 'd') || (d === 'del') ? 'deletion' : d);
    });

    return dataSeries;
};

var VariantViewer = function(catTitle, features, container, fv, variantHeight, titleContainer) {
    var variantViewer = this;
    variantViewer.height = variantHeight;
    variantViewer.width = fv.width;
    variantViewer.showManual = true;
    variantViewer.showAutomatic = true;
    variantViewer.xScale = fv.xScale;
    variantViewer.margin = { top: 20, bottom: 10 };
    variantViewer.features = features;

    variantViewer.filter = new VariantFilterDialog(fv, titleContainer, variantViewer);

    variantViewer.yScale = d3.scale.ordinal()
        .domain(aaList)
        .rangePoints([0, variantViewer.height - variantViewer.margin.top - variantViewer.margin.bottom]);

    var variationPlot = function() {
        var xScale = d3.scale.ordinal(),
            yScale = d3.scale.linear();

        var frequency = d3.scale.pow()
            .exponent(0.001)
            .domain([0, 1])
            .range([5, 10]);

        var variationPlot = function(selection) {
            var series, bars;

            selection.each(function(data) {
                // Generate chart
                series = d3.select(this);

                var withVariants = _.filter(data, function(elem) {
                    return elem.variants.length !== 0;
                });

                bars = series.selectAll('.up_pftv_var-series')
                    .data(withVariants, function(d) {
                        return d.pos;
                    });

                bars.enter()
                    .append('g')
                    .transition()
                    .duration(250)
                    .attr('class', 'up_pftv_var-series');

                drawVariants(variantViewer, bars, frequency, fv, container, catTitle);
                bars.exit().transition().duration(250).remove();
            });
        };

        variationPlot.xScale = function(value) {
            if (!arguments.length) {
                return xScale;
            }
            xScale = value;
            return variationPlot;
        };

        variationPlot.yScale = function(value) {
            if (!arguments.length) {
                return yScale;
            }
            yScale = value;
            return variationPlot;
        };

        return variationPlot;
    };

    var svg = ViewerHelper.createSVG(container, variantViewer.width, variantViewer.height, fv, 'up_pftv_variants-svg');

    // Data series
    var series = variationPlot()
        .xScale(variantViewer.xScale)
        .yScale(variantViewer.yScale);

    var dataSeries = createDataSeries(fv, variantViewer, svg, features, series);

    this.update = function() {
        dataSeries.call(series);
        if (fv.selectedFeature) {
            ViewerHelper.updateHighlight(fv);
        } else if (fv.highlight) {
            ViewerHelper.updateHighlight(fv);
        }
    };

    this.updateData = function(data) {
        dataSeries.datum(data);
        this.update();
    };

    this.reset = function() {
        this.filter.reset();
        this.updateData(this.features);
    };

    return this;
};

module.exports = VariantViewer;<|endoftext|>


<commit_before>/*jslint nomen: true */
/*globals define: true */

define([
  'jquery',
  'lib/lodash',
  'backbone',
  'settings'
],

function($, _, Backbone, settings) {
  'use strict';

  var Forms = {};

  var ANSWER = 'response';
  var NOANSWER = 'no response';

  // Forms model
  Forms.Model = Backbone.Model.extend({
    initialize: function(options) {
      this.surveyId = options.surveyId;
    },

    url: function() {
      if (this.get("id") !== undefined) {
        return settings.api.baseurl + '/surveys/' + this.surveyId + '/forms';
      }
      return settings.api.baseurl + '/surveys/' + this.surveyId + '/forms';
    },

    // Flatten the most recent form into a mapping of question names to
    // immediate question info (text and possible answers)
    // { 'are-you-cool': {text: 'Are you cool?', answers: [{value: 'yes', text: 'Yes'}, {value: 'no', text: 'No'}]}}
    // TODO: It may be better to return an array of questions.
    getFlattenedForm: function getFlattenedForm(questions) {
      var result = {};

      if(!questions) {
        questions = this.get('questions');
      }

      function makeUnique(name) {
        if (result[name] === undefined) {
          return name;
        }
        // The question name is not unique.
        var i = 1;
        while (result[name + '-' + i] !== undefined) {
          i += 1;
        }
        return name + '-' + i;
      }

      function flattenHelper(question) {
        if (question.type === 'checkbox') {
          _.each(question.answers, function (answer) {
            var name = makeUnique(answer.name);
            result[name] = {
              text: question.text + ': ' + answer.text,
              answers: [
                {
                  value: answer.value,
                  text: answer.value
                },
                {
                  value: NOANSWER,
                  text: NOANSWER
                }
              ]
            };

            if (answer.questions) {
              _.each(answer.questions, flattenHelper);
            }
          });
          return;
        }

        var name = question.name;
        var type;
        var answerInfo = [];
        if (question.answers && question.answers.length > 0) {
          _.each(question.answers, function (answer) {
            answerInfo.push({
              value: answer.value,
              text: answer.text
            });
          });
        } else {
          type = question.type;
          answerInfo.push({
            value: ANSWER,
            text: ANSWER
          });
        }

        answerInfo.push({
          value: NOANSWER,
          text: NOANSWER
        });

        var questionInfo = {
          text: question.text,
          answers: answerInfo
        };

        if (type) {
          questionInfo.type = type;
        }

        if (result[name] === undefined) {
          result[name] = questionInfo;
        } else {
          // The question name is not unique.
          var i = 1;
          while (result[name + '-' + i] !== undefined) {
            i += 1;
          }
          result[name + '-' + i] = questionInfo;
        }

        if (question.answers) {
          _.each(question.answers, function (answer) {
            if (answer.questions) {
              _.each(answer.questions, flattenHelper);
            }
          });
        }
      }

      _.each(questions, flattenHelper);

      return result;
    },

    /**
     * Return the flattened form for subquestions of a particular question.
     * Only works for top-level questions.
     * @param  {String} question
     * @param  {String} answer
     * @return {Object}          Flattened form
     */
    getSubquestionsFor: function(question, answer) {
      var questions = this.get('questions');
      var q = _.findWhere(questions, { name: question} );
      var a = _.findWhere(q.answers, { value: answer } );
      var subquestions = a.questions;
      console.log("GOT QUESTION....", q);
      return this.getFlattenedForm(subquestions);
    }
  });

  // Forms collection
  Forms.Collection = Backbone.Collection.extend({
    model: Forms.Model,

    initialize: function(options) {
      this.surveyId = options.surveyId;
      this.fetch({ reset: true });
    },

    url: function() {
      return settings.api.baseurl + '/surveys/' + this.surveyId + '/forms';
    },

    parse: function(response) {
      if (response.forms.length === 0) {
        return [];
      }

      return response.forms;
    },

    // Find the most recent form of the given type (usually "mobile" or "paper")
    getMostRecentForm: function(type) {
      var i;

      // find mobile forms by default
      if (type === undefined) {
        type = "mobile";
      }

      for (i = 0; i < this.models.length; i += 1) {
        if (this.models[i].get('type') === type) {
          return this.models[i];
        }
      }
    },

    // Flatten the most recent form into a mapping of question names to
    // immediate question info.
    // { 'are-you-cool': {text: 'Are you cool?', answers: [{value: 'yes', text: 'Yes'}, {value: 'no', text: 'No'}]}}
    getFlattenedForm: function getFlattenedForm() {
      if (!this.getMostRecentForm()) {
        return undefined;
      }
      return this.getMostRecentForm().getFlattenedForm();
    },

    getQuestions: function() {
      var flattenedForm = this.getFlattenedForm();
      var questions = {};
      _.each(_.keys(flattenedForm), function (name) {
        questions[name] = flattenedForm[name].text;
      });
      return questions;
    },

    getSubquestionsFor: function(question, answer) {
      if (!this.getMostRecentForm()) {
        return undefined;
      }
      return this.getMostRecentForm().getSubquestionsFor(question, answer);
    }
  });

  return Forms;

}); // End Forms module

<commit_msg>Expose existing forms in the upcoming form structure<commit_after>/*jslint nomen: true */
/*globals define: true */

define([
  'jquery',
  'lib/lodash',
  'backbone',
  'settings'
],

function($, _, Backbone, settings) {
  'use strict';

  var Forms = {};

  var ANSWER = 'response';
  var NOANSWER = 'no response';

  // Forms model
  Forms.Model = Backbone.Model.extend({
    initialize: function(options) {
      this.surveyId = options.surveyId;
    },

    url: function() {
      if (this.get("id") !== undefined) {
        return settings.api.baseurl + '/surveys/' + this.surveyId + '/forms';
      }
      return settings.api.baseurl + '/surveys/' + this.surveyId + '/forms';
    },

    // Return a v2-structured form:
    // Form is an array of questions
    // Question has a slug, id, type, text, possibly a group, possibly array of answers, and possibly a condition.
    // Answer has a slug, id, and text.
    // IDs are unique and are format-wise valid as slugs. Slugs are not necessarily unique.
    // Condition has a type and type-specific fields.
    getFormV2: function getFormV2() {
      var list = [];

      var ids = {};
      function uniqueId(slug) {
        var id = slug;
        var dupe = ids[id];
        var i = 0;
        while (dupe) {
          i += 1;
          id = slug + '-' + i;
          dupe = ids[id];
        }
        ids[id] = true;
        return id;
      }

      function transformAnswer(old) {
        return {
          value: old.value,
          id: uniqueId(old.value),
          text: old.text
        };
      }

      function transformQuestion(old, condition) {
        var type = old.type;
        if (!type) {
          type = 'radio';
        }
        var question = {};

        if (condition) {
          question.condition = condition;
        }

        if (type !== 'checkbox') {
          question.type = type;
          question.slug = old.name;
          question.id = uniqueId(question.slug);
          question.text = old.text;

          list.push(question);
          if (old.answers) {
            // For each answer, create a v2 answer object.
            question.answers = _.map(old.answers, transformAnswer);

            // Process each answer's sub-questions with transformQuestion.
            _.each(old.answers, function (answer, i) {
              if (answer.questions) {
                _.forEach(answer.questions, _.curry(transformQuestion)(_, {
                  type: 'answer-selection',
                  question: question.id,
                  answer: question.answers[i].id
                }));
              }
            });
          }
        } else {
          // Checkbox questions were previously organized as one question with
          // multiple answers. For storing the responses, we would merge the
          // question and answer slug into a synthetic question slug, and the
          // answer would be "Yes".
          // For v2, we treat each checkbox as an independent question from the
          // beginning, and we make them part of a single group.

          // Group
          list.push({
            type: 'group',
            slug: old.name,
            id: uniqueId(old.name),
            text: old.text
          });

          var subs = [];
          // Individual checkboxes.
          _.forEach(old.answers, function (item) {
            var id = uniqueId(item.name);
            list.push({
              type: 'checkbox',
              slug: item.name,
              id: id,
              text: item.text
            });
            subs.push({
              questions: item.questions,
              id: id,
              answer: 'yes'
            });
          });

          // Sub-questions
          _.forEach(subs, function (item) {
            if (item.questions) {
              _.forEach(item.questions, _.curry(transformQuestion)(_, {
                type: 'answer-selection',
                question: item.id,
                answer: item.answer
              }));
            }
          });
        }
      }

      _.forEach(this.get('questions'), transformQuestion);
      return { questions: list };
    },

    // Flatten the most recent form into a mapping of question names to
    // immediate question info (text and possible answers)
    // { 'are-you-cool': {text: 'Are you cool?', answers: [{value: 'yes', text: 'Yes'}, {value: 'no', text: 'No'}]}}
    // TODO: It may be better to return an array of questions.
    getFlattenedForm: function getFlattenedForm(questions) {
      var result = {};

      if(!questions) {
        questions = this.get('questions');
      }

      function makeUnique(name) {
        if (result[name] === undefined) {
          return name;
        }
        // The question name is not unique.
        var i = 1;
        while (result[name + '-' + i] !== undefined) {
          i += 1;
        }
        return name + '-' + i;
      }

      function flattenHelper(question) {
        if (question.type === 'checkbox') {
          _.each(question.answers, function (answer) {
            var name = makeUnique(answer.name);
            result[name] = {
              text: question.text + ': ' + answer.text,
              answers: [
                {
                  value: answer.value,
                  text: answer.value
                },
                {
                  value: NOANSWER,
                  text: NOANSWER
                }
              ]
            };

            if (answer.questions) {
              _.each(answer.questions, flattenHelper);
            }
          });
          return;
        }

        var name = question.name;
        var type;
        var answerInfo = [];
        if (question.answers && question.answers.length > 0) {
          _.each(question.answers, function (answer) {
            answerInfo.push({
              value: answer.value,
              text: answer.text
            });
          });
        } else {
          type = question.type;
          answerInfo.push({
            value: ANSWER,
            text: ANSWER
          });
        }

        answerInfo.push({
          value: NOANSWER,
          text: NOANSWER
        });

        var questionInfo = {
          text: question.text,
          answers: answerInfo
        };

        if (type) {
          questionInfo.type = type;
        }

        if (result[name] === undefined) {
          result[name] = questionInfo;
        } else {
          // The question name is not unique.
          var i = 1;
          while (result[name + '-' + i] !== undefined) {
            i += 1;
          }
          result[name + '-' + i] = questionInfo;
        }

        if (question.answers) {
          _.each(question.answers, function (answer) {
            if (answer.questions) {
              _.each(answer.questions, flattenHelper);
            }
          });
        }
      }

      _.each(questions, flattenHelper);

      return result;
    },

    /**
     * Return the flattened form for subquestions of a particular question.
     * Only works for top-level questions.
     * @param  {String} question
     * @param  {String} answer
     * @return {Object}          Flattened form
     */
    getSubquestionsFor: function(question, answer) {
      var questions = this.get('questions');
      var q = _.findWhere(questions, { name: question} );
      var a = _.findWhere(q.answers, { value: answer } );
      var subquestions = a.questions;
      console.log("GOT QUESTION....", q);
      return this.getFlattenedForm(subquestions);
    }
  });

  // Forms collection
  Forms.Collection = Backbone.Collection.extend({
    model: Forms.Model,

    initialize: function(options) {
      this.surveyId = options.surveyId;
      this.fetch({ reset: true });
    },

    url: function() {
      return settings.api.baseurl + '/surveys/' + this.surveyId + '/forms';
    },

    parse: function(response) {
      if (response.forms.length === 0) {
        return [];
      }

      return response.forms;
    },

    // Find the most recent form of the given type (usually "mobile" or "paper")
    getMostRecentForm: function(type) {
      var i;

      // find mobile forms by default
      if (type === undefined) {
        type = "mobile";
      }

      for (i = 0; i < this.models.length; i += 1) {
        if (this.models[i].get('type') === type) {
          return this.models[i];
        }
      }
    },

    // Flatten the most recent form into a mapping of question names to
    // immediate question info.
    // { 'are-you-cool': {text: 'Are you cool?', answers: [{value: 'yes', text: 'Yes'}, {value: 'no', text: 'No'}]}}
    getFlattenedForm: function getFlattenedForm() {
      var form = this.getMostRecentForm();
      if (!form) {
        return undefined;
      }
      return form.getFlattenedForm();
    },

    getQuestions: function() {
      var flattenedForm = this.getFlattenedForm();
      var questions = {};
      _.each(_.keys(flattenedForm), function (name) {
        questions[name] = flattenedForm[name].text;
      });
      return questions;
    },

    getSubquestionsFor: function(question, answer) {
      if (!this.getMostRecentForm()) {
        return undefined;
      }
      return this.getMostRecentForm().getSubquestionsFor(question, answer);
    }
  });

  return Forms;

}); // End Forms module
<|endoftext|>


<commit_before>/**
 * @class WebRtcContent
 *
 * @param {String} ws_uri: URI of the server WebSocket endpoint. Alternatively,
 *   it can be used a WebSocket or DataChannel object.
 */
function WebRtcContent(url, options)
{
  var self = this;

  // Initialize options and object status
  options = options || {};

  $.jsonRPC.setup({endPoint: url});

  var sessionId = null;
  var pollingTimeout = null;

  // Error dispatcher functions

  /**
   * Common function to dispatch the error to the user application
   *
   * @param {Error} error: error object to be dispatched
   */
  function onerror(error)
  {
    if(self.onerror)
       self.onerror(error);
  };

  /**
   * Adaptor from jsonRPC errors to standard Javascript Error objects
   *
   * @param response: jsonRPC error object
   */
  function onerror_jsonrpc(response)
  {
    onerror(new Error(response.error || response));
  };

  // Create the PeerConnection object
  var pc = new RTCPeerConnection(
  {
    iceServers: [{url: 'stun:'+'stun.l.google.com:19302'}]
  });

  // Add the local stream if defined
  if(options.stream)
    pc.addStream(options.stream);

  // Dispatch 'close' event if signaling gets closed
  pc.onsignalingstatechange = function(event)
  {
    if(pc.signalingState == "closed"
    && self.onclose)
       self.onclose(new Event('close'));
  };

  // RPC calls

  // Start

  var mediaConstraints =
  {
    'mandatory':
    {
      'OfferToReceiveAudio': true,
      'OfferToReceiveVideo': true
    }
  };

  pc.createOffer(function(offer)
  {
    // Set the peer local description
    pc.setLocalDescription(offer,
    function()
    {
      console.info("LocalDescription correctly set");
    },
    onerror);
  },
  onerror,
  mediaConstraints);

  pc.onicecandidate = function(event)
  {
    if(event.candidate)
      return;

    var params =
    {
      sdp: pc.localDescription.sdp
    };

    /**
     * Callback when connection is succesful
     *
     * @param {Object} response: JsonRPC response
     */
    function success(response)
    {
      var result = response.result;

      sessionId = result.sessionId;

      pc.setRemoteDescription(new RTCSessionDescription(
      {
        type: 'answer',
        sdp: result.sdp
      }),
      function()
      {
        // Init MediaEvents polling
        pollMediaEvents();

        // Notify to the user about the new stream
        if(self.onopen)
        {
          var streams = pc.getRemoteStreams();

          if(streams && streams[0])
          {
            var event = new Event('open');
                event.stream = streams[0];

            self.onopen(event);
          }
          else
            onerror(new Error("No streams are available"));
        }
      },
      onerror);
    }

    $.jsonRPC.request('start',
    {
      params:  params,
      success: success,
      error:   onerror_jsonrpc
    });
  };

  /**
   * Terminate the connection with the WebRTC media server
   */
  this.terminate = function()
  {
    // Stop polling
    clearTimeout(pollingTimeout);
    pollingTimeout = 'stopped';

    // Notify to the WebRTC endpoint server
    var params =
    {
      sessionId: sessionId
    };

    $.jsonRPC.request('terminate',
    {
      params: params,
      error:  onerror_jsonrpc
    });

    // Close the PeerConnection
    pc.close();
  };

  // Pool

  /**
   * Pool for events dispatched on the server pipeline
   */
  function pollMediaEvents()
  {
    var timeout = 5*1000;  // Request events each 5 seconds

    var params =
    {
      sessionId: sessionId
    };

    $.jsonRPC.request('poll',
    {
      params: params,
      success: function(response)
      {
        var result = response.result;

        if(result.events)
          for(var i=0, data; data=result.events[i]; i++)
            if(self.onMediaEvent)
            {
              var event = new Event('MediaEvent');
                  event.data = data;

              self.onMediaEvent(event);
            }

        if(pollingTimeout != 'stopped')
           pollingTimeout = setTimeout(pollMediaEvents, timeout);
      },
      error: function(event)
      {
        onerror_jsonrpc(event);

        if(pollingTimeout != 'stopped')
           pollingTimeout = setTimeout(pollMediaEvents, timeout);
      }
    });
  }

  // Mediacontrol

  /**
   * Start a new media communication
   *
   * @param {String} mediaId: identifier of the media communication
   * @param {Object} options: configuration options
   *   {Enum(null, 'send', 'recv', 'sendrecv')} audio: audio stream mode
   *   {Enum(null, 'send', 'recv', 'sendrecv')} video: video stream mode
   * @param {Function(Error, Media)} callback: continuation function
   */
/*  this.startMedia = function(mediaId, options, callback)
  {
    // Adjust arguments if options is not defined
    if(typeof options == 'function')
    {
      callback = options;
      options = undefined;
    }

    options = options || {};

    // We can't disable both audio and video on a stream, raise error
    if(options.audio == null
    && options.video == null)
    {
      callback(new Error("No audio or video medias are defined"));
      return
    }
*/
    /**
     * Decode the mode of the streams
     */
/*    function decodeMode(type, mode)
    {
      var result = {};

      switch(mode)
      {
        case undefined:  // If not defined, set send & receive by default
        case 'sendrecv':
          result.local = true;
          result.remote = true;
        break;

        case 'send':
          result.local = true;
          result.remote = false;
        break;

        case 'recv':
          result.local = false;
          result.remote = true;
        break;

        default:
          callback(new Error("Invalid "+type+" media mode"));
          return
      }

      return result;
    }

    // Audio media
    var audio = decodeMode("audio", options.audio);
    if(!audio) return;

    // Video media
    var video = decodeMode("video", options.video);
    if(!video) return;
*/
    /**
     * Send an offer with the current and new streams and constraints
     */
/*
    // Mode set to send local audio and/or video stream
    if(audio.local || video.local)
      navigator.getUserMedia({'audio': audio.local, 'video': video.local},
      function(stream)
      {
        pc.addStream(stream);
        sendOffer();
      },
      onerror)

    // Mode set to only receive a stream, not send it
    else
      sendOffer();
  }*/
}
<commit_msg>Decoding the audio and video constraints<commit_after>/**
 * @class WebRtcContent
 *
 * @param {String} url: URL of the WebRTC endpoint server.
 * @param {Object} options: optional configurations
 *   {Enum('none', 'send', 'recv', 'sendrecv')} audio: audio stream mode
 *   {Enum('none', 'send', 'recv', 'sendrecv')} video: video stream mode
 */
function WebRtcContent(url, options)
{
  var self = this;

  // Initialize options and object status
  options = options || {};

  /**
   * Decode the mode of the streams
   *
   * @param {String} type: name of the constraints to decode (only for debug)
   * @param {String} mode: constraints to decode
   */
  function decodeMode(type, mode)
  {
    var result = {};

    switch(mode)
    {
      case undefined:  // If not defined, set send & receive by default
      case null:
      case 'sendrecv':
        result.local  = true;
        result.remote = true;
      break;

      case 'send':
        result.local  = true;
        result.remote = false;
      break;

      case 'recv':
        result.local  = false;
        result.remote = true;
      break;

      case 'none':
        result.local  = false;
        result.remote = false;
      break;

      default:
        throw new RangeError("Invalid "+type+" media mode");
    }

    return result;
  }

  // We can't disable both audio and video on a stream, raise error
  if(options.audio == 'none' && options.video == 'none')
    throw new RangeError("At least one audio or video must to be enabled");

  // Audio media
  var audio = decodeMode("audio", options.audio);

  // Video media
  var video = decodeMode("video", options.video);

  $.jsonRPC.setup({endPoint: url});

  var sessionId = null;
  var pollingTimeout = null;

  // Error dispatcher functions

  /**
   * Common function to dispatch the error to the user application
   *
   * @param {Error} error: error object to be dispatched
   */
  function onerror(error)
  {
    if(self.onerror)
       self.onerror(error);
  };

  /**
   * Adaptor from jsonRPC errors to standard Javascript Error objects
   *
   * @param response: jsonRPC error object
   */
  function onerror_jsonrpc(response)
  {
    onerror(new Error(response.error || response));
  };

  // Create the PeerConnection object
  var pc = new RTCPeerConnection(
  {
    iceServers: [{url: 'stun:'+'stun.l.google.com:19302'}]
  });

  // Add the local stream if defined
  if(options.stream)
    pc.addStream(options.stream);

  // Dispatch 'close' event if signaling gets closed
  pc.onsignalingstatechange = function(event)
  {
    if(pc.signalingState == "closed"
    && self.onclose)
       self.onclose(new Event('close'));
  };

  // RPC calls

  // Start

  var mediaConstraints =
  {
    'mandatory':
    {
      'OfferToReceiveAudio': true,
      'OfferToReceiveVideo': true
    }
  };

  pc.createOffer(function(offer)
  {
    // Set the peer local description
    pc.setLocalDescription(offer,
    function()
    {
      console.info("LocalDescription correctly set");
    },
    onerror);
  },
  onerror,
  mediaConstraints);

  pc.onicecandidate = function(event)
  {
    if(event.candidate)
      return;

    var params =
    {
      sdp: pc.localDescription.sdp
    };

    /**
     * Callback when connection is succesful
     *
     * @param {Object} response: JsonRPC response
     */
    function success(response)
    {
      var result = response.result;

      sessionId = result.sessionId;

      pc.setRemoteDescription(new RTCSessionDescription(
      {
        type: 'answer',
        sdp: result.sdp
      }),
      function()
      {
        // Init MediaEvents polling
        pollMediaEvents();

        // Notify to the user about the new stream
        if(self.onopen)
        {
          var streams = pc.getRemoteStreams();

          if(streams && streams[0])
          {
            var event = new Event('open');
                event.stream = streams[0];

            self.onopen(event);
          }
          else
            onerror(new Error("No streams are available"));
        }
      },
      onerror);
    }

    $.jsonRPC.request('start',
    {
      params:  params,
      success: success,
      error:   onerror_jsonrpc
    });
  };

  /**
   * Terminate the connection with the WebRTC media server
   */
  this.terminate = function()
  {
    // Stop polling
    clearTimeout(pollingTimeout);
    pollingTimeout = 'stopped';

    // Notify to the WebRTC endpoint server
    var params =
    {
      sessionId: sessionId
    };

    $.jsonRPC.request('terminate',
    {
      params: params,
      error:  onerror_jsonrpc
    });

    // Close the PeerConnection
    pc.close();
  };

  // Pool

  /**
   * Pool for events dispatched on the server pipeline
   */
  function pollMediaEvents()
  {
    var timeout = 5*1000;  // Request events each 5 seconds

    var params =
    {
      sessionId: sessionId
    };

    $.jsonRPC.request('poll',
    {
      params: params,
      success: function(response)
      {
        var result = response.result;

        if(result.events)
          for(var i=0, data; data=result.events[i]; i++)
            if(self.onMediaEvent)
            {
              var event = new Event('MediaEvent');
                  event.data = data;

              self.onMediaEvent(event);
            }

        if(pollingTimeout != 'stopped')
           pollingTimeout = setTimeout(pollMediaEvents, timeout);
      },
      error: function(event)
      {
        onerror_jsonrpc(event);

        if(pollingTimeout != 'stopped')
           pollingTimeout = setTimeout(pollMediaEvents, timeout);
      }
    });
  }

  // Mediacontrol

  /**
   * Start a new media communication
   *
   * @param {String} mediaId: identifier of the media communication
   * @param {Function(Error, Media)} callback: continuation function
   */
/*  this.startMedia = function(mediaId, options, callback)
  {
*/

    /**
     * Send an offer with the current and new streams and constraints
     */
/*
    // Mode set to send local audio and/or video stream
    if(audio.local || video.local)
      navigator.getUserMedia({'audio': audio.local, 'video': video.local},
      function(stream)
      {
        pc.addStream(stream);
        sendOffer();
      },
      onerror)

    // Mode set to only receive a stream, not send it
    else
      sendOffer();
  }*/
}<|endoftext|>


<commit_before>this.mmooc=this.mmooc||{};

this.mmooc.menu = function() {

    function _renderCourseMenu(course, selectedMenuItem, title) {
        var menuItems = [];

        var courseId = course.id;

        menuItems[menuItems.length] = {"title": "Kursforside", url: "/courses/" + courseId};
        menuItems[menuItems.length] = {"title": "Kunngjøringer", url: "/courses/" + courseId + "/announcements"};
        menuItems[menuItems.length] = {"title": "Diskusjoner", url: "/courses/" + courseId + "/discussion_topics"};
        menuItems[menuItems.length] = mmooc.menu.extractBadgesLinkFromPage();

        var subtitle = course.name;
        if (title == null) {
            title = course.name;
            subtitle = "";
        }

        var html = mmooc.util.renderTemplateWithData("coursemenu", {course: course, menuItems: menuItems, selectedMenuItem: selectedMenuItem, title: title, subtitle: subtitle });
        document.getElementById('header').insertAdjacentHTML('afterend', html);
    }

    function createStyleSheet () {
        var style = document.createElement("style");

        // WebKit hack :(
        style.appendChild(document.createTextNode(""));

        document.head.appendChild(style);

        return style.sheet;
    }

    var stylesheet = createStyleSheet();

    return {
        listModuleItems: function() {
            mmooc.api.getCurrentModule(function(module) {
                var courseId = mmooc.api.getCurrentCourseId();
                var html = mmooc.util.renderTemplateWithData("moduleitems", {module: module, courseId: courseId});
                document.getElementById('left-side').insertAdjacentHTML('afterbegin', html);
            });
        },
        showLeftMenu: function() {
            stylesheet.insertRule("body.with-left-side #main { margin-left: 305px !important }", stylesheet.cssRules.length);
            stylesheet.insertRule(".with-left-side #left-side { display: block !important }", stylesheet.cssRules.length);
        },

        showTeacherAdminMenu: function() {
            if (mmooc.util.isTeacherOrAdmin()) {
                this.showLeftMenu();

                $("#section-tabs-header").show();
                $("nav[aria-label='context']").show();
                $("#edit_discussions_settings").show();
                $("#availability_options").show();
                $("#group_category_options").show();
                $("#editor_tabs").show();

                // Done via CSS since content is loaded using AJAX
                stylesheet.insertRule("body.pages .header-bar-outer-container { display: block }", stylesheet.cssRules.length);
                stylesheet.insertRule("#discussion-managebar { display: block }", stylesheet.cssRules.length);
            }

            var roles = mmooc.api.getRoles();
            if (roles != null && roles.indexOf('admin') != -1) {
                // Admin needs original canvas Course dropdown to access site admin settings
                $("#courses_menu_item").show();

                // Admin needs more profile settings
                $(".add_access_token_link").show();
                $("body.profile_settings").find("#content > table, #content > h2, #content > p").show();
            } else {
                document.getElementById('menu').insertAdjacentHTML('afterbegin', '
Kurs
');
            }
        },

        hideRightMenu: function() {
            $("#right-side").hide();
            $("body").removeClass('with-right-side');
        },

        showUserMenu: function() {
            var menu = document.getElementById('menu');
            if (menu !=  null) {
                var html = mmooc.util.renderTemplateWithData("usermenu", {user: mmooc.api.getUser()});
                menu.insertAdjacentHTML('afterend', html);

                var msgBadge = $("#mmooc-unread-messages-count");
                if (mmooc.api.getUnreadMessageSize() === 0) {
                  msgBadge.hide();
                }
                else {
                  msgBadge.html(mmooc.api.getUnreadMessageSize());
                  msgBadge.show();
                }

                mmooc.api.getActivityStreamForUser(function(activities) {
                    var unreadNotifications = 0;
                    for (var i = 0; i < activities.length; i++) {
                        if (mmooc.menu.checkReadStateFor(activities[i])) {
                            unreadNotifications++;
                        }
                    }

                    var badge = $("#mmooc-notification-count");
                    if (unreadNotifications == 0) {
                        badge.hide();
                    } else {
                        badge.html(unreadNotifications);
                        badge.show();
                    }

                    document.getElementById('mmooc-activity-stream').innerHTML = mmooc.util.renderTemplateWithData("activitystream", {activities: activities});

                    var notifications = $("#mmooc-notifications").find("li");
                    if (notifications.size() == 0) {
                        $("#mmooc-notifications").hide();
                    } else {
                        $("#mmooc-notifications").show();
                    }

                    var showAllItems = $("#mmooc-notifications-showall");
                    if (notifications.size() > 10) {
                        notifications.slice(10).addClass("hidden");

                        showAllItems.click(function() {
                            notifications.removeClass("hidden");
                            showAllItems.hide();
                        });
                    } else {
                        showAllItems.hide();
                    }

                });
            }
        },

        showCourseMenu: function(courseId, selectedMenuItem, title) {
            $("body").addClass("with-course-menu");
            mmooc.api.getCourse(courseId, function(course) {
                _renderCourseMenu(course, selectedMenuItem, title);
            });
        },

        showBackButton: function(url, title) {
            var buttonHTML = mmooc.util.renderTemplateWithData("backbutton", {url: url, title: title});
            document.getElementById('content-wrapper').insertAdjacentHTML('afterbegin', buttonHTML);
        },

        showGroupHeader: function() {
            var groupId = mmooc.api.getCurrentGroupId();
            var groupHeaderHTML = mmooc.util.renderTemplateWithData("backbutton", {groupId: groupId});
            document.getElementById('content-wrapper').insertAdjacentHTML('afterbegin', groupHeaderHTML);
        },

        showDiscussionGroupMenu: function() {
            var groupId = mmooc.api.getCurrentGroupId();
            if (groupId != null) {
                mmooc.api.getGroup(groupId, function(group) {
                    // For discussion pages we only want the title to be "<discussion>" instead of "Discussion: <discussion>"
                    var title = mmooc.util.getPageTitleAfterColon();
                    mmooc.menu.showCourseMenu(group.course_id, "Grupper", title);

                    var headerHTML = mmooc.util.renderTemplateWithData("groupdiscussionheader", { group: group});
                    document.getElementById('content-wrapper').insertAdjacentHTML('afterbegin', headerHTML);
                });
            }
        },

        checkReadStateFor: function (activity) {
            return activity.read_state === false;
        },

        extractBadgesLinkFromPage: function () {
            var href = $('li.section:contains("BadgeSafe")').find('a').attr('href');
            return {"title": mmooc.i18n.Badgesafe, url: href};
        },

        injectGroupsPage: function() {
          $('#courses_menu_item').after('
Grupper
');
        },

        alterHomeLink: function() {
          $('#header-logo').attr('href', '/courses');
        },

        alterCourseLink: function() {
          $('#menu > li:first-child a').attr('href', '/courses');
        }
    };
}();
<commit_msg>Fixes dropdown bug in alterCourseLink<commit_after>this.mmooc=this.mmooc||{};

this.mmooc.menu = function() {

    function _renderCourseMenu(course, selectedMenuItem, title) {
        var menuItems = [];

        var courseId = course.id;

        menuItems[menuItems.length] = {"title": "Kursforside", url: "/courses/" + courseId};
        menuItems[menuItems.length] = {"title": "Kunngjøringer", url: "/courses/" + courseId + "/announcements"};
        menuItems[menuItems.length] = {"title": "Diskusjoner", url: "/courses/" + courseId + "/discussion_topics"};
        menuItems[menuItems.length] = mmooc.menu.extractBadgesLinkFromPage();

        var subtitle = course.name;
        if (title == null) {
            title = course.name;
            subtitle = "";
        }

        var html = mmooc.util.renderTemplateWithData("coursemenu", {course: course, menuItems: menuItems, selectedMenuItem: selectedMenuItem, title: title, subtitle: subtitle });
        document.getElementById('header').insertAdjacentHTML('afterend', html);
    }

    function createStyleSheet () {
        var style = document.createElement("style");

        // WebKit hack :(
        style.appendChild(document.createTextNode(""));

        document.head.appendChild(style);

        return style.sheet;
    }

    var stylesheet = createStyleSheet();

    return {
        listModuleItems: function() {
            mmooc.api.getCurrentModule(function(module) {
                var courseId = mmooc.api.getCurrentCourseId();
                var html = mmooc.util.renderTemplateWithData("moduleitems", {module: module, courseId: courseId});
                document.getElementById('left-side').insertAdjacentHTML('afterbegin', html);
            });
        },
        showLeftMenu: function() {
            stylesheet.insertRule("body.with-left-side #main { margin-left: 305px !important }", stylesheet.cssRules.length);
            stylesheet.insertRule(".with-left-side #left-side { display: block !important }", stylesheet.cssRules.length);
        },

        showTeacherAdminMenu: function() {
            if (mmooc.util.isTeacherOrAdmin()) {
                this.showLeftMenu();

                $("#section-tabs-header").show();
                $("nav[aria-label='context']").show();
                $("#edit_discussions_settings").show();
                $("#availability_options").show();
                $("#group_category_options").show();
                $("#editor_tabs").show();

                // Done via CSS since content is loaded using AJAX
                stylesheet.insertRule("body.pages .header-bar-outer-container { display: block }", stylesheet.cssRules.length);
                stylesheet.insertRule("#discussion-managebar { display: block }", stylesheet.cssRules.length);
            }

            var roles = mmooc.api.getRoles();
            if (roles != null && roles.indexOf('admin') != -1) {
                // Admin needs original canvas Course dropdown to access site admin settings
                $("#courses_menu_item").show();

                // Admin needs more profile settings
                $(".add_access_token_link").show();
                $("body.profile_settings").find("#content > table, #content > h2, #content > p").show();
            } else {
                document.getElementById('menu').insertAdjacentHTML('afterbegin', '
Kurs
');
            }
        },

        hideRightMenu: function() {
            $("#right-side").hide();
            $("body").removeClass('with-right-side');
        },

        showUserMenu: function() {
            var menu = document.getElementById('menu');
            if (menu !=  null) {
                var html = mmooc.util.renderTemplateWithData("usermenu", {user: mmooc.api.getUser()});
                menu.insertAdjacentHTML('afterend', html);

                var msgBadge = $("#mmooc-unread-messages-count");
                if (mmooc.api.getUnreadMessageSize() === 0) {
                  msgBadge.hide();
                }
                else {
                  msgBadge.html(mmooc.api.getUnreadMessageSize());
                  msgBadge.show();
                }

                mmooc.api.getActivityStreamForUser(function(activities) {
                    var unreadNotifications = 0;
                    for (var i = 0; i < activities.length; i++) {
                        if (mmooc.menu.checkReadStateFor(activities[i])) {
                            unreadNotifications++;
                        }
                    }

                    var badge = $("#mmooc-notification-count");
                    if (unreadNotifications == 0) {
                        badge.hide();
                    } else {
                        badge.html(unreadNotifications);
                        badge.show();
                    }

                    document.getElementById('mmooc-activity-stream').innerHTML = mmooc.util.renderTemplateWithData("activitystream", {activities: activities});

                    var notifications = $("#mmooc-notifications").find("li");
                    if (notifications.size() == 0) {
                        $("#mmooc-notifications").hide();
                    } else {
                        $("#mmooc-notifications").show();
                    }

                    var showAllItems = $("#mmooc-notifications-showall");
                    if (notifications.size() > 10) {
                        notifications.slice(10).addClass("hidden");

                        showAllItems.click(function() {
                            notifications.removeClass("hidden");
                            showAllItems.hide();
                        });
                    } else {
                        showAllItems.hide();
                    }

                });
            }
        },

        showCourseMenu: function(courseId, selectedMenuItem, title) {
            $("body").addClass("with-course-menu");
            mmooc.api.getCourse(courseId, function(course) {
                _renderCourseMenu(course, selectedMenuItem, title);
            });
        },

        showBackButton: function(url, title) {
            var buttonHTML = mmooc.util.renderTemplateWithData("backbutton", {url: url, title: title});
            document.getElementById('content-wrapper').insertAdjacentHTML('afterbegin', buttonHTML);
        },

        showGroupHeader: function() {
            var groupId = mmooc.api.getCurrentGroupId();
            var groupHeaderHTML = mmooc.util.renderTemplateWithData("backbutton", {groupId: groupId});
            document.getElementById('content-wrapper').insertAdjacentHTML('afterbegin', groupHeaderHTML);
        },

        showDiscussionGroupMenu: function() {
            var groupId = mmooc.api.getCurrentGroupId();
            if (groupId != null) {
                mmooc.api.getGroup(groupId, function(group) {
                    // For discussion pages we only want the title to be "<discussion>" instead of "Discussion: <discussion>"
                    var title = mmooc.util.getPageTitleAfterColon();
                    mmooc.menu.showCourseMenu(group.course_id, "Grupper", title);

                    var headerHTML = mmooc.util.renderTemplateWithData("groupdiscussionheader", { group: group});
                    document.getElementById('content-wrapper').insertAdjacentHTML('afterbegin', headerHTML);
                });
            }
        },

        checkReadStateFor: function (activity) {
            return activity.read_state === false;
        },

        extractBadgesLinkFromPage: function () {
            var href = $('li.section:contains("BadgeSafe")').find('a').attr('href');
            return {"title": mmooc.i18n.Badgesafe, url: href};
        },

        injectGroupsPage: function() {
          $('#courses_menu_item').after('
Grupper
');
        },

        alterHomeLink: function() {
          $('#header-logo').attr('href', '/courses');
        },

        alterCourseLink: function() {
          if ($('#menu > li:first-child a').hasClass('menu-item-no-drop')) {
            $('#menu > li:first-child a').attr('href', '/courses');
          }
        }
    };
}();
<|endoftext|>


<commit_before>/* Main menu of the game */
Menu.prototype = Object.create(Phaser.Group.prototype);
Menu.prototype.constructor = Menu;

function Menu () {
	Phaser.Group.call(this, game, null); // Parent constructor.
	var centerX = game.world.centerX;
	var centerY = game.world.centerY;

	/* Add menu button. */
	game.add.button(5, 5, 'wood', function () { showMenu(true); }, this, 0, 0, 1, 0, this);
	game.add.text(125, 15, '=', { // These position values were set by trial and error
		font: '70pt ' +  GLOBAL.FONT,
		stroke: '#000000',
		strokeThickness: 5
	}, this).angle = 90;

	/* For skipping timelines */
	var skipper = null;
	var skipButton = new TextButton('>>', {
		x: 75, y: 5, size: 56, fontSize: 30,
		background: 'wood',
		onClick: function () {
			if (skipper) {
				skipper.totalProgress(1);
			}
		}
	});
	skipButton.visible = false;
	this.add(skipButton);

	EventSystem.subscribe(GLOBAL.EVENT.skippable, function (timeline) {
		skipper = timeline;
		skipButton.visible = !!timeline;
	});

	/* The menu group will be shown when the button is clicked. */
	var menuGroup = game.add.group(this);
	showMenu(false);

	// Create a background behind the menu, traps all mouse events.
	menuGroup.add(new Cover('#056449', 0.7));

	var bmd = game.add.bitmapData(parseInt(game.world.width/3), parseInt(game.world.height/2));
	bmd.ctx.fillStyle = '#b9d384';
	bmd.ctx.roundRect(0, 0, bmd.width, bmd.height, 20).fill();
	game.add.sprite(game.world.width/3, centerY - centerY/3, bmd, null, menuGroup).alpha = 0.7;

	var title = game.add.text(centerX, centerY/2, LANG.TEXT.title, {
		font: '50pt ' +  GLOBAL.FONT,
		fill: '#ffff00',
		stroke: '#000000',
		strokeThickness: 5
	}, menuGroup);
	title.anchor.set(0.5);

	var resume = game.add.text(centerX, centerY, LANG.TEXT.resume, {
		font: '50pt ' +  GLOBAL.FONT,
		fill: '#dd00dd'
	}, menuGroup);
	resume.anchor.set(0.5);
	resume.inputEnabled = true;
	resume.events.onInputDown.add(function () {
		showMenu(false);
	}, this);

	/* Volume slider */
	var volumeSlider = new Slider(
		centerX - bmd.width * 0.2,
		centerY/0.8,
		bmd.width * 0.5,
		50,
		function (value) {
			game.sound.volume = value;
			localStorage.mainVolume = value;
			if (value > 0) {
				muteButton.text = 'V';
				muteButton.muteValue = value;
			} else {
				muteButton.text = 'X';
			}
		},
		game.sound.volume
	);
	menuGroup.add(volumeSlider);

	// TODO: Change graphics to volume object
	var muteButton = new TextButton(game.sound.volume > 0 ? 'V' : 'X', {
		x: centerX - bmd.width * 0.35,
		y: centerY/0.8 - volumeSlider.height/2,
		fontSize: 20,
		size: volumeSlider.height,
		background: 'wood',
		onClick: function () {
			if (this.text === 'X') {
				volumeSlider.value = this.muteValue > 0.1 ? this.muteValue : 1;
			} else {
				volumeSlider.value = 0;
			}
		}
	});
	muteButton.muteValue = volumeSlider.value;
	menuGroup.add(muteButton);

	var quit = game.add.text(centerX, centerY/0.65, LANG.TEXT.quit, {
		font: '50pt ' +  GLOBAL.FONT,
		fill: '#000000'
	}, menuGroup);
	quit.anchor.set(0.5);
	quit.inputEnabled = true;
	quit.events.onInputDown.add(function () {
		game.state.start(GLOBAL.STATE.entry);
	}, this);

	function showMenu (value) {
		menuGroup.visible = value;
	}

	return this;
}<commit_msg>Menu: Now says "MENU" instead of a pause button<commit_after>/* Main menu of the game */
Menu.prototype = Object.create(Phaser.Group.prototype);
Menu.prototype.constructor = Menu;

function Menu () {
	Phaser.Group.call(this, game, null); // Parent constructor.
	var centerX = game.world.centerX;
	var centerY = game.world.centerY;

	/* Add menu button. */
	var button = game.add.button(5, 5, 'wood', function () { showMenu(true); }, this, 0, 0, 1, 0, this);
	var menuSplit = Math.ceil(LANG.TEXT.menu.length/2);
	var menuText = game.add.text(
		button.x + button.width/2,
		button.y + button.height/2 + 7,
		'\n' + LANG.TEXT.menu.substr(0, menuSplit) + '\n' + LANG.TEXT.menu.substr(menuSplit),
		{
			font: '20pt ' +  GLOBAL.FONT,
			stroke: '#000000',
			strokeThickness: 2,
			align: 'center'
		},
		this
	);
	menuText.lineSpacing = -30;
	menuText.anchor.set(0.5);

	/* For skipping timelines */
	var skipper = null;
	var skipButton = new TextButton('>>', {
		x: 75, y: 5, size: 56, fontSize: 30,
		background: 'wood',
		onClick: function () {
			if (skipper) {
				skipper.totalProgress(1);
			}
		}
	});
	skipButton.visible = false;
	this.add(skipButton);

	EventSystem.subscribe(GLOBAL.EVENT.skippable, function (timeline) {
		skipper = timeline;
		skipButton.visible = !!timeline;
	});

	/* The menu group will be shown when the button is clicked. */
	var menuGroup = game.add.group(this);
	showMenu(false);

	// Create a background behind the menu, traps all mouse events.
	menuGroup.add(new Cover('#056449', 0.7));

	var bmd = game.add.bitmapData(parseInt(game.world.width/3), parseInt(game.world.height/2));
	bmd.ctx.fillStyle = '#b9d384';
	bmd.ctx.roundRect(0, 0, bmd.width, bmd.height, 20).fill();
	game.add.sprite(game.world.width/3, centerY - centerY/3, bmd, null, menuGroup).alpha = 0.7;

	var title = game.add.text(centerX, centerY/2, LANG.TEXT.title, {
		font: '50pt ' +  GLOBAL.FONT,
		fill: '#ffff00',
		stroke: '#000000',
		strokeThickness: 5
	}, menuGroup);
	title.anchor.set(0.5);

	var resume = game.add.text(centerX, centerY, LANG.TEXT.resume, {
		font: '50pt ' +  GLOBAL.FONT,
		fill: '#dd00dd'
	}, menuGroup);
	resume.anchor.set(0.5);
	resume.inputEnabled = true;
	resume.events.onInputDown.add(function () {
		showMenu(false);
	}, this);

	/* Volume slider */
	var volumeSlider = new Slider(
		centerX - bmd.width * 0.2,
		centerY/0.8,
		bmd.width * 0.5,
		50,
		function (value) {
			game.sound.volume = value;
			localStorage.mainVolume = value;
			if (value > 0) {
				muteButton.text = 'V';
				muteButton.muteValue = value;
			} else {
				muteButton.text = 'X';
			}
		},
		game.sound.volume
	);
	menuGroup.add(volumeSlider);

	// TODO: Change graphics to volume object
	var muteButton = new TextButton(game.sound.volume > 0 ? 'V' : 'X', {
		x: centerX - bmd.width * 0.35,
		y: centerY/0.8 - volumeSlider.height/2,
		fontSize: 20,
		size: volumeSlider.height,
		background: 'wood',
		onClick: function () {
			if (this.text === 'X') {
				volumeSlider.value = this.muteValue > 0.1 ? this.muteValue : 1;
			} else {
				volumeSlider.value = 0;
			}
		}
	});
	muteButton.muteValue = volumeSlider.value;
	menuGroup.add(muteButton);

	var quit = game.add.text(centerX, centerY/0.65, LANG.TEXT.quit, {
		font: '50pt ' +  GLOBAL.FONT,
		fill: '#000000'
	}, menuGroup);
	quit.anchor.set(0.5);
	quit.inputEnabled = true;
	quit.events.onInputDown.add(function () {
		game.state.start(GLOBAL.STATE.entry);
	}, this);

	function showMenu (value) {
		menuGroup.visible = value;
	}

	return this;
}<|endoftext|>


<commit_before>import { h, Component } from 'preact'
import { humanDuration } from '../utils/index'
import PrimaryButton from './buttons/PrimaryButton'
import styled from 'styled-components'
import v from 'css/variables'

const Wrapper = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  padding: 16px 12px;
  background-color: white;
  z-index: 9999;
  box-shadow: 0 1px 1px 0 rgba(0,0,0,0.1);
`

const Strong = styled.div`
  font-size: 20px;
  line-height: 1.4em;
  font-weight: bolder;
  font-family: ${v.fontHeader};
`
const ButtonHint = styled.div`
  font-size: 12px;
  color: #555;
  text-align: center;
`

class Header extends Component {
  render ({idleTime, idleTabsID, onEnterButton, onLeaveButton, onClickButton}) {
    return (
      <Wrapper>
        <PrimaryButton
          onMouseEnter={onEnterButton}
          onMouseLeave={onLeaveButton}
          onClick={onClickButton}
          disabled={idleTabsID.length === 0}
        >
          <ButtonHint>close tabs idle for</ButtonHint>
          {humanDuration(idleTime)}
        </PrimaryButton>
      </Wrapper>
    )
  }
}

export default Header
<commit_msg>change mouse event from mouseEnter to mouseOver<commit_after>import { h, Component } from 'preact'
import { humanDuration } from '../utils/index'
import PrimaryButton from './buttons/PrimaryButton'
import styled from 'styled-components'
import v from 'css/variables'

const Wrapper = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  padding: 16px 12px;
  background-color: white;
  z-index: 9999;
  box-shadow: 0 1px 1px 0 rgba(0,0,0,0.1);
`

const Strong = styled.div`
  font-size: 20px;
  line-height: 1.4em;
  font-weight: bolder;
  font-family: ${v.fontHeader};
`
const ButtonHint = styled.div`
  font-size: 12px;
  color: #555;
  text-align: center;
`

class Header extends Component {
  render ({idleTime, idleTabsID, onEnterButton, onLeaveButton, onClickButton}) {
    return (
      <Wrapper>
        <PrimaryButton
          onMouseOver={onEnterButton}
          onMouseLeave={onLeaveButton}
          onClick={onClickButton}
          disabled={idleTabsID.length === 0}
        >
          <ButtonHint>close tabs idle for</ButtonHint>
          {humanDuration(idleTime)}
        </PrimaryButton>
      </Wrapper>
    )
  }
}

export default Header
<|endoftext|>


<commit_before>
import React from 'react'
import PropTypes from 'prop-types'
import {Motion, spring} from 'react-motion'

const PADDING_TOP_WITH_LABEL = -14
const PADDING_LEFT = 16
const LIST_ITEM_HEIGHT = 48

// list length
const MAX_LIST_LENGTH = 5

/**
 * Select component
 */
export default class Select extends React.Component {
  static propTypes = {
    disabled: PropTypes.bool,
    label: PropTypes.string,
    options: PropTypes.array,
    placeholder: PropTypes.string,
    onChange: PropTypes.func.isRequired,
    name: PropTypes.string
  }

  static defaultProps = {
    options: [
      {value: 'one', label: 'one'},
      {value: 'two', label: 'two'},
      {value: 'three', label: 'three'}
    ],
    placeholder: 'Placeholder',
    value: ''
  }

  state = {
    open: false
  }

  /**
   * Show list
   */
  open = () => {
    this.setState({
      open: true
    })
    document.addEventListener('click', this.close)
  }

  /**
   * Hide list
   */
  close = () => {
    this.setState({
      open: false
    })
    document.removeEventListener('click', this.close)
  }

  /**
   * Check if Select component is inside table.
   */
  findTableTag = (node) => {
    while (node.parentNode) {
      node = node.parentNode
      if (node.tagName === 'TABLE') {
        return true
      }
    }
    return false
  }

  /**
   * Make dropdown list as wide as parent placeholder including caret
   */
  componentDidMount () {
    const selectRect = this.ref.getBoundingClientRect()

    const isInsideTable = this.findTableTag(this.ref)

    this.setState({
      isInsideTable,
      width: selectRect.width
    })

    window.addEventListener('resize', this.resize)
  }

  componentWillUnmount () {
    window.removeEventListener('resize', this.resize)
  }

  resize = () => {
    const {width} = this.ref.getBoundingClientRect()
    this.setState({
      width
    })
  }

  onChange = (item) => this.props.onChange({
    target: {
      name: this.props.name,
      ...item
    }
  })

  /**
   * Component should not update when it is open. In that case it should
   * only update when open state changes.
   * It should not update itself when list is open and value comes in again.
   * This would causes scrolling and that would irritate the user.
   */
  shouldComponentUpdate (nextProps, nextState) {
    if (this.state.open && (nextState.open === this.state.open)) {
      return false
    }
    return true
  }

  render () {
    const {label, disabled, options, value, name} = this.props
    const {open} = this.state
    const selectedIndex = options.findIndex(option => option.value === value)
    const empty = selectedIndex === -1
    const text = empty ? this.props.placeholder : this.props.options[selectedIndex].label

    return (

 { this.ref = c }}>
        {
          label &&
          {this.props.label}
        }
        <button name={name} className='mdc-Select-body' onClick={this.open} disabled={disabled}>

            {text}

        </button>
        <Motion style={{
          opacity: spring(open ? 1 : 0)
        }}>
          {style =>
            open &&
            <List
              style={{
                opacity: style.opacity
              }}
              hasLabel={!!this.props.label}
              options={this.props.options}
              selectedIndex={selectedIndex}
              onClick={this.onChange}
              width={this.state.width}
              isInsideTable={this.state.isInsideTable}
            />
          }
        </Motion>

    )
  }
}

/**
 * List component
 */
class List extends React.Component {
  static propTypes = {
    hasLabel: PropTypes.bool,
    options: PropTypes.array.isRequired,
    isInsideTable: PropTypes.bool,
    selectedIndex: PropTypes.number.isRequired,
    onClick: PropTypes.func.isRequired,
    width: PropTypes.number
  }

  /**
   * Scroll to correct list item
   */
  componentDidMount () {
    const index = this.props.selectedIndex

    // create boolean helper variables
    const scrollable = this.props.options.length > MAX_LIST_LENGTH
    const indexWithinFirstTwoItems = index < 2

    if (scrollable && !indexWithinFirstTwoItems) {
      const scrollTop = (LIST_ITEM_HEIGHT * (index - 2))
      this.ref.scrollTop = scrollTop
    }
  }

  /**
   * Handle click on list item
   */
  onClick = (event) => {
    const index = parseInt(event.target.getAttribute('data-id'))
    event.preventDefault()
    this.props.onClick(this.props.options[index])
  }

  /**
   * Render list component
   */
  render () {
    // CSS space
    let PADDING_TOP = 10
    if (this.props.isInsideTable) {
      // when select component is inside a table cell we have to remove
      // border bottom, padding top and padding bottom.
      // this changes the location of the text.
      // that leads to the list menu / overlay no being directly on top of the text.
      // we therefore have to modify the padding top to fix the overlay position.
      PADDING_TOP = 17
    }

    const {options, selectedIndex} = this.props

    // handle list absolute position top
    const paddingTop = this.props.hasLabel ? PADDING_TOP_WITH_LABEL : PADDING_TOP

    let top
    if (selectedIndex === -1) {
      // set position to first element, i.e. selectedIndex = 0
      top = -1 * paddingTop
    } else {
      top = -1 * (paddingTop + (LIST_ITEM_HEIGHT * selectedIndex))
    }

    // handle scrollable lists with more than 5 list items
    if (options.length > MAX_LIST_LENGTH) {
      if (selectedIndex >= 2 && selectedIndex <= options.length - 3) {
        // handle "center" items => always set to third position
        top = -1 * (paddingTop + (LIST_ITEM_HEIGHT * 2))
      } else if (selectedIndex === options.length - 2) {
        // handle second last item
        top = -1 * (paddingTop + (LIST_ITEM_HEIGHT * 3))
      } else if (selectedIndex === options.length - 1) {
        // handle last item
        top = -1 * (paddingTop + (LIST_ITEM_HEIGHT * 4))
      }
    }

    let width = this.props.width + (2 * PADDING_LEFT)
    let left = this.props.isInsideTable ? -17 : -16
    let padding = 16

    // check if select overlay is wider than window which would cause horizontal overflow
    // if so decrease padding left and right from 16px to 8px
    // adjust absolute position left and inner link padding accordingly
    if (width > document.body.clientWidth) {
      width = this.props.width + PADDING_LEFT
      left = -8
      padding = 8
    }

    const style = {top, width, left}

    return (

 { this.ref = c }}
        className='mdc-Select-list'
        style={Object.assign(style, this.props.style)}
      >
        {options.map((item, i) =>
          <li key={i}
            className='mdc-Select-listItem'
            data-id={i}
            onClick={this.onClick}
            style={{padding: `0 ${padding}px`}}
          >
            {item.label}

        )}

    )
  }
}
<commit_msg>select: set button type to button to fix weird behavior inside form (#324)<commit_after>
import React from 'react'
import PropTypes from 'prop-types'
import {Motion, spring} from 'react-motion'

const PADDING_TOP_WITH_LABEL = -14
const PADDING_LEFT = 16
const LIST_ITEM_HEIGHT = 48

// list length
const MAX_LIST_LENGTH = 5

/**
 * Select component
 */
export default class Select extends React.Component {
  static propTypes = {
    disabled: PropTypes.bool,
    label: PropTypes.string,
    options: PropTypes.array,
    placeholder: PropTypes.string,
    onChange: PropTypes.func.isRequired,
    name: PropTypes.string
  }

  static defaultProps = {
    options: [
      {value: 'one', label: 'one'},
      {value: 'two', label: 'two'},
      {value: 'three', label: 'three'}
    ],
    placeholder: 'Placeholder',
    value: ''
  }

  state = {
    open: false
  }

  /**
   * Show list
   */
  open = () => {
    this.setState({
      open: true
    })
    document.addEventListener('click', this.close)
  }

  /**
   * Hide list
   */
  close = () => {
    this.setState({
      open: false
    })
    document.removeEventListener('click', this.close)
  }

  /**
   * Check if Select component is inside table.
   */
  findTableTag = (node) => {
    while (node.parentNode) {
      node = node.parentNode
      if (node.tagName === 'TABLE') {
        return true
      }
    }
    return false
  }

  /**
   * Make dropdown list as wide as parent placeholder including caret
   */
  componentDidMount () {
    const selectRect = this.ref.getBoundingClientRect()

    const isInsideTable = this.findTableTag(this.ref)

    this.setState({
      isInsideTable,
      width: selectRect.width
    })

    window.addEventListener('resize', this.resize)
  }

  componentWillUnmount () {
    window.removeEventListener('resize', this.resize)
  }

  resize = () => {
    const {width} = this.ref.getBoundingClientRect()
    this.setState({
      width
    })
  }

  onChange = (item) => this.props.onChange({
    target: {
      name: this.props.name,
      ...item
    }
  })

  /**
   * Component should not update when it is open. In that case it should
   * only update when open state changes.
   * It should not update itself when list is open and value comes in again.
   * This would causes scrolling and that would irritate the user.
   */
  shouldComponentUpdate (nextProps, nextState) {
    if (this.state.open && (nextState.open === this.state.open)) {
      return false
    }
    return true
  }

  render () {
    const {label, disabled, options, value, name} = this.props
    const {open} = this.state
    const selectedIndex = options.findIndex(option => option.value === value)
    const empty = selectedIndex === -1
    const text = empty ? this.props.placeholder : this.props.options[selectedIndex].label

    return (

 { this.ref = c }}>
        {
          label &&
          {this.props.label}
        }
        <button type='button' name={name} className='mdc-Select-body' onClick={this.open} disabled={disabled}>

            {text}

        </button>
        <Motion style={{
          opacity: spring(open ? 1 : 0)
        }}>
          {style =>
            open &&
            <List
              style={{
                opacity: style.opacity
              }}
              hasLabel={!!this.props.label}
              options={this.props.options}
              selectedIndex={selectedIndex}
              onClick={this.onChange}
              width={this.state.width}
              isInsideTable={this.state.isInsideTable}
            />
          }
        </Motion>

    )
  }
}

/**
 * List component
 */
class List extends React.Component {
  static propTypes = {
    hasLabel: PropTypes.bool,
    options: PropTypes.array.isRequired,
    isInsideTable: PropTypes.bool,
    selectedIndex: PropTypes.number.isRequired,
    onClick: PropTypes.func.isRequired,
    width: PropTypes.number
  }

  /**
   * Scroll to correct list item
   */
  componentDidMount () {
    const index = this.props.selectedIndex

    // create boolean helper variables
    const scrollable = this.props.options.length > MAX_LIST_LENGTH
    const indexWithinFirstTwoItems = index < 2

    if (scrollable && !indexWithinFirstTwoItems) {
      const scrollTop = (LIST_ITEM_HEIGHT * (index - 2))
      this.ref.scrollTop = scrollTop
    }
  }

  /**
   * Handle click on list item
   */
  onClick = (event) => {
    const index = parseInt(event.target.getAttribute('data-id'))
    event.preventDefault()
    this.props.onClick(this.props.options[index])
  }

  /**
   * Render list component
   */
  render () {
    // CSS space
    let PADDING_TOP = 10
    if (this.props.isInsideTable) {
      // when select component is inside a table cell we have to remove
      // border bottom, padding top and padding bottom.
      // this changes the location of the text.
      // that leads to the list menu / overlay no being directly on top of the text.
      // we therefore have to modify the padding top to fix the overlay position.
      PADDING_TOP = 17
    }

    const {options, selectedIndex} = this.props

    // handle list absolute position top
    const paddingTop = this.props.hasLabel ? PADDING_TOP_WITH_LABEL : PADDING_TOP

    let top
    if (selectedIndex === -1) {
      // set position to first element, i.e. selectedIndex = 0
      top = -1 * paddingTop
    } else {
      top = -1 * (paddingTop + (LIST_ITEM_HEIGHT * selectedIndex))
    }

    // handle scrollable lists with more than 5 list items
    if (options.length > MAX_LIST_LENGTH) {
      if (selectedIndex >= 2 && selectedIndex <= options.length - 3) {
        // handle "center" items => always set to third position
        top = -1 * (paddingTop + (LIST_ITEM_HEIGHT * 2))
      } else if (selectedIndex === options.length - 2) {
        // handle second last item
        top = -1 * (paddingTop + (LIST_ITEM_HEIGHT * 3))
      } else if (selectedIndex === options.length - 1) {
        // handle last item
        top = -1 * (paddingTop + (LIST_ITEM_HEIGHT * 4))
      }
    }

    let width = this.props.width + (2 * PADDING_LEFT)
    let left = this.props.isInsideTable ? -17 : -16
    let padding = 16

    // check if select overlay is wider than window which would cause horizontal overflow
    // if so decrease padding left and right from 16px to 8px
    // adjust absolute position left and inner link padding accordingly
    if (width > document.body.clientWidth) {
      width = this.props.width + PADDING_LEFT
      left = -8
      padding = 8
    }

    const style = {top, width, left}

    return (

 { this.ref = c }}
        className='mdc-Select-list'
        style={Object.assign(style, this.props.style)}
      >
        {options.map((item, i) =>
          <li key={i}
            className='mdc-Select-listItem'
            data-id={i}
            onClick={this.onClick}
            style={{padding: `0 ${padding}px`}}
          >
            {item.label}

        )}

    )
  }
}
<|endoftext|>


<commit_before>/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var PropertyValueInc = require('./PropertyValueInc');

/**
 * Takes an array of Game Objects, or any objects that have public `x` and `y` properties,
 * and then adds the given value to each of them.
 *
 * The optional `stepX` and `stepY` properties are applied incrementally, multiplied by each item in the array.
 *
 * To use this with a Group: `IncXY(group.getChildren(), value, step)`
 *
 * @function Phaser.Actions.IncXY
 * @since 3.0.0
 *
 * @param {array|Phaser.GameObjects.GameObject[]} items - The array of items to be updated by this action.
 * @param {number} x - The amount to be added to the `x` property.
 * @param {number} [y] - The amount to be added to the `y` property. If `undefined` or `null` it uses the `x` value.
 * @param {number} [stepX=0] - This is added to the `x` amount, multiplied by the iteration counter.
 * @param {number} [stepY=0] - This is added to the `y` amount, multiplied by the iteration counter.
 * @param {integer} [index=0] - An optional offset to start searching from within the items array.
 * @param {integer} [direction=1] - The direction to iterate through the array. 1 is from beginning to end, -1 from end to beginning.
 *
 * @return {array} The array of objects that were passed to this Action.
 */
var IncXY = function (items, x, y, stepX, stepY, index, direction)
{
    if (y === undefined || y === null) { y = x; }

    PropertyValueInc(items, 'x', x, stepX, index, direction);

    return PropertyValueInc(items, 'y', y, stepY, index, direction);
};

module.exports = IncXY;
<commit_msg>Fixed docs<commit_after>/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var PropertyValueInc = require('./PropertyValueInc');

/**
 * Takes an array of Game Objects, or any objects that have public `x` and `y` properties,
 * and then adds the given value to each of them.
 *
 * The optional `stepX` and `stepY` properties are applied incrementally, multiplied by each item in the array.
 *
 * To use this with a Group: `IncXY(group.getChildren(), x, y, stepX, stepY)`
 *
 * @function Phaser.Actions.IncXY
 * @since 3.0.0
 *
 * @param {array|Phaser.GameObjects.GameObject[]} items - The array of items to be updated by this action.
 * @param {number} x - The amount to be added to the `x` property.
 * @param {number} [y] - The amount to be added to the `y` property. If `undefined` or `null` it uses the `x` value.
 * @param {number} [stepX=0] - This is added to the `x` amount, multiplied by the iteration counter.
 * @param {number} [stepY=0] - This is added to the `y` amount, multiplied by the iteration counter.
 * @param {integer} [index=0] - An optional offset to start searching from within the items array.
 * @param {integer} [direction=1] - The direction to iterate through the array. 1 is from beginning to end, -1 from end to beginning.
 *
 * @return {array} The array of objects that were passed to this Action.
 */
var IncXY = function (items, x, y, stepX, stepY, index, direction)
{
    if (y === undefined || y === null) { y = x; }

    PropertyValueInc(items, 'x', x, stepX, index, direction);

    return PropertyValueInc(items, 'y', y, stepY, index, direction);
};

module.exports = IncXY;
<|endoftext|>