<commit_before>var path = require('path');
var exceptions = require(path.resolve(__dirname,'../utils/exceptions'));
var generalUtils = require(path.resolve(__dirname,'../utils/general'));
var Leaderboard = require('agoragames-leaderboard');

//Open connection to general leaderboards (not timebased)
var generalLeaderboard = new Leaderboard('topteamer:general');

//---------------------------------------------------------------------------------------------------------------------------
// private functions
//---------------------------------------------------------------------------------------------------------------------------

//---------------------------------------------------------------------------------------------------------------------------
// Get the general contest leaderboard
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getContestLeaderboard = getContestLeaderboard;
function getContestLeaderboard(contestId) {
    return new Leaderboard('topteamer:contest_' + contestId);
}

//---------------------------------------------------------------------------------------------------------------------------
// Get the contest leaderboard of a specific team
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getTeamLeaderboard = getTeamLeaderboard;
function getTeamLeaderboard(contestId, teamId) {
    return new Leaderboard('topteamer:contest_' + contestId + '_team' + teamId);
}

//---------------------------------------------------------------------------------------------------------------------------
// Get the weekly leaderboard
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getWeeklyLeaderboard = getWeeklyLeaderboard;
function getWeeklyLeaderboard() {
    return new Leaderboard('topteamer:weekly_' + generalUtils.getYearWeek());
}

//---------------------------------------------------------------------------------------------------------------------------
// prepareLeaderObject - parse member_data
//---------------------------------------------------------------------------------------------------------------------------
function prepareLeaderObject(id, leader, outsideLeaderboard) {
    var memberDataParts = leader.member_data.split('|');
    var leaderObject = {
        'id': id,
        'rank': leader.rank,
        'score': leader.score,
        'avatar': memberDataParts[0],
        'name': memberDataParts[1],
    };

    if (outsideLeaderboard) {
        leaderObject.outside = true;
    }

    return leaderObject;
}

//---------------------------------------------------------------------------------------------------------------------------
// addScore
//
// The following leaderboards are updated:
// =======================================
// 1. Contest general leaderboard - 'contest_<contestId>'
// 2. Contest team leaderboard - 'contest_<contestId>_team_<teamId>'
// 3. General Leaderboard (ever) - 'general' (will be used to display my friends' scores)
// 4. Weekly leaderboard - weekly_<YearWeek>
//
// @deltaScore - the score currently achieved which should be increased in all leaderboards
// @facebookUserId - used as the primary member key in all leaderboards (to be able to retrieve friends leaderboard)
//---------------------------------------------------------------------------------------------------------------------------
module.exports.addScore = addScore;
function addScore(contestId, teamId, deltaScore, facebookUserId, name, avatar) {

    var contestGeneralLeaderboard = getContestLeaderboard(contestId);
    var contestTeamLeaderboard = getTeamLeaderboard(contestId, teamId);
    var weeklyLeaderboard = getWeeklyLeaderboard();

    generalLeaderboard.changeScoreFor(facebookUserId, deltaScore, function (reply) {
        generalLeaderboard.updateMemberData(facebookUserId, avatar + '|' + name);
    });

    contestGeneralLeaderboard.changeScoreFor(facebookUserId, deltaScore, function (reply) {
        contestGeneralLeaderboard.updateMemberData(facebookUserId, avatar + '|' + name, function (reply) {
            contestGeneralLeaderboard.disconnect();
        });
    });

    contestTeamLeaderboard.changeScoreFor(facebookUserId, deltaScore, function (reply) {
        contestTeamLeaderboard.updateMemberData(facebookUserId, avatar + '|' + name, function (reply) {
            contestTeamLeaderboard.disconnect();
        });
    });

    weeklyLeaderboard.changeScoreFor(facebookUserId, deltaScore, function (reply) {
        weeklyLeaderboard.updateMemberData(facebookUserId, avatar + '|' + name, function (reply) {
            weeklyLeaderboard.disconnect();
        });
    });
};

//---------------------------------------------------------------------------------------------------------------------------
// getLeaders
//
// Retrieve the first page of the input leaderboard. If my user is not included in this page, add myself to the bottom
// with flagging outside=true.
//
// data: leaderboard
// output: data.clientResponse
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getLeaders = getLeaders;
function getLeaders(data, callback) {

    var options = {
        'withMemberData': true,
        'sortBy': 'rank',
        'pageSize': generalUtils.settings.server.leaderboard.pageSize
    };

    data.clientResponse = [];

    data.leaderboard.leaders(0, options, function (leaders) {
        for (var i = 0; i < leaders.length; i++) {

            if (leaders[i].member === data.session.facebookUserId) {
                data.inLeaderboard = true;
            }

            data.clientResponse.push(prepareLeaderObject(i, leaders[i]));

        }

        if (!data.inLeaderboard && data.clientResponse.length > 0) {

            //I am not in the first page of the leaderboard
            var options = {'withMemberData': true, 'sortBy': 'rank', 'pageSize': 1};
            data.leaderboard.aroundMe(data.session.facebookUserId, options, function (leaders) {
                if (leaders && leaders.length > 0) {
                    //I am in the leaderboard (not at the first page)
                    data.clientResponse.push(prepareLeaderObject(data.clientResponse.length, leaders[0], true));
                    callback(null, data);
                }
                else {
                    //I am not in the leaderboard at all (never played for that leaderboard)
                    callback(null, data);
                }
            });
        }
        else {
            //I am in the first page of the leaderboard
            callback(null, data);
        }
    });
}

//---------------------------------------------------------------------------------------------------------------------------
// getFriends
//
// Retrieve me and my friends from the general leaderboard
//
// data: session (including friends.list array of id,name objects)
// output: data.clientResponse
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getFriends = getFriends;
function getFriends(data, callback) {

    var options = {
        withMemberData: false
        //pageSize: generalUtils.settings.server.leaderboard.pageSize + 1 //Including myself
    };

    data.clientResponse = [];

    var members = [];
    for (var i = 0; i < data.session.friends.list.length; i++) {
        members.push(data.session.friends.list[i].id);
    }
    //Push myself as well
    members.push(data.session.facebookUserId);

    generalLeaderboard.rankedInList(members, options, function (leadersWithoutMemberData) {

      var trueLeaderboardMembers = [];
      for (var i = 0; i < leadersWithoutMemberData.length; i++) {

        //Check that rank exist - otherwise this friends did not play yet and he/she is not in the leaderboard
        if (leadersWithoutMemberData[i].rank) {
          trueLeaderboardMembers.push(leadersWithoutMemberData[i].member);
        }
      }

      options.withMemberData = true;
      options.sortBy = 'rank';
      options.reverse = true;

      generalLeaderboard.rankedInList(trueLeaderboardMembers, options, function (leaders) {
        for (var i = 0; i < leaders.length; i++) {
          data.clientResponse.push(prepareLeaderObject(i, leaders[i]));
        }

        callback(null, data);
      });

    })

}

//---------------------------------------------------------------------------------------------------------------------------
// getFriendsAboveMe
//
// getFriendsAboveMe - returns x (based on general settings) friends above me in the leaderboard
//
// data: session
// output: data.friendsAboveMe array
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getFriendsAboveMe = getFriendsAboveMe;
function getFriendsAboveMe(data, callback) {

    var options = {
        'withMemberData': true,
        'sortBy': 'rank',
        'pageSize': generalUtils.settings.server.leaderboard.friendsAboveMePageSize
    };

    data.friendsAboveMe = [];

    generalLeaderboard.aroundMe(data.session.facebookUserId, options, function (leaders) {
        if (leaders && leaders.length > 0) {

            //I will be in that list as the last one - all my friends that are above me - will be first in the array)
            //.id property for each item in the friendsAboveMe list is the facebookUserId - for later to compute the passedFriends
            for (var i = 0; i < leaders.length; i++) {
                data.friendsAboveMe.push(prepareLeaderObject(leaders[i].member, leaders[i]));
                if (leaders[i].member === data.session.facebookUserId) {
                    break;
                }
            }
        }

        callback(null, data);

    });
};

//---------------------------------------------------------------------------------------------------------------------------
// getPassedFriends
//
// getPassedFriends - returns all the friends that I passed (in relation to data.friendsAboveMe)
//
// data: session, friendsAboveMe (friends that previously were above me - I will be last in this array)
// output: data.passedFriends array
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getPassedFriends = getPassedFriends;
function getPassedFriends(data, callback) {

    var options = {
        'withMemberData': true,
        'sortBy': 'rank',
        'pageSize': data.friendsAboveMe.length
    };

    data.passedFriends = [];

    var members = [];
    for (var i = 0; i < data.friendsAboveMe.length; i++) {
        members.push(data.friendsAboveMe[i].id); //Id is the facebookUserId which is the key of the member in the leaderboard
    }

    generalLeaderboard.rankedInList(members, options, function (leaders) {
        var reachedMyself = false;
        var friendsAfterMe = 0;

        for (var i = 0; i < leaders.length; i++) {

            if (reachedMyself) {
                data.passedFriends.push(prepareLeaderObject(leaders[i].member, leaders[i]));
                friendsAfterMe++;
            }
            else if (leaders[i].member === data.session.facebookUserId) {
                reachedMyself = true;
            }
        }

        callback(null, data);
    });

};

<commit_msg>fix friends leaderboard<commit_after>var path = require('path');
var exceptions = require(path.resolve(__dirname,'../utils/exceptions'));
var generalUtils = require(path.resolve(__dirname,'../utils/general'));
var Leaderboard = require('agoragames-leaderboard');

//Open connection to general leaderboards (not timebased)
var generalLeaderboard = new Leaderboard('topteamer:general');

