Проблемы с динамическим созданием кнопок с jQuery - PullRequest
0 голосов
/ 07 августа 2020

Я пытаюсь динамически добавлять кнопки операторов в свой калькулятор, но мне просто не везет. Я создал функции для динамического создания кнопок с цифрами и операторами. Числа были созданы успешно, но как только я пытаюсь добавить операторов, ничего не происходит. Я пытаюсь добавить операторы между кнопками в скобках и кнопкой оценки, используя вместо l oop. Кажется, когда я создаю кнопку оценки перед операторами, кнопка оценки создается успешно, а операторы - нет. Если я сдвину код для создания операторов перед кнопкой оценки, ни то, ни другое не появятся. Я почти уверен, что проблема заключается в моем for-in l oop, но я не совсем уверен, где. Любая помощь / руководство приветствуется!

 var opsData = {
  add: {
    precedence: 1,
    name: 'add',
    operation: function (a, b) {return a + b;},
    output: function (a, b) {return a + ' + ' + b;},
    buttonHTML: '+'
  },
  subtract: {
    precedence: 1,
    name: 'subtract',
    operation: function (a, b) {return a - b;},
    output: function (a, b) {return a + ' - ' + b;},
    buttonHTML: '-'
  },
  multiply: {
    precedence: 2,
    name: 'multiply',
    operation: function (a, b) {return a * b;},
    output: function (a, b) {return a + ' * ' + b;},
    buttonHTML: '*'
  },
  divide: {
    precedence: 2,
    name: 'divide',
    operation: function (a, b) {return a / b;},
    isInvalidInput: function (a, b) {return b == 0 ? 'division by 0' : false;},
    output: function (a, b) {return a + ' / ' + b;},
    buttonHTML: '/'
  }
}

$.fn.addButton = function(html, className, onclick) {
  $('<button />', {
    html: html,
    'class': 'button ' + className,
    click: onclick
    }).appendTo(this);
  return this;
}

var addOperatorButton = function(op, click) {
  $operators.addButton(op.buttonHTML, 'operator ' + op.name, function(e) {
    click.call(this, e);
    $currentCalc.text(inputStack.getCalculationString());
    $collapsedCalc.text(inputStack.getCalculationString(true));
    $input.text(inputStackgetPartialResult());
    $input.data({clearOnInput: true});
  });
};

var getInput = () => {
  var input = $input.text();
  return input.match(/error/i) ? 0 : parseFloat($input.text())
}

for (var i in opsData) {
  (function(i) {
    if (!opsData.buttonHTML[i]) return;
    addOperatorButton(opsData[i], () => {
      inputStack.push(getInput(), new Operation(opsData[i]));
    })
  }(i))
}

Ссылка на мой полный код здесь: https://codepen.io/tazmancooks/pen/PoNwGMX

Извините, если моя проблема не хорошо сформулирован, я еще новичок в jQuery и Javascript в целом.

Ответы [ 2 ]

2 голосов
/ 07 августа 2020

Вы обратились к объекту opsData неверно: измените if (!opsData.buttonHTML[i]) return; на if (!opsData[i].buttonHTML) return; в строке 352

Кнопки уже есть, но вам нужно чтобы изменить CSS еще.

