Совсем недавно столкнулся с не совсем обычным таском в таскменеджере. ПроджектМенеджер просто напрямую скопировал текст заказчика — «Хочу, чтобы попап был как в вконтакте». Что значит «как в вконтакте» я понял не сразу. «Потестив» попап в соц.сети я понял, чего же от меня хотят — при открытом попапе основной контент не должен скроллиться, но в то же время, если размеры попапа больше, чем окно пользователя — должен скроллиться попап (основной контент естественно так же не скроллиться и в этом случае).
Сложного в этом таске я ничего не увидел и приступил к его выполнени. Логика была следующая — при откртии попапа даем 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.