Пример создания калькулятора на основе HTML-элемента

1
input type="range"
. Это слайдер, у которого можно плавно изменять значения при помощи ползунка. Текущее значение хранится в атрибуте
1
value
данного элемента.

На этом принципе и основана работа калькулятора, который рассмотрим ниже.

Что будет считать калькулятор? В качестве примера - сумму чаевых в кафе. Есть основная сумма ($), задается процент (%) чаевых для основной суммы.

Нужно посчитать результирующую сумму ($).

HTML разметка

Разметка калькулятора основана на элементах

1
form
,
1
input type="text"
,
1
input type="range"
. Она простая и представлена ниже:

<div class="wrapper">
    <h1 class="wrapper__title">javascript slider calculator</h1>
    <form class="calculator">
        <div class="calculator__row">
            <label for="bill">enter the bill amount for your meal: $</label>
            <input type="text" id="bill" class="calculator__bill" value="5" required/>
        </div>
        <div class="calculator__row">
            <label for="tip">tip amount: <span class="tip-amount"></span></label>
            <input type="range" min="0" max="100" value="0" step="1" class="calculator__tip" id="tip" required/>
        </div>
        <div class="calculator__row">
            <h2 class="calculator__info">tip to leave: <span class="calculator__result"></span></h2>
        </div>
    </form>
</div>

CSS стили

Стилизация для калькулятора также не отличается объемом и сложностью. Что-либо отмечать отдельно нет необходимости:

.wrapper {
    width: 800px;
    margin: 50px auto 0;
    padding: 60px 30px;
    border: 1px solid #000;
}

.wrapper__title {
    text-transform: capitalize;
    margin: 0 0 40px;
}

.calculator__row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 0 0 30px;
}

.calculator__row:last-child {
    margin-bottom: 0;
}

.calculator__row label {
    text-transform: capitalize;
    font-weight: 700;
}

.calculator__bill {
    width: 50%;
}

.calculator__tip {
    width: 60%;
}

.calculator__info {
    text-transform: capitalize;
    margin: 0;
}

JavaScript код

Функция для вычисления результирующей суммы “повешена” на элемент

1
input type="range"
и событие
1
change
.

Другими словами, при перемещении ползунка слайдера каждый раз будет вызываться функция и производится пересчет на основе изменившегося значения атрибута

1
value
.

Принцип работы функции следующий. “Забираем” значения из элементов

1
input type="text"
и
1
input type="range"
. Первое значение - эту основная сумма ($); второе значение - процент (%) от основной суммы ($).

Получаем результирующую сумму путем сложения - основной суммы ($) + процент (%) от основной суммы. Результирующий вывод округляем до двух значений после запятой при помощи метода toFixed().

Помимо этого, проверяем правильность и наличие ввода в поле основной суммы ($)

1
input type="text"
. Условие будет верным, если поле ввода окажется не пустым и содержащим только цифры:

// RANGE FUNCTION
// ----------------------------------------------------------
calculatorTip.on('change', function () {

    if ( calculatorBill.val() === '' || isNaN( calculatorBill.val() ) ) {
        alert('Enter bill amount, please!')
    } else {
        amount = calculatorBill.val() * 1;
    }

    tipAmount.text( calculatorTip.val() + '%' );
    percent = calculatorTip.val() * 1;
    result = amount + amount * ( percent / 100 );
    calculatorResult.text( result.toFixed(2) + '$' );
});

Для “украшательства” можно создать еще одну функцию, которая будет запускаться при загрузке страницы и инициализировать значения суммы ($), процентов (%) от суммы и результирующей суммы ($):

// INIT FUNCTION
// ----------------------------------------------------------
$(window).on('DOMContentLoaded', function () {
    tipAmount.text( calculatorTip.val() + '%' );
    amount = calculatorBill.val() * 1;
    percent = calculatorTip.val() * 1;
    result = amount + amount * ( percent / 100 );
    calculatorResult.text( result.toFixed(2) + '$' );
});

Весь JavaScript-код для обработки калькулятора можно представить ниже:

$(document).ready( function () {


	// VARIABLES
	// ----------------------------------------------------------

	var amount, percent, result;
	var calculator = $('.calculator');
	var calculatorBill = calculator.find('.calculator__bill');
	var calculatorTip = calculator.find('.calculator__tip');
	var calculatorResult = calculator.find('.calculator__result');
	var tipAmount = calculator.find('.tip-amount');


	// INIT FUNCTION
	// ----------------------------------------------------------

	$(window).on('DOMContentLoaded', function () {
	    tipAmount.text( calculatorTip.val() + '%' );
	    amount = calculatorBill.val() * 1;
	    percent = calculatorTip.val() * 1;
	    result = amount + amount * ( percent / 100 );
	    calculatorResult.text( result.toFixed(2) + '$' );
	});


	// RANGE FUNCTION
	// ----------------------------------------------------------

	calculatorTip.on('change', function () {

	    if ( calculatorBill.val() === '' || isNaN( calculatorBill.val() ) ) {
	        alert('Enter bill amount, please!')
	    } else {
	        amount = calculatorBill.val() * 1;
	    }

	    tipAmount.text( calculatorTip.val() + '%' );
	    percent = calculatorTip.val() * 1;
	    result = amount + amount * ( percent / 100 );
	    calculatorResult.text( result.toFixed(2) + '$' );
	});

});