$(function() {

  var opsData = {
    add: {
      precedence: 1,
      name: 'add',
      operation: function(a, b) {
        return a + b;
      },
      output: function(a, b) {
        return a + ' + ' + b;
      },
      buttonHTML: '+'
    },
    subtract: {
      precedence: 1,
      name: 'subtract',
      operation: function(a, b) {
        return a - b;
      },
      output: function(a, b) {
        return a + ' - ' + b;
      },
      buttonHTML: '-'
    },
    multiply: {
      precedence: 2,
      name: 'multiply',
      operation: function(a, b) {
        return a * b;
      },
      output: function(a, b) {
        return a + ' * ' + b;
      },
      buttonHTML: '*'
    },
    divide: {
      precedence: 2,
      name: 'divide',
      operation: function(a, b) {
        return a / b;
      },
      isInvalidInput: function(a, b) {
        return b == 0 ? 'division by 0' : false;
      },
      output: function(a, b) {
        return a + ' / ' + b;
      },
      buttonHTML: '/'
    }
  }

  var Operation = function(options) {
    var inputs = [];

    for (var key in options) {
      this[key] = options[key];
    };

    //Check if another input is needed, if not push current input to inputs array
    this.addInput = function(input) {
      if (this.isSaturated) return this;
      inputs.push(input)
      return this;
    }

    this.invalidInput = this.invalidInput || function() {
      return false;
    };

    //Check if operation already has all needed inputs
    this.isSaturated = () => {
      var inputCount = this.singleinput ? 1 : 2
      for (var i = 0; i < inputCount; i++) {
        if (inputs[i] === null || isNaN(inputs[i])) return false;
      }
      return true;
    }

    this.execute = () => {
      //If error is thrown, return
      if (this.error) return this;

      //Check if inputs are missing OR if operation was already executed
      if (!this.isSaturated || this.value != null) return this;

      //Map inputs to numerical values since inputs can also be operational objects [addition(1, multiplication(2, 3))]
      var inputValues = inputs.map(function(input) {
        return Number(input);
      });

      //Throw an error if there is invalid input
      this.error = this.isInvalidInput.apply(this, inputValues);
      if (this.error) {
        throw new Error(this.error);
      }

      this.calculationString = this.getCalculationString();
      this.value = this.operation.apply(this, inputValues);
      return this

    }

    this.getCalculationString = function(lastInput, collapsed) {
      if (collapsed) {
        this.execute();
        if (this.value != null) return this.value.toString();
      }

      var singleInput = this.singleInput;

      //Maps inputs to a string
      var inputValues = inputs.map(function(input) {
        var inputValue = input.getCalculationString ?
          input.getCalculationString(lastInput, collapsed) :
          input.toString();

        //Remove parenthases from any single input operations
        return singleInput ? inputValue.replace(/^\((.*)\)$/g, '$1') : inputValue;
      });
      return options.output.apply(this, inputValues.concat([lastInput]))
    }

    // Translate numerical value of the operation result
    // Execute operation if no result yet
    this.valueOf = () => {
      if (this.value == null) {
        this.execute()
      }
      return this.value
    }

    this.toString = () => {
      if (this.getCalculationString == null) {
        this.execute();
      }
      return this.getCalculationString();
    }

    var inputStack = (function() {
      var levels;

      var closedContext;

      var partialResult;

      var error;

      var Stack = function() {
        this.peek = function() {
          return this[this.length - 1];
        }
      };
      Stack.prototype = [];

      var reset = function() {
        levels = new Stack;
        levels.push(new Stack);
        closedContext = error = null
      };

      var wrapLastOperation = function(operation) {
        var stack = levels.peek();
        stack.push(operation.addInput(stack.pop()))
        collapse(operation.precedence)
      };

      var collapse = function(precedence) {
        var stack = levels.peek();
        var currentOperation = stack.pop();
        var previousOperation = stack.peek()

        if (!currentOperation) return;

        if (!currentOperation.isSaturated()) {
          stack.push(currentOperation);
          return;

          try {
            partialResult = Number(currentOperation);
          } catch (e) {
            partialResult = error = 'Error: ' + e.message;
          };

          if (previousOperation && previousOperation.precedence >= precedence) {
            previousOperation.addInput(currentOperation);
            collapse(precedence);
          } else {
            stack.push(currentOperation);
          }
        }
      };

      reset();

      return {
        push: function(number, operation) {
          //If an error already exists, reset
          error && reset();

          var stack = levels.peek();
          var lastOperation = stack.peek();

          var input = closedContext || number;
          closedContext = null

          partialResult = Number(input);

          if (!lastOperation || operation.precedence > lastoperation.precedence) {
            stack.push(operation.addInput(input))

            collapse(operation.precedence)
          } else {
            lastOperation.addInput(input);
            collapse(operation.precedence);
            wrapLastOperation(operation);
          }
          return this;
        },
        openContext: function() {
          error && reset();

          var lastOperation = levels.peek().peek();

          if (closedContext || lastOperation && lastOperation.isSaturated()) return

          //Opening new context means creating a new level to the stack
          levels.push(new Stack);

          return this;
        },
        closeContext: function(number) {
          error && reset();

          //If there's only one level, there's no need to close the context.
          if (levels.length <= 1) return;

          var inpute = closedContext || number

          //Grab last used operation
          var stack = levels.peek();
          var lastOperation = stack.peek()

          closedContext = new Operation(operationData.context).addInput(
            lastOperation ? (function() {
              lastOperation.addInput(input);
              collapse(0);
              return stack.pop();
            }()) :
            input
          )

          partialResult = Number(closedContext);

          levels.pop();
          return this;
        },
        evaluate: function(number) {
          error && reset();

          var input = closedContext || number

          //If no operator provided (just a number & '='), set result to the number.
          partialResult = Number(input);

          //If '=' is used without closing all parenthases, close the context.
          while (levels.length > 1) {
            this.closeContext(input)

            var lastOperation = levels.peek().peek();
            lastOperation && lastOperation.addInput(input);

            collapse(0);
            reset();

            return this;
          }
        },
        getPartialResult: function() {
          var _partialResult = partialResult;
          partialResult = 0;
          return _partialResult
        },
        getCalculationString: function(collapsed) {
          var result = closedContext ? closedContext.getCalculationString('', collapsed) : '';

          for (var i = levels.length - 1; i >= 0; i--) {
            for (var j = levels[i].length - 1; j >= 0; j--) {
              result = levels[i][j].getCalculationString(result, collapsed);
            }
            if (i > 0) {
              result = '(' + result;
            }
          }
          return result
        }
      }
    })

  }

  //Build calculator interface

  //Prototype for adding buttons
  $.fn.addButton = function(html, className, onclick) {
    $('<button />', {
      html: html,
      'class': 'button ' + className,
      click: onclick
    }).appendTo(this);
    return this;
  }

  var addNumberButton = function(num) {
    $numbers.addButton(num, 'number ' + (num === '.' ? 'dot' : 'number-' + num), () => {
      if ($input.text().match(/\./) && num == '.') return;
      if ($input.text() == 0 && num != '.' || $input.data('clearOnInput')) {
        $input.text('');
      }
      $input.data({
        clearOnInput: false
      });
      $input.text($input.text() + $(this).text());
    })
  };

  var addOperatorButton = function(op, click) {
    $operators.addButton(op.buttonHTML, 'operator ' + op.name, function(e) {
      click.call(this, e);
      $currentCalc.text(inputStack.getCalculationString());
      $collapsedCalc.text(inputStack.getCalculationString(true));
      $input.text(inputStackgetPartialResult());
      $input.data({
        clearOnInput: true
      });
    });
  };

  var getInput = () => {
    var input = $input.text();
    return input.match(/error/i) ? 0 : parseFloat($input.text())
  }

  var $calculator = $('#calculator');
  var $calcDisplay = $('<div/>', {
    'class': 'calcDisplay'
  }).appendTo($calculator);
  var $currentCalc = $('<div/>', {
    'class': 'currentCalc'
  }).appendTo($calcDisplay);
  var $collapsedCalc = $('<div/>', {
    'class': 'collapsedCalc'
  }).appendTo($calcDisplay);
  var $input = $('<div/>', {
    'class': 'input'
  }).appendTo($calcDisplay);
  var $numbers = $('<div/>', {
    'class': 'numbers'
  }).appendTo($calculator);
  var $operators = $('<div/>', {
    'class': 'operators'
  }).appendTo($calculator);

  $numbers.addButton('del', 'del', () => {
    $input.text($input.text().replace(/.$/, ''))
  })

  $numbers.addButton('CE', 'clear-entry', () => {
    $input.text('0')
  })

  $numbers.addButton('C', 'clear')

  $.each('7894561230.'.split(''), (itm, value) => {
    addNumberButton(value)
  });

  addOperatorButton({
    buttonHTML: '(',
    name: 'openContext'
  }, () => {
    inputStack.openContext();
  });

  addOperatorButton({
    buttonHTML: ')',
    name: 'closeContext'
  }, () => {
    inputStack.closeContext(getInput());
  });


  for (var i in opsData) {
    (function(i) {
      if (!opsData[i].buttonHTML) return;
      addOperatorButton(opsData[i], () => {
        inputStack.push(getInput(), new Operation(opsData[i]));
      })
    }(i))
  }

  addOperatorButton({
    buttonHTML: '=',
    name: 'evaluate'
  }, () => {
    inputStack.evaluate(getInput());
  });
});
html,
body {
  background-color: black;
}