//---------------------------------------------------------------------------------------------------------------------------
// private functions
//---------------------------------------------------------------------------------------------------------------------------

//---------------------------------------------------------------------------------------------------------------------------
// Get the general contest leaderboard
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getContestLeaderboard = getContestLeaderboard;
function getContestLeaderboard(contestId) {
    return new Leaderboard('topteamer:contest_' + contestId);
}

//---------------------------------------------------------------------------------------------------------------------------
// Get the contest leaderboard of a specific team
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getTeamLeaderboard = getTeamLeaderboard;
function getTeamLeaderboard(contestId, teamId) {
    return new Leaderboard('topteamer:contest_' + contestId + '_team' + teamId);
}

//---------------------------------------------------------------------------------------------------------------------------
// Get the weekly leaderboard
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getWeeklyLeaderboard = getWeeklyLeaderboard;
function getWeeklyLeaderboard() {
    return new Leaderboard('topteamer:weekly_' + generalUtils.getYearWeek());
}

//---------------------------------------------------------------------------------------------------------------------------
// prepareLeaderObject - parse member_data
//---------------------------------------------------------------------------------------------------------------------------
function prepareLeaderObject(id, leader, outsideLeaderboard) {
    var memberDataParts = leader.member_data.split('|');
    var leaderObject = {
        'id': id,
        'rank': leader.rank,
        'score': leader.score,
        'avatar': memberDataParts[0],
        'name': memberDataParts[1],
    };

    if (outsideLeaderboard) {
        leaderObject.outside = true;
    }

    return leaderObject;
}

//---------------------------------------------------------------------------------------------------------------------------
// addScore
//
// The following leaderboards are updated:
// =======================================
// 1. Contest general leaderboard - 'contest_<contestId>'
// 2. Contest team leaderboard - 'contest_<contestId>_team_<teamId>'
// 3. General Leaderboard (ever) - 'general' (will be used to display my friends' scores)
// 4. Weekly leaderboard - weekly_<YearWeek>
//
// @deltaScore - the score currently achieved which should be increased in all leaderboards
// @facebookUserId - used as the primary member key in all leaderboards (to be able to retrieve friends leaderboard)
//---------------------------------------------------------------------------------------------------------------------------
module.exports.addScore = addScore;
function addScore(contestId, teamId, deltaScore, facebookUserId, name, avatar) {

    var contestGeneralLeaderboard = getContestLeaderboard(contestId);
    var contestTeamLeaderboard = getTeamLeaderboard(contestId, teamId);
    var weeklyLeaderboard = getWeeklyLeaderboard();

    generalLeaderboard.changeScoreFor(facebookUserId, deltaScore, function (reply) {
        generalLeaderboard.updateMemberData(facebookUserId, avatar + '|' + name);
    });

    contestGeneralLeaderboard.changeScoreFor(facebookUserId, deltaScore, function (reply) {
        contestGeneralLeaderboard.updateMemberData(facebookUserId, avatar + '|' + name, function (reply) {
            contestGeneralLeaderboard.disconnect();
        });
    });

    contestTeamLeaderboard.changeScoreFor(facebookUserId, deltaScore, function (reply) {
        contestTeamLeaderboard.updateMemberData(facebookUserId, avatar + '|' + name, function (reply) {
            contestTeamLeaderboard.disconnect();
        });
    });

    weeklyLeaderboard.changeScoreFor(facebookUserId, deltaScore, function (reply) {
        weeklyLeaderboard.updateMemberData(facebookUserId, avatar + '|' + name, function (reply) {
            weeklyLeaderboard.disconnect();
        });
    });
};

//---------------------------------------------------------------------------------------------------------------------------
// getLeaders
//
// Retrieve the first page of the input leaderboard. If my user is not included in this page, add myself to the bottom
// with flagging outside=true.
//
// data: leaderboard
// output: data.clientResponse
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getLeaders = getLeaders;
function getLeaders(data, callback) {

    var options = {
        'withMemberData': true,
        'sortBy': 'rank',
        'pageSize': generalUtils.settings.server.leaderboard.pageSize
    };

    data.clientResponse = [];

    data.leaderboard.leaders(0, options, function (leaders) {
        for (var i = 0; i < leaders.length; i++) {

            if (leaders[i].member === data.session.facebookUserId) {
                data.inLeaderboard = true;
            }

            data.clientResponse.push(prepareLeaderObject(i, leaders[i]));

        }

        if (!data.inLeaderboard && data.clientResponse.length > 0) {

            //I am not in the first page of the leaderboard
            var options = {'withMemberData': true, 'sortBy': 'rank', 'pageSize': 1};
            data.leaderboard.aroundMe(data.session.facebookUserId, options, function (leaders) {
                if (leaders && leaders.length > 0) {
                    //I am in the leaderboard (not at the first page)
                    data.clientResponse.push(prepareLeaderObject(data.clientResponse.length, leaders[0], true));
                    callback(null, data);
                }
                else {
                    //I am not in the leaderboard at all (never played for that leaderboard)
                    callback(null, data);
                }
            });
        }
        else {
            //I am in the first page of the leaderboard
            callback(null, data);
        }
    });
}

//---------------------------------------------------------------------------------------------------------------------------
// getFriends
//
// Retrieve me and my friends from the general leaderboard
//
// data: session (including friends.list array of id,name objects)
// output: data.clientResponse
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getFriends = getFriends;
function getFriends(data, callback) {

    data.clientResponse = [];

  var options = {
    withMemberData: false,
    pageSize: data.session.friends.list.length + 1 //Including myself
  };

  var members = [];
    for (var i = 0; i < data.session.friends.list.length; i++) {
        members.push(data.session.friends.list[i].id);
    }
    //Push myself as well
    members.push(data.session.facebookUserId);

    generalLeaderboard.rankedInList(members, options, function (leadersWithoutMemberData) {

      var trueLeaderboardMembers = [];
      for (var i = 0; i < leadersWithoutMemberData.length; i++) {

        //Check that rank exist - otherwise this friends did not play yet and he/she is not in the leaderboard
        if (leadersWithoutMemberData[i].rank) {
          trueLeaderboardMembers.push(leadersWithoutMemberData[i].member);
        }
      }

      options.withMemberData = true;
      options.sortBy = 'rank';
      options.reverse = true;
      options.pageSize = trueLeaderboardMembers.length;

      generalLeaderboard.rankedInList(trueLeaderboardMembers, options, function (leaders) {
        for (var i = 0; i < leaders.length; i++) {
          data.clientResponse.push(prepareLeaderObject(i, leaders[i]));
        }

        callback(null, data);
      });

    })

}

//---------------------------------------------------------------------------------------------------------------------------
// getFriendsAboveMe
//
// getFriendsAboveMe - returns x (based on general settings) friends above me in the leaderboard
//
// data: session
// output: data.friendsAboveMe array
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getFriendsAboveMe = getFriendsAboveMe;
function getFriendsAboveMe(data, callback) {

    var options = {
        'withMemberData': true,
        'sortBy': 'rank',
        'pageSize': generalUtils.settings.server.leaderboard.friendsAboveMePageSize
    };

    data.friendsAboveMe = [];

    generalLeaderboard.aroundMe(data.session.facebookUserId, options, function (leaders) {
        if (leaders && leaders.length > 0) {

            //I will be in that list as the last one - all my friends that are above me - will be first in the array)
            //.id property for each item in the friendsAboveMe list is the facebookUserId - for later to compute the passedFriends
            for (var i = 0; i < leaders.length; i++) {
                data.friendsAboveMe.push(prepareLeaderObject(leaders[i].member, leaders[i]));
                if (leaders[i].member === data.session.facebookUserId) {
                    break;
                }
            }
        }

        callback(null, data);

    });
};

//---------------------------------------------------------------------------------------------------------------------------
// getPassedFriends
//
// getPassedFriends - returns all the friends that I passed (in relation to data.friendsAboveMe)
//
// data: session, friendsAboveMe (friends that previously were above me - I will be last in this array)
// output: data.passedFriends array
//---------------------------------------------------------------------------------------------------------------------------
module.exports.getPassedFriends = getPassedFriends;
function getPassedFriends(data, callback) {

    var options = {
        'withMemberData': true,
        'sortBy': 'rank',
        'pageSize': data.friendsAboveMe.length
    };

    data.passedFriends = [];

    var members = [];
    for (var i = 0; i < data.friendsAboveMe.length; i++) {
        members.push(data.friendsAboveMe[i].id); //Id is the facebookUserId which is the key of the member in the leaderboard
    }

    generalLeaderboard.rankedInList(members, options, function (leaders) {
        var reachedMyself = false;
        var friendsAfterMe = 0;

        for (var i = 0; i < leaders.length; i++) {

            if (reachedMyself) {
                data.passedFriends.push(prepareLeaderObject(leaders[i].member, leaders[i]));
                friendsAfterMe++;
            }
            else if (leaders[i].member === data.session.facebookUserId) {
                reachedMyself = true;
            }
        }

        callback(null, data);
    });

};

<|endoftext|>


