Pull to refresh

Расстановка точек над onmousewheel и немного о луковом супе

JavaScript
Я уже писал о своих экспериментах со скроллбарами на сайтах и в веб-приложениях, но эти опыты удались не вполне. Поэтому я пока оставил идею кастомизации скроллбаров, но решил досканально разобраться с событиями, вызываемыми прокруткой колеса мыши.

Итак, задача: реализовать реакцию на события прокрутки мышиного колеса над определённым блоком, то есть не трогая «родной» скролл окна браузера. Реализация должна быть кроссбраузерной и не использовать какие-либо фреймворки.

Забегая вперёд, скажу, что этот экперимент удался вполне, а итоговый результат работает во всех десктопных браузерах, начиная с IE7 (по идее, должно работать и в шестом, но сейчас нет возможности это проверить). Также, хочу выразить благодарность поисковой системе Гугл. Без неё жизнь была бы соткана из уныния и отчаяния.

Сначала я решил задачу ловли onmousewheel для всего окна (объекта window). Решилось так:
window.onload = function() {
	if (window.addEventListener) window.addEventListener("DOMMouseScroll", mouse_wheel, false);
	window.onmousewheel = document.onmousewheel = mouse_wheel;
}

Первая строчка поймает событие в Firefox и веб-китовских браузерах (Chrome, Safari), вторая нужна для ловли в Опере и IE. Реакция на событие — функция mouse_wheel(), которая выглядит пока что следующим образом:
var mouse_wheel = function(event) {
	if (false == !!event) event = window.event;
	var direction = ((event.wheelDelta) ? event.wheelDelta/120 : event.detail/-3) || false;
}

С первой строкой всё ясно — это костылёк для IE. О второй строке расскажу подробнее. В ней я определяю, в каком направлении вращается колесо. Все браузеры кроме Firefox передают направление в свойстве wheelDelta как равное 120 или -120 в зависимости от того, куда крутится колесо. Мозилла же оперирует свойством detail, присваивая ему значения -3 или 3, соответственно. Да, именно так, знак не совпадает. Поэтому второй строчкой я привожу данные разных браузеров к +1, если колесо крутится вверх (от себя) и -1, если колесо крутится вниз (на себя). Ну и надеваю презерватив в виде «|| false» на случай ядерной войны или ещё какой-нибудь экзотики.

Программа минимум выполнена. Я ловлю событие прокрутки колеса мыши во всех браузерах и даже знаю в каком направлении крутится колесо. Едем дальше.

Теперь нужно поймать это событие над определённым блоком, а с ним уже что-то делать. Это может быть, например, та же прокрутка содержимого или регулятор громкости плеера. Я же, в качестве примера решил сделать эдакий colorpicker, в котором значения красной, зелёной или синей компоненты устанавливаются прокруткой колеса над соответствующим блоком. То есть, что-то в этом духе:

image

Действовать я решил самым простым и может даже слегка кондовым способом: завёл глобальную переменную wheel_handle (null или другое false-like значение по умолчанию), которой при событии onmouseover на определённом блоке будет присваиваться некая функция, определённая заранее. Выполняться функция будет при срабатывании onmousewheel и в качестве параметра в неё будет передаваться направление прокрутки. Код стал выглядеть так:
var wheel_handle = null;

var mouse_wheel = function(event) {
	if (false == !!event) event = window.event;
	var direction = ((event.wheelDelta) ? event.wheelDelta/120 : event.detail/-3) || false;
	if (direction && !!wheel_handle && typeof wheel_handle == "function") {
		wheel_handle(direction);
	}
}

var set_handle = function(id, func) {
	document.getElementById(id).onmouseover = function() {
		wheel_handle = func;
	}
	document.getElementById(id).onmouseout = function() {
		wheel_handle = null;
	}
}

window.onload = function() {
	if (window.addEventListener) window.addEventListener("DOMMouseScroll", mouse_wheel, false);
	window.onmousewheel = document.onmousewheel = mouse_wheel;
} 

