<commit_before>// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
(function(cloudStack) {
  cloudStack.sections.domains = {
    title: 'label.menu.domains',
    id: 'domains',

    // Domain tree
    treeView: {
      // Details
      detailView: {
        name: 'Domain details',
        viewAll: {
          label: 'label.accounts',
          path: 'accounts'
        },

        // Detail actions
        actions: {
          'delete': {
            label: 'label.action.delete.domain',
            messages: {
              notification: function(args) {
                return 'label.action.delete.domain';
              }
            },

            createForm: {
              title: 'label.action.delete.domain',
              desc: 'message.action.delete.domain',
              createLabel: 'label.delete',
              preFilter: function(args) {
                if(isAdmin()) {
                  args.$form.find('.form-item[rel=isForced]').css('display', 'inline-block');
                }
              },
              fields: {
                isForced: {
                  label: 'force.delete',
                  isBoolean: true,
                  isHidden: true
                }
              }
            },

            action: function(args) {
              var array1 = [];
              if(args.$form.find('.form-item[rel=isForced]').css("display") != "none") //uncomment after Brian fix it to include $form in args
                array1.push("&cleanup=" + (args.data.isForced == "on"));

              $.ajax({
                url: createURL("deleteDomain&id=" + args.context.domains[0].id + array1.join("")),
                dataType: "json",
                async: false,
                success: function(json) {
                  var jid = json.deletedomainresponse.jobid;
                  args.response.success(
                    {_custom:
                     {jobId: jid}
                    }
                  );

                  // Quick fix for proper UI reaction to delete domain
                  var $item = $('.name.selected').closest('li');
                  var $itemParent = $item.closest('li');
                  $itemParent.parent().parent().find('.name:first').click();
                  $item.remove();
                }
              });
            },
            notification: {
              poll: pollAsyncJobResult
            }
          },

          // Edit domain
          edit: {
            label: 'label.action.edit.domain',
            messages: {
              notification: function(args) {
                return 'label.action.edit.domain';
              }
            },
            action: function(args) {
              var domainObj;

							var data = {
							  id: args.context.domains[0].id,
							  networkdomain: args.data.networkdomain
							};

							if(args.data.name != null) {
							  $.extend(data, {
								  name: args.data.name
								});
							}

              $.ajax({
                url: createURL("updateDomain"),
                async: false,
                data: data,
                success: function(json) {
                  domainObj = json.updatedomainresponse.domain;
                }
              });

							if(args.data.vmLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=0&max=" + args.data.vmLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["vmLimit"] = args.data.vmLimit;
									}
								});
							}

							if(args.data.ipLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=1&max=" + args.data.ipLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["ipLimit"] = args.data.ipLimit;
									}
								});
							}

              if(args.data.volumeLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=2&max=" + args.data.volumeLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["volumeLimit"] = args.data.volumeLimit;
									}
								});
							}

              if(args.data.snapshotLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=3&max=" + args.data.snapshotLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["snapshotLimit"] = args.data.snapshotLimit;
									}
								});
							}

              if(args.data.templateLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=4&max=" + args.data.templateLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["templateLimit"] = args.data.templateLimit;
									}
								});
							}

              if(args.data.vpcLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=7&max=" + args.data.vpcLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["vpcLimit"] = args.data.vpcLimit;
									}
								});
							}

              args.response.success({data: domainObj});
            }
          },

          // Add domain
          create: {
            label: 'label.add.domain',

            action: function(args) {
              var array1 = [];
              array1.push("&parentdomainid=" + args.context.domains[0].id);
              array1.push("&name=" + todb(args.data.name));
              if(args.data.networkdomain != null && args.data.networkdomain.length > 0)
                array1.push("&networkdomain=" + todb(args.data.networkdomain));

              $.ajax({
                url: createURL("createDomain" + array1.join("")),
                dataType: "json",
                async: false,
                success: function(json) {
                  var item = json.createdomainresponse.domain;
                  args.response.success({data: item});
                },
                error: function(XMLHttpResponse) {
                  var errorMsg = parseXMLHttpResponse(XMLHttpResponse);
                  args.response.error(errorMsg);
                }
              });
            },

            messages: {
              notification: function(args) {
                return 'label.add.domain';
              }
            },

            createForm: {
              title: 'label.add.domain',
              desc: 'message.add.domain',
              fields: {
                name: {
                  label: 'label.name',
                  validation: { required: true }
                },
                networkdomain: {
                  label: 'label.network.domain',
                  validation: { required: false }
                }
              }
            }
          },

					updateResourceCount: {
						label: 'label.action.update.resource.count',
						messages: {
							confirm: function(args) {
								return 'message.update.resource.count';
							},
							notification: function(args) {
								return 'label.action.update.resource.count';
							}
						},
						action: function(args) {
							$.ajax({
								url: createURL("updateResourceCount&domainid=" + args.context.domains[0].id),
								dataType: "json",
								async: true,
								success: function(json) {
									//var resourcecounts= json.updateresourcecountresponse.resourcecount;   //do nothing
									args.response.success();
								}
							});
						},
						notification: {
							poll: function(args) {
								args.complete();
							}
						}
					}
        },
        tabs: {
          details: {
            title: 'label.details',
            fields: [
              {
                name: {
								  label: 'label.name',
									isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to change domain name
										  return true;
										else
											return false;
									}
								}
              },
              {
                id: { label: 'ID' },

								path: { label: 'label.full.path' },

                networkdomain: {
                  label: 'label.network.domain',
                  isEditable: true
                },
                vmLimit: {
                  label: 'label.instance.limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                ipLimit: {
                  label: 'label.ip.limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                volumeLimit: {
                  label: 'label.volume.limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                snapshotLimit: {
                  label: 'label.snapshot.limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                templateLimit: {
                  label: 'label.template.limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                vpcLimit: {
                  label: 'VPC limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                accountTotal: { label: 'label.accounts' },
                vmTotal: { label: 'label.instances' },
                volumeTotal: { label: 'label.volumes' }
              }
            ],
            dataProvider: function(args) {
              var domainObj = args.context.domains[0];
              $.ajax({
                url: createURL("listAccounts&domainid=" + domainObj.id),
                async: false,
                dataType: "json",
                success: function(json) {
                  var items = json.listaccountsresponse.account;
                  var total;
                  if (items != null)
                    total = items.length;
                  else
                    total = 0;
                  domainObj["accountTotal"] = total;
                }
              });

              $.ajax({
                url: createURL("listVirtualMachines&details=min&domainid=" + domainObj.id),
                async: false,
                dataType: "json",
                success: function(json) {
                  var items = json.listvirtualmachinesresponse.virtualmachine;
                  var total;
                  if (items != null)
                    total = items.length;
                  else
                    total = 0;
                  domainObj["vmTotal"] = total;
                }
              });

              $.ajax({
                url: createURL("listVolumes&domainid=" + domainObj.id),
                async: false,
                dataType: "json",
                success: function(json) {
                  var items = json.listvolumesresponse.volume;
                  var total;
                  if (items != null)
                    total = items.length;
                  else
                    total = 0;
                  domainObj["volumeTotal"] = total;
                }
              });

              $.ajax({
                url: createURL("listResourceLimits&domainid=" + domainObj.id),
                async: false,
                dataType: "json",
                success: function(json) {
                  var limits = json.listresourcelimitsresponse.resourcelimit;
                  if (limits != null) {
                    for (var i = 0; i < limits.length; i++) {
                      var limit = limits[i];
                      switch (limit.resourcetype) {
                      case "0":
                        domainObj["vmLimit"] = limit.max;
                        break;
                      case "1":
                        domainObj["ipLimit"] = limit.max;
                        break;
                      case "2":
                        domainObj["volumeLimit"] = limit.max;
                        break;
                      case "3":
                        domainObj["snapshotLimit"] = limit.max;
                        break;
                      case "4":
                        domainObj["templateLimit"] = limit.max;
                        break;
                      case "7":
                        domainObj["vpcLimit"] = limit.max;
                        break;
                      }
                    }
                  }
                }
              });

              args.response.success({
                data: domainObj,
                actionFilter: domainActionfilter
              });
            }
          }
        }
      },
      labelField: 'name',
      dataProvider: function(args) {
        var parentDomain = args.context.parentDomain;
        if(parentDomain == null) { //draw root node
          $.ajax({
            url: createURL("listDomains&id=" + g_domainid + '&listAll=true'),
            dataType: "json",
            async: false,
            success: function(json) {
              var domainObjs = json.listdomainsresponse.domain;
              args.response.success({
                actionFilter: domainActionfilter,
                data: domainObjs
              });
            }
          });
        }
        else {
          $.ajax({
            url: createURL("listDomainChildren&id=" + parentDomain.id),
            dataType: "json",
            async: false,
            success: function(json) {
              var domainObjs = json.listdomainchildrenresponse.domain;
              args.response.success({
                actionFilter: domainActionfilter,
                data: domainObjs
              });
            }
          });
        }
      }
    }
  };

  var domainActionfilter = function(args) {
    var jsonObj = args.context.item;
    var allowedActions = [];
    if(isAdmin()) {
      allowedActions.push("create");
			allowedActions.push("edit"); //merge updateResourceLimit into edit
			if(jsonObj.level != 0) { //ROOT domain (whose level is 0) is not allowed to delete
        allowedActions.push("delete");
      }
    }
    allowedActions.push("updateResourceCount");
    return allowedActions;
  }

})(cloudStack);
<commit_msg>CS-16413: cloudstack UI - domain page - pass details=min to listAccounts API.<commit_after>// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
(function(cloudStack) {
  cloudStack.sections.domains = {
    title: 'label.menu.domains',
    id: 'domains',

    // Domain tree
    treeView: {
      // Details
      detailView: {
        name: 'Domain details',
        viewAll: {
          label: 'label.accounts',
          path: 'accounts'
        },

        // Detail actions
        actions: {
          'delete': {
            label: 'label.action.delete.domain',
            messages: {
              notification: function(args) {
                return 'label.action.delete.domain';
              }
            },

            createForm: {
              title: 'label.action.delete.domain',
              desc: 'message.action.delete.domain',
              createLabel: 'label.delete',
              preFilter: function(args) {
                if(isAdmin()) {
                  args.$form.find('.form-item[rel=isForced]').css('display', 'inline-block');
                }
              },
              fields: {
                isForced: {
                  label: 'force.delete',
                  isBoolean: true,
                  isHidden: true
                }
              }
            },

            action: function(args) {
              var array1 = [];
              if(args.$form.find('.form-item[rel=isForced]').css("display") != "none") //uncomment after Brian fix it to include $form in args
                array1.push("&cleanup=" + (args.data.isForced == "on"));

              $.ajax({
                url: createURL("deleteDomain&id=" + args.context.domains[0].id + array1.join("")),
                dataType: "json",
                async: false,
                success: function(json) {
                  var jid = json.deletedomainresponse.jobid;
                  args.response.success(
                    {_custom:
                     {jobId: jid}
                    }
                  );

                  // Quick fix for proper UI reaction to delete domain
                  var $item = $('.name.selected').closest('li');
                  var $itemParent = $item.closest('li');
                  $itemParent.parent().parent().find('.name:first').click();
                  $item.remove();
                }
              });
            },
            notification: {
              poll: pollAsyncJobResult
            }
          },

          // Edit domain
          edit: {
            label: 'label.action.edit.domain',
            messages: {
              notification: function(args) {
                return 'label.action.edit.domain';
              }
            },
            action: function(args) {
              var domainObj;

							var data = {
							  id: args.context.domains[0].id,
							  networkdomain: args.data.networkdomain
							};

							if(args.data.name != null) {
							  $.extend(data, {
								  name: args.data.name
								});
							}

              $.ajax({
                url: createURL("updateDomain"),
                async: false,
                data: data,
                success: function(json) {
                  domainObj = json.updatedomainresponse.domain;
                }
              });

							if(args.data.vmLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=0&max=" + args.data.vmLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["vmLimit"] = args.data.vmLimit;
									}
								});
							}

							if(args.data.ipLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=1&max=" + args.data.ipLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["ipLimit"] = args.data.ipLimit;
									}
								});
							}

              if(args.data.volumeLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=2&max=" + args.data.volumeLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["volumeLimit"] = args.data.volumeLimit;
									}
								});
							}

              if(args.data.snapshotLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=3&max=" + args.data.snapshotLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["snapshotLimit"] = args.data.snapshotLimit;
									}
								});
							}

              if(args.data.templateLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=4&max=" + args.data.templateLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["templateLimit"] = args.data.templateLimit;
									}
								});
							}

              if(args.data.vpcLimit != null) {
								$.ajax({
									url: createURL("updateResourceLimit&domainid=" + args.context.domains[0].id + "&resourceType=7&max=" + args.data.vpcLimit),
									dataType: "json",
									async: false,
									success: function(json) {
										domainObj["vpcLimit"] = args.data.vpcLimit;
									}
								});
							}

              args.response.success({data: domainObj});
            }
          },

          // Add domain
          create: {
            label: 'label.add.domain',

            action: function(args) {
              var array1 = [];
              array1.push("&parentdomainid=" + args.context.domains[0].id);
              array1.push("&name=" + todb(args.data.name));
              if(args.data.networkdomain != null && args.data.networkdomain.length > 0)
                array1.push("&networkdomain=" + todb(args.data.networkdomain));

              $.ajax({
                url: createURL("createDomain" + array1.join("")),
                dataType: "json",
                async: false,
                success: function(json) {
                  var item = json.createdomainresponse.domain;
                  args.response.success({data: item});
                },
                error: function(XMLHttpResponse) {
                  var errorMsg = parseXMLHttpResponse(XMLHttpResponse);
                  args.response.error(errorMsg);
                }
              });
            },

            messages: {
              notification: function(args) {
                return 'label.add.domain';
              }
            },

            createForm: {
              title: 'label.add.domain',
              desc: 'message.add.domain',
              fields: {
                name: {
                  label: 'label.name',
                  validation: { required: true }
                },
                networkdomain: {
                  label: 'label.network.domain',
                  validation: { required: false }
                }
              }
            }
          },

					updateResourceCount: {
						label: 'label.action.update.resource.count',
						messages: {
							confirm: function(args) {
								return 'message.update.resource.count';
							},
							notification: function(args) {
								return 'label.action.update.resource.count';
							}
						},
						action: function(args) {
							$.ajax({
								url: createURL("updateResourceCount&domainid=" + args.context.domains[0].id),
								dataType: "json",
								async: true,
								success: function(json) {
									//var resourcecounts= json.updateresourcecountresponse.resourcecount;   //do nothing
									args.response.success();
								}
							});
						},
						notification: {
							poll: function(args) {
								args.complete();
							}
						}
					}
        },
        tabs: {
          details: {
            title: 'label.details',
            fields: [
              {
                name: {
								  label: 'label.name',
									isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to change domain name
										  return true;
										else
											return false;
									}
								}
              },
              {
                id: { label: 'ID' },

								path: { label: 'label.full.path' },

                networkdomain: {
                  label: 'label.network.domain',
                  isEditable: true
                },
                vmLimit: {
                  label: 'label.instance.limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                ipLimit: {
                  label: 'label.ip.limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                volumeLimit: {
                  label: 'label.volume.limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                snapshotLimit: {
                  label: 'label.snapshot.limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                templateLimit: {
                  label: 'label.template.limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                vpcLimit: {
                  label: 'VPC limits',
                  isEditable: function(context) {
                    if(context.domains[0].level != 0) //ROOT domain (whose level is 0) is not allowed to updateResourceLimits
										  return true;
										else
											return false;
									}
                },
                accountTotal: { label: 'label.accounts' },
                vmTotal: { label: 'label.instances' },
                volumeTotal: { label: 'label.volumes' }
              }
            ],
            dataProvider: function(args) {
              var domainObj = args.context.domains[0];
              $.ajax({
                url: createURL("listAccounts&domainid=" + domainObj.id),
                async: false,
                data: {
								  details: 'min'
								},
                success: function(json) {
                  var items = json.listaccountsresponse.account;
                  var total;
                  if (items != null)
                    total = items.length;
                  else
                    total = 0;
                  domainObj["accountTotal"] = total;
                }
              });

              $.ajax({
                url: createURL("listVirtualMachines&details=min&domainid=" + domainObj.id),
                async: false,
                dataType: "json",
                success: function(json) {
                  var items = json.listvirtualmachinesresponse.virtualmachine;
                  var total;
                  if (items != null)
                    total = items.length;
                  else
                    total = 0;
                  domainObj["vmTotal"] = total;
                }
              });

              $.ajax({
                url: createURL("listVolumes&domainid=" + domainObj.id),
                async: false,
                dataType: "json",
                success: function(json) {
                  var items = json.listvolumesresponse.volume;
                  var total;
                  if (items != null)
                    total = items.length;
                  else
                    total = 0;
                  domainObj["volumeTotal"] = total;
                }
              });

              $.ajax({
                url: createURL("listResourceLimits&domainid=" + domainObj.id),
                async: false,
                dataType: "json",
                success: function(json) {
                  var limits = json.listresourcelimitsresponse.resourcelimit;
                  if (limits != null) {
                    for (var i = 0; i < limits.length; i++) {
                      var limit = limits[i];
                      switch (limit.resourcetype) {
                      case "0":
                        domainObj["vmLimit"] = limit.max;
                        break;
                      case "1":
                        domainObj["ipLimit"] = limit.max;
                        break;
                      case "2":
                        domainObj["volumeLimit"] = limit.max;
                        break;
                      case "3":
                        domainObj["snapshotLimit"] = limit.max;
                        break;
                      case "4":
                        domainObj["templateLimit"] = limit.max;
                        break;
                      case "7":
                        domainObj["vpcLimit"] = limit.max;
                        break;
                      }
                    }
                  }
                }
              });

              args.response.success({
                data: domainObj,
                actionFilter: domainActionfilter
              });
            }
          }
        }
      },
      labelField: 'name',
      dataProvider: function(args) {
        var parentDomain = args.context.parentDomain;
        if(parentDomain == null) { //draw root node
          $.ajax({
            url: createURL("listDomains&id=" + g_domainid + '&listAll=true'),
            dataType: "json",
            async: false,
            success: function(json) {
              var domainObjs = json.listdomainsresponse.domain;
              args.response.success({
                actionFilter: domainActionfilter,
                data: domainObjs
              });
            }
          });
        }
        else {
          $.ajax({
            url: createURL("listDomainChildren&id=" + parentDomain.id),
            dataType: "json",
            async: false,
            success: function(json) {
              var domainObjs = json.listdomainchildrenresponse.domain;
              args.response.success({
                actionFilter: domainActionfilter,
                data: domainObjs
              });
            }
          });
        }
      }
    }
  };

  var domainActionfilter = function(args) {
    var jsonObj = args.context.item;
    var allowedActions = [];
    if(isAdmin()) {
      allowedActions.push("create");
			allowedActions.push("edit"); //merge updateResourceLimit into edit
			if(jsonObj.level != 0) { //ROOT domain (whose level is 0) is not allowed to delete
        allowedActions.push("delete");
      }
    }
    allowedActions.push("updateResourceCount");
    return allowedActions;
  }

})(cloudStack);
<|endoftext|>