Готовый пример калькулятора-слайдера можно посмотреть здесь - JavaScript Slider Calculator


На этом все. В ближайшее время буду продолжать нести JavaScript-flood )

Обзор посвящен созданию JavaScript-калькулятора. Домашнее задание для каждого начинающего JavaScript-ниндзя! ) В коде используется не чистый JavaScript, а JavaScript + jQuery.

Пример и материал не является оригинальным, основан на зарубежном источнике. Указывать источник не буду, потому что это не имеет смысла - таких кратких видеоуроков просто огромное количество - выбирай любой, какой понравится. )

HTML разметка

HTML-разметка для будущего калькулятора основана на HTML-элементах

1
form
,
1
input
,
1
button
. Ничего сверхестественного в ней нет.

Единственный момент - вопрос компоновки кнопок, куда правильно “засунуть” кнопки с арифметическими операциями:

<!-- CALCULATOR -->
<div class="calculator">
    <!-- CALCULATOR FORM -->
    <form class="calculator__form">
        <!-- CALCULATOR ROW -->
        <div class="calculator__row">
            <input class="calculator__display" id="display" type="text" disabled />
        </div>
        <!-- CALCULATOR ROW -->
        <div class="calculator__row">
            <button type="button" value="c" class="calculator__key calculator__clear"></button>
            <button type="button" value="<--" class="calculator__key calculator__backspace"></button>
            <button type="button" value="^3" class="calculator__key calculator__power"></button>
            <button type="button" value="+" class="calculator__key calculator__button"></button>
        </div>
        <!-- CALCULATOR ROW -->
        <div class="calculator__row">
            <button type="button" value="9" class="calculator__key calculator__button"></button>
            <button type="button" value="8" class="calculator__key calculator__button"></button>
            <button type="button" value="7" class="calculator__key calculator__button"></button>
            <button type="button" value="-" class="calculator__key calculator__button"></button>
        </div>
        <!-- CALCULATOR ROW -->
        <div class="calculator__row">
            <button type="button" value="6" class="calculator__key calculator__button"></button>
            <button type="button" value="5" class="calculator__key calculator__button"></button>
            <button type="button" value="4" class="calculator__key calculator__button"></button>
            <button type="button" value="*" class="calculator__key calculator__button"></button>
        </div>
        <!-- CALCULATOR ROW -->
        <div class="calculator__row">
            <button type="button" value="3" class="calculator__key calculator__button"></button>
            <button type="button" value="2" class="calculator__key calculator__button"></button>
            <button type="button" value="1" class="calculator__key calculator__button"></button>
            <button type="button" value="/" class="calculator__key calculator__button"></button>
        </div>
        <!-- CALCULATOR ROW -->
        <div class="calculator__row">
            <button type="button" value="0" class="calculator__key calculator__button"></button>
            <button type="button" value="." class="calculator__key calculator__button"></button>
            <button type="button" value="=" class="calculator__key calculator__key--equal"></button>
        </div>
    </form>
</div>

CSS стили

Со стилизацией будущего калькулятора тоже проблем не должно возникнуть. Я использовал flexbox для выравнивания кнопок:

.calculator {
    width: 250px;
    height: 350px;
    border: 2px solid black;
    margin: 100px auto 0;
    text-align: center;
    background-color: yellowgreen;
    box-shadow: 0 0 30px grey;
    border-radius: 4px;
}

.calculator__form {
    height: 100%;
    padding: 20px;
}

.calculator__row {
    margin: 10px 0;
    display: flex;
    justify-content: space-between;
}

.calculator__display {
    margin: 0 0 20px;
    width: 100%;
    border: 1px solid darkslateblue;
    padding: 4px 2px;
    text-align: right;
    font: 700 16px/1 Arial, sans-serif;
    color: darkslateblue;
    background-color: whitesmoke;
}

.calculator__key {
    width: 41px;
    height: 35px;
    cursor: pointer;
    border: none;
    transition: all .2s;
    text-transform: uppercase;
}

.calculator__key--equal {
    width: 98px;
}

.calculator__key:hover {
    background-color: yellow;
    font-weight: 700;
}

.calculator__key::-moz-focus-inner {
    border: none
}

JavaScript код

Переходим к самому интересному - созданию JavaScript-кода для калькулятора. В своем примере я использовал JavaScript + jQuery.

Последний применил только из-за удобства манипуляции с DOM. Недавно узнал о существовании библиотеки Underscore.js как альтернативы jQuery, но меньшего размера. Надо опробовать Underscore.js обязательно! )