В принципе, всё должно быть понятно, но на всякий случай. Функция mouse_wheel при наличие direction и wheel_handle с типом «функция» исполняет функцию, которая присвоена wheel_handle. Функция же set_handle определяет, над какими блоками какую функцию исполнять. То есть, чуть забежав вперёд, для моего примера конструкция

set_handle("r", set_red);

будет означать, что при наведении мыши на блок с идентификатором «r», переменной wheel_handle присвоится функция set_red, которая с параметром direction будет выполнена в случае события onmousewheel.

Оформляю пример:
<!DOCTYPE html>
<html>
	<head>
		<style type="text/css">
			#r { position: absolute; left: 10px; top: 10px; width: 100px; height: 100px; font-size: 50px; color: #fff; text-align: center; line-height: 100px; background: #000; } 
			#g { position: absolute; left: 120px; top: 10px; width: 100px; height: 100px; font-size: 50px; color: #fff; text-align: center; line-height:100px; background: #000; }
			#b { position: absolute; left: 230px; top: 10px; width: 100px; height: 100px; font-size: 50px; color: #fff; text-align: center; line-height:100px; background: #000; }
			#result { position: absolute; left: 10px; top: 120px; width: 320px; height: 100px; background: #000; }
		</style>
		
		<script type="text/javascript">
			var wheel_handle = null;
			
			var rgb = {
				r: 0,
				g: 0,
				b: 0,
				result: 0
			}
					
			var mouse_wheel = function(event) {
				if (false == !!event) event = window.event;
				var direction = ((event.wheelDelta) ? event.wheelDelta/120 : event.detail/-3) || false;
				if (direction && !!wheel_handle && typeof wheel_handle == "function") {
					wheel_handle(direction);
				}
			} 
			
			var set_handle = function(id, func) {
				document.getElementById(id).onmouseover = function() {
					wheel_handle = func;
				};
				document.getElementById(id).onmouseout = function() {
					wheel_handle = null;
				};
			}
									
			var set_red = function(direction) {
				rgb.r += direction;
				if (rgb.r < 0) rgb.r = 0;
				if (rgb.r > 255) rgb.r = 255;
				document.getElementById("r").innerHTML = rgb.r;
				document.getElementById("r").style.backgroundColor = "rgb(" + rgb.r + ", 0, 0)";
				set_result();
			}
			
			var set_green = function(direction) {
				rgb.g += direction;
				if (rgb.g < 0) rgb.g = 0;
				if (rgb.g > 255) rgb.g = 255;
				document.getElementById("g").innerHTML = rgb.g;
				document.getElementById("g").style.backgroundColor = "rgb(0, " + rgb.g + ", 0)";
				set_result();				
			}

			var set_blue = function(direction) {
				rgb.b += direction;
				if (rgb.b < 0) rgb.b = 0;
				if (rgb.b > 255) rgb.b = 255;
				document.getElementById("b").innerHTML = rgb.b;
				document.getElementById("b").style.backgroundColor = "rgb(0, 0, " + rgb.b + ")";				
				set_result();
			}

			var set_result = function() {
				document.getElementById("result").style.backgroundColor = "rgb(" + rgb.r + ", " + rgb.g + ", " + rgb.b + ")";				
			}			
			
			window.onload = function() {
				if (window.addEventListener) window.addEventListener("DOMMouseScroll", mouse_wheel, false);
				window.onmousewheel = document.onmousewheel = mouse_wheel;
				
				set_handle("r", set_red);
				set_handle("g", set_green);
				set_handle("b", set_blue);
			}
		</script>
	</head>
	<body style="height: 1000px;">
		<div id="r">0</div>
		<div id="g">0</div>
		<div id="b">0</div>
		<div id="result"></div>
	</body>
</html>

