<commit_before>/**
  * require_module: "z-calendar"
  * require_module: "z-button"
 **/
function DateTimePicker(input) {
  var flyout;

  var init = function() {
    if (input.tagName == "Z-INPUT") {
      input = input.querySelector("input");
    }

    // no input provided
    if (input.tagName != "INPUT") {
      console.log("DateTimePicker Error: no input provided");
      return;
    }

    var tpl = $.getTemplate("widgets/z-datetimepicker", "z-datetimepicker-base-tpl");
    // insert tpl after input
    input.style.cursor = "pointer";
    input.parentNode.insertBefore(tpl, input.nextSibling);
    //FIXME
    flyout = input.nextElementSibling;

    // don't close on click within datetimepicker
    flyout.addEventListener("click", function(e) {
      e.stopPropagation();
    }, false);

    flyout.querySelector("button").addEventListener("click", function(e) {
      apply();
    }, false);

    handleVisibility();
    controlTimeInput();
  };

  var hide = function() {
    flyout.removeAttribute("data-active");

    window.removeEventListener("click", onWindowClick, false);
  };

  var show = function() {
    onTimeChange();
    var pos = input.getBoundingClientRect();

    // set flyout top and left position
    flyout.style.top = (pos.top + pos.height) + "px";
    flyout.setAttribute("data-active", "active");
    flyout.style.left = (pos.left - (flyout.offsetWidth - pos.width) / 2) + "px";

    window.addEventListener("click", onWindowClick, false);
  };

  var onWindowClick = function() {
    hide();
  };

  /*************************** PRIVATE **********************************/

  var handleVisibility = function() {

    input.addEventListener("click", function(e) {
      e.stopPropagation();
      if (flyout.hasAttribute("data-active")) {
        hide();
      } else {
        show();
      }
    }, false);
  };

  var controlTimeInput = function() {
    var inputs = flyout.querySelectorAll("input");

    for (var i = 0; i < inputs.length; i++) {
      inputs[i].addEventListener("blur", function() {

        switch (this.value.length) {
          // if no value is given set it to 00
          case 0:
            this.value = "00";
            break;

          case 1:
            var value = parseInt(this.value, 10);
            // non-integer value
            if (isNaN(value)) {
              renderTimeControlError(this);
              return;
            }
            this.value = DateUtil.appendLeadingZero(value);
            break;

          case 2:
            // non-integer value
            if (isNaN(parseInt(this.value[0], 10)) ||
                isNaN(parseInt(this.value[1], 10))) {
              renderTimeControlError(this);
              return;
            }
            break;
        }

        var value = parseInt(this.value, 10);
        if (this.getAttribute("data-factor") == "3600") {
          // hours value > 24
          if (value > 23) {
            renderTimeControlError(this);
            return;
          }
        } else {
          // minutes or seconds value > 59
          if (value > 59) {
           renderTimeControlError(this);
            return;
          }
        }

        // correct input value
        removeTimeControlError(this);
      }, false);
    }
  };

  var removeTimeControlError = function(time_control) {
    time_control.classList.remove("error");

    // if only correct input values enable apply button
    if (flyout.querySelector("input.error") == null) {
      flyout.querySelector("button").removeAttribute("data-state");
    }
  };

  var renderTimeControlError = function(time_control) {
    time_control.classList.add("error");

    // disable apply button
    flyout.querySelector("button").setAttribute("data-state", "disabled");
  };

  var renderErrorMessage = function() {
    flyout.querySelector("z-datetimepicker-error")
      .setAttribute("data-active", "active");
  };

  var removeErrorMessage = function() {
    flyout.querySelector("z-datetimepicker-error")
        .removeAttribute("data-active", "active");
  };

  // set time value and human formatted time string
  var onTimeChange = function() {
    var timestamp = Math.floor(
      parseInt(input.getAttribute("data-timestamp"), 10));

    // set input value as human formatted string
    input.value = DateUtil.printTimestamp(timestamp);

    timestamp = Math.floor(timestamp / 1000);
    var date = new Date(timestamp);
    var utc_day_start = DateUtil.getStartOfDay(timestamp) +
      date.getTimezoneOffset() * DateUtil.millisPerMinute;

    // set z-calendar selection
    flyout.querySelector("z-calendar").setAttribute("data-selected", utc_day_start);
    // set hours input value
    flyout.querySelector("input[data-value='hours']").value =
      DateUtil.appendLeadingZero(date.getHours());
    // set minutes input value
    flyout.querySelector("input[data-value='minutes']").value =
      DateUtil.appendLeadingZero(date.getMinutes());
    // set seconds input value
    flyout.querySelector("input[data-value='seconds']").value =
      DateUtil.appendLeadingZero(date.getSeconds());
  };

  // get selected datetime as timestamp
  var getTimeValue = function() {
    var timestamp = DateUtil.parseTimestamp(
      parseInt(flyout.querySelector("z-calendar")
        .getAttribute("data-selected"), 10));

    var inputs = flyout.querySelectorAll("input");
    for (var i = 0; i < inputs.length; i++) {
      var value = parseInt(inputs[i].value, 10);
      timestamp += value * parseInt(inputs[i].getAttribute("data-factor"), 10);
    }

    timestamp = timestamp * 1000;
    return timestamp;
  };

  // checks if the selected date is in the past
  var isValidTimeValue = function(timestamp) {
    // error -> future datetime
    if (Math.floor(timestamp / 1000) > Date.now()) {
      renderErrorMessage();
      return false;
    }

    removeErrorMessage();
    return true;
  };

  var apply = function() {
    var timestamp = getTimeValue();

    // invalid timestamp
    if (!isValidTimeValue(timestamp)) {
      return;
    }

    input.setAttribute("data-timestamp", timestamp);
    onTimeChange();
    hide();

    // fire change event on input
    var evt = new CustomEvent("z-datetimepicker-change");
    input.dispatchEvent(evt);
  };

  init();
  return {
    show: show,
    hide: hide
  };
};
<commit_msg>formatting<commit_after>/**
  * require_module: "z-calendar"
  * require_module: "z-button"
 **/
function DateTimePicker(input) {
  var flyout;

  var init = function() {
    if (input.tagName == "Z-INPUT") {
      input = input.querySelector("input");
    }

    // no input provided
    if (input.tagName != "INPUT") {
      console.log("DateTimePicker Error: no input provided");
      return;
    }

    var tpl = $.getTemplate("widgets/z-datetimepicker", "z-datetimepicker-base-tpl");
    // insert tpl after input
    input.style.cursor = "pointer";
    input.parentNode.insertBefore(tpl, input.nextSibling);
    //FIXME
    flyout = input.nextElementSibling;

    // don't close on click within datetimepicker
    flyout.addEventListener("click", function(e) {
      e.stopPropagation();
    }, false);

    flyout.querySelector("button").addEventListener("click", function(e) {
      apply();
    }, false);

    handleVisibility();
    controlTimeInput();
  };

  var handleVisibility = function() {

    input.addEventListener("click", function(e) {
      e.stopPropagation();
      if (flyout.hasAttribute("data-active")) {
        hide();
      } else {
        show();
      }
    }, false);
  };

  var controlTimeInput = function() {
    var inputs = flyout.querySelectorAll("input");

    for (var i = 0; i < inputs.length; i++) {
      inputs[i].addEventListener("blur", function() {

        switch (this.value.length) {
          // if no value is given set it to 00
          case 0:
            this.value = "00";
            break;

          case 1:
            var value = parseInt(this.value, 10);
            // non-integer value
            if (isNaN(value)) {
              renderTimeControlError(this);
              return;
            }
            this.value = DateUtil.appendLeadingZero(value);
            break;

          case 2:
            // non-integer value
            if (isNaN(parseInt(this.value[0], 10)) ||
                isNaN(parseInt(this.value[1], 10))) {
              renderTimeControlError(this);
              return;
            }
            break;
        }

        var value = parseInt(this.value, 10);
        if (this.getAttribute("data-factor") == "3600") {
          // hours value > 24
          if (value > 23) {
            renderTimeControlError(this);
            return;
          }
        } else {
          // minutes or seconds value > 59
          if (value > 59) {
           renderTimeControlError(this);
            return;
          }
        }

        // correct input value
        removeTimeControlError(this);
      }, false);
    }
  };

  var removeTimeControlError = function(time_control) {
    time_control.classList.remove("error");

    // if only correct input values enable apply button
    if (flyout.querySelector("input.error") == null) {
      flyout.querySelector("button").removeAttribute("data-state");
    }
  };

  var renderTimeControlError = function(time_control) {
    time_control.classList.add("error");

    // disable apply button
    flyout.querySelector("button").setAttribute("data-state", "disabled");
  };

  var renderErrorMessage = function() {
    flyout.querySelector("z-datetimepicker-error")
      .setAttribute("data-active", "active");
  };

  var removeErrorMessage = function() {
    flyout.querySelector("z-datetimepicker-error")
        .removeAttribute("data-active", "active");
  };

  // set time value and human formatted time string
  var onTimeChange = function() {
    var timestamp = Math.floor(
      parseInt(input.getAttribute("data-timestamp"), 10));

    // set input value as human formatted string
    input.value = DateUtil.printTimestamp(timestamp);

    timestamp = Math.floor(timestamp / 1000);
    var date = new Date(timestamp);
    var utc_day_start = DateUtil.getStartOfDay(timestamp) +
      date.getTimezoneOffset() * DateUtil.millisPerMinute;

    // set z-calendar selection
    flyout.querySelector("z-calendar").setAttribute("data-selected", utc_day_start);
    // set hours input value
    flyout.querySelector("input[data-value='hours']").value =
      DateUtil.appendLeadingZero(date.getHours());
    // set minutes input value
    flyout.querySelector("input[data-value='minutes']").value =
      DateUtil.appendLeadingZero(date.getMinutes());
    // set seconds input value
    flyout.querySelector("input[data-value='seconds']").value =
      DateUtil.appendLeadingZero(date.getSeconds());
  };

  // get selected datetime as timestamp
  var getTimeValue = function() {
    var timestamp = DateUtil.parseTimestamp(
      parseInt(flyout.querySelector("z-calendar")
        .getAttribute("data-selected"), 10));

    var inputs = flyout.querySelectorAll("input");
    for (var i = 0; i < inputs.length; i++) {
      var value = parseInt(inputs[i].value, 10);
      timestamp += value * parseInt(inputs[i].getAttribute("data-factor"), 10);
    }

    timestamp = timestamp * 1000;
    return timestamp;
  };

  // checks if the selected date is in the past
  var isValidTimeValue = function(timestamp) {
    // error -> future datetime
    if (Math.floor(timestamp / 1000) > Date.now()) {
      renderErrorMessage();
      return false;
    }

    removeErrorMessage();
    return true;
  };

  var apply = function() {
    var timestamp = getTimeValue();

    // invalid timestamp
    if (!isValidTimeValue(timestamp)) {
      return;
    }

    input.setAttribute("data-timestamp", timestamp);
    onTimeChange();
    hide();

    // fire change event on input
    var evt = new CustomEvent("z-datetimepicker-change");
    input.dispatchEvent(evt);
  };

  var hide = function() {
    flyout.removeAttribute("data-active");

    window.removeEventListener("click", onWindowClick, false);
  };

  var show = function() {
    onTimeChange();
    var pos = input.getBoundingClientRect();

    // set flyout top and left position
    flyout.style.top = (pos.top + pos.height) + "px";
    flyout.setAttribute("data-active", "active");
    flyout.style.left = (pos.left - (flyout.offsetWidth - pos.width) / 2) + "px";

    window.addEventListener("click", onWindowClick, false);
  };

  var onWindowClick = function() {
    hide();
  };

  init();
  return {
    show: show,
    hide: hide
  };
};
<|endoftext|>