<commit_before>import SPELLS from 'common/SPELLS';
import ITEMS from 'common/ITEMS';
import SPECS from 'common/SPECS';
import { calculateSecondaryStatDefault } from 'common/stats';
import { formatMilliseconds } from 'common/format';

import Analyzer from 'Parser/Core/Analyzer';
import Combatants from 'Parser/Core/Modules/Combatants';

const debug = false;

// TODO: stat constants somewhere else? they're largely copied from combatant
class StatTracker extends Analyzer {
  static dependencies = {
    combatants: Combatants,
  };

  static STAT_BUFFS = {
    // region Potions
    [SPELLS.POTION_OF_PROLONGED_POWER.id]: { strength: 2500, agility: 2500, intellect: 2500 },
    // endregion
    // TODO: add flasks
    // TODO: add food
    // TODO: add runes

    // region Trinkets
    [SPELLS.SHADOWS_STRIKE.id]: {
      itemId: ITEMS.DREADSTONE_OF_ENDLESS_SHADOWS.id,
      crit: (_, item) => calculateSecondaryStatDefault(845, 3480, item.itemLevel),
    },
    [SPELLS.SHADOW_MASTER.id]: {
      itemId: ITEMS.DREADSTONE_OF_ENDLESS_SHADOWS.id,
      mastery: (_, item) => calculateSecondaryStatDefault(845, 3480, item.itemLevel),
    },
    [SPELLS.SWARMING_SHADOWS.id]: {
      itemId: ITEMS.DREADSTONE_OF_ENDLESS_SHADOWS.id,
      haste: (_, item) => calculateSecondaryStatDefault(845, 3480, item.itemLevel),
    },
    // Event weirdness makes it impossible to handle CotRT normally, it's handled instead by the CharmOfTheRisingTide module
    //[SPELLS.RISING_TIDES.id]: {
    //  itemId: ITEMS.CHARM_OF_THE_RISING_TIDE.id,
    //  haste: (_, item) => calculateSecondaryStatDefault(900, 576, item.itemLevel),
    //},
    [SPELLS.ACCELERANDO.id]: {
      itemId: ITEMS.ERRATIC_METRONOME.id,
      haste: (_, item) => calculateSecondaryStatDefault(870, 657, item.itemLevel),
    },
    // endregion

    // region Misc
    [SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_STRENGTH.id]: { // check numbers
      strength: combatant => 4000 + (combatant.traitsBySpellId[SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_TRAIT.id] - 1) * 300,
    },
    [SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_AGILITY.id]: { // check numbers
      agility: combatant => 4000 + (combatant.traitsBySpellId[SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_TRAIT.id] - 1) * 300,
    },
    [SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_INTELLECT.id]: { // check numbers
      intellect: combatant => 4000 + (combatant.traitsBySpellId[SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_TRAIT.id] - 1) * 300,
    },
    [SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_VERSATILITY.id]: { // check numbers
      versatility: combatant => 1500 + 100 * (combatant.traitsBySpellId[SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_TRAIT.id] - 1) * 300,
    },
    [SPELLS.JACINS_RUSE.id]: { mastery: 3000 },
    [SPELLS.MARK_OF_THE_CLAW.id]: { crit: 1000, haste: 1000 },
    // endregion

    // region Druid
    [SPELLS.ASTRAL_HARMONY.id]: { mastery: 4000 },
    // endregion

    // region Mage
    [SPELLS.WARMTH_OF_THE_PHOENIX.id]: { crit: 800 },
    // endregion

    // region Priest
    [SPELLS.MIND_QUICKENING.id]: { haste: 800 },
    // endregion
  };