#calculator {
  background-color: grey;
  width: 250px;
  padding-top: 20px;
  padding-bottom: 30px;
  border-bottom-right-radius: 2em;
  border-bottom-left-radius: 2em;
  margin-right: auto;
  margin-left: auto;
  overflow: auto;
}

.calcDisplay {
  border: 1px solid;
  height: 50px;
  margin: 4px;
  padding: 2px;
  text-align: right;
  overflow: hide;
  position: relative;
  background: white;
}

.button {
  width: 60px;
  height: 60px;
  padding: 0px;
  line-height: 30px;
  text-align: center;
  border: 1px solid;
  cursor: pointer;
  float: left;
  margin: 4px;
  border-radius: 50%;
}

.number {
  background: #fff;
}

.number-0 {
  width: 129px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='calculator'></div>
1 голос
/ 07 августа 2020

Проблема заключается в том, что вы пытаетесь выполнить l oop через объект opsData.

for (var i in opsData) {
  (function(i) {
    if (!opsData.buttonHTML[i]) return;
      addOperatorButton(opsData[i], () => {
      inputStack.push(getInput(), new Operation(opsData[i]));
    })
  }(i))
}

Вы получаете доступ к свойству [i] кнопки HTML, когда вам нужно получить доступ к opsData [i ]. кнопка HTML

...