<commit_before>(function() {
  'use strict';

  angular.module('dimApp')
    .factory('dimManifestService', ManifestService);

  ManifestService.$inject = ['$q', 'dimBungieService', '$http', 'toaster', 'dimSettingsService'];

  function ManifestService($q, dimBungieService, $http, toaster, dimSettingsService) {
    // Testing flags
    const alwaysLoadRemote = false;

    let manifestPromise = null;

    const makeStatement = _.memoize(function(table, db) {
      return db.prepare(`select json from ${table} where id = ?`);
    });

    const service = {
      isLoaded: true,
      isError: false,
      statusText: null,
      version: null,

      // This tells users to reload the extension. It fires no more
      // often than every 10 seconds, and only warns if the manifest
      // version has actually changed.
      warnMissingDefinition: _.debounce(function() {
        dimBungieService.getManifest()
          .then(function(data) {
            const language = dimSettingsService.language;
            const path = data.mobileWorldContentPaths[language] || data.mobileWorldContentPaths.en;

            // The manifest has updated!
            if (path !== service.version) {
              toaster.pop('error', 'Outdated Destiny Info', "Bungie has updated their Destiny info database. Reload DIM to pick up the new info. Note that some things in DIM may not work for a few hours after Bungie updates Destiny, as the new data propagates through their systems.");
            }
          });
      }, 10000, true),

      getManifest: function() {
        if (manifestPromise) {
          return manifestPromise;
        }

        service.isLoaded = false;

        manifestPromise = dimBungieService.getManifest()
          .then(function(data) {
            const language = dimSettingsService.language;
            const path = data.mobileWorldContentPaths[language] || data.mobileWorldContentPaths.en;

            // Use the path as the version, rather than the "version" field, because
            // Bungie can update the manifest file without changing that version.
            const version = path;
            service.version = version;

            return loadManifestFromCache(version + 1)
              .catch(function(e) {
                return loadManifestRemote(version, language, path);
              })
              .then(function(typedArray) {
                service.statusText = 'Building Destiny info database...';
                const db = new SQL.Database(typedArray);
                // do a small request, just to test it out
                service.getAllRecords(db, 'DestinyRaceDefinition');
                return db;
              });
          })
          .catch((e) => {
            service.statusText = "Error loading Destiny info: " + e.message + ". Reload to retry.";
            manifestPromise = null;
            service.isError = true;
            return deleteManifestFile().finally(() => $q.reject(e));
          });

        return manifestPromise;
      },

      getRecord: function(db, table, id) {
        const statement = makeStatement(table, db);
        // The ID in sqlite is a signed 32-bit int, while the id we
        // use is unsigned, so we must convert
        const sqlId = new Int32Array([id])[0];
        const result = statement.get([sqlId]);
        statement.reset();
        if (result.length) {
          return JSON.parse(result[0]);
        }
        return null;
      },

      getAllRecords: function(db, table) {
        const rows = db.exec(`SELECT json FROM ${table}`);
        const result = {};
        rows[0].values.forEach((row) => {
          const obj = JSON.parse(row);
          result[obj.hash] = obj;
        });
        return result;
      }
    };

    return service;

    /**
     * Returns a promise for the manifest data as a Uint8Array. Will cache it on succcess.
     */
    function loadManifestRemote(version, language, path) {
      service.statusText = 'Downloading latest Destiny info from Bungie...';
      return $http.get("https://www.bungie.net" + path, { responseType: "blob" })
        .then(function(response) {
          service.statusText = 'Unzipping latest Destiny info...';
          return unzipManifest(response.data);
        })
        .then(function(arraybuffer) {
          service.statusText = 'Saving latest Destiny info...';

          getLocalManifestFile()
            .then((fileEntry) => {
              fileEntry.createWriter((fileWriter) => {
                fileWriter.onwriteend = function(e) {
                  if (fileWriter.length === 0) { // truncate finished
                    fileWriter.write(new Blob([arraybuffer], { type: "application/octet-stream" }));
                  } else { // blob write finished
                    localStorage.setItem('manifest-version', version);
                    console.log("Sucessfully stored " + fileWriter.length + " byte manifest file.");
                    _gaq.push(['_trackEvent', 'Manifest', 'Downloaded']);
                  }
                };

                fileWriter.onerror = function(e) {
                  console.error('Write of manifest file failed', e);
                };

                fileWriter.truncate(0); // clear it out first
              });
            })
            .catch((e) => console.log('Error saving manifest file', e));

          var typedArray = new Uint8Array(arraybuffer);
          return typedArray;
        });
    }

    function getLocalManifestFile() {
      return $q((resolve, reject) => {
        const requestFileSystem = (window.requestFileSystem || window.webkitRequestFileSystem);
        // Ask for 60MB of temporary space. If Chrome gets rid of it we can always redownload.
        requestFileSystem(window.TEMPORARY, 60 * 1024 * 1024, (fs) => {
          fs.root.getFile('dimManifest', { create: true, exclusive: false }, (f) => resolve(f), (e) => reject(e));
        }, (e) => reject(e));
      });
    }

    function deleteManifestFile() {
      localStorage.removeItem('manifest-version');
      getLocalManifestFile().then((fileEntry) => {
        return $q((resolve, reject) => {
          fileEntry.remove(resolve, reject);
        });
      });
    }

    /**
     * Returns a promise for the cached manifest of the specified
     * version as a Uint8Array, or rejects.
     */
    function loadManifestFromCache(version) {
      if (alwaysLoadRemote) {
        return $q.reject(new Error("Testing - always load remote"));
      }

      service.statusText = "Loading saved Destiny info...";
      var currentManifestVersion = localStorage.getItem('manifest-version');
      if (currentManifestVersion === version) {
        // One version of this used chrome.storage.local with a
        // base64-encoded string, which is a bit slower. We may need
        // to do that again if the requestFileSystem API gets
        // removed.
        return getLocalManifestFile()
          .then((fileEntry) => {
            return $q((resolve, reject) => {
              fileEntry.file((file) => {
                var reader = new FileReader();
                reader.addEventListener("error", (e) => { reject(e); });
                reader.addEventListener("load", () => {
                  var typedArray = new Uint8Array(reader.result);
                  if (typedArray.length) {
                    resolve(typedArray);
                  } else {
                    reject(new Error("Empty cached manifest file"));
                  }
                });
                reader.readAsArrayBuffer(file);
              }, (e) => reject(e));
            });
          });
      } else {
        _gaq.push(['_trackEvent', 'Manifest', 'Need New Manifest']);
        return $q.reject(new Error("version mismatch: " + version + ' ' + currentManifestVersion));
      }
    }

    /**
     * Unzip a file from a ZIP Blob into an ArrayBuffer. Returns a promise.
     */
    function unzipManifest(blob) {
      return $q(function(resolve, reject) {
        zip.useWebWorkers = true;
        zip.workerScriptsPath = "vendor/zip.js/WebContent/";
        zip.createReader(new zip.BlobReader(blob), function(zipReader) {
          // get all entries from the zip
          zipReader.getEntries(function(entries) {
            if (entries.length) {
              entries[0].getData(new zip.BlobWriter(), function(blob) {
                var blobReader = new FileReader();
                blobReader.addEventListener("error", (e) => { reject(e); });
                blobReader.addEventListener("load", function() {
                  zipReader.close(function() {
                    resolve(blobReader.result);
                  });
                });
                blobReader.readAsArrayBuffer(blob);
              });
            }
          });
        }, function(error) {
          reject(error);
        });
      });
    }
  }
})();
<commit_msg>remove debug thing<commit_after>(function() {
  'use strict';

  angular.module('dimApp')
    .factory('dimManifestService', ManifestService);

  ManifestService.$inject = ['$q', 'dimBungieService', '$http', 'toaster', 'dimSettingsService'];

  function ManifestService($q, dimBungieService, $http, toaster, dimSettingsService) {
    // Testing flags
    const alwaysLoadRemote = false;

    let manifestPromise = null;

    const makeStatement = _.memoize(function(table, db) {
      return db.prepare(`select json from ${table} where id = ?`);
    });

    const service = {
      isLoaded: true,
      isError: false,
      statusText: null,
      version: null,

      // This tells users to reload the extension. It fires no more
      // often than every 10 seconds, and only warns if the manifest
      // version has actually changed.
      warnMissingDefinition: _.debounce(function() {
        dimBungieService.getManifest()
          .then(function(data) {
            const language = dimSettingsService.language;
            const path = data.mobileWorldContentPaths[language] || data.mobileWorldContentPaths.en;

            // The manifest has updated!
            if (path !== service.version) {
              toaster.pop('error', 'Outdated Destiny Info', "Bungie has updated their Destiny info database. Reload DIM to pick up the new info. Note that some things in DIM may not work for a few hours after Bungie updates Destiny, as the new data propagates through their systems.");
            }
          });
      }, 10000, true),

      getManifest: function() {
        if (manifestPromise) {
          return manifestPromise;
        }

        service.isLoaded = false;

        manifestPromise = dimBungieService.getManifest()
          .then(function(data) {
            const language = dimSettingsService.language;
            const path = data.mobileWorldContentPaths[language] || data.mobileWorldContentPaths.en;

            // Use the path as the version, rather than the "version" field, because
            // Bungie can update the manifest file without changing that version.
            const version = path;
            service.version = version;

            return loadManifestFromCache(version)
              .catch(function(e) {
                return loadManifestRemote(version, language, path);
              })
              .then(function(typedArray) {
                service.statusText = 'Building Destiny info database...';
                const db = new SQL.Database(typedArray);
                // do a small request, just to test it out
                service.getAllRecords(db, 'DestinyRaceDefinition');
                return db;
              });
          })
          .catch((e) => {
            service.statusText = "Error loading Destiny info: " + e.message + ". Reload to retry.";
            manifestPromise = null;
            service.isError = true;
            return deleteManifestFile().finally(() => $q.reject(e));
          });

        return manifestPromise;
      },

      getRecord: function(db, table, id) {
        const statement = makeStatement(table, db);
        // The ID in sqlite is a signed 32-bit int, while the id we
        // use is unsigned, so we must convert
        const sqlId = new Int32Array([id])[0];
        const result = statement.get([sqlId]);
        statement.reset();
        if (result.length) {
          return JSON.parse(result[0]);
        }
        return null;
      },

      getAllRecords: function(db, table) {
        const rows = db.exec(`SELECT json FROM ${table}`);
        const result = {};
        rows[0].values.forEach((row) => {
          const obj = JSON.parse(row);
          result[obj.hash] = obj;
        });
        return result;
      }
    };

    return service;

    /**
     * Returns a promise for the manifest data as a Uint8Array. Will cache it on succcess.
     */
    function loadManifestRemote(version, language, path) {
      service.statusText = 'Downloading latest Destiny info from Bungie...';
      return $http.get("https://www.bungie.net" + path, { responseType: "blob" })
        .then(function(response) {
          service.statusText = 'Unzipping latest Destiny info...';
          return unzipManifest(response.data);
        })
        .then(function(arraybuffer) {
          service.statusText = 'Saving latest Destiny info...';

          getLocalManifestFile()
            .then((fileEntry) => {
              fileEntry.createWriter((fileWriter) => {
                fileWriter.onwriteend = function(e) {
                  if (fileWriter.length === 0) { // truncate finished
                    fileWriter.write(new Blob([arraybuffer], { type: "application/octet-stream" }));
                  } else { // blob write finished
                    localStorage.setItem('manifest-version', version);
                    console.log("Sucessfully stored " + fileWriter.length + " byte manifest file.");
                    _gaq.push(['_trackEvent', 'Manifest', 'Downloaded']);
                  }
                };

                fileWriter.onerror = function(e) {
                  console.error('Write of manifest file failed', e);
                };

                fileWriter.truncate(0); // clear it out first
              });
            })
            .catch((e) => console.log('Error saving manifest file', e));

          var typedArray = new Uint8Array(arraybuffer);
          return typedArray;
        });
    }

    function getLocalManifestFile() {
      return $q((resolve, reject) => {
        const requestFileSystem = (window.requestFileSystem || window.webkitRequestFileSystem);
        // Ask for 60MB of temporary space. If Chrome gets rid of it we can always redownload.
        requestFileSystem(window.TEMPORARY, 60 * 1024 * 1024, (fs) => {
          fs.root.getFile('dimManifest', { create: true, exclusive: false }, (f) => resolve(f), (e) => reject(e));
        }, (e) => reject(e));
      });
    }

    function deleteManifestFile() {
      localStorage.removeItem('manifest-version');
      getLocalManifestFile().then((fileEntry) => {
        return $q((resolve, reject) => {
          fileEntry.remove(resolve, reject);
        });
      });
    }

    /**
     * Returns a promise for the cached manifest of the specified
     * version as a Uint8Array, or rejects.
     */
    function loadManifestFromCache(version) {
      if (alwaysLoadRemote) {
        return $q.reject(new Error("Testing - always load remote"));
      }

      service.statusText = "Loading saved Destiny info...";
      var currentManifestVersion = localStorage.getItem('manifest-version');
      if (currentManifestVersion === version) {
        // One version of this used chrome.storage.local with a
        // base64-encoded string, which is a bit slower. We may need
        // to do that again if the requestFileSystem API gets
        // removed.
        return getLocalManifestFile()
          .then((fileEntry) => {
            return $q((resolve, reject) => {
              fileEntry.file((file) => {
                var reader = new FileReader();
                reader.addEventListener("error", (e) => { reject(e); });
                reader.addEventListener("load", () => {
                  var typedArray = new Uint8Array(reader.result);
                  if (typedArray.length) {
                    resolve(typedArray);
                  } else {
                    reject(new Error("Empty cached manifest file"));
                  }
                });
                reader.readAsArrayBuffer(file);
              }, (e) => reject(e));
            });
          });
      } else {
        _gaq.push(['_trackEvent', 'Manifest', 'Need New Manifest']);
        return $q.reject(new Error("version mismatch: " + version + ' ' + currentManifestVersion));
      }
    }

    /**
     * Unzip a file from a ZIP Blob into an ArrayBuffer. Returns a promise.
     */
    function unzipManifest(blob) {
      return $q(function(resolve, reject) {
        zip.useWebWorkers = true;
        zip.workerScriptsPath = "vendor/zip.js/WebContent/";
        zip.createReader(new zip.BlobReader(blob), function(zipReader) {
          // get all entries from the zip
          zipReader.getEntries(function(entries) {
            if (entries.length) {
              entries[0].getData(new zip.BlobWriter(), function(blob) {
                var blobReader = new FileReader();
                blobReader.addEventListener("error", (e) => { reject(e); });
                blobReader.addEventListener("load", function() {
                  zipReader.close(function() {
                    resolve(blobReader.result);
                  });
                });
                blobReader.readAsArrayBuffer(blob);
              });
            }
          });
        }, function(error) {
          reject(error);
        });
      });
    }
  }
})();
<|endoftext|>


<commit_before>import React from 'react';

import SPELLS from 'common/SPELLS';
import ITEMS from 'common/ITEMS';
import SpellLink from 'common/SpellLink';
import ItemLink from 'common/ItemLink';

import ISSUE_IMPORTANCE from 'Parser/Core/ISSUE_IMPORTANCE';

import CoreCastEfficiency from 'Parser/Core/Modules/CastEfficiency';

/* eslint-disable no-unused-vars */

class CastEfficiency extends CoreCastEfficiency {
  static CPM_ABILITIES = [
    ...CoreCastEfficiency.CPM_ABILITIES,
    {
      spell: SPELLS.WAKE_OF_ASHES,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 30,
      recommendedCastEfficiency: 0.9,
      extraSuggestion: 'It has a high damage per execute time and generates a lot of holy power. It is better to waste 1-2 holy power than to hold the ability. Only hold the ability if adds are coming out in less than 3 seconds',
    },
    {
      spell: SPELLS.WAKE_OF_ASHES,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 30,
      isActive: combatant => combatant.hasShoulder(ITEMS.ASHES_TO_DUST.id),
      recommendedCastEfficiency: 1,
      extraSuggestion: With <ItemLink id={ITEMS.ASHES_TO_DUST.id} /> it is imperative you cast this on cooldown to get the damage bonus.,
      importance: ISSUE_IMPORTANCE.MAJOR,
    },
    {
      spell: SPELLS.CRUSADE_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 120,
      isActive: combatant => combatant.hasTalent(SPELLS.CRUSADE_TALENT.id),
      recommendedCastEfficiency: 0.9,
      importance: ISSUE_IMPORTANCE.MAJOR,
      extraSuggestion: This is our only cooldown and where most of our damage comes from. You really want to not lose a cast of this over a fight.
Note: It may be off by one cast if you use <SpellLink id={SPELLS.CRUSADE_TALENT.id} /> before the fight starts. You want to avoid doing this since your first GCD with the <SpellLink id={SPELLS.CRUSADE_TALENT.id} /> buff should be a spender.,
    },
    {
      spell: SPELLS.AVENGING_WRATH,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 120,
      hideWithZeroCasts: true,
      recommendedCastEfficiency: 0.9,
    },
    {
      spell: SPELLS.HOLY_WRATH_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 180,
      isActive: combatant => combatant.hasTalent(SPELLS.HOLY_WRATH_TALENT.id),
    },
    {
      spell: SPELLS.CRUSADER_STRIKE,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 3.5 / (1 + haste),
      isActive: combatant => combatant.hasTalent(SPELLS.THE_FIRES_OF_JUSTICE_TALENT.id),
    },
    {
      spell: SPELLS.CRUSADER_STRIKE,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 4.5 / (1 + haste),
      isActive: combatant => combatant.hasTalent(SPELLS.GREATER_JUDGMENT_TALENT.id),
    },
    {
      spell: SPELLS.ZEAL_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 4.5 / (1 + haste),
      isActive: combatant => combatant.hasTalent(SPELLS.ZEAL_TALENT.id),
    },
    {
      spell: SPELLS.JUDGMENT_CAST,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 12 / (1 + haste),
    },
    //This is the judgment CE with t20
    {
      spell: SPELLS.JUDGMENT_CAST,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 12 / (1 + haste),
      isActive: combatant => combatant.hasBuff(SPELLS.RET_PALADIN_T20_2SET_BONUS_BUFF.id),
      recommendedCastEfficiency: 0.95,
      extraSuggestion: 'With tier 20 2 peice it is even more important to use <SpellLink id={SPELLS.JUDGMENT_CAST.id} /> on cooldown to keep up the buff',
    },
    {
      spell: SPELLS.BLADE_OF_JUSTICE,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => null, // 10.5 / (1 + haste)
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.DIVINE_HAMMER_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 12 / (1 + haste),
      isActive: combatant => combatant.hasTalent(SPELLS.DIVINE_HAMMER_TALENT.id),
    },
    {
      spell: SPELLS.TEMPLARS_VERDICT,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => null,
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.DIVINE_STORM,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => null,
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.EXECUTION_SENTENCE_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 20 / (1 + haste),
      isActive: combatant => combatant.hasTalent(SPELLS.EXECUTION_SENTENCE_TALENT.id),
    },
    {
      spell: SPELLS.SHIELD_OF_VENGEANCE,
      category: CastEfficiency.SPELL_CATEGORIES.UTILITY,
      getCooldown: (haste, combatant) => 120 - (combatant.traitsBySpellId[SPELLS.DEFLECTION.id] || 0) * 10,
      noCanBeImproved: true,
      importance: ISSUE_IMPORTANCE.MINOR,
    },
    {
      spell: SPELLS.JUSTICARS_VENGEANCE_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.UTILITY,
      getCooldown: haste => null,
      isActive: combatant => combatant.hasTalent(SPELLS.JUSTICARS_VENGEANCE_TALENT.id),
      noSuggestion: true,
      noCanBeImproved: true,
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.EYE_FOR_AN_EYE_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.UTILITY,
      getCooldown: haste => null,
      isActive: combatant => combatant.hasTalent(SPELLS.EYE_FOR_AN_EYE_TALENT.id),
      noSuggestion: true,
      noCanBeImproved: true,
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.WORD_OF_GLORY_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.UTILITY,
      getCooldown: haste => null,
      isActive: combatant => combatant.hasTalent(SPELLS.WORD_OF_GLORY_TALENT.id),
      noSuggestion: true,
      noCanBeImproved: true,
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.ARCANE_TORRENT,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 90,
      hideWithZeroCasts: true,
    },
  ];
}