Первым делом инициализируем кнопки калькулятора. Для этого забираем значения из атрибута

1
value
кнопок
1
button
и динамически вставляем в HTML-разметку:

// INIT CALC KEYS
calcKeys.each(function () {
    var current = $(this).attr('value');
    $(this).text(current);
});

Исходный вид калькулятора подготовили - он теперь выглядит как настоящий калькулятор! Если не принимать во внимание кошмарных цветов и их сочетания ) Ну мы не дизайнеры, нам простительно )

Теперь стоит задача - при нажатии на кнопку калькулятора чтобы ее значение появлялось в окошке последнего. Тоже ничего сложного, если подумать.

Просто нужно забрать у активной кнопки значение ее атрибута

1
value
и передать как значение элемента
1
input
.

Небольшая тонкость здесь - нужно конкатенировать текущее значение элемента

1
button
с текущим значением элемента
1
input
; иначе будет происходить простое замещение предыдущего значения элемента
1
input
текущим значением элемента
1
button
:

// ADD NUMBERS TO INPUT
calcButton.on('click', function () {
    calcDisplay.val( calcDisplay.val() + $(this).attr('value') );
});

Полная очистка экрана калькулятора элементарно проста - достаточно при нажатии на соответствующую кнопку передать в элемент

1
input
пустое значение:

// CLEAR INPUT
calcClear.on('click', function () {
    calcDisplay.val('');
});

Дальше уже немного интереснее - будем оживлять кнопку

1
=
. То есть - ввели в окошко калькулятора, к примеру,
1
2 + 3
. По нажатии на кнопку
1
=
в окошке должен появиться результат этой арифметической операции.

На помощь приходит функция JavaScript под названием

1
eval()
. На javascript.ru эта функция подробно расписана.

В результате код для “оживления” кнопки

1
=
будет выглядеть “скромно”:

// SHOW RESULT
calcEqual.on('click', function () {
    calcDisplay.val( eval( calcDisplay.val() ) );
});

Наш калькулятор почти готов. Осталось добавить жизни к двум последним кнопкам - возведения в степень и посимвольной очистки экрана калькулятора.

Для возведения в степень воспользуемся стандартной JavaScript-библиотекой

1
Math
и ее методом
1
pow()
.

Заберем у элемента

1
input
его текущее значение и передадим в качестве одного из аргументов в метод
1
pow()
. Второй аргумент в нашем случае - это константа 3:

// POWER BUTTON
calcPower.on('click', function () {
    calcDisplay.val( Math.pow( calcDisplay.val(), 3 ) );
});

Последний шаг к успеху создания калькулятора - это “оживление” кнопки посимвольной очистки экрана калькулятора.

В этом случае воспользуемся стандартным методом JavaScript -

1
substring()
. Это метод JavaScript(), который извлекает из строки подстроку и возвращает ее в виде новой строки - почитать с примерами.

В качестве аргументов принимает два параметра - начальную и конечную позицию, которые выступают как начальный и конечный индекс массива. Массивом в данном случае является строка.

Функция (на кнопке посимвольной очистки) в нашем случае будет работать так - забираем у элемента

1
input
его текущее значение. Значение возвращается в виде строки, конечно.

Поэтому находим ее длину (

1
length
) и укорачиваем на один (последний) символ -
1
length - 1
. Таким образом мы динамически укорачиваем текущее значение в окне калькулятора на один символ.

Затем нам нужно заменить текущее значение окна калькулятора укороченным на один символ значением. Для этого берем метод

1
substring()
и с помощью него “обрезаем” текущую строку; этот метод возвращает “обрезанный” вариант. Нам осталось только вставить его в окно калькулятора.

Результат будет выглядеть таким образом:

// BACKSPACE BUTTON
calcSpace.on('click', function () {
    calcDisplay.val( calcDisplay.val().substring(0, calcDisplay.val().length-1) );
});

Вот наш калькулятор и готов - ниже полный JavaScript-код:

$(document).ready(function () {

    // VARIABLES
    var calc = $('.calculator');
    var calcDisplay = calc.find('.calculator__display');
    var calcKeys = calc.find('.calculator__key');
    var calcButton = calc.find('.calculator__button');
    var calcClear = calc.find('.calculator__clear');
    var calcEqual = calc.find('.calculator__key--equal');
    var calcPower = calc.find('.calculator__power');
    var calcSpace = calc.find('.calculator__backspace');

    // INIT CALC KEYS
    calcKeys.each(function () {
        var current = $(this).attr('value');
        $(this).text(current);
    });

    // ADD NUMBERS TO INPUT
    calcButton.on('click', function () {
        calcDisplay.val( calcDisplay.val() + $(this).attr('value') );
    });

    // CLEAR INPUT
    calcClear.on('click', function () {
        calcDisplay.val('');
    });

    // SHOW RESULT
    calcEqual.on('click', function () {
        calcDisplay.val( eval( calcDisplay.val() ) );
    });

    // POWER BUTTON
    calcPower.on('click', function () {
        calcDisplay.val( Math.pow( calcDisplay.val(), 3 ) );
    });

    // BACKSPACE BUTTON
    calcSpace.on('click', function () { // http://www.w3schools.com/jsref/jsref_substring.asp
        calcDisplay.val( calcDisplay.val().substring(0, calcDisplay.val().length-1) );
    });

});

