Popup на jQuery c отключением скрола основного контента

от автора

Совсем недавно столкнулся с не совсем обычным таском в таскменеджере. ПроджектМенеджер просто напрямую скопировал текст заказчика — «Хочу, чтобы попап был как в вконтакте». Что значит «как в вконтакте» я понял не сразу. «Потестив» попап в соц.сети я понял, чего же от меня хотят — при открытом попапе основной контент не должен скроллиться, но в то же время, если размеры попапа больше, чем окно пользователя — должен скроллиться попап (основной контент естественно так же не скроллиться и в этом случае).
Сложного в этом таске я ничего не увидел и приступил к его выполнени. Логика была следующая — при откртии попапа даем body overflow = hidden, а при закрытии естественно его возвращаем в auto. Сам попап кладем в div-обвертку и даем ему overflow = auto, чтобы в случаях, когда размер окна меньше, чем размер попапа — пользователь мог его скроллить.
Под OS X все работало идеально. Но я не учел одного нюанса — в остальных системах в браузере есть постоянно видимый скролл, который имеет свою ширину. И если просто делать блоку, у которого есть скролл, overflow hidden — скролл пропадает, ширина блока становится больше на ширину скролла и контент «прыгает». Тогда я решил написать плагин, который решает и эту проблему. Еще раз посмотрев, как работает сей чудный попап в вконтакте, я понял логику и приступил к выполнению.

(Мне кажется, сайты, которые не используют jQuery — уже редкость. Поэтому писал я плагин с использованием jQuery)
Так как я хотел написать такой плагин, для работы которого не нужно было дописывать какие-либо конструкции в Html и в css, плагин должен это все сделать сам. Для начала стоит разобраться, какой Html-каркас будет нужен плагину. Так как у попапа должен быть фон, котороый будет перекрывать основной контент, в body должен появиться div, который и будет являться непосредственно самим фоном. Это первый элемент. Второй — обертка попапа. Она нужна для того, чтобы во время, когда у body стоит overflow hidden, если наш попап больше по размеру, чем размер окна пользователя — пользователь мог проскролить контейнер с попапом. Для этого у оболочки будет прописано css свойство overflow равное auto. Теперь нужно организвать работу плагина так, чтобы он подставлял эти объекты в наш html.
За работу с попапом будет отвечать объект _JPopup. У него же есть метод init(), который будет «дергаться» после загрузки страницы. В этом методе плагин и будет добавлять нужные для его работы html теги в body.