<commit_before>angular.module("categoryModule")

    .controller("categoryListController", [
        "$scope",
        "$location",
        "$route",
        "$routeParams",
        "$categoryApiService",
        "$designService",
        "$designImageService",
        "$categoryService",
        "$visitorLoginService",
        "$cartService",
        "$pdpProductService",
        "$commonUtilService",
        "$timeout",
        "GENERAL_CATEGORY_URI",
        "SEARCH_KEY_NAME",
        function ($scope, $location, $route, $routeParams, $categoryApiService, $designService,
                  $designImageService, $categoryService, $visitorLoginService, $cartService,
                  $pdpProductService, $commonUtilService, $timeout, GENERAL_CATEGORY_URI, SEARCH_KEY_NAME) {

            var init, getPage, addCategoryCrumbs, getFilters, setFilters, getParams, initWatchers,
                defaultFilterSet, defaultOptionSet, changeLocation;

            getPage = function () {
                var param, page;

                page = 0;
                param = $location.search();

                if (typeof param.p !== "undefined") {
                    page = (param.p - 1);
                }

                return page;
            };

            addCategoryCrumbs = function () {

                if ($scope.isShop) {
                    $scope.$emit("add-breadcrumbs", {"label": "Shop", "url": $location.path()});
                } else {
                    var list, i, category;
                    list = $categoryService.getChainCategories($scope.categoryId);

                    for (i = 0; i < list.length; i += 1) {
                        category = list[i];
                        $scope.$emit("add-breadcrumbs", {
                            "label": category.name,
                            "url": $categoryService.getUrl(category.id)
                        });
                    }
                }
            };

            getParams = function (withoutLimit) {
                var search, result, key;

                result = {};
                search = $location.search();

                for (key in search) {
                    if (search.hasOwnProperty(key)) {
                        result[key] = search[key];
                    }
                }

                if (!withoutLimit) {

                    if ($scope.currentPage === 0) {
                        result.limit = "0," + $scope.itemsPerPage;
                    } else {
                        result.limit = ($scope.currentPage * $scope.itemsPerPage) + "," + $scope.itemsPerPage;
                    }
                }

                return result;
            };

            init = function () {
                /**
                 * Variables for paginator
                 */
                $scope.currentPage = getPage();
                $scope.itemsPerPage = angular.appConfigValue("general.app.category.itemsPerPage");
                $scope.productsList = [];
                $scope.paths = [];
                $scope.isShop = (GENERAL_CATEGORY_URI === $location.path());
                $scope.categoryId = $routeParams.id || null;
                $scope.searchField = SEARCH_KEY_NAME;

                $scope.category = {};
                $scope.popupProduct = {};
                $scope.productService = $pdpProductService;
                $scope.options = {};
                $scope.filters = {};
                $scope.blocks = {
                    "sort": false,
                    "search": false,
                    "filter": false
                };

                var getSearchText = function () {
                    $scope.searchText = "";
                    if (typeof $routeParams[$scope.searchField] !== "undefined") {
                        $scope.searchText = $routeParams[$scope.searchField].trim("~").replace(/,/g, " ");
                    }
                };
                getSearchText();

                addCategoryCrumbs();
            };
            init();

            changeLocation = function () {
                var filterStr, url;
                filterStr = getFilters();
                if (typeof filterStr !== "undefined") {
                    if ($scope.categoryId === null && $scope.isShop) {
                        url = GENERAL_CATEGORY_URI;
                    } else {
                        url = $categoryService.getUrl($scope.categoryId);
                    }

                    $categoryService.setFiltersInLocation(url, filterStr);
                }
            };

            initWatchers = function () {
                defaultFilterSet = $scope.$watch("filters", changeLocation, true);

                defaultOptionSet = $scope.$watch("options", function () {
                    $pdpProductService.setOptions($scope.options);
                    $scope.popupProduct = $pdpProductService.getProduct();
                }, true);
            };

            setFilters = function () {
                var params, values, i, initFilter;
                params = $location.search();
                initFilter = function (attr) {
                    if (typeof $scope.filters[attr] === "undefined") {
                        $scope.filters[attr] = {};
                    }
                };

                for (var attr in params) {

                    if (params.hasOwnProperty(attr)) {
                        initFilter(attr);

                        if (typeof params[attr] === "string") {
                            values = params[attr].replace(/[\?~]/, "").split(",");
                            for (i = 0; i < values.length; i += 1) {
                                $scope.filters[attr.replace(/[\?~]/, "")][values[i]] = true;
                            }
                        } else {
                            $scope.filters[attr.replace(/[\?~]/, "")][params[attr]] = true;
                        }

                    }

                }

            };

            setFilters();

            getFilters = function () {
                var filters, prepareFilters, hasFilter;
                filters = [];
                hasFilter = false;

                prepareFilters = function () {
                    var getFilterValues;

                    getFilterValues = function (attr) {
                        var values, val;
                        values = [];

                        for (val in $scope.filters[attr]) {
                            if ($scope.filters[attr].hasOwnProperty(val) &&
                                $scope.filters[attr][val] === true) {
                                values.push(val);
                                hasFilter = true;
                            }
                        }

                        return values;
                    };
                    for (var attr in $scope.filters) {
                        if ($scope.filters.hasOwnProperty(attr)) {
                            var values = getFilterValues(attr);
                            if (values.length > 0 && attr !== SEARCH_KEY_NAME) {
                                filters.push(attr + "=" + values.join(","));
                            } else if (values.length > 0 && attr === SEARCH_KEY_NAME && values.join(",") !== "") {
                                filters.push(attr + "=~" + values.join(","));
                            }
                        }
                    }
                };

                prepareFilters();

                if (!hasFilter) {
                    return "";
                }

                return filters.join("&");
            };

            /**
             * Gets number items into collection
             */
            $scope.getCountProduct = function () {
                if ($scope.isShop) {
                    $categoryApiService.getShopCountProducts(getParams(true)).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.totalItems = result;
                        $scope.pages = Math.ceil($scope.totalItems / $scope.itemsPerPage);
                    });
                } else {
                    var params = getParams(true);
                    params["categoryID"] = $scope.categoryId;
                    $categoryApiService.getCountProducts(params).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.totalItems = result;
                        $scope.pages = Math.ceil($scope.totalItems / $scope.itemsPerPage);
                    });
                }

            };

            /**
             * Gets list of products
             */
            $scope.getProducts = function () {
                if ($scope.isShop) {
                    $categoryApiService.getShopProducts(getParams(), {}).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.productsList = result;
                    });
                } else {
                    var params = getParams();
                    params["categoryID"] = $scope.categoryId;
                    $categoryApiService.getProductsByCategoryId(params).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.productsList = result;
                    });
                }
            };

            /**
             * Gets layers for category
             */
            $scope.getLayered = function () {
                if ($scope.isShop) {
                    $categoryApiService.getShopLayered($location.search(), {}).$promise.then(function (response) {
                            var result = response.result || [];
                            $scope.layered = result;
                            for (var filter in $scope.layered) {
                                if ($scope.layered.hasOwnProperty(filter)) {
                                    $scope.filters[filter] = {};
                                }
                            }
                            setFilters();
                        }
                    );
                } else {
                    var params = $location.search();
                    params["categoryID"] = $scope.categoryId;
                    $categoryApiService.getLayered(params).$promise.then(function (response) {
                            var result = response.result || [];
                            $scope.layered = result;
                            for (var filter in $scope.layered) {
                                if ($scope.layered.hasOwnProperty(filter)) {
                                    $scope.filters[filter] = {};
                                }
                            }
                            setFilters();
                        }
                    );
                }
            };

            $scope.init = function () {
                var tree;

                tree = $categoryService.getTree();
                if (typeof tree === "undefined") {
                    $categoryApiService.getCategories().$promise.then(
                        function (response) {
                            var categories = response.result || [];
                            $categoryService.setTree(categories);
                            addCategoryCrumbs();
                        }
                    );
                }
                $scope.getLayered();
                $scope.getProducts();
                $scope.getCountProduct();

                if ($scope.categoryId !== null) {
                    /**
                     * Gets category
                     */
                    $categoryApiService.load({"id": $scope.categoryId}).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.category = result;
                        $scope.initCategoryImages();
                    });
                }

                initWatchers();
            };

            $scope.toggleBlock = function (activeBlock) {
                var block;

                for (block in $scope.blocks) {

                    if ($scope.blocks.hasOwnProperty(block)) {
                        if (block === activeBlock) {
                            if ($scope.blocks[block]) {
                                $scope.blocks[block] = false;
                            } else {
                                $scope.blocks[block] = true;
                            }
                        } else {
                            $scope.blocks[block] = false;
                        }
                    }

                }

                return true;
            };

            $scope.closeBlock = function (nameBlock) {
                $scope.blocks[nameBlock] = false;
                jQuery('.list-bar span').removeClass('active');
                jQuery('.shadow').css('display', 'none');
            };

            $scope.viewDetails = function (product) {

                //TODO: we should get away from touching the dom in the
                // controllers
                $("#quick-view").modal('hide');
                $timeout(function () {
                    var url = $pdpProductService.getUrl(product._id);
                    $location.path(url);
                }, 250);
            }

            $scope.addToCart = function (product) {
                var miniCart, addItem;
                miniCart = $(".mini-cart");
                addItem = function () {
                    $cartService.add(product._id, 1, $pdpProductService.getOptions()).then(
                        function (response) {
                            if (response.error !== null) {
                                $scope.openPopUp(product);
                                $scope.message = $commonUtilService.getMessage(response);
                            } else {
                                $pdpProductService.setOptions({});
                                $("#quick-view").modal('hide');

                                miniCart.modal('show');
                                setTimeout(function () {
                                    miniCart.modal('hide');
                                }, 2000);
                            }
                        }
                    );
                };

                if (angular.appConfigValue("general.checkout.guest_checkout")) {
                    addItem();
                } else {
                    $visitorLoginService.isLoggedIn().then(function (isLoggedIn) {
                        if (isLoggedIn) {
                            addItem();
                        } else {
                            $("#form-login").modal("show");
                        }
                    });
                }

            };

            /**
             * Set category image and images attributes value as a path
             * $scope.category.image - default image path
             * $scope.category.images - list of all images in this category
             */
            $scope.initCategoryImages = function () {
                var categoryImageBasePath;

                $categoryApiService.getImagePath({"categoryID": $scope.categoryId}).$promise.then(function (response) {
                    categoryImageBasePath = response.result || [];
                });

                $categoryApiService.listImages({"categoryID": $scope.categoryId}).$promise.then(function (response) {
                    $scope.category.images = response.result || [];
                    var categoryImagesPath = {};
                    for (var i = 0, imageName, imagePath; i < $scope.category.images.length; i += 1) {
                        imageName = $scope.category.images[i];
                        imagePath = $designImageService.getFullImagePath("", categoryImageBasePath + imageName);
                        categoryImagesPath[imageName] = imagePath;
                    }
                    $scope.category.images = categoryImagesPath;
                    if (typeof $scope.category.image !== "undefined") {
                        $scope.category.image = $designImageService.getFullImagePath("", categoryImageBasePath + $scope.category.image);
                    }
                });
            };

            $scope.sortByPrice = function (order) {
                var orderStr;

                if (order === "asc") {
                    orderStr = "price";
                } else {
                    orderStr = "^price";
                }

                $scope.filters.sort = {};
                $scope.filters.sort[orderStr] = true;
            };

            $scope.sortByName = function (order) {
                var orderStr;

                if (order === "asc") {
                    orderStr = "name";
                } else {
                    orderStr = "^name";
                }

                $scope.filters.sort = {};
                $scope.filters.sort[orderStr] = true;
            };

            $scope.openPopUp = function (product) {
                $scope.message = {};
                $scope.options = {};
                $pdpProductService.setProduct(product);
                $scope.popupProduct = $pdpProductService.getProduct();
                $scope.productService.getRatingInfo(product._id);
                $("#quick-view").modal('show');
                setTimeout(function () {
                    try {
                        $('.rating').rating('update', $scope.productService.getAverageRating());
                    } catch (e) {

                    }
                }, 300);
            };

            $scope.showMoreBtn = function () {
                var countLoadedGoods;
                countLoadedGoods = ($scope.currentPage + 1) * $scope.itemsPerPage;

                if (countLoadedGoods >= $scope.totalItems) {
                    return false;
                }

                return true;
            };

            $scope.loadMore = function () {
                $scope.clickMore = true;
                $scope.currentPage += 1;

                var params = getParams();
                params["categoryID"] = $scope.categoryId;

                $categoryApiService.getProductsByCategoryId(params).$promise.then(
                    function (response) {
                        var result = response.result || [];
                        $scope.productsList = $scope.productsList.concat(result);
                    }
                );
                if ($scope.isShop) {
                    $categoryApiService.getShopProducts(params).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.productsList = $scope.productsList.concat(result);
                    });
                } else {
                    $categoryApiService.getProductsByCategoryId(params).$promise.then(
                        function (response) {
                            var result = response.result || [];
                            $scope.productsList = $scope.productsList.concat(result);
                        }
                    );
                }
            };

            $scope.search = function () {
                var searchObj, values;
                searchObj = {};
                values = this.searchText.split(/[, ]/);
                searchObj[$scope.searchField] = {};
                for (var i = 0; i < values.length; i += 1) {
                    searchObj[$scope.searchField][values[i]] = true;
                }
                $scope.filters[$scope.searchField] = searchObj[$scope.searchField];
            };
        }
    ]);<commit_msg>[#99042554] we no longer need to fetch images separately<commit_after>angular.module("categoryModule")

    .controller("categoryListController", [
        "$scope",
        "$location",
        "$route",
        "$routeParams",
        "$categoryApiService",
        "$designService",
        "$designImageService",
        "$categoryService",
        "$visitorLoginService",
        "$cartService",
        "$pdpProductService",
        "$commonUtilService",
        "$timeout",
        "GENERAL_CATEGORY_URI",
        "SEARCH_KEY_NAME",
        function ($scope, $location, $route, $routeParams, $categoryApiService, $designService,
                  $designImageService, $categoryService, $visitorLoginService, $cartService,
                  $pdpProductService, $commonUtilService, $timeout, GENERAL_CATEGORY_URI, SEARCH_KEY_NAME) {

            var init, getPage, addCategoryCrumbs, getFilters, setFilters, getParams, initWatchers,
                defaultFilterSet, defaultOptionSet, changeLocation;

            getPage = function () {
                var param, page;

                page = 0;
                param = $location.search();

                if (typeof param.p !== "undefined") {
                    page = (param.p - 1);
                }

                return page;
            };

            addCategoryCrumbs = function () {

                if ($scope.isShop) {
                    $scope.$emit("add-breadcrumbs", {"label": "Shop", "url": $location.path()});
                } else {
                    var list, i, category;
                    list = $categoryService.getChainCategories($scope.categoryId);

                    for (i = 0; i < list.length; i += 1) {
                        category = list[i];
                        $scope.$emit("add-breadcrumbs", {
                            "label": category.name,
                            "url": $categoryService.getUrl(category.id)
                        });
                    }
                }
            };

            getParams = function (withoutLimit) {
                var search, result, key;

                result = {};
                search = $location.search();

                for (key in search) {
                    if (search.hasOwnProperty(key)) {
                        result[key] = search[key];
                    }
                }

                if (!withoutLimit) {

                    if ($scope.currentPage === 0) {
                        result.limit = "0," + $scope.itemsPerPage;
                    } else {
                        result.limit = ($scope.currentPage * $scope.itemsPerPage) + "," + $scope.itemsPerPage;
                    }
                }

                return result;
            };

            init = function () {
                /**
                 * Variables for paginator
                 */
                $scope.currentPage = getPage();
                $scope.itemsPerPage = angular.appConfigValue("general.app.category.itemsPerPage");
                $scope.productsList = [];
                $scope.paths = [];
                $scope.isShop = (GENERAL_CATEGORY_URI === $location.path());
                $scope.categoryId = $routeParams.id || null;
                $scope.searchField = SEARCH_KEY_NAME;

                $scope.category = {};
                $scope.popupProduct = {};
                $scope.productService = $pdpProductService;
                $scope.options = {};
                $scope.filters = {};
                $scope.blocks = {
                    "sort": false,
                    "search": false,
                    "filter": false
                };

                var getSearchText = function () {
                    $scope.searchText = "";
                    if (typeof $routeParams[$scope.searchField] !== "undefined") {
                        $scope.searchText = $routeParams[$scope.searchField].trim("~").replace(/,/g, " ");
                    }
                };
                getSearchText();

                addCategoryCrumbs();
            };
            init();

            changeLocation = function () {
                var filterStr, url;
                filterStr = getFilters();
                if (typeof filterStr !== "undefined") {
                    if ($scope.categoryId === null && $scope.isShop) {
                        url = GENERAL_CATEGORY_URI;
                    } else {
                        url = $categoryService.getUrl($scope.categoryId);
                    }

                    $categoryService.setFiltersInLocation(url, filterStr);
                }
            };

            initWatchers = function () {
                defaultFilterSet = $scope.$watch("filters", changeLocation, true);

                defaultOptionSet = $scope.$watch("options", function () {
                    $pdpProductService.setOptions($scope.options);
                    $scope.popupProduct = $pdpProductService.getProduct();
                }, true);
            };

            setFilters = function () {
                var params, values, i, initFilter;
                params = $location.search();
                initFilter = function (attr) {
                    if (typeof $scope.filters[attr] === "undefined") {
                        $scope.filters[attr] = {};
                    }
                };

                for (var attr in params) {

                    if (params.hasOwnProperty(attr)) {
                        initFilter(attr);

                        if (typeof params[attr] === "string") {
                            values = params[attr].replace(/[\?~]/, "").split(",");
                            for (i = 0; i < values.length; i += 1) {
                                $scope.filters[attr.replace(/[\?~]/, "")][values[i]] = true;
                            }
                        } else {
                            $scope.filters[attr.replace(/[\?~]/, "")][params[attr]] = true;
                        }

                    }

                }

            };

            setFilters();

            getFilters = function () {
                var filters, prepareFilters, hasFilter;
                filters = [];
                hasFilter = false;

                prepareFilters = function () {
                    var getFilterValues;

                    getFilterValues = function (attr) {
                        var values, val;
                        values = [];

                        for (val in $scope.filters[attr]) {
                            if ($scope.filters[attr].hasOwnProperty(val) &&
                                $scope.filters[attr][val] === true) {
                                values.push(val);
                                hasFilter = true;
                            }
                        }

                        return values;
                    };
                    for (var attr in $scope.filters) {
                        if ($scope.filters.hasOwnProperty(attr)) {
                            var values = getFilterValues(attr);
                            if (values.length > 0 && attr !== SEARCH_KEY_NAME) {
                                filters.push(attr + "=" + values.join(","));
                            } else if (values.length > 0 && attr === SEARCH_KEY_NAME && values.join(",") !== "") {
                                filters.push(attr + "=~" + values.join(","));
                            }
                        }
                    }
                };

                prepareFilters();

                if (!hasFilter) {
                    return "";
                }

                return filters.join("&");
            };

            /**
             * Gets number items into collection
             */
            $scope.getCountProduct = function () {
                if ($scope.isShop) {
                    $categoryApiService.getShopCountProducts(getParams(true)).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.totalItems = result;
                        $scope.pages = Math.ceil($scope.totalItems / $scope.itemsPerPage);
                    });
                } else {
                    var params = getParams(true);
                    params["categoryID"] = $scope.categoryId;
                    $categoryApiService.getCountProducts(params).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.totalItems = result;
                        $scope.pages = Math.ceil($scope.totalItems / $scope.itemsPerPage);
                    });
                }

            };

            /**
             * Gets list of products
             */
            $scope.getProducts = function () {
                if ($scope.isShop) {
                    $categoryApiService.getShopProducts(getParams(), {}).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.productsList = result;
                    });
                } else {
                    var params = getParams();
                    params["categoryID"] = $scope.categoryId;
                    $categoryApiService.getProductsByCategoryId(params).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.productsList = result;
                    });
                }
            };

            /**
             * Gets layers for category
             */
            $scope.getLayered = function () {
                if ($scope.isShop) {
                    $categoryApiService.getShopLayered($location.search(), {}).$promise.then(function (response) {
                            var result = response.result || [];
                            $scope.layered = result;
                            for (var filter in $scope.layered) {
                                if ($scope.layered.hasOwnProperty(filter)) {
                                    $scope.filters[filter] = {};
                                }
                            }
                            setFilters();
                        }
                    );
                } else {
                    var params = $location.search();
                    params["categoryID"] = $scope.categoryId;
                    $categoryApiService.getLayered(params).$promise.then(function (response) {
                            var result = response.result || [];
                            $scope.layered = result;
                            for (var filter in $scope.layered) {
                                if ($scope.layered.hasOwnProperty(filter)) {
                                    $scope.filters[filter] = {};
                                }
                            }
                            setFilters();
                        }
                    );
                }
            };

            $scope.init = function () {
                var tree;

                tree = $categoryService.getTree();
                if (typeof tree === "undefined") {
                    $categoryApiService.getCategories().$promise.then(
                        function (response) {
                            var categories = response.result || [];
                            $categoryService.setTree(categories);
                            addCategoryCrumbs();
                        }
                    );
                }
                $scope.getLayered();
                $scope.getProducts();
                $scope.getCountProduct();

                if ($scope.categoryId !== null) {
                    /**
                     * Gets category
                     */
                    $categoryApiService.load({"id": $scope.categoryId}).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.category = result;
                    });
                }

                initWatchers();
            };

            $scope.toggleBlock = function (activeBlock) {
                var block;

                for (block in $scope.blocks) {

                    if ($scope.blocks.hasOwnProperty(block)) {
                        if (block === activeBlock) {
                            if ($scope.blocks[block]) {
                                $scope.blocks[block] = false;
                            } else {
                                $scope.blocks[block] = true;
                            }
                        } else {
                            $scope.blocks[block] = false;
                        }
                    }

                }

                return true;
            };

            $scope.closeBlock = function (nameBlock) {
                $scope.blocks[nameBlock] = false;
                jQuery('.list-bar span').removeClass('active');
                jQuery('.shadow').css('display', 'none');
            };

            $scope.viewDetails = function (product) {

                //TODO: we should get away from touching the dom in the
                // controllers
                $("#quick-view").modal('hide');
                $timeout(function () {
                    var url = $pdpProductService.getUrl(product._id);
                    $location.path(url);
                }, 250);
            }

            $scope.addToCart = function (product) {
                var miniCart, addItem;
                miniCart = $(".mini-cart");
                addItem = function () {
                    $cartService.add(product._id, 1, $pdpProductService.getOptions()).then(
                        function (response) {
                            if (response.error !== null) {
                                $scope.openPopUp(product);
                                $scope.message = $commonUtilService.getMessage(response);
                            } else {
                                $pdpProductService.setOptions({});
                                $("#quick-view").modal('hide');

                                miniCart.modal('show');
                                setTimeout(function () {
                                    miniCart.modal('hide');
                                }, 2000);
                            }
                        }
                    );
                };

                if (angular.appConfigValue("general.checkout.guest_checkout")) {
                    addItem();
                } else {
                    $visitorLoginService.isLoggedIn().then(function (isLoggedIn) {
                        if (isLoggedIn) {
                            addItem();
                        } else {
                            $("#form-login").modal("show");
                        }
                    });
                }

            };

            $scope.sortByPrice = function (order) {
                var orderStr;

                if (order === "asc") {
                    orderStr = "price";
                } else {
                    orderStr = "^price";
                }

                $scope.filters.sort = {};
                $scope.filters.sort[orderStr] = true;
            };

            $scope.sortByName = function (order) {
                var orderStr;

                if (order === "asc") {
                    orderStr = "name";
                } else {
                    orderStr = "^name";
                }

                $scope.filters.sort = {};
                $scope.filters.sort[orderStr] = true;
            };

            $scope.openPopUp = function (product) {
                $scope.message = {};
                $scope.options = {};
                $pdpProductService.setProduct(product);
                $scope.popupProduct = $pdpProductService.getProduct();
                $scope.productService.getRatingInfo(product._id);
                $("#quick-view").modal('show');
                setTimeout(function () {
                    try {
                        $('.rating').rating('update', $scope.productService.getAverageRating());
                    } catch (e) {

                    }
                }, 300);
            };

            $scope.showMoreBtn = function () {
                var countLoadedGoods;
                countLoadedGoods = ($scope.currentPage + 1) * $scope.itemsPerPage;

                if (countLoadedGoods >= $scope.totalItems) {
                    return false;
                }

                return true;
            };

            $scope.loadMore = function () {
                $scope.clickMore = true;
                $scope.currentPage += 1;

                var params = getParams();
                params["categoryID"] = $scope.categoryId;

                $categoryApiService.getProductsByCategoryId(params).$promise.then(
                    function (response) {
                        var result = response.result || [];
                        $scope.productsList = $scope.productsList.concat(result);
                    }
                );
                if ($scope.isShop) {
                    $categoryApiService.getShopProducts(params).$promise.then(function (response) {
                        var result = response.result || [];
                        $scope.productsList = $scope.productsList.concat(result);
                    });
                } else {
                    $categoryApiService.getProductsByCategoryId(params).$promise.then(
                        function (response) {
                            var result = response.result || [];
                            $scope.productsList = $scope.productsList.concat(result);
                        }
                    );
                }
            };

            $scope.search = function () {
                var searchObj, values;
                searchObj = {};
                values = this.searchText.split(/[, ]/);
                searchObj[$scope.searchField] = {};
                for (var i = 0; i < values.length; i += 1) {
                    searchObj[$scope.searchField][values[i]] = true;
                }
                $scope.filters[$scope.searchField] = searchObj[$scope.searchField];
            };
        }
    ]);<|endoftext|>