Конечно, примеры кода из этой статьи хоть и не являются укороченными, но не дают полного представления о разрабатываемом нами калькуляторе.

Поэтому привожу ссылку на готовый вариант калькулятора, который создавался в этой статье. На CodePen можно его посмотреть и разобрать детально (при желании).

Как вариант для сравнения, можно взглянуть на более сложный пример создания калькулятора, на чистом JavaScript. Пример был найден мною на просторах CodePen.

Заключение

Рассмотренный в статье пример лично для меня оказался на удивление прост - я ожидал большей (и более запутанной) кучи кода.

На этом все. В дальнейшем буду продолжать флудить на тему JavaScript, ибо для себя с удивлением обнаружил, что мне в последнее вермя доставляет удовольствие разбирать и рассматривать готовые примеры на JavaScript.

Единственное, что меня огорчает - тот факт, что они чужие )


Имеется такая задача - на странице расположен блок. Страница скроллится, то есть наличествует событие scroll.

Нужно сделать так, чтобы при возникновении события scroll и выполнении определенного условия срабатывала некая функция. Но при этом срабатывала только один раз!

Вариантов решения этой задачи может быть несколько. В этой статье будут показаны два из них. Все они будут выполнены на JavaScript + jQuery.

Вариант первый - метод off()

Этот способ основан на методе jQuery - .off(). Как говорится в описании, метод

1
.off()
удаляет событие, вызванное с помощью метода
1
.on()
. То есть, выполняет обратную операцию.

Рассмотрим пример ниже:

var blockScrolled = $('.scrolled');
var spins = $('.spin');

spins.text('0');

$(window).on('scroll', function () {
    if ( $(window).scrollTop() > blockScrolled.offset().top - $(window).height() / 2 ) {
        spins.each( function () {
            var current = $(this);
            $( { Spin: 0 }).animate( { Spin: current.attr('data-stop') }, {
                duration: 2000,
                easing: 'swing',
                step: function ( now ) {
                    current.text( Math.ceil( now ) );
                }
            });
        });
        $(window).off('scroll');
    }
});

Что мы имеем в этом коде? Видим, что на окно