  _startingStats = {};
  _stats = {};

  on_initialized() {
    this._startingStats = {
      strength: this.combatants.selected.strength,
      agility: this.combatants.selected.agility,
      intellect: this.combatants.selected.intellect,
      stamina: this.combatants.selected.stamina,
      crit: this.combatants.selected.critRating,
      haste: this.combatants.selected.hasteRating,
      mastery: this.combatants.selected.masteryRating,
      versatility: this.combatants.selected.versatilityRating,
      avoidance: this.combatants.selected.avoidanceRating,
      leech: this.combatants.selected.leechRating,
      speed: this.combatants.selected.speedRating,
    };
    this._stats = this._startingStats;

    debug && this._debugPrintStats(this._stats);
  }

  /*
   * Stat rating at pull.
   * Should be identical to what you get from Combatant.
   */
  get startingStrengthRating() {
    return this._startingStats.strength;
  }
  get startingAgilityRating() {
    return this._startingStats.agility;
  }
  get startingIntellectRating() {
    return this._startingStats.intellect;
  }
  get startingStaminaRating() {
    return this._startingStats.stamina;
  }
  get startingCritRating() {
    return this._startingStats.crit;
  }
  get startingHasteRating() {
    return this._startingStats.haste;
  }
  get startingMasteryRating() {
    return this._startingStats.mastery;
  }
  get startingVersatilityRating() {
    return this._startingStats.versatility;
  }
  get startingAvoidanceRating() {
    return this._startingStats.avoidance;
  }
  get startingLeechRating() {
    return this._startingStats.leech;
  }
  get startingSpeedRating() {
    return this._startingStats.speed;
  }

  /*
   * Current stat rating, as tracked by this module.
   */
  get currentStrengthRating() {
    return this._stats.strength;
  }
  get currentAgilityRating() {
    return this._stats.agility;
  }
  get currentIntellectRating() {
    return this._stats.intellect;
  }
  get currentStaminaRating() {
    return this._stats.stamina;
  }
  get currentCritRating() {
    return this._stats.crit;
  }
  get currentHasteRating() {
    return this._stats.haste;
  }
  get currentMasteryRating() {
    return this._stats.mastery;
  }
  get currentVersatilityRating() {
    return this._stats.versatility;
  }
  get currentAvoidanceRating() {
    return this._stats.avoidance;
  }
  get currentLeechRating() {
    return this._stats.leech;
  }
  get currentSpeedRating() {
    return this._stats.speed;
  }

  /*
   * For percentage stats, the percentage you'd have with zero rating.
   * These values don't change.
   */
  get baseCritPercentage() {
    return 0.08; // TODO is this the same for all classes?
  }
  get baseHastePercentage() {
    return 0;
  }
  get baseMasteryPercentage() {
    switch (this.combatants.selected.spec) {
      case SPECS.HOLY_PALADIN:
        return 0.12;
      case SPECS.HOLY_PRIEST:
        return 0.05;
      case SPECS.RESTORATION_SHAMAN:
        return 0.24;
      case SPECS.ENHANCEMENT_SHAMAN:
        return 0.2;
      case SPECS.RESTORATION_DRUID:
        return 0.048;
      default:
        throw new Error('Mastery hasn\'t been implemented for this spec yet.');
    }
  }
  get baseVersatilityPercentage() {
    return 0;
  }
  get baseAvoidancePercentage() {
    return 0;
  }
  get baseLeechPercentage() {
    return 0;
  }
  get baseSpeedPercentage() {
    return 0;
  }

  /*
   * For percentage stats, this is the divider to go from rating to percent (expressed from 0 to 1)
   * These values don't change.
   */
  get critRatingPerPercent() {
    return 40000;
  }
  get hasteRatingPerPercent() {
    return 37500;
  }
  get masteryRatingPerPercent() {
    switch (this.combatants.selected.spec) {
      case SPECS.HOLY_PALADIN:
        return 26667;
      case SPECS.HOLY_PRIEST:
        return 32000;
      case SPECS.RESTORATION_SHAMAN:
        return 13333;
      case SPECS.ENHANCEMENT_SHAMAN:
        return 13333;
      case SPECS.RESTORATION_DRUID:
        return 66667;
      default:
        throw new Error('Mastery hasn\'t been implemented for this spec yet.');
    }
  }
  get versatilityRatingPerPercent() {
    return 47500;
  }
  get avoidanceRatingPerPercent() {
    return 11000;
  }
  get leechRatingPerPercent() {
    return 23000;
  }
  get speedRatingPerPercent() {
    throw new Error('Speed hasn\'t been implemented yet.');
  }

  /*
   * For percentage stats, the current stat percentage as tracked by this module.
   */
  get currentCritPercentage() {
    return this.baseCritPercentage + (this.currentCritRating / this.critRatingPerPercent);
  }
  get currentHastePercentage() {
    return this.baseHastePercentage + (this.currentHasteRating / this.hasteRatingPerPercent);
  }
  get currentMasteryPercentage() {
    return this.baseMasteryPercentage + (this.currentMasteryRating / this.masteryRatingPerPercent);
  }
  get currentVersatilityPercentage() {
    return this.baseVersatilityPercentage + (this.currentVersatilityRating / this.versatilityRatingPerPercent);
  }
  get currentAvoidancePercentage() {
    return this.baseAvoidancePercentage + (this.currentAvoidanceRating / this.avoidanceRatingPerPercent);
  }
  get currentLeechPercentage() {
    return this.baseLeechPercentage + (this.currentLeechRating / this.leechRatingPerPercent);
  }
  get currentSpeedPercentage() {
    return this.baseSpeedPercentage + (this.currentSpeedRating / this.speedRatingPerPercent);
  }

  on_toPlayer_changebuffstack(event) {
    this._changeBuffStack(event);
  }

  on_toPlayer_changedebuffstack(event) {
    this._changeBuffStack(event);
  }

  /*
   * This interface allows an external analyzer to force a stat change.
   * It should ONLY be used if a stat buff is so non-standard that it can't be handled by the buff format in this module.
   * change is a stat buff object just like those in the STAT_BUFFS structure above, it is required.
   * eventReason is the WCL event object that caused this change, it is not required.
   */
   // For an example of how / why this function would be used, see the CharmOfTheRisingTide module.
  forceChangeStats(change, eventReason) {
    const before = Object.assign({}, this._stats);
    const delta = this._changeStats(change, 1);
    const after = Object.assign({}, this._stats);
    this._triggerChangeStats(eventReason, before, delta, after);
    if(debug) {
      const spellName = eventReason && eventReason.ability ? eventReason.ability.name : 'unspecified';
      console.log(`StatTracker: FORCED CHANGE from ${spellName} - Change: ${this._statPrint(delta)}`);
      this._debugPrintStats(this._stats);
    }
  }

  _changeBuffStack(event) {
    const spellId = event.ability.guid;
    const statBuff = this.constructor.STAT_BUFFS[spellId];
    if (statBuff) {
      // ignore prepull buff application, as they're already accounted for in combatantinfo
      // we have to check the stacks count because Entities incorrectly copies the prepull property onto changes and removal following the application
      if (event.oldStacks === 0 && event.prepull) {
        debug && console.log(`StatTracker prepull application IGNORED for ${SPELLS[spellId] ? SPELLS[spellId].name : spellId}`);
        return;
      }

      const before = Object.assign({}, this._stats);
      const delta = this._changeStats(statBuff, event.newStacks - event.oldStacks);
      const after = Object.assign({}, this._stats);
      this._triggerChangeStats(event, before, delta, after);
      debug && console.log(`StatTracker: (${event.oldStacks} -> ${event.newStacks}) ${SPELLS[spellId] ? SPELLS[spellId].name : spellId} @ ${formatMilliseconds(this.owner.currentTimestamp)} - Change: ${this._statPrint(delta)}`);
      debug && this._debugPrintStats(this._stats);
    }
  }

  _changeStats(change, factor) {
    const delta = {
      strength: this._getBuffValue(change, change.strength) * factor,
      agility: this._getBuffValue(change, change.agility) * factor,
      intellect: this._getBuffValue(change, change.intellect) * factor,
      stamina: this._getBuffValue(change, change.stamina) * factor,
      crit: this._getBuffValue(change, change.crit) * factor,
      haste: this._getBuffValue(change, change.haste) * factor,
      mastery: this._getBuffValue(change, change.mastery) * factor,
      versatility: this._getBuffValue(change, change.versatility) * factor,
      avoidance: this._getBuffValue(change, change.avoidance) * factor,
      leech: this._getBuffValue(change, change.leech) * factor,
      speed: this._getBuffValue(change, change.speed) * factor,
    };

    Object.keys(this._stats).forEach(key => {
      this._stats[key] += delta[key];
    });

    return delta;
  }

  /*
   * Fabricates an event indicating when stats change
   */
  _triggerChangeStats(event, before, delta, after) {
    this.owner.triggerEvent('changestats', {
      timestamp: event ? event.timestamp : this.owner.currentTimestamp,
      type: 'changestats',
      sourceID: event ? event.sourceID : this.owner.playerId,
      targetID: this.owner.playerId,
      reason: event,
      before,
      delta,
      after,
    });
  }

  /**
   * Gets the actual stat value in whatever format it is.
   * a number value will be returned as is
   * a function value will be called with (selectedCombatant, itemDetails) and the result returned
   * an undefined stat will default to 0.
   */
  _getBuffValue(buffObj, statVal) {
    if (statVal === undefined) {
      return 0;
    } else if (typeof statVal === 'function') {
      const selectedCombatant = this.combatants.selected;
      let itemDetails;
      if (buffObj.itemId) {
        itemDetails = this.combatants.selected.getItem(buffObj.itemId);
        if (!itemDetails) {
          console.warn('Failed to retrieve item information for item with ID:', buffObj.itemId,
            ' ...unable to handle stats buff, making no stat change.');
          return 0;
        }
      }
      return statVal(selectedCombatant, itemDetails);
    } else {
      return statVal;
    }
  }

  _debugPrintStats(stats) {
    console.log(`StatTracker: ${formatMilliseconds(this.owner.currentTimestamp)} - ${this._statPrint(stats)}`);
  }

  _statPrint(stats) {
    return `STR=${stats.strength} AGI=${stats.agility} INT=${stats.intellect} STM=${stats.stamina} CRT=${stats.crit} HST=${stats.haste} MST=${stats.mastery} VRS=${stats.versatility} AVD=${this._stats.avoidance} LCH=${stats.leech} SPD=${stats.speed}`;
  }

}

export default StatTracker;
<commit_msg>Rename StatTracker private props<commit_after>import SPELLS from 'common/SPELLS';
import ITEMS from 'common/ITEMS';
import SPECS from 'common/SPECS';
import { calculateSecondaryStatDefault } from 'common/stats';
import { formatMilliseconds } from 'common/format';

import Analyzer from 'Parser/Core/Analyzer';
import Combatants from 'Parser/Core/Modules/Combatants';

const debug = false;

// TODO: stat constants somewhere else? they're largely copied from combatant
class StatTracker extends Analyzer {
  static dependencies = {
    combatants: Combatants,
  };

  static STAT_BUFFS = {
    // region Potions
    [SPELLS.POTION_OF_PROLONGED_POWER.id]: { strength: 2500, agility: 2500, intellect: 2500 },
    // endregion
    // TODO: add flasks
    // TODO: add food
    // TODO: add runes

    // region Trinkets
    [SPELLS.SHADOWS_STRIKE.id]: {
      itemId: ITEMS.DREADSTONE_OF_ENDLESS_SHADOWS.id,
      crit: (_, item) => calculateSecondaryStatDefault(845, 3480, item.itemLevel),
    },
    [SPELLS.SHADOW_MASTER.id]: {
      itemId: ITEMS.DREADSTONE_OF_ENDLESS_SHADOWS.id,
      mastery: (_, item) => calculateSecondaryStatDefault(845, 3480, item.itemLevel),
    },
    [SPELLS.SWARMING_SHADOWS.id]: {
      itemId: ITEMS.DREADSTONE_OF_ENDLESS_SHADOWS.id,
      haste: (_, item) => calculateSecondaryStatDefault(845, 3480, item.itemLevel),
    },
    // Event weirdness makes it impossible to handle CotRT normally, it's handled instead by the CharmOfTheRisingTide module
    //[SPELLS.RISING_TIDES.id]: {
    //  itemId: ITEMS.CHARM_OF_THE_RISING_TIDE.id,
    //  haste: (_, item) => calculateSecondaryStatDefault(900, 576, item.itemLevel),
    //},
    [SPELLS.ACCELERANDO.id]: {
      itemId: ITEMS.ERRATIC_METRONOME.id,
      haste: (_, item) => calculateSecondaryStatDefault(870, 657, item.itemLevel),
    },
    // endregion

    // region Misc
    [SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_STRENGTH.id]: { // check numbers
      strength: combatant => 4000 + (combatant.traitsBySpellId[SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_TRAIT.id] - 1) * 300,
    },
    [SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_AGILITY.id]: { // check numbers
      agility: combatant => 4000 + (combatant.traitsBySpellId[SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_TRAIT.id] - 1) * 300,
    },
    [SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_INTELLECT.id]: { // check numbers
      intellect: combatant => 4000 + (combatant.traitsBySpellId[SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_TRAIT.id] - 1) * 300,
    },
    [SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_VERSATILITY.id]: { // check numbers
      versatility: combatant => 1500 + 100 * (combatant.traitsBySpellId[SPELLS.CONCORDANCE_OF_THE_LEGIONFALL_TRAIT.id] - 1) * 300,
    },
    [SPELLS.JACINS_RUSE.id]: { mastery: 3000 },
    [SPELLS.MARK_OF_THE_CLAW.id]: { crit: 1000, haste: 1000 },
    // endregion

    // region Druid
    [SPELLS.ASTRAL_HARMONY.id]: { mastery: 4000 },
    // endregion

    // region Mage
    [SPELLS.WARMTH_OF_THE_PHOENIX.id]: { crit: 800 },
    // endregion

    // region Priest
    [SPELLS.MIND_QUICKENING.id]: { haste: 800 },
    // endregion
  };

  _pullStats = {};
  _currentStats = {};

  on_initialized() {
    this._pullStats = {
      strength: this.combatants.selected.strength,
      agility: this.combatants.selected.agility,
      intellect: this.combatants.selected.intellect,
      stamina: this.combatants.selected.stamina,
      crit: this.combatants.selected.critRating,
      haste: this.combatants.selected.hasteRating,
      mastery: this.combatants.selected.masteryRating,
      versatility: this.combatants.selected.versatilityRating,
      avoidance: this.combatants.selected.avoidanceRating,
      leech: this.combatants.selected.leechRating,
      speed: this.combatants.selected.speedRating,
    };
    this._currentStats = this._pullStats;

    this._debugPrintStats(this._currentStats);
  }

  /*
   * Stat rating at pull.
   * Should be identical to what you get from Combatant.
   */
  get startingStrengthRating() {
    return this._pullStats.strength;
  }
  get startingAgilityRating() {
    return this._pullStats.agility;
  }
  get startingIntellectRating() {
    return this._pullStats.intellect;
  }
  get startingStaminaRating() {
    return this._pullStats.stamina;
  }
  get startingCritRating() {
    return this._pullStats.crit;
  }
  get startingHasteRating() {
    return this._pullStats.haste;
  }
  get startingMasteryRating() {
    return this._pullStats.mastery;
  }
  get startingVersatilityRating() {
    return this._pullStats.versatility;
  }
  get startingAvoidanceRating() {
    return this._pullStats.avoidance;
  }
  get startingLeechRating() {
    return this._pullStats.leech;
  }
  get startingSpeedRating() {
    return this._pullStats.speed;
  }

  /*
   * Current stat rating, as tracked by this module.
   */
  get currentStrengthRating() {
    return this._currentStats.strength;
  }
  get currentAgilityRating() {
    return this._currentStats.agility;
  }
  get currentIntellectRating() {
    return this._currentStats.intellect;
  }
  get currentStaminaRating() {
    return this._currentStats.stamina;
  }
  get currentCritRating() {
    return this._currentStats.crit;
  }
  get currentHasteRating() {
    return this._currentStats.haste;
  }
  get currentMasteryRating() {
    return this._currentStats.mastery;
  }
  get currentVersatilityRating() {
    return this._currentStats.versatility;
  }
  get currentAvoidanceRating() {
    return this._currentStats.avoidance;
  }
  get currentLeechRating() {
    return this._currentStats.leech;
  }
  get currentSpeedRating() {
    return this._currentStats.speed;
  }

  /*
   * For percentage stats, the percentage you'd have with zero rating.
   * These values don't change.
   */
  get baseCritPercentage() {
    return 0.08; // TODO is this the same for all classes?
  }
  get baseHastePercentage() {
    return 0;
  }
  get baseMasteryPercentage() {
    switch (this.combatants.selected.spec) {
      case SPECS.HOLY_PALADIN:
        return 0.12;
      case SPECS.HOLY_PRIEST:
        return 0.05;
      case SPECS.RESTORATION_SHAMAN:
        return 0.24;
      case SPECS.ENHANCEMENT_SHAMAN:
        return 0.2;
      case SPECS.RESTORATION_DRUID:
        return 0.048;
      default:
        throw new Error('Mastery hasn\'t been implemented for this spec yet.');
    }
  }
  get baseVersatilityPercentage() {
    return 0;
  }
  get baseAvoidancePercentage() {
    return 0;
  }
  get baseLeechPercentage() {
    return 0;
  }
  get baseSpeedPercentage() {
    return 0;
  }

  /*
   * For percentage stats, this is the divider to go from rating to percent (expressed from 0 to 1)
   * These values don't change.
   */
  get critRatingPerPercent() {
    return 40000;
  }
  get hasteRatingPerPercent() {
    return 37500;
  }
  get masteryRatingPerPercent() {
    switch (this.combatants.selected.spec) {
      case SPECS.HOLY_PALADIN:
        return 26667;
      case SPECS.HOLY_PRIEST:
        return 32000;
      case SPECS.RESTORATION_SHAMAN:
        return 13333;
      case SPECS.ENHANCEMENT_SHAMAN:
        return 13333;
      case SPECS.RESTORATION_DRUID:
        return 66667;
      default:
        throw new Error('Mastery hasn\'t been implemented for this spec yet.');
    }
  }
  get versatilityRatingPerPercent() {
    return 47500;
  }
  get avoidanceRatingPerPercent() {
    return 11000;
  }
  get leechRatingPerPercent() {
    return 23000;
  }
  get speedRatingPerPercent() {
    throw new Error('Speed hasn\'t been implemented yet.');
  }

  /*
   * For percentage stats, the current stat percentage as tracked by this module.
   */
  get currentCritPercentage() {
    return this.baseCritPercentage + (this.currentCritRating / this.critRatingPerPercent);
  }
  get currentHastePercentage() {
    return this.baseHastePercentage + (this.currentHasteRating / this.hasteRatingPerPercent);
  }
  get currentMasteryPercentage() {
    return this.baseMasteryPercentage + (this.currentMasteryRating / this.masteryRatingPerPercent);
  }
  get currentVersatilityPercentage() {
    return this.baseVersatilityPercentage + (this.currentVersatilityRating / this.versatilityRatingPerPercent);
  }
  get currentAvoidancePercentage() {
    return this.baseAvoidancePercentage + (this.currentAvoidanceRating / this.avoidanceRatingPerPercent);
  }
  get currentLeechPercentage() {
    return this.baseLeechPercentage + (this.currentLeechRating / this.leechRatingPerPercent);
  }
  get currentSpeedPercentage() {
    return this.baseSpeedPercentage + (this.currentSpeedRating / this.speedRatingPerPercent);
  }

  on_toPlayer_changebuffstack(event) {
    this._changeBuffStack(event);
  }

  on_toPlayer_changedebuffstack(event) {
    this._changeBuffStack(event);
  }

  /*
   * This interface allows an external analyzer to force a stat change.
   * It should ONLY be used if a stat buff is so non-standard that it can't be handled by the buff format in this module.
   * change is a stat buff object just like those in the STAT_BUFFS structure above, it is required.
   * eventReason is the WCL event object that caused this change, it is not required.
   */
   // For an example of how / why this function would be used, see the CharmOfTheRisingTide module.
  forceChangeStats(change, eventReason) {
    const before = Object.assign({}, this._currentStats);
    const delta = this._changeStats(change, 1);
    const after = Object.assign({}, this._currentStats);
    this._triggerChangeStats(eventReason, before, delta, after);
    if(debug) {
      const spellName = eventReason && eventReason.ability ? eventReason.ability.name : 'unspecified';
      console.log(`StatTracker: FORCED CHANGE from ${spellName} - Change: ${this._statPrint(delta)}`);
      this._debugPrintStats(this._currentStats);
    }
  }

  _changeBuffStack(event) {
    const spellId = event.ability.guid;
    const statBuff = this.constructor.STAT_BUFFS[spellId];
    if (statBuff) {
      // ignore prepull buff application, as they're already accounted for in combatantinfo
      // we have to check the stacks count because Entities incorrectly copies the prepull property onto changes and removal following the application
      if (event.oldStacks === 0 && event.prepull) {
        debug && console.log(`StatTracker prepull application IGNORED for ${SPELLS[spellId] ? SPELLS[spellId].name : spellId}`);
        return;
      }

      const before = Object.assign({}, this._currentStats);
      const delta = this._changeStats(statBuff, event.newStacks - event.oldStacks);
      const after = Object.assign({}, this._currentStats);
      this._triggerChangeStats(event, before, delta, after);
      debug && console.log(`StatTracker: (${event.oldStacks} -> ${event.newStacks}) ${SPELLS[spellId] ? SPELLS[spellId].name : spellId} @ ${formatMilliseconds(this.owner.currentTimestamp)} - Change: ${this._statPrint(delta)}`);
      debug && this._debugPrintStats(this._currentStats);
    }
  }

  _changeStats(change, factor) {
    const delta = {
      strength: this._getBuffValue(change, change.strength) * factor,
      agility: this._getBuffValue(change, change.agility) * factor,
      intellect: this._getBuffValue(change, change.intellect) * factor,
      stamina: this._getBuffValue(change, change.stamina) * factor,
      crit: this._getBuffValue(change, change.crit) * factor,
      haste: this._getBuffValue(change, change.haste) * factor,
      mastery: this._getBuffValue(change, change.mastery) * factor,
      versatility: this._getBuffValue(change, change.versatility) * factor,
      avoidance: this._getBuffValue(change, change.avoidance) * factor,
      leech: this._getBuffValue(change, change.leech) * factor,
      speed: this._getBuffValue(change, change.speed) * factor,
    };

    Object.keys(this._currentStats).forEach(key => {
      this._currentStats[key] += delta[key];
    });

    return delta;
  }

  /*
   * Fabricates an event indicating when stats change
   */
  _triggerChangeStats(event, before, delta, after) {
    this.owner.triggerEvent('changestats', {
      timestamp: event ? event.timestamp : this.owner.currentTimestamp,
      type: 'changestats',
      sourceID: event ? event.sourceID : this.owner.playerId,
      targetID: this.owner.playerId,
      reason: event,
      before,
      delta,
      after,
    });
  }

  /**
   * Gets the actual stat value in whatever format it is.
   * a number value will be returned as is
   * a function value will be called with (selectedCombatant, itemDetails) and the result returned
   * an undefined stat will default to 0.
   */
  _getBuffValue(buffObj, statVal) {
    if (statVal === undefined) {
      return 0;
    } else if (typeof statVal === 'function') {
      const selectedCombatant = this.combatants.selected;
      let itemDetails;
      if (buffObj.itemId) {
        itemDetails = this.combatants.selected.getItem(buffObj.itemId);
        if (!itemDetails) {
          console.warn('Failed to retrieve item information for item with ID:', buffObj.itemId,
            ' ...unable to handle stats buff, making no stat change.');
          return 0;
        }
      }
      return statVal(selectedCombatant, itemDetails);
    } else {
      return statVal;
    }
  }

  _debugPrintStats(stats) {
    console.log(`StatTracker: ${formatMilliseconds(this.owner.fightDuration)} - ${this._statPrint(stats)}`);
  }

  _statPrint(stats) {
    return `STR=${stats.strength} AGI=${stats.agility} INT=${stats.intellect} STM=${stats.stamina} CRT=${stats.crit} HST=${stats.haste} MST=${stats.mastery} VRS=${stats.versatility} AVD=${this._currentStats.avoidance} LCH=${stats.leech} SPD=${stats.speed}`;
  }

}

export default StatTracker;
<|endoftext|>


<commit_before>/*
* Copyright (c) 2017 Ken Hibino.
* Licensed under the MIT License (MIT).
* See https://kenny-hibino.github.io/react-places-autocomplete
*/

import React from 'react'
import defaultStyles from './defaultStyles'

class PlacesAutocompleteWithTypeAhead extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      autocompleteItems: [],
      firstSuggestion: '',
      userTypedValue: '',
      shouldTypeAhead: true,
    }

    this.autocompleteCallback = this.autocompleteCallback.bind(this)
    this.handleInputKeyDown = this.handleInputKeyDown.bind(this)
    this.handleInputChange = this.handleInputChange.bind(this)
  }

  componentDidMount() {
    this.autocompleteService = new google.maps.places.AutocompleteService()
    this.autocompleteOK = google.maps.places.PlacesServiceStatus.OK
    this.autocompleteZeroResult = google.maps.places.PlacesServiceStatus.ZERO_RESULTS
  }

  componentDidUpdate(prevProps, prevState) {
    const { firstSuggestion, userTypedValue } = this.state

    if (userTypedValue.length < prevState.userTypedValue.length) {
      return // noop
    }

    if (this._shouldSoftAutcomplete()) {
      this.refs.inputField.setSelectionRange(userTypedValue.length, firstSuggestion.length)
    }
  }

  autocompleteCallback(predictions, status) {
    if (status === this.autocompleteZeroResult) {
      this.setState({
        autocompleteItems: [],
        firstSuggestion: '',
      })
      return
    }

    if (status != this.autocompleteOK) {
      this.props.onError(status)
      if (this.props.clearItemsOnError) { this.clearAutocomplete() }
      return
    }

    this.setState({
      autocompleteItems: predictions.map((p, idx) => ({
        suggestion: p.description,
        placeId: p.place_id,
        active: false,
        index: idx,
        formattedSuggestion: this._formattedSuggestion(p.structured_formatting),
      })),
      firstSuggestion: predictions[0].description,
    })

    this._updateInputValue()
  }

  _formattedSuggestion(structured_formatting) {
    return { mainText: structured_formatting.main_text, secondaryText: structured_formatting.secondary_text }
  }

  clearAutocomplete() {
    this.setState({ autocompleteItems: [] })
  }

  selectAddress(address, placeId) {
    this.clearAutocomplete()
    this._handleSelect(address, placeId)
  }

  _handleSelect(address, placeId) {
    this.props.onSelect ? this.props.onSelect(address, placeId) : this.props.onChange(address)
  }

  handleAutocompleteItemMouseDown(address, placeId) {
    this.selectAddress(address, placeId)
    this.setState({
      userTypedValue: address,
    })
  }

  _getActiveItem() {
    return this.state.autocompleteItems.find(item => item.active)
  }

  _selectActiveItemAtIndex(index) {
    const activeName = this.state.autocompleteItems.find(item => item.index === index).suggestion
    this._setActiveItemAtIndex(index)
    this.props.onChange(activeName)
    this.setState({ userTypedValue: activeName })
  }

  _handleEnterKey() {
    const activeItem = this._getActiveItem()
    if (activeItem === undefined) {
      this._handleEnterKeyWithoutActiveItem()
    } else {
      this.selectAddress(activeItem.suggestion, activeItem.placeId)
    }

    this.refs.inputField.focus()
    this.refs.inputField.setSelectionRange(0,0)
    this.setState({
      userTypedValue: this.props.value
    })
  }

  _handleEnterKeyWithoutActiveItem() {
    if (this.props.onEnterKeyDown) {
      this.props.onEnterKeyDown(this.props.value)
      this.clearAutocomplete()
    } else {
      return //noop
    }
  }

  _handleDownKey() {
    const activeItem = this._getActiveItem()
    if (activeItem === undefined) {
      this._selectActiveItemAtIndex(0)
    } else {
      const nextIndex = (activeItem.index + 1) % this.state.autocompleteItems.length
      this._selectActiveItemAtIndex(nextIndex)
    }
  }

  _handleUpKey() {
    const activeItem = this._getActiveItem()
    if (activeItem === undefined) {
      this._selectActiveItemAtIndex(this.state.autocompleteItems.length - 1)
    } else {
      let prevIndex
      if (activeItem.index === 0) {
        prevIndex = this.state.autocompleteItems.length - 1
      } else {
        prevIndex = (activeItem.index - 1) % this.state.autocompleteItems.length
      }
      this._selectActiveItemAtIndex(prevIndex)
    }
  }

  _handleDeleteKey() {
    const { userTypedValue, firstSuggestion } = this.state
    if (userTypedValue.length === 0) {
      return // noop
    }
    const selectionString = window.getSelection().toString()
    const cursorPosition = this.refs.inputField.selectionStart
    const isPlainBackSpace =  (
      selectionString === firstSuggestion.replace(userTypedValue, '') ||
      selectionString.length === 0
    )

    if (isPlainBackSpace) {
      this.setState({
        userTypedValue: userTypedValue.slice(0, cursorPosition - 1) + userTypedValue.slice(cursorPosition, userTypedValue.length),
        shouldTypeAhead: false,
        firstSuggestion: '',
      })
    } else {
      this.setState({
        userTypedValue: firstSuggestion.replace(selectionString, ''),
        shouldTypeAhead: false,
        firstSuggestion: '',
      })
    }
  }

  _handleTabKey() {
    this.refs.inputField.focus()
    this.refs.inputField.setSelectionRange(0,0)
    this.refs.inputField.blur()
    this.setState({
      userTypedValue: this.props.value
    })
  }

  handleInputKeyDown(event) {
    const onlyShiftKeyDown = (event.shiftKey && event.keyCode === 16)
    const onlyAltKeyDown = (event.altKey && event.keyCode === 18)
    if ( onlyShiftKeyDown || onlyAltKeyDown || event.ctrlKey || event.metaKey) {
      return // noop
    }

    const ARROW_UP = 38
    const ARROW_DOWN = 40
    const ENTER_KEY = 13
    const ESC_KEY = 27
    const DELETE_KEY = 8
    const TAB_KEY = 9

    switch (event.keyCode) {
      case ENTER_KEY:
        event.preventDefault()
        this._handleEnterKey()
        break
      case ARROW_DOWN:
        this._handleDownKey()
        break
      case ARROW_UP:
        this._handleUpKey()
        break
      case ESC_KEY:
        this.clearAutocomplete()
        break
      case DELETE_KEY:
        this._handleDeleteKey()
        break
      case TAB_KEY:
        this._handleTabKey()
        break;
      default:
        this.setState({
          userTypedValue: this._fixCasing(this.state.userTypedValue + event.key),
          shouldTypeAhead: true,
        })
    }
  }

  _fixCasing(newValue) {
    const { firstSuggestion} = this.state
    if (firstSuggestion.length === 0) {
      return newValue
    }

    if (this._isMatch(newValue, firstSuggestion)) {
      return firstSuggestion.substr(0, newValue.length)
    }

    return newValue
  }

  _setActiveItemAtIndex(index) {
    this.setState({
      autocompleteItems: this.state.autocompleteItems.map((item, idx) => {
        if (idx === index) {
          return { ...item, active: true }
        } else {
          return { ...item, active: false }
        }
      }),
    })
  }

  _updateInputValue() {
    const { firstSuggestion, userTypedValue } = this.state
    if (this._shouldSoftAutcomplete()) {
      this.props.onChange(firstSuggestion)
    } else {
      this.props.onChange(userTypedValue)
    }
  }

  _shouldSoftAutcomplete() {
    const { firstSuggestion, userTypedValue, shouldTypeAhead } = this.state
    return (
      (firstSuggestion !== '' && userTypedValue !== '') &&
      this._isMatch(userTypedValue, firstSuggestion) &&
      shouldTypeAhead
    )
  }

  handleInputChange(event) {
    this._updateInputValue()

    const { userTypedValue } = this.state
    if (userTypedValue.length === 0) {
      this.clearAutocomplete()
      return
    }

    if (this.state.shouldTypeAhead) {
      this.autocompleteService.getPlacePredictions({ ...this.props.options, input: userTypedValue }, this.autocompleteCallback)
    }
  }

  autocompleteItemStyle(active) {
    if (active) {
      return { ...defaultStyles.autocompleteItemActive, ...this.props.styles.autocompleteItemActive }
    } else {
      return {}
    }
  }

  renderAutocomplete() {
    const { autocompleteItems } = this.state
    const { styles } = this.props
    if (autocompleteItems.length === 0) { return null }
    return (
      <div
        id="PlacesAutocomplete__autocomplete-container"
        className={this.props.classNames.autocompleteContainer || ''}
        style={{ ...defaultStyles.autocompleteContainer, ...styles.autocompleteContainer }}>
        {autocompleteItems.map((p, idx) => (
          <div
            key={p.placeId}
            onMouseOver={() => this._setActiveItemAtIndex(p.index)}
            onMouseDown={() => this.handleAutocompleteItemMouseDown(p.suggestion, p.placeId)}
            style={{ ...defaultStyles.autocompleteItem, ...styles.autocompleteItem, ...this.autocompleteItemStyle(p.active) }}>
            {this.props.autocompleteItem({ suggestion: p.suggestion, formattedSuggestion: p.formattedSuggestion })}
          </div>
        ))}
      </div>
    )
  }

  _isMatch(subject, target) {
    const normalizedSubject = subject.toLowerCase()
    const normalizedTarget = target.toLowerCase()
    return normalizedSubject === normalizedTarget.substr(0, subject.length)
  }

  renderInput() {
    const { firstSuggestion, userTypedValue } = this.state
    const { classNames, placeholder, styles, value, autoFocus, inputName } = this.props
    return (
      <input
        type="text"
        placeholder={placeholder}
        className={classNames.input || ''}
        value={value}
        onChange={this.handleInputChange}
        onKeyDown={this.handleInputKeyDown}
        onBlur={() => this.clearAutocomplete()}
        style={styles.input}
        autoFocus={autoFocus}
        name={inputName || ''}
        ref="inputField"
      />
    )
  }

  render() {
    const { classNames, styles } = this.props
    return (
      <div
        style={{ ...defaultStyles.root, ...styles.root }}
        className={classNames.root || ''}>
        {this.renderInput()}
        {this.renderAutocomplete()}
      </div>
    )
  }
}

export default PlacesAutocompleteWithTypeAhead
<commit_msg>fix(typeAhead): Deal with Selection on Keydown event<commit_after>/*
* Copyright (c) 2017 Ken Hibino.
* Licensed under the MIT License (MIT).
* See https://kenny-hibino.github.io/react-places-autocomplete
*/

import React from 'react'
import defaultStyles from './defaultStyles'

class PlacesAutocompleteWithTypeAhead extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      autocompleteItems: [],
      firstSuggestion: '',
      userTypedValue: '',
      shouldTypeAhead: true,
    }

    this.autocompleteCallback = this.autocompleteCallback.bind(this)
    this.handleInputKeyDown = this.handleInputKeyDown.bind(this)
    this.handleInputChange = this.handleInputChange.bind(this)
  }

  componentDidMount() {
    this.autocompleteService = new google.maps.places.AutocompleteService()
    this.autocompleteOK = google.maps.places.PlacesServiceStatus.OK
    this.autocompleteZeroResult = google.maps.places.PlacesServiceStatus.ZERO_RESULTS
  }

  componentDidUpdate(prevProps, prevState) {
    const { firstSuggestion, userTypedValue } = this.state

    if (userTypedValue.length < prevState.userTypedValue.length) {
      return // noop
    }

    if (this._shouldSoftAutcomplete()) {
      this.refs.inputField.setSelectionRange(userTypedValue.length, firstSuggestion.length)
    }
  }

  autocompleteCallback(predictions, status) {
    if (status === this.autocompleteZeroResult) {
      this.setState({
        autocompleteItems: [],
        firstSuggestion: '',
      })
      return
    }

    if (status != this.autocompleteOK) {
      this.props.onError(status)
      if (this.props.clearItemsOnError) { this.clearAutocomplete() }
      return
    }

    this.setState({
      autocompleteItems: predictions.map((p, idx) => ({
        suggestion: p.description,
        placeId: p.place_id,
        active: false,
        index: idx,
        formattedSuggestion: this._formattedSuggestion(p.structured_formatting),
      })),
      firstSuggestion: predictions[0].description,
    })

    this._updateInputValue()
  }

  _formattedSuggestion(structured_formatting) {
    return { mainText: structured_formatting.main_text, secondaryText: structured_formatting.secondary_text }
  }

  clearAutocomplete() {
    this.setState({ autocompleteItems: [] })
  }

  selectAddress(address, placeId) {
    this.clearAutocomplete()
    this._handleSelect(address, placeId)
  }

  _handleSelect(address, placeId) {
    this.props.onSelect ? this.props.onSelect(address, placeId) : this.props.onChange(address)
  }

  handleAutocompleteItemMouseDown(address, placeId) {
    this.selectAddress(address, placeId)
    this.setState({
      userTypedValue: address,
    })
  }

  _getActiveItem() {
    return this.state.autocompleteItems.find(item => item.active)
  }

  _selectActiveItemAtIndex(index) {
    const activeName = this.state.autocompleteItems.find(item => item.index === index).suggestion
    this._setActiveItemAtIndex(index)
    this.props.onChange(activeName)
    this.setState({ userTypedValue: activeName })
  }

  _handleEnterKey() {
    const activeItem = this._getActiveItem()
    if (activeItem === undefined) {
      this._handleEnterKeyWithoutActiveItem()
    } else {
      this.selectAddress(activeItem.suggestion, activeItem.placeId)
    }

    this.refs.inputField.focus()
    this.refs.inputField.setSelectionRange(0,0)
    this.setState({
      userTypedValue: this.props.value
    })
  }

  _handleEnterKeyWithoutActiveItem() {
    if (this.props.onEnterKeyDown) {
      this.props.onEnterKeyDown(this.props.value)
      this.clearAutocomplete()
    } else {
      return //noop
    }
  }

  _handleDownKey() {
    const activeItem = this._getActiveItem()
    if (activeItem === undefined) {
      this._selectActiveItemAtIndex(0)
    } else {
      const nextIndex = (activeItem.index + 1) % this.state.autocompleteItems.length
      this._selectActiveItemAtIndex(nextIndex)
    }
  }

  _handleUpKey() {
    const activeItem = this._getActiveItem()
    if (activeItem === undefined) {
      this._selectActiveItemAtIndex(this.state.autocompleteItems.length - 1)
    } else {
      let prevIndex
      if (activeItem.index === 0) {
        prevIndex = this.state.autocompleteItems.length - 1
      } else {
        prevIndex = (activeItem.index - 1) % this.state.autocompleteItems.length
      }
      this._selectActiveItemAtIndex(prevIndex)
    }
  }

  _handleDeleteKey() {
    const { userTypedValue, firstSuggestion } = this.state
    if (userTypedValue.length === 0) {
      return // noop
    }
    const selectionString = window.getSelection().toString()
    const cursorPosition = this.refs.inputField.selectionStart
    const isPlainBackSpace =  (
      selectionString === firstSuggestion.replace(userTypedValue, '') ||
      selectionString.length === 0
    )

    if (isPlainBackSpace) {
      this.setState({
        userTypedValue: userTypedValue.slice(0, cursorPosition - 1) + userTypedValue.slice(cursorPosition, userTypedValue.length),
        shouldTypeAhead: false,
        firstSuggestion: '',
      })
    } else {
      this.setState({
        userTypedValue: firstSuggestion.replace(selectionString, ''),
        shouldTypeAhead: false,
        firstSuggestion: '',
      })
    }
  }

  _handleTabKey() {
    this.refs.inputField.focus()
    this.refs.inputField.setSelectionRange(0,0)
    this.refs.inputField.blur()
    this.setState({
      userTypedValue: this.props.value
    })
  }

  _handleRightLeftKey() {
    this.setState({
      userTypedValue: this.props.value,
      shouldTypeAhead: false,
    })
  }

  _handleDefaultKey(event) {
    if (event.key.length > 1) { return }
    const { userTypedValue } = this.state
    const selectionString = window.getSelection().toString()
    if (this.props.value === `${userTypedValue}${selectionString}`) {
      this.setState({
        userTypedValue: this._fixCasing(this.state.userTypedValue + event.key),
        shouldTypeAhead: true,
      })
    } else {
      this.setState({
        userTypedValue: this.props.value.replace(selectionString, event.key),
        shouldTypeAhead: false,
      })
    }
  }

  handleInputKeyDown(event) {
    const onlyShiftKeyDown = (event.shiftKey && event.keyCode === 16)
    const onlyAltKeyDown = (event.altKey && event.keyCode === 18)
    if ( onlyShiftKeyDown || onlyAltKeyDown || event.ctrlKey || event.metaKey) {
      return // noop
    }

    const ARROW_LEFT = 37
    const ARROW_UP = 38
    const ARROW_RIGHT = 39
    const ARROW_DOWN = 40
    const ENTER_KEY = 13
    const ESC_KEY = 27
    const DELETE_KEY = 8
    const TAB_KEY = 9

    switch (event.keyCode) {
      case ENTER_KEY:
        event.preventDefault()
        this._handleEnterKey()
        break
      case ARROW_DOWN:
        this._handleDownKey()
        break
      case ARROW_UP:
        this._handleUpKey()
        break
      case ESC_KEY:
        this.clearAutocomplete()
        break
      case DELETE_KEY:
        this._handleDeleteKey()
        break
      case TAB_KEY:
        this._handleTabKey()
        break;
      case ARROW_LEFT:
      case ARROW_RIGHT:
        this._handleRightLeftKey()
        break
      default:
        this._handleDefaultKey(event)
    }
  }

  _fixCasing(newValue) {
    const { firstSuggestion} = this.state
    if (firstSuggestion.length === 0) {
      return newValue
    }

    if (this._isMatch(newValue, firstSuggestion)) {
      return firstSuggestion.substr(0, newValue.length)
    }

    return newValue
  }

  _setActiveItemAtIndex(index) {
    this.setState({
      autocompleteItems: this.state.autocompleteItems.map((item, idx) => {
        if (idx === index) {
          return { ...item, active: true }
        } else {
          return { ...item, active: false }
        }
      }),
    })
  }

  _updateInputValue() {
    const { firstSuggestion, userTypedValue } = this.state
    if (this._shouldSoftAutcomplete()) {
      this.props.onChange(firstSuggestion)
    } else {
      this.props.onChange(userTypedValue)
    }
  }

  _shouldSoftAutcomplete() {
    const { firstSuggestion, userTypedValue, shouldTypeAhead } = this.state
    return (
      (firstSuggestion !== '' && userTypedValue !== '') &&
      this._isMatch(userTypedValue, firstSuggestion) &&
      shouldTypeAhead
    )
  }

  handleInputChange(event) {
    this._updateInputValue()

    const { userTypedValue } = this.state
    if (userTypedValue.length === 0) {
      this.clearAutocomplete()
      return
    }

    if (this.state.shouldTypeAhead) {
      this.autocompleteService.getPlacePredictions({ ...this.props.options, input: userTypedValue }, this.autocompleteCallback)
    }
  }

  autocompleteItemStyle(active) {
    if (active) {
      return { ...defaultStyles.autocompleteItemActive, ...this.props.styles.autocompleteItemActive }
    } else {
      return {}
    }
  }

  renderAutocomplete() {
    const { autocompleteItems } = this.state
    const { styles } = this.props
    if (autocompleteItems.length === 0) { return null }
    return (
      <div
        id="PlacesAutocomplete__autocomplete-container"
        className={this.props.classNames.autocompleteContainer || ''}
        style={{ ...defaultStyles.autocompleteContainer, ...styles.autocompleteContainer }}>
        {autocompleteItems.map((p, idx) => (
          <div
            key={p.placeId}
            onMouseOver={() => this._setActiveItemAtIndex(p.index)}
            onMouseDown={() => this.handleAutocompleteItemMouseDown(p.suggestion, p.placeId)}
            style={{ ...defaultStyles.autocompleteItem, ...styles.autocompleteItem, ...this.autocompleteItemStyle(p.active) }}>
            {this.props.autocompleteItem({ suggestion: p.suggestion, formattedSuggestion: p.formattedSuggestion })}
          </div>
        ))}
      </div>
    )
  }

  _isMatch(subject, target) {
    const normalizedSubject = subject.toLowerCase()
    const normalizedTarget = target.toLowerCase()
    return normalizedSubject === normalizedTarget.substr(0, subject.length)
  }

  renderInput() {
    const { firstSuggestion, userTypedValue } = this.state
    const { classNames, placeholder, styles, value, autoFocus, inputName } = this.props
    return (
      <input
        type="text"
        placeholder={placeholder}
        className={classNames.input || ''}
        value={value}
        onChange={this.handleInputChange}
        onKeyDown={this.handleInputKeyDown}
        onBlur={() => this.clearAutocomplete()}
        style={styles.input}
        autoFocus={autoFocus}
        name={inputName || ''}
        ref="inputField"
      />
    )
  }

  render() {
    const { classNames, styles } = this.props
    return (
      <div
        style={{ ...defaultStyles.root, ...styles.root }}
        className={classNames.root || ''}>
        {this.renderInput()}
        {this.renderAutocomplete()}
      </div>
    )
  }
}

export default PlacesAutocompleteWithTypeAhead
<|endoftext|>


<commit_before><commit_msg>[FIX[ web_tour: simplify docstring<commit_after><|endoftext|>


<commit_before><commit_msg>Reset total area<commit_after><|endoftext|>


<commit_before><commit_msg>jshint<commit_after><|endoftext|>


<commit_before><commit_msg>split code into multiple lines<commit_after><|endoftext|>


<commit_before><commit_msg>rm console.log from general_transform.js<commit_after><|endoftext|>