<commit_before>import _ from '../lib/utils'
import App from './app'

let Cards = {
  Plains:   { url: '136' },
  Island:   { url: '137' },
  Swamp:    { url: '138' },
  Mountain: { url: '139' },
  Forest:   { url: '140' }
}

export let BASICS = Object.keys(Cards)

for (let name in Cards)
  Object.assign(Cards[name], { name,
    cmc: 0,
    code: 'UNH',
    color: 'colorless',
    rarity: 'basic',
    type: 'Land',
    url: `http://magiccards.info/scans/en/uh/${Cards[name].url}.jpg`
  })

let rawPack, clicked
export let Zones = {
  pack: {},
  main: {},
  side: {},
  junk: {}
}

function hash() {
  let {main, side} = Zones
  App.send('hash', { main, side })
}

let events = {
  side() {
    let dst = Zones[App.state.side ? 'side' : 'main']

    let srcName = App.state.side ? 'main' : 'side'
    let src = Zones[srcName]

    _.add(src, dst)
    Zones[srcName] = {}
  },
  add(cardName) {
    let zone = Zones[App.state.side ? 'side' : 'main']
    zone[cardName] || (zone[cardName] = 0)
    zone[cardName]++
    App.update()
  },
  click(zoneName, cardName, e) {
    if (zoneName === 'pack')
      return clickPack(cardName)

    let src = Zones[zoneName]
    let dst = Zones[e.shiftKey
      ? zoneName === 'junk' ? 'main' : 'junk'
      : zoneName === 'side' ? 'main' : 'side']

    dst[cardName] || (dst[cardName] = 0)

    src[cardName]--
    dst[cardName]++

    if (!src[cardName])
      delete src[cardName]

    App.update()
  },
  copy(ref) {
    let node = ref.getDOMNode()
    node.value = filetypes.txt()
    node.select()
    hash()
  },
  download() {
    let {filename, filetype} = App.state
    let data = filetypes[filetype]()
    _.download(data, filename + '.' + filetype)
    hash()
  },
  start() {
    let {bots, timer} = App.state
    let options = [bots, timer]
    App.send('start', options)
  },
  pack(cards) {
    rawPack = cards
    let pack = Zones.pack = {}

    for (let card of cards) {
      let {name} = card
      Cards[name] = card
      pack[name] = 1
    }
    App.update()
    if (App.state.beep)
      document.getElementById('beep').play()
  },
  create() {
    let {type, seats} = App.state
    seats = Number(seats)
    let options = { type, seats }

    if (/cube/.test(type))
      options.cube = cube()
    else {
      let {sets} = App.state
      if (type === 'draft')
        sets = sets.slice(0, 3)
      options.sets = sets
    }

    App.send('create', options)
  },
  pool(cards) {
    ['main', 'side', 'junk'].forEach(zoneName => Zones[zoneName] = {})

    let zone = Zones[App.state.side
      ? 'side'
      : 'main']

    for (let card of cards) {
      let {name} = card
      Cards[name] = card

      zone[name] || (zone[name] = 0)
      zone[name]++
    }
    App.update()
  },
  land(zoneName, cardName, e) {
    let n = Number(e.target.value)
    if (n)
      Zones[zoneName][cardName] = n
    else
      delete Zones[zoneName][cardName]
    App.update()
  },
}

for (let event in events)
  App.on(event, events[event])

function codify(zone) {
  let arr = []
  for (let name in zone)
    arr.push(`    <card number="${zone[name]}" name="${name}"/>`)
  return arr.join('\n')
}

let filetypes = {
  cod() {
    return `\
<?xml version="1.0" encoding="UTF-8"?>
<cockatrice_deck version="1">
  <deckname>${App.state.filename}</deckname>
  <zone name="main">
${codify(Zones.main)}
  </zone>
  <zone name="side">
${codify(Zones.side)}
  </zone>
</cockatrice_deck>`
  },
  mwdeck() {
    let arr = []
    ;['main', 'side'].forEach(zoneName => {
      let prefix = zoneName === 'side' ? 'SB: ' : ''
      let zone = Zones[zoneName]
      for (let name in zone) {
        let {code} = Cards[name]
        let count = zone[name]
        name = name.replace(' // ', '/')
        arr.push(`${prefix}${count} [${code}] ${name}`)
      }
    })
    return arr.join('\n')
  },
  json() {
    let {main, side} = Zones
    return JSON.stringify({ main, side }, null, 2)
  },
  txt() {
    let arr = []
    ;['main', 'side'].forEach(zoneName => {
      if (zoneName === 'side')
        arr.push('Sideboard')
      let zone = Zones[zoneName]
      for (let name in zone) {
        let count = zone[name]
        arr.push(count + ' ' + name)
      }
    })
    return arr.join('\n')
  }
}

function cube() {
  let {list, cards, packs} = App.state
  cards = Number(cards)
  packs = Number(packs)

  list = list
    .split('\n')
    .map(x => x
      .trim()
      .replace(/^\d+.\s*/, '')
      .replace(/\s*\/+\s*/g, ' // ')
      .toLowerCase())
    .filter(x => x)
    .join('\n')

  return { list, cards, packs }
}

function clickPack(cardName) {
  if (clicked !== cardName)
    return clicked = cardName

  let index = rawPack.findIndex(x => x.name === cardName)
  clicked = null
  Zones.pack = {}
  App.update()
  App.send('pick', index)
}

function Key(groups, sort) {
  let keys = Object.keys(groups)

  switch(sort) {
    case 'cmc':
      let arr = []
      for (let key in groups)
        if (parseInt(key) > 6) {
          ;[].push.apply(arr, groups[key])
          delete groups[key]
        }

      if (arr.length) {
        groups['6'] || (groups['6'] = [])
        ;[].push.apply(groups['6'], arr)
      }
      return groups

    case 'color':
      keys =
        ['colorless', 'white', 'blue', 'black', 'red', 'green', 'multicolor']
        .filter(x => keys.indexOf(x) > -1)
      break
    case 'rarity':
      keys =
        ['basic', 'common', 'uncommon', 'rare', 'mythic', 'special']
        .filter(x => keys.indexOf(x) > -1)
      break
    case 'type':
      keys = keys.sort()
      break
  }

  let o = {}
  for (let key of keys)
    o[key] = groups[key]
  return o
}

export function getZone(zoneName) {
  let zone = Zones[zoneName]

  let cards = []
  for (let cardName in zone)
    for (let i = 0; i < zone[cardName]; i++)
      cards.push(Cards[cardName])

  let {sort} = App.state
  let groups = _.group(cards, sort)
  for (let key in groups)
    _.sort(groups[key], 'color', 'cmc', 'name')

  groups = Key(groups, sort)

  return groups
}
<commit_msg>rip magiccards.info?<commit_after>import _ from '../lib/utils'
import App from './app'

let Cards = {
  Plains:   401994,
  Island:   401927,
  Swamp:    402059,
  Mountain: 401961,
  Forest:   401886
}

export let BASICS = Object.keys(Cards)

for (let name in Cards)
  Cards[name] = {name,
    cmc: 0,
    code: 'BFZ',
    color: 'colorless',
    rarity: 'basic',
    type: 'Land',
    url: 'http://gatherer.wizards.com/Handlers/Image.ashx?type=card&' +
      `multiverseid=${Cards[name]}`
  }

let rawPack, clicked
export let Zones = {
  pack: {},
  main: {},
  side: {},
  junk: {}
}

function hash() {
  let {main, side} = Zones
  App.send('hash', { main, side })
}

let events = {
  side() {
    let dst = Zones[App.state.side ? 'side' : 'main']

    let srcName = App.state.side ? 'main' : 'side'
    let src = Zones[srcName]

    _.add(src, dst)
    Zones[srcName] = {}
  },
  add(cardName) {
    let zone = Zones[App.state.side ? 'side' : 'main']
    zone[cardName] || (zone[cardName] = 0)
    zone[cardName]++
    App.update()
  },
  click(zoneName, cardName, e) {
    if (zoneName === 'pack')
      return clickPack(cardName)

    let src = Zones[zoneName]
    let dst = Zones[e.shiftKey
      ? zoneName === 'junk' ? 'main' : 'junk'
      : zoneName === 'side' ? 'main' : 'side']

    dst[cardName] || (dst[cardName] = 0)

    src[cardName]--
    dst[cardName]++

    if (!src[cardName])
      delete src[cardName]

    App.update()
  },
  copy(ref) {
    let node = ref.getDOMNode()
    node.value = filetypes.txt()
    node.select()
    hash()
  },
  download() {
    let {filename, filetype} = App.state
    let data = filetypes[filetype]()
    _.download(data, filename + '.' + filetype)
    hash()
  },
  start() {
    let {bots, timer} = App.state
    let options = [bots, timer]
    App.send('start', options)
  },
  pack(cards) {
    rawPack = cards
    let pack = Zones.pack = {}

    for (let card of cards) {
      let {name} = card
      Cards[name] = card
      pack[name] = 1
    }
    App.update()
    if (App.state.beep)
      document.getElementById('beep').play()
  },
  create() {
    let {type, seats} = App.state
    seats = Number(seats)
    let options = { type, seats }

    if (/cube/.test(type))
      options.cube = cube()
    else {
      let {sets} = App.state
      if (type === 'draft')
        sets = sets.slice(0, 3)
      options.sets = sets
    }

    App.send('create', options)
  },
  pool(cards) {
    ['main', 'side', 'junk'].forEach(zoneName => Zones[zoneName] = {})

    let zone = Zones[App.state.side
      ? 'side'
      : 'main']

    for (let card of cards) {
      let {name} = card
      Cards[name] = card

      zone[name] || (zone[name] = 0)
      zone[name]++
    }
    App.update()
  },
  land(zoneName, cardName, e) {
    let n = Number(e.target.value)
    if (n)
      Zones[zoneName][cardName] = n
    else
      delete Zones[zoneName][cardName]
    App.update()
  },
}

for (let event in events)
  App.on(event, events[event])

function codify(zone) {
  let arr = []
  for (let name in zone)
    arr.push(`    <card number="${zone[name]}" name="${name}"/>`)
  return arr.join('\n')
}

let filetypes = {
  cod() {
    return `\
<?xml version="1.0" encoding="UTF-8"?>
<cockatrice_deck version="1">
  <deckname>${App.state.filename}</deckname>
  <zone name="main">
${codify(Zones.main)}
  </zone>
  <zone name="side">
${codify(Zones.side)}
  </zone>
</cockatrice_deck>`
  },
  mwdeck() {
    let arr = []
    ;['main', 'side'].forEach(zoneName => {
      let prefix = zoneName === 'side' ? 'SB: ' : ''
      let zone = Zones[zoneName]
      for (let name in zone) {
        let {code} = Cards[name]
        let count = zone[name]
        name = name.replace(' // ', '/')
        arr.push(`${prefix}${count} [${code}] ${name}`)
      }
    })
    return arr.join('\n')
  },
  json() {
    let {main, side} = Zones
    return JSON.stringify({ main, side }, null, 2)
  },
  txt() {
    let arr = []
    ;['main', 'side'].forEach(zoneName => {
      if (zoneName === 'side')
        arr.push('Sideboard')
      let zone = Zones[zoneName]
      for (let name in zone) {
        let count = zone[name]
        arr.push(count + ' ' + name)
      }
    })
    return arr.join('\n')
  }
}

function cube() {
  let {list, cards, packs} = App.state
  cards = Number(cards)
  packs = Number(packs)

  list = list
    .split('\n')
    .map(x => x
      .trim()
      .replace(/^\d+.\s*/, '')
      .replace(/\s*\/+\s*/g, ' // ')
      .toLowerCase())
    .filter(x => x)
    .join('\n')

  return { list, cards, packs }
}

function clickPack(cardName) {
  if (clicked !== cardName)
    return clicked = cardName

  let index = rawPack.findIndex(x => x.name === cardName)
  clicked = null
  Zones.pack = {}
  App.update()
  App.send('pick', index)
}

function Key(groups, sort) {
  let keys = Object.keys(groups)

  switch(sort) {
    case 'cmc':
      let arr = []
      for (let key in groups)
        if (parseInt(key) > 6) {
          ;[].push.apply(arr, groups[key])
          delete groups[key]
        }

      if (arr.length) {
        groups['6'] || (groups['6'] = [])
        ;[].push.apply(groups['6'], arr)
      }
      return groups

    case 'color':
      keys =
        ['colorless', 'white', 'blue', 'black', 'red', 'green', 'multicolor']
        .filter(x => keys.indexOf(x) > -1)
      break
    case 'rarity':
      keys =
        ['basic', 'common', 'uncommon', 'rare', 'mythic', 'special']
        .filter(x => keys.indexOf(x) > -1)
      break
    case 'type':
      keys = keys.sort()
      break
  }

  let o = {}
  for (let key of keys)
    o[key] = groups[key]
  return o
}

export function getZone(zoneName) {
  let zone = Zones[zoneName]

  let cards = []
  for (let cardName in zone)
    for (let i = 0; i < zone[cardName]; i++)
      cards.push(Cards[cardName])

  let {sort} = App.state
  let groups = _.group(cards, sort)
  for (let key in groups)
    _.sort(groups[key], 'color', 'cmc', 'name')

  groups = Key(groups, sort)

  return groups
}
<|endoftext|>


<commit_before>var cacheName = 'cache-one';
var urlsToCache = [
    '/',
    '/offline/',
    '/static/starwarspwa/stylesheets/starwars.css',
    '/static/starwarspwa/javascripts/starwars.js',
    '/static/starwarspwa/javascripts/jquery.js',
    '/static/starwarspwa/javascripts/sw-register.js',
    '/static/starwarspwa/fonts/quicksand/regular.woff2',
    '/static/starwarspwa/fonts/quicksand/bold.woff2',
    '/static/starwarspwa/images/wifi-offline.png'
];

self.addEventListener('install', function(e) {
    e.waitUntil(
        caches.open(cacheName).then(function(cache) {
            return cache.addAll(urlsToCache);
        }).then(function() {
            console.info('Caching for "' + cacheName + '" completed."');
            return self.skipWaiting();
        }).catch(function(error) {
            console.error('Failed to cache resources.', error);
        })
    );
});

self.addEventListener('activate', function(e) {
    e.waitUntil(
        caches.keys().then(function(cacheKeys) {
            var cachesToDelete = cacheKeys.filter(function(cacheKey) {
                return cacheKey !== cacheName;
            });
            return Promise.all(cachesToDelete.map(function(cacheKey) {
                return caches.delete(cacheKey);
            }));
        }).then(function() {
            return self.clients.claim();
        })
    );
});

self.addEventListener('fetch', function(e) {
    if (e.request.url.includes('/transmissions/')) {
        e.respondWith(
            fetch(e.request).catch(function() {
                return caches.match('/offline/');
            })
        );
    } else {
        e.respondWith(
            caches.match(e.request).then(function(response) {
                if (response) {
                    return response;
                }
                return fetch(e.request);
            })
        );
    }
});

self.addEventListener('push', function(e) {
    if (e.data === null) {
        var notification = {
            title: 'Transmission Received',
            body: 'Tap/click to view all transmissions',
            icon: '/static/starwarspwa/images/jedi-icon.png'
        };
    } else {
        var notification = e.data.json();
    }
    e.waitUntil(self.registration.showNotification(notification.title, notification));
});

self.addEventListener('notificationclick', function(e) {
    e.notification.close();
    if (e.action === 'reply') {
        var redirectUrl = '/';
    } else {
        var redirectUrl = '/transmissions/';
    }

    e.waitUntil(self.clients.matchAll({
        includeUncontrolled: true,
        type: 'window'
    }).then(function(clientWindows) {
        if (clientWindows.length > 0) {
            clientWindows[0].navigate(redirectUrl);
            clientWindows[0].focus();
        } else {
            self.clients.openWindow(redirectUrl);
        }
    }));
});
<commit_msg>chain catch methods to service worker promise chains<commit_after>var cacheName = 'cache-one';
var urlsToCache = [
    '/',
    '/offline/',
    '/static/starwarspwa/stylesheets/starwars.css',
    '/static/starwarspwa/javascripts/starwars.js',
    '/static/starwarspwa/javascripts/jquery.js',
    '/static/starwarspwa/javascripts/sw-register.js',
    '/static/starwarspwa/fonts/quicksand/regular.woff2',
    '/static/starwarspwa/fonts/quicksand/bold.woff2',
    '/static/starwarspwa/images/wifi-offline.png'
];

self.addEventListener('install', function(e) {
    e.waitUntil(
        caches.open(cacheName).then(function(cache) {
            return cache.addAll(urlsToCache);
        }).then(function() {
            console.info('Caching for "' + cacheName + '" completed."');
            return self.skipWaiting();
        }).catch(function(error) {
            console.error('Failed to cache resources.', error);
        })
    );
});

self.addEventListener('activate', function(e) {
    e.waitUntil(
        caches.keys().then(function(cacheKeys) {
            var cachesToDelete = cacheKeys.filter(function(cacheKey) {
                return cacheKey !== cacheName;
            });
            return Promise.all(cachesToDelete.map(function(cacheKey) {
                return caches.delete(cacheKey);
            }));
        }).then(function() {
            return self.clients.claim();
        }).catch(function(error) {
            console.error('Failed to delete old caches.', error);
        })
    );
});

self.addEventListener('fetch', function(e) {
    if (e.request.url.includes('/transmissions/')) {
        e.respondWith(
            fetch(e.request).catch(function() {
                return caches.match('/offline/');
            })
        );
    } else {
        e.respondWith(
            caches.match(e.request).then(function(response) {
                if (response) {
                    return response;
                }
                return fetch(e.request);
            }).catch(function(error) {
                console.error('Error when trying to match request in cache.', error);
            })
        );
    }
});

self.addEventListener('push', function(e) {
    if (e.data === null) {
        var notification = {
            title: 'Transmission Received',
            body: 'Tap/click to view all transmissions',
            icon: '/static/starwarspwa/images/jedi-icon.png'
        };
    } else {
        var notification = e.data.json();
    }
    e.waitUntil(self.registration.showNotification(notification.title, notification));
});

self.addEventListener('notificationclick', function(e) {
    e.notification.close();
    if (e.action === 'reply') {
        var redirectUrl = '/';
    } else {
        var redirectUrl = '/transmissions/';
    }

    e.waitUntil(self.clients.matchAll({
        includeUncontrolled: true,
        type: 'window'
    }).then(function(clientWindows) {
        if (clientWindows.length > 0) {
            clientWindows[0].navigate(redirectUrl);
            clientWindows[0].focus();
        } else {
            self.clients.openWindow(redirectUrl);
        }
    }).catch(function(error) {
        console.error('Failed to get client windows.', error);
    }));
});
<|endoftext|>


<commit_before>(function () {
    function SongPlayer() {
        var SongPlayer = {};

        var currentSong = null;
        var currentBuzzObject = null;

        /**
        * @function setSong
        * @desc Stops currently playing song and loads new audio file as currentBuzzObject
        * @param {Object} song
        */
        var setSong = function setSong(song) {
            if (currentBuzzObject) {
                currentBuzzObject.stop();
                currentSong.playing = null;
            }

            /**
            * @desc Buzz object audio file
            * @type {Object}
            */
            currentBuzzObject = new buzz.sound(song.audioUrl, {
                formats: ['mp3'],
                preload: true
            });

            currentSong = song;
        };

        var playSong = function playSong(song) {
            currentBuzzObject.play();
            song.playing = true;
        };

        SongPlayer.play = function play(song) {
            if (currentSong !== song) {
                setSong(song);
                playSong(song);
            } else if (currentSong === song) {
                if (currentBuzzObject.isPaused()) {
                    playSong(song);
                }
            }
        };

        SongPlayer.pause = function pause(song) {
            currentBuzzObject.pause();
            song.playing = false;
        };

        return SongPlayer;
    }

    angular
        .module('blocJams')
        .factory('SongPlayer', SongPlayer);
})();
<commit_msg>Update documentation of SongPlayer.js<commit_after>(function () {
    /**
    * @function SongPlayer
    * @desc Constructs the SongPlayer object
    * @returns {Object}
    */
    function SongPlayer() {
        /**
        * @desc The instantiated SongPlayer object
        * @type {Object}
        */
        var SongPlayer = {};

        /**
        * @desc The currently selected song
        * @type {Object}
        */
        var currentSong = null;

        /**
        * @desc Buzz object audio file
        * @type {Object}
        */
        var currentBuzzObject = null;

        /**
        * @function setSong
        * @desc Stops currently playing song and loads new audio file as currentBuzzObject
        * @param {Object} song
        */
        var setSong = function setSong(song) {
            if (currentBuzzObject) {
                currentBuzzObject.stop();
                currentSong.playing = null;
            }

            currentBuzzObject = new buzz.sound(song.audioUrl, {
                formats: ['mp3'],
                preload: true
            });

            currentSong = song;
        };

        /**
        * @function playSong
        * @desc Starts or resumes playback of the currentBuzzObject audio file
        * @param {Object} song
        */
        var playSong = function playSong(song) {
            currentBuzzObject.play();
            song.playing = true;
        };

        /**
        * @function play
        * @desc Assigns and plays a new audio file or resumes playback of the curent one
        * @param {Object} song
        */
        SongPlayer.play = function play(song) {
            if (currentSong !== song) {
                setSong(song);
                playSong(song);
            } else if (currentSong === song) {
                if (currentBuzzObject.isPaused()) {
                    playSong(song);
                }
            }
        };

        /**
        @function pause
        @desc Pauses playback of the current audio file
        */
        SongPlayer.pause = function pause(song) {
            currentBuzzObject.pause();
            song.playing = false;
        };

        return SongPlayer;
    }

    angular
        .module('blocJams')
        .factory('SongPlayer', SongPlayer);
})();
<|endoftext|>


<commit_before>import React, { Component } from "react";
import classNames from "classnames";
import Select from "react-select";
import * as TYPES from "../middleware/constants";
import _ from "lodash";

const helper = require("../middleware/helper");

export default class SingleDropdownRange extends Component {
	constructor(props) {
		super(props);
		this.state = {
			selected: null
		};
		this.type = "range";
		this.urlParams = helper.URLParams.get(this.props.componentId);
		this.handleChange = this.handleChange.bind(this);
		this.customQuery = this.customQuery.bind(this);
	}

	// Set query information
	componentWillMount() {
		this.setQueryInfo(this.props);
		this.checkDefault(this.props);
		this.listenFilter();
	}

	componentWillReceiveProps(nextProps) {
		if (this.props.defaultSelected !== nextProps.defaultSelected) {
			this.changeValue(nextProps.defaultSelected);
		}
		if (this.props.showFilter !== nextProps.showFilter || this.props.filterLabel !== nextProps.filterLabel) {
			this.setQueryInfo(nextProps);
			this.handleChange(this.state.selected);
		}
	}

	componentWillUnmount() {
		if(this.filterListener) {
			this.filterListener.remove();
		}
	}

	listenFilter() {
		this.filterListener = helper.sensorEmitter.addListener("clearFilter", (data) => {
			if(data === this.props.componentId) {
				this.changeValue(null);
			}
		});
	}

	checkDefault(props) {
		this.urlParams = helper.URLParams.get(props.componentId);
		const defaultValue = this.urlParams !== null ? this.urlParams : props.defaultSelected;
		this.changeValue(defaultValue);
	}

	changeValue(defaultValue) {
		if (!_.isEqual(this.defaultSelected, defaultValue)) {
			this.defaultSelected = defaultValue;
			if(this.defaultSelected) {
				const records = this.props.data.filter(record => record.label === this.defaultSelected);
				if (records && records.length) {
					setTimeout(this.handleChange.bind(this, records[0]), 1000);
				} else {
					this.handleChange(null);
				}
			} else {
				this.handleChange(null);
			}
		}
	}

	// set the query type and input data
	setQueryInfo(props) {
		const obj = {
			key: props.componentId,
			value: {
				queryType: this.type,
				inputData: props.dataField,
				customQuery: props.customQuery ? props.customQuery : this.customQuery,
				reactiveId: this.context.reactiveId,
				showFilter: props.showFilter,
				filterLabel: props.filterLabel ? props.filterLabel : props.componentId,
				component: "SingleDropdownRange",
				defaultSelected: this.urlParams !== null ? this.urlParams : props.defaultSelected
			}
		};
		helper.selectedSensor.setSensorInfo(obj);
	}

	// build query for this sensor only
	customQuery(record) {
		if (record) {
			return {
				range: {
					[this.props.dataField]: {
						gte: record.start,
						lte: record.end,
						boost: 2.0
					}
				}
			};
		}
	}

	// handle the input change and pass the value inside sensor info
	handleChange(record) {
		this.setState({
			selected: record
		});
		const obj = {
			key: this.props.componentId,
			value: record
		};
		const execQuery = () => {
			const isExecuteQuery = true;
			if (this.props.onValueChange) {
				this.props.onValueChange(obj.value);
			}
			this.defaultSelected = record;
			helper.URLParams.update(this.props.componentId, record ? record.label : null, this.props.URLParams);
			helper.selectedSensor.set(obj, isExecuteQuery);
		};

		if (this.props.beforeValueChange) {
			this.props.beforeValueChange(obj.value)
			.then(() => {
				execQuery();
			})
			.catch((e) => {
				console.warn(`${this.props.componentId} - beforeValueChange rejected the promise with`, e);
			});
		} else {
			execQuery();
		}
	}

	// render
	render() {
		let title = null;
		if (this.props.title) {
			title = (
{this.props.title}
);
		}

		const data = this.props.data.map((item) => {
			item.value = item.label;
			return item;
		});

		const cx = classNames({
			"rbc-title-active": this.props.title,
			"rbc-title-inactive": !this.props.title,
			"rbc-placeholder-active": this.props.placeholder,
			"rbc-placeholder-inactive": !this.props.placeholder
		});

		return (
			<div className={`rbc rbc-singledropdownrange col s12 col-xs-12 card thumbnail ${cx}`} style={this.props.componentStyle}>

					{title}

						<Select
							options={data}
							clearable={false}
							value={this.state.selected}
							onChange={this.handleChange}
							placeholder={this.props.placeholder}
							searchable
						/>

		);
	}
}

SingleDropdownRange.propTypes = {
	componentId: React.PropTypes.string.isRequired,
	dataField: React.PropTypes.string.isRequired,
	title: React.PropTypes.oneOfType([
		React.PropTypes.string,
		React.PropTypes.element
	]),
	placeholder: React.PropTypes.string,
	data: React.PropTypes.any.isRequired,
	defaultSelected: React.PropTypes.string,
	customQuery: React.PropTypes.func,
	beforeValueChange: React.PropTypes.func,
	onValueChange: React.PropTypes.func,
	componentStyle: React.PropTypes.object,
	URLParams: React.PropTypes.bool,
	showFilter: React.PropTypes.bool,
	filterLabel: React.PropTypes.string
};

// Default props value
SingleDropdownRange.defaultProps = {
	componentStyle: {},
	URLParams: false,
	showFilter: true
};

// context type
SingleDropdownRange.contextTypes = {
	appbaseRef: React.PropTypes.any.isRequired,
	type: React.PropTypes.any.isRequired,
	reactiveId: React.PropTypes.number
};

SingleDropdownRange.types = {
	componentId: TYPES.STRING,
	dataField: TYPES.STRING,
	dataFieldType: TYPES.NUMBER,
	data: TYPES.OBJECT,
	defaultSelected: TYPES.STRING,
	title: TYPES.STRING,
	placeholder: TYPES.STRING,
	customQuery: TYPES.FUNCTION,
	componentStyle: TYPES.OBJECT,
	URLParams: TYPES.BOOLEAN,
	showFilter: TYPES.BOOLEAN,
	filterLabel: TYPES.STRING
};
<commit_msg>Add onQueryChange to SingleDropdownList for #172<commit_after>import React, { Component } from "react";
import classNames from "classnames";
import Select from "react-select";
import * as TYPES from "../middleware/constants";
import _ from "lodash";

const helper = require("../middleware/helper");

export default class SingleDropdownRange extends Component {
	constructor(props) {
		super(props);
		this.state = {
			selected: null
		};
		this.type = "range";
		this.urlParams = helper.URLParams.get(this.props.componentId);
		this.handleChange = this.handleChange.bind(this);
		this.customQuery = this.customQuery.bind(this);
	}

	// Set query information
	componentWillMount() {
		this.previousQuery = null;	// initial value for onQueryChange
		this.setQueryInfo(this.props);
		this.checkDefault(this.props);
		this.listenFilter();
	}

	componentWillReceiveProps(nextProps) {
		if (this.props.defaultSelected !== nextProps.defaultSelected) {
			this.changeValue(nextProps.defaultSelected);
		}
		if (this.props.showFilter !== nextProps.showFilter || this.props.filterLabel !== nextProps.filterLabel) {
			this.setQueryInfo(nextProps);
			this.handleChange(this.state.selected);
		}
	}

	componentWillUnmount() {
		if(this.filterListener) {
			this.filterListener.remove();
		}
	}

	listenFilter() {
		this.filterListener = helper.sensorEmitter.addListener("clearFilter", (data) => {
			if(data === this.props.componentId) {
				this.changeValue(null);
			}
		});
	}

	checkDefault(props) {
		this.urlParams = helper.URLParams.get(props.componentId);
		const defaultValue = this.urlParams !== null ? this.urlParams : props.defaultSelected;
		this.changeValue(defaultValue);
	}

	changeValue(defaultValue) {
		if (!_.isEqual(this.defaultSelected, defaultValue)) {
			this.defaultSelected = defaultValue;
			if(this.defaultSelected) {
				const records = this.props.data.filter(record => record.label === this.defaultSelected);
				if (records && records.length) {
					setTimeout(this.handleChange.bind(this, records[0]), 1000);
				} else {
					this.handleChange(null);
				}
			} else {
				this.handleChange(null);
			}
		}
	}

	// set the query type and input data
	setQueryInfo(props) {
		const getQuery = (value) => {
			const currentQuery = props.customQuery ? props.customQuery(value) : this.customQuery(value);
			if (this.props.onQueryChange && JSON.stringify(this.previousQuery) !== JSON.stringify(currentQuery)) {
				this.props.onQueryChange(this.previousQuery, currentQuery);
			}
			this.previousQuery = currentQuery;
			return currentQuery;
		};
		const obj = {
			key: props.componentId,
			value: {
				queryType: this.type,
				inputData: props.dataField,
				customQuery: getQuery,
				reactiveId: this.context.reactiveId,
				showFilter: props.showFilter,
				filterLabel: props.filterLabel ? props.filterLabel : props.componentId,
				component: "SingleDropdownRange",
				defaultSelected: this.urlParams !== null ? this.urlParams : props.defaultSelected
			}
		};
		helper.selectedSensor.setSensorInfo(obj);
	}

	// build query for this sensor only
	customQuery(record) {
		if (record) {
			return {
				range: {
					[this.props.dataField]: {
						gte: record.start,
						lte: record.end,
						boost: 2.0
					}
				}
			};
		}
	}

	// handle the input change and pass the value inside sensor info
	handleChange(record) {
		this.setState({
			selected: record
		});
		const obj = {
			key: this.props.componentId,
			value: record
		};
		const execQuery = () => {
			const isExecuteQuery = true;
			if (this.props.onValueChange) {
				this.props.onValueChange(obj.value);
			}
			this.defaultSelected = record;
			helper.URLParams.update(this.props.componentId, record ? record.label : null, this.props.URLParams);
			helper.selectedSensor.set(obj, isExecuteQuery);
		};

		if (this.props.beforeValueChange) {
			this.props.beforeValueChange(obj.value)
			.then(() => {
				execQuery();
			})
			.catch((e) => {
				console.warn(`${this.props.componentId} - beforeValueChange rejected the promise with`, e);
			});
		} else {
			execQuery();
		}
	}

	// render
	render() {
		let title = null;
		if (this.props.title) {
			title = (
{this.props.title}
);
		}

		const data = this.props.data.map((item) => {
			item.value = item.label;
			return item;
		});

		const cx = classNames({
			"rbc-title-active": this.props.title,
			"rbc-title-inactive": !this.props.title,
			"rbc-placeholder-active": this.props.placeholder,
			"rbc-placeholder-inactive": !this.props.placeholder
		});

		return (
			<div className={`rbc rbc-singledropdownrange col s12 col-xs-12 card thumbnail ${cx}`} style={this.props.componentStyle}>

					{title}

						<Select
							options={data}
							clearable={false}
							value={this.state.selected}
							onChange={this.handleChange}
							placeholder={this.props.placeholder}
							searchable
						/>

		);
	}
}

SingleDropdownRange.propTypes = {
	componentId: React.PropTypes.string.isRequired,
	dataField: React.PropTypes.string.isRequired,
	title: React.PropTypes.oneOfType([
		React.PropTypes.string,
		React.PropTypes.element
	]),
	placeholder: React.PropTypes.string,
	data: React.PropTypes.any.isRequired,
	defaultSelected: React.PropTypes.string,
	customQuery: React.PropTypes.func,
	beforeValueChange: React.PropTypes.func,
	onValueChange: React.PropTypes.func,
	componentStyle: React.PropTypes.object,
	URLParams: React.PropTypes.bool,
	showFilter: React.PropTypes.bool,
	onQueryChange: React.PropTypes.func,
	filterLabel: React.PropTypes.string
};

// Default props value
SingleDropdownRange.defaultProps = {
	componentStyle: {},
	URLParams: false,
	showFilter: true
};

// context type
SingleDropdownRange.contextTypes = {
	appbaseRef: React.PropTypes.any.isRequired,
	type: React.PropTypes.any.isRequired,
	reactiveId: React.PropTypes.number
};

SingleDropdownRange.types = {
	componentId: TYPES.STRING,
	dataField: TYPES.STRING,
	dataFieldType: TYPES.NUMBER,
	data: TYPES.OBJECT,
	defaultSelected: TYPES.STRING,
	title: TYPES.STRING,
	placeholder: TYPES.STRING,
	customQuery: TYPES.FUNCTION,
	componentStyle: TYPES.OBJECT,
	URLParams: TYPES.BOOLEAN,
	showFilter: TYPES.BOOLEAN,
	onQueryChange: TYPES.FUNCTION,
	filterLabel: TYPES.STRING
};
<|endoftext|>


<commit_before>const createRetry = require('../retry')
const { KafkaJSError } = require('../errors')

const isTestMode = process.env.NODE_ENV === 'test'

const isRebalancing = e =>
  e.type === 'REBALANCE_IN_PROGRESS' || e.type === 'NOT_COORDINATOR_FOR_GROUP'

const isKafkaJSError = e => e instanceof KafkaJSError