Финкции вида set_* очевидны и, на мой взгляд в комментариях не нуждаются. А вот обратить внимание на стиль тэга body надо. Я задал 1000 пикселей высоты для того, чтобы страница гарантированно не поместилась в экран и у неё образовалась своя полоса прокрутки. Если запустить пример в таком виде то события onmousewheel на блоках-то работать будут, но при этом и сама страница будет прокручиться вниз. Перечитываю постановку задачи, вношу маленький штришок в функцию mouse_wheel();
var mouse_wheel = function(event) {
	if (false == !!event) event = window.event;
	var direction = ((event.wheelDelta) ? event.wheelDelta/120 : event.detail/-3) || false;
	if (direction && !!wheel_handle && typeof wheel_handle == "function") {
		if (event.preventDefault) event.preventDefault();
		event.returnValue = false;
		wheel_handle(direction);
	}
} 

preventDefault() нужен здесь для отмены события. Да и вообще он нужен для отмены события. Вобщем, процитирую я немного Гугла:
Если действие события можно отменить, метод preventDefault() объекта Event используется для отмены события. Это означает, что действие, выполняемое в конкретной реализации, при возникновении данного события выполняться не будет. Если на любой фазе развития события вызывается метод preventDefault(), событие отменяется. Действие, выполняемое этим событием по умолчанию, не выполняется. Вызов данного метода для события, которое нельзя отменить, не оказывает никакого влияния на дальнейшее выполнение события. После вызова метод preventDefault() будет действовать на всем протяжении дальнейшего распространения события. Метод preventDefault() можно вызывать на любой фазе потока события.


event.returnValue — это для IE, который о preventDefault() не знает.

Отменили естественную реакцию на событие — выполняем нашу функцию. Под Windows в восьмом Firefox я отметил не всегда корректное срабатывание preventDefault(). Хотя, может это и некоторая специфика onmouseenter, потому что было замечено, что некорректные срабатывания случаются, когда курсор находится на текстом в блоке. Если кто-нибудь в курсе этих катаклизмов, просветите, пожалуйста — здесь Гугл не помог.

Но, вобщем-то и всё. Исходный код для копипасты полностью:
<!DOCTYPE html>
<html>
	<head>
		<style type="text/css">
			#r { position: absolute; left: 10px; top: 10px; width: 100px; height: 100px; font-size: 50px; color: #fff; text-align: center; line-height: 100px; background: #000; } 
			#g { position: absolute; left: 120px; top: 10px; width: 100px; height: 100px; font-size: 50px; color: #fff; text-align: center; line-height:100px; background: #000; }
			#b { position: absolute; left: 230px; top: 10px; width: 100px; height: 100px; font-size: 50px; color: #fff; text-align: center; line-height:100px; background: #000; }
			
			#result { position: absolute; left: 10px; top: 120px; width: 320px; height: 100px; background: #000; }
		</style>
		
		<script type="text/javascript">
			var wheel_handle = null;
			
			var rgb = {
				r: 0,
				g: 0,
				b: 0,
				result: 0
			}
					
			var mouse_wheel = function(event) {
				if (false == !!event) event = window.event;
				var direction = ((event.wheelDelta) ? event.wheelDelta/120 : event.detail/-3) || false;
				if (direction && !!wheel_handle && typeof wheel_handle == "function") {
					if (event.preventDefault) event.preventDefault();
					event.returnValue = false;
					wheel_handle(direction);
				}
			} 
			
			var set_handle = function(id, func) {
				document.getElementById(id).onmouseover = function() {
					wheel_handle = func;
				}
				document.getElementById(id).onmouseout = function() {
					wheel_handle = null;
				}
			}
									
			var set_red = function(direction) {
				rgb.r += direction;
				if (rgb.r < 0) rgb.r = 0;
				if (rgb.r > 255) rgb.r = 255;
				document.getElementById("r").innerHTML = rgb.r;
				document.getElementById("r").style.backgroundColor = "rgb(" + rgb.r + ", 0, 0)";
				set_result();
			}
			
			var set_green = function(direction) {
				rgb.g += direction;
				if (rgb.g < 0) rgb.g = 0;
				if (rgb.g > 255) rgb.g = 255;
				document.getElementById("g").innerHTML = rgb.g;
				document.getElementById("g").style.backgroundColor = "rgb(0, " + rgb.g + ", 0)";
				set_result();				
			}

			var set_blue = function(direction) {
				rgb.b += direction;
				if (rgb.b < 0) rgb.b = 0;
				if (rgb.b > 255) rgb.b = 255;
				document.getElementById("b").innerHTML = rgb.b;
				document.getElementById("b").style.backgroundColor = "rgb(0, 0, " + rgb.b + ")";				
				set_result();
			}

			var set_result = function() {
				document.getElementById("result").style.backgroundColor = "rgb(" + rgb.r + ", " + rgb.g + ", " + rgb.b + ")";				
			}			
			
			window.onload = function() {
				if (window.addEventListener) window.addEventListener("DOMMouseScroll", mouse_wheel, false);
				window.onmousewheel = document.onmousewheel = mouse_wheel;
				
				set_handle("r", set_red);
				set_handle("g", set_green);
				set_handle("b", set_blue);
			}
		</script>
	</head>
	<body style="height: 1000px;">
		<div id="r">0</div>
		<div id="g">0</div>
		<div id="b">0</div>
		<div id="result"></div>
	</body>