$(function () { 	_JPopup.init(); }); var _JPopup = {     /** 	 * div который является фоном попапа 	 * jQuery object 	 */ 	$background: null,  	/** 	 * Блок, в котором будут находиться все попапы 	 * jQuery object 	 */ 	$popupBlock: null,      $body: null,      init: function () {         // Наш затухающий фон. Это div, который имеет 100-процентные ширину и высоту 		this.$background = $('<div style="display: none; position: absolute; z-index: 9997; width: 100%; height: 100%; background-color: #000" id="j_background"></div>');         // оболочка попапа в которую будут вставляться html конструкции 		this.$popupBlock = $('<div style="display:none;position: fixed; height: 100%; width: 100%; z-index: 9999; overflow: auto;" id="j_popup"></div>');  		this.$body = $('body'); 		this.$body 			.prepend(this.$background) 			.prepend(this.$popupBlock); 	} } 

Так же при инициализации попапа стоит подумать и о том, как он будет закрываться. Мне кажется, что большинство пользователей уже привыкло к тому, что при нажатии на контент за пределами попапа сам попап должен закрыться. В нашем случае пользователь не сможет нажать на какой-либо контент за пределами блока $popupBlock, так как у него 100-процентные размеры и самый большой z-index. Получается, что нужно закрывать попап после клика на $popupBlock. Для этого добавляем в метод init() следующий код:

var _JPopup = {     ...     init: function () {         ...         this.$popupBlock.click(function (e) {     	    _JPopup.hidePopup();         });     } } 

Сам метод _JPopup.hidePopup() мы разберем чуть позже.
Теперь приступим к самому интересному — появлению попапа. В первую очередь нам нужно знать, какой блок будет являться попапом. Есть много вариантов, как реализовать указание плагину на то, какой блок нужно брать. К примеру передавать селектор как параметр, либо же передавать непосредственно сам jQuery объект. Мне же нравится работать с методами jQuery и поэтому хотелось получить возможность вызывать появление попапа подобным способом: $(‘#someId’).showPopup(); Для этого добавим метод jQuery:

$.fn.showPopup = function () { 	_JPopup.showPopup($(this)); 	return this; }; 

Все, что делает данный метод — вызывает метод объекта _JPopup showPopup() и передает ему объект jQuery элемента, к которому была применена функция. Затем, чтобы не нарушать возможность ведения цепочки методов, возвращаем this.
А вот метод _JPopup.showPopup() и будет показывать нужный нам попап.

var _JPopup = {     ...     showPopup: function ($self) {         if($.browser.msie && $.browser.version < 9)             this.$background.show();        else            this.$background.css('opacity',0).show().fadeTo('slow', 0.7);        this.$background.css({'top': this.$body.scrollTop(),'left': this.$body.scrollLeft()});         this.$body.css({'overflow':'hidden','width':$(document).width()-this.getScrollBarWidth()});         var $html = $self.clone();         this.$popupBlock.html('').append($html).show();         $html.css({            'position': 'absolute',            'z-index': 9999,            'top':'50%',            'left':'50%'        }).show();         this.setAlign();         $html.click(function (e) {            e.stopPropagation();        });     } } 

Разберем этот метод. С самого начала мы показываем полупрозрачный фон. В случае, если пользователь использует IE8 и ниже, фон просто появится, если же юзер использует более цивилизованный браузер — фон появится плавно и будет полупрозрачным за счет css свойства opacity. Плавное появление мы добиваемся с помощью метода fadeTo(‘slow’, 0.7). Если контент страницы будет проскроллен, и так как у нашего фона прописан position: absolute, он не перекроет весь контент. Для того, чтобы такого не произошло, изменим его css значения top и left ровно на столько, на сколько был проскроллен документ.
Затем, мы отключаем скролл у body прописывая ‘overflow’:’hidden’ и в то же время даем ему ширину, равную ширине документа минус ширину скролла. Так как в разных ОС и разных браузерах ширина скролла может быть разная (где-то она вообще равна нулю), мы должны ее (ширину) определить. Для этого я воспользовался кодом, который когда-то где-то нашел. Честно говоря уже даже не помню где. Но он до сих пор верно помогает мне в определении ширины скролла. Учитывая, что ширина скролла не может меняться в одном браузере, для того, чтобы каждый раз не тратить время на ее нахождение, мы создадим переменную, равную null, и только в случае если она равна null будем определять ширину скролла и присваивать это значение этой переменной. В таком случае при повторном обращении мы не будем тратить драгоценное время.

var _JPopup = {     ...     scrollbarWidth: null,      getScrollBarWidth: function () {         if ( this.scrollbarWidth === null ) { 			if ( $.browser.msie && parseInt($.browser.version, 10) === 8) { 				var $textarea1 = $('<textarea cols="10" rows="2"></textarea>') 					.css({ position: 'absolute', top: -1000, left: -1000 }).appendTo('body'), 				$textarea2 = $('<textarea cols="10" rows="2" style="overflow: hidden;"></textarea>') 					.css({ position: 'absolute', top: -1000, left: -1000 }).appendTo('body'); 				this.scrollbarWidth = $textarea1.width() - $textarea2.width(); 				$textarea1.add($textarea2).remove(); 			} else { 				var $div = $('<div />') 					.css({ width: 100, height: 100, overflow: 'auto', position: 'absolute', top: -1000, left: -1000 }) 					.prependTo('body').append('<div />').find('div') 					.css({ width: '100%', height: 200 }); 				this.scrollbarWidth = 100 - $div.width(); 				$div.parent().remove(); 			} 		} 		return this.scrollbarWidth; 	} } 

После этого мы получаем Html контента попапа. Для этого воспользуемся ф-цией jQuery clone(). Конечно можно было пойти немного иным способом и вставлять в нашу оболочку непосредственно сам объект, к которому применили метод showPopup, а после закрытия попапа возвращать его на место. Но в таком случае, если в попапе есть, к примеру, инпуты — нужно будет следить за их очисткой. В общем мне показалось что это был бы лишний функционал. Итак. Мы получили html нужного контента для попапа. Теперь вставляем его в оболочку. Причем вставляем мы его заменяя весь Html, который был в оболочке. Это нужно для того, чтобы если вдруг там был другой попап — не вывести заодно и его. Ну и методом show() заменяем display: none оболочки на display: block.
Для того, чтобы контент попапа был «выше» всего остального, ему нужно прописать самый большой z-index. А так же нашей задачей является выравнивание попапа по центру. Для этого дадим ему значения left и top равные 50%. Теперь верхний левый угол попапа находится посередине страницы. А для того, чтобы по центру страницы находился центр попапа нужно дать ему отрицательные margin-left и margin-top равные половине его ширины и высоты соответственно. Вынесем эту логику в отдельный метод setAlign:

var _JPopup = {     ...     setAlign: function () { 		var marginLeft = this.$popupBlock.children().width()/ 2, 			marginTop = this.$popupBlock.children().height()/2; 		if($(window).width()/2 < marginLeft) 			marginLeft = $(window).width()/2-10; 		if($(window).height()/2 < marginTop) 			marginTop = $(window).height()/2-10; 		this.$popupBlock.children().css({ 			'margin-left': -marginLeft, 			'margin-top': -marginTop, 			'padding-bottom': '10px' 		}); 	}, } 

В этом методе мы учитываем и те случаи, когда размер попапа больше размера окна. В этом случае, если задавать margin-left и margin-top равные половине ширины и высоты попапа умноженные на -1 — часть попапа просто скроется и не будет видна пользователю. Поэтому мы проверяем, если половина ширины окна браузера меньше чем половина ширины попапа (лобо если проще — ширина окна меньше ширины попапа) — ‘margin-left’ будет равен половине ширины окна. Плюс делаем отбивочку в 10 пикселей чтобы попап не прилипал к краю окна.
Хотелось бы вернуться к событию клика по попапу, после которого попап закрывается. Если оставить все как есть — попап будет закрываться даже тогда, когда пользователь будет кликать по контенту попапа. Для решения этой проблемы я воспользовался эфектом всплытия событий в jQuery. Как извесно, если у нас span лежит в div и юзер кликнул по span — сначала сработает событие у спана, а затем произойдет событие у дива. Для решения нашей проблемы мы просто предотвращаем всплытие события методом stopPropagation().
Осталось только, как я и обещал, описать метод закрытия попапа. Он будет совсем несложным:

var _JPopup = {     ...     hidePopup: function () { 		this.$popupBlock.html('').hide(); 		this.$background.hide(); 		this.$body.css('overflow','auto'); 		this.$body.css('width','auto'); 	}, } 

Метод прячет оболочку попапа с помощью метода hide(). Этим же методом он прячет фон попапа. Затем мы возвращаем возможность скролла у body и даем ему первоначальную ширину. И напишем для него внешнуюю функцию, которую можно вызвать для закрытия попапа «вручну». К примеру по клику на кнопку «Закрыть попап»

function closeJPopup() { 	_JPopup.hidePopup(); } 

Теперь, к примеру, у нас есть блок

<div id="popup" style="display: none;"> 	<div style="width: 300px;height: 300px; background-color: white;"> 		some text  		<input type="text"/> 	</div> </div> 

Все, что нужно для того, чтобы показать его в попапе — вызвать метод $(‘#popup’).showPopup();

Просмотреть работу попапа можно здесь. Там можно проверить как работают попапы как небольших размеров, так и «длинные» попапы — нужно пролистать страницу ниже.
Если кому будет интересно, скачать файл попапа можно по этой ссылке. Там находится более «допиленная» версия плагина. Основное дополнение — возможность передать каллбэк функцию при появлении попапа. А вторым параметром можно передать параметры, которые нужно передать в каллбэк ф-цию. Эта возможность удобна для тех случаев, когда нужно инитить какой-то плагин на контент в попапе. К примеру — JClever.

ссылка на оригинал статьи http://habrahabr.ru/post/218111/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *