<commit_before>"use strict";

mft = {
  // CONFIG ITEMS:

  STEMMING: 'porter', // doesn't do anything yet
  TOKENIZING: 'basic',// doesn't do anything yet

  EXTRACTED_TERMS_FIELD: '_extracted_terms',
  SEARCH_ALL_PSEUDO_FIELD: '$search', // magic "field name" that specifies we want a fulltext search
            // (abusing the '$' notation somewhat, which is often for search operators)
  SEARCH_ANY_PSEUDO_FIELD: '$searchany', // magic "field name" that specifies we want a fulltext search matching any, not all

  // WORKHORSE VARS:
  _STEM_FUNCTION: null,
  _TOKENIZE_FUNCTION: null
};

mft.indexedFieldsAndWeights = function(coll_name) {
  // we expect a special collection named '_fulltext_config', with items having elems 'collection_name', and 'fields',
  // with 'fields' having keys being the field name, and the values being the weight.
  //eg:
      // {
      //   collection_name: 'test',
      //   fields: {
      //     title: 5,
      //     content: 1
      //   }
      // }

  //> fc = {collection_name: 'gallery_collection_items', fields: {'title': 10, 'further_information': 1}}//
  // {
  //         "collection_name" : "gallery_collection_items",
  //         "fields" : {
  //                 "title" : 10,
  //                 "further_information" : 1
  //         }
  // }
  // > db.fulltext_config.save(fc)
  // >

  collection_conf = db.fulltext_config.findOne({collection_name: coll_name});
  return collection_conf.fields;
};

mft.search = function(coll_name, query_obj) {
  // check for $search member on query_obj
  // if it doesn't exist, pass through to regular .find
  // if it does, parse the ft query string, and add the appropriate filter
  // clause to the non-ft-search components, execute that, then
  // score every remaining document, and put those sorted IDs and scores in a record in
  // a private collection (hashed by the whole query obj, which we can check next time around)
  // then iterate through the IDs and scores and return the corrpesponding records with the IDs
  // attached to them, in a way that emulates a cursor object.
  var search_query_string;
  var require_all;
  if (query_obj[mft.SEARCH_ALL_PSEUDO_FIELD]) {
    search_query_string = query_obj[mft.SEARCH_ALL_PSEUDO_FIELD];
    require_all = true;
  } else if (query_obj[mft.SEARCH_ALL_PSEUDO_FIELD]) {
    search_query_string = query_obj[mft.SEARCH_ANY_PSEUDO_FIELD];
    require_all = false;
  } else {
    return db[coll_name].find(query_obj); // no need to call search, you chump
  }
  print("DEBUG: query string is " + search_query_string);
  var query_terms = mft.processQueryString(search_query_string);
  print("DEBUG: query terms is " + (query_terms.join(',') + " with length " + query_terms.length));
  query_obj[mft.EXTRACTED_TERMS_FIELD] = mft.filterArg(coll_name, query_terms, require_all);
  if (require_all) {
    delete(query_obj[mft.SEARCH_ALL_PSEUDO_FIELD]); // need to get rid f pseudo args, as they stop .find() from returning anything
  } else {
    delete(query_obj[mft.SEARCH_ANY_PSEUDO_FIELD]);
  }
  print("DEBUG: query_obj=" + tojson(query_obj));
  var filtered = db[coll_name].find(query_obj);
  var scores_and_ids = Array();
  print("DEBUG: num recs found: " + filtered.count());
  filtered.forEach(
    function(record) {
      var score = mft.scoreRecordAgainstQuery(coll_name, record, query_terms);
      scores_and_ids.push([score, record._id]);
    });
  return new mft.SearchPseudoCursor(coll_name, scores_and_ids);
  // scores_and_ids.sort(mft.sortNumericFirstDescending); // need to provide a custom search function anyway, as JS does sorts alphabetically
  // var scored_records = Array();
  // // this is the dodgy way - need to do a cursor in the future
  // for (var i = 0; i < scores_and_ids.length; i++) {
  //   var score_and_id = scores_and_ids[i];
  //   record = db[coll_name].findOne({_id: score_and_id[1]});
  //   record.score = score_and_id[0];
  //   scored_records.push(record);
  // }
  // return scored_records;
};

mft.sortNumericFirstDescending = function(a, b) {
  return b[0] - a[0];
};

mft.scoreRecordAgainstQuery = function(coll_name, record, query_terms) {
  var record_terms = record[mft.EXTRACTED_TERMS_FIELD];
  print("DEBUG: record=" + record);
  var query_terms_set = {};
  var score = 0.0;
  for (var i = 0; i < query_terms.length; i++) {
    query_terms_set[query_terms[i]] = true; // to avoid needing to iterate
  }
  print("DEBUG: query_terms_set=" + tojson(query_terms_set));
  var idf_cache = {};
  for (var j = 0; j < record_terms.length; j++) {
    var term = record_terms[j];
    if (term in query_terms_set) {
      var term_idf = idf_cache[term];
      if (term_idf === undefined) {
        term_idf = mft.getTermIdf(coll_name, term);
        idf_cache[term] = term_idf;
      }
      score += term_idf;
    }
  }
  return score/Math.sqrt(record_terms.length);
  // for cosine similarity, we should be normalizing the document vector against the sqrt of the sums of the sqares of all term
  // but that would require knowing the IDF scores of all terms, rather than just the ones in the query which is potentially
  // expensive unless we precompute (either the IDFs or normalized vector for each doc).
  /// However that would make updating the index a much bigger can of worms.
  // This should provide a nice approximation that will give decent results at least relative to the query, which is all we care about.
};

mft.calcTermIdf = function(coll_name, term) {
  // this currently doesn't have any caching smarts.
  // we could cache the IDF for each doc in the collection, but that would make updating more complicated
  // for the moment I'll gamble on mongodb being quick enough to make it not a problem
  //
  var term_filter_obj = {};
  term_filter_obj[mft.EXTRACTED_TERMS_FIELD] = mft.filterArg(coll_name, [term], true);
  var term_count = db[coll_name].find(term_filter_obj).count();
  if (term_count === 0) { return 0.0; }
  var num_docs = db[coll_name].find().count(); // TODO: memoize, or find a better method for getting this
  return Math.log(num_docs) - Math.log(term_count);
};

mft.getTermIdf = function(coll_name, term) {
  var score_record = db.fulltext_term_scores.findOne({collection_name: coll_name, term: term});
  print("DEBUG: score_record=" + tojson(score_record));
  if (score_record === null) {
    print("WARNING: no score cached for term " + term);
    return 0.0;
  } else {
    if (score_record.dirty) {
      print("WARNING: score for term " + term + " may be incorrect");
    }
    return score_record.score;
  }
};

mft.calcAndStoreTermIdf = function(coll_name, term) {
  idf_score = mft.calcTermIdf(coll_name, term);
  print("DEBUG: calculated IDF for term " + term + " as " + idf_score);
  db.fulltext_term_scores.update({collection_name: coll_name, term: term}, {collection_name: coll_name, term: term, score: idf_score, dirty: false}, {upsert: true});
};

mft.storeEmptyTermIdf = function(coll_name, term) {
  // adds the term into the index if it's not already there, but marks it as dirty
  print("DEBUG: storing empty score for term " + term);
  db.fulltext_term_scores.update({collection_name: coll_name, term: term}, {collection_name: coll_name, term: term, dirty: true}, {upsert: true});
};

mft.filterArg = function(coll_name, query_terms, require_all) {
  if (require_all === undefined) {
    require_all = true;
  }
  var filter_obj = {};
  filter_obj[require_all ? '$all' : '$in'] = query_terms;
  return filter_obj;
};

mft.processQueryString = function(query_string) {
  return mft.stemAndTokenize(query_string); // maybe tokenizing should be different for queries?
};

mft.indexAll = function(coll_name) {
  print("DEBUG: indexing all records in " + coll_name);
  var cur = db[coll_name].find();
  indexed_fields = mft.indexedFieldsAndWeights(coll_name);
  print("DEBUG: indexed fields and weights: " + tojson(indexed_fields));
  cur.forEach(function(x) { mft.indexSingleRecord(coll_name, x, indexed_fields, false); });
  mft.fillDirtyIdfScores(coll_name);
};

mft.indexSingleRecord = function(coll_name, record, indexed_fields, calculate_idf) {
  if (typeof indexed_fields === 'undefined') {// we can pass this in to save CPU in bulk indexing, but might not
    indexed_fields = mft.indexedFieldsAndWeights(coll_name);
  }
  if (typeof calculate_idf === 'undefined') {
    calculate_idf = true; // assume we're just indexing this one doc - so we probably want to cal at the time
  }
  var all_extracted_terms = Array();
  for (var field in indexed_fields) {
    all_extracted_terms = all_extracted_terms.concat(mft.extractFieldTokens(coll_name, record, field, indexed_fields[field]));
  }
  record[mft.EXTRACTED_TERMS_FIELD] = all_extracted_terms;
  print("DEBUG: record is now: " + tojson(record));
  db[coll_name].save(record);
  if (calculate_idf) { // if we're doing just one doc
    all_extracted_terms.forEach(function(x) {mft.calcAndStoreTermIdf(coll_name, x);});
  } else { // we're doing it in bulk, so defer calcs until later
    all_extracted_terms.forEach(function(x) {mft.storeEmptyTermIdf(coll_name, x);});
  }
};

mft.indexSingleRecordFromId = function(coll_name, record_id) {
  var rec = db[coll_name].findOne({'_id': record_id});
  mft.indexSingleRecord(coll_name, rec);
};

mft.fillDirtyIdfScores = function(coll_name) {
  var cur = db.fulltext_term_scores.find({collection_name: coll_name, dirty: true});
  cur.forEach( function(x) { mft.calcAndStoreTermIdf(coll_name, x.term); });
};

mft.extractFieldTokens = function(coll_name, record, field, upweighting) {
  // extracts tokens in stemmed and tokenised form and upweights them as specified in the config if necessary
  var contents = record[field];
  if (typeof contents == 'object') {
    contents = contents.join(" ");
  }
  print("DEBUG: contents for field " + field + ": " + contents);
  if (!contents) { // eg the field doesn't exist on this particular record, we silently fail
    return;
  }
  var processed_contents = mft.stemAndTokenize(contents);
  if (upweighting == 1) { // special -casing for the common case - may be slightly quicker avoiding the array copy
    return processed_contents;
  } else {
    var upweighted_contents = processed_contents;
    for (var i = 1; i < upweighting; i++) {
      upweighted_contents = upweighted_contents.concat(processed_contents);
    }
    return upweighted_contents; // this upweighting shouldn't damage our scores as long as we TF IDF, since IDF won't be affect by linear multipliers
  }
};

mft.stemAndTokenize = function(field_contents) {
  return mft.stem(mft.tokenize(field_contents.toLowerCase())); //TODO: actually stem as promised
};

mft.tokenizeBasic = function(field_contents) {
  var token_re = /\b(\w[\w'-]*\w|\w)\b/g;
  return field_contents.match(token_re);
};

mft.stem = function(field_tokens) {
  var stem_fn = mft.getStemFunction();
  var stemmed = Array();
  for (var i = 0; i < field_tokens.length; i++) {
    stemmed.push(stem_fn(field_tokens[i]));
  }
  return stemmed;
};

mft.tokenize = function(field_contents) {
  var tokenize_fn = mft.getTokenizeFunction();
  return tokenize_fn(field_contents);
};

mft.getStemFunction = function() {
  if (mft._STEM_FUNCTION) {
    return mft._STEM_FUNCTION;
  } else {
    if (mft.STEMMING == 'porter') { // no others available
      return (mft._STEM_FUNCTION = mft_stemming.porterStemmerCreator()); // porterStemmer() returns a porter stemming fn when called
    }
  }
};

mft.getTokenizeFunction = function() {
  if (mft._TOKENIZE_FUNCTION) {
    return mft._TOKENIZE_FUNCTION;
  } else {
    if (mft.TOKENIZING == 'basic') { // no others available
      return (mft._TOKENIZE_FUNCTION = mft.tokenizeBasic);
    }
  }
};

mft.SearchPseudoCursor = function(coll_name, scores_and_ids) {
  // class to vaguely efficiently act as a store for the the retrived records while not chewing up lots of
  // memory, and not taking lots of time to sort results we may not need - hence the heap
  this.coll_name = coll_name;
  var scores_and_ids_heap = new mft_heap.BinaryHeap(function(x) { return -x[0]; });
  // print("DEBUG: score functino running: " + scores_and_ids_heap.scoreFunction([[1, 2], [3,1]]);
  scores_and_ids.forEach( function(x) {
    scores_and_ids_heap.push(x); // in-place would be better, but let's leave that unless we think it would be useful
  });
  this.scores_and_ids_heap = scores_and_ids_heap;

  this.hasNext = function() {
    return this.scores_and_ids_heap.size() > 0;
  };

  this.next = function() {
    return this.fetchScoredRecord(this.scores_and_ids_heap.pop());
  };

  this.toArray = function() {
    output = [];
    while (this.hasNext()) {
      output.push(this.next());
    }
    return output;
  };

  this.fetchById = function(record_id) {
    return db[this.coll_name].findOne({_id: record_id});
  };

  this.fetchScoredRecord = function(score_and_id) {
    rec = this.fetchById(score_and_id[1]);
    rec.score = score_and_id[0];
    return rec;
  };

};

_all = {
  mft: mft
};
  <commit_msg>commented out some debug code<commit_after>"use strict";

mft = {
  // CONFIG ITEMS:

  STEMMING: 'porter', // doesn't do anything yet
  TOKENIZING: 'basic',// doesn't do anything yet

  EXTRACTED_TERMS_FIELD: '_extracted_terms',
  SEARCH_ALL_PSEUDO_FIELD: '$search', // magic "field name" that specifies we want a fulltext search
            // (abusing the '$' notation somewhat, which is often for search operators)
  SEARCH_ANY_PSEUDO_FIELD: '$searchany', // magic "field name" that specifies we want a fulltext search matching any, not all

  // WORKHORSE VARS:
  _STEM_FUNCTION: null,
  _TOKENIZE_FUNCTION: null
};

mft.indexedFieldsAndWeights = function(coll_name) {
  // we expect a special collection named '_fulltext_config', with items having elems 'collection_name', and 'fields',
  // with 'fields' having keys being the field name, and the values being the weight.
  //eg:
      // {
      //   collection_name: 'test',
      //   fields: {
      //     title: 5,
      //     content: 1
      //   }
      // }

  //> fc = {collection_name: 'gallery_collection_items', fields: {'title': 10, 'further_information': 1}}//
  // {
  //         "collection_name" : "gallery_collection_items",
  //         "fields" : {
  //                 "title" : 10,
  //                 "further_information" : 1
  //         }
  // }
  // > db.fulltext_config.save(fc)
  // >

  collection_conf = db.fulltext_config.findOne({collection_name: coll_name});
  return collection_conf.fields;
};

mft.search = function(coll_name, query_obj) {
  // check for $search member on query_obj
  // if it doesn't exist, pass through to regular .find
  // if it does, parse the ft query string, and add the appropriate filter
  // clause to the non-ft-search components, execute that, then
  // score every remaining document, and put those sorted IDs and scores in a record in
  // a private collection (hashed by the whole query obj, which we can check next time around)
  // then iterate through the IDs and scores and return the corrpesponding records with the IDs
  // attached to them, in a way that emulates a cursor object.
  var search_query_string;
  var require_all;
  if (query_obj[mft.SEARCH_ALL_PSEUDO_FIELD]) {
    search_query_string = query_obj[mft.SEARCH_ALL_PSEUDO_FIELD];
    require_all = true;
  } else if (query_obj[mft.SEARCH_ALL_PSEUDO_FIELD]) {
    search_query_string = query_obj[mft.SEARCH_ANY_PSEUDO_FIELD];
    require_all = false;
  } else {
    return db[coll_name].find(query_obj); // no need to call search, you chump
  }
  print("DEBUG: query string is " + search_query_string);
  var query_terms = mft.processQueryString(search_query_string);
  print("DEBUG: query terms is " + (query_terms.join(',') + " with length " + query_terms.length));
  query_obj[mft.EXTRACTED_TERMS_FIELD] = mft.filterArg(coll_name, query_terms, require_all);
  if (require_all) {
    delete(query_obj[mft.SEARCH_ALL_PSEUDO_FIELD]); // need to get rid f pseudo args, as they stop .find() from returning anything
  } else {
    delete(query_obj[mft.SEARCH_ANY_PSEUDO_FIELD]);
  }
  print("DEBUG: query_obj=" + tojson(query_obj));
  var filtered = db[coll_name].find(query_obj);
  var scores_and_ids = Array();
  print("DEBUG: num recs found: " + filtered.count());
  filtered.forEach(
    function(record) {
      var score = mft.scoreRecordAgainstQuery(coll_name, record, query_terms);
      scores_and_ids.push([score, record._id]);
    });
  return new mft.SearchPseudoCursor(coll_name, scores_and_ids);
  // scores_and_ids.sort(mft.sortNumericFirstDescending); // need to provide a custom search function anyway, as JS does sorts alphabetically
  // var scored_records = Array();
  // // this is the dodgy way - need to do a cursor in the future
  // for (var i = 0; i < scores_and_ids.length; i++) {
  //   var score_and_id = scores_and_ids[i];
  //   record = db[coll_name].findOne({_id: score_and_id[1]});
  //   record.score = score_and_id[0];
  //   scored_records.push(record);
  // }
  // return scored_records;
};

mft.sortNumericFirstDescending = function(a, b) {
  return b[0] - a[0];
};

mft.scoreRecordAgainstQuery = function(coll_name, record, query_terms) {
  var record_terms = record[mft.EXTRACTED_TERMS_FIELD];
  print("DEBUG: record=" + record);
  var query_terms_set = {};
  var score = 0.0;
  for (var i = 0; i < query_terms.length; i++) {
    query_terms_set[query_terms[i]] = true; // to avoid needing to iterate
  }
  print("DEBUG: query_terms_set=" + tojson(query_terms_set));
  var idf_cache = {};
  for (var j = 0; j < record_terms.length; j++) {
    var term = record_terms[j];
    if (term in query_terms_set) {
      var term_idf = idf_cache[term];
      if (term_idf === undefined) {
        term_idf = mft.getTermIdf(coll_name, term);
        idf_cache[term] = term_idf;
      }
      score += term_idf;
    }
  }
  return score/Math.sqrt(record_terms.length);
  // for cosine similarity, we should be normalizing the document vector against the sqrt of the sums of the sqares of all term
  // but that would require knowing the IDF scores of all terms, rather than just the ones in the query which is potentially
  // expensive unless we precompute (either the IDFs or normalized vector for each doc).
  /// However that would make updating the index a much bigger can of worms.
  // This should provide a nice approximation that will give decent results at least relative to the query, which is all we care about.
};

mft.calcTermIdf = function(coll_name, term) {
  // this currently doesn't have any caching smarts.
  // we could cache the IDF for each doc in the collection, but that would make updating more complicated
  // for the moment I'll gamble on mongodb being quick enough to make it not a problem
  //
  var term_filter_obj = {};
  term_filter_obj[mft.EXTRACTED_TERMS_FIELD] = mft.filterArg(coll_name, [term], true);
  var term_count = db[coll_name].find(term_filter_obj).count();
  if (term_count === 0) { return 0.0; }
  var num_docs = db[coll_name].find().count(); // TODO: memoize, or find a better method for getting this
  return Math.log(num_docs) - Math.log(term_count);
};

mft.getTermIdf = function(coll_name, term) {
  var score_record = db.fulltext_term_scores.findOne({collection_name: coll_name, term: term});
  print("DEBUG: score_record=" + tojson(score_record));
  if (score_record === null) {
    print("WARNING: no score cached for term " + term);
    return 0.0;
  } else {
    if (score_record.dirty) {
      print("WARNING: score for term " + term + " may be incorrect");
    }
    return score_record.score;
  }
};

mft.calcAndStoreTermIdf = function(coll_name, term) {
  idf_score = mft.calcTermIdf(coll_name, term);
  // print("DEBUG: calculated IDF for term " + term + " as " + idf_score);
  db.fulltext_term_scores.update({collection_name: coll_name, term: term}, {collection_name: coll_name, term: term, score: idf_score, dirty: false}, {upsert: true});
};

mft.storeEmptyTermIdf = function(coll_name, term) {
  // adds the term into the index if it's not already there, but marks it as dirty
  db.fulltext_term_scores.update({collection_name: coll_name, term: term}, {collection_name: coll_name, term: term, dirty: true}, {upsert: true});
};

mft.filterArg = function(coll_name, query_terms, require_all) {
  if (require_all === undefined) {
    require_all = true;
  }
  var filter_obj = {};
  filter_obj[require_all ? '$all' : '$in'] = query_terms;
  return filter_obj;
};

mft.processQueryString = function(query_string) {
  return mft.stemAndTokenize(query_string); // maybe tokenizing should be different for queries?
};

mft.indexAll = function(coll_name) {
  print("DEBUG: indexing all records in " + coll_name);
  var cur = db[coll_name].find();
  indexed_fields = mft.indexedFieldsAndWeights(coll_name);
  print("DEBUG: indexed fields and weights: " + tojson(indexed_fields));
  recs_indexed = 0;
  cur.forEach(function(x) {
    mft.indexSingleRecord(coll_name, x, indexed_fields, false);
    recs_indexed++;
    if (recs_indexed % 100 == 0) {
      print(recs_indexed);
    }
  });
  mft.fillDirtyIdfScores(coll_name);
};

mft.indexSingleRecord = function(coll_name, record, indexed_fields, calculate_idf) {
  if (typeof indexed_fields === 'undefined') {// we can pass this in to save CPU in bulk indexing, but might not
    indexed_fields = mft.indexedFieldsAndWeights(coll_name);
  }
  if (typeof calculate_idf === 'undefined') {
    calculate_idf = true; // assume we're just indexing this one doc - so we probably want to cal at the time
  }
  var all_extracted_terms = Array();
  for (var field in indexed_fields) {
    all_extracted_terms = all_extracted_terms.concat(mft.extractFieldTokens(coll_name, record, field, indexed_fields[field]));
  }
  record[mft.EXTRACTED_TERMS_FIELD] = all_extracted_terms;
  // print("DEBUG: record is now: " + tojson(record));
  db[coll_name].save(record);
  if (calculate_idf) { // if we're doing just one doc
    all_extracted_terms.forEach(function(x) {mft.calcAndStoreTermIdf(coll_name, x);});
  } else { // we're doing it in bulk, so defer calcs until later
    all_extracted_terms.forEach(function(x) {mft.storeEmptyTermIdf(coll_name, x);});
  }
};

mft.indexSingleRecordFromId = function(coll_name, record_id) {
  var rec = db[coll_name].findOne({'_id': record_id});
  mft.indexSingleRecord(coll_name, rec);
};

mft.fillDirtyIdfScores = function(coll_name) {
  var cur = db.fulltext_term_scores.find({collection_name: coll_name, dirty: true});
  cur.forEach( function(x) { mft.calcAndStoreTermIdf(coll_name, x.term); });
};

mft.extractFieldTokens = function(coll_name, record, field, upweighting) {
  // extracts tokens in stemmed and tokenised form and upweights them as specified in the config if necessary
  var contents = record[field];
  if (typeof contents == 'object') {
    contents = contents.join(" ");
  }
  if (!contents) { // eg the field doesn't exist on this particular record, we silently fail
    return;
  }
  var processed_contents = mft.stemAndTokenize(contents);
  if (upweighting == 1) { // special -casing for the common case - may be slightly quicker avoiding the array copy
    return processed_contents;
  } else {
    var upweighted_contents = processed_contents;
    for (var i = 1; i < upweighting; i++) {
      upweighted_contents = upweighted_contents.concat(processed_contents);
    }
    return upweighted_contents; // this upweighting shouldn't damage our scores as long as we TF IDF, since IDF won't be affect by linear multipliers
  }
};

mft.stemAndTokenize = function(field_contents) {
  return mft.stem(mft.tokenize(field_contents.toLowerCase())); //TODO: actually stem as promised
};

mft.tokenizeBasic = function(field_contents) {
  var token_re = /\b(\w[\w'-]*\w|\w)\b/g;
  return field_contents.match(token_re);
};

mft.stem = function(field_tokens) {
  var stem_fn = mft.getStemFunction();
  var stemmed = Array();
  for (var i = 0; i < field_tokens.length; i++) {
    stemmed.push(stem_fn(field_tokens[i]));
  }
  return stemmed;
};

mft.tokenize = function(field_contents) {
  var tokenize_fn = mft.getTokenizeFunction();
  return tokenize_fn(field_contents);
};

mft.getStemFunction = function() {
  if (mft._STEM_FUNCTION) {
    return mft._STEM_FUNCTION;
  } else {
    if (mft.STEMMING == 'porter') { // no others available
      return (mft._STEM_FUNCTION = mft_stemming.porterStemmerCreator()); // porterStemmer() returns a porter stemming fn when called
    }
  }
};

mft.getTokenizeFunction = function() {
  if (mft._TOKENIZE_FUNCTION) {
    return mft._TOKENIZE_FUNCTION;
  } else {
    if (mft.TOKENIZING == 'basic') { // no others available
      return (mft._TOKENIZE_FUNCTION = mft.tokenizeBasic);
    }
  }
};

mft.SearchPseudoCursor = function(coll_name, scores_and_ids) {
  // class to vaguely efficiently act as a store for the the retrived records while not chewing up lots of
  // memory, and not taking lots of time to sort results we may not need - hence the heap
  this.coll_name = coll_name;
  var scores_and_ids_heap = new mft_heap.BinaryHeap(function(x) { return -x[0]; });
  // print("DEBUG: score functino running: " + scores_and_ids_heap.scoreFunction([[1, 2], [3,1]]);
  scores_and_ids.forEach( function(x) {
    scores_and_ids_heap.push(x); // in-place would be better, but let's leave that unless we think it would be useful
  });
  this.scores_and_ids_heap = scores_and_ids_heap;

  this.hasNext = function() {
    return this.scores_and_ids_heap.size() > 0;
  };

  this.next = function() {
    return this.fetchScoredRecord(this.scores_and_ids_heap.pop());
  };

  this.toArray = function() {
    output = [];
    while (this.hasNext()) {
      output.push(this.next());
    }
    return output;
  };

  this.fetchById = function(record_id) {
    return db[this.coll_name].findOne({_id: record_id});
  };

  this.fetchScoredRecord = function(score_and_id) {
    rec = this.fetchById(score_and_id[1]);
    rec.score = score_and_id[0];
    return rec;
  };

};

_all = {
  mft: mft
};
  <|endoftext|>


<commit_before>var templateHtml = require( '../templates/dirPickerSettingVariable.html' );
var DirPickerSettingVariableRowView = require( './dirPickerSettingVariableRow' );

//noinspection JSUnusedGlobalSymbols
var ViewsDirPickerSettingVariable = Backbone.Marionette.CompositeView.extend( {
  model             : undefined,//variableModel
  collection        : undefined,//variableModel.listCollection
  template          : templateHtml,
  childView         : DirPickerSettingVariableRowView,
  childViewContainer: '.js-variable-row-list-container',
  className         : 'col-lg-4 col-md-6',
  reorderOnSort     : true,
  ui                : {
    name              : '.js-variable-name',
    nameInput         : '.js-variable-name-input',
    delBtn            : '.js-variable-del-btn',
    handle            : '.js-variable-handle',
    rowHandle         : '.js-variable-row-handle',
    childViewContainer: '.js-variable-row-list-container',
    addBtn            : '.js-variable-row-add-btn',
    closeBtn          : '.js-variable-row-list-close-btn',
    badge             : '.js-variable-list-num-badge',
    collapse          : '.collapse'
  },
  events            : {
    'click @ui.name'          : 'onClickName',
    'keyup @ui.nameInput'     : 'onKeyupNameInput',
    'focusout @ui.nameInput'  : 'changeNameInput',
    'click @ui.delBtn'        : 'onClickDelBtn',
    'click @ui.addBtn'        : 'onClickAddBtn',
    'click @ui.closeBtn'      : 'onClickCloseBtn',
    'mouseenter @ui.rowHandle': 'sortStart',
    'mouseleave @ui.rowHandle': 'sortDestroy'

  },
  modelEvents       : {
    'change:name': 'redrawName',
    'badgeUpdate': 'badgeUpdate'
  },

  /**
   * renderし直したくないんで
   */
  badgeUpdate        : function () {
    this.ui.badge.text( this.collection.length );
  },
  onClickCloseBtn    : function () {
    this.ui.collapse.collapse( 'toggle' );
  },
  initialize         : function () {
    //this.debugEvents('VariableView');
    this.collection = this.model.listCollection
  },
  sortStart          : function () {
    var _self = this;
    this.ui.childViewContainer.sortable( {
      forcePlaceholderSize: true,
      handle              : '.js-variable-row-handle'
    } ).bind( 'sortupdate', function () {
      //console.log("variablesSort");
      _self.updateItemSortIndex( _self.ui.childViewContainer.find( '.js-variable-row-model-id' ) );
    } );
  },
  sortDestroy        : function () {
    if (this.ui.childViewContainer.sortable) {
      this.ui.childViewContainer.sortable( 'destroy' );
    }
  },
  onRender           : function () {
    this.ui.handle.tooltip();
  },
  onBeforeDestroy    : function () {
    this.ui.handle.tooltip( 'destroy' );
    this.sortDestroy()
  },
  onKeyupNameInput   : function ( e ) {
    if (e.which == 13) {
      this.ui.nameInput.blur();
    }
  },
  onClickName        : function () {
    this.ui.name.addClass( 'hide' );
    this.ui.nameInput.removeClass( 'hide' );
    this.ui.nameInput.focus();
  },
  redrawName         : function () {
    this.ui.name.html( _.escape( this.model.get( 'name' ) ) );
    this.ui.name.removeClass( 'hide' );
    this.ui.nameInput.addClass( 'hide' );
  },
  onClickDelBtn      : function () {
    this.model.destroy();
  },
  changeNameInput    : function () {
    this.model.save( 'name', this.ui.nameInput.val(), {silent: false} );
    //this.model.trigger( 'change' );
  },
  onClickAddBtn      : function () {
    this.collection.add( {}, {wait: true} );
    this.ui.collapse.collapse( 'show' );
    this.onRender();
  },
  /**
   *
   * @param elements data-model-idの指定があるエレメントの配列
   */
  updateItemSortIndex: function ( elements ) {
    var _self = this;
    _.each( elements, function ( element, index ) {
      //noinspection JSUnresolvedVariable
      _self.collection.get( element.dataset.modelId ).set( 'sort', index, {silent: true} );
    } );
    this.model.updateList();
  }

} );

module.exports = ViewsDirPickerSettingVariable;
<commit_msg>Fix name input action on focusOut<commit_after>var templateHtml = require( '../templates/dirPickerSettingVariable.html' );
var DirPickerSettingVariableRowView = require( './dirPickerSettingVariableRow' );

//noinspection JSUnusedGlobalSymbols
var ViewsDirPickerSettingVariable = Backbone.Marionette.CompositeView.extend( {
  model             : undefined,//variableModel
  collection        : undefined,//variableModel.listCollection
  template          : templateHtml,
  childView         : DirPickerSettingVariableRowView,
  childViewContainer: '.js-variable-row-list-container',
  className         : 'col-lg-4 col-md-6',
  reorderOnSort     : true,
  ui                : {
    name              : '.js-variable-name',
    nameInput         : '.js-variable-name-input',
    delBtn            : '.js-variable-del-btn',
    handle            : '.js-variable-handle',
    rowHandle         : '.js-variable-row-handle',
    childViewContainer: '.js-variable-row-list-container',
    addBtn            : '.js-variable-row-add-btn',
    closeBtn          : '.js-variable-row-list-close-btn',
    badge             : '.js-variable-list-num-badge',
    collapse          : '.collapse'
  },
  events            : {
    'click @ui.name'          : 'onClickName',
    'keyup @ui.nameInput'     : 'onKeyupNameInput',
    'focusout @ui.nameInput'  : 'changeNameInput',
    'click @ui.delBtn'        : 'onClickDelBtn',
    'click @ui.addBtn'        : 'onClickAddBtn',
    'click @ui.closeBtn'      : 'onClickCloseBtn',
    'mouseenter @ui.rowHandle': 'sortStart',
    'mouseleave @ui.rowHandle': 'sortDestroy'

  },
  modelEvents       : {
    'change:name': 'redrawName',
    'badgeUpdate': 'badgeUpdate'
  },

  /**
   * renderし直したくないんで
   */
  badgeUpdate        : function () {
    this.ui.badge.text( this.collection.length );
  },
  onClickCloseBtn    : function () {
    this.ui.collapse.collapse( 'toggle' );
  },
  initialize         : function () {
    //this.debugEvents('VariableView');
    this.collection = this.model.listCollection
  },
  sortStart          : function () {
    var _self = this;
    this.ui.childViewContainer.sortable( {
      forcePlaceholderSize: true,
      handle              : '.js-variable-row-handle'
    } ).bind( 'sortupdate', function () {
      //console.log("variablesSort");
      _self.updateItemSortIndex( _self.ui.childViewContainer.find( '.js-variable-row-model-id' ) );
    } );
  },
  sortDestroy        : function () {
    if (this.ui.childViewContainer.sortable) {
      this.ui.childViewContainer.sortable( 'destroy' );
    }
  },
  onRender           : function () {
    this.ui.handle.tooltip();
  },
  onBeforeDestroy    : function () {
    this.ui.handle.tooltip( 'destroy' );
    this.sortDestroy()
  },
  onKeyupNameInput   : function ( e ) {
    if (e.which == 13) {
      this.ui.nameInput.blur();
    }
  },
  onClickName        : function () {
    this.ui.name.addClass( 'hide' );
    this.ui.nameInput.removeClass( 'hide' );
    this.ui.nameInput.focus();
  },
  redrawName         : function () {
    this.ui.name.html( _.escape( this.model.get( 'name' ) ) );
    this.ui.name.removeClass( 'hide' );
    this.ui.nameInput.addClass( 'hide' );
  },
  onClickDelBtn      : function () {
    this.model.destroy();
  },
  changeNameInput    : function () {
    if (this.model.save( 'name', this.ui.nameInput.val(), {silent: false} )) {
      // saveが実行されなければ…(変更が無い、もしくはerror)
      this.redrawName();
    }
  },
  onClickAddBtn      : function () {
    this.collection.add( {}, {wait: true} );
    this.ui.collapse.collapse( 'show' );
    this.onRender();
  },
  /**
   *
   * @param elements data-model-idの指定があるエレメントの配列
   */
  updateItemSortIndex: function ( elements ) {
    var _self = this;
    _.each( elements, function ( element, index ) {
      //noinspection JSUnresolvedVariable
      _self.collection.get( element.dataset.modelId ).set( 'sort', index, {silent: true} );
    } );
    this.model.updateList();
  }

} );

module.exports = ViewsDirPickerSettingVariable;
<|endoftext|>


<commit_before>$(document).ready(function(){

        var $accordions = $( ".js-accordion" );
        if ( $accordions.length  ) { // if there are at least one :)

           // init
           $accordions.each( function( index ) {
                var $this = $( this );
                    $options = $this.data(),
                    $accordions_headers = $this.find( ".js-accordion__header" ),
                    $accordions_prefix_classes = $options.accordionPrefixClasses || '',
                    $index_accordion = index+1 ;

                $this.attr({
                      "role": "tablist",
                      "aria-multiselectable": "true",
                      "class": $accordions_prefix_classes
                });

                $accordions_headers.each( function( index_h ) {
                      var $that = $( this ),
                          $text = $that.text(),
                          $accordion_panel = $that.next( ".js-accordion__panel" ),
                          $index_header = index_h+1 ;

                      $accordion_panel.prepend( $that.removeClass( "js-accordion__header" ).addClass( $accordions_prefix_classes + "__title" ).attr( "tabindex", "0") );

                      $accordion_header = $( '<button class="js-accordion__header ' + $accordions_prefix_classes + '__header">' + $text + '</button>' );
                      $accordion_panel.before( $accordion_header );

                      $accordion_header.attr({
                            "aria-controls": "accordion" + $index_accordion + "_panel" + $index_header,
                            "aria-expanded": "false",
                            "role": "tab",
                            "id": "accordion" + $index_accordion + "_tab" + $index_header,
                            "tabindex": "-1",
                            "aria-selected": "false"
                      });
                      $accordion_panel.attr({
                            "aria-labelledby": "accordion" + $index_accordion + "_tab" + $index_header,
                            "role": "tabpanel",
                            "id": "accordion" + $index_accordion + "_panel" + $index_header,
                            "aria-hidden": "true"
                      }).addClass( $accordions_prefix_classes + "__panel" );

                      // if opened by default
                      if ( $that.attr( "data-accordion-opened" ) == "true" ) {
                         $accordion_header.attr( "aria-expanded", "true" ).removeAttr( "data-accordion-opened" );
                         $accordion_panel.attr( "aria-hidden", "false" );
                         }

                      // init first one focusable
                      if ( $index_header === 1 ) {
                         $accordion_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" );
                         }

                });
           });

           /* Events ---------------------------------------------------------------------------------------------------------- */
           /* click on a tab link */
           $( "body" ).on( "focus", ".js-accordion__header", function( event ) {
                var $this = $( this ),
                    $accordion = $this.parent(),
                    $all_accordion_headers = $accordion.find( ".js-accordion__header" );

                // selected
                $all_accordion_headers.attr({
                      "tabindex": "-1",
                      "aria-selected": "false"
                });
                $this.attr( "aria-selected", "true" ).removeAttr( "tabindex" );

           })
           .on( "click", ".js-accordion__header", function( event ) {
                var $this = $( this ),
                    $this_panel = $( "#" + $this.attr("aria-controls" )),
                    $accordion = $this.parent(),
                    $all_accordion_headers = $accordion.find( ".js-accordion__header" );

                $all_accordion_headers.attr( "aria-selected", "false" );
                $this.attr( "aria-selected", "true" );

                // opened or closed?
                if ( $this.attr( "aria-expanded" ) == "false" ) { // closed
                    $this.attr( "aria-expanded", "true" );
                    $this_panel.attr( "aria-hidden", "false" );
                    }
                    else { // opened
                           $this.attr( "aria-expanded", "false" );
                           $this_panel.attr( "aria-hidden", "true" );
                          }
                event.preventDefault();

           })
           .on( "keydown", ".js-accordion__header", function( event ) {
               var $this = $( this ),
                   $accordion = $this.parent(),
                   $all_accordion_headers = $accordion.find( ".js-accordion__header" ),
                   $first_header = $accordion.find( ".js-accordion__header" ).first(),
                   $last_header = $accordion.find( ".js-accordion__header" ).last(),
                   $prev_header = $this.prevAll( ".js-accordion__header" ).first(),
                   $next_header = $this.nextAll( ".js-accordion__header" ).first();

               // strike up or left in the tab => previous tab
               if ( ( event.keyCode == 37 || event.keyCode == 38 ) && !event.ctrlKey ) {
                   $all_accordion_headers.attr({
                         "tabindex": "-1",
                         "aria-selected": "false"
                   });
                   // if we are on first one, activate last
                   if ( $this.is( $first_header ) ) {
                      $last_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" ).focus();
                      }
                      // else activate previous
                      else {
                            $prev_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" ).focus();
                           }
                   event.preventDefault();
               }
               // strike down or right in the tab => next tab
               else if ( ( event.keyCode == 40 || event.keyCode == 39 ) && !event.ctrlKey ) {
                       $all_accordion_headers.attr({
                             "tabindex": "-1",
                             "aria-selected": "false"
                       });
                       // if we are on last one, activate first
                       if ( $this.is( $last_header ) ) {
                           $first_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" ).focus();
                          }
                          // else activate next
                          else {
                                $next_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" ).focus();
                                }
                       event.preventDefault();
               }
               // strike home in the tab => 1st tab
               else if ( event.keyCode == 36 ) {
                       $all_accordion_headers.attr({
                             "tabindex": "-1",
                             "aria-selected": "false"
                       });
                       $first_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" ).focus();
                       event.preventDefault();
               }
               // strike end in the tab => last tab
               else if ( event.keyCode == 35 ) {
                       $all_accordion_headers.attr({
                             "tabindex": "-1",
                             "aria-selected": "false"
                       });
                       $last_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" ).focus();
                       event.preventDefault();
               }
           })
           .on( "keydown", ".js-accordion__panel", function( event ) {
               var $this = $( this ),
                   $this_header = $( "#" + $this.attr("aria-labelledby" )),
                   $accordion = $this.parent( ),
                   $first_header = $accordion.find( ".js-accordion__header" ).first(),
                   $prev_header = $this_header.prevAll( ".js-accordion__header" ).first(),
                   $next_header = $this_header.nextAll( ".js-accordion__header" ).first(),
                   $last_header = $accordion.find( ".js-accordion__header" ).last();

               // strike up + ctrl => go to header
               if ( event.keyCode == 38 && event.ctrlKey ) {
                   $this_header.focus();
                   event.preventDefault();
               }
               // strike pageup + ctrl => go to prev header
               else if ( event.keyCode == 33 && event.ctrlKey ) {
                       if ( $this_header.is( $first_header ) ) {
                           $last_header.focus();
                           event.preventDefault();
                       }
                       else {
                             $prev_header.focus();
                             event.preventDefault();
                            }
               }
               // strike pagedown + ctrl => go to next header
               else if ( event.keyCode == 34 && event.ctrlKey ) {
                       if ( $this_header.is( $last_header ) ) {
                           $first_header.focus();
                           event.preventDefault();
                       }
                       else {
                             $next_header.focus();
                             event.preventDefault();
                            }
               }

           });

        }

});
<commit_msg>Fix for iOS bug for focus<commit_after>$(document).ready(function(){

        var $accordions = $( ".js-accordion" );
        if ( $accordions.length  ) { // if there are at least one :)

           // init
           $accordions.each( function( index ) {
                var $this = $( this );
                    $options = $this.data(),
                    $accordions_headers = $this.find( ".js-accordion__header" ),
                    $accordions_prefix_classes = $options.accordionPrefixClasses || '',
                    $index_accordion = index+1 ;

                $this.attr({
                      "role": "tablist",
                      "aria-multiselectable": "true",
                      "class": $accordions_prefix_classes
                });

                $accordions_headers.each( function( index_h ) {
                      var $that = $( this ),
                          $text = $that.text(),
                          $accordion_panel = $that.next( ".js-accordion__panel" ),
                          $index_header = index_h+1 ;

                      $accordion_panel.prepend( $that.removeClass( "js-accordion__header" ).addClass( $accordions_prefix_classes + "__title" ).attr( "tabindex", "0") );

                      $accordion_header = $( '<button class="js-accordion__header ' + $accordions_prefix_classes + '__header">' + $text + '</button>' );
                      $accordion_panel.before( $accordion_header );

                      $accordion_header.attr({
                            "aria-controls": "accordion" + $index_accordion + "_panel" + $index_header,
                            "aria-expanded": "false",
                            "role": "tab",
                            "id": "accordion" + $index_accordion + "_tab" + $index_header,
                            "tabindex": "-1",
                            "aria-selected": "false"
                      });
                      $accordion_panel.attr({
                            "aria-labelledby": "accordion" + $index_accordion + "_tab" + $index_header,
                            "role": "tabpanel",
                            "id": "accordion" + $index_accordion + "_panel" + $index_header,
                            "aria-hidden": "true"
                      }).addClass( $accordions_prefix_classes + "__panel" );

                      // if opened by default
                      if ( $that.attr( "data-accordion-opened" ) == "true" ) {
                         $accordion_header.attr( "aria-expanded", "true" ).removeAttr( "data-accordion-opened" );
                         $accordion_panel.attr( "aria-hidden", "false" );
                         }

                      // init first one focusable
                      if ( $index_header === 1 ) {
                         $accordion_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" );
                         }

                });
           });

           /* Events ---------------------------------------------------------------------------------------------------------- */
           /* click on a tab link */
           $( "body" ).on( "focus", ".js-accordion__header", function( event ) {
                var $this = $( this ),
                    $accordion = $this.parent(),
                    $all_accordion_headers = $accordion.find( ".js-accordion__header" );

                // selected
                $all_accordion_headers.attr({
                      "tabindex": "-1",
                      "aria-selected": "false"
                });
                $this.attr( "aria-selected", "true" ).removeAttr( "tabindex" );

           })
           .on( "click", ".js-accordion__header", function( event ) {
                var $this = $( this ),
                    $this_panel = $( "#" + $this.attr("aria-controls" )),
                    $accordion = $this.parent(),
                    $all_accordion_headers = $accordion.find( ".js-accordion__header" );

                $all_accordion_headers.attr( "aria-selected", "false" );
                $this.attr( "aria-selected", "true" );

                // opened or closed?
                if ( $this.attr( "aria-expanded" ) == "false" ) { // closed
                    $this.attr( "aria-expanded", "true" );
                    $this_panel.attr( "aria-hidden", "false" );
                    }
                    else { // opened
                           $this.attr( "aria-expanded", "false" );
                           $this_panel.attr( "aria-hidden", "true" );
                          }
                setTimeout(function(){ $this.focus(); }, 0);
                event.preventDefault();

           })
           .on( "keydown", ".js-accordion__header", function( event ) {
               var $this = $( this ),
                   $accordion = $this.parent(),
                   $all_accordion_headers = $accordion.find( ".js-accordion__header" ),
                   $first_header = $accordion.find( ".js-accordion__header" ).first(),
                   $last_header = $accordion.find( ".js-accordion__header" ).last(),
                   $prev_header = $this.prevAll( ".js-accordion__header" ).first(),
                   $next_header = $this.nextAll( ".js-accordion__header" ).first();

               // strike up or left in the tab => previous tab
               if ( ( event.keyCode == 37 || event.keyCode == 38 ) && !event.ctrlKey ) {
                   $all_accordion_headers.attr({
                         "tabindex": "-1",
                         "aria-selected": "false"
                   });
                   // if we are on first one, activate last
                   if ( $this.is( $first_header ) ) {
                      $last_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" );
                      setTimeout(function(){ $last_header.focus(); }, 0);
                      }
                      // else activate previous
                      else {
                            $prev_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" );
                            setTimeout(function(){ $prev_header.focus(); }, 0);
                           }
                   event.preventDefault();
               }
               // strike down or right in the tab => next tab
               else if ( ( event.keyCode == 40 || event.keyCode == 39 ) && !event.ctrlKey ) {
                       $all_accordion_headers.attr({
                             "tabindex": "-1",
                             "aria-selected": "false"
                       });
                       // if we are on last one, activate first
                       if ( $this.is( $last_header ) ) {
                           $first_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" );
                           setTimeout(function(){ $first_header.focus(); }, 0);
                          }
                          // else activate next
                          else {
                                $next_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" );
                                setTimeout(function(){ $next_header.focus(); }, 0);
                                }
                       event.preventDefault();
               }
               // strike home in the tab => 1st tab
               else if ( event.keyCode == 36 ) {
                       $all_accordion_headers.attr({
                             "tabindex": "-1",
                             "aria-selected": "false"
                       });
                       $first_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" );
                       setTimeout(function(){ $first_header.focus(); }, 0);
                       event.preventDefault();
               }
               // strike end in the tab => last tab
               else if ( event.keyCode == 35 ) {
                       $all_accordion_headers.attr({
                             "tabindex": "-1",
                             "aria-selected": "false"
                       });
                       $last_header.attr( "aria-selected", "true" ).removeAttr( "tabindex" );
                       setTimeout(function(){ $last_header.focus(); }, 0);
                       event.preventDefault();
               }
           })
           .on( "keydown", ".js-accordion__panel", function( event ) {
               var $this = $( this ),
                   $this_header = $( "#" + $this.attr("aria-labelledby" )),
                   $accordion = $this.parent( ),
                   $first_header = $accordion.find( ".js-accordion__header" ).first(),
                   $prev_header = $this_header.prevAll( ".js-accordion__header" ).first(),
                   $next_header = $this_header.nextAll( ".js-accordion__header" ).first(),
                   $last_header = $accordion.find( ".js-accordion__header" ).last();

               // strike up + ctrl => go to header
               if ( event.keyCode == 38 && event.ctrlKey ) {
                   setTimeout(function(){ $this_header.focus(); }, 0);
                   event.preventDefault();
               }
               // strike pageup + ctrl => go to prev header
               else if ( event.keyCode == 33 && event.ctrlKey ) {
                       if ( $this_header.is( $first_header ) ) {
                           setTimeout(function(){ $last_header.focus(); }, 0);
                           event.preventDefault();
                       }
                       else {
                             setTimeout(function(){ $prev_header.focus(); }, 0);
                             event.preventDefault();
                            }
               }
               // strike pagedown + ctrl => go to next header
               else if ( event.keyCode == 34 && event.ctrlKey ) {
                       if ( $this_header.is( $last_header ) ) {
                           setTimeout(function(){ $first_header.focus(); }, 0);
                           event.preventDefault();
                       }
                       else {
                             setTimeout(function(){ $next_header.focus(); }, 0);
                             event.preventDefault();
                            }
               }

           });

        }

});
<|endoftext|>


<commit_before>// Copyright 2002-2014, University of Colorado

/**
 * Handles the stage to canvas transforms that allow scaling based on how large the canvas is to a fixed stage size.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */

define( function( require ) {
  "use strict";

  var inherit = require( 'PHET_CORE/inherit' );
  var Matrix4 = require( 'DOT/Matrix4' );
  var Transform4 = require( 'DOT/Transform4' );
  var Property = require( 'DOT/TemporaryProperty' );

  var StageCenteringCanvasTransform = function( canvasSizeProperty, stageSize ) {
    this.canvasSizeProperty = canvasSizeProperty;
    this.stageSize = stageSize;
    this.transform = new Property( null ); // updated by the listener below

    var that = this;
    canvasSizeProperty.link( function() {
      that.updateTransform();
    }, true );
  };

  // due to various scaling intricacies and the default way of constructing a perspective projection matrix
  StageCenteringCanvasTransform.fieldOfViewYFactor = function( canvasSize, stageSize ) {
    var sx = canvasSize.width / stageSize.width;
    var sy = canvasSize.height / stageSize.height;
    if ( sx === 0 || sy === 0 ) {
      return 1;
    }
    return sy > sx ? sy / sx : 1;
  };

  // returns a 4x4 matrix
  StageCenteringCanvasTransform.compute = function( canvasSize, stageSize ) {
    var sx = canvasSize.width / stageSize.width;
    var sy = canvasSize.height / stageSize.height;

    //use the smaller and maintain aspect ratio so that circles don't become ellipses
    var scale = sx < sy ? sx : sy;
    scale = scale <= 0 ? 1.0 : scale;//if scale is negative or zero, just use scale=1

    var scaledStageWidth = scale * stageSize.width;
    var scaledStageHeight = scale * stageSize.height;

    //center it in width and height
    return Matrix4.translation( canvasSize.width / 2 - scaledStageWidth / 2, canvasSize.height / 2 - scaledStageHeight / 2 ).timesMatrix( Matrix4.scaling( scale ) );
  };

  return inherit( Object, StageCenteringCanvasTransform, {

    fieldOfViewYFactor: function() {
      return StageCenteringCanvasTransform.fieldOfViewYFactor( this.canvasSizeProperty.get(), this.stageSize );
    },

    updateTransform: function() {
      var matrix4 = StageCenteringCanvasTransform.compute( this.canvasSizeProperty.get(), this.stageSize );

      this.transform.set( new Transform4( matrix4 ) );
    }
  } );
} );
<commit_msg>Fix for property reference (somehow didn't error on desktop)<commit_after>// Copyright 2002-2014, University of Colorado

/**
 * Handles the stage to canvas transforms that allow scaling based on how large the canvas is to a fixed stage size.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */

define( function( require ) {
  "use strict";

  var inherit = require( 'PHET_CORE/inherit' );
  var Matrix4 = require( 'DOT/Matrix4' );
  var Transform4 = require( 'DOT/Transform4' );
  var Property = require( 'AXON/Property' );

  var StageCenteringCanvasTransform = function( canvasSizeProperty, stageSize ) {
    this.canvasSizeProperty = canvasSizeProperty;
    this.stageSize = stageSize;
    this.transform = new Property( null ); // updated by the listener below

    var that = this;
    canvasSizeProperty.link( function() {
      that.updateTransform();
    }, true );
  };

  // due to various scaling intricacies and the default way of constructing a perspective projection matrix
  StageCenteringCanvasTransform.fieldOfViewYFactor = function( canvasSize, stageSize ) {
    var sx = canvasSize.width / stageSize.width;
    var sy = canvasSize.height / stageSize.height;
    if ( sx === 0 || sy === 0 ) {
      return 1;
    }
    return sy > sx ? sy / sx : 1;
  };

  // returns a 4x4 matrix
  StageCenteringCanvasTransform.compute = function( canvasSize, stageSize ) {
    var sx = canvasSize.width / stageSize.width;
    var sy = canvasSize.height / stageSize.height;

    //use the smaller and maintain aspect ratio so that circles don't become ellipses
    var scale = sx < sy ? sx : sy;
    scale = scale <= 0 ? 1.0 : scale;//if scale is negative or zero, just use scale=1

    var scaledStageWidth = scale * stageSize.width;
    var scaledStageHeight = scale * stageSize.height;

    //center it in width and height
    return Matrix4.translation( canvasSize.width / 2 - scaledStageWidth / 2, canvasSize.height / 2 - scaledStageHeight / 2 ).timesMatrix( Matrix4.scaling( scale ) );
  };

  return inherit( Object, StageCenteringCanvasTransform, {

    fieldOfViewYFactor: function() {
      return StageCenteringCanvasTransform.fieldOfViewYFactor( this.canvasSizeProperty.get(), this.stageSize );
    },

    updateTransform: function() {
      var matrix4 = StageCenteringCanvasTransform.compute( this.canvasSizeProperty.get(), this.stageSize );

      this.transform.set( new Transform4( matrix4 ) );
    }
  } );
} );
<|endoftext|>


<commit_before>// Copyright 2014-2021, University of Colorado Boulder

/**
 * Main entry point for the 'Energy Forms and Changes' sim.
 *
 * @author John Blanco
 * @author Martin Veillette (Berea College)
 * @author Andrew Adare
 */

import Sim from '../../joist/js/Sim.js';
import simLauncher from '../../joist/js/simLauncher.js';
import Tandem from '../../tandem/js/Tandem.js';
import energyFormsAndChangesStrings from './energyFormsAndChangesStrings.js';
import EFACIntroScreen from './intro/EFACIntroScreen.js';
import SystemsScreen from './systems/SystemsScreen.js';

const energyFormsAndChangesTitleString = energyFormsAndChangesStrings[ 'energy-forms-and-changes' ].title;

// constants
const tandem = Tandem.ROOT;

const simOptions = {
  credits: {
    leadDesign: 'Noah Podolefsky, Amy Rouinfar',
    softwareDevelopment: 'Andrew Adare, John Blanco, Chris Klusendorf',
    team: 'Trish Loeblein, Emily B. Moore, Ariel Paul, Kathy Perkins, and in cooperation with the Next-Lab project',
    qualityAssurance: 'Logan Bray, Steele Dalton, Megan Lai, Brooklyn Lash, Liam Mulhall, Devon Quispe,
' +
                      'Laura Rea, Jacob Romero, Kathryn Woessner, Kelly Wurtz, Bryan Yoelin',
    graphicArts: 'Cheryl McCutchan, Mariah Hermsmeyer, Megan Lai',
    thanks: ''
  },
  phetioDesigned: true
};

simLauncher.launch( () => {

  const sim = new Sim( energyFormsAndChangesTitleString, [
    new EFACIntroScreen( tandem.createTandem( 'introScreen' ) ),
    new SystemsScreen( tandem.createTandem( 'systemsScreen' ) )
  ], simOptions );

  sim.start();
} );<commit_msg>update copyright dates from daily grunt work<commit_after>// Copyright 2014-2022, University of Colorado Boulder

/**
 * Main entry point for the 'Energy Forms and Changes' sim.
 *
 * @author John Blanco
 * @author Martin Veillette (Berea College)
 * @author Andrew Adare
 */

import Sim from '../../joist/js/Sim.js';
import simLauncher from '../../joist/js/simLauncher.js';
import Tandem from '../../tandem/js/Tandem.js';
import energyFormsAndChangesStrings from './energyFormsAndChangesStrings.js';
import EFACIntroScreen from './intro/EFACIntroScreen.js';
import SystemsScreen from './systems/SystemsScreen.js';

const energyFormsAndChangesTitleString = energyFormsAndChangesStrings[ 'energy-forms-and-changes' ].title;

// constants
const tandem = Tandem.ROOT;

const simOptions = {
  credits: {
    leadDesign: 'Noah Podolefsky, Amy Rouinfar',
    softwareDevelopment: 'Andrew Adare, John Blanco, Chris Klusendorf',
    team: 'Trish Loeblein, Emily B. Moore, Ariel Paul, Kathy Perkins, and in cooperation with the Next-Lab project',
    qualityAssurance: 'Logan Bray, Steele Dalton, Megan Lai, Brooklyn Lash, Liam Mulhall, Devon Quispe,
' +
                      'Laura Rea, Jacob Romero, Kathryn Woessner, Kelly Wurtz, Bryan Yoelin',
    graphicArts: 'Cheryl McCutchan, Mariah Hermsmeyer, Megan Lai',
    thanks: ''
  },
  phetioDesigned: true
};

simLauncher.launch( () => {

  const sim = new Sim( energyFormsAndChangesTitleString, [
    new EFACIntroScreen( tandem.createTandem( 'introScreen' ) ),
    new SystemsScreen( tandem.createTandem( 'systemsScreen' ) )
  ], simOptions );

  sim.start();
} );<|endoftext|>


<commit_before>var cryptoUtil = require('../util/crypto');
var log = require('../util/log');
var LocalStorage = require('./LocalStorage');
var inherits = require('inherits');

var SEPARATOR = '@#$';

function EncryptedLocalStorage(config) {
  LocalStorage.apply(this, [config]);
}
inherits(EncryptedLocalStorage, LocalStorage);

EncryptedLocalStorage.prototype._brokenDecrypt = function(body) {
  var key = cryptoUtil.kdf(this.password + this.email, 'mjuBtGybi/4=', 100);
  log.debug('Trying legacy decrypt')
  var decryptedJson = cryptoUtil.decrypt(key, body);
  return decryptedJson;
};

EncryptedLocalStorage.prototype._brokenDecryptUndef = function(body) {
  var badkey = undefined + SEPARATOR + undefined;
  return cryptoUtil.decrypt(badkey, body);
};

EncryptedLocalStorage.prototype.getItem = function(name, callback) {
  var self = this;
  LocalStorage.prototype.getItem.apply(this, [name,
    function(err, body) {
      var decryptedJson = cryptoUtil.decrypt(self.email + SEPARATOR + self.password, body);
      if (!decryptedJson) {
        log.debug('Could not decrypt value using current decryption schema');
        decryptedJson = self._brokenDecrypt(body);
      }

      if (!decryptedJson) {
        decryptedJson = self._brokenDecryptUndef(body);
      }

      if (!decryptedJson) {
        log.debug('Could not decrypt value.');
        return callback('PNOTFOUND');
      }

      return callback(null, decryptedJson);
    }
  ]);
};

EncryptedLocalStorage.prototype.setItem = function(name, value, callback) {
  if (!_.isString(value)) {
    value = JSON.stringify(value);
  }
  var record = cryptoUtil.encrypt(this.email + SEPARATOR + this.password, value);
  LocalStorage.prototype.setItem.apply(this, [name, record, callback]);
};

EncryptedLocalStorage.prototype.removeItem = function(name, callback) {
  LocalStorage.prototype.removeItem.apply(this, [name, callback]);
};

EncryptedLocalStorage.prototype.clear = function(callback) {
  LocalStorage.prototype.clear.apply(this, [callback]);
};

module.exports = EncryptedLocalStorage;
<commit_msg>add preconditions<commit_after>var cryptoUtil = require('../util/crypto');
var log = require('../util/log');
var LocalStorage = require('./LocalStorage');
var inherits = require('inherits');
var preconditions = require('preconditions').singleton();

var SEPARATOR = '@#$';

function EncryptedLocalStorage(config) {
  LocalStorage.apply(this, [config]);
}
inherits(EncryptedLocalStorage, LocalStorage);

EncryptedLocalStorage.prototype._brokenDecrypt = function(body) {
  var key = cryptoUtil.kdf(this.password + this.email, 'mjuBtGybi/4=', 100);
  log.debug('Trying legacy decrypt')
  var decryptedJson = cryptoUtil.decrypt(key, body);
  return decryptedJson;
};

EncryptedLocalStorage.prototype._brokenDecryptUndef = function(body) {
  var badkey = undefined + SEPARATOR + undefined;
  return cryptoUtil.decrypt(badkey, body);
};

EncryptedLocalStorage.prototype.getItem = function(name, callback) {
  var self = this;
  preconditions.checkState(self.email);

  LocalStorage.prototype.getItem.apply(this, [name,
    function(err, body) {

      var decryptedJson = cryptoUtil.decrypt(self.email + SEPARATOR + self.password, body);
      if (!decryptedJson) {
        log.debug('Could not decrypt value using current decryption schema');
        decryptedJson = self._brokenDecrypt(body);
      }

      if (!decryptedJson) {
        decryptedJson = self._brokenDecryptUndef(body);
      }

      if (!decryptedJson) {
        log.debug('Could not decrypt value.');
        return callback('PNOTFOUND');
      }

      return callback(null, decryptedJson);
    }
  ]);
};

EncryptedLocalStorage.prototype.setItem = function(name, value, callback) {
  if (!_.isString(value)) {
    value = JSON.stringify(value);
  }
  var record = cryptoUtil.encrypt(this.email + SEPARATOR + this.password, value);
  LocalStorage.prototype.setItem.apply(this, [name, record, callback]);
};

EncryptedLocalStorage.prototype.removeItem = function(name, callback) {
  LocalStorage.prototype.removeItem.apply(this, [name, callback]);
};

EncryptedLocalStorage.prototype.clear = function(callback) {
  LocalStorage.prototype.clear.apply(this, [callback]);
};

module.exports = EncryptedLocalStorage;
<|endoftext|>


<commit_before>/**
 * Sets up workspace drop zones for DnD and other interaction
 */
var WorkspaceDropZone = Backbone.View.extend({
    template: function() {
        return _.template($("#template-workspace-dropzones").html())();
    },

    events: {
        'sortstop': 'select_dimension',
        'click a': 'selections',
        'click span': 'selections'
    },

    initialize: function(args) {
        // Keep track of parent workspace
        this.workspace = args.workspace;

        // Maintain `this` in jQuery event handlers
        _.bindAll(this, "select_dimension", "move_dimension",
                "remove_dimension");
    },

    render: function() {
        // Generate drop zones from template
        $(this.el).html(this.template());

        // Activate drop zones
        $(this.el).find('.connectable').sortable({
            connectWith: $(this.el).find('.connectable'),
            cursorAt: {
                top: 10,
                left: 35
            },
            forcePlaceholderSize: true,
            items: '> li',
            opacity: 0.60,
            placeholder: 'placeholder',
            tolerance: 'pointer'
        });

        return this;
    },

    select_dimension: function(event, ui) {
        // Short circuit if this is a move
        if (ui.item.hasClass('d_measure') ||
                ui.item.hasClass('d_dimension')) {
            this.move_dimension(event, ui);
            return;
        }

        // Make the element and its parent bold
        var original_href = ui.item.find('a').attr('href');
        var $original = $(this.workspace.el).find('.sidebar')
            .find('a[href="' + original_href + '"]').parent('li');
        $original
            .css({fontWeight: "bold"})
            .draggable('disable');
        $original.parents('.parent_dimension')
            .find('.root')
            .css({fontWeight: "bold"});

        // Wrap with the appropriate parent element
        if (ui.item.find('a').hasClass('dimension')) {
            var $icon = $("<span />").addClass('sprite');
            ui.item.addClass('d_dimension').prepend($icon);
        } else {
            ui.item.addClass('d_measure');
        }

        // Notify the model of the change
        var dimension = ui.item.find('a').attr('href').replace('#', '');
        var index = ui.item.parent('.connectable').children().index(ui.item);
        this.workspace.query.move_dimension(dimension,
                $(event.target).parent(), index);

        // Prevent workspace from getting this event
        event.stopPropagation();
    },

    move_dimension: function(event, ui) {
        // Notify the model of the change
        var dimension = ui.item.find('a').attr('href').replace('#', '');
        var index = ui.item.parent('.connectable').children().index(ui.item);
        if (! ui.item.hasClass('deleted')) {
            this.workspace.query.move_dimension(dimension,
                $(event.target).parent(), index);
        }

        // Prevent workspace from getting this event
        event.stopPropagation();
        return false;
    },

    remove_dimension: function(event, ui) {
        // Reenable original element
        var original_href = ui.draggable.find('a').attr('href');
        var $original = $(this.workspace.el).find('.sidebar')
            .find('a[href="' + original_href + '"]').parent('li');
        $original
            .draggable('enable')
            .css({ fontWeight: 'normal' });

        // Unhighlight the parent if applicable
        if ($original.parents('.parent_dimension')
                .children().children('.ui-state-disabled').length === 0) {
            $original.parents('.parent_dimension')
                .find('.root')
                .css({fontWeight: "normal"});
        }

        // Notify server
        var target = '';
        var dimension = original_href.replace('#', '');
        $target_el = ui.draggable.parent().parent('div.fields_list_body');
        if ($target_el.hasClass('rows')) target = "ROWS";
        if ($target_el.hasClass('columns')) target = "COLUMNS";
        if ($target_el.hasClass('filter')) target = "FILTER";

        var url = "/axis/" + target + "/dimension/" + dimension;
        this.workspace.query.action.del(url, {
            success: this.workspace.query.run
        });

        // Remove element
        ui.draggable.addClass('deleted').remove();

        // Prevent workspace from getting this event
        event.stopPropagation();
        event.preventDefault();
        return false;
    },

    selections: function(event, ui) {
        // Determine dimension
        var $target = $(event.target).hasClass('sprite') ?
            $(event.target).parent().find('.dimension') :
            $(event.target);
        var key = $target.attr('href').replace('#', '');

        // Launch selections dialog
        (new SelectionsModal({
            target: $target,
            name: $target.text(),
            key: key,
            workspace: this.workspace
        })).open();

        // Prevent default action
        try {
            event.preventDefault();
        } catch (e) { }
        return false;
    }
});<commit_msg>Moving dimension to different axis now posts it to new axis. Fixes #59<commit_after>/**
 * Sets up workspace drop zones for DnD and other interaction
 */
var WorkspaceDropZone = Backbone.View.extend({
    template: function() {
        return _.template($("#template-workspace-dropzones").html())();
    },

    events: {
        'sortstop': 'select_dimension',
        'click a': 'selections',
        'click span': 'selections'
    },

    initialize: function(args) {
        // Keep track of parent workspace
        this.workspace = args.workspace;

        // Maintain `this` in jQuery event handlers
        _.bindAll(this, "select_dimension", "move_dimension",
                "remove_dimension");
    },

    render: function() {
        // Generate drop zones from template
        $(this.el).html(this.template());

        // Activate drop zones
        $(this.el).find('.connectable').sortable({
            connectWith: $(this.el).find('.connectable'),
            cursorAt: {
                top: 10,
                left: 35
            },
            forcePlaceholderSize: true,
            items: '> li',
            opacity: 0.60,
            placeholder: 'placeholder',
            tolerance: 'pointer'
        });

        return this;
    },

    select_dimension: function(event, ui) {
        // Short circuit if this is a move
        if (ui.item.hasClass('d_measure') ||
                ui.item.hasClass('d_dimension')) {
            this.move_dimension(event, ui);
            return;
        }

        // Make the element and its parent bold
        var original_href = ui.item.find('a').attr('href');
        var $original = $(this.workspace.el).find('.sidebar')
            .find('a[href="' + original_href + '"]').parent('li');
        $original
            .css({fontWeight: "bold"})
            .draggable('disable');
        $original.parents('.parent_dimension')
            .find('.root')
            .css({fontWeight: "bold"});

        // Wrap with the appropriate parent element
        if (ui.item.find('a').hasClass('dimension')) {
            var $icon = $("<span />").addClass('sprite');
            ui.item.addClass('d_dimension').prepend($icon);
        } else {
            ui.item.addClass('d_measure');
        }

        // Notify the model of the change
        var dimension = ui.item.find('a').attr('href').replace('#', '');
        var index = ui.item.parent('.connectable').children().index(ui.item);
        this.workspace.query.move_dimension(dimension,
                $(event.target).parent(), index);

        // Prevent workspace from getting this event
        event.stopPropagation();
    },

    move_dimension: function(event, ui) {
        // Notify the model of the change
        var dimension = ui.item.find('a').attr('href').replace('#', '');
        var index = ui.item.parent('.connectable').children().index(ui.item);
        if (! ui.item.hasClass('deleted')) {
            this.workspace.query.move_dimension(dimension,
                ui.item.parents('.fields_list_body'), index);
        }

        // Prevent workspace from getting this event
        event.stopPropagation();
        return false;
    },

    remove_dimension: function(event, ui) {
        // Reenable original element
        var original_href = ui.draggable.find('a').attr('href');
        var $original = $(this.workspace.el).find('.sidebar')
            .find('a[href="' + original_href + '"]').parent('li');
        $original
            .draggable('enable')
            .css({ fontWeight: 'normal' });

        // Unhighlight the parent if applicable
        if ($original.parents('.parent_dimension')
                .children().children('.ui-state-disabled').length === 0) {
            $original.parents('.parent_dimension')
                .find('.root')
                .css({fontWeight: "normal"});
        }

        // Notify server
        var target = '';
        var dimension = original_href.replace('#', '');
        $target_el = ui.draggable.parent().parent('div.fields_list_body');
        if ($target_el.hasClass('rows')) target = "ROWS";
        if ($target_el.hasClass('columns')) target = "COLUMNS";
        if ($target_el.hasClass('filter')) target = "FILTER";

        var url = "/axis/" + target + "/dimension/" + dimension;
        this.workspace.query.action.del(url, {
            success: this.workspace.query.run
        });

        // Remove element
        ui.draggable.addClass('deleted').remove();

        // Prevent workspace from getting this event
        event.stopPropagation();
        event.preventDefault();
        return false;
    },

    selections: function(event, ui) {
        // Determine dimension
        var $target = $(event.target).hasClass('sprite') ?
            $(event.target).parent().find('.dimension') :
            $(event.target);
        var key = $target.attr('href').replace('#', '');

        // Launch selections dialog
        (new SelectionsModal({
            target: $target,
            name: $target.text(),
            key: key,
            workspace: this.workspace
        })).open();

        // Prevent default action
        try {
            event.preventDefault();
        } catch (e) { }
        return false;
    }
});<|endoftext|>


<commit_before>'use strict'

// ----------------------------------------------------------------------------

const log = require ('ololog')
    , ansi = require ('ansicolor').nice
    , chai = require ('chai')
    , expect = chai.expect
    , assert = chai.assert

/*  ------------------------------------------------------------------------ */

module.exports = (exchange, item, code, now) => {
    expect (item).to.be.an ('object')
    expect (item).to.have.property ('id')
    expect (typeof item.id).to.be.oneOf (['string', 'undefined'])
    expect (item).to.have.property ('direction')
    expect (item.direction).to.be.oneOf (['in', 'out'])
    expect (item).to.have.property ('account')
    expect (typeof item.account).to.be.oneOf (['string', 'undefined'])
    expect (item).to.have.property ('referenceId')
    expect (typeof item.referenceId).to.be.oneOf (['string', 'undefined'])
    expect (item).to.have.property ('referenceAccount')
    expect (typeof item.referenceAccount).to.be.oneOf (['string', 'undefined'])
    expect (item).to.have.property ('type')
    expect (item.type).to.be.oneOf (['trade', 'transaction', 'margin', 'cashback', 'referral', 'transfer', 'fee', /* TODO: add more types here */ ])
    expect (item).to.have.property ('currency')
    expect (exchange.currencies).to.have.property (item.currency)
    expect (item).to.have.property ('amount')
    expect (typeof item.amount).to.be.oneOf (['number', 'undefined'])
    expect (item).to.have.property ('balanceBefore')
    expect (typeof item.balanceBefore).to.be.oneOf (['number', 'undefined'])
    expect (item).to.have.property ('balanceAfter')
    expect (typeof item.balanceAfter).to.be.oneOf (['number', 'undefined'])
    expect (item).to.have.property ('timestamp')
    expect (typeof item.timestamp).to.be.oneOf (['number', 'undefined'])
    expect (item.timestamp).to.be.gt (1230940800000)
    expect (item.timestamp).to.be.lt (now)
    expect (item).to.have.property ('datetime')
    expect (item.datetime).to.be.equal (exchange.iso8601 (item.timestamp))
    expect (item).to.have.property ('fee').that.is.an ('object')
    expect (item.fee).to.have.property ('cost')
    expect (typeof item.fee.cost).to.be.oneOf (['number', 'undefined'])
    expect (item.fee).to.have.property ('currency')
    expect (item.fee.currency).to.be.equal (item.currency)
    expect (item).to.have.property ('info').that.is.an ('object')
}
<commit_msg>relaxed test.ledgerItem.js on type<commit_after>'use strict'

// ----------------------------------------------------------------------------

const log = require ('ololog')
    , ansi = require ('ansicolor').nice
    , chai = require ('chai')
    , expect = chai.expect
    , assert = chai.assert

/*  ------------------------------------------------------------------------ */

module.exports = (exchange, item, code, now) => {
    expect (item).to.be.an ('object')
    expect (item).to.have.property ('id')
    expect (typeof item.id).to.be.oneOf (['string', 'undefined'])
    expect (item).to.have.property ('direction')
    expect (item.direction).to.be.oneOf (['in', 'out'])
    expect (item).to.have.property ('account')
    expect (typeof item.account).to.be.oneOf (['string', 'undefined'])
    expect (item).to.have.property ('referenceId')
    expect (typeof item.referenceId).to.be.oneOf (['string', 'undefined'])
    expect (item).to.have.property ('referenceAccount')
    expect (typeof item.referenceAccount).to.be.oneOf (['string', 'undefined'])
    expect (item).to.have.property ('type')
    // expect (item.type).to.be.oneOf (['trade', 'transaction', 'margin', 'cashback', 'referral', 'transfer', 'fee', /* TODO: add more types here */ ])
    expect (item).to.have.property ('currency')
    expect (exchange.currencies).to.have.property (item.currency)
    expect (item).to.have.property ('amount')
    expect (typeof item.amount).to.be.oneOf (['number', 'undefined'])
    expect (item).to.have.property ('before')
    expect (typeof item.balanceBefore).to.be.oneOf (['number', 'undefined'])
    expect (item).to.have.property ('after')
    expect (typeof item.balanceAfter).to.be.oneOf (['number', 'undefined'])
    expect (item).to.have.property ('timestamp')
    expect (typeof item.timestamp).to.be.oneOf (['number', 'undefined'])
    expect (item.timestamp).to.be.gt (1230940800000)
    expect (item.timestamp).to.be.lt (now)
    expect (item).to.have.property ('datetime')
    expect (item.datetime).to.be.equal (exchange.iso8601 (item.timestamp))
    expect (item).to.have.property ('fee').that.is.an ('object')
    expect (item.fee).to.have.property ('cost')
    expect (typeof item.fee.cost).to.be.oneOf (['number', 'undefined'])
    expect (item.fee).to.have.property ('currency')
    expect (item.fee.currency).to.be.equal (item.currency)
    expect (item).to.have.property ('info').that.is.an ('object')
}
<|endoftext|>


<commit_before>var config = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
  databaseURL: "https://YOUR_PROJECT_ID.firebaseio.com",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_PROJECT_ID.appspot.com",
  messagingSenderId: "YOUR_MESSAGE_ID"
};

firebase.initializeApp(config);
<commit_msg>Remove firebase config<commit_after><|endoftext|>


<commit_before>//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
//>>description: Generates dividers for listview items
//>>label: Listview Autodividers
//>>group: Widgets
define( [ "jquery", "./listview" ], function( jQuery ) {
//>>excludeEnd("jqmBuildExclude");
(function( $, undefined ) {

$.mobile.listview.prototype.options.autodividers = false;
$.mobile.listview.prototype.options.autodividersSelector = function( elt ) {
	// look for the text in the given element
	var text = $.trim( elt.text() ) || null;

	if ( !text ) {
		return null;
	}

	// create the text for the divider (first uppercased letter)
	text = text.slice( 0, 1 ).toUpperCase();

	return text;
};

$.mobile.document.delegate( "ul,ol", "listviewcreate", function() {

	var list = $( this ),
			listview = list.data( "mobile-listview" );

	if ( !listview || !listview.options.autodividers ) {
		return;
	}

	var replaceDividers = function () {
		list.find( "li:jqmData(role='list-divider')" ).remove();

		var lis = list.find( 'li' ),
			lastDividerText = null, li, dividerText;

		for ( var i = 0; i < lis.length ; i++ ) {
			li = lis[i];
			dividerText = listview.options.autodividersSelector( $( li ) );

			if ( dividerText && lastDividerText !== dividerText ) {
				var divider = document.createElement( 'li' );
				divider.appendChild( document.createTextNode( dividerText ) );
				divider.setAttribute( 'data-' + $.mobile.ns + 'role', 'list-divider' );
				li.parentNode.insertBefore( divider, li );
			}

			lastDividerText = dividerText;
		}
	};

	var afterListviewRefresh = function () {
		list.unbind( 'listviewafterrefresh', afterListviewRefresh );
		replaceDividers();
		listview.refresh();
		list.bind( 'listviewafterrefresh', afterListviewRefresh );
	};

	afterListviewRefresh();
});

})( jQuery );
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
});
//>>excludeEnd("jqmBuildExclude");
<commit_msg>Listview: Autodividers: Converting to extension.<commit_after>//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
//>>description: Generates dividers for listview items
//>>label: Listview Autodividers
//>>group: Widgets
define( [ "jquery", "./listview" ], function( jQuery ) {
//>>excludeEnd("jqmBuildExclude");
(function( $, undefined ) {

function defaultAutodividersSelector( elt ) {
	// look for the text in the given element
	var text = $.trim( elt.text() ) || null;

	if ( !text ) {
		return null;
	}

	// create the text for the divider (first uppercased letter)
	text = text.slice( 0, 1 ).toUpperCase();

	return text;
}

$.widget( "mobile.listview", $.mobile.listview, {
	options: {
		autodividers: false,
		autodividersSelector: defaultAutodividersSelector
	},

	_afterListviewRefresh: function() {
		var el = this.element;
		this._off( el, "listviewafterrefresh" );
		this._replaceDividers();
		this.refresh();
		this._on( el, { listviewafterrefresh: "_afterListviewRefresh" } );
	},

	_replaceDividers: function() {
		var i, lis, li, dividerText,
			lastDividerText = null,
			list = this.element;

		list.find( "li:jqmData(role='list-divider')" ).remove();

		lis = list.find( 'li' );

		for ( i = 0; i < lis.length ; i++ ) {
			li = lis[ i ];
			dividerText = this.options.autodividersSelector( $( li ) );

			if ( dividerText && lastDividerText !== dividerText ) {
				var divider = document.createElement( 'li' );
				divider.appendChild( document.createTextNode( dividerText ) );
				divider.setAttribute( 'data-' + $.mobile.ns + 'role', 'list-divider' );
				li.parentNode.insertBefore( divider, li );
			}

			lastDividerText = dividerText;
		}
	},

	_create: function() {
		this._super();

		if ( !this.options.autodividers ) {
			return;
		}

		this._afterListviewRefresh();
	}
});

})( jQuery );
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
});
//>>excludeEnd("jqmBuildExclude");
<|endoftext|>