</html>


Ссылка на рабочий пример

NB! Ссылка на рабочий пример, дополненный с учётом советов уважаемых комментаторов
— исправлена привязка к «магическим константам»;
— добавлена возможность указать контекст вызова handle: функция привязки теперь выглядит как mousewheel.set_wheel_handle(id, func, context);
— всё, касающееся onmousewheel завёрнуто в библиотечку Mousewheel.js.

И, собственно, при чём здесь луковый суп. Ну, во-первых. А, во-вторых, я не знаю как у вас, а у нас в Москве морозы ударили. -10, конечно, терпимо, но разномастные простуды цветут пышным цветом именно сейчас. Неприятно, согласитесь?

Я не знаю лучшего средства для для профилактики всяких простудных неприятностей, чем плошка горячего, ароматного лукового супа руанским манером, и готовлю я его так:
  • ставлю в духовку минут на 50—60 всё необходимое для бульона: пару красных луковиц, большую морковь, корень сельдерея, головку чеснока и, конечно, мясо — кусок хорошей говядины и пару-тройку крупных костей;
  • режу кольцами лук, обычный репчатый или шалот — довольно много, грамм 800, килограмм — и кладу его в разогретую толстую чугунную кастрюлю или казан, в котором предварительно растапливаю грамм 150—200 сливочного масла, убавляю огонь до минимума и оставляю пассероваться часа три, периодически помешивая;
  • варю бульон — плотный консоме, залив всё, что запекалось в духовке двумя литрами холодной (!) воды и поставив на минимально возможный огонь. Кипеть практически не должно: два-три пузырька в центре — и хватит. Если кипит сильнее, зачёрпываю половником и выливаю обратно с высоты;
  • когда (часа через три) лук станет почти однородной массой и приобретёт цвет молочного шоколада, я вливаю стакан белого вина, довожу до кипения и вливаю приготовленный и процеженый бульон. Так же, на этом этапе можно добавить грамм сто мягкого сливочного сыра, но это не обязательно;
  • минут через тридцать я добавляю двести грамм сливок, предварительно влив в суп разведённую в полустакане воды ложку муки, тщательно перемешиваю: суп должен быть практически однородным (если есть сомнения можно довести до ума погружным блендером), добавляю молотый перец и травы, которым вариться нужно совсем немного — чабрец, розмарин, лавровый лист или весь букет Гарни скопом;
  • ещё минут двадцать и суп готов: вынимаю лавровый лист и выбрасываю, а остальное подаю в керамических или глиняных глубоких тарелках с гренками и тонко нарезаным сычужным сыром. Гренки — в тарелку, сыр — отдельно.


Приятного аппетита, вобщем. Конечно, приготовление занимает минимум 4 часа, но результат того стоит, уж поверьте. Да и вообще я усматриваю ряд общих черт между поварами и программистами, но об этом в следующий раз и, наверное всё-таки, в Хабраюморе.

Не болейте!
Tags:javascriptonmousewheelmousewheelкроссбраузерностьпрокрутка
Hubs: JavaScript
Total votes 102: ↑95 and ↓7 +88
Views15K

Popular right now

Top of the last 24 hours