1
window
повешен обработчик ( метод
1
.on()
`) события scroll.

При возникновении этого события выполняется проверка условия. Если условие true, то для каждого из элементов коллекции spins выполнить функцию.

Как только функция закончит свою работу, выполнение кода перемещается на строку

1
$(window).off('scroll')
. Эта строка “говорит” - на объекте
1
window
остановить событие scroll.

Мы получили нужный результат - при событии scroll и выполнении условия функция выполнится только один раз.

Один важный момент - после выполнения строки

1
$(window).off('scroll')
событие scroll будет “прибито” и дальнейшие попытки повесить handler на него не увенчаются успехом.

Другими словами, если дальше на странице есть еще элементы, на которых нужно обработать событие scroll, то ничего не получится.

Вариант второй - маркер

Второй способ не использует метод

1
.off()
, но применяет специальный маркер. Код простой, но эффективный. И в этом случае событие scroll не “убивается”, а функция исполняется один раз:

var marker = true;

function count() {
    // function code here ...
    marker = false;
}

$( window ).on('scroll', function () {
    if ( $( window ).scrollTop() > overview.offset().top - $( window ).height() * 0.5 ) {
        if ( marker ) {
            count();
        }
    }
});

Что есть в этом коде? Есть глобальная переменная

1
marker
, у которой изначально значение
1
true
.

При выполнении условия функция

1
count()
запускается и по ходу дела меняет значение
1
marker
на противоположный. Таким образом функция
1
count()
сработает только один раз.

Но событие scroll останется “невредимым”.

Заключение

За бортом остался еще один jQuery-метод - .one(). Его задача - также запустить на исполнение функцию только один раз.

Например, в коде ниже клик мыши на каждом из элементов коллекции likeLinks сработает только один раз:

var likeLinks = $('.overlay__link-heart');
likeLinks.one('click', function () {

// увеличить значение счетчика на единицу при клике
// -----------------------------------------------------------
var currentCount = $(this).find('.overlay__link-count');
currentCount.html( currentCount.html() * 1 + 1 );

Однако, применить метод

1
.one()
для обработки события scroll и вызова на исполнение функции только один раз у меня не получилось. Событие scroll вообще не срабатывало.

На этом все. Похоже на то, что все верно написал и описал. Но если будут здравые комментарии и замечания, то буду только рад )


P.S. Сразу на ум пришло одно у самого себя - заменить

1
$( window )
на переменную. Но уже лень менять. )

Создание игры Dice Game (игра в кости) на JavaScript. Вот нашлось время и силы познакомиться с игрой такого типа. Сразу оговорюсь, что материал и пример не мой, а взят из зарубежного ресурса для изучения.

В примере рассмотрен основной принцип создания игры Dice. Можно сказать - чисто схематично. Однако этого достаточно, чтобы остальное дополнить по необходимости.

Код оказался на удивление прост. Я не сомневаюсь, что в Интернете есть примеры более сложного и совершенного JavaScript-кода для игр подобного типа. Но цель статьи - познакомиться с основным принципом создания игры, не более.

HTML разметка и стили

Разметка очень простая и состоит из четырех элементов:

<div class="wrapper">
    <div class="column">
        <div id="dice-side-1" class="dice">0</div>
        <div id="dice-side-2" class="dice">0</div>
    </div>
    <div class="column">
        <button type="button" class="dice-roll">roll dice</button>
        <h2 id="status"></h2>
    </div>
</div>

Элементы

1
id="dice-side-1"
и
1
id="dice-side-2"
- это иммитация двух игральных костей; как если бы эти кости были обращены лицевой стороной к зрителю.

Кнопка

1
class="dice-roll"
служит для управления; с помощью нее мы будем “бросать” игральные кости.

Заголовок

1
id="status"
носит информативный характер - в нем будет выводиться текущая информация.

CSS-стили в комментариях не нуждаются, ибо они небольшого размера и “прозрачные”:

body {
    position: relative;
}

.wrapper {
    width: 400px;
    margin: 0 auto;
    display: flex;
    justify-content: space-between;
}

.column {
    display: flex;
    align-items: center;
}

#status {
    position: absolute;
    top: 1rem;
    right: 1rem;
    min-width: 400px;
    margin: 0;
}

.dice {
    width: 32px;
    float: left;
    background-color: lightcoral;
    border: 1px solid black;
    padding: 10px;
    font-size: 24px;
    text-align: center;
    margin: 5px;
    border-radius: 5px;
}

.dice-roll {
    cursor: pointer;
    text-transform: capitalize;
}

JavaScript - оживляем игральные кости

Далее приступим к более интересной части задачи - созданию js-кода для “оживления” наших игральных костей.

Для этого “выберем” из HTML кнопку и “повесим” на нее функцию rollDice для обработки клика по кнопке:

window.addEventListener('DOMContentLoaded', function () {

    var buttonRollDice = document.querySelector('.dice-roll');
    buttonRollDice.addEventListener('click', rollDice, false);

}, false);

Затем начнем описывать функцию rollDice. Создадим три переменные, в которые поместим обе игральные кости и информативный заголовок:

window.addEventListener('DOMContentLoaded', function () {

    function rollDice () {

        var diceSide1 = document.getElementById('dice-side-1');
        var diceSide2 = document.getElementById('dice-side-2');
        var status = document.getElementById('status');

    }

}, false);

Сгенерируем два случайных числа из диапазона от 1 до 6. Игральная кость имеет шесть сторон - поэтому такой диапазон. Эти числа будут иммитацией одной из шести сторон каждой игральной кости.

Другими словами. На всех сторонах игрального кубика “выбито” точками число - от 1 до 6. Поэтому можно сказать иначе - диапазон от 1 до 6 - это диапазон возможных значений, которые выпадают на каждой из игральных костей:

window.addEventListener('DOMContentLoaded', function () {

    function rollDice () {

        var diceSide1 = document.getElementById('dice-side-1');
        var diceSide2 = document.getElementById('dice-side-2');
        var status = document.getElementById('status');

        var side1 = Math.floor( Math.random() * 6 ) + 1;
        var side2 = Math.floor( Math.random() * 6 ) + 1;
        var diceTotal = side1 + side2;

    }

}, false);

Переменная

1
diceTotal
служит для информативных целей - будем показывать сообщение о том, сколько в сумме выпало очков на игральных костях.

Осталось поместить случайно сгенерированную пару чисел в HTML-код. Помимо этого вывести информационное сообщение, сколько очков в сумме выпало. И предупредить, если на обеих костях выпавшее число одинаковое - тогда предоставить игроку еще один ход:

window.addEventListener('DOMContentLoaded', function () {

    function rollDice () {

        var diceSide1 = document.getElementById('dice-side-1');
        var diceSide2 = document.getElementById('dice-side-2');
        var status = document.getElementById('status');

        var side1 = Math.floor( Math.random() * 6 ) + 1;
        var side2 = Math.floor( Math.random() * 6 ) + 1;
        var diceTotal = side1 + side2;

        diceSide1.innerHTML = side1;
        diceSide2.innerHTML = side2;

        status.innerHTML = 'You rolled ' + diceTotal + '.';

        if ( side1 === side2 ) {
            status.innerHTML += ' Doubles! You get a free turn!';
        }
    }

}, false);

Наш JavaScript-код готов и выглядит таким несложным способом:

window.addEventListener('DOMContentLoaded', function () {

    function rollDice () {

        var diceSide1 = document.getElementById('dice-side-1');
        var diceSide2 = document.getElementById('dice-side-2');
        var status = document.getElementById('status');

        var side1 = Math.floor( Math.random() * 6 ) + 1;
        var side2 = Math.floor( Math.random() * 6 ) + 1;
        var diceTotal = side1 + side2;

        diceSide1.innerHTML = side1;
        diceSide2.innerHTML = side2;

        status.innerHTML = 'You rolled ' + diceTotal + '.';

        if ( side1 === side2 ) {
            status.innerHTML += ' Doubles! You get a free turn!';
        }
    }

    var buttonRollDice = document.querySelector('.dice-roll');
    buttonRollDice.addEventListener('click', rollDice, false);

}, false);

Готовый пример Dice Game можно потестить по этой ссылке - JavaScript Dice Game.

Заключение

В заключение, как мне кажется, стоит сказать пару слов по поводу рассмотренного примера. Он является достаточно схематичным. Однако никто не мешает дополнить код возможностью смены изображений, иммитирующих грани костей.

Плюс - добавить анимацию. И получится весьма неплохой результат, как мне представляется.


На этом все.

Знакомство с техникой Drag-and-Drop и механизмом ее реализации с помощью JavaScript. Данный обзор не претендует на полноту покрытия материала. Задача статьи - познакомиться с созданием Drag-and-Drop на JavaScript. Понять сам принцип механизма и научиться применять основные инструменты для его реализации.

Определение Drag-and-Drop

Сам механизм Drag-and-Drop интуитивно понятен - “схватил-перетащил-бросил”. Преимущество внедрения Drag-and-Drop в интерфейсы заключается в упрощении реализации задач; в уменьшении количества пунктов меню типа “Copy-Paste”.

События Drag-and-Drop

Механизм Drag-and-Drop имеет в своем составе целую группу событий, с помощью которых можно контролировать процесс перетаскивания:

  • 1
    
    dragstart
    - пользователь начинает перетаскивание элемента
  • 1
    
    dragenter
    - перетаскиваемый элемент входит в область целевого объекта
  • 1
    
    dragover
    - перетаскиваемый элемент перемещается в области целевого объекта
  • 1
    
    dragleave
    - перетаскиваемый элемент покидает область целевого объекта
  • 1
    
    drag
    - момент начала процесса перетаскивания объекта
  • 1
    
    drop
    - момент, когда отпускается зажатая клавиша мыши (перетаскиваемый объект “роняется”)
  • 1
    
    dragend
    - момент завершения процесса перетаскивания объекта

Объект dataTransfer

Механизм Drag-and-Drop также имеет в своем составе объект dataTransfer, который служит для вспомогательных целей. В этом объекте хранится необходимая информация о событии перетаскивания. Помимо этого, в объект dataTransfer можно добавлять данные; а также считывать из него данные.

Свойства (наиболее важные) объекта dataTransfer:

  • 1
    
    dataTransfer.effectAllowed
    - задаем тип перетаскивания, которое пользователь может выполнять с элементом
  • 1
    
    dataTransfer.dropEffect
    - задаем внешний вид курсора мыши в соответствии с заданным типом перетаскивания

Методы (наиболее важные) объекта dataTransfer:

  • 1
    
    setData()
    - добавляет данные в нужном формате
  • 1
    
    clearData()
    - удаляет данные
  • 1
    
    setDragImage()
    - устанавливает изображение для перетаскивания с координатами курсора (0, 0 — левый верхний угол)
  • 1
    
    getData()
    - возвращает данные

Ниже будет рассматриваться практический пример реализации Drag-and-Drop на JavaScript.

HTML разметка

Базовая HTML-разметка будет простой:

<h2 id="dropStatus">application status</h2>
<h1 id="dropTitle">drop zone</h1>
<div id="dropZone"></div>
<div id="objectsZone">
    <div id="object1" class="objects">object 1</div>
    <div id="object2" class="objects">object 2</div>
    <div id="object3" class="objects">object 3</div>
</div>
<hr/>
<button type="button" id="readDropZone">get object data</button>

Что для чего служит в этой разметке?

Заголовок

1
id="dropStatus"
будет отображать текущее состояние процесса Drag-and-Drop. В него мы будет отправлять информацию о текущем состоянии Drag-and-Drop при помощи событий, о который говорилось выше.

Заголовок

1
id="dropTitle"
служит просто для декоративных целей.

Блок

1
id="dropZone"
является целевой областью - в нее мы будет перетаскивать объекты.

Объекты

1
id="object1"
,
1
id="object2"
,
1
id="object3"
- это перетаскиваемые объекты; их мы будем перемещать в область блока
1
id="dropZone"
.

Кнопка

1
id="readDropZone"
будет выводить информацию об перемещенных объектах.

В итоге разметка совместно со стилями будут выглядеть таким образом - JavaScript - Drag’n’Drop - Part 1.

JavaScript - разбираемся с событиями

Прежде чем детально рассматривать работу каждой из будущих функций по обработке событий, мне кажется, будет лучше просто понять, какие события и куда мы будем “вешать”.

Итак, начнем с перетаскиваемых элементов

1
id="object1"
,
1
id="object2"
,
1
id="object3"
. На каждый из них мы повесим два события:

  • 1
    
    dragstart
    - событие начала процесса перетаскивания элемента
  • 1
    
    dragend
    - событие окончания процесса перетаскивания элемента

Для каждого из элементов, при возникновении на нем события, мы будем запускать соответствующую функцию

1
dragStart
или
1
dragEnd
:

var objects = document.querySelectorAll('#objectsZone > .objects');
...
if ( objects ) {
    [].forEach.call(objects, function (el) {
        el.setAttribute('draggable', 'true');
        el.addEventListener('dragstart', dragStart, false);
        el.addEventListener('dragend', dragEnd, false);
    });
}

Обратим внимание на строку

1
el.setAttribute('draggable', 'true');
- здесь мы динамически добавляем для всех элементов с классом
1
.objects
атрибут
1
draggable="true"
, тем самым делая (благодаря HTML5) эти элементы доступными для перетаскивания.

На элемент

1
id="dropZone"
мы “повесим” гораздо больше событий:

  • 1
    
    dragenter
    - перетаскиваемый объект (например,
    1
    
    id="object1"
    ) входит в область целевого объекта (
    1
    
    id="dropZone"
    )
  • 1
    
    dragleave
    - перетаскиваемый объект (например,
    1
    
    id="object1"
    ) выходит из области целевого объекта (
    1
    
    id="dropZone"
    )
  • 1
    
    dragover
    - перетаскиваемый объект (например,
    1
    
    id="object1"
    ) перемещается внутри области целевого объекта (
    1
    
    id="dropZone"
    )
  • 1
    
    drop
    - перетаскиваемый объект (например,
    1
    
    id="object1"
    ) помещается внутри целевого объекта (
    1
    
    id="dropZone"
    )

И конечно же, для каждого события будет своя функция. JavaScript-код в итоге будет выглядеть таким образом:

var dropZone = document.querySelector('#dropZone');
...
if ( dropZone ) {
    dropZone.addEventListener('dragenter', dragEnter, false);
    dropZone.addEventListener('dragleave', dragLeave, false);
    dropZone.addEventListener('dragover', dragOver, false);
    dropZone.addEventListener('drop', dragDrop, false);
}

Ну и на кнопку

1
id="readDropZone"
мы “повесим” обычный код с функцией
1
readZone
:

var dropButton = document.querySelector('#readDropZone');
...
if ( dropButton ) {
    dropButton.addEventListener('click', readZone, false);
}

Если суммировать все вышесказанное, то общий вид handler’ов в нашем случае будет выглядеть таким образом:

// LISTENERS

if ( objects ) {
    [].forEach.call(objects, function (el) {
        el.setAttribute('draggable', 'true');
        el.addEventListener('dragstart', dragStart, false);
        el.addEventListener('dragend', dragEnd, false);
    });
}

if ( dropZone ) {
    dropZone.addEventListener('dragenter', dragEnter, false);
    dropZone.addEventListener('dragleave', dragLeave, false);
    dropZone.addEventListener('dragover', dragOver, false);
    dropZone.addEventListener('drop', dragDrop, false);
}

if ( dropButton ) {
    dropButton.addEventListener('click', readZone, false);
}

Далее будет детально останавливаться на каждой из функций - что она делает и для чего.

Функция dragStart (event)

Начнем с начала и запустим функцию для обработки старта события перетаскивания - события

1
dragstart
. Хочу сразу оговориться, что в процессе написания кода для обработки события перетаскивания важно четко представлять себе, какое событие и на каком элементе происходит.

В данном случае мы будем обрабатывать событие

1
dragstart
, которое возникает на перетаскиваемом элементе (
1
id="object1"
,
1
id="object2"
или
1
id="object3"
- не важно).

Событие

1
dragstart
в момент своего возникновения автоматически генерирует объект dataTransfer, который (как мне кажется) можно в общих чертах сравнить с событийным объектом Event; последний также хранит в себе множество данных о произошедшем событии. Некоторыми методами и свойствами объекта Event мы воспользуемся в нашем примере:

var dropStatus = document.querySelector('#dropStatus');
...
function dragStart (event) {
    dropStatus.innerHTML = 'Dragging the ' + event.target.getAttribute('id');
    event.dataTransfer.dropEffect = 'move';
    event.dataTransfer.setData('text', event.target.getAttribute('id'));
}

Функция

1
dragStart
при возникновении события “берет” элемент
1
dropStatus
и методом
1
innerHTML
“пихает” внутрь него строку, часть которой представляет из себя значение атрибута
1
id
элемента, на котором произошло событие (
1
event.target
).

Для объекта dataTransfer задается значение его свойства

1
dropEffect
-
1
move
.

В третьей строке для объекта dataTransfer с помощью метода

1
setData()
задается имя переменной
1
text
и значение для этой переменной - ID текущего элемента.

Функции dragEnter(), dragLeave(), dragOver()

Три функции, каждая из которых отслеживает событие, возникающее на элементе

1
dropZone
:

function dragEnter (event) {
    dropStatus.innerHTML = 'You are dragging over ' + event.target.getAttribute('id');
    this.classList.add('over');
}

function dragLeave (event) {
    dropStatus.innerHTML = 'You left the ' + event.target.getAttribute('id');
    this.classList.remove('over');
    this.removeAttribute('class');
}

function dragOver (event) {
    event.preventDefault();
}

Первые две функции -

1
dragEnter (event)
и
1
dragLeave (event)
очень похожи между собой. Каждая из них манипулирует содержимым заголовка
1
dropStatus
, сигнализируя о происходящем событии.

Третья функция

1
dragOver (event)
может показаться странной. Все ее назначение - это отмена действия по-умолчанию. Что это за действие по-умолчанию? Дело в том, что у браузеров имеется свой собственный (помимо HTML5) механизм реализации события перетаскивания Drag-and-Drop. И если его не отключить, то он не даст срабатывать нашему механизму.

Функция dragDrop (event)

Самая большая и самая важная функция в нашем коде. Она также срабатывает на событие, возникающее на элементе

1
dropZone
:

var droppedIN = false;
...
function dragDrop (event) {
    event.preventDefault();
    var elementID = event.dataTransfer.getData('text');
    var element = document.getElementById(elementID);
    event.target.appendChild(element);
    element.removeAttribute('draggable');
    element.classList.add('dragged');
    element.style.cursor = 'default';
    droppedIN = true;
    dropStatus.innerHTML = 'Element ' + elementID + ' dropped into the ' + event.target.getAttribute('id');
}

В строке

1
event.preventDefault();
мы снова отменяем действие по-умолчанию. На этот раз это касается самого перетаскиваемого элемента - ведь он может быть ссылкой и браузер выполнит переход по ней (действие по-умолчанию), что нам совсем не нужно.

В строке:

var elementID = event.dataTransfer.getData('text');

… мы из объекта dataTransfer получаем ID перетаскиваемого элемента. Вы же помните, что в функции

1
dragStart (event)
с помощью строки:

event.dataTransfer.setData('text', event.target.getAttribute('id'));

…мы его как раз получали?

Далее находим перетаскиваемый элемент по его ID:

var element = document.getElementById(elementID);

И помещаем его внутрь текущего активного элемента:

event.target.appendChild(element);

Далее убираем у перетаскиваемого элемента атрибут

1
draggable
- он больше не перетаскиваемый. Визуально сигнализируем об этом, изменив вид курсора мыши:

element.style.cursor = 'default';

И сообщаем об изменившемся статусе в заголовке:

dropStatus.innerHTML = 'Element ' + elementID + ' dropped into the ' + event.target.getAttribute('id');

Отдельного упоминания стоит строка

1
droppedIN = true;
. Это флаг, с помощью которого мы определяем, произошло ли событие
1
drop
или нет.

Может случиться так, что объект мы перетащили в область элемента

1
dropZone
, но передумали его помещать туда. И “отпустили” перетаскиваемый элемент за областью элемента
1
dropZone
. В результате событие
1
dragend
произошло, но событие
1
drop
не выполнилось.

Такую ситуацию обрабатывает функция

1
dragEnd()
:

function dragEnd() {
    if ( droppedIN === false ) {
        dropStatus.innerHTML = 'You let the ' + event.target.getAttribute('id') + ' to go!';
    }
    droppedIN = false;
}

Функция readZone ()

Последняя функция из нашего примера - это функция-счетчик. Ее задача - просто посчитать, сколько элементов на данный момент мы “бросили” в область

1
dropZone
:

function readZone () {
    var dropZoneChild = dropZone.children;
    for ( var i = 0; i < dropZoneChild.length; i++ ) {
        alert('Object ' + dropZoneChild[i].getAttribute('id') + ' is in ' + dropZone.getAttribute('id'));
    }
}

Нажимаем кнопку

1
dropButton
и alert’ом последовательно выводим все элементы, помещенные внутрь объекта
1
dropZone
.

Вот, в принципе, и все, что можно вкратце сказать. Осталось только взглянуть на готовый пример работы кода - JavaScript - Drag’n’Drop - Part 2.

На этом все. Здоровая критика и полезные замечания только приветствуются.


Этот скромный обзор не смог бы появиться, если бы не было двух полезных для меня ресурсов:

Есть более детальный обзор и более интересный пример задачи на JavaScript Drag-and-Drop размещен здесь: