<commit_before>var Glosser = Glosser || {};

Glosser.downloadPrecedenceRules = function(corpusname, callback){
  $.ajax({
    type : 'GET',
    url : "https://ilanguage.iriscouch.com/" + corpusname
        + "/_design/get_precedence_rules_from_morphemes/_view/precedence_rules?group=true",
    success : function(rules) {
      // Parse the rules from JSON into an object
      rules = JSON.parse(rules);

      // Reduce the rules such that rules which are found in multiple source
      // words are only used/included once.
      var reducedRules = _.chain(rules.rows).groupBy(function(rule) {
        return rule.key.x + "-" + rule.key.y;
      }).value();

      // Save the reduced precedence rules in localStorage
      localStorage.setItem("precendenceRules", JSON.stringify(reducedRules));
      if(typeof callback == "function"){
        callback();
      }
    },
    error : function(e) {
      console.log("error getting precedence rules:", e);
    },
    dataType : ""
  });
}
/**
 * Takes in an utterance line and, based on our current set of precendence
 * rules, guesses what the morpheme line would be. The algorithm is
 * very conservative.
 *
 * @param {String} unparsedUtterance The raw utterance line.
 *
 * @return {String} The guessed morphemes line.
 */
Glosser.morphemefinder = function(unparsedUtterance) {
  var potentialParse = '';

  // Get the precedence rules from localStorage
  var rules = localStorage.getItem("precendenceRules");

  if (rules) {
    // Parse the rules from JSON into an object
    rules = JSON.parse(rules);

    // Divide the utterance line into words
    var unparsedWords = unparsedUtterance.trim().split(/ +/);

    var parsedWords = [];
    for (var word in unparsedWords) {
      // Add the start/end-of-word character to the word
      unparsedWords[word] = "@" + unparsedWords[word] + "@";

      // Find the rules which match in local precedence
      var matchedRules = [];
      for (var r in rules) {
        if (unparsedWords[word].indexOf(r.replace(/-/, "")) >= 0) {
          matchedRules.push({
            r : rules[r]
          })
        }
      }

      // Attempt to find the longest template which the matching rules can
      // generate from start to end
      var prefixtemplate = [];
      prefixtemplate.push("@");
      for (var i = 0; i < 10; i++) {
        if (prefixtemplate[i] == undefined) {
          break;
        }
        for (var j in matchedRules) {
          if (prefixtemplate[i] == matchedRules[j].r[0].key.x) {
            if (prefixtemplate[i + 1]) { // ambiguity (two potential following
                                          // morphemes)
              prefixtemplate.pop();
              break;
            } else {
              prefixtemplate[i + 1] = matchedRules[j].r[0].key.y;
            }
          }
        }
      }

      // If the prefix template hit ambiguity in the middle, try from the suffix
      // in until it hits ambiguity
      var suffixtemplate = [];
      if (prefixtemplate[prefixtemplate.length - 1] != "@" || prefixtemplate.length == 1) {
        // Suffix:
        suffixtemplate.push("@")
        for (var i = 0; i < 10; i++) {
          if (suffixtemplate[i] == undefined) {
            break;
          }
          for (var j in matchedRules) {
            if (suffixtemplate[i] == matchedRules[j].r[0].key.y) {
              if (suffixtemplate[i + 1]) { // ambiguity (two potential
                                            // following morphemes)
                suffixtemplate.pop();
                break;
              } else {
                suffixtemplate[i + 1] = matchedRules[j].r[0].key.x;
              }
            }
          }
        }
      }

      // Combine prefix and suffix templates into one regular expression which
      // can be tested against the word to find a potential parse.
      // Regular expressions will look something like
      //    (@)(.*)(hall)(.*)(o)(.*)(wa)(.*)(n)(.*)(@)
      var template = [];
      template = prefixtemplate.concat(suffixtemplate.reverse())
      for (var slot in template) {
        template[slot] = "(" + template[slot] + ")";
      }
      var regex = new RegExp(template.join("(.*)"), "");

      // Use the regular expression to find a guessed morphemes line
      potentialParse = unparsedWords[word]
          .replace(regex, "$1-$2-$3-$4-$5-$6-$7-$8-$9") // Use backreferences to parse into morphemes
          .replace(/\$[0-9]/g, "")// Remove any backreferences that weren't used
          .replace(/@/g, "")      // Remove the start/end-of-line symbol
          .replace(/--+/g, "-")   // Ensure that there is only ever one "-" in a row
          .replace(/^-/, "")      // Remove "-" at the start of the word
          .replace(/-$/, "");     // Remove "-" at the end of the word
      Utils.debug("Potential parse of " + unparsedWords[word].replace(/@/g, "")
          + " is " + potentialParse);

      parsedWords.push(potentialParse);
    }
  }

  return parsedWords.join(" ");
}
Glosser.toastedUserToSync = false;
Glosser.toastedUserToImport = 0;
Glosser.glossFinder = function(morphemesLine){
  //Guess a gloss
  var morphemeGroup = morphemesLine.split(/ +/);
  var glossGroups = [];
  if(! window.app.get("corpus")){
    return "";
  }
  if(! window.app.get("corpus").lexicon.get("lexiconNodes")){
    var corpusSize = app.get("corpus").get("dataLists").models[app.get("corpus").get("dataLists").models.length-1].get("datumIds").length;
    if(corpusSize > 30 && !Glosser.toastedUserToSync){
      Glosser.toastedUserToSync = true;
      alert("You probably have enough data to train an autoglosser for your corpus.\n\nIf you sync your data with the team server then this will automatically run the auto glosser.");
    }else{
      Glosser.toastedUserToImport ++;
      if(Glosser.toastedUserToImport % 10 == 0){
        alert("You have "+corpusSize+" datum, if you have around 30 datum, then you have enough data to train an autoglosser for your corpus.");
      }
    }
    return "";
  }
  var lexiconNodes = window.app.get("corpus").lexicon.get("lexiconNodes");
  for (var group in morphemeGroup) {
    var morphemes = morphemeGroup[group].split("-");
    var glosses = [];
    for (var m in morphemes) {
      // Take the first gloss for this morpheme
      var matchingNode = _.max(lexiconNodes.where({morpheme: morphemes[m]}), function(node) { return node.get("value"); });
//      console.log(matchingNode);
      var gloss = "?";   // If there's no matching gloss, use question marks
      if (matchingNode) {
        gloss = matchingNode.get("gloss");
      }
      glosses.push(gloss);
    }

    glossGroups.push(glosses.join("-"));
  }

  // Replace the gloss line with the guessed glosses
  return glossGroups.join(" ");
}
/**
 * Takes as a parameters an array of rules which came from CouchDB precedence rule query.
 * Example Rule: {"key":{"x":"@","relation":"preceeds","y":"aqtu","context":"aqtu-nay-wa-n"},"value":2}
 */
Glosser.generateForceDirectedRulesJsonForD3 = function(rules) {

  /*
   * Cycle through the precedence rules, convert them into graph edges with the morpheme index in the morpheme array as the source/target values
   */
  morphemeLinks = [];
  morphemes = [];
  for ( var i in rules) {
    var xpos = morphemes.indexOf(rules[i].key.x);
    if (xpos < 0) {
      morphemes.push(rules[i].key.x);
      xpos = morphemes.length - 1;
    }
    var ypos = morphemes.indexOf(rules[i].key.y);
    if (ypos < 0) {
      morphemes.push(rules[i].key.y);
      ypos = morphemes.length - 1;
    }
    if (rules[i].key.y != "@") {
      morphemeLinks.push({
        source : xpos,
        target : ypos,
        value : 1 //TODO use the context counting to get a weight measure
      });
    }
  }

  /*
   * Build the morphemes into nodes and color them by their morpheme length, could be a good measure of outliers
   */
  var morphemenodes = [];
  for (m in morphemes) {
    morphemenodes.push({
      name : morphemes[m],
      group : morphemes[m].length
    });
  }

  /*
   * Create the JSON required by D3
   */
  var rulesGraph = {};
  rulesGraph.links = morphemeLinks;
  rulesGraph.nodes = morphemenodes;

  return rulesGraph;
}

/*
 * Some sample D3 from the force-html.html example
 *
 */
Glosser.visualizeMorphemesAsForceDirectedGraph = function(){

  var width = 960,
      height = 500,
      radius = 6,
      fill = d3.scale.category20();

  var force = d3.layout.force()
      .charge(-120)
      .linkDistance(30)
      .size([width, height]);

  var vis = d3.select("body").append("div")
      .style("width", width + "px")
      .style("height", height + "px");

  var tooltip = null;

  d3.json("rules.json", function(json) {
    var link = vis.selectAll("div.link")
        .data(json.links)
      .enter().append("div")
        .attr("class", "link");

    var node = vis.selectAll("div.node")
        .data(json.nodes)
      .enter().append("div")
        .attr("class", "node")
        .style("background", function(d) { return fill(d.group); })
        .style("border-color", function(d) { return d3.rgb(fill(d.group)).darker(); })
      .on("mouseover", function(d) {
        tooltip = d3.select("body")
          .append("div")
          .style("position", "absolute")
          .style("z-index", "10")
          .style("visibility", "visible")
          .text(d.name)
        })
      .on("mouseout", function() {
          tooltip.style("visibility", "hidden");
        })
      .call(force.drag);

    force
        .nodes(json.nodes)
        .links(json.links)
        .on("tick", tick)
        .start();

    function tick() {
      node.style("left", function(d) { return (d.x = Math.max(radius, Math.min(width - radius, d.x))) + "px"; })
          .style("top", function(d) { return (d.y = Math.max(radius, Math.min(height - radius, d.y))) + "px"; });

      link.style("left", function(d) { return d.source.x + "px"; })
          .style("top", function(d) { return d.source.y + "px"; })
          .style("width", length)
          .style("-webkit-transform", transform)
          .style("-moz-transform", transform)
          .style("-ms-transform", transform)
          .style("-o-transform", transform)
          .style("transform", transform);
    }

    function transform(d) {
      return "rotate(" + Math.atan2(d.target.y - d.source.y, d.target.x - d.source.x) * 180 / Math.PI + "deg)";
    }

    function length(d) {
      var dx = d.target.x - d.source.x,
          dy = d.target.y - d.source.y;
      return Math.sqrt(dx * dx + dy * dy) + "px";
    }
  });

}<commit_msg>tested the new glosser functionality, it seems to work pretty well, and its kind of fun!<commit_after>var Glosser = Glosser || {};
Glosser.currentCorpusName = "";
Glosser.downloadPrecedenceRules = function(corpusname, callback){
  $.ajax({
    type : 'GET',
    url : "https://ilanguage.iriscouch.com/" + corpusname
        + "/_design/get_precedence_rules_from_morphemes/_view/precedence_rules?group=true",
    success : function(rules) {
      // Parse the rules from JSON into an object
      rules = JSON.parse(rules);

      // Reduce the rules such that rules which are found in multiple source
      // words are only used/included once.
      var reducedRules = _.chain(rules.rows).groupBy(function(rule) {
        return rule.key.x + "-" + rule.key.y;
      }).value();

      // Save the reduced precedence rules in localStorage
      localStorage.setItem(corpusname+"PrecendenceRules", JSON.stringify(reducedRules));
      Glosser.currentCorpusName = corpusname;
      if(typeof callback == "function"){
        callback();
      }
    },
    error : function(e) {
      console.log("error getting precedence rules:", e);
    },
    dataType : ""
  });
}
/**
 * Takes in an utterance line and, based on our current set of precendence
 * rules, guesses what the morpheme line would be. The algorithm is
 * very conservative.
 *
 * @param {String} unparsedUtterance The raw utterance line.
 *
 * @return {String} The guessed morphemes line.
 */
Glosser.morphemefinder = function(unparsedUtterance) {
  var potentialParse = '';

  // Get the precedence rules from localStorage
  var rules = localStorage.getItem(Glosser.currentCorpusName+"precendenceRules");

  if (rules) {
    // Parse the rules from JSON into an object
    rules = JSON.parse(rules);

    // Divide the utterance line into words
    var unparsedWords = unparsedUtterance.trim().split(/ +/);

    var parsedWords = [];
    for (var word in unparsedWords) {
      // Add the start/end-of-word character to the word
      unparsedWords[word] = "@" + unparsedWords[word] + "@";

      // Find the rules which match in local precedence
      var matchedRules = [];
      for (var r in rules) {
        if (unparsedWords[word].indexOf(r.replace(/-/, "")) >= 0) {
          matchedRules.push({
            r : rules[r]
          })
        }
      }

      // Attempt to find the longest template which the matching rules can
      // generate from start to end
      var prefixtemplate = [];
      prefixtemplate.push("@");
      for (var i = 0; i < 10; i++) {
        if (prefixtemplate[i] == undefined) {
          break;
        }
        for (var j in matchedRules) {
          if (prefixtemplate[i] == matchedRules[j].r[0].key.x) {
            if (prefixtemplate[i + 1]) { // ambiguity (two potential following
                                          // morphemes)
              prefixtemplate.pop();
              break;
            } else {
              prefixtemplate[i + 1] = matchedRules[j].r[0].key.y;
            }
          }
        }
      }

      // If the prefix template hit ambiguity in the middle, try from the suffix
      // in until it hits ambiguity
      var suffixtemplate = [];
      if (prefixtemplate[prefixtemplate.length - 1] != "@" || prefixtemplate.length == 1) {
        // Suffix:
        suffixtemplate.push("@")
        for (var i = 0; i < 10; i++) {
          if (suffixtemplate[i] == undefined) {
            break;
          }
          for (var j in matchedRules) {
            if (suffixtemplate[i] == matchedRules[j].r[0].key.y) {
              if (suffixtemplate[i + 1]) { // ambiguity (two potential
                                            // following morphemes)
                suffixtemplate.pop();
                break;
              } else {
                suffixtemplate[i + 1] = matchedRules[j].r[0].key.x;
              }
            }
          }
        }
      }

      // Combine prefix and suffix templates into one regular expression which
      // can be tested against the word to find a potential parse.
      // Regular expressions will look something like
      //    (@)(.*)(hall)(.*)(o)(.*)(wa)(.*)(n)(.*)(@)
      var template = [];
      template = prefixtemplate.concat(suffixtemplate.reverse())
      for (var slot in template) {
        template[slot] = "(" + template[slot] + ")";
      }
      var regex = new RegExp(template.join("(.*)"), "");

      // Use the regular expression to find a guessed morphemes line
      potentialParse = unparsedWords[word]
          .replace(regex, "$1-$2-$3-$4-$5-$6-$7-$8-$9") // Use backreferences to parse into morphemes
          .replace(/\$[0-9]/g, "")// Remove any backreferences that weren't used
          .replace(/@/g, "")      // Remove the start/end-of-line symbol
          .replace(/--+/g, "-")   // Ensure that there is only ever one "-" in a row
          .replace(/^-/, "")      // Remove "-" at the start of the word
          .replace(/-$/, "");     // Remove "-" at the end of the word
      Utils.debug("Potential parse of " + unparsedWords[word].replace(/@/g, "")
          + " is " + potentialParse);

      parsedWords.push(potentialParse);
    }
  }

  return parsedWords.join(" ");
}
Glosser.toastedUserToSync = false;
Glosser.toastedUserToImport = 0;
Glosser.glossFinder = function(morphemesLine){
  //Guess a gloss
  var morphemeGroup = morphemesLine.split(/ +/);
  var glossGroups = [];
  if(! window.app.get("corpus")){
    return "";
  }
  if(! window.app.get("corpus").lexicon.get("lexiconNodes")){
    var corpusSize = app.get("corpus").get("dataLists").models[app.get("corpus").get("dataLists").models.length-1].get("datumIds").length;
    if(corpusSize > 30 && !Glosser.toastedUserToSync){
      Glosser.toastedUserToSync = true;
      alert("You probably have enough data to train an autoglosser for your corpus.\n\nIf you sync your data with the team server then editing the morphemes will automatically run the auto glosser.");
    }else{
      Glosser.toastedUserToImport ++;
      if(Glosser.toastedUserToImport % 10 == 1){
        alert("You have roughly "+corpusSize+" datum saved in your pouch, if you have around 30 datum, then you have enough data to train an autoglosser for your corpus.");
      }
    }
    return "";
  }
  var lexiconNodes = window.app.get("corpus").lexicon.get("lexiconNodes");
  for (var group in morphemeGroup) {
    var morphemes = morphemeGroup[group].split("-");
    var glosses = [];
    for (var m in morphemes) {
      // Take the first gloss for this morpheme
      var matchingNode = _.max(lexiconNodes.where({morpheme: morphemes[m]}), function(node) { return node.get("value"); });
//      console.log(matchingNode);
      var gloss = "?";   // If there's no matching gloss, use question marks
      if (matchingNode) {
        gloss = matchingNode.get("gloss");
      }
      glosses.push(gloss);
    }

    glossGroups.push(glosses.join("-"));
  }

  // Replace the gloss line with the guessed glosses
  return glossGroups.join(" ");
}
/**
 * Takes as a parameters an array of rules which came from CouchDB precedence rule query.
 * Example Rule: {"key":{"x":"@","relation":"preceeds","y":"aqtu","context":"aqtu-nay-wa-n"},"value":2}
 */
Glosser.generateForceDirectedRulesJsonForD3 = function(rules) {

  /*
   * Cycle through the precedence rules, convert them into graph edges with the morpheme index in the morpheme array as the source/target values
   */
  morphemeLinks = [];
  morphemes = [];
  for ( var i in rules) {
    var xpos = morphemes.indexOf(rules[i].key.x);
    if (xpos < 0) {
      morphemes.push(rules[i].key.x);
      xpos = morphemes.length - 1;
    }
    var ypos = morphemes.indexOf(rules[i].key.y);
    if (ypos < 0) {
      morphemes.push(rules[i].key.y);
      ypos = morphemes.length - 1;
    }
    if (rules[i].key.y != "@") {
      morphemeLinks.push({
        source : xpos,
        target : ypos,
        value : 1 //TODO use the context counting to get a weight measure
      });
    }
  }

  /*
   * Build the morphemes into nodes and color them by their morpheme length, could be a good measure of outliers
   */
  var morphemenodes = [];
  for (m in morphemes) {
    morphemenodes.push({
      name : morphemes[m],
      group : morphemes[m].length
    });
  }

  /*
   * Create the JSON required by D3
   */
  var rulesGraph = {};
  rulesGraph.links = morphemeLinks;
  rulesGraph.nodes = morphemenodes;

  return rulesGraph;
}

/*
 * Some sample D3 from the force-html.html example
 *
 */
Glosser.visualizeMorphemesAsForceDirectedGraph = function(){

  var width = 960,
      height = 500,
      radius = 6,
      fill = d3.scale.category20();

  var force = d3.layout.force()
      .charge(-120)
      .linkDistance(30)
      .size([width, height]);

  var vis = d3.select("body").append("div")
      .style("width", width + "px")
      .style("height", height + "px");

  var tooltip = null;

  d3.json("rules.json", function(json) {
    var link = vis.selectAll("div.link")
        .data(json.links)
      .enter().append("div")
        .attr("class", "link");

    var node = vis.selectAll("div.node")
        .data(json.nodes)
      .enter().append("div")
        .attr("class", "node")
        .style("background", function(d) { return fill(d.group); })
        .style("border-color", function(d) { return d3.rgb(fill(d.group)).darker(); })
      .on("mouseover", function(d) {
        tooltip = d3.select("body")
          .append("div")
          .style("position", "absolute")
          .style("z-index", "10")
          .style("visibility", "visible")
          .text(d.name)
        })
      .on("mouseout", function() {
          tooltip.style("visibility", "hidden");
        })
      .call(force.drag);

    force
        .nodes(json.nodes)
        .links(json.links)
        .on("tick", tick)
        .start();

    function tick() {
      node.style("left", function(d) { return (d.x = Math.max(radius, Math.min(width - radius, d.x))) + "px"; })
          .style("top", function(d) { return (d.y = Math.max(radius, Math.min(height - radius, d.y))) + "px"; });

      link.style("left", function(d) { return d.source.x + "px"; })
          .style("top", function(d) { return d.source.y + "px"; })
          .style("width", length)
          .style("-webkit-transform", transform)
          .style("-moz-transform", transform)
          .style("-ms-transform", transform)
          .style("-o-transform", transform)
          .style("transform", transform);
    }

    function transform(d) {
      return "rotate(" + Math.atan2(d.target.y - d.source.y, d.target.x - d.source.x) * 180 / Math.PI + "deg)";
    }

    function length(d) {
      var dx = d.target.x - d.source.x,
          dy = d.target.y - d.source.y;
      return Math.sqrt(dx * dx + dy * dy) + "px";
    }
  });

}<|endoftext|>


<commit_before>/* global CKEDITOR */

import { softNavigate } from './helpers/navigation';

const getDataValue = function(attr) {
    return function() {
        const value = $('.section-upload').data(attr);
		return (value || undefined);
    };
};

const getOwnerId = getDataValue('owner');
const getCurrentParent = getDataValue('parent');

function archiveTask(e){
    e.preventDefault();
    e.stopPropagation();
    // loading animation
    let btntext = this.innerHTML;
    $(this).find("i").attr("class", "fa fa-spinner fa-spin");
    // send request to server
    let request = $.ajax({
        type: "PATCH",
        url: this.getAttribute("href"),
        data: this.getAttribute("data"),
        context: this,
        error: function(){showAJAXError(); this.innerHTML = btntext;}
    });
    request.done(function(r) {
        // switch text (innerHTML <--> alt-text)
        const temp = $(this).attr("alt-text");
        $(this).attr("alt-text", btntext);
        this.innerHTML = temp;
        // grey out if removed from list
        $(this).parents(".disableable").toggleClass("disabled");
        // change data
        $(this).attr("data",(this.getAttribute("data")=="archive=done")?"archive=open":"archive=done");
    });
    return false;
}
function importSubmission(e){
    e.preventDefault();
    const submissionid = this.getAttribute("data");
    this.disabled = true;
    this.innerHTML = 'importiere

';
    if(confirm("Möchten Sie wirklich Ihre Bewertung durch die Abgabe des Schülers ersetzen?")){
        $.ajax({
            url: "/homework/submit/"+submissionid+"/import",
            context: this
        }).done(function(r) {
            CKEDITOR.instances["evaluation "+submissionid].setData( r.comment );
            this.disabled = false;
            this.innerHTML = "Abgabe des Schülers importieren";
        });
    }
}

window.addEventListener("DOMContentLoaded", function(){
    /* FEATHERS FILTER MODULE */
    const filterModule = document.getElementById("filter");
    if(!filterModule){return;}
    filterModule.addEventListener('newFilter', (e) => {
        document.querySelectorAll("circular-progress").forEach(graphic => {graphic.remove();});
        const filter = e.detail;
        const newurl = "?filterQuery=" + escape(JSON.stringify(filter[0]));
        softNavigate(newurl, ".homework", ".pagination");
    });
    document.querySelector(".filter").dispatchEvent(new CustomEvent("getFilter"));
});
$(document).ready(function() {
	const editorInstanceNames = Object.keys((window.CKEDITOR || {}).instances || {});
	editorInstanceNames
		.filter(function (e) { return e.startsWith('evaluation'); })
		.forEach(function (name) {
			const editor = window.CKEDITOR.instances[name];
			editor.on('change', () => {
				// find the closest submit button and disable it if no content is given
				const submitButton = $(editor.element.$.closest('form')).find('button[type="submit"]')[0];
				const content = editor.document.getBody().getText();
				submitButton.disabled = !content.trim();
			});
		});

    function showAJAXError(req, textStatus, errorThrown) {
        if (textStatus === "timeout") {
            $.showNotification("Zeitüberschreitung der Anfrage", "danger");
        } else if (errorThrown === "Conflict") {
            $.showNotification("Dieser Dateiname existiert bereits in Ihren Dateien. Bitte benennen Sie die Datei um.", "danger");
        } else {
            $.showNotification(errorThrown, "danger", 15000);
        }
    }

    function ajaxForm(element, after, contentTest){
        const submitButton = element.find('[type=submit]')[0];
        let submitButtonText = submitButton.innerHTML || submitButton.value;
        submitButtonText = submitButtonText.replace('  (error)',"");
        submitButton.innerHTML = submitButtonText+'

';
        submitButton.disabled = true;
        const submitButtonStyleDisplay = submitButton.getAttribute("style");
        submitButton.style["display"]="inline-block";

        const url     = element.attr("action");
        const method  = element.attr("method");
        // update value of ckeditor instances
        let ckeditorInstance = element.find('textarea.customckeditor').attr("id");
		if (ckeditorInstance) CKEDITOR.instances[ckeditorInstance].updateElement();
		const content = element.serialize();
        if(contentTest){
            if(contentTest(content) == false){
                $.showNotification("Form validation failed", "danger", 15000);
                return;
            }
		}
        let request = $.ajax({
            type: method,
            url: url,
            data: content,
            context: element
        });
        request.done(function(r) {
            var saved = setInterval(_ => {
                submitButton.innerHTML = submitButtonText;
                submitButton.disabled = false;
                submitButton.setAttribute("style",submitButtonStyleDisplay);
                clearInterval(saved);
            }, 2500);
            submitButton.innerHTML = "gespeichert 😊";
            if(after){after(this, element.serializeArray());}
        });
        request.fail(function() {
            if(request.getResponseHeader("error-message")){
                showAJAXError(undefined, undefined,request.getResponseHeader("error-message"));
            }
            submitButton.disabled = false;
            submitButton.innerHTML = submitButtonText+'  (error)';
        });
    }
    // Abgabe speichern
    $('form.submissionForm.ajaxForm').on("submit",function(e){
        if(e) e.preventDefault();
        ajaxForm($(this), function(element, content){
            let teamMembers = [];
            content.forEach(e => {
                if(e.name == "teamMembers"){
                    teamMembers.push(e.value);
                }
            });
            if(teamMembers != [] && $(".me").val() && !teamMembers.includes($(".me").val())){
                location.reload();
            }
		});
        return false;
    });

    // Abgabe löschen
    $('a[data-method="delete-submission"]').on('click', function(e) {
        e.stopPropagation();
        e.preventDefault();
        var $buttonContext = $(this);
        let $deleteModal = $('.delete-modal');
        $deleteModal.appendTo('body').modal('show');
        $deleteModal.find('.modal-title').text("Bist du dir sicher, dass du '" + $buttonContext.data('name') + "' löschen möchtest?");
        $deleteModal.find('.btn-submit').unbind('click').on('click', function() {
            window.location.href = $buttonContext.attr('href');
        });
    });

    //validate teamMembers
    var lastTeamMembers = null;
    const maxTeamMembers = parseInt($("#maxTeamMembers").html());
    $('#teamMembers').change(function(event) {
        if ($(this).val().length > maxTeamMembers) {
            $(this).val(lastTeamMembers);
            $.showNotification("Die maximale Teamgröße beträgt " + maxTeamMembers + " Mitglieder", "warning", 5000);
        } else {
            lastTeamMembers = $(this).val();
        }
        $(this).chosen().trigger("chosen:updated");
    });

    $('#teamMembers').chosen().change(function(event, data) {
        if(data.deselected && data.deselected == $('.owner').val()){
            $(".owner").prop('selected', true);
            $('#teamMembers').trigger("chosen:updated");
            $.showNotification("Du darfst den Ersteller der Aufgabe nicht entfernen!", "warning", 5000);
        }
    });

    // Bewertung speichern
    $('.evaluation #comment form').on("submit",function(e){
        if(e) e.preventDefault();
        ajaxForm($(this),function(c){
            $.showNotification("Bewertung wurde gespeichert!", "success", 5000);
        },function(c){
            return (c.grade || c.gradeComment);
        });
        return false;
    });

    // Kommentar erstellen
    $('.discussionarea form[action="/homework/comment"]').on("submit",function(e){
        if(e) e.preventDefault();
        ajaxForm($(this),function(t){
            $(t).parent().prev().append('
'+$(t).find("div[data-username]").attr('data-username')+'
'+$(t).find("textarea")[0].value+'

');
            $(t).find("textarea")[0].value = "";
        });
        return false;
    });

    // Kommentar löschen
    $('.discussionarea ul.comments form').on("submit",function(e){
        if(e) e.preventDefault();
        if(confirm("Kommentar endgültig löschen?")){
            ajaxForm($(this),function(t){
                $(t).closest("li.comment").remove();
            });
        }
        return false;
    });

    document.querySelectorAll('.btn-archive').forEach(btn => {btn.addEventListener("click", archiveTask);});

    function updateSearchParameter(key, value) {
        let url = window.location.search;
        let reg = new RegExp('('+key+'=)[^\&]+');
        window.location.search = (url.indexOf(key) !== -1)?(url.replace(reg, '$1' + value)):(url + ((url.indexOf('?') == -1)? "?" : "&") + key + "=" + value);
    }

    document.querySelectorAll('.importsubmission').forEach(btn => {btn.addEventListener("click", importSubmission);});

    // file upload stuff, todo: maybe move or make it more flexible when also uploading to homework-assignment
    let $uploadForm = $(".form-upload");
    let $progressBar = $('.progress-bar');
    let $progress = $progressBar.find('.bar');
    let $percentage = $progressBar.find('.percent');

    let progressBarActive = false;
    let finishedFilesSize = 0;

    /**
     * adds a new file item in the uploaded file section without reload, when no submission exists
     * @param section - the major file list
     * @param file - the new file
     */
    function addNewUploadedFile(section, file) {
        let filesCount = section.children().length === 0 ? -1 : section.children().length;
        let $fileListItem = $(`<li class="list-group-item"><i class="fa fa-file" aria-hidden="true"></i>${file.name}</li>`)
            .append(`<input type="hidden" name="fileIds[${filesCount + 1}]" value="${file._id}" />`);
        section.append($fileListItem);
    }

    $uploadForm.dropzone ? $uploadForm.dropzone({
        accept: function (file, done) {
            // get signed url before processing the file
            // this is called on per-file basis

            $.post('/files/file', {
                parent: getCurrentParent(),
                type: file.type,
                filename: file.name,
            }, function (data) {
                file.signedUrl = data.signedUrl;
                done();
            })
            .fail(showAJAXError);
        },
        createImageThumbnails: false,
        method: 'put',
        init: function () {
            // this is called on per-file basis
            this.on("processing", function (file) {
                if(!progressBarActive) {
                    $progress.css('width', '0%');

                    $uploadForm.fadeOut(50, function () {
                        $progressBar.fadeIn(50);
                    });

                    progressBarActive = true;
                }

                this.options.url = file.signedUrl.url;
                this.options.headers = file.signedUrl.header;
            });

            this.on("sending", function (file, xhr, formData) {
                let _send = xhr.send;
                xhr.send = function () {
                    _send.call(xhr, file);
                };
            });

            this.on("totaluploadprogress", function (progress, total, uploaded) {
                const realProgress = (uploaded + finishedFilesSize) / ((total + finishedFilesSize) / 100);

                $progress.stop().animate({'width': realProgress + '%'}, {
                    step: function (now) {
                        $percentage.html(Math.ceil(now) + '%');
                    }
                });
            });

            this.on("queuecomplete", function (file, response) {
                progressBarActive = false;
                finishedFilesSize = 0;

                $progressBar.fadeOut(50, function () {
                    $uploadForm.fadeIn(50);
                    // delay for error messages
                    setTimeout(function() {
                        // just reload if submission already exists
                        $("input[name='submissionId']").val() ? window.location.reload() : '';
                    }, 1500);
                });
            });

            this.on("success", function (file, response) {
                finishedFilesSize += file.size;

                var parentId = getCurrentParent();
                var params = {
                    name: file.name,
                    owner: getOwnerId(),
                    type: file.type,
                    size: file.size,
                    storageFileName: file.signedUrl.header['x-amz-meta-flat-name'],
                    thumbnail: file.signedUrl.header['x-amz-meta-thumbnail']
                };

                if( parentId ) {
                    params.parent = parentId;
				}

                // post file meta to proxy file service for persisting data
                $.post('/files/fileModel', params , (data) => {
                    // add submitted file reference to submission
                    // hint: this only runs when an submission is already existing. if not, the file submission will be
                    // only saved when hitting the save button in the corresponding submission form
                    let submissionId = $("input[name='submissionId']").val();
                    let homeworkId = $("input[name='homeworkId']").val();

                    let teamMembers = $('#teamMembers').val();
                    if (submissionId) {
                        $.post(`/homework/submit/${submissionId}/files`, {fileId: data._id, teamMembers: teamMembers}, _ => {
                            $.post(`/homework/submit/${submissionId}/files/${data._id}/permissions`, {teamMembers: teamMembers});
                        });
                    } else {
                        addNewUploadedFile($('.list-group-files'), data);

                        // 'empty' submissionId is ok because the route takes the homeworkId first
                        $.post(`/homework/submit/0/files/${data._id}/permissions`, {homeworkId: homeworkId});
                    }
                }).fail(showAJAXError);

                this.removeFile(file);

            });

            this.on("dragover", function (file, response) {
                $uploadForm.addClass('focus');
            });

            this.on("dragleave", function () {
                $uploadForm.removeClass('focus');
            });

            this.on("dragend", function (file, response) {
                $uploadForm.removeClass('focus');
            });

            this.on("drop", function (file, response) {
                $uploadForm.removeClass('focus');
            });
        }
    }) : '';

    /**
     * deletes a) the file itself, b) the reference to the submission
     */
    $('a[data-method="delete-file"]').on('click', function (e) {
        e.stopPropagation();
        e.preventDefault();
        let $buttonContext = $(this);
        let $deleteModal = $('.delete-modal');
        let fileId = $buttonContext.data('file-id');

        $deleteModal.appendTo('body').modal('show');
        $deleteModal.find('.modal-title').text("Bist du dir sicher, dass du '" + $buttonContext.data('file-name') + "' löschen möchtest?");

        $deleteModal.find('.btn-submit').unbind('click').on('click', function () {
            $.ajax({
                url: $buttonContext.attr('href'),
                type: 'DELETE',
                data: {
                    key: $buttonContext.data('file-key')
                },
                success: function (_) {
                    // delete reference in submission
                    let submissionId = $("input[name='submissionId']").val();
                    let teamMembers = $('#teamMembers').val();
                    $.ajax({
                        url: `/homework/submit/${submissionId}/files`,
                        data: {fileId: fileId, teamMembers: teamMembers},
                        type: 'DELETE',
                        success: function (_) {
                            window.location.reload();
                        }
                    });
                },
                error: showAJAXError
            });
        });
    });
});
<commit_msg>Use arrow functions<commit_after>/* global CKEDITOR */

import { softNavigate } from './helpers/navigation';

const getDataValue = function(attr) {
    return function() {
        const value = $('.section-upload').data(attr);
		return (value || undefined);
    };
};

const getOwnerId = getDataValue('owner');
const getCurrentParent = getDataValue('parent');

function archiveTask(e){
    e.preventDefault();
    e.stopPropagation();
    // loading animation
    let btntext = this.innerHTML;
    $(this).find("i").attr("class", "fa fa-spinner fa-spin");
    // send request to server
    let request = $.ajax({
        type: "PATCH",
        url: this.getAttribute("href"),
        data: this.getAttribute("data"),
        context: this,
        error: function(){showAJAXError(); this.innerHTML = btntext;}
    });
    request.done(function(r) {
        // switch text (innerHTML <--> alt-text)
        const temp = $(this).attr("alt-text");
        $(this).attr("alt-text", btntext);
        this.innerHTML = temp;
        // grey out if removed from list
        $(this).parents(".disableable").toggleClass("disabled");
        // change data
        $(this).attr("data",(this.getAttribute("data")=="archive=done")?"archive=open":"archive=done");
    });
    return false;
}
function importSubmission(e){
    e.preventDefault();
    const submissionid = this.getAttribute("data");
    this.disabled = true;
    this.innerHTML = 'importiere

';
    if(confirm("Möchten Sie wirklich Ihre Bewertung durch die Abgabe des Schülers ersetzen?")){
        $.ajax({
            url: "/homework/submit/"+submissionid+"/import",
            context: this
        }).done(function(r) {
            CKEDITOR.instances["evaluation "+submissionid].setData( r.comment );
            this.disabled = false;
            this.innerHTML = "Abgabe des Schülers importieren";
        });
    }
}

window.addEventListener("DOMContentLoaded", function(){
    /* FEATHERS FILTER MODULE */
    const filterModule = document.getElementById("filter");
    if(!filterModule){return;}
    filterModule.addEventListener('newFilter', (e) => {
        document.querySelectorAll("circular-progress").forEach(graphic => {graphic.remove();});
        const filter = e.detail;
        const newurl = "?filterQuery=" + escape(JSON.stringify(filter[0]));
        softNavigate(newurl, ".homework", ".pagination");
    });
    document.querySelector(".filter").dispatchEvent(new CustomEvent("getFilter"));
});
$(document).ready(function() {
	const editorInstanceNames = Object.keys((window.CKEDITOR || {}).instances || {});
	editorInstanceNames
		.filter((e) => e.startsWith('evaluation'))
		.forEach((name) => {
			const editor = window.CKEDITOR.instances[name];
			editor.on('change', () => {
				// find the closest submit button and disable it if no content is given
				const submitButton = $(editor.element.$.closest('form')).find('button[type="submit"]')[0];
				const content = editor.document.getBody().getText();
				submitButton.disabled = !content.trim();
			});
		});

    function showAJAXError(req, textStatus, errorThrown) {
        if (textStatus === "timeout") {
            $.showNotification("Zeitüberschreitung der Anfrage", "danger");
        } else if (errorThrown === "Conflict") {
            $.showNotification("Dieser Dateiname existiert bereits in Ihren Dateien. Bitte benennen Sie die Datei um.", "danger");
        } else {
            $.showNotification(errorThrown, "danger", 15000);
        }
    }

    function ajaxForm(element, after, contentTest){
        const submitButton = element.find('[type=submit]')[0];
        let submitButtonText = submitButton.innerHTML || submitButton.value;
        submitButtonText = submitButtonText.replace('  (error)',"");
        submitButton.innerHTML = submitButtonText+'

';
        submitButton.disabled = true;
        const submitButtonStyleDisplay = submitButton.getAttribute("style");
        submitButton.style["display"]="inline-block";

        const url     = element.attr("action");
        const method  = element.attr("method");
        // update value of ckeditor instances
        let ckeditorInstance = element.find('textarea.customckeditor').attr("id");
		if (ckeditorInstance) CKEDITOR.instances[ckeditorInstance].updateElement();
		const content = element.serialize();
        if(contentTest){
            if(contentTest(content) == false){
                $.showNotification("Form validation failed", "danger", 15000);
                return;
            }
		}
        let request = $.ajax({
            type: method,
            url: url,
            data: content,
            context: element
        });
        request.done(function(r) {
            var saved = setInterval(_ => {
                submitButton.innerHTML = submitButtonText;
                submitButton.disabled = false;
                submitButton.setAttribute("style",submitButtonStyleDisplay);
                clearInterval(saved);
            }, 2500);
            submitButton.innerHTML = "gespeichert 😊";
            if(after){after(this, element.serializeArray());}
        });
        request.fail(function() {
            if(request.getResponseHeader("error-message")){
                showAJAXError(undefined, undefined,request.getResponseHeader("error-message"));
            }
            submitButton.disabled = false;
            submitButton.innerHTML = submitButtonText+'  (error)';
        });
    }
    // Abgabe speichern
    $('form.submissionForm.ajaxForm').on("submit",function(e){
        if(e) e.preventDefault();
        ajaxForm($(this), function(element, content){
            let teamMembers = [];
            content.forEach(e => {
                if(e.name == "teamMembers"){
                    teamMembers.push(e.value);
                }
            });
            if(teamMembers != [] && $(".me").val() && !teamMembers.includes($(".me").val())){
                location.reload();
            }
		});
        return false;
    });

    // Abgabe löschen
    $('a[data-method="delete-submission"]').on('click', function(e) {
        e.stopPropagation();
        e.preventDefault();
        var $buttonContext = $(this);
        let $deleteModal = $('.delete-modal');
        $deleteModal.appendTo('body').modal('show');
        $deleteModal.find('.modal-title').text("Bist du dir sicher, dass du '" + $buttonContext.data('name') + "' löschen möchtest?");
        $deleteModal.find('.btn-submit').unbind('click').on('click', function() {
            window.location.href = $buttonContext.attr('href');
        });
    });

    //validate teamMembers
    var lastTeamMembers = null;
    const maxTeamMembers = parseInt($("#maxTeamMembers").html());
    $('#teamMembers').change(function(event) {
        if ($(this).val().length > maxTeamMembers) {
            $(this).val(lastTeamMembers);
            $.showNotification("Die maximale Teamgröße beträgt " + maxTeamMembers + " Mitglieder", "warning", 5000);
        } else {
            lastTeamMembers = $(this).val();
        }
        $(this).chosen().trigger("chosen:updated");
    });

    $('#teamMembers').chosen().change(function(event, data) {
        if(data.deselected && data.deselected == $('.owner').val()){
            $(".owner").prop('selected', true);
            $('#teamMembers').trigger("chosen:updated");
            $.showNotification("Du darfst den Ersteller der Aufgabe nicht entfernen!", "warning", 5000);
        }
    });

    // Bewertung speichern
    $('.evaluation #comment form').on("submit",function(e){
        if(e) e.preventDefault();
        ajaxForm($(this),function(c){
            $.showNotification("Bewertung wurde gespeichert!", "success", 5000);
        },function(c){
            return (c.grade || c.gradeComment);
        });
        return false;
    });

    // Kommentar erstellen
    $('.discussionarea form[action="/homework/comment"]').on("submit",function(e){
        if(e) e.preventDefault();
        ajaxForm($(this),function(t){
            $(t).parent().prev().append('
'+$(t).find("div[data-username]").attr('data-username')+'
'+$(t).find("textarea")[0].value+'

');
            $(t).find("textarea")[0].value = "";
        });
        return false;
    });

    // Kommentar löschen
    $('.discussionarea ul.comments form').on("submit",function(e){
        if(e) e.preventDefault();
        if(confirm("Kommentar endgültig löschen?")){
            ajaxForm($(this),function(t){
                $(t).closest("li.comment").remove();
            });
        }
        return false;
    });

    document.querySelectorAll('.btn-archive').forEach(btn => {btn.addEventListener("click", archiveTask);});

    function updateSearchParameter(key, value) {
        let url = window.location.search;
        let reg = new RegExp('('+key+'=)[^\&]+');
        window.location.search = (url.indexOf(key) !== -1)?(url.replace(reg, '$1' + value)):(url + ((url.indexOf('?') == -1)? "?" : "&") + key + "=" + value);
    }

    document.querySelectorAll('.importsubmission').forEach(btn => {btn.addEventListener("click", importSubmission);});

    // file upload stuff, todo: maybe move or make it more flexible when also uploading to homework-assignment
    let $uploadForm = $(".form-upload");
    let $progressBar = $('.progress-bar');
    let $progress = $progressBar.find('.bar');
    let $percentage = $progressBar.find('.percent');

    let progressBarActive = false;
    let finishedFilesSize = 0;

    /**
     * adds a new file item in the uploaded file section without reload, when no submission exists
     * @param section - the major file list
     * @param file - the new file
     */
    function addNewUploadedFile(section, file) {
        let filesCount = section.children().length === 0 ? -1 : section.children().length;
        let $fileListItem = $(`<li class="list-group-item"><i class="fa fa-file" aria-hidden="true"></i>${file.name}</li>`)
            .append(`<input type="hidden" name="fileIds[${filesCount + 1}]" value="${file._id}" />`);
        section.append($fileListItem);
    }

    $uploadForm.dropzone ? $uploadForm.dropzone({
        accept: function (file, done) {
            // get signed url before processing the file
            // this is called on per-file basis

            $.post('/files/file', {
                parent: getCurrentParent(),
                type: file.type,
                filename: file.name,
            }, function (data) {
                file.signedUrl = data.signedUrl;
                done();
            })
            .fail(showAJAXError);
        },
        createImageThumbnails: false,
        method: 'put',
        init: function () {
            // this is called on per-file basis
            this.on("processing", function (file) {
                if(!progressBarActive) {
                    $progress.css('width', '0%');

                    $uploadForm.fadeOut(50, function () {
                        $progressBar.fadeIn(50);
                    });

                    progressBarActive = true;
                }

                this.options.url = file.signedUrl.url;
                this.options.headers = file.signedUrl.header;
            });

            this.on("sending", function (file, xhr, formData) {
                let _send = xhr.send;
                xhr.send = function () {
                    _send.call(xhr, file);
                };
            });

            this.on("totaluploadprogress", function (progress, total, uploaded) {
                const realProgress = (uploaded + finishedFilesSize) / ((total + finishedFilesSize) / 100);

                $progress.stop().animate({'width': realProgress + '%'}, {
                    step: function (now) {
                        $percentage.html(Math.ceil(now) + '%');
                    }
                });
            });

            this.on("queuecomplete", function (file, response) {
                progressBarActive = false;
                finishedFilesSize = 0;

                $progressBar.fadeOut(50, function () {
                    $uploadForm.fadeIn(50);
                    // delay for error messages
                    setTimeout(function() {
                        // just reload if submission already exists
                        $("input[name='submissionId']").val() ? window.location.reload() : '';
                    }, 1500);
                });
            });

            this.on("success", function (file, response) {
                finishedFilesSize += file.size;

                var parentId = getCurrentParent();
                var params = {
                    name: file.name,
                    owner: getOwnerId(),
                    type: file.type,
                    size: file.size,
                    storageFileName: file.signedUrl.header['x-amz-meta-flat-name'],
                    thumbnail: file.signedUrl.header['x-amz-meta-thumbnail']
                };

                if( parentId ) {
                    params.parent = parentId;
				}

                // post file meta to proxy file service for persisting data
                $.post('/files/fileModel', params , (data) => {
                    // add submitted file reference to submission
                    // hint: this only runs when an submission is already existing. if not, the file submission will be
                    // only saved when hitting the save button in the corresponding submission form
                    let submissionId = $("input[name='submissionId']").val();
                    let homeworkId = $("input[name='homeworkId']").val();

                    let teamMembers = $('#teamMembers').val();
                    if (submissionId) {
                        $.post(`/homework/submit/${submissionId}/files`, {fileId: data._id, teamMembers: teamMembers}, _ => {
                            $.post(`/homework/submit/${submissionId}/files/${data._id}/permissions`, {teamMembers: teamMembers});
                        });
                    } else {
                        addNewUploadedFile($('.list-group-files'), data);

                        // 'empty' submissionId is ok because the route takes the homeworkId first
                        $.post(`/homework/submit/0/files/${data._id}/permissions`, {homeworkId: homeworkId});
                    }
                }).fail(showAJAXError);

                this.removeFile(file);

            });

            this.on("dragover", function (file, response) {
                $uploadForm.addClass('focus');
            });

            this.on("dragleave", function () {
                $uploadForm.removeClass('focus');
            });

            this.on("dragend", function (file, response) {
                $uploadForm.removeClass('focus');
            });

            this.on("drop", function (file, response) {
                $uploadForm.removeClass('focus');
            });
        }
    }) : '';

    /**
     * deletes a) the file itself, b) the reference to the submission
     */
    $('a[data-method="delete-file"]').on('click', function (e) {
        e.stopPropagation();
        e.preventDefault();
        let $buttonContext = $(this);
        let $deleteModal = $('.delete-modal');
        let fileId = $buttonContext.data('file-id');

        $deleteModal.appendTo('body').modal('show');
        $deleteModal.find('.modal-title').text("Bist du dir sicher, dass du '" + $buttonContext.data('file-name') + "' löschen möchtest?");

        $deleteModal.find('.btn-submit').unbind('click').on('click', function () {
            $.ajax({
                url: $buttonContext.attr('href'),
                type: 'DELETE',
                data: {
                    key: $buttonContext.data('file-key')
                },
                success: function (_) {
                    // delete reference in submission
                    let submissionId = $("input[name='submissionId']").val();
                    let teamMembers = $('#teamMembers').val();
                    $.ajax({
                        url: `/homework/submit/${submissionId}/files`,
                        data: {fileId: fileId, teamMembers: teamMembers},
                        type: 'DELETE',
                        success: function (_) {
                            window.location.reload();
                        }
                    });
                },
                error: showAJAXError
            });
        });
    });
});
<|endoftext|>


<commit_before>ngapp.service('contextMenuFactory', function() {
    let uneditableValueTypes = [xelib.vtUnknown, xelib.vtArray, xelib.vtStruct];

    let divider = {
        visible: (scope, items) => { return items.length > 0 && !items.last().divider; },
        build: (scope, items) => items.push({ divider: true })
    };

    let testNodes = function(nodes, testFn) {
        return nodes.reduce(function (b, node) {
            return b && testFn(node);
        }, true);
    };

    this.mainTreeItems = [{
        id: 'Add',
        visible: (scope) => {
            let node = scope.selectedNodes.last();
            return node.element_type !== xelib.etMainRecord && xelib.GetCanAdd(node.handle);
        },
        build: (scope, items) => {
            let node = scope.selectedNodes.last(),
                addList = xelib.GetAddList(node.handle),
                isFile = node.element_type === xelib.etFile;
            items.push({
                label: `Add ${isFile ? 'Group' : 'Record'}`,
                hotkey: 'Insert',
                disabled: !addList.length,
                children: addList.map(function(item) {
                    return {
                        label: item,
                        callback: () => scope.addElement(node, item)
                    };
                })
            });
        }
    }, {
        id: 'Delete',
        visible: (scope) => {
            return testNodes(scope.selectedNodes, function(node) {
                return xelib.GetIsRemoveable(node.handle);
            });
        },
        build: (scope, items) => {
            items.push({
                label: 'Delete',
                hotkey: 'Del',
                callback: () => scope.deleteElements()
            });
        }
    }, {
        id: 'Refactor',
        visible: (scope) => {
            let nodes = scope.selectedNodes, node = nodes.last();
            return node.element_type === xelib.etFile && xelib.GetIsEditable(node.handle) ||
                testNodes(nodes, function(node) {
                    return node.element_type === xelib.etMainRecord && xelib.GetIsEditable(node.handle);
                });
        },
        build: (scope, items) => {
            let node = scope.selectedNodes.last(),
                children = [];
            if (node.element_type === xelib.etFile) {
                children.push({
                    label: 'Rename File',
                    hotkey: 'F2, Alt+Shift+R',
                    callback: () => scope.changeFileName(node)
                }, {
                    label: 'Change Author',
                    hotkey: 'Alt+Shift+A',
                    callback: () => scope.changeFileAuthor(node)
                }, {
                    label: 'Change Description',
                    hotkey: 'Alt+Shift+D',
                    callback: () => scope.changeFileDescription(node)
                });
            } else if (node.element_type === xelib.etMainRecord) {
                children.push({
                    label: 'Rename',
                    hotkey: 'F2, Alt+Shift+R',
                    disabled: true,
                    callback: () => scope.toggleRefactorModal(true, 'Name')
                }, {
                    label: 'Change EditorIDs',
                    hotkey: 'Alt+Shift+E',
                    disabled: true,
                    callback: () => scope.toggleRefactorModal(true, 'EditorID')
                }, {
                    label: 'Change FormIDs',
                    hotkey: 'Alt+Shift+F',
                    disabled: true,
                    callback: () => scope.toggleRefactorModal(true, 'FormID')
                });
            }
            items.push({
                label: 'Refactor',
                children: children
            });
        }
    }, {
        id: 'Enable Editing',
        visible: (scope) => {
            return testNodes(scope.selectedNodes, function(node) {
                return node.element_type === xelib.etFile && !xelib.GetIsEditable(node.handle);
            });
        },
        build: (scope, items) => {
            items.push({
                label: 'Enable Editing',
                hotkey: 'Ctrl+E',
                disabled: true,
                callback: () => scope.enableEditing(node)
            })
        }
    }, divider, {
        id: 'Automate',
        visible: () => { return true; },
        build: (scope, items) => {
            items.push({
                label: 'Automate',
                hotkey: 'Ctrl+M',
                callback: () => scope.$emit('showAutomateModal')
            })
        }
    }, {
        id: 'Advanced Search',
        visible: () => { return true; },
        build: (scope, items) => {
            items.push({
                label: 'Advanced Search',
                hotkey: 'Ctrl+Shift+F',
                disabled: true,
                callback: () => scope.toggleSearchModal(true)
            })
        }
    }, {
        id: 'Open',
        visible: (scope) => {
            let node = scope.selectedNodes.last();
            return node.element_type !== xelib.etGroupRecord;
        },
        build: (scope, items) => {
            let node = scope.selectedNodes.last();
            items.push({
                label: 'Open in Record View',
                hotkey: 'Enter',
                callback: () => scope.open(node)
            })
        }
    }, divider, {
        id: 'Copy',
        visible: () => { return true; },
        build: (scope, items) => {
            let node = scope.selectedNodes.last();
            items.push({
                label: 'Copy',
                hotkey: 'Ctrl+C',
                disabled: true,//!node,
                callback: () => scope.copyNodes()
            })
        }
    }, {
        id: 'Copy Path',
        visible: () => { return true; },
        build: (scope, items) => {
            let node = scope.selectedNodes.last();
            items.push({
                label: 'Copy Path',
                hotkey: 'Ctrl+Shift+C',
                disabled: true,//!node,
                callback: () => scope.copyNodePaths()
            })
        }
    }, {
        id: 'Paste',
        visible: () => { return true },
        build: (scope, items) => {
            items.push({
                label: 'Paste',
                hotkey: 'Ctrl+V',
                disabled: true,//!scope.canPaste(),
                callback: () => scope.paste()
            })
        }
    }, {
        id: 'Paste as Override',
        visible: () => { return true; },
        build: (scope, items) => {
            items.push({
                label: 'Paste as Override',
                hotkey: 'Ctrl+Shift+V',
                disabled: true,//!scope.canPaste(true),
                callback: () => scope.paste(true)
            })
        }
    }];

    this.recordTreeItems = [{
        id: 'Add',
        visible: (scope) => {
            let node = scope.selectedNodes.last(),
                index = scope.focusedIndex - 1,
                handle = node.handles[index],
                record = index ? scope.overrides[index - 1] : scope.record,
                parentAvailable = !node.depth || (node.parent && node.parent.handles[index]);
            if (!xelib.GetIsEditable(record)) return false;
            return parentAvailable && (handle === 0 || node.value_type === xelib.vtArray);
        },
        build: (scope, items) => {
            let node = scope.selectedNodes.last(),
                index = scope.focusedIndex - 1;
            items.push({
                label: 'Add',
                hotkey: 'Insert',
                callback: () => scope.addElement(node, index)
            });
        }
    }, {
        id: 'Edit',
        visible: (scope) => {
            let node = scope.selectedNodes.last(),
                index = scope.focusedIndex - 1,
                handle = node.handles[index],
                record = index === 0 ? scope.record : scope.overrides[index - 1];
            if (!xelib.GetIsEditable(record)) return false;
            return handle !== 0 && !uneditableValueTypes.contains(node.value_type);
        },
        build: (scope, items) => {
            let node = scope.selectedNodes.last(),
                index = scope.focusedIndex;
            items.push({
                label: 'Edit',
                hotkey: 'Enter',
                callback: () => scope.editElement(node, index)
            });
        }
    }, {
        id: 'Delete',
        visible: (scope) => {
            let index = scope.focusedIndex - 1;
            return testNodes(scope.selectedNodes, function(node) {
                let handle = node.handles[index];
                return handle && xelib.GetIsRemoveable(handle);
            });
        },
        build: (scope, items) => {
            items.push({
                label: 'Delete',
                hotkey: 'Del',
                callback: () => scope.deleteElements()
            });
        }
    }, divider, {
        id: 'Copy',
        visible: () => { return true; },
        build: (scope, items) => {
            let node = scope.selectedNodes.last();
            items.push({
                label: 'Copy',
                hotkey: 'Ctrl+C',
                disabled: true,//!node,
                callback: () => scope.copyNodes()
            })
        }
    }, {
        id: 'Copy Path',
        visible: () => { return true; },
        build: (scope, items) => {
            let node = scope.selectedNodes.last();
            items.push({
                label: 'Copy Path',
                hotkey: 'Ctrl+Shift+C',
                disabled: true,//!node,
                callback: () => scope.copyNodePaths()
            })
        }
    }, {
        id: 'Paste',
        visible: () => { return true },
        build: (scope, items) => {
            items.push({
                label: 'Paste',
                hotkey: 'Ctrl+V',
                disabled: true,//!scope.canPaste(),
                callback: () => scope.paste()
            })
        }
    }]
});<commit_msg>added Add File context menu options to main tree context menu<commit_after>ngapp.service('contextMenuFactory', function() {
    let uneditableValueTypes = [xelib.vtUnknown, xelib.vtArray, xelib.vtStruct];

    let divider = {
        visible: (scope, items) => { return items.length > 0 && !items.last().divider; },
        build: (scope, items) => items.push({ divider: true })
    };

    let testNodes = function(nodes, testFn) {
        return nodes.reduce(function (b, node) {
            return b && testFn(node);
        }, true);
    };

    this.mainTreeItems = [{
        id: 'Add',
        visible: (scope) => {
            let node = scope.selectedNodes.last();
            return node.element_type !== xelib.etMainRecord && xelib.GetCanAdd(node.handle);
        },
        build: (scope, items) => {
            let node = scope.selectedNodes.last(),
                addList = xelib.GetAddList(node.handle),
                isFile = node.element_type === xelib.etFile;
            items.push({
                label: `Add ${isFile ? 'Group' : 'Record'}`,
                hotkey: 'Insert',
                disabled: !addList.length,
                children: addList.map(function(item) {
                    return {
                        label: item,
                        callback: () => scope.addElement(node, item)
                    };
                })
            });
        }
    }, {
        id: 'Add File',
        visible: (scope) => {
            return !scope.selectedNodes.length;
        },
        build: (scope, items) => {
            items.push({
                label: 'Add File',
                hotkey: 'Insert',
                callback: () => scope.addFile()
            });
        }
    }, {
        id: 'Delete',
        visible: (scope) => {
            return testNodes(scope.selectedNodes, function(node) {
                return xelib.GetIsRemoveable(node.handle);
            });
        },
        build: (scope, items) => {
            items.push({
                label: 'Delete',
                hotkey: 'Del',
                callback: () => scope.deleteElements()
            });
        }
    }, {
        id: 'Refactor',
        visible: (scope) => {
            let nodes = scope.selectedNodes, node = nodes.last();
            return node.element_type === xelib.etFile && xelib.GetIsEditable(node.handle) ||
                testNodes(nodes, function(node) {
                    return node.element_type === xelib.etMainRecord && xelib.GetIsEditable(node.handle);
                });
        },
        build: (scope, items) => {
            let node = scope.selectedNodes.last(),
                children = [];
            if (node.element_type === xelib.etFile) {
                children.push({
                    label: 'Rename File',
                    hotkey: 'F2, Alt+Shift+R',
                    callback: () => scope.changeFileName(node)
                }, {
                    label: 'Change Author',
                    hotkey: 'Alt+Shift+A',
                    callback: () => scope.changeFileAuthor(node)
                }, {
                    label: 'Change Description',
                    hotkey: 'Alt+Shift+D',
                    callback: () => scope.changeFileDescription(node)
                });
            } else if (node.element_type === xelib.etMainRecord) {
                children.push({
                    label: 'Rename',
                    hotkey: 'F2, Alt+Shift+R',
                    disabled: true,
                    callback: () => scope.toggleRefactorModal(true, 'Name')
                }, {
                    label: 'Change EditorIDs',
                    hotkey: 'Alt+Shift+E',
                    disabled: true,
                    callback: () => scope.toggleRefactorModal(true, 'EditorID')
                }, {
                    label: 'Change FormIDs',
                    hotkey: 'Alt+Shift+F',
                    disabled: true,
                    callback: () => scope.toggleRefactorModal(true, 'FormID')
                });
            }
            items.push({
                label: 'Refactor',
                children: children
            });
        }
    }, {
        id: 'Enable Editing',
        visible: (scope) => {
            return testNodes(scope.selectedNodes, function(node) {
                return node.element_type === xelib.etFile && !xelib.GetIsEditable(node.handle);
            });
        },
        build: (scope, items) => {
            items.push({
                label: 'Enable Editing',
                hotkey: 'Ctrl+E',
                disabled: true,
                callback: () => scope.enableEditing(node)
            })
        }
    }, divider, {
        id: 'Automate',
        visible: () => { return true; },
        build: (scope, items) => {
            items.push({
                label: 'Automate',
                hotkey: 'Ctrl+M',
                callback: () => scope.$emit('showAutomateModal')
            })
        }
    }, {
        id: 'Advanced Search',
        visible: () => { return true; },
        build: (scope, items) => {
            items.push({
                label: 'Advanced Search',
                hotkey: 'Ctrl+Shift+F',
                disabled: true,
                callback: () => scope.toggleSearchModal(true)
            })
        }
    }, {
        id: 'Open',
        visible: (scope) => {
            let node = scope.selectedNodes.last();
            return node.element_type !== xelib.etGroupRecord;
        },
        build: (scope, items) => {
            let node = scope.selectedNodes.last();
            items.push({
                label: 'Open in Record View',
                hotkey: 'Enter',
                callback: () => scope.open(node)
            })
        }
    }, divider, {
        id: 'Copy',
        visible: () => { return true; },
        build: (scope, items) => {
            let node = scope.selectedNodes.last();
            items.push({
                label: 'Copy',
                hotkey: 'Ctrl+C',
                disabled: true,//!node,
                callback: () => scope.copyNodes()
            })
        }
    }, {
        id: 'Copy Path',
        visible: () => { return true; },
        build: (scope, items) => {
            let node = scope.selectedNodes.last();
            items.push({
                label: 'Copy Path',
                hotkey: 'Ctrl+Shift+C',
                disabled: true,//!node,
                callback: () => scope.copyNodePaths()
            })
        }
    }, {
        id: 'Paste',
        visible: () => { return true },
        build: (scope, items) => {
            items.push({
                label: 'Paste',
                hotkey: 'Ctrl+V',
                disabled: true,//!scope.canPaste(),
                callback: () => scope.paste()
            })
        }
    }, {
        id: 'Paste as Override',
        visible: () => { return true; },
        build: (scope, items) => {
            items.push({
                label: 'Paste as Override',
                hotkey: 'Ctrl+Shift+V',
                disabled: true,//!scope.canPaste(true),
                callback: () => scope.paste(true)
            })
        }
    }];

    this.recordTreeItems = [{
        id: 'Add',
        visible: (scope) => {
            let node = scope.selectedNodes.last(),
                index = scope.focusedIndex - 1,
                handle = node.handles[index],
                record = index ? scope.overrides[index - 1] : scope.record,
                parentAvailable = !node.depth || (node.parent && node.parent.handles[index]);
            if (!xelib.GetIsEditable(record)) return false;
            return parentAvailable && (handle === 0 || node.value_type === xelib.vtArray);
        },
        build: (scope, items) => {
            let node = scope.selectedNodes.last(),
                index = scope.focusedIndex - 1;
            items.push({
                label: 'Add',
                hotkey: 'Insert',
                callback: () => scope.addElement(node, index)
            });
        }
    }, {
        id: 'Edit',
        visible: (scope) => {
            let node = scope.selectedNodes.last(),
                index = scope.focusedIndex - 1,
                handle = node.handles[index],
                record = index === 0 ? scope.record : scope.overrides[index - 1];
            if (!xelib.GetIsEditable(record)) return false;
            return handle !== 0 && !uneditableValueTypes.contains(node.value_type);
        },
        build: (scope, items) => {
            let node = scope.selectedNodes.last(),
                index = scope.focusedIndex;
            items.push({
                label: 'Edit',
                hotkey: 'Enter',
                callback: () => scope.editElement(node, index)
            });
        }
    }, {
        id: 'Delete',
        visible: (scope) => {
            let index = scope.focusedIndex - 1;
            return testNodes(scope.selectedNodes, function(node) {
                let handle = node.handles[index];
                return handle && xelib.GetIsRemoveable(handle);
            });
        },
        build: (scope, items) => {
            items.push({
                label: 'Delete',
                hotkey: 'Del',
                callback: () => scope.deleteElements()
            });
        }
    }, divider, {
        id: 'Copy',
        visible: () => { return true; },
        build: (scope, items) => {
            let node = scope.selectedNodes.last();
            items.push({
                label: 'Copy',
                hotkey: 'Ctrl+C',
                disabled: true,//!node,
                callback: () => scope.copyNodes()
            })
        }
    }, {
        id: 'Copy Path',
        visible: () => { return true; },
        build: (scope, items) => {
            let node = scope.selectedNodes.last();
            items.push({
                label: 'Copy Path',
                hotkey: 'Ctrl+Shift+C',
                disabled: true,//!node,
                callback: () => scope.copyNodePaths()
            })
        }
    }, {
        id: 'Paste',
        visible: () => { return true },
        build: (scope, items) => {
            items.push({
                label: 'Paste',
                hotkey: 'Ctrl+V',
                disabled: true,//!scope.canPaste(),
                callback: () => scope.paste()
            })
        }
    }]
});<|endoftext|>


<commit_before>/*!
 * Collection
 * https://github.com/kobezzza/Collection
 *
 * Released under the MIT license
 * https://github.com/kobezzza/Collection/blob/master/LICENSE
 */

describe('Collection methods with streams', () => {
	if (typeof window !== 'undefined') {
		return;
	}

	const
		{Transform} = require('stream');

	function generateStream(i, error = false, method = 'push') {
		const stream = new Transform({
			readableObjectMode: true,
			writableObjectMode: true,
			transform(chunk, enc, cb) {
				cb(null, chunk);
			}
		});

		let
			j = 0;

		(function loop() {
			setTimeout(() => {
				if (i--) {
					if (error && j) {
						stream.destroy(error);
						return;
					}

					stream[method](j++);
					loop();

				} else {
					stream.end();
				}
			}, 50);
		})();

		return stream;
	}

	it('map from a stream', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(length)).map((el) => el * 2)
			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('async map from a stream', async (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(length)).async.map((el) => Promise.resolve(el * 2))
			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('map from a stream with a water mark', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C([0, 1, 2, 3, 4])
			.to(new Transform({
				readableObjectMode: true,
				writableObjectMode: true,
				highWaterMark: 2,
				transform(chunk, enc, cb) {
					setImmediate(() => cb(null, chunk));
				}
			}))

			.map((el) => el * 2)
			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('map from a stream with an error', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler'),
			errorHandler = jasmine.createSpy('errorHandler');

		const
			err = new Error('boom!');

		$C(generateStream(length)).map((el, i) => {
			if (i) {
				throw err;
			}

			return el * 2;
		})
			.addListener('data', dataHandler)
			.addListener('error', (err) => {
				errorHandler(err);
			})

			.addListener('close', () => setImmediate(() => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);

				expect(errorHandler).toHaveBeenCalledTimes(1);
				expect(errorHandler).toHaveBeenCalledWith(err);
				done();
			}));
	});

	it('map from a stream with a readable error', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler'),
			errorHandler = jasmine.createSpy('errorHandler');

		const
			err = new Error('boom!');

		$C(generateStream(length, err)).map((el, i) => {
			if (i) {
				throw err;
			}

			return el * 2;
		})
			.addListener('data', dataHandler)
			.addListener('error', errorHandler)
			.addListener('close', () => setImmediate(() => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);

				expect(errorHandler).toHaveBeenCalledTimes(1);
				expect(errorHandler).toHaveBeenCalledWith(err);
				done();
			}));
	});

	it('map from a stream with a writable error', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler'),
			errorHandler = jasmine.createSpy('errorHandler');

		const
			err = new Error('boom!');

		let
			i = 0;

		$C(generateStream(length))
			.to(new Transform({
				readableObjectMode: true,
				writableObjectMode: true,
				highWaterMark: 2,
				transform(chunk, enc, cb) {
					if (i++) {
						cb(err);
						return;
					}

					setImmediate(() => cb(null, chunk));
				}
			}))

			.map((el) => el * 2)

			.addListener('data', dataHandler)
			.addListener('error', errorHandler)
			.addListener('close', () => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);

				expect(errorHandler).toHaveBeenCalledTimes(1);
				expect(errorHandler).toHaveBeenCalledWith(err);
				done();
			});
	});

	it('map from a stream with a write error', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler'),
			errorHandler = jasmine.createSpy('errorHandler');

		const
			err = new Error('boom!');

		let
			i = 0;

		$C(generateStream(length))
			.to(new Transform({
				readableObjectMode: true,
				writableObjectMode: true,
				highWaterMark: 2,
				transform(chunk, enc, cb) {
					if (i++) {
						throw err;
					}

					setImmediate(() => cb(null, chunk));
				}
			}))

			.map((el) => el * 2)

			.addListener('data', dataHandler)
			.addListener('error', errorHandler)
			.addListener('close', () => setImmediate(() => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);

				expect(errorHandler).toHaveBeenCalledTimes(1);
				expect(errorHandler).toHaveBeenCalledWith(err);
				done();
			}));
	});

	it('map from a stream with closing', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler'),
			errorHandler = jasmine.createSpy('errorHandler');

		let
			i = 0;

		$C(generateStream(length))
			.to(new Transform({
				readableObjectMode: true,
				writableObjectMode: true,
				highWaterMark: 2,
				transform(chunk, enc, cb) {
					if (i++) {
						this.destroy();
						return;
					}

					setImmediate(() => cb(null, chunk));
				}
			}))

			.map((el) => el * 2)

			.addListener('data', dataHandler)
			.addListener('error', errorHandler)
			.addListener('close', () => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(errorHandler).toHaveBeenCalledTimes(0);
				done();
			});
	});

	it('map from a stream with break', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(length))
			.map((el, i, data, o) => {
				if (i) {
					return o.break;
				}

				return el * 2;
			})

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);
				done();
			});
	});

	it('map from a write stream', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(length, false, 'write')).map((el) => el * 2)
			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('map from a stream with parameters', (done) => {
		const
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(9))
			.filter((el) => el % 2)
			.from(1)
			.count(2)
			.map()

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(2);
				expect(dataHandler).toHaveBeenCalledWith(3);
				expect(dataHandler).toHaveBeenCalledWith(5);
				done();
			});
	});

	it('map from a stream with .one helper', (done) => {
		const
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(5))
			.one
			.map()

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).not.toHaveBeenCalledWith(1);
				done();
			});
	});

	it('reduce from a stream to an array', async () => {
		expect(
			await $C(generateStream(3)).to([]).reduce((res, el) => {
				res.push(el * 2);
				return res;
			})
		).toEqual([0, 2, 4]);
	});

	it('reduce from a stream to a stream', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(length)).toStream().reduce((res, el) => {
			res.write(el * 2);
			return res;
		})
			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('map from reduce from a stream to a stream', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(
			$C(generateStream(length)).toStream().reduce((res, el) => {
				res.write(el);
				return res;
			})
		)
			.map((el) => el * 2)

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('reduce from a stream to an array with parameters', async () => {
		expect(
			await $C(generateStream(9))
				.filter((el) => el % 2)
				.from(1)
				.count(2)
				.to([])
				.reduce((res, el) => {
					res.push(el);
					return res;
				})
		).toEqual([3, 5]);
	});

	it('reduce from a stream to a stream with parameters', (done) => {
		const
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(9))
			.filter((el) => el % 2)
			.from(1)
			.count(2)
			.toStream()
			.reduce((res, el) => {
				res.write(el);
				return res;
			})

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(2);
				expect(dataHandler).toHaveBeenCalledWith(3);
				expect(dataHandler).toHaveBeenCalledWith(5);
				done();
			});
	});

	it('reduce from a stream with .one helper', (done) => {
		const
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(5))
			.one
			.toStream()
			.reduce((res, el) => {
				res.write(el);
				return res;
			})

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).not.toHaveBeenCalledWith(1);
				done();
			});
	});

	it('race map with a stream', (done) => {
		const
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(5))
			.race(2)
			.map()

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(2);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(1);
				done();
			});
	});
});
<commit_msg>Fixed node 10<commit_after>/*!
 * Collection
 * https://github.com/kobezzza/Collection
 *
 * Released under the MIT license
 * https://github.com/kobezzza/Collection/blob/master/LICENSE
 */

describe('Collection methods with streams', () => {
	if (typeof window !== 'undefined') {
		return;
	}

	const
		{Transform} = require('stream');

	function generateStream(i, error = false, method = 'push') {
		const stream = new Transform({
			readableObjectMode: true,
			writableObjectMode: true,
			transform(chunk, enc, cb) {
				cb(null, chunk);
			}
		});

		let
			j = 0;

		(function loop() {
			setTimeout(() => {
				if (i--) {
					if (error && j) {
						stream.destroy(error);
						return;
					}

					stream[method](j++);
					loop();

				} else {
					stream.end();
				}
			}, 50);
		})();

		return stream;
	}

	it('map from a stream', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(length)).map((el) => el * 2)
			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('async map from a stream', async (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(length)).async.map((el) => Promise.resolve(el * 2))
			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('map from a stream with a water mark', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C([0, 1, 2, 3, 4])
			.to(new Transform({
				readableObjectMode: true,
				writableObjectMode: true,
				highWaterMark: 2,
				transform(chunk, enc, cb) {
					setImmediate(() => cb(null, chunk));
				}
			}))

			.map((el) => el * 2)
			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('map from a stream with an error', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler'),
			errorHandler = jasmine.createSpy('errorHandler');

		const
			err = new Error('boom!');

		$C(generateStream(length)).map((el, i) => {
			if (i) {
				throw err;
			}

			return el * 2;
		})
			.addListener('data', dataHandler)
			.addListener('error', (err) => {
				errorHandler(err);
			})

			.addListener('close', () => setImmediate(() => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);

				expect(errorHandler).toHaveBeenCalledTimes(1);
				expect(errorHandler).toHaveBeenCalledWith(err);
				done();
			}));
	});

	it('map from a stream with a readable error', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler'),
			errorHandler = jasmine.createSpy('errorHandler');

		const
			err = new Error('boom!');

		$C(generateStream(length, err)).map((el, i) => {
			if (i) {
				throw err;
			}

			return el * 2;
		})
			.addListener('data', dataHandler)
			.addListener('error', errorHandler)
			.addListener('close', () => setImmediate(() => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);

				expect(errorHandler).toHaveBeenCalledTimes(1);
				expect(errorHandler).toHaveBeenCalledWith(err);
				done();
			}));
	});

	it('map from a stream with a writable error', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler'),
			errorHandler = jasmine.createSpy('errorHandler');

		const
			err = new Error('boom!');

		let
			i = 0;

		$C(generateStream(length))
			.to(new Transform({
				readableObjectMode: true,
				writableObjectMode: true,
				highWaterMark: 2,
				transform(chunk, enc, cb) {
					if (i++) {
						cb(err);
						return;
					}

					setImmediate(() => cb(null, chunk));
				}
			}))

			.map((el) => el * 2)

			.addListener('data', dataHandler)
			.addListener('error', errorHandler)
			.addListener('close', () => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(errorHandler).toHaveBeenCalledWith(err);
				done();
			});
	});

	it('map from a stream with a write error', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler'),
			errorHandler = jasmine.createSpy('errorHandler');

		const
			err = new Error('boom!');

		let
			i = 0;

		$C(generateStream(length))
			.to(new Transform({
				readableObjectMode: true,
				writableObjectMode: true,
				highWaterMark: 2,
				transform(chunk, enc, cb) {
					if (i++) {
						throw err;
					}

					setImmediate(() => cb(null, chunk));
				}
			}))

			.map((el) => el * 2)

			.addListener('data', dataHandler)
			.addListener('error', errorHandler)
			.addListener('close', () => setImmediate(() => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);

				expect(errorHandler).toHaveBeenCalledTimes(1);
				expect(errorHandler).toHaveBeenCalledWith(err);
				done();
			}));
	});

	it('map from a stream with closing', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler'),
			errorHandler = jasmine.createSpy('errorHandler');

		let
			i = 0;

		$C(generateStream(length))
			.to(new Transform({
				readableObjectMode: true,
				writableObjectMode: true,
				highWaterMark: 2,
				transform(chunk, enc, cb) {
					if (i++) {
						this.destroy();
						return;
					}

					setImmediate(() => cb(null, chunk));
				}
			}))

			.map((el) => el * 2)

			.addListener('data', dataHandler)
			.addListener('error', errorHandler)
			.addListener('close', () => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(errorHandler).toHaveBeenCalledTimes(0);
				done();
			});
	});

	it('map from a stream with break', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(length))
			.map((el, i, data, o) => {
				if (i) {
					return o.break;
				}

				return el * 2;
			})

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);
				done();
			});
	});

	it('map from a write stream', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(length, false, 'write')).map((el) => el * 2)
			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('map from a stream with parameters', (done) => {
		const
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(9))
			.filter((el) => el % 2)
			.from(1)
			.count(2)
			.map()

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(2);
				expect(dataHandler).toHaveBeenCalledWith(3);
				expect(dataHandler).toHaveBeenCalledWith(5);
				done();
			});
	});

	it('map from a stream with .one helper', (done) => {
		const
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(5))
			.one
			.map()

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).not.toHaveBeenCalledWith(1);
				done();
			});
	});

	it('reduce from a stream to an array', async () => {
		expect(
			await $C(generateStream(3)).to([]).reduce((res, el) => {
				res.push(el * 2);
				return res;
			})
		).toEqual([0, 2, 4]);
	});

	it('reduce from a stream to a stream', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(length)).toStream().reduce((res, el) => {
			res.write(el * 2);
			return res;
		})
			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('map from reduce from a stream to a stream', (done) => {
		const
			length = 5,
			dataHandler = jasmine.createSpy('dataHandler');

		$C(
			$C(generateStream(length)).toStream().reduce((res, el) => {
				res.write(el);
				return res;
			})
		)
			.map((el) => el * 2)

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(length);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(2);
				expect(dataHandler).toHaveBeenCalledWith(4);
				expect(dataHandler).toHaveBeenCalledWith(6);
				expect(dataHandler).toHaveBeenCalledWith(8);
				expect(dataHandler).not.toHaveBeenCalledWith(10);
				done();
			});
	});

	it('reduce from a stream to an array with parameters', async () => {
		expect(
			await $C(generateStream(9))
				.filter((el) => el % 2)
				.from(1)
				.count(2)
				.to([])
				.reduce((res, el) => {
					res.push(el);
					return res;
				})
		).toEqual([3, 5]);
	});

	it('reduce from a stream to a stream with parameters', (done) => {
		const
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(9))
			.filter((el) => el % 2)
			.from(1)
			.count(2)
			.toStream()
			.reduce((res, el) => {
				res.write(el);
				return res;
			})

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(2);
				expect(dataHandler).toHaveBeenCalledWith(3);
				expect(dataHandler).toHaveBeenCalledWith(5);
				done();
			});
	});

	it('reduce from a stream with .one helper', (done) => {
		const
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(5))
			.one
			.toStream()
			.reduce((res, el) => {
				res.write(el);
				return res;
			})

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(1);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).not.toHaveBeenCalledWith(1);
				done();
			});
	});

	it('race map with a stream', (done) => {
		const
			dataHandler = jasmine.createSpy('dataHandler');

		$C(generateStream(5))
			.race(2)
			.map()

			.addListener('data', dataHandler)
			.addListener('end', () => {
				expect(dataHandler).toHaveBeenCalledTimes(2);
				expect(dataHandler).toHaveBeenCalledWith(0);
				expect(dataHandler).toHaveBeenCalledWith(1);
				done();
			});
	});
});
<|endoftext|>