В этом практическом уроке я хочу показать вам реализацию калькулятора на чистом JavaScript. Также в ходе работы мы будем использовать Grid CSS и немного поговорим о безопасности нашего скрипта.
В качестве основы я буду использовать базовый HTML-каркас к которому подключен один CSS-файл style.css
. Откроем его и напишем базовые стили для нашего проекта. На глобальном уровне изменим алгоритм блочной модели на значение border-box
. А также сбросим внешние отступы
у элемента body
.
* {
box-sizing: border-box;
}
body {
margin: 0;
}
Разметка калькулятора
Теперь давайте перейдем к разметке. Сам калькулятор я обозначу через тег <article>
, поскольку это самодостаточный автономный компонент. И задам ему соответствующий класс calc
:
<article class="calc">
</article>
Результат вычислений калькулятора мы будем выводить в элемент <output>
, который и был для этого создан. Дадим ему класс cacl__result
, а также идентификатор result
для более простого обращения:
<article class="calc">
<!-- Результат вычислений -->
<output class="calc__result" id="result"></output>
</article>
Далее нам нужно определить кнопки нашего калькулятора. Для этого мы будем использовать элементы <button>
с типом button
и классом calc__btn
. Всего нам понадобится 18 кнопок. Для быстрого их создания воспользуемся Emmet:
button[type=button].calc__btn*18
В результате мы получим следующую структуру:
<article class="calc">
<!-- Результат вычислений -->
<output class="calc__result" id="result"></output>
<!-- Клавиши калькулятора -->
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button">/</button>
<button class="calc__btn" type="button"></button>
<button class="calc__btn" type="button"></button>
</article>
Для удобства работы визуально мы разбили все кнопки в группы по 4 элемента. Именно столько кнопок будет содержать калькулятор в каждом ряду. Теперь давайте заполним их значениями.
В первом ряду у нас будут находится цифры 7, 8, 9 и оператор плюс +
. Во втором - 4, 5, 6 и оператор минус -
. В третьем - 1, 2, 3 и оператор умножения *
. И в четвертом мы разместим 0, десятичную точку, кнопку с двумя нулями и оператор деления /
.
Последний ряд у нас содержит всего 2 элемента. Первый из них будет кнопкой сброса, обозначим ее через заглавную C
. А второй - оператором равно =
:
<article class="calc">
<!-- Результат вычислений -->
<output class="calc__result" id="result"></output>
<!-- Клавиши калькулятора -->
<button class="calc__btn" type="button">7</button>
<button class="calc__btn" type="button">8</button>
<button class="calc__btn" type="button">9</button>
<button class="calc__btn" type="button">+</button>
<button class="calc__btn" type="button">4</button>
<button class="calc__btn" type="button">5</button>
<button class="calc__btn" type="button">6</button>
<button class="calc__btn" type="button">-</button>
<button class="calc__btn" type="button">1</button>
<button class="calc__btn" type="button">2</button>
<button class="calc__btn" type="button">3</button>
<button class="calc__btn" type="button">*</button>
<button class="calc__btn" type="button">0</button>
<button class="calc__btn" type="button">.</button>
<button class="calc__btn" type="button">00</button>
<button class="calc__btn" type="button">/</button>
<button class="calc__btn" type="button">C</button>
<button class="calc__btn" type="button">=</button>
</article>
На этом с разметкой мы закончили и можем переходить к ее оформлению.
Стилизация калькулятора
Начнем мы с класса .calc
:
- установим ему ширину в
600px
- зададим видимую границу
- небольшой радиус в
10px
- фоновую заливку
- и внутренние поля в
20px
.calc {
width: 600px;
border: solid 2px #555;
border-radius: 10px;
background-color: #888;
padding: 20px;
}
Расположение кнопок
Теперь давайте займемся расположением кнопок. Поскольку мы имеем дело с двунаправленным макетом, идеальным кандидатом для него будет Grid CSS. В первую очередь, поменяем тип отображения нашего блока на grid
и установим шаблон для колонок. В данном случае мне нужно получить 4 колонки одинаковой ширины. Укажу это при помощи функции repeat
:
.calc {
width: 600px;
border: solid 2px #555;
border-radius: 10px;
background-color: #888;
padding: 20px;
display: grid;
grid-template-columns: repeat(4, 1fr);
}
Теперь я использую свойство grid-auto-rows
, чтобы задать размер для всех рядов по умолчанию. Пусть он будет равным 80px
. И последнее, что я хочу сделать, это задать отступы между нашими элементами. Для этого я использую свойство gap
в значении 20px
.
.calc {
width: 600px;
border: solid 2px #555;
border-radius: 10px;
background-color: #888;
padding: 20px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 80px;
gap: 20px;
}
Выравнивание по центру экрана
Наш калькулятор уже начинает обретать форму.Но давайте выравняем его по центру экрана. Для этого я обращусь к элементу body
и задам ему минимальную высоту, равную высоте области просмотра. Далее сделаю его флекс-контейнером и выравняю его элементы по центру главной
и поперечной оси.
body {
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
Блок с выводом результатов
Так уже гораздо лучше. Давайте перейдем к следующему блоку. А именно к блоку с выводом результата. В первую очередь, нам нужно растянуть его на всю ширину контейнера. И поскольку он является элементом сетки, мы легко можем это сделать.
Используем свойство grid-column
и зададим его расположение от первой до последней линии:
.calc__result {
grid-column: 1 / -1;
}
Как мы видим, наш блок удачно растянулся и сместил остальные элементы на новый ряд.
Давайте доработаем этот блок:
- зададим ему фоновую заливку
- поменяем цвет текста
- установим размер шрифта
- и внутренние поля
.calc__result {
grid-column: 1 / -1;
background-color: #333;
color: #fff;
font-size: 56px;
padding: 10px;
}
На этом наш блок результатов готов.
Оформление кнопок
Клавиши калькулятора мы представили элементом button
, у которого уже есть некоторые стили по умолчанию. Давайте их сбросим и назначим свои.
- в первую очередь, уберем с кнопок обводку
- добавим небольшой радиус
- изменим цвет заливки
- а также поменяем тип курсора
После чего, увеличим размер шрифта и установим небольшую тень, чтобы создать эффект выпуклых кнопок:
.calc__btn {
border: none;
border-radius: 5px;
background-color: #eee;
cursor: pointer;
font-size: 30px;
box-shadow: 2px 3px 2px rgb(0 0 0 / 50%);
}
Не забудем определить и их поведение при наведении мыши. Например, пусть это будет небольшая прозрачность:
.calc__btn:hover {
opacity: 0.9;
}
При нажатии мы также уменьшим нашу тень, чтобы создать видимость вдавленной кнопки:
.calc__btn:active {
box-shadow: 1px 1px 2px rgb(0 0 0 / 50%);
}
Операторы
И последнее, что я хочу сделать - это как-то выделить наши кнопки с операторами. Примиксуем к ним модификаторы .calc__btn_operator
. Кнопкам сброса и вычисления также добавим свои модификаторы:
-
.calc__btn_reset
(рисэт) - и
.calc__btn_equal
(иквэл)
<article class="calc">
<!-- Результат вычислений -->
<output class="calc__result" id="result"></output>
<!-- Клавиши калькулятора -->
<button class="calc__btn" type="button">7</button>
<button class="calc__btn" type="button">8</button>
<button class="calc__btn" type="button">9</button>
<button class="calc__btn calc__btn_operator" type="button">+</button>
<button class="calc__btn" type="button">4</button>
<button class="calc__btn" type="button">5</button>
<button class="calc__btn" type="button">6</button>
<button class="calc__btn calc__btn_operator" type="button">-</button>
<button class="calc__btn" type="button">1</button>
<button class="calc__btn" type="button">2</button>
<button class="calc__btn" type="button">3</button>
<button class="calc__btn calc__btn_operator" type="button">*</button>
<button class="calc__btn" type="button">0</button>
<button class="calc__btn" type="button">.</button>
<button class="calc__btn" type="button">00</button>
<button class="calc__btn calc__btn_operator" type="button">/</button>
<button class="calc__btn calc__btn_reset" type="button">C</button>
<button class="calc__btn calc__btn_equal" type="button">=</button>
</article>
Теперь перейдем к их оформлению. Пусть у операторов будет бледно-оранжевый фон:
.calc__btn_operator {
background-color: #ffe2ae;
}
Для кнопки сброса мы установим красный фон и поменяем цвет текста на белый:
.calc__btn_reset {
background-color: #ff5050;
color: #fff;
}
Кнопку вычисления мы сделаем синей, а цвет текста также поменяем на белый. Последнее, что нам осталось сделать, это растянуть кнопку "равно" до края сетки. Используем уже знакомое нам свойство grid-column
и задаем расположение от второй до последней линии:
.calc__btn_equal {
background-color: #5b50ff;
color: #fff;
grid-column: 2 / -1;
}
Интерфейс нашего калькулятора готов, пора сделать его рабочим.
Логика калькулятора на JavaScript
Для этого я создам файл calc.js
и подключу его в самом низу тела документа. Так мы будем точно уверены, что на момент исполнения сценария нам доступен весь DOM документа.
Делегирование
Для работы калькулятора мы должны отслеживать нажатие его кнопок. Первое что приходит на ум - обратиться ко всем элементам button
, и повесить на них событие click
. Однако, такая операция очень избыточна и не эффективна.
Вместо этого мы можем делегировать событие клика на сам элемент калькулятора:
- находим его по классу
calc
- и вешаем нужное нам событие
click
const calc = document.querySelector('.calc');
calc.addEventListener('click', function(event) {
});
Кто вызвал событие
Чтобы определить, какой именно элемент вызвал событие, мы можем использовать объект событий event
, а конкретно его свойство target
.
Для демонстрации давайте выведем его в консоль:
const calc = document.querySelector('.calc');
calc.addEventListener('click', function(event) {
console.log(event.target);
});
Нажимая на кнопки, мы видим, что наше событие срабатывает успешно и в консоли выводятся правильные элементы.
Ограничение цели
Однако сейчас событие сработает и при нажатии на строку результатов. Чтобы ее исключить, мы будем проверять таргет события на наличие у него класса .calc__btn
. Если такого класса нет, то мы просто выходим из функции:
const calc = document.querySelector('.calc');
calc.addEventListener('click', function(event) {
if(!e.target.classList.contains('calc__btn')) return;
console.log(event.target);
});
Теперь мы можем работать только с кнопками, исключая лишние элементы.
Значение кнопки
Поскольку значения кнопок хранятся в качестве их содержимого, мы будем получать их через свойство innerText
. Для дальнейшего использования запишем это значение в переменную value
:
const calc = document.querySelector('.calc');
calc.addEventListener('click', function(event) {
if(!e.target.classList.contains('calc__btn')) return;
let value = event.target.innerText;
});
Вывод значений в строку результатов
Теперь мы можем обратиться к блоку result
, и выводить в него полученные значения. Для этого мы также используем свойство innerText
:
const calc = document.querySelector('.calc');
const result = document.querySelector('#result');
calc.addEventListener('click', function(event) {
if(!e.target.classList.contains('calc__btn')) return;
let value = event.target.innerText;
result.innerText += value;
});
Сброс и вычисление
Последнее, что нам осталось сделать это реализовать логику сброса и вычисления, потому что сейчас эти кнопки просто выводят свои значения как и другие. Для этого я буду использовать условную конструкцию switch
по переменной value
.
При значении C
мы будем записывать в блок результата пустую строку. А по знаку равенства производить вычисления. Для этого я использую функцию eval()
, которая выполняет переданный ей JavaScript-код в виде строки. В нашем случае это содержимое строки result
. Также я использую метод toFixed()
со значением два, чтобы оставлять только 2 знака после десятичной точки.
Ну а поведение остальных клавиш мы сделаем операцией по умолчанию:
const calc = document.querySelector('.calc');
const result = document.querySelector('#result');
calc.addEventListener('click', function(event) {
if(!e.target.classList.contains('calc__btn')) return;
let value = event.target.innerText;
switch(value) {
case 'C':
result.innerText = '';
break;
case '=':
result.innerText = eval(result.innerText).toFixed(2);
break;
default:
result.innerText += value;
}
});
Вот такой не сложный скрипт у нас получился. В случае необходимости вы можете легко добавить новые операторы и реализовать их поведение, если оно отличается от обычного.
Вопрос безопасности
Теперь давайте немного поговорим о безопасности нашего скрипта. Для вычисления выражения мы используем функцию eval()
, которая считается крайне не безопасной, поскольку позволяет выполнить произвольный JavaScript.
Потенциально злоумышленник может передать в него какой-то вредоносный код. Например, давайте откроем консоль, и заменим содержимое клавиши, на команду calc.remove()
. Теперь, если мы нажмем на кнопку равно, наш калькулятор будет удален. Однако, это произойдет только в браузере у самого злоумышленника. Другие пользователи никак от этого не пострадают. Тоже самое, мы могли бы сделать и просто, выполнив эту команду в консоли.
Поскольку наш калькулятор не использует никаких пользовательских данных, функция eval()
здесь не представляет опасности. Тем не менее,
мы можем добавить еще один слой защиты, просто проверяя выражение на допустимые символы.
Для этого я буду использовать метод search
, который выводит позицию первого совпадения с регулярным выражением. В регулярном выражении, мы просто будем искать все кроме цифр и наших операторов. Если совпадений не будет, то метод search()
вернет нам значение -1
. В противном случае мы просто выходим из функции:
const calc = document.querySelector('.calc');
const result = document.querySelector('#result');
calc.addEventListener('click', function(event) {
if(!e.target.classList.contains('calc__btn')) return;
let value = event.target.innerText;
switch(value) {
case 'C':
result.innerText = '';
break;
case '=':
// Проверка выражения
if(result.innerText.search(/[^0-9*/+-]/mi) != -1) return;
// Исполнение выражения
result.innerText = eval(result.innerText).toFixed(2);
break;
default:
result.innerText += value;
}
});
Проверим, что наш калькулятор до сих пор работает. Но теперь, если мы передадим в выражение произвольный код, который содержит буквы и другие символы, то выполнен он не будет.