export default CastEfficiency;
<commit_msg>fixed a spelllink bug<commit_after>import React from 'react';

import SPELLS from 'common/SPELLS';
import ITEMS from 'common/ITEMS';
import SpellLink from 'common/SpellLink';
import ItemLink from 'common/ItemLink';

import ISSUE_IMPORTANCE from 'Parser/Core/ISSUE_IMPORTANCE';

import CoreCastEfficiency from 'Parser/Core/Modules/CastEfficiency';

/* eslint-disable no-unused-vars */

class CastEfficiency extends CoreCastEfficiency {
  static CPM_ABILITIES = [
    ...CoreCastEfficiency.CPM_ABILITIES,
    {
      spell: SPELLS.WAKE_OF_ASHES,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 30,
      recommendedCastEfficiency: 0.9,
      extraSuggestion: 'It has a high damage per execute time and generates a lot of holy power. It is better to waste 1-2 holy power than to hold the ability. Only hold the ability if adds are coming out in less than 3 seconds',
    },
    {
      spell: SPELLS.WAKE_OF_ASHES,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 30,
      isActive: combatant => combatant.hasShoulder(ITEMS.ASHES_TO_DUST.id),
      recommendedCastEfficiency: 1,
      extraSuggestion: With <ItemLink id={ITEMS.ASHES_TO_DUST.id} /> it is imperative you cast this on cooldown to get the damage bonus.,
      importance: ISSUE_IMPORTANCE.MAJOR,
    },
    {
      spell: SPELLS.CRUSADE_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 120,
      isActive: combatant => combatant.hasTalent(SPELLS.CRUSADE_TALENT.id),
      recommendedCastEfficiency: 0.9,
      importance: ISSUE_IMPORTANCE.MAJOR,
      extraSuggestion: This is our only cooldown and where most of our damage comes from. You really want to not lose a cast of this over a fight.
Note: It may be off by one cast if you use <SpellLink id={SPELLS.CRUSADE_TALENT.id} /> before the fight starts. You want to avoid doing this since your first GCD with the <SpellLink id={SPELLS.CRUSADE_TALENT.id} /> buff should be a spender.,
    },
    {
      spell: SPELLS.AVENGING_WRATH,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 120,
      hideWithZeroCasts: true,
      recommendedCastEfficiency: 0.9,
    },
    {
      spell: SPELLS.HOLY_WRATH_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 180,
      isActive: combatant => combatant.hasTalent(SPELLS.HOLY_WRATH_TALENT.id),
    },
    {
      spell: SPELLS.CRUSADER_STRIKE,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 3.5 / (1 + haste),
      isActive: combatant => combatant.hasTalent(SPELLS.THE_FIRES_OF_JUSTICE_TALENT.id),
    },
    {
      spell: SPELLS.CRUSADER_STRIKE,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 4.5 / (1 + haste),
      isActive: combatant => combatant.hasTalent(SPELLS.GREATER_JUDGMENT_TALENT.id),
    },
    {
      spell: SPELLS.ZEAL_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 4.5 / (1 + haste),
      isActive: combatant => combatant.hasTalent(SPELLS.ZEAL_TALENT.id),
    },
    {
      spell: SPELLS.JUDGMENT_CAST,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 12 / (1 + haste),
    },
    //This is the judgment CE with t20
    {
      spell: SPELLS.JUDGMENT_CAST,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 12 / (1 + haste),
      isActive: combatant => combatant.hasBuff(SPELLS.RET_PALADIN_T20_2SET_BONUS_BUFF.id),
      recommendedCastEfficiency: 0.95,
      extraSuggestion: With tier 20 2 peice it is even more important to use <SpellLink id={SPELLS.JUDGMENT_CAST.id} /> on cooldown to keep up the buff,
    },
    {
      spell: SPELLS.BLADE_OF_JUSTICE,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => null, // 10.5 / (1 + haste)
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.DIVINE_HAMMER_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 12 / (1 + haste),
      isActive: combatant => combatant.hasTalent(SPELLS.DIVINE_HAMMER_TALENT.id),
    },
    {
      spell: SPELLS.TEMPLARS_VERDICT,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => null,
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.DIVINE_STORM,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => null,
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.EXECUTION_SENTENCE_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.ROTATIONAL,
      getCooldown: haste => 20 / (1 + haste),
      isActive: combatant => combatant.hasTalent(SPELLS.EXECUTION_SENTENCE_TALENT.id),
    },
    {
      spell: SPELLS.SHIELD_OF_VENGEANCE,
      category: CastEfficiency.SPELL_CATEGORIES.UTILITY,
      getCooldown: (haste, combatant) => 120 - (combatant.traitsBySpellId[SPELLS.DEFLECTION.id] || 0) * 10,
      noCanBeImproved: true,
      importance: ISSUE_IMPORTANCE.MINOR,
    },
    {
      spell: SPELLS.JUSTICARS_VENGEANCE_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.UTILITY,
      getCooldown: haste => null,
      isActive: combatant => combatant.hasTalent(SPELLS.JUSTICARS_VENGEANCE_TALENT.id),
      noSuggestion: true,
      noCanBeImproved: true,
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.EYE_FOR_AN_EYE_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.UTILITY,
      getCooldown: haste => null,
      isActive: combatant => combatant.hasTalent(SPELLS.EYE_FOR_AN_EYE_TALENT.id),
      noSuggestion: true,
      noCanBeImproved: true,
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.WORD_OF_GLORY_TALENT,
      category: CastEfficiency.SPELL_CATEGORIES.UTILITY,
      getCooldown: haste => null,
      isActive: combatant => combatant.hasTalent(SPELLS.WORD_OF_GLORY_TALENT.id),
      noSuggestion: true,
      noCanBeImproved: true,
      hideWithZeroCasts: true,
    },
    {
      spell: SPELLS.ARCANE_TORRENT,
      category: CastEfficiency.SPELL_CATEGORIES.COOLDOWNS,
      getCooldown: haste => 90,
      hideWithZeroCasts: true,
    },
  ];
}

export default CastEfficiency;
<|endoftext|>


<commit_before>var semver = Npm.require('semver');

ConstraintSolver = {};

ConstraintSolver.Dependency = {
  packageName: String,
  version: Match.OneOf(String, null), // XXX 'x.y.z' or 'x.y.z'
  exact: Match.Optional(Boolean),
  weak: Match.Optional(Boolean),
  unordered: Match.Optional(Boolean)
};

// main class
ConstraintSolver.Resolver = function (Packages, Versions, Builds, options) {
  var self = this;

  options = options || {};
  var architecture = options.architecture || "all";

  // map package-name -> map
  //  map version -> object
  //    - dependendencies
  //    - earliestCompatibleVersion
  self.packageDeps = {};

  // package name -> list of version strings that we know about for the
  // package, sorted in ascending semver order
  self.sortedVersionsForPackage = {};

  Packages.find().forEach(function (packageDef) {
    self.packageDeps[packageDef.name] = {};

    Versions.find({ packageName: packageDef.name }).forEach(function (versionDef) {
      // version is a string #version-name-conflict
      var packageDep = {};
      packageDep.earliestCompatibleVersion = versionDef.earliestCompatibleVersion;
      packageDep.dependencies = _.map(versionDef.dependencies, function (dep, packageName) {
        return _.extend({packageName: packageName},
                        PackageVersion.parseVersionConstraint(dep.constraint));
      });

      self.packageDeps[packageDef.name][versionDef.version] = packageDep;
    });

    self.sortedVersionsForPackage[packageDef.name] = _.pluck(Versions.find({
      packageName: packageDef.name
    }).fetch(), 'version');
    console.log("Sorted versions for", packageDef.name,
                self.sortedVersionsForPackage[packageDef.name]);
    self.sortedVersionsForPackage[packageDef.name].sort(semver.compare);
  });
};

// The propagation of exact dependencies
// XXX empties the exactDepsStack
// XXX extends the depsDict
// XXX after this depsStack can contain duplicates
ConstraintSolver.Resolver.prototype._propagateExactDeps =
  function (depsDict, exactDepsStack) {
  var self = this;
  var picks = {};

  _.each(exactDepsStack, function (dep) { picks[dep.packageName] = dep.version; });

  while (exactDepsStack.length > 0) {
    var currentPick = exactDepsStack.pop();
    try {
      var currentDependencies =
        self.packageDeps[currentPick.packageName][currentPick.version].dependencies;
    } catch (err) {
      if (! _.has(self.packageDeps, currentPick.packageName))
        throw new Error("There is no required package found: " + currentPick.packageName);
      if (! _.has(self.packageDeps[currentPick.packageName], currentPick.version))
        throw new Error("There is no required package version found for the requested architecture: " + currentPick.packageName + "@" + currentPick.version);
    }

    _.each(pickExactDeps(currentDependencies), function (dep) {
      if (_.has(picks, dep.packageName)) {
        // XXX this error message should be improved so you can get a lot more
        // context, like what are initial exact dependencies (those user
        // specified) and what is the eventual conflict.
        if (picks[dep.packageName] !== dep.version)
          throw new Error("Unresolvable: two exact dependencies conflict: " +
                          dep.packageName + " versions: " +
                          [picks[dep.packageName], dep.version].join(", "));
      } else {
        picks[dep.packageName] = dep.version;
        exactDepsStack.push(dep);
      }
    });

    _.each(rejectExactDeps(currentDependencies), function (dep) {
      depsDict[dep.packageName] = depsDict[dep.packageName] || [];
      depsDict[dep.packageName].push(dep);
    });
  };

  return picks;
};

ConstraintSolver.Resolver.prototype._resolve = function (dependencies, state) {
  check(dependencies, [ConstraintSolver.Dependency]);

  state = state || {};
  state.picks = state.picks || {};

  var self = this;

  state.depsDict = state.depsDict || {};
  _.each(rejectExactDeps(dependencies), function (dep) {
    state.depsDict[dep.packageName] = state.depsDict[dep.packageName] || [];
    state.depsDict[dep.packageName].push(dep);
  });

  state.exactDepsStack = state.exactDepsStack || pickExactDeps(dependencies);

  var exactPicks = self._propagateExactDeps(state.depsDict, state.exactDepsStack);

  // add all exact dependencies the propagator picked to the set of picks
  _.each(exactPicks, function (version, packageName) {
    if (_.has(state.picks, packageName)) {
      if (state.picks[packageName] !== version)
        throw new Error("Exact dependencies contradict with already picked version for a package: "
                        + packageName + " " + state.picks[packageName] + ": " + version);
    } else {
      state.picks[packageName] = version;
    }
  });

  // check if all non-exact dependencies are still satisfied
  _.each(state.picks, function (version, packageName) {
    _.each(state.depsDict[packageName], function (dep) {
      if (! self.dependencyIsSatisfied(dep, version))
        throw new Error("Exact dependency contradicts on of the constraints for a package: "
                        + packageName + " " + version + ": " + dep.version);
    });
  });

  // calculate packages we depend on but didn't pick a version for yet
  // pick one of those and try different versions
  var candidatePackageNames = _.difference(_.keys(state.depsDict), _.keys(state.picks));
  if (_.isEmpty(candidatePackageNames)) {
    //console.log('there is nothing to look for! successsss');
    return state.picks;
  }

  var candidatePackageName = candidatePackageNames[0];
  var availableVersions = self.sortedVersionsForPackage[candidatePackageName];
  var satisfyingVersions = _.filter(availableVersions, function (version) {
    return _.all(state.depsDict[candidatePackageName], function (dep) {
      return self.dependencyIsSatisfied(dep, version);
    });
  });

  if (_.isEmpty(satisfyingVersions))
    throw new Error("Cannot find a satisfying versions of package: "
                    + candidatePackageName);

  for (var i = 0; i < satisfyingVersions.length; i++) {
    var version = satisfyingVersions[i];
    var newState = EJSON.clone(state);
    newState.picks[candidatePackageName] = version;
    newState.exactDepsStack.push({
      packageName: candidatePackageName,
      version: version
    });
    //console.log('trying ' + candidatePackageName + ' v.' + versionDef.version);
    // recurse
    try {
      // if not failed, return a happy result
      return self._resolve(dependencies, newState);
    } catch (err) {
      //console.log('picking ' + candidatePackageName + ' v.' + version + ' kinda failed, lets not do that: ' + err.message);
    }
  };

  throw new Error("Cannot pick a satisfying version of package " + candidatePackageName);
};

ConstraintSolver.Resolver.prototype.resolve = function (dependencies) {
  var self = this;
  return self._resolve(toStructuredDeps(dependencies));
};

ConstraintSolver.Resolver.prototype.propagatedExactDeps = function (dependencies) {
  var self = this;

  dependencies = toStructuredDeps(dependencies);
  var depsStack = rejectExactDeps(dependencies);
  var exactDepsStack = pickExactDeps(dependencies);
  return self._propagateExactDeps(depsStack, exactDepsStack);
};

ConstraintSolver.Resolver.prototype.dependencyIsSatisfied =
  function (dep, version) {
  // XXX check for exact
  var self = this;
  var versionSpec = self.packageDeps[dep.packageName][version];
  return semver.lte(dep.version, version) &&
    semver.lte(versionSpec.earliestCompatibleVersion, dep.version);
};

// helpers
var isExact = function (dep) { return dep.exact; };
var pickExactDeps = function (deps) { return _.filter(deps, isExact); };
var rejectExactDeps = function (deps) { return _.reject(deps, isExact); };

// converts dependencies from simple format to the structured format
var toStructuredDeps = function (dependencies) {
  var structuredDeps = [];
  _.each(dependencies, function (details, packageName) {
    if (typeof details === "string") {
      structuredDeps.push(_.extend(
        { packageName: packageName },
        PackageVersion.parseVersionConstraint(details)));
    } else {
      structuredDeps.push(_.extend({ packageName: packageName }, details));
    }
  });

  return structuredDeps;
};
<commit_msg>Remove stray console.log<commit_after>var semver = Npm.require('semver');

ConstraintSolver = {};

ConstraintSolver.Dependency = {
  packageName: String,
  version: Match.OneOf(String, null), // XXX 'x.y.z' or 'x.y.z'
  exact: Match.Optional(Boolean),
  weak: Match.Optional(Boolean),
  unordered: Match.Optional(Boolean)
};

// main class
ConstraintSolver.Resolver = function (Packages, Versions, Builds, options) {
  var self = this;

  options = options || {};
  var architecture = options.architecture || "all";

  // map package-name -> map
  //  map version -> object
  //    - dependendencies
  //    - earliestCompatibleVersion
  self.packageDeps = {};

  // package name -> list of version strings that we know about for the
  // package, sorted in ascending semver order
  self.sortedVersionsForPackage = {};

  Packages.find().forEach(function (packageDef) {
    self.packageDeps[packageDef.name] = {};

    Versions.find({ packageName: packageDef.name }).forEach(function (versionDef) {
      // version is a string #version-name-conflict
      var packageDep = {};
      packageDep.earliestCompatibleVersion = versionDef.earliestCompatibleVersion;
      packageDep.dependencies = _.map(versionDef.dependencies, function (dep, packageName) {
        return _.extend({packageName: packageName},
                        PackageVersion.parseVersionConstraint(dep.constraint));
      });

      self.packageDeps[packageDef.name][versionDef.version] = packageDep;
    });

    self.sortedVersionsForPackage[packageDef.name] = _.pluck(Versions.find({
      packageName: packageDef.name
    }).fetch(), 'version');
    self.sortedVersionsForPackage[packageDef.name].sort(semver.compare);
  });
};

// The propagation of exact dependencies
// XXX empties the exactDepsStack
// XXX extends the depsDict
// XXX after this depsStack can contain duplicates
ConstraintSolver.Resolver.prototype._propagateExactDeps =
  function (depsDict, exactDepsStack) {
  var self = this;
  var picks = {};

  _.each(exactDepsStack, function (dep) { picks[dep.packageName] = dep.version; });

  while (exactDepsStack.length > 0) {
    var currentPick = exactDepsStack.pop();
    try {
      var currentDependencies =
        self.packageDeps[currentPick.packageName][currentPick.version].dependencies;
    } catch (err) {
      if (! _.has(self.packageDeps, currentPick.packageName))
        throw new Error("There is no required package found: " + currentPick.packageName);
      if (! _.has(self.packageDeps[currentPick.packageName], currentPick.version))
        throw new Error("There is no required package version found for the requested architecture: " + currentPick.packageName + "@" + currentPick.version);
    }

    _.each(pickExactDeps(currentDependencies), function (dep) {
      if (_.has(picks, dep.packageName)) {
        // XXX this error message should be improved so you can get a lot more
        // context, like what are initial exact dependencies (those user
        // specified) and what is the eventual conflict.
        if (picks[dep.packageName] !== dep.version)
          throw new Error("Unresolvable: two exact dependencies conflict: " +
                          dep.packageName + " versions: " +
                          [picks[dep.packageName], dep.version].join(", "));
      } else {
        picks[dep.packageName] = dep.version;
        exactDepsStack.push(dep);
      }
    });

    _.each(rejectExactDeps(currentDependencies), function (dep) {
      depsDict[dep.packageName] = depsDict[dep.packageName] || [];
      depsDict[dep.packageName].push(dep);
    });
  };

  return picks;
};

ConstraintSolver.Resolver.prototype._resolve = function (dependencies, state) {
  check(dependencies, [ConstraintSolver.Dependency]);

  state = state || {};
  state.picks = state.picks || {};

  var self = this;

  state.depsDict = state.depsDict || {};
  _.each(rejectExactDeps(dependencies), function (dep) {
    state.depsDict[dep.packageName] = state.depsDict[dep.packageName] || [];
    state.depsDict[dep.packageName].push(dep);
  });

  state.exactDepsStack = state.exactDepsStack || pickExactDeps(dependencies);

  var exactPicks = self._propagateExactDeps(state.depsDict, state.exactDepsStack);

  // add all exact dependencies the propagator picked to the set of picks
  _.each(exactPicks, function (version, packageName) {
    if (_.has(state.picks, packageName)) {
      if (state.picks[packageName] !== version)
        throw new Error("Exact dependencies contradict with already picked version for a package: "
                        + packageName + " " + state.picks[packageName] + ": " + version);
    } else {
      state.picks[packageName] = version;
    }
  });

  // check if all non-exact dependencies are still satisfied
  _.each(state.picks, function (version, packageName) {
    _.each(state.depsDict[packageName], function (dep) {
      if (! self.dependencyIsSatisfied(dep, version))
        throw new Error("Exact dependency contradicts on of the constraints for a package: "
                        + packageName + " " + version + ": " + dep.version);
    });
  });

  // calculate packages we depend on but didn't pick a version for yet
  // pick one of those and try different versions
  var candidatePackageNames = _.difference(_.keys(state.depsDict), _.keys(state.picks));
  if (_.isEmpty(candidatePackageNames)) {
    //console.log('there is nothing to look for! successsss');
    return state.picks;
  }

  var candidatePackageName = candidatePackageNames[0];
  var availableVersions = self.sortedVersionsForPackage[candidatePackageName];
  var satisfyingVersions = _.filter(availableVersions, function (version) {
    return _.all(state.depsDict[candidatePackageName], function (dep) {
      return self.dependencyIsSatisfied(dep, version);
    });
  });

  if (_.isEmpty(satisfyingVersions))
    throw new Error("Cannot find a satisfying versions of package: "
                    + candidatePackageName);

  for (var i = 0; i < satisfyingVersions.length; i++) {
    var version = satisfyingVersions[i];
    var newState = EJSON.clone(state);
    newState.picks[candidatePackageName] = version;
    newState.exactDepsStack.push({
      packageName: candidatePackageName,
      version: version
    });
    //console.log('trying ' + candidatePackageName + ' v.' + versionDef.version);
    // recurse
    try {
      // if not failed, return a happy result
      return self._resolve(dependencies, newState);
    } catch (err) {
      //console.log('picking ' + candidatePackageName + ' v.' + version + ' kinda failed, lets not do that: ' + err.message);
    }
  };

  throw new Error("Cannot pick a satisfying version of package " + candidatePackageName);
};

ConstraintSolver.Resolver.prototype.resolve = function (dependencies) {
  var self = this;
  return self._resolve(toStructuredDeps(dependencies));
};

ConstraintSolver.Resolver.prototype.propagatedExactDeps = function (dependencies) {
  var self = this;

  dependencies = toStructuredDeps(dependencies);
  var depsStack = rejectExactDeps(dependencies);
  var exactDepsStack = pickExactDeps(dependencies);
  return self._propagateExactDeps(depsStack, exactDepsStack);
};

ConstraintSolver.Resolver.prototype.dependencyIsSatisfied =
  function (dep, version) {
  // XXX check for exact
  var self = this;
  var versionSpec = self.packageDeps[dep.packageName][version];
  return semver.lte(dep.version, version) &&
    semver.lte(versionSpec.earliestCompatibleVersion, dep.version);
};

// helpers
var isExact = function (dep) { return dep.exact; };
var pickExactDeps = function (deps) { return _.filter(deps, isExact); };
var rejectExactDeps = function (deps) { return _.reject(deps, isExact); };

// converts dependencies from simple format to the structured format
var toStructuredDeps = function (dependencies) {
  var structuredDeps = [];
  _.each(dependencies, function (details, packageName) {
    if (typeof details === "string") {
      structuredDeps.push(_.extend(
        { packageName: packageName },
        PackageVersion.parseVersionConstraint(details)));
    } else {
      structuredDeps.push(_.extend({ packageName: packageName }, details));
    }
  });

  return structuredDeps;
};
<|endoftext|>


<commit_before>/**
 * Copyright IBM Corp. 2016, 2018
 *
 * This source code is licensed under the Apache-2.0 license found in the
 * LICENSE file in the root directory of this source tree.
 */

import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { ChevronDown16 } from '@carbon/icons-react';
import { settings } from 'carbon-components';
import { keys, match, matches } from '../../internal/keyboard';

const { prefix } = settings;

export default class Tabs extends React.Component {
  static propTypes = {
    /**
     * Specify the text to be read by screen-readers when visiting the <Tabs>
     * component
     */
    ariaLabel: PropTypes.string,

    /**
     * Pass in a collection of <Tab> children to be rendered depending on the
     * currently selected tab
     */
    children: PropTypes.node,

    /**
     * Provide a className that is applied to the root  component for the
     * <Tabs>
     */
    className: PropTypes.string,

    /**
     * Specify whether the Tab content is hidden
     */
    hidden: PropTypes.bool,

    /**
     * By default, this value is "navigation". You can also provide an alternate
     * role if it makes sense from the accessibility-side
     */
    role: PropTypes.string.isRequired,

    /**
     * Provide the type of Tab
     */
    type: PropTypes.oneOf(['default', 'container']),

    /**
     * Optionally provide an `onClick` handler that is invoked when a <Tab> is
     * clicked
     */
    onClick: PropTypes.func,

    /**
     * Optionally provide an `onKeyDown` handler that is invoked when keyed
     * navigation is triggered
     */
    onKeyDown: PropTypes.func,

    /**
     * Provide an optional handler that is called whenever the selection
     * changes. This method is called with the index of the tab that was
     * selected
     */
    onSelectionChange: PropTypes.func,

    /**
     * Provide a string that represents the `href` for the triggered <Tab>
     */
    triggerHref: PropTypes.string.isRequired,

    /**
     * Optionally provide an index for the currently selected <Tab>
     */
    selected: PropTypes.number,

    /**
     * Provide a description that is read out when a user visits the caret icon
     * for the dropdown menu of items
     */
    iconDescription: PropTypes.string.isRequired,

    /**
     * Provide a className that is applied to the <TabContent> components
     */
    tabContentClassName: PropTypes.string,
  };

  static defaultProps = {
    iconDescription: 'show menu options',
    role: 'navigation',
    type: 'default',
    triggerHref: '#',
    selected: 0,
    ariaLabel: 'listbox',
  };

  state = {
    dropdownHidden: true,
  };

  static getDerivedStateFromProps({ selected }, state) {
    const { prevSelected } = state;
    return prevSelected === selected
      ? null
      : {
          selected,
          prevSelected: selected,
        };
  }

  getTabs() {
    return React.Children.map(this.props.children, tab => tab);
  }

  getEnabledTabs = () =>
    React.Children.toArray(this.props.children).reduce(
      (acc, tab, index) => (!tab.props.disabled ? acc.concat(index) : acc),
      []
    );

  getTabAt = (index, useFresh) => {
    return (
      (!useFresh && this[`tab${index}`]) ||
      React.Children.toArray(this.props.children)[index]
    );
  };

  setTabAt = (index, tabRef) => {
    this[`tab${index}`] = tabRef;
  };

  // following functions (handle*) are Props on Tab.js, see Tab.js for parameters
  handleTabClick = onSelectionChange => {
    return (index, evt) => {
      evt.preventDefault();

      this.selectTabAt(index, onSelectionChange);
      this.setState({
        dropdownHidden: true,
      });
    };
  };

  getDirection = evt => {
    if (match(evt, keys.ArrowLeft)) {
      return -1;
    }
    if (match(evt, keys.ArrowRight)) {
      return 1;
    }
    return 0;
  };

  getNextIndex = (index, direction) => {
    const enabledTabs = this.getEnabledTabs();
    const nextIndex = Math.max(
      enabledTabs.indexOf(index) + direction,
      -1 /* For `tab` not found in `enabledTabs` */
    );
    const nextIndexLooped =
      nextIndex >= 0 && nextIndex < enabledTabs.length
        ? nextIndex
        : nextIndex - Math.sign(nextIndex) * enabledTabs.length;
    return enabledTabs[nextIndexLooped];
  };

  handleTabKeyDown = onSelectionChange => {
    return (index, evt) => {
      if (matches(evt, [keys.Enter, keys.Space])) {
        this.selectTabAt(index, onSelectionChange);
        this.setState({
          dropdownHidden: true,
        });
      }

      if (window.matchMedia('(min-width: 42rem)').matches) {
        evt.preventDefault();
        const nextIndex = this.getNextIndex(index, this.getDirection(evt));
        const tab = this.getTabAt(nextIndex);
        if (tab) {
          this.selectTabAt(nextIndex, onSelectionChange);
          if (tab.tabAnchor) {
            tab.tabAnchor.focus();
          }
        }
      }
    };
  };

  handleDropdownClick = () => {
    this.setState({
      dropdownHidden: !this.state.dropdownHidden,
    });
  };

  selectTabAt = (index, onSelectionChange) => {
    if (this.state.selected !== index) {
      this.setState({
        selected: index,
      });
      if (typeof onSelectionChange === 'function') {
        onSelectionChange(index);
      }
    }
  };

  render() {
    const {
      ariaLabel,
      iconDescription,
      className,
      triggerHref,
      role,
      type,
      onSelectionChange,
      tabContentClassName,
      ...other
    } = this.props;

    /**
     * The tab panel acts like a tab panel when the screen is wider, but acts
     * like a select list when the screen is narrow.  In the wide case we want
     * to allow the user to use the tab key to set the focus in the tab panel
     * and then use the left and right arrow keys to navigate the tabs.  In the
     * narrow case we want to use the tab key to select different options in
     * the list.
     *
     * We set the tab index based on the different states so the browser will treat
     * the whole tab panel as a single focus component when it looks like a tab
     * panel and separate components when it looks like a select list.
     */
    const tabsWithProps = this.getTabs().map((tab, index) => {
      const tabPanelIndex = index === this.state.selected ? 0 : -1;
      const tabIndex = !this.state.dropdownHidden ? 0 : tabPanelIndex;
      const newTab = React.cloneElement(tab, {
        index,
        selected: index === this.state.selected,
        handleTabClick: this.handleTabClick(onSelectionChange),
        tabIndex,
        ref: e => {
          this.setTabAt(index, e);
        },
        handleTabKeyDown: this.handleTabKeyDown(onSelectionChange),
      });

      return newTab;
    });

    const tabContentWithProps = React.Children.map(tabsWithProps, tab => {
      const {
        id: tabId,
        children,
        selected,
        renderContent: TabContent,
      } = tab.props;

      return (
        <TabContent
          id={tabId && `${tabId}__panel`}
          className={tabContentClassName}
          aria-hidden={!selected}
          hidden={!selected}
          selected={selected}
          aria-labelledby={tabId}>
          {children}
        </TabContent>
      );
    });

    const classes = {
      tabs: classNames(`${prefix}--tabs`, className, {
        [`${prefix}--tabs--container`]: type === 'container',
      }),
      tablist: classNames(`${prefix}--tabs__nav`, {
        [`${prefix}--tabs__nav--hidden`]: this.state.dropdownHidden,
      }),
    };

    const selectedTab = this.getTabAt(this.state.selected, true);
    const selectedLabel = selectedTab ? selectedTab.props.label : '';

    return (
      <>

          <div
            role="listbox"
            aria-label={ariaLabel}
            tabIndex={0}
            className={`${prefix}--tabs-trigger`}
            onClick={this.handleDropdownClick}
            onKeyPress={this.handleDropdownClick}>
            <a
              tabIndex={-1}
              className={`${prefix}--tabs-trigger-text`}
              href={triggerHref}
              onClick={this.handleDropdownClick}>
              {selectedLabel}

            <ChevronDown16 aria-hidden="true">
              {iconDescription && <title>{iconDescription}</title>}
            </ChevronDown16>

            {tabsWithProps}

        {tabContentWithProps}
      </>
    );
  }
}
<commit_msg>fix(Tabs): allow keyboard event defaults (#5124)<commit_after>/**
 * Copyright IBM Corp. 2016, 2018
 *
 * This source code is licensed under the Apache-2.0 license found in the
 * LICENSE file in the root directory of this source tree.
 */

import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { ChevronDown16 } from '@carbon/icons-react';
import { settings } from 'carbon-components';
import { keys, match, matches } from '../../internal/keyboard';

const { prefix } = settings;

export default class Tabs extends React.Component {
  static propTypes = {
    /**
     * Specify the text to be read by screen-readers when visiting the <Tabs>
     * component
     */
    ariaLabel: PropTypes.string,

    /**
     * Pass in a collection of <Tab> children to be rendered depending on the
     * currently selected tab
     */
    children: PropTypes.node,

    /**
     * Provide a className that is applied to the root  component for the
     * <Tabs>
     */
    className: PropTypes.string,

    /**
     * Specify whether the Tab content is hidden
     */
    hidden: PropTypes.bool,

    /**
     * By default, this value is "navigation". You can also provide an alternate
     * role if it makes sense from the accessibility-side
     */
    role: PropTypes.string.isRequired,

    /**
     * Provide the type of Tab
     */
    type: PropTypes.oneOf(['default', 'container']),

    /**
     * Optionally provide an `onClick` handler that is invoked when a <Tab> is
     * clicked
     */
    onClick: PropTypes.func,

    /**
     * Optionally provide an `onKeyDown` handler that is invoked when keyed
     * navigation is triggered
     */
    onKeyDown: PropTypes.func,

    /**
     * Provide an optional handler that is called whenever the selection
     * changes. This method is called with the index of the tab that was
     * selected
     */
    onSelectionChange: PropTypes.func,

    /**
     * Provide a string that represents the `href` for the triggered <Tab>
     */
    triggerHref: PropTypes.string.isRequired,

    /**
     * Optionally provide an index for the currently selected <Tab>
     */
    selected: PropTypes.number,

    /**
     * Provide a description that is read out when a user visits the caret icon
     * for the dropdown menu of items
     */
    iconDescription: PropTypes.string.isRequired,

    /**
     * Provide a className that is applied to the <TabContent> components
     */
    tabContentClassName: PropTypes.string,
  };

  static defaultProps = {
    iconDescription: 'show menu options',
    role: 'navigation',
    type: 'default',
    triggerHref: '#',
    selected: 0,
    ariaLabel: 'listbox',
  };

  state = {
    dropdownHidden: true,
  };

  static getDerivedStateFromProps({ selected }, state) {
    const { prevSelected } = state;
    return prevSelected === selected
      ? null
      : {
          selected,
          prevSelected: selected,
        };
  }

  getTabs() {
    return React.Children.map(this.props.children, tab => tab);
  }

  getEnabledTabs = () =>
    React.Children.toArray(this.props.children).reduce(
      (acc, tab, index) => (!tab.props.disabled ? acc.concat(index) : acc),
      []
    );

  getTabAt = (index, useFresh) => {
    return (
      (!useFresh && this[`tab${index}`]) ||
      React.Children.toArray(this.props.children)[index]
    );
  };

  setTabAt = (index, tabRef) => {
    this[`tab${index}`] = tabRef;
  };

  // following functions (handle*) are Props on Tab.js, see Tab.js for parameters
  handleTabClick = onSelectionChange => {
    return (index, evt) => {
      evt.preventDefault();

      this.selectTabAt(index, onSelectionChange);
      this.setState({
        dropdownHidden: true,
      });
    };
  };

  getDirection = evt => {
    if (match(evt, keys.ArrowLeft)) {
      return -1;
    }
    if (match(evt, keys.ArrowRight)) {
      return 1;
    }
    return 0;
  };

  getNextIndex = (index, direction) => {
    const enabledTabs = this.getEnabledTabs();
    const nextIndex = Math.max(
      enabledTabs.indexOf(index) + direction,
      -1 /* For `tab` not found in `enabledTabs` */
    );
    const nextIndexLooped =
      nextIndex >= 0 && nextIndex < enabledTabs.length
        ? nextIndex
        : nextIndex - Math.sign(nextIndex) * enabledTabs.length;
    return enabledTabs[nextIndexLooped];
  };

  handleTabKeyDown = onSelectionChange => {
    return (index, evt) => {
      if (matches(evt, [keys.Enter, keys.Space])) {
        this.selectTabAt(index, onSelectionChange);
        this.setState({
          dropdownHidden: true,
        });
      }

      if (window.matchMedia('(min-width: 42rem)').matches) {
        const nextIndex = this.getNextIndex(index, this.getDirection(evt));
        const tab = this.getTabAt(nextIndex);
        if (tab && matches(evt, [keys.ArrowLeft, keys.ArrowRight])) {
          evt.preventDefault();
          this.selectTabAt(nextIndex, onSelectionChange);
          if (tab.tabAnchor) {
            tab.tabAnchor.focus();
          }
        }
      }
    };
  };

  handleDropdownClick = () => {
    this.setState({
      dropdownHidden: !this.state.dropdownHidden,
    });
  };

  selectTabAt = (index, onSelectionChange) => {
    if (this.state.selected !== index) {
      this.setState({
        selected: index,
      });
      if (typeof onSelectionChange === 'function') {
        onSelectionChange(index);
      }
    }
  };

  render() {
    const {
      ariaLabel,
      iconDescription,
      className,
      triggerHref,
      role,
      type,
      onSelectionChange,
      tabContentClassName,
      ...other
    } = this.props;

    /**
     * The tab panel acts like a tab panel when the screen is wider, but acts
     * like a select list when the screen is narrow.  In the wide case we want
     * to allow the user to use the tab key to set the focus in the tab panel
     * and then use the left and right arrow keys to navigate the tabs.  In the
     * narrow case we want to use the tab key to select different options in
     * the list.
     *
     * We set the tab index based on the different states so the browser will treat
     * the whole tab panel as a single focus component when it looks like a tab
     * panel and separate components when it looks like a select list.
     */
    const tabsWithProps = this.getTabs().map((tab, index) => {
      const tabPanelIndex = index === this.state.selected ? 0 : -1;
      const tabIndex = !this.state.dropdownHidden ? 0 : tabPanelIndex;
      const newTab = React.cloneElement(tab, {
        index,
        selected: index === this.state.selected,
        handleTabClick: this.handleTabClick(onSelectionChange),
        tabIndex,
        ref: e => {
          this.setTabAt(index, e);
        },
        handleTabKeyDown: this.handleTabKeyDown(onSelectionChange),
      });

      return newTab;
    });

    const tabContentWithProps = React.Children.map(tabsWithProps, tab => {
      const {
        id: tabId,
        children,
        selected,
        renderContent: TabContent,
      } = tab.props;

      return (
        <TabContent
          id={tabId && `${tabId}__panel`}
          className={tabContentClassName}
          aria-hidden={!selected}
          hidden={!selected}
          selected={selected}
          aria-labelledby={tabId}>
          {children}
        </TabContent>
      );
    });

    const classes = {
      tabs: classNames(`${prefix}--tabs`, className, {
        [`${prefix}--tabs--container`]: type === 'container',
      }),
      tablist: classNames(`${prefix}--tabs__nav`, {
        [`${prefix}--tabs__nav--hidden`]: this.state.dropdownHidden,
      }),
    };

    const selectedTab = this.getTabAt(this.state.selected, true);
    const selectedLabel = selectedTab ? selectedTab.props.label : '';

    return (
      <>

          <div
            role="listbox"
            aria-label={ariaLabel}
            tabIndex={0}
            className={`${prefix}--tabs-trigger`}
            onClick={this.handleDropdownClick}
            onKeyPress={this.handleDropdownClick}>
            <a
              tabIndex={-1}
              className={`${prefix}--tabs-trigger-text`}
              href={triggerHref}
              onClick={this.handleDropdownClick}>
              {selectedLabel}

            <ChevronDown16 aria-hidden="true">
              {iconDescription && <title>{iconDescription}</title>}
            </ChevronDown16>

            {tabsWithProps}

        {tabContentWithProps}
      </>
    );
  }
}
<|endoftext|>