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

    function WavesWalletController($scope, $timeout, $interval, constants, autocomplete, applicationContext,
                              dialogService, addressService, utilityService, apiService, notificationService,
                              formattingService, transferService, transactionLoadingService) {
        var wallet = this;
        var transaction, refreshPromise;
        var refreshDelay = 10 * 1000;
        var minimumPayment = new Money(constants.MINIMUM_PAYMENT_AMOUNT, Currency.WAV);
        var minimumFee = new Money(constants.MINIMUM_TRANSACTION_FEE, Currency.WAV);

        function unimplementedFeature() {
            $scope.home.featureUnderDevelopment();
        }

        function findWalletByCurrency(currency) {
            return _.find(wallet.wallets, function (w) {
                return w.balance.currency === currency;
            });
        }

        wallet.wallets = [
            {
                balance: new Money(0, Currency.USD)
            },
            {
                balance: new Money(0, Currency.EUR)
            },
            {
                balance: new Money(0, Currency.BTC)
            },
            {
                balance: new Money(0, Currency.WAV)
            },
            {
                balance: new Money(0, Currency.CNY)
            }
        ];
        wallet.current = wallet.wallets[0];
        wallet.transactions = [];
        wallet.confirm = {
            amount: {
                value: '0',
                currency: ''
            },
            fee: {
                value: '0',
                currency: ''
            },
            recipient: '',
            paymentPending: false
        };
        wallet.transfer = autocomplete;
        wallet.paymentValidationOptions = {
            rules: {
                wavesrecipient: {
                    required: true,
                    address: true
                },
                wavessendamount: {
                    required: true,
                    decimal: Currency.WAV.precision,
                    min: minimumPayment.amount
                },
                wavessendfee: {
                    required: true,
                    decimal: Currency.WAV.precision,
                    min: minimumFee.amount
                }
            },
            messages: {
                wavesrecipient: {
                    required: 'Recipient account number is required'
                },
                wavessendamount: {
                    required: 'Amount to send is required',
                    decimal: 'The amount to send must be a number with no more than ' +
                        minimumPayment.currency.precision + ' digits after the decimal point (.)',
                    min: 'Payment amount is too small. It should be greater or equal to ' +
                        minimumPayment.formatAmount(false)
                },
                wavessendfee: {
                    required: 'Transaction fee is required',
                    decimal: 'Transaction fee must be with no more than ' +
                        minimumFee.currency.precision + ' digits after the decimal point (.)',
                    min: 'Transaction fee is too small. It should be greater or equal to ' +
                        minimumFee.formatAmount(true)
                }
            }
        };
        wallet.send = send;
        wallet.withdraw = withdraw;
        wallet.trade = trade;
        wallet.submitPayment = submitPayment;
        wallet.broadcastSendTransaction = broadcastSendTransaction;

        resetPaymentForm();
        loadDataFromBackend();

        $scope.$on('$destroy', function () {
            if (angular.isDefined(refreshPromise)) {
                $interval.cancel(refreshPromise);
                refreshPromise = undefined;
            }
        });

        function send (currency) {
            switch (currency) {
                case Currency.WAV:
                    dialogService.open('#wB-butSend-WAV');
                    break;

                default:
                    unimplementedFeature();
            }

            wallet.current = findWalletByCurrency(currency);
        }

        function withdraw (currency) {
            unimplementedFeature();
        }

        function trade (currency) {
            unimplementedFeature();
        }

        function getPaymentForm() {
            // here we have a direct markup dependency
            // but other ways of getting the form from a child scope are even more ugly
            return angular.element('#send-waves-form').scope().sendWavesForm;
        }

        function submitPayment() {
            var paymentForm = getPaymentForm();
            var invalid = paymentForm.invalid();
            wallet.transfer.fee.isValid = angular.isDefined(invalid.wavessendfee) ?
                !invalid.wavessendfee : true;
            if (!paymentForm.validate())
                // prevent payment dialog from closing if it's not valid
                return false;

            wallet.transfer.fee.amount = wallet.transfer.getFeeAmount();

            var currentCurrency = wallet.current.balance.currency;
            var payment = {
                amount: new Money(wallet.transfer.amount, currentCurrency),
                fee: new Money(wallet.transfer.fee.amount, currentCurrency),
                recipient: addressService.cleanupOptionalPrefix(wallet.transfer.recipient),
                time: utilityService.getTime()
            };

            var sender = {
                address: applicationContext.account.address,
                publicKey: applicationContext.account.keyPair.public,
                privateKey: applicationContext.account.keyPair.private
            };

            // creating the transaction and waiting for confirmation
            transaction = transferService.createTransaction(payment, sender);

            // setting data for the confirmation dialog
            wallet.confirm.amount.value = payment.amount.formatAmount(true);
            wallet.confirm.amount.currency = payment.amount.currency.displayName;
            wallet.confirm.fee.value = payment.fee.formatAmount(true);
            wallet.confirm.fee.currency = payment.fee.currency.displayName;
            wallet.confirm.recipient = payment.recipient;

            // open confirmation dialog
            // doing it async because this method is called while another dialog is open
            $timeout(function () {
                dialogService.open('#send-payment-confirmation');
            }, 1);

            resetPaymentForm();

            // it's ok to close payment dialog
            return true;
        }

        function broadcastSendTransaction() {
            // checking if transaction was saved
            if (angular.isUndefined(transaction))
                return;

            // prevent method execution when there is a pending payment request
            if (wallet.confirm.paymentPending)
                return;

            //disable confirm button
            wallet.confirm.paymentPending = true;

            apiService.broadcastPayment(transaction).then(function () {
                var amount = Money.fromCoins(transaction.amount, wallet.current.balance.currency);
                var address = transaction.recipient;
                var displayMessage = 'Sent ' + amount.formatAmount(true) + amount.currency.symbol +
                    '
Recipient ' + address.substr(0,15) + '...
Date: ' +
                    formattingService.formatTimestamp(transaction.timestamp);
                notificationService.notice(displayMessage);
                //enable confirm button
                wallet.confirm.paymentPending = false;
                transaction = undefined;
            }, function (response) {
                if (angular.isDefined(response.data))
                    notificationService.error('Error:' + response.data.error + ' - ' + response.data.message);
                else
                    notificationService.error('Request failed. Status: ' + response.status + ' - ' +
                        response.statusText);
                //enable confirm button
                wallet.confirm.paymentPending = false;
                transaction = undefined;
            });
        }

        function fillUpTransactionCache (transactions) {
            applicationContext.cache.assets.grab(transactions);
        }

        function loadDataFromBackend() {
            refreshWallets();
            refreshTransactions(fillUpTransactionCache);

            refreshPromise = $interval(function() {
                refreshWallets();
                refreshTransactions();
            }, refreshDelay);
        }

        function refreshWallets() {
            _.forEach(wallet.wallets, function (item) {
                if (item.balance.currency === Currency.WAV) {
                    apiService.address.balance(applicationContext.account.address)
                        .then(function (response) {
                            item.balance = Money.fromCoins(response.balance, item.balance.currency);
                        });
                }
            });
        }

        function refreshTransactions(transactionHandler) {
            transactionLoadingService.loadTransactions(applicationContext.account.address)
                .then(function (transactions) {
                    if (angular.isDefined(transactionHandler))
                        transactionHandler(transactions);

                    wallet.transactions = transactions;
                });
        }

        function resetPaymentForm() {
            wallet.transfer.recipient = '';
            wallet.transfer.amount = '0';
            wallet.transfer.fee = {
                amount: '0.001',
                isValid: true
            };
        }
    }

    WavesWalletController.$inject = ['$scope', '$timeout', '$interval', 'constants.ui',
        'autocomplete.fees', 'applicationContext',
        'dialogService', 'addressService', 'utilityService', 'apiService', 'notificationService',
        'formattingService', 'transferService', 'transactionLoadingService'];

    angular
        .module('app.wallet')
        .controller('walletController', WavesWalletController);
})();
<commit_msg>fix(wallet): wallet dialog fee and amount validation fix<commit_after>(function () {
    'use strict';

    function WavesWalletController($scope, $timeout, $interval, constants, autocomplete, applicationContext,
                              dialogService, addressService, utilityService, apiService, notificationService,
                              formattingService, transferService, transactionLoadingService) {
        var wallet = this;
        var transaction, refreshPromise;
        var refreshDelay = 10 * 1000;
        var minimumPayment = new Money(constants.MINIMUM_PAYMENT_AMOUNT, Currency.WAV);
        var minimumFee = new Money(constants.MINIMUM_TRANSACTION_FEE, Currency.WAV);

        function unimplementedFeature() {
            $scope.home.featureUnderDevelopment();
        }

        function findWalletByCurrency(currency) {
            return _.find(wallet.wallets, function (w) {
                return w.balance.currency === currency;
            });
        }

        wallet.wallets = [
            {
                balance: new Money(0, Currency.USD)
            },
            {
                balance: new Money(0, Currency.EUR)
            },
            {
                balance: new Money(0, Currency.BTC)
            },
            {
                balance: new Money(0, Currency.WAV)
            },
            {
                balance: new Money(0, Currency.CNY)
            }
        ];
        wallet.current = wallet.wallets[0];
        wallet.transactions = [];
        wallet.confirm = {
            amount: {
                value: '0',
                currency: ''
            },
            fee: {
                value: '0',
                currency: ''
            },
            recipient: '',
            paymentPending: false
        };
        wallet.transfer = autocomplete;
        wallet.paymentValidationOptions = {
            rules: {
                wavesrecipient: {
                    required: true,
                    address: true
                },
                wavessendamount: {
                    required: true,
                    decimal: Currency.WAV.precision,
                    min: minimumPayment.toTokens()
                },
                wavessendfee: {
                    required: true,
                    decimal: Currency.WAV.precision,
                    min: minimumFee.toTokens()
                }
            },
            messages: {
                wavesrecipient: {
                    required: 'Recipient account number is required'
                },
                wavessendamount: {
                    required: 'Amount to send is required',
                    decimal: 'The amount to send must be a number with no more than ' +
                        minimumPayment.currency.precision + ' digits after the decimal point (.)',
                    min: 'Payment amount is too small. It should be greater or equal to ' +
                        minimumPayment.formatAmount(false)
                },
                wavessendfee: {
                    required: 'Transaction fee is required',
                    decimal: 'Transaction fee must be with no more than ' +
                        minimumFee.currency.precision + ' digits after the decimal point (.)',
                    min: 'Transaction fee is too small. It should be greater or equal to ' +
                        minimumFee.formatAmount(true)
                }
            }
        };
        wallet.send = send;
        wallet.withdraw = withdraw;
        wallet.trade = trade;
        wallet.submitPayment = submitPayment;
        wallet.broadcastSendTransaction = broadcastSendTransaction;

        resetPaymentForm();
        loadDataFromBackend();

        $scope.$on('$destroy', function () {
            if (angular.isDefined(refreshPromise)) {
                $interval.cancel(refreshPromise);
                refreshPromise = undefined;
            }
        });

        function send (currency) {
            switch (currency) {
                case Currency.WAV:
                    dialogService.open('#wB-butSend-WAV');
                    break;

                default:
                    unimplementedFeature();
            }

            wallet.current = findWalletByCurrency(currency);
        }

        function withdraw (currency) {
            unimplementedFeature();
        }

        function trade (currency) {
            unimplementedFeature();
        }

        function getPaymentForm() {
            // here we have a direct markup dependency
            // but other ways of getting the form from a child scope are even more ugly
            return angular.element('#send-waves-form').scope().sendWavesForm;
        }

        function submitPayment() {
            var paymentForm = getPaymentForm();
            var invalid = paymentForm.invalid();
            wallet.transfer.fee.isValid = angular.isDefined(invalid.wavessendfee) ?
                !invalid.wavessendfee : true;
            if (!paymentForm.validate())
                // prevent payment dialog from closing if it's not valid
                return false;

            wallet.transfer.fee.amount = wallet.transfer.getFeeAmount();

            var currentCurrency = wallet.current.balance.currency;
            var payment = {
                amount: new Money(wallet.transfer.amount, currentCurrency),
                fee: new Money(wallet.transfer.fee.amount, currentCurrency),
                recipient: addressService.cleanupOptionalPrefix(wallet.transfer.recipient),
                time: utilityService.getTime()
            };

            var sender = {
                address: applicationContext.account.address,
                publicKey: applicationContext.account.keyPair.public,
                privateKey: applicationContext.account.keyPair.private
            };

            // creating the transaction and waiting for confirmation
            transaction = transferService.createTransaction(payment, sender);

            // setting data for the confirmation dialog
            wallet.confirm.amount.value = payment.amount.formatAmount(true);
            wallet.confirm.amount.currency = payment.amount.currency.displayName;
            wallet.confirm.fee.value = payment.fee.formatAmount(true);
            wallet.confirm.fee.currency = payment.fee.currency.displayName;
            wallet.confirm.recipient = payment.recipient;

            // open confirmation dialog
            // doing it async because this method is called while another dialog is open
            $timeout(function () {
                dialogService.open('#send-payment-confirmation');
            }, 1);

            resetPaymentForm();

            // it's ok to close payment dialog
            return true;
        }

        function broadcastSendTransaction() {
            // checking if transaction was saved
            if (angular.isUndefined(transaction))
                return;

            // prevent method execution when there is a pending payment request
            if (wallet.confirm.paymentPending)
                return;

            //disable confirm button
            wallet.confirm.paymentPending = true;

            apiService.broadcastPayment(transaction).then(function () {
                var amount = Money.fromCoins(transaction.amount, wallet.current.balance.currency);
                var address = transaction.recipient;
                var displayMessage = 'Sent ' + amount.formatAmount(true) + amount.currency.symbol +
                    '
Recipient ' + address.substr(0,15) + '...
Date: ' +
                    formattingService.formatTimestamp(transaction.timestamp);
                notificationService.notice(displayMessage);
                //enable confirm button
                wallet.confirm.paymentPending = false;
                transaction = undefined;
            }, function (response) {
                if (angular.isDefined(response.data))
                    notificationService.error('Error:' + response.data.error + ' - ' + response.data.message);
                else
                    notificationService.error('Request failed. Status: ' + response.status + ' - ' +
                        response.statusText);
                //enable confirm button
                wallet.confirm.paymentPending = false;
                transaction = undefined;
            });
        }

        function fillUpTransactionCache (transactions) {
            applicationContext.cache.assets.grab(transactions);
        }

        function loadDataFromBackend() {
            refreshWallets();
            refreshTransactions(fillUpTransactionCache);

            refreshPromise = $interval(function() {
                refreshWallets();
                refreshTransactions();
            }, refreshDelay);
        }

        function refreshWallets() {
            _.forEach(wallet.wallets, function (item) {
                if (item.balance.currency === Currency.WAV) {
                    apiService.address.balance(applicationContext.account.address)
                        .then(function (response) {
                            item.balance = Money.fromCoins(response.balance, item.balance.currency);
                        });
                }
            });
        }

        function refreshTransactions(transactionHandler) {
            transactionLoadingService.loadTransactions(applicationContext.account.address)
                .then(function (transactions) {
                    if (angular.isDefined(transactionHandler))
                        transactionHandler(transactions);

                    wallet.transactions = transactions;
                });
        }

        function resetPaymentForm() {
            wallet.transfer.recipient = '';
            wallet.transfer.amount = '0';
            wallet.transfer.fee = {
                amount: '0.001',
                isValid: true
            };
        }
    }

    WavesWalletController.$inject = ['$scope', '$timeout', '$interval', 'constants.ui',
        'autocomplete.fees', 'applicationContext',
        'dialogService', 'addressService', 'utilityService', 'apiService', 'notificationService',
        'formattingService', 'transferService', 'transactionLoadingService'];

    angular
        .module('app.wallet')
        .controller('walletController', WavesWalletController);
})();
<|endoftext|>


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

  var app = angular.module('radar.marmoset', []);

  app.directive('checkboxes', function() {
    return {
      require: '?ngModel',
      scope: {
        options: '='
      },
      template: function(element, attrs) {
        if (attrs.inline === 'true') {
          return '<input type="checkbox" ng-model="states[$index]" ng-change="update($index)" /> {{option.label}}';
        } else {
          return '
<input type="checkbox" ng-model="states[$index]" ng-change="update($index)" /> {{option.label}}
';
        }
      },
      link: function(scope, element, attr, ngModel) {
        if (!ngModel) {
          return;
        }

        function updateState() {
          var value = ngModel.$viewValue;
          var options = scope.options || [];

          for (var i = 0; i < options.length; i++) {
            scope.states[i] = options[i].value === value;
          }
        }

        scope.update = function(index) {
          var value = scope.states[index] ? scope.options[index].value : null;
          ngModel.$setViewValue(value);
          updateState();
        };

        scope.states = {};

        scope.$watch('options', updateState);
        ngModel.$render = updateState;
      }
    }
  });

  function intWidget(scope, field) {
    var element = angular.element('<input />');
    element.attr('class', 'form-control');
    element.attr('integer-validator', '');
    element.attr('name', field.name);
    element.attr('type', 'text');
    element.attr('ng-model', 'field.data[field.name]');
    element.attr('ng-required', 'field.required()');
    return element;
  }

  function floatWidget(scope, field) {
    var element = angular.element('<input />');
    element.attr('class', 'form-control');
    element.attr('number-validator', '');
    element.attr('name', field.name);
    element.attr('type', 'text');
    element.attr('ng-model', 'field.data[field.name]');
    element.attr('ng-required', 'field.required()');
    return element;
  }

  function stringWidget(scope, field) {
    return angular.element('<input class="form-control" type="text" ng-model="field.data[field.name]" />');
  }

  function dateWidget(scope, field) {
    var element = angular.element('<input />');
    element.attr('class', 'form-control');
    element.attr('date-validator', '');
    element.attr('frm-date-picker', '');
    element.attr('name', field.name);
    element.attr('ng-model', 'field.data[field.name]');
    element.attr('ng-required', 'field.required()');
    return element;
  }

  function selectWidget(scope, field) {
    // TODO add option for null

    var element = angular.element('<select></select>');
    element.attr('class', 'form-control');
    element.attr('name', field.name);
    element.attr('ng-model', 'field.data[field.name]');
    element.attr('ng-options', 'x.value as x.label for x in field.options');
    element.attr('ng-required', 'field.required()');
    return element;
  }

  function radioWidget(scope, field, data) {
    // TODO add radio for null

    var labelElement = angular.element('');

    var inputElement = angular.element('<input />');
    inputElement.attr('name', field.name);
    inputElement.attr('type', 'radio');
    inputElement.attr('ng-value', 'option.value');
    inputElement.attr('ng-model', 'field.data[field.name]');
    inputElement.attr('ng-required', 'field.required()');
    labelElement.append(inputElement);

    labelElement.append('{{option.label}}');

    var rootElement;

    if (data.inline) {
      labelElement.attr('class', 'radio-inline');
      labelElement.attr('ng-repeat', 'option in field.options');
      rootElement = labelElement;
    } else {
      var divElement = angular.element('

');
      divElement.attr('class', 'radio');
      divElement.attr('ng-repeat', 'option in field.options');
      divElement.append(labelElement);
      rootElement = divElement;
    }

    return rootElement;
  }

  function yesNoRadioWidget(scope, field) {
    scope.options = [
      {value: true, label: 'Yes'},
      {value: false, label: 'No'},
      {value: null, label: 'Not Answered'}
    ];

    var divElement = angular.element('

');
    divElement.attr('class', 'radio');
    divElement.attr('ng-repeat', 'option in options');

    var labelElement = angular.element('');
    divElement.append(labelElement);

    var inputElement = angular.element('<input />');
    inputElement.attr('name', field.name);
    inputElement.attr('type', 'radio');
    inputElement.attr('ng-value', 'option.value');
    inputElement.attr('ng-model', 'field.data[field.name]');
    inputElement.attr('ng-required', 'field.required()');
    labelElement.append(inputElement);

    labelElement.append('{{option.label}}');

    return divElement;
  }

  function checkboxesWidget(scope, field, data) {
    var element = angular.element('

')
    element.attr('checkboxes', '');
    element.attr('ng-model', 'field.data[field.name]');
    element.attr('ng-required', 'field.required()');
    element.attr('options', 'field.options');
    element.attr('inline', data.inline ? 'true' : 'false');
    return element;
  }

  function formulaWidget(scope, field) {
    if (field.type === 'date') {
      return angular.element('
{{field.data[field.name] | dateFormat}}
');
    } else {
      return angular.element('
{{field.data[field.name] | missing}}
');
    }
  }

  function Registry() {
    this.widgets = {};
    this.defaultWidgets = {};
    this.defaultSelectWidget = null;
    this.required = {};
    this.visible = {};
  }

  /** Register a widget factory. */
  Registry.prototype.addWidget = function(name, factory) {
    this.widgets[name] = factory;
  };

  /** Return the widget factory for the given field. */
  Registry.prototype.getWidget = function(field) {
    if (field.formula) {
      return formulaWidget; // TODO
    }

    var name = field.widget === undefined ? null : field.widget.name;

    // No explicit widget in the field definition so we'll use
    // the default widget for this type of field.
    if (!name) {
      var type = field.type;
      var select = field.options !== undefined;

      if (select) {
        name = this.defaultSelectWidget || this.defaultWidgets[type];
      } else {
        name = this.defaultWidgets[type];
      }
    }

    var factory = this.widgets[name];

    if (factory === undefined) {
      throw 'No widget factory for: ' + name;
    }

    return factory;
  };

  /** Set the default widget for a type. */
  Registry.prototype.setDefaultWidget = function(type, name) {
    this.defaultWidgets[type] = name;
  };

  /** Set the default select widget (for fields with options). */
  Registry.prototype.setDefaultSelectWidget = function(name) {
    this.defaultSelectWidget = name;
  };

  /** Register a required function. */
  Registry.prototype.addRequired = function(type, f) {
    this.required[type] = f;
  };

  /** Get a required function. */
  Registry.prototype.getRequired = function(type) {
    var f = this.required[type];

    if (f === undefined) {
      throw 'No required function for: ' + type;
    }

    return f;
  };

  /** Register a visible function. */
  Registry.prototype.addVisible = function(type, f) {
    this.visible[type] = f;
  };

  /** Get a visible function. */
  Registry.prototype.getVisible = function(type) {
    var f = this.visible[type];

    if (f === undefined) {
      throw 'No visible function for: ' + type;
    }

    return f;
  };

  function jsHandler(field, data) {
    /*jshint evil:true */
    var f = new Function('form', 'path', data.value);

    return function(wrapped) {
      return f(wrapped.data, [wrapped.name]);
    };
  }

  var registry = new Registry();

  registry.addWidget('string', stringWidget);
  registry.addWidget('date', dateWidget);
  registry.addWidget('int', intWidget);
  registry.addWidget('float', floatWidget);
  registry.addWidget('select', selectWidget);
  registry.addWidget('radio', radioWidget);
  registry.addWidget('yesNoRadio', yesNoRadioWidget);
  registry.addWidget('checkboxes', checkboxesWidget);

  registry.setDefaultWidget('int', 'int');
  registry.setDefaultWidget('float', 'float');
  registry.setDefaultWidget('string', 'string');
  registry.setDefaultWidget('boolean', 'yesNoRadio');
  registry.setDefaultWidget('date', 'date');
  registry.setDefaultWidget('datetime', 'string');

  registry.setDefaultSelectWidget('select');

  registry.addRequired('js', jsHandler);
  registry.addVisible('js', jsHandler);

  /** Wrap a value in a function. */
  function wrap(value) {
    return function() {
      return value;
    };
  }

  /** A form schema. */
  function Schema(registry, data) {
    this.registry = registry;
    this.fields = [];

    for (var i = 0; i < data.length; i++) {
      var field = new Field(this, data[i]);
      this.fields.push(field);
    }
  }

  /** A field in the form schema. */
  function Field(schema, data) {
    this.schema = schema;
    this.name = data.name;
    this.type = data.type;
    this.label = data.label;
    this.help = data.help;

    // Default to required
    var required = data.required === undefined ? true : data.required;

    if (required === true) {
      this.required = wrap(true);
    } else if (required === false) {
      this.required = wrap(false);
    } else {
      this.required = this.schema.registry.getRequired(required.type)(this, required);
    }

    /// Default to visible
    var visible = data.visible === undefined ? true : data.visible;

    if (visible === true) {
      this.visible = wrap(true);
    } else if (visible === false) {
      this.visible = wrap(false);
    } else {
      this.visible = this.schema.registry.getVisible(visible.type)(this, visible);
    }

    this.options = data.options;

    // Return a widget element
    this.widget = function(scope) {
      return this.schema.registry.getWidget(data)(scope, this, data.widget || {});
    };
  }

  /** Wrap a field so it has access to it's value and any errors. */
  Field.prototype.wrap = function(data, errors) {
    var self = this;

    var wrapped = Object.create(this);

    wrapped.data = data;
    wrapped.errors = errors;

    wrapped.required = function() {
      return self.required(wrapped);
    };

    wrapped.visible = function() {
      return self.visible(wrapped);
    };

    wrapped.valid = function() {
      // Valid if there aren't any errors
      return !errors[self.name];
    };

    wrapped.value = function() {
      return data[self.name];
    };

    return wrapped;
  };

  // Directive to render a schema
  app.directive('marmosetSchema', ['$compile', function($compile) {
    return {
      scope: {
        schema: '=marmosetSchema',
        data: '=',
        errors: '='
      },
      link: function(scope, element) {
        // TODO
        if (scope.errors === undefined) {
          scope.errors = {};
        }

        // Note: add the container to the DOM here so we can $compile in the for loop
        var container = angular.element('

');
        element.append(container);

        // TODO formula fields

        // Loop through the fields in the schema
        for (var i = 0; i < scope.schema.fields.length; i++) {
          var field = scope.schema.fields[i];
          var wrapped = field.wrap(scope.data, scope.errors);

          var fieldScope = scope.$new();
          fieldScope.field = wrapped;

          // Note: important that the element is compiled after being added to the DOM
          var fieldElement = angular.element('

');
          container.append(fieldElement);
          $compile(fieldElement)(fieldScope);
        }
      }
    };
  }]);

  // Directive to render a field
  app.directive('marmosetField', ['$compile', '$templateCache', function($compile, $templateCache) {
    return {
      require: '?^form',
      scope: {
        field: '=marmosetField'
      },
      templateUrl: 'app/marmoset/field.html',
      link: function(scope, element, attr, formCtrl) {
        scope.form = formCtrl;

        var targetElement = element.find('[widget]');
        targetElement.removeAttr('widget');

        // Create a new child scope for the widget
        var widgetScope = scope.$new();
        var widgetElement = scope.field.widget(widgetScope);

        // Anything that uses ng-model must be compiled *after* being added
        // to the DOM otherwise it won't be able to find the form controller.
        targetElement.append(widgetElement);
        $compile(widgetElement)(widgetScope);

        // Set the field to null when it is hidden
        scope.$watch(function() {
          return scope.field.visible();
        }, function(visible) {
          // Hidden
          if (!visible) {
            scope.field.data[scope.field.name] = null;
          }
        });

        function getModelCtrl() {
          // frmCtrl (FormController) will be null if this directive isn't inside a form
          // modelCtrl (NgModelController) will be undefined on load
          return formCtrl ? (formCtrl[scope.field.name] || null) : null;
        }

        scope.error = function(name) {
          var modelCtrl = getModelCtrl();

          if (!modelCtrl) {
            return false;
          }

          return !!modelCtrl.$error[name];
        };

        // Return true if the field is valid (no client-side or server-side errors)
        scope.valid = function() {
          var modelCtrl = getModelCtrl();

          // If modelCtrl isn't availble yet treat the field as valid and pristine
          var valid = modelCtrl ? modelCtrl.$valid : true;
          var pristine = modelCtrl ? modelCtrl.$pristine : false;

          // Note: we ignore client-side errors if the field is pristine (not-edited)
          return (valid || pristine) && scope.field.valid();
        };
      }
    };
  }]);

  // TODO
  app.factory('createSchema', function() {
    return function(data) {
      return new Schema(registry, data);
    };
  });
})();
<commit_msg>Add support for formula fields<commit_after>(function() {
  'use strict';

  var app = angular.module('radar.marmoset', []);

  app.directive('checkboxes', function() {
    return {
      require: '?ngModel',
      scope: {
        options: '='
      },
      template: function(element, attrs) {
        if (attrs.inline === 'true') {
          return '<input type="checkbox" ng-model="states[$index]" ng-change="update($index)" /> {{option.label}}';
        } else {
          return '
<input type="checkbox" ng-model="states[$index]" ng-change="update($index)" /> {{option.label}}
';
        }
      },
      link: function(scope, element, attr, ngModel) {
        if (!ngModel) {
          return;
        }

        function updateState() {
          var value = ngModel.$viewValue;
          var options = scope.options || [];

          for (var i = 0; i < options.length; i++) {
            scope.states[i] = options[i].value === value;
          }
        }

        scope.update = function(index) {
          var value = scope.states[index] ? scope.options[index].value : null;
          ngModel.$setViewValue(value);
          updateState();
        };

        scope.states = {};

        scope.$watch('options', updateState);
        ngModel.$render = updateState;
      }
    }
  });

  function intWidget(scope, field) {
    var element = angular.element('<input />');
    element.attr('class', 'form-control');
    element.attr('integer-validator', '');
    element.attr('name', field.name);
    element.attr('type', 'text');
    element.attr('ng-model', 'field.data[field.name]');
    element.attr('ng-required', 'field.required()');
    return element;
  }

  function floatWidget(scope, field) {
    var element = angular.element('<input />');
    element.attr('class', 'form-control');
    element.attr('number-validator', '');
    element.attr('name', field.name);
    element.attr('type', 'text');
    element.attr('ng-model', 'field.data[field.name]');
    element.attr('ng-required', 'field.required()');
    return element;
  }

  function stringWidget(scope, field) {
    return angular.element('<input class="form-control" type="text" ng-model="field.data[field.name]" />');
  }

  function dateWidget(scope, field) {
    var element = angular.element('<input />');
    element.attr('class', 'form-control');
    element.attr('date-validator', '');
    element.attr('frm-date-picker', '');
    element.attr('name', field.name);
    element.attr('ng-model', 'field.data[field.name]');
    element.attr('ng-required', 'field.required()');
    return element;
  }

  function selectWidget(scope, field) {
    // TODO add option for null

    var element = angular.element('<select></select>');
    element.attr('class', 'form-control');
    element.attr('name', field.name);
    element.attr('ng-model', 'field.data[field.name]');
    element.attr('ng-options', 'x.value as x.label for x in field.options');
    element.attr('ng-required', 'field.required()');
    return element;
  }

  function radioWidget(scope, field, data) {
    // TODO add radio for null

    var labelElement = angular.element('');

    var inputElement = angular.element('<input />');
    inputElement.attr('name', field.name);
    inputElement.attr('type', 'radio');
    inputElement.attr('ng-value', 'option.value');
    inputElement.attr('ng-model', 'field.data[field.name]');
    inputElement.attr('ng-required', 'field.required()');
    labelElement.append(inputElement);

    labelElement.append('{{option.label}}');

    var rootElement;

    if (data.inline) {
      labelElement.attr('class', 'radio-inline');
      labelElement.attr('ng-repeat', 'option in field.options');
      rootElement = labelElement;
    } else {
      var divElement = angular.element('

');
      divElement.attr('class', 'radio');
      divElement.attr('ng-repeat', 'option in field.options');
      divElement.append(labelElement);
      rootElement = divElement;
    }

    return rootElement;
  }

  function yesNoRadioWidget(scope, field) {
    scope.options = [
      {value: true, label: 'Yes'},
      {value: false, label: 'No'},
      {value: null, label: 'Not Answered'}
    ];

    var divElement = angular.element('

');
    divElement.attr('class', 'radio');
    divElement.attr('ng-repeat', 'option in options');

    var labelElement = angular.element('');
    divElement.append(labelElement);

    var inputElement = angular.element('<input />');
    inputElement.attr('name', field.name);
    inputElement.attr('type', 'radio');
    inputElement.attr('ng-value', 'option.value');
    inputElement.attr('ng-model', 'field.data[field.name]');
    inputElement.attr('ng-required', 'field.required()');
    labelElement.append(inputElement);

    labelElement.append('{{option.label}}');

    return divElement;
  }

  function checkboxesWidget(scope, field, data) {
    var element = angular.element('

')
    element.attr('checkboxes', '');
    element.attr('ng-model', 'field.data[field.name]');
    element.attr('ng-required', 'field.required()');
    element.attr('options', 'field.options');
    element.attr('inline', data.inline ? 'true' : 'false');
    return element;
  }

  function staticWidget(scope, field) {
    if (field.type === 'date') {
      return angular.element('
{{field.value() | dateFormat}}
');
    } else if (field.type === 'datetime') {
      return angular.element('
{{field.value() | dateTimeFormat}}
');
    } else if (field.type === 'boolean') {
      return angular.element('

');
    } else {
      return angular.element('
{{field.value() | missing}}
');
    }
  }

  function Registry() {
    this.widgets = {};
    this.defaultWidgets = {};
    this.defaultSelectWidget = null;
    this.defaultFormulaWidgets = {};
    this.required = {};
    this.visible = {};
    this.formula = {};
  }

  /** Register a widget factory. */
  Registry.prototype.addWidget = function(name, factory) {
    this.widgets[name] = factory;
  };

  /** Return the widget factory for the given field. */
  Registry.prototype.getWidget = function(field) {
    var name = field.widget === undefined ? null : field.widget.name;

    // No explicit widget in the field definition so we'll use
    // the default widget for this type of field.
    if (!name) {
      var type = field.type;
      var formula = field.formula !== undefined;
      var select = field.options !== undefined;

      if (formula) {
        name = this.defaultFormulaWidgets[type];
      } else if (select) {
        name = this.defaultSelectWidget || this.defaultWidgets[type];
      } else {
        name = this.defaultWidgets[type];
      }
    }

    var factory = this.widgets[name];

    if (factory === undefined) {
      throw 'No widget factory for: ' + name;
    }

    return factory;
  };

  /** Set the default widget for a type. */
  Registry.prototype.setDefaultWidget = function(type, name) {
    this.defaultWidgets[type] = name;
  };

  /** Set the default formula widget for a type. */
  Registry.prototype.setDefaultFormulaWidget = function(type, name) {
    this.defaultFormulaWidgets[type] = name;
  };

  /** Set the default select widget (for fields with options). */
  Registry.prototype.setDefaultSelectWidget = function(name) {
    this.defaultSelectWidget = name;
  };

  /** Register a required function. */
  Registry.prototype.addRequired = function(name, f) {
    this.required[name] = f;
  };

  /** Get a required function. */
  Registry.prototype.getRequired = function(name) {
    var f = this.required[name];

    if (f === undefined) {
      throw 'No required function for: ' + name;
    }

    return f;
  };

  /** Register a visible function. */
  Registry.prototype.addVisible = function(name, f) {
    this.visible[name] = f;
  };

  /** Get a visible function. */
  Registry.prototype.getVisible = function(name) {
    var f = this.visible[name];

    if (f === undefined) {
      throw 'No visible function for: ' + name;
    }

    return f;
  };

  /** Register a formula function. */
  Registry.prototype.addFormula = function(name, f) {
    this.formula[name] = f;
  };

  /** Get a formula function. */
  Registry.prototype.getFormula = function(name) {
    var f = this.formula[name];

    if (f === undefined) {
      throw 'No formula function for: ' + name;
    }

    return f;
  };

  function jsHandler(field, data) {
    /* jshint evil:true */
    var f = new Function('form', 'path', data.value);

    return function(wrapped) {
      return f(wrapped.data, [wrapped.name]);
    };
  }

  function jsFormula(field, data) {
    /* jshint evil:true */
    var f = new Function('form', 'path', data.value);

    return function(wrapped) {
      var newValue = f(wrapped.data, [wrapped.name]);

      // Update the field's value
      wrapped.value(newValue);
    };
  }

  var registry = new Registry();

  // Register widgets
  registry.addWidget('string', stringWidget);
  registry.addWidget('date', dateWidget);
  registry.addWidget('int', intWidget);
  registry.addWidget('float', floatWidget);
  registry.addWidget('select', selectWidget);
  registry.addWidget('radio', radioWidget);
  registry.addWidget('yesNoRadio', yesNoRadioWidget);
  registry.addWidget('checkboxes', checkboxesWidget);
  registry.addWidget('static', staticWidget);

  // Set default widgets
  registry.setDefaultWidget('int', 'int');
  registry.setDefaultWidget('float', 'float');
  registry.setDefaultWidget('string', 'string');
  registry.setDefaultWidget('boolean', 'yesNoRadio');
  registry.setDefaultWidget('date', 'date');
  registry.setDefaultWidget('datetime', 'string');
  registry.setDefaultFormulaWidget('int', 'static');
  registry.setDefaultFormulaWidget('float', 'static');
  registry.setDefaultFormulaWidget('string', 'static');
  registry.setDefaultFormulaWidget('boolean', 'static');
  registry.setDefaultFormulaWidget('date', 'static');
  registry.setDefaultFormulaWidget('datetime', 'static');
  registry.setDefaultSelectWidget('select');

  // Register functions
  registry.addRequired('js', jsHandler);
  registry.addVisible('js', jsHandler);
  registry.addFormula('js', jsFormula);

  /** Wrap a value in a function. */
  function wrap(value) {
    return function() {
      return value;
    };
  }

  /** Do nothing. */
  function noop() {
    return;
  }

  /** A form schema. */
  function Schema(registry, data) {
    this.registry = registry;
    this.fields = [];

    for (var i = 0; i < data.length; i++) {
      var field = new Field(this, data[i]);
      this.fields.push(field);
    }
  }

  /** A field in the form schema. */
  function Field(schema, data) {
    var self = this;

    self.schema = schema;
    self.name = data.name;
    self.type = data.type;
    self.label = data.label;
    self.help = data.help;

    var registy = schema.registry;

    var required;

    if (data.formula) {
      // Formula fields are never required
      required = false;
    } else if (data.required === undefined) {
      // Default to required
      required = true;
    } else {
      // Use supplied value
      required = data.required;
    }

    if (required === true) {
      self.required = wrap(true);
    } else if (required === false) {
      self.required = wrap(false);
    } else {
      self.required = registry.getRequired(required.name)(self, required);
    }

    // Default to visible
    var visible = data.visible === undefined ? true : data.visible;

    if (visible === true) {
      self.visible = wrap(true);
    } else if (visible === false) {
      self.visible = wrap(false);
    } else {
      self.visible = registry.getVisible(visible.name)(self, visible);
    }

    self.options = data.options;

    var widgetFactory = registry.getWidget(data);

    // Return a widget element
    this.widget = function(scope) {
      return widgetFactory(scope, self, data.widget || {});
    };

    var formula = data.formula;

    if (formula === undefined) {
      this.update = wrap(noop);
    } else {
      this.update = registry.getFormula(formula.name)(self, formula);
    }
  }

  /** Wrap a field so it has access to it's value and any errors. */
  Field.prototype.wrap = function(data, errors) {
    var self = this;

    var wrapped = Object.create(self);

    wrapped.data = data;
    wrapped.errors = errors;

    wrapped.required = function() {
      return self.required(wrapped);
    };

    wrapped.visible = function() {
      return self.visible(wrapped);
    };

    wrapped.valid = function() {
      // Valid if there aren't any errors
      return !errors[self.name];
    };

    wrapped.value = function(value) {
      if (value === undefined) {
        return data[self.name];
      } else {
        data[self.name] = value;
      }
    };

    wrapped.update = function() {
      console.log('update!');
      return self.update(wrapped);
    };

    return wrapped;
  };

  // Directive to render a schema
  app.directive('marmosetSchema', ['$compile', function($compile) {
    return {
      scope: {
        schema: '=marmosetSchema',
        data: '=',
        errors: '='
      },
      link: function(scope, element) {
        // TODO
        if (scope.errors === undefined) {
          scope.errors = {};
        }

        // Note: add the container to the DOM here so we can $compile in the for loop
        var container = angular.element('

');
        element.append(container);

        // Loop through the fields in the schema
        for (var i = 0; i < scope.schema.fields.length; i++) {
          var field = scope.schema.fields[i];
          var wrapped = field.wrap(scope.data, scope.errors);

          var fieldScope = scope.$new();
          fieldScope.field = wrapped;

          // Note: important that the element is compiled after being added to the DOM
          var fieldElement = angular.element('

');
          container.append(fieldElement);
          $compile(fieldElement)(fieldScope);
        }
      }
    };
  }]);

  // Directive to render a field
  app.directive('marmosetField', ['$compile', '$templateCache', function($compile, $templateCache) {
    return {
      require: '?^form',
      scope: {
        field: '=marmosetField'
      },
      templateUrl: 'app/marmoset/field.html',
      link: function(scope, element, attr, formCtrl) {
        scope.form = formCtrl;

        var targetElement = element.find('[widget]');
        targetElement.removeAttr('widget');

        // Create a new child scope for the widget
        var widgetScope = scope.$new();
        var widgetElement = scope.field.widget(widgetScope);

        // Anything that uses ng-model must be compiled *after* being added
        // to the DOM otherwise it won't be able to find the form controller.
        targetElement.append(widgetElement);
        $compile(widgetElement)(widgetScope);

        // Set the field to null when it is hidden
        scope.$watch(function() {
          return scope.field.visible();
        }, function(visible) {
          // Hidden
          if (!visible) {
            scope.field.data[scope.field.name] = null;
          }
        });

        // Notify the field when other fields change
        scope.$watch('field.data', scope.field.update, true);

        function getModelCtrl() {
          // frmCtrl (FormController) will be null if this directive isn't inside a form
          // modelCtrl (NgModelController) will be undefined on load
          return formCtrl ? (formCtrl[scope.field.name] || null) : null;
        }

        scope.error = function(name) {
          var modelCtrl = getModelCtrl();

          if (!modelCtrl) {
            return false;
          }

          return !!modelCtrl.$error[name];
        };

        // Return true if the field is valid (no client-side or server-side errors)
        scope.valid = function() {
          var modelCtrl = getModelCtrl();

          // If modelCtrl isn't availble yet treat the field as valid and pristine
          var valid = modelCtrl ? modelCtrl.$valid : true;
          var pristine = modelCtrl ? modelCtrl.$pristine : false;

          // Note: we ignore client-side errors if the field is pristine (not-edited)
          return (valid || pristine) && scope.field.valid();
        };
      }
    };
  }]);

  app.factory('createSchema', function() {
    return function(data) {
      return new Schema(registry, data);
    };
  });
})();
<|endoftext|>


<commit_before>/* global chrome, $ */

import { options, isCloning } from "./options-storage.js";
import { getCloneBounds } from "./clone.js";

// Helper functions
// These should be functions that are called in more than one place
// -----------------------------------------------------------------------------

function getFromId(id, root=document) {
  return root.getElementById(id);
}

function getFromClass(className, root=document) {
  return Array.from(root.getElementsByClassName(className));
}

function getFromTag(tagName, root=document) {
  return Array.from(root.getElementsByTagName(tagName));
}

// window that will be focused on pop-out
function getFocusedName() {
  const focused = getFromClass("focus-option").find(option => option.checked);
  return focused === undefined ? "original" : focused.id.replace("focus-", "");
}

// save current state
function save() {
  options.set("focus", getFocusedName());
  options.set("resizeOriginal", getFromId("resize-original").checked);
  options.set("copyFullscreen", getFromId("copy-fullscreen").checked);
  options.set("cloneMode",  getFromClass("clone-mode-option").find(cp => cp.checked).id);
  options.set("menuButtonType", getFromClass("menu-button-option").find(mb => mb.checked).getAttribute("data-value"));

  // dimensions
  getFromClass("window").forEach(win => {
    [["Width",  "Width" ],
     ["Height", "Height"],
     ["Left",   "Width" ],
     ["Top",    "Height"]].forEach(([prop, dim]) => {
      const windowDimension = win[`offset${prop}`];
      const screenDimension = getFromId("screen")[`offset${dim}`];
      const value = windowDimension / screenDimension;
      options.setForWindow(win.id, prop, value);
    });
  });

  options.save();
}

// changing draggable/resizable windows, used when radio buttons override
// resizing and positioning
function updateWindowHandling(inputId, windowId, enableIfChecked) {
  const checked = getFromId(inputId).checked;
  const action = enableIfChecked === checked ? "enable" : "disable";
  const $win = $(`#${windowId}`);
  $win.draggable(action);
  $win.resizable(action);
}

function updateResizeOriginal() {
  updateWindowHandling("resize-original", "original", true);
  const originalWin = getFromId("original");
  const isResizing = getFromId("resize-original").checked;
  isResizing
    ? originalWin.classList.remove("disabled")
    : originalWin.classList.add("disabled");
}

function updateResizeNew() {
  const inputId = "clone-mode-no";
  const windowId = "new";
  const enableIfChecked = true;
  updateWindowHandling(inputId, windowId, enableIfChecked);
}

function updateClone() {
  const originalWin = getFromId("original");
  const newWin = getFromId("new");
  newWin.style.width = originalWin.style.width;
  newWin.style.height = originalWin.style.height;

  const monitor = getFromId("monitor");
  const displayBounds = {
    left: 0,
    top: 0,
    width: monitor.clientWidth,
    height: monitor.clientHeight
  };

  const origBounds = {
    left:   originalWin.offsetLeft,
    top:    originalWin.offsetTop,
    width:  originalWin.offsetWidth,
    height: originalWin.offsetHeight
  };

  const newBounds = getCloneBounds(origBounds, displayBounds, options.get("cloneMode"));

  Object.entries(newBounds).forEach(([key, value]) => {
    newWin.style[key] = `${value}px`;
  });
}

// update appearance of windows depending on if they are active or not
function updateFocus() {
  getFromClass("window").forEach(win => {
    const isBlurred = win.id !== getFocusedName();
    isBlurred
      ? win.classList.add("blurred")
      : win.classList.remove("blurred");
  });
}

// Main Function
// -----------------------------------------------------------------------------
// Each chunk has specifically *not* been broken out into a named function
// as then it's more difficult to tell when / where they are being called
// and if it's more than one

function main() {
  // display shortcuts
  {
    chrome.commands.getAll(cmds => {
      if (cmds.length === 0) { return; }

      cmds.filter(cmd => cmd.name !== "_execute_browser_action")
          .forEach(cmd => {
            const name = document.createElement("span");
            name.textContent = `${cmd.description}:`;
            name.classList.add("shortcut-label");

            const shortcut = document.createElement("span");
            shortcut.classList.add("shortcut");
            shortcut.textContent = cmd.shortcut;

            const li = document.createElement("li");
            [name, shortcut].forEach(el => li.appendChild(el));

            getFromId("shortcut-list").appendChild(li);
          });
    });
  }

  const gridsize = 20; // px to use for window grid
  // Set monitor aspect ratio to match user's
  {
    const monitor = getFromId("monitor");
    const ratio = screen.height / screen.width;
    const height = Math.round((monitor.clientWidth * ratio) / gridsize) * gridsize;
    monitor.style.height = `${height}px`;
  }

  // restore options
  {
    getFromClass("focus-option").forEach(opt => {
      opt.checked = opt.id.includes(options.get("focus"));
    });
    getFromId("resize-original").checked = options.get("resizeOriginal");
    const curCloneOption = getFromClass("clone-mode-option").find(cp =>
      cp.id === options.get("cloneMode"));
    curCloneOption.checked = true;
    getFromId("copy-fullscreen").checked = options.get("copyFullscreen");
    getFromClass("menu-button-option").forEach(opt => {
      opt.checked = opt.id.includes(options.get("menuButtonType"));
    });
  }

  // setup windows
  {
    getFromClass("window").forEach(win => {
      // Restore positions from options
      ["width", "height", "left", "top"].forEach(prop => {
        win.style[prop] = `${options.getForWindow(win.id, prop) * 100}%`;
      });

      const grid = ["clientWidth", "clientHeight"].map(d => {
        return getFromId("screen")[d] / gridsize;
      });

      let saveTimeout;
      function update() {
        const shouldUpdateClone = win.id === "original" && isCloning();
        getFromClass("window").forEach(_win => _win.classList.remove("current"));
        win.classList.add("current");
        if (shouldUpdateClone) { updateClone(); }

        clearTimeout(saveTimeout);
        saveTimeout = setTimeout(save, 200);
      }

      const $win = $(win);

      $win.draggable({
        containment: "parent",
        grid,
        drag: update,
        start: update,
        stop: update
      });

      $win.resizable({
        containment: "parent",
        handles:     "all",
        grid:        grid,
        minWidth:    $win.parent().width() * 0.2,
        minHeight:   $win.parent().height() * 0.2,
        resize: update,
        start: update,
        stop: update
      });
    });

    updateResizeOriginal();
    updateResizeNew();
    updateFocus();
    if (isCloning()) { updateClone(); }
  }

  // add input handlers
  {
    getFromId("resize-original").onchange = updateResizeOriginal;
    getFromClass("focus-option").forEach(el => el.onchange = updateFocus);
    getFromTag("input").forEach(el => el.onclick = save);
    getFromId("commandsUrl").onclick = event => {
      chrome.tabs.create({ url: event.target.href });
    };
    getFromClass("clone-mode-option").forEach(el => {
      el.addEventListener("change", () => {
        if (isCloning()) { updateClone(); }
        updateResizeNew();
      }, false);
    });
  }
}

// Loading
// -----------------------------------------------------------------------------

document.addEventListener("DOMContentLoaded", () => {
  options.loadPromise.then(main);
});
<commit_msg>options: original window always on top when cloning<commit_after>/* global chrome, $ */

import { options, isCloning } from "./options-storage.js";
import { getCloneBounds } from "./clone.js";

// Helper functions
// These should be functions that are called in more than one place
// -----------------------------------------------------------------------------

function getFromId(id, root=document) {
  return root.getElementById(id);
}

function getFromClass(className, root=document) {
  return Array.from(root.getElementsByClassName(className));
}

function getFromTag(tagName, root=document) {
  return Array.from(root.getElementsByTagName(tagName));
}

// window that will be focused on pop-out
function getFocusedName() {
  const focused = getFromClass("focus-option").find(option => option.checked);
  return focused === undefined ? "original" : focused.id.replace("focus-", "");
}

// save current state
function save() {
  options.set("focus", getFocusedName());
  options.set("resizeOriginal", getFromId("resize-original").checked);
  options.set("copyFullscreen", getFromId("copy-fullscreen").checked);
  options.set("cloneMode",  getFromClass("clone-mode-option").find(cp => cp.checked).id);
  options.set("menuButtonType", getFromClass("menu-button-option").find(mb => mb.checked).getAttribute("data-value"));

  // dimensions
  getFromClass("window").forEach(win => {
    [["Width",  "Width" ],
     ["Height", "Height"],
     ["Left",   "Width" ],
     ["Top",    "Height"]].forEach(([prop, dim]) => {
      const windowDimension = win[`offset${prop}`];
      const screenDimension = getFromId("screen")[`offset${dim}`];
      const value = windowDimension / screenDimension;
      options.setForWindow(win.id, prop, value);
    });
  });

  options.save();
}

// changing draggable/resizable windows, used when radio buttons override
// resizing and positioning
function updateWindowHandling(inputId, windowId, enableIfChecked) {
  const checked = getFromId(inputId).checked;
  const action = enableIfChecked === checked ? "enable" : "disable";
  const $win = $(`#${windowId}`);
  $win.draggable(action);
  $win.resizable(action);
}

function updateResizeOriginal() {
  updateWindowHandling("resize-original", "original", true);
  const originalWin = getFromId("original");
  const isResizing = getFromId("resize-original").checked;
  isResizing
    ? originalWin.classList.remove("disabled")
    : originalWin.classList.add("disabled");
}

function updateResizeNew() {
  const inputId = "clone-mode-no";
  const windowId = "new";
  const enableIfChecked = true;
  updateWindowHandling(inputId, windowId, enableIfChecked);
}

function updateClone() {
  const originalWin = getFromId("original");
  const newWin = getFromId("new");
  newWin.style.width = originalWin.style.width;
  newWin.style.height = originalWin.style.height;

  const monitor = getFromId("monitor");
  const displayBounds = {
    left: 0,
    top: 0,
    width: monitor.clientWidth,
    height: monitor.clientHeight
  };

  const origBounds = {
    left:   originalWin.offsetLeft,
    top:    originalWin.offsetTop,
    width:  originalWin.offsetWidth,
    height: originalWin.offsetHeight
  };

  const newBounds = getCloneBounds(origBounds, displayBounds, options.get("cloneMode"));

  Object.entries(newBounds).forEach(([key, value]) => {
    newWin.style[key] = `${value}px`;
  });
}

// update appearance of windows depending on if they are active or not
function updateFocus() {
  getFromClass("window").forEach(win => {
    const isBlurred = win.id !== getFocusedName();
    isBlurred
      ? win.classList.add("blurred")
      : win.classList.remove("blurred");
  });
}

function setWindowAsCurrent(win) {
  getFromClass("window").forEach(_win => {
    _win === win
      ? _win.classList.add("current")
      : _win.classList.remove("current");
  });
}

// Main Function
// -----------------------------------------------------------------------------
// Each chunk has specifically *not* been broken out into a named function
// as then it's more difficult to tell when / where they are being called
// and if it's more than one

function main() {
  // display shortcuts
  {
    chrome.commands.getAll(cmds => {
      if (cmds.length === 0) { return; }

      cmds.filter(cmd => cmd.name !== "_execute_browser_action")
          .forEach(cmd => {
            const name = document.createElement("span");
            name.textContent = `${cmd.description}:`;
            name.classList.add("shortcut-label");

            const shortcut = document.createElement("span");
            shortcut.classList.add("shortcut");
            shortcut.textContent = cmd.shortcut;

            const li = document.createElement("li");
            [name, shortcut].forEach(el => li.appendChild(el));

            getFromId("shortcut-list").appendChild(li);
          });
    });
  }

  const gridsize = 20; // px to use for window grid
  // Set monitor aspect ratio to match user's
  {
    const monitor = getFromId("monitor");
    const ratio = screen.height / screen.width;
    const height = Math.round((monitor.clientWidth * ratio) / gridsize) * gridsize;
    monitor.style.height = `${height}px`;
  }

  // restore options
  {
    getFromClass("focus-option").forEach(opt => {
      opt.checked = opt.id.includes(options.get("focus"));
    });
    getFromId("resize-original").checked = options.get("resizeOriginal");
    const curCloneOption = getFromClass("clone-mode-option").find(cp =>
      cp.id === options.get("cloneMode"));
    curCloneOption.checked = true;
    getFromId("copy-fullscreen").checked = options.get("copyFullscreen");
    getFromClass("menu-button-option").forEach(opt => {
      opt.checked = opt.id.includes(options.get("menuButtonType"));
    });
  }

  // setup windows
  {
    getFromClass("window").forEach(win => {
      // Restore positions from options
      ["width", "height", "left", "top"].forEach(prop => {
        win.style[prop] = `${options.getForWindow(win.id, prop) * 100}%`;
      });

      const grid = ["clientWidth", "clientHeight"].map(d => {
        return getFromId("screen")[d] / gridsize;
      });

      let saveTimeout;
      function update() {
        const shouldUpdateClone = win.id === "original" && isCloning();
        if (shouldUpdateClone) { updateClone(); }

        clearTimeout(saveTimeout);
        saveTimeout = setTimeout(save, 200);
      }

      const $win = $(win);

      $win.draggable({
        containment: "parent",
        grid,
        drag: update,
        start: update,
        stop: update
      });

      $win.resizable({
        containment: "parent",
        handles:     "all",
        grid:        grid,
        minWidth:    $win.parent().width() * 0.2,
        minHeight:   $win.parent().height() * 0.2,
        resize: update,
        start: update,
        stop: update
      });

      win.addEventListener("mousedown", () => setWindowAsCurrent(win), false);
      win.addEventListener("touchstart", () => setWindowAsCurrent(win), false);
    });

    updateResizeOriginal();
    updateResizeNew();
    updateFocus();
    if (isCloning()) { updateClone(); }
  }

  // add input handlers
  {
    getFromId("resize-original").onchange = updateResizeOriginal;
    getFromClass("focus-option").forEach(el => el.onchange = updateFocus);
    getFromTag("input").forEach(el => el.onclick = save);
    getFromId("commandsUrl").onclick = event => {
      chrome.tabs.create({ url: event.target.href });
    };
    getFromClass("clone-mode-option").forEach(el => {
      el.addEventListener("change", () => {
        if (isCloning()) { updateClone(); }
        setWindowAsCurrent(getFromId("original"));
        updateResizeNew();
      }, false);
    });
  }
}

// Loading
// -----------------------------------------------------------------------------

document.addEventListener("DOMContentLoaded", () => {
  options.loadPromise.then(main);
});
<|endoftext|>


<commit_before>import {ArrayRepeatStrategy} from 'aurelia-templating-resources/array-repeat-strategy';
import {createFullOverrideContext, updateOverrideContexts, updateOverrideContext, refreshBinding} from 'aurelia-templating-resources/repeat-utilities';

/**
* A strategy for repeating a template over an array.
*/
export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy {
  // create first item to calculate the heights
  createFirstItem(repeat) {
    var overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1);
    var view = repeat.viewFactory.create();
    view.bind(overrideContext.bindingContext, overrideContext);
    repeat.viewSlot.add(view);
  }
  /**
  * Handle the repeat's collection instance changing.
  * @param repeat The repeater instance.
  * @param items The new array instance.
  */
  instanceChanged(repeat, items) {
    this._inPlaceProcessItems(repeat, items);
  }

  _standardProcessInstanceChanged(repeat, items) {
    for(var i = 1, ii = repeat._viewsLength; i < ii; ++i){
      let overrideContext = createFullOverrideContext(repeat, items[i], i, ii);
      let view = repeat.viewFactory.create();
      view.bind(overrideContext.bindingContext, overrideContext);
      repeat.viewSlot.add(view);
    }
  }

  _inPlaceProcessItems(repeat, items) {
    let itemsLength = items.length;
    let viewsLength = repeat.viewSlot.children.length;
    let first = repeat._first;
    // remove unneeded views.
    while (viewsLength > repeat._viewsLength) {
      viewsLength--;
      repeat.viewSlot.removeAt(viewsLength, true);
    }
    // avoid repeated evaluating the property-getter for the "local" property.
    let local = repeat.local;
    // re-evaluate bindings on existing views.
    for (let i = 0; i < viewsLength; i++) {
      let view = repeat.viewSlot.children[i];
      let last = i === itemsLength - 1;
      let middle = i !== 0 && !last;
      // any changes to the binding context?
      if (view.bindingContext[local] === items[i + first] && view.overrideContext.$middle === middle && view.overrideContext.$last === last) {
        // no changes. continue...
        continue;
      }
      // update the binding context and refresh the bindings.
      view.bindingContext[local] = items[i + first];
      view.overrideContext.$middle = middle;
      view.overrideContext.$last = last;
      let j = view.bindings.length;
      while (j--) {
        refreshBinding(view.bindings[j]);
      }
      j = view.controllers.length;
      while (j--) {
        let k = view.controllers[j].boundProperties.length;
        while (k--) {
          let binding = view.controllers[j].boundProperties[k].binding;
          refreshBinding(binding);
        }
      }
    }
    // add new views
    for (let i = viewsLength; i < repeat._viewsLength; i++) {
      let overrideContext = createFullOverrideContext(repeat, items[i], i, itemsLength);
      let view = repeat.viewFactory.create();
      view.bind(overrideContext.bindingContext, overrideContext);
      repeat.viewSlot.add(view);
    }
  }

  /**
  * Handle the repeat's collection instance mutating.
  * @param repeat The repeat instance.
  * @param array The modified array.
  * @param splices Records of array changes.
  */
  instanceMutated(repeat, array, splices) {
    this._updateViews(repeat, repeat.items, splices);
  }

  _standardProcessInstanceMutated(repeat, array, splices) {
    let removeDelta = 0;
    let viewSlot = repeat.viewSlot;
    let rmPromises = [];

    for (let i = 0, ii = splices.length; i < ii; ++i) {
      let splice = splices[i];
      let removed = splice.removed;
      let viewIndex = this._getViewIndex(repeat, viewSlot, splice.index);
      if (viewIndex >= 0) {
        for (let j = 0, jj = removed.length; j < jj; ++j) {
          let viewOrPromise = viewSlot.removeAt(viewIndex + removeDelta + rmPromises.length, true);

          // TODO Create view without trigger view lifecycle - or better solution
          let length = viewSlot.children.length;
          let overrideContext = createFullOverrideContext(repeat, repeat.items[length], length, repeat.items.length);
          let view = repeat.viewFactory.create();
          view.bind(overrideContext.bindingContext, overrideContext);
          repeat.viewSlot.isAttached = false;
          repeat.viewSlot.add(view);
          repeat.viewSlot.isAttached = true;

          if (viewOrPromise instanceof Promise) {
            rmPromises.push(viewOrPromise);
          }
        }
        removeDelta -= splice.addedCount;
       }
    }

    if (rmPromises.length > 0) {
      Promise.all(rmPromises).then(() => {
        this._handleAddedSplices(repeat, array, splices);
        this._updateViews(repeat, array, splices);
      });
    } else {
      this._handleAddedSplices(repeat, array, splices);
      this._updateViews(repeat, array, splices);
    }
  }

  _updateViews(repeat, items, splices) {
    let totalAdded = 0;
    let totalRemoved = 0;
    repeat.items = items;

    for(let i = 0, ii = splices.length; i < ii; ++i){
      let splice = splices[0];
      totalAdded += splice.addedCount;
      totalRemoved += splice.removed.length;
    }

    let index = repeat._getIndexOfFirstView() - totalRemoved;

    if(index < 0) {
      index = 0;
    }

    let  viewSlot = repeat.viewSlot;

    for(let i = 0, ii = viewSlot.children.length; i < ii; ++i){
      let view = viewSlot.children[i];
      let nextIndex = index + i;
      let itemsLength = items.length;
      if((nextIndex) <= itemsLength - 1) {
        view.bindingContext[repeat.local] = items[nextIndex];
        updateOverrideContext(view.overrideContext, nextIndex, itemsLength);
      }
    }

    let bufferDelta = repeat.itemHeight * totalAdded + repeat.itemHeight * -totalRemoved;

    if(repeat._bottomBufferHeight + bufferDelta < 0) {
      repeat._topBufferHeight = repeat._topBufferHeight + bufferDelta;
    } else {
      repeat._bottomBufferHeight = repeat._bottomBufferHeight + bufferDelta;
    }

    if(repeat._bottomBufferHeight > 0) {
      repeat.isLastIndex = false;
    }

    repeat._adjustBufferHeights();
  }

  _handleAddedSplices(repeat, array, splices) {
    let spliceIndexLow;
    let arrayLength = array.length;
    for (let i = 0, ii = splices.length; i < ii; ++i) {
      let splice = splices[i];
      let addIndex = splice.index;
      let end = splice.index + splice.addedCount;

      if (typeof spliceIndexLow === 'undefined' || spliceIndexLow === null || spliceIndexLow > splice.index) {
        spliceIndexLow = addIndex;
      }

      for (; addIndex < end; ++addIndex) {
        let overrideContext = createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength);
        let view = repeat.viewFactory.create();
        view.bind(overrideContext.bindingContext, overrideContext);
        repeat.viewSlot.insert(addIndex, view);
      }
    }

    return spliceIndexLow;
  }

  _isIndexInDom(viewSlot, index) {
    if(viewSlot.children.length === 0) {
      return false;
    }

    let indexLow = viewSlot.children[0].overrideContext.$index;
    let indexHi = viewSlot.children[viewSlot.children.length - 1].overrideContext.$index;

    return index >= indexLow && index <= indexHi;
  }

  _getViewIndex(repeat, viewSlot, index) {
    if(viewSlot.children.length === 0) {
      return -1;
    }
    let indexLow = viewSlot.children[0].overrideContext.$index;
    let viewIndex = index - indexLow;
    if(viewIndex > repeat._viewsLength - 1) {
      viewIndex = -1;
    }
    return viewIndex;
  }
}
<commit_msg>fix(array-virtual-repeat-strategy): update to work with latest templating-resources<commit_after>import {ArrayRepeatStrategy} from 'aurelia-templating-resources/array-repeat-strategy';
import {createFullOverrideContext, updateOverrideContexts, updateOverrideContext, updateOneTimeBinding} from 'aurelia-templating-resources/repeat-utilities';

/**
* A strategy for repeating a template over an array.
*/
export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy {
  // create first item to calculate the heights
  createFirstItem(repeat) {
    var overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1);
    var view = repeat.viewFactory.create();
    view.bind(overrideContext.bindingContext, overrideContext);
    repeat.viewSlot.add(view);
  }
  /**
  * Handle the repeat's collection instance changing.
  * @param repeat The repeater instance.
  * @param items The new array instance.
  */
  instanceChanged(repeat, items) {
    this._inPlaceProcessItems(repeat, items);
  }

  _standardProcessInstanceChanged(repeat, items) {
    for(var i = 1, ii = repeat._viewsLength; i < ii; ++i){
      let overrideContext = createFullOverrideContext(repeat, items[i], i, ii);
      let view = repeat.viewFactory.create();
      view.bind(overrideContext.bindingContext, overrideContext);
      repeat.viewSlot.add(view);
    }
  }

  _inPlaceProcessItems(repeat, items) {
    let itemsLength = items.length;
    let viewsLength = repeat.viewSlot.children.length;
    let first = repeat._first;
    // remove unneeded views.
    while (viewsLength > repeat._viewsLength) {
      viewsLength--;
      repeat.viewSlot.removeAt(viewsLength, true);
    }
    // avoid repeated evaluating the property-getter for the "local" property.
    let local = repeat.local;
    // re-evaluate bindings on existing views.
    for (let i = 0; i < viewsLength; i++) {
      let view = repeat.viewSlot.children[i];
      let last = i === itemsLength - 1;
      let middle = i !== 0 && !last;
      // any changes to the binding context?
      if (view.bindingContext[local] === items[i + first] && view.overrideContext.$middle === middle && view.overrideContext.$last === last) {
        // no changes. continue...
        continue;
      }
      // update the binding context and refresh the bindings.
      view.bindingContext[local] = items[i + first];
      view.overrideContext.$middle = middle;
      view.overrideContext.$last = last;
      let j = view.bindings.length;
      while (j--) {
        updateOneTimeBinding(view.bindings[j]);
      }
      j = view.controllers.length;
      while (j--) {
        let k = view.controllers[j].boundProperties.length;
        while (k--) {
          let binding = view.controllers[j].boundProperties[k].binding;
          updateOneTimeBinding(binding);
        }
      }
    }
    // add new views
    for (let i = viewsLength; i < repeat._viewsLength; i++) {
      let overrideContext = createFullOverrideContext(repeat, items[i], i, itemsLength);
      let view = repeat.viewFactory.create();
      view.bind(overrideContext.bindingContext, overrideContext);
      repeat.viewSlot.add(view);
    }
  }

  /**
  * Handle the repeat's collection instance mutating.
  * @param repeat The repeat instance.
  * @param array The modified array.
  * @param splices Records of array changes.
  */
  instanceMutated(repeat, array, splices) {
    this._updateViews(repeat, repeat.items, splices);
  }

  _standardProcessInstanceMutated(repeat, array, splices) {
    let removeDelta = 0;
    let viewSlot = repeat.viewSlot;
    let rmPromises = [];

    for (let i = 0, ii = splices.length; i < ii; ++i) {
      let splice = splices[i];
      let removed = splice.removed;
      let viewIndex = this._getViewIndex(repeat, viewSlot, splice.index);
      if (viewIndex >= 0) {
        for (let j = 0, jj = removed.length; j < jj; ++j) {
          let viewOrPromise = viewSlot.removeAt(viewIndex + removeDelta + rmPromises.length, true);

          // TODO Create view without trigger view lifecycle - or better solution
          let length = viewSlot.children.length;
          let overrideContext = createFullOverrideContext(repeat, repeat.items[length], length, repeat.items.length);
          let view = repeat.viewFactory.create();
          view.bind(overrideContext.bindingContext, overrideContext);
          repeat.viewSlot.isAttached = false;
          repeat.viewSlot.add(view);
          repeat.viewSlot.isAttached = true;

          if (viewOrPromise instanceof Promise) {
            rmPromises.push(viewOrPromise);
          }
        }
        removeDelta -= splice.addedCount;
       }
    }

    if (rmPromises.length > 0) {
      Promise.all(rmPromises).then(() => {
        this._handleAddedSplices(repeat, array, splices);
        this._updateViews(repeat, array, splices);
      });
    } else {
      this._handleAddedSplices(repeat, array, splices);
      this._updateViews(repeat, array, splices);
    }
  }

  _updateViews(repeat, items, splices) {
    let totalAdded = 0;
    let totalRemoved = 0;
    repeat.items = items;

    for(let i = 0, ii = splices.length; i < ii; ++i){
      let splice = splices[0];
      totalAdded += splice.addedCount;
      totalRemoved += splice.removed.length;
    }

    let index = repeat._getIndexOfFirstView() - totalRemoved;

    if(index < 0) {
      index = 0;
    }

    let  viewSlot = repeat.viewSlot;

    for(let i = 0, ii = viewSlot.children.length; i < ii; ++i){
      let view = viewSlot.children[i];
      let nextIndex = index + i;
      let itemsLength = items.length;
      if((nextIndex) <= itemsLength - 1) {
        view.bindingContext[repeat.local] = items[nextIndex];
        updateOverrideContext(view.overrideContext, nextIndex, itemsLength);
      }
    }

    let bufferDelta = repeat.itemHeight * totalAdded + repeat.itemHeight * -totalRemoved;

    if(repeat._bottomBufferHeight + bufferDelta < 0) {
      repeat._topBufferHeight = repeat._topBufferHeight + bufferDelta;
    } else {
      repeat._bottomBufferHeight = repeat._bottomBufferHeight + bufferDelta;
    }

    if(repeat._bottomBufferHeight > 0) {
      repeat.isLastIndex = false;
    }

    repeat._adjustBufferHeights();
  }

  _handleAddedSplices(repeat, array, splices) {
    let spliceIndexLow;
    let arrayLength = array.length;
    for (let i = 0, ii = splices.length; i < ii; ++i) {
      let splice = splices[i];
      let addIndex = splice.index;
      let end = splice.index + splice.addedCount;

      if (typeof spliceIndexLow === 'undefined' || spliceIndexLow === null || spliceIndexLow > splice.index) {
        spliceIndexLow = addIndex;
      }

      for (; addIndex < end; ++addIndex) {
        let overrideContext = createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength);
        let view = repeat.viewFactory.create();
        view.bind(overrideContext.bindingContext, overrideContext);
        repeat.viewSlot.insert(addIndex, view);
      }
    }

    return spliceIndexLow;
  }

  _isIndexInDom(viewSlot, index) {
    if(viewSlot.children.length === 0) {
      return false;
    }

    let indexLow = viewSlot.children[0].overrideContext.$index;
    let indexHi = viewSlot.children[viewSlot.children.length - 1].overrideContext.$index;

    return index >= indexLow && index <= indexHi;
  }

  _getViewIndex(repeat, viewSlot, index) {
    if(viewSlot.children.length === 0) {
      return -1;
    }
    let indexLow = viewSlot.children[0].overrideContext.$index;
    let viewIndex = index - indexLow;
    if(viewIndex > repeat._viewsLength - 1) {
      viewIndex = -1;
    }
    return viewIndex;
  }
}
<|endoftext|>


<commit_before>class ResultController {
  constructor($state, $log, $window, feedbackURL, SocketService) {
    'ngInject';

    this.$log = $log;
    this.$state = $state;
    this.$window = $window;
    this.SocketService = SocketService;
    this.feedbackURL = feedbackURL;

    this.current = 'result.menu';
    this.showTab();

    this.rematchOptionsHidden = true;

    this.SocketService.extendedHandler = null;
  }

  changeTab(value) {
    this.current = value;
    this.showTab();
  }

  showTab() {
    this.$state.transitionTo(this.current);
  }

  openFeedback() {
    this.$window.open(this.feedbackURL, '_blank');
  }

  createNewGame() {
    this.$state.go('select');
  }

  joinAnotherGame() {
    this.$state.go('main');
    this.SocketService.disconnect();
  }
}

export default ResultController;
<commit_msg>Disconnect when other players create game<commit_after>class ResultController {
  constructor($state, $log, $window, feedbackURL, SocketService) {
    'ngInject';

    this.$log = $log;
    this.$state = $state;
    this.$window = $window;
    this.SocketService = SocketService;
    this.feedbackURL = feedbackURL;

    this.current = 'result.menu';
    this.showTab();

    this.rematchOptionsHidden = true;

    this.SocketService.extendedHandler = (message) => {
      if(message.type === 'player_create') {
        this.SocketService.disconnect();
      }
    };
  }

  changeTab(value) {
    this.current = value;
    this.showTab();
  }

  showTab() {
    this.$state.transitionTo(this.current);
  }

  openFeedback() {
    this.$window.open(this.feedbackURL, '_blank');
  }

  createNewGame() {
    this.$state.go('select');
  }

  joinAnotherGame() {
    this.$state.go('main');
    this.SocketService.disconnect();
  }
}

export default ResultController;
<|endoftext|>