module.exports = class Runner {
  constructor({
    consumerGroup,
    logger,
    eachBatchAutoResolve = true,
    eachBatch,
    eachMessage,
    heartbeatInterval,
    onCrash,
    retry,
  }) {
    this.consumerGroup = consumerGroup
    this.logger = logger.namespace('Runner')
    this.eachBatchAutoResolve = eachBatchAutoResolve
    this.eachBatch = eachBatch
    this.eachMessage = eachMessage
    this.heartbeatInterval = heartbeatInterval
    this.retrier = createRetry(Object.assign({}, retry))
    this.onCrash = onCrash

    this.running = false
    this.consuming = false
  }

  async join() {
    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        await this.consumerGroup.join()
        await this.consumerGroup.sync()
        this.logger.info('Consumer has joined the group', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          leaderId: this.consumerGroup.leaderId,
        })
      } catch (e) {
        if (isRebalancing(e)) {
          // Rebalance in progress isn't a retriable error since the consumer
          // has to go through find coordinator and join again before it can
          // actually retry. Throwing a retriable error to allow the retrier
          // to keep going
          throw new KafkaJSError('The group is rebalancing')
        }

        bail(e)
      }
    })
  }

  async start() {
    if (this.running) {
      return
    }

    try {
      await this.join()

      this.running = true
      this.scheduleFetch()
    } catch (e) {
      this.onCrash(e)
    }
  }

  async stop() {
    this.logger.debug('stop consumer group', {
      groupId: this.consumerGroup.groupId,
      memberId: this.consumerGroup.memberId,
    })
    this.running = false

    try {
      if (!isTestMode) {
        await this.waitForConsumer()
      }
      await this.consumerGroup.leave()
    } catch (e) {}
  }

  waitForConsumer() {
    return new Promise(resolve => {
      const scheduleWait = () => {
        this.logger.debug('waiting for consumer to finish...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
        })

        setTimeout(() => (!this.consuming ? resolve() : scheduleWait()), 1000)
      }

      if (!this.consuming) {
        return resolve()
      }

      scheduleWait()
    })
  }

  async processEachMessage(batch) {
    const { topic, partition } = batch

    for (let message of batch.messages) {
      if (!this.running) {
        break
      }

      try {
        await this.eachMessage({ topic, partition, message })
      } catch (e) {
        if (!isKafkaJSError(e)) {
          this.logger.error(`Error when calling eachMessage`, {
            topic,
            partition,
            offset: message.offset,
            stack: e.stack,
          })
        }

        // In case of errors, commit the previously consumed offsets
        await this.consumerGroup.commitOffsets()
        throw e
      }

      this.consumerGroup.resolveOffset({ topic, partition, offset: message.offset })
      await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
    }
  }

  async processEachBatch(batch) {
    const { topic, partition } = batch

    try {
      await this.eachBatch({
        batch,
        resolveOffset: offset => {
          this.consumerGroup.resolveOffset({ topic, partition, offset })
        },
        heartbeat: async () => {
          await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
        },
        isRunning: () => this.running,
      })
    } catch (e) {
      if (!isKafkaJSError(e)) {
        this.logger.error(`Error when calling eachBatch`, {
          topic,
          partition,
          offset: batch.firstOffset(),
          stack: e.stack,
        })
      }

      // eachBatch has a special resolveOffset which can be used
      // to keep track of the messages
      await this.consumerGroup.commitOffsets()
      throw e
    }

    // resolveOffset for the last offset can be disabled to allow the users of eachBatch to
    // stop their consumers without resolving unprocessed offsets (issues/18)
    if (this.eachBatchAutoResolve) {
      this.consumerGroup.resolveOffset({ topic, partition, offset: batch.lastOffset() })
    }
  }

  async fetch() {
    const batches = await this.consumerGroup.fetch()
    for (let batch of batches) {
      if (!this.running) {
        break
      }

      if (batch.isEmpty()) {
        this.consumerGroup.resetOffset(batch)
        continue
      }

      if (this.eachMessage) {
        await this.processEachMessage(batch)
      } else if (this.eachBatch) {
        await this.processEachBatch(batch)
      }
    }

    await this.consumerGroup.commitOffsets()
    await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
  }

  async scheduleFetch() {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      return
    }

    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        this.consuming = true
        await this.fetch()
        this.consuming = false
        setImmediate(() => this.scheduleFetch())
      } catch (e) {
        if (isRebalancing(e)) {
          this.logger.error('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          await this.join()
          this.scheduleFetch()
          return
        }

        if (e.type === 'UNKNOWN_MEMBER_ID') {
          this.logger.error('The coordinator is not aware of this member, re-joining the group', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.consumerGroup.memberId = null
          await this.join()
          this.scheduleFetch()
          return
        }

        if (e.name === 'KafkaJSOffsetOutOfRange') {
          this.scheduleFetch()
          return
        }

        this.logger.debug('Error while fetching data, trying again...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          error: e.message,
          stack: e.stack,
          retryCount,
          retryTime,
        })

        throw e
      } finally {
        this.consuming = false
      }
    }).catch(this.onCrash)
  }
}
<commit_msg>Use conditional commit when running each message<commit_after>const createRetry = require('../retry')
const { KafkaJSError } = require('../errors')

const isTestMode = process.env.NODE_ENV === 'test'

const isRebalancing = e =>
  e.type === 'REBALANCE_IN_PROGRESS' || e.type === 'NOT_COORDINATOR_FOR_GROUP'

const isKafkaJSError = e => e instanceof KafkaJSError

module.exports = class Runner {
  constructor({
    consumerGroup,
    logger,
    eachBatchAutoResolve = true,
    eachBatch,
    eachMessage,
    heartbeatInterval,
    onCrash,
    retry,
  }) {
    this.consumerGroup = consumerGroup
    this.logger = logger.namespace('Runner')
    this.eachBatchAutoResolve = eachBatchAutoResolve
    this.eachBatch = eachBatch
    this.eachMessage = eachMessage
    this.heartbeatInterval = heartbeatInterval
    this.retrier = createRetry(Object.assign({}, retry))
    this.onCrash = onCrash

    this.running = false
    this.consuming = false
  }

  async join() {
    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        await this.consumerGroup.join()
        await this.consumerGroup.sync()
        this.logger.info('Consumer has joined the group', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          leaderId: this.consumerGroup.leaderId,
        })
      } catch (e) {
        if (isRebalancing(e)) {
          // Rebalance in progress isn't a retriable error since the consumer
          // has to go through find coordinator and join again before it can
          // actually retry. Throwing a retriable error to allow the retrier
          // to keep going
          throw new KafkaJSError('The group is rebalancing')
        }

        bail(e)
      }
    })
  }

  async start() {
    if (this.running) {
      return
    }

    try {
      await this.join()

      this.running = true
      this.scheduleFetch()
    } catch (e) {
      this.onCrash(e)
    }
  }

  async stop() {
    this.logger.debug('stop consumer group', {
      groupId: this.consumerGroup.groupId,
      memberId: this.consumerGroup.memberId,
    })
    this.running = false

    try {
      if (!isTestMode) {
        await this.waitForConsumer()
      }
      await this.consumerGroup.leave()
    } catch (e) {}
  }

  waitForConsumer() {
    return new Promise(resolve => {
      const scheduleWait = () => {
        this.logger.debug('waiting for consumer to finish...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
        })

        setTimeout(() => (!this.consuming ? resolve() : scheduleWait()), 1000)
      }

      if (!this.consuming) {
        return resolve()
      }

      scheduleWait()
    })
  }

  async processEachMessage(batch) {
    const { topic, partition } = batch

    for (let message of batch.messages) {
      if (!this.running) {
        break
      }

      try {
        await this.eachMessage({ topic, partition, message })
      } catch (e) {
        if (!isKafkaJSError(e)) {
          this.logger.error(`Error when calling eachMessage`, {
            topic,
            partition,
            offset: message.offset,
            stack: e.stack,
          })
        }

        // In case of errors, commit the previously consumed offsets
        await this.consumerGroup.commitOffsets()
        throw e
      }

      this.consumerGroup.resolveOffset({ topic, partition, offset: message.offset })
      await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
      await this.consumerGroup.commitOffsetsIfNecessary()
    }
  }

  async processEachBatch(batch) {
    const { topic, partition } = batch

    try {
      await this.eachBatch({
        batch,
        resolveOffset: offset => {
          this.consumerGroup.resolveOffset({ topic, partition, offset })
        },
        heartbeat: async () => {
          await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
        },
        isRunning: () => this.running,
      })
    } catch (e) {
      if (!isKafkaJSError(e)) {
        this.logger.error(`Error when calling eachBatch`, {
          topic,
          partition,
          offset: batch.firstOffset(),
          stack: e.stack,
        })
      }

      // eachBatch has a special resolveOffset which can be used
      // to keep track of the messages
      await this.consumerGroup.commitOffsets()
      throw e
    }

    // resolveOffset for the last offset can be disabled to allow the users of eachBatch to
    // stop their consumers without resolving unprocessed offsets (issues/18)
    if (this.eachBatchAutoResolve) {
      this.consumerGroup.resolveOffset({ topic, partition, offset: batch.lastOffset() })
    }
  }

  async fetch() {
    const batches = await this.consumerGroup.fetch()
    for (let batch of batches) {
      if (!this.running) {
        break
      }

      if (batch.isEmpty()) {
        this.consumerGroup.resetOffset(batch)
        continue
      }

      if (this.eachMessage) {
        await this.processEachMessage(batch)
      } else if (this.eachBatch) {
        await this.processEachBatch(batch)
      }
    }

    await this.consumerGroup.commitOffsets()
    await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
  }

  async scheduleFetch() {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      return
    }

    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        this.consuming = true
        await this.fetch()
        this.consuming = false
        setImmediate(() => this.scheduleFetch())
      } catch (e) {
        if (isRebalancing(e)) {
          this.logger.error('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          await this.join()
          this.scheduleFetch()
          return
        }

        if (e.type === 'UNKNOWN_MEMBER_ID') {
          this.logger.error('The coordinator is not aware of this member, re-joining the group', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.consumerGroup.memberId = null
          await this.join()
          this.scheduleFetch()
          return
        }

        if (e.name === 'KafkaJSOffsetOutOfRange') {
          this.scheduleFetch()
          return
        }

        this.logger.debug('Error while fetching data, trying again...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          error: e.message,
          stack: e.stack,
          retryCount,
          retryTime,
        })

        throw e
      } finally {
        this.consuming = false
      }
    }).catch(this.onCrash)
  }
}
<|endoftext|>